changeset 62:b798387b085c

added 3DHOP viewer version 4.3
author Sebastien Jodogne <s.jodogne@gmail.com>
date Sat, 15 Jun 2024 16:08:52 +0200
parents 5dc3f3dcc092
children 1fd6d0f8fdc9
files .hgignore .reuse/dep5 CMakeLists.txt NEWS Resources/CMake/3DHOP.cmake Resources/CMake/3dhop-4.3.patch Resources/CreateJavaScriptLibraries.sh Resources/EmbedStaticAssets.py Resources/Nexus.txt Resources/Nexus/threejs.html Sources/OrthancExplorer.js Sources/Plugin.cpp WebApplications/o3dv.html WebApplications/three.html WebApplications/three.js
diffstat 15 files changed, 300 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed May 22 15:35:07 2024 +0200
+++ b/.hgignore	Sat Jun 15 16:08:52 2024 +0200
@@ -6,7 +6,8 @@
 ThirdPartyDownloads/
 JavaScriptLibraries/Online3DViewer-*.tar.gz
 JavaScriptLibraries/three.js-*.tar.gz
-JavaScriptLibraries/dist
+JavaScriptLibraries/dist-o3dv/
+JavaScriptLibraries/dist-three/
 i/
 s/
 *.orig
--- a/.reuse/dep5	Wed May 22 15:35:07 2024 +0200
+++ b/.reuse/dep5	Sat Jun 15 16:08:52 2024 +0200
@@ -3,7 +3,7 @@
 Upstream-Contact: Sebastien Jodogne <s.jodogne@gmail.com>
 Source: https://orthanc.uclouvain.be/
 
-Files: NEWS README WebApplications/o3dv.html WebApplications/three.html Resources/CreateThreeDist.txt Resources/Nexus.txt
+Files: NEWS README WebApplications/o3dv.html WebApplications/three.html Resources/CreateThreeDist.txt Resources/Nexus.txt Resources/CMake/3dhop-4.3.patch
 Copyright: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
 License: GPL-3.0-or-later
 
--- a/CMakeLists.txt	Wed May 22 15:35:07 2024 +0200
+++ b/CMakeLists.txt	Sat Jun 15 16:08:52 2024 +0200
@@ -61,6 +61,9 @@
 # New in release 1.1
 set(ENABLE_NEXUS ON CACHE BOOL "Include support for Nexus 3D models")
 
+# New in release 1.2
+set(ENABLE_3DHOP ON CACHE BOOL "Include support for 3DHOP viewer")
+
 # Advanced parameters to fine-tune linking against system libraries
 SET(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
 
@@ -171,17 +174,28 @@
 ## Create the autogenerated files
 #####################################################################
 
+if (ENABLE_3DHOP AND NOT
+    ENABLE_NEXUS)
+  message(FATAL_ERROR "3DHOP necessitates Nexus support")
+endif()
+
 set(EMBEDDED_RESOURCES
+  ORTHANC_EXPLORER   ${CMAKE_SOURCE_DIR}/Sources/OrthancExplorer.js
+
+  # These resources correspond to the "Basic viewer built using Three.js"
   THREE_HTML         ${CMAKE_SOURCE_DIR}/WebApplications/three.html
   THREE_JS           ${CMAKE_SOURCE_DIR}/WebApplications/three.js
+
+  # These resources correspond to Online3DViewer
   O3DV_HTML          ${CMAKE_SOURCE_DIR}/WebApplications/o3dv.html
   O3DV_JS            ${CMAKE_SOURCE_DIR}/WebApplications/o3dv.js
-  ORTHANC_EXPLORER   ${CMAKE_SOURCE_DIR}/Sources/OrthancExplorer.js
   )
 
-set(STATIC_ASSETS
-  ${CMAKE_SOURCE_DIR}/JavaScriptLibraries/dist
-  )
+set(STATIC_ASSETS_PREFIXES "o3dv")
+set(STATIC_ASSETS_CONTENT  "${CMAKE_SOURCE_DIR}/JavaScriptLibraries/dist-o3dv")
+
+list(APPEND STATIC_ASSETS_PREFIXES "basic-viewer")
+list(APPEND STATIC_ASSETS_CONTENT  "${CMAKE_SOURCE_DIR}/JavaScriptLibraries/dist-three")
 
 if (ENABLE_NEXUS)
   set(NEXUS_ASSETS_DIR ${AUTOGENERATED_DIR}/nexus)
@@ -192,6 +206,11 @@
     "https://orthanc.uclouvain.be/downloads/third-party-downloads/STL/three-84.js.gz"
     "${NEXUS_ASSETS_DIR}/three-84.js")
 
+  list(APPEND STATIC_ASSETS_PREFIXES "nexus")
+  list(APPEND STATIC_ASSETS_CONTENT
+    ${NEXUS_ASSETS_DIR}   # This adds "three-84.js" that is needed by the Nexus viewer
+    )
+
   list(APPEND EMBEDDED_RESOURCES
     NEXUS_HTML           ${CMAKE_SOURCE_DIR}/Resources/Nexus/threejs.html
     NEXUS_JS             ${CMAKE_SOURCE_DIR}/Resources/Nexus/js/nexus.js
@@ -200,13 +219,19 @@
     NEXUS_TRACKBALL_JS   ${CMAKE_SOURCE_DIR}/Resources/Nexus/js/TrackballControls.js
     )
 
-  list(APPEND STATIC_ASSETS
-    ${NEXUS_ASSETS_DIR}
-    )
+  add_definitions(-DORTHANC_ENABLE_NEXUS=1)
 
-  add_definitions(-DORTHANC_ENABLE_NEXUS=1)
+  if (ENABLE_3DHOP)
+    include(${CMAKE_SOURCE_DIR}/Resources/CMake/3DHOP.cmake)
+    add_definitions(-DORTHANC_ENABLE_3DHOP=1)
+  else()
+    add_definitions(-DORTHANC_ENABLE_3DHOP=0)
+  endif()
 else()
-  add_definitions(-DORTHANC_ENABLE_NEXUS=0)
+  add_definitions(
+    -DORTHANC_ENABLE_NEXUS=0
+    -DORTHANC_ENABLE_3DHOP=0
+    )
 endif()
 
 EmbedResources(${EMBEDDED_RESOURCES})
@@ -218,10 +243,11 @@
   ${PYTHON_EXECUTABLE}
   ${CMAKE_SOURCE_DIR}/Resources/EmbedStaticAssets.py
   ${AUTOGENERATED_DIR}/StaticAssets.cpp
-  ${STATIC_ASSETS}
+  ${STATIC_ASSETS_PREFIXES}
+  ${STATIC_ASSETS_CONTENT}
   DEPENDS
   ${CMAKE_SOURCE_DIR}/Resources/EmbedStaticAssets.py
-  ${STATIC_ASSETS}
+  ${STATIC_ASSETS_CONTENT}
   )
 
 list(APPEND AUTOGENERATED_SOURCES 
--- a/NEWS	Wed May 22 15:35:07 2024 +0200
+++ b/NEWS	Sat Jun 15 16:08:52 2024 +0200
@@ -1,6 +1,8 @@
 Pending changes in the mainline
 ===============================
 
+* Added 3DHOP viewer (version 4.3) for Nexus models
+
 
 Version 1.1 (2024-05-22)
 ========================
@@ -8,7 +10,7 @@
 => Minimum SDK version: 1.2.0 <=
 
 * Support for DICOM-ization of adaptive 3D models in the Nexus format:
-  https://vcg.isti.cnr.it/nexus/
+  https://vcg.isti.cnr.it/nexus/ (version 4.2 - Nexus 2018)
 
 
 Version 1.0 (2024-04-06)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/3DHOP.cmake	Sat Jun 15 16:08:52 2024 +0200
@@ -0,0 +1,47 @@
+# SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+
+# STL plugin for Orthanc
+# Copyright (C) 2023-2024 Sebastien Jodogne, UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+DownloadPackage(
+  "9e0dee1e12668d5667aa9be0ae5937e0"
+  "https://orthanc.uclouvain.be/downloads/third-party-downloads/STL/3DHOP_4.3.zip"
+  "${CMAKE_BINARY_DIR}/3DHOP_4.3")
+
+set(3DHOP_DIR  ${CMAKE_CURRENT_BINARY_DIR}/3dhop)
+file(MAKE_DIRECTORY ${3DHOP_DIR})
+
+file(COPY
+  ${CMAKE_BINARY_DIR}/3DHOP_4.3/minimal/3DHOP_all_tools.html
+  ${CMAKE_BINARY_DIR}/3DHOP_4.3/minimal/js
+  ${CMAKE_BINARY_DIR}/3DHOP_4.3/minimal/skins
+  ${CMAKE_BINARY_DIR}/3DHOP_4.3/minimal/stylesheet
+  DESTINATION
+  ${3DHOP_DIR}
+  )
+
+execute_process(
+  COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+  ${CMAKE_CURRENT_LIST_DIR}/3dhop-4.3.patch
+  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+  RESULT_VARIABLE Failure
+  )
+
+list(APPEND STATIC_ASSETS_PREFIXES "3dhop")
+list(APPEND STATIC_ASSETS_CONTENT  ${3DHOP_DIR})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/3dhop-4.3.patch	Sat Jun 15 16:08:52 2024 +0200
@@ -0,0 +1,38 @@
+diff -urEb 3dhop.orig/3DHOP_all_tools.html 3dhop/3DHOP_all_tools.html
+--- 3dhop.orig/3DHOP_all_tools.html	2024-06-15 15:43:28.329045772 +0200
++++ 3dhop/3DHOP_all_tools.html	2024-06-15 15:50:04.629862005 +0200
+@@ -106,6 +106,25 @@
+ </body>
+ 
+ <script type="text/javascript">
++// http://stackoverflow.com/a/21903119/881731
++function GetUrlParameter(sParam)
++{
++  var sPageURL = decodeURIComponent(window.location.search.substring(1));
++  var sURLVariables = sPageURL.split('&');
++
++  for (var i = 0; i < sURLVariables.length; i++) {
++    var sParameterName = sURLVariables[i].split('=');
++
++    if (sParameterName[0] === sParam) {
++      return sParameterName[1] === undefined ? '' : sParameterName[1];
++    }
++  }
++
++  return '';
++};
++
++var instanceId = GetUrlParameter('instance');
++
+ var presenter = null;
+ 
+ function setup3dhop() {
+@@ -113,7 +132,7 @@
+ 
+ 	presenter.setScene({
+ 		meshes: {
+-			"mesh_1" : { url: "models/gargo.nxz" }
++			"mesh_1" : { url: "../3dhop-instances/" + instanceId + ".nxz" }
+ 		},
+ 		modelInstances : {
+ 			"model_1" : { 
--- a/Resources/CreateJavaScriptLibraries.sh	Wed May 22 15:35:07 2024 +0200
+++ b/Resources/CreateJavaScriptLibraries.sh	Sat Jun 15 16:08:52 2024 +0200
@@ -22,8 +22,8 @@
 
 
 # This command-line script uses the "npm" tool to populate the
-# "JavaScriptLibraries/dist" folder. It uses Docker to this end, in
-# order to be usable on our CIS.
+# "JavaScriptLibraries/dist-o3dv" and "JavaScriptLibraries/dist-three"
+# folders. It uses Docker to this end, in order to be usable on our CIS.
 
 set -ex
 
@@ -40,12 +40,18 @@
 ROOT_DIR=`dirname $(readlink -f $0)`/..
 IMAGE=orthanc-stl-node
 
-if [ -e "${ROOT_DIR}/JavaScriptLibraries/dist/" ]; then
+if [ -e "${ROOT_DIR}/JavaScriptLibraries/dist-o3dv/" ]; then
     echo "Target folder is already existing, aborting"
     exit -1
 fi
 
-mkdir -p ${ROOT_DIR}/JavaScriptLibraries/dist/
+if [ -e "${ROOT_DIR}/JavaScriptLibraries/dist-three/" ]; then
+    echo "Target folder is already existing, aborting"
+    exit -1
+fi
+
+mkdir -p ${ROOT_DIR}/JavaScriptLibraries/dist-o3dv/
+mkdir -p ${ROOT_DIR}/JavaScriptLibraries/dist-three/
 
 ( cd ${ROOT_DIR}/Resources/CreateJavaScriptLibraries && \
       docker build --no-cache -t ${IMAGE} . )
@@ -70,7 +76,7 @@
        --user $(id -u):$(id -g) \
        -v ${ROOT_DIR}/Resources/CreateJavaScriptLibraries/build-o3dv.sh:/source/build-o3dv.sh:ro \
        -v ${ROOT_DIR}/JavaScriptLibraries/${O3DV}.tar.gz:/source/${O3DV}.tar.gz:ro \
-       -v ${ROOT_DIR}/JavaScriptLibraries/dist/:/target:rw \
+       -v ${ROOT_DIR}/JavaScriptLibraries/dist-o3dv/:/target:rw \
        ${IMAGE} \
        bash /source/build-o3dv.sh ${O3DV}
 
@@ -93,6 +99,6 @@
        --user $(id -u):$(id -g) \
        -v ${ROOT_DIR}/Resources/CreateJavaScriptLibraries/build-three.sh:/source/build-three.sh:ro \
        -v ${ROOT_DIR}/JavaScriptLibraries/${THREE}.tar.gz:/source/${THREE}.tar.gz:ro \
-       -v ${ROOT_DIR}/JavaScriptLibraries/dist/:/target:rw \
+       -v ${ROOT_DIR}/JavaScriptLibraries/dist-three/:/target:rw \
        ${IMAGE} \
        bash /source/build-three.sh ${THREE}
--- a/Resources/EmbedStaticAssets.py	Wed May 22 15:35:07 2024 +0200
+++ b/Resources/EmbedStaticAssets.py	Sat Jun 15 16:08:52 2024 +0200
@@ -27,14 +27,37 @@
 import sys
 
 if len(sys.argv) <= 2:
-    raise Exception('Usage: %s [target C++] [source folders]' % sys.argv[0])
+    raise Exception('Usage: %s [target C++] [folder prefixes] [source folders]' % sys.argv[0])
 
 SOURCES = sys.argv[2:]
 TARGET = sys.argv[1]
 
-for source in SOURCES:
-    if not os.path.isdir(source):
-        raise Exception('Nonexistent source folder: %s' % source)
+if len(SOURCES) % 2 != 0:
+    raise Exception('There must be an even number of sources')
+
+FILES = []
+
+for i in range(len(SOURCES) // 2):
+    prefix = SOURCES[i]
+    if '/' in prefix:
+        raise Exception('Prefix cannot contain a slash, but found: %s' % prefix)
+
+    folder = SOURCES[i + len(SOURCES) // 2]
+
+    if not os.path.isdir(folder):
+        raise Exception('Nonexistent source folder: %s' % folder)
+
+    for root, dirs, files in os.walk(folder):
+        files.sort()
+        dirs.sort()
+
+        for f in files:
+            FILES.append({
+                'path' : os.path.join(root, f),
+                'key' : prefix + '/' + os.path.relpath(os.path.join(root, f), folder),
+            })
+
+FILES = sorted(FILES, key = lambda x: x['key'])
 
 
 def EncodeFileAsCString(f, variable, content):
@@ -95,43 +118,35 @@
     index = {}
     count = 0
 
-    for source in SOURCES:
-        for root, dirs, files in os.walk(source):
-            files.sort()
-            dirs.sort()
+    for file in FILES:
+        variable = 'data_%06d' % count
+        count += 1
 
-            for f in files:
-                fullPath = os.path.join(root, f)
-                relativePath = os.path.relpath(os.path.join(root, f), source)
-                variable = 'data_%06d' % count
-
-                with open(fullPath, 'rb') as f:
-                    content = f.read()
+        with open(file['path'], 'rb') as f:
+            content = f.read()
 
-                if sys.version_info < (3, 0):
-                    # Python 2.7
-                    fileobj = io.BytesIO()
-                    gzip.GzipFile(fileobj=fileobj, mode='w', mtime=0).write(content)
-                    compressed = fileobj.getvalue()
-                else:
-                    # Python 3.x
-                    compressed = gzip.compress(content, mtime=0)
+        if sys.version_info < (3, 0):
+            # Python 2.7
+            fileobj = io.BytesIO()
+            gzip.GzipFile(fileobj=fileobj, mode='w', mtime=0).write(content)
+            compressed = fileobj.getvalue()
+        else:
+            # Python 3.x
+            compressed = gzip.compress(content, mtime=0)
 
-                EncodeFileAsCString(g, variable, compressed)
-                WriteChecksum(g, variable + '_md5', content)
+        EncodeFileAsCString(g, variable, compressed)
+        WriteChecksum(g, variable + '_md5', content)
 
-                index[relativePath] = variable
-
-                count += 1
+        file['variable'] = variable
     
     g.write('void ReadStaticAsset(std::string& target, const std::string& path)\n')
     g.write('{\n')
-    for (path, variable) in sorted(index.items()):
-        g.write('  if (path == "%s")\n' % path)
+    for file in FILES:
+        g.write('  if (path == "%s")\n' % file['key'])
         g.write('  {\n')
-        g.write('    Uncompress(target, %s, sizeof(%s) - 1, %s_md5);\n' % (variable, variable, variable))
+        g.write('    Uncompress(target, %s, sizeof(%s) - 1, %s_md5);\n' % (file['variable'], file['variable'], file['variable']))
         g.write('    return;\n')
         g.write('  }\n\n')
 
-    g.write('  throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Unknown Three.js resource: " + path);\n')
+    g.write('  throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Unknown static asset: " + path);\n')
     g.write('}\n')
--- a/Resources/Nexus.txt	Wed May 22 15:35:07 2024 +0200
+++ b/Resources/Nexus.txt	Sat Jun 15 16:08:52 2024 +0200
@@ -10,7 +10,7 @@
 
 is replaced by the line:
 
-<script src="../app/libs/three-84.js"></script>
+<script src="three-84.js"></script>
 
 
 WARNING: Releases 4.2.1, 4.2.2, and 4.3 do not seem to work anymore.
--- a/Resources/Nexus/threejs.html	Wed May 22 15:35:07 2024 +0200
+++ b/Resources/Nexus/threejs.html	Sat Jun 15 16:08:52 2024 +0200
@@ -5,7 +5,7 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 <style>body { margin: 0px; overflow: hidden; }</style>
-<script src="../app/libs/three-84.js"></script>
+<script src="three-84.js"></script>
 <script src="js/TrackballControls.js"></script>
 <script src="js/nexus.js"></script>
 <script src="js/nexus_three.js"></script>
--- a/Sources/OrthancExplorer.js	Wed May 22 15:35:07 2024 +0200
+++ b/Sources/OrthancExplorer.js	Sat Jun 15 16:08:52 2024 +0200
@@ -27,7 +27,7 @@
 const STL_PLUGIN_SOP_CLASS_UID_RAW = '1.2.840.10008.5.1.4.1.1.66';
 
 
-function AddStlViewer(target, name, callback) {
+function AddViewer(target, name, callback) {
   var li = $('<li>', {
     name: name,
   }).click(callback);
@@ -59,11 +59,11 @@
         .attr('data-divider-theme', 'd')
         .attr('data-role', 'listview');
 
-    AddStlViewer(viewers, 'Basic viewer built using Three.js', function() {
+    AddViewer(viewers, 'Basic viewer built using Three.js', function() {
       window.open('../stl/app/three.html?instance=' + instanceId);
     });
 
-    AddStlViewer(viewers, 'Online3DViewer', function() {
+    AddViewer(viewers, 'Online3DViewer', function() {
       window.open('../stl/app/o3dv.html?instance=' + instanceId);
     });
 
@@ -510,7 +510,32 @@
           b.insertAfter($('#' + parent));
 
           b.click(function() {
-            window.open('../stl/nexus/threejs.html?model=../../instances/' + instanceId + '/nexus');
+            if (${IS_3DHOP_ENABLED}) {
+              var viewers = $('<ul>')
+                  .attr('data-divider-theme', 'd')
+                  .attr('data-role', 'listview');
+
+              AddViewer(viewers, 'Basic Nexus viewer', function() {
+                window.open('../stl/nexus/threejs.html?model=../../instances/' + instanceId + '/nexus');
+              });
+
+              AddViewer(viewers, '3DHOP', function() {
+                window.open('../stl/3dhop/3DHOP_all_tools.html?instance=' + instanceId);
+              });
+
+              // Launch the dialog
+              $('#dialog').simpledialog2({
+                mode: 'blank',
+                animate: false,
+                headerText: 'Choose Nexus viewer',
+                headerClose: true,
+                forceInput: false,
+                width: '100%',
+                blankContent: viewers
+              });
+            } else {
+              window.open('../stl/nexus/threejs.html?model=../../instances/' + instanceId + '/nexus');
+            }
           });
         }
       }
--- a/Sources/Plugin.cpp	Wed May 22 15:35:07 2024 +0200
+++ b/Sources/Plugin.cpp	Sat Jun 15 16:08:52 2024 +0200
@@ -26,6 +26,10 @@
 #  error Macro ORTHANC_ENABLE_NEXUS must be defined
 #endif
 
+#if !defined(ORTHANC_ENABLE_3DHOP)
+#  error Macro ORTHANC_ENABLE_3DHOP must be defined
+#endif
+
 #include "StructureSetGeometry.h"
 #include "STLToolbox.h"
 #include "VTKToolbox.h"
@@ -177,9 +181,10 @@
 
   const std::string file = request->groups[0];
 
-  if (boost::starts_with(file, "libs/"))
+  if (boost::starts_with(file, "basic-viewer/") ||
+      boost::starts_with(file, "o3dv/"))
   {
-    cache_.Answer(output, file.substr(5));
+    cache_.Answer(output, file);
   }
   else
   {
@@ -767,43 +772,50 @@
 
   const std::string file = request->groups[0];
 
-  Orthanc::EmbeddedResources::FileResourceId resourceId;
-  Orthanc::MimeType mimeType;
-
-  if (file == "threejs.html")
-  {
-    resourceId = Orthanc::EmbeddedResources::NEXUS_HTML;
-    mimeType = Orthanc::MimeType_Html;
-  }
-  else if (file == "js/meco.js")
+  if (file == "three-84.js")
   {
-    resourceId = Orthanc::EmbeddedResources::NEXUS_MECO_JS;
-    mimeType = Orthanc::MimeType_JavaScript;
-  }
-  else if (file == "js/nexus.js")
-  {
-    resourceId = Orthanc::EmbeddedResources::NEXUS_JS;
-    mimeType = Orthanc::MimeType_JavaScript;
-  }
-  else if (file == "js/nexus_three.js")
-  {
-    resourceId = Orthanc::EmbeddedResources::NEXUS_THREE_JS;
-    mimeType = Orthanc::MimeType_JavaScript;
-  }
-  else if (file == "js/TrackballControls.js")
-  {
-    resourceId = Orthanc::EmbeddedResources::NEXUS_TRACKBALL_JS;
-    mimeType = Orthanc::MimeType_JavaScript;
+    cache_.Answer(output, "nexus/three-84.js");
   }
   else
   {
-    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
-    return;
-  }
+    Orthanc::EmbeddedResources::FileResourceId resourceId;
+    Orthanc::MimeType mimeType;
 
-  std::string s;
-  Orthanc::EmbeddedResources::GetFileResource(s, resourceId);
-  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(mimeType));
+    if (file == "threejs.html")
+    {
+      resourceId = Orthanc::EmbeddedResources::NEXUS_HTML;
+      mimeType = Orthanc::MimeType_Html;
+    }
+    else if (file == "js/meco.js")
+    {
+      resourceId = Orthanc::EmbeddedResources::NEXUS_MECO_JS;
+      mimeType = Orthanc::MimeType_JavaScript;
+    }
+    else if (file == "js/nexus.js")
+    {
+      resourceId = Orthanc::EmbeddedResources::NEXUS_JS;
+      mimeType = Orthanc::MimeType_JavaScript;
+    }
+    else if (file == "js/nexus_three.js")
+    {
+      resourceId = Orthanc::EmbeddedResources::NEXUS_THREE_JS;
+      mimeType = Orthanc::MimeType_JavaScript;
+    }
+    else if (file == "js/TrackballControls.js")
+    {
+      resourceId = Orthanc::EmbeddedResources::NEXUS_TRACKBALL_JS;
+      mimeType = Orthanc::MimeType_JavaScript;
+    }
+    else
+    {
+      OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
+      return;
+    }
+
+    std::string s;
+    Orthanc::EmbeddedResources::GetFileResource(s, resourceId);
+    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(mimeType));
+  }
 }
 
 
@@ -1005,6 +1017,25 @@
 #endif
 
 
+#if ORTHANC_ENABLE_3DHOP == 1
+
+void Serve3DHOPAssets(OrthancPluginRestOutput* output,
+                      const char* url,
+                      const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET");
+    return;
+  }
+
+  const std::string file = request->groups[0];
+  cache_.Answer(output, "3dhop/" + file);
+}
+
+#endif
+
+
 extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
@@ -1068,6 +1099,17 @@
       OrthancPlugins::RegisterRestCallback<ExtractNexusModel>("/instances/([0-9a-f-]+)/nexus", true);
       OrthancPlugins::RegisterRestCallback<ServeNexusAssets>("/stl/nexus/(.*)", true);
 
+#if ORTHANC_ENABLE_3DHOP == 1
+      OrthancPlugins::RegisterRestCallback<Serve3DHOPAssets>("/stl/3dhop/(.*)", true);
+
+      /**
+       * The 3DHOP viewer only supports ".nxs", ".nxz", and ".ply" extensions,
+       * as can be seen in the "minimal/js/presenter.js" file. Furthermore, it
+       * requires the extension to be part of the URL.
+       **/
+      OrthancPlugins::RegisterRestCallback<ExtractNexusModel>("/stl/3dhop-instances/([0-9a-f-]+).nxz", true);
+#endif
+
       const bool hasCreateNexus_ = OrthancPlugins::CheckMinimalOrthancVersion(1, 9, 4);
 
       if (hasCreateNexus_)
@@ -1108,6 +1150,7 @@
       dictionary["HAS_CREATE_DICOM_STL"] = (hasCreateDicomStl ? "true" : "false");
       dictionary["SHOW_NIFTI_BUTTON"] = (configuration.GetBooleanValue("EnableNIfTI", false) ? "true" : "false");
       dictionary["IS_NEXUS_ENABLED"] = (enableNexus ? "true" : "false");
+      dictionary["IS_3DHOP_ENABLED"] = ((ORTHANC_ENABLE_3DHOP == 1) ? "true" : "false");
       FillOrthancExplorerCreatorVersionUid(dictionary);
 
       explorer = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
--- a/WebApplications/o3dv.html	Wed May 22 15:35:07 2024 +0200
+++ b/WebApplications/o3dv.html	Sat Jun 15 16:08:52 2024 +0200
@@ -30,7 +30,7 @@
   <body>
     <div class="online_3d_viewer" id="viewer"></div>
 
-    <script type="text/javascript" src="libs/o3dv.min.js"></script>
+    <script type="text/javascript" src="o3dv/o3dv.min.js"></script>
     <script type="text/javascript" src="o3dv.js"></script>
   </body>
 
--- a/WebApplications/three.html	Wed May 22 15:35:07 2024 +0200
+++ b/WebApplications/three.html	Sat Jun 15 16:08:52 2024 +0200
@@ -8,12 +8,12 @@
     </style>
   </head>
   <body>
-    <script async src="libs/es-module-shims.js"></script>
+    <script async src="basic-viewer/es-module-shims.js"></script>
 
     <script type="importmap">
       {
         "imports": {
-          "three": "./libs/three.module.min.js"
+          "three": "./basic-viewer/three.module.min.js"
         }
       }
     </script>
--- a/WebApplications/three.js	Wed May 22 15:35:07 2024 +0200
+++ b/WebApplications/three.js	Sat Jun 15 16:08:52 2024 +0200
@@ -24,8 +24,8 @@
 
 import * as THREE from 'three';
 
-import { OrbitControls } from './libs/OrbitControls.js';
-import { STLLoader } from './libs/STLLoader.js';
+import { OrbitControls } from './basic-viewer/OrbitControls.js';
+import { STLLoader } from './basic-viewer/STLLoader.js';
 
 
 // http://stackoverflow.com/a/21903119/881731