// reactive component for displaying the menu $editorPopup = (el) => new Proxy({ html: (opts) => ` <div> <b>#${$editor.selected.name}</b> <table class="editorPopup"> <tbody> <tr> <td><b class="badge">href</a></td> <td> <input type="text" id="href" placeholder="https://foo.com" maxlength="255" list="objects" onkeydown="document.querySelector('#editActions').classList.add('show')" onkeyup="$editor.selected.edited = $editor.selected.userData.href = this.value" value="${$editor.selected.userData.href||''}" /> </td> </tr> <tr> <td><b class="badge">src</a></td> <td> <input type="text" id="src" placeholder="https://foo.com" maxlength="255" list="objects" onkeydown="document.querySelector('#editActions').classList.add('show')" onkeyup="$editor.selected.edited = $editor.selected.userData.src = this.value" value="${$editor.selected.userData.src||''}" /> </td> </tr> <tr> <td><b class="badge">tag</a></td> <td> <input type="text" id="tag" placeholder="foo bar" maxlength="255" onkeydown="document.querySelector('#editActions').classList.add('show')" onkeyup="$editor.selected.edited = $editor.selected.userData.tag = this.value" value="${$editor.selected.userData.tag||''}" /> </td> </tr> </tbody> </table> <datalist id="objects"> <option>https://xrfragment.org/index.glb#pos=start</option> <option> ${opts.objectNames.join('</option><option>')} </option> </datalist> <br> <div id="editActions"> <button class="download" onclick="$editor.export()"><i class="gg-software-download"></i> download scene file</button> <br> NOTE: updates to src-values will require reloading the scene </div> </div> <style type="text/css"> table.editorPopup input{ min-width:200px; } table.editorPopup tr td:nth-child(1){ text-align:left; } #editActions{ visibility:hidden; } #editActions.show{ visibility:visible; } </style> `, init(opts){ el.innerHTML = this.html(opts) return (this.el = el) }, },{ get(me,k,v){ return me[k] }, set(me,k,v){ me[k] = v } }) $editor = (el,opts) => new Proxy({ html: ` <div style="position:absolute; width:100%; text-align:right; right:166px;"> <button class="btn edit-btn"> <i class="gg-pen"></i> </button> </div> <style type="text/css"> .xrf button.edit-btn{ height: 32px; width: 30px; margin-top: 7px; } .edit-btn.enabled, .edit-btn.enabled:hover{ background:black; } .edit-btn i.gg-pen{ margin-top: -26px; margin-left: 4px; width: 10px; color: var(--xrf-white); } </style> `, selecting: false, editing: false, helper: null, selected: null, objectNames: [], init(opts){ el.innerHTML = this.html window.frontend.el.querySelector('#topbar').appendChild(el); el.querySelector('.edit-btn').addEventListener('click', () => { if( $editor.selecting || $editor.editing ) this.reset() else{ $editor.selecting = true $editor.editing = false } }) xrf.addEventListener('export', (e) => this.updateOriginalScene(e) ) xrf.addEventListener('href', (opts) => { if( $editor.selecting || $editor.editing ) return opts.promise().reject("$editor should block hrefs while editing") // never resolve (block hrefs from interfering) }) return this }, reset(){ if( this.helper) xrf.scene.remove(this.helper) $editor.selecting = false $editor.editing = false }, export(){ window.frontend.download() this.reset() }, editNode(){ if( !this.selecting ) return console.log("not editing") this.reset() $editor.editing = true this.collectObjects() //`<b>XR Fragment:</b> #${this.selected.name}<br><br>${this.getMetaData(this.selected)}`),{ notify( $editorPopup( document.createElement('div') ).init(this) , { timeout:false, onclose: () => this.reset() }) }, collectObjects(){ this.objectNames = [] const escape = (str) => { let d = document.createElement('div') d.innerText = str return d.innerHTML } xrf.scene.traverse( (n) => { if( n.userData && n.userData.href ){ this.objectNames.push( escape(n.userData.href) ) } }) xrf.scene.traverse( (n) => { if( n.name ) this.objectNames.push( escape('#'+n.name) ) }) }, initEdit(scene){ if( !this.listenersInstalled ){ AFRAME.scenes[0].addEventListener('click', () => this.editNode() ) this.listenersInstalled = true } scene.traverse( (n) => { let highlight = (n) => (e) => { if( !this.selecting || this.editing ) return // do nothing if( this.helper){ if( this.helper.selected == n.uuid ) return // already selected xrf.scene.remove(this.helper) } this.selected = n this.helper = new THREE.BoxHelper( n, 0xFF00FF ) this.helper.material.linewidth = 4 this.helper.material.color = xrf.focusLine.material.color this.helper.material.dashSize = xrf.focusLine.material.dashSize this.helper.material.gapSize = xrf.focusLine.material.gapSize this.helper.selected = n.uuid xrf.scene.add(this.helper) let div = document.createElement('div') notify(`<b>#${n.name}</b><br>${this.getMetaData(this.selected)}`) } if( n.material ) n.addEventListener('mousemove', n.highlightOnMouseMove = highlight(n) ) }) }, getMetaData(n){ let html = `${n.userData.href ? `<b class="badge">href</b>${n.userData.href}<br>`:''}` html += `${n.userData.src ? `<b class="badge">src</b>${n.userData.src}<br>` :''}` html += `${n.userData.tag ? `<b class="badge">tag</b>${n.userData.tag}<br>` :''}` return html }, updateOriginalScene(e){ const {scene,ext} = e scene.traverse( (n) => { if( !n.name ) return // overwrite node with modified userData from scene let o = xrf.scene.getObjectByName(n.name) if( o && o.edited ){ for( let i in o.userData ) n.userData[i] = o.userData[i] } }) } }, { get(me,k,v){ return me[k] }, set(me,k,v){ me[k] = v switch( k ){ case "selecting":{ lookctl = $('[look-controls]').components['look-controls'] if( v ){ lookctl.pause() // prevent click-conflict notify("click an object to reveal XR Fragment metadata") xrf.interactive.raycastAll = true me.initEdit(xrf.scene) lookctl.pause() // prevent click-conflict el.querySelector('.edit-btn').classList.add(['enabled']) }else{ lookctl.pause() // prevent click-conflict xrf.scene.traverse( (n) => { if( n.highlightOnMouseMove ){ n.removeEventListener( 'mousemove', n.highlightOnMouseMove ) } }) lookctl.play() // prevent click-conflict (resume) el.querySelector('.edit-btn').classList.remove(['enabled']) } break; } } }, }) // reactify component! document.addEventListener('frontend:ready', (e) => { window.$editor = $editor( document.createElement('div') ).init(e.detail) })