Skip to content

Commit

Permalink
Merge pull request #73 from Doxel-AI/72-add-property-updates-to-loaders
Browse files Browse the repository at this point in the history
72 add property updates to loaders
  • Loading branch information
SaFrMo authored Jul 3, 2024
2 parents 007afee + e2108e6 commit eebc99f
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 47 deletions.
23 changes: 21 additions & 2 deletions packages/docs/components/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ mesh.material = material; // <-- this is the `attach="material"` on the mesh-bas
Any component ending with `-loader` (`<texture-loader>`, for example) will do the following in Lunchbox:

1. Execute its `load` method, passing its `src` attribute as the URL
2. Save the result of its `load` method to its `loaded` property
3. (Optional, if `attach` attribute provided) Attach the loaded result to the parent
2. Save the result of its `load` method to its `instance` property
3. Save the loader itself to the `loader` property
4. (Optional, if `attach` attribute provided) Attach the loaded result to the parent

For example, this is how to load a texture in Lunchbox:

Expand All @@ -66,6 +67,24 @@ For example, this is how to load a texture in Lunchbox:
</three-mesh>
</three-lunchbox>

Since the loaded content itself is in the `instance` property, you can set its properties on the loader component directly:

For example, this is how to load a texture in Lunchbox:

```html
<three-mesh>
<box-geometry></box-geometry>
<mesh-basic-material>
<!-- Load the ThreeJS favicon, attach as a map, and set its anisotropy to 32 -->
<texture-loader
src="/three-favicon.png"
attach="map"
anisotropy="32">
</texture-loader>
</mesh-basic-material>
</three-mesh>
```

## Special arguments

Sometimes, you need to access the scene, camera, etc in `args`. For example, after [extend](component-guide.html#custom-components-via-extend)ing OrbitControls:
Expand Down
15 changes: 12 additions & 3 deletions packages/docs/components/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ All [auto-registered](/components/component-guide.html#auto-registered-component

Note that a [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) passes data via its `detail` property; this is the unique data of each event, and so is the payload in the `detail` column below.

| Name | `detail` | Notes |
| ----------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `instancecreated` | `{ instance: /* the ThreeJS object */ }` | Fired when the underling [instance](/components/component-guide.html#the-instance-property) is created. |
| Name | `detail` | Notes |
| ----------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `instancecreated` | `{ instance: /* the ThreeJS object */ }` | Fired when the underlying [instance](/components/component-guide.html#the-instance-property) is created. |

### Examples

Expand All @@ -63,3 +63,12 @@ mesh.addEventListener('instancecreated', (event: CustomEvent<InstanceEvent<THREE
console.log(event.detail.instance);
});
```


## Special events

Some components and component types trigger their own special events:

| Component | Event name | `detail` | Notes |
| -------------------------------- | ---------- | --------------------------------------- | --------------------------------------------------------------------- |
| Any component ending in `loader` | `loaded` | `{ instance: /* the loaded object */ }` | Fired when a loader's `load` method has finished loading its resource |
34 changes: 33 additions & 1 deletion packages/lunchboxjs/cypress/e2e/loader.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,49 @@ describe('vanilla HTML spec', () => {
.should('have.a.property', 'map')
.should('not.exist');

(mat.get(0) as unknown as HTMLElement).innerHTML = ` <texture-loader
// add image of black rectangle
(mat.get(0) as unknown as HTMLElement).innerHTML = `<texture-loader
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAABNJREFUCB1jZGBg+A/EDEwgAgQADigBA//q6GsAAAAASUVORK5CYII%3D"
attach="map"></texture-loader>`;
});

// ensure instance map is updated
cy.get('mesh-basic-material').then(mat => {
cy.wrap(mat.get(0))
.should('have.a.property', 'instance')
.should('have.a.property', 'map')
.should('have.a.property', 'isTexture')
.should('be.true');
});

// ensure texture is accessible in loader
cy.get('texture-loader').then(loader => {
// ensure the loaded content is the instance
cy.wrap(loader.get(0))
.should('have.a.property', 'instance')
.should('have.a.property', 'isTexture')
.should('be.true');

// ensure the loader is accessible
cy.wrap(loader.get(0))
.should('have.a.property', 'loader')
.should('have.a.property', 'load');

// ensure the loaded content is starting with the correct property
cy.wrap(loader.get(0))
.should('have.a.property', 'instance')
.should('have.a.property', 'anisotropy')
.should('eq', 1)
.then(() => {
// update anisotropy
(loader.get(0) as unknown as HTMLElement).setAttribute('anisotropy', '32');
});

cy.wrap(loader.get(0))
.should('have.a.property', 'instance')
.should('have.a.property', 'anisotropy')
.should('eq', 32);
});
});
});
});
2 changes: 1 addition & 1 deletion packages/lunchboxjs/src/setThreeProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const setThreeProperty = <T extends object>(target: T, split: string[], p

// handle common update functions
const targetAsMaterial = target as THREE.Material;
if (targetAsMaterial.type?.toLowerCase().endsWith('material')) {
if (typeof targetAsMaterial.type === 'string' && targetAsMaterial.type?.toLowerCase().endsWith('material')) {
targetAsMaterial.needsUpdate = true;
}
};
120 changes: 80 additions & 40 deletions packages/lunchboxjs/src/three-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
return null;
}

const isLoader = targetClass.toString().toLowerCase().endsWith('loader');

/** Standard ThreeJS class */
class ThreeBase<U extends IsClass = T> extends LitElement {
@property({ type: Array })
args: ConstructorParameters<U> = [] as unknown as ConstructorParameters<U>;
instance: U | null = null;
loaded: unknown | null = null;

dispose: (() => void)[] = [];

mutationObserver: MutationObserver | null = null;

connectedCallback(): void {
super.connectedCallback();

observeAttributes() {
// Attribute mutation observation
// ==================
this.mutationObserver = new MutationObserver(mutations => {
Expand All @@ -47,10 +47,15 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
this.mutationObserver.observe((this as unknown as Node), {
attributes: true,
});
}

createUnderlyingThreeObject() {
// Instance creation
// ==================
this.instance = new (threeClass as U)(...this.args.map(arg => parseAttributeValue(arg, this))) as unknown as U;
}

refreshAttributes() {
// Populate initial attributes
this.getAttributeNames().forEach(attName => {
const attr = this.attributes.getNamedItem(attName);
Expand All @@ -59,32 +64,25 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
}
});
Array.from(this.attributes).forEach(this.updateProperty.bind(this));
}

// Instance bookkeeping
// ==================
onUnderlyingThreeObjectReady() {
const instanceAsObject3d = this.instance as unknown as THREE.Object3D;
if (instanceAsObject3d.uuid) {
this.setAttribute(THREE_UUID_ATTRIBUTE_NAME, instanceAsObject3d.uuid);
}

// Fire instancecreated event
this.dispatchEvent(new CustomEvent('instancecreated', {
detail: {
instance: this.instance,
},
}));


// Do some attaching based on common use cases
// ==================
const parent = this.parentElement as ThreeBase;
if (parent.instance) {
const thisAsGeometry = this.instance as unknown as THREE.BufferGeometry;
const thisAsMaterial = this.instance as unknown as THREE.Material;
const parentAsMesh = parent.instance as unknown as THREE.Mesh;
const thisAsLoader = this.instance as unknown as THREE.Loader;
// const thisAsLoader = this.instance as unknown as THREE.Loader<U>;
const parentAsAddTarget = parent.instance as unknown as { add?: (item: THREE.Object3D) => void };
const thisIsALoader = this.tagName.toString().toLowerCase().endsWith('-loader');
// const thisIsALoader = this.tagName.toString().toLowerCase().endsWith('-loader');
const instanceAsObject3d = this.instance as unknown as THREE.Object3D;

// if we're a geometry or material, attach to parent
if (thisAsGeometry.type?.toLowerCase().includes('geometry') && parentAsMesh.geometry) {
Expand All @@ -94,26 +92,6 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
parentAsMesh.material = thisAsMaterial;
}
// if we're a loader, start loading
else if (thisIsALoader) {
const src = this.getAttribute('src');
if (!src) throw new Error('Loader requires a source.');

// load and try attaching
thisAsLoader.load(src, loaded => {
this.loaded = loaded;
const attachAttribute = this.getAttribute('attach');
if (attachAttribute) {
this.executeAttach(attachAttribute, loaded);
}
this.dispatchEvent(new CustomEvent('loaded', {
detail: {
loaded,
},
}));
}, undefined, error => {
throw new Error(`error loading: ${src}` + error);
});
}
// otherwise, try to add as a child of the parent
else if (parentAsAddTarget.add) {
// If parent is an add target, add to parent
Expand All @@ -127,12 +105,29 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
// try executing attachment if we can
// (skip if this is a loader to avoid race condition)
const attachAttribute = this.getAttribute('attach');
if (!thisIsALoader && attachAttribute) {
if (attachAttribute) {
this.executeAttach(attachAttribute, this.instance);
}
}
}

connectedCallback(): void {
super.connectedCallback();

this.observeAttributes.call(this);
this.createUnderlyingThreeObject.call(this);
this.refreshAttributes.call(this);

// Fire instancecreated event
this.dispatchEvent(new CustomEvent('instancecreated', {
detail: {
instance: this.instance,
},
}));

this.onUnderlyingThreeObjectReady.call(this);
}

/** Update an instance's property. When creating a `<mesh position-y="0.5">`, for example, this sets `mesh.position.y = 0.5`. */
updateProperty(att: Attr) {
const { name, value } = att;
Expand Down Expand Up @@ -180,7 +175,11 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
disconnectedCallback(): void {
super.disconnectedCallback();

const toDispose = [this.instance, this.loaded];
const toDispose = [this.instance];
this.disposeThreeObjects.call(this, toDispose);
}

disposeThreeObjects(toDispose: unknown[]): void {
toDispose.forEach(target => {
if (!target) return;

Expand All @@ -190,14 +189,55 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
instanceAsDisposable.dispose?.();
instanceAsRemovableFromParent.removeFromParent?.();
});

}

/** Render */
render() {
return html`<slot></slot>`;
}
}
return ThreeBase;

/** Loader class */
class ThreeLoader<L extends THREE.Loader<U>, U extends IsClass = T> extends ThreeBase<U> {
loader: L | null = null;

createUnderlyingThreeObject(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.loader = new (threeClass as any)(...this.args.map(arg => parseAttributeValue(arg, this)));
}

onUnderlyingThreeObjectReady(): void {
const src = this.getAttribute('src');
if (!src) throw new Error('Loader requires a source.');

if (!this.loader) throw new Error(`Missing loader ${this.tagName}`);

// load and try attaching
this.loader.load(src, loaded => {
this.instance = loaded;
const attachAttribute = this.getAttribute('attach');
if (attachAttribute) {
this.executeAttach(attachAttribute, loaded);
}
this.refreshAttributes.call(this);
this.dispatchEvent(new CustomEvent('loaded', {
detail: {
instance: loaded,
},
}));
}, undefined, error => {
throw new Error(`error loading: ${src}` + error);
});
}

disconnectedCallback(): void {
super.disconnectedCallback();
this.disposeThreeObjects.call(this, [this.loader]);
}

}


return isLoader ? ThreeLoader : ThreeBase;
};

0 comments on commit eebc99f

Please sign in to comment.