src/SOM.js
/**
Functions that generate Spatial Object Model (SOM) elements like som.group(...)
Underlying the SOM is the Three.js scene som
*/
import AssetLoader from './AssetLoader.js'
import Attributes from './style/Attributes.js'
import { SelectorFragmentList } from './style/Selector.js'
const som = {}
export default som
const assetLoader = AssetLoader.Singleton
const fontLoader = new THREE.FontLoader()
const mtlLoader = new THREE.MTLLoader()
const objLoader = new THREE.OBJLoader()
const _textureLoader = new THREE.TextureLoader()
som.textureLoader = function() {
return _textureLoader
}
/**
The behind the scene function that generates an enhanced Object3D when you call som.foo(...)
if the first elements in `params` is an array, the values of the array will be passed as separate parameters into the constructor of the instance
*/
som.nodeFunction = function(clazz, ...params) {
let instance = null
let consumedFirstParam = false
if (Array.isArray(params[0])) {
consumedFirstParam = true
instance = new THREE[clazz](...params[0])
} else {
instance = new THREE[clazz]()
}
// Append the children parameters
for (let i = 0; i < params.length; i++) {
if (i == 0 && consumedFirstParam) continue
instance.append(params[i])
}
return instance
}
som.fonts = new Map() // url => THREE.Font
const _shapeCurveSegments = 4
function loadText(resultGroup, text, fontURL, options) {
text = String(text)
if (!text || text.trim().length === 0) {
if (resultGroup.geometry) {
resultGroup.geometry.dispose()
resultGroup.geometry = new THREE.ShapeGeometry([], 1)
}
return
}
if (som.fonts.has(fontURL)) {
const shapes = som.fonts.get(fontURL).generateShapes(text, options.size)
if (resultGroup.geometry) resultGroup.geometry.dispose()
resultGroup.geometry = new THREE.ShapeGeometry(shapes, _shapeCurveSegments)
resultGroup.styles.geometryIsDirty = true
resultGroup.styles.setAncestorsLayoutDirty()
} else {
assetLoader.get(fontURL).then(blob => {
if (!blob) {
console.error('Failed to fetch the font', fontURL)
return
}
const blobURL = URL.createObjectURL(blob)
fontLoader.load(
blobURL,
loadedFont => {
som.fonts.set(fontURL, loadedFont)
const shapes = loadedFont.generateShapes(text, options.size)
if (resultGroup.geometry) resultGroup.geometry.dispose()
resultGroup.geometry = new THREE.ShapeGeometry(shapes, _shapeCurveSegments)
resultGroup.styles.geometryIsDirty = true
resultGroup.styles.setAncestorsLayoutDirty()
URL.revokeObjectURL(blobURL)
},
() => {},
err => {
console.error('Could not load font', fontURL, err)
URL.revokeObjectURL(blobURL)
}
)
})
}
}
/**
Creates a THREE.Group that manages a chunk of text
*/
som.text = (text = '', options = {}) => {
options = Object.assign(
{
size: 0.12,
height: 0.05,
curveSegments: _shapeCurveSegments,
bevelEnabled: false,
material: null,
color: 0x444444,
fontURL: '/static/potassium-es/fonts/helvetiker_regular.typeface.json'
},
options
)
if (options.material === null) {
options.material = new THREE.MeshStandardMaterial({
color: options.color,
side: THREE.DoubleSide
})
}
const fontOptions = {
size: options.size,
height: options.height,
curveSegments: options.curveSegments,
bevelEnabled: options.bevelEnabled
}
let currentText = null
const resultGroup = som.mesh([undefined, options.material])
resultGroup.name = 'Text'
resultGroup.addClass('text')
resultGroup.isText = true
resultGroup.setFontOptions = newOptions => {
Object.assign(fontOptions, newOptions)
resultGroup.setText(currentText, true)
}
resultGroup.setText = (newText, force = false) => {
newText = newText || ''
if (force === false && newText === currentText) return
currentText = newText
loadText(resultGroup, currentText, options.fontURL, fontOptions)
}
resultGroup.setText(currentText)
return resultGroup
}
/**
Creates a THREE.Mesh containing a THREE.BoxBufferGeometry
*/
som.cube = (size = 1, options = {}) => {
options = Object.assign(
{
color: 0x444444
},
options
)
let material = null
if (options.material) {
material = options.material
} else {
material = new THREE.MeshStandardMaterial({ color: options.color })
}
const result = new THREE.Mesh(THREE.MakeCubeGeometry(size), material)
// set up for kss element selection, like `cube {}` or `node[name=Cube] {}`
result.name = 'Cube'
result.isCube = true
return result
}
/**
Load an OBJ file
@return {THREE.Group}
*/
som.obj = (objPath, successCallback = null, failureCallback = null) => {
const group = som.group()
loadObj(objPath)
.then(obj => {
group.add(obj)
if (successCallback !== null) successCallback(group, obj)
})
.catch((...params) => {
if (failureCallback !== null) failureCallback(group, ...params)
})
return group
}
/**
The methods created from these info just pass through any params to the class constructor.
For example, creating a MeshBasicMaterial will be som.meshBasicMaterial(...params).
*/
som.SUPPORT_CLASSES = [
{ class: 'Box3', name: 'box3' },
{ class: 'Line', name: 'line' },
{ class: 'Euler', name: 'euler' },
{ class: 'Matrix4', name: 'matrix4' },
{ class: 'Vector3', name: 'vector3' },
{ class: 'Geometry', name: 'geometry' },
{ class: 'SphereBufferGeometry', name: 'sphereBufferGeometry' },
{ class: 'MeshBasicMaterial', name: 'meshBasicMaterial' },
{ class: 'LineBasicMaterial', name: 'lineBasicMaterial' },
{ class: 'MeshLambertMaterial', name: 'meshLambertMaterial' },
{ class: 'MeshStandardMaterial', name: 'meshStandardMaterial' }
]
for (const classInfo of som.SUPPORT_CLASSES) {
const innerClazz = classInfo.class
som[classInfo.name] = function(...params) {
return new THREE[innerClazz](...params)
}
}
/**
The methods created from these classes use the som.nodeFuction (see below)
*/
som.GRAPH_CLASSES = [
{ class: 'Mesh', name: 'mesh' },
{ class: 'Scene', name: 'scene' },
{ class: 'Group', name: 'group' },
{ class: 'AmbientLight', name: 'ambientLight' },
{ class: 'HemisphereLight', name: 'hemisphereLight' },
{ class: 'DirectionalLight', name: 'directionalLight' },
{ class: 'PerspectiveCamera', name: 'perspectiveCamera' }
]
// This loop generates the element generating functions like som.group(...)
for (const somClassInfo of som.GRAPH_CLASSES) {
const innerClazz = somClassInfo.class
som[somClassInfo.name] = function(...params) {
return som.nodeFunction(innerClazz, ...params)
}
}
function loadObj(objPath) {
const objName = objPath.split('/')[objPath.split('/').length - 1]
const baseURL = objPath.substring(0, objPath.length - objName.length)
const mtlName = objName.split('.')[objName.split(':').length - 1] + '.mtl'
const mtlPath = baseURL + mtlName
return new Promise((resolve, reject) => {
assetLoader.get(mtlPath).then(mtlBlob => {
if (mtlBlob === null) {
reject(`Could not load ${mtlPath}`)
return
}
assetLoader.get(objPath).then(objBlob => {
if (objBlob === null) {
reject(`Could not load ${objPath}`)
return
}
const objURL = URL.createObjectURL(objBlob)
const mtlURL = URL.createObjectURL(mtlBlob)
mtlLoader.setTexturePath(baseURL)
mtlLoader.load(
mtlURL,
materials => {
materials.preload()
objLoader.setMaterials(materials)
objLoader.load(
objURL,
obj => {
URL.revokeObjectURL(objURL)
obj.name = 'OBJ'
resolve(obj)
},
() => {},
(...params) => {
console.error('Failed to load obj', ...params)
reject(...params)
URL.revokeObjectURL(objURL)
}
)
URL.revokeObjectURL(mtlURL)
},
() => {},
(...params) => {
console.error('Failed to load mtl', ...params)
reject(...params)
URL.revokeObjectURL(mtlURL)
}
)
})
})
})
}