Skip to content

Commit

Permalink
create the <lume-collada-model> element
Browse files Browse the repository at this point in the history
Also ensures that GltfModel stuff is re-exported.
  • Loading branch information
trusktr committed Feb 17, 2021
1 parent 298d795 commit 8af9b93
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 5 deletions.
69 changes: 69 additions & 0 deletions packages/lume/src/core/ColladaModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Node, {NodeAttributes} from './Node.js'

import type {ColladaModelBehavior, ColladaModelBehaviorAttributes} from '../html/index.js'

export type ColladaModelAttributes = NodeAttributes

/**
* @element lume-collada-model
* @class ColladaModel - Defines the `<lume-collada-model>` element, for loading 3D
* models in the Collada format (.dae files). It is similar to an `<img>` tag, but for 3D.
*
* HTML Example:
*
* ```html
* <lume-scene webgl>
* <lume-collada-model src="path/to/model.dae"></lume-collada-model>
* </lume-scene>
* ```
*
* JavaScript Example:
*
* ```js
* const scene = new Scene
* document.body.append(scene)
* const model = new ColladaModel
* model.src = 'path/to/model.dae'
* scene.add(model)
* ```
*/
export default class ColladaModel extends Node {
static defaultElementName = 'lume-collada-model'
static defaultBehaviors = ['collada-model']

// FIXME, without this accessor, the src property of the
// ColladaModelBehavior may not be initially triggered when using JSX as in
// <lume-collada-model src={"path/to/file.dae"}>. Why?
// This accessor should not be required, because the behaviors already
// set up the observation mechanism on their host elements.
get src() {
return this.__src
}
set src(v) {
this.__src = v
}

private __src: string = ''
}

export {ColladaModel}

import type {ElementAttributes} from '@lume/element'

declare module '@lume/element' {
namespace JSX {
interface IntrinsicElements {
'lume-collada-model': ElementAttributes<
ColladaModel,
ColladaModelAttributes,
ElementAttributes<ColladaModelBehavior, ColladaModelBehaviorAttributes>
>
}
}
}

declare global {
interface HTMLElementTagNameMap {
'lume-collada-model': ColladaModel
}
}
9 changes: 7 additions & 2 deletions packages/lume/src/core/Events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {Constructor} from 'lowclass'
import type {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js'
import type {Collada} from 'three/examples/jsm/loaders/ColladaLoader.js'
import type {Group} from 'three/src/objects/Group.js'

export class EventTypes {
Expand Down Expand Up @@ -34,12 +35,16 @@ export class EventTypes {
// This event is fired when an obj-model element, or a node element with an
// obj-model behavior, has loaded it's model.
public MODEL_LOAD: {format: string; model: Group},
// Fired when a gltf-model element has loaded the GLTF model.
public GLTF_LOAD: {model: GLTF},
// Fired by elements that load resources. See
// https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent
public PROGRESS: ProgressEvent,
// XXX Maybe we should combine loader events into one, instead of a new set of events per loader?
// Fired when a gltf-model element has loaded the GLTF model.
public GLTF_LOAD: {model: GLTF},
public GLTF_ERROR: {src: string; dracoDecoderPath: string},
// Fired when a collada-model element has loaded the Collada model.
public COLLADA_LOAD: {model: Collada},
public COLLADA_ERROR: {src: string},
) {}
}

Expand Down
24 changes: 24 additions & 0 deletions packages/lume/src/core/GltfModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ export type GltfModelAttributes = NodeAttributes
export default class GltfModel extends Node {
static defaultElementName = 'lume-gltf-model'
static defaultBehaviors = ['gltf-model']

// FIXME, without the following accessors, the src and dracoDecoder
// properties of the GltfModelBehavior may not be initially triggered when
// using JSX as in <lume-gltf-model src={"path/to/file.gltf"}>. Why?
// These accessors should not be required, because the behaviors already
// set up the observation mechanism on their host elements.

get src() {
return this.__src
}
set src(v) {
this.__src = v
}

private __src: string = ''

get dracoDecoder() {
return this.__dracoDecoder
}
set dracoDecoder(v) {
this.__dracoDecoder = v
}

private __dracoDecoder: string = ''
}

export {GltfModel}
Expand Down
2 changes: 2 additions & 0 deletions packages/lume/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export * from './Sizeable.js'
export * from './Sphere.js'
export * from './Transformable.js'
export * from './TreeNode.js'
export * from './GltfModel.js'
export * from './ColladaModel.js'
import * as Utility from './Utility.js'
export {Utility}
export * from './WebGLRendererThree.js'
Expand Down
106 changes: 106 additions & 0 deletions packages/lume/src/html/behaviors/ColladaModelBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'element-behaviors'
import {reactive, autorun, stringAttribute} from '@lume/element'
import {ColladaLoader} from 'three/examples/jsm/loaders/ColladaLoader.js'
import {disposeObjectTree} from '../../utils/three.js'
import {Events} from '../../core/Events.js'
import {RenderableBehavior} from './RenderableBehavior.js'

import type {StopFunction} from '@lume/element'
import type {Collada} from 'three/examples/jsm/loaders/ColladaLoader.js'

export type ColladaModelBehaviorAttributes = 'src'

@reactive
export class ColladaModelBehavior extends RenderableBehavior {
/** Path to a .dae file. */
@stringAttribute('') src = ''

colladaLoader?: ColladaLoader
model: Collada | null = null

protected static _observedProperties = ['src', ...(RenderableBehavior._observedProperties || [])]

// This is incremented any time we need a pending load() to cancel (f.e. on
// src change, or unloadGL cycle), so that the loader will ignore the
// result when a version change has happened.
private __version = 0

private __stopFns: StopFunction[] = []

loadGL() {
if (!super.loadGL()) return false

this.colladaLoader = new ColladaLoader()

this.__stopFns.push(
autorun(() => {
this.src
console.log(this.src)

this.__cleanupModel()

// TODO We can update only the material or model specifically
// instead of reloading the whole object.
this.__version++
this.__loadObj()
}),
)

return true
}

unloadGL() {
if (!super.unloadGL()) return false

for (const stop of this.__stopFns) stop()

this.colladaLoader = undefined

this.__cleanupModel()

// Increment this in case the loader is still loading, so it will ignore the result.
this.__version++

return true
}

private __cleanupModel() {
if (this.model) disposeObjectTree(this.model.scene)
this.model = null
}

private __loadObj() {
const {src, __version} = this

console.log('load model!!!!', src)

if (!src) return

// In the following colladaLoader.load() callbacks, if __version doesn't
// match, it means this.src or this.dracoDecoder changed while
// a previous model was loading, in which case we ignore that
// result and wait for the next model to load.

this.colladaLoader!.load(
src,
model => __version == this.__version && this.__setModel(model),
progress => __version == this.__version && this.element.emit(Events.PROGRESS, progress),
error => __version == this.__version && this.__onError(src, error),
)
}

private __onError(src: string, error: ErrorEvent) {
const message = error?.message ?? `Failed to load ${this.element.tagName.toLowerCase()} with src "${src}".`
console.warn(message)
this.element.emit(Events.COLLADA_ERROR, {src})
}

private __setModel(model: Collada) {
this.model = model
this.element.three.add(model.scene)
this.element.emit(Events.COLLADA_LOAD, {model})
this.element.needsUpdate()
}
}

elementBehaviors.define('collada-model', ColladaModelBehavior)
13 changes: 10 additions & 3 deletions packages/lume/src/html/behaviors/GltfModelBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default class GltfModelBehavior extends RenderableBehavior {

protected static _observedProperties = ['src', 'dracoPath', ...(RenderableBehavior._observedProperties || [])]

// This is incremented any time we need a pending load() to cancel (f.e. on
// src change, or unloadGL cycle), so that the loader will ignore the
// result when a version change has happened.
private __version = 0

private __stopFns: StopFunction[] = []
Expand All @@ -50,7 +53,7 @@ export default class GltfModelBehavior extends RenderableBehavior {
this.src
this.dracoDecoder

if (!firstRun) this.__cleanupModel()
this.__cleanupModel()

// TODO We can update only the material or model specifically
// instead of reloading the whole object.
Expand All @@ -75,6 +78,9 @@ export default class GltfModelBehavior extends RenderableBehavior {

this.__cleanupModel()

// Increment this in case the loader is still loading, so it will ignore the result.
this.__version++

return true
}

Expand All @@ -88,7 +94,7 @@ export default class GltfModelBehavior extends RenderableBehavior {

if (!src) return

// In the followinggltfLoader.load() callbacks, if __version doesn't
// In the following gltfLoader.load() callbacks, if __version doesn't
// match, it means this.src or this.dracoDecoder changed while
// a previous model was loading, in which case we ignore that
// result and wait for the next model to load.
Expand All @@ -103,7 +109,8 @@ export default class GltfModelBehavior extends RenderableBehavior {

private __onError(src: string, dracoDecoder: string, error: ErrorEvent) {
const message =
error?.message ?? `Failed to load <gltf-model> with src "${src}" and dracoDecoder "${dracoDecoder}"`
error?.message ??
`Failed to load ${this.element.tagName.toLowerCase()} with src "${src}" and dracoDecoder "${dracoDecoder}".`
console.warn(message)
this.element.emit(Events.GLTF_ERROR, {src, dracoDecoder})
}
Expand Down
2 changes: 2 additions & 0 deletions packages/lume/src/html/behaviors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export * from './BaseMaterialBehavior.js'
// Concrete behaviors
export * from './BasicMaterialBehavior.js'
export * from './BoxGeometryBehavior.js'
export * from './ColladaModelBehavior.js'
export * from './DOMNodeGeometryBehavior.js'
export * from './DOMNodeMaterialBehavior.js'
export * from './GltfModelBehavior.js'
export * from './GltfModelBehavior.js'
export * from './ObjModelBehavior.js'
export * from './PhongMaterialBehavior.js'
export * from './PlaneGeometryBehavior.js'
Expand Down
3 changes: 3 additions & 0 deletions packages/lume/src/html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './HTMLNode.js'
export * from './HTMLScene.js'
// export HTMLPushPaneLayout from './HTMLPushPaneLayout.js'

// TODO replace the non-DRY code here with the same pattern as with the defineElements() call below.
import Scene from '../core/Scene.js'
import Node from '../core/Node.js'
import Mesh from '../core/Mesh.js'
Expand All @@ -17,6 +18,7 @@ import PerspectiveCamera from '../core/PerspectiveCamera.js'
import AutoLayoutNode from '../layout/AutoLayoutNode.js'
import ObjModel from '../core/ObjModel.js'
import GltfModel from '../core/GltfModel.js'
import {ColladaModel} from '../core/ColladaModel.js'
// import PushPaneLayout from '../components/PushPaneLayout.js'
import RoundedRectangle from '../core/RoundedRectangle.js'

Expand All @@ -40,6 +42,7 @@ export function useDefaultNames() {
AutoLayoutNode,
ObjModel,
GltfModel,
ColladaModel,
// PushPaneLayout,
RoundedRectangle,
]
Expand Down

0 comments on commit 8af9b93

Please sign in to comment.