io3fix
Version:
toolkit for interior apps
400 lines (370 loc) • 12.7 kB
JavaScript
// set api keys
io3d.config({
// home staging need api keys
// get yours from https://3d.io
publishableApiKey: null
})
// UI elements
var toolButtons = document.querySelectorAll('.btn-tool')
var styleButtons = document.querySelectorAll('.btn-style')
var btnFurnishEl = document.querySelector('#furnish')
var btnBuildEl = document.querySelector('#build')
var btnFloorPlanEl = document.querySelector('#floorplan')
var btnRestartEl = document.querySelector('#restart')
var controlsEl = document.querySelector('.controls')
var toolsEl = document.querySelector('#tools')
var stylesEl = document.querySelector('#styles')
// aframe elements
var rayCasterEl = document.querySelector('#raycaster')
var sceneEl = document.querySelector('a-scene')
var camEl = document.querySelector('a-camera')
var planeEl = document.querySelector('a-plane')
var furnishingEl = document.querySelector('#furnishings')
var structureEl = document.querySelector('#structure')
var drawingEl = document.querySelector('#drawing')
var cursor3dEl = document.querySelector('#cursor-3d')
var canvasEl
// inital settings
var isMobile = AFRAME.utils.device.isMobile()
if (isMobile) {
rayCasterEl.setAttribute('cursor', {rayOrigin: 'entity', fuse: false})
// rayCasterEl.setAttribute('geometry', {primitive: 'sphere', radius: 0.01})
// add shadow material to plane
planeEl.setAttribute('shadow-material', true)
// wait for aframe to load to get the canvas
document.querySelector('a-scene').addEventListener('loaded', initTracking)
} else {
document.querySelector('a-scene').addEventListener('loaded', function () {
io3d.utils.ui.message('click on the floor to draw a wall')
initDrawing()
})
}
// internals
var points = []
var drawMode = null
var sceneStructure
var addedItems = []
var initMsg
// bind events
toolButtons.forEach(btn => {
btn.addEventListener('click', switchMode)
})
styleButtons.forEach(btn => {
btn.addEventListener('click', changeStyle)
})
rayCasterEl.addEventListener('click', addPoint)
btnFurnishEl.addEventListener('click', getFurnishing)
btnBuildEl.addEventListener('click', buildStructure)
btnFloorPlanEl.addEventListener('click', getFloorPlan)
btnRestartEl.addEventListener('click', restart)
// workaround to get the initial plane position
function initTracking () {
initMsg = io3d.utils.ui.message('tap on a floor to initialize')
canvasEl = document.querySelector('canvas')
canvasEl.addEventListener('touchstart', hitTest, false)
}
function initDrawing () {
rayCasterEl.setAttribute('visible', true)
controlsEl.style.display = 'block'
}
function addPoint (e) {
// get data from 3d cursor
const hitWall = cursor3dEl.components['cursor-3d'].hitWall
const position = cursor3dEl.getAttribute('position') //components['cursor-3d'].position
if (drawMode === 'wall' || drawMode === 'window' || drawMode === 'door') points.push(position)
// position.y += 0.025
if (drawMode === 'wall' && points.length >= 2) {
var i = points.length - 1
// add a wall
addLine({start: points[i - 1], end: points[i], color: '#222'})
if (hitWall) points = []
} else if (hitWall && drawMode && points.length === 2) {
let i = points.length - 1
let color = drawMode === 'window' ? '#ddd' : '#f0f0f0'
let type = drawMode === 'window' ? 'window' : 'door'
let pW = hitWall.getAttribute('position')
// verify that points are in the correct order
let flipPoints = distance(points[i - 1], pW) < distance(points[1], pW)
let p1 = {x: distance(points[i - 1], pW), y: 0.01, z: 0}
let p2 = {x: distance(points[1], pW), y: 0.01, z: 0}
let start = flipPoints ? p1 : p2
let end = flipPoints ? p2 : p1
// add a window or door to the corresponding wall
addLine({start, end, color, type, parent: hitWall})
points = []
}
}
function addLine (args) {
var
start = args.start,
end = args.end,
type = args.type,
color = args.color || 'white',
parent = args.parent || drawingEl
box = document.createElement('a-entity'),
l = distance(start, end),
angle = pointAngle(start, end),
w = 0.1,
h = 0.005
box.setAttribute('position', AFRAME.utils.coordinates.stringify(start))
box.setAttribute('rotation', `0 ${angle} 0`)
box.setAttribute('line-element', {
type: type,
l,
w,
h,
color: color
})
box.setAttribute('class', 'collidable')
parent.appendChild(box)
}
function getFurnishing () {
// convert drawn aframe elements into scene structure
getSceneStructure()
// send sceneStructure to home staging ai
// optional set space label: io3d.staging.getFurnishings(result, {label: bedroom})
.then(result => {
var messages = [
'looking for furniture at the conference',
'buying furniture from nearby store',
'calling an interior designer',
'there is a chair, there is a sofa ... ',
'You said you want furniture?',
'How about something that goes really well with the floor?'
]
var space = result.find(el => el.type === 'polyfloor')
var area = space && getPolygonArea(space.polygon)
var label = 'dining_living'
if (area && area < 15) {
io3d.utils.ui.message('That is a tiny tiny room')
label = 'dining'
}
io3d.utils.ui.message(messages[Math.floor(Math.random() * messages.length)])
console.log(area, label)
return io3d.staging.getFurnishings(result, {spaceId: space.id, label: label})
})
// update sceneStructure for later use
.then(result => {
// let's hide the drawing on mobile
if (isMobile) drawingEl.setAttribute('visible', false)
cursor3dEl.setAttribute('visible', false)
toolsEl.style.display = 'none'
stylesEl.style.display = 'block'
rayCasterEl.removeEventListener('click', addPoint)
sceneStructure = result
return result
})
.then(placeFurniture)
.catch(err => {
io3d.utils.ui.message.error('nothing found - room too small?')
console.log(err)
})
}
function changeStyle (e) {
var style = e.srcElement.id
if (!sceneStructure) return
io3d.staging.replaceFurniture(sceneStructure, {query: style})
.then(placeFurniture)
.catch(err => {
console.log(err)
})
}
function placeFurniture (input) {
// convert sceneStructure to aframe Html so we can place it in the scene
var elements = io3d.scene.getAframeElementsFromSceneStructure(input)
// check for previously added elements and remove them
addedItems.forEach(el => {
el.parentNode.removeChild(el)
})
// add elements to the scene
elements.forEach(el => {
furnishingEl.appendChild(el)
})
// store added elements for later removal
addedItems = elements
}
function getSceneStructure () {
console.log('getting sceneStructure')
let _sceneStructure = []
const elements = document.querySelectorAll('a-entity[line-element]')
elements.forEach(el => {
let data = getElement3d(el)
if (data.type === 'wall') {
data.children = []
const children = el.childNodes
children.forEach(c => {
data.children.push(getElement3d(c))
})
_sceneStructure.push(data)
}
})
// snap walls
_sceneStructure = io3d.scene.snapWalls(_sceneStructure)
_sceneStructure = JSON.parse(JSON.stringify(_sceneStructure))
// FIXME: workaround for offline support
// return Promise.resolve(sceneStructure)
// get polygonal floor
return io3d.utils.services.call('Recognizer.recognizeFloors', {
walls: _sceneStructure.filter(el3d => el3d.type === 'wall')
})
.then(floors => {
if (floors.length === 0) {
io3d.utils.ui.message.error('walls are not closed - try again', {expire: 2000})
}
return Promise.resolve(_sceneStructure.concat(floors))
})
// normalize scene structure ( add default values and uuids )
.then(io3d.scene.normalizeSceneStructure)
}
function buildStructure () {
const visible = structureEl.getAttribute('visible')
if (visible) {
structureEl.setAttribute('visible', false)
return
} else {
structureEl.setAttribute('visible', true)
}
while (structureEl.firstChild) {
structureEl.removeChild(structureEl.firstChild)
}
getSceneStructure()
.then(result => {
const elements = io3d.scene.getAframeElementsFromSceneStructure(result)
elements.forEach(el => {
structureEl.appendChild(el)
})
})
.catch(console.error)
}
function getFloorPlan () {
console.log('get floorplan')
const svgEl = document.querySelector('#svg-container')
if (svgEl.style.display === 'block') {
svgEl.style.display = 'none'
svgEl.innerHTML = ''
toolsEl.style.display = 'block'
btnFurnishEl.style.display = 'block'
btnBuildEl.style.display = 'block'
return
}
getSceneStructure()
.then(result => {
var space = result.find(el => el.type === 'polyfloor')
if (!space) {
io3d.utils.ui.message.error('try to close the room first')
return
}
toolsEl.style.display = 'none'
btnFurnishEl.style.display = 'none'
btnBuildEl.style.display = 'none'
var message = {
method: 'svgFloorPlan',
params: {sceneStructure: result},
jsonrpc: '2.0',
id: Math.round(Math.random() * 1e20)
}
return fetch('https://io3d-tasks.herokuapp.com/', {
method: 'POST', body: JSON.stringify(message)
}).then(function (response) {
return response.json()
}).then(function (body) {
return body.result
}).catch(console.error)
})
.then(svgStr => {
svgEl.style.display = 'block'
svgEl.innerHTML = svgStr
})
.catch(console.error)
}
function switchMode (e) {
var el = e.srcElement
points = []
if (el.id === drawMode) {
drawMode = null
el.classList.remove('active')
} else {
drawMode = el.id
el.classList.add('active')
toolButtons.forEach(btn => {
if (btn.id !== el.id) btn.classList.remove('active')
})
}
console.log(el.id, drawMode)
}
function hitTest (e) {
console.log('click')
if (!e.touches[0]) {
return
}
var x = e.touches[0].pageX / window.innerWidth
var y = e.touches[0].pageY / window.innerHeight
console.log(x, y)
var vrDisplay = camEl.components.camera.camera.vrDisplay
var hits = vrDisplay.hitTest(x, y)
if (hits && hits.length) getPosFromHit(hits)
else io3d.utils.ui.message.error('try again - ideally standing', {expire: 2000})
}
function getPosFromHit (hits) {
// lets choose the last one which is most likely the floor
console.log(hits.length, 'hits')
var hit = hits[hits.length - 1]
if (!hit || !hit.modelMatrix) {
throw new Error('placeObjectAtHit requires a VRHit object')
}
const model = new THREE.Matrix4()
const tempPos = new THREE.Vector3()
const tempQuat = new THREE.Quaternion()
const tempScale = new THREE.Vector3()
model.fromArray(hit.modelMatrix)
model.decompose(tempPos, tempQuat, tempScale)
planeEl.setAttribute('position', AFRAME.utils.coordinates.stringify(tempPos))
furnishingEl.setAttribute('position', `0 ${tempPos.y} 0`)
structureEl.setAttribute('position', `0 ${tempPos.y} 0`)
canvasEl.removeEventListener('touchstart', hitTest, false)
initMsg.close()
io3d.utils.ui.message.success('floor detected - start', {expire: 1000})
// show drawing menu and cursor
setTimeout(function () {
initDrawing()
}, 1000)
}
function restart () {
drawingEl.setAttribute('visible', true)
cursor3dEl.setAttribute('visible', true)
toolsEl.style.display = 'block'
stylesEl.style.display = 'none'
rayCasterEl.addEventListener('click', addPoint)
const svgEl = document.querySelector('#svg-container')
sceneStructure = null
addedItems.forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el)
})
addedItems = []
const elements = document.querySelectorAll('a-entity[line-element]')
const walls = document.querySelectorAll('[io3d-wall]')
const floors = document.querySelectorAll('[io3d-polyfloor]')
elements.forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el)
})
walls.forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el)
})
floors.forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el)
})
svgEl.style.display = 'none'
}
// helper functions
function pointAngle (u, v) {
if (!u) u = {x: 0, z: 0}
return round((Math.atan2(-v.z + u.z, v.x - u.x)) * 180 / Math.PI)
}
function distance (a, b) {
var dist = Math.sqrt(Math.pow((b.x - a.x), 2) + Math.pow((b.y - a.y), 2) + Math.pow((b.z - a.z), 2))
return dist
}
function round (a, factor) {
if (!factor) factor = 1e2
return Math.round(a * factor) / factor
}