UNPKG

io3fix

Version:

toolkit for interior apps

385 lines (338 loc) 13.2 kB
import runtime from '../../core/runtime.js' import poll from '../../utils/poll.js' import getFromStorage from '../../storage/get.js' import getDefaultsByType from './validate/get-defaults-by-type' var validTypes = [ 'closet', 'column', 'door', 'floor', 'group', 'interior', 'kitchen', 'level', 'object', 'plan', 'polyfloor', 'railing', 'wall', 'window' ] var noIo3dComponents = ['plan', 'level', 'group'] export default function toAframeElements(sceneStructure, options) { if (!sceneStructure) { console.error('nothing to convert') return } // check if the request was made by a browser runtime.assertBrowser() // api options = options || {} var isArray = Array.isArray(sceneStructure) sceneStructure = isArray ? sceneStructure : [sceneStructure] // start parsing var html = getAframeElementsFromSceneStructure(sceneStructure) // camera bookmarks are separately extracted from the sceneStructure // and added as waypoints to an aframe camera parent var camHtml = parseCameraBookmarks(sceneStructure, isArray ? html : html[0]) var result // let's make sure input type is similar to output type if (!camHtml) { result = isArray ? html : html[0] } else { result = (Array.isArray(html) ? html : [html]).concat(camHtml) } return result } // recursive parsing through sceneStructre function getAframeElementsFromSceneStructure(sceneStructure, parent) { var collection = parent ? null : [] // use collection or parent sceneStructure.forEach(function(element3d) { // check if type is supported in aframe if (validTypes.indexOf(element3d.type) > -1) { // get html attributes from element3d objects var el = addEntity({ attributes: getAttributes(element3d), parent: parent }) // the level scene might be baked if (element3d.type === 'level') { createBakedElement(el, element3d) } // recursively proceed through sceneStructure if (element3d.children && element3d.children.length) getAframeElementsFromSceneStructure(element3d.children, el) if (collection) collection.push(el) } }) return collection } // get html attributes from element3d params export const getAttributes = function(element3d) { var attributes = {} // map type specific attributes // camera-bookmarks and bakedModel are handled separately var type = element3d.type var validParams = getDefaultsByType(type).params // support for old material definitions // TODO: this should be cleaned up in the database var elKeys = Object.keys(element3d) elKeys.forEach(function(key) { if (key.indexOf('Material') > -1) { if (!element3d.materials) element3d.materials = {} element3d.materials[key.replace('Material', '')] = element3d[key] delete element3d[key] } else if (key === 'material' && type === 'floor') { if (!element3d.materials) element3d.materials = {} element3d.materials.top = element3d[key] } }) var paramKeys = Object.keys(validParams) // plan, level, group don't have their own component var skipComponent = noIo3dComponents.indexOf(type) > -1 if (!skipComponent) { attributes['io3d-' + type] = '' paramKeys.forEach(function(param) { if (element3d[param] !== undefined && !validParams[param].skipInAframe) { // materials have to be serialized if (param === 'materials') attributes['io3d-' + type] += stringifyMaterials(element3d.materials) // polygons have to be serialized else if (param === 'polygon') attributes['io3d-' + type] += param + ': ' + JSON.stringify(element3d.polygon) + '; ' // stringify window segmentation arrays else if (param === 'columnRatios' || param === 'rowRatios') attributes['io3d-' + type] += param + ': ' + JSON.stringify(element3d[param]) + '; ' // skip plan and level and map all remaining params else if (type !== 'plan' && type !== 'level') attributes['io3d-' + type] += param + ': ' + element3d[param] + '; ' } }) } switch (element3d.type) { case 'plan': attributes['class'] = 'io3d-scene' break case 'level': attributes['class'] = 'io3d-level' break case 'group': attributes['class'] = 'io3d-group' break case 'interior': if (element3d.src) { attributes['io3d-furniture'] = 'id: ' + element3d.src.substring(1) // apply custom material settings for furniture items if (element3d.materials) { var mats = element3d.materials // materials can be saved as arrays if (Array.isArray(mats)) { var matObj = {} mats.forEach(function (mat) { if (mat.mesh && mat.material) matObj[mat.mesh] = mat.material }) mats = matObj } // apply alternative material setting to io3d-furniture attribute if (typeof mats === 'object') { Object.keys(mats).forEach(function (mesh) { if (mesh && mats[mesh]) attributes['io3d-furniture'] += '; material_' + mesh.replace(/\s/g, '_') + ':' + mats[mesh] }) } } attributes['shadow'] = 'cast: true; receive: false' } else console.warn('unsupported interior type') break case 'object': attributes['io3d-data3d'] = 'key: ' + element3d.object attributes['shadow'] = 'cast: true; receive: true' break } // and generic attributes that apply for all nodes // check for custom scale if (element3d.sourceScale && element3d.sourceScale !== 1) attributes.scale = element3d.sourceScale + ' ' + element3d.sourceScale + ' ' + element3d.sourceScale // check for axis flipping of source file if (element3d.flipYZ) element3d.rx = element3d.rx ? element3d.rx -= 90 : -90 // toggle visibility if (element3d.bake && element3d.bakeStatus === 'done') attributes.visible = false if (element3d.visible && !element3d.visible.bird && !element3d.visible.person && !element3d.visible.floorplan) attributes.visible = false // make sure we have a valid position element3d.x = element3d.x || 0 element3d.y = element3d.y || 0 element3d.z = element3d.z || 0 element3d.ry = element3d.ry|| 0 // stringify location objects attributes.position = element3d.x + ' ' + element3d.y + ' ' + element3d.z attributes.rotation = (element3d.rx || 0) + ' ' + element3d.ry + ' 0' attributes['io3d-uuid'] = element3d.id return attributes } // creates a child for a baked model in the current element function createBakedElement(parentElem, element3d) { // we might have a scene that has no baked level if (!element3d.bakedModelUrl && !element3d.bakePreviewStatusFileKey) { console.warn('Level without bakedModelUrl: ', element3d) return } // set data3d.buffer file key var attributes = { 'io3d-data3d': 'key: ' + element3d.bakedModelUrl, shadow: 'cast: false; receive: true' } // set lightmap settings if (element3d.lightMapIntensity) { attributes['io3d-data3d'] += '; lightMapIntensity: ' + element3d.lightMapIntensity + '; lightMapExposure: ' + element3d.lightMapCenter } var bakedElem = addEntity({ attributes: attributes, parent: parentElem }) if (element3d.bakeRegularStatusFileKey || element3d.bakePreviewStatusFileKey) { updateOnBake(bakedElem, element3d) } } // creates a camera and tour-waypoints from scene structure export const parseCameraBookmarks = function (sceneStructure, planRoot) { var bookmarks = flattenSceneStructure(sceneStructure[0]).filter(function (element) { return element.type === 'camera-bookmark' }) if (bookmarks.length === 0) return var lastSavePosition = bookmarks.find(function (element) { return element.name === 'lastSavePosition' }) var camPosition = "0 1.6 0" var camRotation = "0 0 0" if (lastSavePosition) { if (lastSavePosition.mode === 'bird') { lastSavePosition.y += lastSavePosition.distance } var location = sphericalToCartesian(lastSavePosition) camPosition = location.position camRotation = location.rotation } var camera = addEntity({ attributes: { camera: '', tour: 'autoStart: false', 'wasd-controls': '', 'look-controls': '', // stringify location objects position: camPosition, rotation: camRotation } }) bookmarks .filter(function (element) { return element.name !== 'lastSavePosition' }) .forEach(function (element) { var location = sphericalToCartesian(element) addEntity({ parent: camera, attributes: { // per default no name is set in the editor 'tour-waypoint': element.name || 'Waypoint', 'io3d-uuid': element.id, // stringify location objects position: location.position, rotation: location.rotation } }) }) return camera } function sphericalToCartesian(element) { // Rotate look-at point on the XZ plane around parent's center var angleY = -element.parent.ry * Math.PI / 180 var rotatedX = element.x * Math.cos(angleY) - element.z * Math.sin(angleY) var rotatedZ = element.z * Math.cos(angleY) + element.x * Math.sin(angleY) // Get world space coordinates for our look-at point var position = {} position.x = element.parent.x + rotatedX position.y = element.parent.y + element.y position.z = element.parent.z + rotatedZ // Get camera position by rotating around the look-at point at a distance of element.distance. // This will make very little difference for 'person'-type bookmarks, but it should be done for // the sake of correctness. var rx = element.rx * Math.PI / 180 var ry = (element.parent.ry + element.ry) * Math.PI / 180 position.x -= element.distance * Math.sin(ry) * Math.cos(rx) position.y -= element.distance * Math.sin(rx) position.z -= element.distance * Math.cos(ry) * Math.cos(rx) // Finally, get camera rotation. Note that it's necessary to add 180 degrees to the rotation angle // because of A-Frame's different convention. Also note that when animating the camera, the rotation // angle should be interpolated through the shortest arc in order to avoid swirling motion. var rotation = {} rotation.x = element.rx rotation.y = 180 + element.parent.ry + element.ry if (element.mode === 'bird' || element.mode === 'floorplan') position.y = Math.max(element.distance - 5, 8) return { rotation: rotation.x + ' ' + rotation.y + ' 0', //rotation, position: position.x + ' ' + position.y + ' ' + position.z // position } } function addEntity(args) { var tag = args.tag || 'a-entity', parent = args.parent, attributes = args.attributes || {} var el = document.createElement(tag) Object.keys(attributes).forEach(function(key) { el.setAttribute(key, attributes[key]) }) if (parent) return parent.appendChild(el) else return el } function updateOnBake(htmlElement, element3d) { var statusFileKey = element3d.bakeRegularStatusFileKey || element3d.bakePreviewStatusFileKey pollStatusFile(statusFileKey) .then(function (bakedModelKey) { var attribValue = 'key: ' + bakedModelKey if (element3d.lightMapIntensity) { attribValue += '; lightMapIntensity: ' + element3d.lightMapIntensity + '; lightMapExposure: ' + element3d.lightMapCenter } htmlElement.setAttribute('io3d-data3d', attribValue) }) } // TODO: Migrate that to a shared helper function pollStatusFile(fileKey) { return poll(function onPoll(onSuccess, onError, next) { /* 1. Read status file content 2. Check if we're done -> call onSuccess Check if it failed -> call onError Otherwise call next */ getFromStorage(fileKey, { type: "json", cdn: false }).then(function checkContent(content) { if (content && content.params) { switch (content.params.status) { case 'SUCCESS': onSuccess(content.params.data) break case 'PROCESSING': case 'ENQUEUED': next() break default: onError(content.params.data) } } }) }) } function flattenSceneStructure(sceneStructure, parent) { var result = [] sceneStructure.parent = parent result.push(sceneStructure) if(sceneStructure.children) { sceneStructure.children.forEach(function (child) { result = result.concat(flattenSceneStructure(child, sceneStructure)) }) } return result } function stringifyMaterials (materials) { var matStr = '' var matKeys = Object.keys(materials) matKeys.forEach(function (key) { // currently only library materials are supported if (typeof materials[key] === 'string') { matStr += 'material_' + key + ':' + materials[key] + '; ' } else if (typeof materials[key] === 'object' && materials[key].mesh && materials[key].material) { matStr += 'material_' + materials[key].mesh + ':' + materials[key].material + '; ' } }) return matStr } function deg2rad(x) { return x * (Math.PI / 180) } function rad2deg(x) { return x * (180 / Math.PI) }