`;
}
function createAnnotationHTML(hotspot) {
return `
`;
}
function switchModel(modelConfig, modelViewer) {
// Store initial camera position before switching (only first time)
if (!modelViewer.hasAttribute('data-initial-orbit')) {
const initialOrbit = modelViewer.getAttribute('camera-orbit') || '45deg 71deg auto';
modelViewer.setAttribute('data-initial-orbit', initialOrbit);
const initialTarget = modelViewer.getAttribute('camera-target') || 'auto auto auto';
modelViewer.setAttribute('data-initial-target', initialTarget);
const initialFov = modelViewer.getAttribute('field-of-view') || '35';
modelViewer.setAttribute('data-initial-fov', initialFov);
}
// Store current camera state BEFORE switching models
const currentOrbit = modelViewer.cameraOrbit;
const currentTarget = modelViewer.cameraTarget;
const currentFov = modelViewer.fieldOfView;
// Update model source
modelViewer.setAttribute('src', modelConfig.url);
modelViewer.setAttribute('data-base_model_url', modelConfig.url);
// Remove existing hotspots
const existingHotspots = modelViewer.querySelectorAll('[slot^="hotspot"]');
existingHotspots.forEach(hotspot => hotspot.remove());
// Remove existing annotation descriptions
const existingDescriptions = modelViewer.querySelector('.vsd-viewer-descriptions');
if (existingDescriptions) {
existingDescriptions.innerHTML = '';
}
// Add new hotspots and annotations
if (modelConfig.hotspots && modelConfig.hotspots.length > 0) {
modelConfig.hotspots.forEach(hotspot => {
// Create and insert hotspot element
const hotspotDiv = document.createElement('div');
hotspotDiv.innerHTML = createHotspotHTML(hotspot);
const hotspotElement = hotspotDiv.firstElementChild;
modelViewer.appendChild(hotspotElement);
// Add annotation description
if (existingDescriptions) {
const annotationDiv = document.createElement('div');
annotationDiv.innerHTML = createAnnotationHTML(hotspot);
existingDescriptions.appendChild(annotationDiv.firstElementChild);
}
});
// Wait for model to load, then restore camera position
modelViewer.addEventListener('load', () => {
// Restore the camera position from before the switch
modelViewer.cameraOrbit = currentOrbit;
modelViewer.cameraTarget = currentTarget;
modelViewer.fieldOfView = currentFov;
// Re-attach handlers
triggerVisodyInit(modelViewer);
}, { once: true });
} else {
// No hotspots, but still restore camera
modelViewer.addEventListener('load', () => {
modelViewer.cameraOrbit = currentOrbit;
modelViewer.cameraTarget = currentTarget;
modelViewer.fieldOfView = currentFov;
}, { once: true });
}
}
function triggerVisodyInit(modelViewer) {
const event = new Event('visody:reinit', { bubbles: true });
modelViewer.dispatchEvent(event);
if (window.Visody && typeof window.Visody.initHotspots === 'function') {
window.Visody.initHotspots(modelViewer);
}
if (window.jQuery) {
jQuery(modelViewer).trigger('visody:init');
}
attachHotspotHandlers(modelViewer);
}
function attachHotspotHandlers(modelViewer) {
const hotspotButtons = modelViewer.querySelectorAll('.vsd-viewer-hotspot-button');
const descriptions = modelViewer.querySelectorAll('.vsd-viewer-annotation-description');
// Store initial camera position for reset
const initialOrbit = modelViewer.getAttribute('data-initial-orbit') || modelViewer.getAttribute('camera-orbit') || '45deg 71deg auto';
const initialTarget = modelViewer.getAttribute('data-initial-target') || modelViewer.getAttribute('camera-target') || 'auto auto auto';
const initialFov = modelViewer.getAttribute('data-initial-fov') || modelViewer.fieldOfView || '35deg';
hotspotButtons.forEach(button => {
// Remove any existing listeners to avoid duplicates
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
newButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const ref = this.getAttribute('data-ref');
const orbit = this.getAttribute('data-orbit');
const target = this.getAttribute('data-target');
const fieldOfView = this.getAttribute('data-view');
const animationName = this.getAttribute('data-animation');
const animationAutoplay = this.getAttribute('data-animation-autoplay');
const isActive = this.classList.contains('active');
if (isActive) {
// Stop any playing animation
if (modelViewer.paused === false) {
modelViewer.pause();
}
// Reset camera to initial position
modelViewer.cameraOrbit = initialOrbit;
modelViewer.cameraTarget = initialTarget;
modelViewer.fieldOfView = initialFov;
// Hide annotation
const targetDescription = Array.from(descriptions).find(
desc => desc.getAttribute('data-ref') === ref
);
if (targetDescription) {
targetDescription.classList.remove('active');
}
// Deactivate button
this.classList.remove('active');
} else {
// ACTIVATE: Show annotation, move camera, play animation
// First, stop any playing animation from previous hotspot
if (modelViewer.paused === false) {
modelViewer.pause();
}
// Reset camera to initial position before moving to new hotspot
modelViewer.cameraOrbit = initialOrbit;
modelViewer.cameraTarget = initialTarget;
modelViewer.fieldOfView = initialFov;
// Small delay to let camera reset
setTimeout(() => {
// Now move to new position
if (orbit) {
modelViewer.cameraOrbit = orbit;
}
if (target) {
modelViewer.cameraTarget = target;
}
if (fieldOfView) {
modelViewer.fieldOfView = fieldOfView + 'deg';
}
}, 50);
// Hide all other annotations and deactivate all other buttons
descriptions.forEach(desc => desc.classList.remove('active'));
hotspotButtons.forEach(btn => btn.classList.remove('active'));
// Handle animation
if (animationName) {
// Wait for model to load if needed
if (modelViewer.loaded) {
playAnimation(modelViewer, animationName, animationAutoplay);
} else {
modelViewer.addEventListener('load', () => {
playAnimation(modelViewer, animationName, animationAutoplay);
}, { once: true });
}
}
// Show this annotation
const targetDescription = Array.from(descriptions).find(
desc => desc.getAttribute('data-ref') === ref
);
if (targetDescription) {
targetDescription.classList.add('active');
}
// Activate button
this.classList.add('active');
}
});
});
}
function playAnimation(modelViewer, animationName, autoplay) {
// Check if model has animations
if (!modelViewer.availableAnimations || modelViewer.availableAnimations.length === 0) {
console.warn('No animations available on this model');
return;
}
// Check if the requested animation exists
if (modelViewer.availableAnimations.includes(animationName)) {
modelViewer.animationName = animationName;
if (autoplay === '1') {
modelViewer.play({ repetitions: 1 });
}
} else {
console.warn(`Animation "${animationName}" not found in model`);
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
const modelViewer = document.querySelector('model-viewer');
if (!modelViewer) {
console.error('Model viewer not found!');
return;
}
// Button container
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('button-container');
// Buttons for each model
models.forEach((model, index) => {
const button = document.createElement('button');
button.textContent = model.name;
button.classList.add('model-switch-btn');
// Set first button as active
if (index === 0) {
button.classList.add('model-btn-activated');
}
button.addEventListener('click', function() {
switchModel(model, modelViewer);
// Update button styles - remove active class from all, add to clicked
document.querySelectorAll('.model-switch-btn').forEach(btn => {
btn.classList.remove('model-btn-activated');
});
this.classList.add('model-btn-activated');
});
buttonContainer.appendChild(button);
});
// Insert buttons above the viewer
const viewerWrapper = document.querySelector('.vsd-model-viewer-wrapper'); if (viewerWrapper) { viewerWrapper.parentNode.insertBefore(buttonContainer, viewerWrapper); } else { modelViewer.parentNode.insertBefore(buttonContainer, modelViewer); }
// Wait for model to load before attaching handlers
if (modelViewer.loaded) {
attachHotspotHandlers(modelViewer);
} else {
modelViewer.addEventListener('load', () => {
attachHotspotHandlers(modelViewer);
}, { once: true });
}
});




