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:
2024-11-18 03:54:29 +00:00
parent d419f6fe7e
commit 0dd9aba2b6
5 changed files with 677 additions and 616 deletions

View File

@@ -72,7 +72,9 @@
"glmv-prev.css"
],
"packageFiles": [
"glmv-prev.js"
"glmv-prev.js",
"glmv-mvconfig.js",
"glmv-hs.js"
]
}
}

150
modules/glmv-hs.js Normal file
View File

@@ -0,0 +1,150 @@
let deleteHotspot = null
let grabHotspot = null
/**
* 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 [_, mvconfig] = extractMvconfig(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`
mvconfig.annotations['Hotspot ' + (Object.keys(mvconfig.annotations).length + 1)] = hsOutput
let newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(mvconfig, null, 2)}\n$2`)
$('#wpTextbox1').val(newText)
}
readMvconfig()
}
/**
* 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()
}
/**
* Delete the selected hotspot
*
* @param {element} hs hotspot element to delete
*/
clickDeleteHotspot = function (hs) {
deleteHotspot = null
enableViewer()
const anName = hs.target.childNodes[0].innerText
let purgeAnnotation = new RegExp('(?<="annotationSets"[\\S\\s]*?)(^.*?' + anName + '.*\n)','gm')
hs.target.remove()
const editText = $('#wpTextbox1').val()
const newText = editText.replace(purgeAnnotation,'')
const finalText = newText.replace(/(,)(\n\s+])/gm,'$2')
$('#wpTextbox1').val(finalText)
writeMvconfig()
readMvconfig()
}
/**
* Check status of delete function
*/
isDeleting = function() {
return deleteHotspot
}
/**
* 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 [_, mvconfig] = extractMvconfig(currentText)
mvconfig.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(mvconfig, null, 2)}\n$2`)
$('#wpTextbox1').val(newText)
}
grabHotspot = null
}

269
modules/glmv-mvconfig.js Normal file
View File

@@ -0,0 +1,269 @@
let currentSet = 'default'
/**
* Convert text in the preview text editor to js object
*
* @return [string, object] full mvconfig text string and mvconfig object
*/
extractMvconfig = function() {
const editText = $('#wpTextbox1').val()
const extractConfig = editText.match(/<mvconfig>([\S\s]*?)<\/mvconfig>/)
let mvconfig = (extractConfig.length >= 2) ? JSON.parse(extractConfig[1]) : {viewerConfig: {}, annotations: {}, annotationSets: {}}
if (mvconfig.viewerConfig === undefined) {
mvconfig.viewerConfig = {
default: {
"camera-controls": true
}
}
}
if (mvconfig.annotations === undefined) {
mvconfig.annotations = {}
}
if (mvconfig.annotationSets === undefined) {
mvconfig.annotationSets = {}
}
return [editText, mvconfig]
}
/**
* Reads the json string in the edit panel
* and updates hotspot elements
*
* @return {bool} true on successful read and update
*/
readMvconfig = function() {
let hotspotsObj = []
let mvconfig
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(mvconfig.annotations[hsLabel]).forEach((prop) => {
newHs.setAttribute(prop, mvconfig.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 {
[_, mvconfig] = extractMvconfig()
} catch (err) {
console.warn('Failed to read model config:' + err.message)
return false
}
if (currentSet != 'default' && mvconfig.annotationSets[currentSet]) {
mvconfig.annotationSets[currentSet].forEach(hs => {
createHotspot(hs, slotNum)
slotNum += 1
})
}
Object.keys(mvconfig.annotations).forEach(hs => {
if (currentSet != 'default' && mvconfig.annotationSets[currentSet] && mvconfig.annotationSets[currentSet].includes(hs)) {
return
}
let label = (currentSet != 'default' && mvconfig.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
*/
writeMvconfig = 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, mvconfig] = extractMvconfig()
mvconfig.annotations = annotationsObj
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(mvconfig, null, 2)}\n$2`)
$('#wpTextbox1').val(newText)
return true
}
/**
* 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(' ')
}
/**
* Set camera control setting for the current view
*
* @param {string} view
* @return {bool} new camera-controls setting
*/
toggleCameraControl = function(view) {
let [currentText, mvconfig] = extractMvconfig()
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] = extractMvconfig()
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] = extractMvconfig()
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] = extractMvconfig()
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)
}
/**
* Change the currently selected annotation set
*
* @param {string} newSet name of annotation set to select
*/
selectAnnotationSet = function(newSet) {
currentSet = newSet
readMvconfig()
}
/*
export default {
extractMvconfig,
readMvconfig,
writeMvconfig,
orb2degree,
toggleCameraControl,
selectViewConfig,
writeCameraOrbit,
writeCameraLimit,
selectAnnotationSet
}
*/

View File

@@ -1,195 +1,282 @@
let [_, origMetadata] = extractMetadata()
//Initialize globals
require('./glmv-mvconfig.js')
require('./glmv-hs.js')
//Annotation Edit Controls
const addHS = new OO.ui.ButtonWidget({
icon: 'mapPinAdd',
label: 'Add annotation',
invisibleLabel: true,
class: 'edit-button'
})
addHS.on('click', readyAddHotspot)
addHS.setDisabled(true)
/**
* Use the OOui js library to create wikis-style menu
* options on the preview edits page for interaction
* with the model
*/
buildPreviewMenu = function() {
let [_, origMetadata] = extractMvconfig()
const updateHS = new OO.ui.ButtonWidget({
icon: 'reload',
label: 'Update annotations',
invisibleLabel: true
})
updateHS.on('click', readMetadata)
updateHS.setDisabled(true)
//Annotation Edit Controls
const addHS = new OO.ui.ButtonWidget({
icon: 'mapPinAdd',
label: 'Add annotation',
invisibleLabel: true,
class: 'edit-button'
})
addHS.on('click', readyAddHotspot)
addHS.setDisabled(true)
const deleteHS = new OO.ui.ButtonWidget({
icon: 'cancel',
label: 'Delete annotation',
invisibleLabel: true
})
deleteHS.on('click', readyDelHotspot)
deleteHS.setDisabled(true)
const updateHS = new OO.ui.ButtonWidget({
icon: 'reload',
label: 'Update annotations',
invisibleLabel: true
})
updateHS.on('click', readMvconfig)
updateHS.setDisabled(true)
const setOptions = ['default', ...Object.keys(origMetadata.annotationSets)]
let setOptionItems = []
setOptions.forEach(opt => {
setOptionItems.push(new OO.ui.MenuOptionWidget({data: opt, label: opt}))
})
const deleteHS = new OO.ui.ButtonWidget({
icon: 'cancel',
label: 'Delete annotation',
invisibleLabel: true
})
deleteHS.on('click', readyDelHotspot)
deleteHS.setDisabled(true)
const setSelectHS = new OO.ui.ButtonMenuSelectWidget({
icon: 'mapTrail',
label: 'Select annotation set',
invisibleLabel: true,
menu: {
items: setOptionItems,
width: 'min-content'
},
$overlay: $('#bodyContent')
})
setSelectHS.getMenu().on( 'choose', selSet => {
selectAnnotationSet(selSet.data)
})
setSelectHS.setDisabled(true)
const setOptions = ['default', ...Object.keys(origMetadata.annotationSets)]
let setOptionItems = []
setOptions.forEach(opt => {
setOptionItems.push(new OO.ui.MenuOptionWidget({data: opt, label: opt}))
})
const hotspotButtons = new OO.ui.ButtonGroupWidget({
items: [ addHS, updateHS, deleteHS, setSelectHS ]
})
const setSelectHS = new OO.ui.ButtonMenuSelectWidget({
icon: 'mapTrail',
label: 'Select annotation set',
invisibleLabel: true,
menu: {
items: setOptionItems,
width: 'min-content'
},
$overlay: $('#bodyContent')
})
setSelectHS.getMenu().on( 'choose', selSet => {
selectAnnotationSet(selSet.data)
})
setSelectHS.setDisabled(true)
//View Edit Controls
const downloadViewerImage = new OO.ui.ButtonWidget({
icon: 'imageAdd',
label: 'Download current image',
invisibleLabel: true
})
downloadViewerImage.on('click', () => {
downloadImage(mw.config.values.wgTitle)
})
downloadViewerImage.setDisabled(true)
const hotspotButtons = new OO.ui.ButtonGroupWidget({
items: [ addHS, updateHS, deleteHS, setSelectHS ]
})
const setView = new OO.ui.ButtonWidget({
icon: 'camera',
label: 'Set Initial View',
invisibleLabel: true
})
setView.on('click', writeCameraOrbit)
setView.setDisabled(true)
//View Edit Controls
const downloadViewerImage = new OO.ui.ButtonWidget({
icon: 'imageAdd',
label: 'Download current image',
invisibleLabel: true
})
downloadViewerImage.on('click', () => {
downloadImage(mw.config.values.wgTitle)
})
downloadViewerImage.setDisabled(true)
const setControl = new OO.ui.ButtonWidget({
icon: 'hand',
label: 'Toggle camera control',
invisibleLabel: true
})
setControl.on('click', () => $('model-viewer')[0].toggleAttribute('camera-controls', toggleCameraControl()))
setControl.setDisabled(true)
const setView = new OO.ui.ButtonWidget({
icon: 'camera',
label: 'Set Initial View',
invisibleLabel: true
})
setView.on('click', writeCameraOrbit)
setView.setDisabled(true)
//View Limit Controls
const setMinYaw = new OO.ui.ButtonWidget({
label: 'Min'
})
setMinYaw.on('click', () => {
writeCameraLimit('yaw','min')
})
const setControl = new OO.ui.ButtonWidget({
icon: 'hand',
label: 'Toggle camera control',
invisibleLabel: true
})
setControl.on('click', () => $('model-viewer')[0].toggleAttribute('camera-controls', toggleCameraControl()))
setControl.setDisabled(true)
const setMaxYaw = new OO.ui.ButtonWidget({
label: 'Max'
})
setMaxYaw.on('click', () => {
writeCameraLimit('yaw','max')
})
//View Limit Controls
const setMinYaw = new OO.ui.ButtonWidget({
label: 'Min'
})
setMinYaw.on('click', () => {
writeCameraLimit('yaw','min')
})
const yawLimitButtons = new OO.ui.ButtonGroupWidget({
items: [ setMinYaw, setMaxYaw ]
})
const setMaxYaw = new OO.ui.ButtonWidget({
label: 'Max'
})
setMaxYaw.on('click', () => {
writeCameraLimit('yaw','max')
})
const labelYaw = new OO.ui.LabelWidget({
label: "Yaw:"
})
const yawLimitButtons = new OO.ui.ButtonGroupWidget({
items: [ setMinYaw, setMaxYaw ]
})
const yawButtons = new OO.ui.HorizontalLayout({
items: [
labelYaw,
yawLimitButtons
],
id: 'yaw-limits'
})
const labelYaw = new OO.ui.LabelWidget({
label: "Yaw:"
})
const setMinPitch = new OO.ui.ButtonWidget({
label: 'Min'
})
setMinPitch.on('click', () => {
writeCameraLimit('pitch','min')
})
const yawButtons = new OO.ui.HorizontalLayout({
items: [
labelYaw,
yawLimitButtons
],
id: 'yaw-limits'
})
const setMaxPitch = new OO.ui.ButtonWidget({
label: 'Max'
})
setMaxPitch.on('click', () => {
writeCameraLimit('pitch','max')
})
const setMinPitch = new OO.ui.ButtonWidget({
label: 'Min'
})
setMinPitch.on('click', () => {
writeCameraLimit('pitch','min')
})
const pitchLimitButtons = new OO.ui.ButtonGroupWidget({
items: [ setMinPitch, setMaxPitch ]
})
const setMaxPitch = new OO.ui.ButtonWidget({
label: 'Max'
})
setMaxPitch.on('click', () => {
writeCameraLimit('pitch','max')
})
const labelPitch = new OO.ui.LabelWidget({
label: "Pitch:"
})
const pitchLimitButtons = new OO.ui.ButtonGroupWidget({
items: [ setMinPitch, setMaxPitch ]
})
const pitchButtons = new OO.ui.HorizontalLayout({
items: [
labelPitch,
pitchLimitButtons
],
id: 'pitch-limits'
})
const labelPitch = new OO.ui.LabelWidget({
label: "Pitch:"
})
const setLims = new OO.ui.PopupButtonWidget({
label: 'Set View Limits',
invisibleLabel: true,
icon: 'tableMergeCells',
popup: {
$content: yawButtons.$element.add(pitchButtons.$element),
padded: true,
position: 'above'
},
$overlay: $('#bodyContent')
})
setLims.setDisabled(true)
const pitchButtons = new OO.ui.HorizontalLayout({
items: [
labelPitch,
pitchLimitButtons
],
id: 'pitch-limits'
})
const setViewConfig = [...Object.keys(origMetadata.viewerConfig)]
let setViewItems = []
setViewConfig.forEach(opt => {
setViewItems.push(new OO.ui.MenuOptionWidget({data: opt, label: opt}))
})
const setLims = new OO.ui.PopupButtonWidget({
label: 'Set View Limits',
invisibleLabel: true,
icon: 'tableMergeCells',
popup: {
$content: yawButtons.$element.add(pitchButtons.$element),
padded: true,
position: 'above'
},
$overlay: $('#bodyContent')
})
setLims.setDisabled(true)
const selectVC = new OO.ui.ButtonMenuSelectWidget({
icon: 'eye',
label: 'Select view configuration',
invisibleLabel: true,
menu: {
items: setViewItems,
width: 'min-content'
},
$overlay: $('#bodyContent')
})
selectVC.getMenu().on( 'choose', selSet => {
selectViewConfig(selSet.data)
})
selectVC.setDisabled(true)
const setViewConfig = [...Object.keys(origMetadata.viewerConfig)]
let setViewItems = []
setViewConfig.forEach(opt => {
setViewItems.push(new OO.ui.MenuOptionWidget({data: opt, label: opt}))
})
const cameraButtons = new OO.ui.ButtonGroupWidget({
items: [ downloadViewerImage, setControl, setView, setLims, selectVC ]
})
const selectVC = new OO.ui.ButtonMenuSelectWidget({
icon: 'eye',
label: 'Select view configuration',
invisibleLabel: true,
menu: {
items: setViewItems,
width: 'min-content'
},
$overlay: $('#bodyContent')
})
selectVC.getMenu().on( 'choose', selSet => {
selectViewConfig(selSet.data)
})
selectVC.setDisabled(true)
//Main Menu
const modelMenu = new OO.ui.HorizontalLayout({
items: [
hotspotButtons,
cameraButtons
],
id: 'edit-model-menu'
})
const cameraButtons = new OO.ui.ButtonGroupWidget({
items: [ downloadViewerImage, setControl, setView, setLims, selectVC ]
})
$('#wikiPreview').after(modelMenu.$element)
//Main Menu
const modelMenu = new OO.ui.HorizontalLayout({
items: [
hotspotButtons,
cameraButtons
],
id: 'edit-model-menu'
})
$('#wikiPreview').after(modelMenu.$element)
return [modelMenu, selectVC]
}
/**
* Enable all the preview menu widgets (called by
* model load event)
*/
enableMenu = function() {
modelMenu.items.forEach(group => {
group.items.forEach(el => el.setDisabled(false))
});
}
}
/**
* Add a selection option to a ButtonMenuSelectWidget
*
* @param {ButtonMenuSelectWidget} menuWidget
*/
addMenuOption = function(menuWidget) {
menuWidget.menu.addItems([new OO.ui.MenuOptionWidget({data: 'New option', label: 'New option'})])
}
/**
* 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)
})
}
//Initialize the menu and get required global objects
const [modelMenu, viewSelector] = buildPreviewMenu()

View File

@@ -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()
}