Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8294edc1d1 | |||
| 846106a1a8 | |||
| 717ef152f1 | |||
| 859183fe2e | |||
| 9953dff4a0 | |||
| 8ed4e1f679 | |||
| 7e353bee24 | |||
| 383818b6f8 | |||
| 88cd5e4727 | |||
| a2246015c9 | |||
| 840b5e34df | |||
| 9857e77a1a | |||
| ea51793e2f | |||
| 5561038c25 | |||
| 95647069eb | |||
| 4c11d44918 | |||
| 996a62f55a | |||
| 99b84272c6 |
46
README.md
46
README.md
@@ -1,3 +1,47 @@
|
|||||||
# GlModelViewer
|
# GlModelViewer
|
||||||
|
|
||||||
MediaWiki extension for using model-viewer to display and annotate 3D models
|
MediaWiki extension for using model-viewer to display and annotate 3D models
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
Model-viewer configurations and annotations can be added as TOML in the file's description.
|
||||||
|
|
||||||
|
### Viewer Configuration
|
||||||
|
|
||||||
|
Configuration of the model-viewer instance is contained in the `[viewerConfig]` object.
|
||||||
|
This object can contain any number of child objects each of which describes a different possible configuration.
|
||||||
|
Each configuration can have properties matching the any of the attributes of the model-viewer element except for `src`, `class`, `interpolation-delay`, and `interaction-prompt`.
|
||||||
|
If adding a poster image, the value of the `poster` property must be the name of an image in the Wiki's File namespace (without the namespace header, e.g., `"My poster.jpg"`).
|
||||||
|
|
||||||
|
### Annotations
|
||||||
|
|
||||||
|
Annotations can be configured using the `[annotations]` object.
|
||||||
|
This object can contain any number of child objects each of which describes a different possible annotation.
|
||||||
|
The label for each annotation is the name of the child object (`[annotations."Label here"]`). Each object has two required properties and three optional properties.
|
||||||
|
|
||||||
|
| Property | Required | Description | Example |
|
||||||
|
|----------|----------|-------------|---------|
|
||||||
|
| **data-position** | Yes | String containing the X, Y, and Z coordinates of the annotation. | "0.02297m -0.02544m 0.01780m" |
|
||||||
|
| **data-normal** | Yes | String containing the X, Y, and Z coordinates of the normal vector of the annotation. | "0.78966m -0.53116m 0.30709m" |
|
||||||
|
| **data-orbit** | No | String containing the orbit vector of the camera to view annotation. | "2.31rad 2.01rad 0.65m" |
|
||||||
|
| **data-target** | No | String containing the X, Y, and Z coordinates of the camera target to view annotation. | "-0.01029m 0.00071m 0.01138m" |
|
||||||
|
| **field-of-view** | No | String containing the angle of the field of view in degrees. | "35.25deg" |
|
||||||
|
|
||||||
|
### Annotation sets
|
||||||
|
|
||||||
|
Annotations sets are specific subsets of the listed annotations that can be shown on the model.
|
||||||
|
They are configured using the `[annotationSets]` object.
|
||||||
|
This object can contain any number properties each of which contains an array of strings matching the names of the annotation to be included.
|
||||||
|
The property name is used as the annotation set identifier.
|
||||||
|
By default, all annotations are rendered on a model.
|
||||||
|
|
||||||
|
## Linking
|
||||||
|
|
||||||
|
When adding the model file to a page, use the standard `[[File:Model name.glb]]` internal file link.
|
||||||
|
The link supports three optional properties.
|
||||||
|
|
||||||
|
| Property | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| **view** | Use one of the non-default configurations specified in the `[viewerConfig]` object. | default viewer configuration |
|
||||||
|
| **set** | Display only the annotations listed in the specified annotation set from the `[annotationSets]` object. | show all annotations |
|
||||||
|
| **transform** | Reorder the displayed annotations. Can be either *rand* or *alpha*. | annotations are numbered in the order they are listed in the `[annotations]` object |
|
||||||
5
composer.json
Normal file
5
composer.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"devium/toml": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"author": "Justin Georgi",
|
"author": "Justin Georgi",
|
||||||
"url": "https://gitea.azgeorgis.net/jgeorgi/mwModelViewer",
|
"url": "https://gitea.azgeorgis.net/jgeorgi/mwModelViewer",
|
||||||
"description": "This extension allows .glb and .gltf files to be added, displayed, and annotated in MediaWiki",
|
"description": "This extension allows .glb and .gltf files to be added, displayed, and annotated in MediaWiki",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"license-name": "MIT",
|
"license-name": "MIT",
|
||||||
"type": "media",
|
"type": "media",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
@@ -74,6 +74,7 @@
|
|||||||
],
|
],
|
||||||
"packageFiles": [
|
"packageFiles": [
|
||||||
"glmv-prev.js",
|
"glmv-prev.js",
|
||||||
|
"mini-st.js",
|
||||||
"glmv-mvconfig.js",
|
"glmv-mvconfig.js",
|
||||||
"glmv-hs.js"
|
"glmv-hs.js"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class GlModelHooks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the config json in a <pre> tag
|
* Render the config toml in a <pre> tag
|
||||||
*
|
*
|
||||||
* @param $input The text inside the custom tag
|
* @param $input The text inside the custom tag
|
||||||
* @param array $args Any attributes given in the tag
|
* @param array $args Any attributes given in the tag
|
||||||
@@ -170,15 +170,8 @@ class GlModelHooks {
|
|||||||
$uploadFormObj->mComment .= <<<CONF
|
$uploadFormObj->mComment .= <<<CONF
|
||||||
|
|
||||||
<mvconfig>
|
<mvconfig>
|
||||||
{
|
[viewerConfig.default]
|
||||||
"viewerConfig": {
|
camera-controls = true
|
||||||
"default": {
|
|
||||||
"camera-controls": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"annotations": {},
|
|
||||||
"annotationSets": {}
|
|
||||||
}
|
|
||||||
</mvconfig>
|
</mvconfig>
|
||||||
CONF;
|
CONF;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace MediaWiki\Extension\GlModelViewer;
|
namespace MediaWiki\Extension\GlModelViewer;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Devium\Toml\Toml;
|
||||||
use MediaWiki\MediaWikiServices;
|
use MediaWiki\MediaWikiServices;
|
||||||
use MediaTransformOutput;
|
use MediaTransformOutput;
|
||||||
use ConfigFactory;
|
use ConfigFactory;
|
||||||
@@ -49,7 +52,7 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
public function toHtml($options = []) {
|
public function toHtml($options = []) {
|
||||||
$descriptText = $this->file->getDescriptionText();
|
$descriptText = $this->file->getDescriptionText();
|
||||||
preg_match('/<pre mvconfig.*?>([\S\s]*?)<\/pre>/',$descriptText,$modelDescript);
|
preg_match('/<pre mvconfig.*?>([\S\s]*?)<\/pre>/',$descriptText,$modelDescript);
|
||||||
$metadata = json_decode($modelDescript[1], true);
|
$metadata = toml_decode($modelDescript[1], true);
|
||||||
|
|
||||||
if ($this->thumb) {
|
if ($this->thumb) {
|
||||||
$poster = $metadata['viewerConfig'][$this->view]['poster'] ?? false;
|
$poster = $metadata['viewerConfig'][$this->view]['poster'] ?? false;
|
||||||
@@ -180,10 +183,10 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
$attrModelView = array_merge(['src' => $srcUrl, 'class' => 'mv-model', 'interpolation-decay' => '100', 'interaction-prompt' => 'none'], $attrModelView);
|
$attrModelView = array_merge(['src' => $srcUrl, 'class' => 'mv-model', 'interpolation-decay' => '100', 'interaction-prompt' => 'none'], $attrModelView);
|
||||||
$attrModelView['style'] = 'width: 100%; height: 100%;';
|
$attrModelView['style'] = 'width: 100%; height: 100%;';
|
||||||
$attrModelView['onload'] = 'modelLoaded(event)';
|
$attrModelView['onload'] = 'modelLoaded(event)';
|
||||||
$hotspotHtml = (isset($hotspots)) ? implode($hotspots) : '';
|
$hotspotHtml = (!empty($hotspots)) ? implode($hotspots) : '';
|
||||||
|
|
||||||
$elModel = Html::rawElement('model-viewer', $attrModelView, $hotspotHtml);
|
$elModel = Html::rawElement('model-viewer', $attrModelView, $hotspotHtml);
|
||||||
$elMenu = self::buildViewMenu();
|
$elMenu = self::buildViewMenu(!empty($hotspots));
|
||||||
|
|
||||||
$elFileLink = '';
|
$elFileLink = '';
|
||||||
if (!isset($viewParams['preview']) && $context->getTitle() != $this->file->getTitle()) {
|
if (!isset($viewParams['preview']) && $context->getTitle() != $this->file->getTitle()) {
|
||||||
@@ -210,9 +213,10 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
/**
|
/**
|
||||||
* Build the button menu used for viewer actions
|
* Build the button menu used for viewer actions
|
||||||
*
|
*
|
||||||
|
* @param bool $annotsDrawn true to enable the annotation buttons
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private static function buildViewMenu() {
|
private static function buildViewMenu($annotsDrawn) {
|
||||||
$attrMenu = array(
|
$attrMenu = array(
|
||||||
'class' => 'glmv-menu awaiting-model',
|
'class' => 'glmv-menu awaiting-model',
|
||||||
'style' => 'display: none;'
|
'style' => 'display: none;'
|
||||||
@@ -227,13 +231,15 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
|
|
||||||
$gotoUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/goto_hs.svg';
|
$gotoUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/goto_hs.svg';
|
||||||
$attrMenuButtonPrev = array (
|
$attrMenuButtonPrev = array (
|
||||||
'class' => 'glmv-menu-button prev-hs disable-on-hide',
|
'class' => 'glmv-menu-button prev-hs disable-on-hide disable-on-none',
|
||||||
|
'disabled' => !$annotsDrawn,
|
||||||
'onclick' => 'prevAnnotation(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
'onclick' => 'prevAnnotation(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
||||||
'onmousedown' => 'event.stopPropagation()',
|
'onmousedown' => 'event.stopPropagation()',
|
||||||
'ontouchstart' => 'event.stopPropagation()'
|
'ontouchstart' => 'event.stopPropagation()'
|
||||||
);
|
);
|
||||||
$attrMenuButtonNext = array (
|
$attrMenuButtonNext = array (
|
||||||
'class' => 'glmv-menu-button next-hs disable-on-hide',
|
'class' => 'glmv-menu-button next-hs disable-on-hide disable-on-none',
|
||||||
|
'disabled' => !$annotsDrawn,
|
||||||
'onclick' => 'nextAnnotation(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
'onclick' => 'nextAnnotation(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
||||||
'onmousedown' => 'event.stopPropagation()',
|
'onmousedown' => 'event.stopPropagation()',
|
||||||
'ontouchstart' => 'event.stopPropagation()'
|
'ontouchstart' => 'event.stopPropagation()'
|
||||||
@@ -241,7 +247,8 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
|
|
||||||
$slideUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/hs_slideshow.svg';
|
$slideUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/hs_slideshow.svg';
|
||||||
$attrMenuButtonSlides = array (
|
$attrMenuButtonSlides = array (
|
||||||
'class' => 'glmv-menu-button disable-on-hide',
|
'class' => 'glmv-menu-button disable-on-hide disable-on-none',
|
||||||
|
'disabled' => !$annotsDrawn,
|
||||||
'onclick' => 'event.target.toggleAttribute("toggled"); slideshowAnnotations(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
'onclick' => 'event.target.toggleAttribute("toggled"); slideshowAnnotations(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
||||||
'onmousedown' => 'event.stopPropagation()',
|
'onmousedown' => 'event.stopPropagation()',
|
||||||
'ontouchstart' => 'event.stopPropagation()'
|
'ontouchstart' => 'event.stopPropagation()'
|
||||||
@@ -249,7 +256,8 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
|
|
||||||
$hideUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/hs_hide.svg';
|
$hideUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/hs_hide.svg';
|
||||||
$attrMenuButtonHide = array (
|
$attrMenuButtonHide = array (
|
||||||
'class' => 'glmv-menu-button',
|
'class' => 'glmv-menu-button disable-on-none',
|
||||||
|
'disabled' => !$annotsDrawn,
|
||||||
'onclick' => 'event.target.toggleAttribute("toggled"); toggleAnnotations(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
'onclick' => 'event.target.toggleAttribute("toggled"); toggleAnnotations(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
||||||
'onmousedown' => 'event.stopPropagation()',
|
'onmousedown' => 'event.stopPropagation()',
|
||||||
'ontouchstart' => 'event.stopPropagation()'
|
'ontouchstart' => 'event.stopPropagation()'
|
||||||
@@ -264,12 +272,21 @@ class GlModelTransformOutput extends MediaTransformOutput {
|
|||||||
'ontouchstart' => 'event.stopPropagation()'
|
'ontouchstart' => 'event.stopPropagation()'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$resetUrl = $mainConfig->get( 'ExtensionAssetsPath' ) . '/GlModelViewer/resources/reset.svg';
|
||||||
|
$attrMenuButtonReset = array (
|
||||||
|
'class' => 'glmv-menu-button',
|
||||||
|
'onclick' => 'resetView(event.target.closest(".glmv-container").querySelector("model-viewer"))',
|
||||||
|
'onmousedown' => 'event.stopPropagation()',
|
||||||
|
'ontouchstart' => 'event.stopPropagation()'
|
||||||
|
);
|
||||||
|
|
||||||
$menuButtons = array(
|
$menuButtons = array(
|
||||||
Html::rawElement('div', $attrMenuButtonPrev, '<img class="awaiting-model" src="' . $gotoUrl . '"></image>'),
|
Html::rawElement('div', $attrMenuButtonPrev, '<img class="awaiting-model" src="' . $gotoUrl . '"></image>'),
|
||||||
Html::rawElement('div', $attrMenuButtonSlides, '<img class="awaiting-model" src="' . $slideUrl . '"></image>'),
|
Html::rawElement('div', $attrMenuButtonSlides, '<img class="awaiting-model" src="' . $slideUrl . '"></image>'),
|
||||||
Html::rawElement('div', $attrMenuButtonNext, '<img class="awaiting-model" src="' . $gotoUrl . '"></image>'),
|
Html::rawElement('div', $attrMenuButtonNext, '<img class="awaiting-model" src="' . $gotoUrl . '"></image>'),
|
||||||
Html::rawElement('div', $attrMenuButtonHide, '<img class="awaiting-model" src="' . $hideUrl . '"></image>'),
|
Html::rawElement('div', $attrMenuButtonHide, '<img class="awaiting-model" src="' . $hideUrl . '"></image>'),
|
||||||
Html::rawElement('div', $attrMenuButtonScreen, '<img class="awaiting-model full-hide" src="' . $screenUpUrl . '"></image><img class="awaiting-model full-show" src="' . $screenDownUrl . '"></image>')
|
Html::rawElement('div', $attrMenuButtonScreen, '<img class="awaiting-model full-hide" src="' . $screenUpUrl . '"></image><img class="awaiting-model full-show" src="' . $screenDownUrl . '"></image>'),
|
||||||
|
Html::rawElement('div', $attrMenuButtonReset, '<img class="awaiting-model" src="' . $resetUrl . '"></image>')
|
||||||
);
|
);
|
||||||
|
|
||||||
return Html::rawElement('div', $attrMenu, $menuImg . implode($menuButtons));
|
return Html::rawElement('div', $attrMenu, $menuImg . implode($menuButtons));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const TOML = require('./mini-st.js')
|
||||||
|
|
||||||
let deleteHotspot = null
|
let deleteHotspot = null
|
||||||
let grabHotspot = null
|
let grabHotspot = null
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ clickAddHotspot = function(e) {
|
|||||||
hsOutput['data-target'] = `${targetObj.x.toFixed(5)}m ${targetObj.y.toFixed(5)}m ${targetObj.z.toFixed(5)}m`
|
hsOutput['data-target'] = `${targetObj.x.toFixed(5)}m ${targetObj.y.toFixed(5)}m ${targetObj.z.toFixed(5)}m`
|
||||||
hsOutput['field-of-view'] = Number.parseFloat(targetModel.getFieldOfView()).toFixed(5) + 'deg'
|
hsOutput['field-of-view'] = Number.parseFloat(targetModel.getFieldOfView()).toFixed(5) + 'deg'
|
||||||
mvconfig.annotations['Hotspot ' + (Object.keys(mvconfig.annotations).length + 1)] = hsOutput
|
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`)
|
let newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${TOML.stringify(mvconfig, null, 2)}\n$2`)
|
||||||
$('#wpTextbox1').val(newText)
|
$('#wpTextbox1').val(newText)
|
||||||
}
|
}
|
||||||
readMvconfig()
|
readMvconfig()
|
||||||
@@ -67,13 +69,15 @@ clickDeleteHotspot = function (hs) {
|
|||||||
deleteHotspot = null
|
deleteHotspot = null
|
||||||
enableViewer()
|
enableViewer()
|
||||||
const anName = hs.target.childNodes[0].innerText
|
const anName = hs.target.childNodes[0].innerText
|
||||||
let purgeAnnotation = new RegExp('(?<="annotationSets"[\\S\\s]*?)(^.*?' + anName + '.*\n)','gm')
|
|
||||||
hs.target.remove()
|
hs.target.remove()
|
||||||
const editText = $('#wpTextbox1').val()
|
let currentText = $('#wpTextbox1').val()
|
||||||
const newText = editText.replace(purgeAnnotation,'')
|
let [_, mvconfig] = extractMvconfig(currentText)
|
||||||
const finalText = newText.replace(/(,)(\n\s+])/gm,'$2')
|
delete mvconfig.annotations[anName]
|
||||||
$('#wpTextbox1').val(finalText)
|
for (anSet in mvconfig.annotationSets) {
|
||||||
writeMvconfig()
|
mvconfig.annotationSets[anSet]=mvconfig.annotationSets[anSet].filter( x => x !== anName )
|
||||||
|
}
|
||||||
|
let newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${TOML.stringify(mvconfig, null, 2)}\n$2`)
|
||||||
|
$('#wpTextbox1').val(newText)
|
||||||
readMvconfig()
|
readMvconfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +95,7 @@ isDeleting = function() {
|
|||||||
*/
|
*/
|
||||||
grabAnnotation = function(e) {
|
grabAnnotation = function(e) {
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
grabHotspot = {x: e.x, y: e.y}
|
grabHotspot = {x: e.x, y: e.y, target: e.target}
|
||||||
const contEl = $('.glmv-container')[0]
|
const contEl = $('.glmv-container')[0]
|
||||||
contEl.addEventListener('mousemove', moveAnnotation)
|
contEl.addEventListener('mousemove', moveAnnotation)
|
||||||
const mvEl = $('model-viewer')[0]
|
const mvEl = $('model-viewer')[0]
|
||||||
@@ -108,7 +112,7 @@ grabAnnotation = function(e) {
|
|||||||
moveAnnotation = function(e) {
|
moveAnnotation = function(e) {
|
||||||
if (grabHotspot) {
|
if (grabHotspot) {
|
||||||
grabHotspot.move = true
|
grabHotspot.move = true
|
||||||
e.target.style['transform'] = `translate(${e.x - grabHotspot.x}px, ${e.y - grabHotspot.y}px) scale(1.1,1.1)`
|
grabHotspot.target.style['transform'] = `translate(${e.x - grabHotspot.x}px, ${e.y - grabHotspot.y}px) scale(1.1,1.1)`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +151,7 @@ releaseAnnotation = function(e) {
|
|||||||
"data-target": newTarg,
|
"data-target": newTarg,
|
||||||
"field-of-view": newFov
|
"field-of-view": newFov
|
||||||
}
|
}
|
||||||
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(mvconfig, null, 2)}\n$2`)
|
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${TOML.stringify(mvconfig, null, 2)}\n$2`)
|
||||||
$('#wpTextbox1').val(newText)
|
$('#wpTextbox1').val(newText)
|
||||||
}
|
}
|
||||||
grabHotspot = null
|
grabHotspot = null
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const TOML = require('./mini-st.js')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert text in the preview text editor to js object
|
* Convert text in the preview text editor to js object
|
||||||
*
|
*
|
||||||
@@ -6,7 +8,7 @@
|
|||||||
extractMvconfig = function() {
|
extractMvconfig = function() {
|
||||||
const editText = $('#wpTextbox1').val()
|
const editText = $('#wpTextbox1').val()
|
||||||
const extractConfig = editText.match(/<mvconfig>([\S\s]*?)<\/mvconfig>/)
|
const extractConfig = editText.match(/<mvconfig>([\S\s]*?)<\/mvconfig>/)
|
||||||
let mvconfig = (extractConfig.length >= 2) ? JSON.parse(extractConfig[1]) : {viewerConfig: {}, annotations: {}, annotationSets: {}}
|
let mvconfig = (extractConfig.length >= 2) ? TOML.parse(extractConfig[1]) : {viewerConfig: {}, annotations: {}, annotationSets: {}}
|
||||||
if (mvconfig.viewerConfig === undefined) {
|
if (mvconfig.viewerConfig === undefined) {
|
||||||
mvconfig.viewerConfig = {
|
mvconfig.viewerConfig = {
|
||||||
default: {
|
default: {
|
||||||
@@ -17,6 +19,12 @@ extractMvconfig = function() {
|
|||||||
if (mvconfig.annotations === undefined) {
|
if (mvconfig.annotations === undefined) {
|
||||||
mvconfig.annotations = {}
|
mvconfig.annotations = {}
|
||||||
}
|
}
|
||||||
|
const mView = $('model-viewer')[0]
|
||||||
|
const hsButtons = [...mView.parentElement.querySelectorAll('.disable-on-none')]
|
||||||
|
hsButtons.forEach( mb => {
|
||||||
|
mb.toggleAttribute('disabled',$.isEmptyObject(mvconfig.annotations))
|
||||||
|
})
|
||||||
|
|
||||||
if (mvconfig.annotationSets === undefined) {
|
if (mvconfig.annotationSets === undefined) {
|
||||||
mvconfig.annotationSets = {}
|
mvconfig.annotationSets = {}
|
||||||
}
|
}
|
||||||
@@ -24,7 +32,7 @@ extractMvconfig = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the json string in the edit panel
|
* Reads the TOML string in the edit panel
|
||||||
* and updates hotspot elements and menu settings
|
* and updates hotspot elements and menu settings
|
||||||
*
|
*
|
||||||
* @return {bool|object} arrays of view and set names on successful read and update false on failure
|
* @return {bool|object} arrays of view and set names on successful read and update false on failure
|
||||||
@@ -34,7 +42,7 @@ readMvconfig = function() {
|
|||||||
let mvconfig
|
let mvconfig
|
||||||
let slotNum = 1
|
let slotNum = 1
|
||||||
|
|
||||||
createHotspot = function(hsLabel, hsSlot, hsTag) {
|
createHotspot = function(hsLabel, hsSlot, hsTag, hsSkip) {
|
||||||
let newHs = document.createElement('button')
|
let newHs = document.createElement('button')
|
||||||
newHs.classList.add('Hotspot')
|
newHs.classList.add('Hotspot')
|
||||||
newHs.setAttribute('slot',`hotspot-${hsSlot}`)
|
newHs.setAttribute('slot',`hotspot-${hsSlot}`)
|
||||||
@@ -42,6 +50,7 @@ readMvconfig = function() {
|
|||||||
newHs.setAttribute('onclick', 'onAnnotation(event)')
|
newHs.setAttribute('onclick', 'onAnnotation(event)')
|
||||||
newHs.setAttribute('onmousedown', 'grabAnnotation(event)')
|
newHs.setAttribute('onmousedown', 'grabAnnotation(event)')
|
||||||
newHs.setAttribute('onmouseup', 'releaseAnnotation(event)')
|
newHs.setAttribute('onmouseup', 'releaseAnnotation(event)')
|
||||||
|
newHs.toggleAttribute('seq-skip', !!hsSkip)
|
||||||
Object.keys(mvconfig.annotations[hsLabel]).forEach((prop) => {
|
Object.keys(mvconfig.annotations[hsLabel]).forEach((prop) => {
|
||||||
newHs.setAttribute(prop, mvconfig.annotations[hsLabel][prop])
|
newHs.setAttribute(prop, mvconfig.annotations[hsLabel][prop])
|
||||||
})
|
})
|
||||||
@@ -73,7 +82,7 @@ readMvconfig = function() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let label = (currentSet != 'default' && mvconfig.annotationSets[currentSet]) ? '-' : null
|
let label = (currentSet != 'default' && mvconfig.annotationSets[currentSet]) ? '-' : null
|
||||||
createHotspot(hs, slotNum, label)
|
createHotspot(hs, slotNum, label, true)
|
||||||
slotNum += 1
|
slotNum += 1
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -90,8 +99,8 @@ readMvconfig = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the current hotspots into json object
|
* Parses the current hotspots into TOML object
|
||||||
* and writes the json string to the edit panel
|
* and writes the TOML string to the edit panel
|
||||||
*
|
*
|
||||||
* @return {bool} true on successful write to edit panel
|
* @return {bool} true on successful write to edit panel
|
||||||
*/
|
*/
|
||||||
@@ -109,7 +118,7 @@ writeMvconfig = function () {
|
|||||||
if (Object.keys(annotationsObj).length === 0) return false
|
if (Object.keys(annotationsObj).length === 0) return false
|
||||||
const [currentText, mvconfig] = extractMvconfig()
|
const [currentText, mvconfig] = extractMvconfig()
|
||||||
mvconfig.annotations = annotationsObj
|
mvconfig.annotations = annotationsObj
|
||||||
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${JSON.stringify(mvconfig, null, 2)}\n$2`)
|
const newText = currentText.replace(/(.*?<mvconfig>)[\S\s]*?(<\/mvconfig>.*)/,`$1\n${TOML.stringify(mvconfig, null, 2)}\n$2`)
|
||||||
$('#wpTextbox1').val(newText)
|
$('#wpTextbox1').val(newText)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -135,8 +144,9 @@ orb2degree = function(orbString, fix = null) {
|
|||||||
}
|
}
|
||||||
if (fix) {
|
if (fix) {
|
||||||
degArray = degArray.map((v, idx) => {
|
degArray = degArray.map((v, idx) => {
|
||||||
let fixReg = new RegExp('(\\d*.\\d{' + (((typeof fix) == 'object') ? fix[idx] : fix) + '})(\\d*)([a-z]*)')
|
//let fixReg = new RegExp('(-?\\d*\\.\\d{' + (((typeof fix) == 'object') ? fix[idx] : fix) + '})(\\d*)([a-z]*)')
|
||||||
return v.replace(fixReg,'$1$3')
|
//return v.replace(fixReg,'$1$3')
|
||||||
|
return Number.parseFloat(v).toFixed((typeof fix == 'object') ? fix[idx] : fix) + v.substring(v.search(/[a-z]/))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return degArray.join(' ')
|
return degArray.join(' ')
|
||||||
@@ -157,7 +167,7 @@ toggleCameraControl = function(view) {
|
|||||||
} else {
|
} else {
|
||||||
delete mvconfig.viewerConfig[currentView]['camera-controls']
|
delete mvconfig.viewerConfig[currentView]['camera-controls']
|
||||||
}
|
}
|
||||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${TOML.stringify(mvconfig, null, 2)}\n`)
|
||||||
$('#wpTextbox1').val(textUpdate)
|
$('#wpTextbox1').val(textUpdate)
|
||||||
return newControl
|
return newControl
|
||||||
}
|
}
|
||||||
@@ -171,7 +181,7 @@ addAnnotationSet = function(newSet) {
|
|||||||
const mView = $('model-viewer')[0]
|
const mView = $('model-viewer')[0]
|
||||||
let [currentText, mvconfig] = extractMvconfig()
|
let [currentText, mvconfig] = extractMvconfig()
|
||||||
mvconfig.annotationSets[newSet] = []
|
mvconfig.annotationSets[newSet] = []
|
||||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${TOML.stringify(mvconfig, null, 2)}\n`)
|
||||||
$('#wpTextbox1').val(textUpdate)
|
$('#wpTextbox1').val(textUpdate)
|
||||||
selectAnnotationSet(newSet)
|
selectAnnotationSet(newSet)
|
||||||
}
|
}
|
||||||
@@ -185,7 +195,7 @@ addViewConfig = function(newView) {
|
|||||||
const mView = $('model-viewer')[0]
|
const mView = $('model-viewer')[0]
|
||||||
let [currentText, mvconfig] = extractMvconfig()
|
let [currentText, mvconfig] = extractMvconfig()
|
||||||
mvconfig.viewerConfig[newView] = { "camera-controls": true }
|
mvconfig.viewerConfig[newView] = { "camera-controls": true }
|
||||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${TOML.stringify(mvconfig, null, 2)}\n`)
|
||||||
$('#wpTextbox1').val(textUpdate)
|
$('#wpTextbox1').val(textUpdate)
|
||||||
selectViewConfig(newView)
|
selectViewConfig(newView)
|
||||||
}
|
}
|
||||||
@@ -235,6 +245,7 @@ selectViewConfig = function(view) {
|
|||||||
mView.removeAttribute(s)
|
mView.removeAttribute(s)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mView.setAttribute('current-view',selectView)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -243,6 +254,7 @@ selectViewConfig = function(view) {
|
|||||||
*/
|
*/
|
||||||
writeCameraOrbit = function() {
|
writeCameraOrbit = function() {
|
||||||
const mView = $('model-viewer')[0]
|
const mView = $('model-viewer')[0]
|
||||||
|
const currentView = mView.getAttribute('current-view') ? mView.getAttribute('current-view') : 'default'
|
||||||
const newOrbit = orb2degree(mView.getCameraOrbit().toString(),[2,2,5])
|
const newOrbit = orb2degree(mView.getCameraOrbit().toString(),[2,2,5])
|
||||||
mView.setAttribute('camera-orbit', newOrbit)
|
mView.setAttribute('camera-orbit', newOrbit)
|
||||||
const targetObj = mView.getCameraTarget()
|
const targetObj = mView.getCameraTarget()
|
||||||
@@ -251,10 +263,10 @@ writeCameraOrbit = function() {
|
|||||||
const newField = mView.getFieldOfView().toFixed(5) + 'deg'
|
const newField = mView.getFieldOfView().toFixed(5) + 'deg'
|
||||||
mView.setAttribute('field-of-view',newField)
|
mView.setAttribute('field-of-view',newField)
|
||||||
let [currentText, mvconfig] = extractMvconfig()
|
let [currentText, mvconfig] = extractMvconfig()
|
||||||
mvconfig.viewerConfig.default['camera-orbit'] = newOrbit
|
mvconfig.viewerConfig[currentView]['camera-orbit'] = newOrbit
|
||||||
mvconfig.viewerConfig.default['camera-target'] = newTarget
|
mvconfig.viewerConfig[currentView]['camera-target'] = newTarget
|
||||||
mvconfig.viewerConfig.default['field-of-view'] = newField
|
mvconfig.viewerConfig[currentView]['field-of-view'] = newField
|
||||||
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${JSON.stringify(mvconfig, null, 2)}\n`)
|
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${TOML.stringify(mvconfig, null, 2)}\n`)
|
||||||
$('#wpTextbox1').val(textUpdate)
|
$('#wpTextbox1').val(textUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +288,6 @@ writeCameraLimit = function(axis, limit) {
|
|||||||
oldOrbitVals[valueIndex] = newOrbitVals[valueIndex]
|
oldOrbitVals[valueIndex] = newOrbitVals[valueIndex]
|
||||||
mvconfig.viewerConfig.default[`${limit}-camera-orbit`] = oldOrbitVals.join(' ')
|
mvconfig.viewerConfig.default[`${limit}-camera-orbit`] = oldOrbitVals.join(' ')
|
||||||
mView.setAttribute(`${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`)
|
const textUpdate = currentText.replace(/(?<=<mvconfig>)([\S\s]*?)(?=<\/mvconfig>)/gm,`\n${TOML.stringify(mvconfig, null, 2)}\n`)
|
||||||
$('#wpTextbox1').val(textUpdate)
|
$('#wpTextbox1').val(textUpdate)
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
width: 196px;
|
width: 232px;
|
||||||
|
|
||||||
& .glmv-menu-image {
|
& .glmv-menu-image {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
width: 294px;
|
width: 342px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ selectAnnotation = function(mView, annotId) {
|
|||||||
*/
|
*/
|
||||||
nextAnnotation = function(mView) {
|
nextAnnotation = function(mView) {
|
||||||
let incrAnnotation = 0
|
let incrAnnotation = 0
|
||||||
const numSpots = [...mView.querySelectorAll('Button')].length
|
const numSpots = [...mView.querySelectorAll('Button:not([seq-skip])')].length
|
||||||
const currentAnnotation = mView.querySelectorAll('Button:has(.HotspotAnnotation:not(.HiddenAnnotation))')[0]
|
const currentAnnotation = mView.querySelectorAll('Button:has(.HotspotAnnotation:not(.HiddenAnnotation))')[0]
|
||||||
if (!currentAnnotation) {
|
if (!currentAnnotation) {
|
||||||
incrAnnotation = 1
|
incrAnnotation = 1
|
||||||
@@ -95,7 +95,7 @@ nextAnnotation = function(mView) {
|
|||||||
*/
|
*/
|
||||||
prevAnnotation = function(mView) {
|
prevAnnotation = function(mView) {
|
||||||
let decrAnnotation = 0
|
let decrAnnotation = 0
|
||||||
const numSpots = [...mView.querySelectorAll('Button')].length
|
const numSpots = [...mView.querySelectorAll('Button:not([seq-skip])')].length
|
||||||
const currentAnnotation = mView.querySelectorAll('Button:has(.HotspotAnnotation:not(.HiddenAnnotation))')[0]
|
const currentAnnotation = mView.querySelectorAll('Button:has(.HotspotAnnotation:not(.HiddenAnnotation))')[0]
|
||||||
if (!currentAnnotation) {
|
if (!currentAnnotation) {
|
||||||
decrAnnotation = numSpots
|
decrAnnotation = numSpots
|
||||||
@@ -158,4 +158,18 @@ toggleFullScreen = function(glCont) {
|
|||||||
} else {
|
} else {
|
||||||
glCont.requestFullscreen()
|
glCont.requestFullscreen()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset view to initial position
|
||||||
|
*
|
||||||
|
* @param {ModelViewer} mView
|
||||||
|
*/
|
||||||
|
resetView = function(mView) {
|
||||||
|
const resetOrb = mView.getAttribute('camera-orbit') || 'auto auto auto'
|
||||||
|
const resetTarg = mView.getAttribute('camera-target') || 'auto auto auto'
|
||||||
|
const resetFov = mView.getAttribute('field-of-view') || 'auto'
|
||||||
|
mView.cameraOrbit = resetOrb
|
||||||
|
mView.cameraTarget = resetTarg
|
||||||
|
mView.fieldOfView = resetFov
|
||||||
}
|
}
|
||||||
1150
modules/mini-st.js
Normal file
1150
modules/mini-st.js
Normal file
File diff suppressed because it is too large
Load Diff
1
resources/reset.svg
Normal file
1
resources/reset.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M320-280q-33 0-56.5-23.5T240-360v-240q0-33 23.5-56.5T320-680h40l40-40h160l40 40h40q33 0 56.5 23.5T720-600v240q0 33-23.5 56.5T640-280H320Zm0-80h320v-240H320v240Zm160-40q33 0 56.5-23.5T560-480q0-33-23.5-56.5T480-560q-33 0-56.5 23.5T400-480q0 33 23.5 56.5T480-400ZM342-940q34-11 68.5-15.5T480-960q94 0 177.5 33.5t148 93Q870-774 911-693.5T960-520h-80q-7-72-38-134.5t-79.5-110Q714-812 651-842t-135-36l62 62-56 56-180-180ZM618-20Q584-9 549.5-4.5T480 0q-94 0-177.5-33.5t-148-93Q90-186 49-266.5T0-440h80q8 72 38.5 134.5t79 110Q246-148 309-118t135 36l-62-62 56-56L618-20ZM480-480Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 696 B |
Reference in New Issue
Block a user