diff --git a/src/Mod/BIM/CMakeLists.txt b/src/Mod/BIM/CMakeLists.txt
index 890fb97f0b..338ab06294 100644
--- a/src/Mod/BIM/CMakeLists.txt
+++ b/src/Mod/BIM/CMakeLists.txt
@@ -173,6 +173,10 @@ SET(bimcommands_SRCS
bimcommands/__init__.py
)
+SET(BIM_templates
+ Resources/templates/webgl_export_template.html
+)
+
SET(nativeifc_SRCS
nativeifc/ifc_commands.py
nativeifc/ifc_diff.py
@@ -223,6 +227,8 @@ SET(bimtests_SRCS
bimtests/TestArchReference.py
bimtests/TestArchSchedule.py
bimtests/TestArchTruss.py
+ bimtests/TestWebGLExport.py
+ bimtests/TestWebGLExportGui.py
)
SOURCE_GROUP("" FILES ${Arch_SRCS})
@@ -246,6 +252,7 @@ ADD_CUSTOM_TARGET(BIM ALL
${bimtests_SRCS}
${nativeifc_SRCS}
${BIMGuiIcon_SVG}
+ ${BIM_templates}
)
ADD_CUSTOM_TARGET(ImporterPythonTestData ALL
@@ -266,6 +273,12 @@ fc_target_copy_resource(BIM
${Arch_presets}
)
+fc_target_copy_resource(BIM
+ ${CMAKE_SOURCE_DIR}/src/Mod/BIM
+ ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Mod/BIM
+ ${BIM_templates}
+)
+
fc_target_copy_resource(ImporterPythonTestData
${CMAKE_SOURCE_DIR}/src/Mod/BIM
${CMAKE_BINARY_DIR}/Mod/BIM
@@ -330,3 +343,9 @@ INSTALL(
"${CMAKE_INSTALL_DATADIR}/Mod/BIM/Resources/icons"
)
+INSTALL(
+ FILES
+ ${BIM_templates}
+ DESTINATION
+ "${CMAKE_INSTALL_DATADIR}/Mod/BIM/Resources/templates"
+ )
diff --git a/src/Mod/BIM/InitGui.py b/src/Mod/BIM/InitGui.py
index 2a61aaa3f8..96f028c7d9 100644
--- a/src/Mod/BIM/InitGui.py
+++ b/src/Mod/BIM/InitGui.py
@@ -707,6 +707,7 @@ FreeCADGui.addPreferencePage(":/ui/preferences-ifc.ui", t)
FreeCADGui.addPreferencePage(":/ui/preferences-ifc-export.ui", t)
FreeCADGui.addPreferencePage(":/ui/preferences-dae.ui", t)
FreeCADGui.addPreferencePage(":/ui/preferences-sh3d-import.ui", t)
+FreeCADGui.addPreferencePage(":/ui/preferences-webgl.ui", t)
# Add unit tests
FreeCAD.__unit_test__ += ["TestArchGui"]
diff --git a/src/Mod/BIM/Resources/Arch.qrc b/src/Mod/BIM/Resources/Arch.qrc
index a202f2480e..0265454ceb 100644
--- a/src/Mod/BIM/Resources/Arch.qrc
+++ b/src/Mod/BIM/Resources/Arch.qrc
@@ -271,6 +271,7 @@
ui/preferences-ifc-export.ui
ui/preferences-ifc.ui
ui/preferences-sh3d-import.ui
+ ui/preferences-webgl.ui
ui/preferencesNativeIFC.ui
diff --git a/src/Mod/BIM/Resources/templates/webgl_export_template.html b/src/Mod/BIM/Resources/templates/webgl_export_template.html
new file mode 100644
index 0000000000..c3498f9205
--- /dev/null
+++ b/src/Mod/BIM/Resources/templates/webgl_export_template.html
@@ -0,0 +1,652 @@
+
+
+
+ $pagetitle
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Mod/BIM/Resources/ui/preferences-webgl.ui b/src/Mod/BIM/Resources/ui/preferences-webgl.ui
new file mode 100644
index 0000000000..38c1b698f7
--- /dev/null
+++ b/src/Mod/BIM/Resources/ui/preferences-webgl.ui
@@ -0,0 +1,129 @@
+
+
+ Gui::Dialog::DlgSettingsArch
+
+
+
+ 0
+ 0
+ 555
+ 729
+
+
+
+ WebGL
+
+
+
+ 6
+
+
+ 9
+
+ -
+
+
+ Export Options
+
+
+
-
+
+
+ A custom WebGL HTML template is used for export. Otherwise, the default template will be used.
+
+The default template is located at:
+<FreeCAD installation directory>/Resources/Mod/BIM/templates/webgl_export_template.html
+
+
+ Use custom export template
+
+
+ false
+
+
+ useCustomWebGLExportTemplate
+
+
+ Mod/BIM
+
+
+
+ -
+
+
-
+
+
+ false
+
+
+ Path to template
+
+
+
+
+ -
+
+
+ false
+
+
+ The path to the custom WebGL HTML template
+
+
+ WebGLTemplateCustomPath
+
+
+ Mod/BIM
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+ Gui::FileChooser
+ QWidget
+
+
+
+ Gui::PrefFileChooser
+ Gui::FileChooser
+
+
+
+
+
+
+ checkBox_useCustomWebGLTemplate
+ toggled(bool)
+ label_4
+ setEnabled(bool)
+
+
+ checkBox_useCustomWebGLTemplate
+ toggled(bool)
+ gui::preffilechooser_2
+ setEnabled(bool)
+
+
+
diff --git a/src/Mod/BIM/TestArch.py b/src/Mod/BIM/TestArch.py
index b60b82c874..03288827c6 100644
--- a/src/Mod/BIM/TestArch.py
+++ b/src/Mod/BIM/TestArch.py
@@ -47,4 +47,4 @@ from bimtests.TestArchReference import TestArchReference
from bimtests.TestArchSchedule import TestArchSchedule
from bimtests.TestArchTruss import TestArchTruss
from bimtests.TestArchComponent import TestArchComponent
-
+from bimtests.TestWebGLExport import TestWebGLExport
diff --git a/src/Mod/BIM/TestArchGui.py b/src/Mod/BIM/TestArchGui.py
index a92658ec7c..c631c6bceb 100644
--- a/src/Mod/BIM/TestArchGui.py
+++ b/src/Mod/BIM/TestArchGui.py
@@ -42,6 +42,8 @@ from draftutils.messages import _msg
if App.GuiUp:
import FreeCADGui
+from bimtests.TestWebGLExportGui import TestWebGLExportGui
+
class ArchTest(unittest.TestCase):
def setUp(self):
diff --git a/src/Mod/BIM/bimtests/TestWebGLExport.py b/src/Mod/BIM/bimtests/TestWebGLExport.py
new file mode 100644
index 0000000000..b2b01730fb
--- /dev/null
+++ b/src/Mod/BIM/bimtests/TestWebGLExport.py
@@ -0,0 +1,140 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2025 baidakovil *
+# * *
+# * This file is part of FreeCAD. *
+# * *
+# * FreeCAD is free software: you can redistribute it and/or modify it *
+# * under the terms of the GNU Lesser General Public License as *
+# * published by the Free Software Foundation, either version 2.1 of the *
+# * License, or (at your option) any later version. *
+# * *
+# * FreeCAD 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 *
+# * Lesser General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Lesser General Public *
+# * License along with FreeCAD. If not, see *
+# * . *
+# * *
+# ***************************************************************************
+
+
+"""
+Unit tests for WebGL export functionality. Gui tests are in `TestWebGLExportGui.py`.
+"""
+
+import os
+import tempfile
+import unittest
+from unittest.mock import patch, mock_open
+
+from BIM.importers import importWebGL
+from .TestArchBase import TestArchBase
+
+
+class TestWebGLExport(TestArchBase):
+
+ def setUp(self):
+ """Using TestArchBase setUp to initialize the document for convenience,
+ but also create a temporary directory for tests."""
+ super().setUp()
+ self.test_dir = tempfile.mkdtemp()
+ self.test_template_content = """
+
+
+ $pagetitle
+ WebGL Content: $data
+
+ """
+
+ def tearDown(self):
+ import shutil
+
+ shutil.rmtree(self.test_dir, ignore_errors=True)
+ super().tearDown()
+
+ def test_actual_default_template_readable_and_valid(self):
+ """Test that the actual default template can be read and contains
+ required placeholders"""
+ operation = "Testing actual default template reading and validation"
+ self.printTestMessage(operation)
+ # Test with real configuration - no mocks
+ with patch(
+ "BIM.importers.importWebGL.params.get_param", return_value=False
+ ): # Disable custom template
+ result = importWebGL.getHTMLTemplate()
+
+ if result is not None: # Only test if template exists
+ self.assertIsInstance(result, str)
+
+ # Check for basic HTML structure
+ self.assertIn(" *
+# * *
+# * This file is part of FreeCAD. *
+# * *
+# * FreeCAD is free software: you can redistribute it and/or modify it *
+# * under the terms of the GNU Lesser General Public License as *
+# * published by the Free Software Foundation, either version 2.1 of the *
+# * License, or (at your option) any later version. *
+# * *
+# * FreeCAD 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 *
+# * Lesser General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Lesser General Public *
+# * License along with FreeCAD. If not, see *
+# * . *
+# * *
+# ***************************************************************************
+
+
+"""
+Unit tests for Gui WebGL export functionality. Tests both the template handling and
+the main export function. Non-gui tests are in `TestWebGLExport.py`.
+"""
+
+import os
+import tempfile
+import unittest
+from unittest.mock import patch, MagicMock, mock_open
+
+from BIM.importers import importWebGL
+from .TestArchBase import TestArchBase
+
+
+class TestWebGLExportGui(TestArchBase):
+
+ def setUp(self):
+ """Using TestArchBase setUp to initialize the document for convenience,
+ but also create a temporary directory for tests."""
+ super().setUp()
+ self.test_dir = tempfile.mkdtemp()
+ self.test_template_content = """
+
+
+ $pagetitle
+ WebGL Content: $data
+
+ """
+
+ def tearDown(self):
+ import shutil
+
+ shutil.rmtree(self.test_dir, ignore_errors=True)
+ super().tearDown()
+
+ @patch("BIM.importers.importWebGL.FreeCADGui")
+ def test_custom_template_not_found_gui_user_accepts_fallback(
+ self, mock_gui
+ ):
+ """Test GUI dialog when custom template not found -
+ user accepts fallback"""
+ operation = "Testing GUI custom template not found - user accepts fallback"
+ self.printTestMessage(operation)
+ from PySide import QtWidgets
+
+ mock_gui.getMainWindow.return_value = MagicMock()
+
+ with patch(
+ "BIM.importers.importWebGL.params.get_param"
+ ) as mock_params:
+ mock_params.side_effect = lambda param, path=None: {
+ "useCustomWebGLExportTemplate": True,
+ "WebGLTemplateCustomPath": "/nonexistent/template.html",
+ }.get(param, False)
+
+ with patch(
+ "PySide.QtWidgets.QMessageBox.question",
+ return_value=QtWidgets.QMessageBox.Yes,
+ ):
+ with patch("os.path.isfile", return_value=True):
+ with patch(
+ "builtins.open",
+ mock_open(read_data=self.test_template_content),
+ ):
+ result = importWebGL.getHTMLTemplate()
+ self.assertIsNotNone(result)
+
+ @patch("BIM.importers.importWebGL.FreeCADGui")
+ def test_custom_template_not_found_gui_user_rejects_fallback(
+ self, mock_gui
+ ):
+ """Test GUI dialog when custom template not found -
+ user rejects fallback"""
+ operation = "Testing GUI custom template not found - user rejects fallback"
+ self.printTestMessage(operation)
+ from PySide import QtWidgets
+
+ mock_gui.getMainWindow.return_value = MagicMock()
+
+ with patch(
+ "BIM.importers.importWebGL.params.get_param"
+ ) as mock_params:
+ mock_params.side_effect = lambda param, path=None: {
+ "useCustomWebGLExportTemplate": True,
+ "WebGLTemplateCustomPath": "/nonexistent/template.html",
+ }.get(param, False)
+
+ with patch(
+ "PySide.QtWidgets.QMessageBox.question",
+ return_value=QtWidgets.QMessageBox.No,
+ ):
+ result = importWebGL.getHTMLTemplate()
+ self.assertIsNone(result)
+
+ def test_export_returns_false_when_no_template(self):
+ """Test that export function returns False when
+ no template is available"""
+ operation = "Testing export returns False when no template available"
+ self.printTestMessage(operation)
+ with patch(
+ "BIM.importers.importWebGL.getHTMLTemplate", return_value=None
+ ):
+ with patch("BIM.importers.importWebGL.FreeCADGui") as mock_gui:
+ # Mock the GUI components that might be accessed
+ mock_active_doc = MagicMock()
+ mock_active_doc.ActiveView = MagicMock()
+ mock_gui.ActiveDocument = mock_active_doc
+
+ result = importWebGL.export(
+ [], os.path.join(self.test_dir, "test.html")
+ )
+ self.assertFalse(result)
+
+ def test_export_returns_true_when_template_available(self):
+ """Test that export function returns True when template is available"""
+ operation = "Testing export returns True when template available"
+ self.printTestMessage(operation)
+ mock_template = """
+ $pagetitle $version $data $threejs_version
+ """
+
+ with patch(
+ "BIM.importers.importWebGL.getHTMLTemplate",
+ return_value=mock_template,
+ ):
+ with patch(
+ "BIM.importers.importWebGL.FreeCAD.ActiveDocument"
+ ) as mock_doc:
+ mock_doc.Label = "Test Document"
+ with patch("BIM.importers.importWebGL.FreeCADGui") as mock_gui:
+ # Mock the GUI components that might be accessed
+ mock_active_doc = MagicMock()
+ mock_active_doc.ActiveView = MagicMock()
+ mock_gui.ActiveDocument = mock_active_doc
+
+ # Mock the functions that populate data to return JSON-serializable values
+ with patch(
+ "BIM.importers.importWebGL.populate_camera"
+ ) as mock_populate_camera:
+ with patch(
+ "BIM.importers.importWebGL.Draft.get_group_contents",
+ return_value=[],
+ ):
+ mock_populate_camera.return_value = (
+ None # Modifies data dict in place
+ )
+
+ result = importWebGL.export(
+ [], os.path.join(self.test_dir, "test.html")
+ )
+ self.assertTrue(result)
+
+
+if __name__ == "__main__":
+ # Allow running tests directly
+ unittest.main(verbosity=2)
diff --git a/src/Mod/BIM/importers/importWebGL.py b/src/Mod/BIM/importers/importWebGL.py
index 81d1da73a3..22369d24f9 100644
--- a/src/Mod/BIM/importers/importWebGL.py
+++ b/src/Mod/BIM/importers/importWebGL.py
@@ -4,6 +4,7 @@
# * *
# * Copyright (c) 2013 Yorik van Havre *
# * Copyright (c) 2020 Travis Apple *
+# * Copyright (c) 2025 baidakovil *
# * *
# * This file is part of FreeCAD. *
# * *
@@ -44,13 +45,16 @@
#
# This module provides tools to export HTML files containing the
# exported objects in WebGL format and a simple three.js-based viewer.
+# Tests are provided in src/Mod/BIM/bimtests/TestWebGLExport.py.
+# The template is provided in src/Mod/BIM/Resources/templates/webgl_export_template.html.
"""FreeCAD WebGL Exporter"""
import json
+import os
import textwrap
from builtins import open as pyopen
-from typing import NotRequired, TypedDict
+from typing_extensions import NotRequired, TypedDict
import numpy as np
@@ -59,9 +63,11 @@ import Draft
import Mesh
import OfflineRenderingUtils
import Part
+from draftutils import params
if FreeCAD.GuiUp:
import FreeCADGui
+ from PySide import QtWidgets
from draftutils.translate import translate
else:
FreeCADGui = None
@@ -77,666 +83,137 @@ threejs_version = "0.172.0"
def getHTMLTemplate():
- return textwrap.dedent("""\
-
-
-
- $pagetitle
-
-
-
-
-
-
-
-
-
-
-
-
- """)
+ return None
def export(
exportList, filename: str, colors: dict[str, str] | None = None, camera: str | None = None
-):
- """Exports objects to an html file"""
+) -> bool:
+ """Exports objects to an html file.
+
+ Returns:
+ bool: True if export was successful, False if not (particulary,
+ False if no template was available).
+ """
+
+ # Check template availability first, before any processing
+ html = getHTMLTemplate()
+ if html is None:
+ # No template available - export failed
+ return False
global disableCompression, base, baseFloat
@@ -894,7 +371,6 @@ def export(
data["objects"].append(objdata)
- html = getHTMLTemplate()
html = html.replace("$pagetitle", FreeCAD.ActiveDocument.Label)
version = FreeCAD.Version()
html = html.replace("$version", f"{version[0]}.{version[1]}.{version[2]}")
@@ -910,6 +386,7 @@ def export(
with pyopen(filename, "w", encoding="utf-8") as outfile:
outfile.write(html)
FreeCAD.Console.PrintMessage(translate("Arch", "Successfully written") + f" {filename}\n")
+ return True
def get_view_properties(obj, label: str, colors: dict[str, str] | None) -> tuple[str, float]:
diff --git a/src/Mod/Draft/draftutils/params.py b/src/Mod/Draft/draftutils/params.py
index f2046e5c52..2720f4d81d 100644
--- a/src/Mod/Draft/draftutils/params.py
+++ b/src/Mod/Draft/draftutils/params.py
@@ -643,7 +643,8 @@ def _get_param_dictionary():
":/ui/preferences-dae.ui",
":/ui/preferences-ifc.ui",
":/ui/preferences-ifc-export.ui",
- ":/ui/preferences-sh3d-import.ui",):
+ ":/ui/preferences-sh3d-import.ui",
+ ":/ui/preferences-webgl.ui",):
# https://stackoverflow.com/questions/14750997/load-txt-file-from-resources-in-python
fd = QtCore.QFile(fnm)