Refactor js files (#48)
This pr is a significant reorg of the js files. 1) All non-basic functions have been moved out of the main glmv.js 2) 'Metadata' has been universally changed to mvconfig to avoid conflict with actual file metadata 3) Configuration changing functions have been split off into glmv-mvconfig.js 4) Hotspot modification functions have been split off into glmv-hs.js Reviewed-on: #48
This commit is contained in:
453
modules/glmv.js
453
modules/glmv.js
@@ -1,7 +1,4 @@
|
||||
let slideShowInterval = null
|
||||
let grabHotspot = null
|
||||
let deleteHotspot = null
|
||||
let currentSet = 'default'
|
||||
|
||||
/**
|
||||
* Disables hiding of various child items
|
||||
@@ -12,174 +9,6 @@ modelLoaded = function(e) {
|
||||
if (typeof enableMenu != 'undefined') {enableMenu()}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the json string in the edit panel
|
||||
* and updates hotspot elements
|
||||
*
|
||||
* @return {bool} true on successful read and update
|
||||
*/
|
||||
readMetadata = function() {
|
||||
let hotspotsObj = []
|
||||
let metadata
|
||||
let slotNum = 1
|
||||
|
||||
createHotspot = function(hsLabel, hsSlot, hsTag) {
|
||||
let newHs = document.createElement('button')
|
||||
newHs.classList.add('Hotspot')
|
||||
newHs.setAttribute('slot',`hotspot-${hsSlot}`)
|
||||
newHs.setAttribute('ontouchstart', 'event.stopPropagation()')
|
||||
newHs.setAttribute('onclick', 'onAnnotation(event)')
|
||||
newHs.setAttribute('onmousedown', 'grabAnnotation(event)')
|
||||
newHs.setAttribute('onmouseup', 'releaseAnnotation(event)')
|
||||
Object.keys(metadata.annotations[hsLabel]).forEach((prop) => {
|
||||
newHs.setAttribute(prop, metadata.annotations[hsLabel][prop])
|
||||
})
|
||||
let newAn = document.createElement('div')
|
||||
newAn.classList.add('HotspotAnnotation', 'HiddenAnnotation')
|
||||
newAn.innerText = hsLabel
|
||||
newHs.appendChild(newAn)
|
||||
newLabel = document.createElement('span')
|
||||
newLabel.innerText = hsTag || (hsSlot)
|
||||
newHs.appendChild(newLabel)
|
||||
hotspotsObj.push(newHs)
|
||||
}
|
||||
|
||||
try {
|
||||
[_, metadata] = extractMetadata()
|
||||
} catch (err) {
|
||||
console.warn('Failed to read model metadata:' + err.message)
|
||||
return false
|
||||
}
|
||||
if (currentSet != 'default' && metadata.annotationSets[currentSet]) {
|
||||
metadata.annotationSets[currentSet].forEach(hs => {
|
||||
createHotspot(hs, slotNum)
|
||||
slotNum += 1
|
||||
})
|
||||
}
|
||||
Object.keys(metadata.annotations).forEach(hs => {
|
||||
if (currentSet != 'default' && metadata.annotationSets[currentSet] && metadata.annotationSets[currentSet].includes(hs)) {
|
||||
return
|
||||
}
|
||||
let label = (currentSet != 'default' && metadata.annotationSets[currentSet]) ? '-' : null
|
||||
createHotspot(hs, slotNum, label)
|
||||
slotNum += 1
|
||||
})
|
||||
|
||||
$('model-viewer button').remove()
|
||||
const mView = $('model-viewer')[0]
|
||||
hotspotsObj.forEach(hs => {
|
||||
mView.appendChild(hs)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current hotspots into json object
|
||||
* and writes the json string to the edit panel
|
||||
*
|
||||
* @return {bool} true on successful write to edit panel
|
||||
*/
|
||||
writeMetadata = function () {
|
||||
let annotationsObj = {}
|
||||
currentButtons = $('.Hotspot').each(function() {
|
||||
let buttonEl = $(this)[0]
|
||||
annotationsObj[buttonEl.childNodes[0].innerText] = {
|
||||
"data-position": buttonEl.getAttribute('data-position'),
|
||||
"data-normal": buttonEl.getAttribute('data-normal'),
|
||||
"data-orbit": buttonEl.getAttribute('data-orbit'),
|
||||
"data-target": buttonEl.getAttribute('data-target')
|
||||
}
|
||||
})
|
||||
if (Object.keys(annotationsObj).length === 0) return false
|
||||
const [currentText, metadata] = extractMetadata()
|
||||
metadata.annotations = annotationsObj
|
||||
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(metadata, null, 2)}\n$2`)
|
||||
$('#wpTextbox1').val(newText)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert text in the preview text editor to js object
|
||||
*
|
||||
* @return object containing metadata information
|
||||
*/
|
||||
extractMetadata = function() {
|
||||
const editText = $('#wpTextbox1').val()
|
||||
const extractMetadata = editText.match(/<mvconfig>([\S\s]*?)<\/mvconfig>/)
|
||||
let metadata = (extractMetadata.length >= 2) ? JSON.parse(extractMetadata[1]) : {viewerConfig: {}, annotations: {}, annotationSets: {}}
|
||||
if (metadata.viewerConfig === undefined) {
|
||||
metadata.viewerConfig = {
|
||||
default: {
|
||||
"camera-controls": true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (metadata.annotations === undefined) {
|
||||
metadata.annotations = {}
|
||||
}
|
||||
if (metadata.annotationSets === undefined) {
|
||||
metadata.annotationSets = {}
|
||||
}
|
||||
return [editText, metadata]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets listener and attributes on model-viewer to
|
||||
* allow for click registering of a new hotspot
|
||||
*/
|
||||
readyAddHotspot = function() {
|
||||
disableViewer('AddingHotspot', clickAddHotspot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener callback to retrieve the info
|
||||
* about the model surface point selected by the
|
||||
* mouse and add that information to the editor
|
||||
* text input
|
||||
*
|
||||
* @param {PointerEvent} e
|
||||
*/
|
||||
clickAddHotspot = function(e) {
|
||||
let hsPosition = null
|
||||
let targetModel = enableViewer()
|
||||
if (targetModel) {
|
||||
hsPosition = targetModel.positionAndNormalFromPoint(e.clientX, e.clientY)
|
||||
}
|
||||
if (hsPosition) {
|
||||
let currentText = $('#wpTextbox1').val()
|
||||
let [_, metadata] = extractMetadata(currentText)
|
||||
let hsOutput = {}
|
||||
hsOutput['data-position'] = hsPosition.position.toString().replaceAll(/(\d{5})(\d*?m)/g,"$1m")
|
||||
hsOutput['data-normal'] = hsPosition.normal.toString().replaceAll(/(\d{5})(\d*?m)/g,"$1m")
|
||||
hsOutput['data-orbit'] = orb2degree(targetModel.getCameraOrbit().toString(),[2,2,5])
|
||||
let targetObj = targetModel.getCameraTarget()
|
||||
hsOutput['data-target'] = `${targetObj.x.toFixed(5)}m ${targetObj.y.toFixed(5)}m ${targetObj.z.toFixed(5)}m`
|
||||
metadata.annotations['Hotspot ' + (Object.keys(metadata.annotations).length + 1)] = hsOutput
|
||||
let newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(metadata, null, 2)}\n$2`)
|
||||
$('#wpTextbox1').val(newText)
|
||||
}
|
||||
readMetadata()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set flag and attributes on model-viewer to
|
||||
* delete the next hotspot that is clicked
|
||||
*/
|
||||
readyDelHotspot = function() {
|
||||
deleteHotspot = true
|
||||
disableViewer('DeletingHotspot', cancelDeleteHotspot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset deleting flag and return normal
|
||||
* function and style to model viewer
|
||||
*/
|
||||
cancelDeleteHotspot = function() {
|
||||
deleteHotspot = null
|
||||
enableViewer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener callback to toggle the visibility
|
||||
* of a hotspot's annotation when the hotspot is
|
||||
@@ -189,19 +18,9 @@ cancelDeleteHotspot = function() {
|
||||
*/
|
||||
onAnnotation = function(e) {
|
||||
e.stopPropagation()
|
||||
if (deleteHotspot) {
|
||||
deleteHotspot = null
|
||||
enableViewer()
|
||||
const anName = e.target.childNodes[0].innerText
|
||||
let purgeAnnotation = new RegExp('(?<="annotationSets"[\\S\\s]*?)(^.*?' + anName + '.*\n)','gm')
|
||||
e.target.remove()
|
||||
const editText = $('#wpTextbox1').val()
|
||||
const newText = editText.replace(purgeAnnotation,'')
|
||||
const finalText = newText.replace(/(,)(\n\s+])/gm,'$2')
|
||||
$('#wpTextbox1').val(finalText)
|
||||
writeMetadata()
|
||||
readMetadata()
|
||||
return
|
||||
if (typeof isDeleting != 'undefined' && isDeleting()) {
|
||||
clickDeleteHotspot(e)
|
||||
return
|
||||
}
|
||||
let targetAnnotation = Number.parseInt(e.target.slot.split('-')[1])
|
||||
let targetModel = e.target.closest('model-viewer')
|
||||
@@ -326,130 +145,6 @@ toggleAnnotations = function(mView) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to drag a hotspot
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
grabAnnotation = function(e) {
|
||||
if (e.ctrlKey) {
|
||||
grabHotspot = {x: e.x, y: e.y}
|
||||
const contEl = $('.glmv-container')[0]
|
||||
contEl.addEventListener('mousemove', moveAnnotation)
|
||||
const mvEl = $('model-viewer')[0]
|
||||
} else {
|
||||
grabHotspot = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag currently clicked hotspot
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
moveAnnotation = function(e) {
|
||||
if (grabHotspot) {
|
||||
grabHotspot.move = true
|
||||
e.target.style['transform'] = `translate(${e.x - grabHotspot.x}px, ${e.y - grabHotspot.y}px) scale(1.1,1.1)`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End dragging a hotspot and update information
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
releaseAnnotation = function(e) {
|
||||
if (grabHotspot && grabHotspot.move) {
|
||||
e.target.style['transform']=''
|
||||
const contEl = $('.glmv-container')[0]
|
||||
contEl.removeEventListener('mousemove', moveAnnotation)
|
||||
const mvEl = $('model-viewer')[0]
|
||||
let newPosition = mvEl.positionAndNormalFromPoint(e.clientX, e.clientY)
|
||||
const newPos = newPosition.position.toString().replaceAll(/(\d{5})(\d*?m)/g,"$1m")
|
||||
const newNorm = newPosition.normal.toString().replaceAll(/(\d{5})(\d*?m)/g,"$1m")
|
||||
mvEl.updateHotspot({
|
||||
name: e.target.slot,
|
||||
position: newPos,
|
||||
normal: newNorm
|
||||
})
|
||||
const newOrb = orb2degree(mvEl.getCameraOrbit().toString(),[2,2,5])
|
||||
e.target.setAttribute('data-orbit', newOrb)
|
||||
let targetObj = mvEl.getCameraTarget()
|
||||
const newTarg = `${targetObj.x.toFixed(5)}m ${targetObj.y.toFixed(5)}m ${targetObj.z.toFixed(5)}m`
|
||||
e.target.setAttribute('data-target', newTarg)
|
||||
let currentText = $('#wpTextbox1').val()
|
||||
let [_, metadata] = extractMetadata(currentText)
|
||||
metadata.annotations[e.target.childNodes[0].innerText] = {
|
||||
"data-position": newPos,
|
||||
"data-normal": newNorm,
|
||||
"data-orbit": newOrb,
|
||||
"data-target": newTarg
|
||||
}
|
||||
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(metadata, null, 2)}\n$2`)
|
||||
$('#wpTextbox1').val(newText)
|
||||
}
|
||||
grabHotspot = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable general interaction with model
|
||||
* viewer for specific additional function
|
||||
*
|
||||
* @param {string} fnClass class to add to model-viewer
|
||||
* @param {callback} viewCall callback function to add to model-viewer
|
||||
* @return {Element} model-viewer element
|
||||
*/
|
||||
disableViewer = function(fnClass, viewCall) {
|
||||
const previewMv = $('model-viewer')
|
||||
if (viewCall) previewMv.one('click', viewCall)
|
||||
if (fnClass) previewMv.addClass(fnClass)
|
||||
previewMv[0].disableTap = true
|
||||
previewMv[0].toggleAttribute('camera-controls', false)
|
||||
return previewMv[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable general interaction with model
|
||||
* viewer
|
||||
*
|
||||
* @return {Element} model-viewer element
|
||||
*/
|
||||
enableViewer = function() {
|
||||
const previewMv = $('model-viewer')
|
||||
previewMv.off('click', clickAddHotspot)
|
||||
previewMv.off('click', cancelDeleteHotspot)
|
||||
previewMv.removeClass('AddingHotspot DeletingHotspot')
|
||||
previewMv[0].disableTap = false
|
||||
previewMv[0].toggleAttribute('camera-controls', true)
|
||||
return previewMv[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the model viewer methods to get image
|
||||
* of current view and download
|
||||
*
|
||||
* @param {string} defName wiki page name to use as base file name
|
||||
*/
|
||||
downloadImage = function(defName) {
|
||||
const imgName = defName.split('.')[0]
|
||||
const mView = $('model-viewer')[0]
|
||||
|
||||
const dlA = document.createElement('a')
|
||||
dlA.setAttribute('download',imgName + '.png')
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener("load", () => {
|
||||
dlA.setAttribute('href',reader.result)
|
||||
dlA.click()
|
||||
},{once: true})
|
||||
|
||||
mView.toBlob(null, null, true)
|
||||
.then(imgBlob => {
|
||||
reader.readAsDataURL(imgBlob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to full screen changes on the gl container
|
||||
*
|
||||
@@ -461,146 +156,4 @@ toggleFullScreen = function(glCont) {
|
||||
} else {
|
||||
glCont.requestFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera control setting for the current view
|
||||
*
|
||||
* @param {string} view
|
||||
* @return {bool} new camera-controls setting
|
||||
*/
|
||||
toggleCameraControl = function(view) {
|
||||
let [currentText, mvconfig] = extractMetadata()
|
||||
const currentView = (mvconfig.viewerConfig[view]) ? view : 'default'
|
||||
const newControl = !mvconfig.viewerConfig[currentView]['camera-controls']
|
||||
if (newControl) {
|
||||
mvconfig.viewerConfig[currentView]['camera-controls'] = newControl
|
||||
} else {
|
||||
delete mvconfig.viewerConfig[currentView]['camera-controls']
|
||||
}
|
||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
||||
$('#wpTextbox1').val(textUpdate)
|
||||
return newControl
|
||||
}
|
||||
|
||||
selectViewConfig = function(view) {
|
||||
const mView = $('model-viewer')[0]
|
||||
let [_, mvconfig] = extractMetadata()
|
||||
const selectView = (mvconfig.viewerConfig[view]) ? view : 'default'
|
||||
const viewConfig = mvconfig.viewerConfig[selectView]
|
||||
const settings = [
|
||||
"camera-controls",
|
||||
"disable-pan",
|
||||
"disable-tap",
|
||||
"touch-action",
|
||||
"disable-zoom",
|
||||
"orbit-sensitivity",
|
||||
"zoom-sensitivity",
|
||||
"pan-sensitivity",
|
||||
"auto-rotate",
|
||||
"auto-rotate-delay",
|
||||
"rotation-per-second",
|
||||
"interaction-prompt-style",
|
||||
"interaction-prompt-threshold",
|
||||
"camera-orbit",
|
||||
"camera-target",
|
||||
"field-of-view",
|
||||
"max-camera-orbit",
|
||||
"min-camera-orbit",
|
||||
"max-field-of-view",
|
||||
"min-field-of-view",
|
||||
"poster",
|
||||
"ar",
|
||||
"ar-modes",
|
||||
"ar-scale",
|
||||
"ar-placement"
|
||||
]
|
||||
settings.forEach(s => {
|
||||
if (viewConfig[s]) {
|
||||
mView.setAttribute(s,viewConfig[s])
|
||||
} else {
|
||||
mView.removeAttribute(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new default camera orbit and send values to the preview
|
||||
* editor
|
||||
*/
|
||||
writeCameraOrbit = function() {
|
||||
const mView = $('model-viewer')[0]
|
||||
const newOrbit = orb2degree(mView.getCameraOrbit().toString(),[2,2,5])
|
||||
mView.setAttribute('camera-orbit', newOrbit)
|
||||
const targetObj = mView.getCameraTarget()
|
||||
const newTarget = `${targetObj.x.toFixed(5)}m ${targetObj.y.toFixed(5)}m ${targetObj.z.toFixed(5)}m`
|
||||
mView.setAttribute('camera-target', newTarget)
|
||||
const newField = mView.getFieldOfView().toFixed(5) + 'deg'
|
||||
mView.setAttribute('field-of-view',newField)
|
||||
let [currentText, mvconfig] = extractMetadata()
|
||||
mvconfig.viewerConfig.default['camera-orbit'] = newOrbit
|
||||
mvconfig.viewerConfig.default['camera-target'] = newTarget
|
||||
mvconfig.viewerConfig.default['field-of-view'] = newField
|
||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
||||
$('#wpTextbox1').val(textUpdate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new camera orbit limits and send values to the preview
|
||||
* editor
|
||||
*
|
||||
* @param {string} axis [yaw|pitch] orbit value to set
|
||||
* @param {string} limit [max|min] limit value to set
|
||||
*/
|
||||
writeCameraLimit = function(axis, limit) {
|
||||
const mView = $('model-viewer')[0]
|
||||
const newOrbit = orb2degree(mView.getCameraOrbit().toString(),[2,2,5])
|
||||
const newOrbitVals = newOrbit.split(' ')
|
||||
const valueIndex = (axis == 'yaw') ? 0 : 1
|
||||
let [currentText, mvconfig] = extractMetadata()
|
||||
const oldOrbit = mvconfig.viewerConfig.default[`${limit}-camera-orbit`]
|
||||
let oldOrbitVals = (oldOrbit) ? oldOrbit.split(' ') : Array(3).fill('auto')
|
||||
oldOrbitVals[valueIndex] = newOrbitVals[valueIndex]
|
||||
mvconfig.viewerConfig.default[`${limit}-camera-orbit`] = oldOrbitVals.join(' ')
|
||||
mView.setAttribute(`${limit}-camera-orbit`, oldOrbitVals.join(' '))
|
||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
||||
$('#wpTextbox1').val(textUpdate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all radian values returned by model-viewer to degrees
|
||||
*
|
||||
* @param {string} orbString string with any number of `(number)rad` sub strings
|
||||
* @param {number|array} fix Optional: number of decimal places to return in converted numbers
|
||||
* @return {string} string with all radian values and units converted to degrees
|
||||
*/
|
||||
orb2degree = function(orbString, fix = null) {
|
||||
let degArray = orbString.split(' ').map(s => {
|
||||
if (s.includes('rad')) {
|
||||
return (Number.parseFloat(s) / Math.PI * 180) + 'deg'
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
})
|
||||
if (fix && !['number', 'object'].includes(typeof fix)) {
|
||||
console.warn('orb2degree: fix parameter invalid type. Ignoring.')
|
||||
fix = null
|
||||
}
|
||||
if (fix) {
|
||||
degArray = degArray.map((v, idx) => {
|
||||
let fixReg = new RegExp('(\\d*.\\d{' + (((typeof fix) == 'object') ? fix[idx] : fix) + '})(\\d*)([a-z]*)')
|
||||
return v.replace(fixReg,'$1$3')
|
||||
})
|
||||
}
|
||||
return degArray.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the currently selected annotation set
|
||||
*
|
||||
* @param {string} newSet name of annotation set to select
|
||||
*/
|
||||
selectAnnotationSet = function(newSet) {
|
||||
currentSet = newSet
|
||||
readMetadata()
|
||||
}
|
||||
Reference in New Issue
Block a user