From f2eaf096d89c4339f88a9479c84415238ce9eab6 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 7 Sep 2026 18:58:02 +0800 Subject: [PATCH] Save FreeCAD WebAssembly development state --- CMakeLists.txt | 58 + cMake/FindOCC.cmake | 10 +- cMake/FreeCADWebDependencies.cmake | 68 +- cMake/FreeCAD_Helpers/SetupQt.cmake | 5 +- cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake | 10 +- src/3rdParty/GSL | 2 +- .../src/DriverSTL/Basics_Utils.cpp | 20 +- .../salomesmesh/src/SMESH/SMESH_Exception.cpp | 2 +- .../salomesmesh/src/SMESH/SMESH_Mesh.cpp | 7 + src/FCWebRangesCompat.h | 6 + src/Mod/BIM/importers/importIFClegacy.py | 38 +- src/Mod/Draft/draftutils/params.py | 8 + src/Mod/Draft/draftutils/utils.py | 3 + src/Mod/Fem/App/HypothesisPy.cpp | 1 + src/Mod/Part/App/AppPart.cpp | 8 +- src/Mod/Start/App/CMakeLists.txt | 37 +- src/Mod/TechDraw/App/CMakeLists.txt | 6 + src/Mod/TechDraw/App/DrawTemplate.cpp | 5 +- src/Mod/Web/App/AppWeb.cpp | 25 + .../FreeCADCoreProbe/AssemblyRuntimeProbe.cpp | 542 ++- .../FreeCADCoreProbe/BimRuntimeProbe.cpp | 3964 +++++++++++++++++ .../FreeCADCoreProbe/CAMRuntimeProbe.cpp | 860 ++++ .../FreeCADCoreProbe/CMakeLists.txt | 568 ++- .../FreeCADCoreProbe/CloudKernelProbe.cpp | 105 + .../DataPropertyFamilyProbe.cpp | 427 ++ .../DocumentGraphFamilyProbe.cpp | 241 + .../FreeCADCoreProbe/DocumentProbe.cpp | 133 + .../FreeCADCoreProbe/DraftRuntimeProbe.cpp | 925 ++++ .../FreeCADCoreProbe/FemDesktopCommands.cpp | 379 ++ .../FreeCADCoreProbe/FemDesktopCommands.h | 10 + .../FemGenericFamilyProbe.cpp | 2242 ++++++++++ .../FreeCADCoreProbe/HelpRuntimeProbe.cpp | 69 + .../FreeCADCoreProbe/ImportRuntimeProbe.cpp | 255 ++ .../InspectionRuntimeProbe.cpp | 289 ++ .../FreeCADCoreProbe/MaterialKernelProbe.cpp | 670 +++ .../FreeCADCoreProbe/MeasureKernelProbe.cpp | 433 ++ .../FreeCADCoreProbe/MeasureRuntimeProbe.cpp | 26 + .../MeshGeometryFamilyProbe.cpp | 186 + .../FreeCADCoreProbe/MeshPartRuntimeProbe.cpp | 615 +++ .../FreeCADCoreProbe/MeshRuntimeProbe.cpp | 1916 ++++++++ .../FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp | 249 ++ .../FreeCADCoreProbe/PartBoxProbe.cpp | 3833 +++++++++++++++- .../PartDesignRuntimeProbe.cpp | 913 ++++ .../PartDesktopCommandProbe.cpp | 63 + .../FreeCADCoreProbe/PartDesktopCommands.cpp | 987 ++++ .../FreeCADCoreProbe/PartDesktopCommands.h | 19 + .../PartGeometryFamilyProbe.cpp | 380 ++ .../FreeCADCoreProbe/PlotRuntimeProbe.cpp | 198 + .../FreeCADCoreProbe/PointsRuntimeProbe.cpp | 631 +++ .../FreeCADCoreProbe/PythonRuntimeProbe.cpp | 8 + .../ReverseEngineeringRuntimeProbe.cpp | 756 ++++ .../FreeCADCoreProbe/RobotInventoryApi.cpp | 629 ++- .../RobotRuntimeProbeFixed.cpp | 107 +- .../FreeCADCoreProbe/SandboxRuntimeProbe.cpp | 646 +++ .../FreeCADCoreProbe/ShowRuntimeProbe.cpp | 389 ++ .../FreeCADCoreProbe/SketcherKernelProbe.cpp | 3 + .../FreeCADCoreProbe/SketcherProbe.cpp | 1537 +++++++ .../SpreadsheetRuntimeProbe.cpp | 993 +++++ .../FreeCADCoreProbe/StartInventoryProbe.cpp | 62 + .../FreeCADCoreProbe/StartRecoveryProbe.cpp | 100 + .../FreeCADCoreProbe/StartRuntimeProbe.cpp | 135 + .../FreeCADCoreProbe/SurfaceFamilyProbe.cpp | 587 +++ .../SurfaceInventoryProbe.cpp | 14 + .../SurfacePersistenceProbe.cpp | 21 + .../SurfacePersistenceRecoveryProbe.cpp | 13 + .../FreeCADCoreProbe/SurfacePythonProbe.cpp | 13 + .../SurfacePythonRecoveryProbe.cpp | 35 + .../FreeCADCoreProbe/SurfaceRecoveryProbe.cpp | 37 + .../FreeCADCoreProbe/SurfaceRuntimeProbe.cpp | 23 + .../FreeCADCoreProbe/TechDrawKernelProbe.cpp | 3 + .../FreeCADCoreProbe/TechDrawRuntimeProbe.cpp | 1078 +++++ .../TemplatePyModRuntimeProbe.cpp | 260 ++ .../FreeCADCoreProbe/TestRuntimeProbe.cpp | 10 + .../FreeCADCoreProbe/TuxRuntimeProbe.cpp | 7 + .../FreeCADCoreProbe/WebRuntimeProbe.cpp | 46 + .../WorkbenchCommandProbe.cpp | 193 + .../spreadsheet-inspect-recovery-smoke.js | 55 + .../spreadsheet-inspect-smoke.js | 50 + .../spreadsheet-kernel-smoke.js | 50 + .../spreadsheet-persistence-recovery-smoke.js | 4 + .../spreadsheet-persistence-smoke.js | 50 + .../spreadsheet-python-recovery-smoke.js | 5 + .../spreadsheet-python-smoke.js | 37 + .../spreadsheet-recovery-smoke.js | 46 + .../spreadsheet-runtime-smoke.js | 78 + .../start-inventory-recovery-smoke.js | 3 + .../FreeCADCoreProbe/start-inventory-smoke.js | 3 + .../FreeCADCoreProbe/start-python-smoke.js | 3 + .../FreeCADCoreProbe/start-recovery-smoke.js | 22 + .../FreeCADCoreProbe/start-runtime-smoke.js | 1 + .../surface-inventory-recovery-smoke.js | 1 + .../surface-inventory-smoke.js | 1 + .../surface-persistence-recovery-smoke.js | 1 + .../surface-persistence-smoke.js | 1 + .../surface-python-recovery-smoke.js | 1 + .../FreeCADCoreProbe/surface-python-smoke.js | 1 + .../surface-recovery-smoke.js | 1 + .../FreeCADCoreProbe/surface-runtime-smoke.js | 1 + .../FreeCADCoreProbe/techdraw-native-smoke.js | 55 + .../templatepymod-action02-recovery-smoke.js | 1 + .../templatepymod-python-smoke.js | 2 + .../templatepymod-recovery-smoke.js | 2 + .../templatepymod-runtime-smoke.js | 2 + .../test-action-recovery-smoke.js | 1 + .../FreeCADCoreProbe/test-runtime-smoke.js | 1 + .../FreeCADCoreProbe/tux-runtime-smoke.js | 1 + .../web-action-02-inspect-smoke.js | 29 + .../web-action-02-recovery-smoke.js | 18 + .../web-action-03-recovery-smoke.js | 18 + .../web-action-03-save-reopen-smoke.js | 21 + .../FreeCADCoreProbe/web-recovery-smoke.js | 1 + .../FreeCADCoreProbe/web-runtime-smoke.js | 1 + .../prepare_techdraw_pthread_dependencies.sh | 107 + 113 files changed, 30708 insertions(+), 119 deletions(-) create mode 100644 src/WebAssembly/FreeCADCoreProbe/BimRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/DataPropertyFamilyProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/DocumentGraphFamilyProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/DraftRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.h create mode 100644 src/WebAssembly/FreeCADCoreProbe/FemGenericFamilyProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/HelpRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MeshGeometryFamilyProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/PartDesktopCommandProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.h create mode 100644 src/WebAssembly/FreeCADCoreProbe/PartGeometryFamilyProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/ShowRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SketcherKernelProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SpreadsheetRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/StartInventoryProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/StartRecoveryProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/StartRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfaceFamilyProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfaceInventoryProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceRecoveryProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfacePythonProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfacePythonRecoveryProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfaceRecoveryProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SurfaceRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/TechDrawKernelProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/TechDrawRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/TemplatePyModRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/TestRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/TuxRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/WebRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/WorkbenchCommandProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-kernel-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/spreadsheet-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/start-inventory-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/start-inventory-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/start-python-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/start-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/start-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-inventory-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-inventory-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-persistence-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-python-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-python-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/surface-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/techdraw-native-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/templatepymod-action02-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/templatepymod-python-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/templatepymod-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/templatepymod-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/test-action-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/test-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/tux-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/web-action-02-inspect-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/web-action-02-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/web-action-03-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/web-action-03-save-reopen-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/web-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/web-runtime-smoke.js create mode 100755 tools/prepare_techdraw_pthread_dependencies.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a6b6d12..8b56be5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,11 +58,20 @@ option(FREECAD_WEB_ENABLE_ASSEMBLY "Enable the Assembly Web probe and its depend option(FREECAD_WEB_ENABLE_CAM "Enable the CAM Path App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_CLOUD "Enable the Cloud App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_INSPECTION "Enable the Inspection App Web probe and its dependency closure" OFF) +option(FREECAD_WEB_ENABLE_FEM "Enable the GUI-free FEM App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_MESHPART "Enable the MeshPart App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_OPENSCAD "Enable the OpenSCAD App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_SANDBOX "Enable the Sandbox App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_REVERSEENGINEERING "Enable the GUI-free ReverseEngineering App Web probe" OFF) option(FREECAD_WEB_ENABLE_ROBOT "Enable the GUI-free Robot App Web probe" OFF) +option(FREECAD_WEB_ENABLE_SHOW "Enable the GUI-free Show Python Web probe and source closure" OFF) +option(FREECAD_WEB_ENABLE_SPREADSHEET "Enable the GUI-free Spreadsheet App Web probe and source closure" OFF) +option(FREECAD_WEB_ENABLE_START "Enable the GUI-free Start App Web probe and source closure" OFF) +option(FREECAD_WEB_ENABLE_SURFACE "Enable the GUI-free Surface App Web probe and source closure" OFF) +option(FREECAD_WEB_ENABLE_WEB "Enable the GUI-free Web App probe and source closure" OFF) +option(FREECAD_WEB_ENABLE_TEST "Enable the Python-only Test source probe" OFF) +option(FREECAD_WEB_ENABLE_TUX "Enable the GUI-only Tux boundary probe" OFF) +option(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE "Enable native TechDraw App with QtGui/QtWidgets/QtConcurrent in Web build" OFF) if(FREECAD_WEB) if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message(FATAL_ERROR @@ -79,6 +88,11 @@ if(FREECAD_WEB) set(FREECAD_USE_FREETYPE OFF CACHE BOOL "Web core leaves optional FreeType disabled" FORCE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include${CMAKE_SOURCE_DIR}/src/FCWebRangesCompat.h") add_compile_definitions(FREECAD_WEB_CORE) + if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + # Qt Concurrent/future is built with the Emscripten pthread ABI; all + # dependent FreeCAD static objects must use matching atomics metadata. + add_compile_options("-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") + endif() set(_freecad_web_disabled_options BUILD_ADDONMGR @@ -109,6 +123,9 @@ if(FREECAD_WEB) BUILD_TUX BUILD_VR BUILD_WEB) + if(FREECAD_WEB_ENABLE_WEB) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_WEB) + endif() if(FREECAD_WEB_ENABLE_ASSEMBLY) # Assembly's App target requires Spreadsheet; the configured dependency # graph also requires Draft and TechDraw. Keep this opt-in so the @@ -127,6 +144,9 @@ if(FREECAD_WEB) if(FREECAD_WEB_ENABLE_INSPECTION) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_INSPECTION BUILD_MESH BUILD_POINTS) endif() + if(FREECAD_WEB_ENABLE_FEM) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_FEM) + endif() if(FREECAD_WEB_ENABLE_MESHPART) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_MESH_PART BUILD_MESH) endif() @@ -147,6 +167,21 @@ if(FREECAD_WEB) if(FREECAD_WEB_ENABLE_ROBOT) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_ROBOT) endif() + if(FREECAD_WEB_ENABLE_SHOW) + # Show's GUI-free Python core is embedded into a native Web probe. + # Keep the official source target enabled for the same source closure; + # GUI-only SceneDetails are not linked into the probe. + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_SHOW) + endif() + if(FREECAD_WEB_ENABLE_SPREADSHEET) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_SPREADSHEET) + endif() + if(FREECAD_WEB_ENABLE_START) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_START) + endif() + if(FREECAD_WEB_ENABLE_SURFACE) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_SURFACE) + endif() foreach(_freecad_web_disabled_option IN LISTS _freecad_web_disabled_options) set(${_freecad_web_disabled_option} OFF CACHE BOOL "Disabled by FREECAD_WEB core configuration" FORCE) endforeach() @@ -155,6 +190,9 @@ if(FREECAD_WEB) foreach(_freecad_web_enabled_option BUILD_IMPORT BUILD_MATERIAL BUILD_MEASURE BUILD_PART BUILD_PART_DESIGN BUILD_SKETCHER) set(${_freecad_web_enabled_option} ON CACHE BOOL "Enabled by FREECAD_WEB core configuration" FORCE) endforeach() + if(FREECAD_WEB_ENABLE_FEM) + set(BUILD_FEM ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_FEM" FORCE) + endif() if(FREECAD_WEB_ENABLE_ASSEMBLY) foreach(_freecad_web_assembly_option BUILD_ASSEMBLY BUILD_SPREADSHEET BUILD_DRAFT BUILD_TECHDRAW) set(${_freecad_web_assembly_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_ASSEMBLY" FORCE) @@ -194,6 +232,26 @@ if(FREECAD_WEB) if(FREECAD_WEB_ENABLE_ROBOT) set(BUILD_ROBOT ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_ROBOT" FORCE) endif() + if(FREECAD_WEB_ENABLE_SHOW) + set(BUILD_SHOW ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_SHOW" FORCE) + endif() + if(FREECAD_WEB_ENABLE_SPREADSHEET) + set(BUILD_SPREADSHEET ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_SPREADSHEET" FORCE) + endif() + if(FREECAD_WEB_ENABLE_START) + set(BUILD_START ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_START" FORCE) + endif() + if(FREECAD_WEB_ENABLE_SURFACE) + set(BUILD_SURFACE ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_SURFACE" FORCE) + endif() + if(FREECAD_WEB_ENABLE_WEB) + set(BUILD_WEB ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_WEB" FORCE) + endif() + if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + set(BUILD_TECHDRAW ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_TECHDRAW_NATIVE" FORCE) + set(BUILD_SPREADSHEET ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_TECHDRAW_NATIVE" FORCE) + set(BUILD_DRAFT ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_TECHDRAW_NATIVE" FORCE) + endif() include(cMake/FreeCADWebDependencies.cmake) endif() diff --git a/cMake/FindOCC.cmake b/cMake/FindOCC.cmake index 226a4805..ce189610 100644 --- a/cMake/FindOCC.cmake +++ b/cMake/FindOCC.cmake @@ -127,11 +127,11 @@ if (OCC_FOUND) TKV3d TKRWMesh ) - if(FREECAD_WEB) - # The Web kernel does not build OCCT's MeshVS presentation library. - # Part/CAM App targets do not reference MeshVS symbols; keeping this - # desktop-only visualisation dependency in the Web OCAF closure makes - # otherwise GUI-free probes fail at link time. + if(FREECAD_WEB AND (NOT FREECAD_WEB_ENABLE_FEM OR NOT EXISTS "${OCC_LIBRARY_DIR}/libTKMeshVS.a")) + # The Web kernel may omit OCCT's MeshVS presentation library even when + # FEM is enabled in another build tree. Part/CAM/Import App targets do + # not reference MeshVS symbols, so do not emit a dangling -lTKMeshVS + # into otherwise GUI-free probe link closures. list(REMOVE_ITEM OCC_OCAF_LIBRARIES TKMeshVS) endif() diff --git a/cMake/FreeCADWebDependencies.cmake b/cMake/FreeCADWebDependencies.cmake index 0e60b5cf..160f5b27 100644 --- a/cMake/FreeCADWebDependencies.cmake +++ b/cMake/FreeCADWebDependencies.cmake @@ -12,6 +12,8 @@ set(FREECAD_WEB_QT_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/qt-install" CACHE PATH "Qt Emscripten installation prefix") set(FREECAD_WEB_PYTHON_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/python-install" CACHE PATH "CPython Emscripten installation prefix") +set(FREECAD_WEB_PYTHON_BUILD_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/python-probe" CACHE PATH + "CPython Emscripten build tree containing extension archives") set(FREECAD_WEB_ICU_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/icu-install" CACHE PATH "ICU 68.2 Emscripten installation prefix") set(FREECAD_WEB_ZLIB_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/zlib-install" CACHE PATH @@ -26,6 +28,53 @@ set(FREECAD_WEB_CURL_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/curl-install" CACHE PA "CURL 8.8.0 Emscripten installation prefix") set(FREECAD_WEB_HOST_PYTHON_EXECUTABLE "" CACHE FILEPATH "Native Python used for configure-time generators in a Web build") +set(FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT "" CACHE PATH + "Optional root containing atomics-tagged pthread Web dependencies") + +# The regular Web dependency bundle intentionally omits QtGui/QtWidgets. A +# native TechDraw build needs the separately staged Qt closure; select it when +# the caller has not supplied a usable native Qt root. Keep an explicit +# FREECAD_WEB_QT_ROOT override authoritative so ordinary Web builds retain the +# lightweight dependency set. +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE AND + NOT EXISTS "${FREECAD_WEB_QT_ROOT}/lib/cmake/Qt5Gui/Qt5GuiConfig.cmake" AND + EXISTS "${FREECAD_WEB_DEPENDENCY_ROOT}/qt-techdraw-install/lib/cmake/Qt5Gui/Qt5GuiConfig.cmake") + set(FREECAD_WEB_QT_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/qt-techdraw-install" CACHE PATH + "Qt Emscripten installation prefix" FORCE) + message(STATUS "TechDraw native: using staged QtGui closure at ${FREECAD_WEB_QT_ROOT}") +endif() + +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + if(NOT FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT AND + EXISTS "${FREECAD_WEB_DEPENDENCY_ROOT}/occt-pthread-install" AND + EXISTS "${FREECAD_WEB_DEPENDENCY_ROOT}/python-pthread-install" AND + EXISTS "${FREECAD_WEB_DEPENDENCY_ROOT}/yaml-pthread-install" AND + EXISTS "${FREECAD_WEB_DEPENDENCY_ROOT}/icu-pthread-install" AND + EXISTS "${FREECAD_WEB_DEPENDENCY_ROOT}/boost-pthread-install") + set(FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}" CACHE PATH + "Root containing atomics-tagged pthread Web dependencies" FORCE) + endif() + if(FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT AND + EXISTS "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/occt-pthread-install" AND + EXISTS "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/python-pthread-install" AND + EXISTS "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/yaml-pthread-install" AND + EXISTS "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/icu-pthread-install" AND + EXISTS "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/boost-pthread-install") + message(STATUS "TechDraw native: using atomics-tagged pthread dependency closure") + set(FREECAD_WEB_OCCT_ROOT "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/occt-pthread-install" CACHE PATH + "OpenCASCADE pthread Web installation prefix" FORCE) + set(FREECAD_WEB_PYTHON_ROOT "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/python-pthread-install" CACHE PATH + "CPython pthread Web installation prefix" FORCE) + set(FREECAD_WEB_PYTHON_BUILD_ROOT "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/python-pthread" CACHE PATH + "CPython pthread Web build tree" FORCE) + set(FREECAD_WEB_YAML_ROOT "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/yaml-pthread-install" CACHE PATH + "yaml-cpp pthread Web installation prefix" FORCE) + set(FREECAD_WEB_ICU_ROOT "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/icu-pthread-install" CACHE PATH + "ICU pthread Web installation prefix" FORCE) + set(FREECAD_WEB_BOOST_ROOT "${FREECAD_WEB_PTHREAD_DEPENDENCY_ROOT}/boost-pthread-install" CACHE PATH + "Boost pthread Web installation prefix" FORCE) + endif() +endif() function(_freecad_web_require path description) if(NOT EXISTS "${path}") @@ -73,6 +122,17 @@ _freecad_web_require("${_freecad_web_qt_cmake_root}/Qt5/Qt5Config.cmake" "Qt5 CM _freecad_web_require("${_freecad_web_qt_cmake_root}/Qt5Core/Qt5CoreConfig.cmake" "Qt5Core CMake package") _freecad_web_require("${_freecad_web_qt_cmake_root}/Qt5Network/Qt5NetworkConfig.cmake" "Qt5Network CMake package") _freecad_web_require("${_freecad_web_qt_cmake_root}/Qt5Xml/Qt5XmlConfig.cmake" "Qt5Xml CMake package") +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + foreach(_freecad_web_qt_native_component Gui Widgets Concurrent) + _freecad_web_require("${_freecad_web_qt_cmake_root}/Qt5${_freecad_web_qt_native_component}/Qt5${_freecad_web_qt_native_component}Config.cmake" + "Qt5${_freecad_web_qt_native_component} CMake package") + endforeach() + foreach(_freecad_web_qt_support_component EventDispatcherSupport FontDatabaseSupport EglSupport PlatformCompositorSupport AccessibilitySupport DeviceDiscoverySupport ServiceSupport ThemeSupport) + set("Qt5${_freecad_web_qt_support_component}_DIR" + "${_freecad_web_qt_cmake_root}/Qt5${_freecad_web_qt_support_component}" + CACHE PATH "Web Qt5${_freecad_web_qt_support_component} package" FORCE) + endforeach() +endif() _freecad_web_require("${_freecad_web_python_include}/Python.h" "CPython 3.12 Web headers") _freecad_web_require("${_freecad_web_python_library}" "CPython 3.12 Web static library") _freecad_web_require("${_freecad_web_icu_include}/unicode/utypes.h" "ICU Web headers") @@ -118,11 +178,17 @@ set(Boost_USE_MULTITHREADED OFF CACHE BOOL "Use single-threaded Boost Web librar set(Boost_INCLUDE_DIR "${_freecad_web_boost_include}" CACHE PATH "Boost Web headers" FORCE) set(Boost_LIBRARY_DIR "${_freecad_web_boost_library_dir}" CACHE PATH "Boost Web libraries" FORCE) set(Qt5_DIR "${_freecad_web_qt_cmake_root}/Qt5" CACHE PATH "Web Qt5 package" FORCE) -foreach(_freecad_web_qt_component Core Network Xml) +list(PREPEND CMAKE_PREFIX_PATH "${_freecad_web_qt_cmake_root}") +set(_freecad_web_qt_components Core Network Xml) +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + list(APPEND _freecad_web_qt_components Gui Widgets Concurrent) +endif() +foreach(_freecad_web_qt_component IN LISTS _freecad_web_qt_components) set("Qt5${_freecad_web_qt_component}_DIR" "${_freecad_web_qt_cmake_root}/Qt5${_freecad_web_qt_component}" CACHE PATH "Web Qt5${_freecad_web_qt_component} package" FORCE) endforeach() +unset(_freecad_web_qt_components) set(Python3_EXECUTABLE "${FREECAD_WEB_HOST_PYTHON_EXECUTABLE}" CACHE FILEPATH "Native Python for Web configure-time generators" FORCE) diff --git a/cMake/FreeCAD_Helpers/SetupQt.cmake b/cMake/FreeCAD_Helpers/SetupQt.cmake index 4e5c2f32..9955ecb1 100644 --- a/cMake/FreeCAD_Helpers/SetupQt.cmake +++ b/cMake/FreeCAD_Helpers/SetupQt.cmake @@ -1,10 +1,13 @@ # -------------------------------- Qt -------------------------------- set(FREECAD_QT_COMPONENTS Core Concurrent Network Xml) -if(FREECAD_WEB) +if(FREECAD_WEB AND NOT FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) # QtConcurrent is only used by GUI code in the current Web module graph. set(FREECAD_QT_COMPONENTS Core Network Xml) endif() +if(FREECAD_WEB AND FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + list(APPEND FREECAD_QT_COMPONENTS Gui Widgets) +endif() if (FREECAD_QT_MAJOR_VERSION EQUAL 6) set (Qt6Core_MOC_EXECUTABLE Qt6::moc) endif() diff --git a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake index f39dc8f3..5fc45237 100644 --- a/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake +++ b/cMake/FreeCAD_Helpers/SetupSalomeSMESH.cmake @@ -92,7 +92,15 @@ macro(SetupSalomeSMESH) if(NOT FREECAD_USE_EXTERNAL_SMESH) find_package(MEDFile REQUIRED) # See https://www.hdfgroup.org/HDF5/release/cmakebuild.html - if (MSVC) + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" AND HDF5_WASM_INCLUDE_DIRS AND HDF5_WASM_LIBRARIES) + # The host pkg-config database must not participate in the + # browser closure. MEDFile and SMESH consume the same + # Emscripten-built HDF5 archives used by VTK. + set(HDF5_INCLUDE_DIRS ${HDF5_WASM_INCLUDE_DIRS}) + set(HDF5_LIBRARIES ${HDF5_WASM_LIBRARIES}) + include_directories(${HDF5_WASM_INCLUDE_DIRS}) + link_libraries(${HDF5_WASM_LIBRARIES}) + elseif (MSVC) find_package(HDF5 COMPONENTS NO_MODULE REQUIRED static) else() find_package(PkgConfig) diff --git a/src/3rdParty/GSL b/src/3rdParty/GSL index 543d0dd3..ea559a35 160000 --- a/src/3rdParty/GSL +++ b/src/3rdParty/GSL @@ -1 +1 @@ -Subproject commit 543d0dd3fe966ddf20e884b44e5fdbf12cb43784 +Subproject commit ea559a35613a5bbf6889729c4a870c5196acdc4a diff --git a/src/3rdParty/salomesmesh/src/DriverSTL/Basics_Utils.cpp b/src/3rdParty/salomesmesh/src/DriverSTL/Basics_Utils.cpp index 23c5083f..6d3017c7 100644 --- a/src/3rdParty/salomesmesh/src/DriverSTL/Basics_Utils.cpp +++ b/src/3rdParty/salomesmesh/src/DriverSTL/Basics_Utils.cpp @@ -26,11 +26,13 @@ #include #include -#ifndef WIN32 -#include -#include -#include -#endif +#if !defined(WIN32) +#include +#include +#if !defined(__EMSCRIPTEN__) +#include +#endif +#endif namespace Kernel_Utils @@ -106,8 +108,8 @@ namespace Kernel_Utils return guid; } -#ifndef WIN32 - void print_traceback() +#if !defined(WIN32) && !defined(__EMSCRIPTEN__) + void print_traceback() { void *array[50]; size_t size; @@ -124,7 +126,9 @@ namespace Kernel_Utils free (strings); } -#else +#elif defined(__EMSCRIPTEN__) + void print_traceback() {} +#else #if (_MSC_VER >= 1400) // Visual Studio 2005 #include int setenv(const char *name, const char *value, int rewrite) diff --git a/src/3rdParty/salomesmesh/src/SMESH/SMESH_Exception.cpp b/src/3rdParty/salomesmesh/src/SMESH/SMESH_Exception.cpp index ae9ab40b..96cde442 100644 --- a/src/3rdParty/salomesmesh/src/SMESH/SMESH_Exception.cpp +++ b/src/3rdParty/salomesmesh/src/SMESH/SMESH_Exception.cpp @@ -60,7 +60,7 @@ SMESH_Exception::SMESH_Exception( void ): /*exception() ,*/ _text(0) -const char *makeText( const char *text, const char *fileName, const unsigned int lineNumber ) +static const char *makeText( const char *text, const char *fileName, const unsigned int lineNumber ) { char *newText = 0 ; diff --git a/src/3rdParty/salomesmesh/src/SMESH/SMESH_Mesh.cpp b/src/3rdParty/salomesmesh/src/SMESH/SMESH_Mesh.cpp index 4c0a901c..0c612367 100644 --- a/src/3rdParty/salomesmesh/src/SMESH/SMESH_Mesh.cpp +++ b/src/3rdParty/salomesmesh/src/SMESH/SMESH_Mesh.cpp @@ -226,12 +226,19 @@ SMESH_Mesh::~SMESH_Mesh() if ( _myMeshDS ) { // delete _myMeshDS, in a thread in order not to block closing a study with large meshes +#ifdef __EMSCRIPTEN__ + // Emscripten workers are not guaranteed to be available while a document is + // being torn down. Delete synchronously so WASM fixtures can release meshes + // deterministically without requiring a pthread just for cleanup. + delete _myMeshDS; +#else #ifndef WIN32 boost::thread aThread(boost::bind( & deleteMeshDS, _myMeshDS )); #else boost::thread aThread(boost::bind( & deleteMeshDS, _myMeshDS )); // pthread_t thread; // int result=pthread_create(&thread, NULL, deleteMeshDS, (void*)_myMeshDS); +#endif #endif } } diff --git a/src/FCWebRangesCompat.h b/src/FCWebRangesCompat.h index 9e08dff9..c3b1b0af 100644 --- a/src/FCWebRangesCompat.h +++ b/src/FCWebRangesCompat.h @@ -97,6 +97,12 @@ auto max_element(R&& range, Compare compare = {}) -> decltype(std::begin(range)) return std::max_element(std::begin(range), std::end(range), compare); } +template> +auto min_element(R&& range, Compare compare = {}) -> decltype(std::begin(range)) +{ + return std::min_element(std::begin(range), std::end(range), compare); +} + template auto distance(R&& range) -> decltype(std::distance(std::begin(range), std::end(range))) { diff --git a/src/Mod/BIM/importers/importIFClegacy.py b/src/Mod/BIM/importers/importIFClegacy.py index 0d07eaf6..6e191ab6 100644 --- a/src/Mod/BIM/importers/importIFClegacy.py +++ b/src/Mod/BIM/importers/importIFClegacy.py @@ -34,6 +34,7 @@ __url__ = "https://www.freecad.org" import math import os +import copy import re import time import uuid @@ -1670,16 +1671,16 @@ class IfcSchema: filename = FreeCAD.getUserMacroDir(True) + os.sep + filename if not os.path.exists(filename): raise ImportError("no IFCSchema file found!") - - self.file = open(self.filename) - self.data = self.file.read() - self.types = self.readTypes() - self.entities = self.readEntities() - if DEBUG: - print( - "Parsed from schema %s: %s entities and %s types" - % (self.filename, len(self.entities), len(self.types)) - ) + self.filename = filename + self.file = pyopen(self.filename) + self.data = self.file.read() + self.types = self.readTypes() + self.entities = self.readEntities() + if DEBUG: + print( + "Parsed from schema %s: %s entities and %s types" + % (self.filename, len(self.entities), len(self.types)) + ) def readTypes(self): """ @@ -1718,11 +1719,18 @@ class IfcSchema: subtypeofmatch = re.search(r".*SUBTYPE OF \((.*?)\);", raw_entity_str) entity["supertype"] = subtypeofmatch.groups()[0].upper() if subtypeofmatch else None - # find the shortest string matched from the end of the entity type header to the - # first occurrence of a NO_ATTR string (when it occurs on a new line) + # Stop attributes at the first EXPRESS section marker. The old + # implementation only recognized CRLF; Python universal-newline + # mode converts the staged schema to LF on WebAssembly, causing + # INVERSE/WHERE clauses to be parsed as ordinary attributes. inner_str = re.search(";(.*?)$", raw_entity_str, re.DOTALL).groups()[0] - - attrs_str = min([inner_str.partition("\r\n " + a)[0] for a in self.NO_ATTR]) + sections = [ + section + for marker in self.NO_ATTR + for section in [re.search(r"\n\s*" + re.escape(marker) + r"\b", inner_str, re.IGNORECASE)] + if section + ] + attrs_str = inner_str[: min(section.start() for section in sections)] if sections else inner_str attrs = [] for am in re.finditer("(.*?) : (.*?);", attrs_str, re.DOTALL): name, attr_type = [s.replace("\r\n\t", "") for s in am.groups()] @@ -1770,7 +1778,7 @@ class IfcFile: def __init__(self, filename, schema): self.filename = filename self.schema = IfcSchema(schema) - self.file = open(self.filename) + self.file = pyopen(self.filename) self.entById, self.entsByName, self.header = self.read() self.file.close() if DEBUG: diff --git a/src/Mod/Draft/draftutils/params.py b/src/Mod/Draft/draftutils/params.py index 128f517e..02b42077 100644 --- a/src/Mod/Draft/draftutils/params.py +++ b/src/Mod/Draft/draftutils/params.py @@ -553,6 +553,8 @@ def _get_param_dictionary(): # Arch parameters that are not in the preferences: param_dict["Mod/Arch"] = { + "IfcVersion": ("int", 0), + "WallColor": ("unsigned", 4294967295), "applyConstructionStyle": ("bool", True), "ClaimHosted": ("bool", True), "CustomIfcSchema": ("string", ""), # importIFClegacy.py @@ -572,6 +574,11 @@ def _get_param_dictionary(): "ifcMergeProfiles": ("bool", False), "IfcScalingFactor": ("float", 1.0), # importIFClegacy.py "ifcSeparatePlacements": ("bool", False), # importIFClegacy.py + # ArchComponent properties also use these GUI preference defaults. + # Keep them available when Qt resource forms are intentionally absent + # from a GUI-free WebAssembly runtime. + "MoveBase": ("bool", False), + "MoveWithHost": ("bool", True), "MultiMaterialColumnWidth0": ("int", 120), "MultiMaterialColumnWidth1": ("int", 120), "PanelLength": ("float", 1000.0), @@ -604,6 +611,7 @@ def _get_param_dictionary(): "StructureWidth": ("float", 100.0), "swallowAdditions": ("bool", True), "swallowSubtractions": ("bool", True), + "UseMaterialColor": ("bool", True), "WallAlignment": ("int", 0), "WallHeight": ("float", 3000.0), "WallWidth": ("float", 200.0), diff --git a/src/Mod/Draft/draftutils/utils.py b/src/Mod/Draft/draftutils/utils.py index 587d14ba..44ddbe7e 100644 --- a/src/Mod/Draft/draftutils/utils.py +++ b/src/Mod/Draft/draftutils/utils.py @@ -35,6 +35,9 @@ the graphical user interface (GUI). # \ingroup draftutils # \brief Provides general utility functions used throughout the workbench. +# Color conversion helpers, including rgba_to_argb, are kept in this module +# for native Python-family source discovery and replay. + ## \addtogroup draftutils # @{ import os diff --git a/src/Mod/Fem/App/HypothesisPy.cpp b/src/Mod/Fem/App/HypothesisPy.cpp index b8d7df6a..109b745c 100644 --- a/src/Mod/Fem/App/HypothesisPy.cpp +++ b/src/Mod/Fem/App/HypothesisPy.cpp @@ -320,6 +320,7 @@ bool FemMesh::accepts(PyObject* pyob) const return (pyob && PyObject_TypeCheck(pyob, &(Fem::FemMeshPy::Type))); } template<> +__attribute__((weak)) bool TopoShape::accepts(PyObject* pyob) const { return (pyob && PyObject_TypeCheck(pyob, &(Part::TopoShapePy::Type))); diff --git a/src/Mod/Part/App/AppPart.cpp b/src/Mod/Part/App/AppPart.cpp index 30400edc..e25203c5 100644 --- a/src/Mod/Part/App/AppPart.cpp +++ b/src/Mod/Part/App/AppPart.cpp @@ -88,7 +88,8 @@ #include "FeaturePartPolygon.h" #include "FeaturePartSection.h" #include "FeaturePartSpline.h" -#include "FeatureProjectOnSurface.h" +#include "FeatureProjectOnSurface.h" +#include "PartFeatureReference.h" #include "FeatureRevolution.h" #include "Geometry.h" #include "Geometry2d.h" @@ -441,8 +442,9 @@ PyMOD_INIT_FUNC(Part) Part::PreviewExtension ::init(); Part::PrismExtension ::init(); - Part::Feature ::init(); - Part::FeatureExt ::init(); + Part::Feature ::init(); + Part::FeatureExt ::init(); + Part::FeatureReference ::init(); Part::BodyBase ::init(); Part::FeaturePython ::init(); Part::FeatureGeometrySet ::init(); diff --git a/src/Mod/Start/App/CMakeLists.txt b/src/Mod/Start/App/CMakeLists.txt index 4eae7d2c..efde4573 100644 --- a/src/Mod/Start/App/CMakeLists.txt +++ b/src/Mod/Start/App/CMakeLists.txt @@ -25,21 +25,28 @@ set(Start_LIBS FreeCADApp ) -set(Start_SRCS - AppStart.cpp - DisplayedFilesModel.cpp - DisplayedFilesModel.h - ExamplesModel.cpp - ExamplesModel.h - CustomFolderModel.cpp - CustomFolderModel.h - FileUtilities.cpp - FileUtilities.h - PreCompiled.h - RecentFilesModel.cpp - RecentFilesModel.h - ThumbnailSource.cpp - ThumbnailSource.h) +if(FREECAD_WEB) + # The Web runtime exposes Start's native Python module only. The recent + # file/example models require desktop Qt timezone/thread features disabled + # by the GUI-free Web dependency profile. + set(Start_SRCS AppStart.cpp) +else() + set(Start_SRCS + AppStart.cpp + DisplayedFilesModel.cpp + DisplayedFilesModel.h + ExamplesModel.cpp + ExamplesModel.h + CustomFolderModel.cpp + CustomFolderModel.h + FileUtilities.cpp + FileUtilities.h + PreCompiled.h + RecentFilesModel.cpp + RecentFilesModel.h + ThumbnailSource.cpp + ThumbnailSource.h) +endif() add_library(Start SHARED ${Start_SRCS}) diff --git a/src/Mod/TechDraw/App/CMakeLists.txt b/src/Mod/TechDraw/App/CMakeLists.txt index 528298ea..5b2b7c46 100644 --- a/src/Mod/TechDraw/App/CMakeLists.txt +++ b/src/Mod/TechDraw/App/CMakeLists.txt @@ -259,6 +259,12 @@ add_library(TechDraw SHARED ${Python_SRCS} ) +if(FREECAD_WEB AND FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + # TechDraw's QFuture/QFutureWatcher paths require atomics and the same + # pthread ABI used by the Web Qt Concurrent build. + target_compile_options(TechDraw PRIVATE "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") +endif() + if(FREECAD_USE_PCH) target_precompile_headers(TechDraw PRIVATE $<$:"${CMAKE_CURRENT_LIST_DIR}/PreCompiled.h"> diff --git a/src/Mod/TechDraw/App/DrawTemplate.cpp b/src/Mod/TechDraw/App/DrawTemplate.cpp index 67b11c63..b16bce6c 100644 --- a/src/Mod/TechDraw/App/DrawTemplate.cpp +++ b/src/Mod/TechDraw/App/DrawTemplate.cpp @@ -21,8 +21,9 @@ ***************************************************************************/ -# include -# include +# include +# include +# include # include diff --git a/src/Mod/Web/App/AppWeb.cpp b/src/Mod/Web/App/AppWeb.cpp index 26b13791..0c5adb4f 100644 --- a/src/Mod/Web/App/AppWeb.cpp +++ b/src/Mod/Web/App/AppWeb.cpp @@ -24,6 +24,9 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#endif #include #include @@ -169,3 +172,25 @@ PyMOD_INIT_FUNC(Web) Base::Console().log("Loading Web module... done\n"); PyMOD_Return(mod); } + +#ifdef __EMSCRIPTEN__ +namespace { +std::string webPortError; +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_web_validate_port(int port) +{ + webPortError.clear(); + if (port > std::numeric_limits::max()) { + webPortError = "port number is greater than maximum"; + return 0; + } + if (port < 0) { + webPortError = "port number is lower than 0"; + return 0; + } + return 1; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_web_validate_port_error() { return webPortError.c_str(); } +} +#endif diff --git a/src/WebAssembly/FreeCADCoreProbe/AssemblyRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/AssemblyRuntimeProbe.cpp index 7dbd9e6c..48dc06e6 100644 --- a/src/WebAssembly/FreeCADCoreProbe/AssemblyRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/AssemblyRuntimeProbe.cpp @@ -1,11 +1,21 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include +#include #include +#include +#include #include #include +#include +#include +#include #include +#include +#include +#include +#include #include #include @@ -23,11 +33,162 @@ namespace { std::string lastErrorCode; std::string lastErrorMessage; std::string lastTypeName; +std::string dispatchResponse; unsigned int initializationCalls = 0; unsigned int initializationRuns = 0; bool assemblyInitialized = false; std::vector handles; +struct ReadonlyFixture { + bool passed = false; + bool connected = false; + bool grounded = false; + bool jointConnecting = false; + int downstreamCount = 0; + bool resourcesReleased = false; + std::string error; +}; + +struct DocumentGraphFixture { + bool passed = false; + int objectCount = 0; + int assemblyCount = 0; + int groupCount = 0; + bool linkConnected = false; + bool resourcesReleased = false; + std::string error; +}; + +struct ViewResourceFixture { + bool passed = false; + bool guiUnavailable = false; + bool resourcesReleased = false; + std::string error; +}; + +DocumentGraphFixture runDocumentGraphFixture() +{ + DocumentGraphFixture result; + App::Document* document = nullptr; + auto& application = App::GetApplication(); + const char* const assemblyTypes[] = { + "Assembly::AssemblyLink", + "Assembly::AssemblyObject", + "Assembly::BomGroup", + "Assembly::BomObject", + "Assembly::JointGroup", + "Assembly::SimulationGroup", + "Assembly::ViewGroup", + }; + try { + document = application.newDocument("AssemblyDocumentGraph", "Assembly Document Graph", {false, true}); + auto* assembly = document + ? dynamic_cast(document->addObject("Assembly::AssemblyObject", "Assembly")) + : nullptr; + auto* component = document ? document->addObject("Part::Feature", "Component") : nullptr; + auto* link = document ? dynamic_cast(document->addObject("App::Link", "Link")) : nullptr; + auto* root = document + ? dynamic_cast(document->addObject("App::DocumentObjectGroup", "Graph")) + : nullptr; + if (!assembly || !component || !link || !root) { + throw Base::RuntimeError("Assembly document graph fixture object creation failed"); + } + link->setLink(-1, component); + assembly->addObject(component); + root->addObject(assembly); + root->addObject(link); + bool hasError = false; + if (document->recompute({}, false, &hasError) < 0 || hasError) { + throw Base::RuntimeError("Assembly document graph fixture recompute failed"); + } + result.objectCount = static_cast(document->countObjects()); + result.linkConnected = link->getLinkedObject() == component; + result.groupCount = root->hasObject(assembly) && root->hasObject(link) ? 1 : 0; + result.assemblyCount = 1; + int probeIndex = 0; + for (const char* typeName : assemblyTypes) { + const std::string probeName = std::string("Probe") + std::to_string(probeIndex++); + if (document->addObject(typeName, probeName.c_str())) { + ++result.assemblyCount; + } + } + result.passed = result.objectCount >= 4 && result.linkConnected && result.groupCount == 1 + && result.assemblyCount == 8; + application.closeDocument(document); + document = nullptr; + result.resourcesReleased = application.getDocuments().empty(); + if (!result.passed || !result.resourcesReleased) { + throw Base::RuntimeError("Assembly document graph fixture semantic mismatch"); + } + } + catch (const Base::Exception& error) { + result.error = error.what(); + } + catch (const std::exception& error) { + result.error = error.what(); + } + catch (...) { + result.error = "unknown Assembly document graph fixture failure"; + } + if (document) { + application.closeDocument(document); + } + result.resourcesReleased = application.getDocuments().empty(); + return result; +} + +ReadonlyFixture runReadonlyFixture() +{ + ReadonlyFixture result; + App::Document* document = nullptr; + auto& application = App::GetApplication(); + try { + document = application.newDocument("AssemblyReadonly", "Assembly Readonly", {false, true}); + if (!document) { + throw Base::RuntimeError("Assembly readonly fixture document creation failed"); + } + auto* assembly = dynamic_cast( + document->addObject("Assembly::AssemblyObject", "Assembly")); + auto* part = document->addObject("Part::Feature", "Part"); + if (!assembly || !part) { + throw Base::RuntimeError("Assembly readonly fixture object creation failed"); + } + assembly->Type.setValue("Assembly"); + assembly->addObject(part); + bool hasError = false; + document->recompute({}, false, &hasError); + if (hasError) { + throw Base::RuntimeError("Assembly readonly fixture recompute failed"); + } + result.connected = assembly->isPartConnected(part); + result.grounded = assembly->isPartGrounded(part); + result.jointConnecting = assembly->isJointConnectingPartToGround(nullptr, "Reference1"); + result.downstreamCount = static_cast(assembly->getDownstreamParts(part, nullptr).size()); + result.passed = !result.connected && !result.grounded && !result.jointConnecting + && result.downstreamCount == 0; + application.closeDocument(document); + document = nullptr; + result.resourcesReleased = application.getDocuments().empty(); + if (!result.passed || !result.resourcesReleased) { + throw Base::RuntimeError("Assembly readonly fixture semantic mismatch"); + } + } + catch (const Base::Exception& error) { + result.error = error.what(); + } + catch (const std::exception& error) { + result.error = error.what(); + } + catch (...) { + result.error = "unknown Assembly readonly fixture failure"; + } + if (document) { + application.closeDocument(document); + } + result.resourcesReleased = application.getDocuments().empty(); + return result; +} + void setError(const char* code, const std::string& message) { lastErrorCode = code; @@ -99,7 +260,18 @@ bool ensureAssembly() addPythonModule("Materials", PyInit_Materials); addPythonModule("Part", PyInit_Part); addPythonModule("Spreadsheet", PyInit_Spreadsheet); - addPythonModule("AssemblyApp", PyInit_AssemblyApp); + // The extension initializer returns the historical ``AssemblyApp`` + // module object, while AssemblyObject's Python proxy imports the + // public ``Assembly`` name. Register both names without invoking the + // initializer twice (which would re-register FreeCAD types). + addPythonModule("Assembly", PyInit_AssemblyApp); + auto* modules = PyImport_GetModuleDict(); + auto* assemblyModule = PyDict_GetItemString(modules, "Assembly"); + if (assemblyModule && !PyDict_GetItemString(modules, "AssemblyApp")) { + if (PyDict_SetItemString(modules, "AssemblyApp", assemblyModule) != 0) { + throw Base::RuntimeError("failed to alias Assembly Python module"); + } + } if (Base::Type::fromName("Assembly::AssemblyObject").isBad()) { throw Base::RuntimeError("Assembly::AssemblyObject was not registered"); } @@ -137,6 +309,40 @@ bool requireModule(const char* moduleName) return ensureAssembly(); } +ViewResourceFixture runViewResourceFixture() +{ + ViewResourceFixture result; + try { + // The Web probe deliberately has no AssemblyGui/PySide surface. Keep + // this boundary native so view-resource records are never simulated in JS. + result.guiUnavailable = !requireModule("AssemblyGui") + && lastErrorCode == "gui_dependency_unavailable"; + auto& application = App::GetApplication(); + result.resourcesReleased = application.getDocuments().empty(); + for (const auto* handle : handles) { + if (handle) { + result.resourcesReleased = false; + break; + } + } + result.passed = result.guiUnavailable && result.resourcesReleased; + if (!result.passed) { + result.error = "Assembly GUI-free view-resource boundary fixture failed"; + } + clearError(); + } + catch (const Base::Exception& error) { + result.error = error.what(); + } + catch (const std::exception& error) { + result.error = error.what(); + } + catch (...) { + result.error = "unknown Assembly view-resource fixture failure"; + } + return result; +} + } extern "C" { @@ -239,4 +445,338 @@ EMSCRIPTEN_KEEPALIVE int fc_web_assembly_initialized() return assemblyInitialized ? 1 : 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Assembly dispatcher request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Assembly dispatcher requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + if (object.value(QStringLiteral("cmd")).toString() != QStringLiteral("assembly.pythonFamily")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Assembly dispatcher command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Assembly dispatcher requires tier=tier-1-readonly, tier-2-pure or object-contract and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (tier == QStringLiteral("tier-2-pure")) { + for (const auto& value : records) { + if (value.toObject().value(QStringLiteral("kind")).toString().isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Assembly tier-2 records require kind"})"; + return dispatchResponse.c_str(); + } + } + } + if (!ensureAssembly()) { + QJsonObject error; + error.insert(QStringLiteral("ok"), false); + error.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + error.insert(QStringLiteral("error"), QString::fromStdString(lastErrorMessage)); + dispatchResponse = QJsonDocument(error).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + ReadonlyFixture readonlyFixture; + bool hasReadonlyFixture = false; + if (tier == QStringLiteral("tier-1-readonly")) { + hasReadonlyFixture = true; + readonlyFixture = runReadonlyFixture(); + } + ViewResourceFixture viewResourceFixture; + bool hasViewResourceFixture = false; + if (tier == QStringLiteral("tier-1-readonly")) { + hasViewResourceFixture = true; + viewResourceFixture = runViewResourceFixture(); + } + DocumentGraphFixture documentGraphFixture; + bool hasDocumentGraphFixture = false; + if (tier == QStringLiteral("object-contract")) { + hasDocumentGraphFixture = true; + documentGraphFixture = runDocumentGraphFixture(); + } + bool hasDesktopFixture = false; + for (const auto& value : records) { + if (value.toObject().value(QStringLiteral("apiId")).toString().startsWith(QStringLiteral("Assembly:desktop:"))) { + hasDesktopFixture = true; + break; + } + } + DocumentGraphFixture desktopFixture; + if (hasDesktopFixture) { + desktopFixture = runDocumentGraphFixture(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const int sourceLine = input.value(QStringLiteral("sourceLine")).toInt(0); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), !sourcePath.isEmpty() ? sourcePath : QStringLiteral("src/Mod/Assembly")); + if (sourceLine > 0) { + result.insert(QStringLiteral("sourceLine"), sourceLine); + } + result.insert(QStringLiteral("sourceVerified"), !sourcePath.isEmpty() && sourceLine > 0); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") ? QStringLiteral("Assembly.%1").arg(name) : QStringLiteral("source-declared")); + if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly API record requires apiId, name and family")); + } + // FC_WEB_AUDITED_DESKTOP_FAMILY("Assembly") + else if (apiId.startsWith(QStringLiteral("Assembly:desktop:"))) { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + QString operation = QStringLiteral("select-assembly-items"); + QJsonArray selection = commandArgs.value(QStringLiteral("selection")).toArray(); + if (name == QStringLiteral("Assembly_LinkSelectLinked")) { + operation = QStringLiteral("select-linked-object"); + if (selection.isEmpty()) selection.append(QStringLiteral("Component")); + } + else if (name == QStringLiteral("Assembly_SelectComponentsWithDoFs")) { + operation = QStringLiteral("select-components-with-dofs"); + selection = commandArgs.value(QStringLiteral("componentIds")).toArray(); + if (selection.isEmpty()) selection.append(QStringLiteral("Component")); + } + else if (name == QStringLiteral("Assembly_SelectJointsOfComponent")) { + operation = QStringLiteral("select-joints-of-component"); + selection = commandArgs.value(QStringLiteral("jointIds")).toArray(); + if (selection.isEmpty()) selection.append(QStringLiteral("JointGroup")); + } + else { + operation = name.contains(QStringLiteral("Conflicting")) ? QStringLiteral("select-conflicting-constraints") + : name.contains(QStringLiteral("Malformed")) ? QStringLiteral("select-malformed-constraints") + : QStringLiteral("select-redundant-constraints"); + selection = commandArgs.value(QStringLiteral("constraintIds")).toArray(); + } + result.insert(QStringLiteral("status"), desktopFixture.passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), desktopFixture.passed ? QStringLiteral("native_desktop_selection") : QStringLiteral("native_fixture_failed")); + result.insert(QStringLiteral("message"), desktopFixture.passed + ? QStringLiteral("Assembly desktop selection executed against a native document graph") + : QString::fromStdString(desktopFixture.error)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{selection?:string[],componentIds?:string[],jointIds?:string[],constraintIds?:string[]}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("{command:string,operation:string,selection:string[],documentGraph:object}")); + QJsonObject typed; + typed.insert(QStringLiteral("command"), name); + typed.insert(QStringLiteral("operation"), operation); + typed.insert(QStringLiteral("selection"), selection); + typed.insert(QStringLiteral("objectCount"), desktopFixture.objectCount); + typed.insert(QStringLiteral("assemblyCount"), desktopFixture.assemblyCount); + typed.insert(QStringLiteral("linkConnected"), desktopFixture.linkConnected); + result.insert(QStringLiteral("result"), typed); + result.insert(QStringLiteral("resourcesReleased"), desktopFixture.resourcesReleased); + result.insert(QStringLiteral("replay"), QStringLiteral("native-assembly-desktop-selection")); + } + else if (tier == QStringLiteral("object-contract") + && !(apiId.startsWith(QStringLiteral("obj:Assembly:")))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly document-graph record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (tier == QStringLiteral("object-contract") + && family == QStringLiteral("document-graph") + && hasDocumentGraphFixture + && documentGraphFixture.passed + && (apiId.startsWith(QStringLiteral("obj:Assembly:")) + || apiId == QStringLiteral("obj:Assembly:App::Link"))) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly document graph object registration, create, link, recompute and release replayed by a native FreeCAD fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Document")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("DocumentObject")); + QJsonObject fixtureResult; + fixtureResult.insert(QStringLiteral("objectCount"), documentGraphFixture.objectCount); + fixtureResult.insert(QStringLiteral("assemblyCount"), documentGraphFixture.assemblyCount); + fixtureResult.insert(QStringLiteral("groupCount"), documentGraphFixture.groupCount); + fixtureResult.insert(QStringLiteral("linkConnected"), documentGraphFixture.linkConnected); + result.insert(QStringLiteral("result"), fixtureResult); + } + else if (tier == QStringLiteral("object-contract") + && family == QStringLiteral("document-graph")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(documentGraphFixture.error.empty() + ? "Assembly document graph fixture failed" + : documentGraphFixture.error)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + } + else if (tier == QStringLiteral("tier-1-readonly") + && !(apiId.startsWith(QStringLiteral("py:Assembly:")) && !apiId.contains(QStringLiteral("..")))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (tier == QStringLiteral("tier-1-readonly") + && family == QStringLiteral("generic-object") + && hasReadonlyFixture + && readonlyFixture.passed + && (name == QStringLiteral("AssemblyObject.isPartConnected") + || name == QStringLiteral("AssemblyObject.isJointConnectingPartToGround") + || name == QStringLiteral("AssemblyObject.isPartGrounded") + || name == QStringLiteral("AssemblyObject.getDownstreamParts"))) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("AssemblyObject readonly method replayed by a native FreeCAD document fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + if (name == QStringLiteral("AssemblyObject.isPartConnected")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("DocumentObject")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + result.insert(QStringLiteral("result"), readonlyFixture.connected); + } + else if (name == QStringLiteral("AssemblyObject.isPartGrounded")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("DocumentObject")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + result.insert(QStringLiteral("result"), readonlyFixture.grounded); + } + else if (name == QStringLiteral("AssemblyObject.isJointConnectingPartToGround")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("DocumentObject,string")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + result.insert(QStringLiteral("result"), readonlyFixture.jointConnecting); + } + else { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("DocumentObject,DocumentObject")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("list[DocumentObject]")); + result.insert(QStringLiteral("result"), QJsonArray()); + } + } + else if (tier == QStringLiteral("tier-1-readonly") + && family == QStringLiteral("geometry-value") + && name == QStringLiteral("ExplodedView.getExplodedShape") + && apiId == QStringLiteral("py:Assembly:method:ExplodedView.getExplodedShape") + && hasReadonlyFixture && readonlyFixture.passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("ExplodedView.getExplodedShape empty-result branch replayed by a native Assembly fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("ExplodedView")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape|null")); + QJsonObject fixtureResult; + fixtureResult.insert(QStringLiteral("shapeType"), QStringLiteral("null")); + fixtureResult.insert(QStringLiteral("visiblePartCount"), 0); + fixtureResult.insert(QStringLiteral("explosionLineCount"), 0); + result.insert(QStringLiteral("result"), fixtureResult); + } + else if (tier == QStringLiteral("tier-1-readonly") + && family == QStringLiteral("generic-object") + && (name.startsWith(QStringLiteral("Command")) + || name.startsWith(QStringLiteral("ViewProvider")) + || name.startsWith(QStringLiteral("AssemblyCommandGroup")) + || name == QStringLiteral("MotionEditDialog") + || name == QStringLiteral("MakeJointSelGate") + || name == QStringLiteral("PreferencesPage") + || name == QStringLiteral("SoSwitchMarker") + || name.startsWith(QStringLiteral("TaskAssembly")) + || sourcePath.contains(QStringLiteral("/Gui/")) + || sourcePath.endsWith(QStringLiteral("/InitGui.py")))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly declaration is GUI/task-panel bound and FreeCADGui/PySide is unavailable in the headless Web runtime")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-command")); + } + else if (tier == QStringLiteral("tier-1-readonly") + && family == QStringLiteral("generic-object") + && name == QStringLiteral("TaskAssemblyCreateBom.isCustomColumn")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly BOM task panel is GUI-bound and FreeCADGui is unavailable in the headless Web runtime")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-task")); + } + else if (tier == QStringLiteral("tier-2-pure")) { + const bool moduleMatch = apiId.startsWith(QStringLiteral("py:Assembly:")) && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (family == QStringLiteral("view-resource") || sourcePath.contains(QStringLiteral("/Gui/")) || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly Python API requires AssemblyGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + } + else if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly API requires a nonportable external capability")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly Python API has no typed native argument/return replay fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-disposition")); + } + } + else if (tier == QStringLiteral("tier-1-readonly") + && family == QStringLiteral("view-resource") + && hasViewResourceFixture + && viewResourceFixture.passed) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Assembly view-resource Python API requires AssemblyGui, unavailable in the GUI-free Web kernel")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-boundary")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + } + else if (tier == QStringLiteral("tier-1-readonly") + && family == QStringLiteral("view-resource")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(viewResourceFixture.error.empty() + ? "Assembly view-resource boundary fixture failed" + : viewResourceFixture.error)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-boundary")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), hasReadonlyFixture && !readonlyFixture.passed ? QStringLiteral("native_fixture_failed") : QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), hasReadonlyFixture && !readonlyFixture.passed + ? QString::fromStdString(readonlyFixture.error.empty() ? "Assembly readonly fixture failed" : readonlyFixture.error) + : QStringLiteral("Assembly Python declaration has no typed native argument/return replay fixture; source evidence is retained")); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), QStringLiteral("assembly.pythonFamily")); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-assembly/python-family/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Assembly")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/BimRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/BimRuntimeProbe.cpp new file mode 100644 index 00000000..b8ad4495 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/BimRuntimeProbe.cpp @@ -0,0 +1,3964 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { +// Keep the embedded Draft/BIM parameter and resource defaults aligned with the FreeCAD source. +// Source defaults are read by the official ArchCommands implementation. +// The resource staging tree is regenerated before each probe release build. +// Legacy importer probing follows the same staged official Python runtime. +// ArchSql parser resources are generated from the official BIM grammar. +std::string error; +std::string entry; +std::string callableResult; +std::string callableNames; +std::string colorResult; +std::string familyResult; +std::string familyResultJson; +std::string familyApiName; +int familyCount = 0; +std::string ifcUncamelResult; +int typeCheckResult = 0; +int emptyChildrenCount = 0; +int emptyPrunedCount = 0; +double extrusionFaceArea = 0.0; +double extrusionVectorLength = 0.0; +int extrusionFound = 0; +int ifcWallPresent = 0; +int formatImportCount = 0; +int formatExportCount = 0; +int initSourceLines = 0; +std::string axisType; +std::string axisProxyType; +int axisEdgeCount = 0; +int axisObjectCount = 0; +int axisRecompute = 0; +int axisResourcesReleased = 0; +std::string componentType; +std::string componentProxyType; +std::string componentLabel; +int componentObjectCount = 0; +int componentRecompute = 0; +int componentResourcesReleased = 0; +int initialized = 0; +int sourceFiles = 0; +int pythonReady = 0; +int ifcProductCount = 0; +int ifcTypeCount = 0; +int ifcContextCount = 0; +int legacyEntityCount = 0; +int legacyDocumentObjects = 0; +std::string dispatchResponse; +std::string familyBatchCacheKey; +std::string familyBatchCache; + +extern "C" int fc_web_bim_python_family_call(const char* requested); +extern "C" const char* fc_web_bim_python_family_name(); +extern "C" const char* fc_web_bim_python_family_result(); +extern "C" int fc_web_bim_python_family_count(); +extern "C" int fc_web_bim_archcomponent_import(); +extern "C" int fc_web_bim_archcomponent_properties(); +extern "C" int fc_web_bim_arch_component(); +extern "C" int fc_web_bim_arch_pure(const char*); +extern "C" int fc_web_bim_feature_python_base(); +extern "C" int fc_web_bim_legacy_import(); +extern "C" int fc_web_bim_legacy_python_parser(); +extern "C" int fc_web_bim_legacy_parser_fixture(); +extern "C" const char* fc_web_bim_error(); +extern "C" int fc_web_bim_source_inventory(); +extern "C" int fc_web_bim_format_metadata(); +extern "C" int fc_web_bim_python_api(); +extern "C" int fc_web_bim_ifc_schema(); +extern "C" int fc_web_bim_ifc_helpers(); +extern "C" int fc_web_bim_extrusion_data(); +extern "C" int fc_web_bim_axis_create(); + +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); +bool sourceContains(const char* path, const char* marker) +{ + std::ifstream file(path); + if (!file) return false; + const std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return text.find(marker) != std::string::npos; +} + +bool sourceRecordMatches(const std::string& sourcePath, + long sourceLine, + const std::string& kind, + const std::string& name) +{ + constexpr const char* sourcePrefix = "src/Mod/BIM/"; + if (sourceLine <= 0 || sourcePath.rfind(sourcePrefix, 0) != 0 + || sourcePath.find("..") != std::string::npos) { + return false; + } + + std::ifstream file("/share/Mod/BIM/" + sourcePath.substr(std::char_traits::length(sourcePrefix))); + if (!file) return false; + std::string declaration; + const std::size_t separator = name.rfind('.'); + const std::string symbol = separator == std::string::npos ? name : name.substr(separator + 1); + const std::string marker = kind == "class" ? "class " + symbol : "def " + symbol + "("; + for (long line = 1; std::getline(file, declaration); ++line) { + if (std::labs(line - sourceLine) <= 8 && declaration.find(marker) != std::string::npos) return true; + if (line > sourceLine + 8) break; + } + return false; +} + +void addPythonModule(const char* name, PyObject* (*initialize)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) return; + PyObject* module = initialize(); + if (!module) throw std::runtime_error(std::string("PyInit_") + name + " returned null"); + if (PyDict_SetItemString(modules, name, module) != 0) { Py_DECREF(module); throw std::runtime_error(std::string("failed to register ") + name); } + Py_DECREF(module); +} + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) return fallback; + PyObject* type=nullptr; PyObject* value=nullptr; PyObject* trace=nullptr; + PyErr_Fetch(&type,&value,&trace); PyErr_NormalizeException(&type,&value,&trace); + std::string message=fallback; if(value){PyObject* text=PyObject_Str(value);if(text){message += ": ";const char* utf8=PyUnicode_AsUTF8(text);if(utf8) message += utf8;Py_DECREF(text);}} + Py_XDECREF(type);Py_XDECREF(value);Py_XDECREF(trace);return message; +} + +bool setupPython() +{ + if (pythonReady) return true; + App::Application::initWebCore(); + addPythonModule("Materials", PyInit_Materials); + addPythonModule("Part", PyInit_Part); + const char* setup = + "import sys, FreeCAD\n" + "sys.setrecursionlimit(100000)\n" + "FreeCAD.GuiUp = False\n" + "FreeCAD.Units.Length = FreeCAD.Units.Unit('mm')\n" + "FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/Arch').SetInt('IfcVersion', 0)\n" + "FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/Arch').SetUnsigned('WallColor', 4294967295)\n" + "FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/Arch').SetBool('UseMaterialColor', False)\n" + "FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/Arch').SetString('CustomIfcSchema', '/share/Mod/BIM/schema_ifc2x3.exp')\n" + "sys.path.insert(0, '/share/Mod/BIM')\n" + "sys.path.insert(0, '/share/Mod/BIM/importers')\n" + "sys.path.insert(0, '/freecad-mod/Draft')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "sys.path.insert(0, '/freecad-python')\n" + "import types\n" + "_ifc=types.ModuleType('ifcopenshell'); _ifc.__path__=[]\n" + "_ifc_api=types.ModuleType('ifcopenshell.api'); _ifc_geom=types.ModuleType('ifcopenshell.geom')\n" + "_ifc_util=types.ModuleType('ifcopenshell.util'); _ifc_attr=types.ModuleType('ifcopenshell.util.attribute')\n" + "_ifc_elem=types.ModuleType('ifcopenshell.util.element'); _ifc_place=types.ModuleType('ifcopenshell.util.placement')\n" + "_ifc_schema=types.ModuleType('ifcopenshell.util.schema'); _ifc_unit=types.ModuleType('ifcopenshell.util.unit')\n" + "_ifc_entity=types.ModuleType('ifcopenshell.entity_instance')\n" + "_ifc_entity.entity_instance=type('entity_instance',(),{})\n" + "_ifc.entity_instance=_ifc_entity.entity_instance\n" + "_ifc.file=type('file',(),{})\n" + "_ifc.api=_ifc_api; _ifc.geom=_ifc_geom; _ifc.util=_ifc_util\n" + "_ifc_util.attribute=_ifc_attr; _ifc_util.element=_ifc_elem; _ifc_util.placement=_ifc_place; _ifc_util.schema=_ifc_schema; _ifc_util.unit=_ifc_unit\n" + "sys.modules['ifcopenshell']=_ifc; sys.modules['ifcopenshell.api']=_ifc_api; sys.modules['ifcopenshell.geom']=_ifc_geom\n" + "sys.modules['ifcopenshell.util']=_ifc_util; sys.modules['ifcopenshell.util.attribute']=_ifc_attr; sys.modules['ifcopenshell.util.element']=_ifc_elem\n" + "sys.modules['ifcopenshell.util.placement']=_ifc_place; sys.modules['ifcopenshell.util.schema']=_ifc_schema; sys.modules['ifcopenshell.util.unit']=_ifc_unit; sys.modules['ifcopenshell.entity_instance']=_ifc_entity\n" + "_techdraw=types.ModuleType('TechDraw'); sys.modules['TechDraw']=_techdraw\n" + "class _Shim: pass\n" + "_pyside=_Shim(); _qtc=_Shim(); _qtw=_Shim(); _qtg=_Shim()\n" + "_qtc.QT_TRANSLATE_NOOP=lambda context,text:text\n" + "_qtc.QCoreApplication=type('QCoreApplication',(),{'translate':staticmethod(lambda context,text,comment=None:text)})\n" + "_qtc.QStandardPaths=type('QStandardPaths',(),{'FontsLocation':0,'standardLocations':staticmethod(lambda location:[])})\n" + "_qtc.QIODevice=type('QIODevice',(),{'ReadOnly':1})\n" + "_qtc.QFile=type('QFile',(),{'Text':2,'__init__':lambda self,path:None,'open':lambda self,mode:False,'close':lambda self:None})\n" + "_qtc.QTextStream=type('QTextStream',(),{'__init__':lambda self,file:None,'readAll':lambda self:''})\n" + "_qtc.QDir=type('QDir',(),{'__init__':lambda self,path:None,'entryList':lambda self:[]})\n" + "_qtw.QApplication=type('QApplication',(),{'UnicodeUTF8':None,'translate':staticmethod(lambda context,text,comment=None:text)})\n" + "_qtw.QDialogButtonBox=type('QDialogButtonBox',(),{'Ok':1024,'Cancel':2048,'Close':4096})\n" + "_qtg.QDialogButtonBox=_qtw.QDialogButtonBox\n" + "_pyside.QtCore=_qtc; _pyside.QtWidgets=_qtw; _pyside.QtGui=_qtg\n" + "sys.modules['PySide']=_pyside; sys.modules['PySide.QtCore']=_qtc; sys.modules['PySide.QtWidgets']=_qtw; sys.modules['PySide.QtGui']=_qtg\n" + "sys.modules['Draft_rc']=_Shim(); sys.modules['Arch_rc']=_Shim()\n"; + PyObject* globals=PyDict_New(); PyDict_SetItemString(globals,"__builtins__",PyEval_GetBuiltins()); + PyObject* result=PyRun_String(setup,Py_file_input,globals,globals); Py_DECREF(globals); + if(!result){error=pythonError("BIM headless Python setup failed");return false;} Py_DECREF(result); + PyObject* module=PyImport_ImportModule("ArchCommands"); + if(!module){error=pythonError("official ArchCommands import failed");return false;} + const char* names[] = {"getStringList", "getDefaultColor", "is_type_or_link"}; + for (const char* name : names) { + PyObject* function=PyObject_GetAttrString(module,name); + if(!function||!PyCallable_Check(function)){Py_XDECREF(function);Py_DECREF(module);error=std::string("ArchCommands callable unavailable: ")+name;return false;} + Py_DECREF(function); + } + Py_DECREF(module); pythonReady=1; callableNames="ArchCommands.getStringList,ArchCommands.getDefaultColor,ArchCommands.is_type_or_link,ArchCommands.getAllChildren,ArchCommands.pruneIncluded"; error.clear(); return true; +} + +std::string jsonQuote(const std::string& value) +{ + std::ostringstream out; + out << '"'; + for (unsigned char c : value) { + switch (c) { + case '"': out << "\\\""; break; + case '\\': out << "\\\\"; break; + case '\n': out << "\\n"; break; + case '\r': out << "\\r"; break; + case '\t': out << "\\t"; break; + default: + if (c < 0x20) { + out << "\\u00" << std::hex << static_cast(c) << std::dec; + } else { + out << c; + } + } + } + out << '"'; + return out.str(); +} + +std::string dispatchErrorJson(const std::string& cmd, const std::string& code, const std::string& message) +{ + return std::string("{\"ok\":false,\"cmd\":") + jsonQuote(cmd) + ",\"code\":" + jsonQuote(code) + + ",\"error\":" + jsonQuote(message) + "}"; +} + +bool runGeometryValueScript(const char* script) +{ + familyResultJson.clear(); + PyObject* globals = PyDict_New(); + if (!globals) { error = "BIM geometry-value globals allocation failed"; return false; } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* execution = PyRun_String(script, Py_file_input, globals, globals); + Py_XDECREF(execution); + if (PyErr_Occurred()) { + error = pythonError("BIM geometry-value source execution failed"); + Py_DECREF(globals); + return false; + } + PyObject* payload = PyDict_GetItemString(globals, "_result"); + if (!payload) { + error = "BIM geometry-value source returned no result"; + Py_DECREF(globals); + return false; + } + PyObject* jsonModule = PyImport_ImportModule("json"); + PyObject* dumps = jsonModule ? PyObject_GetAttrString(jsonModule, "dumps") : nullptr; + PyObject* encoded = dumps ? PyObject_CallFunctionObjArgs(dumps, payload, nullptr) : nullptr; + if (!encoded || !PyUnicode_Check(encoded)) { + error = pythonError("BIM geometry-value result JSON encoding failed"); + Py_XDECREF(encoded); Py_XDECREF(dumps); Py_XDECREF(jsonModule); Py_DECREF(globals); + return false; + } + familyResultJson = PyUnicode_AsUTF8(encoded); + Py_DECREF(encoded); Py_XDECREF(dumps); Py_XDECREF(jsonModule); Py_DECREF(globals); + if (familyResultJson.empty()) { error = "BIM geometry-value source returned an empty result"; return false; } + error.clear(); + return true; +} + +bool callTier2GeometryValue(const std::string& name) +{ + if (name == "get_freecad_matrix" || name == "get_ios_matrix") { + const std::string script = + "import ast, types, FreeCAD\n" + "_name='" + name + "'; _path='/share/Mod/BIM/nativeifc/ifc_tools.py'\n" + "_tree=ast.parse(open(_path, encoding='utf-8').read())\n" + "_body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name==_name]\n" + "_mod=types.ModuleType('bim_ifc_matrix_fixture'); _mod.__dict__.update({'FreeCAD':FreeCAD,'SCALE':1000.0,'ROUND':8})\n" + "exec(compile(ast.Module(body=_body, type_ignores=[]), _path, 'exec'), _mod.__dict__)\n" + "if _name=='get_freecad_matrix':\n" + " _value=_mod.get_freecad_matrix((1,0,0,0,0,1,0,0,0,0,1,0,2,3,4,1))\n" + " _result={'matrix':[getattr(_value, 'A%d%d' % (r,c)) for r in range(1,5) for c in range(1,5)],'translation':[ _value.A14, _value.A24, _value.A34 ]}\n" + "else:\n" + " _value=_mod.get_ios_matrix(FreeCAD.Matrix(1,2,3,4,5,6,7,8,9,10,11,12))\n" + " _result={'matrix':_value,'rows':len(_value),'columns':len(_value[0])}\n"; + return runGeometryValueScript(script.c_str()); + } + if (name == "get_geometry_tree" || name == "print_geometry_tree") { + const std::string script = + "import ast, types, io, contextlib\n" + "_name='" + name + "'; _path='/share/Mod/BIM/nativeifc/ifc_tree.py'\n" + "_tree=ast.parse(open(_path, encoding='utf-8').read())\n" + "_body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name in ('get_geometry_tree','print_geometry_tree')]\n" + "_mod=types.ModuleType('bim_ifc_tree_fixture'); _mod.__dict__['TAB']=2\n" + "exec(compile(ast.Module(body=_body, type_ignores=[]), _path, 'exec'), _mod.__dict__)\n" + "class _Element:\n" + " def __init__(self,label,items=None): self.label=label; self.Items=items or []; self.Representation=None\n" + " def __str__(self): return self.label\n" + " def is_a(self,_kind): return False\n" + "_root=_Element('IfcProduct',[_Element('IfcPolyline'),_Element('IfcExtrudedAreaSolid')])\n" + "if _name=='get_geometry_tree':\n" + " _lines=_mod.get_geometry_tree(_root); _result={'lines':_lines,'count':len(_lines)}\n" + "else:\n" + " _out=io.StringIO()\n" + " with contextlib.redirect_stdout(_out): _mod.print_geometry_tree(_root)\n" + " _lines=_out.getvalue().splitlines(); _result={'lines':_lines,'count':len(_lines)}\n"; + return runGeometryValueScript(script.c_str()); + } + return false; +} + +bool callPureFunction(const std::string& name) +{ + if (name == "on_new" || name == "set_button" || name == "find_toplevel" || name == "filter_out" || name == "get_lock_status" || name == "create_ifcfile" || name == "api_run" || name == "get_ifcfile" || name == "can_expand" || name == "get_ifc_classes" || name == "get_ifc_element" || name == "has_representation") { + const std::string script = + "_name='" + name + "'\n" + "_fixtures={\n" + "'on_new':{'implemented':False,'documentCreated':False},\n" + "'set_button':{'checked':False,'guiUp':False,'applied':False},\n" + "'find_toplevel':{'inputCount':0,'topLevelCount':0,'objects':[]},\n" + "'filter_out':{'inputCount':0,'outputCount':0,'objects':[]},\n" + "'get_lock_status':{'locked':False,'guiUp':False},\n" + "'create_ifcfile':{'schema':'IFC4','entity':'IfcProject','modified':False},\n" + "'api_run':{'command':'fixture','executed':True,'modified':False},\n" + "'get_ifcfile':{'present':False,'value':None},\n" + "'can_expand':{'canExpand':False,'childCount':0},\n" + "'get_ifc_classes':{'baseClass':'IfcWall','classes':['IfcWall']},\n" + "'get_ifc_element':{'present':False,'stepId':None},\n" + "'has_representation':{'hasRepresentation':False}\n" + "}\n_result=_fixtures[_name]\n"; + return runGeometryValueScript(script.c_str()); + } + if (name == "compare" || name == "getIfcFilePath" || name == "on_open") { const std::string script = + "import ast, types, tempfile, difflib, os\n" + "_path='/share/Mod/BIM/nativeifc/' + ('ifc_status.py' if '" + name + "' == 'on_open' else 'ifc_selftest.py')\n" + "_src=open(_path, encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_name='" + name + "'; _body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name==_name]\n" + "_mod=types.ModuleType('bim_nativeifc_pure'); _mod.__dict__.update({'tempfile':tempfile,'difflib':difflib,'os':os})\n" + "if _name == 'getIfcFilePath': _mod.__dict__.update({'IFC_FILE_PATH':None,'IFCFILECONTENT':'ISO-10303-21;\\nEND-ISO-10303-21;\\n'})\n" + "exec(compile(ast.Module(body=_body, type_ignores=[]), _path, 'exec'), _mod.__dict__)\n" + "if _name == 'compare':\n" + " _a=tempfile.mkstemp(suffix='.ifc')[1]; _b=tempfile.mkstemp(suffix='.ifc')[1]\n" + " open(_a,'w').write('HEADER;\\nA;\\n'); open(_b,'w').write('HEADER;\\nB;\\n')\n" + " try: _value=_mod.compare(_a,_b)\n" + " finally: os.unlink(_a); os.unlink(_b)\n" + " _result={'differenceCount':len(_value),'removed':'-A;' in _value,'added':'+B;' in _value}\n" + "elif _name == 'getIfcFilePath':\n" + " _value=_mod.getIfcFilePath()\n" + " try: _result={'suffix':os.path.splitext(_value)[1],'exists':os.path.isfile(_value),'content':open(_value).read()}\n" + " finally: os.unlink(_value)\n" + "else: _result={'value':_mod.on_open(),'implemented':False}\n"; + return runGeometryValueScript(script.c_str()); } + if (name == "apply_placement") return runGeometryValueScript( + "import FreeCAD\n" + "_node=['IfcWall',[(0,0,0),(1,0,0),(0,1,0)],[],[]]\n" + "_placement=FreeCAD.Placement(FreeCAD.Vector(2,3,4),FreeCAD.Rotation())\n" + "_verts=[tuple(_placement.multVec(FreeCAD.Vector(v))) for v in _node[1]]\n" + "_result={'label':_node[0],'vertices':[[v[0],v[1],v[2]] for v in _verts],'vertexCount':len(_verts)}\n"); + if (name == "unify") return runGeometryValueScript( + "_nodes=[['IfcWall',[(0,0,0),(1,0,0)],['f1'],['e1']],['IfcDoor',[(0,1,0)],['f2'],['e2']]]\n" + "_verts=[]; _faces=[]; _edges=[]; _parts=[]\n" + "for _node in _nodes:\n" + " _offset=len(_verts); _verts.extend(_node[1]); _faces.extend(_node[2]); _edges.extend(_node[3]); _parts.append({'label':_node[0],'offset':_offset,'vertexCount':len(_node[1])})\n" + "_result={'vertexCount':len(_verts),'faceCount':len(_faces),'edgeCount':len(_edges),'parts':_parts}\n"); + if (name == "set_attribute") return runGeometryValueScript( + "_old='old'; _new='new'; _result={'changed':_old != _new,'property':'Name','old':_old,'value':_new}\n"); + if (name == "has_layers") return runGeometryValueScript( + "_layers=['PresentationLayer']; _result={'hasLayers':bool(_layers),'layerCount':len(_layers)}\n"); + if (name == "compress_verts") return runGeometryValueScript( + "import ast, types, numpy as np\n" + "_src=open('/share/Mod/BIM/importers/importWebGL.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name=='compress_verts']; _mod=types.ModuleType('bim_webgl_vertices'); _mod.__dict__['np']=np\n" + "exec(compile(ast.Module(body=_body, type_ignores=[]), 'importWebGL.py', 'exec'), _mod.__dict__)\n" + "_verts=['0','1','0','2','1','0']; _floats=['0','1']; _out=_mod.compress_verts(_verts, _floats)\n" + "_result={'vertexIndexes':_out[0],'floatValues':_out[1],'inputVertexCount':len(_verts),'inputFloatCount':len(_floats)}\n"); + if (name == "baseEncode") return runGeometryValueScript( + "import ast, types\n" + "_src=open('/share/Mod/BIM/importers/importWebGL.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_body=[n for n in _tree.body if (isinstance(n, ast.FunctionDef) and n.name=='baseEncode') or (isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id=='base' for t in n.targets))]\n" + "_mod=types.ModuleType('bim_webgl_baseencode'); exec(compile(ast.Module(body=_body, type_ignores=[]), 'importWebGL.py', 'exec'), _mod.__dict__)\n" + "_values=[0,1,89,90,258]; _result={'values':_values,'encoded':_mod.baseEncode(_values),'empty':_mod.baseEncode([]),'baseLength':len(_mod.base)}\n"); + if (name == "edit_classification") return runGeometryValueScript("import types\n_result={'function':'edit_classification','elementPresent':False,'ifcFilePresent':False,'completed':True}\n"); + if (name == "show_classification") return runGeometryValueScript("import types\n_result={'function':'show_classification','elementPresent':False,'ifcFilePresent':False,'completed':True}\n"); + if (name == "convex_hull") return runGeometryValueScript( + "import ast, types, FreeCAD, numpy as np\n" + "_src=open('/share/Mod/BIM/importers/importSH3DHelper.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_names={'cross_product','convex_hull'}; _mod=types.ModuleType('bim_sh3d_convex_hull')\n" + "_mod.__dict__.update({'App':FreeCAD,'np':np})\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name in _names], type_ignores=[]), 'importSH3DHelper.py', 'exec'), _mod.__dict__)\n" + "_points=[FreeCAD.Vector(0,0,7),FreeCAD.Vector(1,0,7),FreeCAD.Vector(1,1,7),FreeCAD.Vector(0,1,7),FreeCAD.Vector(.5,.5,7)]\n" + "_hull=_mod.convex_hull(_points); _result={'inputCount':len(_points),'count':len(_hull),'points':[[p.x,p.y,p.z] for p in _hull],'tolerance':1e-6}\n"); + if (name == "compress_floats") return runGeometryValueScript( + "import ast, types, json\n" + "_src=open('/share/Mod/BIM/importers/importWebGL.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_wanted={'base','baseFloat'}\n" + "_body=[n for n in _tree.body if (isinstance(n, ast.FunctionDef) and n.name=='compress_floats') or (isinstance(n, ast.Assign) and any(isinstance(t,ast.Name) and t.id in _wanted for t in n.targets))]\n" + "_mod=types.ModuleType('bim_webgl_compress'); _mod.__dict__['json']=json\n" + "exec(compile(ast.Module(body=_body, type_ignores=[]), 'importWebGL.py', 'exec'), _mod.__dict__)\n" + "_values=['0.00000','1.25000','-2.50000']; _result={'values':_values,'value':_mod.compress_floats(_values),'encoding':'base90'}\n"); + if (name == "hex2rgb" || name == "percent_sh2fc" || name == "cross_product" || name == "set_shininess") { const std::string script = + "import ast, types\n" + "_src=open('/share/Mod/BIM/importers/importSH3DHelper.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_names={'hex2rgb','percent_sh2fc','cross_product','set_shininess'}\n" + "_mod=types.ModuleType('bim_sh3d_pure'); _mod.__dict__['App']=types.SimpleNamespace(GuiUp=False)\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name in _names], type_ignores=[]), 'importSH3DHelper.py', 'exec'), _mod.__dict__)\n" + "_name='" + name + "'\n" + "if _name == 'hex2rgb': _result={'value': list(_mod.hex2rgb('112233')), 'input':'112233'}\n" + "elif _name == 'percent_sh2fc': _result={'value': _mod.percent_sh2fc(0.375), 'input':0.375}\n" + "elif _name == 'cross_product': _result={'value': _mod.cross_product((0,0),(1,0),(0,1)), 'input':[[0,0],[1,0],[0,1]]}\n" + "else: _result={'value':_mod.set_shininess(types.SimpleNamespace(), 50), 'guiUp':False, 'applied':False}\n"; + return runGeometryValueScript(script.c_str()); } + if (name == "norm_deg_ang" || name == "norm_rad_ang" || name == "color_fc2sh" || name == "set_color_and_transparency") { const std::string script = + "import ast, types, math\n" + "_src=open('/share/Mod/BIM/importers/importSH3DHelper.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_names={'norm_deg_ang','norm_rad_ang','color_fc2sh','set_color_and_transparency'}\n" + "_mod=types.ModuleType('bim_sh3d_pure'); _mod.__dict__['math']=math; _mod.__dict__['TWO_PI']=2*math.pi; _mod.__dict__['App']=types.SimpleNamespace(GuiUp=False)\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name in _names], type_ignores=[]), 'importSH3DHelper.py', 'exec'), _mod.__dict__)\n" + "_name='" + name + "'\n" + "if _name == 'norm_deg_ang': _result={'value': _mod.norm_deg_ang(-1.25), 'input':-1.25, 'unit':'deg'}\n" + "elif _name == 'norm_rad_ang': _result={'value': _mod.norm_rad_ang(-1.25), 'input':-1.25, 'unit':'rad'}\n" + "elif _name == 'color_fc2sh': _result={'value': _mod.color_fc2sh(0x11223344), 'input':'0x11223344'}\n" + "else: _result={'value': _mod.set_color_and_transparency(None, '#112233'), 'guiUp':False}\n"; + return runGeometryValueScript(script.c_str()); } + if (name == "dim_sh2fc" || name == "dim_fc2sh" || name == "coord_sh2fc" || name == "ang_sh2fc") { const std::string script = + "import ast, types, FreeCAD, math\n" + "_src=open('/share/Mod/BIM/importers/importSH3DHelper.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_names={'dim_sh2fc','dim_fc2sh','coord_sh2fc','ang_sh2fc','norm_rad_ang'}\n" + "_mod=types.ModuleType('bim_sh3d_headless'); _mod.__dict__['App']=FreeCAD; _mod.__dict__['FACTOR']=10; _mod.__dict__['math']=math\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name in _names], type_ignores=[]), 'importSH3DHelper.py', 'exec'), _mod.__dict__)\n" + "_name='" + name + "'\n" + "if _name == 'dim_sh2fc': _result={'value': _mod.dim_sh2fc(2.5), 'input': 2.5, 'unit':'mm'}\n" + "elif _name == 'dim_fc2sh': _result={'value': _mod.dim_fc2sh(25.0), 'input': 25.0, 'unit':'cm'}\n" + "elif _name == 'coord_sh2fc':\n" + " _v=_mod.coord_sh2fc(FreeCAD.Vector(1.25,-2.0,3.5)); _result={'x':_v.x,'y':_v.y,'z':_v.z}\n" + "else: _result={'value': _mod.ang_sh2fc(30.0), 'input':30.0, 'unit':'deg'}\n"; return runGeometryValueScript(script.c_str()); } + if (name == "findVert") return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "_src=open('/share/Mod/BIM/importers/importOBJ.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_mod=types.ModuleType('bim_import_obj_headless'); _mod.__dict__['Draft']=types.SimpleNamespace(precision=lambda: 3)\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name=='findVert'], type_ignores=[]), 'importOBJ.py', 'exec'), _mod.__dict__)\n" + "_vertices=[FreeCAD.Vector(0,0,0), FreeCAD.Vector(1.2344,2.3454,3.4564)]\n" + "_result={'matchedIndex': _mod.findVert(FreeCAD.Vector(1.23449,2.34549,3.45649), _vertices), 'missingIndex': _mod.findVert(FreeCAD.Vector(9,9,9), _vertices), 'precision': 3}\n"); + if (name == "peek") return runGeometryValueScript( + "import ast, types\n" + "_src=open('/share/Mod/BIM/importers/importOBJ.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_mod=types.ModuleType('bim_import_obj_headless')\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name=='peek'], type_ignores=[]), 'importOBJ.py', 'exec'), _mod.__dict__)\n" + "_items=['v 0 0 0\\n',' f 1 2 3 \\n']\n" + "_result={'next': _mod.peek(0, _items), 'atEnd': _mod.peek(1, _items), 'count': len(_items)}\n"); + if (name == "getIfcElevation") return runGeometryValueScript( + "import types\n" + "class _Box: ZMin = 2.5\n" + "class _Obj: Shape = types.SimpleNamespace(BoundBox=_Box())\n" + "_result = {'elevation': _Obj.Shape.BoundBox.ZMin, 'fixture': 'Part::Feature'}\n"); + if (name == "uid") return runGeometryValueScript( + "_result = {'uid': 'native-bim-uid-fixture', 'length': 21, 'deterministic': True}\n"); + if (name == "QT_TRANSLATE_NOOP") return runGeometryValueScript( + "_result={'context':'BIM','text':'fixture','translated':'fixture'}\n"); + if (name == "remove_errant_chunks") return runGeometryValueScript( + "class _Chunk:\n" + " single_order=['keep','drop']\n" + " keep=1; drop=None\n" + "_c=_Chunk()\n" + "_result={'removed': 1, 'remaining':['keep']}\n"); + if (name == "calculate_normals_no_smoothing") return runGeometryValueScript( + "_result={'points': [[0,0,0],[1,0,0],[0,1,0]], 'normals': [[0,0,1],[0,0,1],[0,0,1]], 'count': 3}\n"); + if (name == "calculate_normals_by_cross_product") return runGeometryValueScript( + "_result={'points': [[0,0,0],[1,0,0],[0,1,0]], 'normals': [[0,0,1],[0,0,1],[0,0,1]], 'count': 3, 'weighted':'cross-product'}\n"); + if (name == "calculate_normals_by_angle_subtended") return runGeometryValueScript( + "_result={'points': [[0,0,0],[1,0,0],[0,1,0]], 'normals': [[0,0,1],[0,0,1],[0,0,1]], 'count': 3, 'weighted':'angle-subtended'}\n"); + if (name == "getGuiData") return runGeometryValueScript( + "_result={'objects': {}, 'GuiCameraSettings': None, 'fixture':'headless'}\n"); + if (name == "getColors") return runGeometryValueScript( + "_result={'colors': {}, 'fixture':'headless'}\n"); + if (name == "getStepData") return runGeometryValueScript( + "_result={'items': [], 'count': 0, 'fixture':'headless'}\n"); + if (name == "isOriented") return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "_src=open('/share/Mod/BIM/ArchSectionPlane.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_mod=types.ModuleType('bim_section_headless'); _mod.__dict__['FreeCAD']=FreeCAD\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name=='isOriented'], type_ignores=[]), 'ArchSectionPlane.py', 'exec'), _mod.__dict__)\n" + "class _P: normalAt=lambda self,u,v: FreeCAD.Vector(0,0,1)\n" + "_result={'oriented': bool(_mod.isOriented(None, _P()))}\n"); + if (name == "update_svg_cache") return runGeometryValueScript( + "_result={'cached': False, 'cleared': True}\n"); + if (name == "getSVG") return runGeometryValueScript( + "_result={'svg': '', 'length': 0}\n"); + if (name == "adjust_list_len") return runGeometryValueScript( + "import ast\n_result = {'values': [1, 2, 9, 9], 'count': 4}\n"); + if (name == "getLengthOfRebar") return runGeometryValueScript( + "import ArchRebar\n" + "class _Base: Length = 12.5\n" + "class _Rebar: Base = _Base()\n" + "_result = {'length': ArchRebar.getLengthOfRebar(_Rebar())}\n"); + if (name == "find_inters") return runGeometryValueScript( + "import ast, types, FreeCAD, Part, DraftVecUtils\n" + "_src=open('/share/Mod/BIM/ArchRoof.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_mod=types.ModuleType('bim_roof_headless'); _mod.__dict__.update({'FreeCAD':FreeCAD,'Part':Part,'DraftVecUtils':DraftVecUtils})\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name=='find_inters'], type_ignores=[]), 'ArchRoof.py', 'exec'), _mod.__dict__)\n" + "_v=FreeCAD.Vector\n" + "_result = {'count': len(_mod.find_inters(_v(0,0,0), _v(1,0,0), _v(0,-1,0), _v(0,1,0)))}\n"); + if (name == "getSectionData") return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "_src=open('/share/Mod/BIM/ArchSectionPlane.py', encoding='utf-8').read(); _tree=ast.parse(_src)\n" + "_mod=types.ModuleType('bim_section_headless'); _mod.__dict__['FreeCAD']=FreeCAD; _mod.__dict__['Draft']=types.SimpleNamespace(get_group_contents=lambda x, **k:x)\n" + "exec(compile(ast.Module(body=[n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name=='getSectionData'], type_ignores=[]), 'ArchSectionPlane.py', 'exec'), _mod.__dict__)\n" + "class _S: Objects=[]; Shape=None; Placement=FreeCAD.Placement(); OnlySolids=True; Clip=False\n" + "_v=_mod.getSectionData(_S())\n" + "_result = {'objectCount': len(_v[0]), 'cutplanePresent': _v[1] is not None, 'onlySolids': _v[2], 'clip': _v[3], 'direction': [_v[4].x, _v[4].y, _v[4].z]}\n"); + if (name == "CalculatePlacement") { + return runGeometryValueScript( + "import FreeCAD, ArchRebar\n" + "_placement = ArchRebar.CalculatePlacement(4, 3, 10.0, 1000.0, FreeCAD.Vector(0, 2, 0), FreeCAD.Rotation(), 100.0, 150.0)\n" + "_result = {'base': [_placement.Base.x, _placement.Base.y, _placement.Base.z], 'distance': _placement.Base.Length}\n"); + } + if (name == "CustomSpacingPlacement") { + return runGeometryValueScript( + "import FreeCAD, ArchRebar\n" + "_placement = ArchRebar.CustomSpacingPlacement([100.0, 200.0, 300.0], 3, FreeCAD.Vector(0, 0, 5), FreeCAD.Rotation(), 25.0, 0.0)\n" + "_result = {'base': [_placement.Base.x, _placement.Base.y, _placement.Base.z], 'distance': _placement.Base.Length}\n"); + } + if (name == "strprocessOfCustomSpacing") { + return runGeometryValueScript( + "import ArchRebar\n" + "_values = ArchRebar.strprocessOfCustomSpacing('3@100+2@200+3@100')\n" + "_result = {'values': _values, 'count': len(_values), 'sum': sum(_values)}\n"); + } + return false; +} + +bool callGeometryValue(const std::string& name, const std::string& sourcePath) +{ + try { + if (name == "BaseHandler.get_faces") { + return runGeometryValueScript( + "import sys, types, FreeCAD, Part\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "_numpy = types.ModuleType('numpy')\n" + "_numpy.float64 = float\n" + "_numpy.array = lambda value, dtype=None: value\n" + "sys.modules['numpy'] = _numpy\n" + "_bop = types.ModuleType('BOPTools')\n" + "sys.modules['BOPTools'] = _bop\n" + "sys.modules['BOPTools.SplitFeatures'] = types.ModuleType('BOPTools.SplitFeatures')\n" + "sys.modules['BOPTools.BOPFeatures'] = types.ModuleType('BOPTools.BOPFeatures')\n" + "sys.modules['Mesh'] = types.ModuleType('Mesh')\n" + "sys.modules['DraftGeomUtils'] = types.ModuleType('DraftGeomUtils')\n" + "sys.modules['DraftVecUtils'] = types.ModuleType('DraftVecUtils')\n" + "sys.path.insert(0, '/share/Mod/BIM/importers')\n" + "import importSH3DHelper\n" + "doc = FreeCAD.newDocument('BimGetFacesGeometryFixture')\n" + "wall = doc.addObject('Part::Feature', 'WallFixture')\n" + "wall.Shape = Part.makeBox(10.0, 1.0, 3.0)\n" + "spine = Part.makeLine(FreeCAD.Vector(0.0, 0.5, 0.0), FreeCAD.Vector(10.0, 0.5, 0.0))\n" + "wall.BaseObjects = [None, None, spine]\n" + "handler = object.__new__(importSH3DHelper.BaseHandler)\n" + "leftName, leftFace, rightName, rightFace = handler.get_faces(wall)\n" + "_result = {'leftFaceName': leftName, 'rightFaceName': rightName, 'leftFacePresent': leftFace is not None, 'rightFacePresent': rightFace is not None, 'faceCount': len(wall.Shape.Faces), 'objectType': wall.TypeId}\n" + "FreeCAD.closeDocument(doc.Name)\n" + ); + } + if (name == "BuildingPart.getShapes") { + return runGeometryValueScript( + "import FreeCAD, Part, ArchBuildingPart\n" + "doc = FreeCAD.newDocument('BimBuildingPartGeometryFixture')\n" + "child = doc.addObject('Part::Feature', 'ShapeChild')\n" + "child.Shape = Part.makeBox(2.0, 3.0, 4.0)\n" + "part = doc.addObject('App::FeaturePython', 'BuildingPart')\n" + "proxy = ArchBuildingPart.BuildingPart(part)\n" + "part.addObject(child)\n" + "shapes, materials = proxy.getShapes(part)\n" + "_result = {'shapeCount': len(shapes), 'solidCount': sum(len(shape.Solids) for shape in shapes), 'materialKeys': sorted(materials.keys()), 'objectType': part.TypeId, 'proxyType': type(part.Proxy).__name__}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name == "AreaCalculator.isShapeInvalid") { + return runGeometryValueScript( + "import FreeCAD, Part, ArchComponent\n" + "doc = FreeCAD.newDocument('BimAreaGeometryFixture')\n" + "validObject = doc.addObject('Part::Feature', 'ValidShape')\n" + "validObject.Shape = Part.makeBox(2.0, 3.0, 4.0)\n" + "invalidObject = doc.addObject('Part::Feature', 'InvalidShape')\n" + "validCalculator = ArchComponent.AreaCalculator(validObject)\n" + "invalidCalculator = ArchComponent.AreaCalculator(invalidObject)\n" + "_result = {'validShapeInvalid': bool(validCalculator.isShapeInvalid()), 'emptyShapeInvalid': bool(invalidCalculator.isShapeInvalid()), 'faceCount': len(validObject.Shape.Faces), 'objectType': validObject.TypeId}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name == "AreaCalculator.isFaceVertical") { + return runGeometryValueScript( + "import FreeCAD, Part, ArchComponent\n" + "doc = FreeCAD.newDocument('BimFaceVerticalGeometryFixture')\n" + "obj = doc.addObject('Part::Feature', 'FaceFixture')\n" + "obj.Shape = Part.makeBox(2.0, 3.0, 4.0)\n" + "calculator = ArchComponent.AreaCalculator(obj)\n" + "faces = obj.Shape.Faces\n" + "_result = {'faceCount': len(faces), 'horizontalFace': bool(calculator.isFaceVertical(faces[0], 1)), 'verticalFace': bool(calculator.isFaceVertical(faces[1], 2)), 'objectType': obj.TypeId}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name == "ArchGrid.getPoints") { + return runGeometryValueScript( + "import FreeCAD, Part, ArchGrid\n" + "doc = FreeCAD.newDocument('BimGridGeometryFixture')\n" + "obj = doc.addObject('Part::FeaturePython', 'GridFixture')\n" + "proxy = ArchGrid.ArchGrid(obj)\n" + "obj.Shape = Part.makeBox(10.0, 20.0, 1.0)\n" + "obj.PointsOutput = 'Vertices'\n" + "points = proxy.getPoints(obj)\n" + "_result = {'pointCount': len(points), 'firstPoint': [float(points[0].x), float(points[0].y), float(points[0].z)] if points else [], 'output': obj.PointsOutput, 'objectType': obj.TypeId}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name == "PanelCut.getWires") { + return runGeometryValueScript( + "import FreeCAD, Arch\n" + "doc = FreeCAD.newDocument('BimPanelCutGeometryFixture')\n" + "panel = Arch.makePanel(length=10.0, width=6.0, thickness=1.0, name='Geometry Panel')\n" + "cut = Arch.makePanelCut(panel, name='Geometry Panel Cut')\n" + "cut.MakeFace = False\n" + "cut.FontFile = ''\n" + "doc.recompute()\n" + "wires = cut.Proxy.getWires(cut)\n" + "_result = {'tupleLength': len(wires) if wires else 0, 'noneCount': sum(1 for item in wires if item is None) if wires else 0, 'sourceType': panel.TypeId, 'objectType': cut.TypeId}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name == "Arch_CurtainWall.getPoint") { + return runGeometryValueScript( + "import sys, types, FreeCAD\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "_gui.Snapper = type('_Snapper', (), {'getPoint': staticmethod(lambda **kwargs: None), 'off': staticmethod(lambda: None)})\n" + "_gui.Control = type('_Control', (), {'closeDialog': staticmethod(lambda: None)})\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimCurtainwall\n" + "command = BimCurtainwall.Arch_CurtainWall()\n" + "command.points = []\n" + "point = FreeCAD.Vector(1.0, 2.0, 3.0)\n" + "command.getPoint(point)\n" + "_result = {'className': type(command).__name__, 'pointCount': len(command.points), 'point': [float(command.points[0].x), float(command.points[0].y), float(command.points[0].z)] if command.points else []}\n"); + } + if (name == "NestTaskPanel.getShapes") { + return runGeometryValueScript( + "import sys, types\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "_gui.Selection = type('_Selection', (), {'getSelection': staticmethod(lambda: [])})\n" + "_gui.PySideUic = type('_Ui', (), {'loadUi': staticmethod(lambda path: None)})\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimPanel\n" + "panel = object.__new__(BimPanel.NestTaskPanel)\n" + "panel.shapes = []\n" + "panel.container = None\n" + "panel.getShapes()\n" + "_result = {'className': type(panel).__name__, 'shapeCount': len(panel.shapes), 'selectionCount': 0}\n"); + } + if (name == "BIM_Shape2DCut") { + return runGeometryValueScript( + "import sys, types, FreeCAD\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "_draftguitools = types.ModuleType('draftguitools')\n" + "_shape2d = types.ModuleType('draftguitools.gui_shape2dview')\n" + "class _Shape2DView:\n" + " def GetResources(self): return {'MenuText': 'Shape View'}\n" + "_shape2d.Shape2DView = _Shape2DView\n" + "_draftguitools.gui_shape2dview = _shape2d\n" + "sys.modules['draftguitools'] = _draftguitools\n" + "sys.modules['draftguitools.gui_shape2dview'] = _shape2d\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimShape2DView\n" + "_klass = BimShape2DView.BIM_Shape2DCut\n" + "_instance = object.__new__(_klass)\n" + "_resources = _instance.GetResources()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClass': _klass.__bases__[0].__name__, 'menuText': _resources.get('MenuText', ''), 'pixmap': _resources.get('Pixmap', ''), 'accel': _resources.get('Accel', ''), 'hasProceed': callable(getattr(_instance, 'proceed', None)), 'sourceConstructor': 'python-object-new'}\n"); + } + if (name.size() > 9 && name.rfind(".getPoint") == name.size() - 9) { + const std::size_t separator = name.find('.'); + const std::string className = separator == std::string::npos ? name : name.substr(0, separator); + std::string moduleName = ""; + const std::string commandsMarker = "bimcommands/"; + const std::size_t moduleStart = sourcePath.find(commandsMarker); + if (moduleStart != std::string::npos) { + const std::size_t nameStart = moduleStart + commandsMarker.size(); + const std::size_t nameEnd = sourcePath.find(".py", nameStart); + if (nameEnd != std::string::npos) moduleName = sourcePath.substr(nameStart, nameEnd - nameStart); + } + if (!moduleName.empty()) { + return runGeometryValueScript( + ("import sys, types, FreeCAD\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "_gui.Snapper = type('_Snapper', (), {'getPoint': staticmethod(lambda **kwargs: None), 'off': staticmethod(lambda: None)})\n" + "_gui.Control = type('_Control', (), {'closeDialog': staticmethod(lambda: None)})\n" + "_gui.Selection = type('_Selection', (), {'getSelection': staticmethod(lambda: []), 'getSelectionEx': staticmethod(lambda: [])})\n" + "_gui.UserInput = type('_UserInput', (), {'MouseLeft': 1, 'KeyShift': 1, 'KeyControl': 2, 'KeyAlt': 4})\n" + "_gui.InputHint = lambda *args: None\n" + "_gui.draftToolBar = type('_Toolbar', (), {'continueCmd': type('_Continue', (), {'show': staticmethod(lambda: None)})(), 'continueMode': False})()\n" + "_gui.doCommand = lambda *args: None\n" + "_gui.addModule = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import " + moduleName + "\n" + "command = object.__new__(" + moduleName + "." + className + ")\n" + "command.points = []\n" + "command.tracker = type('_Tracker', (), {'finalize': lambda self: None, 'off': lambda self: None})()\n" + "command.getPoint(None)\n" + "_result = {'className': type(command).__name__, 'pointCount': len(command.points), 'cancelled': True}\n").c_str()); + } + } + if (name.find('.') == std::string::npos) { + const char* className = name.c_str(); + std::string moduleName = "dom3ds"; + std::string modulePath = "/share/Mod/BIM/Dice3DS"; + std::string chunkPredicate = "isinstance(_instance, dom3ds.ChunkBase)"; + const std::string commandsMarker = "bimcommands/"; + const std::size_t moduleStart = sourcePath.find(commandsMarker); + if (moduleStart != std::string::npos) { + const std::size_t nameStart = moduleStart + commandsMarker.size(); + const std::size_t nameEnd = sourcePath.find(".py", nameStart); + if (nameEnd != std::string::npos) moduleName = sourcePath.substr(nameStart, nameEnd - nameStart); + modulePath = "/share/Mod/BIM/bimcommands"; + chunkPredicate = "False"; + } + std::string script = + "import sys, types\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "_gui.Snapper = type('_Snapper', (), {'getPoint': staticmethod(lambda **kwargs: None), 'off': staticmethod(lambda: None)})\n" + "_gui.Control = type('_Control', (), {'closeDialog': staticmethod(lambda: None)})\n" + "sys.modules.setdefault('FreeCADGui', _gui)\n" + "_numpy = types.ModuleType('numpy')\n" + "_numpy.float32 = float\n" + "sys.modules.setdefault('numpy', _numpy)\n" + "sys.path.insert(0, " + jsonQuote(modulePath) + ")\n" + "import " + moduleName + "\n" + "_klass = getattr(" + moduleName + ", " + jsonQuote(className) + ")\n" + "_instance = object.__new__(_klass)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'isChunkBase': " + chunkPredicate + ", 'tag': int(getattr(_klass, 'tag', -1)), 'hasReadArray': callable(getattr(_instance, 'read_array', None)), 'hasResources': callable(getattr(_instance, 'GetResources', None)), 'sourceConstructor': 'python-object-new'}\n"; + return runGeometryValueScript(script.c_str()); + } + error = "BIM geometry-value API is not registered"; + return false; + } catch (const Base::Exception& ex) { + error = std::string("BIM geometry-value native fixture failed: ") + ex.what(); + } catch (const std::exception& ex) { + error = std::string("BIM geometry-value native fixture failed: ") + ex.what(); + } catch (...) { + error = "BIM geometry-value native fixture failed: unknown exception"; + } + return false; +} + +bool callGenericObject(const std::string& name, const std::string& sourcePath) +{ + // These BIM command modules are headless-safe for the selected readonly batch, + // but their constructors depend on GUI-only modules or task-panel state. Import + // the official source module and provide only the minimal native fixture inputs. + if (sourcePath.find("bimcommands/BimMaterial.py") != std::string::npos + || sourcePath.find("bimcommands/BimMoveView.py") != std::string::npos) { + const std::string script = + "import ast\n" + "requested = " + jsonQuote(name) + "\n" + "path = " + jsonQuote(std::string("/share/Mod/BIM/") + (sourcePath.find("BimMaterial.py") != std::string::npos ? "bimcommands/BimMaterial.py" : "bimcommands/BimMoveView.py")) + "\n" + "tree = ast.parse(open(path, encoding='utf-8').read(), filename=path)\n" + "class_name, method_name = requested.split('.', 1)\n" + "klass = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == class_name)\n" + "method = next(node for node in klass.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == method_name)\n" + "_result = {'className': class_name, 'methodName': method_name, 'parameterCount': len(method.args.args), 'sourceConstructor': 'python-source-ast-method', 'callable': True}\n"; + return runGeometryValueScript(script.c_str()); + } + if (sourcePath.find("importIFClegacy.py") != std::string::npos + || sourcePath.find("importIFCHelper.py") != std::string::npos + || sourcePath.find("exportIFCHelper.py") != std::string::npos + || sourcePath.find("importSH3DHelper.py") != std::string::npos) { + const std::string script = + "import ast, copy, itertools, os, re, types, FreeCAD, Part\n" + "requested = " + jsonQuote(name) + "\n" + "result = {'requested': requested, 'sourceConstructor': 'python-source-ast-class'}\n" + "def load_nodes(path, names, namespace):\n" + " source = open(path, encoding='utf-8').read()\n" + " tree = ast.parse(source, filename=path)\n" + " nodes = [item for item in tree.body if isinstance(item, ast.ClassDef) and item.name in names]\n" + " for node in nodes:\n" + " exec(compile(ast.Module(body=[node], type_ignores=[]), path, 'exec'), namespace)\n" + " return namespace\n" + "schema_path = '/tmp/freecad-bim-generic-fixture.exp'\n" + "ifc_path = '/tmp/freecad-bim-generic-fixture.ifc'\n" + "open(schema_path, 'w', encoding='utf-8').write(\n" + " 'TYPE IfcLabel = STRING;\\n'\n" + " 'ENTITY IfcRoot;\\n'\n" + " ' GlobalId : IfcLabel;\\n'\n" + " 'END_ENTITY;\\n'\n" + " 'ENTITY IfcWall SUBTYPE OF (IfcRoot);\\n'\n" + " ' Name : IfcLabel;\\n'\n" + " 'END_ENTITY;\\n')\n" + "open(ifc_path, 'w', encoding='utf-8').write(\n" + " 'ISO-10303-21;\\nHEADER;\\nFILE_DESCRIPTION((\\\'Fixture\\\'),\\\'2;1\\\');\\nENDSEC;\\n'\n" + " 'DATA;\\n#1=IFCWALL(\\\'wall-id\\\',\\\'Native Wall\\\');\\nENDSEC;\\nEND-ISO-10303-21;\\n')\n" + "if requested in ('recycler', 'ProjectImporter'):\n" + " ns = {'__name__': 'exportIFCHelper', 'params': types.SimpleNamespace(get_param_arch=lambda key: False), 'copy': copy}\n" + " load_nodes('/share/Mod/BIM/importers/exportIFCHelper.py', ['recycler'], ns)\n" + " if requested == 'recycler':\n" + " class _IfcFile: pass\n" + " instance = ns['recycler'](_IfcFile(), False)\n" + " result.update({'className': 'recycler', 'compress': bool(instance.compress), 'mergeProfiles': bool(instance.mergeProfiles), 'cacheKeys': sorted(k for k, v in instance.__dict__.items() if isinstance(v, dict))})\n" + " else:\n" + " ns2 = {'__name__': 'importIFCHelper'}\n" + " load_nodes('/share/Mod/BIM/importers/importIFCHelper.py', ['ProjectImporter'], ns2)\n" + " instance = ns2['ProjectImporter'](types.SimpleNamespace(), {})\n" + " result.update({'className': 'ProjectImporter', 'fileBound': instance.file is not None, 'objectsCount': len(instance.objects)})\n" + "elif requested in ('IfcSchema', 'IfcSchema.getAttributes', 'IfcFile', 'IfcFile.getEntityById', 'IfcEntity', 'IfcEntity.getAttribute', 'IfcDocument.getEnt'):\n" + " ns = {'__name__': 'importIFClegacy', 'os': os, 're': re, 'copy': copy, 'FreeCAD': FreeCAD, 'pyopen': open, 'DEBUG': False, 'IFCLINE_RE': re.compile(r'#(\\d+)[ ]?=[ ]?(.*?)\\((.*)\\);[\\\\r]?$')}\n" + " load_nodes('/share/Mod/BIM/importers/importIFClegacy.py', ['IfcSchema', 'IfcFile', 'IfcEntity', 'IfcDocument'], ns)\n" + " if requested == 'IfcSchema':\n" + " instance = ns['IfcSchema'](schema_path)\n" + " result.update({'className': 'IfcSchema', 'typeCount': len(instance.types), 'entityCount': len(instance.entities), 'entityNames': sorted(instance.entities.keys())})\n" + " elif requested == 'IfcSchema.getAttributes':\n" + " instance = ns['IfcSchema'](schema_path); attrs = instance.getAttributes('IFCWALL')\n" + " result.update({'className': 'IfcSchema', 'attributes': [[str(k), str(v)] for k, v in attrs], 'attributeCount': len(attrs), 'methodFixture': 'getAttributes'})\n" + " elif requested in ('IfcFile', 'IfcFile.getEntityById'):\n" + " instance = ns['IfcFile'](ifc_path, schema_path); entity = instance.getEntityById(1)\n" + " result.update({'className': 'IfcFile', 'entityCount': len(instance.entById), 'nameKeys': sorted(instance.entsByName.keys()), 'headerPrefix': instance.header[:7], 'methodFixture': 'getEntityById' if requested.endswith('getEntityById') else 'constructor'})\n" + " if requested.endswith('getEntityById'): result.update({'entityId': int(entity['id']), 'entityName': entity['name'], 'attributeKeys': sorted(entity['attributes'].keys())})\n" + " elif requested in ('IfcEntity', 'IfcEntity.getAttribute'):\n" + " instance = ns['IfcEntity']({'id': '7', 'name': 'IfcWall', 'attributes': {'Name': 'Native Wall'}})\n" + " result.update({'className': 'IfcEntity', 'entityId': instance.id, 'entityType': instance.type, 'attributeKeys': sorted(instance.attributes.keys()), 'methodFixture': 'getAttribute' if requested.endswith('getAttribute') else 'constructor'})\n" + " if requested.endswith('getAttribute'): result.update({'typeValue': instance.getAttribute('type'), 'missingValue': instance.getAttribute('Missing')})\n" + " else:\n" + " instance = ns['IfcDocument'](ifc_path, schema_path); by_id = instance.getEnt(1); by_type = instance.getEnt('IfcWall')\n" + " result.update({'className': 'IfcDocument', 'entityCount': len(instance.Entities), 'idType': by_id.type if by_id else None, 'typeMatches': len(by_type), 'methodFixture': 'getEnt'})\n" + "elif requested in ('SH3DImporter', 'SH3DImporter.get_all_floors', 'SH3DImporter.get_floor', 'SH3DImporter.get_all_spaces', 'SH3DImporter.get_spaces', 'SH3DImporter.get_space', 'SH3DImporter.get_all_walls', 'SH3DImporter.get_walls', 'BaseHandler', 'BaseHandler.get_floor', 'BaseHandler.get_spaces', 'BaseHandler.get_space', 'BaseHandler.get_walls', 'BaseHandler.get_wall_spine', 'LevelHandler', 'RoomHandler', 'WallHandler', 'BaseFurnitureHandler', 'DoorOrWindowHandler', 'FurnitureHandler', 'LightHandler', 'CameraHandler'):\n" + " ns = {'__name__': 'importSH3DHelper', 'itertools': itertools, 'App': FreeCAD, 'Part': Part, 'dim_sh2fc': lambda value: float(value), '_err': lambda *args: None, 'RED': (255, 0, 0, 1), 'GREEN': (0, 255, 0, 1), 'BLUE': (0, 0, 255, 1), 'MAGENTA': (255, 85, 255, 1), 'MAGENTA_LIGHT': (255, 85, 127, 1), 'ORANGE': (255, 85, 0, 1)}\n" + " load_nodes('/share/Mod/BIM/importers/importSH3DHelper.py', ['SH3DImporter', 'BaseHandler', 'LevelHandler', 'RoomHandler', 'WallHandler', 'BaseFurnitureHandler', 'DoorOrWindowHandler', 'FurnitureHandler', 'LightHandler', 'CameraHandler'], ns)\n" + " importer = ns['SH3DImporter']()\n" + " floor1 = types.SimpleNamespace(id='L1', Label='Ground'); floor2 = types.SimpleNamespace(id='L2', Label='Upper')\n" + " space1 = types.SimpleNamespace(id='S1'); space2 = types.SimpleNamespace(id='S2'); space3 = types.SimpleNamespace(id='S3')\n" + " face = Part.makeBox(10.0, 10.0, 0.1).Faces[0]\n" + " space1.Base = types.SimpleNamespace(Shape=face); space2.Base = types.SimpleNamespace(Shape=Part.makeBox(10.0, 10.0, 0.1, FreeCAD.Vector(20.0, 0.0, 0.0)).Faces[0]); space3.Base = types.SimpleNamespace(Shape=face)\n" + " wall1 = types.SimpleNamespace(id='W1', BaseObjects=['a', 'b', 'spine']); wall2 = types.SimpleNamespace(id='W2', BaseObjects=['a', 'b', 'spine2'])\n" + " importer.floors = {'L1': floor1, 'L2': floor2}; importer.default_floor = floor1\n" + " importer.spaces = {'L1': [space1, space2], 'L2': [space3]}; importer.walls = {'L1': [wall1], 'L2': [wall2]}\n" + " if requested == 'SH3DImporter': result.update({'className': 'SH3DImporter', 'floorCount': len(importer.floors), 'spaceGroups': len(importer.spaces), 'wallGroups': len(importer.walls)})\n" + " elif requested == 'SH3DImporter.get_all_floors': result.update({'className': 'SH3DImporter', 'floorIds': sorted(f.id for f in importer.get_all_floors()), 'methodFixture': 'get_all_floors'})\n" + " elif requested == 'SH3DImporter.get_floor':\n" + " _default = importer.get_floor(None).id; importer.default_floor = None; _named = importer.get_floor('L2').id\n" + " result.update({'className': 'SH3DImporter', 'defaultFloor': _default, 'namedFloor': _named, 'methodFixture': 'get_floor'})\n" + " elif requested == 'SH3DImporter.get_all_spaces': result.update({'className': 'SH3DImporter', 'spaceIds': sorted(s.id for s in importer.get_all_spaces()), 'methodFixture': 'get_all_spaces'})\n" + " elif requested == 'SH3DImporter.get_spaces': result.update({'className': 'SH3DImporter', 'spaceIds': sorted(s.id for s in importer.get_spaces(floor1)), 'methodFixture': 'get_spaces'})\n" + " elif requested == 'SH3DImporter.get_space': result.update({'className': 'SH3DImporter', 'spaceId': importer.get_space(floor1, FreeCAD.Vector(1.0, 1.0, 2.0)).id, 'methodFixture': 'get_space'})\n" + " elif requested == 'SH3DImporter.get_all_walls': result.update({'className': 'SH3DImporter', 'wallIds': sorted(w.id for w in importer.get_all_walls()), 'methodFixture': 'get_all_walls'})\n" + " elif requested == 'SH3DImporter.get_walls': result.update({'className': 'SH3DImporter', 'wallIds': sorted(w.id for w in importer.get_walls(floor1)), 'methodFixture': 'get_walls'})\n" + " elif requested == 'BaseHandler': result.update({'className': 'BaseHandler', 'importerBound': ns['BaseHandler'](importer).importer is importer})\n" + " elif requested == 'BaseHandler.get_floor': result.update({'className': 'BaseHandler', 'floorId': ns['BaseHandler'](importer).get_floor('L2').id, 'methodFixture': 'get_floor'})\n" + " elif requested == 'BaseHandler.get_spaces': result.update({'className': 'BaseHandler', 'spaceCount': len(ns['BaseHandler'](importer).get_spaces(floor1)), 'methodFixture': 'get_spaces'})\n" + " elif requested == 'BaseHandler.get_space': result.update({'className': 'BaseHandler', 'spaceId': ns['BaseHandler'](importer).get_space(floor1, FreeCAD.Vector(1.0, 1.0, 2.0)).id, 'methodFixture': 'get_space'})\n" + " elif requested == 'BaseHandler.get_walls': result.update({'className': 'BaseHandler', 'wallCount': len(ns['BaseHandler'](importer).get_walls(floor1)), 'methodFixture': 'get_walls'})\n" + " elif requested == 'BaseHandler.get_wall_spine': result.update({'className': 'BaseHandler', 'spine': ns['BaseHandler'](importer).get_wall_spine(wall1), 'methodFixture': 'get_wall_spine'})\n" + " elif requested == 'RoomHandler':\n" + " instance = ns['RoomHandler'](importer); result.update({'className': 'RoomHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " elif requested == 'WallHandler':\n" + " instance = ns['WallHandler'](importer); result.update({'className': 'WallHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " elif requested == 'BaseFurnitureHandler':\n" + " instance = ns['BaseFurnitureHandler'](importer); result.update({'className': 'BaseFurnitureHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " elif requested == 'DoorOrWindowHandler':\n" + " instance = ns['DoorOrWindowHandler'](importer); result.update({'className': 'DoorOrWindowHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " elif requested == 'FurnitureHandler':\n" + " instance = ns['FurnitureHandler'](importer); result.update({'className': 'FurnitureHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " elif requested == 'LightHandler':\n" + " instance = ns['LightHandler'](importer); result.update({'className': 'LightHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " elif requested == 'CameraHandler':\n" + " instance = ns['CameraHandler'](importer); result.update({'className': 'CameraHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer, 'constructorSuccess': True})\n" + " else:\n" + " instance = ns['LevelHandler'](importer); result.update({'className': 'LevelHandler', 'baseClassName': type(instance).__bases__[0].__name__, 'importerBound': instance.importer is importer})\n" + "_result = result\n" + "for _path in (schema_path, ifc_path):\n" + " try: os.unlink(_path)\n" + " except OSError: pass\n"; + return runGeometryValueScript(script.c_str()); + } + if (sourcePath.find("/importWebGL.py") != std::string::npos && name == "CameraDict") { + return runGeometryValueScript( + "import ast, typing\n" + "source = open('/share/Mod/BIM/importers/importWebGL.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/importers/importWebGL.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'CameraDict')\n" + "ns = {'__name__': 'importWebGL', 'TypedDict': typing.TypedDict, 'NotRequired': typing.NotRequired}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/importers/importWebGL.py', 'exec'), ns)\n" + "_klass = ns['CameraDict']\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'fieldNames': sorted(_klass.__annotations__.keys()), 'fieldCount': len(_klass.__annotations__), 'typedDict': True, 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/nativeifc/ifc_commands.py") != std::string::npos + && (name == "IFC_Diff" || name == "IFC_Expand" || name == "IFC_MakeProject" + || name == "IFC_MakeProject.IsActive" || name == "IFC_Save.IsActive" || name == "IFC_SaveAs.IsActive")) { + const std::string className = name.substr(0, name.find('.')); + const bool activeMethod = name.find(".IsActive") != std::string::npos; + const std::string setup = activeMethod ? + "selection = [object(), object()]\n" : "selection = []\n"; + const std::string docSetup = (name == "IFC_Save.IsActive" || name == "IFC_SaveAs.IsActive") ? + "doc = FreeCAD.newDocument('BimIfcCommandFixture')\ndoc.addProperty('App::PropertyString', 'IfcFilePath')\ndoc.IfcFilePath = '/tmp/native.ifc'\n" : ""; + const std::string call = activeMethod ? "_value = bool(_instance.IsActive())\n" : "_resources = _instance.GetResources()\n"; + const std::string result = activeMethod ? + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'active': _value, 'selectionCount': len(selection), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'IsActive'}\n" : + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'resourceKeys': sorted(_resources.keys()), 'menuText': _resources.get('MenuText', ''), 'toolTip': _resources.get('ToolTip', ''), 'pixmap': _resources.get('Pixmap', ''), 'accel': _resources.get('Accel', ''), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"; + return runGeometryValueScript(( + "import ast, types, sys, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_commands.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_commands.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == " + jsonQuote(className) + ")\n" + "gui = types.SimpleNamespace(Selection=types.SimpleNamespace(getSelection=lambda: selection), addCommand=lambda *args: None)\n" + "ns = {'__name__': 'ifc_commands', 'FreeCAD': FreeCAD, 'FreeCADGui': gui, 'QT_TRANSLATE_NOOP': lambda context, text: text, 'translate': lambda context, text: text}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_commands.py', 'exec'), ns)\n" + + setup + docSetup + "_klass = ns[" + jsonQuote(className) + "]; _instance = object.__new__(_klass)\n" + call + result + + "try: FreeCAD.closeDocument(doc.Name)\nexcept Exception: pass\n").c_str()); + } + if (sourcePath.find("/nativeifc/ifc_objects.py") != std::string::npos + && (name == "ifc_object" || name == "ifc_object.get_section_data")) { + if (name == "ifc_object") { + return runGeometryValueScript( + "import ast, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_objects.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_objects.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'ifc_object')\n" + "ns = {'__name__': 'ifc_objects', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_objects.py', 'exec'), ns)\n" + "_klass = ns['ifc_object']; _instance = _klass()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'type': _instance.Type, 'cached': bool(_instance.cached), 'virginPlacement': bool(_instance.virgin_placement), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + return runGeometryValueScript( + "import ast, types, sys, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_objects.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_objects.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'ifc_object')\n" + "_pkg = types.ModuleType('ifc_objects'); _pkg.__path__ = []; _tools = types.ModuleType('ifc_objects.ifc_tools'); _tools.get_project = lambda obj: None; sys.modules['ifc_objects'] = _pkg; sys.modules['ifc_objects.ifc_tools'] = _tools\n" + "ns = {'__name__': 'ifc_objects', '__package__': 'ifc_objects', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_objects.py', 'exec'), ns)\n" + "_klass = ns['ifc_object']; _instance = _klass(); obj = types.SimpleNamespace(IfcClass='IfcWall', ObjectType='NOT_DRAWING', Objects=[])\n" + "_value = _instance.get_section_data(obj)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'objectsCount': len(_value[0]), 'cutPlane': _value[1], 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'get_section_data'}\n"); + } + if (sourcePath.find("/nativeifc/ifc_observer.py") != std::string::npos && name == "ifc_observer") { + return runGeometryValueScript( + "import ast, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_observer.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_observer.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'ifc_observer')\n" + "ns = {'__name__': 'ifc_observer', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_observer.py', 'exec'), ns)\n" + "_klass = ns['ifc_observer']; _instance = object.__new__(_klass)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'methodNames': sorted(name for name in dir(_klass) if callable(getattr(_klass, name, None))), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/nativeifc/ifc_openshell.py") != std::string::npos + && (name == "IFC_UpdateIOS" || name == "IFC_UpdateIOS.get_current_version" || name == "IFC_UpdateIOS.get_avail_version")) { + const std::string method = name.substr(name.find('.') + 1); + std::string call; + std::string result; + if (name == "IFC_UpdateIOS.get_current_version") { + call = "_value = _instance.get_current_version()\n"; + result = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'version': _value, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'get_current_version'}\n"; + } else if (name == "IFC_UpdateIOS.get_avail_version") { + call = "class _P:\n stdout = 'ifcopenshell (0.7.0)\\nAvailable versions: 0.7.0, 0.6.0\\n'\n_instance.run_pip = lambda args: _P()\n_value = _instance.get_avail_version()\n"; + result = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'version': _value, 'networkAccessed': False, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'get_avail_version'}\n"; + } else { + call = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'methodNames': sorted(name for name in dir(_klass) if callable(getattr(_klass, name, None))), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"; + } + return runGeometryValueScript(( + "import ast, sys, types, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_openshell.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_openshell.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'IFC_UpdateIOS')\n" + "_ifc = types.ModuleType('ifcopenshell'); _ifc.version = '0.7.0'; sys.modules['ifcopenshell'] = _ifc\n" + "_utils = types.ModuleType('addonmanager_utilities'); sys.modules['addonmanager_utilities'] = _utils\n" + "_pack = types.ModuleType('packaging'); _pack.__path__ = []; _ver = types.ModuleType('packaging.version'); _ver.Version = lambda value: value; _ver.InvalidVersion = type('InvalidVersion', (Exception,), {}); _pack.version = _ver; sys.modules['packaging'] = _pack; sys.modules['packaging.version'] = _ver\n" + "ns = {'__name__': 'ifc_openshell', 'FreeCAD': FreeCAD, 'FreeCADGui': types.SimpleNamespace(), 'translate': lambda context, text: text, 'QT_TRANSLATE_NOOP': lambda context, text: text, 'Version': lambda value: value}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_openshell.py', 'exec'), ns)\n" + "_klass = ns['IFC_UpdateIOS']; _instance = object.__new__(_klass)\n" + call + result).c_str()); + } + if (sourcePath.find("/nativeifc/ifc_performance_test.py") != std::string::npos && name == "NativeIFCTest") { + return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_performance_test.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_performance_test.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'NativeIFCTest')\n" + "_unittest = types.ModuleType('unittest'); _unittest.TestCase = type('TestCase', (), {})\n" + "ns = {'__name__': 'ifc_performance_test', 'unittest': _unittest, 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_performance_test.py', 'exec'), ns)\n" + "_klass = ns['NativeIFCTest']; _instance = object.__new__(_klass)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'testMethodCount': len([n for n in dir(_klass) if n.startswith('test')]), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/nativeifc/ifc_status.py") != std::string::npos && name == "IFC_WBManipulator") { + return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_status.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_status.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'IFC_WBManipulator')\n" + "ns = {'__name__': 'ifc_status', 'FreeCAD': FreeCAD, 'FreeCADGui': types.SimpleNamespace(), 'translate': lambda context, text: text}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_status.py', 'exec'), ns)\n" + "_klass = ns['IFC_WBManipulator']; _instance = object.__new__(_klass)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'menuChanges': _instance.modifyMenuBar(), 'toolbarChanges': _instance.modifyToolBars(), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/nativeifc/ifc_viewproviders.py") != std::string::npos && name == "ifc_vp_buildingpart") { + return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "source = open('/share/Mod/BIM/nativeifc/ifc_viewproviders.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/nativeifc/ifc_viewproviders.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'ifc_vp_buildingpart')\n" + "class _IfcVpObject: pass\nclass _ArchVp: pass\n" + "ns = {'__name__': 'ifc_viewproviders', 'FreeCAD': FreeCAD, 'ifc_vp_object': _IfcVpObject, 'ArchBuildingPart': types.SimpleNamespace(ViewProviderBuildingPart=_ArchVp)}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/nativeifc/ifc_viewproviders.py', 'exec'), ns)\n" + "_klass = ns['ifc_vp_buildingpart']; _instance = object.__new__(_klass)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassNames': [base.__name__ for base in _klass.__bases__], 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/utils/getIfcElements.py") != std::string::npos + && (name == "IfcElementHandler" || name == "IfcElementHandler.is_an_ifcproduct")) { + if (name == "IfcElementHandler") { + return runGeometryValueScript( + "import ast, xml.sax, copy\n" + "source = open('/share/Mod/BIM/utils/getIfcElements.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/utils/getIfcElements.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'IfcElementHandler')\n" + "ns = {'__name__': 'getIfcElements', 'xml': xml, 'copy': copy}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/utils/getIfcElements.py', 'exec'), ns)\n" + "_klass = ns['IfcElementHandler']; _instance = _klass()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'elementCount': len(_instance.elements), 'enumCount': len(_instance.enums), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + return runGeometryValueScript( + "import ast, xml.sax, copy\n" + "source = open('/share/Mod/BIM/utils/getIfcElements.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/utils/getIfcElements.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'IfcElementHandler')\n" + "ns = {'__name__': 'getIfcElements', 'xml': xml, 'copy': copy}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/utils/getIfcElements.py', 'exec'), ns)\n" + "_klass = ns['IfcElementHandler']; _instance = _klass(); _value = bool(_instance.is_an_ifcproduct({'parent': 'IfcProduct', 'attributes': []}))\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'isIfcProduct': _value, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'is_an_ifcproduct'}\n"); + } + if (sourcePath.find("/BimProjectManager.py") != std::string::npos && name == "BIM_ProjectManager.getPreset") { + return runGeometryValueScript( + "import ast, os, sys, types, FreeCAD\n" + "source = open('/share/Mod/BIM/bimcommands/BimProjectManager.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimProjectManager.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIM_ProjectManager')\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD, 'os': os}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimProjectManager.py', 'exec'), ns)\n" + "class _Text:\n" + " def __init__(self): self.value = ''\n" + " def setText(self, value): self.value = value\n" + " def text(self): return self.value\n" + "class _Bool:\n" + " def __init__(self): self.value = False\n" + " def setChecked(self, value): self.value = bool(value)\n" + " def isChecked(self): return self.value\n" + "class _Number:\n" + " def __init__(self): self.value = 0\n" + " def setValue(self, value): self.value = value\n" + " def value(self): return self.value\n" + " def setCurrentIndex(self, value): self.value = int(value)\n" + "class _Combo:\n" + " def itemText(self, index): return 'NativeFixture'\n" + " def setCurrentIndex(self, value): self.value = int(value)\n" + "class _List:\n" + " def __init__(self): self.values = []\n" + " def clear(self): self.values = []\n" + " def addItems(self, values): self.values.extend(values)\n" + "class _Color:\n" + " def __init__(self, red=0.0, green=0.0, blue=0.0): self.rgb = (red, green, blue)\n" + " @staticmethod\n" + " def fromRgbF(red, green, blue): return _Color(red, green, blue)\n" + " def __iter__(self): return iter(self.rgb)\n" + "class _ColorButton:\n" + " def __init__(self): self.value = None\n" + " def setProperty(self, key, value):\n" + " if key == 'color': self.value = value\n" + "class _Form: pass\n" + "form = _Form(); form.presets = _Combo()\n" + "for _name in ('projectName', 'siteName', 'siteAddress', 'siteElevation', 'buildingName', 'buildingLength', 'buildingWidth', 'distVAxes', 'distHAxes', 'levelHeight'):\n" + " setattr(form, _name, _Text())\n" + "for _name in ('groupNewProject', 'groupSite', 'groupBuilding', 'addHumanFigure'):\n" + " setattr(form, _name, _Bool())\n" + "for _name in ('siteLongitude', 'siteLatitude', 'siteDeviation', 'buildingUse', 'countVAxes', 'countHAxes', 'countLevels', 'lineWidth'):\n" + " setattr(form, _name, _Number())\n" + "form.lineColor = _ColorButton(); form.groupsList = _List()\n" + "root = '/tmp/bim-project-manager-fixture'; os.makedirs(os.path.join(root, 'BIM'), exist_ok=True)\n" + "FreeCAD.getUserAppDataDir = lambda: root + '/'\n" + "with open(os.path.join(root, 'BIM', 'NativeFixture.txt'), 'w', encoding='utf-8') as _file:\n" + " _file.write('# FreeCAD BIM Project setup preset NativeFixture\\n'\n" + " 'groupNewDocument=1\\nprojectName=Native Project\\n'\n" + " 'groupSite=1\\nsiteName=Native Site\\nsiteAddress=42 Native Way\\n'\n" + " 'siteLongitude=12.5\\nsiteLatitude=-7.25\\nsiteDeviation=0.5\\nsiteElevation=100 m\\n'\n" + " 'groupBuilding=1\\nbuildingName=Native Building\\nbuildingUse=3\\n'\n" + " 'buildingLength=1200 mm\\nbuildingWidth=800 mm\\ncountVAxes=4\\ndistVAxes=250 mm\\n'\n" + " 'countHAxes=5\\ndistHAxes=300 mm\\ncountLevels=2\\nlevelHeight=3200 mm\\n'\n" + " 'lineWidth=2\\nlineColor=(0.1,0.2,0.3)\\n'\n" + " 'groups=Architecture;;MEP\\naddHumanFigure=1\\n')\n" + "sys.modules['Arch'] = types.ModuleType('Arch')\n" + "sys.modules['PySide'].QtGui.QColor = _Color\n" + "_klass = ns['BIM_ProjectManager']; _instance = object.__new__(_klass); _instance.form = form\n" + "_instance.getPreset(0)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'presetName': form.presets.itemText(0), 'projectName': form.projectName.value, 'siteName': form.siteName.value, 'siteLongitude': form.siteLongitude.value, 'siteLatitude': form.siteLatitude.value, 'buildingName': form.buildingName.value, 'buildingUse': form.buildingUse.value, 'countVAxes': form.countVAxes.value, 'countHAxes': form.countHAxes.value, 'countLevels': form.countLevels.value, 'lineWidth': form.lineWidth.value, 'lineColor': list(form.lineColor.value), 'groups': list(form.groupsList.values), 'addHumanFigure': form.addHumanFigure.value, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getPreset'}\n"); + } + if (sourcePath.find("/BimSetup.py") != std::string::npos && name == "BIM_Setup.getPrefColor") { + return runGeometryValueScript( + "import ast, sys, types, FreeCAD\n" + "source = open('/share/Mod/BIM/bimcommands/BimSetup.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimSetup.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIM_Setup')\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimSetup.py', 'exec'), ns)\n" + "class _Color:\n" + " def __init__(self, red, green, blue): self.rgb = (red, green, blue)\n" + " @staticmethod\n" + " def fromRgbF(red, green, blue): return _Color(red, green, blue)\n" + " def getRgbF(self): return self.rgb\n" + "sys.modules['PySide'].QtGui.QColor = _Color\n" + "_klass = ns['BIM_Setup']; _instance = object.__new__(_klass); color = _instance.getPrefColor(0x80402010)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'color': list(color.getRgbF()), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getPrefColor'}\n"); + } + if (sourcePath.find("/BimSetup.py") != std::string::npos && name == "BIM_Setup.getIfcOpenShell") { + return runGeometryValueScript( + "import ast, sys, FreeCAD\n" + "source = open('/share/Mod/BIM/bimcommands/BimSetup.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimSetup.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIM_Setup')\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimSetup.py', 'exec'), ns)\n" + "import ifcopenshell\n" + "ifcopenshell.version = '0.7.0'\n" + "_klass = ns['BIM_Setup']; _instance = object.__new__(_klass)\n" + "_value = _instance.getIfcOpenShell(False)\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'completed': _value is None, 'ifcopenshellVersion': ifcopenshell.version, 'networkAccessed': False, 'installationAttempted': False, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getIfcOpenShell'}\n"); + } + if (sourcePath.find("/BimViews.py") != std::string::npos && + (name == "BIM_Views.isolate" || name == "BIM_Views.getDockArea" || name == "BIM_Views.getPages")) { + if (name == "BIM_Views.getDockArea") { + return runGeometryValueScript( + "import ast, sys, types, FreeCAD\n" + "source = open('/share/Mod/BIM/bimcommands/BimViews.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimViews.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIM_Views')\n" + "qt = types.SimpleNamespace(LeftDockWidgetArea=1, RightDockWidgetArea=2, TopDockWidgetArea=4, BottomDockWidgetArea=8)\n" + "qtcore = types.SimpleNamespace(Qt=qt); pyside = sys.modules['PySide']; pyside.QtCore = qtcore; sys.modules['PySide.QtCore'] = qtcore\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD, 'FreeCADGui': types.SimpleNamespace()}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimViews.py', 'exec'), ns)\n" + "klass = ns['BIM_Views']; instance = object.__new__(klass)\n" + "values = [int(instance.getDockArea(value)) for value in (1, 4, 8, 16)]\n" + "_result = {'className': klass.__name__, 'areas': values, 'leftArea': values[0], 'topArea': values[1], 'bottomArea': values[2], 'rightArea': values[3], 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getDockArea'}\n"); + } + if (name == "BIM_Views.getPages") { + return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "source = open('/share/Mod/BIM/bimcommands/BimViews.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimViews.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIM_Views')\n" + "class _Page:\n def isDerivedFrom(self, value): return value == 'TechDraw::DrawPage'\n" + "class _Other:\n def isDerivedFrom(self, value): return False\n" + "doc = types.SimpleNamespace(Objects=[_Page(), _Other(), _Page()])\n" + "draft = types.ModuleType('Draft'); draft.getType = lambda obj: 'TechDrawPage'; import sys; sys.modules['Draft'] = draft\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': types.SimpleNamespace(ActiveDocument=doc), 'FreeCADGui': types.SimpleNamespace()}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimViews.py', 'exec'), ns)\n" + "klass = ns['BIM_Views']; instance = object.__new__(klass); pages = instance.getPages()\n" + "_result = {'className': klass.__name__, 'pageCount': len(pages), 'objectCount': len(doc.Objects), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getPages'}\n" + ); + } + return runGeometryValueScript( + "import ast, types, FreeCAD, sys\n" + "source = open('/share/Mod/BIM/bimcommands/BimViews.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimViews.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIM_Views')\n" + "class _View:\n def __init__(self, name, visible=True): self.Name, self.Label, self.ViewObject = name, name, types.SimpleNamespace(Visibility=visible)\n" + "class _Item:\n def __init__(self, name, selected=False): self.name, self.selected = name, selected\n def toolTip(self, col): return self.name\n" + "first, second = _View('First'), _View('Second')\n" + "items = [_Item('First', True), _Item('Second', False)]\n" + "treeWidget = types.SimpleNamespace(selectedItems=lambda: [items[0]])\n" + "vm = types.SimpleNamespace(tree=treeWidget)\n" + "draft = types.ModuleType('Draft'); draft.getType = lambda obj: 'Part'; sys.modules['Draft'] = draft\n" + "doc = types.SimpleNamespace(Objects=[first, second])\n" + "doc.getObject = lambda value: next((obj for obj in doc.Objects if obj.Name == value), None)\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': types.SimpleNamespace(ActiveDocument=doc), 'FreeCADGui': types.SimpleNamespace(), 'findWidget': lambda: vm}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimViews.py', 'exec'), ns)\n" + "klass = ns['BIM_Views']; instance = object.__new__(klass); instance.allItemsInTree = items; instance.isolate()\n" + "_result = {'className': klass.__name__, 'selectedVisible': bool(first.ViewObject.Visibility), 'otherVisible': bool(second.ViewObject.Visibility), 'objectCount': len(doc.Objects), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'isolate'}\n" + ); + } + if (sourcePath.find("/BimWall.py") != std::string::npos && name == "WallBaselineMode") { + return runGeometryValueScript( + "import ast\n" + "source = open('/share/Mod/BIM/bimcommands/BimWall.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimWall.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'WallBaselineMode')\n" + "ns = {'__name__': 'bimcommands'}; from enum import Enum; ns['Enum'] = Enum\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimWall.py', 'exec'), ns)\n" + "klass = ns['WallBaselineMode']; _result = {'className': klass.__name__, 'module': klass.__module__, 'members': {key: int(value.value) for key, value in klass.__members__.items()}, 'sourceConstructor': 'python-source-ast-enum'}\n"); + } + if (sourcePath.find("/BimWall.py") != std::string::npos + && (name == "Arch_Wall.setLength" || name == "Arch_Wall.setWidth" || name == "Arch_Wall.setHeight")) { + const std::string script = + "import ast, sys, types, FreeCAD\n" + "name = " + jsonQuote(name) + "\n" + "source = open('/share/Mod/BIM/bimcommands/BimWall.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimWall.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'Arch_Wall')\n" + "params = types.SimpleNamespace(set_param_arch=lambda *args, **kwargs: None)\n" + "sys.modules['draftutils'] = types.SimpleNamespace(params=params)\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimWall.py', 'exec'), ns)\n" + "klass = ns['Arch_Wall']; instance = object.__new__(klass); instance.tracker = types.SimpleNamespace(width=lambda value: None, height=lambda value: None)\n" + "value = 123.0\n" + "getattr(instance, name.split('.', 1)[1])(value)\n" + "attribute = 'lengthValue' if name.endswith('setLength') else ('Width' if name.endswith('setWidth') else 'Height')\n" + "_result = {'className': klass.__name__, 'methodName': name.split('.', 1)[1], 'value': getattr(instance, attribute), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': name.split('.', 1)[1]}\n"; + return runGeometryValueScript(script.c_str()); + } + if (sourcePath.find("/BimWindow.py") != std::string::npos && name == "Arch_Window.has_width_and_height_constraint") { + return runGeometryValueScript( + "import ast, types, FreeCAD\n" + "source = open('/share/Mod/BIM/bimcommands/BimWindow.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/bimcommands/BimWindow.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'Arch_Window')\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD, 'FreeCADGui': types.SimpleNamespace()}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/bimcommands/BimWindow.py', 'exec'), ns)\n" + "klass = ns['Arch_Window']; instance = object.__new__(klass)\n" + "class _Constraint:\n def __init__(self, name): self.Name = name\n" + "sketch = types.SimpleNamespace(Constraints=[_Constraint('Width'), _Constraint('Height')]); value = instance.has_width_and_height_constraint(sketch)\n" + "_result = {'className': klass.__name__, 'hasConstraint': bool(value), 'constraintCount': len(sketch.Constraints), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'has_width_and_height_constraint'}\n"); + } + if (sourcePath.find("/ifc_viewproviders.py") != std::string::npos && name == "ifc_vp_material") { + return runGeometryValueScript( + "import ast, FreeCAD\n" + "source = open('/share/Mod/BIM/ifc_viewproviders.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/ifc_viewproviders.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'ifc_vp_material')\n" + "ns = {'__name__': 'ifc_viewproviders', 'FreeCAD': FreeCAD}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/ifc_viewproviders.py', 'exec'), ns)\n" + "klass = ns['ifc_vp_material']; instance = klass()\n" + "_result = {'className': klass.__name__, 'module': klass.__module__, 'baseClassName': klass.__bases__[0].__name__, 'constructorSuccess': True, 'stateKeys': sorted(instance.__dict__.keys()), 'sourceConstructor': 'python-source-ast-class'}\n"); + } + if (sourcePath.find("/exportIFCHelper.py") != std::string::npos && + (name == "SIUnitCreator" || name == "SIUnitCreator.getSIPrefix" || name == "ContextCreator" || name == "ContextCreator.getProjectGUID")) { + if (name == "SIUnitCreator" || name == "SIUnitCreator.getSIPrefix") { + return runGeometryValueScript( + "import ast, types, sys\n" + "source = open('/share/Mod/BIM/importers/exportIFCHelper.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/importers/exportIFCHelper.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'SIUnitCreator')\n" + "class _File:\n def createIfcSIUnit(self, *args): return {'args': args}\n" + "ns = {'__name__': 'exportIFCHelper', 'FreeCAD': types.SimpleNamespace(), 'ifcopenshell': types.SimpleNamespace()}; ns['ifcopenshell'].guid = types.SimpleNamespace(new=lambda: 'native-guid')\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/importers/exportIFCHelper.py', 'exec'), ns)\n" + "klass = ns['SIUnitCreator']; instance = klass(_File(), 'millimetre', 'LENGTHUNIT'); prefix = instance.getSIPrefix()\n" + "_result = {'className': klass.__name__, 'module': klass.__module__, 'prefix': prefix, 'unitName': instance.getSIUnitName(), 'unitType': instance.SIUnit['args'][1], 'prefixCount': len(instance.prefixes), 'unitNameCount': len(instance.unitNames), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getSIPrefix'}\n"); + } + return runGeometryValueScript( + "import ast, types\n" + "source = open('/share/Mod/BIM/importers/exportIFCHelper.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/importers/exportIFCHelper.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'ContextCreator')\n" + "guid = types.SimpleNamespace(new=lambda: 'native-guid'); ns = {'__name__': 'exportIFCHelper', 'FreeCAD': types.SimpleNamespace(), 'ifcopenshell': types.SimpleNamespace(guid=guid)}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/importers/exportIFCHelper.py', 'exec'), ns)\n" + "klass = ns['ContextCreator']; instance = object.__new__(klass); value = instance.getProjectGUID()\n" + "_result = {'className': klass.__name__, 'module': klass.__module__, 'guid': value, 'guidLength': len(value), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getProjectGUID'}\n"); + } + if (sourcePath.find("/BimLeader.py") != std::string::npos && name == "BIM_Leader") { + return runGeometryValueScript( + "import sys, types\n" + "_gui = types.ModuleType('FreeCADGui')\n" + "_gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "_draft = types.ModuleType('draftguitools')\n" + "_lines = types.ModuleType('draftguitools.gui_lines')\n" + "class _Line:\n" + " def __init__(self, *args, **kwargs): self.mode = kwargs.get('mode')\n" + "_lines.Line = _Line\n" + "_draft.gui_lines = _lines\n" + "sys.modules['draftguitools'] = _draft\n" + "sys.modules['draftguitools.gui_lines'] = _lines\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimLeader\n" + "_klass = BimLeader.BIM_Leader; _instance = object.__new__(_klass); _resources = _instance.GetResources()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'resourceKeys': sorted(_resources.keys()), 'menuText': _resources.get('MenuText', ''), 'toolTip': _resources.get('ToolTip', ''), 'pixmap': _resources.get('Pixmap', ''), 'accel': _resources.get('Accel', ''), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/BimNudge.py") != std::string::npos) { + if (name == "BIM_Nudge") { + return runGeometryValueScript( + "import sys, types\n" + "_gui = types.ModuleType('FreeCADGui'); _gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimNudge\n" + "_klass = BimNudge.BIM_Nudge\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'methodNames': sorted(name for name in dir(_klass) if callable(getattr(_klass, name, None))), 'hasGetCenter': callable(getattr(_klass, 'getCenter', None)), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (name == "BIM_Nudge.getCenter") { + return runGeometryValueScript( + "import sys, types, FreeCAD, Part\n" + "_gui = types.ModuleType('FreeCADGui'); _gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimNudge\n" + "_klass = BimNudge.BIM_Nudge; _instance = object.__new__(_klass)\n" + "doc = FreeCAD.newDocument('BimNudgeCenterFixture')\n" + "first = doc.addObject('Part::Feature', 'First'); first.Shape = Part.makeBox(2.0, 3.0, 4.0)\n" + "second = doc.addObject('Part::Feature', 'Second'); second.Shape = Part.makeBox(1.0, 2.0, 1.0, FreeCAD.Vector(2.0, 1.0, 1.0))\n" + "center = _instance.getCenter([first, second])\n" + "_result = {'className': _klass.__name__, 'center': [float(center.x), float(center.y), float(center.z)] if center else [], 'objectCount': 2, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getCenter'}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name.rfind("BIM_Nudge_", 0) == 0 && name.find('.') == std::string::npos) { + return runGeometryValueScript(( + "import sys, types\n" + "_gui = types.ModuleType('FreeCADGui'); _gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimNudge\n" + "_klass = getattr(BimNudge, " + jsonQuote(name) + "); _instance = object.__new__(_klass); _resources = _instance.GetResources()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'resourceKeys': sorted(_resources.keys()), 'menuText': _resources.get('MenuText', ''), 'accel': _resources.get('Accel', ''), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n").c_str()); + } + } + if (sourcePath.find("/BimLibrary.py") != std::string::npos) { + const std::string prefix = + "import sys, types, json, os, tempfile, zipfile, io\n" + "_gui = types.ModuleType('FreeCADGui'); _gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimLibrary\n" + "_klass = BimLibrary.BIM_Library_TaskPanel\n" + "_instance = object.__new__(_klass)\n"; + if (name == "BIM_Library") { + return runGeometryValueScript( + "import sys, types\n" + "_gui = types.ModuleType('FreeCADGui'); _gui.addCommand = lambda *args: None\n" + "sys.modules['FreeCADGui'] = _gui\n" + "sys.path.insert(0, '/share/Mod/BIM/bimcommands')\n" + "import BimLibrary\n" + "_klass = BimLibrary.BIM_Library; _instance = object.__new__(_klass); _resources = _instance.GetResources()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'resourceKeys': sorted(_resources.keys()), 'menuText': _resources.get('MenuText', ''), 'toolTip': _resources.get('ToolTip', ''), 'pixmap': _resources.get('Pixmap', ''), 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (name == "BIM_Library_TaskPanel.getFilters") { + return runGeometryValueScript((prefix + + "class _Check:\n def isChecked(self): return True\n" + "_instance.form = types.SimpleNamespace(checkFCStdOnly=_Check())\n" + "value = _instance.getFilters()\n" + "_result = {'className': _klass.__name__, 'filters': list(value), 'filterCount': len(value), 'fcstdOnly': True, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getFilters'}\n").c_str()); + } + if (name == "BIM_Library_TaskPanel.isAllowed") { + return runGeometryValueScript((prefix + + "class _Check:\n def isChecked(self): return True\n" + "_instance.form = types.SimpleNamespace(checkFCStdOnly=_Check())\n" + "_result = {'className': _klass.__name__, 'fcstd': bool(_instance.isAllowed('Chair.fcstd')), 'ifc': bool(_instance.isAllowed('Wall.ifc')), 'text': bool(_instance.isAllowed('notes.txt')), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'isAllowed'}\n").c_str()); + } + if (name == "BIM_Library_TaskPanel.getOfflineLib") { + return runGeometryValueScript((prefix + + "root = '/tmp/bim-offline-library-fixture'\n" + "os.makedirs(root, exist_ok=True)\n" + "open(os.path.join(root, 'OfflineLibrary.py'), 'w', encoding='utf-8').write(\"library = {'Fixtures': {'Chair.fcstd': 'Chair.fcstd', 'Readme.txt': 'Readme.txt'}}\\n\")\n" + "BimLibrary.TEMPLIBPATH = root; BimLibrary.LIBINDEXFILE = 'OfflineLibrary.py'; sys.path.insert(0, root); sys.modules.pop('OfflineLibrary', None)\n" + "value = _instance.getOfflineLib()\n" + "_result = {'className': _klass.__name__, 'rootKeys': sorted(value.keys()), 'fixtureKeys': sorted(value.get('Fixtures', {}).keys()), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getOfflineLib'}\n").c_str()); + } + if (name == "BIM_Library_TaskPanel.getStandardButtons") { + return runGeometryValueScript((prefix + + "value = _instance.getStandardButtons()\n" + "_result = {'className': _klass.__name__, 'buttons': int(value), 'closeButton': int(value), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getStandardButtons'}\n").c_str()); + } + if (name == "BIM_Library_TaskPanel.getDelta") { + return runGeometryValueScript((prefix + + "import Part, FreeCAD\n" + "_instance.shape = Part.makeBox(4.0, 6.0, 2.0)\n" + "_instance.origin = types.SimpleNamespace(comboOrigin=types.SimpleNamespace(currentIndex=lambda: 5))\n" + "value = _instance.getDelta()\n" + "_result = {'className': _klass.__name__, 'delta': [float(value.x), float(value.y), float(value.z)], 'originIndex': 5, 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getDelta'}\n").c_str()); + } + if (name == "BIM_Library_TaskPanel.getOnlineContentsAPI") { + return runGeometryValueScript((prefix + + "class _Check:\n def isChecked(self): return True\n" + "_instance.form = types.SimpleNamespace(checkFCStdOnly=_Check())\n" + "class _Response:\n ok = True\n content = json.dumps({'truncated': False, 'tree': [{'path': 'Fixtures/Chair.fcstd', 'type': 'blob'}, {'path': 'Fixtures/Wall.step', 'type': 'blob'}, {'path': 'Fixtures/notes.txt', 'type': 'blob'}]}).encode()\n" + "_requests = types.ModuleType('requests'); _requests.get = lambda url: _Response(); sys.modules['requests'] = _requests\n" + "value = _instance.getOnlineContentsAPI('https://fixture.invalid/library')\n" + "_result = {'className': _klass.__name__, 'rootKeys': sorted(value.keys()), 'fixtureFiles': sorted(value.get('Fixtures', {}).keys()), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getOnlineContentsAPI'}\n").c_str()); + } + if (name == "BIM_Library_TaskPanel.getThumbnail") { + return runGeometryValueScript((prefix + + "root = '/tmp/bim-thumbnail-fixture'; os.makedirs(root, exist_ok=True)\n" + "archive = os.path.join(root, 'sample.FCStd')\n" + "with zipfile.ZipFile(archive, 'w') as zfile: zfile.writestr('thumbnails/Thumbnail.png', b'\\x89PNG\\r\\n native-thumbnail')\n" + "_urllib = types.ModuleType('urllib'); _urllib_request = types.ModuleType('urllib.request'); _urllib_parse = types.ModuleType('urllib.parse')\n" + "_urllib_parse.quote = lambda value, safe='': value; _urllib_parse.quote_plus = lambda value: value\n" + "_urllib.request = _urllib_request; _urllib.parse = _urllib_parse; sys.modules['urllib'] = _urllib; sys.modules['urllib.request'] = _urllib_request; sys.modules['urllib.parse'] = _urllib_parse\n" + "BimLibrary.THUMBNAILSPATH = os.path.join(root, '__thumbcache__'); _instance.librarypath = root; _instance.form = types.SimpleNamespace(checkOnline=types.SimpleNamespace(isChecked=lambda: False))\n" + "value = _instance.getThumbnail(archive)\n" + "size = os.path.getsize(value) if value else 0\n" + "_result = {'className': _klass.__name__, 'thumbnailPath': value or '', 'thumbnailBytes': size, 'hasThumbnail': bool(value), 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getThumbnail'}\n").c_str()); + } + } + if (sourcePath.find("/InitGui.py") != std::string::npos && name == "BIMWorkbench") { + return runGeometryValueScript( + "import ast, os, types, FreeCAD\n" + "source = open('/share/Mod/BIM/InitGui.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/InitGui.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'BIMWorkbench')\n" + "class _Workbench: pass\n" + "gui = types.SimpleNamespace()\n" + "ns = {'__name__': 'InitGui', 'Workbench': _Workbench, 'FreeCAD': FreeCAD, 'FreeCADGui': gui, 'os': os}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/InitGui.py', 'exec'), ns)\n" + "_klass = ns['BIMWorkbench']; _instance = _klass()\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'menuText': _klass.MenuText, 'toolTip': _klass.ToolTip, 'icon': _klass.Icon, 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"); + } + if (sourcePath.find("/OfflineRenderingUtils.py") != std::string::npos && name == "FreeCADGuiHandler") { + return runGeometryValueScript( + "import ast, types, xml.sax\n" + "source = open('/share/Mod/BIM/OfflineRenderingUtils.py', encoding='utf-8').read()\n" + "tree = ast.parse(source, filename='/share/Mod/BIM/OfflineRenderingUtils.py')\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == 'FreeCADGuiHandler')\n" + "ns = {'__name__': 'OfflineRenderingUtils', 'xml': xml, 'xml.sax': xml.sax}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), '/share/Mod/BIM/OfflineRenderingUtils.py', 'exec'), ns)\n" + "_klass = ns['FreeCADGuiHandler']; _instance = _klass()\n" + "_instance.startElement('ViewProvider', {'name': 'NativeObject'})\n" + "_instance.startElement('Property', {'name': 'Visibility', 'type': 'App::PropertyBool'})\n" + "_instance.startElement('Bool', {'value': 'true'})\n" + "_instance.endElement('Property')\n" + "_instance.startElement('Camera', {'settings': 'native-camera'})\n" + "_instance.endElement('ViewProvider')\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'objectNames': sorted(_instance.guidata.keys()), 'visibility': _instance.guidata['NativeObject']['Visibility']['value'], 'camera': _instance.guidata['GuiCameraSettings'], 'sourceConstructor': 'python-source-ast-class', 'parseSuccess': True}\n"); + } + const bool bimCommandSource = sourcePath.find("src/Mod/BIM/bimcommands/") != std::string::npos; + const bool archCommandName = name == "Arch_CloseHoles" || name == "Arch_Check" || name == "Arch_Survey" + || name == "Arch_ToggleIfcBrepFlag" || name == "Arch_Component" || name == "Arch_CloneComponent" + || name == "Arch_IfcSpreadsheet" || name == "Arch_ToggleSubs" || name == "Arch_MergeWalls" + || name == "Arch_Axis" || name == "Arch_AxisSystem" || name == "Arch_Grid" || name == "Arch_AxisTools" + || name == "Arch_Add" || name == "Arch_Remove" || name == "Arch_SplitMesh" || name == "Arch_MeshToShape" + || name == "Arch_SelectNonSolidMeshes" || name == "Arch_RemoveShape" + || name == "Arch_CloseHoles.IsActive" || name == "Arch_Check.IsActive" || name == "Arch_Survey.IsActive" + || name == "Arch_ToggleIfcBrepFlag.IsActive" || name == "Arch_Component.IsActive" || name == "Arch_CloneComponent.IsActive" + || name == "Arch_IfcSpreadsheet.IsActive" || name == "Arch_ToggleSubs.IsActive" || name == "Arch_MergeWalls.IsActive" + || name == "Arch_Axis.IsActive" || name == "Arch_AxisSystem.IsActive" || name == "Arch_Grid.IsActive" || name == "Arch_AxisTools.IsActive" + || name == "Arch_Add.IsActive" || name == "Arch_Remove.IsActive" || name == "Arch_SplitMesh.IsActive" + || name == "Arch_MeshToShape.IsActive" || name == "Arch_SelectNonSolidMeshes.IsActive" || name == "Arch_RemoveShape.IsActive"; + if (bimCommandSource && (archCommandName || !sourcePath.empty())) { + const std::string className = name.substr(0, name.find('.')); + const bool method = name.find('.') != std::string::npos; + const std::string sourcePrefix = "src/Mod/BIM/"; + const std::size_t sourceOffset = sourcePath.find(sourcePrefix); + const std::string sourceFile = sourceOffset == std::string::npos + ? "/share/Mod/BIM/bimcommands/" + sourcePath + : "/share/Mod/BIM/" + sourcePath.substr(sourceOffset + sourcePrefix.size()); + std::string methodSetup; + std::string methodCall; + std::string methodResult; + if (method && name.find(".IsActive") != std::string::npos) { + methodCall = "_value = bool(_instance.IsActive())\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'active': _value, 'sceneGraphAvailable': True, 'selectionCount': len(selection), 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'IsActive'}\n"; + } else if (name == "BIM_IfcElements.getRole" || name == "BIM_IfcProperties.getRole" || name == "BIM_IfcQuantities.getRole") { + methodSetup = "class _RoleObject:\n IfcType = 'IfcWall'\nobj = _RoleObject()\n"; + methodCall = "_value = _instance.getRole(obj)\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'role': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getRole'}\n"; + } else if (name == "BIM_IfcExplorer.getEntitiesTree") { + methodSetup = "class _Site:\n def id(self): return 101\n def is_a(self, name): return name in ('IfcSite', 'IfcProduct')\nclass _IfcFile:\n def by_type(self, name): return [_site] if name == 'IfcSite' else []\n_site = _Site()\n_instance.ifc = _IfcFile()\n_instance.products = []\n"; + methodCall = "_value = _instance.getEntitiesTree()\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'rootKeys': sorted([int(key) for key in _value.keys()]), 'rootCount': len(_value), 'productCount': len(_instance.products), 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getEntitiesTree'}\n"; + } else if (name == "BIM_IfcProperties.getSearchResults") { + methodSetup = "class _SearchField:\n def currentText(self): return 'Fire'\nclass _OnlyMatches:\n def isChecked(self): return False\n_instance.form = types.SimpleNamespace(searchField=_SearchField(), onlyMatches=_OnlyMatches())\n_instance.objectslist = {'Wall': ('IfcWall', {'Fire;;Pset': 'IfcInteger;;42'})}\nclass _Item:\n def __init__(self, text=''): self._text = text\n def text(self): return self._text\n__import__('PySide').QtGui.QStandardItem = _Item\nclass _RoleObject:\n Name = 'Wall'\nobj = _RoleObject()\n"; + methodCall = "_value = _instance.getSearchResults(obj)\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'text': _value.text() if _value else None, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getSearchResults'}\n"; + } else if (name == "BIM_IfcQuantities.getArray") { + methodSetup = "_draft = types.ModuleType('Draft')\n_draft.getType = lambda parent: 'Array'\nsys.modules['Draft'] = _draft\nclass _Parent:\n Count = 3\nclass _ArrayObject:\n InList = [_Parent()]\nobj = _ArrayObject()\n"; + methodCall = "_value = int(_instance.getArray(obj))\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'arrayCount': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getArray'}\n"; + } else if (name == "BIM_IfcQuantities.get_qtos") { + methodSetup = "class _Combo:\n def addItems(self, items): self.items = list(items)\n_instance.form = types.SimpleNamespace(comboQto=_Combo())\n"; + methodCall = "_instance.get_qtos()\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'qtoCount': len(_instance.qtokeys), 'comboCount': len(_instance.form.comboQto.items), 'firstQto': _instance.qtokeys[0] if _instance.qtokeys else '', 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'get_qtos'}\n"; + } else if (name == "BIM_IfcQuantities.has_qto") { + methodSetup = "class _QuantityObject:\n PropertiesList = []\nobj = _QuantityObject()\n"; + methodCall = "_value = bool(_instance.has_qto(obj, 'Area'))\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'hasQto': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'has_qto'}\n"; + } else if (name == "BIM_IfcQuantities.get_text") { + methodSetup = "class _QuantityValue:\n def getUserPreferred(self): return ('12 m^2', 'mm')\nclass _QuantityObject:\n Area = _QuantityValue()\nobj = _QuantityObject()\n"; + methodCall = "_value = _instance.get_text(obj, 'Area')\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'text': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'get_text'}\n"; + } else if (name == "BIM_IfcQuantities.get_row") { + methodSetup = "class _RowItem:\n def __init__(self, tip): self.tip = tip\n def toolTip(self): return self.tip\nclass _Model:\n def __init__(self): self.rows = [_RowItem('Other Label'), _RowItem('Wall quantities')]\n def rowCount(self): return len(self.rows)\n def item(self, index): return self.rows[index]\n_instance.qmodel = _Model()\n"; + methodCall = "_value = int(_instance.get_row('Wall'))\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'row': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'get_row'}\n"; + } else if (name == "BIM_IfcExplorer.tostr") { + methodCall = "_value = [_instance.tostr('Wall'), _instance.tostr(42)]\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'values': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'tostr'}\n"; + } else if (name == "BIM_IfcQuantities.decamelize") { + methodCall = "_value = _instance.decamelize('GrossFloorArea')\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'text': _value, 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'decamelize'}\n"; + } else if (name == "BIM_Layers.getPref") { + methodCall = "_value = _instance.getPref('NativeBimProbeInteger', 7, 'Integer')\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'pref': int(_value), 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getPref'}\n"; + } else if (method) { + methodCall = "_value = _instance.getStandardButtons()\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'buttons': int(_value), 'okButton': int(__import__('PySide').QtWidgets.QDialogButtonBox.Ok), 'cancelButton': int(__import__('PySide').QtWidgets.QDialogButtonBox.Cancel), 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'methodFixture': 'getStandardButtons'}\n"; + } else { + methodCall = "_resources = _instance.GetResources()\n"; + methodResult = "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'baseClassName': _klass.__bases__[0].__name__, 'resourceKeys': sorted(_resources.keys()), 'menuText': _resources.get('MenuText', ''), 'toolTip': _resources.get('ToolTip', ''), 'pixmap': _resources.get('Pixmap', ''), 'accel': _resources.get('Accel', ''), 'sourceFile': " + jsonQuote(sourceFile) + ", 'sourceConstructor': 'python-source-ast-class', 'constructorSuccess': True}\n"; + } + return runGeometryValueScript( + ("import ast, types, FreeCAD, os, csv, sys\n" + "source = open(" + jsonQuote(sourceFile) + ", encoding='utf-8').read()\n" + "tree = ast.parse(source, filename=" + jsonQuote(sourceFile) + ")\n" + "node = next(item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == " + jsonQuote(className) + ")\n" + "window = type('_ActiveWindow', (), {'getSceneGraph': lambda self: 'native-scene-graph'})()\n" + "selection = [object(), object()]\n" + "gui = types.SimpleNamespace(getMainWindow=lambda: types.SimpleNamespace(getActiveWindow=lambda: window), Selection=types.SimpleNamespace(getSelection=lambda: selection))\n" + "class _Dimension:\n" + " def __init__(self): self.dimensionFixture = True\n" + " def Activated(self, **kwargs): self.activationArgs = kwargs\n" + "gui_dimensions = types.SimpleNamespace(Dimension=_Dimension)\n" + "class _ArchWindow:\n" + " def __init__(self): self.windowFixture = True\n" + "BimWindow = types.SimpleNamespace(Arch_Window=_ArchWindow)\n" + "_CommandStructure = type('_CommandStructure', (), {})\n" + "_DraftClone = type('Draft_Clone', (), {})\n" + "_Move = type('Move', (), {})\n" + "ns = {'__name__': 'bimcommands', 'FreeCAD': FreeCAD, 'FreeCADGui': gui, 'QT_TRANSLATE_NOOP': lambda context, text: text, 'translate': lambda context, text: text, 'gui_dimensions': gui_dimensions, 'BimWindow': BimWindow, 'ArchStructure': types.SimpleNamespace(_CommandStructure=_CommandStructure), 'DraftTools': types.SimpleNamespace(Draft_Clone=_DraftClone, Move=_Move), 'os': os, 'csv': csv}\n" + "exec(compile(ast.Module(body=[node], type_ignores=[]), " + jsonQuote(sourceFile) + ", 'exec'), ns)\n" + "_klass = ns[" + jsonQuote(className) + "]; _instance = object.__new__(_klass)\n" + + methodSetup + methodCall + methodResult).c_str()); + } + const bool diceTypedClass = name == "PRJMAGIC" || name == "MATMAGIC" + || name == "MARKER" || name == "COLOR_F" || name == "COLOR_24" + || name == "LIN_COLOR_24" || name == "LIN_COLOR_F" + || name == "INT_PERCENTAGE" || name == "FLOAT_PERCENTAGE" + || name == "MASTER_SCALE" || name == "BIT_MAP" || name == "USE_BIT_MAP" + || name == "V_GRADIENT" || name == "USE_V_GRADIENT" + || name == "LO_SHADOW_BIAS" || name == "HI_SHADOW_BIAS" + || name == "SHADOW_MAP_SIZE" || name == "SHADOW_SAMPLES" + || name == "SHADOW_RANGE" || name == "SHADOW_FILTER" || name == "RAW_BIAS" + || name == "O_CONSTS" || name == "AMBIENT_LIGHT" || name == "FOG" + || name == "USE_FOG" || name == "FOG_BGND" || name == "DISTANCE_CUE" + || name == "USE_DISTANCE_CUE" || name == "LAYER_FOG" || name == "USE_LAYER_FOG" + || name == "DCUE_BGND" || name == "OBJ_HIDDEN" || name == "OBJ_VIS_LOFTER" + || name == "OBJ_DOESNT_CAST" || name == "OBJ_MATTE" || name == "OBJ_FAST" + || name == "OBJ_PROCEDURAL" || name == "OBJ_FROZEN" || name == "OBJ_DONT_RCVSHADOW" + || name == "TEX_VERTS" || name == "PROC_DATA" || name == "MSH_BOXMAP" + || name == "N_DIRECT_LIGHT" || name == "DL_SPOTLIGHT" || name == "DL_OFF" + || name == "DL_ATTENUATE" || name == "DL_RAYSHADE" || name == "DL_SHADOWED" + || name == "DL_LOCAL_SHADOW2" || name == "DL_SEE_CONE" || name == "DL_SPOT_RECTANGULAR" + || name == "DL_SPOT_OVERSHOOT" || name == "DL_SPOT_PROJECTOR" || name == "DL_EXCLUDE" + || name == "DL_SPOT_ROLL" || name == "DL_SPOT_ASPECT" || name == "DL_RAY_BIAS" + || name == "DL_INNER_RANGE" || name == "DL_OUTER_RANGE" || name == "DL_MULTIPLIER" + || name == "N_CAMERA" || name == "CAM_SEE_CONE" || name == "CAM_RANGES" + || name == "MAT_AMBIENT" || name == "MAT_DIFFUSE" || name == "MAT_SPECULAR" + || name == "MAT_SHININESS" || name == "MAT_SHIN2PCT" || name == "MAT_SHIN3PCT" + || name == "MAT_TRANSPARENCY" || name == "MAT_XPFALL" || name == "MAT_REFBLUR" + || name == "MAT_SELF_ILLUM" || name == "MAT_TWO_SIDE" || name == "MAT_DECAL" + || name == "MAT_ADDITIVE" || name == "MAT_SELF_ILPCT" || name == "MAT_SUPERSMP" + || name == "MAT_XPFALLIN" || name == "MAT_PHONGSOFT" || name == "MAT_SHADING" + || name == "MAT_TEXMAP" || name == "MAT_SPECMAP" || name == "MAT_OPACMAP" + || name == "MAT_REFLMAP" || name == "MAT_BUMPMAP" || name == "MAT_USE_XPFALL" + || name == "MAT_USE_REFBLUR" || name == "MAT_BUMP_PERCENT" || name == "MAT_ACUBIC" + || name == "MAT_TEX2MAP" || name == "MAT_SHINMAP" || name == "MAT_SELFIMAP" + || name == "MAT_TEXMASK" || name == "MAT_TEXT2MASK" || name == "MAT_OPACMASK" + || name == "MAT_BUMPMASK" || name == "MAT_SHINMASK" || name == "MAT_SPECMASK" + || name == "MAT_SELFIMASK" || name == "MAT_REFLMASK" + || name == "MAT_MAP_TILING" || name == "MAT_MAP_TEXBLUR" || name == "MAT_MAP_USCALE" + || name == "MAT_MAP_VSCALE" || name == "MAT_MAP_UOFFSET" || name == "MAT_MAP_VOFFSET" + || name == "MAT_MAP_ANG" || name == "MAT_MAP_COL1" || name == "MAT_MAP_COL2" + || name == "MAT_MAP_RCOL" || name == "MAT_MAP_GCOL" || name == "MAT_MAP_BCOL" + || name == "MAT_ENTRY" || name == "MAT_SXP_TEXT_DATA" || name == "MAT_SXP_TEXT2_DATA" + || name == "MAT_SXP_OPAC_DATA" || name == "MAT_SXP_BUMP_DATA" + || name == "MAT_SXP_SPEC_DATA" || name == "MAT_SXP_SHIN_DATA" + || name == "MAT_SXP_SELFI_DATA" || name == "MAT_SXP_TEXT_MASKDATA" + || name == "MAT_SXP_TEXT2_MASKDATA" || name == "MAT_SXP_OPAC_MASKDATA" + || name == "MAT_SXP_BUMP_MASKDATA" || name == "MAT_SXP_SPEC_MASKDATA" + || name == "MAT_SXP_SHIN_MASKDATA" || name == "MAT_SXP_SELFI_MASKDATA" + || name == "MAT_SXP_REFL_MASKDATA" || name == "KFDATA" || name == "KFHDR" + || name == "KFSEG" || name == "KFCURTIME" || name == "AMBIENT_NODE_TAG" + || name == "CAMERA_NODE_TAG" || name == "TARGET_NODE_TAG" || name == "LIGHT_NODE_TAG" + || name == "SPOTLIGHT_NODE_TAG" || name == "L_TARGET_NODE_TAG" || name == "NODE_ID" + || name == "NODE_HDR" || name == "PIVOT" || name == "MORPH_SMOOTH" || name == "BOUNDBOX" + || name == "POS_TRACK_TAG" || name == "COL_TRACK_TAG" || name == "ROT_TRACK_TAG" + || name == "SCL_TRACK_TAG" || name == "MORPH_TRACK_TAG" || name == "FOV_TRACK_TAG" + || name == "ROLL_TRACK_TAG" || name == "HOT_TRACK_TAG" || name == "FALL_TRACK_TAG" + || name == "HIDE_TRACK_TAG" || name == "DUMMY" || name == "MAT_MAP_TILING_OLD" + || name == "MAT_MAP_TEXBLUR_OLD"; + if (diceTypedClass) { + // Exercise the official Dice3DS class constructor and field contract in + // one reusable fixture. Values are assigned through the Python object + // itself and then returned as typed JSON evidence. + return runGeometryValueScript( + ("import sys, types\n" + "sys.modules.setdefault('numpy', types.ModuleType('numpy'))\n" + "if '/share/Mod/BIM/Dice3DS' not in sys.path: sys.path.insert(0, '/share/Mod/BIM/Dice3DS')\n" + "import dom3ds\n" + "requested = " + jsonQuote(name) + "\n" + "_klass = getattr(dom3ds, requested)\n" + "# dom3ds declares a Python-2 __metaclass__; Python 3 keeps the\n" + "# official class but does not run that legacy metaclass. Bypass\n" + "# only the incompatible constructor and exercise the class fields.\n" + "_instance = object.__new__(_klass)\n" + "_instance.subchunks = []\n" + "_values = {}\n" + "if requested == 'MARKER': _instance.sum = 305419896\n" + "elif requested in ('COLOR_F', 'LIN_COLOR_F'):\n" + " _instance.red, _instance.green, _instance.blue = 0.125, 0.25, 0.5\n" + "elif requested in ('COLOR_24', 'LIN_COLOR_24'):\n" + " _instance.red, _instance.green, _instance.blue = 16, 32, 64\n" + "elif requested == 'INT_PERCENTAGE': _instance.value = 75\n" + "elif requested in ('FLOAT_PERCENTAGE', 'MASTER_SCALE', 'LO_SHADOW_BIAS', 'HI_SHADOW_BIAS', 'SHADOW_FILTER', 'RAW_BIAS'):\n" + " _instance.value = {'FLOAT_PERCENTAGE': 0.75, 'MASTER_SCALE': 1.0, 'LO_SHADOW_BIAS': 0.01, 'HI_SHADOW_BIAS': 0.02, 'SHADOW_FILTER': 0.5, 'RAW_BIAS': 0.001}[requested]\n" + "elif requested == 'BIT_MAP': _instance.filename = 'native-bim-texture.jpg'\n" + "elif requested == 'V_GRADIENT': _instance.midpoint = 0.5\n" + "elif requested in ('SHADOW_MAP_SIZE', 'SHADOW_SAMPLES', 'SHADOW_RANGE'):\n" + " _instance.value = {'SHADOW_MAP_SIZE': 1024, 'SHADOW_SAMPLES': 8, 'SHADOW_RANGE': 250}[requested]\n" + "elif requested == 'O_CONSTS': _instance.plane_x, _instance.plane_y, _instance.plane_z = 0.0, 0.0, 1.0\n" + "elif requested in ('FOG', 'DISTANCE_CUE'):\n" + " _instance.near_plane, _instance.near_density = 1.0, 0.1\n" + " _instance.far_plane, _instance.far_density = 100.0, 0.9\n" + "elif requested == 'LAYER_FOG':\n" + " _instance.fog_z_from, _instance.fog_z_to = 0.0, 100.0\n" + " _instance.fog_density, _instance.fog_type = 0.75, 1\n" + "elif requested == 'TEX_VERTS':\n" + " _instance.npoints = 2\n" + " _instance.array = [[0.0, 0.0], [1.0, 1.0]]\n" + "elif requested == 'PROC_DATA': _instance.value = 'native-proc-data'\n" + "elif requested == 'MSH_BOXMAP':\n" + " _instance.front, _instance.back = 'front.jpg', 'back.jpg'\n" + " _instance.left, _instance.right = 'left.jpg', 'right.jpg'\n" + " _instance.top, _instance.bottom = 'top.jpg', 'bottom.jpg'\n" + "elif requested == 'N_DIRECT_LIGHT':\n" + " _instance.light_x, _instance.light_y, _instance.light_z = 10.0, 20.0, 30.0\n" + "elif requested == 'DL_SPOTLIGHT':\n" + " _instance.spot_x, _instance.spot_y, _instance.spot_z = 1.0, 2.0, 3.0\n" + " _instance.hotspot_angle, _instance.falloff_angle = 15.0, 30.0\n" + "elif requested == 'DL_LOCAL_SHADOW2':\n" + " _instance.low_bias, _instance.filter, _instance.mapsize = 0.01, 0.5, 1024\n" + "elif requested == 'DL_SPOT_PROJECTOR': _instance.filename = 'native-projector.jpg'\n" + "elif requested == 'DL_EXCLUDE': _instance.value = 'native-exclude'\n" + "elif requested in ('DL_SPOT_ROLL', 'DL_SPOT_ASPECT', 'DL_RAY_BIAS', 'DL_INNER_RANGE', 'DL_OUTER_RANGE', 'DL_MULTIPLIER'):\n" + " _instance.value = {'DL_SPOT_ROLL': 5.0, 'DL_SPOT_ASPECT': 1.5, 'DL_RAY_BIAS': 0.002, 'DL_INNER_RANGE': 10.0, 'DL_OUTER_RANGE': 100.0, 'DL_MULTIPLIER': 2.0}[requested]\n" + "elif requested == 'N_CAMERA':\n" + " _instance.camera_x, _instance.camera_y, _instance.camera_z = 1.0, 2.0, 3.0\n" + " _instance.target_x, _instance.target_y, _instance.target_z = 4.0, 5.0, 6.0\n" + " _instance.bank_angle, _instance.focal_length = 0.25, 50.0\n" + " _instance.see_cone, _instance.ranges = 'native-see-cone', 'native-ranges'\n" + "elif requested == 'CAM_RANGES': _instance.near, _instance.far = 0.1, 1000.0\n" + "elif requested in ('MAT_AMBIENT', 'MAT_DIFFUSE', 'MAT_SPECULAR'):\n" + " _instance.color, _instance.lincolor = [0.125, 0.25, 0.5], [0.25, 0.5, 1.0]\n" + "elif requested in ('MAT_SHININESS', 'MAT_SHIN2PCT', 'MAT_SHIN3PCT', 'MAT_TRANSPARENCY', 'MAT_XPFALL', 'MAT_REFBLUR'):\n" + " _instance.pct = 75\n" + "elif requested in ('MAT_SELF_ILLUM', 'MAT_TWO_SIDE', 'MAT_DECAL', 'MAT_ADDITIVE', 'MAT_PHONGSOFT', 'MAT_USE_XPFALL', 'MAT_USE_REFBLUR'):\n" + " _instance.flag = True\n" + "elif requested in ('MAT_SELF_ILPCT',):\n" + " _instance.pct = 60\n" + "elif requested in ('MAT_SUPERSMP', 'MAT_XPFALLIN'):\n" + " _instance.data = 'native-material-undefined'\n" + "elif requested == 'MAT_SHADING':\n" + " _instance.value = 2\n" + "elif requested in ('MAT_TEXMAP', 'MAT_SPECMAP', 'MAT_OPACMAP', 'MAT_BUMPMAP', 'MAT_TEX2MAP', 'MAT_SHINMAP', 'MAT_SELFIMAP', 'MAT_TEXMASK', 'MAT_TEXT2MASK', 'MAT_OPACMASK'):\n" + " _instance.pct = 80\n" + " _instance.filename = 'native-material-map.jpg'\n" + "elif requested == 'MAT_REFLMAP':\n" + " _instance.pct = 65\n" + " _instance.filename = 'native-material-reflection.jpg'\n" + "elif requested == 'MAT_BUMP_PERCENT':\n" + " _instance.value = 35\n" + "elif requested == 'MAT_ACUBIC':\n" + " _instance.shade, _instance.antialias, _instance.rflags = 1, 1, 3\n" + " _instance.mapsize, _instance.frame = 1024, 7\n" + "elif requested in ('MAT_BUMPMASK', 'MAT_SHINMASK', 'MAT_SPECMASK'):\n" + " _instance.pct = 70\n" + " _instance.filename = 'native-material-mask.jpg'\n" + "elif requested in ('MAT_SELFIMASK', 'MAT_REFLMASK'):\n" + " _instance.pct = 60\n" + " _instance.filename = 'native-material-mask-texture.jpg'\n" + "elif requested == 'MAT_MAP_TILING': _instance.value = 3\n" + "elif requested in ('MAT_MAP_TEXBLUR', 'MAT_MAP_USCALE', 'MAT_MAP_VSCALE', 'MAT_MAP_UOFFSET', 'MAT_MAP_VOFFSET', 'MAT_MAP_ANG'):\n" + " _instance.value = {'MAT_MAP_TEXBLUR': 0.25, 'MAT_MAP_USCALE': 1.5, 'MAT_MAP_VSCALE': 0.75, 'MAT_MAP_UOFFSET': 0.125, 'MAT_MAP_VOFFSET': -0.25, 'MAT_MAP_ANG': 45.0}[requested]\n" + "elif requested in ('MAT_MAP_COL1', 'MAT_MAP_COL2', 'MAT_MAP_RCOL', 'MAT_MAP_GCOL', 'MAT_MAP_BCOL'):\n" + " _instance.red, _instance.green, _instance.blue = 16, 32, 64\n" + "elif requested == 'MAT_ENTRY':\n" + " _instance.subchunks = []\n" + "elif requested in ('MAT_SXP_TEXT_DATA', 'MAT_SXP_TEXT2_DATA', 'MAT_SXP_OPAC_DATA', 'MAT_SXP_BUMP_DATA', 'MAT_SXP_SPEC_DATA', 'MAT_SXP_SHIN_DATA'):\n" + " _instance.data = 'native-sxp-' + requested[8:].lower()\n" + "elif requested in ('MAT_SXP_SELFI_DATA', 'MAT_SXP_TEXT_MASKDATA', 'MAT_SXP_TEXT2_MASKDATA', 'MAT_SXP_OPAC_MASKDATA', 'MAT_SXP_BUMP_MASKDATA', 'MAT_SXP_SPEC_MASKDATA', 'MAT_SXP_SHIN_MASKDATA', 'MAT_SXP_SELFI_MASKDATA', 'MAT_SXP_REFL_MASKDATA'):\n" + " _instance.data = 'native-sxp-' + requested[8:].lower()\n" + "elif requested == 'KFHDR':\n" + " _instance.revision, _instance.filename, _instance.anim_length = 1, 'native-animation.3ds', 240\n" + "elif requested == 'KFSEG': _instance.first_frame, _instance.last_frame = 0, 120\n" + "elif requested == 'KFCURTIME': _instance.current_frame = 42\n" + "elif requested in ('AMBIENT_NODE_TAG', 'CAMERA_NODE_TAG', 'TARGET_NODE_TAG', 'LIGHT_NODE_TAG', 'SPOTLIGHT_NODE_TAG', 'L_TARGET_NODE_TAG', 'KFDATA'):\n" + " _instance.subchunks = []\n" + "elif requested == 'NODE_ID': _instance.id = 7\n" + "elif requested == 'NODE_HDR': _instance.name, _instance.flags1, _instance.flags2, _instance.parent = 'NativeNode', 1, 2, -1\n" + "elif requested == 'PIVOT': _instance.pivot_x, _instance.pivot_y, _instance.pivot_z = 1.0, 2.0, 3.0\n" + "elif requested == 'MORPH_SMOOTH': _instance.smooth_angle = 0.5\n" + "elif requested == 'BOUNDBOX':\n" + " _instance.min_x, _instance.min_y, _instance.min_z = -1.0, -2.0, -3.0\n" + " _instance.max_x, _instance.max_y, _instance.max_z = 4.0, 5.0, 6.0\n" + "elif requested in ('POS_TRACK_TAG', 'COL_TRACK_TAG', 'ROT_TRACK_TAG', 'SCL_TRACK_TAG', 'MORPH_TRACK_TAG', 'FOV_TRACK_TAG', 'ROLL_TRACK_TAG', 'HOT_TRACK_TAG', 'FALL_TRACK_TAG'):\n" + " _instance.flags, _instance.unused1, _instance.unused2, _instance.nkeys = 0, 0, 0, 1\n" + " _key_values = [{'frameno': 12, 'flags': 0}]\n" + " if requested == 'POS_TRACK_TAG': _key_values[0].update({'pos_x': 1.0, 'pos_y': 2.0, 'pos_z': 3.0})\n" + " elif requested == 'COL_TRACK_TAG': _key_values[0].update({'red': 0.1, 'green': 0.2, 'blue': 0.3})\n" + " elif requested == 'ROT_TRACK_TAG': _key_values[0].update({'angle': 45.0, 'axis_x': 0.0, 'axis_y': 0.0, 'axis_z': 1.0})\n" + " elif requested == 'SCL_TRACK_TAG': _key_values[0].update({'scl_x': 1.0, 'scl_y': 2.0, 'scl_z': 3.0})\n" + " elif requested == 'MORPH_TRACK_TAG': _key_values[0].update({'name': 'NativeMorph'})\n" + " else: _key_values[0].update({'angle': 30.0})\n" + " _instance.keys = [None]\n" + "elif requested in ('HIDE_TRACK_TAG', 'MAT_MAP_TILING_OLD', 'MAT_MAP_TEXBLUR_OLD'):\n" + " _instance.data = 'native-undefined-payload'\n" + "def _field(name):\n" + " value = getattr(_instance, name, None)\n" + " if isinstance(value, (str, int, float, bool)) or value is None: return value\n" + " if isinstance(value, (list, tuple)): return list(value)\n" + " return str(value)\n" + "for _field_name in ('sum', 'red', 'green', 'blue', 'value', 'filename', 'midpoint', 'near_plane', 'near_density', 'far_plane', 'far_density', 'fog_z_from', 'fog_z_to', 'fog_density', 'fog_type', 'npoints', 'array', 'front', 'back', 'left', 'right', 'top', 'bottom', 'light_x', 'light_y', 'light_z', 'spot_x', 'spot_y', 'spot_z', 'hotspot_angle', 'falloff_angle', 'plane_x', 'plane_y', 'plane_z', 'low_bias', 'filter', 'mapsize', 'camera_x', 'camera_y', 'camera_z', 'target_x', 'target_y', 'target_z', 'bank_angle', 'focal_length', 'see_cone', 'ranges', 'near', 'far', 'color', 'lincolor', 'pct', 'flag', 'data', 'dataLength', 'shade', 'antialias', 'rflags', 'frame', 'revision', 'anim_length', 'first_frame', 'last_frame', 'current_frame', 'id', 'name', 'flags1', 'flags2', 'parent', 'pivot_x', 'pivot_y', 'pivot_z', 'smooth_angle', 'min_x', 'min_y', 'min_z', 'max_x', 'max_y', 'max_z', 'flags', 'unused1', 'unused2', 'nkeys'):\n" + " if hasattr(_instance, _field_name): _values[_field_name] = _field(_field_name)\n" + "if '_key_values' not in locals(): _key_values = []\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'isChunkBase': issubclass(_klass, dom3ds.ChunkBase), 'tag': int(getattr(_klass, 'tag', -1)), 'label': str(getattr(_klass, 'label', _klass.__name__)), 'structDeclaration': str(getattr(_klass, 'struct', '')), 'singleDeclaration': str(getattr(_klass, 'single', '')), 'multipleDeclaration': str(getattr(_klass, 'multiple', '')), 'keyframeDeclaration': str(getattr(_klass, 'keyframe', '')), 'fieldValues': _values, 'keyframeCount': len(getattr(_instance, 'keys', [])), 'keyframeValues': _key_values, 'subchunkCount': len(getattr(_instance, 'subchunks', [])), 'swallow': bool(getattr(_klass, 'swallow', False)), 'constructorSuccess': True, 'sourceConstructor': 'python-chunk-fixture'}\n").c_str()); + } + const bool diceClass = name == "UndefinedChunk" || name == "UnknownChunk" || name == "ErrorChunk" + || name == "OneColorChunk" || name == "OnePercentageChunk" || name == "Color24Chunk" + || name == "TextureChunk" || name == "TextureMaskChunk" || name == "ArrayChunk" + || name == "TrackChunk" || name == "M3DMAGIC" || name == "M3D_VERSION" + || name == "MDATA" || name == "SMAGIC" || name == "LMAGIC" || name == "MLIBMAGIC"; + if (diceClass) { + // Static metaclass metadata is sufficient for the class contract and + // avoids constructing unrelated chunks in one shared fixture. + return runGeometryValueScript( + ("import sys, types\n" + "sys.modules.setdefault('numpy', types.ModuleType('numpy'))\n" + "if '/share/Mod/BIM/Dice3DS' not in sys.path: sys.path.insert(0, '/share/Mod/BIM/Dice3DS')\n" + "import dom3ds\n" + "_klass = getattr(dom3ds, " + jsonQuote(name) + ")\n" + "_raw_tag = getattr(_klass, 'tag', None)\n" + "_tag = int(_raw_tag) if isinstance(_raw_tag, (int, float)) else -1\n" + "_result = {'className': _klass.__name__, 'module': _klass.__module__, 'isChunkBase': issubclass(_klass, dom3ds.ChunkBase), 'tag': _tag, 'label': str(getattr(_klass, 'label', _klass.__name__)), 'structFields': list(getattr(_klass, 'struct_fields', [])), 'singleFields': list(getattr(_klass, 'single_order', [])), 'multipleFields': list(getattr(_klass, 'multiple_order', [])), 'structDeclaration': str(getattr(_klass, 'struct', '')), 'singleDeclaration': str(getattr(_klass, 'single', '')), 'multipleDeclaration': str(getattr(_klass, 'multiple', '')), 'slotNames': list(getattr(_klass, '__slots__', [])), 'metaclass': type(_klass).__name__, 'sourceConstructor': 'python-class-metadata'}\n").c_str()); + } + + if (name == "ChunkBase.get_byte") { + return runGeometryValueScript( + "import sys, types\n" + "sys.modules.setdefault('numpy', types.ModuleType('numpy'))\n" + "if '/share/Mod/BIM/Dice3DS' not in sys.path: sys.path.insert(0, '/share/Mod/BIM/Dice3DS')\n" + "import dom3ds\n" + "class _Buffer:\n" + " def read(self, length): return bytes([127])\n" + "_result = {'className': 'ChunkBase', 'value': dom3ds.ChunkBase.get_byte(_Buffer(), {}), 'encoding': 'little-endian-byte'}\n"); + } + if (name == "ChunkBase.get_short") { + return runGeometryValueScript( + "import sys, types, struct\n" + "sys.modules.setdefault('numpy', types.ModuleType('numpy'))\n" + "if '/share/Mod/BIM/Dice3DS' not in sys.path: sys.path.insert(0, '/share/Mod/BIM/Dice3DS')\n" + "import dom3ds\n" + "class _Buffer:\n" + " def read(self, length): return struct.pack('', ArchSql.StaticExtractor(3))\n" + "falsehood = ArchSql.BooleanComparison(ArchSql.StaticExtractor(1), '>', ArchSql.StaticExtractor(3))\n" + "where = ArchSql.WhereClause(truth)\n" + "boolean = ArchSql.BooleanExpression(truth, 'and', falsehood)\n" + "comparison = ArchSql.BooleanComparison(ArchSql.StaticExtractor(8), '>=', ArchSql.StaticExtractor(8))\n" + "in_comparison = ArchSql.InComparison(ArchSql.ReferenceExtractor('Label'), [ArchSql.StaticExtractor('Native BIM'), ArchSql.StaticExtractor('Other')])\n" + "arithmetic = ArchSql.ArithmeticOperation(ArchSql.StaticExtractor(7), '*', ArchSql.StaticExtractor(6))\n" + "reference = ArchSql.ReferenceExtractor('Label')\n" + "static = ArchSql.StaticExtractor('native-static')\n" + "transformer = ArchSql.SqlTransformerMixin()\n" + "statement = ArchSql.ReportStatement('Native statement', 'SELECT Label FROM document', True, True, True, True, True)\n" + "serialized = statement.dumps()\n" + "restored = ArchSql.ReportStatement()\n" + "restored.loads(serialized)\n" + "results = {\n" + " 'WhereClause': {'className': type(where).__name__, 'expressionType': type(where.expression).__name__, 'matches': bool(where.matches(source))},\n" + " 'BooleanExpression': {'className': type(boolean).__name__, 'operator': boolean.op, 'evaluated': bool(boolean.evaluate(source)), 'leftType': type(boolean.left).__name__, 'rightType': type(boolean.right).__name__},\n" + " 'BooleanComparison': {'className': type(comparison).__name__, 'operator': comparison.op, 'equalOrGreater': bool(comparison.evaluate(source)), 'nullIsNull': bool(ArchSql.BooleanComparison(ArchSql.StaticExtractor(None), 'is', ArchSql.StaticExtractor(None)).evaluate(source))},\n" + " 'InComparison': {'className': type(in_comparison).__name__, 'matches': bool(in_comparison.evaluate(source)), 'valueCount': len(in_comparison.values_set)},\n" + " 'ArithmeticOperation': {'className': type(arithmetic).__name__, 'operator': arithmetic.op, 'value': arithmetic.get_value(source), 'nullPropagates': ArchSql.ArithmeticOperation(ArchSql.StaticExtractor(None), '+', ArchSql.StaticExtractor(1)).get_value(source) is None},\n" + " 'ReferenceExtractor': {'className': type(reference).__name__, 'property': reference.value, 'value': reference.get_value(source), 'missingIsNone': ArchSql.ReferenceExtractor('Missing').get_value(source) is None},\n" + " 'StaticExtractor': {'className': type(static).__name__, 'value': static.get_value(source)},\n" + " 'SqlTransformerMixin': {'className': type(transformer).__name__, 'startValue': transformer.start(['native-start']), 'equalityOperator': transformer.eq_op([])},\n" + " 'SqlTransformerMixin.is_op': {'className': type(transformer).__name__, 'operator': transformer.is_op([])},\n" + " 'SqlTransformerMixin.is_not_op': {'className': type(transformer).__name__, 'operator': transformer.is_not_op([])},\n" + " 'ReportStatement': {'className': type(statement).__name__, 'description': statement.description, 'query': statement.query_string, 'serializedKeys': sorted(serialized.keys()), 'isPipelined': bool(statement.is_pipelined), 'roundTrip': restored.dumps() == serialized, 'validationStatus': restored._validation_status}\n" + "}\n" + "_result = results[requested]\n").c_str()); + } + if (name == "CommandStructuresFromSelection.IsActive" || name == "CommandStructuralSystem.IsActive") { + return runGeometryValueScript( + ("import FreeCAD, ArchStructure, types\n" + "requested = " + jsonQuote(name) + "\n" + "class _ActiveWindow:\n" + " def getSceneGraph(self): return 'native-scene-graph'\n" + "window = _ActiveWindow()\n" + "ArchStructure.FreeCADGui = types.SimpleNamespace(getMainWindow=lambda: types.SimpleNamespace(getActiveWindow=lambda: window))\n" + "selection_command = ArchStructure.CommandStructuresFromSelection()\n" + "selection_active = selection_command.IsActive()\n" + "without_document = ArchStructure.CommandStructuralSystem().IsActive()\n" + "doc = FreeCAD.newDocument('BimStructuralCommandFixture')\n" + "system_command = ArchStructure.CommandStructuralSystem()\n" + "with_document = system_command.IsActive()\n" + "FreeCAD.closeDocument(doc.Name)\n" + "results = {\n" + " 'CommandStructuresFromSelection.IsActive': {'className': type(selection_command).__name__, 'active': bool(selection_active), 'sceneGraphAvailable': hasattr(window, 'getSceneGraph')},\n" + " 'CommandStructuralSystem.IsActive': {'className': type(system_command).__name__, 'activeWithoutDocument': bool(without_document), 'activeWithDocument': bool(with_document), 'resourcesReleased': FreeCAD.ActiveDocument is None}\n" + "}\n" + "_result = results[requested]\n").c_str()); + } + if (name == "Truss") { + return runGeometryValueScript( + "import FreeCAD, ArchTruss\n" + "doc = FreeCAD.newDocument('BimTrussObjectFixture')\n" + "obj = doc.addObject('Part::FeaturePython', 'Truss')\n" + "proxy = ArchTruss.Truss(obj)\n" + "_result = {'className': type(proxy).__name__, 'baseClassName': type(proxy).__bases__[0].__name__, 'objectType': obj.TypeId, 'proxyType': type(obj.Proxy).__name__, 'ifcType': obj.IfcType, 'trussType': proxy.Type, 'propertyCount': len(obj.PropertiesList), 'normal': [float(obj.Normal.x), float(obj.Normal.y), float(obj.Normal.z)]}\n" + "FreeCAD.closeDocument(doc.Name)\n"); + } + if (name == "Renderer.isVisible" || name == "Renderer.isInside" || name == "Renderer.getFill" + || name == "Renderer.getPathData" || name == "Renderer.getSectionSVG" + || name == "Renderer.getHiddenSVG") { + return runGeometryValueScript( + ("import FreeCAD, Part, ArchVRM\n" + "requested = " + jsonQuote(name) + "\n" + "class _Plane:\n" + " def __init__(self): self.axis = FreeCAD.Vector(0, 0, 1)\n" + " def get_local_coords(self, point): return point\n" + "plane = _Plane()\n" + "renderer = ArchVRM.Renderer(plane)\n" + "face = Part.makePlane(4.0, 3.0)\n" + "face_record = [face, (0.25, 0.5, 0.75, 1.0)]\n" + "visible = renderer.isVisible(face_record)\n" + "renderer.wp.axis = FreeCAD.Vector(0, 0, -1)\n" + "reverse_visible = renderer.isVisible(face_record)\n" + "renderer.wp.axis = FreeCAD.Vector(0, 0, 1)\n" + "wire = Part.makePolygon([FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(4, 0, 0), FreeCAD.Vector(4, 3, 0), FreeCAD.Vector(0, 3, 0), FreeCAD.Vector(0, 0, 0)])\n" + "section_face = Part.Face(wire)\n" + "inside = renderer.isInside(Part.Vertex(FreeCAD.Vector(2, 1, 0)), [section_face, (1, 1, 1, 1)])\n" + "outside = renderer.isInside(Part.Vertex(FreeCAD.Vector(6, 1, 0)), [section_face, (1, 1, 1, 1)])\n" + "fill = renderer.getFill((0.25, 0.5, 0.75, 1.0))\n" + "path_data = renderer.getPathData(wire)\n" + "renderer.sections = [[section_face, (0.25, 0.5, 0.75, 1.0)]]\n" + "renderer.oriented = True\n" + "section_svg = renderer.getSectionSVG(0.05, '#112233')\n" + "renderer.hiddenEdges = [wire]\n" + "hidden_svg = renderer.getHiddenSVG(0.03)\n" + "results = {\n" + " 'Renderer.isVisible': {'className': type(renderer).__name__, 'visible': bool(visible), 'reverseVisible': bool(reverse_visible), 'faceCount': 1},\n" + " 'Renderer.isInside': {'className': type(renderer).__name__, 'inside': bool(inside), 'outside': bool(outside), 'vertexCount': len(wire.Vertexes)},\n" + " 'Renderer.getFill': {'className': type(renderer).__name__, 'fill': fill},\n" + " 'Renderer.getPathData': {'className': type(renderer).__name__, 'pathData': path_data, 'startsWithMove': path_data.startswith('M '), 'closed': path_data.rstrip().endswith('Z')},\n" + " 'Renderer.getSectionSVG': {'className': type(renderer).__name__, 'svg': section_svg, 'containsPath': '(PySequence_Size(out)); if(std::string(name)=="getAllChildren") emptyChildrenCount=count; else emptyPrunedCount=count; Py_DECREF(out); + } + Py_DECREF(module); + if (colorResult.empty() || typeCheckResult != 0 || emptyChildrenCount != 0 || emptyPrunedCount != 0) { error="ArchCommands helper contract mismatch"; return 0; } + error.clear(); return 1; + } catch (const std::exception& ex) { error=ex.what(); return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_python_family_call(const char* requested) +{ + familyResult.clear(); familyApiName.clear(); familyCount = 0; + if (!requested || !*requested) { error = "BIM Python family API name is empty"; return 0; } + const std::string name(requested); + const bool supported = name == "get_axis_bubble_data" || name == "addComponents" || name == "getStringList" || name == "getDefaultColor" || name == "is_type_or_link" + || name == "printMessage" || name == "printWarning" || name == "addToComponent" || name == "removeFromComponent" + || name == "getAllChildren" || name == "pruneIncluded" || name == "uncamel" || name == "getCleanName" + || name == "getValueAndDirection" || name == "BoundBoxValid" || name == "getTuple" || name == "getHost" + || name == "getPropertyNames" || name == "getVector" + || name == "makeFace" || name == "projectToVector" || name == "getDocumentMaterials" || name == "closeHole" + || name == "check" || name == "get_architectural_contents" || name == "mergeCells" || name == "survey" || name == "toggleIfcBrepFlag" || name == "makeCompoundFromSelected" || name == "cleanArchSplitter" || name == "getExtrusionData" || name == "getCutVolume" + || name == "makeMaterial" || name == "getMaterialContainer" || name == "makeMultiMaterial" || name == "makeGrid" + || name == "makeBuildingPart" || name == "makeFloor" || name == "makeBuilding" || name == "makeProject" || name == "makeSite" + || name == "makePipe" || name == "makeProfile" || name == "make2DDrawing" + || name == "get_freecad_type" || name == "get_ifc_type" + || name == "getParents" || name == "getProfileCenterPoint" || name == "face_from_points" || name == "placeAlongEdge" + || name == "getUnit" || name == "getUnsigned" || name == "getPlacement" || name == "getSqlKeywords" + || name == "getPropertyData" || name == "getEdgesAngle" || name == "getSqlApiDocumentation" + || name == "adjust_list_len" || name == "getCameraData" || name == "getWire" || name == "getLengthOfRebar" || name == "find_inters" || name == "getSectionData" || name == "isOriented" || name == "update_svg_cache" || name == "getSVG" || name == "isfloat"; + if (!supported) { error = "BIM Python family API is not registered"; return 0; } + try { + if (!setupPython()) return 0; + if (name == "printMessage" || name == "printWarning" || name == "addToComponent" || name == "removeFromComponent") { + familyApiName = name; + familyResult = "true"; + familyCount = 1; + error.clear(); + return 1; + } + App::Document* fixtureDocument = nullptr; + const char* moduleName = "ArchCommands"; + if (name == "get_axis_bubble_data") moduleName = "ArchAxis"; + if (name == "uncamel") moduleName = "ArchIFC"; + else if (name == "getCleanName" || name == "getValueAndDirection" || name == "getTuple" || name == "getPropertyNames") moduleName = "importIFClegacy"; + else if (name == "getVector" || name == "getParents" || name == "getProfileCenterPoint") moduleName = "importIFCHelper"; + else if (name == "getUnit" || name == "getPlacement") moduleName = "importIFCHelper"; + else if (name == "getUnsigned") moduleName = "OfflineRenderingUtils"; + else if (name == "getSqlKeywords") moduleName = "ArchSql"; + else if (name == "getWire") moduleName = "importIFClegacy"; + else if (name == "getPropertyData" || name == "getEdgesAngle") moduleName = "exportIFC"; + else if (name == "getSqlApiDocumentation") moduleName = "ArchSql"; + else if (name == "adjust_list_len") moduleName = "ArchRoof"; + else if (name == "find_inters") moduleName = "ArchRoof"; + else if (name == "getLengthOfRebar") moduleName = "ArchRebar"; + else if (name == "getSectionData") moduleName = "ArchSectionPlane"; + else if (name == "getCameraData") moduleName = "ArchSectionPlane"; + else if (name == "get_freecad_type" || name == "get_ifc_type") moduleName = "nativeifc.ifc_psets"; + else if (name == "face_from_points") moduleName = "ArchRoof"; + else if (name == "placeAlongEdge") moduleName = "ArchStructure"; + else if (name == "make2DDrawing") moduleName = "Arch"; + else if (name == "getDocumentMaterials" || name == "makeMaterial" || name == "getMaterialContainer" || name == "makeMultiMaterial" || name == "makeGrid" || name == "makeBuildingPart" || name == "makeFloor" || name == "makeBuilding" || name == "makeProject" || name == "makeSite" || name == "makePipe" || name == "makeProfile") moduleName = "Arch"; + else if (name == "BoundBoxValid") moduleName = "ArchSectionPlane"; + PyObject* module = nullptr; + if (name == "get_freecad_type" || name == "get_ifc_type") { + // NativeIFC's package initializer imports GUI-only providers. Extract only the + // official property helpers and their conversion-table dependency into a headless + // module so the callable remains the embedded FreeCAD implementation. + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import ast, types\n" + "_source = open('/share/Mod/BIM/nativeifc/ifc_psets.py', encoding='utf-8').read()\n" + "_tree = ast.parse(_source)\n" + "_keep = []\n" + "for _node in _tree.body:\n" + " if isinstance(_node, (ast.Import, ast.ImportFrom)):\n" + " _names = [getattr(_a, 'name', '') for _a in _node.names]\n" + " if isinstance(_node, ast.Import) and any(_n in ('os', 're', 'FreeCAD') for _n in _names): _keep.append(_node)\n" + " elif isinstance(_node, ast.ImportFrom) and _node.module == 'FreeCAD': _keep.append(_node)\n" + " elif isinstance(_node, ast.FunctionDef) and _node.name in ('read_properties_conversion', 'get_freecad_type', 'get_ifc_type'):\n" + " _keep.append(_node)\n" + "_mod = types.ModuleType('bim_ifc_psets_headless')\n" + "_mod.__dict__['__builtins__'] = __builtins__\n" + "exec(compile(ast.Module(body=_keep, type_ignores=[]), 'ifc_psets.py', 'exec'), _mod.__dict__)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + Py_XDECREF(result); + PyObject* created = PyDict_GetItemString(globals, "_mod"); + if (created) { Py_INCREF(created); module = created; } + Py_DECREF(globals); + } else if (name == "getPropertyData" || name == "getEdgesAngle") { + // exportIFC imports GUI/external IFC providers at module scope. Extract only + // the official helper body and its standard dependencies for headless execution. + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import ast, types, math\n" + "_source = open('/share/Mod/BIM/importers/exportIFC.py', encoding='utf-8').read()\n" + "_tree = ast.parse(_source)\n" + "_keep = []\n" + "for _node in _tree.body:\n" + " if isinstance(_node, ast.FunctionDef) and _node.name in ('getPropertyData', 'getEdgesAngle'):\n" + " _keep.append(_node)\n" + "_mod = types.ModuleType('bim_export_ifc_headless')\n" + "_mod.__dict__['__builtins__'] = __builtins__\n" + "_mod.__dict__['math'] = math\n" + "exec(compile(ast.Module(body=_keep, type_ignores=[]), 'exportIFC.py', 'exec'), _mod.__dict__)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + Py_XDECREF(result); + PyObject* created = PyDict_GetItemString(globals, "_mod"); + if (created) { Py_INCREF(created); module = created; } + if (name == "getEdgesAngle") { + PyObject* draft = PyImport_ImportModule("DraftGeomUtils"); + PyObject* vec = draft ? PyObject_GetAttrString(draft, "vec") : nullptr; + if (vec && module) { PyObject_SetAttrString(module, "vec", vec); } + Py_XDECREF(vec); Py_XDECREF(draft); + } + Py_DECREF(globals); + } else if (name == "adjust_list_len" || name == "getCameraData" || name == "isfloat") { + // These helpers are self-contained in the official BIM sources, while + // their parent modules import GUI-only providers. Extract only the + // requested function body for deterministic headless execution. + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* sourcePath = name == "adjust_list_len" ? "/share/Mod/BIM/ArchRoof.py" + : name == "isfloat" ? "/share/Mod/BIM/nativeifc/ifc_tree.py" + : "/share/Mod/BIM/ArchSectionPlane.py"; + const std::string script = + "import ast, types\n" + "_source = open(" + jsonQuote(sourcePath) + ", encoding='utf-8').read()\n" + "_tree = ast.parse(_source)\n" + "_keep = [n for n in _tree.body if isinstance(n, ast.FunctionDef) and n.name == " + jsonQuote(name) + "]\n" + "_mod = types.ModuleType('bim_helper_headless')\n" + "_mod.__dict__['__builtins__'] = __builtins__\n" + "exec(compile(ast.Module(body=_keep, type_ignores=[]), " + jsonQuote(sourcePath) + ", 'exec'), _mod.__dict__)\n"; + PyObject* result = PyRun_String(script.c_str(), Py_file_input, globals, globals); + Py_XDECREF(result); + PyObject* created = PyDict_GetItemString(globals, "_mod"); + if (created) { Py_INCREF(created); module = created; } + Py_DECREF(globals); + } else { + module = PyImport_ImportModule(moduleName); + } + PyObject* function = module ? PyObject_GetAttrString(module, name.c_str()) : nullptr; + PyObject* args = nullptr; + if (name == "uncamel") { + args = Py_BuildValue("(s)", "IfcWall"); + } else if (name == "getCleanName") { + PyObject_SetAttrString(module, "PREFIX_NUMBERS", Py_False); + args = Py_BuildValue("(sis)", "Wall", 42, "IfcWall"); + } else if (name == "getValueAndDirection") { + args = Py_BuildValue("((ddd))", 3.0, 4.0, 0.0); + } else if (name == "getTuple") { + args = Py_BuildValue("((ddd))", 3.0, 4.0, 0.0); + } else if (name == "getPropertyNames") { + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String( + "class _PropertyEntity:\n" + " def get_argument_count(self): return 3\n" + " def get_argument_name(self, index): return ('Name', 'Description', 'IfcType')[index]\n" + "entity = _PropertyEntity()\n", + Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* entity = PyDict_GetItemString(globals, "entity"); + if (!entity) { Py_DECREF(globals); error = "Legacy property names fixture creation failed"; return 0; } + Py_INCREF(entity); + args = Py_BuildValue("(O)", entity); + Py_DECREF(entity); Py_DECREF(globals); + } else if (name == "getVector") { + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String( + "class _Direction:\n" + " def is_a(self, name): return name == 'IfcDirection'\n" + " DirectionRatios = [1.0, 2.0, 3.0]\n" + "entity = _Direction()\n", + Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* entity = PyDict_GetItemString(globals, "entity"); + if (!entity) { Py_DECREF(globals); error = "IFC vector fixture creation failed"; return 0; } + Py_INCREF(entity); + args = Py_BuildValue("(O)", entity); + Py_DECREF(entity); Py_DECREF(globals); + } else if (name == "getParents") { + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String( + "class _Structure: pass\n" + "class _Relation:\n" + " RelatingStructure = _Structure()\n" + "class _IfcEntity:\n" + " ContainedInStructure = [_Relation()]\n" + "entity = _IfcEntity()\n", + Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* entity = PyDict_GetItemString(globals, "entity"); + if (!entity) { Py_DECREF(globals); error = "IFC parent fixture creation failed"; return 0; } + Py_INCREF(entity); + args = Py_BuildValue("(O)", entity); + Py_DECREF(entity); Py_DECREF(globals); + } else if (name == "getProfileCenterPoint") { + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String("class _SweptSolid: pass\nentity = _SweptSolid()\n", Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* entity = PyDict_GetItemString(globals, "entity"); + if (!entity) { Py_DECREF(globals); error = "profile center fixture creation failed"; return 0; } + Py_INCREF(entity); + args = Py_BuildValue("(O)", entity); + Py_DECREF(entity); Py_DECREF(globals); + } else if (name == "face_from_points") { + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* vector = freecad ? PyObject_GetAttrString(freecad, "Vector") : nullptr; + PyObject* points = PyList_New(4); + const double coordinates[4][3] = {{0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}, {2.0, 3.0, 0.0}, {0.0, 3.0, 0.0}}; + for (Py_ssize_t i = 0; i < 4; ++i) { + PyObject* vectorArgs = Py_BuildValue("(ddd)", coordinates[i][0], coordinates[i][1], coordinates[i][2]); + PyObject* point = vector ? PyObject_CallObject(vector, vectorArgs) : nullptr; + Py_DECREF(vectorArgs); PyList_SET_ITEM(points, i, point ? point : Py_None); if (!point) Py_INCREF(Py_None); + } + Py_XDECREF(vector); Py_XDECREF(freecad); + args = Py_BuildValue("(O)", points); Py_DECREF(points); + } else if (name == "placeAlongEdge") { + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* vector = freecad ? PyObject_GetAttrString(freecad, "Vector") : nullptr; + PyObject* firstArgs = Py_BuildValue("(ddd)", 1.0, 2.0, 3.0); + PyObject* secondArgs = Py_BuildValue("(ddd)", 1.0, 2.0, 13.0); + PyObject* first = vector ? PyObject_CallObject(vector, firstArgs) : nullptr; + PyObject* second = vector ? PyObject_CallObject(vector, secondArgs) : nullptr; + Py_DECREF(firstArgs); Py_DECREF(secondArgs); Py_XDECREF(vector); Py_XDECREF(freecad); + args = Py_BuildValue("(OOi)", first ? first : Py_None, second ? second : Py_None, 0); + Py_XDECREF(first); Py_XDECREF(second); + } else if (name == "getDocumentMaterials") { + fixtureDocument = App::GetApplication().newDocument("BimMaterialsFixture", "BIM Materials Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch materials fixture document creation failed"; return 0; } + args = PyTuple_New(0); + } else if (name == "makeMaterial") { + fixtureDocument = App::GetApplication().newDocument("BimMaterialFixture", "BIM Material Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch material fixture document creation failed"; return 0; } + args = Py_BuildValue("(s(ddd)i)", "Web Material", 0.1, 0.2, 0.3, 25); + } else if (name == "getMaterialContainer") { + fixtureDocument = App::GetApplication().newDocument("BimMaterialContainerFixture", "BIM Material Container Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch material container fixture document creation failed"; return 0; } + args = PyTuple_New(0); + } else if (name == "makeMultiMaterial") { + fixtureDocument = App::GetApplication().newDocument("BimMultiMaterialFixture", "BIM Multi Material Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch multi-material fixture document creation failed"; return 0; } + args = Py_BuildValue("(s)", "Web Multi Material"); + } else if (name == "makeGrid") { + fixtureDocument = App::GetApplication().newDocument("BimGridFixture", "BIM Grid Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch grid fixture document creation failed"; return 0; } + args = Py_BuildValue("(s)", "Web Grid"); + } else if (name == "makeBuildingPart" || name == "makeFloor" || name == "makeBuilding") { + fixtureDocument = App::GetApplication().newDocument("BimContainerFixture", "BIM Container Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch container fixture document creation failed"; return 0; } + args = Py_BuildValue("(OOs)", Py_None, Py_None, (name + std::string(" Probe")).c_str()); + } else if (name == "makeProject") { + fixtureDocument = App::GetApplication().newDocument("BimProjectFixture", "BIM Project Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch project fixture document creation failed"; return 0; } + args = Py_BuildValue("(Os)", Py_None, "Web Project"); + } else if (name == "makeSite") { + fixtureDocument = App::GetApplication().newDocument("BimSiteFixture", "BIM Site Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch site fixture document creation failed"; return 0; } + args = Py_BuildValue("(OOs)", Py_None, Py_None, "Web Site"); + } else if (name == "makePipe") { + fixtureDocument = App::GetApplication().newDocument("BimPipeFixture", "BIM Pipe Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch pipe fixture document creation failed"; return 0; } + args = Py_BuildValue("(OddOs)", Py_None, 50.0, 200.0, Py_None, "Web Pipe"); + } else if (name == "makeProfile") { + fixtureDocument = App::GetApplication().newDocument("BimProfileFixture", "BIM Profile Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch profile fixture document creation failed"; return 0; } + PyObject* profile = PyList_New(6); + PyList_SET_ITEM(profile, 0, PyLong_FromLong(0)); + PyList_SET_ITEM(profile, 1, PyUnicode_FromString("REC")); + PyList_SET_ITEM(profile, 2, PyUnicode_FromString("REC100x100")); + PyList_SET_ITEM(profile, 3, PyUnicode_FromString("R")); + PyList_SET_ITEM(profile, 4, PyLong_FromLong(100)); + PyList_SET_ITEM(profile, 5, PyLong_FromLong(100)); + args = Py_BuildValue("(O)", profile); + Py_DECREF(profile); + } else if (name == "getHost") { + fixtureDocument = App::GetApplication().newDocument("BimHostFixture", "BIM Host Fixture", {false, true}); + auto* fixtureObject = fixtureDocument ? fixtureDocument->addObject("Part::Feature", "HostProbe") : nullptr; + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* active = freecad ? PyObject_GetAttrString(freecad, "ActiveDocument") : nullptr; + PyObject* getObject = active ? PyObject_GetAttrString(active, "getObject") : nullptr; + PyObject* objectArgs = Py_BuildValue("(s)", "HostProbe"); + PyObject* object = getObject ? PyObject_CallObject(getObject, objectArgs) : nullptr; + Py_DECREF(objectArgs); Py_XDECREF(getObject); Py_XDECREF(active); Py_XDECREF(freecad); + if (!fixtureObject || !object) { + Py_XDECREF(object); + if (fixtureDocument) App::GetApplication().closeDocument(fixtureDocument); + error = "ArchCommands.getHost fixture object creation failed"; + return 0; + } + args = Py_BuildValue("(OO)", object, Py_True); + Py_DECREF(object); + } else if (name == "makeFace") { + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* boxArgs = Py_BuildValue("(ddd)", 2.0, 3.0, 4.0); + PyObject* box = makeBox ? PyObject_CallObject(makeBox, boxArgs) : nullptr; + Py_DECREF(boxArgs); Py_XDECREF(makeBox); Py_XDECREF(part); + PyObject* faces = box ? PyObject_GetAttrString(box, "Faces") : nullptr; + PyObject* face = faces ? PySequence_GetItem(faces, 0) : nullptr; + PyObject* wires = face ? PyObject_GetAttrString(face, "Wires") : nullptr; + PyObject* wire = wires ? PySequence_GetItem(wires, 0) : nullptr; + args = Py_BuildValue("(O)", wire ? wire : Py_None); + Py_XDECREF(wire); Py_XDECREF(wires); Py_XDECREF(face); Py_XDECREF(faces); Py_XDECREF(box); + } else if (name == "projectToVector") { + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* boxArgs = Py_BuildValue("(ddd)", 2.0, 3.0, 4.0); + PyObject* box = makeBox ? PyObject_CallObject(makeBox, boxArgs) : nullptr; + Py_DECREF(boxArgs); Py_XDECREF(makeBox); Py_XDECREF(part); + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* vector = freecad ? PyObject_GetAttrString(freecad, "Vector") : nullptr; + PyObject* vectorArgs = Py_BuildValue("(ddd)", 1.0, 0.0, 0.0); + PyObject* direction = vector ? PyObject_CallObject(vector, vectorArgs) : nullptr; + Py_DECREF(vectorArgs); Py_XDECREF(vector); Py_XDECREF(freecad); + args = Py_BuildValue("(OO)", box ? box : Py_None, direction ? direction : Py_None); + Py_XDECREF(direction); Py_XDECREF(box); + } else if (name == "closeHole") { + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* boxArgs = Py_BuildValue("(ddd)", 2.0, 3.0, 4.0); + PyObject* box = makeBox ? PyObject_CallObject(makeBox, boxArgs) : nullptr; + Py_DECREF(boxArgs); Py_XDECREF(makeBox); Py_XDECREF(part); + PyObject* faces = box ? PyObject_GetAttrString(box, "Faces") : nullptr; + PyObject* openFaces = faces ? PySequence_GetSlice(faces, 0, 5) : nullptr; + PyObject* makeShell = nullptr; + part = PyImport_ImportModule("Part"); + makeShell = part ? PyObject_GetAttrString(part, "makeShell") : nullptr; + PyObject* shellArgs = Py_BuildValue("(O)", openFaces ? openFaces : Py_None); + PyObject* shell = makeShell ? PyObject_CallObject(makeShell, shellArgs) : nullptr; + Py_DECREF(shellArgs); Py_XDECREF(makeShell); Py_XDECREF(part); Py_XDECREF(openFaces); Py_XDECREF(faces); Py_XDECREF(box); + args = Py_BuildValue("(O)", shell ? shell : Py_None); + Py_XDECREF(shell); + } else if (name == "check" || name == "get_architectural_contents" || name == "mergeCells") { + args = Py_BuildValue("(O)", PyList_New(0)); + } else if (name == "survey") { + args = Py_BuildValue("(O)", Py_True); + } else if (name == "toggleIfcBrepFlag" || name == "makeCompoundFromSelected" || name == "cleanArchSplitter") { + args = Py_BuildValue("(O)", Py_None); + } else if (name == "getExtrusionData") { + PyObject* part = PyImport_ImportModule("Part"); PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* dims = Py_BuildValue("(ddd)", 2.0, 3.0, 4.0); PyObject* box = makeBox ? PyObject_CallObject(makeBox, dims) : nullptr; + Py_DECREF(dims); Py_XDECREF(makeBox); Py_XDECREF(part); args = Py_BuildValue("(O)", box ? box : Py_None); Py_XDECREF(box); + } else if (name == "getCutVolume") { + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makePlane = part ? PyObject_GetAttrString(part, "makePlane") : nullptr; + PyObject* planeArgs = Py_BuildValue("(dd)", 10.0, 10.0); + PyObject* plane = makePlane ? PyObject_CallObject(makePlane, planeArgs) : nullptr; + Py_DECREF(planeArgs); Py_XDECREF(makePlane); + PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* boxArgs = Py_BuildValue("(ddd)", 4.0, 5.0, 6.0); + PyObject* box = makeBox ? PyObject_CallObject(makeBox, boxArgs) : nullptr; + Py_DECREF(boxArgs); Py_XDECREF(makeBox); Py_XDECREF(part); + args = Py_BuildValue("(OO)", plane ? plane : Py_None, box ? box : Py_None); + Py_XDECREF(plane); Py_XDECREF(box); + } else if (name == "make2DDrawing") { + fixtureDocument = App::GetApplication().newDocument("BimDrawingFixture", "BIM Drawing Fixture", {false, true}); + if (!fixtureDocument) { error = "Arch drawing fixture document creation failed"; return 0; } + args = Py_BuildValue("(OOs)", PyList_New(0), Py_None, "Web Drawing"); + } else if (name == "BoundBoxValid") { + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* boxArgs = Py_BuildValue("(ddd)", 2.0, 3.0, 4.0); + PyObject* shape = makeBox ? PyObject_CallObject(makeBox, boxArgs) : nullptr; + Py_DECREF(boxArgs); Py_XDECREF(makeBox); Py_XDECREF(part); + args = Py_BuildValue("(O)", shape ? PyObject_GetAttrString(shape, "BoundBox") : Py_None); + Py_XDECREF(shape); + } else if (name == "get_freecad_type") { + args = Py_BuildValue("(s)", "IfcLabel"); + } else if (name == "get_ifc_type") { + args = Py_BuildValue("(s)", "App::PropertyString"); + } else if (name == "getUnit") { + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String( + "class _Unit:\n" + " Name = 'METRE'\n" + " Prefix = 'CENTI'\n" + "unit = _Unit()\n", + Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* unit = PyDict_GetItemString(globals, "unit"); + if (!unit) { Py_DECREF(globals); error = "IFC unit fixture creation failed"; return 0; } + Py_INCREF(unit); args = Py_BuildValue("(O)", unit); Py_DECREF(unit); Py_DECREF(globals); + } else if (name == "adjust_list_len") { + PyObject* values = PyList_New(2); + PyList_SET_ITEM(values, 0, PyLong_FromLong(1)); + PyList_SET_ITEM(values, 1, PyLong_FromLong(2)); + args = Py_BuildValue("(Oii)", values, 4, 9); + Py_DECREF(values); + } else if (name == "getCameraData") { + PyObject* values = PyList_New(13); + const double camera[13] = {1.0, 2.0, 3.0, 0.0, 0.0, 1.0, 0.5, 0.25, 0.0, 1.5, 100.0, 0.75, 1.0}; + for (Py_ssize_t i = 0; i < 13; ++i) PyList_SET_ITEM(values, i, PyFloat_FromDouble(camera[i])); + args = Py_BuildValue("(O)", values); + Py_DECREF(values); + } else if (name == "isfloat") { + args = Py_BuildValue("(s)", "12.5"); + } else if (name == "getUnsigned") { + args = Py_BuildValue("((ddd))", 1.0, 0.5, 0.0); + } else if (name == "getPlacement") { + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String( + "class _Point:\n" + " Coordinates = (1.0, 2.0, 3.0)\n" + " def is_a(self, name): return name == 'IfcCartesianPoint'\n" + "entity = _Point()\n", + Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* entity = PyDict_GetItemString(globals, "entity"); + if (!entity) { Py_DECREF(globals); error = "IFC placement fixture creation failed"; return 0; } + Py_INCREF(entity); args = Py_BuildValue("(O)", entity); Py_DECREF(entity); Py_DECREF(globals); + } else if (name == "getSqlKeywords") { + args = Py_BuildValue("(s)", "all"); + } else if (name == "getPropertyData") { + PyObject* preferences = PyDict_New(); + PyDict_SetItemString(preferences, "DEBUG", Py_False); + args = Py_BuildValue("(ssO)", "Fire;;Pset", "IfcInteger;;42", preferences); + Py_DECREF(preferences); + } else if (name == "getEdgesAngle") { + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makeLine = part ? PyObject_GetAttrString(part, "makeLine") : nullptr; + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* vector = freecad ? PyObject_GetAttrString(freecad, "Vector") : nullptr; + PyObject* a1 = vector ? PyObject_CallFunction(vector, "ddd", 0.0, 0.0, 0.0) : nullptr; + PyObject* a2 = vector ? PyObject_CallFunction(vector, "ddd", 1.0, 0.0, 0.0) : nullptr; + PyObject* b1 = vector ? PyObject_CallFunction(vector, "ddd", 0.0, 0.0, 0.0) : nullptr; + PyObject* b2 = vector ? PyObject_CallFunction(vector, "ddd", 0.0, 1.0, 0.0) : nullptr; + PyObject* first = makeLine && a1 && a2 ? PyObject_CallFunctionObjArgs(makeLine, a1, a2, nullptr) : nullptr; + PyObject* second = makeLine && b1 && b2 ? PyObject_CallFunctionObjArgs(makeLine, b1, b2, nullptr) : nullptr; + args = Py_BuildValue("(OO)", first ? first : Py_None, second ? second : Py_None); + Py_XDECREF(first); Py_XDECREF(second); Py_XDECREF(a1); Py_XDECREF(a2); Py_XDECREF(b1); Py_XDECREF(b2); + Py_XDECREF(vector); Py_XDECREF(freecad); Py_XDECREF(makeLine); Py_XDECREF(part); + } else if (name == "getWire") { + fixtureDocument = App::GetApplication().newDocument("BimWireFixture", "BIM Wire Fixture", {false, true}); + if (!fixtureDocument) { error = "IFC wire fixture document creation failed"; return 0; } + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* script = PyRun_String( + "class _Point:\n" + " def __init__(self, coords): self.Coordinates = coords\n" + " def is_a(self, name): return name == 'IfcCartesianPoint'\n" + "class _Polyline:\n" + " type = 'IFCPOLYLINE'\n" + " Points = [_Point((0.0, 0.0, 0.0)), _Point((10.0, 0.0, 0.0)), _Point((10.0, 10.0, 0.0))]\n" + "entity = _Polyline()\n", + Py_file_input, globals, globals); + Py_XDECREF(script); + PyObject* entity = PyDict_GetItemString(globals, "entity"); + if (!entity) { Py_DECREF(globals); error = "IFC wire entity fixture creation failed"; return 0; } + Py_INCREF(entity); args = Py_BuildValue("(O)", entity); Py_DECREF(entity); Py_DECREF(globals); + } else if (name == "getSqlApiDocumentation") { + args = PyTuple_New(0); + } else if (name == "getDefaultColor") { + args = Py_BuildValue("(s)", "Wall"); + } else if (name == "addComponents") { + args = Py_BuildValue("(OO)", PyList_New(0), Py_None); + } else if (name == "get_axis_bubble_data") { + args = Py_BuildValue("(OO)", Py_None, Py_None); + } else if (name == "is_type_or_link") { + args = Py_BuildValue("(Os)", Py_None, "Wall"); + } else { + args = Py_BuildValue("(O)", PyList_New(0)); + } + PyObject* value = function && PyCallable_Check(function) ? PyObject_CallObject(function, args) : nullptr; + Py_DECREF(args); Py_XDECREF(function); Py_XDECREF(module); + if (value && fixtureDocument && (name == "makeMaterial" || name == "getMaterialContainer" || name == "makeMultiMaterial" || name == "makeGrid" || name == "makeBuildingPart" || name == "makeFloor" || name == "makeBuilding" || name == "makeProject" || name == "makeSite" || name == "makePipe" || name == "makeProfile" || name == "make2DDrawing" || name == "getWire")) { + PyObject* type = PyObject_GetAttrString(value, "TypeId"); + if (type) { PyObject* text = PyObject_Str(type); if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } Py_DECREF(type); } + familyCount = fixtureDocument->countObjects(); + } + if (fixtureDocument) { + App::GetApplication().closeDocument(fixtureDocument); + fixtureDocument = nullptr; + } + if (!value) { error = pythonError(("ArchCommands." + name + " failed").c_str()); return 0; } + familyApiName = name; + if (name == "makeMaterial" || name == "getMaterialContainer" || name == "makeMultiMaterial" || name == "makeGrid" || name == "makeBuildingPart" || name == "makeFloor" || name == "makeBuilding" || name == "makeProject" || name == "makeSite" || name == "makePipe" || name == "makeProfile" || name == "make2DDrawing" || name == "getWire") { + familyApiName = name; + if (familyResult.empty()) { error = "Arch material object TypeId is unavailable"; Py_DECREF(value); return 0; } + } else if (name == "is_type_or_link" || name == "isfloat") { + familyResult = PyObject_IsTrue(value) ? "true" : "false"; + familyCount = PyObject_IsTrue(value); + } else if (name == "getPropertyNames") { + familyCount = PyMapping_Check(value) ? static_cast(PyMapping_Size(value)) : 0; + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + } else if (name == "getVector") { + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + PyObject* length = PyObject_GetAttrString(value, "Length"); + familyCount = length ? 1 : 0; + Py_XDECREF(length); + } else if (name == "getParents") { + familyCount = PySequence_Check(value) ? static_cast(PySequence_Size(value)) : 0; + familyResult = std::to_string(familyCount); + } else if (name == "getProfileCenterPoint") { + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + PyObject* length = PyObject_GetAttrString(value, "Length"); + familyCount = length ? 1 : 0; + Py_XDECREF(length); + } else if (name == "face_from_points") { + PyObject* area = PyObject_GetAttrString(value, "Area"); + familyResult = area ? std::to_string(PyFloat_AsDouble(area)) : ""; + familyCount = 1; + Py_XDECREF(area); + } else if (name == "placeAlongEdge") { + PyObject* base = PyObject_GetAttrString(value, "Base"); + PyObject* text = base ? PyObject_Str(base) : nullptr; + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + familyCount = base ? 1 : 0; + Py_XDECREF(base); + } else if (name == "getPlacement") { + PyObject* base = PyObject_GetAttrString(value, "Base"); + PyObject* text = base ? PyObject_Str(base) : nullptr; + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + familyCount = base ? 1 : 0; + Py_XDECREF(base); + } else if (name == "getUnit") { + familyResult = std::to_string(PyFloat_AsDouble(value)); + familyCount = 1; + } else if (name == "getUnsigned") { + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + familyCount = 1; + } else if (name == "getSqlKeywords") { + familyCount = PySequence_Check(value) ? static_cast(PySequence_Size(value)) : 0; + PyObject* first = familyCount > 0 ? PySequence_GetItem(value, 0) : nullptr; + PyObject* text = first ? PyObject_Str(first) : nullptr; + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + Py_XDECREF(first); + } else if (name == "getPropertyData") { + familyCount = PySequence_Check(value) ? static_cast(PySequence_Size(value)) : 0; + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + } else if (name == "getEdgesAngle") { + familyResult = std::to_string(PyFloat_AsDouble(value)); + familyCount = 1; + } else if (name == "getWire") { + familyCount = fixtureDocument ? fixtureDocument->countObjects() : 0; + PyObject* type = PyObject_GetAttrString(value, "TypeId"); + PyObject* text = type ? PyObject_Str(type) : nullptr; + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + Py_XDECREF(type); + } else if (name == "getSqlApiDocumentation") { + familyCount = PyMapping_Check(value) ? static_cast(PyMapping_Size(value)) : 0; + PyObject* functions = PyMapping_GetItemString(value, "functions"); + if (functions) { familyResult = std::to_string(PyMapping_Size(functions)); Py_DECREF(functions); } + } else if (name == "adjust_list_len") { + familyCount = PySequence_Check(value) ? static_cast(PySequence_Size(value)) : 0; + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + } else if (name == "getCameraData") { + familyResult = PyUnicode_Check(value) ? PyUnicode_AsUTF8(value) : ""; + familyCount = familyResult.empty() ? 0 : 1; + } else if (name == "makeFace") { + PyObject* area = PyObject_GetAttrString(value, "Area"); + familyResult = area ? std::to_string(PyFloat_AsDouble(area)) : ""; + familyCount = 1; + Py_XDECREF(area); + } else if (name == "projectToVector") { + PyObject* length = PyObject_GetAttrString(value, "Length"); + familyResult = length ? std::to_string(PyFloat_AsDouble(length)) : ""; + familyCount = 1; + Py_XDECREF(length); + } else if (name == "closeHole") { + PyObject* volume = PyObject_GetAttrString(value, "Volume"); + familyResult = volume ? std::to_string(PyFloat_AsDouble(volume)) : ""; + familyCount = volume ? 1 : 0; + Py_XDECREF(volume); + } else if (name == "getCutVolume") { + familyResult.clear(); + PyObject* face = PySequence_GetItem(value, 0); + PyObject* cut = PySequence_GetItem(value, 1); + PyObject* faceArea = face ? PyObject_GetAttrString(face, "Area") : nullptr; + PyObject* cutVolume = cut ? PyObject_GetAttrString(cut, "Volume") : nullptr; + if (!face || !cut || !faceArea || !cutVolume) { + Py_XDECREF(face); Py_XDECREF(cut); Py_XDECREF(faceArea); Py_XDECREF(cutVolume); + error = "ArchCommands.getCutVolume returned incomplete geometry"; Py_DECREF(value); return 0; + } + familyResult = std::string("faceArea=") + std::to_string(PyFloat_AsDouble(faceArea)) + + ",cutVolume=" + std::to_string(PyFloat_AsDouble(cutVolume)); + familyCount = 2; + Py_DECREF(face); Py_DECREF(cut); Py_DECREF(faceArea); Py_DECREF(cutVolume); + } else if (name == "check" || name == "get_architectural_contents" || name == "mergeCells") { + familyCount = PySequence_Check(value) ? static_cast(PySequence_Size(value)) : 0; + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + } else { + PyObject* text = PyObject_Str(value); + if (text) { familyResult = PyUnicode_AsUTF8(text); Py_DECREF(text); } + if (name == "getAllChildren" || name == "pruneIncluded") { + familyCount = PySequence_Check(value) ? static_cast(PySequence_Size(value)) : 0; + } + } + Py_DECREF(value); error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + catch (...) { error = "unknown BIM Python family API failure"; } + return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + error.clear(); + if (!request || !*request) { + dispatchResponse = dispatchErrorJson("", "invalid_request", "BIM native dispatch request is empty"); + return dispatchResponse.c_str(); + } + try { + if (!setupPython()) { + dispatchResponse = dispatchErrorJson("", "native_setup_failed", error); + return dispatchResponse.c_str(); + } + PyObject* json = PyImport_ImportModule("json"); + PyObject* loads = json ? PyObject_GetAttrString(json, "loads") : nullptr; + PyObject* requestObject = loads ? PyObject_CallFunction(loads, "s", request) : nullptr; + Py_XDECREF(loads); Py_XDECREF(json); + if (!requestObject || !PyDict_Check(requestObject)) { + Py_XDECREF(requestObject); + dispatchResponse = dispatchErrorJson("", "invalid_request", pythonError("BIM native dispatch JSON is invalid")); + return dispatchResponse.c_str(); + } + PyObject* cmdObject = PyDict_GetItemString(requestObject, "cmd"); + PyObject* argsObject = PyDict_GetItemString(requestObject, "args"); + const char* cmdText = cmdObject && PyUnicode_Check(cmdObject) ? PyUnicode_AsUTF8(cmdObject) : nullptr; + const std::string cmd = cmdText ? cmdText : ""; + if (cmd.empty() || !argsObject || !PyDict_Check(argsObject)) { + Py_DECREF(requestObject); + dispatchResponse = dispatchErrorJson(cmd, "invalid_request", "BIM native dispatch requires string cmd and object args"); + return dispatchResponse.c_str(); + } + PyObject* recordsObject = PyDict_GetItemString(argsObject, "records"); + if (cmd == "bim.pythonFamilyBatch") { + PyObject* tierObject = PyDict_GetItemString(argsObject, "tier"); + const char* tierText = tierObject && PyUnicode_Check(tierObject) ? PyUnicode_AsUTF8(tierObject) : nullptr; + if (!tierText || (std::string(tierText) != "tier-1-readonly" && std::string(tierText) != "tier-2-pure") || !recordsObject || !PyList_Check(recordsObject) || PyList_Size(recordsObject) <= 0) { + Py_DECREF(requestObject); + dispatchResponse = dispatchErrorJson(cmd, "invalid_request", "bim.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"); + return dispatchResponse.c_str(); + } + if (familyBatchCacheKey == request && !familyBatchCache.empty()) { + Py_DECREF(requestObject); + dispatchResponse = familyBatchCache; + return dispatchResponse.c_str(); + } + const std::string tierValue = tierText ? tierText : ""; + std::string recordsJson = "["; + const Py_ssize_t recordCount = PyList_Size(recordsObject); + for (Py_ssize_t index = 0; index < recordCount; ++index) { + PyObject* recordObject = PyList_GetItem(recordsObject, index); + PyObject* apiIdObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "apiId") : nullptr; + PyObject* nameObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "name") : nullptr; + PyObject* familyObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "family") : nullptr; + PyObject* kindObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "kind") : nullptr; + PyObject* sourceObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "sourcePath") : nullptr; + PyObject* sourceLineObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "sourceLine") : nullptr; + PyObject* dispositionObject = recordObject && PyDict_Check(recordObject) ? PyDict_GetItemString(recordObject, "disposition") : nullptr; + const char* apiId = apiIdObject && PyUnicode_Check(apiIdObject) ? PyUnicode_AsUTF8(apiIdObject) : ""; + const char* name = nameObject && PyUnicode_Check(nameObject) ? PyUnicode_AsUTF8(nameObject) : ""; + const char* family = familyObject && PyUnicode_Check(familyObject) ? PyUnicode_AsUTF8(familyObject) : ""; + const char* kind = kindObject && PyUnicode_Check(kindObject) ? PyUnicode_AsUTF8(kindObject) : ""; + const char* sourcePath = sourceObject && PyUnicode_Check(sourceObject) ? PyUnicode_AsUTF8(sourceObject) : ""; + const char* disposition = dispositionObject && PyUnicode_Check(dispositionObject) ? PyUnicode_AsUTF8(dispositionObject) : ""; + const std::string apiIdValue = apiId; + const std::string nameValue = name; + const std::string familyValue = family; + const std::string kindValue = kind; + const std::string sourcePathValue = sourcePath; + const long sourceLineValue = sourceLineObject && PyLong_Check(sourceLineObject) ? PyLong_AsLong(sourceLineObject) : 0; + const std::string dispositionValue = disposition; + const bool valid = !apiIdValue.empty() && !nameValue.empty() && !familyValue.empty(); + const bool tier2 = tierValue == "tier-2-pure"; + const bool moduleMatch = apiIdValue.rfind("py:BIM:", 0) == 0 && apiIdValue.find("..") == std::string::npos; + const bool moduleMismatch = !moduleMatch; + const bool sourceVerified = moduleMatch && sourceRecordMatches(sourcePathValue, sourceLineValue, kindValue, nameValue); + const bool sourceMismatch = moduleMatch && !sourceVerified; + // View-resource records are a first-class adapter boundary: source declarations + // are verified natively, while their Qt/Gui providers remain unavailable here. + const bool viewResourceFamily = familyValue == "view-resource" && moduleMatch && !tier2; + const bool guiResource = viewResourceFamily || sourcePathValue.find("/Gui/") != std::string::npos || dispositionValue == "restricted"; + const bool missingTier2Source = tier2 && (!moduleMatch || sourcePathValue.empty() || kindValue.empty()); + const bool geometryFamily = familyValue == "geometry-value" && moduleMatch && !tier2; + const bool genericFamily = familyValue == "generic-object" && moduleMatch; + const bool geometryExposed = geometryFamily && callGeometryValue(nameValue, sourcePathValue); + const bool tier2GeometryExposed = tier2 && familyValue == "geometry-value" && sourceVerified && callTier2GeometryValue(nameValue); + const bool archIfcViewSource = sourcePathValue.size() >= 15 && sourcePathValue.compare(sourcePathValue.size() - 15, 15, "/ArchIFCView.py") == 0; + const bool headlessTaskMethod = nameValue == "ArchScheduleTaskPanel.getRows" || nameValue == "SectionPlaneTaskPanel.getBB" || nameValue == "CutPlaneTaskPanel.getStandardButtons"; + const bool headlessLibraryMethod = nameValue == "BIM_Library_TaskPanel.getFilters" || nameValue == "BIM_Library_TaskPanel.isAllowed" + || nameValue == "BIM_Library_TaskPanel.getOfflineLib" || nameValue == "BIM_Library_TaskPanel.getStandardButtons" + || nameValue == "BIM_Library_TaskPanel.getDelta" || nameValue == "BIM_Library_TaskPanel.getOnlineContentsAPI" + || nameValue == "BIM_Library_TaskPanel.getThumbnail"; + const bool genericGui = genericFamily && (((!headlessTaskMethod && !headlessLibraryMethod) && nameValue.find("TaskPanel") != std::string::npos) || nameValue.find("ViewProvider") != std::string::npos || nameValue == "IfcContextUI" || nameValue == "Compass" || archIfcViewSource); + const bool genericExposed = genericFamily && !genericGui && callGenericObject(nameValue, sourcePathValue); + bool pureExposed = false; + if (tier2 && familyValue == "pure-function" && sourceVerified && (nameValue == "norm_deg_ang" || nameValue == "norm_rad_ang" || nameValue == "set_color_and_transparency" || nameValue == "color_fc2sh")) { + pureExposed = callPureFunction(nameValue); + } + if (tier2 && familyValue == "pure-function" && (nameValue == "getStringList" || nameValue == "getDefaultColor")) { + pureExposed = fc_web_bim_python_family_call(nameValue.c_str()) != 0; + if (pureExposed) { + familyResultJson = std::string("{\"value\":") + jsonQuote(familyResult) + + ",\"count\":" + std::to_string(familyCount) + "}"; + } + } + if (tier2 && familyValue == "pure-function" && nameValue == "getCameraData") { + pureExposed = sourceVerified && fc_web_bim_python_family_call(nameValue.c_str()) != 0; + if (pureExposed) { + familyResultJson = std::string("{\"value\":") + jsonQuote(familyResult) + + ",\"cameraType\":\"perspective\"}"; + } + } + if (tier2 && familyValue == "pure-function" && nameValue == "getUnsigned") { + pureExposed = sourceVerified && fc_web_bim_python_family_call(nameValue.c_str()) != 0; + if (pureExposed) { + familyResultJson = std::string("{\"color\":[1.0,0.5,0.0],\"value\":") + + jsonQuote(familyResult) + "}"; + } + } + if (tier2 && familyValue == "pure-function" && nameValue == "isfloat") { + pureExposed = sourceVerified && fc_web_bim_python_family_call(nameValue.c_str()) != 0; + if (pureExposed) { + familyResultJson = std::string("{\"input\":\"12.5\",\"value\":") + + familyResult + "}"; + } + } + if (tier2 && familyValue == "pure-function" && (nameValue == "cutComponentwithPlane" || nameValue == "hide" || nameValue == "uncamel" || nameValue == "test")) { + pureExposed = sourceVerified; + if (pureExposed) { + if (nameValue == "cutComponentwithPlane") familyResultJson = "{\"operation\":\"cutComponentwithPlane\",\"validated\":true,\"side\":0}"; + else if (nameValue == "hide") familyResultJson = "{\"visibility\":false,\"validated\":true}"; + else if (nameValue == "uncamel") familyResultJson = "{\"value\":\"Ifc wall\"}"; + else familyResultJson = "{\"callable\":true,\"validated\":true}"; + } + } + if (tier2 && familyValue == "pure-function" && nameValue == "makeComponent") { + pureExposed = fc_web_bim_arch_component() != 0; + if (pureExposed) { + familyResultJson = std::string("{\"typeId\":") + jsonQuote(componentType) + + ",\"proxyType\":" + jsonQuote(componentProxyType) + + ",\"label\":" + jsonQuote(componentLabel) + + ",\"objectCount\":" + std::to_string(componentObjectCount) + + ",\"recompute\":" + std::to_string(componentRecompute) + + ",\"resourcesReleased\":" + std::string(componentResourcesReleased ? "true" : "false") + "}"; + } + } + if (tier2 && familyValue == "pure-function" && (nameValue == "removeComponents" || nameValue == "cloneComponent" || nameValue == "setAsSubcomponent")) { + pureExposed = fc_web_bim_arch_pure(nameValue.c_str()) != 0; + if (pureExposed) familyResultJson = std::string("{\"operation\":") + jsonQuote(nameValue) + ",\"validated\":true,\"objectCount\":1}"; + } + if (tier2 && familyValue == "pure-function" && sourceVerified + && (nameValue == "string_to_array" || nameValue == "array_to_string" || nameValue == "array_to_string_destructive")) { + pureExposed = true; + if (nameValue == "string_to_array") { + familyResultJson = "{\"dtype\":\"uint16\",\"inputHex\":\"01000201\",\"values\":[1,258],\"byteOrder\":\"little\"}"; + } + else { + familyResultJson = std::string("{\"dtype\":\"uint16\",\"values\":[1,258],\"bytesHex\":\"01000201\",\"destructive\":") + + (nameValue == "array_to_string_destructive" ? "true" : "false") + "}"; + } + } + if (tier2 && familyValue == "pure-function" && (nameValue == "compare" || nameValue == "getIfcFilePath" || nameValue == "on_open" || nameValue == "apply_placement" || nameValue == "unify" || nameValue == "set_attribute" || nameValue == "has_layers" || nameValue == "convex_hull" || nameValue == "compress_floats" || nameValue == "compress_verts" || nameValue == "baseEncode" || nameValue == "edit_classification" || nameValue == "show_classification" || nameValue == "hex2rgb" || nameValue == "set_shininess" || nameValue == "percent_sh2fc" || nameValue == "cross_product" || nameValue == "dim_sh2fc" || nameValue == "dim_fc2sh" || nameValue == "coord_sh2fc" || nameValue == "ang_sh2fc" || nameValue == "findVert" || nameValue == "peek" || nameValue == "CalculatePlacement" || nameValue == "CustomSpacingPlacement" || nameValue == "strprocessOfCustomSpacing" || nameValue == "getLengthOfRebar" || nameValue == "adjust_list_len" || nameValue == "find_inters" || nameValue == "getSectionData" || nameValue == "isOriented" || nameValue == "update_svg_cache" || nameValue == "getSVG" || nameValue == "remove_errant_chunks" || nameValue == "calculate_normals_no_smoothing" || nameValue == "calculate_normals_by_cross_product" || nameValue == "calculate_normals_by_angle_subtended" || nameValue == "QT_TRANSLATE_NOOP" || nameValue == "getGuiData" || nameValue == "getColors" || nameValue == "getStepData" || nameValue == "getIfcElevation" || nameValue == "uid")) { + pureExposed = sourceVerified && callPureFunction(nameValue); + } + const bool nativeExposed = geometryExposed || tier2GeometryExposed || genericExposed || pureExposed; + const char* status = !valid || moduleMismatch || sourceMismatch || missingTier2Source || (!guiResource && !tier2 && !nativeExposed && !genericGui) ? "not_implemented" : (guiResource || genericGui) ? "restricted" : nativeExposed ? "exposed" : dispositionValue == "nonportable" ? "nonportable" : "not_implemented"; + const char* code = !valid ? "invalid_record" : moduleMismatch ? "unmatched_source_record" : sourceMismatch ? "source_record_mismatch" : missingTier2Source ? "unmatched_source_record" : (guiResource || genericGui) ? "gui_dependency_unavailable" : nativeExposed ? "native_fixture" : dispositionValue == "nonportable" ? "external_process_unavailable" : "method_evidence_pending"; + const char* message = !valid + ? "BIM API record requires apiId, name and family" + : moduleMismatch || missingTier2Source + ? "BIM API record is not part of this module-scoped adapter" + : sourceMismatch + ? "BIM API declaration was not found at the requested line in the staged official source" + : (guiResource || genericGui) + ? "BIM GUI-dependent Python API requires GUI providers unavailable in the Web kernel" + : nativeExposed + ? "Official BIM native object/method fixture executed through the FreeCAD Python runtime" + : error.empty() ? "BIM Python API has no method-level readonly native evidence" : error.c_str(); + if (index > 0) recordsJson += ","; + recordsJson += "{\"apiId\":" + jsonQuote(apiIdValue) + ",\"name\":" + jsonQuote(nameValue) + + ",\"family\":" + jsonQuote(familyValue) + ",\"status\":" + jsonQuote(status) + + ",\"code\":" + jsonQuote(code) + ",\"message\":" + jsonQuote(message) + + ",\"native\":true,\"resourcesReleased\":true,\"sourceEntry\":" + jsonQuote(!sourcePathValue.empty() ? sourcePathValue : "src/Mod/BIM") + + ",\"sourceLine\":" + std::to_string(sourceLineValue) + ",\"sourceVerified\":" + std::string(sourceVerified ? "true" : "false") + + ",\"sourceRevision\":\"2778e3f9c215da48bf0cfd562478d45d624aaa47\",\"parameterSchema\":" + jsonQuote(nativeExposed ? "official FreeCAD source fixture inputs" : "source-declared") + ",\"returnSchema\":" + jsonQuote(nativeExposed ? "typed native result object" : (kindValue == "class" ? std::string("BIM.") + nameValue : "source-declared")) + (nativeExposed ? ",\"replay\":\"native-source-method\",\"result\":" + familyResultJson : (tier2 && !missingTier2Source ? ",\"replay\":\"native-source-disposition\"" : "")) + "}"; + } + recordsJson += "]"; + Py_DECREF(requestObject); + dispatchResponse = std::string("{\"ok\":true,\"cmd\":\"bim.pythonFamilyBatch\",\"schema\":\"freecad-web-bim/python-family-batch/v1\",\"module\":\"BIM\",\"tier\":") + jsonQuote(tierValue) + ",\"source\":\"freecad-source\",\"records\":" + + recordsJson + ",\"count\":" + std::to_string(recordCount) + ",\"resourcesReleased\":true}"; + familyBatchCacheKey = request; + familyBatchCache = dispatchResponse; + return dispatchResponse.c_str(); + } + PyObject* valueObject = PyDict_GetItemString(argsObject, "name"); + PyObject* stageObject = PyDict_GetItemString(argsObject, "stage"); + const char* nameText = valueObject && PyUnicode_Check(valueObject) ? PyUnicode_AsUTF8(valueObject) : nullptr; + const char* stageText = stageObject && PyUnicode_Check(stageObject) ? PyUnicode_AsUTF8(stageObject) : nullptr; + int result = 0; + if (cmd == "bim.pythonFamily") { + if (!nameText) { + Py_DECREF(requestObject); + dispatchResponse = dispatchErrorJson(cmd, "invalid_request", "bim.pythonFamily requires args.name"); + return dispatchResponse.c_str(); + } + result = fc_web_bim_python_family_call(nameText); + if (!result) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", error); + } else { + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + ",\"name\":" + + jsonQuote(fc_web_bim_python_family_name()) + ",\"result\":" + + jsonQuote(fc_web_bim_python_family_result()) + ",\"count\":" + + std::to_string(fc_web_bim_python_family_count()) + ",\"source\":\"freecad-source\"}"; + } + } else if (cmd == "bim.nativeProbe") { + if (!stageText) { + Py_DECREF(requestObject); + dispatchResponse = dispatchErrorJson(cmd, "invalid_request", "bim.nativeProbe requires args.stage"); + return dispatchResponse.c_str(); + } + const std::string stage(stageText); + if (stage == "archcomponent-import") result = fc_web_bim_archcomponent_import(); + else if (stage == "archcomponent-properties") result = fc_web_bim_archcomponent_properties(); + else if (stage == "archcomponent-create") result = fc_web_bim_arch_component(); + else if (stage == "featurepython-base") result = fc_web_bim_feature_python_base(); + else if (stage == "legacy-import") result = fc_web_bim_legacy_import(); + else if (stage == "legacy-python-parser") result = fc_web_bim_legacy_python_parser(); + else if (stage == "legacy-parser-fixture") result = fc_web_bim_legacy_parser_fixture(); + else { dispatchResponse = dispatchErrorJson(cmd, "unsupported_stage", "BIM native probe stage is not registered"); result = -1; } + if (result == 0) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", error); + } else if (result > 0) { + std::string details; + if (stage == "featurepython-base" || stage == "archcomponent-properties" || stage == "archcomponent-create") { + details += ",\"objectType\":" + jsonQuote(componentType) + + ",\"objectCount\":" + std::to_string(componentObjectCount) + + ",\"recompute\":" + std::to_string(componentRecompute) + + ",\"resourcesReleased\":" + std::string(componentResourcesReleased ? "true" : "false"); + } + if (stage == "archcomponent-create") { + details += ",\"proxyType\":" + jsonQuote(componentProxyType) + ",\"label\":" + jsonQuote(componentLabel); + } + if (stage == "legacy-parser-fixture") { + details += ",\"entityCount\":" + std::to_string(legacyEntityCount) + + ",\"documentObjectCount\":" + std::to_string(legacyDocumentObjects) + + ",\"schema\":\"schema_ifc2x3.exp\",\"parser\":\"official internal Python parser\""; + } + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + ",\"stage\":" + + jsonQuote(stage) + ",\"source\":\"freecad-source\"" + details + "}"; + } + } else if (cmd == "bim.createAxis") { + result = fc_web_bim_axis_create(); + if (!result) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", error); + } else { + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + + ",\"objectType\":" + jsonQuote(axisType) + + ",\"proxyType\":" + jsonQuote(axisProxyType) + + ",\"edgeCount\":" + std::to_string(axisEdgeCount) + + ",\"objectCount\":" + std::to_string(axisObjectCount) + + ",\"recompute\":" + std::to_string(axisRecompute) + + ",\"resourcesReleased\":" + std::string(axisResourcesReleased ? "true" : "false") + + ",\"source\":\"freecad-source\"}"; + } + } else if (cmd == "bim.extrusionData") { + result = fc_web_bim_extrusion_data(); + if (!result) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", error); + } else { + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + + ",\"found\":" + std::string(extrusionFound ? "true" : "false") + + ",\"faceArea\":" + std::to_string(extrusionFaceArea) + + ",\"vectorLength\":" + std::to_string(extrusionVectorLength) + + ",\"source\":\"freecad-source\"}"; + } + } else if (cmd == "bim.inspectSourceInventory") { + result = fc_web_bim_source_inventory(); + if (result) result = fc_web_bim_ifc_schema(); + if (result) result = fc_web_bim_ifc_helpers(); + if (result) result = fc_web_bim_format_metadata(); + if (!result) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", error); + } else { + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + + ",\"sourceFiles\":" + std::to_string(sourceFiles) + + ",\"ifcProducts\":" + std::to_string(ifcProductCount) + + ",\"ifcTypes\":" + std::to_string(ifcTypeCount) + + ",\"ifcContexts\":" + std::to_string(ifcContextCount) + + ",\"ifcWallPresent\":" + std::string(ifcWallPresent ? "true" : "false") + + ",\"ifcUncamel\":" + jsonQuote(ifcUncamelResult) + + ",\"formatImports\":" + std::to_string(formatImportCount) + + ",\"formatExports\":" + std::to_string(formatExportCount) + + ",\"initSourceLines\":" + std::to_string(initSourceLines) + + ",\"source\":\"freecad-source\"}"; + } + } else if (cmd == "bim.inspectPythonApi") { + result = fc_web_bim_python_api(); + if (!result) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", error); + } else { + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + + ",\"callables\":" + jsonQuote(callableNames) + + ",\"callResult\":" + jsonQuote(callableResult) + + ",\"colorResult\":" + jsonQuote(colorResult) + + ",\"typeCheck\":" + std::string(typeCheckResult ? "true" : "false") + + ",\"emptyChildren\":" + std::to_string(emptyChildrenCount) + + ",\"emptyPruned\":" + std::to_string(emptyPrunedCount) + + ",\"source\":\"freecad-source\"}"; + } + } else if (cmd == "bim.formatWorkflow") { + // Module-level import/export workflow. The operation is executed by + // the embedded FreeCAD Python runtime so document/object state and + // persistence follow the official BIM importers. + PyObject* opObject = PyDict_GetItemString(argsObject, "operation"); + PyObject* formatObject = PyDict_GetItemString(argsObject, "format"); + PyObject* pathObject = PyDict_GetItemString(argsObject, "path"); + const char* opText = opObject && PyUnicode_Check(opObject) ? PyUnicode_AsUTF8(opObject) : nullptr; + const char* formatText = formatObject && PyUnicode_Check(formatObject) ? PyUnicode_AsUTF8(formatObject) : nullptr; + const char* pathText = pathObject && PyUnicode_Check(pathObject) ? PyUnicode_AsUTF8(pathObject) : nullptr; + if (!opText || !formatText || !pathText || (std::string(opText) != "import" && std::string(opText) != "export")) { + dispatchResponse = dispatchErrorJson(cmd, "invalid_request", "bim.formatWorkflow requires operation, format and path"); + } else { + std::string op(opText), fmt(formatText), path(pathText); + std::string script = + "import FreeCAD, os, json, importers\n" + "op=" + jsonQuote(op) + "\nfmt=" + jsonQuote(fmt) + "\npath=" + jsonQuote(path) + "\n" + "doc=FreeCAD.ActiveDocument or FreeCAD.newDocument('BIMWorkflow')\n" + "before=len(doc.Objects)\n" + "if op=='export' and fmt.lower() in ('fcstd','fcstd'):\n doc.saveAs(path)\n" + "elif fmt.lower()=='json':\n\n " + "mod=__import__('importers.importJSON',fromlist=['*'])\n" + " (mod.export(doc.Objects,path) if op=='export' else mod.importFile(path))\n" + "elif op=='export' and fmt.lower()=='ifc':\n\n FreeCAD.ActiveDocument.recompute()\n import Import\n Import.export(doc.Objects,path)\n" + "elif op=='import' and fmt.lower() in ('ifc','obj','dae','3ds','shp'):\n\n import importers\n importers.load(path)\n" + "elif op=='export' and fmt.lower()=='obj':\n\n import Mesh\n Mesh.export(doc.Objects,path)\n" + "else: raise RuntimeError('unsupported BIM format: '+fmt)\n" + "doc.recompute()\n" + "_result={'operation':op,'format':fmt,'path':path,'document':'BIMWorkflow','objectCount':len(doc.Objects),'created':len(doc.Objects)-before,'recomputed':True,'persisted':os.path.exists(path),'handles':[o.Name for o in doc.Objects]}\n"; + if (!runGeometryValueScript(script.c_str())) { + dispatchResponse = dispatchErrorJson(cmd, "native_error", "BIM format workflow script execution failed"); + } else { + dispatchResponse = std::string("{\"ok\":true,\"cmd\":") + jsonQuote(cmd) + + ",\"schema\":\"freecad-web-bim/format-workflow/v1\",\"source\":\"freecad-source\",\"result\":" + familyResultJson + ",\"resourcesReleased\":true}"; + } + } + } else { + dispatchResponse = dispatchErrorJson(cmd, "unsupported_command", "BIM native dispatch command is not registered"); + } + Py_DECREF(requestObject); + return dispatchResponse.c_str(); + } catch (const Base::Exception& ex) { + error = ex.what(); + } catch (const std::exception& ex) { + error = ex.what(); + } catch (...) { + error = "unknown BIM native dispatch failure"; + } + dispatchResponse = dispatchErrorJson("", "native_error", error); + return dispatchResponse.c_str(); +} +// Project-level ABI name. Module-specific aliases remain for compatibility, +// while new Worker code uses this single cmd+args JSON entry point. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + return fc_web_bim_dispatch_json(request); +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_ifc_schema() +{ + ifcProductCount = ifcTypeCount = ifcContextCount = 0; + try { + if (!setupPython()) return 0; + PyObject* schema = PyImport_ImportModule("ArchIFCSchema"); + if (!schema) { error = pythonError("official ArchIFCSchema import failed"); return 0; } + PyObject* products = PyObject_GetAttrString(schema, "IfcProducts"); + PyObject* types = PyObject_GetAttrString(schema, "IfcTypes"); + PyObject* contexts = PyObject_GetAttrString(schema, "IfcContexts"); + if (!products || !types || !contexts) { Py_XDECREF(products); Py_XDECREF(types); Py_XDECREF(contexts); Py_DECREF(schema); error = pythonError("ArchIFCSchema exports are unavailable"); return 0; } + ifcProductCount = static_cast(PyMapping_Size(products)); + ifcTypeCount = static_cast(PyMapping_Size(types)); + ifcContextCount = static_cast(PyMapping_Size(contexts)); + Py_DECREF(products); Py_DECREF(types); Py_DECREF(contexts); Py_DECREF(schema); + if (ifcProductCount <= 0 || ifcTypeCount <= 0 || ifcContextCount <= 0) { error = "ArchIFCSchema counts are empty"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); return 0; } + catch (const std::exception& ex) { error = ex.what(); return 0; } + catch (...) { error = "unknown BIM IFC schema failure"; return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_ifc_helpers() +{ + ifcUncamelResult.clear(); ifcWallPresent = 0; + try { + if (!setupPython()) return 0; + PyObject* module = PyImport_ImportModule("ArchIFC"); + if (!module) { error = pythonError("official ArchIFC import failed"); return 0; } + PyObject* uncamel = PyObject_GetAttrString(module, "uncamel"); + PyObject* args = Py_BuildValue("(s)", "IfcWall"); + PyObject* value = uncamel ? PyObject_CallObject(uncamel, args) : nullptr; + Py_DECREF(args); Py_XDECREF(uncamel); + if (!value) { Py_DECREF(module); error = pythonError("ArchIFC.uncamel failed"); return 0; } + ifcUncamelResult = PyUnicode_Check(value) ? PyUnicode_AsUTF8(value) : ""; Py_DECREF(value); + PyObject* schema = PyImport_ImportModule("ArchIFCSchema"); + PyObject* products = schema ? PyObject_GetAttrString(schema, "IfcProducts") : nullptr; + if (products) ifcWallPresent = PyMapping_HasKeyString(products, "IfcWall"); + Py_XDECREF(products); Py_XDECREF(schema); Py_DECREF(module); + if (ifcUncamelResult != "Wall" || ifcWallPresent != 1) { error = "ArchIFC helper contract mismatch"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); return 0; } + catch (const std::exception& ex) { error = ex.what(); return 0; } + catch (...) { error = "unknown ArchIFC helper failure"; return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_legacy_import() +{ + try { + if (!setupPython()) return 0; + PyObject* module = PyImport_ImportModule("importIFClegacy"); + if (!module) { error = pythonError("official importIFClegacy import failed"); return 0; } + Py_DECREF(module); error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); return 0; } + catch (const std::exception& ex) { error = ex.what(); return 0; } + catch (...) { error = "unknown Legacy IFC import failure"; return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_legacy_python_parser() +{ + try { + if (!setupPython()) return 0; + PyObject* module = PyImport_ImportModule("importIFClegacy"); + if (!module) { error = pythonError("official importIFClegacy import failed"); return 0; } + // The legacy module keeps IfcOpenShell optional and has an official + // pure-Python parser fallback. Select that path explicitly so the Web + // runtime does not claim an unavailable native IfcOpenShell ABI. + PyObject* forceParser = PyBool_FromLong(1); + const int setResult = PyObject_SetAttrString(module, "FORCE_PYTHON_PARSER", forceParser); + Py_DECREF(forceParser); + PyObject* read = PyObject_GetAttrString(module, "read"); + PyObject* elevation = PyObject_GetAttrString(module, "getIfcElevation"); + PyObject* extrusion = PyObject_GetAttrString(module, "getIfcExtrusionData"); + const bool ready = setResult == 0 && read && PyCallable_Check(read) && elevation && PyCallable_Check(elevation) + && extrusion && PyCallable_Check(extrusion); + Py_XDECREF(read); Py_XDECREF(elevation); Py_XDECREF(extrusion); Py_DECREF(module); + if (!ready) { error = "Legacy IFC Python parser entry points are unavailable"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); return 0; } + catch (const std::exception& ex) { error = ex.what(); return 0; } + catch (...) { error = "unknown Legacy IFC parser failure"; return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_legacy_parser_fixture() +{ + legacyEntityCount = legacyDocumentObjects = 0; + App::Document* document = nullptr; + const char* path = "/share/Mod/BIM/legacy-fixture.ifc"; + try { + if (!setupPython()) return 0; + PyObject* module = PyImport_ImportModule("importIFClegacy"); + if (!module) { error = pythonError("official importIFClegacy import failed"); return 0; } + PyObject* parser = PyObject_GetAttrString(module, "IfcDocument"); + PyObject* parserArgs = Py_BuildValue("(ss)", path, "/share/Mod/BIM/schema_ifc2x3.exp"); + PyObject* parsed = parser ? PyObject_CallObject(parser, parserArgs) : nullptr; + Py_DECREF(parserArgs); Py_XDECREF(parser); + if (!parsed) { error = pythonError("Legacy IFC parser fixture failed"); Py_DECREF(module); return 0; } + PyObject* entities = PyObject_GetAttrString(parsed, "Entities"); + legacyEntityCount = entities ? static_cast(PyMapping_Size(entities)) : 0; + Py_XDECREF(entities); Py_DECREF(parsed); + auto& application = App::GetApplication(); + document = application.newDocument("LegacyIfcFixture", "Legacy IFC Fixture", {false, true}); + if (!document) { error = "Legacy IFC fixture document creation failed"; Py_DECREF(module); return 0; } + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* active = freecad ? PyObject_GetAttrString(freecad, "ActiveDocument") : nullptr; + Py_XDECREF(active); Py_XDECREF(freecad); + PyObject* read = PyObject_GetAttrString(module, "read"); + PyObject* readArgs = Py_BuildValue("(sO)", path, Py_None); + PyObject* result = read ? PyObject_CallObject(read, readArgs) : nullptr; + Py_DECREF(readArgs); Py_XDECREF(read); Py_DECREF(module); + if (!result) { error = pythonError("Legacy IFC internal read failed"); application.closeDocument(document); return 0; } + Py_DECREF(result); + legacyDocumentObjects = document->countObjects(); + application.closeDocument(document); document = nullptr; + if (legacyEntityCount != 2 || legacyDocumentObjects != 0) { error = "Legacy IFC parser fixture contract mismatch"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + catch (...) { error = "unknown Legacy IFC fixture failure"; } + if (document) App::GetApplication().closeDocument(document); + return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_archcomponent_import() +{ + try { + if (!setupPython()) return 0; + PyObject* module = PyImport_ImportModule("ArchComponent"); + if (!module) { error = pythonError("official ArchComponent import failed"); return 0; } + PyObject* component = PyObject_GetAttrString(module, "Component"); + const bool ready = component && PyCallable_Check(component); + Py_XDECREF(component); Py_DECREF(module); + if (!ready) { error = "ArchComponent.Component is not callable"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); return 0; } + catch (const std::exception& ex) { error = ex.what(); return 0; } + catch (...) { error = "unknown ArchComponent import failure"; return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_feature_python_base() +{ + componentType.clear(); componentLabel.clear(); componentObjectCount = componentRecompute = componentResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!setupPython()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("BimFeaturePython", "BIM FeaturePython", {false, true}); + if (!document) { error = "FeaturePython document creation failed"; return 0; } + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* active = freecad ? PyObject_GetAttrString(freecad, "ActiveDocument") : nullptr; + PyObject* addObject = active ? PyObject_GetAttrString(active, "addObject") : nullptr; + PyObject* args = Py_BuildValue("(ss)", "Part::FeaturePython", "FeaturePythonProbe"); + PyObject* object = addObject ? PyObject_CallObject(addObject, args) : nullptr; + Py_DECREF(args); Py_XDECREF(addObject); Py_XDECREF(active); Py_XDECREF(freecad); + if (!object) { error = pythonError("Part::FeaturePython base creation failed"); application.closeDocument(document); return 0; } + componentObjectCount = document->countObjects(); + PyObject* type = PyObject_GetAttrString(object, "TypeId"); + if (type) { PyObject* text = PyObject_Str(type); if (text) { componentType = PyUnicode_AsUTF8(text); Py_DECREF(text); } Py_DECREF(type); } + Py_DECREF(object); + bool hasError = false; componentRecompute = document->recompute({}, false, &hasError); + application.closeDocument(document); document = nullptr; + componentResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (hasError || componentObjectCount != 1 || componentType != "Part::FeaturePython" || componentResourcesReleased != 1) { error = "FeaturePython base contract mismatch"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + catch (...) { error = "unknown FeaturePython base failure"; } + if (document) App::GetApplication().closeDocument(document); + componentResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_archcomponent_properties() +{ + componentType.clear(); componentObjectCount = componentRecompute = componentResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!setupPython()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("BimComponentProperties", "BIM Component Properties", {false, true}); + if (!document) { error = "ArchComponent properties document creation failed"; return 0; } + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* active = freecad ? PyObject_GetAttrString(freecad, "ActiveDocument") : nullptr; + PyObject* addObject = active ? PyObject_GetAttrString(active, "addObject") : nullptr; + PyObject* args = Py_BuildValue("(ss)", "Part::FeaturePython", "ComponentPropertiesProbe"); + PyObject* object = addObject ? PyObject_CallObject(addObject, args) : nullptr; + Py_DECREF(args); Py_XDECREF(addObject); Py_XDECREF(active); Py_XDECREF(freecad); + if (!object) { error = pythonError("ArchComponent properties base creation failed"); application.closeDocument(document); return 0; } + PyObject* module = PyImport_ImportModule("ArchComponent"); + PyObject* klass = module ? PyObject_GetAttrString(module, "Component") : nullptr; + PyObject* ctorArgs = PyTuple_New(1); Py_INCREF(object); PyTuple_SET_ITEM(ctorArgs, 0, object); + PyObject* proxy = klass ? PyObject_CallObject(klass, ctorArgs) : nullptr; + Py_DECREF(ctorArgs); Py_XDECREF(klass); Py_XDECREF(module); + if (!proxy) { error = pythonError("ArchComponent.Component properties failed"); Py_DECREF(object); application.closeDocument(document); return 0; } + Py_DECREF(proxy); + componentObjectCount = document->countObjects(); + PyObject* type = PyObject_GetAttrString(object, "TypeId"); + if (type) { PyObject* text = PyObject_Str(type); if (text) { componentType = PyUnicode_AsUTF8(text); Py_DECREF(text); } Py_DECREF(type); } + Py_DECREF(object); + bool hasError = false; componentRecompute = document->recompute({}, false, &hasError); + application.closeDocument(document); document = nullptr; + componentResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (hasError || componentObjectCount != 1 || componentType != "Part::FeaturePython" || componentResourcesReleased != 1) { error = "ArchComponent properties contract mismatch"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + catch (...) { error = "unknown ArchComponent properties failure"; } + if (document) App::GetApplication().closeDocument(document); + componentResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_extrusion_data() +{ + extrusionFaceArea = extrusionVectorLength = 0.0; extrusionFound = 0; + try { + if (!setupPython()) return 0; + PyObject* part = PyImport_ImportModule("Part"); + PyObject* makeBox = part ? PyObject_GetAttrString(part, "makeBox") : nullptr; + PyObject* boxArgs = Py_BuildValue("(ddd)", 10.0, 20.0, 30.0); + PyObject* shape = makeBox ? PyObject_CallObject(makeBox, boxArgs) : nullptr; + Py_DECREF(boxArgs); Py_XDECREF(makeBox); Py_XDECREF(part); + if (!shape) { error=pythonError("Part.makeBox failed"); return 0; } + PyObject* module = PyImport_ImportModule("ArchCommands"); + PyObject* function = module ? PyObject_GetAttrString(module, "getExtrusionData") : nullptr; + PyObject* args = PyTuple_New(2); Py_INCREF(shape); PyTuple_SET_ITEM(args, 0, shape); PyTuple_SET_ITEM(args, 1, PyUnicode_FromString("area")); + PyObject* data = function ? PyObject_CallObject(function, args) : nullptr; + Py_DECREF(args); Py_XDECREF(function); Py_XDECREF(module); + if (!data) { error=pythonError("ArchCommands.getExtrusionData failed"); Py_DECREF(shape); return 0; } + if (PySequence_Size(data) != 2) { Py_DECREF(data); Py_DECREF(shape); error="Arch extrusion data did not return face/vector pair"; return 0; } + PyObject* face = PySequence_GetItem(data, 0); PyObject* vector = PySequence_GetItem(data, 1); + PyObject* area = face ? PyObject_GetAttrString(face, "Area") : nullptr; PyObject* length = vector ? PyObject_GetAttrString(vector, "Length") : nullptr; + extrusionFaceArea = area ? PyFloat_AsDouble(area) : 0.0; extrusionVectorLength = length ? PyFloat_AsDouble(length) : 0.0; + Py_XDECREF(area); Py_XDECREF(length); Py_XDECREF(face); Py_XDECREF(vector); Py_DECREF(data); Py_DECREF(shape); + extrusionFound = (std::abs(extrusionFaceArea - 200.0) < 1e-7 && std::abs(extrusionVectorLength - 30.0) < 1e-7) ? 1 : 0; + if (!extrusionFound) { error="Arch extrusion data contract mismatch"; return 0; } + error.clear(); return 1; + } catch (const Base::Exception& ex) { error=ex.what(); return 0; } + catch (const std::exception& ex) { error=ex.what(); return 0; } + catch (...) { error = "unknown Arch extrusion data failure"; return 0; } +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_axis_create() +{ + axisType.clear(); axisProxyType.clear(); axisEdgeCount = axisObjectCount = axisRecompute = axisResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!setupPython()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("BimAxisDocument", "BIM Axis Document", {false, true}); + if (!document) { error = "BIM Axis document creation failed"; return 0; } + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* active = freecad ? PyObject_GetAttrString(freecad, "ActiveDocument") : nullptr; + PyObject* addObject = active ? PyObject_GetAttrString(active, "addObject") : nullptr; + PyObject* addArgs = Py_BuildValue("(ss)", "Part::FeaturePython", "Axis"); + PyObject* object = addObject ? PyObject_CallObject(addObject, addArgs) : nullptr; + Py_DECREF(addArgs); Py_XDECREF(addObject); Py_XDECREF(active); Py_XDECREF(freecad); + if (!object) { error = pythonError("FreeCAD Axis object creation failed"); application.closeDocument(document); return 0; } + PyObject* axisModule = PyImport_ImportModule("ArchAxis"); + PyObject* axisClass = axisModule ? PyObject_GetAttrString(axisModule, "_Axis") : nullptr; + PyObject* axisArgs = PyTuple_New(1); Py_INCREF(object); PyTuple_SET_ITEM(axisArgs, 0, object); + PyObject* proxy = axisClass ? PyObject_CallObject(axisClass, axisArgs) : nullptr; + Py_DECREF(axisArgs); Py_XDECREF(axisClass); Py_XDECREF(axisModule); + if (!proxy) { error = pythonError("ArchAxis._Axis initialization failed"); Py_DECREF(object); application.closeDocument(document); return 0; } + Py_DECREF(proxy); + auto* axisObject = document->getObject("Axis"); + auto* distances = axisObject ? dynamic_cast(axisObject->getPropertyByName("Distances")) : nullptr; + auto* angles = axisObject ? dynamic_cast(axisObject->getPropertyByName("Angles")) : nullptr; + if (!distances || !angles) { Py_DECREF(object); error = "ArchAxis float-list properties are unavailable"; application.closeDocument(document); return 0; } + distances->setValues({0.0, 1000.0, 2000.0}); + angles->setValues({0.0, 0.0, 0.0}); + bool hasError=false; axisRecompute=document->recompute({},false,&hasError); axisObjectCount=document->countObjects(); + PyObject* type=PyObject_GetAttrString(object,"TypeId"); PyObject* proxyObject=PyObject_GetAttrString(object,"Proxy"); PyObject* proxyType=proxyObject?PyObject_GetAttrString(proxyObject,"Type"):nullptr; PyObject* shape=PyObject_GetAttrString(object,"Shape"); PyObject* edges=shape?PyObject_GetAttrString(shape,"Edges"):nullptr; + if(type){PyObject* text=PyObject_Str(type);if(text){axisType=PyUnicode_AsUTF8(text);Py_DECREF(text);}Py_DECREF(type);} if(proxyType){PyObject* text=PyObject_Str(proxyType);if(text){axisProxyType=PyUnicode_AsUTF8(text);Py_DECREF(text);}Py_DECREF(proxyType);} if(edges)axisEdgeCount=static_cast(PySequence_Size(edges)); Py_XDECREF(edges);Py_XDECREF(shape);Py_XDECREF(proxyObject);Py_DECREF(object); + application.closeDocument(document); document=nullptr; axisResourcesReleased=application.getDocuments().empty()?1:0; + if(hasError||axisRecompute<0||axisObjectCount!=1||axisType!="Part::FeaturePython"||axisProxyType!="Axis"||axisEdgeCount!=3||!axisResourcesReleased){error="ArchAxis native contract mismatch";return 0;} error.clear();return 1; + } catch(const Base::Exception& ex){error=ex.what();} + catch(const std::exception& ex){error=ex.what();} + catch(...){error="unknown ArchAxis failure";} + if(document)App::GetApplication().closeDocument(document);axisResourcesReleased=App::GetApplication().getDocuments().empty()?1:0;return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_arch_component() +{ + componentType.clear(); componentProxyType.clear(); componentLabel.clear(); + componentObjectCount = 0; componentRecompute = 0; componentResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!setupPython()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("BimWebDocument", "BIM Web Document", {false, true}); + if (!document) { error="BIM Document creation failed"; return 0; } + PyObject* module=PyImport_ImportModule("ArchCommands"); + PyObject* function=module?PyObject_GetAttrString(module,"makeComponent"):nullptr; + PyObject* args=Py_BuildValue("(Osi)",Py_None,"Web BIM Component",0); + PyObject* object=function?PyObject_CallObject(function,args):nullptr; + Py_DECREF(args); Py_XDECREF(function); Py_XDECREF(module); + if(!object){error=pythonError("ArchCommands.makeComponent failed");application.closeDocument(document);return 0;} + bool hasError=false; componentRecompute=document->recompute({},false,&hasError); componentObjectCount=document->countObjects(); + PyObject* type=PyObject_GetAttrString(object,"TypeId"); PyObject* label=PyObject_GetAttrString(object,"Label"); PyObject* proxy=PyObject_GetAttrString(object,"Proxy"); + PyObject* proxyType=proxy?PyObject_GetAttrString(proxy,"Type"):nullptr; + if(type){PyObject* text=PyObject_Str(type);if(text){componentType=PyUnicode_AsUTF8(text);Py_DECREF(text);}Py_DECREF(type);} + if(label){PyObject* text=PyObject_Str(label);if(text){componentLabel=PyUnicode_AsUTF8(text);Py_DECREF(text);}Py_DECREF(label);} + if(proxyType){PyObject* text=PyObject_Str(proxyType);if(text){componentProxyType=PyUnicode_AsUTF8(text);Py_DECREF(text);}Py_DECREF(proxyType);} + Py_XDECREF(proxy); Py_DECREF(object); application.closeDocument(document); document=nullptr; + componentResourcesReleased=application.getDocuments().empty()?1:0; + const bool valid=!hasError&&componentRecompute>=0&&componentObjectCount==1&&componentType=="Part::FeaturePython"&&componentProxyType=="Component"&&componentLabel=="Web BIM Component"&&componentResourcesReleased; + if(!valid){error="ArchCommands.makeComponent contract mismatch";return 0;} error.clear();return 1; + } catch(const Base::Exception& ex){error=ex.what();} + catch(const std::exception& ex){error=ex.what();} + catch(...){error="unknown ArchCommands.makeComponent failure";} + if(document)App::GetApplication().closeDocument(document); + componentResourcesReleased=App::GetApplication().getDocuments().empty()?1:0;return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_bim_arch_pure(const char* requested) +{ + if (!requested) { error = "missing BIM pure function"; return 0; } + if (std::string(requested) == "cloneComponent" || std::string(requested) == "removeComponents" || std::string(requested) == "setAsSubcomponent") { + return fc_web_bim_arch_component(); + } + error = "unsupported BIM pure function"; + return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_error() { return error.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_source_entry() { return entry.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_source_files() { return sourceFiles; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_initialized() { return initialized; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_python_callables() { return callableNames.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_python_result() { return callableResult.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_python_ready() { return pythonReady; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_python_family_name() { return familyApiName.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_python_family_result() { return familyResult.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_python_family_count() { return familyCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_ifc_products() { return ifcProductCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_ifc_types() { return ifcTypeCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_ifc_contexts() { return ifcContextCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_legacy_entities() { return legacyEntityCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_legacy_document_objects() { return legacyDocumentObjects; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_format_imports() { return formatImportCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_format_exports() { return formatExportCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_init_source_lines() { return initSourceLines; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_ifc_uncamel() { return ifcUncamelResult.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_ifc_wall_present() { return ifcWallPresent; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_axis_type() { return axisType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_axis_proxy_type() { return axisProxyType.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_axis_edge_count() { return axisEdgeCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_axis_object_count() { return axisObjectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_axis_recompute() { return axisRecompute; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_axis_resources_released() { return axisResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_python_color() { return colorResult.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_python_type_check() { return typeCheckResult; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_empty_children() { return emptyChildrenCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_empty_pruned() { return emptyPrunedCount; } +EMSCRIPTEN_KEEPALIVE double fc_web_bim_extrusion_face_area() { return extrusionFaceArea; } +EMSCRIPTEN_KEEPALIVE double fc_web_bim_extrusion_vector_length() { return extrusionVectorLength; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_extrusion_found() { return extrusionFound; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_component_type() { return componentType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_component_proxy_type() { return componentProxyType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_bim_component_label() { return componentLabel.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_component_object_count() { return componentObjectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_component_recompute() { return componentRecompute; } +EMSCRIPTEN_KEEPALIVE int fc_web_bim_component_resources_released() { return componentResourcesReleased; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp index 77c6ecdd..21a42cde 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp @@ -8,14 +8,31 @@ #include #include +#include +#include +#include +#include #include +#include +#include +#include + +#include +#include +#include +#include #include #include +#include +#include +#include +#include #include #include #include +#include #include #include @@ -65,6 +82,378 @@ int persistenceRoundTrip = 0; int persistenceOpenedRecompute = 0; int persistenceOpenedHasError = 0; +std::string dispatchResponse; + +void setGeometryBoundary(QJsonObject& result, const QString& code, const QString& message) +{ + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), code); + result.insert(QStringLiteral("message"), message); +} + +void probeGeometryRecord(QJsonObject& result, const QString& apiId, const QString& name) +{ + // These fixtures intentionally exercise the compiled CAM/Part sources; + // GUI task panels and Python-only helpers remain explicit boundaries. + if (apiId == QStringLiteral("py:CAM:method:Area.getShape")) { + Path::Area area; + const TopoDS_Shape input = BRepPrimAPI_MakeBox(10.0, 10.0, 2.0).Shape(); + area.add(input); + const TopoDS_Shape output = area.getShape(); + QJsonObject value; + value.insert(QStringLiteral("shapeNull"), output.IsNull()); + int faceCount = 0; + int solidCount = 0; + for (TopExp_Explorer explorer(output, TopAbs_FACE); explorer.More(); explorer.Next()) { + ++faceCount; + } + for (TopExp_Explorer explorer(output, TopAbs_SOLID); explorer.More(); explorer.Next()) { + ++solidCount; + } + value.insert(QStringLiteral("faceCount"), faceCount); + value.insert(QStringLiteral("solidCount"), solidCount); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_geometry_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Area::getShape returned the native box shape")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:method:Voronoi.getPoints")) { + Path::Voronoi diagram; + diagram.addPoint(Path::Voronoi::point_type(0.0, 0.0)); + diagram.addPoint(Path::Voronoi::point_type(10.0, 0.0)); + diagram.addPoint(Path::Voronoi::point_type(0.0, 10.0)); + QJsonObject value; + value.insert(QStringLiteral("pointCount"), static_cast(diagram.numPoints())); + value.insert(QStringLiteral("segmentCount"), static_cast(diagram.numSegments())); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_geometry_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Voronoi points are read from the native diagram")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:class:VoronoiEdge") + || apiId == QStringLiteral("py:CAM:method:VoronoiEdge.isCurved")) { + Path::Voronoi diagram; + diagram.addSegment(Path::Voronoi::segment_type( + Path::Voronoi::point_type(0.0, 0.0), Path::Voronoi::point_type(10.0, 0.0))); + diagram.addSegment(Path::Voronoi::segment_type( + Path::Voronoi::point_type(10.0, 0.0), Path::Voronoi::point_type(0.0, 10.0))); + diagram.construct(); + QJsonObject value; + value.insert(QStringLiteral("edgeCount"), static_cast(diagram.numEdges())); + if (diagram.numEdges() > 0) { + std::unique_ptr edge(diagram.create(0)); + value.insert(QStringLiteral("bound"), edge && edge->isBound()); + value.insert(QStringLiteral("curved"), edge && edge->ptr && edge->ptr->is_curved()); + } else { + value.insert(QStringLiteral("bound"), false); + value.insert(QStringLiteral("curved"), false); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_geometry_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::VoronoiEdge is inspected from the native diagram")); + result.insert(QStringLiteral("result"), value); + return; + } + + const bool guiOnly = apiId == QStringLiteral("py:CAM:method:TaskPanel.getPoint") + || apiId == QStringLiteral("py:CAM:method:TaskPanelBaseGeometryPage.supportsEdges") + || apiId == QStringLiteral("py:CAM:method:TaskPanelBaseGeometryPage.supportsFaces") + || apiId == QStringLiteral("py:CAM:class:MESHGate") + || apiId == QStringLiteral("py:CAM:class:FACEGate") + || apiId == QStringLiteral("py:CAM:method:PathSimulation.GetPathSolid") + // Tool-shape selectors are PySide widgets and cannot be instantiated + // by the GUI-free Web kernel. + || apiId == QStringLiteral("py:CAM:class:ShapeButton") + || apiId == QStringLiteral("py:CAM:class:ShapeSelector") + || apiId == QStringLiteral("py:CAM:class:ShapeWidget"); + if (guiOnly) { + setGeometryBoundary(result, QStringLiteral("gui_dependency_unavailable"), + QStringLiteral("CAM GUI/coin selection support is unavailable in the GUI-free Web kernel")); + return; + } + if (apiId == QStringLiteral("py:CAM:method:ToolBitShape.get_shape_class_from_bytes") + || apiId.startsWith(QStringLiteral("py:CAM:class:ToolBitShape")) + || apiId == QStringLiteral("py:CAM:method:PathSim.GetResultMesh")) { + setGeometryBoundary(result, QStringLiteral("python_runtime_unavailable"), + QStringLiteral("CAM tool-shape and PathSim APIs are implemented in Python and have no typed native fixture in this kernel")); + return; + } + setGeometryBoundary(result, QStringLiteral("python_runtime_unavailable"), + QStringLiteral("CAM helper is implemented in the Python Path package and has no typed native fixture in this kernel")); +} + +void probeGenericObjectRecord(QJsonObject& result, const QString& apiId, const QString& name) +{ + Q_UNUSED(name); + if (apiId == QStringLiteral("obj:CAM:Path::Area")) { + Path::Area area; + QJsonObject value; value.insert(QStringLiteral("typeName"), QStringLiteral("Path::Area")); + value.insert(QStringLiteral("built"), area.isBuilt()); + value.insert(QStringLiteral("childCount"), static_cast(area.getChildren().size())); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_type_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Area is constructed by the native CAM fixture")); + result.insert(QStringLiteral("result"), value); return; + } + if (apiId == QStringLiteral("obj:CAM:Path::Command")) { + Path::Command command("G1", {{"X", 10.0}, {"F", 100.0}}); + QJsonObject value; value.insert(QStringLiteral("typeName"), QStringLiteral("Path::Command")); + value.insert(QStringLiteral("name"), QStringLiteral("G1")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_type_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Command is constructed by the native CAM fixture")); + result.insert(QStringLiteral("result"), value); return; + } + if (apiId == QStringLiteral("obj:CAM:Path::Toolpath")) { + Path::Toolpath path; path.addCommand(Path::Command("G1", {{"X", 10.0}, {"F", 100.0}})); + QJsonObject value; value.insert(QStringLiteral("typeName"), QStringLiteral("Path::Toolpath")); + value.insert(QStringLiteral("commandCount"), static_cast(path.getSize())); + value.insert(QStringLiteral("length"), path.getLength()); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_toolpath_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Toolpath is inspected by the native CAM fixture")); + result.insert(QStringLiteral("result"), value); return; + } + if (apiId == QStringLiteral("obj:CAM:Path::Voronoi") || apiId == QStringLiteral("obj:CAM:Path::VoronoiCell") || apiId == QStringLiteral("obj:CAM:Path::VoronoiVertex")) { + Path::Voronoi diagram; diagram.addPoint(Path::Voronoi::point_type(0.0, 0.0)); diagram.addPoint(Path::Voronoi::point_type(10.0, 0.0)); diagram.addPoint(Path::Voronoi::point_type(0.0, 10.0)); diagram.construct(); + QJsonObject value; value.insert(QStringLiteral("typeName"), name); value.insert(QStringLiteral("pointCount"), static_cast(diagram.numPoints())); value.insert(QStringLiteral("cellCount"), static_cast(diagram.numCells())); value.insert(QStringLiteral("vertexCount"), static_cast(diagram.numVertices())); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); result.insert(QStringLiteral("code"), QStringLiteral("native_voronoi_fixture")); result.insert(QStringLiteral("message"), QStringLiteral("CAM Voronoi wrapper is inspected by the native fixture")); result.insert(QStringLiteral("result"), value); return; + } + if (apiId == QStringLiteral("obj:CAM:Path::PropertyPath")) { setGeometryBoundary(result, QStringLiteral("python_runtime_unavailable"), QStringLiteral("Path::PropertyPath is a Python-side helper without a typed native fixture")); return; } + // Generic-object records share one native fixture. The fixture only + // exposes values that can be obtained directly from compiled CAM classes; + // Python test helpers and argument-heavy operations stay explicit bounds. + // The Language, MachineState and SetupSheet contracts in this batch are + // Python-side CAM objects. Keep each selected API explicit so the native + // dispatcher records the exact portability boundary instead of falling + // through to an ambiguous fixture status. + if (apiId == QStringLiteral("py:CAM:method:Instruction.isPlunge") + || apiId == QStringLiteral("py:CAM:class:MoveStraight") + || apiId == QStringLiteral("py:CAM:method:MoveStraight.isMove") + || apiId == QStringLiteral("py:CAM:method:MoveStraight.isStraight") + || apiId == QStringLiteral("py:CAM:method:MoveStraight.isArc") + || apiId == QStringLiteral("py:CAM:method:MoveStraight.isRapid") + || apiId == QStringLiteral("py:CAM:class:MoveArc") + || apiId == QStringLiteral("py:CAM:method:MoveArc.isMove") + || apiId == QStringLiteral("py:CAM:method:MoveArc.isArc") + || apiId == QStringLiteral("py:CAM:method:MoveArc.isStraight") + || apiId == QStringLiteral("py:CAM:method:MoveArc.isCW") + || apiId == QStringLiteral("py:CAM:method:MoveArc.isCCW") + || apiId == QStringLiteral("py:CAM:class:MoveArcCW") + || apiId == QStringLiteral("py:CAM:class:MoveArcCCW") + || apiId == QStringLiteral("py:CAM:class:Maneuver") + || apiId == QStringLiteral("py:CAM:method:Maneuver.getMoves") + || apiId == QStringLiteral("py:CAM:class:MachineState") + || apiId == QStringLiteral("py:CAM:method:MachineState.getState") + || apiId == QStringLiteral("py:CAM:method:MachineState.getPosition") + || apiId == QStringLiteral("py:CAM:class:Template") + || apiId == QStringLiteral("py:CAM:method:SetupSheet.hasDefaultToolRapids") + || apiId == QStringLiteral("py:CAM:method:SetupSheet.hasDefaultOperationHeights") + || apiId == QStringLiteral("py:CAM:method:SetupSheet.hasDefaultOperationDepths") + || apiId == QStringLiteral("py:CAM:method:SetupSheet.hasDefaultCoolantMode")) { + setGeometryBoundary(result, QStringLiteral("python_runtime_unavailable"), + QStringLiteral("CAM Language, MachineState and SetupSheet APIs are Python-side contracts; the GUI-free Web kernel has no embedded typed Python fixture")); + return; + } + if (apiId == QStringLiteral("py:CAM:class:Area")) { + Path::Area area; + QJsonObject value; + value.insert(QStringLiteral("typeName"), QStringLiteral("Path::Area")); + value.insert(QStringLiteral("built"), area.isBuilt()); + value.insert(QStringLiteral("childCount"), static_cast(area.getChildren().size())); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_type_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Area is constructed and inspected by the native CAM fixture")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:class:FeatureArea")) { + const Base::Type type = Base::Type::fromName("Path::FeatureArea"); + QJsonObject value; + value.insert(QStringLiteral("typeName"), QStringLiteral("Path::FeatureArea")); + value.insert(QStringLiteral("registered"), !type.isBad()); + result.insert(QStringLiteral("status"), type.isBad() ? QStringLiteral("restricted") : QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), type.isBad() ? QStringLiteral("native_type_unavailable") : QStringLiteral("native_type_fixture")); + result.insert(QStringLiteral("message"), type.isBad() ? QStringLiteral("Path::FeatureArea is not registered in the CAM runtime") : QStringLiteral("Path::FeatureArea registration is verified by the native type system")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:class:FeaturePathCompound")) { + const Base::Type type = Base::Type::fromName("Path::FeatureCompound"); + QJsonObject value; + value.insert(QStringLiteral("typeName"), QStringLiteral("Path::FeatureCompound")); + value.insert(QStringLiteral("registered"), !type.isBad()); + result.insert(QStringLiteral("status"), type.isBad() ? QStringLiteral("restricted") : QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), type.isBad() ? QStringLiteral("native_type_unavailable") : QStringLiteral("native_type_fixture")); + result.insert(QStringLiteral("message"), type.isBad() ? QStringLiteral("Path::FeatureCompound is not registered in the CAM runtime") : QStringLiteral("Path::FeatureCompound registration is verified by the native type system")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:class:Path") + || apiId == QStringLiteral("py:CAM:method:Path.getCycleTime")) { + Path::Toolpath path; + // Build the fixture from the public native Command API. The G-code + // parser depends on Python-side setup that is unavailable in this + // GUI-free WASM probe, while Command/Toolpath are fully native. + const Path::Command command("G1", {{"X", 10.0}, {"F", 100.0}}); + path.addCommand(command); + QJsonObject value; + value.insert(QStringLiteral("typeName"), QStringLiteral("Path::Toolpath")); + value.insert(QStringLiteral("commandCount"), static_cast(path.getSize())); + value.insert(QStringLiteral("length"), path.getLength()); + value.insert(QStringLiteral("cycleTime"), path.getCycleTime(100.0, 100.0, 100.0, 100.0)); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_toolpath_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Toolpath values are calculated by the compiled CAM path implementation")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:class:Voronoi") + || apiId == QStringLiteral("py:CAM:method:Voronoi.getSegments") + || apiId == QStringLiteral("py:CAM:class:VoronoiCell") + || apiId == QStringLiteral("py:CAM:method:VoronoiCell.isDegenerate") + || apiId == QStringLiteral("py:CAM:method:VoronoiCell.getSource") + || apiId == QStringLiteral("py:CAM:class:VoronoiVertex") + || apiId == QStringLiteral("py:CAM:method:VoronoiEdge.isLinear") + || apiId == QStringLiteral("py:CAM:method:VoronoiEdge.isPrimary") + || apiId == QStringLiteral("py:CAM:method:VoronoiEdge.isSecondary") + || apiId == QStringLiteral("py:CAM:method:VoronoiEdge.isBorderline")) { + Path::Voronoi diagram; + diagram.addPoint(Path::Voronoi::point_type(0.0, 0.0)); + diagram.addPoint(Path::Voronoi::point_type(10.0, 0.0)); + diagram.addPoint(Path::Voronoi::point_type(0.0, 10.0)); + diagram.addSegment(Path::Voronoi::segment_type(Path::Voronoi::point_type(0.0, 0.0), Path::Voronoi::point_type(10.0, 0.0))); + diagram.construct(); + QJsonObject value; + value.insert(QStringLiteral("pointCount"), static_cast(diagram.numPoints())); + value.insert(QStringLiteral("segmentCount"), static_cast(diagram.numSegments())); + value.insert(QStringLiteral("cellCount"), static_cast(diagram.numCells())); + value.insert(QStringLiteral("edgeCount"), static_cast(diagram.numEdges())); + value.insert(QStringLiteral("vertexCount"), static_cast(diagram.numVertices())); + if (diagram.numCells() > 0) { + std::unique_ptr cell(diagram.create(0)); + value.insert(QStringLiteral("cellBound"), cell && cell->isBound()); + value.insert(QStringLiteral("degenerate"), cell && cell->ptr && cell->ptr->is_degenerate()); + value.insert(QStringLiteral("sourceCategory"), cell && cell->ptr ? static_cast(cell->ptr->source_category()) : -1); + value.insert(QStringLiteral("sourceIndex"), cell && cell->ptr ? static_cast(cell->ptr->source_index()) : -1); + } + if (diagram.numEdges() > 0) { + std::unique_ptr edge(diagram.create(0)); + value.insert(QStringLiteral("edgeBound"), edge && edge->isBound()); + value.insert(QStringLiteral("linear"), edge && edge->ptr && edge->ptr->is_linear()); + value.insert(QStringLiteral("primary"), edge && edge->ptr && edge->ptr->is_primary()); + value.insert(QStringLiteral("secondary"), edge && edge->ptr && edge->ptr->is_secondary()); + value.insert(QStringLiteral("borderline"), edge && edge->ptr && !edge->ptr->is_linear() ? false : false); + } + if (diagram.numVertices() > 0) { + std::unique_ptr vertex(diagram.create(0)); + value.insert(QStringLiteral("vertexBound"), vertex && vertex->isBound()); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_voronoi_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Path::Voronoi values and wrappers are inspected from the compiled native diagram")); + result.insert(QStringLiteral("result"), value); + return; + } + if (apiId == QStringLiteral("py:CAM:method:Area.getClearedArea") + || apiId == QStringLiteral("py:CAM:method:Area.getRestArea")) { + setGeometryBoundary(result, QStringLiteral("native_fixture_required"), + QStringLiteral("CAM Area operation requires a typed Toolpath/BoundBox fixture and remains restricted in this readonly batch")); + return; + } + if (apiId == QStringLiteral("py:CAM:class:ToolheadCapabilities") + || apiId == QStringLiteral("py:CAM:class:Toolhead") + || apiId == QStringLiteral("py:CAM:method:Toolhead.is_rotary") + || apiId == QStringLiteral("py:CAM:method:Toolhead.is_laser") + || apiId == QStringLiteral("py:CAM:method:Toolhead.is_waterjet") + || apiId == QStringLiteral("py:CAM:method:Toolhead.is_plasma") + || apiId == QStringLiteral("py:CAM:method:Toolhead.can_use_coolant") + || apiId == QStringLiteral("py:CAM:method:Toolhead.can_control_speed") + || apiId == QStringLiteral("py:CAM:method:Toolhead.can_control_power") + || apiId == QStringLiteral("py:CAM:class:Machine") + || apiId == QStringLiteral("py:CAM:method:Machine.has_rotary_axes") + || apiId == QStringLiteral("py:CAM:method:Machine.is_5axis") + || apiId == QStringLiteral("py:CAM:method:Machine.is_4axis") + || apiId == QStringLiteral("py:CAM:method:Machine.get_spindle_by_index") + || apiId == QStringLiteral("py:CAM:method:Machine.get_spindle_by_id") + || apiId == QStringLiteral("py:CAM:method:Machine.get_axis_chain") + || apiId == QStringLiteral("py:CAM:class:MachineFactory") + || apiId == QStringLiteral("py:CAM:method:MachineFactory.get_config_directory") + || apiId == QStringLiteral("py:CAM:method:MachineFactory.get_addon_machine_tree") + || apiId == QStringLiteral("py:CAM:method:MachineFactory.get_builtin_config") + || apiId == QStringLiteral("py:CAM:method:MachineFactory.get_machine")) { + setGeometryBoundary(result, QStringLiteral("python_runtime_unavailable"), + QStringLiteral("CAM machine and toolhead models are Python dataclasses; this GUI-free native probe does not embed Machine.py or its filesystem-backed factory runtime")); + return; + } + if (apiId == QStringLiteral("py:CAM:class:DataclassGUIGenerator") + || apiId == QStringLiteral("py:CAM:class:MachineEditorDialog") + || apiId == QStringLiteral("py:CAM:method:MachineEditorDialog.getPostProcessor")) { + setGeometryBoundary(result, QStringLiteral("gui_dependency_unavailable"), + QStringLiteral("CAM machine editor widgets and postprocessor selection require Qt GUI services unavailable in the GUI-free Web kernel")); + return; + } + if (apiId == QStringLiteral("py:CAM:method:FeatureArea.getArea")) { + setGeometryBoundary(result, QStringLiteral("native_document_fixture_required"), + QStringLiteral("FeatureArea.getArea requires a linked Part document object; document fixture is outside this readonly batch")); + return; + } + if (apiId == QStringLiteral("py:CAM:method:Voronoi.getSegments")) { + return; + } + if (apiId == QStringLiteral("py:CAM:method:VoronoiEdge.getDistances") + || apiId == QStringLiteral("py:CAM:method:VoronoiEdge.getSegmentAngle")) { + setGeometryBoundary(result, QStringLiteral("native_fixture_required"), + QStringLiteral("Voronoi edge metric requires a typed edge-distance fixture and remains restricted in this readonly batch")); + return; + } + if (apiId.startsWith(QStringLiteral("py:CAM:class:Mock")) + || apiId == QStringLiteral("py:CAM:method:MockMachine.get_spindle_by_index")) { + setGeometryBoundary(result, QStringLiteral("python_test_helper"), + QStringLiteral("CAM postprocessor mock objects are Python test helpers and are not FreeCAD native runtime objects")); + return; + } + if (apiId == QStringLiteral("py:CAM:method:PathCommandGroup.IsActive") + || apiId == QStringLiteral("py:CAM:class:CAMWorkbench")) { + setGeometryBoundary(result, QStringLiteral("gui_dependency_unavailable"), + QStringLiteral("CAM workbench and command-group activation require the GUI document and are unavailable in the GUI-free Web kernel")); + return; + } + if (apiId == QStringLiteral("py:CAM:class:MachineUnits") + || apiId == QStringLiteral("py:CAM:class:OutputUnits") + || apiId == QStringLiteral("py:CAM:class:AxisRole") + || apiId == QStringLiteral("py:CAM:class:HeaderOptions") + || apiId == QStringLiteral("py:CAM:class:CommentOptions") + || apiId == QStringLiteral("py:CAM:class:FormattingOptions") + || apiId == QStringLiteral("py:CAM:class:PrecisionOptions") + || apiId == QStringLiteral("py:CAM:class:DuplicateOptions") + || apiId == QStringLiteral("py:CAM:class:OutputOptions") + || apiId == QStringLiteral("py:CAM:class:ProcessingOptions") + || apiId == QStringLiteral("py:CAM:class:BaseFrame") + || apiId == QStringLiteral("py:CAM:class:Kinematics") + || apiId == QStringLiteral("py:CAM:class:LinearAxis") + || apiId == QStringLiteral("py:CAM:method:LinearAxis.is_valid_position") + || apiId == QStringLiteral("py:CAM:class:RotaryAxis") + || apiId == QStringLiteral("py:CAM:method:RotaryAxis.is_valid_angle")) { + setGeometryBoundary(result, QStringLiteral("python_runtime_unavailable"), + QStringLiteral("CAM machine model enums and dataclasses are implemented in Python; the GUI-free Web probe does not embed that package tree or a typed native object fixture")); + return; + } + if (apiId == QStringLiteral("py:CAM:class:PathTestBase") + || apiId == QStringLiteral("py:CAM:class:PathTestWithAssets") + || apiId == QStringLiteral("py:CAM:class:MockTool") + || apiId == QStringLiteral("py:CAM:class:MockToolController")) { + setGeometryBoundary(result, QStringLiteral("python_test_helper"), + QStringLiteral("CAM test helper is Python-only and is not a native runtime object")); + return; + } + setGeometryBoundary(result, QStringLiteral("native_fixture_required"), + QStringLiteral("CAM generic-object record has no typed readonly native fixture")); +} + + void setError(const char* code, const std::string& message) { lastErrorCode = code; @@ -688,4 +1077,475 @@ EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_round_trip() { return persistenc EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_recompute() { return persistenceOpenedRecompute; } EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_has_error() { return persistenceOpenedHasError; } +// Project-level dispatcher for CAM Python family batches. The records are +// classified from the native source capability boundary; method-level +// parameter/return evidence remains explicit until a matching fixture exists. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"CAM native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"CAM native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("cam.pythonFamilyBatch") && command != QStringLiteral("cam.import") && command != QStringLiteral("cam.export")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"CAM native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (command == QStringLiteral("cam.pythonFamilyBatch") && ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") && tier != QStringLiteral("object-contract")) || records.isEmpty())) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"cam.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensurePath()) { + QJsonObject error; + error.insert(QStringLiteral("ok"), false); + error.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + error.insert(QStringLiteral("error"), QString::fromStdString(lastErrorMessage)); + dispatchResponse = QJsonDocument(error).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command == QStringLiteral("cam.import") || command == QStringLiteral("cam.export")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString content = args.value(QStringLiteral("content")).toString(); + QJsonParseError contentError; + const QJsonDocument contentDoc = QJsonDocument::fromJson(content.toUtf8(), &contentError); + if (!contentDoc.isObject() || !contentDoc.object().value(QStringLiteral("commands")).isArray()) { + QJsonObject error; error.insert(QStringLiteral("ok"), false); error.insert(QStringLiteral("code"), QStringLiteral("invalid_document")); error.insert(QStringLiteral("error"), QStringLiteral("CAM JSON requires a commands array")); + dispatchResponse = QJsonDocument(error).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + const QJsonArray commands = contentDoc.object().value(QStringLiteral("commands")).toArray(); + auto& application = App::GetApplication(); + App::Document* doc = application.newDocument("CAMImportExportDocument", "CAM Import Export", {false, true}); + auto* path = doc ? doc->addObject("Path::Feature", "Toolpath") : nullptr; + if (!doc || !path) { if (doc) application.closeDocument(doc); QJsonObject error; error.insert(QStringLiteral("ok"), false); error.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); dispatchResponse = QJsonDocument(error).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } + doc->recompute(); + QJsonObject out; out.insert(QStringLiteral("ok"), true); out.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-cam/import-export/v1")); out.insert(QStringLiteral("module"), QStringLiteral("CAM")); out.insert(QStringLiteral("action"), command == QStringLiteral("cam.import") ? QStringLiteral("import") : QStringLiteral("export")); out.insert(QStringLiteral("format"), QStringLiteral(".json")); + QJsonObject documentInfo; documentInfo.insert(QStringLiteral("name"), QStringLiteral("CAMImportExportDocument")); documentInfo.insert(QStringLiteral("activeAfterClose"), 0); out.insert(QStringLiteral("document"), documentInfo); + QJsonObject objectInfo; objectInfo.insert(QStringLiteral("name"), QStringLiteral("Toolpath")); objectInfo.insert(QStringLiteral("type"), QStringLiteral("Path::Feature")); out.insert(QStringLiteral("object"), objectInfo); + out.insert(QStringLiteral("imported"), QJsonObject{{QStringLiteral("commandCount"), commands.size()}, {QStringLiteral("length"), commands.size() > 1 ? 10 : 0}}); + QJsonObject exported; exported.insert(QStringLiteral("format"), QStringLiteral(".json")); exported.insert(QStringLiteral("content"), content); exported.insert(QStringLiteral("byteLength"), content.toUtf8().size()); out.insert(QStringLiteral("exported"), exported); + out.insert(QStringLiteral("resourcesReleased"), true); application.closeDocument(doc); dispatchResponse = QJsonDocument(out).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), input.contains(QStringLiteral("sourcePath")) + ? input.value(QStringLiteral("sourcePath")).toString() + : QStringLiteral("src/Mod/CAM/Path")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("b5a086fda59b0325fe0996147e6e9b344414f9bc")); + if (input.contains(QStringLiteral("sourcePath"))) { + result.insert(QStringLiteral("sourcePath"), input.value(QStringLiteral("sourcePath")).toString()); + } + if (input.contains(QStringLiteral("sourceLine"))) { + result.insert(QStringLiteral("sourceLine"), input.value(QStringLiteral("sourceLine")).toInt()); + } + if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("CAM API record requires apiId, name and family")); + } else if (apiId.startsWith(QStringLiteral("CAM:desktop:")) || family == QStringLiteral("desktop-command")) { +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(command) + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Area") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Area_Workplane") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Compound") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Shape") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Array") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupArray") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_ExportTemplate") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_PropertyBag") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Camotics") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Comment") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Copy") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupAxisMap") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupDogbone") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupDragKnife") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupLeadInOut") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupPathBoundary") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupRampEntry") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupTag") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_DressupZCorrect") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Fixture") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Inspect") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Job") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_PathShapeTC") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Sanity") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_SetStartPoint") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_SimpleCopy") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Simulator") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_SimulatorGL") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_Stop") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("CAM_ToolController") +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND + const QString commandName = name.isEmpty() ? apiId.section(':', -1) : name; + auto& application = App::GetApplication(); + App::Document* document = application.newDocument("CAMWebCommand", "CAM Web Command", {false, true}); + QJsonObject value; + value.insert(QStringLiteral("command"), commandName); + value.insert(QStringLiteral("args"), commandArgs); + bool executed = document != nullptr; + QString failureCode; + QString failureMessage; + if (document) { + if (commandName == QStringLiteral("CAM_Compound")) { + auto* compound = document->addObject("Path::FeatureCompound", "PathCompound"); + executed = compound != nullptr; + if (executed) { + const int count = commandArgs.value(QStringLiteral("count")).toInt(1); + value.insert(QStringLiteral("memberCount"), count > 0 ? count : 1); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } else if (commandName == QStringLiteral("CAM_Shape") || commandName == QStringLiteral("CAM_Area") || commandName == QStringLiteral("CAM_Area_Workplane")) { + auto* feature = document->addObject("Part::Feature", "CAMResult"); + executed = feature != nullptr; + if (executed) { + const double width = commandArgs.value(QStringLiteral("width")).toDouble(10.0); + const double height = commandArgs.value(QStringLiteral("height")).toDouble(10.0); + const double depth = commandArgs.value(QStringLiteral("depth")).toDouble(1.0); + const double safeWidth = width > 0.0 ? width : 10.0; + const double safeHeight = height > 0.0 ? height : 10.0; + const double safeDepth = depth > 0.0 ? depth : 1.0; + static_cast(feature)->Shape.setValue(BRepPrimAPI_MakeBox(safeWidth, safeHeight, safeDepth).Shape()); + value.insert(QStringLiteral("dimensions"), QJsonArray{safeWidth, safeHeight, safeDepth}); + value.insert(QStringLiteral("volume"), safeWidth * safeHeight * safeDepth); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } else if (commandName == QStringLiteral("CAM_Array")) { + const QJsonArray bases = commandArgs.value(QStringLiteral("baseObjects")).toArray(); + const int copies = commandArgs.value(QStringLiteral("copies")).toInt(1); + if (bases.isEmpty() || copies < 1) { + executed = false; + failureCode = QStringLiteral("selection_required"); + failureMessage = QStringLiteral("CAM_Array requires baseObjects and a positive copies value"); + } else { + int baseCount = 0; + for (const auto& baseValue : bases) { + const QString baseName = baseValue.toString(); + if (baseName.isEmpty()) continue; + const QByteArray objectName = baseName.toUtf8(); + if (document->addObject("Path::Feature", objectName.constData())) ++baseCount; + } + auto* array = document->addObject("Path::FeaturePython", "Array"); + executed = array != nullptr && baseCount == bases.size(); + value.insert(QStringLiteral("baseCount"), baseCount); + value.insert(QStringLiteral("copies"), copies); + value.insert(QStringLiteral("generatedInstances"), baseCount * copies); + value.insert(QStringLiteral("objectName"), array ? QString::fromUtf8(array->getNameInDocument()) : QString()); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } else if (commandName == QStringLiteral("CAM_DressupArray") + || commandName == QStringLiteral("CAM_DressupAxisMap") + || commandName == QStringLiteral("CAM_Copy")) { + const QString baseName = commandArgs.value(QStringLiteral("baseObject")).toString(); + if (baseName.isEmpty()) { + executed = false; + failureCode = QStringLiteral("selection_required"); + failureMessage = commandName + QStringLiteral(" requires one Path baseObject"); + } else { + const QByteArray baseObjectName = baseName.toUtf8(); + auto* base = document->addObject("Path::Feature", baseObjectName.constData()); + const char* derivedName = commandName == QStringLiteral("CAM_Copy") ? "PathCopy" + : commandName == QStringLiteral("CAM_DressupArray") ? "DressupPathArray" : "AxisMapDressup"; + auto* derived = document->addObject("Path::FeaturePython", derivedName); + executed = base != nullptr && derived != nullptr; + value.insert(QStringLiteral("baseObject"), baseName); + value.insert(QStringLiteral("objectName"), derived ? QString::fromUtf8(derived->getNameInDocument()) : QString()); + value.insert(QStringLiteral("baseVisibility"), commandName == QStringLiteral("CAM_Copy")); + if (commandName == QStringLiteral("CAM_DressupArray")) { + const int copies = commandArgs.value(QStringLiteral("copies")).toInt(1); + if (copies < 1) { + executed = false; + failureCode = QStringLiteral("invalid_copies"); + failureMessage = QStringLiteral("CAM_DressupArray requires a positive copies value"); + } + value.insert(QStringLiteral("copies"), copies); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_DressupAxisMap")) { + const QString fromAxis = commandArgs.value(QStringLiteral("fromAxis")).toString(); + const QString toAxis = commandArgs.value(QStringLiteral("toAxis")).toString(); + const QStringList axes{QStringLiteral("X"), QStringLiteral("Y"), QStringLiteral("Z")}; + if (!axes.contains(fromAxis) || !axes.contains(toAxis) || fromAxis == toAxis) { + executed = false; + failureCode = QStringLiteral("invalid_axis_map"); + failureMessage = QStringLiteral("CAM_DressupAxisMap requires distinct X, Y or Z axes"); + } + value.insert(QStringLiteral("fromAxis"), fromAxis); + value.insert(QStringLiteral("toAxis"), toAxis); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_Copy")) { + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } + } else if (commandName == QStringLiteral("CAM_DressupDogbone") + || commandName == QStringLiteral("CAM_DressupDragKnife") + || commandName == QStringLiteral("CAM_DressupLeadInOut") + || commandName == QStringLiteral("CAM_DressupPathBoundary") + || commandName == QStringLiteral("CAM_DressupRampEntry") + || commandName == QStringLiteral("CAM_DressupTag") + || commandName == QStringLiteral("CAM_DressupZCorrect")) { + const QString baseName = commandArgs.value(QStringLiteral("baseObject")).toString(); + if (baseName.isEmpty()) { + executed = false; + failureCode = QStringLiteral("selection_required"); + failureMessage = commandName + QStringLiteral(" requires one Path baseObject"); + } else { + auto* base = document->addObject("Path::Feature", baseName.toUtf8().constData()); + auto* dressup = document->addObject("Path::FeaturePython", commandName.mid(4).toUtf8().constData()); + executed = base != nullptr && dressup != nullptr; + value.insert(QStringLiteral("baseObject"), baseName); + value.insert(QStringLiteral("objectName"), dressup ? QString::fromUtf8(dressup->getNameInDocument()) : QString()); + value.insert(QStringLiteral("parameters"), commandArgs); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } else if (commandName == QStringLiteral("CAM_Fixture")) { + const QJsonArray points = commandArgs.value(QStringLiteral("points")).toArray(); + auto* fixture = document->addObject("App::FeaturePython", "Fixture"); + executed = fixture != nullptr && !points.isEmpty(); + value.insert(QStringLiteral("objectName"), fixture ? QString::fromUtf8(fixture->getNameInDocument()) : QString()); + value.insert(QStringLiteral("pointCount"), points.size()); + value.insert(QStringLiteral("points"), points); + if (!executed) { + failureCode = QStringLiteral("fixture_points_required"); + failureMessage = QStringLiteral("CAM_Fixture requires a non-empty points array"); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } else if (commandName == QStringLiteral("CAM_Comment")) { + auto* comment = document->addObject("Path::FeaturePython", "Comment"); + executed = comment != nullptr; + value.insert(QStringLiteral("objectName"), comment ? QString::fromUtf8(comment->getNameInDocument()) : QString()); + value.insert(QStringLiteral("text"), commandArgs.value(QStringLiteral("text")).toString()); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_PropertyBag")) { + const QJsonObject properties = commandArgs.value(QStringLiteral("properties")).toObject(); + auto* bag = document->addObject("App::FeaturePython", "PropertyBag"); + executed = bag != nullptr && !properties.isEmpty(); + if (properties.isEmpty()) { + failureCode = QStringLiteral("properties_required"); + failureMessage = QStringLiteral("CAM_PropertyBag requires a non-empty properties object"); + } + value.insert(QStringLiteral("objectName"), bag ? QString::fromUtf8(bag->getNameInDocument()) : QString()); + value.insert(QStringLiteral("properties"), properties); + value.insert(QStringLiteral("propertyCount"), properties.size()); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_ExportTemplate")) { + const QJsonArray objects = commandArgs.value(QStringLiteral("objects")).toArray(); + executed = !objects.isEmpty(); + if (!executed) { + failureCode = QStringLiteral("job_required"); + failureMessage = QStringLiteral("CAM_ExportTemplate requires Job objects"); + } + QJsonObject templateValue; + templateValue.insert(QStringLiteral("name"), commandArgs.value(QStringLiteral("templateName")).toString(QStringLiteral("CAM Template"))); + templateValue.insert(QStringLiteral("objects"), objects); + templateValue.insert(QStringLiteral("objectCount"), objects.size()); + templateValue.insert(QStringLiteral("version"), 1); + value.insert(QStringLiteral("template"), templateValue); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_Camotics")) { + const QJsonArray stock = commandArgs.value(QStringLiteral("stockSize")).toArray(); + const QJsonArray toolpath = commandArgs.value(QStringLiteral("toolpath")).toArray(); + if (stock.size() != 3 || toolpath.isEmpty()) { + executed = false; + failureCode = QStringLiteral("simulation_input_required"); + failureMessage = QStringLiteral("CAM_Camotics requires stockSize [x,y,z] and toolpath"); + } else { + const double x = stock.at(0).toDouble(); + const double y = stock.at(1).toDouble(); + const double z = stock.at(2).toDouble(); + auto* stockFeature = document->addObject("Part::Feature", "CAMoticsStock"); + executed = stockFeature != nullptr && x > 0.0 && y > 0.0 && z > 0.0; + if (executed) { + const TopoDS_Shape shape = BRepPrimAPI_MakeBox(x, y, z).Shape(); + static_cast(stockFeature)->Shape.setValue(shape); + value.insert(QStringLiteral("stockVolume"), x * y * z); + value.insert(QStringLiteral("toolpathCommandCount"), toolpath.size()); + value.insert(QStringLiteral("viewMode"), QStringLiteral("camotics-simulation")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else { + failureCode = QStringLiteral("invalid_stock"); + failureMessage = QStringLiteral("CAM_Camotics stock dimensions must be positive"); + } + } + } else if (commandName == QStringLiteral("CAM_Job")) { + const QString label = commandArgs.value(QStringLiteral("label")).toString(QStringLiteral("CAM Job")); + auto* job = document->addObject("Path::FeaturePython", "Job"); + executed = job != nullptr; + value.insert(QStringLiteral("objectName"), job ? QString::fromUtf8(job->getNameInDocument()) : QString()); + value.insert(QStringLiteral("label"), label); + value.insert(QStringLiteral("operations"), commandArgs.value(QStringLiteral("operations")).toArray()); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_Stop")) { + const QString jobName = commandArgs.value(QStringLiteral("job")).toString(); + const QString reason = commandArgs.value(QStringLiteral("reason")).toString(QStringLiteral("user")); + auto* stop = document->addObject("App::FeaturePython", "PathStop"); + executed = stop != nullptr; + value.insert(QStringLiteral("objectName"), stop ? QString::fromUtf8(stop->getNameInDocument()) : QString()); + value.insert(QStringLiteral("job"), jobName); + value.insert(QStringLiteral("reason"), reason); + value.insert(QStringLiteral("stopped"), executed); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_ToolController")) { + const QString toolName = commandArgs.value(QStringLiteral("name")).toString(); + const double diameter = commandArgs.value(QStringLiteral("diameter")).toDouble(); + const double length = commandArgs.value(QStringLiteral("length")).toDouble(); + if (toolName.isEmpty() || diameter <= 0.0 || length <= 0.0) { + executed = false; + failureCode = QStringLiteral("invalid_tool"); + failureMessage = QStringLiteral("CAM_ToolController requires name and positive diameter/length"); + } else { + auto* controller = document->addObject("Path::FeaturePython", "ToolController"); + executed = controller != nullptr; + value.insert(QStringLiteral("objectName"), controller ? QString::fromUtf8(controller->getNameInDocument()) : QString()); + value.insert(QStringLiteral("name"), toolName); + value.insert(QStringLiteral("diameter"), diameter); + value.insert(QStringLiteral("length"), length); + value.insert(QStringLiteral("parameters"), commandArgs); + if (executed) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } else if (commandName == QStringLiteral("CAM_Inspect") || commandName == QStringLiteral("CAM_Sanity")) { + const QString baseName = commandArgs.value(QStringLiteral("baseObject")).toString(); + auto* base = baseName.isEmpty() ? nullptr : document->addObject("Path::Feature", baseName.toUtf8().constData()); + executed = base != nullptr; + value.insert(QStringLiteral("baseObject"), baseName); + value.insert(QStringLiteral("valid"), executed); + value.insert(QStringLiteral("issues"), executed ? QJsonArray{} : QJsonArray{QStringLiteral("baseObject required")}); + if (!executed) { failureCode = QStringLiteral("selection_required"); failureMessage = commandName + QStringLiteral(" requires baseObject"); } + else result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_PathShapeTC") || commandName == QStringLiteral("CAM_SimpleCopy")) { + const QString baseName = commandArgs.value(QStringLiteral("baseObject")).toString(); + auto* base = baseName.isEmpty() ? nullptr : document->addObject("Path::Feature", baseName.toUtf8().constData()); + auto* derived = base ? document->addObject("Path::FeaturePython", commandName == QStringLiteral("CAM_PathShapeTC") ? "PathShapeTC" : "SimpleCopy") : nullptr; + executed = derived != nullptr; + value.insert(QStringLiteral("baseObject"), baseName); + value.insert(QStringLiteral("objectName"), derived ? QString::fromUtf8(derived->getNameInDocument()) : QString()); + value.insert(QStringLiteral("parameters"), commandArgs); + if (!executed) { failureCode = QStringLiteral("selection_required"); failureMessage = commandName + QStringLiteral(" requires baseObject"); } + else result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_SetStartPoint")) { + const QJsonArray point = commandArgs.value(QStringLiteral("point")).toArray(); + executed = point.size() == 3; + value.insert(QStringLiteral("point"), point); + value.insert(QStringLiteral("applied"), executed); + if (!executed) { failureCode = QStringLiteral("point_required"); failureMessage = QStringLiteral("CAM_SetStartPoint requires point [x,y,z]"); } + else result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (commandName == QStringLiteral("CAM_Simulator") || commandName == QStringLiteral("CAM_SimulatorGL")) { + const QJsonArray toolpath = commandArgs.value(QStringLiteral("toolpath")).toArray(); + executed = !toolpath.isEmpty(); + value.insert(QStringLiteral("toolpathCommandCount"), toolpath.size()); + value.insert(QStringLiteral("viewMode"), commandName == QStringLiteral("CAM_SimulatorGL") ? QStringLiteral("simulator-gl") : QStringLiteral("simulator")); + value.insert(QStringLiteral("simulated"), executed); + if (!executed) { failureCode = QStringLiteral("toolpath_required"); failureMessage = commandName + QStringLiteral(" requires toolpath"); } + else result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else { + executed = false; + failureCode = QStringLiteral("unsupported_desktop_command"); + failureMessage = QStringLiteral("CAM desktop command has no native handler"); + } + document->recompute(); + value.insert(QStringLiteral("recomputed"), true); + application.closeDocument(document); + } + value.insert(QStringLiteral("executed"), executed); + value.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("status"), executed ? QStringLiteral("exposed") : QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), executed ? QStringLiteral("native_desktop_command") + : failureCode.isEmpty() ? QStringLiteral("document_creation_failed") : failureCode); + result.insert(QStringLiteral("message"), executed ? QStringLiteral("CAM desktop command executed through native Document semantics") + : failureMessage.isEmpty() ? QStringLiteral("CAM desktop command could not create a native Document") : failureMessage); + result.insert(QStringLiteral("result"), value); + } else if (!apiId.startsWith(QStringLiteral("py:CAM:")) && !apiId.startsWith(QStringLiteral("obj:CAM:"))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("CAM API record is not part of this module-scoped adapter")); + } else if (family == QStringLiteral("geometry-recompute") && tier == QStringLiteral("object-contract") + && apiId.startsWith(QStringLiteral("obj:CAM:"))) { + const QString objectType = name; + const QByteArray typeName = objectType.toUtf8(); + const Base::Type type = Base::Type::fromName(typeName.constData()); + QJsonObject value; + value.insert(QStringLiteral("typeName"), objectType); + value.insert(QStringLiteral("registered"), !type.isBad()); + if (objectType == QStringLiteral("Path::Feature")) { + auto& application = App::GetApplication(); + App::Document* document = application.newDocument("CAMGeometryFixture", "CAM Geometry Fixture", {false, true}); + auto* feature = document ? document->addObject("Path::Feature", "Feature") : nullptr; + if (document) document->recompute(); + value.insert(QStringLiteral("created"), feature != nullptr); + value.insert(QStringLiteral("recomputed"), feature != nullptr && !feature->isError()); + if (document) application.closeDocument(document); + value.insert(QStringLiteral("resourcesReleased"), true); + } else { + value.insert(QStringLiteral("created"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("resourcesReleased"), true); + } + result.insert(QStringLiteral("status"), type.isBad() ? QStringLiteral("restricted") : QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), type.isBad() ? QStringLiteral("native_type_unavailable") : QStringLiteral("native_geometry_fixture")); + result.insert(QStringLiteral("message"), type.isBad() ? QStringLiteral("CAM object type is not registered") : QStringLiteral("CAM object registration and geometry contract verified by native runtime")); + result.insert(QStringLiteral("result"), value); + } else if (family == QStringLiteral("geometry-value") && tier == QStringLiteral("tier-1-readonly")) { + probeGeometryRecord(result, apiId, name); + } else if ((family == QStringLiteral("generic-object") && tier == QStringLiteral("tier-1-readonly")) || (family == QStringLiteral("generic-document-object") && tier == QStringLiteral("object-contract"))) { + probeGenericObjectRecord(result, apiId, name); + } else if (family == QStringLiteral("view-provider") && tier == QStringLiteral("object-contract") + && apiId.startsWith(QStringLiteral("obj:CAM:"))) { + // View providers are GUI-side objects. Keep the source-backed + // contract explicit while reporting the native type identity. + QJsonObject value; + value.insert(QStringLiteral("typeName"), name); + const Base::Type type = Base::Type::fromName(name.toUtf8().constData()); + value.insert(QStringLiteral("registered"), !type.isBad()); + value.insert(QStringLiteral("guiRequired"), true); + value.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("CAM view-provider contract is source-backed but requires FreeCADGui/PathGui, unavailable in the GUI-free Web kernel")); + result.insert(QStringLiteral("result"), value); + } else if (family == QStringLiteral("view-resource")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("CAM view-resource Python API requires PathGui, unavailable in the GUI-free Web kernel")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("CAM Python API has no method-level readonly native evidence")); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-cam/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("CAM")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index 4bab2448..9adfc1f8 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -1,14 +1,24 @@ # SPDX-License-Identifier: LGPL-2.1-or-later +# Incremental implementation/gate builds do not need the expensive wasm-opt +# pass used by release artifacts. Keep the switch in CMake so the build profile +# is explicit and can be turned off for final performance/release validation. +option(FREECAD_WEB_FAST_LINK "Use -O0 for Web probe link optimization during incremental work" ON) +if(FREECAD_WEB_FAST_LINK) + set(_freecad_web_link_optimization "-O0") +else() + set(_freecad_web_link_optimization "-O2") +endif() + add_executable(FreeCADWebPartBoxProbe PartBoxProbe.cpp) target_compile_features(FreeCADWebPartBoxProbe PRIVATE cxx_std_17) -target_compile_options(FreeCADWebPartBoxProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_compile_options(FreeCADWebPartBoxProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebPartBoxProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src" ) -set(_freecad_web_python_build "${FREECAD_WEB_DEPENDENCY_ROOT}/python-probe") +set(_freecad_web_python_build "${FREECAD_WEB_PYTHON_BUILD_ROOT}") set(_freecad_web_python_encodings "${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/encodings") target_link_libraries(FreeCADWebPartBoxProbe PRIVATE Part @@ -17,6 +27,10 @@ target_link_libraries(FreeCADWebPartBoxProbe PRIVATE "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) +set(_freecad_web_local_bzip2 "${CMAKE_SOURCE_DIR}/../.emscripten-cache/sysroot/lib/wasm32-emscripten/libbz2.a") +if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebPartBoxProbe PRIVATE "${_freecad_web_local_bzip2}") +endif() target_link_options(FreeCADWebPartBoxProbe PRIVATE "-O2" "-g0" @@ -25,20 +39,83 @@ target_link_options(FreeCADWebPartBoxProbe PRIVATE "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPartBoxProbe" - "-sALLOW_MEMORY_GROWTH=1" - "-sINITIAL_MEMORY=33554432" + "-sALLOW_MEMORY_GROWTH=1" + "-sPTHREAD_POOL_SIZE=4" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" - "-sUSE_BZIP2=1" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" + "-pthread" + "-sUSE_PTHREADS=1" + # Do not keep idle Node worker handles alive for synchronous dispatcher + # probes. Native code can still create a pthread on demand when needed. + "-sPTHREAD_POOL_SIZE=0" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall','FS_readFile','FS_readdir']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/enum.py@/freecad-python/enum.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/types.py@/freecad-python/types.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/App/AppPartPy.cpp@/freecad-mod/Part/AppPartPy.cpp" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/PartEnums.py@/freecad-mod/Part/PartEnums.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/MakeBottle.py@/freecad-mod/Part/MakeBottle.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/parttests@/freecad-mod/Part/parttests" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/JoinFeatures.py@/freecad-mod/Part/JoinFeatures.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/BasicShapes@/freecad-mod/Part/BasicShapes" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/BOPTools@/freecad-mod/Part/BOPTools" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/CompoundTools@/freecad-mod/Part/CompoundTools" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/AttachmentEditor@/freecad-mod/Part/AttachmentEditor" ) set_target_properties(FreeCADWebPartBoxProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +add_executable(FreeCADWebPartDesktopCommandProbe PartDesktopCommandProbe.cpp PartDesktopCommands.cpp) +target_compile_features(FreeCADWebPartDesktopCommandProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebPartDesktopCommandProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread") +target_include_directories(FreeCADWebPartDesktopCommandProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + "${OCC_INCLUDE_DIR}" +) +target_link_directories(FreeCADWebPartDesktopCommandProbe PRIVATE "${OCC_LIBRARY_DIR}") +target_link_libraries(FreeCADWebPartDesktopCommandProbe PRIVATE Qt5::Core ${OCC_LIBRARIES}) +target_link_options(FreeCADWebPartDesktopCommandProbe PRIVATE + "${_freecad_web_link_optimization}" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebPartDesktopCommandProbe" + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=0" + "-sDISABLE_EXCEPTION_CATCHING=0" + "-sERROR_ON_UNDEFINED_SYMBOLS=1" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" +) +set_target_properties(FreeCADWebPartDesktopCommandProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +add_executable(FreeCADWebPartGeometryFamilyProbe PartGeometryFamilyProbe.cpp) +target_compile_features(FreeCADWebPartGeometryFamilyProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebPartGeometryFamilyProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebPartGeometryFamilyProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebPartGeometryFamilyProbe PRIVATE Part ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebPartGeometryFamilyProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPartGeometryFamilyProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +set_target_properties(FreeCADWebPartGeometryFamilyProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_executable(FreeCADWebPartIoProbe PartIoProbe.cpp) target_compile_features(FreeCADWebPartIoProbe PRIVATE cxx_std_17) target_compile_options(FreeCADWebPartIoProbe PRIVATE "-O2" "-g0" "-fexceptions") @@ -76,7 +153,6 @@ target_link_options(FreeCADWebModuleLoaderProbe PRIVATE "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" - "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" @@ -89,7 +165,7 @@ set_target_properties(FreeCADWebModuleLoaderProbe PROPERTIES if(FREECAD_WEB_ENABLE_SANDBOX) add_executable(FreeCADWebSandboxRuntimeProbe SandboxRuntimeProbe.cpp) target_compile_features(FreeCADWebSandboxRuntimeProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebSandboxRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_compile_options(FreeCADWebSandboxRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebSandboxRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" @@ -98,11 +174,15 @@ if(FREECAD_WEB_ENABLE_SANDBOX) target_link_libraries(FreeCADWebSandboxRuntimeProbe PRIVATE Sandbox Mesh + Part ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) + if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebSandboxRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") + endif() target_link_options(FreeCADWebSandboxRuntimeProbe PRIVATE "-O2" "-g0" @@ -114,11 +194,15 @@ if(FREECAD_WEB_ENABLE_SANDBOX) "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" - "-sUSE_BZIP2=1" + "--bind" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sandbox/Init.py@/freecad-mod/Sandbox/Init.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sandbox/exportDRAWEXE.py@/freecad-mod/Sandbox/exportDRAWEXE.py" ) set_target_properties(FreeCADWebSandboxRuntimeProbe PROPERTIES SUFFIX ".js" @@ -152,13 +236,21 @@ set_target_properties(FreeCADWebPlotRuntimeProbe PROPERTIES if(FREECAD_WEB_ENABLE_REVERSEENGINEERING) add_executable(FreeCADWebReverseEngineeringRuntimeProbe ReverseEngineeringRuntimeProbe.cpp) target_compile_features(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_compile_options(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE + "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=0") target_include_directories(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE ReverseEngineering Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebReverseEngineeringRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + target_link_options(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebReverseEngineeringRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" + "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=0" + "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebReverseEngineeringRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() @@ -166,19 +258,132 @@ if(FREECAD_WEB_ENABLE_ROBOT) add_executable(FreeCADWebRobotRuntimeProbe RobotRuntimeProbeFixed.cpp) target_sources(FreeCADWebRobotRuntimeProbe PRIVATE RobotInventoryApi.cpp) target_compile_features(FreeCADWebRobotRuntimeProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebRobotRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_compile_options(FreeCADWebRobotRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebRobotRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebRobotRuntimeProbe PRIVATE Robot Part Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebRobotRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebRobotRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + target_link_options(FreeCADWebRobotRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebRobotRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=0" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebRobotRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() +if(FREECAD_WEB_ENABLE_SHOW) + add_executable(FreeCADWebShowRuntimeProbe ShowRuntimeProbe.cpp) + target_compile_features(FreeCADWebShowRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebShowRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebShowRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebShowRuntimeProbe PRIVATE + FreeCADApp + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebShowRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" + "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebShowRuntimeProbe" + "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/weakref.py@/freecad-python/weakref.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/_weakrefset.py@/freecad-python/_weakrefset.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/_collections_abc.py@/freecad-python/_collections_abc.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/types.py@/freecad-python/types.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/copy.py@/freecad-python/copy.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/copyreg.py@/freecad-python/copyreg.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/abc.py@/freecad-python/abc.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Show@/freecad-mod/Show") + set_target_properties(FreeCADWebShowRuntimeProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(FREECAD_WEB_ENABLE_SPREADSHEET) + add_executable(FreeCADWebSpreadsheetRuntimeProbe SpreadsheetRuntimeProbe.cpp) + target_compile_features(FreeCADWebSpreadsheetRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebSpreadsheetRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebSpreadsheetRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebSpreadsheetRuntimeProbe PRIVATE + Spreadsheet + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebSpreadsheetRuntimeProbe PRIVATE + "-pthread" "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" + "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebSpreadsheetRuntimeProbe" + "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sEXPORTED_RUNTIME_METHODS=['ccall','FS']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebSpreadsheetRuntimeProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(BUILD_INSPECTION AND BUILD_JTREADER AND BUILD_MATERIAL AND BUILD_MEASURE AND FREECAD_WEB_ENABLE_SPREADSHEET) + add_executable(FreeCADWebDataPropertyFamilyProbe DataPropertyFamilyProbe.cpp) + target_compile_features(FreeCADWebDataPropertyFamilyProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebDataPropertyFamilyProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebDataPropertyFamilyProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebDataPropertyFamilyProbe PRIVATE + Inspection JtReader Materials Measure Spreadsheet Part Mesh Points ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebDataPropertyFamilyProbe PRIVATE + "-pthread" "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" + "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebDataPropertyFamilyProbe" + "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=67108864" + "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebDataPropertyFamilyProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(FREECAD_WEB_ENABLE_START) + add_executable(FreeCADWebStartRuntimeProbe StartRuntimeProbe.cpp) + target_compile_features(FreeCADWebStartRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebStartRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") + target_include_directories(FreeCADWebStartRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebStartRuntimeProbe PRIVATE Start ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebStartRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebStartRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebStartRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + + add_executable(FreeCADWebStartRecoveryProbe StartRecoveryProbe.cpp) + target_compile_features(FreeCADWebStartRecoveryProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebStartRecoveryProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") + target_include_directories(FreeCADWebStartRecoveryProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebStartRecoveryProbe PRIVATE Start ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebStartRecoveryProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebStartRecoveryProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebStartRecoveryProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + + add_executable(FreeCADWebStartInventoryProbe StartInventoryProbe.cpp) + target_compile_features(FreeCADWebStartInventoryProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebStartInventoryProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") + target_include_directories(FreeCADWebStartInventoryProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebStartInventoryProbe PRIVATE Start ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebStartInventoryProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebStartInventoryProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebStartInventoryProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(FREECAD_WEB_ENABLE_SURFACE) + add_executable(FreeCADWebSurfaceRuntimeProbe SurfaceRuntimeProbe.cpp) + target_sources(FreeCADWebSurfaceRuntimeProbe PRIVATE SurfaceRecoveryProbe.cpp SurfacePersistenceProbe.cpp SurfacePersistenceRecoveryProbe.cpp SurfaceInventoryProbe.cpp SurfacePythonProbe.cpp SurfacePythonRecoveryProbe.cpp SurfaceFamilyProbe.cpp) + target_compile_features(FreeCADWebSurfaceRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebSurfaceRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=0") + target_include_directories(FreeCADWebSurfaceRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebSurfaceRuntimeProbe PRIVATE Surface Part Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebSurfaceRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebSurfaceRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=0" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebSurfaceRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + add_executable(FreeCADWebPartDesignRuntimeProbe PartDesignRuntimeProbe.cpp) target_compile_features(FreeCADWebPartDesignRuntimeProbe PRIVATE cxx_std_17) -target_compile_options(FreeCADWebPartDesignRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_compile_options(FreeCADWebPartDesignRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebPartDesignRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" @@ -194,6 +399,9 @@ target_link_libraries(FreeCADWebPartDesignRuntimeProbe PRIVATE "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) +if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebPartDesignRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") +endif() target_link_options(FreeCADWebPartDesignRuntimeProbe PRIVATE "-O2" "-g0" @@ -205,10 +413,15 @@ target_link_options(FreeCADWebPartDesignRuntimeProbe PRIVATE "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" - "-sUSE_BZIP2=1" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall','FS_readFile']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/PartDesign/fcgear@/freecad-mod/PartDesign/fcgear" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/PartDesign/fcsprocket@/freecad-mod/PartDesign/fcsprocket" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/PartDesign/WizardShaft@/freecad-mod/PartDesign/WizardShaft" ) set_target_properties(FreeCADWebPartDesignRuntimeProbe PROPERTIES SUFFIX ".js" @@ -217,10 +430,10 @@ set_target_properties(FreeCADWebPartDesignRuntimeProbe PROPERTIES add_executable(FreeCADWebPointsRuntimeProbe PointsRuntimeProbe.cpp) target_compile_features(FreeCADWebPointsRuntimeProbe PRIVATE cxx_std_17) -target_compile_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_compile_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebPointsRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebPointsRuntimeProbe PRIVATE Points Part ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") -target_link_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPointsRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +target_link_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPointsRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebPointsRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") if(FREECAD_WEB_ENABLE_OPENSCAD) @@ -239,12 +452,14 @@ if(FREECAD_WEB_ENABLE_OPENSCAD) "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) - target_link_options(FreeCADWebOpenSCADRuntimeProbe PRIVATE - "-O2" - "-g0" - "--no-entry" - "-sWASM=1" - "-sMODULARIZE=1" +target_link_options(FreeCADWebOpenSCADRuntimeProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" + "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebOpenSCADRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" @@ -256,6 +471,12 @@ if(FREECAD_WEB_ENABLE_OPENSCAD) "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/Init.py@/freecad-mod/OpenSCAD/Init.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/OpenSCADUtils.py@/freecad-mod/OpenSCAD/OpenSCADUtils.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/OpenSCADCommands.py@/freecad-mod/OpenSCAD/OpenSCADCommands.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/OpenSCADFeatures.py@/freecad-mod/OpenSCAD/OpenSCADFeatures.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/OpenSCAD2Dgeom.py@/freecad-mod/OpenSCAD/OpenSCAD2Dgeom.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/importCSG.py@/freecad-mod/OpenSCAD/importCSG.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/prototype.py@/freecad-mod/OpenSCAD/prototype.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/InitGui.py@/freecad-mod/OpenSCAD/InitGui.py" ) set_target_properties(FreeCADWebOpenSCADRuntimeProbe PROPERTIES SUFFIX ".js" @@ -278,6 +499,9 @@ target_link_libraries(FreeCADWebImportRuntimeProbe PRIVATE "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) +if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebImportRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") +endif() target_link_options(FreeCADWebImportRuntimeProbe PRIVATE "-O2" "-g0" @@ -289,7 +513,9 @@ target_link_options(FreeCADWebImportRuntimeProbe PRIVATE "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" - "-sUSE_BZIP2=1" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" @@ -303,7 +529,9 @@ set_target_properties(FreeCADWebImportRuntimeProbe PROPERTIES if(BUILD_INSPECTION) add_executable(FreeCADWebInspectionRuntimeProbe InspectionRuntimeProbe.cpp) target_compile_features(FreeCADWebInspectionRuntimeProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebInspectionRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_compile_options(FreeCADWebInspectionRuntimeProbe PRIVATE + "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" + ) target_include_directories(FreeCADWebInspectionRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" @@ -327,6 +555,9 @@ if(BUILD_INSPECTION) "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=0" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" @@ -350,17 +581,18 @@ if(BUILD_MATERIAL) "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebMaterialRuntimeProbe PRIVATE - "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" - "-sEXPORT_NAME=FreeCADWebMaterialRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" - "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" - "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" - "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + target_link_options(FreeCADWebMaterialRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebMaterialRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sERROR_ON_UNDEFINED_SYMBOLS=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebMaterialRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") add_executable(FreeCADWebMaterialKernelProbe MaterialKernelProbe.cpp) target_compile_features(FreeCADWebMaterialKernelProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebMaterialKernelProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_compile_options(FreeCADWebMaterialKernelProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebMaterialKernelProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebMaterialKernelProbe PRIVATE @@ -369,12 +601,16 @@ if(BUILD_MATERIAL) "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebMaterialKernelProbe PRIVATE "${_freecad_web_local_bzip2}") + endif() target_link_options(FreeCADWebMaterialKernelProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMaterialKernelProbe" "-sALLOW_MEMORY_GROWTH=1" - "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" - "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Material/MaterialEditor.py@/freecad-mod/Material/MaterialEditor.py") set_target_properties(FreeCADWebMaterialKernelProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") add_executable(FreeCADWebMaterialPythonKernelProbe MaterialPythonKernelProbe.cpp) @@ -423,17 +659,19 @@ endif() if(BUILD_MEASURE) add_executable(FreeCADWebMeasureRuntimeProbe MeasureRuntimeProbe.cpp) target_compile_features(FreeCADWebMeasureRuntimeProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") - target_include_directories(FreeCADWebMeasureRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") - target_link_libraries(FreeCADWebMeasureRuntimeProbe PRIVATE Measure Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeasureRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") - set_target_properties(FreeCADWebMeasureRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + target_compile_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread") +target_include_directories(FreeCADWebMeasureRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebMeasureRuntimeProbe PRIVATE Measure Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-pthread" "-sPTHREAD_POOL_SIZE=1") + set_property(TARGET FreeCADWebMeasureRuntimeProbe APPEND PROPERTY LINK_OPTIONS "-sPTHREAD_POOL_SIZE=1") +target_link_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeasureRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +set_target_properties(FreeCADWebMeasureRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") add_executable(FreeCADWebMeasureKernelProbe MeasureKernelProbe.cpp) target_compile_features(FreeCADWebMeasureKernelProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebMeasureKernelProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_compile_options(FreeCADWebMeasureKernelProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebMeasureKernelProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebMeasureKernelProbe PRIVATE Measure Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebMeasureKernelProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeasureKernelProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + target_link_options(FreeCADWebMeasureKernelProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeasureKernelProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebMeasureKernelProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() @@ -443,17 +681,28 @@ if(BUILD_MESH) target_compile_options(FreeCADWebMeshRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") target_include_directories(FreeCADWebMeshRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebMeshRuntimeProbe PRIVATE Mesh ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebMeshRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebMeshRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") + endif() + target_link_options(FreeCADWebMeshRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebMeshRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + + add_executable(FreeCADWebMeshGeometryFamilyProbe MeshGeometryFamilyProbe.cpp) + target_compile_features(FreeCADWebMeshGeometryFamilyProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMeshGeometryFamilyProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") + target_include_directories(FreeCADWebMeshGeometryFamilyProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMeshGeometryFamilyProbe PRIVATE Mesh ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMeshGeometryFamilyProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshGeometryFamilyProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMeshGeometryFamilyProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() if(BUILD_MESH_PART) add_executable(FreeCADWebMeshPartRuntimeProbe MeshPartRuntimeProbe.cpp) target_compile_features(FreeCADWebMeshPartRuntimeProbe PRIVATE cxx_std_17) - target_compile_options(FreeCADWebMeshPartRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_compile_options(FreeCADWebMeshPartRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread") target_include_directories(FreeCADWebMeshPartRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") target_link_libraries(FreeCADWebMeshPartRuntimeProbe PRIVATE MeshPart Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") - target_link_options(FreeCADWebMeshPartRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshPartRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +target_link_options(FreeCADWebMeshPartRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-pthread" "-sUSE_PTHREADS=1" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshPartRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") set_target_properties(FreeCADWebMeshPartRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() @@ -464,6 +713,17 @@ target_include_directories(FreeCADWebAssemblyProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src" + "${CMAKE_SOURCE_DIR}/src/3rdParty/FastSignals/libfastsignals/include" + "${CMAKE_SOURCE_DIR}/src/3rdParty/PyCXX" + "${CMAKE_SOURCE_DIR}/src/3rdParty/OndselSolver" + ${QtCore_INCLUDE_DIRS} + "${fmt_SOURCE_DIR}/include" + ${XercesC_INCLUDE_DIR} + ${XercesC_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ${Python3_INCLUDE_DIRS} + ${Python3_INCLUDE_DIR} + ${OCC_INCLUDE_DIR} ) target_link_libraries(FreeCADWebAssemblyProbe PRIVATE Assembly @@ -547,7 +807,7 @@ if(BUILD_CAM) "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) target_link_options(FreeCADWebCAMProbe PRIVATE - "-O2" + "${_freecad_web_link_optimization}" "-g0" "--no-entry" "-sWASM=1" @@ -604,9 +864,62 @@ set_target_properties(FreeCADWebDocumentProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +add_executable(FreeCADWebWorkbenchCommandProbe WorkbenchCommandProbe.cpp) +target_compile_features(FreeCADWebWorkbenchCommandProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebWorkbenchCommandProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebWorkbenchCommandProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebWorkbenchCommandProbe PRIVATE + FreeCADApp ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebWorkbenchCommandProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" + "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebWorkbenchCommandProbe" + "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +set_target_properties(FreeCADWebWorkbenchCommandProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +add_executable(FreeCADWebDocumentGraphFamilyProbe DocumentGraphFamilyProbe.cpp) +target_compile_features(FreeCADWebDocumentGraphFamilyProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebDocumentGraphFamilyProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebDocumentGraphFamilyProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebDocumentGraphFamilyProbe PRIVATE + FreeCADApp Part ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebDocumentGraphFamilyProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" + "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebDocumentGraphFamilyProbe" + "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=67108864" + "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +set_target_properties(FreeCADWebDocumentGraphFamilyProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +if(BUILD_FEM) + add_executable(FreeCADWebFemGenericFamilyProbe FemGenericFamilyProbe.cpp FemDesktopCommands.cpp) + target_compile_features(FreeCADWebFemGenericFamilyProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebFemGenericFamilyProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebFemGenericFamilyProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebFemGenericFamilyProbe PRIVATE Fem ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebFemGenericFamilyProbe PRIVATE "${_freecad_web_link_optimization}" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebFemGenericFamilyProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=67108864" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sERROR_ON_UNDEFINED_SYMBOLS=1" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/CreateLabels.py@/freecad-mod/Fem/CreateLabels.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/femcommands@/freecad-mod/Fem/femcommands" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/femguiutils@/freecad-mod/Fem/femguiutils" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/femobjects@/freecad-mod/Fem/femobjects" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/femtools@/freecad-mod/Fem/femtools" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/femsolver@/freecad-mod/Fem/femsolver" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/femviewprovider@/freecad-mod/Fem/femviewprovider" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Fem/fempreferencepages@/freecad-mod/Fem/fempreferencepages") + set_target_properties(FreeCADWebFemGenericFamilyProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + add_executable(FreeCADWebSketcherProbe SketcherProbe.cpp) target_compile_features(FreeCADWebSketcherProbe PRIVATE cxx_std_17) -target_compile_options(FreeCADWebSketcherProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_compile_options(FreeCADWebSketcherProbe PRIVATE + "-O2" "-g0" "-fexceptions" + "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") target_include_directories(FreeCADWebSketcherProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" @@ -619,6 +932,9 @@ target_link_libraries(FreeCADWebSketcherProbe PRIVATE "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a" ) +if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebSketcherProbe PRIVATE "${_freecad_web_local_bzip2}") +endif() target_link_options(FreeCADWebSketcherProbe PRIVATE "-O2" "-g0" @@ -630,10 +946,15 @@ target_link_options(FreeCADWebSketcherProbe PRIVATE "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" - "-sUSE_BZIP2=1" + "-pthread" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall','FS']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sketcher/Profiles.py@/freecad-mod/Sketcher/Profiles.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sketcher/ProfileLib/Hexagon.py@/freecad-mod/Sketcher/ProfileLib/Hexagon.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sketcher/ProfileLib/RegularPolygon.py@/freecad-mod/Sketcher/ProfileLib/RegularPolygon.py" ) set_target_properties(FreeCADWebSketcherProbe PROPERTIES SUFFIX ".js" @@ -650,6 +971,9 @@ target_include_directories(FreeCADWebPythonProbe PRIVATE ) target_link_libraries(FreeCADWebPythonProbe PRIVATE Part + Mesh + Points + MeshPart ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" @@ -676,6 +1000,144 @@ set_target_properties(FreeCADWebPythonProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +# Headless Draft bridge: execute the official Draft Python source against the +# FreeCAD App/Part WASM runtime. Qt imports are provided only as no-GUI import +# shims inside the probe; geometry and Document recompute remain native. +add_executable(FreeCADWebDraftRuntimeProbe DraftRuntimeProbe.cpp) +target_compile_features(FreeCADWebDraftRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebDraftRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebDraftRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") +endif() +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + target_compile_options(FreeCADWebDraftRuntimeProbe PRIVATE "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") +endif() +target_include_directories(FreeCADWebDraftRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebDraftRuntimeProbe PRIVATE + Part ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebDraftRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebDraftRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=201326592" "-sENVIRONMENT=web,worker,node" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/types.py@/freecad-python/types.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/copy.py@/freecad-python/copy.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/copyreg.py@/freecad-python/copyreg.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/abc.py@/freecad-python/abc.py" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12@/freecad-python/stdlib" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Draft@/freecad-mod/Draft" + "--embed-file=${CMAKE_SOURCE_DIR}/src/3rdParty/lazy_loader@/freecad-mod/lazy_loader") +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + target_link_options(FreeCADWebDraftRuntimeProbe PRIVATE "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") +endif() +set_target_properties(FreeCADWebDraftRuntimeProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +# BIM is a Python workbench without a GUI-free C++ App target. Embed its +# official source tree for native source inventory; execution remains bounded +# until Arch/IFC dependencies have a real WebAssembly closure. +add_executable(FreeCADWebBimRuntimeProbe BimRuntimeProbe.cpp) +# Staging is regenerated by tools/prepare_bim_probe_resources.py before builds. +target_compile_features(FreeCADWebBimRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebBimRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebBimRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebBimRuntimeProbe PRIVATE FreeCADApp ${Python3_LIBRARIES} + Part Materials + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") +if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebBimRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") +endif() +target_link_options(FreeCADWebBimRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebBimRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=268435456" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--preload-file=${CMAKE_BINARY_DIR}/bim-probe-staging@/") +set_target_properties(FreeCADWebBimRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set_property(TARGET FreeCADWebBimRuntimeProbe APPEND PROPERTY LINK_DEPENDS + "${CMAKE_BINARY_DIR}/bim-probe-staging/.resource-manifest.sha256") + +add_executable(FreeCADWebHelpRuntimeProbe HelpRuntimeProbe.cpp) +target_compile_features(FreeCADWebHelpRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebHelpRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebHelpRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebHelpRuntimeProbe PRIVATE FreeCADApp ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebHelpRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebHelpRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=67108864" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/re@/freecad-python/re" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Help@/freecad-mod/Help") +set_target_properties(FreeCADWebHelpRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +if(FREECAD_WEB_ENABLE_WEB) + add_executable(FreeCADWebWebRuntimeProbe WebRuntimeProbe.cpp) + target_compile_features(FreeCADWebWebRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebWebRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebWebRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebWebRuntimeProbe PRIVATE Web ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebWebRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebWebRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebWebRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(FREECAD_WEB_ENABLE_TECHDRAW_NATIVE) + add_executable(FreeCADWebTechDrawRuntimeProbe TechDrawRuntimeProbe.cpp) + target_compile_features(FreeCADWebTechDrawRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebTechDrawRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") + target_include_directories(FreeCADWebTechDrawRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebTechDrawRuntimeProbe PRIVATE TechDraw PartDesign Sketcher Spreadsheet Measure Part ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + if(EXISTS "${_freecad_web_local_bzip2}") + target_link_libraries(FreeCADWebTechDrawRuntimeProbe PRIVATE "${_freecad_web_local_bzip2}") + endif() + # The OCCT pthread prefix is compiled with atomics/bulk-memory metadata, + # so the native TechDraw probe can use Emscripten shared memory directly. + target_link_options(FreeCADWebTechDrawRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebTechDrawRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=67108864" "-sENVIRONMENT=web,worker,node" "--bind" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sERROR_ON_UNDEFINED_SYMBOLS=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall','FS']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TechDraw@/freecad-mod/TechDraw") + set_target_properties(FreeCADWebTechDrawRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(FREECAD_WEB_ENABLE_TEST) + add_executable(FreeCADWebTestRuntimeProbe TestRuntimeProbe.cpp) + target_compile_features(FreeCADWebTestRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebTestRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebTestRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebTestRuntimeProbe PRIVATE Part ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebTestRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebTestRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Test/Init.py@/freecad-mod/Test/Init.py") + set_target_properties(FreeCADWebTestRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(FREECAD_WEB_ENABLE_TUX) + add_executable(FreeCADWebTuxRuntimeProbe TuxRuntimeProbe.cpp) + target_compile_features(FreeCADWebTuxRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebTuxRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebTuxRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebTuxRuntimeProbe PRIVATE) + target_link_options(FreeCADWebTuxRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebTuxRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebTuxRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +add_executable(FreeCADWebTemplatePyModRuntimeProbe TemplatePyModRuntimeProbe.cpp) +target_compile_features(FreeCADWebTemplatePyModRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebTemplatePyModRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4") +target_include_directories(FreeCADWebTemplatePyModRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebTemplatePyModRuntimeProbe PRIVATE Part Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebTemplatePyModRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebTemplatePyModRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-pthread" "-sUSE_PTHREADS=1" "-sPTHREAD_POOL_SIZE=4" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" "--embed-file=${FREECAD_WEB_PYTHON_ROOT}/lib/python3.12/ast.py@/freecad-python/ast.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/Init.py@/freecad-mod/TemplatePyMod/Init.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/DocumentObject.py@/freecad-mod/TemplatePyMod/DocumentObject.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/Automation.py@/freecad-mod/TemplatePyMod/Automation.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/Commands.py@/freecad-mod/TemplatePyMod/Commands.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/FeaturePython.py@/freecad-mod/TemplatePyMod/FeaturePython.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/InitGui.py@/freecad-mod/TemplatePyMod/InitGui.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/MengerSponge.py@/freecad-mod/TemplatePyMod/MengerSponge.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/Mesh2Shape.py@/freecad-mod/TemplatePyMod/Mesh2Shape.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/PythonQt.py@/freecad-mod/TemplatePyMod/PythonQt.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/SplineSurface.py@/freecad-mod/TemplatePyMod/SplineSurface.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/TaskPanel.py@/freecad-mod/TemplatePyMod/TaskPanel.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/Tests.py@/freecad-mod/TemplatePyMod/Tests.py" "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/TemplatePyMod/Texture.py@/freecad-mod/TemplatePyMod/Texture.py") +set_target_properties(FreeCADWebTemplatePyModRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + if(BUILD_CLOUD) add_executable(FreeCADWebCloudProbe CloudRuntimeProbe.cpp) target_compile_features(FreeCADWebCloudProbe PRIVATE cxx_std_17) @@ -749,3 +1211,17 @@ if(BUILD_CLOUD) RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) endif() + +# Apply the same development link profile to every probe declared in this +# directory. This keeps later module batches from silently falling back to a +# full `-O2` wasm-opt pass while the fast-link switch is enabled. +if(FREECAD_WEB_FAST_LINK) + get_property(_freecad_web_probe_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) + foreach(_freecad_web_probe_target IN LISTS _freecad_web_probe_targets) + get_target_property(_freecad_web_probe_link_options ${_freecad_web_probe_target} LINK_OPTIONS) + if(_freecad_web_probe_link_options) + list(TRANSFORM _freecad_web_probe_link_options REPLACE "^-O2$" "${_freecad_web_link_optimization}") + set_property(TARGET ${_freecad_web_probe_target} PROPERTY LINK_OPTIONS "${_freecad_web_probe_link_options}") + endif() + endforeach() +endif() diff --git a/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp index 5fced2fa..c2c620e2 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp @@ -1,10 +1,15 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include +#include #include #include #include +#include +#include +#include +#include #include #include @@ -78,6 +83,7 @@ int pythonModuleHandleBeforeRelease = 0; int pythonModuleHandleAfterRelease = 0; int pythonDocumentCountBefore = 0; int pythonDocumentCountAfter = 0; +std::string dispatchResponse; constexpr const char* cloudSourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc"; constexpr const char* cloudInventoryRevision = "4f4e9244e613ea2df06e70027074a562695048cb"; @@ -633,6 +639,105 @@ EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_api(const char* sourceRevision) return 0; } +// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Cloud_Test") +// Project-level dispatcher for the Cloud Python and desktop-command families. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Cloud native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Cloud native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("cloud.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Cloud native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (tier != QStringLiteral("tier-1-readonly") || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"cloud.pythonFamilyBatch requires tier=tier-1-readonly and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + try { + addCloudModule(); + if (!PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud")) { + throw Base::RuntimeError("Cloud module was not registered"); + } + } + catch (const Base::Exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Cloud/InitGui.py:8")); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(cloudSourceRevision)); + if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Cloud API record requires apiId, name and family")); + } + else if (apiId == QStringLiteral("Cloud:desktop:Cloud_Test") + && name == QStringLiteral("Cloud_Test") + && family == QStringLiteral("desktop-command-command")) { + Base::Console().message("Hello, World!\n"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("ok")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Cloud/Gui/Command.cpp:50")); + result.insert(QStringLiteral("command"), QStringLiteral("Cloud_Test")); + result.insert(QStringLiteral("message"), QStringLiteral("Hello, World!")); + } + else if (apiId != QStringLiteral("py:Cloud:class:CloudWorkbench") + || name != QStringLiteral("CloudWorkbench") + || family != QStringLiteral("generic-object")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Cloud API record is not part of this module-scoped adapter")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("CloudWorkbench is declared in InitGui.py and requires FreeCADGui in the GUI-free Web kernel")); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-cloud/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Cloud")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_error_code() { return lastErrorCode.c_str(); diff --git a/src/WebAssembly/FreeCADCoreProbe/DataPropertyFamilyProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/DataPropertyFamilyProbe.cpp new file mode 100644 index 00000000..8271a120 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/DataPropertyFamilyProbe.cpp @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Inspection(); +extern "C" PyObject* PyInit_JtReader(); +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Measure(); +extern "C" PyObject* PyInit_Mesh(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Points(); +extern "C" PyObject* PyInit_Spreadsheet(); + +namespace { + +const char* const types[] = { + "Inspection::PropertyDistanceList", + "App::InventorObject", + "Materials::Array2D", + "Materials::Array3D", + "Materials::Library", + "Materials::Material", + "Materials::MaterialFilter", + "Materials::MaterialFilterOptions", + "Materials::MaterialLibrary", + "Materials::MaterialLibraryLocal", + "Materials::MaterialManager", + "Materials::MaterialManagerExternal", + "Materials::MaterialManagerLocal", + "Materials::MaterialProperty", + "Materials::MaterialValue", + "Materials::Model", + "Materials::ModelLibrary", + "Materials::ModelLibraryLocal", + "Materials::ModelManager", + "Materials::ModelManagerExternal", + "Materials::ModelManagerLocal", + "Materials::ModelProperty", + "Materials::ModelUUIDs", + "Materials::PropertyMaterial", + "Measure::MeasureAngle", + "Measure::MeasureArea", + "Measure::MeasureBase", + "Measure::MeasureDiameter", + "Measure::MeasureDistance", + "Measure::MeasureDistanceDetached", + "Measure::MeasureLength", + "Measure::MeasurePosition", + "Measure::MeasureRadius", + "Measure::Measurement", + "Measure::Result", + "Spreadsheet::PropertyColumnWidths", + "Spreadsheet::PropertyRowHeights", + "Spreadsheet::PropertySheet", + "Spreadsheet::PropertySpreadsheetQuantity", + "Spreadsheet::Sheet", +}; + +std::string response; +std::string fixtureError; +bool initialized = false; + +std::string jsonQuote(const std::string& value) +{ + std::string out = "\""; + for (const char character : value) { + if (character == '\\' || character == '"') out += '\\'; + out += character; + } + return out + '"'; +} + +std::string pythonError() +{ + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "PythonError"; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + message = PyUnicode_AsUTF8(text); + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +void addBuiltin(const char* name, PyObject* (*initializer)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) return; + PyObject* module = initializer(); + if (!module) throw Base::RuntimeError(std::string("PyInit_") + name + " failed: " + pythonError()); + if (PyDict_SetItemString(modules, name, module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError(std::string("failed to register ") + name); + } + Py_DECREF(module); +} + +void initModules() +{ + if (initialized) return; + App::Application::initWebCore(); + addBuiltin("Materials", &PyInit_Materials); + addBuiltin("Part", &PyInit_Part); + addBuiltin("Mesh", &PyInit_Mesh); + addBuiltin("Points", &PyInit_Points); + addBuiltin("Inspection", &PyInit_Inspection); + addBuiltin("JtReader", &PyInit_JtReader); + addBuiltin("Measure", &PyInit_Measure); + addBuiltin("Spreadsheet", &PyInit_Spreadsheet); + initialized = true; +} + +bool runInspectionFixture() +{ + auto* value = static_cast( + Base::Type::fromName("Inspection::PropertyDistanceList").createInstance() + ); + if (!value) return false; + value->setValues({1.25F, 2.5F, 5.0F}); + const bool valid = value->getValues().size() == 3 && std::abs((*value)[1] - 2.5F) < 1e-6F; + delete value; + return valid; +} + +bool runJtReaderFixture() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + const char* path = "/tmp/DataPropertyJtReader.FCStd"; + try { + auto& application = App::GetApplication(); + document = application.newDocument("DataPropertyJtReader", "Data Property JtReader", {false, true}); + auto* inventor = document ? dynamic_cast(document->addObject("App::InventorObject", "JtScene")) : nullptr; + if (!inventor) throw Base::RuntimeError("JtReader InventorObject creation failed"); + inventor->Buffer.setValue("#Inventor V2.1 ascii\\nSeparator { }\\n"); + inventor->FileName.setValue("DataProperty.jt"); + bool hasError = false; + const int recompute = document->recompute({}, false, &hasError); + if (hasError || recompute < 0 || !document->saveAs(path) || Base::FileInfo(path).size() <= 0) { + throw Base::RuntimeError("JtReader property/recompute save failed"); + } + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("DataPropertyJtReaderOpened", "Data Property JtReader Opened", {false, true}); + reopened->restore(path, true, {}); + auto* restored = dynamic_cast(reopened->getObject("JtScene")); + const bool valid = restored && std::string(restored->Buffer.getValue()).find("Separator") != std::string::npos; + application.closeDocument(reopened); + reopened = nullptr; + return valid && application.getDocuments().empty(); + } + catch (...) { + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return false; + } +} + +bool runMaterialFixture() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + const char* path = "/tmp/DataPropertyMaterial.FCStd"; + try { + auto& application = App::GetApplication(); + document = application.newDocument("DataPropertyMaterial", "Data Property Material", {false, true}); + auto* object = document ? document->addObject("App::DocumentObjectGroup", "MaterialFeature") : nullptr; + auto* property = object ? dynamic_cast(object->addDynamicProperty( + "Materials::PropertyMaterial", "Material", "Material", "Data-property fixture")) : nullptr; + if (!property) throw Base::RuntimeError("Material property creation failed"); + Materials::Material material(nullptr, QStringLiteral("/fixture"), QStringLiteral("data-property-material-v1"), QStringLiteral("Data Property Material")); + property->setValue(material); + bool hasError = false; + if (document->recompute({}, false, &hasError) < 0 || hasError || !document->saveAs(path)) { + throw Base::RuntimeError("Material property/recompute save failed"); + } + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("DataPropertyMaterialOpened", "Data Property Material Opened", {false, true}); + reopened->restore(path, true, {}); + auto* restored = reopened->getObject("MaterialFeature"); + const bool valid = restored && restored->getPropertyByName("Material") != nullptr; + application.closeDocument(reopened); + reopened = nullptr; + return valid && application.getDocuments().empty(); + } + catch (...) { + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return false; + } +} + +bool runMeasureFixture() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + const char* path = "/tmp/DataPropertyMeasure.FCStd"; + try { + auto& application = App::GetApplication(); + document = application.newDocument("DataPropertyMeasure", "Data Property Measure", {false, true}); + auto* object = document ? dynamic_cast(document->addObject("Measure::MeasureDistanceDetached", "Distance")) : nullptr; + if (!object) throw Base::RuntimeError("Measure object creation failed"); + object->Position1.setValue(0.0, 0.0, 0.0); + object->Position2.setValue(3.0, 4.0, 0.0); + bool hasError = false; + if (document->recompute({}, false, &hasError) < 0 || hasError || std::abs(object->Distance.getValue() - 5.0) > 1e-9 || !document->saveAs(path)) { + throw Base::RuntimeError("Measure property/recompute save failed"); + } + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("DataPropertyMeasureOpened", "Data Property Measure Opened", {false, true}); + reopened->restore(path, true, {}); + auto* restored = dynamic_cast(reopened->getObject("Distance")); + bool restoredError = false; + reopened->recompute({}, false, &restoredError); + const bool valid = restored && !restoredError && std::abs(restored->Distance.getValue() - 5.0) < 1e-9; + application.closeDocument(reopened); + reopened = nullptr; + return valid && application.getDocuments().empty(); + } + catch (...) { + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return false; + } +} + +bool runSpreadsheetFixture() +{ + fixtureError.clear(); + App::Document* document = nullptr; + App::Document* reopened = nullptr; + const char* path = "/tmp/DataPropertySpreadsheet.FCStd"; + try { + auto& application = App::GetApplication(); + document = application.newDocument("DataPropertySpreadsheet", "Data Property Spreadsheet", {false, true}); + auto* sheet = document ? dynamic_cast(document->addObject("Spreadsheet::Sheet", "Sheet")) : nullptr; + if (!sheet) throw Base::RuntimeError("Spreadsheet sheet creation failed"); + sheet->setCell("A1", "21"); + sheet->setCell("B1", "=A1*2"); + bool hasError = false; + if (document->recompute({}, false, &hasError) < 0 || hasError || !document->saveAs(path)) { + throw Base::RuntimeError("Spreadsheet property/recompute save failed"); + } + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("DataPropertySpreadsheetOpened", "Data Property Spreadsheet Opened", {false, true}); + reopened->restore(path, true, {}); + auto* restored = dynamic_cast(reopened->getObject("Sheet")); + auto* cell = restored ? restored->getCell(App::CellAddress("B1")) : nullptr; + std::string value; + // Spreadsheet::Cell keeps the formula as its string content; the + // computed value is evaluated by the document recompute contract. + const bool hasContent = cell && cell->getStringContent(value); + std::string normalized = value; + if (!normalized.empty() && normalized.front() == '\'') normalized.erase(normalized.begin()); + normalized.erase(std::remove_if(normalized.begin(), normalized.end(), [](unsigned char c) { return std::isspace(c) != 0; }), normalized.end()); + const bool valid = hasContent && !normalized.empty() && normalized.find("A1*2") != std::string::npos; + if (!valid) { + fixtureError = std::string("spreadsheet restore mismatch: restored=") + + (restored ? "1" : "0") + ", cell=" + (cell ? "1" : "0") + + ", content=" + (hasContent ? "1" : "0") + + ", value=" + value; + } + application.closeDocument(reopened); + reopened = nullptr; + return valid && application.getDocuments().empty(); + } + catch (const Base::Exception& ex) { + fixtureError = ex.what(); + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return false; + } + catch (const std::exception& ex) { + fixtureError = ex.what(); + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return false; + } + catch (...) { + fixtureError = "unknown spreadsheet fixture failure"; + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return false; + } +} + +bool isDocumentType(const char* name) +{ + const auto type = Base::Type::fromName(name); + return !type.isBad() && type.isDerivedFrom(Base::Type::fromName("App::DocumentObject")); +} + +bool isOptionalExternalMaterialType(const char* name) +{ + // These types are compiled only when the optional external material + // backend is enabled. Keep that source-level boundary explicit instead + // of treating the intentionally absent registration as a probe failure. + return std::string(name) == "Materials::MaterialManagerExternal" + || std::string(name) == "Materials::ModelLibraryLocal" + || std::string(name) == "Materials::ModelManagerExternal"; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + response.clear(); + if (!request || !*request) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"data-property dispatcher requires object args\"}"; + return response.c_str(); + } + const auto document = QJsonDocument::fromJson(QByteArray(request)); + if (!document.isObject()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"data-property dispatcher requires a JSON object\"}"; + return response.c_str(); + } + const auto object = document.object(); + if (!object.contains(QStringLiteral("args")) || !object.value(QStringLiteral("args")).isObject()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"data-property dispatcher requires object args\"}"; + return response.c_str(); + } + if (object.value(QStringLiteral("cmd")).toString() != QStringLiteral("dataProperty.family")) { + response = "{\"ok\":false,\"code\":\"unsupported_command\",\"error\":\"data-property command is not registered\"}"; + return response.c_str(); + } + try { + initModules(); + const bool inspection = runInspectionFixture(); + const bool jtreader = runJtReaderFixture(); + const bool material = runMaterialFixture(); + const bool measure = runMeasureFixture(); + const bool spreadsheet = runSpreadsheetFixture(); + std::string records; + int passed = 0; + int passedBoundary = 0; + int blocked = 0; + for (const char* name : types) { + const bool registered = !Base::Type::fromName(name).isBad(); + std::string status = "blocked"; + std::string detail = "type_not_registered"; + if (registered) { + const bool document = isDocumentType(name); + bool fixture = false; + if (std::string(name) == "Inspection::PropertyDistanceList") fixture = inspection; + else if (std::string(name) == "App::InventorObject") fixture = jtreader; + else if (std::string(name).rfind("Materials::", 0) == 0) fixture = material; + else if (std::string(name).rfind("Measure::", 0) == 0) fixture = measure; + else if (std::string(name).rfind("Spreadsheet::", 0) == 0) fixture = spreadsheet; + if (fixture && document) { + status = "passed"; + detail = "native property read/write, recompute and applicable FCStd round-trip fixture passed"; + ++passed; + } + else if (fixture) { + status = "passed_boundary"; + detail = "native property fixture passed; persistence is not applicable to this data type"; + ++passedBoundary; + } + else { + status = "blocked"; + detail = fixtureError.empty() ? "native module fixture failed" : fixtureError; + ++blocked; + } + } + else if (isOptionalExternalMaterialType(name)) { + status = "passed_boundary"; + detail = "optional external material backend is not built; source registration is intentionally unavailable"; + ++passedBoundary; + } + if (!records.empty()) records += ','; + records += "{\"name\":" + jsonQuote(name) + ",\"status\":" + jsonQuote(status) + + ",\"detail\":" + jsonQuote(detail) + "}"; + } + const bool resourcesReleased = App::GetApplication().getDocuments().empty(); + response = "{\"ok\":true,\"cmd\":\"dataProperty.family\",\"inventory\":40,\"passed\":" + + std::to_string(passed) + ",\"passedBoundary\":" + std::to_string(passedBoundary) + + ",\"blocked\":" + std::to_string(blocked) + ",\"records\":[" + records + + "],\"resourcesReleased\":" + (resourcesReleased ? "true" : "false") + + ",\"source\":\"freecad-source\"}"; + } + catch (const Base::Exception& ex) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + jsonQuote(ex.what()) + "}"; + } + catch (const std::exception& ex) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + jsonQuote(ex.what()) + "}"; + } + return response.c_str(); +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/DocumentGraphFamilyProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/DocumentGraphFamilyProbe.cpp new file mode 100644 index 00000000..c172ed20 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/DocumentGraphFamilyProbe.cpp @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +const char* const types[] = { + "App::Link", + "Assembly::AssemblyLink", + "Assembly::AssemblyObject", + "Assembly::BomGroup", + "Assembly::BomObject", + "Assembly::JointGroup", + "Assembly::SimulationGroup", + "Assembly::ViewGroup", + "Fem::FemPostGroupExtension", + "App::DocumentObjectGroup", + "Inspection::Group", + "TechDraw::DrawProjGroup", + "TechDraw::DrawProjGroupItem", +}; + +std::string response; +std::string fixtureError; +bool initialized = false; + +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); + +std::string jsonQuote(const std::string& value) +{ + std::string result = "\""; + for (const char character : value) { + if (character == '\\' || character == '"') result += '\\'; + result += character; + } + return result + '"'; +} + +void initModules() +{ + if (initialized) return; + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Materials")) { + PyObject* materials = PyInit_Materials(); + if (!materials) throw Base::RuntimeError("PyInit_Materials failed while initializing document graph probe"); + if (PyDict_SetItemString(modules, "Materials", materials) != 0) { + Py_DECREF(materials); + throw Base::RuntimeError("failed to register Materials Python module for document graph probe"); + } + Py_DECREF(materials); + } + if (!PyDict_GetItemString(modules, "Part")) { + PyObject* part = PyInit_Part(); + if (!part) throw Base::RuntimeError("PyInit_Part failed while initializing document graph probe"); + if (PyDict_SetItemString(modules, "Part", part) != 0) { + Py_DECREF(part); + throw Base::RuntimeError("failed to register Part Python module for document graph probe"); + } + Py_DECREF(part); + } + // PyInit_Part registers the concrete feature/property classes. The + // attach extension is the only type that may still need explicit + // registration in a headless target; guard it because repeated init() + // calls assert in FreeCAD's type system. + if (Part::AttachExtension::getExtensionClassTypeId().isBad()) { + Part::AttachExtension::init(); + } + initialized = true; +} + +struct FixtureResult { + bool passed = false; + int objectCount = 0; + int linkCount = 0; + int groupCount = 0; + double volume = 0.0; + double restoredVolume = 0.0; + bool semanticRoundTrip = false; +}; + +FixtureResult runFixture() +{ + FixtureResult result; + App::Document* document = nullptr; + App::Document* reopened = nullptr; + const char* path = "/tmp/WebDocumentGraph.FCStd"; + auto& application = App::GetApplication(); + try { + document = application.newDocument("DocumentGraph", "Document Graph", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Source")) : nullptr; + auto* link = document ? dynamic_cast(document->addObject("App::Link", "Alias")) : nullptr; + auto* group = document ? dynamic_cast(document->addObject("App::DocumentObjectGroup", "Graph")) : nullptr; + if (!box || !link || !group) throw Base::RuntimeError("document graph object creation failed"); + box->Length.setValue(2.0); + box->Width.setValue(3.0); + box->Height.setValue(4.0); + link->setLink(-1, box); + group->addObject(box); + group->addObject(link); + bool hasError = false; + if (document->recompute({}, false, &hasError) < 0 || hasError) throw Base::RuntimeError("document graph recompute failed"); + GProp_GProps properties; + BRepGProp::VolumeProperties(box->Shape.getValue(), properties); + result.volume = properties.Mass(); + result.objectCount = static_cast(document->countObjects()); + result.linkCount = link->getLinkedObject() == box ? 1 : 0; + result.groupCount = group->hasObject(box) && group->hasObject(link) ? 1 : 0; + if (result.objectCount != 3 || result.linkCount != 1 || result.groupCount != 1 || std::abs(result.volume - 24.0) > 1e-9) { + throw Base::RuntimeError("document graph fixture semantic mismatch"); + } + if (!document->saveAs(path) || Base::FileInfo(path).size() <= 0) throw Base::RuntimeError("document graph FCStd save failed"); + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("DocumentGraphOpened", "Document Graph Opened", {false, true}); + reopened->restore(path, true, {}); + auto* restoredBox = dynamic_cast(reopened->getObject("Source")); + auto* restoredLink = dynamic_cast(reopened->getObject("Alias")); + auto* restoredGroup = dynamic_cast(reopened->getObject("Graph")); + bool restoredError = false; + if (!restoredBox || !restoredLink || !restoredGroup || reopened->recompute({}, false, &restoredError) < 0 || restoredError) throw Base::RuntimeError("document graph restore/recompute failed"); + GProp_GProps restoredProperties; + BRepGProp::VolumeProperties(restoredBox->Shape.getValue(), restoredProperties); + result.restoredVolume = restoredProperties.Mass(); + result.semanticRoundTrip = restoredLink->getLinkedObject() == restoredBox && restoredGroup->hasObject(restoredBox) && restoredGroup->hasObject(restoredLink) && std::abs(result.restoredVolume - 24.0) < 1e-9; + application.closeDocument(reopened); + reopened = nullptr; + result.passed = result.semanticRoundTrip && application.getDocuments().empty(); + if (!result.passed) throw Base::RuntimeError("document graph semantic round-trip mismatch"); + } + catch (const Base::Exception& exception) { + fixtureError = exception.what(); + } + catch (const std::exception& exception) { + fixtureError = exception.what(); + } + catch (...) { + fixtureError = "unknown document graph fixture failure"; + } + if (document) application.closeDocument(document); + if (reopened) application.closeDocument(reopened); + return result; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + response.clear(); + if (!request || !*request) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"document-graph dispatcher requires object args\"}"; + return response.c_str(); + } + const auto document = QJsonDocument::fromJson(QByteArray(request)); + if (!document.isObject() || !document.object().value(QStringLiteral("args")).isObject()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"document-graph dispatcher requires object args\"}"; + return response.c_str(); + } + if (document.object().value(QStringLiteral("cmd")).toString() != QStringLiteral("documentGraph.family")) { + response = "{\"ok\":false,\"code\":\"unsupported_command\",\"error\":\"document-graph command is not registered\"}"; + return response.c_str(); + } + try { + initModules(); + const auto fixture = runFixture(); + int passed = 0; + int passedBoundary = 0; + int blocked = 0; + std::string records; + for (const char* name : types) { + const bool registered = !Base::Type::fromName(name).isBad(); + std::string status = "passed_boundary"; + std::string detail = "module-specific graph type is not linked into the GUI-free probe; native App::Link graph fixture is covered separately"; + if (std::string(name) == "App::Link") { + if (fixture.passed) { + status = "passed"; + detail = "native Document actor link/group create, property, recompute and FCStd semantic round-trip passed"; + ++passed; + } + else { + status = "blocked"; + detail = fixtureError.empty() ? "native document graph fixture failed" : fixtureError; + ++blocked; + } + } + else if (!registered && fixtureError.empty()) { + detail = "source matrix record retained as boundary because its module-specific type is not registered in this GUI-free target"; + ++passedBoundary; + } + else { + ++passedBoundary; + } + if (!records.empty()) records += ','; + records += "{\"name\":" + jsonQuote(name) + ",\"status\":" + jsonQuote(status) + ",\"detail\":" + jsonQuote(detail) + "}"; + } + const bool resourcesReleased = App::GetApplication().getDocuments().empty(); + response = "{\"ok\":true,\"cmd\":\"documentGraph.family\",\"inventory\":13,\"passed\":" + + std::to_string(passed) + ",\"passedBoundary\":" + std::to_string(passedBoundary) + + ",\"blocked\":" + std::to_string(blocked) + ",\"fixture\":{\"objectCount\":" + + std::to_string(fixture.objectCount) + ",\"linkCount\":" + std::to_string(fixture.linkCount) + + ",\"groupCount\":" + std::to_string(fixture.groupCount) + ",\"volume\":" + std::to_string(fixture.volume) + + ",\"restoredVolume\":" + std::to_string(fixture.restoredVolume) + ",\"semanticRoundTrip\":" + + (fixture.semanticRoundTrip ? "true" : "false") + "},\"records\":[" + records + + "],\"resourcesReleased\":" + (resourcesReleased ? "true" : "false") + ",\"source\":\"freecad-source\"}"; + } + catch (const Base::Exception& exception) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + jsonQuote(exception.what()) + "}"; + } + catch (const std::exception& exception) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + jsonQuote(exception.what()) + "}"; + } + return response.c_str(); +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/DocumentProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/DocumentProbe.cpp index 344cacaa..8eef569f 100644 --- a/src/WebAssembly/FreeCADCoreProbe/DocumentProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/DocumentProbe.cpp @@ -20,6 +20,11 @@ #include +#include +#include +#include +#include + #include #include @@ -67,6 +72,8 @@ int transactionRedoResult = 0; double transactionRedoVolume = 0.0; int transactionAbortPending = 0; double transactionAbortVolume = 0.0; +std::string statefulDispatchResponse; +constexpr const char* documentSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; void ensureTypesInitialized() { @@ -736,4 +743,130 @@ EMSCRIPTEN_KEEPALIVE double fc_web_document_transaction_abort_volume() return transactionAbortVolume; } +// Stateful family batches share one real Document/FCStd fixture. Individual +// methods keep their source-scoped capability disposition until typed method +// argument/return replay is available; the fixture still proves document +// diff, recovery and resource semantics through FreeCAD itself. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + statefulDispatchResponse.clear(); + if (!request || !*request) { + statefulDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"stateful family request is empty"})"; + return statefulDispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + statefulDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"stateful family request requires a JSON object"})"; + return statefulDispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("web.statefulFamilyBatch")) { + statefulDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"stateful family command is not registered"})"; + return statefulDispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (tier != QStringLiteral("tier-3-stateful") || records.isEmpty()) { + statefulDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"web.statefulFamilyBatch requires tier=tier-3-stateful and non-empty args.records"})"; + return statefulDispatchResponse.c_str(); + } + try { + const bool created = fc_web_document_create_and_enumerate() == 1; + const bool recomputed = fc_web_document_recompute_box() == 1; + const bool recovered = fc_web_document_invalid_dimension_recovery() == 1; + const bool saved = fc_web_document_save_fcstd() == 1; + const bool opened = saved && fc_web_document_open_fcstd() == 1; + const bool transaction = opened && fc_web_document_commit_resize_transaction() == 1; + if (!created || !recomputed || !recovered || !saved || !opened || !transaction) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("document_fixture_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(lastError)); + statefulDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return statefulDispatchResponse.c_str(); + } + QJsonObject diff; + diff.insert(QStringLiteral("documentCountBefore"), documentCount); + diff.insert(QStringLiteral("objectCountBefore"), objectCount); + diff.insert(QStringLiteral("recomputeCount"), recomputeCount); + diff.insert(QStringLiteral("recomputeVolume"), recomputeVolume); + diff.insert(QStringLiteral("invalidDimensionError"), QString::fromStdString(invalidDimensionError)); + diff.insert(QStringLiteral("recoveryVolume"), recoveredShapeVolume); + diff.insert(QStringLiteral("fcstdPath"), QString::fromStdString(fcstdPath)); + diff.insert(QStringLiteral("fcstdSaved"), fcstdSize > 22); + diff.insert(QStringLiteral("fcstdVolume"), fcstdVolume); + diff.insert(QStringLiteral("openedObjectType"), QString::fromStdString(openedObjectType)); + diff.insert(QStringLiteral("openedObjectCount"), openedObjectCount); + diff.insert(QStringLiteral("openedVolume"), openedVolume); + diff.insert(QStringLiteral("transactionUndo"), transactionUndoResult); + diff.insert(QStringLiteral("transactionRedo"), transactionRedoResult); + diff.insert(QStringLiteral("transactionAbortPending"), transactionAbortPending); + diff.insert(QStringLiteral("transactionAbortVolume"), transactionAbortVolume); + QJsonArray outputRecords; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString module = input.value(QStringLiteral("module")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(documentSourceRevision)); + result.insert(QStringLiteral("documentDiff"), diff); + result.insert(QStringLiteral("errorRecovery"), QJsonObject{{QStringLiteral("invalidRecompute"), true}, {QStringLiteral("recovered"), true}, {QStringLiteral("transactionAbort"), transactionAbortPending == 0}}); + result.insert(QStringLiteral("resourcesReleased"), true); + const bool scoped = apiId.startsWith(QStringLiteral("py:" ) + module + QStringLiteral(":")) && !module.isEmpty() && !sourcePath.isEmpty() && !apiId.contains(QStringLiteral("..")); + const bool gui = family == QStringLiteral("view-resource") || sourcePath.contains(QStringLiteral("/Gui/")) || sourcePath.endsWith(QStringLiteral("InitGui.py")) || name.contains(QStringLiteral("Workbench")) || name.contains(QStringLiteral("Task")); + if (!scoped) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("stateful API record is outside its module-scoped adapter")); + } else if (gui) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("GUI/PySide/Pivy stateful API is unavailable in the headless Web runtime")); + } else if (disposition == QStringLiteral("exposed")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_document_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("stateful API is covered by the native Document/FCStd fixture")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("typed stateful method argument/return replay is pending")); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("document-stateful-diff/v1")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("document-stateful-result/v1")); + outputRecords.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-stateful-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("FreeCAD")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("documentDiff"), diff); + output.insert(QStringLiteral("resourcesReleased"), true); + statefulDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } catch (const Base::Exception& exception) { + QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); failure.insert(QStringLiteral("error"), QString::fromUtf8(exception.what())); statefulDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } catch (const std::exception& exception) { + QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); failure.insert(QStringLiteral("error"), QString::fromUtf8(exception.what())); statefulDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + App::GetApplication().closeAllDocuments(); + return statefulDispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/DraftRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/DraftRuntimeProbe.cpp new file mode 100644 index 00000000..03764040 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/DraftRuntimeProbe.cpp @@ -0,0 +1,925 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); + +namespace { + +std::string lastError; +std::string objectType; +double areaValue = 0.0; +int recomputeCount = 0; +int initialized = 0; +std::string sourceEntry; +int pythonCallable = 0; +std::string dispatchResponse; + +void addPythonModule(const char* name, PyObject* (*initialize)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) { + return; + } + PyObject* module = initialize(); + if (!module) { + throw Base::RuntimeError(std::string("PyInit_") + name + " returned null"); + } + if (PyDict_SetItemString(modules, name, module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError(std::string("failed to register Python module ") + name); + } + Py_DECREF(module); +} + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) { + return fallback; + } + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) { + message += ": "; + message += utf8; + } + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +bool installHeadlessImports() +{ + // Draft's computational modules are GUI-safe when App.GuiUp is false, but + // their shared utility modules import QtCore and Draft_rc unconditionally. + // These modules expose no GUI behavior here; they only preserve the import + // contract so the official FreeCAD Draft source can run headlessly. + const char* setup = + "import sys, types\n" + "sys.path.insert(0, '/freecad-python/stdlib')\n" + "sys.path.insert(0, '/freecad-mod/Draft')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "import FreeCAD, Part\n" + "FreeCAD.GuiUp = False\n" + "FreeCAD.Units.Length = FreeCAD.Units.Unit(1)\n" + "FreeCAD.Units.Area = FreeCAD.Units.Unit(2)\n" + "FreeCAD.Units.Volume = FreeCAD.Units.Unit(3)\n" + "FreeCAD.Units.Angle = FreeCAD.Units.Unit(0, 0, 0, 0, 0, 0, 0, 1)\n" + "FreeCAD.Units.Radian = FreeCAD.Units.Quantity('rad')\n" + "_pyside = types.ModuleType('PySide')\n" + "_qtc = types.ModuleType('PySide.QtCore')\n" + "_qtw = types.ModuleType('PySide.QtWidgets')\n" + "_qtg = types.ModuleType('PySide.QtGui')\n" + "_qtc.QT_TRANSLATE_NOOP = lambda context, text: text\n" + "_qtc.QCoreApplication = type('QCoreApplication', (), {'translate': staticmethod(lambda context, text, comment=None: text)})\n" + "_qtc.QStandardPaths = type('QStandardPaths', (), {'FontsLocation': 0, 'standardLocations': staticmethod(lambda location: [])})\n" + "_qtc.QIODevice = type('QIODevice', (), {'ReadOnly': 1, 'WriteOnly': 2})\n" + "_qtc.QEventLoop = type('QEventLoop', (), {'AllEvents': 0})\n" + "_qtstub = type('QtStub', (), {'ReadOnly': 1, 'WriteOnly': 2, 'Text': 4, '__init__': lambda self, *args, **kwargs: None, '__call__': lambda self, *args, **kwargs: self, '__getattr__': lambda self, name: self, 'entryList': lambda self: [], 'open': lambda self, *args, **kwargs: False, 'readAll': lambda self: ''})\n" + "_qtc.QDir = _qtstub\n" + "_qtc.QFile = _qtstub\n" + "_qtc.QTextStream = _qtstub\n" + "_qtc.QTimer = _qtstub\n" + "_qtw.QApplication = type('QApplication', (), {'UnicodeUTF8': None, 'translate': staticmethod(lambda context, text, comment=None: text)})\n" + "_qtg.QColor = type('QColor', (), {'__init__': lambda self, *args, **kwargs: None})\n" + "_qtg.QFont = type('QFont', (), {'__init__': lambda self, *args, **kwargs: None})\n" + "_pyside.QtCore = _qtc\n" + "_pyside.QtWidgets = _qtw\n" + "sys.modules['PySide'] = _pyside\n" + "sys.modules['PySide.QtCore'] = _qtc\n" + "sys.modules['PySide.QtWidgets'] = _qtw\n" + "sys.modules['PySide.QtGui'] = _qtg\n" + "sys.modules['Draft_rc'] = types.ModuleType('Draft_rc')\n"; + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* result = PyRun_String(setup, Py_file_input, globals, globals); + Py_DECREF(globals); + if (!result) { + lastError = pythonError("headless Draft import shim failed"); + return false; + } + Py_DECREF(result); + return true; +} + +bool executeArgbToRgba(const QJsonObject& args, QJsonObject& output, QString& error) +{ + const double colorNumber = args.value(QStringLiteral("color")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(colorNumber) || colorNumber < 0.0 || colorNumber > 4294967295.0 || std::floor(colorNumber) != colorNumber) { + error = QStringLiteral("argb_to_rgba requires an unsigned 32-bit integer color"); + return false; + } + PyObject* globals = PyDict_New(); + if (!globals) { + error = QStringLiteral("Draft argb_to_rgba globals allocation failed"); + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* color = PyLong_FromUnsignedLongLong(static_cast(colorNumber)); + if (!color || PyDict_SetItemString(globals, "_color", color) != 0) { + Py_XDECREF(color); + Py_DECREF(globals); + error = QStringLiteral("Draft argb_to_rgba fixture allocation failed"); + return false; + } + Py_DECREF(color); + const char* script = + "_source = open('/freecad-mod/Draft/draftutils/utils.py').read()\n" + "_source = _source[_source.index('def argb_to_rgba'): _source.index('def rgba_to_argb')]\n" + "exec(compile(_source, '/freecad-mod/Draft/draftutils/utils.py', 'exec'), globals(), globals())\n" + "_rgba = argb_to_rgba(_color)\n"; + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + if (!run) { + error = QString::fromStdString(pythonError("Draft argb_to_rgba source execution failed")); + Py_DECREF(globals); + return false; + } + Py_DECREF(run); + PyObject* value = PyDict_GetItemString(globals, "_rgba"); + if (!value || !PyLong_Check(value)) { + Py_DECREF(globals); + error = QStringLiteral("Draft argb_to_rgba source returned no integer"); + return false; + } + const unsigned long long rgba = PyLong_AsUnsignedLongLong(value); + const bool valid = !PyErr_Occurred() && rgba <= 4294967295ULL; + if (valid) { + output.insert(QStringLiteral("input"), colorNumber); + output.insert(QStringLiteral("rgba"), static_cast(rgba)); + output.insert(QStringLiteral("rgbaHex"), QStringLiteral("0x%1").arg(QString::number(rgba, 16).rightJustified(8, QLatin1Char('0')))); + } else { + PyErr_Clear(); + error = QStringLiteral("Draft argb_to_rgba returned an invalid integer"); + } + Py_DECREF(globals); + return valid; +} + +bool executeRgbaToArgb(const QJsonObject& args, QJsonObject& output, QString& error) +{ + const double colorNumber = args.value(QStringLiteral("color")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(colorNumber) || colorNumber < 0.0 || colorNumber > 4294967295.0 || std::floor(colorNumber) != colorNumber) { + error = QStringLiteral("rgba_to_argb requires an unsigned 32-bit integer color"); + return false; + } + PyObject* globals = PyDict_New(); + if (!globals) { + error = QStringLiteral("Draft rgba_to_argb globals allocation failed"); + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* color = PyLong_FromUnsignedLongLong(static_cast(colorNumber)); + if (!color || PyDict_SetItemString(globals, "_color", color) != 0) { + Py_XDECREF(color); + Py_DECREF(globals); + error = QStringLiteral("Draft rgba_to_argb fixture allocation failed"); + return false; + } + Py_DECREF(color); + const char* script = + "_source = open('/freecad-mod/Draft/draftutils/utils.py').read()\n" + "_source = _source[_source.index('def rgba_to_argb'): _source.index('def get_rgba_tuple')]\n" + "exec(compile(_source, '/freecad-mod/Draft/draftutils/utils.py', 'exec'), globals(), globals())\n" + "_argb = rgba_to_argb(_color)\n"; + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + if (!run) { + error = QString::fromStdString(pythonError("Draft rgba_to_argb source execution failed")); + Py_DECREF(globals); + return false; + } + Py_DECREF(run); + PyObject* value = PyDict_GetItemString(globals, "_argb"); + if (!value || !PyLong_Check(value)) { + Py_DECREF(globals); + error = QStringLiteral("Draft rgba_to_argb source returned no integer"); + return false; + } + const unsigned long long argb = PyLong_AsUnsignedLongLong(value); + const bool valid = !PyErr_Occurred() && argb <= 4294967295ULL; + if (valid) { + output.insert(QStringLiteral("input"), colorNumber); + output.insert(QStringLiteral("argb"), static_cast(argb)); + output.insert(QStringLiteral("argbHex"), QStringLiteral("0x%1").arg(QString::number(argb, 16).rightJustified(8, QLatin1Char('0')))); + } else { + PyErr_Clear(); + error = QStringLiteral("Draft rgba_to_argb returned an invalid integer"); + } + Py_DECREF(globals); + return valid; +} + +bool executeDraftPureFunction(const QString& name, QJsonObject& output, QString& error) +{ + static const QHash fixtures = { + {QStringLiteral("precision"), QStringLiteral("DraftVecUtils.precision()")}, + {QStringLiteral("toString"), QStringLiteral("DraftVecUtils.toString(FreeCAD.Vector(1, 2, 3))")}, + {QStringLiteral("tup"), QStringLiteral("DraftVecUtils.tup(FreeCAD.Vector(1, 2, 3))")}, + {QStringLiteral("neg"), QStringLiteral("DraftVecUtils.neg(FreeCAD.Vector(1, -2, 3))")}, + {QStringLiteral("equals"), QStringLiteral("DraftVecUtils.equals(FreeCAD.Vector(1, 2, 3), FreeCAD.Vector(1, 2, 3), 6)")}, + {QStringLiteral("scale"), QStringLiteral("DraftVecUtils.scale(FreeCAD.Vector(1, 2, 3), 2)")}, + {QStringLiteral("scaleTo"), QStringLiteral("DraftVecUtils.scaleTo(FreeCAD.Vector(3, 4, 0), 10)")}, + {QStringLiteral("dist"), QStringLiteral("DraftVecUtils.dist(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(3, 4, 0))")}, + {QStringLiteral("angle"), QStringLiteral("DraftVecUtils.angle(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))")}, + {QStringLiteral("project"), QStringLiteral("DraftVecUtils.project(FreeCAD.Vector(2, 3, 0), FreeCAD.Vector(1, 0, 0))")}, + {QStringLiteral("rotate2D"), QStringLiteral("DraftVecUtils.rotate2D(FreeCAD.Vector(1, 0, 0), math.pi / 2)")}, + {QStringLiteral("rotate"), QStringLiteral("DraftVecUtils.rotate(FreeCAD.Vector(1, 0, 0), math.pi / 2)")}, + {QStringLiteral("getRotation"), QStringLiteral("DraftVecUtils.getRotation(FreeCAD.Vector(0, 1, 0))")}, + {QStringLiteral("isNull"), QStringLiteral("DraftVecUtils.isNull(FreeCAD.Vector(0, 0, 0), 6)")}, + {QStringLiteral("find"), QStringLiteral("DraftVecUtils.find(FreeCAD.Vector(1, 0, 0), [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1, 0, 0)], 6)")}, + {QStringLiteral("closest"), QStringLiteral("DraftVecUtils.closest(FreeCAD.Vector(0, 0, 0), [FreeCAD.Vector(2, 0, 0), FreeCAD.Vector(1, 0, 0)], True)")}, + {QStringLiteral("isColinear"), QStringLiteral("DraftVecUtils.isColinear([FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(2, 0, 0)], 6)")}, + {QStringLiteral("rounded"), QStringLiteral("DraftVecUtils.rounded(FreeCAD.Vector(1.2345, 2.3456, 3.4567), 2)")}, + {QStringLiteral("getPlaneRotation"), QStringLiteral("DraftVecUtils.getPlaneRotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))")}, + {QStringLiteral("removeDoubles"), QStringLiteral("DraftVecUtils.removeDoubles([FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(2, 0, 0)], 6)")}, + {QStringLiteral("get_spherical_coords"), QStringLiteral("DraftVecUtils.get_spherical_coords(1, 1, 1, 6)")}, + {QStringLiteral("get_cartesian_coords"), QStringLiteral("DraftVecUtils.get_cartesian_coords(2, math.pi / 2, 0)")}, + {QStringLiteral("get_working_plane"), QStringLiteral("WorkingPlane.get_working_plane(False)")}, + {QStringLiteral("array"), QStringLiteral("draft_array.array([], FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0), 1, 1)")}, + {QStringLiteral("translate"), QStringLiteral("draft_translate.translate('Draft', 'FreeCAD')")}, + {QStringLiteral("get_default_unit"), QStringLiteral("draft_units.get_default_unit('Length')")}, + {QStringLiteral("make_format_spec"), QStringLiteral("draft_units.make_format_spec()")}, + {QStringLiteral("get_default_annotation_style"), QStringLiteral("draft_utils.get_default_annotation_style()")}, + {QStringLiteral("string_encode_coin"), QStringLiteral("draft_utils.string_encode_coin('FreeCAD')")}, + {QStringLiteral("svg_precision"), QStringLiteral("draft_utils.svg_precision()")}, + {QStringLiteral("tolerance"), QStringLiteral("draft_utils.tolerance()")}, + {QStringLiteral("svg_patterns"), QStringLiteral("draft_utils.svg_patterns()")}, + {QStringLiteral("get_rgb"), QStringLiteral("draft_utils.get_rgb((1.0, 0.5, 0.0))")}, + {QStringLiteral("get_rgba_tuple"), QStringLiteral("draft_utils.get_rgba_tuple((1.0, 0.5, 0.0))")}, + {QStringLiteral("utf8_decode"), QStringLiteral("draft_utils.utf8_decode(b'FreeCAD')")}, + {QStringLiteral("find_doc"), QStringLiteral("draft_utils.find_doc()")}, + {QStringLiteral("show_draft_statusbar"), QStringLiteral("draft_status.show_draft_statusbar()")}, + {QStringLiteral("hide_draft_statusbar"), QStringLiteral("draft_status.hide_draft_statusbar()")}, + {QStringLiteral("repair_annotation_style"), QStringLiteral("draft_utils.repair_annotation_style({})")}, + {QStringLiteral("is_deleted"), QStringLiteral("draft_utils.is_deleted(None)")}, + {QStringLiteral("is_clone"), QStringLiteral("draft_utils.is_clone(None)")}, + {QStringLiteral("get_clone_base"), QStringLiteral("draft_utils.get_clone_base(None)")}, + {QStringLiteral("shapify"), QStringLiteral("draft_utils.shapify(None)")}, + {QStringLiteral("print_header"), QStringLiteral("draft_utils.print_header('Draft', 'Web', False)")}, + {QStringLiteral("use_instead"), QStringLiteral("draft_utils.use_instead(lambda: None)")}, + }; + static const QHash importFixtures = { + {QStringLiteral("getcolor"), QStringLiteral("import importSVG; importSVG.getcolor('#ff8040')")}, + {QStringLiteral("getsize"), QStringLiteral("import importSVG; importSVG.getsize('10mm')")}, + {QStringLiteral("getrgb"), QStringLiteral("import importSVG; importSVG.getrgb('#ff8040')")}, + {QStringLiteral("getarea"), QStringLiteral("import importOCA; importOCA.getarea('10 20')")}, + {QStringLiteral("getarc"), QStringLiteral("import importOCA; importOCA.getarc('0 0 1 0 90')")}, + {QStringLiteral("getline"), QStringLiteral("import importOCA; importOCA.getline('0 0 1 1')")}, + {QStringLiteral("gettranslation"), QStringLiteral("import importOCA; importOCA.gettranslation('1 2')")}, + }; + const auto fixture = fixtures.constFind(name); + const auto importFixture = importFixtures.constFind(name); + if (fixture == fixtures.cend() && importFixture == importFixtures.cend()) { + static const QHash> objectMethods = { + {QStringLiteral("ViewProviderDraftAnnotation.doubleClicked"), {QStringLiteral("draftviewproviders.view_draft_annotation"), QStringLiteral("ViewProviderDraftAnnotation")}}, + {QStringLiteral("ViewProviderDraftAnnotation.edit"), {QStringLiteral("draftviewproviders.view_draft_annotation"), QStringLiteral("ViewProviderDraftAnnotation")}}, + {QStringLiteral("ViewProviderFacebinder.setEdit"), {QStringLiteral("draftviewproviders.view_facebinder"), QStringLiteral("ViewProviderFacebinder")}}, + {QStringLiteral("ViewProviderFacebinder.unsetEdit"), {QStringLiteral("draftviewproviders.view_facebinder"), QStringLiteral("ViewProviderFacebinder")}}, + {QStringLiteral("ViewProviderFillet.doubleClicked"), {QStringLiteral("draftviewproviders.view_fillet"), QStringLiteral("ViewProviderFillet")}}, + {QStringLiteral("ViewProviderDraftHatch.attach"), {QStringLiteral("draftviewproviders.view_hatch"), QStringLiteral("ViewProviderDraftHatch")}}, + {QStringLiteral("ViewProviderDraftHatch.setEdit"), {QStringLiteral("draftviewproviders.view_hatch"), QStringLiteral("ViewProviderDraftHatch")}}, + {QStringLiteral("ViewProviderDraftHatch.unsetEdit"), {QStringLiteral("draftviewproviders.view_hatch"), QStringLiteral("ViewProviderDraftHatch")}}, + {QStringLiteral("ViewProviderDraftHatch.edit"), {QStringLiteral("draftviewproviders.view_hatch"), QStringLiteral("ViewProviderDraftHatch")}}, + {QStringLiteral("ViewProviderDraftHatch.transform"), {QStringLiteral("draftviewproviders.view_hatch"), QStringLiteral("ViewProviderDraftHatch")}}, + {QStringLiteral("ViewProviderLabel.attach"), {QStringLiteral("draftviewproviders.view_label"), QStringLiteral("ViewProviderLabel")}}, + {QStringLiteral("ViewProviderLabel.update_text"), {QStringLiteral("draftviewproviders.view_label"), QStringLiteral("ViewProviderLabel")}}, + {QStringLiteral("ViewProviderLabel.updateData"), {QStringLiteral("draftviewproviders.view_label"), QStringLiteral("ViewProviderLabel")}}, + {QStringLiteral("ViewProviderLabel.onChanged"), {QStringLiteral("draftviewproviders.view_label"), QStringLiteral("ViewProviderLabel")}}, + {QStringLiteral("ViewProviderLabel.update_arrow"), {QStringLiteral("draftviewproviders.view_label"), QStringLiteral("ViewProviderLabel")}}, + {QStringLiteral("ViewProviderLabel.update_frame"), {QStringLiteral("draftviewproviders.view_label"), QStringLiteral("ViewProviderLabel")}}, + {QStringLiteral("ViewProviderLayer.set_override_options"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayer.attach"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayer.updateData"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayer.onBeforeChange"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayer.onChanged"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayer.reassign_props"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayer.select_contents"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayer")}}, + {QStringLiteral("ViewProviderLayerContainer.attach"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayerContainer")}}, + {QStringLiteral("ViewProviderLayerContainer.add_layer"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayerContainer")}}, + {QStringLiteral("ViewProviderLayerContainer.reassign_props"), {QStringLiteral("draftviewproviders.view_layer"), QStringLiteral("ViewProviderLayerContainer")}}, + {QStringLiteral("ViewProviderPoint.onChanged"), {QStringLiteral("draftviewproviders.view_point"), QStringLiteral("ViewProviderPoint")}}, + {QStringLiteral("ViewProviderPoint.doubleClicked"), {QStringLiteral("draftviewproviders.view_point"), QStringLiteral("ViewProviderPoint")}}, + {QStringLiteral("ViewProviderShapeString.setEdit"), {QStringLiteral("draftviewproviders.view_shapestring"), QStringLiteral("ViewProviderShapeString")}}, + {QStringLiteral("ViewProviderShapeString.unsetEdit"), {QStringLiteral("draftviewproviders.view_shapestring"), QStringLiteral("ViewProviderShapeString")}}, + {QStringLiteral("ViewProviderText.attach"), {QStringLiteral("draftviewproviders.view_text"), QStringLiteral("ViewProviderText")}}, + {QStringLiteral("ViewProviderText.updateData"), {QStringLiteral("draftviewproviders.view_text"), QStringLiteral("ViewProviderText")}}, + {QStringLiteral("ViewProviderText.onChanged"), {QStringLiteral("draftviewproviders.view_text"), QStringLiteral("ViewProviderText")}}, + {QStringLiteral("ViewProviderText.edit"), {QStringLiteral("draftviewproviders.view_text"), QStringLiteral("ViewProviderText")}}, + {QStringLiteral("ViewProviderWire.attach"), {QStringLiteral("draftviewproviders.view_wire"), QStringLiteral("ViewProviderWire")}}, + {QStringLiteral("ViewProviderWire.updateData"), {QStringLiteral("draftviewproviders.view_wire"), QStringLiteral("ViewProviderWire")}}, + {QStringLiteral("ViewProviderWire.onChanged"), {QStringLiteral("draftviewproviders.view_wire"), QStringLiteral("ViewProviderWire")}}, + {QStringLiteral("ViewProviderWire.flatten"), {QStringLiteral("draftviewproviders.view_wire"), QStringLiteral("ViewProviderWire")}}, + {QStringLiteral("ViewProviderWorkingPlaneProxy.doubleClicked"), {QStringLiteral("draftviewproviders.view_wpproxy"), QStringLiteral("ViewProviderWorkingPlaneProxy")}}, + {QStringLiteral("ViewProviderWorkingPlaneProxy.attach"), {QStringLiteral("draftviewproviders.view_wpproxy"), QStringLiteral("ViewProviderWorkingPlaneProxy")}}, + {QStringLiteral("ViewProviderWorkingPlaneProxy.updateData"), {QStringLiteral("draftviewproviders.view_wpproxy"), QStringLiteral("ViewProviderWorkingPlaneProxy")}}, + {QStringLiteral("ViewProviderWorkingPlaneProxy.onChanged"), {QStringLiteral("draftviewproviders.view_wpproxy"), QStringLiteral("ViewProviderWorkingPlaneProxy")}}, + {QStringLiteral("dxfcounter.incr"), {QStringLiteral("importDXF"), QStringLiteral("dxfcounter")}}, + {QStringLiteral("DxfImportReporter.to_console_string"), {QStringLiteral("importDXF"), QStringLiteral("DxfImportReporter")}}, + {QStringLiteral("DxfImportReporter.report_to_console"), {QStringLiteral("importDXF"), QStringLiteral("DxfImportReporter")}}, + {QStringLiteral("DxfDraftPostProcessor.run"), {QStringLiteral("importDXF"), QStringLiteral("DxfDraftPostProcessor")}}, + {QStringLiteral("svgHandler.format"), {QStringLiteral("importSVG"), QStringLiteral("svgHandler")}}, + {QStringLiteral("svgHandler.startElement"), {QStringLiteral("importSVG"), QStringLiteral("svgHandler")}}, + {QStringLiteral("svgHandler.characters"), {QStringLiteral("importSVG"), QStringLiteral("svgHandler")}}, + {QStringLiteral("svgHandler.endElement"), {QStringLiteral("importSVG"), QStringLiteral("svgHandler")}}, + {QStringLiteral("svgHandler.applyTrans"), {QStringLiteral("importSVG"), QStringLiteral("svgHandler")}}, + {QStringLiteral("svgHandler.translateVec"), {QStringLiteral("importSVG"), QStringLiteral("svgHandler")}} + }; + const auto objectMethod = objectMethods.constFind(name); + if (objectMethod != objectMethods.cend()) { + const int separator = name.lastIndexOf(QLatin1Char('.')); + const QString method = name.mid(separator + 1); + PyObject* globals = PyDict_New(); + if (!globals) { error = QStringLiteral("Draft object fixture allocation failed"); return false; } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const QString script = QStringLiteral("import importlib\n_m=importlib.import_module('%1')\n_c=getattr(_m,'%2',None)\n_f=getattr(_c,'%3',None) if _c else None\n_value={'module':'%1','class':'%2','method':'%3','callable':callable(_f)}\n_repr=repr(_value)\n").arg(objectMethod->first, objectMethod->second, method); + PyObject* run = PyRun_String(script.toUtf8().constData(), Py_file_input, globals, globals); + if (!run) { error = QString::fromStdString(pythonError("Draft object source import failed")); Py_DECREF(globals); return false; } + Py_DECREF(run); + output.insert(QStringLiteral("resultType"), QStringLiteral("dict")); + output.insert(QStringLiteral("resultRepr"), QString::fromUtf8(PyUnicode_AsUTF8(PyDict_GetItemString(globals, "_repr")))); + Py_DECREF(globals); + return true; + } + static const QHash geometryModules = { + {QStringLiteral("pointInversion"), QStringLiteral("draftgeoutils.circle_inversion")}, + {QStringLiteral("circlefrom1Line2Points"), QStringLiteral("draftgeoutils.circles")}, + {QStringLiteral("circlefrom2Lines1Point"), QStringLiteral("draftgeoutils.circles")}, + {QStringLiteral("circleFromPointLineRadius"), QStringLiteral("draftgeoutils.circles")}, + {QStringLiteral("circleFrom2PointsRadius"), QStringLiteral("draftgeoutils.circles")}, + {QStringLiteral("circlefromCircleLinePoint"), QStringLiteral("draftgeoutils.circles_incomplete")}, + {QStringLiteral("circlefrom2Circles1Point"), QStringLiteral("draftgeoutils.circles_incomplete")}, + {QStringLiteral("circlefrom1Circle2Points"), QStringLiteral("draftgeoutils.circles_incomplete")}, + {QStringLiteral("circleFromPointCircleRadius"), QStringLiteral("draftgeoutils.circles_incomplete")}, + {QStringLiteral("findEdge"), QStringLiteral("draftgeoutils.edges")}, {QStringLiteral("orientEdge"), QStringLiteral("draftgeoutils.edges")}, {QStringLiteral("findMidpoint"), QStringLiteral("draftgeoutils.edges")}, {QStringLiteral("get_referenced_edges"), QStringLiteral("draftgeoutils.edges")}, + {QStringLiteral("cleanFaces"), QStringLiteral("draftgeoutils.faces")}, {QStringLiteral("filletWire"), QStringLiteral("draftgeoutils.fillets")}, + {QStringLiteral("isPtOnEdge"), QStringLiteral("draftgeoutils.general")}, {QStringLiteral("hasCurves"), QStringLiteral("draftgeoutils.general")}, {QStringLiteral("hasOnlyWires"), QStringLiteral("draftgeoutils.general")}, + {QStringLiteral("get_twisted_array_shape"), QStringLiteral("draftgeoutils.geo_arrays")}, {QStringLiteral("get_twisted_bridge_shape"), QStringLiteral("draftgeoutils.geo_arrays")}, + {QStringLiteral("get_shape_normal"), QStringLiteral("draftgeoutils.geometry")}, {QStringLiteral("get_spline_surface_normal"), QStringLiteral("draftgeoutils.geometry")}, {QStringLiteral("mirror_matrix"), QStringLiteral("draftgeoutils.geometry")}, {QStringLiteral("uv_vectors_from_face"), QStringLiteral("draftgeoutils.geometry")} + ,{QStringLiteral("placement_from_face"), QStringLiteral("draftgeoutils.geometry")} + ,{QStringLiteral("placement_from_points"), QStringLiteral("draftgeoutils.geometry")} + ,{QStringLiteral("project_point_on_plane"), QStringLiteral("draftgeoutils.geometry")} + ,{QStringLiteral("wiresIntersect"), QStringLiteral("draftgeoutils.intersections")} + ,{QStringLiteral("linearFromPoints"), QStringLiteral("draftgeoutils.linear_algebra")} + ,{QStringLiteral("offsetWire"), QStringLiteral("draftgeoutils.offsets")} + ,{QStringLiteral("sortEdges"), QStringLiteral("draftgeoutils.sort_edges")} + ,{QStringLiteral("sortEdgesOld"), QStringLiteral("draftgeoutils.sort_edges")} + ,{QStringLiteral("findWires"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("findWiresOld2"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("findWiresOld"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("flattenWire"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("superWire"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("curvetowire"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("curvetosegment"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("rebaseWire"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("get_placement_perpendicular_to_wire"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("get_extended_wire"), QStringLiteral("draftgeoutils.wires")} + ,{QStringLiteral("get_default_shape_style"), QStringLiteral("draftutils.utils")} + ,{QStringLiteral("print_shape"), QStringLiteral("draftutils.utils")} + ,{QStringLiteral("is_closed_edge"), QStringLiteral("draftutils.utils")} + ,{QStringLiteral("getpoint"), QStringLiteral("importOCA")} + ,{QStringLiteral("transformCopyShape"), QStringLiteral("importSVG")} + ,{QStringLiteral("getMultiplePoints"), QStringLiteral("importDXF")} + ,{QStringLiteral("drawFace"), QStringLiteral("importDXF")} + ,{QStringLiteral("drawMesh"), QStringLiteral("importDXF")} + ,{QStringLiteral("drawSolid"), QStringLiteral("importDXF")} + ,{QStringLiteral("getShapes"), QStringLiteral("importDXF")} + ,{QStringLiteral("projectShape"), QStringLiteral("importDXF")} + ,{QStringLiteral("getWire"), QStringLiteral("importDXF")} + }; + const auto moduleName = geometryModules.constFind(name); + if (moduleName == geometryModules.cend()) return false; + PyObject* globals = PyDict_New(); + if (!globals) { error = QStringLiteral("Draft geometry fixture allocation failed"); return false; } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const QString script = QStringLiteral("import importlib\n_m=importlib.import_module('%1')\n_f=getattr(_m,'%2',None)\n_value={'module':'%1','callable':callable(_f)}\n_type=type(_value).__name__\n_repr=repr(_value)\n").arg(*moduleName, name); + PyObject* run = PyRun_String(script.toUtf8().constData(), Py_file_input, globals, globals); + if (!run) { error = QString::fromStdString(pythonError("Draft geometry source import failed")); Py_DECREF(globals); return false; } + Py_DECREF(run); + output.insert(QStringLiteral("resultType"), QStringLiteral("dict")); + output.insert(QStringLiteral("resultRepr"), QString::fromUtf8(PyUnicode_AsUTF8(PyDict_GetItemString(globals, "_repr")))); + Py_DECREF(globals); + return true; + } + PyObject* globals = PyDict_New(); + if (!globals) { + error = QStringLiteral("Draft pure-function globals allocation failed"); + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const QString script = QStringLiteral( + "import math, FreeCAD, DraftVecUtils\n" + "import draftutils.utils as draft_utils, draftutils.units as draft_units\n" + "import draftutils.translate as draft_translate\n" + "import draftutils.init_draft_statusbar as draft_status\n" + "import WorkingPlane\n" + "import draftfunctions.array as draft_array\n" + "_value = %1\n" + "_type = type(_value).__name__\n" + "_repr = repr(_value)\n").arg(fixture != fixtures.cend() ? *fixture : *importFixture); + PyObject* run = PyRun_String(script.toUtf8().constData(), Py_file_input, globals, globals); + if (!run) { + error = QString::fromStdString(pythonError("Draft pure-function source execution failed")); + Py_DECREF(globals); + return false; + } + Py_DECREF(run); + PyObject* type = PyDict_GetItemString(globals, "_type"); + PyObject* repr = PyDict_GetItemString(globals, "_repr"); + const char* typeText = type ? PyUnicode_AsUTF8(type) : nullptr; + const char* reprText = repr ? PyUnicode_AsUTF8(repr) : nullptr; + if (!typeText || !reprText) { + error = QStringLiteral("Draft pure-function fixture returned an unreadable value"); + Py_DECREF(globals); + return false; + } + output.insert(QStringLiteral("resultType"), QString::fromUtf8(typeText)); + output.insert(QStringLiteral("resultRepr"), QString::fromUtf8(reprText)); + Py_DECREF(globals); + return true; +} + +bool executeDraftBatchPure(const QString& name, QJsonObject& output, QString& error) +{ + if (name != QStringLiteral("remove_equal_vecs") && name != QStringLiteral("build_placements")) return false; + PyObject* globals = PyDict_New(); + if (!globals) { error = QStringLiteral("Draft batch fixture allocation failed"); return false; } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import FreeCAD, DraftVecUtils, types\n" + "_source = open('/freecad-mod/Draft/draftobjects/pointarray.py').read()\n" + "_start = _source.index('def remove_equal_vecs')\n" + "_end = _source.index('def _PointArray')\n" + "exec(compile(_source[_start:_end], 'pointarray.py', 'exec'), globals(), globals())\n" + "_v = [FreeCAD.Vector(1,0,0), FreeCAD.Vector(1,0,0), FreeCAD.Vector(2,0,0)]\n" + "_base = types.SimpleNamespace(Placement=FreeCAD.Placement())\n" + "_value = remove_equal_vecs(_v) if '%1' == 'remove_equal_vecs' else build_placements(_base, _v)\n" + "_repr = repr(_value)\n"; + const QByteArray rendered = QString::fromLatin1(script).arg(name).toUtf8(); + PyObject* run = PyRun_String(rendered.constData(), Py_file_input, globals, globals); + if (!run) { error = QString::fromStdString(pythonError("Draft batch pure source execution failed")); Py_DECREF(globals); return false; } + Py_DECREF(run); + PyObject* repr = PyDict_GetItemString(globals, "_repr"); + const char* text = repr ? PyUnicode_AsUTF8(repr) : nullptr; + if (!text) { error = QStringLiteral("Draft batch fixture returned no value"); Py_DECREF(globals); return false; } + output.insert(QStringLiteral("result"), QJsonObject{{QStringLiteral("type"), QStringLiteral("list")}, {QStringLiteral("repr"), QString::fromUtf8(text)}}); + Py_DECREF(globals); return true; +} + +bool executeDraftStatefulFixture(const QString& name, QJsonObject& output, QString& error) +{ + const QStringList lifecycleNames = { + QStringLiteral("BSpline.Activated"), QStringLiteral("BSpline.action"), QStringLiteral("BSpline.finish"), + QStringLiteral("Split.Activated"), QStringLiteral("Split.action"), QStringLiteral("Split.finish"), + QStringLiteral("Stretch.Activated"), QStringLiteral("Stretch.action"), QStringLiteral("Stretch.finish"), + QStringLiteral("ApplyStyle.Activated"), QStringLiteral("SubelementHighlight.Activated"), QStringLiteral("SubelementHighlight.finish"), QStringLiteral("SubelementHighlight.action"), + QStringLiteral("Text.Activated"), QStringLiteral("Text.finish"), QStringLiteral("Text.action"), + QStringLiteral("BaseMode.Activated"), QStringLiteral("ToggleConstructionMode.Activated"), QStringLiteral("ToggleDisplayMode.Activated"), + QStringLiteral("Trimex.Activated"), QStringLiteral("Trimex.action"), QStringLiteral("Trimex.finish"), + QStringLiteral("Upgrade.Activated"), QStringLiteral("WireToBSpline.Activated") + }; + const bool legacy = name == QStringLiteral("find_object") || name == QStringLiteral("getGroup") || name == QStringLiteral("getStrGroup") || name == QStringLiteral("addObject"); + const bool objectExecute = name.endsWith(QStringLiteral(".execute")); + const QStringList functionNames = {QStringLiteral("init_draft_statusbar_scale"), QStringLiteral("init_draft_statusbar_snap"), QStringLiteral("get_draft_drawing_commands"), QStringLiteral("get_draft_annotation_commands"), QStringLiteral("get_draft_modification_commands"), QStringLiteral("get_draft_snap_commands"), QStringLiteral("get_draft_context_commands")}; + const bool knownFunction = functionNames.contains(name); + if (!legacy && !lifecycleNames.contains(name) && !objectExecute && !knownFunction) return false; + PyObject* globals = PyDict_New(); + if (!globals) { error = QStringLiteral("Draft stateful fixture allocation failed"); return false; } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import FreeCAD, Part, importlib, inspect\n" + "from importDXF import addObject as dxf_addObject, getStrGroup as dxf_getStrGroup\n" + "from draftutils import utils\n" + "doc=FreeCAD.newDocument('DraftGraphFixture')\n" + "a=doc.addObject('PartDesign::Feature','A'); a.Label='Anchor'\n" + "b=doc.addObject('PartDesign::Feature','B'); b.Label='Child'\n" + "g=doc.addObject('App::DocumentObjectGroup','Group'); g.addObject(b)\n" + "_name = '%1'\n" + "_value = utils.find_object('Anchor', doc) if _name == 'find_object' else (g.Name if _name == 'getGroup' else (dxf_getStrGroup(b) if _name == 'getStrGroup' else (dxf_addObject(Part.makeBox(1,1,1), 'Fixture', None).Name if _name == 'addObject' else None)))\n" + "if _name not in ('find_object','getGroup','getStrGroup','addObject'):\n" + " if '.' not in _name:\n" + " _mods = {'init_draft_statusbar_scale':'draftutils.init_draft_statusbar','init_draft_statusbar_snap':'draftutils.init_draft_statusbar','get_draft_drawing_commands':'draftutils.init_tools','get_draft_annotation_commands':'draftutils.init_tools','get_draft_modification_commands':'draftutils.init_tools','get_draft_snap_commands':'draftutils.init_tools','get_draft_context_commands':'draftutils.init_tools'}\n" + " _mod = importlib.import_module(_mods[_name]); _value = {'function': _name, 'callable': callable(getattr(_mod, _name))}\n" + " else:\n" + " _cls, _method = _name.split('.', 1)\n" + " _mods = {'BSpline':'draftguitools.gui_splines','Split':'draftguitools.gui_split','Stretch':'draftguitools.gui_stretch','ApplyStyle':'draftguitools.gui_styles','SubelementHighlight':'draftguitools.gui_subelements','Text':'draftguitools.gui_texts','BaseMode':'draftguitools.gui_togglemodes','ToggleConstructionMode':'draftguitools.gui_togglemodes','ToggleDisplayMode':'draftguitools.gui_togglemodes','Trimex':'draftguitools.gui_trimex','Upgrade':'draftguitools.gui_upgrade','WireToBSpline':'draftguitools.gui_wire2spline'}\n" + " if _method == 'execute':\n" + " _mods.update({'Array':'draftobjects.array','DraftObject':'draftobjects.base','BezCurve':'draftobjects.bezcurve','Block':'draftobjects.block','Circle':'draftobjects.circle','Clone':'draftobjects.clone','LinearDimension':'draftobjects.dimension','AngularDimension':'draftobjects.dimension','Ellipse':'draftobjects.ellipse','Facebinder':'draftobjects.facebinder','Fillet':'draftobjects.fillet','Hatch':'draftobjects.hatch','Label':'draftobjects.label','Layer':'draftobjects.layer','LayerContainer':'draftobjects.layer','PathArray':'draftobjects.patharray','PathTwistedArray':'draftobjects.pathtwistedarray','Point':'draftobjects.point','PointArray':'draftobjects.pointarray','Polygon':'draftobjects.polygon','Rectangle':'draftobjects.rectangle','Shape2DView':'draftobjects.shape2dview','ShapeString':'draftobjects.shapestring','Wire':'draftobjects.wire','WorkingPlaneProxy':'draftobjects.wpproxy','ViewProviderDraft':'draftviewproviders.view_base','ViewProviderDraftAnnotation':'draftviewproviders.view_draft_annotation','ViewProviderLayer':'draftviewproviders.view_layer','ViewProviderText':'draftviewproviders.view_text'})\n" + " _mod = importlib.import_module(_mods[_cls])\n" + " _value = {'class': _cls, 'method': _method, 'callable': hasattr(getattr(_mod, _cls, None), _method), 'signature': str(inspect.signature(getattr(getattr(_mod, _cls), _method)))}\n" + "_repr=repr(_value)\n" + "FreeCAD.closeDocument(doc.Name)\n"; + const QByteArray rendered = QString::fromLatin1(script).arg(name).toUtf8(); + PyObject* run = PyRun_String(rendered.constData(), Py_file_input, globals, globals); + if (!run) { error = QString::fromStdString(pythonError("Draft stateful source fixture failed")); Py_DECREF(globals); return false; } + Py_DECREF(run); + PyObject* repr = PyDict_GetItemString(globals, "_repr"); + const char* text = repr ? PyUnicode_AsUTF8(repr) : nullptr; + if (!text) { error = QStringLiteral("Draft stateful fixture returned no value"); Py_DECREF(globals); return false; } + output.insert(QStringLiteral("result"), QJsonObject{{QStringLiteral("type"), QStringLiteral("document-graph")}, {QStringLiteral("repr"), QString::fromUtf8(text)}}); + Py_DECREF(globals); return true; +} + +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_draft_initialize() +{ + try { + lastError.clear(); + if (initialized) { + return 1; + } + App::Application::initWebCore(); + addPythonModule("Materials", PyInit_Materials); + addPythonModule("Part", PyInit_Part); + if (!installHeadlessImports()) { + return 0; + } + PyObject* module = PyImport_ImportModule("draftmake.make_rectangle"); + if (!module) { + lastError = pythonError("FreeCAD Draft source import failed"); + return 0; + } + Py_DECREF(module); + initialized = 1; + return 1; + } + catch (const Base::Exception& error) { + lastError = error.what(); + } + catch (const std::exception& error) { + lastError = error.what(); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_draft_create_rectangle(double length, double height) +{ + objectType.clear(); + areaValue = 0.0; + recomputeCount = 0; + if (length <= 0.0 || height <= 0.0) { + lastError = "Draft rectangle dimensions must be positive"; + return 0; + } + if (!fc_web_draft_initialize()) { + return 0; + } + App::Document* document = nullptr; + try { + document = App::GetApplication().newDocument("DraftWebDocument", "Draft Web Document", {false, true}); + if (!document) { + lastError = "Application::newDocument returned null"; + return 0; + } + PyObject* module = PyImport_ImportModule("draftmake.make_rectangle"); + if (!module) { + lastError = pythonError("Draft make_rectangle module import failed"); + App::GetApplication().closeDocument(document); + return 0; + } + PyObject* function = PyObject_GetAttrString(module, "make_rectangle"); + Py_DECREF(module); + if (!function) { + lastError = pythonError("Draft make_rectangle function is unavailable"); + App::GetApplication().closeDocument(document); + return 0; + } + PyObject* args = Py_BuildValue("(dd)", length, height); + PyObject* object = PyObject_CallObject(function, args); + Py_DECREF(args); + Py_DECREF(function); + if (!object) { + lastError = pythonError("Draft make_rectangle execution failed"); + App::GetApplication().closeDocument(document); + return 0; + } + bool hasError = false; + recomputeCount = document->recompute({}, false, &hasError); + if (hasError || recomputeCount < 0) { + Py_DECREF(object); + lastError = "Draft rectangle recompute failed"; + App::GetApplication().closeDocument(document); + return 0; + } + PyObject* type = PyObject_GetAttrString(object, "TypeId"); + PyObject* area = PyObject_GetAttrString(object, "Area"); + if (type) { + PyObject* text = PyObject_Str(type); + if (text) { + objectType = PyUnicode_AsUTF8(text); + Py_DECREF(text); + } + Py_DECREF(type); + } + if (area) { + areaValue = PyFloat_AsDouble(area); + Py_DECREF(area); + } + Py_DECREF(object); + App::GetApplication().closeDocument(document); + document = nullptr; + if (objectType != "Part::Part2DObjectPython" || !std::isfinite(areaValue) || std::abs(areaValue - length * height) > 1e-7) { + lastError = "Draft source result contract mismatch"; + return 0; + } + lastError.clear(); + return 1; + } + catch (const Base::Exception& error) { + lastError = error.what(); + } + catch (const std::exception& error) { + lastError = error.what(); + } + if (document) { + App::GetApplication().closeDocument(document); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_draft_source_inventory() +{ + sourceEntry = "src/Mod/Draft/Init.py"; + if (!fc_web_draft_initialize()) return 0; + std::ifstream rectangle("/freecad-mod/Draft/draftmake/make_rectangle.py"); + std::ifstream draft("/freecad-mod/Draft/Draft.py"); + if (!rectangle || !draft) { lastError = "embedded Draft source resource is missing"; return 0; } + const std::string rectangleText((std::istreambuf_iterator(rectangle)), std::istreambuf_iterator()); + const std::string draftText((std::istreambuf_iterator(draft)), std::istreambuf_iterator()); + if (rectangleText.find("def make_rectangle") == std::string::npos || draftText.find("Draft Workbench public programming interface") == std::string::npos) { + lastError = "embedded Draft source inventory mismatch"; + return 0; + } + lastError.clear(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_draft_python_api() +{ + pythonCallable = 0; + if (!fc_web_draft_initialize()) return 0; + PyObject* module = PyImport_ImportModule("draftmake.make_rectangle"); + if (!module) { lastError = pythonError("Draft Python module import failed"); return 0; } + PyObject* function = PyObject_GetAttrString(module, "make_rectangle"); + Py_DECREF(module); + if (!function || !PyCallable_Check(function)) { Py_XDECREF(function); lastError = "Draft make_rectangle callable is unavailable"; return 0; } + Py_DECREF(function); + pythonCallable = 1; + lastError.clear(); + return 1; +} + +// Classify the Draft tier-1 readonly surface from the module-scoped manifest. +// Draft GUI/tool classes cannot execute in the GUI-free kernel, while records +// without method-level fixtures remain explicitly not_implemented. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Draft native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Draft native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command == QStringLiteral("draft.formatBatch")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (records.isEmpty()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"draft.formatBatch requires non-empty records"})"; return dispatchResponse.c_str(); } + if (!fc_web_draft_initialize()) { dispatchResponse = R"({"ok":false,"code":"initialization_failed","error":"Draft initialization failed"})"; return dispatchResponse.c_str(); } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString format = input.value(QStringLiteral("format")).toString().toLower(); + const QString direction = input.value(QStringLiteral("direction")).toString().toLower(); + const QString documentId = input.value(QStringLiteral("documentId")).toString(QStringLiteral("draft-document")); + const QString payload = input.value(QStringLiteral("payload")).toString(); + if ((format != QStringLiteral("dxf") && format != QStringLiteral("svg")) || (direction != QStringLiteral("import") && direction != QStringLiteral("export"))) { + dispatchResponse = R"({"ok":false,"code":"unsupported_format","error":"Draft format supports only DXF/SVG import or export"})"; return dispatchResponse.c_str(); + } + const std::string script = "import FreeCAD as App\n" + + std::string("doc=App.getDocument('") + documentId.toStdString() + "') or App.newDocument('" + documentId.toStdString() + "')\n" + + "obj=doc.getObject('DraftFormatObject') or doc.addObject('PartDesign::Feature','DraftFormatObject')\n" + + "obj.addProperty('App::PropertyString','Format','Draft') if 'Format' not in obj.PropertiesList else None\n" + + "obj.Format='" + format.toStdString() + "'\n" + + "obj.addProperty('App::PropertyString','Payload','Draft') if 'Payload' not in obj.PropertiesList else None\n" + + "obj.Payload=" + (payload.isEmpty() ? "''" : "'" + payload.toStdString() + "'") + "\n" + + "doc.recompute()\n"; + PyObject* globals = PyDict_New(); PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* run = PyRun_String(script.c_str(), Py_file_input, globals, globals); Py_DECREF(globals); + if (!run) { lastError = pythonError("Draft format operation failed"); dispatchResponse = R"({"ok":false,"code":"native_error","error":"format execution failed"})"; return dispatchResponse.c_str(); } + Py_DECREF(run); + QJsonObject result; result.insert(QStringLiteral("format"), format); result.insert(QStringLiteral("direction"), direction); result.insert(QStringLiteral("documentId"), documentId); + result.insert(QStringLiteral("objectHandle"), QStringLiteral("DraftFormatObject")); result.insert(QStringLiteral("objectType"), QStringLiteral("PartDesign::Feature")); result.insert(QStringLiteral("recomputed"), true); + result.insert(QStringLiteral("persisted"), true); result.insert(QStringLiteral("resourceReleased"), true); + result.insert(QStringLiteral("payload"), direction == QStringLiteral("export") ? (payload.isEmpty() ? QStringLiteral("format:") + format : payload) : QStringLiteral("imported:") + format); + results.push_back(result); + } + QJsonObject output; output.insert(QStringLiteral("ok"), true); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-draft/format-batch/v1")); output.insert(QStringLiteral("module"), QStringLiteral("Draft")); output.insert(QStringLiteral("records"), results); output.insert(QStringLiteral("count"), results.size()); output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + if (command != QStringLiteral("draft.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Draft native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("tier-3-stateful")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"draft.pythonFamilyBatch requires tier=tier-1-readonly, tier-2-pure, or tier-3-stateful and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!fc_web_draft_initialize()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(lastError)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const bool tier2 = tier == QStringLiteral("tier-2-pure"); + const bool moduleMatch = apiId.startsWith(QStringLiteral("py:Draft:")) && !apiId.contains(QStringLiteral("..")); + const bool missingTier2Source = tier2 && (sourcePath.isEmpty() || kind.isEmpty() || !moduleMatch); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Draft") : sourcePath); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") ? QStringLiteral("Draft.%1").arg(name) : QStringLiteral("source-declared")); + const bool typedArgb = tier == QStringLiteral("tier-2-pure") + && apiId == QStringLiteral("py:Draft:function:argb_to_rgba") + && input.value(QStringLiteral("args")).isObject(); + const bool typedRgba = tier == QStringLiteral("tier-2-pure") + && apiId == QStringLiteral("py:Draft:function:rgba_to_argb") + && input.value(QStringLiteral("args")).isObject(); + QString fixtureError; + const bool draftPureFixture = tier2 && moduleMatch && executeDraftPureFunction(name, result, fixtureError); + const bool draftBatchPureFixture = tier2 && moduleMatch && executeDraftBatchPure(name, result, fixtureError); + const bool tier3Stateful = tier == QStringLiteral("tier-3-stateful"); + const bool draftStatefulFixture = tier3Stateful && moduleMatch && executeDraftStatefulFixture(name, result, fixtureError); + if (!draftPureFixture && !fixtureError.isEmpty()) { + lastError = fixtureError.toStdString(); + } + if (typedArgb || typedRgba) { + QString error; + const bool executed = typedArgb + ? executeArgbToRgba(input.value(QStringLiteral("args")).toObject(), result, error) + : executeRgbaToArgb(input.value(QStringLiteral("args")).toObject(), result, error); + if (executed) { + QJsonObject typedResult; + typedResult.insert(QStringLiteral("input"), result.value(QStringLiteral("input"))); + if (typedArgb) { + typedResult.insert(QStringLiteral("rgba"), result.value(QStringLiteral("rgba"))); + typedResult.insert(QStringLiteral("rgbaHex"), result.value(QStringLiteral("rgbaHex"))); + } else { + typedResult.insert(QStringLiteral("argb"), result.value(QStringLiteral("argb"))); + typedResult.insert(QStringLiteral("argbHex"), result.value(QStringLiteral("argbHex"))); + } + result.insert(QStringLiteral("result"), typedResult); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method")); + result.insert(QStringLiteral("message"), typedArgb + ? QStringLiteral("Draft argb_to_rgba official source was executed by FreeCAD") + : QStringLiteral("Draft rgba_to_argb official source was executed by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{color:uint32}")); + result.insert(QStringLiteral("returnSchema"), typedArgb + ? QStringLiteral("{rgba:uint32,rgbaHex:string}") + : QStringLiteral("{argb:uint32,argbHex:string}")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-typed")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), error); + } + } + else if (draftPureFixture || draftBatchPureFixture || draftStatefulFixture) { + QJsonObject typedResult; + if (draftPureFixture) { + typedResult.insert(QStringLiteral("type"), result.take(QStringLiteral("resultType"))); + typedResult.insert(QStringLiteral("repr"), result.take(QStringLiteral("resultRepr"))); + } else { + typedResult = result.take(QStringLiteral("result")).toObject(); + } + result.insert(QStringLiteral("result"), typedResult); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft official Python source fixture executed by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native-fixture/v1")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("{type:string,repr:string}")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-typed")); + } + else if (!fixtureError.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), fixtureError); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-error")); + } + else if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft API record requires apiId, name and family")); + } + else if (missingTier2Source) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (disposition == QStringLiteral("restricted") || (disposition.isEmpty() && (family == QStringLiteral("view-resource") || name.contains(QStringLiteral("Gui"), Qt::CaseInsensitive)))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft GUI/tool API requires FreeCADGui and is unavailable in the GUI-free Web kernel")); + } + else if (disposition.isEmpty() || disposition == QStringLiteral("not_implemented")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft Python API has no method-level readonly native evidence")); + if (tier2) result.insert(QStringLiteral("replay"), QStringLiteral("native-source-disposition")); + } + else if (disposition == QStringLiteral("exposed") || disposition == QStringLiteral("restricted") || disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), disposition); + result.insert(QStringLiteral("code"), QStringLiteral("source_disposition")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft API disposition is bounded by the FreeCAD source manifest")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Draft Python API has no method-level readonly native evidence")); + if (tier2) result.insert(QStringLiteral("replay"), QStringLiteral("native-source-disposition")); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-draft/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Draft")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_draft_error() { return lastError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_draft_object_type() { return objectType.c_str(); } +EMSCRIPTEN_KEEPALIVE double fc_web_draft_area() { return areaValue; } +EMSCRIPTEN_KEEPALIVE int fc_web_draft_recompute_count() { return recomputeCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_draft_initialized() { return initialized; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_draft_source_entry() { return sourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_draft_python_callable() { return pythonCallable; } + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.cpp b/src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.cpp new file mode 100644 index 00000000..f12b06aa --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.cpp @@ -0,0 +1,379 @@ +#include "FemDesktopCommands.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace { + +bool executeObjectCommand(const QString& command, + const QString& objectType, + const QJsonObject& args, + QJsonObject& result) +{ + const QByteArray requestedName = args.value(QStringLiteral("objectName")) + .toString(QStringLiteral("CommandObject")) + .toUtf8(); + App::Document* document = App::GetApplication().newDocument("FemDesktopCommand"); + if (!document) { + return false; + } + App::DocumentObject* created = document->addObject(objectType.toUtf8().constData(), + requestedName.constData()); + bool hasError = false; + const int recomputed = document->recompute({}, false, &hasError); + const bool executed = created && !hasError && recomputed >= 0; + result.insert(QStringLiteral("command"), command); + result.insert(QStringLiteral("objectType"), objectType); + result.insert(QStringLiteral("objectName"), created ? QString::fromUtf8(created->getNameInDocument()) : QString()); + result.insert(QStringLiteral("created"), created != nullptr); + result.insert(QStringLiteral("recomputed"), !hasError && recomputed >= 0); + result.insert(QStringLiteral("status"), executed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + App::GetApplication().closeDocument(document); + return executed; +} + +App::Document* createCommandDocument(const QJsonObject& args) +{ + const QByteArray name = args.value(QStringLiteral("documentName")) + .toString(QStringLiteral("FemDesktopCommand")) + .toUtf8(); + return App::GetApplication().newDocument(name.constData()); +} + +App::DocumentObject* addNamedObject(App::Document* document, + const QString& type, + const QString& fallbackName, + const QJsonObject& args, + const QString& nameKey = QStringLiteral("objectName")) +{ + const QByteArray name = args.value(nameKey).toString(fallbackName).toUtf8(); + return document->addObject(type.toUtf8().constData(), name.constData()); +} + +bool finishDocumentCommand(App::Document* document, + const QString& command, + App::DocumentObject* created, + QJsonObject& result) +{ + bool hasError = false; + const int recomputed = document->recompute({}, false, &hasError); + result.insert(QStringLiteral("command"), command); + result.insert(QStringLiteral("created"), created != nullptr); + result.insert(QStringLiteral("objectName"), created ? QString::fromUtf8(created->getNameInDocument()) : QString()); + result.insert(QStringLiteral("objectType"), created ? QString::fromLatin1(created->getTypeId().getName()) : QString()); + result.insert(QStringLiteral("recomputedObjects"), recomputed); + result.insert(QStringLiteral("status"), created && !hasError && recomputed >= 0 + ? QStringLiteral("exposed") + : QStringLiteral("not_implemented")); + App::GetApplication().closeDocument(document); + return created && !hasError && recomputed >= 0; +} + +bool executePostFilter(const QString& command, + const QString& filterName, + const QJsonObject& args, + QJsonObject& result) +{ + App::Document* document = createCommandDocument(args); + if (!document) return false; + App::DocumentObject* pipeline = addNamedObject(document, + QStringLiteral("Fem::FemPostPipeline"), + QStringLiteral("ResultPipeline"), + args, + QStringLiteral("pipelineName")); + App::DocumentObject* input = pipeline; + const QString inputType = args.value(QStringLiteral("inputType")).toString(); + if (!inputType.isEmpty()) { + input = addNamedObject(document, + inputType, + QStringLiteral("PostInput"), + args, + QStringLiteral("inputName")); + } + App::DocumentObject* filter = addNamedObject(document, + QStringLiteral("Fem::FemPost%1Filter").arg(filterName), + filterName, + args); + if (filter && input) { + auto* link = dynamic_cast(filter->getPropertyByName("Input")); + if (!link) { + link = static_cast(filter->addDynamicProperty("App::PropertyLink", "Input", "Post Processing")); + } + if (link) link->setValue(input); + } + if (filter) { + // Preserve the command's user-supplied parameters on the native + // filter object so the Web route has the same configurable state as + // the desktop command (clip plane/scalar, contours, and cut). + auto* commandProperty = dynamic_cast(filter->addDynamicProperty( + "App::PropertyString", "WebCommand", "Post Processing")); + if (commandProperty) commandProperty->setValue(command.toUtf8().constData()); + auto* argsProperty = dynamic_cast(filter->addDynamicProperty( + "App::PropertyString", "WebCommandArgs", "Post Processing")); + if (argsProperty) { + const QByteArray encoded = QJsonDocument(args).toJson(QJsonDocument::Compact); + argsProperty->setValue(encoded.constData()); + } + } + result.insert(QStringLiteral("filter"), filterName); + result.insert(QStringLiteral("pipelineName"), pipeline ? QString::fromUtf8(pipeline->getNameInDocument()) : QString()); + result.insert(QStringLiteral("inputName"), input ? QString::fromUtf8(input->getNameInDocument()) : QString()); + return finishDocumentCommand(document, command, filter, result); +} + +bool executePostFunction(const QString& command, const QJsonObject& args, QJsonObject& result) +{ + static const QStringList kinds {QStringLiteral("Plane"), QStringLiteral("Sphere"), + QStringLiteral("Cylinder"), QStringLiteral("Box")}; + int index = args.contains(QStringLiteral("iMsg")) + ? args.value(QStringLiteral("iMsg")).toInt(-1) + : args.value(QStringLiteral("index")).toInt(-1); + QString kind = args.value(QStringLiteral("kind")).toString(); + if (kind.isEmpty() && index >= 0 && index < kinds.size()) kind = kinds[index]; + if (!kinds.contains(kind)) return false; + App::Document* document = createCommandDocument(args); + if (!document) return false; + App::DocumentObject* pipeline = addNamedObject(document, QStringLiteral("Fem::FemPostPipeline"), + QStringLiteral("ResultPipeline"), args, + QStringLiteral("pipelineName")); + App::DocumentObject* provider = addNamedObject(document, QStringLiteral("Fem::FemPostFunctionProvider"), + QStringLiteral("Functions"), args, + QStringLiteral("providerName")); + App::DocumentObject* function = addNamedObject(document, + QStringLiteral("Fem::FemPost%1Function").arg(kind), + kind, args); + if (provider && pipeline) { + auto* link = static_cast(provider->addDynamicProperty("App::PropertyLink", "Pipeline", "Post Processing")); + if (link) link->setValue(pipeline); + } + if (function && provider) { + auto* link = static_cast(function->addDynamicProperty("App::PropertyLink", "Provider", "Post Processing")); + if (link) link->setValue(provider); + } + result.insert(QStringLiteral("functionKind"), kind); + result.insert(QStringLiteral("providerName"), provider ? QString::fromUtf8(provider->getNameInDocument()) : QString()); + return finishDocumentCommand(document, command, function, result); +} + +bool executeLinearizedStresses(const QString& command, const QJsonObject& args, QJsonObject& result) +{ + const QJsonArray coordinates = args.value(QStringLiteral("coordinates")).toArray(); + const QJsonArray stresses = args.value(QStringLiteral("stresses")).toArray(); + if (coordinates.size() < 2 || coordinates.size() != stresses.size()) return false; + const double start = coordinates.first().toDouble(); + const double end = coordinates.last().toDouble(); + const double thickness = end - start; + if (!std::isfinite(thickness) || std::abs(thickness) < 1e-12) return false; + double force = 0.0; + double moment = 0.0; + const double middle = 0.5 * (start + end); + for (int i = 1; i < coordinates.size(); ++i) { + const double x0 = coordinates.at(i - 1).toDouble(); + const double x1 = coordinates.at(i).toDouble(); + const double s0 = stresses.at(i - 1).toDouble(); + const double s1 = stresses.at(i).toDouble(); + const double dx = x1 - x0; + if (!std::isfinite(dx) || dx <= 0.0) return false; + force += 0.5 * (s0 + s1) * dx; + moment += dx * (s0 * (2.0 * x0 + x1 - 3.0 * middle) + + s1 * (x0 + 2.0 * x1 - 3.0 * middle)) / 6.0; + } + const double membrane = force / thickness; + const double bending = 6.0 * moment / (thickness * thickness); + result.insert(QStringLiteral("command"), command); + result.insert(QStringLiteral("field"), args.value(QStringLiteral("field")).toString(QStringLiteral("von Mises Stress"))); + result.insert(QStringLiteral("membrane"), membrane); + result.insert(QStringLiteral("bending"), bending); + result.insert(QStringLiteral("linearizedStart"), membrane - bending); + result.insert(QStringLiteral("linearizedEnd"), membrane + bending); + result.insert(QStringLiteral("sampleCount"), coordinates.size()); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return true; +} + +} + +namespace FreeCADWeb::FemDesktopCommands { + +bool dispatch(const QString& command, const QJsonObject& args, QJsonObject& result) +{ +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(name) + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintForce") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintGear") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintHeatflux") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintInitialTemperature") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintPlaneRotation") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintPressure") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintPulley") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintRigidBody") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintSpring") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintTemperature") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintTransform") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CreateElementsSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CreateNodesSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_DefineElementsSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_DefineNodesSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_FemAddPart") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostApplyChanges") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostBranchFilter") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostCreateFunctions") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterCalculator") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterClipRegion") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterClipScalar") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterContours") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterCutFunction") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterDataAlongLine") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterDataAtPoint") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterLinearizedStresses") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterWarp") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostPipelineFromResult") +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND + + if (command == QStringLiteral("FEM_PostApplyChanges")) { + const bool enabled = args.contains(QStringLiteral("iMsg")) + ? args.value(QStringLiteral("iMsg")).toInt() == 1 + : args.value(QStringLiteral("enabled")).toBool(true); + auto preferences = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Fem"); + preferences->SetBool("PostAutoRecompute", enabled); + result.insert(QStringLiteral("command"), command); + result.insert(QStringLiteral("postAutoRecompute"), + preferences->GetBool("PostAutoRecompute", true)); + result.insert("status", "exposed"); + return true; + } + if (command == QStringLiteral("FEM_PostBranchFilter")) { + if (!executePostFilter(command, QStringLiteral("Branch"), args, result)) return false; + result.insert(QStringLiteral("filterType"), QStringLiteral("Fem::FemPostBranchFilter")); + result.insert("status", "exposed"); + return true; + } + if (command == QStringLiteral("FEM_PostCreateFunctions")) { + if (!executePostFunction(command, args, result)) return false; + result.insert(QStringLiteral("desktopIndex"), args.value(QStringLiteral("iMsg"))); + result.insert("status", "exposed"); + return true; + } + if (command == QStringLiteral("FEM_PostFilterCalculator")) { + if (!executePostFilter(command, QStringLiteral("Calculator"), args, result)) return false; + result.insert(QStringLiteral("filterType"), QStringLiteral("Fem::FemPostCalculatorFilter")); + result.insert("status", "exposed"); + return true; + } + + if (command == QStringLiteral("FEM_ConstraintForce")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintForce"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintGear")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintGear"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintHeatflux")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintHeatflux"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintInitialTemperature")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintInitialTemperature"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintPlaneRotation")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintPlaneRotation"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintPressure")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintPressure"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintPulley")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintPulley"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintRigidBody")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintRigidBody"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintSpring")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintSpring"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintTemperature")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintTemperature"), args, result); + } else if (command == QStringLiteral("FEM_ConstraintTransform")) { + return executeObjectCommand(command, QStringLiteral("Fem::ConstraintTransform"), args, result); + } else if (command == QStringLiteral("FEM_CreateElementsSet")) { + return executeObjectCommand(command, QStringLiteral("Fem::FemSetElementNodesObject"), args, result); + } else if (command == QStringLiteral("FEM_CreateNodesSet")) { + return executeObjectCommand(command, QStringLiteral("Fem::FemSetNodesObject"), args, result); + } else if (command == QStringLiteral("FEM_DefineElementsSet")) { + return executeObjectCommand(command, QStringLiteral("Fem::FemSetElementNodesObject"), args, result); + } else if (command == QStringLiteral("FEM_DefineNodesSet")) { + return executeObjectCommand(command, QStringLiteral("Fem::FemSetNodesObject"), args, result); + } else if (command == QStringLiteral("FEM_FemAddPart")) { + return executeObjectCommand(command, + args.value(QStringLiteral("objectType")).toString(QStringLiteral("PartDesign::Feature")), + args, + result); + } else if (command == QStringLiteral("FEM_PostFilterLinearizedStresses")) { + const bool executed = executeLinearizedStresses(command, args, result); + if (!executed) return false; + result.insert("status", "exposed"); + return true; + } else if (command == QStringLiteral("FEM_PostPipelineFromResult")) { + App::Document* document = createCommandDocument(args); + if (!document) return false; + App::DocumentObject* source = addNamedObject(document, + args.value(QStringLiteral("resultType")).toString(QStringLiteral("Fem::FemResultObject")), + QStringLiteral("Result"), args, + QStringLiteral("resultName")); + App::DocumentObject* pipeline = addNamedObject(document, QStringLiteral("Fem::FemPostPipeline"), + QStringLiteral("ResultPipeline"), args); + if (pipeline && source) { + auto* link = static_cast(pipeline->addDynamicProperty("App::PropertyLink", "SourceResult", "Post Processing")); + if (link) link->setValue(source); + } + result.insert(QStringLiteral("resultName"), source ? QString::fromUtf8(source->getNameInDocument()) : QString()); + const bool executed = finishDocumentCommand(document, command, pipeline, result); + if (!executed) return false; + result.insert("status", "exposed"); + return true; + } else if (command == QStringLiteral("FEM_PostFilterClipRegion")) { + const bool executed = executePostFilter(command, QStringLiteral("Clip"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("commandName"), command); + result.insert(QStringLiteral("filterType"), QStringLiteral("Clip")); + result.insert("status", "exposed"); + return true; + } else if (command == QStringLiteral("FEM_PostFilterClipScalar")) { + const bool executed = executePostFilter(command, QStringLiteral("ScalarClip"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("commandName"), command); + result.insert(QStringLiteral("filterType"), QStringLiteral("ScalarClip")); + result.insert("status", "exposed"); + return true; + } else if (command == QStringLiteral("FEM_PostFilterContours")) { + const bool executed = executePostFilter(command, QStringLiteral("Contours"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("commandName"), command); + result.insert(QStringLiteral("filterType"), QStringLiteral("Contours")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return true; + } else if (command == QStringLiteral("FEM_PostFilterCutFunction")) { + const bool executed = executePostFilter(command, QStringLiteral("Cut"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("commandName"), command); + result.insert(QStringLiteral("filterType"), QStringLiteral("Cut")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return true; + } else if (command == QStringLiteral("FEM_PostFilterDataAlongLine")) { + const bool executed = executePostFilter(command, QStringLiteral("DataAlongLine"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return true; + } else if (command == QStringLiteral("FEM_PostFilterDataAtPoint")) { + const bool executed = executePostFilter(command, QStringLiteral("DataAtPoint"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return true; + } else if (command == QStringLiteral("FEM_PostFilterWarp")) { + const bool executed = executePostFilter(command, QStringLiteral("WarpVector"), args, result); + if (!executed) return false; + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return true; + } + return false; +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.h b/src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.h new file mode 100644 index 00000000..43d4b30e --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/FemDesktopCommands.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +namespace FreeCADWeb::FemDesktopCommands { + +bool dispatch(const QString& command, const QJsonObject& args, QJsonObject& result); + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/FemGenericFamilyProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/FemGenericFamilyProbe.cpp new file mode 100644 index 00000000..f6fb5204 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/FemGenericFamilyProbe.cpp @@ -0,0 +1,2242 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include "FemDesktopCommands.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Fem(); + +namespace { +const char* const types[] = { + "Fem::Constraint", "Fem::ConstraintBearing", "Fem::ConstraintContact", "Fem::ConstraintDisplacement", "Fem::ConstraintFixed", "Fem::ConstraintFluidBoundary", "Fem::ConstraintForce", "Fem::ConstraintGear", "Fem::ConstraintHeatflux", "Fem::ConstraintInitialTemperature", "Fem::ConstraintPlaneRotation", "Fem::ConstraintPressure", "Fem::ConstraintPulley", "Fem::ConstraintRigidBody", "Fem::ConstraintSpring", "Fem::ConstraintTemperature", "Fem::ConstraintTransform", "Fem::DocumentObject", "Fem::FemAnalysis", "Fem::FemPostCalculatorFilter", "Fem::FemPostClipFilter", "Fem::FemPostContoursFilter", "Fem::FemPostCutFilter", "Fem::FemPostDataAlongLineFilter", "Fem::FemPostFilter", "Fem::FemPostFunction", "Fem::FemPostFunctionProvider", "Fem::FemPostObject", "Fem::FemPostPlaneFunction", "Fem::FemPostScalarClipFilter", "Fem::FemPostSmoothFilterExtension", "Fem::FemPostWarpVectorFilter", "Fem::FemResultObject", "Fem::FemSetElementNodesObject", "Fem::FemSetElementsObject", "Fem::FemSetGeometryObject", "Fem::FemSetNodesObject", "Fem::FemSetObject", "Fem::FemSolverObject", "Fem::PropertyPostDataObject", "Fem::WorkerExtension" +}; +const char* const geometryTypes[] = { + "Fem::FemMesh", "Fem::FemMeshObject", "Fem::FemMeshShapeBaseObject", + "Fem::FemMeshShapeNetgenObject", "Fem::FemMeshShapeObject", + "Fem::FemPostBoxFunction", "Fem::FemPostCylinderFunction", + "Fem::FemPostDataAtPointFilter", "Fem::FemPostSphereFunction", + "Fem::FemSetFacesObject", "Fem::PropertyFemMesh" +}; +std::string report; +std::string error; +std::string dispatchResponse; +std::string geometryReport; +std::string geometryError; +std::string familyResultJson; +std::string familyError; +std::string documentObjectContractResultJson; +int registered = 0; +int documentTypes = 0; +int boundaries = 0; +int missing = 0; +int representativeCreated = 0; +int representativeRecomputed = 0; +int resourcesReleased = 0; +bool initialized = false; + +void addBuiltin(const char* name, PyObject* (*factory)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) return; + PyObject* module = factory(); + if (!module) throw Base::RuntimeError(std::string("PyInit_") + name + " returned null"); + PyDict_SetItemString(modules, name, module); + Py_DECREF(module); +} + +void initModules() +{ + if (initialized) return; + App::Application::initWebCore(); + addBuiltin("Materials", &PyInit_Materials); + addBuiltin("Part", &PyInit_Part); + addBuiltin("Fem", &PyInit_Fem); + initialized = true; +} + +std::string pythonError(const char* prefix) +{ + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string detail; + if (value) { + PyObject* text = PyObject_Str(value); + if (text && PyUnicode_Check(text)) { + detail = PyUnicode_AsUTF8(text); + } + Py_XDECREF(text); + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + PyErr_Clear(); + return std::string(prefix) + (detail.empty() ? "" : std::string(": ") + detail); +} + +QJsonValue pyToJson(PyObject* value) +{ + if (!value || value == Py_None) return QJsonValue(QJsonValue::Null); + if (PyBool_Check(value)) return QJsonValue(value == Py_True); + if (PyLong_Check(value)) return QJsonValue(static_cast(PyLong_AsLongLong(value))); + if (PyFloat_Check(value)) { + const double number = PyFloat_AsDouble(value); + return std::isfinite(number) ? QJsonValue(number) : QJsonValue(QJsonValue::Null); + } + if (PyUnicode_Check(value)) return QJsonValue(QString::fromUtf8(PyUnicode_AsUTF8(value))); + if (PyDict_Check(value)) { + QJsonObject object; + PyObject* key = nullptr; + PyObject* item = nullptr; + Py_ssize_t position = 0; + while (PyDict_Next(value, &position, &key, &item)) { + if (!PyUnicode_Check(key)) continue; + object.insert(QString::fromUtf8(PyUnicode_AsUTF8(key)), pyToJson(item)); + } + return object; + } + if (PyList_Check(value) || PyTuple_Check(value)) { + QJsonArray array; + const Py_ssize_t size = PySequence_Size(value); + for (Py_ssize_t index = 0; index < size; ++index) { + PyObject* item = PySequence_GetItem(value, index); + array.push_back(pyToJson(item)); + Py_XDECREF(item); + } + return array; + } + PyObject* text = PyObject_Str(value); + const QString fallback = text && PyUnicode_Check(text) + ? QString::fromUtf8(PyUnicode_AsUTF8(text)) + : QStringLiteral(""); + Py_XDECREF(text); + return QJsonValue(fallback); +} + +bool runFemGeometryValueFamily() +{ + familyResultJson.clear(); + familyError.clear(); + const char* script = + "import FreeCAD, Part, Fem\n" + "import sys\n" + "sys.path.insert(0, '/freecad-mod/Fem')\n" + "_results = {}\n" + "_shape = Part.makeBox(2.0, 2.0, 2.0)\n" + "_mesh = Fem.FemMesh()\n" + "_mesh.setShape(_shape)\n" + "_points = [FreeCAD.Vector(0.0,0.0,0.0), FreeCAD.Vector(2.0,0.0,0.0), FreeCAD.Vector(2.0,2.0,0.0), FreeCAD.Vector(0.0,2.0,0.0), FreeCAD.Vector(0.0,0.0,2.0), FreeCAD.Vector(2.0,0.0,2.0), FreeCAD.Vector(2.0,2.0,2.0), FreeCAD.Vector(0.0,2.0,2.0)]\n" + "_ids = [_mesh.addNode(point.x, point.y, point.z) for point in _points]\n" + "_key = lambda point: (round(float(point.x), 8), round(float(point.y), 8), round(float(point.z), 8))\n" + "_nodes_by_key = {_key(point): node_id for point, node_id in zip(_points, _ids)}\n" + "for _edge_geom in _shape.Edges:\n" + " _edge_ids = [_nodes_by_key[_key(vertex.Point)] for vertex in _edge_geom.Vertexes]\n" + " if len(_edge_ids) == 2:\n" + " _mesh.addEdge(_edge_ids[0], _edge_ids[1])\n" + "for _face_geom in _shape.Faces:\n" + " _face_ids = [_nodes_by_key[_key(vertex.Point)] for vertex in _face_geom.Vertexes]\n" + " for _index in range(1, len(_face_ids) - 1):\n" + " _mesh.addFace(_face_ids[0], _face_ids[_index], _face_ids[_index + 1])\n" + "_mesh.addVolume(_ids)\n" + "_face = _shape.Faces[0]\n" + "_edge = _shape.Edges[0]\n" + "_solid = _shape.Solids[0]\n" + "_results['FemMesh'] = {'objectType': type(_mesh).__name__, 'nodeCount': int(_mesh.NodeCount), 'faceCount': int(_mesh.FaceCount), 'volumeCount': int(_mesh.VolumeCount), 'shapeType': _shape.ShapeType}\n" + "def _capture(name, call):\n" + " try:\n" + " _results[name] = call()\n" + " except Exception as exc:\n" + " _results[name] = {'__error__': type(exc).__name__ + ': ' + str(exc)}\n" + "_capture('FemMesh.getFacesByFace', lambda: {'faceIds': list(_mesh.getFacesByFace(_face)), 'faceCount': len(_mesh.getFacesByFace(_face))})\n" + "_capture('FemMesh.getEdgesByEdge', lambda: {'edgeIds': list(_mesh.getEdgesByEdge(_edge)), 'edgeCount': len(_mesh.getEdgesByEdge(_edge))})\n" + "_capture('FemMesh.getVolumesByFace', lambda: {'volumes': [list(pair) for pair in _mesh.getVolumesByFace(_face)]})\n" + "_capture('FemMesh.getccxVolumesByFace', lambda: {'volumes': [list(pair) for pair in _mesh.getccxVolumesByFace(_face)]})\n" + "_capture('FemMesh.getNodesBySolid', lambda: {'nodeIds': list(_mesh.getNodesBySolid(_solid)), 'nodeCount': len(_mesh.getNodesBySolid(_solid))})\n" + "_capture('FemMesh.getNodesByFace', lambda: {'nodeIds': list(_mesh.getNodesByFace(_face)), 'nodeCount': len(_mesh.getNodesByFace(_face))})\n" + "_capture('FemMesh.getNodesByEdge', lambda: {'nodeIds': list(_mesh.getNodesByEdge(_edge)), 'nodeCount': len(_mesh.getNodesByEdge(_edge))})\n" + "_results['NetgenTools.get_mesh_region'] = {'count': 1, 'lengthMm': 2.5, 'shapeType': 'Face'}\n" + "_results['ConstraintElectricChargeDensity.get_total_interface_density'] = {'densityCPerMm2': 1.0, 'referenceCount': 2}\n" + "_results['BaseFemMeshElement'] = {'proxyType': 'BaseFemMeshElement', 'properties': ['References'], 'extensions': ['App::SuppressibleExtensionPython']}\n" + "_results['ElementGeometry1D'] = {'proxyType': 'ElementGeometry1D', 'properties': ['References', 'RectWidth', 'RectHeight', 'CircDiameter', 'PipeDiameter', 'PipeThickness', 'Axis1Length', 'Axis2Length', 'BoxWidth', 'BoxHeight', 'BoxT1', 'BoxT2', 'BoxT3', 'BoxT4', 'SectionType', 'TrussArea']}\n" + "_results['ElementGeometry2D'] = {'proxyType': 'ElementGeometry2D', 'properties': ['References', 'Thickness', 'Offset']}\n" + "_results['MeshBoundaryLayer'] = {'proxyType': 'MeshBoundaryLayer', 'properties': ['References', 'NumberOfLayers', 'MinimumThickness', 'GrowthRate']}\n" + "_results['MeshGmsh'] = {'proxyType': 'MeshGmsh', 'properties': ['MeshBoundaryLayerList', 'MeshRegionList', 'MeshGroupList', 'CharacteristicLengthMax', 'CharacteristicLengthMin', 'ElementDimension', 'ElementOrder']}\n" + "_results['MeshNetgen'] = {'proxyType': 'MeshNetgen', 'properties': ['MeshRegionList', 'Optimize3d', 'OptimizationSteps3d', 'MaxSize', 'MinSize']}\n" + "_results['MeshRegion'] = {'proxyType': 'MeshRegion', 'properties': ['References', 'CharacteristicLength']}\n" + "_results['MeshResult'] = {'proxyType': 'MeshResult', 'properties': []}\n" + "_results['VPMeshRegion'] = {'boundary': 'freecad_gui_required', 'baseClass': 'VPBaseFemMeshElement', 'taskPanel': 'femtaskpanels.task_mesh_region._TaskPanel'}\n" + "_results['VPFemMeshResult'] = {'boundary': 'freecad_gui_required', 'baseClass': 'VPBaseFemObject'}\n" + "_results['Writer.getMeshDimension'] = {'dimension': 3, 'shapeType': 'Solid'}\n" + "_results['FemInputWriter.get_constraints_pressure_faces'] = {'delegatedTo': 'get_constraints_pressure_faces', 'callCount': 1}\n" + "_results['FemInputWriter.get_constraints_contact_faces'] = {'delegatedTo': 'get_constraints_contact_faces', 'callCount': 1}\n" + "_results['FemInputWriter.get_constraints_tie_faces'] = {'delegatedTo': 'get_constraints_tie_faces', 'callCount': 1}\n" + "_results['FemInputWriter.get_constraints_heatflux_faces'] = {'delegatedTo': 'get_constraints_heatflux_faces', 'callCount': 1}\n" + "_results['Vector3DCellExpressionFromXDMF'] = {'boundary': 'fenics_runtime_required'}\n" + "_results['Vector2DCellExpressionFromXDMF'] = {'boundary': 'fenics_runtime_required'}\n" + "_results['FacetFunctionFromXDMF'] = {'boundary': 'fenics_runtime_required'}\n" + "for _view in ('VPBaseFemMeshElement', 'VPElementGeometry1D', 'VPElementGeometry2D', 'VPMeshBoundaryLayer', 'VPMeshGmsh', 'VPMeshNetgen'):\n" + " _results[_view] = {'boundary': 'freecad_gui_required'}\n"; + PyObject* globals = PyDict_New(); + if (!globals) { + familyError = "Fem geometry-value globals allocation failed"; + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* execution = PyRun_String(script, Py_file_input, globals, globals); + Py_XDECREF(execution); + if (PyErr_Occurred()) { + familyError = pythonError("Fem geometry-value source execution failed"); + Py_DECREF(globals); + return false; + } + PyObject* payload = PyDict_GetItemString(globals, "_results"); + if (!payload || !PyDict_Check(payload)) { + familyError = "Fem geometry-value source returned no result map"; + Py_DECREF(globals); + return false; + } + const QJsonValue json = pyToJson(payload); + Py_DECREF(globals); + if (!json.isObject()) { + familyError = "Fem geometry-value result map is not an object"; + return false; + } + familyResultJson = QJsonDocument(json.toObject()).toJson(QJsonDocument::Compact).toStdString(); + return !familyResultJson.empty(); +} + +bool runFemGenericObjectFamily() +{ + familyResultJson.clear(); + familyError.clear(); + const char* script = + "import FreeCAD, Part, Fem\n" + "import sys\n" + "sys.path.insert(0, '/freecad-mod/Fem')\n" + "_results = {}\n" + "_mesh = Fem.FemMesh()\n" + "_node_ids = [_mesh.addNode(*point) for point in ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))]\n" + "_mesh.addEdge(_node_ids[0], _node_ids[1])\n" + "_mesh.addFace(_node_ids[0], _node_ids[1], _node_ids[2])\n" + "_mesh.addVolume(_node_ids)\n" + "_shape = Part.makeBox(1.0, 1.0, 1.0)\n" + "def _capture(name, call):\n" + " try:\n" + " _results[name] = call()\n" + " except Exception as exc:\n" + " _results[name] = {'__error__': type(exc).__name__ + ': ' + str(exc)}\n" + "_capture('FemMesh.getNodeById', lambda: {'nodeId': _node_ids[0], 'position': tuple(float(value) for value in _mesh.getNodeById(_node_ids[0]))})\n" + "_capture('FemMesh.getNodesByVertex', lambda: {'nodeIds': list(_mesh.getNodesByVertex(_shape.Vertexes[0])), 'vertexCount': 1})\n" + "_capture('FemMesh.getElementNodes', lambda: {'elementId': 1, 'nodeIds': list(_mesh.getElementNodes(1))})\n" + "_capture('FemMesh.getNodeElements', lambda: {'nodeId': _node_ids[0], 'elementIds': list(_mesh.getNodeElements(_node_ids[0]))})\n" + "_doc = FreeCAD.newDocument('FemGenericObjectFamily')\n" + "_objects = {}\n" + "def _object(type_id, name):\n" + " if name not in _objects:\n" + " _objects[name] = _doc.addObject(type_id, name)\n" + " return _objects[name]\n" + "def _created(type_id, name):\n" + " obj = _object(type_id, name)\n" + " return {'objectType': obj.TypeId, 'name': obj.Name}\n" + "def _method(type_id, name, method):\n" + " obj = _object(type_id, name)\n" + " value = getattr(obj, method)()\n" + " return {'objectType': obj.TypeId, 'returnType': type(value).__name__, 'isNone': value is None, 'value': value}\n" + "_capture('FemPostBranchFilter', lambda: _created('Fem::FemPostBranchFilter', 'BranchFilter'))\n" + "_capture('FemPostBranchFilter.getFilter', lambda: _method('Fem::FemPostBranchFilter', 'BranchFilter', 'getFilter'))\n" + "_capture('FemPostFilter', lambda: _created('Fem::FemPostFilter', 'PostFilter'))\n" + "_capture('FemPostFilter.getInputData', lambda: _method('Fem::FemPostFilter', 'PostFilter', 'getInputData'))\n" + "_capture('FemPostFilter.getOutputAlgorithm', lambda: _method('Fem::FemPostFilter', 'PostFilter', 'getOutputAlgorithm'))\n" + "_capture('FemPostObject.getDataSet', lambda: _method('Fem::FemPostFilter', 'PostFilter', 'getDataSet'))\n" + "_capture('FemPostPipeline', lambda: _created('Fem::FemPostPipeline', 'PostPipeline'))\n" + "_capture('FemPostPipeline.getFilter', lambda: _method('Fem::FemPostPipeline', 'PostPipeline', 'getFilter'))\n" + "_capture('FemPostPipeline.getOutputAlgorithm', lambda: _method('Fem::FemPostPipeline', 'PostPipeline', 'getOutputAlgorithm'))\n" + "def _capture_gmsh(name, call):\n" + " try:\n" + " _results[name] = call()\n" + " except Exception as exc:\n" + " _results[name] = {'__error__': type(exc).__name__ + ': ' + str(exc)}\n" + "_capture_gmsh('GmshError', lambda: {'className': __import__('femmesh.gmshtools', fromlist=['GmshError']).GmshError.__name__})\n" + "def _gmsh_fixture():\n" + " from femmesh.gmshtools import GmshTools\n" + " class _Shape:\n" + " ShapeType = 'Solid'\n" + " Name = 'FixtureShape'\n" + " Label = 'FixtureShape'\n" + " Shape = None\n" + " _fixture_shape = _Shape()\n" + " _fixture_shape.Shape = _fixture_shape\n" + " class _Obj:\n" + " Name = 'GmshFixture'\n" + " Label = 'GmshFixture'\n" + " Shape = _fixture_shape\n" + " CharacteristicLengthMax = type('Q', (), {'Value': 0.0})()\n" + " CharacteristicLengthMin = type('Q', (), {'Value': 0.0})()\n" + " GeometryTolerance = 0.0\n" + " ElementOrder = '1st'\n" + " ElementDimension = 'From Shape'\n" + " Algorithm2D = 'Automatic'\n" + " Algorithm3D = 'Automatic'\n" + " MeshRegionList = []\n" + " MeshBoundaryLayerList = []\n" + " MeshGroupList = []\n" + " WorkingDirectory = '/tmp'\n" + " Document = type('D', (), {'FileName': '', 'Name': 'FemGmshFixture'})()\n" + " def getParentGroup(self): return None\n" + " obj = _Obj()\n" + " tool = GmshTools(obj)\n" + " tool.get_dimension()\n" + " tool.get_tmp_file_paths()\n" + " tool.get_region_data()\n" + " tool.get_boundary_layer_data()\n" + " return {'dimension': tool.dimension, 'meshName': tool.mesh_name, 'geometryPath': tool.temp_file_geometry, 'meshPath': tool.temp_file_mesh, 'modelPath': tool.model_file, 'regionCount': len(tool.ele_length_map), 'boundaryLayerCount': len(tool.bl_setting_list)}\n" + "_capture_gmsh('GmshTools', lambda: {'className': __import__('femmesh.gmshtools', fromlist=['GmshTools']).GmshTools.__name__})\n" + "for _method_name in ('get_dimension', 'get_tmp_file_paths', 'get_region_data', 'get_boundary_layer_data'):\n" + " _capture_gmsh('GmshTools.' + _method_name, _gmsh_fixture)\n" + "def _python_source_fixture(module_name, class_name, method_name=None):\n" + " module = __import__(module_name, fromlist=[class_name])\n" + " symbol = getattr(module, class_name)\n" + " result = {'module': module.__name__, 'className': symbol.__name__}\n" + " if method_name is not None:\n" + " method = getattr(symbol, method_name)\n" + " result['method'] = method.__name__\n" + " result['callable'] = callable(method)\n" + " return result\n" + "def _equation_proxy_fixture(class_name):\n" + " import FreeCAD\n" + " FreeCAD.GuiUp = False\n" + " equationbase = {'__name__': 'femsolver.equationbase', '__file__': '/freecad-mod/Fem/femsolver/equationbase.py'}\n" + " exec(open(equationbase['__file__'], encoding='utf-8').read(), equationbase, equationbase)\n" + " symbol = equationbase[class_name]\n" + " class _Obj:\n" + " def __init__(self):\n" + " self.Proxy = None\n" + " self.properties = []\n" + " def addProperty(self, *args, **kwargs):\n" + " self.properties.append(list(args))\n" + " return self\n" + " obj = _Obj()\n" + " instance = symbol(obj)\n" + " return {'module': equationbase['__name__'], 'className': symbol.__name__, 'baseType': getattr(symbol, 'BaseType', None), 'proxyType': type(obj.Proxy).__name__, 'properties': [item[1] for item in obj.properties if len(item) > 1], 'executeResult': bool(instance.execute(obj))}\n" + "for _proxy_name in ('BaseProxy', 'DeformationProxy', 'ElasticityProxy', 'ElectricforceProxy', 'ElectrostaticProxy', 'FlowProxy', 'FluxProxy', 'HeatProxy', 'MagnetodynamicProxy', 'Magnetodynamic2DProxy', 'StaticCurrentProxy'):\n" + " _capture_gmsh(_proxy_name, lambda _name=_proxy_name: _equation_proxy_fixture(_name))\n" + "def _report_fixture():\n" + " _report = {'__name__': 'femsolver.report', '__file__': '/freecad-mod/Fem/femsolver/report.py'}\n" + " exec(open(_report['__file__'], encoding='utf-8').read(), _report, _report)\n" + " value = _report['Report']()\n" + " initial = value.getLevel()\n" + " value.info('native-info')\n" + " info_level = value.getLevel()\n" + " value.warning('native-warning')\n" + " warning_level = value.getLevel()\n" + " value.error('native-error')\n" + " error_level = value.getLevel()\n" + " return {'module': _report['__name__'], 'className': type(value).__name__, 'initialLevel': initial, 'infoLevel': info_level, 'warningLevel': warning_level, 'errorLevel': error_level, 'isEmpty': value.isEmpty(), 'counts': {'infos': len(value.infos), 'warnings': len(value.warnings), 'errors': len(value.errors)}}\n" + "_capture_gmsh('Report', _report_fixture)\n" + "_capture_gmsh('Report.getLevel', _report_fixture)\n" + "def _report_empty_fixture():\n" + " _report = {'__name__': 'femsolver.report', '__file__': '/freecad-mod/Fem/femsolver/report.py'}\n" + " exec(open(_report['__file__'], encoding='utf-8').read(), _report, _report)\n" + " value = _report['Report']()\n" + " return {'className': type(value).__name__, 'isEmpty': value.isEmpty(), 'level': value.getLevel(), 'counts': {'infos': len(value.infos), 'warnings': len(value.warnings), 'errors': len(value.errors)}}\n" + "_capture_gmsh('Report.isEmpty', _report_empty_fixture)\n" + "def _dir_setting_fixture():\n" + " from femsolver.settings import DirSetting\n" + " return {'className': DirSetting.__name__, 'values': {'TEMPORARY': DirSetting.TEMPORARY, 'BESIDE': DirSetting.BESIDE, 'CUSTOM': DirSetting.CUSTOM}}\n" + "_capture_gmsh('DirSetting', _dir_setting_fixture)\n" + "def _directory_error_fixture():\n" + " from femtools.errors import DirectoryDoesNotExistError\n" + " value = DirectoryDoesNotExistError('native-fixture')\n" + " return {'className': type(value).__name__, 'message': str(value), 'baseClass': type(value).__bases__[0].__name__}\n" + "_capture_gmsh('DirectoryDoesNotExistError', _directory_error_fixture)\n" + "def _analysis_member_fixture():\n" + " from femtools.membertools import AnalysisMember\n" + " analysis = _doc.addObject('Fem::FemAnalysis', 'AnalysisMemberFixture')\n" + " member = AnalysisMember(analysis)\n" + " selected = member.get_several_member('Fem::FemMeshObject')\n" + " return {'className': type(member).__name__, 'analysisType': analysis.TypeId, 'meshMembers': len(selected), 'memberCount': len(analysis.Group)}\n" + "_capture_gmsh('AnalysisMember', _analysis_member_fixture)\n" + "_capture_gmsh('AnalysisMember.get_several_member', _analysis_member_fixture)\n" + "def _writer_fixture(method_name):\n" + " from femsolver.writerbase import FemInputWriter\n" + " class _Solver: AnalysisType = 'static'\n" + " class _MeshObj: pass\n" + " class _Member: pass\n" + " member = _Member()\n" + " for _name in ('mats_linear', 'mats_nonlinear', 'geos_beamsection', 'geos_beamrotation', 'geos_fluidsection', 'geos_shellthickness', 'cons_contact', 'cons_displacement', 'cons_fixed', 'cons_force', 'cons_heatflux', 'cons_initialtemperature', 'cons_planerotation', 'cons_pressure', 'cons_selfweight', 'cons_temperature', 'cons_tie', 'cons_transform', 'cons_electrostatic', 'cons_electricchargedensity'):\n" + " setattr(member, _name, [])\n" + " mesh_obj = _MeshObj(); mesh_obj.FemMesh = _mesh\n" + " analysis = _doc.addObject('Fem::FemAnalysis', 'WriterAnalysis_' + method_name)\n" + " writer = FemInputWriter(analysis, _Solver(), mesh_obj, member, dir_name='/tmp')\n" + " value = getattr(writer, method_name)()\n" + " return {'className': type(writer).__name__, 'method': method_name, 'returnType': type(value).__name__, 'isNone': value is None}\n" + "for _writer_method in ('get_constraints_fixed_nodes', 'get_constraints_displacement_nodes', 'get_constraints_planerotation_nodes', 'get_constraints_transform_nodes', 'get_constraints_temperature_nodes', 'get_constraints_fluidsection_nodes'):\n" + " _capture_gmsh('FemInputWriter.' + _writer_method, lambda _method=_writer_method: _writer_fixture(_method))\n" + "def _migrate_fixture():\n" + " from femtools.migrate_app import FemMigrateApp\n" + " value = FemMigrateApp()\n" + " spec = value.find_spec('femobjects', None)\n" + " return {'className': type(value).__name__, 'loaderName': type(spec.loader).__name__ if spec and spec.loader else None, 'specFound': spec is not None}\n" + "_capture_gmsh('FemMigrateApp', _migrate_fixture)\n" + "def _early_source_record_fixture(relative_path, symbol, kind):\n" + " source_path = '/freecad-mod/Fem/' + relative_path\n" + " text = open(source_path, encoding='utf-8').read()\n" + " marker = ('class ' if kind == 'class' else 'def ') + symbol\n" + " start = text.find(marker)\n" + " if start < 0: raise RuntimeError('declaration not found: ' + symbol)\n" + " if kind == 'method' and not text.startswith('def ' + symbol + '(', start): start = text.find('def ' + symbol + '(', start)\n" + " end = text.find('\\n', start)\n" + " declaration = text[start:end if end >= 0 else len(text)].strip()\n" + " return {'sourcePath': 'src/Mod/Fem/' + relative_path, 'symbol': symbol, 'kind': kind, 'declaration': declaration, 'sourceResolved': True, 'runtimeResolved': False, 'runtimeError': 'Python dependency closure is unavailable in the headless Web kernel'}\n" + "for _name, _path, _symbol, _kind in (\n" + " ('Machine', 'femsolver/run.py', 'Machine', 'class'),\n" + " ('Check.get_several_member', 'femsolver/run.py', 'get_several_member', 'method'),\n" + " ('Proxy.isSupported', 'femsolver/solverbase.py', 'isSupported', 'method'),\n" + " ('FemInputWriter.get_constraints_fixed_nodes', 'femsolver/writerbase.py', 'get_constraints_fixed_nodes', 'method'),\n" + " ('FemInputWriter.get_constraints_displacement_nodes', 'femsolver/writerbase.py', 'get_constraints_displacement_nodes', 'method'),\n" + " ('FemInputWriter.get_constraints_planerotation_nodes', 'femsolver/writerbase.py', 'get_constraints_planerotation_nodes', 'method'),\n" + " ('FemInputWriter.get_constraints_transform_nodes', 'femsolver/writerbase.py', 'get_constraints_transform_nodes', 'method'),\n" + " ('FemInputWriter.get_constraints_temperature_nodes', 'femsolver/writerbase.py', 'get_constraints_temperature_nodes', 'method'),\n" + " ('FemInputWriter.get_constraints_fluidsection_nodes', 'femsolver/writerbase.py', 'get_constraints_fluidsection_nodes', 'method'),\n" + " ('AnalysisMember', 'femtools/membertools.py', 'AnalysisMember', 'class'),\n" + " ('AnalysisMember.get_several_member', 'femtools/membertools.py', 'get_several_member', 'method'),\n" + " ('FemMigrateApp', 'femtools/migrate_app.py', 'FemMigrateApp', 'class'),\n" + " ('FemToolsCcx', 'femtools/ccxtools.py', 'FemToolsCcx', 'class'),\n" + " ('FemToolsCcx.get_ccx_version', 'femtools/ccxtools.py', 'get_ccx_version', 'method'),\n" + " ('FemToolsCcx.has_no_material_assigned', 'femtools/ccxtools.py', 'has_no_material_assigned', 'method'),\n" + " ('FemToolsCcx.has_nonpositive_jacobians', 'femtools/ccxtools.py', 'has_nonpositive_jacobians', 'method'),\n" + " ('CcxTools', 'femtools/ccxtools.py', 'CcxTools', 'class'),\n" + "):\n" + " _capture_gmsh(_name, lambda _path=_path, _symbol=_symbol, _kind=_kind: _early_source_record_fixture(_path, _symbol, _kind))\n" + "for _name, _module, _class, _method in (\n" + " ('NetgenTools', 'femmesh.netgentools', 'NetgenTools', None),\n" + " ('BaseFemElement', 'femobjects.base_femelement', 'BaseFemElement', None),\n" + " ('Extractor', 'femobjects.base_fempostextractors', 'Extractor', None),\n" + " ('Extractor.get_vtk_table', 'femobjects.base_fempostextractors', 'Extractor', 'get_vtk_table'),\n" + " ('Extractor1D', 'femobjects.base_fempostextractors', 'Extractor1D', None),\n" + " ('Extractor2D', 'femobjects.base_fempostextractors', 'Extractor2D', None),\n" + " ('PostVisualization.getLongestColumnLength', 'femobjects.base_fempostvisualizations', 'PostVisualization', 'getLongestColumnLength'),\n" + " ('ConstantVacuumPermittivity', 'femobjects.constant_vacuumpermittivity', 'ConstantVacuumPermittivity', None),\n" + " ('ConstraintBodyHeatSource', 'femobjects.constraint_bodyheatsource', 'ConstraintBodyHeatSource', None),\n" + " ('ConstraintCentrif', 'femobjects.constraint_centrif', 'ConstraintCentrif', None),\n" + " ('ConstraintCurrentDensity', 'femobjects.constraint_currentdensity', 'ConstraintCurrentDensity', None),\n" + " ('ConstraintElectricChargeDensity', 'femobjects.constraint_electricchargedensity', 'ConstraintElectricChargeDensity', None),\n" + " ('ConstraintElectricChargeDensity.get_total_source_density', 'femobjects.constraint_electricchargedensity', 'ConstraintElectricChargeDensity', 'get_total_source_density'),\n" + "):\n" + " _capture_gmsh(_name, lambda _module=_module, _class=_class, _method=_method: _python_source_fixture(_module, _class, _method))\n" + "def _source_symbol_fixture(filename, symbol):\n" + " source_path = '/freecad-mod/Fem/femobjects/' + filename\n" + " text = open(source_path, encoding='utf-8').read()\n" + " marker = 'class ' + symbol + '('\n" + " start = text.find(marker)\n" + " if start < 0: raise RuntimeError('class declaration not found: ' + symbol)\n" + " end = text.find('\\n', start)\n" + " declaration = text[start:end if end >= 0 else len(text)].strip()\n" + " type_marker = ' Type = \\\"'\n" + " type_start = text.find(type_marker, start)\n" + " declared_type = None\n" + " if type_start >= 0:\n" + " type_end = text.find('\\\"', type_start + len(type_marker))\n" + " if type_end >= 0: declared_type = text[type_start + len(type_marker):type_end]\n" + " return {'sourcePath': 'src/Mod/Fem/femobjects/' + filename, 'className': symbol, 'declaration': declaration, 'declaredType': declared_type, 'sourceResolved': True}\n" + "def _class_fixture(filename, symbol):\n" + " result = _source_symbol_fixture(filename, symbol)\n" + " result.update({'runtimeResolved': False, 'runtimeError': 'Python class constructor fixture requires a dependency closure unavailable in the headless Web kernel'})\n" + " return result\n" + "def _source_record_fixture(relative_path, symbol, kind):\n" + " source_path = '/freecad-mod/Fem/' + relative_path\n" + " text = open(source_path, encoding='utf-8').read()\n" + " marker = ('class ' if kind == 'class' else 'def ') + symbol\n" + " start = text.find(marker)\n" + " if start < 0: raise RuntimeError('declaration not found: ' + symbol)\n" + " if kind == 'method' and not text.startswith('def ' + symbol + '(', start):\n" + " start = text.find('def ' + symbol + '(', start)\n" + " end = text.find('\\n', start)\n" + " declaration = text[start:end if end >= 0 else len(text)].strip()\n" + " return {'sourcePath': 'src/Mod/Fem/' + relative_path, 'symbol': symbol, 'kind': kind, 'declaration': declaration, 'sourceResolved': True, 'runtimeResolved': False, 'runtimeError': 'Python dependency closure is unavailable in the headless Web kernel'}\n" + "for _name, _path, _symbol, _kind in (\n" + " ('Writer.getDensity', 'femsolver/elmer/writer.py', 'getDensity', 'method'),\n" + " ('Writer.getAllBodies', 'femsolver/elmer/writer.py', 'getAllBodies', 'method'),\n" + " ('Writer.getMember', 'femsolver/elmer/writer.py', 'getMember', 'method'),\n" + " ('Writer.getSingleMember', 'femsolver/elmer/writer.py', 'getSingleMember', 'method'),\n" + " ('Check', 'femsolver/mystran/tasks.py', 'Check', 'class'),\n" + " ('Prepare', 'femsolver/mystran/tasks.py', 'Prepare', 'class'),\n" + " ('Solve', 'femsolver/mystran/tasks.py', 'Solve', 'class'),\n" + " ('Results', 'femsolver/mystran/tasks.py', 'Results', 'class'),\n" + " ('CellExpressionFromXDMF', 'femsolver/fenics/fenics_tools.py', 'CellExpressionFromXDMF', 'class'),\n" + " ('ScalarCellExpressionFromXDMF', 'femsolver/fenics/fenics_tools.py', 'ScalarCellExpressionFromXDMF', 'class'),\n" + " ('FacetFunctionFromXDMF.getDirichletBCs', 'femsolver/fenics/fenics_tools.py', 'getDirichletBCs', 'method'),\n" + "):\n" + " _capture_gmsh(_name, lambda _path=_path, _symbol=_symbol, _kind=_kind: _source_record_fixture(_path, _symbol, _kind))\n" + "for _name, _filename, _symbol in (\n" + " ('ConstraintElectromagnetic', 'constraint_electromagnetic.py', 'ConstraintElectromagnetic'),\n" + " ('ConstraintFlowVelocity', 'constraint_flowvelocity.py', 'ConstraintFlowVelocity'),\n" + " ('ConstraintMagnetization', 'constraint_magnetization.py', 'ConstraintMagnetization'),\n" + " ('ConstraintSectionPrint', 'constraint_sectionprint.py', 'ConstraintSectionPrint'),\n" + " ('ConstraintSelfWeight', 'constraint_selfweight.py', 'ConstraintSelfWeight'),\n" + " ('ConstraintTie', 'constraint_tie.py', 'ConstraintTie'),\n" + " ('ElementFluid1D', 'element_fluid1D.py', 'ElementFluid1D'),\n" + " ('ElementRotation1D', 'element_rotation1D.py', 'ElementRotation1D'),\n" + " ('MaterialCommon', 'material_common.py', 'MaterialCommon'),\n" + " ('MaterialMechanicalNonlinear', 'material_mechanicalnonlinear.py', 'MaterialMechanicalNonlinear'),\n" + " ('MaterialReinforced', 'material_reinforced.py', 'MaterialReinforced'),\n" + " ('PostIndexOverFrames1D', 'post_extract1D.py', 'PostIndexOverFrames1D'),\n" + " ('PostIndexOverFrames2D', 'post_extract2D.py', 'PostIndexOverFrames2D'),\n" + " ('PostGlyphFilter', 'post_glyphfilter.py', 'PostGlyphFilter'),\n" + " ('PostHistogramIndexOverFrames', 'post_histogram.py', 'PostHistogramIndexOverFrames'),\n" + " ('PostHistogram', 'post_histogram.py', 'PostHistogram'),\n" + " ('PostLineplotIndexOverFrames', 'post_lineplot.py', 'PostLineplotIndexOverFrames'),\n" + " ('PostLineplot', 'post_lineplot.py', 'PostLineplot'),\n" + " ('PostTableIndexOverFrames', 'post_table.py', 'PostTableIndexOverFrames'),\n" + " ('PostTable', 'post_table.py', 'PostTable'),\n" + " ('ResultMechanical', 'result_mechanical.py', 'ResultMechanical'),\n" + " ('SolverCalculiX', 'solver_calculix.py', 'SolverCalculiX'),\n" + " ('SolverCcxTools', 'solver_ccxtools.py', 'SolverCcxTools'),\n" + " ('SolverElmer', 'solver_elmer.py', 'SolverElmer'),\n" + "):\n" + " _capture_gmsh(_name, lambda _filename=_filename, _symbol=_symbol: _class_fixture(_filename, _symbol))\n" + "for _name, _path, _symbol, _kind in (\n" + " ('DlgSettingsNetgen', 'fempreferencepages/dlg_settings_netgen.py', 'DlgSettingsNetgen', 'class'),\n" + " ('CalculiXTools', 'femsolver/calculix/calculixtools.py', 'CalculiXTools', 'class'),\n" + " ('ElmerTools', 'femsolver/elmer/elmertools.py', 'ElmerTools', 'class'),\n" + " ('Proxy', 'femsolver/elmer/equations/deformation.py', 'Proxy', 'class'),\n" + " ('DeformationWriter.getDeformationSolver', 'femsolver/elmer/equations/deformation_writer.py', 'getDeformationSolver', 'method'),\n" + " ('ElasticityWriter.getElasticitySolver', 'femsolver/elmer/equations/elasticity_writer.py', 'getElasticitySolver', 'method'),\n" + " ('EFwriter.getElectricforceSolver', 'femsolver/elmer/equations/electricforce_writer.py', 'getElectricforceSolver', 'method'),\n" + " ('ESwriter.getElectrostaticSolver', 'femsolver/elmer/equations/electrostatic_writer.py', 'getElectrostaticSolver', 'method'),\n" + " ('Flowwriter.getFlowSolver', 'femsolver/elmer/equations/flow_writer.py', 'getFlowSolver', 'method'),\n" + " ('Fluxwriter.getFluxSolver', 'femsolver/elmer/equations/flux_writer.py', 'getFluxSolver', 'method'),\n" + " ('Heatwriter.getHeatSolver', 'femsolver/elmer/equations/heat_writer.py', 'getHeatSolver', 'method'),\n" + " ('MgDyn2Dwriter.getMagnetodynamic2DSolver', 'femsolver/elmer/equations/magnetodynamic2D_writer.py', 'getMagnetodynamic2DSolver', 'method'),\n" + " ('MgDyn2Dwriter.getMagnetodynamic2DSolverPost', 'femsolver/elmer/equations/magnetodynamic2D_writer.py', 'getMagnetodynamic2DSolverPost', 'method'),\n" + " ('MgDynwriter.getMagnetodynamicSolver', 'femsolver/elmer/equations/magnetodynamic_writer.py', 'getMagnetodynamicSolver', 'method'),\n" + " ('MgDynwriter.getMagnetodynamicSolverPost', 'femsolver/elmer/equations/magnetodynamic_writer.py', 'getMagnetodynamicSolverPost', 'method'),\n" + " ('SCwriter.getStaticCurrentSolver', 'femsolver/elmer/equations/staticcurrent_writer.py', 'getStaticCurrentSolver', 'method'),\n" + " ('Builder', 'femsolver/elmer/sifio.py', 'Builder', 'class'),\n" + " ('Sif', 'femsolver/elmer/sifio.py', 'Sif', 'class'),\n" + " ('Section', 'femsolver/elmer/sifio.py', 'Section', 'class'),\n" + " ('FileAttr', 'femsolver/elmer/sifio.py', 'FileAttr', 'class'),\n" + " ('Writer.getHandledConstraints', 'femsolver/elmer/writer.py', 'getHandledConstraints', 'method'),\n" + " ('Writer.getFromUi', 'femsolver/elmer/writer.py', 'getFromUi', 'method'),\n" + " ('Writer.isBodyMaterialFluid', 'femsolver/elmer/writer.py', 'isBodyMaterialFluid', 'method'),\n" + " ('Writer.getBodyMaterial', 'femsolver/elmer/writer.py', 'getBodyMaterial', 'method'),\n" + "):\n" + " _capture_gmsh(_name, lambda _path=_path, _symbol=_symbol, _kind=_kind: _source_record_fixture(_path, _symbol, _kind))\n" + "def _meshsets_fixture(method_name):\n" + " from femmesh.meshsetsgetter import MeshSetsGetter\n" + " class _Analysis: pass\n" + " class _Solver: AnalysisType = 'static'\n" + " class _MeshObj: pass\n" + " analysis = _Analysis(); analysis.Document = _doc\n" + " mesh_obj = _MeshObj(); mesh_obj.FemMesh = _mesh\n" + " member = type('Member', (), {})()\n" + " for _name in ('cons_fixed', 'cons_rigidbody', 'cons_displacement', 'cons_planerotation', 'cons_transform', 'cons_temperature', 'geos_fluidsection', 'geos_shellthickness', 'geos_beamsection', 'geos_beamrotation', 'cons_electrostatic', 'cons_electricchargedensity', 'cons_centrif', 'cons_bodyheatsource', 'mats_linear'):\n" + " setattr(member, _name, [])\n" + " getter = MeshSetsGetter(analysis, _Solver(), mesh_obj, member)\n" + " getattr(getter, method_name)()\n" + " return {'method': method_name, 'nodeSets': len(getter.femnodes_mesh), 'elementSets': len(getter.femelement_table), 'conflicts': len(getter.constraint_conflict_nodes)}\n" + "for _method_name in ('get_constraints_fixed_nodes', 'get_constraints_rigidbody_nodes', 'get_constraints_displacement_nodes', 'get_constraints_planerotation_nodes', 'get_constraints_transform_nodes', 'get_constraints_temperature_nodes', 'get_constraints_fluidsection_nodes', 'get_constraints_electrostatic_nodes', 'get_constraints_electricchargedensity_nodes', 'get_constraints_centrif_elements', 'get_constraints_bodyheatsource_elements', 'get_element_rotation1D_elements', 'get_element_fluid1D_elements', 'get_material_elements', 'get_mat_geo_sets_single_mat_single_beam', 'get_mat_geo_sets_single_mat_multiple_beam', 'get_mat_geo_sets_multiple_mat_single_beam', 'get_mat_geo_sets_multiple_mat_multiple_beam', 'get_mat_geo_sets_single_mat_single_fluid', 'get_mat_geo_sets_single_mat_multiple_fluid', 'get_mat_geo_sets_multiple_mat_single_fluid', 'get_mat_geo_sets_multiple_mat_multiple_fluid', 'get_mat_geo_sets_single_mat_single_shell', 'get_mat_geo_sets_single_mat_multiple_shell', 'get_mat_geo_sets_multiple_mat_single_shell', 'get_mat_geo_sets_multiple_mat_multiple_shell'):\n" + " _capture_gmsh('MeshSetsGetter.' + _method_name, lambda _name=_method_name: _meshsets_fixture(_name))\n" + "FreeCAD.closeDocument(_doc.Name)\n"; + PyObject* globals = PyDict_New(); + if (!globals) { + familyError = "Fem generic-object globals allocation failed"; + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* execution = PyRun_String(script, Py_file_input, globals, globals); + Py_XDECREF(execution); + if (PyErr_Occurred()) { + familyError = pythonError("Fem generic-object source execution failed"); + Py_DECREF(globals); + return false; + } + PyObject* payload = PyDict_GetItemString(globals, "_results"); + if (!payload || !PyDict_Check(payload)) { + familyError = "Fem generic-object source returned no result map"; + Py_DECREF(globals); + return false; + } + const QJsonValue json = pyToJson(payload); + Py_DECREF(globals); + if (!json.isObject()) { + familyError = "Fem generic-object result map is not an object"; + return false; + } + familyResultJson = QJsonDocument(json.toObject()).toJson(QJsonDocument::Compact).toStdString(); + return !familyResultJson.empty() && App::GetApplication().getDocuments().empty(); +} + +bool runFemDocumentObjectContractFamily() +{ + documentObjectContractResultJson.clear(); + familyError.clear(); + QJsonObject results; + App::Document* document = nullptr; + auto& application = App::GetApplication(); + const char* const objectTypes[] = { + "Fem::FemAnalysis", + "Fem::DocumentObject", + "Fem::Constraint", + "Fem::ConstraintBearing", + "Fem::ConstraintContact", + "Fem::ConstraintDisplacement", + "Fem::ConstraintFixed", + "Fem::ConstraintFluidBoundary", + "Fem::ConstraintForce", + "Fem::ConstraintGear", + "Fem::ConstraintHeatflux", + "Fem::ConstraintInitialTemperature", + "Fem::ConstraintPlaneRotation", + "Fem::ConstraintPressure", + "Fem::ConstraintPulley", + "Fem::ConstraintRigidBody", + "Fem::ConstraintSpring", + "Fem::ConstraintTemperature", + "Fem::ConstraintTransform", + "Fem::FemPostFilter", + "Fem::FemPostSmoothFilterExtension", + "Fem::FemPostDataAlongLineFilter", + "Fem::FemPostClipFilter", + "Fem::FemPostContoursFilter", + "Fem::FemPostCutFilter", + "Fem::FemPostScalarClipFilter", + "Fem::FemPostWarpVectorFilter", + "Fem::FemPostCalculatorFilter", + "Fem::FemPostFunction", + "Fem::FemPostFunctionProvider", + "Fem::FemPostPlaneFunction", + "Fem::FemPostObject", + "Fem::FemPostGroupExtension", + "Fem::FemResultObject", + "Fem::FemSetElementNodesObject", + "Fem::FemSetElementsObject", + "Fem::FemSetGeometryObject", + "Fem::FemSetNodesObject", + "Fem::FemSetObject", + "Fem::FemSolverObject", + "Fem::PropertyPostDataObject", + "Fem::WorkerExtension", + }; + try { + document = application.newDocument("FemDocumentObjectContract", "Fem Document Object Contract", {false, true}); + if (!document) { + familyError = "Fem document-object contract fixture document creation failed"; + return false; + } + std::vector> constructedObjects; + int index = 0; + for (const char* typeName : objectTypes) { + const std::string objectName = std::string("ObjectContract") + std::to_string(index++); + QJsonObject value; + value.insert(QStringLiteral("typeName"), QString::fromLatin1(typeName)); + const Base::Type type = Base::Type::fromName(typeName); + const bool registeredType = !type.isBad(); + const bool documentObjectType = registeredType + && type.isDerivedFrom(App::DocumentObject::getClassTypeId()); + value.insert(QStringLiteral("registered"), registeredType); + value.insert(QStringLiteral("documentObjectType"), documentObjectType); + if (!documentObjectType) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + } + else { + try { + auto* object = document->addObject(typeName, objectName.c_str()); + if (!object) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + } + else { + value.insert(QStringLiteral("objectType"), QString::fromLatin1(object->getTypeId().getName())); + value.insert(QStringLiteral("name"), QString::fromLatin1(object->getNameInDocument())); + value.insert(QStringLiteral("constructible"), true); + constructedObjects.emplace_back(typeName, object); + } + } + catch (const Base::Exception& ex) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QString::fromUtf8(ex.what())); + } + catch (const std::exception& ex) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QString::fromUtf8(ex.what())); + } + catch (...) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QStringLiteral("unknown native construction exception")); + } + } + results.insert(QString::fromLatin1(typeName), value); + } + for (const auto& [typeName, object] : constructedObjects) { + QJsonObject value = results.value(QString::fromStdString(typeName)).toObject(); + try { + bool hasError = false; + const int recomputeCount = document->recompute({object}, false, &hasError); + value.insert(QStringLiteral("recomputeCount"), recomputeCount); + value.insert(QStringLiteral("recomputed"), !hasError); + if (hasError && typeName == std::string("Fem::FemMeshShapeNetgenObject")) { + value.insert(QStringLiteral("dependencyUnavailable"), true); + value.insert(QStringLiteral("dependency"), QStringLiteral("NETGEN")); + value.insert(QStringLiteral("recomputeError"), QStringLiteral("The FEM module is built without NETGEN support")); + } + } + catch (const Base::Exception& ex) { + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("recomputeError"), QString::fromUtf8(ex.what())); + } + catch (const std::exception& ex) { + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("recomputeError"), QString::fromUtf8(ex.what())); + } + catch (...) { + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("recomputeError"), QStringLiteral("unknown native recompute exception")); + } + results.insert(QString::fromStdString(typeName), value); + } + application.closeDocument(document); + document = nullptr; + documentObjectContractResultJson = QJsonDocument(results).toJson(QJsonDocument::Compact).toStdString(); + return application.getDocuments().empty(); + } + catch (const Base::Exception& ex) { + familyError = ex.what(); + } + catch (const std::exception& ex) { + familyError = ex.what(); + } + catch (...) { + familyError = "Fem geometry-recompute fixture raised an unknown native exception"; + } + if (document) { + application.closeDocument(document); + } + return application.getDocuments().empty(); +} + +bool runFemGeometryRecomputeFamily() +{ + documentObjectContractResultJson.clear(); + familyError.clear(); + QJsonObject results; + App::Document* document = nullptr; + auto& application = App::GetApplication(); + const char* const objectTypes[] = { + "Fem::FemMesh", + "Fem::FemMeshObject", + "Fem::PropertyFemMesh", + "Fem::FemMeshShapeNetgenObject", + "Fem::FemMeshShapeBaseObject", + "Fem::FemMeshShapeObject", + "Fem::FemPostDataAtPointFilter", + "Fem::FemPostBoxFunction", + "Fem::FemPostCylinderFunction", + "Fem::FemPostSphereFunction", + "Fem::FemSetFacesObject", + }; + try { + document = application.newDocument("FemGeometryRecompute", "Fem Geometry Recompute", {false, true}); + if (!document) { + familyError = "Fem geometry-recompute fixture document creation failed"; + return false; + } + + // Shape-backed mesh objects require a real Part feature link. Keep the + // fixture deterministic and self-contained by using one native box. + auto* sourceShapeObject = document->addObject("Part::Feature", "GeometrySource"); + auto* sourceShape = dynamic_cast(sourceShapeObject); + if (!sourceShape) { + familyError = "Fem geometry-recompute fixture source shape creation failed"; + application.closeDocument(document); + document = nullptr; + return false; + } + sourceShape->Shape.setValue(BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape()); + + std::vector> constructedObjects; + int index = 0; + for (const char* typeName : objectTypes) { + const std::string objectName = std::string("GeometryContract") + std::to_string(index++); + QJsonObject value; + value.insert(QStringLiteral("typeName"), QString::fromLatin1(typeName)); + const Base::Type type = Base::Type::fromName(typeName); + const bool registeredType = !type.isBad(); + const bool documentObjectType = registeredType + && type.isDerivedFrom(App::DocumentObject::getClassTypeId()); + value.insert(QStringLiteral("registered"), registeredType); + value.insert(QStringLiteral("documentObjectType"), documentObjectType); + const bool shapeDependent = std::string(typeName).find("FemMeshShape") != std::string::npos; + if (!documentObjectType) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + } + else { + try { + auto* object = document->addObject(typeName, objectName.c_str()); + if (!object) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + } + else { + if (shapeDependent) { + auto* shape = dynamic_cast(object->getPropertyByName("Shape")); + if (!shape) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QStringLiteral("Shape property link is unavailable")); + results.insert(QString::fromLatin1(typeName), value); + continue; + } + shape->setValue(sourceShape); + } + value.insert(QStringLiteral("objectType"), QString::fromLatin1(object->getTypeId().getName())); + value.insert(QStringLiteral("name"), QString::fromLatin1(object->getNameInDocument())); + value.insert(QStringLiteral("constructible"), true); + constructedObjects.emplace_back(typeName, object); + } + } + catch (const Base::Exception& ex) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QString::fromUtf8(ex.what())); + } + catch (const std::exception& ex) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QString::fromUtf8(ex.what())); + } + catch (...) { + value.insert(QStringLiteral("constructible"), false); + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("constructionError"), QStringLiteral("unknown native construction exception")); + } + } + results.insert(QString::fromLatin1(typeName), value); + } + for (const auto& [typeName, object] : constructedObjects) { + QJsonObject value = results.value(QString::fromStdString(typeName)).toObject(); + try { + bool hasError = false; + const int recomputeCount = document->recompute({object}, false, &hasError); + value.insert(QStringLiteral("recomputeCount"), recomputeCount); + value.insert(QStringLiteral("recomputed"), !hasError); + if (hasError && typeName == std::string("Fem::FemMeshShapeNetgenObject")) { + value.insert(QStringLiteral("dependencyUnavailable"), true); + value.insert(QStringLiteral("dependency"), QStringLiteral("NETGEN")); + value.insert(QStringLiteral("recomputeError"), QStringLiteral("The FEM module is built without NETGEN support")); + } + } + catch (const Base::Exception& ex) { + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("recomputeError"), QString::fromUtf8(ex.what())); + } + catch (const std::exception& ex) { + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("recomputeError"), QString::fromUtf8(ex.what())); + } + catch (...) { + value.insert(QStringLiteral("recomputed"), false); + value.insert(QStringLiteral("recomputeError"), QStringLiteral("unknown native recompute exception")); + } + results.insert(QString::fromStdString(typeName), value); + } + application.closeDocument(document); + document = nullptr; + documentObjectContractResultJson = QJsonDocument(results).toJson(QJsonDocument::Compact).toStdString(); + return application.getDocuments().empty(); + } + catch (const Base::Exception& ex) { + familyError = ex.what(); + } + catch (const std::exception& ex) { + familyError = ex.what(); + } + catch (...) { + familyError = "Fem geometry-recompute fixture raised an unknown native exception"; + } + if (document) { + application.closeDocument(document); + } + return application.getDocuments().empty(); +} + +bool runFemViewResourceFamily() +{ + familyResultJson.clear(); + familyError.clear(); + const char* script = R"PY( +import FreeCAD +import sys + +sys.path.insert(0, '/freecad-mod/Fem') +_results = {} + +def _capture(name, call): + try: + _results[name] = call() + except Exception as exc: + _results[name] = {'__error__': type(exc).__name__ + ': ' + str(exc)} + +def _capture_raised(name, call): + try: + _results[name] = call() + except Exception as exc: + _results[name] = {'raised': type(exc).__name__, 'message': str(exc)} + +def _load_post_visualization(): + # QtCore belongs to adjacent command classes; the registry itself is App-safe. + qtcore = type(sys)('PySide.QtCore') + pyside = type(sys)('PySide') + pyside.QtCore = qtcore + sys.modules['PySide'] = pyside + sys.modules['PySide.QtCore'] = qtcore + source_path = '/freecad-mod/Fem/femguiutils/post_visualization.py' + module = type(sys)('femguiutils.post_visualization') + module.__file__ = source_path + sys.modules[module.__name__] = module + namespace = module.__dict__ + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + return namespace + +def _registry_fixture(): + namespace = _load_post_visualization() + register = namespace['register_visualization'] + listed = namespace['get_registered_visualizations'] + register('NativeView', 'FemNative.svg', 'FemNative', 'create') + entry = listed()['NativeView'] + return {'registrySize': len(listed()), 'visualizationType': entry.name, 'module': entry.module, 'factory': entry.factory} + +def _base_view_fixture(): + FreeCAD.GuiUp = False + source_path = '/freecad-mod/Fem/femsolver/equationbase.py' + namespace = {'__name__': 'femsolver.equationbase', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + base_view_proxy = namespace['BaseViewProxy'] + class ViewObject: + Proxy = None + view_object = ViewObject() + proxy = base_view_proxy(view_object) + return {'className': type(proxy).__name__, 'proxyAssigned': view_object.Proxy is proxy, 'displayModes': proxy.getDisplayModes(view_object), 'defaultDisplayMode': proxy.getDefaultDisplayMode(), 'setDisplayMode': proxy.setDisplayMode('Default')} + +def _equation_view_fixture(class_name): + FreeCAD.GuiUp = False + source_path = '/freecad-mod/Fem/femsolver/equationbase.py' + namespace = {'__name__': 'femsolver.equationbase', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + class ViewObject: + Proxy = None + view_object = ViewObject() + proxy = namespace[class_name](view_object) + return {'className': type(proxy).__name__, 'proxyAssigned': view_object.Proxy is proxy, 'icon': proxy.getIcon()} + +def _report_display_fixture(): + source_path = '/freecad-mod/Fem/femsolver/report.py' + namespace = {'__name__': 'femsolver.report', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + report = namespace['Report']() + report.info('native fem report') + namespace['display'](report, 'Native report') + return {'level': report.getLevel(), 'isEmpty': report.isEmpty(), 'infoCount': len(report.infos), 'guiUp': FreeCAD.GuiUp} + +def _display_log_fixture(): + source_path = '/freecad-mod/Fem/femsolver/report.py' + namespace = {'__name__': 'femsolver.report', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + report = namespace['Report']() + report.info('native fem info') + report.warning('native fem warning') + report.error('native fem error') + namespace['displayLog'](report) + return {'infoCount': len(report.infos), 'warningCount': len(report.warnings), 'errorCount': len(report.errors), 'level': report.getLevel()} + +def _post_extractor_fixture(method): + pyside = type(sys)('PySide') + pyside.QtGui = type(sys)('PySide.QtGui') + sys.modules['PySide'] = pyside + sys.modules['PySide.QtGui'] = pyside.QtGui + gui = type(sys)('FreeCADGui') + sys.modules['FreeCADGui'] = gui + sys.modules['femtaskpanels'] = type(sys)('femtaskpanels') + sys.modules['femtaskpanels.task_post_extractor'] = type(sys)('femtaskpanels.task_post_extractor') + source_path = '/freecad-mod/Fem/femviewprovider/view_base_fempostextractors.py' + namespace = {'__name__': 'femviewprovider.view_base_fempostextractors', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + class ViewObject: + Proxy = None + PropertiesList = [] + view_object = ViewObject() + proxy = namespace['VPPostExtractor'](view_object) + try: + call = getattr(proxy, method) + if method == 'get_view_edit_widget': + call(None) + else: + call() + except Exception as exc: + return {'className': type(proxy).__name__, 'raised': type(exc).__name__, 'message': str(exc)} + return {'className': type(proxy).__name__, 'raised': None} + +def _post_visualization_fixture(method): + gui = type(sys)('FreeCADGui') + sys.modules['FreeCADGui'] = gui + source_path = '/freecad-mod/Fem/femviewprovider/view_base_fempostvisualization.py' + namespace = {'__name__': 'femviewprovider.view_base_fempostvisualization', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + class ViewObject: + Proxy = None + PropertiesList = [] + def addExtension(self, extension): + self.extension = extension + if method == 'class': + proxy = namespace['VPPostVisualization'](ViewObject()) + return {'className': type(proxy).__name__, 'displayModes': proxy.getDisplayModes(None)} + if method == 'getDisplayModes': + proxy = namespace['VPPostVisualization'](ViewObject()) + return {'displayModes': proxy.getDisplayModes(None)} + if method == 'childViewPropertyChanged': + class Derived(namespace['VPPostVisualization']): + def update_visualization(self): + self.updates += 1 + proxy = Derived(ViewObject()) + proxy.updates = 0 + proxy.childViewPropertyChanged(None, 'Legend') + return {'updates': proxy.updates} + proxy = namespace['VPPostVisualization'](ViewObject()) + _capture_raised('_abstract', getattr(proxy, method)) + return _results.pop('_abstract') + +def _glyph_filter_fixture(method): + pyside = type(sys)('PySide') + pyside.QtGui = type(sys)('PySide.QtGui') + sys.modules['PySide'] = pyside + sys.modules['PySide.QtGui'] = pyside.QtGui + sys.modules['FemGui'] = type(sys)('FemGui') + task = type(sys)('femtaskpanels.task_post_glyphfilter') + sys.modules['femtaskpanels'] = type(sys)('femtaskpanels') + sys.modules['femtaskpanels.task_post_glyphfilter'] = task + source_path = '/freecad-mod/Fem/femviewprovider/view_post_glyphfilter.py' + namespace = {'__name__': 'femviewprovider.view_post_glyphfilter', '__file__': source_path} + with open(source_path, encoding='utf-8') as handle: + exec(compile(handle.read(), source_path, 'exec'), namespace, namespace) + class ViewObject: + Proxy = None + proxy = namespace['VPPostGlyphFilter'](ViewObject()) + if method == 'getIcon': + return {'icon': proxy.getIcon()} + if method == 'getDisplayModes': + return {'displayModes': proxy.getDisplayModes(None)} + return {'displayMode': proxy.setDisplayMode('Wireframe')} + +_capture('register_visualization', _registry_fixture) +_capture('get_registered_visualizations', _registry_fixture) +_capture('BaseViewProxy', _base_view_fixture) +_capture('BaseViewProxy.getDisplayModes', _base_view_fixture) +_capture('BaseViewProxy.getDefaultDisplayMode', _base_view_fixture) +_capture('BaseViewProxy.setDisplayMode', _base_view_fixture) +for _class_name in ('DeformationViewProxy', 'ElasticityViewProxy', 'ElectricforceViewProxy', 'ElectrostaticViewProxy', 'FlowViewProxy', 'FluxViewProxy', 'HeatViewProxy', 'MagnetodynamicViewProxy', 'Magnetodynamic2DViewProxy', 'StaticCurrentViewProxy'): + _capture(_class_name, lambda _name=_class_name: _equation_view_fixture(_name)) +for _class_name in ('DeformationViewProxy', 'ElasticityViewProxy', 'ElectricforceViewProxy', 'ElectrostaticViewProxy', 'FlowViewProxy', 'FluxViewProxy', 'HeatViewProxy', 'MagnetodynamicViewProxy', 'Magnetodynamic2DViewProxy', 'StaticCurrentViewProxy'): + _capture(_class_name + '.getIcon', lambda _name=_class_name: _equation_view_fixture(_name)) +_capture('display', _report_display_fixture) +_capture('displayLog', _display_log_fixture) +_capture('VPPostExtractor.get_view_edit_widget', lambda: _post_extractor_fixture('get_view_edit_widget')) +_capture('VPPostExtractor.get_preview', lambda: _post_extractor_fixture('get_preview')) +_capture('VPPostVisualization', lambda: _post_visualization_fixture('class')) +_capture('VPPostVisualization.getDisplayModes', lambda: _post_visualization_fixture('getDisplayModes')) +_capture('VPPostVisualization.childViewPropertyChanged', lambda: _post_visualization_fixture('childViewPropertyChanged')) +_capture('VPPostVisualization.update_visualization', lambda: _post_visualization_fixture('update_visualization')) +_capture('VPPostVisualization.show_visualization', lambda: _post_visualization_fixture('show_visualization')) +_capture('VPPostGlyphFilter.getIcon', lambda: _glyph_filter_fixture('getIcon')) +_capture('VPPostGlyphFilter.getDisplayModes', lambda: _glyph_filter_fixture('getDisplayModes')) +_capture('VPPostGlyphFilter.setDisplayMode', lambda: _glyph_filter_fixture('setDisplayMode')) +)PY"; + PyObject* globals = PyDict_New(); + if (!globals) { + familyError = "Fem view-resource globals allocation failed"; + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* execution = PyRun_String(script, Py_file_input, globals, globals); + Py_XDECREF(execution); + if (PyErr_Occurred()) { + familyError = pythonError("Fem view-resource source execution failed"); + Py_DECREF(globals); + return false; + } + PyObject* payload = PyDict_GetItemString(globals, "_results"); + if (!payload || !PyDict_Check(payload)) { + familyError = "Fem view-resource source returned no result map"; + Py_DECREF(globals); + return false; + } + const QJsonValue json = pyToJson(payload); + Py_DECREF(globals); + if (!json.isObject()) { + familyError = "Fem view-resource result map is not an object"; + return false; + } + familyResultJson = QJsonDocument(json.toObject()).toJson(QJsonDocument::Compact).toStdString(); + return !familyResultJson.empty() && App::GetApplication().getDocuments().empty(); +} + +bool isFemViewResourceFixture(const QString& name) +{ + return name == QStringLiteral("displayLog") + || name == QStringLiteral("VPPostExtractor.get_view_edit_widget") + || name == QStringLiteral("VPPostExtractor.get_preview") + || name == QStringLiteral("VPPostVisualization") + || name == QStringLiteral("VPPostVisualization.getDisplayModes") + || name == QStringLiteral("VPPostVisualization.childViewPropertyChanged") + || name == QStringLiteral("VPPostVisualization.update_visualization") + || name == QStringLiteral("VPPostVisualization.show_visualization") + || name == QStringLiteral("VPPostGlyphFilter.getIcon") + || name == QStringLiteral("VPPostGlyphFilter.getDisplayModes") + || name == QStringLiteral("VPPostGlyphFilter.setDisplayMode") + || name == QStringLiteral("register_visualization") + || name == QStringLiteral("get_registered_visualizations") + || name == QStringLiteral("BaseViewProxy") + || name == QStringLiteral("BaseViewProxy.getDisplayModes") + || name == QStringLiteral("BaseViewProxy.getDefaultDisplayMode") + || name == QStringLiteral("BaseViewProxy.setDisplayMode") + || name == QStringLiteral("DeformationViewProxy") + || name == QStringLiteral("DeformationViewProxy.getIcon") + || name == QStringLiteral("ElasticityViewProxy") + || name == QStringLiteral("ElasticityViewProxy.getIcon") + || name == QStringLiteral("ElectricforceViewProxy") + || name == QStringLiteral("ElectricforceViewProxy.getIcon") + || name == QStringLiteral("ElectrostaticViewProxy") + || name == QStringLiteral("ElectrostaticViewProxy.getIcon") + || name == QStringLiteral("FlowViewProxy") + || name == QStringLiteral("FlowViewProxy.getIcon") + || name == QStringLiteral("FluxViewProxy") + || name == QStringLiteral("FluxViewProxy.getIcon") + || name == QStringLiteral("HeatViewProxy") + || name == QStringLiteral("HeatViewProxy.getIcon") + || name == QStringLiteral("MagnetodynamicViewProxy") + || name == QStringLiteral("MagnetodynamicViewProxy.getIcon") + || name == QStringLiteral("Magnetodynamic2DViewProxy") + || name == QStringLiteral("Magnetodynamic2DViewProxy.getIcon") + || name == QStringLiteral("StaticCurrentViewProxy") + || name == QStringLiteral("StaticCurrentViewProxy.getIcon") + || name == QStringLiteral("display"); +} + +bool isFemViewResourceVtkBoundary(const QString& name) +{ + return name == QStringLiteral("is_visualization_object") + || name == QStringLiteral("get_visualization_type") + || name == QStringLiteral("is_visualization_extractor_type") + || name == QStringLiteral("PostVisualization") + || name == QStringLiteral("VPPostHistogram.show_visualization") + || name == QStringLiteral("VPPostHistogram.update_visualization") + || name == QStringLiteral("VPPostLineplot.show_visualization") + || name == QStringLiteral("VPPostLineplot.update_visualization") + || name == QStringLiteral("VPPostTable.show_visualization") + || name == QStringLiteral("VPPostTable.update_visualization"); +} + +bool isFemViewResourcePythonRuntimeBoundary(const QString& name) +{ + return name == QStringLiteral("VPConstantVacuumPermittivity.getIcon") + || name == QStringLiteral("VPMaterialCommon.getIcon") + || name == QStringLiteral("VPMeshGmsh.getIcon") + || name == QStringLiteral("VPMeshNetgen.getIcon") + || name == QStringLiteral("VPPostHistogramFieldData.getIcon") + || name == QStringLiteral("VPPostHistogramFieldData.get_view_edit_widget") + || name == QStringLiteral("VPPostHistogramFieldData.get_preview") + || name == QStringLiteral("VPPostHistogramIndexOverFrames.getIcon") + || name == QStringLiteral("VPPostHistogram.getIcon"); +} + +bool isFemViewResourceGuiBoundary(const QString& name) +{ + return name == QStringLiteral("VPBaseFemObject.getIcon") + || name == QStringLiteral("EditViewWidget") + || name == QStringLiteral("createLabel.display") + || name == QStringLiteral("ViewProviderFemConstraint") + || name == QStringLiteral("ViewProviderFemMesh") + || name == QStringLiteral("ViewProviderFemPostFilter") + || name == QStringLiteral("ViewProviderFemPostFilter.createDisplayTaskWidget") + || name == QStringLiteral("ViewProviderFemPostPipeline") + || name == QStringLiteral("CommandManager.GetResources") + || name == QStringLiteral("build_new_visualization_tree_model") + || name == QStringLiteral("build_add_to_visualization_tree_model") + || name == QStringLiteral("ExtractLinkView") + || name == QStringLiteral("ExtractLinkView.newVisualization") + || name == QStringLiteral("ExtractLinkView.addExtractionToVisualization") + || name == QStringLiteral("SmallListView") + || name == QStringLiteral("VtkTableView") + || name == QStringLiteral("ViewProxy") + || name == QStringLiteral("ViewProxy.getIcon") + || name == QStringLiteral("displayGui") + // These view providers import FreeCADGui/Qt/task panels even when the + // requested method only returns an icon or preview value. + || name == QStringLiteral("VPPostLineplotFieldData.getIcon") + || name == QStringLiteral("VPPostLineplotFieldData.get_view_edit_widget") + || name == QStringLiteral("VPPostLineplotFieldData.get_preview") + || name == QStringLiteral("VPPostLineplotIndexOverFrames.getIcon") + || name == QStringLiteral("VPPostLineplot.getIcon") + || name == QStringLiteral("VPPostTableFieldData.getIcon") + || name == QStringLiteral("VPPostTableFieldData.get_view_edit_widget") + || name == QStringLiteral("VPPostTableFieldData.get_preview") + || name == QStringLiteral("VPPostTableIndexOverFrames.getIcon") + || name == QStringLiteral("VPPostTable.getIcon") + || name == QStringLiteral("VPSolverCalculiX.getIcon") + || name == QStringLiteral("VPSolverCcxTools.getIcon") + || name == QStringLiteral("VPSolverElmer.getIcon"); +} + +bool isFemGeometryFixture(const QString& name) +{ + return name == QStringLiteral("FemMesh") + || name == QStringLiteral("FemMesh.getFacesByFace") + || name == QStringLiteral("FemMesh.getEdgesByEdge") + || name == QStringLiteral("FemMesh.getVolumesByFace") + || name == QStringLiteral("FemMesh.getccxVolumesByFace") + || name == QStringLiteral("FemMesh.getNodesBySolid") + || name == QStringLiteral("FemMesh.getNodesByFace") + || name == QStringLiteral("FemMesh.getNodesByEdge"); +} + +bool isFemGenericObjectFixture(const QString& name) +{ + return name == QStringLiteral("Report.isEmpty") + || name == QStringLiteral("DirSetting") + || name == QStringLiteral("DirectoryDoesNotExistError") + || name == QStringLiteral("FemMesh.getNodeById") + || name == QStringLiteral("FemMesh.getNodesByVertex") + || name == QStringLiteral("FemMesh.getElementNodes") + || name == QStringLiteral("FemMesh.getNodeElements") + || name == QStringLiteral("FemInputWriter.get_constraints_fixed_nodes") + || name == QStringLiteral("FemInputWriter.get_constraints_displacement_nodes") + || name == QStringLiteral("FemInputWriter.get_constraints_planerotation_nodes") + || name == QStringLiteral("FemInputWriter.get_constraints_transform_nodes") + || name == QStringLiteral("FemInputWriter.get_constraints_temperature_nodes") + || name == QStringLiteral("FemInputWriter.get_constraints_fluidsection_nodes") + || name == QStringLiteral("FemPostBranchFilter") + || name == QStringLiteral("FemPostBranchFilter.getFilter") + || name == QStringLiteral("FemPostFilter") + || name == QStringLiteral("FemPostFilter.getInputData") + || name == QStringLiteral("FemPostFilter.getOutputAlgorithm") + || name == QStringLiteral("FemPostObject.getDataSet") + || name == QStringLiteral("FemPostPipeline") + || name == QStringLiteral("FemPostPipeline.getFilter") + || name == QStringLiteral("FemPostPipeline.getOutputAlgorithm") + || name == QStringLiteral("GmshError") + || name == QStringLiteral("GmshTools") + || name == QStringLiteral("GmshTools.get_dimension") + || name == QStringLiteral("GmshTools.get_tmp_file_paths") + || name == QStringLiteral("GmshTools.get_region_data") + || name == QStringLiteral("GmshTools.get_boundary_layer_data") + || name == QStringLiteral("MeshSetsGetter.get_constraints_fixed_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_rigidbody_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_displacement_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_planerotation_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_transform_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_temperature_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_fluidsection_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_electrostatic_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_electricchargedensity_nodes") + || name == QStringLiteral("MeshSetsGetter.get_constraints_centrif_elements") + || name == QStringLiteral("MeshSetsGetter.get_constraints_bodyheatsource_elements") + || name == QStringLiteral("MeshSetsGetter.get_element_rotation1D_elements") + || name == QStringLiteral("MeshSetsGetter.get_element_fluid1D_elements") + || name == QStringLiteral("MeshSetsGetter.get_material_elements") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_single_mat_single_beam") + || name == QStringLiteral("BaseProxy") + || name == QStringLiteral("DeformationProxy") + || name == QStringLiteral("ElasticityProxy") + || name == QStringLiteral("ElectricforceProxy") + || name == QStringLiteral("ElectrostaticProxy") + || name == QStringLiteral("FlowProxy") + || name == QStringLiteral("FluxProxy") + || name == QStringLiteral("HeatProxy") + || name == QStringLiteral("MagnetodynamicProxy") + || name == QStringLiteral("Magnetodynamic2DProxy") + || name == QStringLiteral("StaticCurrentProxy") + || name == QStringLiteral("Report") + || name == QStringLiteral("Report.getLevel"); +} + +bool isFemGenericObjectSourceProbe(const QString& name) +{ + return name == QStringLiteral("Machine") + || name == QStringLiteral("Check.get_several_member") + || name == QStringLiteral("Proxy.isSupported") + || name == QStringLiteral("AnalysisMember") + || name == QStringLiteral("AnalysisMember.get_several_member") + || name == QStringLiteral("FemMigrateApp") + || name == QStringLiteral("CalculiXTools") + || name == QStringLiteral("ElmerTools") + || name == QStringLiteral("Proxy") + || name == QStringLiteral("DeformationWriter.getDeformationSolver") + || name == QStringLiteral("ElasticityWriter.getElasticitySolver") + || name == QStringLiteral("EFwriter.getElectricforceSolver") + || name == QStringLiteral("ESwriter.getElectrostaticSolver") + || name == QStringLiteral("Flowwriter.getFlowSolver") + || name == QStringLiteral("Fluxwriter.getFluxSolver") + || name == QStringLiteral("Heatwriter.getHeatSolver") + || name == QStringLiteral("MgDyn2Dwriter.getMagnetodynamic2DSolver") + || name == QStringLiteral("MgDyn2Dwriter.getMagnetodynamic2DSolverPost") + || name == QStringLiteral("MgDynwriter.getMagnetodynamicSolver") + || name == QStringLiteral("MgDynwriter.getMagnetodynamicSolverPost") + || name == QStringLiteral("SCwriter.getStaticCurrentSolver") + || name == QStringLiteral("Builder") + || name == QStringLiteral("Sif") + || name == QStringLiteral("Section") + || name == QStringLiteral("FileAttr") + || name == QStringLiteral("Writer.getHandledConstraints") + || name == QStringLiteral("Writer.getFromUi") + || name == QStringLiteral("Writer.isBodyMaterialFluid") + || name == QStringLiteral("Writer.getBodyMaterial") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_single_mat_multiple_beam") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_multiple_mat_single_beam") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_multiple_mat_multiple_beam") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_single_mat_single_fluid") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_single_mat_multiple_fluid") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_multiple_mat_single_fluid") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_multiple_mat_multiple_fluid") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_single_mat_single_shell") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_single_mat_multiple_shell") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_multiple_mat_single_shell") + || name == QStringLiteral("MeshSetsGetter.get_mat_geo_sets_multiple_mat_multiple_shell") + || name == QStringLiteral("NetgenTools") + || name == QStringLiteral("BaseFemElement") + || name == QStringLiteral("Extractor") + || name == QStringLiteral("Extractor.get_vtk_table") + || name == QStringLiteral("Extractor1D") + || name == QStringLiteral("Extractor2D") + || name == QStringLiteral("PostVisualization.getLongestColumnLength") + || name == QStringLiteral("ConstantVacuumPermittivity") + || name == QStringLiteral("ConstraintBodyHeatSource") + || name == QStringLiteral("ConstraintCentrif") + || name == QStringLiteral("ConstraintCurrentDensity") + || name == QStringLiteral("ConstraintElectricChargeDensity") + || name == QStringLiteral("ConstraintElectricChargeDensity.get_total_source_density") + || name == QStringLiteral("Writer.getDensity") + || name == QStringLiteral("Writer.getAllBodies") + || name == QStringLiteral("Writer.getMember") + || name == QStringLiteral("Writer.getSingleMember") + || name == QStringLiteral("Check") + || name == QStringLiteral("Prepare") + || name == QStringLiteral("Solve") + || name == QStringLiteral("Results"); +} + +bool isFemExternalSolverBoundary(const QString& name) +{ + return name == QStringLiteral("FemToolsCcx") + || name == QStringLiteral("FemToolsCcx.get_ccx_version") + || name == QStringLiteral("FemToolsCcx.has_no_material_assigned") + || name == QStringLiteral("FemToolsCcx.has_nonpositive_jacobians") + || name == QStringLiteral("CcxTools"); +} + +bool isFemSelectedClassSourceProbe(const QString& name) +{ + return name == QStringLiteral("ConstraintElectromagnetic") + || name == QStringLiteral("ConstraintFlowVelocity") + || name == QStringLiteral("ConstraintMagnetization") + || name == QStringLiteral("ConstraintSectionPrint") + || name == QStringLiteral("ConstraintSelfWeight") + || name == QStringLiteral("ConstraintTie") + || name == QStringLiteral("ElementFluid1D") + || name == QStringLiteral("ElementRotation1D") + || name == QStringLiteral("MaterialCommon") + || name == QStringLiteral("MaterialMechanicalNonlinear") + || name == QStringLiteral("MaterialReinforced") + || name == QStringLiteral("PostIndexOverFrames1D") + || name == QStringLiteral("PostIndexOverFrames2D") + || name == QStringLiteral("PostGlyphFilter") + || name == QStringLiteral("PostHistogramIndexOverFrames") + || name == QStringLiteral("PostHistogram") + || name == QStringLiteral("PostLineplotIndexOverFrames") + || name == QStringLiteral("PostLineplot") + || name == QStringLiteral("PostTableIndexOverFrames") + || name == QStringLiteral("PostTable") + || name == QStringLiteral("ResultMechanical") + || name == QStringLiteral("SolverCalculiX") + || name == QStringLiteral("SolverCcxTools") + || name == QStringLiteral("SolverElmer"); +} + +bool isFemGenericObjectGuiBoundary(const QString& name) +{ + return name == QStringLiteral("ReportDialog") + || name == QStringLiteral("ControlTaskPanel.getStandardButtons") + || name == QStringLiteral("ControlWidget") + || name == QStringLiteral("VPBaseFemConstraint") + || name == QStringLiteral("DlgSettingsNetgen") + || name == QStringLiteral("createLabel.get_position") + || name == QStringLiteral("FemWorkbench") + || name == QStringLiteral("CommandManager.IsActive") + || name == QStringLiteral("FemExamples") + || name == QStringLiteral("HighlightContext") + || name == QStringLiteral("FemMigrateGui") + || name == QStringLiteral("BoundarySelector") + || name == QStringLiteral("GeometryElementsSelection.get_references") + || name == QStringLiteral("GeometryElementsSelection.get_item_text") + || name == QStringLiteral("GeometryElementsSelection.get_allitems_text") + || name == QStringLiteral("VtkTableModel") + || name == QStringLiteral("VtkTableModel.getTable") + || name == QStringLiteral("VtkTableSummaryModel") + || name == QStringLiteral("VtkTableSummaryModel.getTable") + || name == QStringLiteral("VPSolverCalculiX") + || name == QStringLiteral("VPSolverCcxTools") + || name == QStringLiteral("VPSolverElmer"); +} + +bool isFemObjectFixture(const QString& name) +{ + return name == QStringLiteral("NetgenTools.get_mesh_region") + || name == QStringLiteral("BaseFemMeshElement") + || name == QStringLiteral("ConstraintElectricChargeDensity.get_total_interface_density") + || name == QStringLiteral("ElementGeometry1D") + || name == QStringLiteral("ElementGeometry2D") + || name == QStringLiteral("MeshBoundaryLayer") + || name == QStringLiteral("MeshGmsh") + || name == QStringLiteral("MeshNetgen") + || name == QStringLiteral("MeshRegion") + || name == QStringLiteral("MeshResult") + || name == QStringLiteral("Writer.getMeshDimension") + || name == QStringLiteral("FemInputWriter.get_constraints_pressure_faces") + || name == QStringLiteral("FemInputWriter.get_constraints_contact_faces") + || name == QStringLiteral("FemInputWriter.get_constraints_tie_faces") + || name == QStringLiteral("FemInputWriter.get_constraints_heatflux_faces"); +} + +bool isFemExternalBoundary(const QString& name) +{ + return name == QStringLiteral("Vector3DCellExpressionFromXDMF") + || name == QStringLiteral("Vector2DCellExpressionFromXDMF") + || name == QStringLiteral("FacetFunctionFromXDMF") + || name == QStringLiteral("CellExpressionFromXDMF") + || name == QStringLiteral("ScalarCellExpressionFromXDMF") + || name == QStringLiteral("FacetFunctionFromXDMF.getDirichletBCs"); +} + +bool isFemGuiBoundary(const QString& name) +{ + return name == QStringLiteral("VPBaseFemElement") + || name == QStringLiteral("VPBaseFemElement.isShow") + || name == QStringLiteral("VPBaseFemMaterial") + || name == QStringLiteral("VPBaseFemMaterial.isShow") + || name == QStringLiteral("VPBaseFemMeshElement") + || name == QStringLiteral("VPBaseFemMeshElement.isShow") + || name == QStringLiteral("VPPostExtractor") + || name == QStringLiteral("VPPostExtractor.isShow") + || name == QStringLiteral("VPPostExtractor.get_kw_args") + || name == QStringLiteral("VPPostExtractor.get_app_edit_widget") + || name == QStringLiteral("VPPostVisualization.isShow") + || name == QStringLiteral("VPPostVisualization.get_next_default_color") + || name == QStringLiteral("VPConstantVacuumPermittivity") + || name == QStringLiteral("VPConstraintBodyHeatSource") + || name == QStringLiteral("VPConstraintCentrif") + || name == QStringLiteral("VPConstraintCurrentDensity") + || name == QStringLiteral("VPConstraintElectricChargeDensity") + || name == QStringLiteral("VPConstraintElectromagnetic") + || name == QStringLiteral("VPConstraintFlowVelocity") + || name == QStringLiteral("VPConstraintMagnetization") + || name == QStringLiteral("VPConstraintSectionPrint") + || name == QStringLiteral("VPConstraintSelfWeight") + || name == QStringLiteral("VPConstraintTie") + || name == QStringLiteral("VPElementFluid1D") + || name == QStringLiteral("VPElementRotation1D") + || name == QStringLiteral("VPElementGeometry1D") + || name == QStringLiteral("VPElementGeometry2D") + || name == QStringLiteral("VPMeshBoundaryLayer") + || name == QStringLiteral("VPMeshGmsh") + || name == QStringLiteral("VPMeshNetgen") + || name == QStringLiteral("VPMeshRegion") + || name == QStringLiteral("VPFemMeshResult") + // These view-provider classes and editor methods require FreeCADGui/Qt. + || name == QStringLiteral("VPMaterialCommon") + || name == QStringLiteral("VPMaterialMechanicalNonlinear") + || name == QStringLiteral("VPMaterialReinforced") + || name == QStringLiteral("VPPostGlyphFilter") + || name == QStringLiteral("EditIndexAppWidget") + || name == QStringLiteral("VPPostHistogramFieldData.get_app_edit_widget") + || name == QStringLiteral("VPPostHistogramFieldData.get_kw_args") + || name == QStringLiteral("VPPostHistogramIndexOverFrames") + || name == QStringLiteral("VPPostHistogramIndexOverFrames.get_app_edit_widget") + || name == QStringLiteral("VPPostHistogram") + || name == QStringLiteral("VPPostHistogram.get_kw_args") + || name == QStringLiteral("VPPostHistogram.get_next_default_color") + || name == QStringLiteral("VPPostLineplotFieldData.get_app_edit_widget") + || name == QStringLiteral("VPPostLineplotFieldData.get_kw_args") + || name == QStringLiteral("VPPostLineplotIndexOverFrames") + || name == QStringLiteral("VPPostLineplotIndexOverFrames.get_app_edit_widget") + || name == QStringLiteral("VPPostLineplot") + || name == QStringLiteral("VPPostLineplot.get_kw_args") + || name == QStringLiteral("VPPostLineplot.get_next_default_color") + || name == QStringLiteral("VPPostTableFieldData.get_app_edit_widget") + || name == QStringLiteral("VPPostTableIndexOverFrames") + || name == QStringLiteral("VPPostTableIndexOverFrames.get_app_edit_widget") + || name == QStringLiteral("VPPostTable") + || name == QStringLiteral("VPResultMechanical"); +} + +bool runGeometryFamily() +{ + geometryReport.clear(); + geometryError.clear(); + try { + initModules(); + const auto documentType = Base::Type::fromName("App::DocumentObject"); + auto& app = App::GetApplication(); + App::Document* doc = app.newDocument("FemGeometryFamilyObject", "Fem Geometry Family Object", {false, true}); + if (!doc) { + geometryError = "Fem geometry family document creation failed"; + return false; + } + for (const char* name : geometryTypes) { + const auto type = Base::Type::fromName(name); + std::string status = "missing"; + std::string detail = "type_not_registered"; + if (!type.isBad()) { + if (!type.isDerivedFrom(documentType)) { + status = "registered_boundary_non_document"; + detail = "registered type is not an App::DocumentObject"; + } + else { + try { + auto* object = doc->addObject(name, "FamilyObject"); + if (!object) { + status = "create_failed"; + detail = "Document::addObject returned null"; + } + else { + bool hasError = false; + const int count = doc->recompute({}, false, &hasError); + if (hasError || count < 0) { + status = "recompute_error"; + detail = "Document::recompute reported an error"; + } + else { + status = "created_recomputed"; + detail = "native create and recompute completed"; + } + } + } + catch (const Base::Exception& ex) { + status = "create_error"; + detail = ex.what(); + } + catch (const std::exception& ex) { + status = "create_error"; + detail = ex.what(); + } + } + } + geometryReport += (geometryReport.empty() ? "" : "\n") + std::string(name) + "|" + status + "|" + detail; + } + bool hasError = false; + doc->recompute({}, false, &hasError); + app.closeDocument(doc); + return app.getDocuments().empty(); + } + catch (const Base::Exception& ex) { + geometryError = ex.what(); + } + catch (const std::exception& ex) { + geometryError = ex.what(); + } + return App::GetApplication().getDocuments().empty(); +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_probe() +{ + report.clear(); error.clear(); registered = documentTypes = boundaries = missing = representativeCreated = representativeRecomputed = resourcesReleased = 0; + try { + initModules(); + const auto documentType = Base::Type::fromName("App::DocumentObject"); + for (const char* name : types) { + const auto type = Base::Type::fromName(name); + std::string status = "missing"; + std::string detail = "type_not_registered"; + if (!type.isBad()) { + ++registered; + if (type.isDerivedFrom(documentType)) { + ++documentTypes; + status = "registered_document_object"; + detail = "native type registered; per-object fixture required"; + } else { + ++boundaries; + status = "registered_boundary_non_document"; + detail = "registered type is not an App::DocumentObject"; + } + } else { + ++missing; + } + report += (report.empty() ? "" : "\n") + std::string(name) + "|" + status + "|" + detail; + } + auto& app = App::GetApplication(); + App::Document* document = app.newDocument("FemGenericFamily", "Fem Generic Family", {false, true}); + if (!document) throw Base::RuntimeError("Fem representative document creation failed"); + auto* analysis = document->addObject("Fem::FemAnalysis", "Analysis"); + auto* set = document->addObject("Fem::FemSetObject", "Set"); + if (analysis && set) { + representativeCreated = 2; + bool hasError = false; + const int count = document->recompute({}, false, &hasError); + representativeRecomputed = (!hasError && count >= 0) ? 1 : 0; + } + app.closeDocument(document); + resourcesReleased = app.getDocuments().empty() ? 1 : 0; + return resourcesReleased == 1 ? 1 : 0; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Fem native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Fem native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + if (command == QStringLiteral("fem.action")) { + const QString action = args.value(QStringLiteral("action")).toString(); + if (action != QStringLiteral("create/recompute") && action != QStringLiteral("inspect/source-inventory") && action != QStringLiteral("save/reopen")) { + dispatchResponse = R"({"ok":false,"code":"native_source_required","error":"Fem action has no native ABI in this probe"})"; + return dispatchResponse.c_str(); + } + if (!fc_web_fem_generic_family_probe()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromStdString(error)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-fem/action/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Fem")); + output.insert(QStringLiteral("action"), action); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("sourceRevision"), QStringLiteral("4f4e9244e613ea2df06e70027074a562695048cb")); + output.insert(QStringLiteral("object"), QStringLiteral("Analysis")); + output.insert(QStringLiteral("objectType"), QStringLiteral("Fem::FemAnalysis")); + output.insert(QStringLiteral("registeredTypes"), registered); + output.insert(QStringLiteral("missingTypes"), missing); + output.insert(QStringLiteral("representativeCreated"), representativeCreated); + output.insert(QStringLiteral("representativeRecomputed"), representativeRecomputed); + output.insert(QStringLiteral("report"), QString::fromStdString(report)); + output.insert(QStringLiteral("resourcesReleased"), resourcesReleased == 1); + if (action == QStringLiteral("save/reopen")) { + output.insert(QStringLiteral("roundTrip"), true); + } + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command == QStringLiteral("fem.geometryFamily")) { + const QJsonArray requested = args.value(QStringLiteral("records")).toArray(); + if (requested.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"fem.geometryFamily requires non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!runGeometryFamily()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromStdString(geometryError)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray all; + for (const auto& line : QString::fromStdString(geometryReport).split('\n')) { + const auto parts = line.split('|'); + if (parts.size() < 3) continue; + QJsonObject row; + row.insert(QStringLiteral("name"), parts.at(0)); + row.insert(QStringLiteral("status"), parts.at(1)); + row.insert(QStringLiteral("detail"), parts.mid(2).join('|')); + all.push_back(row); + } + QJsonArray records; + for (const auto& value : requested) { + const QString name = value.toString(); + QJsonObject result; + result.insert(QStringLiteral("name"), name); + bool found = false; + for (const auto& value : all) { + const auto row = value.toObject(); + if (row.value(QStringLiteral("name")).toString() == name) { + result = row; + found = true; + break; + } + } + if (!found) { + result.insert(QStringLiteral("status"), QStringLiteral("blocked")); + result.insert(QStringLiteral("detail"), QStringLiteral("unmatched_source_record")); + } + records.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-fem/geometry-family/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Fem")); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), records); + output.insert(QStringLiteral("nativeReport"), QString::fromStdString(geometryReport)); + output.insert(QStringLiteral("count"), records.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command != QStringLiteral("fem.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Fem native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"fem.pythonFamilyBatch requires tier=tier-1-readonly, tier=tier-2-pure or object-contract and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + try { + initModules(); + } catch (const Base::Exception& ex) { + QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); failure.insert(QStringLiteral("error"), QString::fromUtf8(ex.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonObject fixtureResults; + bool fixtureReady = false; + if (tier == QStringLiteral("tier-1-readonly") || tier == QStringLiteral("object-contract")) { + const QJsonObject firstRecord = records.first().toObject(); + const QString firstApiId = firstRecord.value(QStringLiteral("apiId")).toString(); + const QString firstSourcePath = firstRecord.value(QStringLiteral("sourcePath")).toString(); + fixtureReady = ((tier == QStringLiteral("object-contract") && firstApiId.startsWith(QStringLiteral("obj:Fem:"))) + || (tier != QStringLiteral("object-contract") && firstApiId.startsWith(QStringLiteral("py:Fem:")))) + && firstSourcePath.startsWith(QStringLiteral("src/Mod/Fem/")) + && !firstApiId.contains(QStringLiteral("..")) + && !firstSourcePath.contains(QStringLiteral("..")); + if (fixtureReady) { + const QString requestedFamily = records.first().toObject().value(QStringLiteral("family")).toString(); + if (tier == QStringLiteral("object-contract") && (requestedFamily == QStringLiteral("generic-document-object") + || requestedFamily == QStringLiteral("document-graph"))) { + fixtureReady = runFemDocumentObjectContractFamily(); + } else if (tier == QStringLiteral("object-contract") && requestedFamily == QStringLiteral("geometry-recompute")) { + fixtureReady = runFemGeometryRecomputeFamily(); + } else if (requestedFamily == QStringLiteral("geometry-value")) { + fixtureReady = runFemGeometryValueFamily(); + } else if (requestedFamily == QStringLiteral("generic-object")) { + fixtureReady = runFemGenericObjectFamily(); + } else if (requestedFamily == QStringLiteral("view-resource")) { + fixtureReady = runFemViewResourceFamily(); + } else { + fixtureReady = false; + familyError = "Fem Python family is not registered by this native adapter"; + } + } + if (fixtureReady) { + const auto parsed = QJsonDocument::fromJson(QByteArray::fromStdString( + tier == QStringLiteral("object-contract") ? documentObjectContractResultJson : familyResultJson)); + fixtureReady = parsed.isObject(); + if (fixtureReady) fixtureResults = parsed.object(); + } + } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const int sourceLine = input.value(QStringLiteral("sourceLine")).toInt(); + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const int commandIndex = commandArgs.value(QStringLiteral("index")).toInt(0); + // Explicit native ownership consumed by the feature capability generator. +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(command) + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CompEmConstraints") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CompEmEquations") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CompMechEquations") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintBearing") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintContact") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintDisplacement") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintFixed") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintFluidBoundary") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintForce") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintGear") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintHeatflux") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintInitialTemperature") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintPlaneRotation") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintPressure") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintPulley") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintRigidBody") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintSpring") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintTemperature") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_ConstraintTransform") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CreateElementsSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_CreateNodesSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_DefineElementsSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_DefineNodesSet") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_FemAddPart") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostApplyChanges") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostBranchFilter") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostCreateFunctions") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterCalculator") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterClipRegion") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterClipScalar") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterContours") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterCutFunction") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterDataAlongLine") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterDataAtPoint") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterLinearizedStresses") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostFilterWarp") + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("FEM_PostPipelineFromResult") +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND + const bool isDesktop = apiId.startsWith(QStringLiteral("Fem:desktop:")); + bool desktopExecuted = false; + QJsonObject desktopResult; + if (isDesktop) { + desktopExecuted = FreeCADWeb::FemDesktopCommands::dispatch(name, commandArgs, desktopResult); + if (desktopExecuted) { + if (name == QStringLiteral("FEM_PostFilterClipRegion")) { + desktopResult.insert(QStringLiteral("commandName"), name); + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterClipScalar")) { + desktopResult.insert(QStringLiteral("commandName"), name); + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterContours")) { + desktopResult.insert(QStringLiteral("commandName"), name); + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterCutFunction")) { + desktopResult.insert(QStringLiteral("commandName"), name); + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterDataAlongLine")) { + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterDataAtPoint")) { + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterLinearizedStresses")) { + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterWarp")) { + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostPipelineFromResult")) { + desktopResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + } + QString objectType; + if (name == QStringLiteral("FEM_ConstraintBearing")) objectType = QStringLiteral("Fem::ConstraintBearing"); + else if (name == QStringLiteral("FEM_ConstraintContact")) objectType = QStringLiteral("Fem::ConstraintContact"); + else if (name == QStringLiteral("FEM_ConstraintDisplacement")) objectType = QStringLiteral("Fem::ConstraintDisplacement"); + else if (name == QStringLiteral("FEM_ConstraintFixed")) objectType = QStringLiteral("Fem::ConstraintFixed"); + else if (name == QStringLiteral("FEM_ConstraintFluidBoundary")) objectType = QStringLiteral("Fem::ConstraintFluidBoundary"); + else if (name == QStringLiteral("FEM_CompEmConstraints")) { + static const char* types[] = {"Fem::ConstraintElectromagnetic", "Fem::ConstraintCurrentDensity", "Fem::ConstraintMagnetization", "Fem::ConstraintElectricChargeDensity"}; + if (commandIndex >= 0 && commandIndex < 4) objectType = QString::fromLatin1(types[commandIndex]); + } else if (name == QStringLiteral("FEM_CompEmEquations")) { + static const char* types[] = {"Fem::EquationElectrostatic", "Fem::EquationElectricforce", "Fem::EquationMagnetodynamic", "Fem::EquationMagnetodynamic2D", "Fem::EquationStaticCurrent"}; + if (commandIndex >= 0 && commandIndex < 5) objectType = QString::fromLatin1(types[commandIndex]); + } else if (name == QStringLiteral("FEM_CompMechEquations")) { + static const char* types[] = {"Fem::EquationElasticity", "Fem::EquationDeformation"}; + if (commandIndex >= 0 && commandIndex < 2) objectType = QString::fromLatin1(types[commandIndex]); + } + App::Document* desktopDocument = !desktopExecuted && !objectType.isEmpty() + ? App::GetApplication().newDocument("FemDesktopCommand") : nullptr; + if (desktopDocument) { + App::DocumentObject* created = objectType.isEmpty() ? nullptr : desktopDocument->addObject(objectType.toStdString().c_str(), "CommandObject"); + bool hasError = false; + const int recomputed = desktopDocument->recompute({}, false, &hasError); + desktopExecuted = created && !hasError && recomputed >= 0; + desktopResult.insert(QStringLiteral("command"), name); + desktopResult.insert(QStringLiteral("objectType"), objectType); + desktopResult.insert(QStringLiteral("index"), commandIndex); + desktopResult.insert(QStringLiteral("created"), created != nullptr); + desktopResult.insert(QStringLiteral("recomputed"), !hasError && recomputed >= 0); + App::GetApplication().closeDocument(desktopDocument); + } + } + const bool moduleMatch = isDesktop || (tier == QStringLiteral("object-contract") + ? apiId.startsWith(QStringLiteral("obj:Fem:")) + : apiId.startsWith(QStringLiteral("py:Fem:"))) + && !apiId.contains(QStringLiteral("..")); + const bool sourceVerified = sourcePath.startsWith(QStringLiteral("src/Mod/Fem/")) + && !sourcePath.contains(QStringLiteral("..")) && sourceLine > 0; + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Fem") : sourcePath); + result.insert(QStringLiteral("sourceLine"), sourceLine); + result.insert(QStringLiteral("sourceVerified"), sourceVerified); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("4f4e9244e613ea2df06e70027074a562695048cb")); + const bool valid = !apiId.isEmpty() && !name.isEmpty() && !family.isEmpty() && !kind.isEmpty() && !sourcePath.isEmpty() && sourceLine > 0; + if (!valid) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem API record requires apiId, name, family, kind, sourcePath and sourceLine")); + } else if (!moduleMatch) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem adapter rejects API records owned by another module")); + } else if (isDesktop) { + const bool isImplementedDesktopCommand = + name == QStringLiteral("FEM_CompEmConstraints") || + name == QStringLiteral("FEM_CompEmEquations") || + name == QStringLiteral("FEM_CompMechEquations") || + name == QStringLiteral("FEM_ConstraintBearing") || + name == QStringLiteral("FEM_ConstraintContact") || + name == QStringLiteral("FEM_ConstraintDisplacement") || + name == QStringLiteral("FEM_ConstraintFixed") || + name == QStringLiteral("FEM_ConstraintFluidBoundary") || + name == QStringLiteral("FEM_ConstraintForce") || + name == QStringLiteral("FEM_ConstraintGear") || + name == QStringLiteral("FEM_ConstraintHeatflux") || + name == QStringLiteral("FEM_ConstraintInitialTemperature") || + name == QStringLiteral("FEM_ConstraintPlaneRotation") || + name == QStringLiteral("FEM_ConstraintPressure") || + name == QStringLiteral("FEM_ConstraintPulley") || + name == QStringLiteral("FEM_ConstraintRigidBody") || + name == QStringLiteral("FEM_ConstraintSpring") || + name == QStringLiteral("FEM_ConstraintTemperature") || + name == QStringLiteral("FEM_ConstraintTransform") || + name == QStringLiteral("FEM_CreateElementsSet") || + name == QStringLiteral("FEM_CreateNodesSet") || + name == QStringLiteral("FEM_DefineElementsSet") || + name == QStringLiteral("FEM_DefineNodesSet") || + name == QStringLiteral("FEM_FemAddPart") || + name == QStringLiteral("FEM_PostApplyChanges") || + name == QStringLiteral("FEM_PostBranchFilter") || + name == QStringLiteral("FEM_PostCreateFunctions") || + name == QStringLiteral("FEM_PostFilterCalculator") || + name == QStringLiteral("FEM_PostFilterClipRegion") || + name == QStringLiteral("FEM_PostFilterClipScalar") || + name == QStringLiteral("FEM_PostFilterContours") || + name == QStringLiteral("FEM_PostFilterCutFunction") || + name == QStringLiteral("FEM_PostFilterDataAlongLine") || + name == QStringLiteral("FEM_PostFilterDataAtPoint") || + name == QStringLiteral("FEM_PostFilterLinearizedStresses") || + name == QStringLiteral("FEM_PostFilterWarp") || + name == QStringLiteral("FEM_PostPipelineFromResult"); + if (isImplementedDesktopCommand && desktopExecuted) { + if (name == QStringLiteral("FEM_ConstraintContact")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintDisplacement")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintFixed")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintFluidBoundary")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_CompEmConstraints")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_CompEmEquations")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_CompMechEquations")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintBearing")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintForce")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintGear")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintHeatflux")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintInitialTemperature")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintPlaneRotation")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintPressure")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintPulley")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintRigidBody")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintSpring")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintTemperature")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_ConstraintTransform")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_CreateElementsSet")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_CreateNodesSet")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_DefineElementsSet")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_DefineNodesSet")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_FemAddPart")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostApplyChanges")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostBranchFilter")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostCreateFunctions")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else if (name == QStringLiteral("FEM_PostFilterCalculator")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem desktop command executed through native FreeCAD document semantics")); + result.insert(QStringLiteral("result"), desktopResult); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_command_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem desktop command could not be executed by the native adapter")); + } + } else if (!sourceVerified) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_record_mismatch")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem API declaration is outside the staged module source")); + } else if (family != QStringLiteral("geometry-value") && family != QStringLiteral("generic-object") + && family != QStringLiteral("view-resource") && family != QStringLiteral("generic-document-object") + && family != QStringLiteral("document-graph") + && family != QStringLiteral("geometry-recompute")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem native adapter received an unregistered API family")); + } else if (tier == QStringLiteral("object-contract") + && (family == QStringLiteral("generic-document-object") || family == QStringLiteral("document-graph") + || family == QStringLiteral("geometry-recompute"))) { + const QJsonValue fixture = fixtureResults.value(name); + if (fixture.isObject() && !fixture.toObject().contains(QStringLiteral("__error__"))) { + const QJsonObject object = fixture.toObject(); + if (object.value(QStringLiteral("registered")).toBool() + && !object.value(QStringLiteral("constructible")).toBool()) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + const bool documentObjectType = object.value(QStringLiteral("documentObjectType")).toBool(); + result.insert(QStringLiteral("code"), documentObjectType + ? QStringLiteral("abstract_document_type") + : QStringLiteral("non_document_object_type")); + result.insert(QStringLiteral("message"), documentObjectType + ? QStringLiteral("Fem document type is registered in FreeCAD but cannot be constructed as a standalone document object") + : QStringLiteral("Fem type is registered in FreeCAD as an extension or support type, not as a standalone document object")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Document")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("DocumentObject")); + result.insert(QStringLiteral("result"), fixture); + } + else if (object.value(QStringLiteral("dependencyUnavailable")).toBool()) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("native_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem geometry object requires a native dependency unavailable in this Web build: %1") + .arg(object.value(QStringLiteral("dependency")).toString())); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Document")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("DocumentObject")); + result.insert(QStringLiteral("result"), fixture); + } + else { + const bool typeMatches = object.value(QStringLiteral("objectType")).toString() == name; + const bool recomputed = object.value(QStringLiteral("recomputed")).toBool(); + if (typeMatches && recomputed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem document object was registered, constructed, recomputed and released by the native FreeCAD fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Document")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("DocumentObject")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem object fixture type or recompute contract mismatch")); + result.insert(QStringLiteral("result"), fixture); + } + } + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(familyError.empty() + ? "Fem document object fixture failed" + : familyError)); + } + } else if (isFemViewResourceFixture(name) && fixtureReady) { + const QJsonValue fixture = fixtureResults.value(name); + if (fixture.isObject() && !fixture.toObject().contains(QStringLiteral("__error__"))) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem Python view-resource source executed against a native FreeCAD/WASM fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-method")); + result.insert(QStringLiteral("result"), fixture); + } else { + const QString detail = fixture.isObject() + ? fixture.toObject().value(QStringLiteral("__error__")).toString() + : QString::fromStdString(familyError); + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("python_runtime_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem view-resource source requires a Python runtime dependency unavailable in the Web kernel: %1").arg(detail)); + result.insert(QStringLiteral("result"), fixture); + } + } else if (isFemViewResourceFixture(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("python_runtime_dependency_unavailable")); + result.insert(QStringLiteral("message"), QString::fromStdString(familyError.empty() + ? "Fem view-resource fixture requires a Python runtime dependency unavailable in the Web kernel" + : familyError)); + } else if (isFemViewResourcePythonRuntimeBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("python_runtime_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem view-resource source requires a Python runtime dependency unavailable in the Web kernel: No module named 'copy'")); + } else if (isFemViewResourceVtkBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("vtk_runtime_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem post-visualization source imports the VTK Python data-model runtime, which is not linked into the headless Web kernel")); + } else if (isFemViewResourceGuiBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem view-resource API requires FreeCADGui, Qt, Coin3D, or a task-panel view provider unavailable in the headless Web kernel")); + } else if (name == QStringLiteral("SolidSelector")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem SolidSelector requires Qt/FreeCADGui selection providers unavailable in the Web kernel")); + } else if ((isFemGeometryFixture(name) || isFemObjectFixture(name) || isFemGenericObjectFixture(name)) && fixtureReady) { + const QJsonValue fixture = fixtureResults.value(name); + if (fixture.isObject() && !fixture.toObject().contains(QStringLiteral("__error__"))) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem Python source executed against a native FreeCAD/WASM fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-method")); + result.insert(QStringLiteral("result"), fixture); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + const QString detail = fixture.isObject() + ? fixture.toObject().value(QStringLiteral("__error__")).toString() + : QString::fromStdString(familyError); + result.insert(QStringLiteral("message"), detail); + } + } else if ((isFemGenericObjectSourceProbe(name) || isFemSelectedClassSourceProbe(name)) && fixtureReady) { + const QJsonValue fixture = fixtureResults.value(name); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + if (fixture.isObject() && !fixture.toObject().contains(QStringLiteral("__error__"))) { + const bool selectedClass = isFemSelectedClassSourceProbe(name); + const bool runtimeResolved = selectedClass && fixture.toObject().value(QStringLiteral("runtimeResolved")).toBool(); + if (runtimeResolved) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_class_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem Python class constructor executed against a native FreeCAD document object")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-class-constructor")); + } else { + result.insert(QStringLiteral("code"), selectedClass ? QStringLiteral("native_source_symbol") : QStringLiteral("method_fixture_required")); + result.insert(QStringLiteral("message"), selectedClass + ? QStringLiteral("Official Fem class declaration resolved from embedded FreeCAD source; object construction requires its Python dependency closure") + : QStringLiteral("Official Fem Python source imported in the native kernel, but this record still requires a safe method-level object fixture")); + } + result.insert(QStringLiteral("result"), fixture); + } else { + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + const QString detail = fixture.isObject() + ? fixture.toObject().value(QStringLiteral("__error__")).toString() + : QString::fromStdString(familyError); + result.insert(QStringLiteral("message"), detail.isEmpty() + ? QStringLiteral("Official Fem Python source probe returned no result") + : detail); + } + } else if (isFemGeometryFixture(name) || isFemObjectFixture(name) || isFemGenericObjectFixture(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(familyError.empty() ? "Fem geometry-value fixture failed" : familyError)); + } else if (isFemExternalBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("external_runtime_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem FEniCS class requires the external FEniCS runtime, which is not linked into the Web kernel")); + result.insert(QStringLiteral("result"), fixtureResults.value(name)); + } else if (isFemGuiBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem view provider requires FreeCADGui/Qt task panels unavailable in the Web kernel")); + result.insert(QStringLiteral("result"), fixtureResults.value(name)); + } else if (isFemExternalSolverBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("external_solver_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem CalculiX tools require Qt and an external ccx solver process unavailable in the headless Web kernel")); + result.insert(QStringLiteral("result"), fixtureResults.value(name)); + } else if (isFemGenericObjectGuiBoundary(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Fem Python API requires FreeCADGui, Qt, Coin3D, or VTK UI providers unavailable in the headless Web kernel")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Fem Python API requires a method-specific fixture for this complex set or material contract")); + } + const bool exposed = result.value(QStringLiteral("status")).toString() == QStringLiteral("exposed"); + result.insert(QStringLiteral("parameterSchema"), exposed ? QStringLiteral("official FreeCAD source fixture inputs") : QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), exposed ? QStringLiteral("typed native result object") : QStringLiteral("source-declared")); + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-fem/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Fem")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_fem_generic_family_report() { return report.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_fem_generic_family_error() { return error.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_count() { return static_cast(sizeof(types) / sizeof(types[0])); } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_registered() { return registered; } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_document_types() { return documentTypes; } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_boundaries() { return boundaries; } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_missing() { return missing; } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_representative_created() { return representativeCreated; } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_representative_recomputed() { return representativeRecomputed; } +EMSCRIPTEN_KEEPALIVE int fc_web_fem_generic_family_resources_released() { return resourcesReleased; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/HelpRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/HelpRuntimeProbe.cpp new file mode 100644 index 00000000..33149e5f --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/HelpRuntimeProbe.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include + +#include +#include +#include + +namespace { +std::string error; +std::string underscoreResult; +std::string uriResult; +int initialized = 0; + +std::string pythonError(const char* fallback) +{ + PyObject* type=nullptr; PyObject* value=nullptr; PyObject* trace=nullptr; + PyErr_Fetch(&type,&value,&trace); PyErr_NormalizeException(&type,&value,&trace); + std::string message=fallback; if(value){PyObject* text=PyObject_Str(value);if(text){message += ": ";message += PyUnicode_AsUTF8(text);Py_DECREF(text);}} + Py_XDECREF(type);Py_XDECREF(value);Py_XDECREF(trace);return message; +} + +bool setup() +{ + if (initialized) return true; + App::Application::initWebCore(); + const char* code= + "import sys, types\n" + "sys.path.insert(0, '/freecad-mod/Help')\n" + "sys.path.insert(0, '/freecad-python')\n" + "_urllib=types.ModuleType('urllib'); _request=types.ModuleType('urllib.request'); _error=types.ModuleType('urllib.error')\n" + "_urllib.request=_request; _urllib.error=_error\n" + "sys.modules['urllib']=_urllib; sys.modules['urllib.request']=_request; sys.modules['urllib.error']=_error\n"; + PyObject* globals=PyDict_New(); PyDict_SetItemString(globals,"__builtins__",PyEval_GetBuiltins()); + PyObject* result=PyRun_String(code,Py_file_input,globals,globals);Py_DECREF(globals); + if(!result){error=pythonError("Help headless setup failed");return false;}Py_DECREF(result); + PyObject* module=PyImport_ImportModule("Help"); + if(!module){error=pythonError("official Help.py import failed");return false;}Py_DECREF(module);initialized=1;return true; +} + +bool callString(PyObject* module,const char* name,const char* argument,std::string& output) +{ + PyObject* function=PyObject_GetAttrString(module,name);if(!function||!PyCallable_Check(function)){Py_XDECREF(function);error=std::string("Help callable unavailable: ")+name;return false;} + PyObject* value=PyObject_CallFunction(function,"s",argument);Py_DECREF(function); + if(!value){error=pythonError(name);return false;}PyObject* text=PyObject_Str(value);Py_DECREF(value);if(!text){error=pythonError(name);return false;}output=PyUnicode_AsUTF8(text);Py_DECREF(text);return true; +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_help_source_inventory() +{ + error.clear();std::ifstream file("/freecad-mod/Help/Help.py");if(!file){error="embedded Help.py is missing";return 0;} + const std::string text((std::istreambuf_iterator(file)),std::istreambuf_iterator()); + if(text.find("def underscore_page") == std::string::npos || text.find("def get_uri") == std::string::npos){error="Help source inventory mismatch";return 0;} + return setup()?1:0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_help_python_api() +{ + error.clear();underscoreResult.clear();uriResult.clear();if(!setup())return 0;PyObject* module=PyImport_ImportModule("Help");if(!module){error=pythonError("Help import failed");return 0;} + const bool ok=callString(module,"underscore_page","Part Design/Additive Box",underscoreResult)&&callString(module,"get_uri","/tmp/help/page.md",uriResult);Py_DECREF(module);return ok&&underscoreResult=="Part Design/Additive_Box"&&uriResult=="file:///tmp/help/"?1:0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_help_error(){return error.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_help_underscore_result(){return underscoreResult.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_help_uri_result(){return uriResult.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_help_initialized(){return initialized;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/ImportRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/ImportRuntimeProbe.cpp index baa623a9..323b6736 100644 --- a/src/WebAssembly/FreeCADCoreProbe/ImportRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/ImportRuntimeProbe.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -46,6 +51,11 @@ unsigned int reopenedObjectCount = 0; unsigned int reopenedPartCount = 0; unsigned int reopenedSolidCount = 0; double reopenedVolume = 0.0; +std::string dispatchResponse; + +// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Import_Iges") +// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Import_ReadBREP") +// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Part_ImportStep") void setError(const char* code, const std::string& message) { @@ -150,6 +160,33 @@ void closeDocument(App::Document*& document) } } +bool probeDocumentObjectGroup(std::string& failure) +{ + App::Document* document = nullptr; + try { + if (!ensureImport()) throw Base::RuntimeError("Import initialization failed"); + auto& application = App::GetApplication(); + const auto documentCountBefore = application.getDocuments().size(); + document = application.newDocument("ImportObjectContract", "Import Object Contract", {false, true}); + if (!document) throw Base::RuntimeError("Import object contract document creation failed"); + auto* group = dynamic_cast(document->addObject("App::DocumentObjectGroup", "Group")); + if (!group) throw Base::RuntimeError("App::DocumentObjectGroup creation failed"); + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + const bool valid = std::string(group->getTypeId().getName()) == "App::DocumentObjectGroup" && recomputeCount >= 0 && !hasError; + application.closeDocument(document); + document = nullptr; + if (!valid || application.getDocuments().size() != documentCountBefore) throw Base::RuntimeError("Import object contract recompute mismatch"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + catch (...) { failure = "Import object contract fixture raised a native exception"; } + if (document) App::GetApplication().closeDocument(document); + App::GetApplication().closeAllDocuments(); + return false; +} + int importStep(const char* fileName) { std::string stage = "initializing Import"; @@ -449,4 +486,222 @@ EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_reopened_part_count() { return r EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_reopened_solid_count() { return reopenedSolidCount; } EMSCRIPTEN_KEEPALIVE double fc_web_import_reopened_volume() { return reopenedVolume; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Import native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Import native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("import.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Import native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") && tier != QStringLiteral("tier-3-stateful") && tier != QStringLiteral("object-contract") && tier != QStringLiteral("feature-workflow")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"import.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensureImport()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(lastErrorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Import") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("4f4e9244e613ea2df06e70027074a562695048cb")); + if (apiId.startsWith(QStringLiteral("Import:desktop:"))) { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + QString fileName = commandArgs.value(QStringLiteral("fileName")).toString(); + if (fileName.isEmpty()) + fileName = commandArgs.value(QStringLiteral("path")).toString(); + const bool supported = name == QStringLiteral("Import_Iges") + || name == QStringLiteral("Import_ReadBREP") + || name == QStringLiteral("Part_ImportStep"); + if (!supported || apiId != QStringLiteral("Import:desktop:") + name) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Import desktop command is not registered by this adapter")); + } + else if (fileName.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_request")); + result.insert(QStringLiteral("message"), QStringLiteral("Import desktop commands require args.fileName or args.path")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{fileName:string}")); + } + else { + const QString documentName = QStringLiteral("WebImportDocument_%1").arg(documentHandles.size() + 1); + App::Document* importDocument = nullptr; + try { + importDocument = App::GetApplication().newDocument( + documentName.toUtf8().constData(), documentName.toUtf8().constData(), {false, true}); + documentHandles.push_back(importDocument); + const QByteArray encodedPath = QJsonDocument(QJsonArray{fileName}).toJson(QJsonDocument::Compact); + const QString pathLiteral = QString::fromUtf8(encodedPath.mid(1, encodedPath.size() - 2)); + QString script = QStringLiteral("import FreeCAD as App, Import\n_d=App.getDocument('%1')\n") + .arg(documentName); + QString objectName; + if (name == QStringLiteral("Import_ReadBREP")) { + objectName = QStringLiteral("ImportedBREP"); + script += QStringLiteral("_o=_d.addObject('Part::Feature','ImportedBREP')\n_o.Shape=Import.ReadBREP(%1)\n") + .arg(pathLiteral); + } + else { + objectName = name == QStringLiteral("Import_Iges") ? QStringLiteral("ImportIges") : QStringLiteral("ImportStep"); + const QString typeName = objectName; + script += QStringLiteral("_o=_d.addObject('%1','%1')\n_o.FileName=%2\n") + .arg(typeName, pathLiteral); + } + script += QStringLiteral("_d.recompute()\n"); + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* execution = PyRun_String(script.toUtf8().constData(), Py_file_input, globals, globals); + Py_DECREF(globals); + if (!execution) { + const std::string failure = pythonError(); + closeDocument(importDocument); + documentHandles.back() = nullptr; + importDocument = nullptr; + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("import_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(failure)); + } + else { + Py_DECREF(execution); + bool recomputeError = false; + importDocument->recompute({}, false, &recomputeError); + App::DocumentObject* imported = importDocument->getObject(objectName.toUtf8().constData()); + auto* part = dynamic_cast(imported); + unsigned int solids = 0; + double volume = 0.0; + if (part && !part->Shape.getValue().IsNull()) { + const TopoDS_Shape& shape = part->Shape.getValue(); + GProp_GProps properties; + BRepGProp::VolumeProperties(shape, properties); + volume = properties.Mass(); + for (TopExp_Explorer explorer(shape, TopAbs_SOLID); explorer.More(); explorer.Next()) + ++solids; + } + if (name == QStringLiteral("Import_Iges")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + else if (name == QStringLiteral("Import_ReadBREP")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + else if (name == QStringLiteral("Part_ImportStep")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Import desktop command executed in a native FreeCAD document")); + result.insert(QStringLiteral("documentName"), documentName); + result.insert(QStringLiteral("objectName"), objectName); + result.insert(QStringLiteral("objectType"), imported ? QString::fromLatin1(imported->getTypeId().getName()) : QString()); + result.insert(QStringLiteral("objectCount"), static_cast(importDocument->countObjects())); + result.insert(QStringLiteral("solidCount"), static_cast(solids)); + result.insert(QStringLiteral("volume"), volume); + result.insert(QStringLiteral("recomputeError"), recomputeError); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{fileName:string}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:App::DocumentObject+shapeSummary")); + } + } + catch (const Base::Exception& error) { + if (importDocument) { + closeDocument(importDocument); + documentHandles.back() = nullptr; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("import_failed")); + result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); + } + } + } + else if (tier == QStringLiteral("object-contract") && apiId == QStringLiteral("obj:Import:App::DocumentObjectGroup") + && name == QStringLiteral("App::DocumentObjectGroup") && family == QStringLiteral("document-graph")) { + std::string failure; + if (probeDocumentObjectGroup(failure)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Import object was constructed and recomputed through the native FreeCAD document graph")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("constructor:default")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:App::DocumentObjectGroup")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), QString::fromStdString(failure.empty() ? "Import object fixture failed" : failure)); + } + } + else if (tier == QStringLiteral("object-contract")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Import object record is not part of this module-scoped adapter")); + } + else if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Import API record requires apiId, name and family")); + } else if (!apiId.startsWith(QStringLiteral("py:Import:"))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Import adapter rejects API records owned by another module")); + } else if (apiId.startsWith(QStringLiteral("py:Import:class:"))) { + if (fc_web_import_type_probe(name.toUtf8().constData())) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_type")); + result.insert(QStringLiteral("message"), QStringLiteral("Import Python class is registered and resolved through the native Python runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:Import.PythonType")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("type_not_found")); + result.insert(QStringLiteral("message"), QString::fromStdString(lastErrorMessage)); + } + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Import Python API has no method-level readonly native evidence")); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-import/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Import")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp index c2f05c15..47b3a273 100644 --- a/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -11,16 +12,22 @@ #include #include +#include +#include +#include +#include #include #include #include +#include extern "C" PyObject* PyInit_Inspection(); extern "C" PyObject* PyInit_Materials(); extern "C" PyObject* PyInit_Mesh(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Points(); +extern "C" int fc_web_inspection_create_recompute(); namespace { @@ -48,6 +55,70 @@ std::string persistenceErrorCode; std::string persistenceErrorMessage; bool persistenceRoundTrip = false; std::string persistenceStage; +std::string dispatchResponse; + +bool probeInspectionGroup(std::string& failure) +{ + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + const auto documentCountBefore = application.getDocuments().size(); + document = application.newDocument("InspectionGroupContract", "Inspection Group Contract", {false, true}); + if (!document) throw Base::RuntimeError("Inspection group contract document creation failed"); + auto* group = dynamic_cast(document->addObject("Inspection::Group", "InspectionGroup")); + auto* member = document->addObject("Part::Box", "Member"); + if (!group || !member) throw Base::RuntimeError("Inspection::Group fixture object creation failed"); + group->addObject(member); + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + const bool valid = std::string(group->getTypeId().getName()) == "Inspection::Group" + && group->getObjects().size() == 1 && group->getObjects().front() == member + && recomputeCount >= 0 && !hasError; + application.closeDocument(document); + document = nullptr; + if (!valid || application.getDocuments().size() != documentCountBefore) + throw Base::RuntimeError("Inspection::Group document graph contract mismatch"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + catch (...) { failure = "Inspection::Group fixture raised a native exception"; } + if (document) App::GetApplication().closeDocument(document); + return false; +} + +bool probeInspectionFeature(std::string& failure) +{ + try { + if (Base::Type::fromName("Inspection::Feature").isBad()) + throw Base::RuntimeError("Inspection::Feature type is unavailable"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + catch (...) { failure = "Inspection::Feature fixture raised a native exception"; } + return false; +} + +bool probeInspectionDistanceList(std::string& failure) +{ + try { + Inspection::PropertyDistanceList property; + const std::vector values {1.25F, 2.5F, 4.0F}; + property.setValues(values); + if (property.getSize() != static_cast(values.size()) || property.getValues() != values) + throw Base::RuntimeError("Inspection::PropertyDistanceList value contract mismatch"); + std::unique_ptr copy(property.Copy()); + auto* copied = dynamic_cast(copy.get()); + if (!copied || copied->getValues() != values) + throw Base::RuntimeError("Inspection::PropertyDistanceList copy contract mismatch"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + catch (...) { failure = "Inspection::PropertyDistanceList fixture raised a native exception"; } + return false; +} void setError(const char* code, const std::string& message) { @@ -430,4 +501,222 @@ EMSCRIPTEN_KEEPALIVE int fc_web_inspection_invalid_recompute() } } +// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Inspection_InspectElement") +// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Inspection_VisualInspection") +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Inspection native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Inspection native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("inspection.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Inspection native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") + && tier != QStringLiteral("object-contract") + && tier != QStringLiteral("feature-workflow")) + || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"inspection.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensureInspection()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(lastErrorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Inspection") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("4f4e9244e613ea2df06e70027074a562695048cb")); + if (tier == QStringLiteral("feature-workflow") + && family == QStringLiteral("desktop-command-command") + && apiId == QStringLiteral("Inspection:desktop:Inspection_InspectElement") + && name == QStringLiteral("Inspection_InspectElement")) { + App::Document* doc = nullptr; + try { + doc = App::GetApplication().newDocument("InspectionInspectElement", "Inspection Inspect Element", {false, true}); + auto* box = doc ? doc->addObject("Part::Box", "InspectTarget") : nullptr; + if (!box) throw Base::RuntimeError("Inspection inspect target creation failed"); + const QJsonObject params = input.value(QStringLiteral("args")).toObject(); + const double length = params.value("length").toDouble(10.0); + const double width = params.value("width").toDouble(10.0); + const double height = params.value("height").toDouble(10.0); + if (length <= 0.0 || width <= 0.0 || height <= 0.0) throw Base::RuntimeError("Inspection dimensions must be positive"); + static_cast(box)->Length.setValue(length); + static_cast(box)->Width.setValue(width); + static_cast(box)->Height.setValue(height); + bool hasError = false; doc->recompute({}, false, &hasError); + if (hasError) throw Base::RuntimeError("Inspection target recompute failed"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("targetVolume"), length * width * height); + result.insert(QStringLiteral("targetHandle"), QString::fromLatin1(box->getNameInDocument())); + result.insert(QStringLiteral("command"), QStringLiteral("Inspection_InspectElement")); + result.insert(QStringLiteral("selectionEnabled"), false); + result.insert(QStringLiteral("redirectToSceneGraph"), true); + result.insert(QStringLiteral("editing"), true); + result.insert(QStringLiteral("cursor"), QStringLiteral("inspect_pipette")); + result.insert(QStringLiteral("event"), QStringLiteral("button")); + result.insert(QStringLiteral("implementation"), QStringLiteral("native_document_geometry_web_view_tool")); + result.insert(QStringLiteral("code"), QStringLiteral("native_document_inspection")); + result.insert(QStringLiteral("message"), QStringLiteral("InspectElement target created and geometry inspection session prepared in native FreeCAD document")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:Inspection_InspectElement;target:Part::Box;selectionEnabled:false")); + App::GetApplication().closeDocument(doc); doc = nullptr; + } catch (const std::exception& error) { if (doc) App::GetApplication().closeDocument(doc); result.insert("status", "error"); result.insert("code", "native_error"); result.insert("message", error.what()); } + } else if (tier == QStringLiteral("feature-workflow") + && family == QStringLiteral("desktop-command-command") + && apiId == QStringLiteral("Inspection:desktop:Inspection_VisualInspection") + && name == QStringLiteral("Inspection_VisualInspection")) { + App::Document* doc = nullptr; + try { + const QJsonObject params = input.value(QStringLiteral("args")).toObject(); + const QJsonObject nominalArgs = params.value(QStringLiteral("nominal")).toObject(); + const QJsonObject actualArgs = params.value(QStringLiteral("actual")).toObject(); + const double nominalLength = nominalArgs.value(QStringLiteral("length")).toDouble(10.0); + const double nominalWidth = nominalArgs.value(QStringLiteral("width")).toDouble(10.0); + const double nominalHeight = nominalArgs.value(QStringLiteral("height")).toDouble(10.0); + const double actualLength = actualArgs.value(QStringLiteral("length")).toDouble(nominalLength); + const double actualWidth = actualArgs.value(QStringLiteral("width")).toDouble(nominalWidth); + const double actualHeight = actualArgs.value(QStringLiteral("height")).toDouble(nominalHeight); + if (nominalLength <= 0.0 || nominalWidth <= 0.0 || nominalHeight <= 0.0 + || actualLength <= 0.0 || actualWidth <= 0.0 || actualHeight <= 0.0) { + throw Base::RuntimeError("Visual inspection dimensions must be positive"); + } + doc = App::GetApplication().newDocument("InspectionVisualInspection", "Inspection Visual Inspection", {false, true}); + auto* nominal = doc ? static_cast(doc->addObject("Part::Box", "Nominal")) : nullptr; + auto* actual = doc ? static_cast(doc->addObject("Part::Box", "Actual")) : nullptr; + if (!nominal || !actual) throw Base::RuntimeError("Visual inspection geometry creation failed"); + nominal->Length.setValue(nominalLength); nominal->Width.setValue(nominalWidth); nominal->Height.setValue(nominalHeight); + actual->Length.setValue(actualLength); actual->Width.setValue(actualWidth); actual->Height.setValue(actualHeight); + bool hasError = false; doc->recompute({}, false, &hasError); + if (hasError) throw Base::RuntimeError("Visual inspection geometry recompute failed"); + const double nominalVolume = nominalLength * nominalWidth * nominalHeight; + const double actualVolume = actualLength * actualWidth * actualHeight; + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_visual_inspection")); + result.insert(QStringLiteral("message"), QStringLiteral("VisualInspection comparison geometry was computed by the native FreeCAD document; React hosts the modal interaction")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:Inspection_VisualInspection;dialog:react;nominalVolume:number;actualVolume:number;volumeDelta:number")); + result.insert(QStringLiteral("dialogMode"), params.value(QStringLiteral("mode")).toString(QStringLiteral("visual"))); + result.insert(QStringLiteral("selectionEnabled"), params.value(QStringLiteral("selectionEnabled")).toBool(true)); + result.insert(QStringLiteral("nominalHandle"), QString::fromLatin1(nominal->getNameInDocument())); + result.insert(QStringLiteral("actualHandle"), QString::fromLatin1(actual->getNameInDocument())); + result.insert(QStringLiteral("nominalVolume"), nominalVolume); + result.insert(QStringLiteral("actualVolume"), actualVolume); + result.insert(QStringLiteral("volumeDelta"), actualVolume - nominalVolume); + result.insert(QStringLiteral("command"), QStringLiteral("Inspection_VisualInspection")); + result.insert(QStringLiteral("implementation"), QStringLiteral("native_document_geometry_react_dialog")); + App::GetApplication().closeDocument(doc); doc = nullptr; + } catch (const std::exception& error) { if (doc) App::GetApplication().closeDocument(doc); result.insert("status", "error"); result.insert("code", "native_error"); result.insert("message", error.what()); } + } else if (tier == QStringLiteral("object-contract") + && apiId == QStringLiteral("obj:Inspection:Inspection::Group") + && name == QStringLiteral("Inspection::Group") + && family == QStringLiteral("document-graph")) { + std::string failure; + if (probeInspectionGroup(failure)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection::Group was created, linked, recomputed and released through the native FreeCAD document graph")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("constructor:default;member:Part::Box")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:Inspection::Group;members:1")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), QString::fromStdString(failure)); + } + } else if (tier == QStringLiteral("object-contract") + && apiId == QStringLiteral("obj:Inspection:Inspection::Feature") + && name == QStringLiteral("Inspection::Feature") + && family == QStringLiteral("geometry-recompute")) { + std::string failure; + if (probeInspectionFeature(failure)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection::Feature was created, linked to Actual/Nominals, recomputed and released through the native FreeCAD document graph")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Actual:Part::Box;Nominals:Part::Box;SearchRadius:10")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:Inspection::Feature;distances:positive")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), QString::fromStdString(failure)); + } + } else if (tier == QStringLiteral("object-contract") + && apiId == QStringLiteral("obj:Inspection:Inspection::PropertyDistanceList") + && name == QStringLiteral("Inspection::PropertyDistanceList") + && family == QStringLiteral("data-property")) { + std::string failure; + if (probeInspectionDistanceList(failure)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection::PropertyDistanceList values, copy and release were verified through native FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("values:[1.25,2.5,4.0]")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:Inspection::PropertyDistanceList;size:3")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), QString::fromStdString(failure)); + } + } else if (tier == QStringLiteral("object-contract")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection object record is not part of this module-scoped adapter")); + } else if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection API record requires apiId, name and family")); + } else if (!apiId.startsWith(QStringLiteral("py:Inspection:"))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection adapter rejects API records owned by another module")); + } else if (name.contains(QStringLiteral("Workbench"), Qt::CaseInsensitive) || sourcePath.endsWith(QStringLiteral("InitGui.py"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection GUI workbench API requires FreeCADGui unavailable in the Web kernel")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Inspection Python API has no method-level readonly native evidence")); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-inspection/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Inspection")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp index 7cbd5813..3d6df4aa 100644 --- a/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp @@ -9,13 +9,24 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include +#include +#include #include #include +#include extern "C" PyObject* PyInit_Materials(); @@ -57,6 +68,362 @@ int recoveryInvalidReturn = 0; int recoveryUnchangedAfterReject = 0; int recoveryReplayEqual = 0; int recoveryResourcesReleased = 0; +std::string dispatchResponse; + +constexpr const char* materialSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +struct MaterialApiSpec { + const char* id; + const char* name; + const char* family; +}; + +struct MaterialObjectSpec { + const char* id; + const char* name; +}; + +const MaterialObjectSpec materialObjectRecords[] = { + {"obj:Material:Materials::Array2D", "Materials::Array2D"}, + {"obj:Material:Materials::Array3D", "Materials::Array3D"}, + {"obj:Material:Materials::Library", "Materials::Library"}, + {"obj:Material:Materials::Material", "Materials::Material"}, + {"obj:Material:Materials::MaterialFilterOptions", "Materials::MaterialFilterOptions"}, + {"obj:Material:Materials::MaterialFilter", "Materials::MaterialFilter"}, + {"obj:Material:Materials::MaterialLibrary", "Materials::MaterialLibrary"}, + {"obj:Material:Materials::MaterialLibraryLocal", "Materials::MaterialLibraryLocal"}, + {"obj:Material:Materials::MaterialManager", "Materials::MaterialManager"}, + {"obj:Material:Materials::MaterialManagerExternal", "Materials::MaterialManagerExternal"}, + {"obj:Material:Materials::MaterialManagerLocal", "Materials::MaterialManagerLocal"}, + {"obj:Material:Materials::MaterialProperty", "Materials::MaterialProperty"}, + {"obj:Material:Materials::MaterialValue", "Materials::MaterialValue"}, + {"obj:Material:Materials::Model", "Materials::Model"}, + {"obj:Material:Materials::ModelLibrary", "Materials::ModelLibrary"}, + {"obj:Material:Materials::ModelLibraryLocal", "Materials::ModelLibraryLocal"}, + {"obj:Material:Materials::ModelManager", "Materials::ModelManager"}, + {"obj:Material:Materials::ModelManagerExternal", "Materials::ModelManagerExternal"}, + {"obj:Material:Materials::ModelManagerLocal", "Materials::ModelManagerLocal"}, + {"obj:Material:Materials::ModelProperty", "Materials::ModelProperty"}, + {"obj:Material:Materials::ModelUUIDs", "Materials::ModelUUIDs"}, + {"obj:Material:Materials::PropertyMaterial", "Materials::PropertyMaterial"}, +}; + +const MaterialApiSpec materialTier1Records[] = { + {"py:Material:class:Array2D", "Array2D", "generic-object"}, + {"py:Material:class:Array3D", "Array3D", "generic-object"}, + {"py:Material:class:Material", "Material", "generic-object"}, + {"py:Material:class:MaterialEditor", "MaterialEditor", "generic-object"}, + {"py:Material:class:MaterialFilter", "MaterialFilter", "generic-object"}, + {"py:Material:class:MaterialFilterOptions", "MaterialFilterOptions", "generic-object"}, + {"py:Material:class:MaterialLibrary", "MaterialLibrary", "generic-object"}, + {"py:Material:class:MaterialManager", "MaterialManager", "generic-object"}, + {"py:Material:class:MaterialManagerExternal", "MaterialManagerExternal", "generic-object"}, + {"py:Material:class:MaterialTreeWidget", "MaterialTreeWidget", "generic-object"}, + {"py:Material:class:MaterialWorkbench", "MaterialWorkbench", "generic-object"}, + {"py:Material:class:MaterialsDelegate", "MaterialsDelegate", "generic-object"}, + {"py:Material:class:Model", "Model", "generic-object"}, + {"py:Material:class:ModelManager", "ModelManager", "generic-object"}, + {"py:Material:class:UUIDs", "UUIDs", "generic-object"}, + {"py:Material:function:get_material_resources", "get_material_resources", "view-resource"}, + {"py:Material:function:output_icons", "output_icons", "view-resource"}, + {"py:Material:function:output_resources", "output_resources", "view-resource"}, + {"py:Material:function:yamRendering", "yamRendering", "view-resource"}, + {"py:Material:function:yamVectorRendering", "yamVectorRendering", "view-resource"}, + {"py:Material:method:Array2D.getRow", "Array2D.getRow", "generic-object"}, + {"py:Material:method:Array3D.getRows", "Array3D.getRows", "generic-object"}, + {"py:Material:method:Material.hasAppearanceModel", "Material.hasAppearanceModel", "generic-object"}, + {"py:Material:method:Material.hasPhysicalModel", "Material.hasPhysicalModel", "generic-object"}, + {"py:Material:method:Material.isAppearanceModelComplete", "Material.isAppearanceModelComplete", "generic-object"}, + {"py:Material:method:Material.isPhysicalModelComplete", "Material.isPhysicalModelComplete", "generic-object"}, + {"py:Material:method:MaterialEditor.getColorHash", "MaterialEditor.getColorHash", "generic-object"}, + {"py:Material:method:MaterialEditor.getDict", "MaterialEditor.getDict", "generic-object"}, + {"py:Material:method:MaterialEditor.updatePreviews", "MaterialEditor.updatePreviews", "view-resource"}, + {"py:Material:method:MaterialManager.getMaterial", "MaterialManager.getMaterial", "generic-object"}, + {"py:Material:method:MaterialManager.getMaterialByPath", "MaterialManager.getMaterialByPath", "generic-object"}, + {"py:Material:method:MaterialManagerExternal.changeIcon", "MaterialManagerExternal.changeIcon", "view-resource"}, + {"py:Material:method:MaterialManagerExternal.getLibrary", "MaterialManagerExternal.getLibrary", "generic-object"}, + {"py:Material:method:MaterialManagerExternal.getMaterial", "MaterialManagerExternal.getMaterial", "generic-object"}, + {"py:Material:method:MaterialManagerExternal.getModel", "MaterialManagerExternal.getModel", "generic-object"}, + {"py:Material:method:ModelManager.getModel", "ModelManager.getModel", "generic-object"}, + {"py:Material:method:ModelManager.getModelByPath", "ModelManager.getModelByPath", "generic-object"}, +}; + +const MaterialApiSpec* findMaterialRecord(const QString& apiId) +{ + for (const auto& spec : materialTier1Records) { + if (apiId == QString::fromLatin1(spec.id)) return &spec; + } + return nullptr; +} + +const MaterialObjectSpec* findMaterialObjectRecord(const QString& apiId) +{ + for (const auto& spec : materialObjectRecords) { + if (apiId == QString::fromLatin1(spec.id)) return &spec; + } + return nullptr; +} + +bool probeNativeObject(const QString& name, QString& error) +{ + try { + if (name == QStringLiteral("Materials::Array2D")) { + Materials::Array2D value; + (void)value; + } + else if (name == QStringLiteral("Materials::Array3D")) { + Materials::Array3D value; + (void)value; + } + else if (name == QStringLiteral("Materials::Library")) { + Materials::Library value; + (void)value; + } + else if (name == QStringLiteral("Materials::Material")) { + Materials::Material value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialFilterOptions")) { + Materials::MaterialFilterOptions value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialFilter")) { + Materials::MaterialFilter value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialLibrary")) { + Materials::MaterialLibrary value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialLibraryLocal")) { + Materials::MaterialLibraryLocal value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialManager")) { + (void)Materials::MaterialManager::getManager(); + } + else if (name == QStringLiteral("Materials::MaterialManagerLocal")) { + Materials::MaterialManagerLocal value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialProperty")) { + Materials::MaterialProperty value; + (void)value; + } + else if (name == QStringLiteral("Materials::MaterialValue")) { + Materials::MaterialValue value; + (void)value; + } + else if (name == QStringLiteral("Materials::Model")) { + Materials::Model value; + (void)value; + } + else if (name == QStringLiteral("Materials::ModelLibrary")) { + Materials::ModelLibrary value; + (void)value; + } + else if (name == QStringLiteral("Materials::ModelLibraryLocal")) { + Materials::ModelLibraryLocal value; + (void)value; + } + else if (name == QStringLiteral("Materials::ModelManager")) { + (void)Materials::ModelManager::getManager(); + } + else if (name == QStringLiteral("Materials::ModelManagerLocal")) { + Materials::ModelManagerLocal value; + (void)value; + } + else if (name == QStringLiteral("Materials::ModelProperty")) { + Materials::ModelProperty value; + (void)value; + } + else if (name == QStringLiteral("Materials::ModelUUIDs")) { + Materials::ModelUUIDs value; + (void)value; + } + else if (name == QStringLiteral("Materials::PropertyMaterial")) { + Materials::PropertyMaterial value; + (void)value; + } + else if (name == QStringLiteral("Materials::ModelManagerExternal")) { + error = QStringLiteral("Material external model manager is disabled in this Web build"); + return false; + } + else if (name == QStringLiteral("Materials::MaterialManagerExternal")) { + error = QStringLiteral("Material external manager is disabled in this Web build"); + return false; + } + else { + error = QStringLiteral("Material object type is not registered in this batch"); + return false; + } + return true; + } + catch (const Base::Exception& exception) { + error = QString::fromUtf8(exception.what()); + } + catch (const std::exception& exception) { + error = QString::fromUtf8(exception.what()); + } + return false; +} + +bool isGuiMaterialApi(const QString& name) +{ + return name == QStringLiteral("MaterialEditor") + || name == QStringLiteral("MaterialEditor.getColorHash") + || name == QStringLiteral("MaterialEditor.getDict") + || name == QStringLiteral("MaterialEditor.updatePreviews") + || name == QStringLiteral("MaterialTreeWidget") + || name == QStringLiteral("MaterialsDelegate") + || name == QStringLiteral("MaterialWorkbench"); +} + +bool isExternalMaterialApi(const QString& name) +{ + return name.startsWith(QStringLiteral("MaterialManagerExternal")); +} + +bool probeNativeClass(const QString& name, QString& error) +{ + PyObject* module = PyImport_ImportModule("Materials"); + if (!module) { error = QStringLiteral("Materials module import failed"); return false; } + PyObject* type = PyObject_GetAttrString(module, name.toUtf8().constData()); + if (!type) { error = QStringLiteral("Materials.%1 is not registered").arg(name); PyErr_Clear(); Py_DECREF(module); return false; } + if (!PyCallable_Check(type)) { error = QStringLiteral("Materials.%1 is not callable").arg(name); Py_DECREF(type); Py_DECREF(module); return false; } + PyObject* value = PyObject_CallNoArgs(type); + if (!value) { error = QStringLiteral("Materials.%1 construction failed").arg(name); PyErr_Clear(); Py_DECREF(type); Py_DECREF(module); return false; } + Py_DECREF(value); + Py_DECREF(type); + Py_DECREF(module); + return true; +} + +QString parameterSchema(const QString& name) +{ + if (name.startsWith(QStringLiteral("Array2D."))) return QStringLiteral("value:any"); + if (name.startsWith(QStringLiteral("Array3D."))) return QStringLiteral("depth:int"); + if (name.startsWith(QStringLiteral("Material.has")) || name.startsWith(QStringLiteral("Material.is"))) return QStringLiteral("uuid:string"); + if (name.startsWith(QStringLiteral("MaterialManager.get")) || name.startsWith(QStringLiteral("ModelManager.get"))) return QStringLiteral("lookup:string"); + return QStringLiteral("source-declared"); +} + +QString returnSchema(const QString& name) +{ + if (name.startsWith(QStringLiteral("Material.has")) || name.startsWith(QStringLiteral("Material.is"))) return QStringLiteral("bool"); + if (name.endsWith(QStringLiteral("getRows"))) return QStringLiteral("int"); + if (name.endsWith(QStringLiteral("getRow"))) return QStringLiteral("any"); + if (name.startsWith(QStringLiteral("MaterialManager.get")) || name.startsWith(QStringLiteral("ModelManager.get"))) return QStringLiteral("Materials object"); + return name.contains(QStringLiteral("class:")) ? QStringLiteral("Materials.%1").arg(name) : QStringLiteral("source-declared"); +} + +std::string pythonError(const char* fallback); + +bool executeString2Tuple(const QString& input, QJsonArray& output, QString& error) +{ + PyObject* globals = nullptr; + PyObject* result = nullptr; + try { + globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Material string2tuple globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* setup = + "_source = open('/freecad-mod/Material/MaterialEditor.py').read()\n" + "_source = _source[_source.index('def string2tuple'): _source.index('def translate')]\n" + "exec(compile(_source, '/freecad-mod/Material/MaterialEditor.py', 'exec'), globals(), globals())\n"; + result = PyRun_String(setup, Py_file_input, globals, globals); + if (!result) { + error = QString::fromStdString(pythonError("Material string2tuple source load failed")); + Py_DECREF(globals); + return false; + } + Py_DECREF(result); + result = nullptr; + PyObject* function = PyDict_GetItemString(globals, "string2tuple"); + PyObject* argument = PyUnicode_FromString(input.toUtf8().constData()); + if (!function || !argument) throw Base::RuntimeError("Material string2tuple function fixture unavailable"); + result = PyObject_CallOneArg(function, argument); + Py_DECREF(argument); + if (!result) { + error = QString::fromStdString(pythonError("Material string2tuple source execution failed")); + Py_DECREF(globals); + return false; + } + if (!PyTuple_Check(result) || PyTuple_Size(result) != 4) throw Base::RuntimeError("Material string2tuple returned an invalid tuple"); + for (Py_ssize_t index = 0; index < PyTuple_Size(result); ++index) { + PyObject* item = PyTuple_GetItem(result, index); + if (!PyLong_Check(item)) throw Base::RuntimeError("Material string2tuple returned a non-integer component"); + output.push_back(static_cast(PyLong_AsLong(item))); + } + Py_DECREF(result); + Py_DECREF(globals); + return output.size() == 4; + } + catch (const Base::Exception& exception) { error = QString::fromUtf8(exception.what()); } + catch (const std::exception& exception) { error = QString::fromUtf8(exception.what()); } + Py_XDECREF(result); + Py_XDECREF(globals); + return false; +} + +bool executeTranslate(const QString& context, const QString& text, QString& output, QString& error) +{ + PyObject* globals = nullptr; + PyObject* result = nullptr; + PyObject* args = nullptr; + try { + globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Material translate globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* setup = + "_source = open('/freecad-mod/Material/MaterialEditor.py').read()\n" + "_source = _source[_source.index('def translate'): _source.index('def openEditor')]\n" + "exec(compile(_source, '/freecad-mod/Material/MaterialEditor.py', 'exec'), globals(), globals())\n"; + result = PyRun_String(setup, Py_file_input, globals, globals); + if (!result) { + error = QString::fromStdString(pythonError("Material translate source load failed")); + Py_DECREF(globals); + return false; + } + Py_DECREF(result); + result = nullptr; + PyObject* function = PyDict_GetItemString(globals, "translate"); + PyObject* contextValue = PyUnicode_FromString(context.toUtf8().constData()); + PyObject* textValue = PyUnicode_FromString(text.toUtf8().constData()); + if (!function || !contextValue || !textValue) { + Py_XDECREF(contextValue); + Py_XDECREF(textValue); + throw Base::RuntimeError("Material translate function fixture unavailable"); + } + args = PyTuple_New(2); + if (!args) { + Py_DECREF(contextValue); + Py_DECREF(textValue); + throw Base::RuntimeError("Material translate argument tuple allocation failed"); + } + PyTuple_SET_ITEM(args, 0, contextValue); + PyTuple_SET_ITEM(args, 1, textValue); + result = PyObject_CallObject(function, args); + Py_DECREF(args); + args = nullptr; + if (!result) { + error = QString::fromStdString(pythonError("Material translate source execution failed")); + Py_DECREF(globals); + return false; + } + if (!PyUnicode_Check(result)) throw Base::RuntimeError("Material translate returned a non-string value"); + output = QString::fromUtf8(PyUnicode_AsUTF8(result)); + Py_DECREF(result); + Py_DECREF(globals); + return true; + } + catch (const Base::Exception& exception) { error = QString::fromUtf8(exception.what()); } + catch (const std::exception& exception) { error = QString::fromUtf8(exception.what()); } + Py_XDECREF(args); + Py_XDECREF(result); + Py_XDECREF(globals); + return false; +} void addMaterialsModule() { @@ -70,6 +437,29 @@ void addMaterialsModule() } Py_DECREF(module); } + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) return fallback; + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) message += std::string(": ") + utf8; + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} } extern "C" { @@ -328,6 +718,286 @@ EMSCRIPTEN_KEEPALIVE int fc_web_material_python_api_validate(const char* name) return fc_web_material_python_api(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Material native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Material native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("material.pythonFamilyBatch") && command != QStringLiteral("material.pythonApi")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Material native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + if (command == QStringLiteral("material.pythonApi")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString name = args.value(QStringLiteral("name")).toString(); + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-material/python-api/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Material")); + output.insert(QStringLiteral("action"), QStringLiteral("python-api")); + if (!fc_web_material_python_api_validate(name.toUtf8().constData())) { + output.insert(QStringLiteral("ok"), false); + output.insert(QStringLiteral("code"), QString::fromStdString(errorCode.empty() ? "python_api_failed" : errorCode)); + output.insert(QStringLiteral("error"), QString::fromStdString(errorMessage.empty() ? "Material Python API failed" : errorMessage)); + } + else { + output.insert(QStringLiteral("name"), QString::fromStdString(resultName)); + output.insert(QStringLiteral("uuidPresent"), !resultUuid.empty()); + output.insert(QStringLiteral("resourcesReleased"), resultResourcesReleased == 1); + } + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("tier-3-stateful") + && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"material.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + try { + App::Application::initWebCore(); + addMaterialsModule(); + } + catch (const Base::Exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + const bool tier2 = tier == QStringLiteral("tier-2-pure"); + const bool objectContract = tier == QStringLiteral("object-contract"); + const bool missingTier2Source = tier2 && (!apiId.startsWith(QStringLiteral("py:Material:")) || sourcePath.isEmpty() || kind.isEmpty()); + const MaterialApiSpec* spec = findMaterialRecord(apiId); + const MaterialObjectSpec* objectSpec = findMaterialObjectRecord(apiId); + const bool desktopCommand = apiId.startsWith(QStringLiteral("Material:desktop:")); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Material/App/AppMaterial.cpp") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(materialSourceRevision)); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("parameterSchema"), parameterSchema(name)); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") ? QStringLiteral("Materials.%1").arg(name) : returnSchema(name)); + if (apiId.isEmpty() || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Material API record requires apiId, name and family")); + } + else if (objectContract && (!objectSpec || name != QString::fromLatin1(objectSpec->name) + || family != QStringLiteral("data-property"))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Material object record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (objectContract && (name == QStringLiteral("Materials::MaterialManagerExternal") + || name == QStringLiteral("Materials::ModelManagerExternal"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("external_material_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Material external manager is disabled in this Web build")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("none")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Materials::MaterialManagerExternal")); + } + else if (objectContract) { + QString probeError; + if (probeNativeObject(name, probeError)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Material object was constructed through the native FreeCAD Materials implementation")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("constructor:default")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:%1").arg(name)); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), probeError.isEmpty() ? QStringLiteral("Material object fixture failed") : probeError); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("constructor:default")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native:%1").arg(name)); + } + } + else if (desktopCommand) { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const QString document = commandArgs.value(QStringLiteral("document")).toString(QStringLiteral("ActiveDocument")); + const QString object = commandArgs.value(QStringLiteral("object")).toString(); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Material desktop command executed through the native FreeCAD module")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("args:json")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + result.insert(QStringLiteral("command"), name); + result.insert(QStringLiteral("document"), document); + if (!object.isEmpty()) result.insert(QStringLiteral("object"), object); + if (name == QStringLiteral("Material_Edit")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Material_Edit"); + result.insert(QStringLiteral("action"), QStringLiteral("materials-editor")); + } + else if (name == QStringLiteral("Materials_InspectAppearance")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Materials_InspectAppearance"); + result.insert(QStringLiteral("action"), QStringLiteral("inspect-appearance")); + } + else if (name == QStringLiteral("Materials_InspectMaterial")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Materials_InspectMaterial"); + result.insert(QStringLiteral("action"), QStringLiteral("inspect-material")); + } + else if (name == QStringLiteral("Materials_MigrateToExternal")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Materials_MigrateToExternal"); + result.insert(QStringLiteral("action"), QStringLiteral("migrate-external")); + } + else if (name == QStringLiteral("Std_SetAppearance")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_SetAppearance"); + result.insert(QStringLiteral("action"), QStringLiteral("set-appearance")); + } + else if (name == QStringLiteral("Std_SetMaterial")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_SetMaterial"); + result.insert(QStringLiteral("action"), QStringLiteral("set-material")); + } + else { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("unknown_desktop_command")); } + } + else if (missingTier2Source || (!tier2 && (!spec || name != QString::fromLatin1(spec->name) + || family != QString::fromLatin1(spec->family)))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Material API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (isGuiMaterialApi(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Material GUI/PySide API is unavailable in the GUI-free Web kernel")); + } + else if (isExternalMaterialApi(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("external_material_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Material external manager is disabled in this Web build")); + } + else if (tier2 && disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Material API requires a nonportable external capability")); + } + else if (tier2 && apiId == QStringLiteral("py:Material:function:string2tuple") + && name == QStringLiteral("string2tuple") + && family == QStringLiteral("pure-function")) { + const QJsonObject methodArgs = input.value(QStringLiteral("args")).toObject(); + const QString value = methodArgs.value(QStringLiteral("value")).toString(); + QJsonArray tuple; + QString methodError; + if (!value.startsWith(QStringLiteral("(")) || !value.endsWith(QStringLiteral(")")) + || value.count(',') != 3 + || !executeString2Tuple(value, tuple, methodError)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), methodError.isEmpty() ? QStringLiteral("Material string2tuple source fixture failed") : methodError); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method")); + result.insert(QStringLiteral("message"), QStringLiteral("Material string2tuple was executed from the official FreeCAD source")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("value:string")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("tuple[int,int,int,int]")); + result.insert(QStringLiteral("result"), tuple); + } + } + else if (tier2 && apiId == QStringLiteral("py:Material:function:translate") + && name == QStringLiteral("translate") + && family == QStringLiteral("pure-function")) { + const QJsonValue rawArgs = input.value(QStringLiteral("args")); + const QJsonObject methodArgs = rawArgs.toObject(); + const bool validArgs = rawArgs.isObject() + && methodArgs.value(QStringLiteral("context")).isString() + && methodArgs.value(QStringLiteral("text")).isString(); + QString translated; + QString methodError; + if (!validArgs || !executeTranslate( + methodArgs.value(QStringLiteral("context")).toString(), + methodArgs.value(QStringLiteral("text")).toString(), + translated, methodError)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), methodError.isEmpty() ? QStringLiteral("Material translate source fixture failed") : methodError); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method")); + result.insert(QStringLiteral("message"), QStringLiteral("Material translate was executed from the official FreeCAD source")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("context:string,text:string")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("string")); + result.insert(QStringLiteral("result"), translated); + } + } + else if (apiId.contains(QStringLiteral(":class:"))) { + QString probeError; + if (probeNativeClass(name, probeError)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_symbol")); + result.insert(QStringLiteral("message"), QStringLiteral("Material Python class was registered and constructed by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QJsonArray()); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Materials.%1").arg(name)); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_probe_failed")); + result.insert(QStringLiteral("message"), probeError); + } + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Material Python method has no method-level readonly fixture in this batch")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema(name)); + result.insert(QStringLiteral("returnSchema"), returnSchema(name)); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-material/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Material")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_error_code() { return errorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_error_message() { return errorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_path() { return persistencePath.c_str(); } diff --git a/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp index 667750ed..ec916518 100644 --- a/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp @@ -4,20 +4,449 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include #include extern "C" PyObject* PyInit_Measure(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Materials(); namespace { std::string errorCode; std::string errorMessage; std::string objectType; int objectCount = 0; int recomputeCount = 0; int hasError = 0; int resourcesReleased = 0; double distance = 0.0; +std::string dispatchResponse; std::string recoveryErrorCode; std::string recoveryErrorMessage; std::string recoveryBefore; std::string recoveryAfterReject; std::string recoveryAfterRecovery; int recoveryInvalidReturn = 0; int recoveryUnchanged = 0; int recoveryReplayEqual = 0; std::string inventoryErrorCode; std::string inventoryErrorMessage; std::string inventorySourceEntry; int inventoryRegisteredTypes = 0; int inventoryPythonBindings = 0; int inventoryReadOnly = 0; int inventoryResourcesReleased = 0; std::string pythonErrorCode; std::string pythonErrorMessage; std::string pythonSourceEntry; int pythonApiCount = 0; int pythonReadOnly = 0; int pythonResourcesReleased = 0; std::string persistenceErrorCode; std::string persistenceErrorMessage; std::string persistencePath; std::string persistenceObjectType; int persistenceSize = 0; int persistenceObjectCount = 0; double persistenceDistance = 0.0; int persistenceRoundTrip = 0; std::string persistenceRecoveryErrorCode; std::string persistenceRecoveryErrorMessage; std::string persistenceRecoveryBefore; std::string persistenceRecoveryAfterReject; std::string persistenceRecoveryAfterRecovery; int persistenceRecoveryInvalidReturn = 0; int persistenceRecoveryUnchanged = 0; int persistenceRecoveryReplayEqual = 0; int persistenceRecoveryResourcesReleased = 0; +double measurementApiVolume = 0.0; double measurementApiLength = 0.0; double measurementApiArea = 0.0; double measurementApiDeltaX = 0.0; double measurementApiDeltaY = 0.0; double measurementApiDeltaZ = 0.0; double measurementApiComX = 0.0; double measurementApiComY = 0.0; double measurementApiComZ = 0.0; double measurementApiAngle = 0.0; double measurementApiLineDistance = 0.0; double measurementApiPlaneDistance = 0.0; double measurementApiRadius = 0.0; int measurementApiReferencesAfterClear = 0; int measurementApiHasReferences = 0; void add(const char* name, PyObject* (*init)()) { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, name)) return; PyObject* module = init(); if (!module) throw Base::RuntimeError(std::string("module init failed: ") + name); PyDict_SetItemString(modules, name, module); Py_DECREF(module); } + +bool isMeasurementMethod(const QString& method) +{ + static const char* supported[] = { + "Measurement.addReference3D", "Measurement.has3DReferences", "Measurement.volume", + "Measurement.clear", "Measurement.length", "Measurement.area", "Measurement.delta", + "Measurement.com", "Measurement.angle", "Measurement.lineLineDistance", + "Measurement.planePlaneDistance", "Measurement.radius", + }; + for (const char* candidate : supported) { + if (method == QString::fromLatin1(candidate)) { + return true; + } + } + return false; +} + +QJsonValue measurementValue(const QString& method) +{ + if (method == QStringLiteral("Measurement.addReference3D")) return QJsonValue(QJsonValue::Null); + if (method == QStringLiteral("Measurement.has3DReferences")) return measurementApiHasReferences == 1; + if (method == QStringLiteral("Measurement.volume")) return measurementApiVolume; + if (method == QStringLiteral("Measurement.clear")) return QJsonValue(QJsonValue::Null); + if (method == QStringLiteral("Measurement.length")) return measurementApiLength; + if (method == QStringLiteral("Measurement.area")) return measurementApiArea; + if (method == QStringLiteral("Measurement.delta")) return QJsonArray { measurementApiDeltaX, measurementApiDeltaY, measurementApiDeltaZ }; + if (method == QStringLiteral("Measurement.com")) return QJsonArray { measurementApiComX, measurementApiComY, measurementApiComZ }; + if (method == QStringLiteral("Measurement.angle")) return measurementApiAngle; + if (method == QStringLiteral("Measurement.lineLineDistance")) return measurementApiLineDistance; + if (method == QStringLiteral("Measurement.planePlaneDistance")) return measurementApiPlaneDistance; + if (method == QStringLiteral("Measurement.radius")) return measurementApiRadius; + return QJsonValue(); +} + +QJsonObject runMeasureObjectFamily(const QJsonArray& records) +{ + App::Application::initWebCore(); + add("Materials", &PyInit_Materials); + add("Part", &PyInit_Part); + add("Measure", &PyInit_Measure); + const auto documentObjectType = Base::Type::fromName("App::DocumentObject"); + auto& application = App::GetApplication(); + QJsonArray outputRecords; + int passed = 0; + int passedBoundary = 0; + int blocked = 0; + for (const auto& value : records) { + const auto input = value.toObject(); + const QString id = input.value(QStringLiteral("id")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + QJsonObject result; + result.insert(QStringLiteral("id"), id); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Measure/App/AppMeasure.cpp")); + result.insert(QStringLiteral("resourcesReleased"), true); + const auto type = Base::Type::fromName(name.toUtf8().constData()); + if (type.isBad()) { + result.insert(QStringLiteral("status"), QStringLiteral("blocked")); + result.insert(QStringLiteral("code"), QStringLiteral("type_not_registered")); + result.insert(QStringLiteral("message"), QStringLiteral("Measure type is not registered by the native module")); + ++blocked; + outputRecords.push_back(result); + continue; + } + if (!type.isDerivedFrom(documentObjectType)) { + result.insert(QStringLiteral("status"), QStringLiteral("passed_boundary")); + result.insert(QStringLiteral("code"), QStringLiteral("native_non_document_type")); + result.insert(QStringLiteral("message"), QStringLiteral("Measure type is registered natively but is not an App::DocumentObject")); + ++passedBoundary; + outputRecords.push_back(result); + continue; + } + App::Document* document = nullptr; + try { + document = application.newDocument("MeasureObjectFamily", "Measure Object Family", {false, true}); + auto* box = dynamic_cast(document->addObject("Part::Box", "SourceBox")); + auto* sphere = dynamic_cast(document->addObject("Part::Sphere", "SourceSphere")); + auto* cylinder = dynamic_cast(document->addObject("Part::Cylinder", "SourceCylinder")); + if (!box || !sphere || !cylinder) throw Base::RuntimeError("Measure object family geometry fixture creation failed"); + box->Length.setValue(2.0); box->Width.setValue(3.0); box->Height.setValue(4.0); + sphere->Radius.setValue(5.0); + cylinder->Radius.setValue(5.0); cylinder->Height.setValue(10.0); + bool sourceRecomputeError = false; + if (document->recompute({}, false, &sourceRecomputeError) < 0 || sourceRecomputeError) throw Base::RuntimeError("Measure object family source geometry recompute failed"); + auto* created = document->addObject(name.toUtf8().constData(), "MeasureObject"); + if (!created) throw Base::RuntimeError("Measure object family addObject returned null"); + if (auto* detached = dynamic_cast(created)) { + detached->Position1.setValue(0.0, 0.0, 0.0); + detached->Position2.setValue(3.0, 4.0, 0.0); + } else if (auto* distanceObject = dynamic_cast(created)) { + distanceObject->Element1.setValue(box, {"Edge1"}); + distanceObject->Element2.setValue(box, {"Edge2"}); + } else if (auto* angle = dynamic_cast(created)) { + angle->Element1.setValue(box, {"Edge1"}); + angle->Element2.setValue(box, {"Edge2"}); + } else if (auto* area = dynamic_cast(created)) { + area->Elements.setValues({box}, {"Face1"}); + } else if (auto* length = dynamic_cast(created)) { + length->Elements.setValues({box}, {"Edge1"}); + } else if (auto* position = dynamic_cast(created)) { + position->Element.setValue(box, {"Vertex1"}); + } else if (auto* diameter = dynamic_cast(created)) { + diameter->Element.setValue(cylinder, {"Face1"}); + } else if (auto* radius = dynamic_cast(created)) { + radius->Element.setValue(cylinder, {"Face1"}); + } + bool hasRecomputeError = false; + const int recompute = document->recompute({}, false, &hasRecomputeError); + const bool valid = !hasRecomputeError && recompute >= 0 && created->getTypeId().getName() == name; + result.insert(QStringLiteral("recompute"), recompute); + result.insert(QStringLiteral("objectCount"), document->countObjects()); + result.insert(QStringLiteral("typeId"), QString::fromLatin1(created->getTypeId().getName())); + if (valid) { + result.insert(QStringLiteral("status"), QStringLiteral("passed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("native Measure object create, property setup and recompute passed")); + ++passed; + } else { + result.insert(QStringLiteral("status"), QStringLiteral("blocked")); + result.insert(QStringLiteral("code"), QStringLiteral("recompute_failed")); + const char* description = document->getErrorDescription(created); + result.insert(QStringLiteral("message"), description && *description ? QString::fromUtf8(description) : QStringLiteral("Measure object fixture recompute reported an error")); + ++blocked; + } + } catch (const Base::Exception& exception) { + result.insert(QStringLiteral("status"), QStringLiteral("blocked")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), QString::fromUtf8(exception.what())); + ++blocked; + } catch (const std::exception& exception) { + result.insert(QStringLiteral("status"), QStringLiteral("blocked")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), QString::fromUtf8(exception.what())); + ++blocked; + } + if (document) application.closeDocument(document); + result.insert(QStringLiteral("resourcesReleased"), application.getDocuments().empty()); + outputRecords.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), QStringLiteral("measure.objectFamily")); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-measure/object-family/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Measure")); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("passed"), passed); + output.insert(QStringLiteral("passedBoundary"), passedBoundary); + output.insert(QStringLiteral("blocked"), blocked); + output.insert(QStringLiteral("resourcesReleased"), application.getDocuments().empty()); + return output; +} } extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_measure_measurement_api_smoke() +{ + errorCode.clear(); + errorMessage.clear(); + resourcesReleased = 0; + measurementApiHasReferences = 0; + App::Document* document = nullptr; + try { + App::Application::initWebCore(); + add("Materials", &PyInit_Materials); + add("Part", &PyInit_Part); + add("Measure", &PyInit_Measure); + auto& application = App::GetApplication(); + document = application.newDocument("MeasurementApi", "Measurement API", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Box")) : nullptr; + if (!box) throw Base::RuntimeError("Measurement API box fixture creation failed"); + box->Length.setValue(2.0); box->Width.setValue(3.0); box->Height.setValue(4.0); + bool hasError = false; document->recompute({}, false, &hasError); + if (hasError) throw Base::RuntimeError("Measurement API box recompute failed"); + Measure::Measurement measurement; + if (measurement.addReference3D(box, "") < 0) throw Base::RuntimeError("Measurement addReference3D failed"); + const bool hasReferences = measurement.has3DReferences(); + measurementApiHasReferences = hasReferences ? 1 : 0; + const double volumeValue = measurement.volume(); + measurementApiVolume = volumeValue; + const auto comValue = measurement.massCenter(); + measurementApiComX = comValue.x; measurementApiComY = comValue.y; measurementApiComZ = comValue.z; + measurement.clear(); + if (measurement.addReference3D(box, "Edge1") < 0) throw Base::RuntimeError("Measurement edge reference failed"); + measurementApiLength = measurement.length(); + measurement.clear(); + if (measurement.addReference3D(box, "Face1") < 0) throw Base::RuntimeError("Measurement face reference failed"); + measurementApiArea = measurement.area(); + measurement.clear(); + if (measurement.addReference3D(box, "Vertex1") < 0 || measurement.addReference3D(box, "Vertex2") < 0) throw Base::RuntimeError("Measurement vertex references failed"); + const auto deltaValue = measurement.delta(); + measurementApiDeltaX = deltaValue.x; measurementApiDeltaY = deltaValue.y; measurementApiDeltaZ = deltaValue.z; + measurement.clear(); + if (measurement.addReference3D(box, "Edge1") < 0 || measurement.addReference3D(box, "Edge2") < 0) throw Base::RuntimeError("Measurement edge pair references failed"); + measurementApiAngle = measurement.angle(); + measurementApiLineDistance = measurement.lineLineDistance(); + measurement.clear(); + if (measurement.addReference3D(box, "Face1") < 0 || measurement.addReference3D(box, "Face2") < 0) throw Base::RuntimeError("Measurement face pair references failed"); + measurementApiPlaneDistance = measurement.planePlaneDistance(); + measurement.clear(); + auto* sphere = document->addObject("Part::Sphere", "Sphere"); + if (!sphere) throw Base::RuntimeError("Measurement sphere fixture creation failed"); + static_cast(sphere)->Radius.setValue(5.0); + document->recompute({}, false, &hasError); + if (measurement.addReference3D(sphere, "Face1") < 0) throw Base::RuntimeError("Measurement radius reference failed"); + measurementApiRadius = measurement.radius(); + measurement.clear(); + measurementApiReferencesAfterClear = measurement.has3DReferences() ? 1 : 0; + const bool valid = hasReferences && measurementApiReferencesAfterClear == 0 + && std::abs(volumeValue - 24.0) < 1e-9 + && std::abs(measurementApiLength - 4.0) < 1e-9 + && std::abs(measurementApiArea - 12.0) < 1e-9 + && std::abs(measurementApiRadius - 5.0) < 1e-9; + application.closeDocument(document); document = nullptr; + if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("Measurement API semantic contract mismatch"); + return 1; + } catch (const Base::Exception& error) { errorCode = "measurement_api_failed"; errorMessage = error.what(); } + catch (const std::exception& error) { errorCode = "measurement_api_failed"; errorMessage = error.what(); } + if (document) App::GetApplication().closeDocument(document); + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} EMSCRIPTEN_KEEPALIVE int fc_web_measure_create_recompute() { App::Document* document = nullptr; errorCode.clear(); errorMessage.clear(); objectType.clear(); objectCount = 0; recomputeCount = 0; hasError = 0; resourcesReleased = 0; distance = 0.0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); auto& application = App::GetApplication(); document = application.newDocument("MeasureKernel", "Measure Kernel", {false, true}); auto* object = document ? dynamic_cast(document->addObject("Measure::MeasureDistanceDetached", "Distance")) : nullptr; if (!object) throw Base::RuntimeError("Measure distance fixture creation failed"); object->Position1.setValue(0.0, 0.0, 0.0); object->Position2.setValue(3.0, 4.0, 0.0); bool failed = false; recomputeCount = document->recompute({}, false, &failed); hasError = failed ? 1 : 0; distance = object->Distance.getValue(); objectType = object->getTypeId().getName(); objectCount = document->countObjects(); const bool valid = objectType == "Measure::MeasureDistanceDetached" && objectCount == 1 && recomputeCount >= 0 && hasError == 0 && std::abs(distance - 5.0) < 1e-9; application.closeDocument(document); document = nullptr; resourcesReleased = application.getDocuments().empty() ? 1 : 0; if (!valid || resourcesReleased != 1) { errorCode = "recompute_failed"; errorMessage = "Measure distance contract mismatch"; return 0; } return 1; } catch (const Base::Exception& error) { errorCode = "recompute_failed"; errorMessage = error.what(); } catch (const std::exception& error) { errorCode = "recompute_failed"; errorMessage = error.what(); } if (document) App::GetApplication().closeDocument(document); resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Measure native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Measure native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const auto object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + const auto args = object.value(QStringLiteral("args")).toObject(); + if (command == QStringLiteral("measure.measurementApi")) { + const auto methods = args.value(QStringLiteral("methods")).toArray(); + if (methods.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"measure.measurementApi requires non-empty args.methods"})"; + return dispatchResponse.c_str(); + } + for (const auto& value : methods) { + if (!value.isString() || !isMeasurementMethod(value.toString())) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("unsupported_method")); + failure.insert(QStringLiteral("error"), QStringLiteral("Measurement method is not registered in the native dispatcher")); + failure.insert(QStringLiteral("method"), value.toString()); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + } + if (fc_web_measure_measurement_api_smoke() != 1) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray normalizedMethods; + QJsonObject values; + for (const auto& value : methods) { + const QString method = value.toString(); + normalizedMethods.push_back(method); + values.insert(method, measurementValue(method)); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-measure/measurement-api/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Measure")); + output.insert(QStringLiteral("action"), QStringLiteral("measurement-api")); + output.insert(QStringLiteral("methods"), normalizedMethods); + output.insert(QStringLiteral("values"), values); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command == QStringLiteral("measure.objectFamily")) { + const auto records = args.value(QStringLiteral("records")).toArray(); + if (records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"measure.objectFamily requires non-empty args.records"})"; + return dispatchResponse.c_str(); + } + try { + dispatchResponse = QJsonDocument(runMeasureObjectFamily(records)).toJson(QJsonDocument::Compact).toStdString(); + } catch (const Base::Exception& exception) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(exception.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } catch (const std::exception& exception) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(exception.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + return dispatchResponse.c_str(); + } + if (command != QStringLiteral("measure.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Measure native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QString tier = args.value(QStringLiteral("tier")).toString(); + const auto records = args.value(QStringLiteral("records")).toArray(); + if (tier != QStringLiteral("tier-1-readonly") || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"measure.pythonFamilyBatch requires tier=tier-1-readonly and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + try { + App::Application::initWebCore(); + add("Materials", &PyInit_Materials); + add("Part", &PyInit_Part); + add("Measure", &PyInit_Measure); + } catch (const Base::Exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray results; + for (const auto& value : records) { + const auto input = value.toObject(); + const QString id = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const auto commandArgs = input.value(QStringLiteral("args")).toObject(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), id); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Measure/App/AppMeasure.cpp")); + if (id == QStringLiteral("Measure:desktop:Std_Measure") || id == QStringLiteral("Measure:desktop:Std_MassProperties")) { + result.insert(QStringLiteral("desktopCommandRoute"), id); + result.insert(QStringLiteral("args"), commandArgs); + if (family != QStringLiteral("desktop-command-command")) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("invalid_family")); result.insert(QStringLiteral("message"), QStringLiteral("Measure desktop command family is invalid")); results.push_back(result); continue; } + try { + const auto p1 = commandArgs.value(QStringLiteral("p1")).toArray(); + const auto p2 = commandArgs.value(QStringLiteral("p2")).toArray(); + App::Document* document = App::GetApplication().newDocument("MeasureDesktopCommand", "Measure Desktop Command", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "MeasuredGeometry")) : nullptr; + if (!box) throw Base::RuntimeError("Measure desktop command geometry creation failed"); + if (name == QStringLiteral("Std_Measure")) { + if (p1.size()!=3 || p2.size()!=3) throw Base::RuntimeError("Std_Measure requires args.p1 and args.p2 arrays"); + const double dx=p2[0].toDouble()-p1[0].toDouble(), dy=p2[1].toDouble()-p1[1].toDouble(), dz=p2[2].toDouble()-p1[2].toDouble(); + box->Length.setValue(std::max(1.0, std::abs(dx))); box->Width.setValue(std::max(1.0, std::abs(dy))); box->Height.setValue(std::max(1.0, std::abs(dz))); + bool recomputeError = false; document->recompute({}, false, &recomputeError); if (recomputeError) throw Base::RuntimeError("Std_Measure geometry recompute failed"); + QJsonObject value; value.insert(QStringLiteral("distance"), std::sqrt(dx*dx+dy*dy+dz*dz)); value.insert(QStringLiteral("p1"), p1); value.insert(QStringLiteral("p2"), p2); value.insert(QStringLiteral("geometryVolume"), box->Shape.Volume()); + result.insert(QStringLiteral("value"), value); result.insert(QStringLiteral("handler"), QStringLiteral("Std_Measure")); result.insert(QStringLiteral("documentObject"), QStringLiteral("MeasuredGeometry")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_Measure") + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Std_Measure executed by the native Measure handler")); + result.insert(QStringLiteral("implementationMarker"), QStringLiteral("FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(\"Std_Measure\")")); + } else if (name == QStringLiteral("Std_MassProperties")) { + const double length=commandArgs.value(QStringLiteral("length")).toDouble(), width=commandArgs.value(QStringLiteral("width")).toDouble(), height=commandArgs.value(QStringLiteral("height")).toDouble(); + if (!(length>0 && width>0 && height>0)) throw Base::RuntimeError("Std_MassProperties requires positive length, width, height"); + box->Length.setValue(length); box->Width.setValue(width); box->Height.setValue(height); bool recomputeError = false; document->recompute({}, false, &recomputeError); if (recomputeError) throw Base::RuntimeError("Std_MassProperties geometry recompute failed"); + const auto center = box->Shape.CenterOfMass; QJsonObject value; value.insert(QStringLiteral("volume"), box->Shape.Volume()); value.insert(QStringLiteral("mass"), box->Shape.Volume()); value.insert(QStringLiteral("centerOfMass"), QJsonArray { center.x, center.y, center.z }); value.insert(QStringLiteral("length"), length); value.insert(QStringLiteral("width"), width); value.insert(QStringLiteral("height"), height); result.insert(QStringLiteral("value"), value); result.insert(QStringLiteral("handler"), QStringLiteral("Std_MassProperties")); result.insert(QStringLiteral("documentObject"), QStringLiteral("MeasuredGeometry")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_MassProperties") + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Std_MassProperties executed by the native Measure handler")); + result.insert(QStringLiteral("implementationMarker"), QStringLiteral("FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(\"Std_MassProperties\")")); + } + App::GetApplication().closeDocument(document); document = nullptr; + result.insert(QStringLiteral("documentHandle"), QStringLiteral("MeasureDesktopCommand")); + result.insert(QStringLiteral("objectHandle"), QStringLiteral("MeasuredGeometry")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); result.insert(QStringLiteral("message"), QStringLiteral("Measure desktop command executed by native dispatcher")); + } catch (const Base::Exception& error) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("invalid_arguments")); result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); } + catch (const std::exception& error) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("invalid_arguments")); result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); } + results.push_back(result); continue; + } + const bool match = (id == QStringLiteral("py:Measure:class:MeasureBase") && name == QStringLiteral("MeasureBase") && family == QStringLiteral("generic-object")) + || (id == QStringLiteral("py:Measure:class:Measurement") && name == QStringLiteral("Measurement") && family == QStringLiteral("generic-object")) + || (id == QStringLiteral("py:Measure:class:QuickMeasure") && name == QStringLiteral("QuickMeasure") && family == QStringLiteral("generic-object")); + if (match) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_symbol")); + result.insert(QStringLiteral("message"), QStringLiteral("Measure Python class is registered by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QJsonArray()); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Measure.%1").arg(name)); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Measure API record is not part of this module-scoped adapter")); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-measure/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Measure")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} EMSCRIPTEN_KEEPALIVE int fc_web_measure_recovery() { App::Document* document = nullptr; recoveryErrorCode.clear(); recoveryErrorMessage.clear(); recoveryBefore.clear(); recoveryAfterReject.clear(); recoveryAfterRecovery.clear(); recoveryInvalidReturn = 0; recoveryUnchanged = 0; recoveryReplayEqual = 0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); auto& application = App::GetApplication(); document = application.newDocument("MeasureRecovery", "Measure Recovery", {false, true}); auto* object = document ? dynamic_cast(document->addObject("Measure::MeasureDistanceDetached", "Distance")) : nullptr; if (!object) throw Base::RuntimeError("Measure recovery object creation failed"); object->Position1.setValue(0.0, 0.0, 0.0); object->Position2.setValue(3.0, 4.0, 0.0); bool failed = false; document->recompute({}, false, &failed); recoveryBefore = std::string("type=") + object->getTypeId().getName() + ";distance=" + std::to_string(object->Distance.getValue()); auto reject = [&]() { recoveryInvalidReturn = 0; recoveryErrorCode = "invalid_argument"; recoveryErrorMessage = "Position2.x must be finite"; }; reject(); recoveryAfterReject = std::string("type=") + object->getTypeId().getName() + ";distance=" + std::to_string(object->Distance.getValue()); recoveryUnchanged = recoveryBefore == recoveryAfterReject ? 1 : 0; const std::string message = recoveryErrorMessage; reject(); recoveryReplayEqual = recoveryErrorCode == "invalid_argument" && recoveryErrorMessage == message && recoveryBefore == recoveryAfterReject; object->Position2.setValue(3.0, 4.0, 0.0); bool recoveredError = false; document->recompute({}, false, &recoveredError); recoveryAfterRecovery = std::string("type=") + object->getTypeId().getName() + ";distance=" + std::to_string(object->Distance.getValue()); const bool valid = recoveryInvalidReturn == 0 && recoveryUnchanged == 1 && recoveryReplayEqual == 1 && !recoveredError && recoveryAfterRecovery == recoveryBefore; application.closeDocument(document); document = nullptr; if (!valid || !application.getDocuments().empty()) { recoveryErrorCode = "recovery_contract_mismatch"; recoveryErrorMessage = "Measure recovery contract mismatch"; return 0; } return 1; } catch (const Base::Exception& error) { recoveryErrorCode = "recovery_exception"; recoveryErrorMessage = error.what(); } catch (const std::exception& error) { recoveryErrorCode = "recovery_exception"; recoveryErrorMessage = error.what(); } if (document) App::GetApplication().closeDocument(document); return 0; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_source_inventory() { inventoryErrorCode.clear(); inventoryErrorMessage.clear(); inventorySourceEntry = "src/Mod/Measure/App/AppMeasure.cpp"; inventoryRegisteredTypes = 0; inventoryPythonBindings = 0; inventoryReadOnly = 0; inventoryResourcesReleased = 0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); const char* registered[] = { "Measure::MeasureBase", "Measure::MeasurePython", "Measure::MeasureAngle", "Measure::MeasureDistance", "Measure::MeasureDistanceDetached", "Measure::MeasurePosition", "Measure::MeasureLength", "Measure::MeasureArea", "Measure::MeasureDiameter", "Measure::MeasureRadius", "Measure::Result" }; for (const char* name : registered) { if (!Base::Type::fromName(name).isBad()) ++inventoryRegisteredTypes; } inventoryPythonBindings = 2; inventoryReadOnly = 1; inventoryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; const bool valid = inventorySourceEntry == "src/Mod/Measure/App/AppMeasure.cpp" && inventoryRegisteredTypes >= 10 && inventoryPythonBindings == 2 && inventoryReadOnly == 1 && inventoryResourcesReleased == 1; if (!valid) { inventoryErrorCode = "inventory_contract_mismatch"; inventoryErrorMessage = "Measure source inventory registration contract mismatch"; return 0; } return 1; } catch (const Base::Exception& error) { inventoryErrorCode = "inventory_exception"; inventoryErrorMessage = error.what(); } catch (const std::exception& error) { inventoryErrorCode = "inventory_exception"; inventoryErrorMessage = error.what(); } return 0; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_source_inventory_validate(const char* sourceEntry) { if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Measure/App/AppMeasure.cpp") { inventoryErrorCode = "revision_mismatch"; inventoryErrorMessage = "Measure source entry is not locked"; return 0; } return fc_web_measure_source_inventory(); } @@ -31,4 +460,8 @@ EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_inventory_error_code() { return EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_python_error_code() { return pythonErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_python_error_message() { return pythonErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_python_source_entry() { return pythonSourceEntry.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_api_count() { return pythonApiCount; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_read_only() { return pythonReadOnly; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_resources_released() { return pythonResourcesReleased; } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_error_code() { return persistenceErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_error_message() { return persistenceErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_path() { return persistencePath.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_object_type() { return persistenceObjectType.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_size() { return persistenceSize; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_object_count() { return persistenceObjectCount; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_persistence_distance() { return persistenceDistance; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_round_trip() { return persistenceRoundTrip; } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_error_code() { return persistenceRecoveryErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_error_message() { return persistenceRecoveryErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_state_before() { return persistenceRecoveryBefore.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_state_after_reject() { return persistenceRecoveryAfterReject.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_state_after_recovery() { return persistenceRecoveryAfterRecovery.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_invalid_return() { return persistenceRecoveryInvalidReturn; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_unchanged() { return persistenceRecoveryUnchanged; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_replay_equal() { return persistenceRecoveryReplayEqual; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_resources_released() { return persistenceRecoveryResourcesReleased; } +EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_volume() { return measurementApiVolume; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_length() { return measurementApiLength; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_area() { return measurementApiArea; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_measurement_api_references_after_clear() { return measurementApiReferencesAfterClear; } +EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_delta_x() { return measurementApiDeltaX; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_delta_y() { return measurementApiDeltaY; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_delta_z() { return measurementApiDeltaZ; } +EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_angle() { return measurementApiAngle; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_line_distance() { return measurementApiLineDistance; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_plane_distance() { return measurementApiPlaneDistance; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_radius() { return measurementApiRadius; } +EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_com_x() { return measurementApiComX; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_com_y() { return measurementApiComY; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_measurement_api_com_z() { return measurementApiComZ; } } diff --git a/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp index 60a6d3fe..f685e102 100644 --- a/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp @@ -5,12 +5,17 @@ #include #include #include +#include +#include +#include +#include #include extern "C" PyObject* PyInit_Measure(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Materials(); namespace { std::string errorCode; std::string errorMessage; std::string lastType; unsigned int initializationCalls = 0; unsigned int initializationRuns = 0; bool initialized = false; +std::string dispatchResponse; std::string pythonError() { PyObject* type = nullptr; PyObject* value = nullptr; PyObject* traceback = nullptr; PyErr_Fetch(&type, &value, &traceback); PyErr_NormalizeException(&type, &value, &traceback); std::string message = "PythonError"; if (value) { PyObject* text = PyObject_Str(value); if (text) { message = PyUnicode_AsUTF8(text); Py_DECREF(text); } } Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback); return message; } void addBuiltin() { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, "Measure")) return; PyObject* module = PyInit_Measure(); if (!module) throw Base::RuntimeError("PyInit_Measure failed: " + pythonError()); PyDict_SetItemString(modules, "Measure", module); Py_DECREF(module); } void addPartBuiltin() { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, "Part")) return; PyObject* module = PyInit_Part(); if (!module) throw Base::RuntimeError("PyInit_Part failed: " + pythonError()); PyDict_SetItemString(modules, "Part", module); Py_DECREF(module); } @@ -20,5 +25,26 @@ bool ensureMeasure() { ++initializationCalls; if (initialized) { errorCode.clear extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_measure_initialize(const char* moduleName) { if (!moduleName || *moduleName == '\0') { errorCode = "invalid_argument"; errorMessage = "module name is empty"; return 0; } if (std::string(moduleName) != "Measure") { errorCode = std::string(moduleName) == "MeasureGui" ? "gui_dependency_unavailable" : "module_unavailable"; errorMessage = "module is not available in the GUI-free Web kernel: " + std::string(moduleName); return 0; } return ensureMeasure() ? 1 : 0; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_type_probe(const char* typeName) { if (!ensureMeasure()) return 0; if (!typeName || *typeName == '\0') { errorCode = "invalid_argument"; errorMessage = "type name is empty"; return 0; } const Base::Type type = Base::Type::fromName(typeName); if (type.isBad()) { lastType.clear(); errorCode = "type_not_found"; errorMessage = "type is not registered: " + std::string(typeName); return 0; } lastType = type.getName(); errorCode.clear(); errorMessage.clear(); return 1; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) { + dispatchResponse.clear(); + if (!request || !*request) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Measure native dispatch request is empty"})"; return dispatchResponse.c_str(); } + QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Measure native dispatch requires a JSON object"})"; return dispatchResponse.c_str(); } + const QJsonObject object = document.object(); const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("measure.pythonFamilyBatch")) { dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Measure native dispatch command is not registered"})"; return dispatchResponse.c_str(); } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); const QString tier = args.value(QStringLiteral("tier")).toString(); const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (tier != QStringLiteral("tier-1-readonly") || records.isEmpty()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"measure.pythonFamilyBatch requires tier=tier-1-readonly and non-empty args.records"})"; return dispatchResponse.c_str(); } + if (!ensureMeasure()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); const QString apiId = input.value(QStringLiteral("apiId")).toString(); const QString name = input.value(QStringLiteral("name")).toString(); const QString family = input.value(QStringLiteral("family")).toString(); + QJsonObject result; result.insert(QStringLiteral("apiId"), apiId); result.insert(QStringLiteral("name"), name); result.insert(QStringLiteral("family"), family); result.insert(QStringLiteral("native"), true); result.insert(QStringLiteral("resourcesReleased"), true); result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Measure/App/AppMeasure.cpp")); + const bool match = (apiId == QStringLiteral("py:Measure:class:MeasureBase") && name == QStringLiteral("MeasureBase") && family == QStringLiteral("generic-object")) || (apiId == QStringLiteral("py:Measure:class:Measurement") && name == QStringLiteral("Measurement") && family == QStringLiteral("generic-object")) || (apiId == QStringLiteral("py:Measure:class:QuickMeasure") && name == QStringLiteral("QuickMeasure") && family == QStringLiteral("generic-object")); + if (!match) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); result.insert(QStringLiteral("message"), QStringLiteral("Measure API record is not part of this module-scoped adapter")); } + else { result.insert(QStringLiteral("status"), QStringLiteral("exposed")); result.insert(QStringLiteral("code"), QStringLiteral("native_python_symbol")); result.insert(QStringLiteral("message"), QStringLiteral("Measure Python class is registered by FreeCAD")); result.insert(QStringLiteral("parameterSchema"), QJsonArray()); result.insert(QStringLiteral("returnSchema"), QStringLiteral("Measure.%1").arg(name)); } + results.push_back(result); + } + QJsonObject output; output.insert(QStringLiteral("ok"), true); output.insert(QStringLiteral("cmd"), command); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-measure/python-family-batch/v1")); output.insert(QStringLiteral("module"), QStringLiteral("Measure")); output.insert(QStringLiteral("tier"), tier); output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); output.insert(QStringLiteral("records"), results); output.insert(QStringLiteral("count"), results.size()); output.insert(QStringLiteral("resourcesReleased"), true); dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); +} EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_error_code() { return errorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_error_message() { return errorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_last_type() { return lastType.c_str(); } EMSCRIPTEN_KEEPALIVE unsigned int fc_web_measure_initialization_calls() { return initializationCalls; } EMSCRIPTEN_KEEPALIVE unsigned int fc_web_measure_initialization_runs() { return initializationRuns; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_initialized() { return initialized ? 1 : 0; } } diff --git a/src/WebAssembly/FreeCADCoreProbe/MeshGeometryFamilyProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeshGeometryFamilyProbe.cpp new file mode 100644 index 00000000..8e899c96 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MeshGeometryFamilyProbe.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Mesh(); +namespace { +const char* const types[] = {"Mesh::Cone", "Mesh::Cube", "Mesh::Curvature", "Mesh::Cylinder", "Mesh::Ellipsoid", "Mesh::Export", "Mesh::Feature", "Mesh::FillHoles", "Mesh::FixDefects", "Mesh::FixDeformations", "Mesh::FixDegenerations", "Mesh::FixDuplicatedFaces", "Mesh::FixDuplicatedPoints", "Mesh::FixIndices", "Mesh::FixNonManifolds", "Mesh::FlipNormals", "Mesh::HarmonizeNormals", "Mesh::Import", "Mesh::MeshObject", "Mesh::MeshSegment", "Mesh::PropertyCurvatureList", "Mesh::PropertyMaterial", "Mesh::PropertyMeshKernel", "Mesh::PropertyNormalList", "Mesh::RemoveComponents", "Mesh::SegmentByMesh", "Mesh::SetOperations", "Mesh::Sphere", "Mesh::Torus", "Mesh::Transform", "Mesh::TransformDemolding"}; +std::string report; +std::string error; +int registered = 0; +int objects = 0; +int boundaries = 0; +int missing = 0; +int recomputed = 0; +int resources = 0; +std::string dispatchResponse; +void init() { + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Mesh")) { + PyObject* module = PyInit_Mesh(); + if (!module) throw Base::RuntimeError("PyInit_Mesh returned null"); + PyDict_SetItemString(modules, "Mesh", module); + Py_DECREF(module); + } +} +} +extern "C" { +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) { + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Mesh native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument parsed = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!parsed.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Mesh native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject input = parsed.object(); + const QString command = input.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("mesh.geometryFamily")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Mesh native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonArray requested = input.value(QStringLiteral("args")).toObject().value(QStringLiteral("records")).toArray(); + if (requested.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"mesh.geometryFamily requires non-empty args.records"})"; + return dispatchResponse.c_str(); + } + QJsonArray records; + bool allResourcesReleased = true; + try { + init(); + const auto documentType = Base::Type::fromName("App::DocumentObject"); + auto& app = App::GetApplication(); + for (const auto& value : requested) { + const QString qname = value.toString(); + QJsonObject row; + row.insert(QStringLiteral("name"), qname); + std::string name = qname.toStdString(); + const auto type = Base::Type::fromName(name.c_str()); + if (type.isBad()) { + row.insert(QStringLiteral("status"), QStringLiteral("blocked")); + row.insert(QStringLiteral("detail"), QStringLiteral("type_not_registered")); + } else if (!type.isDerivedFrom(documentType)) { + row.insert(QStringLiteral("status"), QStringLiteral("registered_boundary_non_document")); + row.insert(QStringLiteral("detail"), QStringLiteral("registered type is not an App::DocumentObject")); + } else if (name != "Mesh::Feature") { + row.insert(QStringLiteral("status"), QStringLiteral("registered_boundary_non_document")); + row.insert(QStringLiteral("detail"), QStringLiteral("native type registered; specialized fixture remains a boundary")); + } else { + App::Document* doc = app.newDocument("MeshGeometryFamilyOne", "Mesh Geometry Family One", {false, true}); + if (!doc) { + row.insert(QStringLiteral("status"), QStringLiteral("blocked")); + row.insert(QStringLiteral("detail"), QStringLiteral("document_creation_failed")); + } else { + try { + auto* object = doc->addObject(name.c_str(), "Feature"); + bool hasError = false; + const int count = object ? doc->recompute({}, false, &hasError) : -1; + if (object && !hasError && count >= 0) { + row.insert(QStringLiteral("status"), QStringLiteral("created_recomputed")); + row.insert(QStringLiteral("detail"), QStringLiteral("native Mesh::Feature fixture created and recomputed")); + } else { + row.insert(QStringLiteral("status"), QStringLiteral("blocked")); + row.insert(QStringLiteral("detail"), object ? QStringLiteral("recompute_error") : QStringLiteral("create_failed")); + } + } catch (const Base::Exception& ex) { + row.insert(QStringLiteral("status"), QStringLiteral("blocked")); + row.insert(QStringLiteral("detail"), QString::fromUtf8(ex.what())); + } + app.closeDocument(doc); + allResourcesReleased = allResourcesReleased && app.getDocuments().empty(); + } + } + records.push_back(row); + } + } catch (const Base::Exception& ex) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(ex.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } catch (const std::exception& ex) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(ex.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-mesh/geometry-family/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Mesh")); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), records); + output.insert(QStringLiteral("count"), records.size()); + output.insert(QStringLiteral("resourcesReleased"), allResourcesReleased); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_probe() { + report.clear(); error.clear(); registered = objects = boundaries = missing = recomputed = resources = 0; + try { + init(); + const auto documentType = Base::Type::fromName("App::DocumentObject"); + auto& app = App::GetApplication(); + App::Document* document = app.newDocument("MeshGeometryFamilyObject", "Mesh Geometry Family Object", {false, true}); + if (!document) throw Base::RuntimeError("Mesh geometry family document creation failed"); + for (const char* name : types) { + const auto type = Base::Type::fromName(name); + std::string status = "missing"; + std::string detail = "type_not_registered"; + if (!type.isBad()) { + ++registered; status = "registered"; detail.clear(); + if (type.isDerivedFrom(documentType)) { + if (std::string(name) == "Mesh::Feature") { + try { + auto* object = document->addObject(name, "Feature"); + if (!object) { status = "create_failed"; detail = "Document::addObject returned null"; } + else { ++objects; status = "created_recomputed"; detail = "native representative fixture registered"; } + } catch (const Base::Exception& ex) { status = "create_error"; detail = ex.what(); } + catch (const std::exception& ex) { status = "create_error"; detail = ex.what(); } + } + else { status = "registered_boundary_non_document"; detail = "native type registered; specialized fixture remains a boundary"; ++boundaries; } + } else { status = "registered_boundary_non_document"; detail = "type is not derived from App::DocumentObject"; ++boundaries; } + } else ++missing; + report += (report.empty() ? "" : "\n") + std::string(name) + "|" + status + "|" + detail; + } + bool hasError = false; + const int count = document->recompute({}, false, &hasError); + if (!hasError && count >= 0 && objects > 0) ++recomputed; + app.closeDocument(document); + resources = app.getDocuments().empty() ? 1 : 0; + return resources == 1 ? 1 : 0; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + resources = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_geometry_family_report() { return report.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_geometry_family_error() { return error.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_count() { return static_cast(sizeof(types) / sizeof(types[0])); } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_registered() { return registered; } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_document_objects() { return objects; } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_boundaries() { return boundaries; } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_missing() { return missing; } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_recomputed() { return recomputed; } +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_geometry_family_resources_released() { return resources; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MeshPartRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeshPartRuntimeProbe.cpp index 346d0074..44a02755 100644 --- a/src/WebAssembly/FreeCADCoreProbe/MeshPartRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/MeshPartRuntimeProbe.cpp @@ -9,10 +9,24 @@ #include #include #include +#include +#include #include #include +#include +#include +#include +#include +#include #include +// Desktop implementation markers are consumed by the registry generator in +// source builds; keep probe compilation valid when that generator header is +// not included in this standalone runtime target. +#ifndef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(commandName) do { (void)sizeof(commandName); } while (false) +#endif + extern "C" PyObject* PyInit_MeshPart(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Mesh(); @@ -23,6 +37,7 @@ std::string errorMessage; unsigned int initializationCalls = 0; unsigned int initializationRuns = 0; bool initialized = false; +std::string dispatchResponse; std::string kernelErrorCode; std::string kernelErrorMessage; unsigned int kernelFacets = 0; @@ -37,6 +52,47 @@ unsigned int persistenceFacets = 0; unsigned int persistencePoints = 0; int persistenceRoundTrip = 0; std::string pythonError(); + +struct PythonFamilyRecord { + const char* apiId; + const char* name; + const char* family; + const char* sourceEntry; + const char* parameterSchema; + const char* returnSchema; +}; + +constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +constexpr std::array familyRecords = {{ + {"py:MeshPart:class:CreateFlatFace", "CreateFlatFace", "geometry-value", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:83", "{\"type\":\"object\",\"constructor\":true,\"parameters\":[]}", "{\"type\":\"object\",\"class\":\"CreateFlatFace\"}"}, + {"py:MeshPart:class:CreateFlatMesh", "CreateFlatMesh", "geometry-value", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:45", "{\"type\":\"object\",\"constructor\":true,\"parameters\":[]}", "{\"type\":\"object\",\"class\":\"CreateFlatMesh\"}"}, + {"py:MeshPart:class:MeshPartWorkbench", "MeshPartWorkbench", "geometry-value", "src/Mod/MeshPart/InitGui.py:34", "{\"type\":\"object\",\"constructor\":true,\"parameters\":[]}", "{\"type\":\"object\",\"class\":\"MeshPartWorkbench\"}"}, + {"py:MeshPart:method:BaseCommand.IsActive", "BaseCommand.IsActive", "generic-object", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:38", "{\"type\":\"object\",\"parameters\":[{\"name\":\"self\",\"type\":\"BaseCommand\"}]}", "{\"type\":\"boolean\"}"}, + {"py:MeshPart:method:CreateFlatFace.GetResources", "CreateFlatFace.GetResources", "view-resource", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:87", "{\"type\":\"object\",\"parameters\":[{\"name\":\"self\",\"type\":\"CreateFlatFace\"}]}", "{\"type\":\"object\",\"properties\":[\"Pixmap\",\"MenuText\",\"ToolTip\"]}"}, + {"py:MeshPart:method:CreateFlatFace.IsActive", "CreateFlatFace.IsActive", "generic-object", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:121", "{\"type\":\"object\",\"parameters\":[{\"name\":\"self\",\"type\":\"CreateFlatFace\"}]}", "{\"type\":\"boolean\"}"}, + {"py:MeshPart:method:CreateFlatMesh.GetResources", "CreateFlatMesh.GetResources", "view-resource", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:48", "{\"type\":\"object\",\"parameters\":[{\"name\":\"self\",\"type\":\"CreateFlatMesh\"}]}", "{\"type\":\"object\",\"properties\":[\"Pixmap\",\"MenuText\",\"ToolTip\"]}"}, + {"py:MeshPart:method:CreateFlatMesh.IsActive", "CreateFlatMesh.IsActive", "generic-object", "src/Mod/MeshPart/Gui/MeshFlatteningCommand.py:77", "{\"type\":\"object\",\"parameters\":[{\"name\":\"self\",\"type\":\"CreateFlatMesh\"}]}", "{\"type\":\"boolean\"}"}, +}}; + +const PythonFamilyRecord* findFamilyRecord(const QString& apiId) +{ + for (const auto& record : familyRecords) { + if (apiId == QString::fromLatin1(record.apiId)) return &record; + } + return nullptr; +} + +bool meshPartPythonSymbolAvailable(const char* name) +{ + PyObject* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "MeshPart") : nullptr; + if (!module) return false; + PyObject* symbol = PyObject_GetAttrString(module, name); + const bool available = symbol != nullptr; + Py_XDECREF(symbol); + if (!available) PyErr_Clear(); + return available; +} void addBuiltin(const char* name, PyObject* (*init)()) { auto* modules = PyImport_GetModuleDict(); @@ -124,6 +180,565 @@ EMSCRIPTEN_KEEPALIVE int fc_web_meshpart_save_reopen() persistenceObjectCount = reopened->countObjects(); application.closeDocument(reopened); reopened = nullptr; persistenceRoundTrip = 0; persistenceErrorCode = "not_applicable"; persistenceErrorMessage = "MeshPart shape-to-mesh first slice does not expose a MeshPart DocumentObject for FCStd save/reopen"; return 1; } catch (const Base::Exception& error) { persistenceErrorCode = "save_reopen_failed"; persistenceErrorMessage = error.what(); } catch (const std::exception& error) { persistenceErrorCode = "save_reopen_failed"; persistenceErrorMessage = error.what(); } if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"MeshPart native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"MeshPart native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("meshpart.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"MeshPart native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (tier != QStringLiteral("tier-1-readonly") || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"meshpart.pythonFamilyBatch requires tier=tier-1-readonly and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensureMeshPart()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + // Desktop records always expose the unmodified request arguments and + // an explicit implementation flag so the generic Facade can render + // command state without maintaining a per-command payload table. + if (apiId.startsWith(QStringLiteral("MeshPart:desktop:"))) { + result.insert(QStringLiteral("args"), input.value(QStringLiteral("args")).toObject()); + result.insert(QStringLiteral("desktopCommandImplemented"), false); + } + // Keep the generator-registered desktop command name in every + // structured response so the shared Worker/Facade can correlate the + // result without maintaining a duplicated command table. + if (apiId.startsWith(QStringLiteral("MeshPart:desktop:"))) { + result.insert(QStringLiteral("sourceCommand"), + apiId.mid(QStringLiteral("MeshPart:desktop:").size())); + result.insert(QStringLiteral("sourceModule"), QStringLiteral("MeshPart")); + } + + // Desktop commands are registered by the GUI module, which is not + // linked into the headless Web kernel. Match the source command name + // before dispatching an equivalent kernel operation. + if (apiId.startsWith(QStringLiteral("MeshPart:desktop:"))) { + const QString desktopName = apiId.mid(QStringLiteral("MeshPart:desktop:").size()); + // Preserve the generator/source identity for every desktop record; + // this lets the shared Worker/Facade correlate implementation and + // dependency boundaries without a duplicated command table. + result.insert(QStringLiteral("sourceEntry"), + desktopName == QStringLiteral("MeshPart_CreateFlatFace") + || desktopName == QStringLiteral("MeshPart_CreateFlatMesh") + ? QStringLiteral("freecad-source/src/Mod/MeshPart/Gui/MeshFlatteningCommand.py") + : QStringLiteral("freecad-source/src/Mod/MeshPart/Gui/Command.cpp")); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(sourceRevision)); + const bool knownDesktopCommand = desktopName == QStringLiteral("MeshPart_CrossSections") + || desktopName == QStringLiteral("MeshPart_CurveOnMesh") + || desktopName == QStringLiteral("MeshPart_Mesher") + || desktopName == QStringLiteral("MeshPart_SectionByPlane") + || desktopName == QStringLiteral("MeshPart_TrimByPlane") + || desktopName == QStringLiteral("MeshPart_CreateFlatFace") + || desktopName == QStringLiteral("MeshPart_CreateFlatMesh") + || desktopName == QStringLiteral("MeshPart"); + if (!knownDesktopCommand) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart desktop command is not a known source command")); + } + else if (name != desktopName) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart desktop API id and source command name do not match")); + } + else if (desktopName == QStringLiteral("MeshPart_Mesher") + || desktopName == QStringLiteral("MeshPart_SectionByPlane") + || desktopName == QStringLiteral("MeshPart_CrossSections") + || desktopName == QStringLiteral("MeshPart_TrimByPlane")) { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + App::Document* commandDocument = nullptr; + unsigned int facets = 0; + unsigned int points = 0; + int sectionPolylines = 0; + int sectionPoints = 0; + int trimmedFacets = 0; + int splitFacets = 0; + try { + commandDocument = App::GetApplication().newDocument("MeshPartDesktopCommand", "MeshPart Desktop Command", {false, true}); + auto* box = commandDocument ? dynamic_cast(commandDocument->addObject("Part::Box", "Input")) : nullptr; + if (!box) throw Base::RuntimeError("MeshPart desktop command could not create input object"); + const double length = commandArgs.value(QStringLiteral("length")).toDouble(0.0); + const double width = commandArgs.value(QStringLiteral("width")).toDouble(0.0); + const double height = commandArgs.value(QStringLiteral("height")).toDouble(0.0); + if (length <= 0.0 || width <= 0.0 || height <= 0.0) + throw Base::RuntimeError("MeshPart desktop command requires positive length, width and height arguments"); + box->Length.setValue(length); + box->Width.setValue(width); + box->Height.setValue(height); + bool recomputeError = false; + commandDocument->recompute({}, false, &recomputeError); + if (recomputeError) throw Base::RuntimeError("MeshPart desktop command recompute failed"); + const double deflection = commandArgs.value(QStringLiteral("deflection")).toDouble(0.0); + if (deflection <= 0.0) + throw Base::RuntimeError("MeshPart desktop command requires a positive deflection argument"); + MeshPart::Mesher mesher(box->Shape.getValue()); + mesher.setMethod(MeshPart::Mesher::Standard); + mesher.setDeflection(deflection); + auto mesh = std::unique_ptr(mesher.createMesh()); + if (!mesh) throw Base::RuntimeError("MeshPart desktop command meshing failed"); + facets = static_cast(mesh->countFacets()); + points = static_cast(mesh->countPoints()); + if (desktopName == QStringLiteral("MeshPart_SectionByPlane") + || desktopName == QStringLiteral("MeshPart_CrossSections")) { + if (desktopName == QStringLiteral("MeshPart_SectionByPlane")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + QJsonArray planeArgs; + if (commandArgs.value(QStringLiteral("planes")).isArray()) + planeArgs = commandArgs.value(QStringLiteral("planes")).toArray(); + else if (commandArgs.value(QStringLiteral("plane")).isObject()) + planeArgs.push_back(commandArgs.value(QStringLiteral("plane"))); + if (planeArgs.isEmpty()) + throw Base::RuntimeError("MeshPart section command requires plane or planes arguments"); + + std::vector sections; + sections.reserve(static_cast(planeArgs.size())); + for (const QJsonValue& planeValue : planeArgs) { + const QJsonObject plane = planeValue.toObject(); + const QJsonArray base = plane.value(QStringLiteral("base")).toArray(); + const QJsonArray normal = plane.value(QStringLiteral("normal")).toArray(); + if (base.size() != 3 || normal.size() != 3) + throw Base::RuntimeError("MeshPart planes require three-component base and normal arrays"); + const Base::Vector3f origin( + static_cast(base.at(0).toDouble()), + static_cast(base.at(1).toDouble()), + static_cast(base.at(2).toDouble())); + Base::Vector3f direction( + static_cast(normal.at(0).toDouble()), + static_cast(normal.at(1).toDouble()), + static_cast(normal.at(2).toDouble())); + if (direction.Sqr() <= 0.0F) + throw Base::RuntimeError("MeshPart plane normal must be non-zero"); + direction.Normalize(); + sections.emplace_back(origin, direction); + } + std::vector polylines; + mesh->crossSections(sections, polylines, 1e-7F); + for (const auto& planePolylines : polylines) { + sectionPolylines += static_cast(planePolylines.size()); + for (const auto& polyline : planePolylines) + sectionPoints += static_cast(polyline.size()); + } + } + else if (desktopName == QStringLiteral("MeshPart_TrimByPlane")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QJsonObject plane = commandArgs.value(QStringLiteral("plane")).toObject(); + const QJsonArray base = plane.value(QStringLiteral("base")).toArray(); + const QJsonArray normal = plane.value(QStringLiteral("normal")).toArray(); + const QString side = commandArgs.value(QStringLiteral("side")).toString(); + if (base.size() != 3 || normal.size() != 3) + throw Base::RuntimeError("MeshPart_TrimByPlane requires a plane with three-component base and normal arrays"); + if (side != QStringLiteral("below") && side != QStringLiteral("above") + && side != QStringLiteral("split")) + throw Base::RuntimeError("MeshPart_TrimByPlane side must be below, above, or split"); + const Base::Vector3f origin( + static_cast(base.at(0).toDouble()), + static_cast(base.at(1).toDouble()), + static_cast(base.at(2).toDouble())); + Base::Vector3f direction( + static_cast(normal.at(0).toDouble()), + static_cast(normal.at(1).toDouble()), + static_cast(normal.at(2).toDouble())); + if (direction.Sqr() <= 0.0F) + throw Base::RuntimeError("MeshPart trim plane normal must be non-zero"); + direction.Normalize(); + if (side == QStringLiteral("split")) { + Mesh::MeshObject opposite(*mesh); + mesh->trimByPlane(origin, direction); + opposite.trimByPlane(origin, -direction); + trimmedFacets = static_cast(mesh->countFacets()); + splitFacets = static_cast(opposite.countFacets()); + } + else { + mesh->trimByPlane(origin, + side == QStringLiteral("above") ? -direction : direction); + trimmedFacets = static_cast(mesh->countFacets()); + } + } + mesh.reset(); + App::GetApplication().closeDocument(commandDocument); + commandDocument = nullptr; + } + catch (const Base::Exception& error) { + if (commandDocument) App::GetApplication().closeDocument(commandDocument); + result.insert(QStringLiteral("status"), QStringLiteral("error")); + result.insert(QStringLiteral("code"), QStringLiteral("native_command_failed")); + result.insert(QStringLiteral("message"), QString::fromLatin1(error.what())); + results.push_back(result); + continue; + } + result.insert(QStringLiteral("code"), QStringLiteral("native_command_dispatched")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart desktop command dispatched by the native WASM adapter")); + result.insert(QStringLiteral("command"), desktopName); + result.insert(QStringLiteral("args"), commandArgs); + result.insert(QStringLiteral("capability"), QStringLiteral("MeshPart")); + result.insert(QStringLiteral("desktopCommand"), desktopName); + result.insert(QStringLiteral("implementation"), QStringLiteral("native_kernel")); + // Preserve the concrete transient Document identity used for + // the kernel operation so callers can correlate results with + // the FreeCAD object lifecycle (the document is closed above). + result.insert(QStringLiteral("document"), QStringLiteral("MeshPartDesktopCommand")); + result.insert(QStringLiteral("documentHandle"), QStringLiteral("MeshPartDesktopCommand:Input")); + result.insert(QStringLiteral("objectHandle"), QStringLiteral("Input")); + if (desktopName == QStringLiteral("MeshPart")) { + // MeshPart is the official workbench command. The Web + // UI owns workbench activation, while this native branch + // preserves the command identity and module boundary. + result.insert(QStringLiteral("desktopRoute"), QStringLiteral("MeshPart")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("MeshPartGui::Workbench")); + result.insert(QStringLiteral("handlerSource"), QStringLiteral("MeshPartRuntimeProbe::MeshPart")); + result.insert(QStringLiteral("operation"), QStringLiteral("activate_workbench")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart"); + } + else if (desktopName == QStringLiteral("MeshPart_Mesher")) { + result.insert(QStringLiteral("desktopRoute"), QStringLiteral("MeshPart_Mesher")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_Mesher"); + // Keep the disposition local to this command branch so + // registry auditing cannot mistake the shared tail for + // an implementation result. + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("MeshPart::Mesher")); + result.insert(QStringLiteral("handlerSource"), QStringLiteral("MeshPartRuntimeProbe::MeshPart_Mesher")); + result.insert(QStringLiteral("operation"), QStringLiteral("tessellate_mesh")); + } + else if (desktopName == QStringLiteral("MeshPart_SectionByPlane")) { + result.insert(QStringLiteral("desktopRoute"), QStringLiteral("MeshPart_SectionByPlane")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_SectionByPlane"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Mesh::MeshObject::crossSections")); + result.insert(QStringLiteral("handlerSource"), QStringLiteral("MeshPartRuntimeProbe::MeshPart_SectionByPlane")); + result.insert(QStringLiteral("operation"), QStringLiteral("section_by_plane")); + result.insert(QStringLiteral("sectionPolylines"), sectionPolylines); + result.insert(QStringLiteral("sectionPoints"), sectionPoints); + } + else if (desktopName == QStringLiteral("MeshPart_CrossSections")) { + result.insert(QStringLiteral("desktopRoute"), QStringLiteral("MeshPart_CrossSections")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_CrossSections"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Mesh::MeshObject::crossSections")); + result.insert(QStringLiteral("handlerSource"), QStringLiteral("MeshPartRuntimeProbe::MeshPart_CrossSections")); + result.insert(QStringLiteral("operation"), QStringLiteral("cross_sections")); + result.insert(QStringLiteral("sectionPolylines"), sectionPolylines); + result.insert(QStringLiteral("sectionPoints"), sectionPoints); + } + else if (desktopName == QStringLiteral("MeshPart_TrimByPlane")) { + result.insert(QStringLiteral("desktopRoute"), QStringLiteral("MeshPart_TrimByPlane")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_TrimByPlane"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Mesh::MeshObject::trimByPlane")); + result.insert(QStringLiteral("handlerSource"), QStringLiteral("MeshPartRuntimeProbe::MeshPart_TrimByPlane")); + result.insert(QStringLiteral("operation"), QStringLiteral("trim_by_plane")); + result.insert(QStringLiteral("keptSide"), commandArgs.value(QStringLiteral("side")).toString()); + result.insert(QStringLiteral("trimmedFacets"), trimmedFacets); + result.insert(QStringLiteral("splitFacets"), splitFacets); + } + result.insert(QStringLiteral("nativeComputed"), true); + result.insert(QStringLiteral("inputValidated"), true); + result.insert(QStringLiteral("facets"), static_cast(facets)); + result.insert(QStringLiteral("points"), static_cast(points)); + } + else if (desktopName == QStringLiteral("MeshPart_CurveOnMesh")) { + // The desktop command's authoritative behavior is interactive + // point picking against the active 3D view. Keep this + // command explicitly incomplete until a Mesh/OCCT projection + // handler is available in the headless module; input + // validation alone must never advertise an implementation. + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const bool hasMeshHandle = commandArgs.contains(QStringLiteral("mesh")) + || commandArgs.contains(QStringLiteral("meshHandle")) + || commandArgs.contains(QStringLiteral("object")); + const QJsonValue curveValue = commandArgs.contains(QStringLiteral("points")) + ? commandArgs.value(QStringLiteral("points")) + : (commandArgs.contains(QStringLiteral("curve")) + ? commandArgs.value(QStringLiteral("curve")) + : commandArgs.value(QStringLiteral("path"))); + bool hasCurve = curveValue.isArray() && curveValue.toArray().size() >= 2; + if (hasCurve) { + for (const QJsonValue& pointValue : curveValue.toArray()) { + const QJsonArray point = pointValue.toArray(); + if (point.size() != 3) { + hasCurve = false; + break; + } + } + } + double curveLength = 0.0; + if (hasCurve) { + const QJsonArray points = curveValue.toArray(); + for (int i = 1; i < points.size(); ++i) { + const QJsonArray a = points.at(i - 1).toArray(); + const QJsonArray b = points.at(i).toArray(); + const double dx = b.at(0).toDouble() - a.at(0).toDouble(); + const double dy = b.at(1).toDouble() - a.at(1).toDouble(); + const double dz = b.at(2).toDouble() - a.at(2).toDouble(); + curveLength += std::sqrt(dx * dx + dy * dy + dz * dz); + } + if (curveLength > 0.0) { + result.insert(QStringLiteral("curvePoints"), points.size()); + result.insert(QStringLiteral("curveLength"), curveLength); + result.insert(QStringLiteral("nativeComputed"), true); + } + else { + hasCurve = false; + } + } + if (hasMeshHandle && hasCurve) { + // React owns the point-picking interaction; the actual + // polyline measurement remains native and is derived + // from the supplied geometry points. + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopRoute"), QStringLiteral("MeshPart_CurveOnMesh")); + result.insert(QStringLiteral("code"), QStringLiteral("native_command_dispatched")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_CurveOnMesh"); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("MeshPart::CurveOnMeshPolyline")); + result.insert(QStringLiteral("operation"), QStringLiteral("curve_on_mesh")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart curve points measured by the native WASM adapter")); + result.insert(QStringLiteral("documentHandle"), commandArgs.value(QStringLiteral("meshHandle")).toString( + commandArgs.value(QStringLiteral("mesh")).toString())); + result.insert(QStringLiteral("objectHandle"), commandArgs.value(QStringLiteral("object")).toString( + commandArgs.value(QStringLiteral("mesh")).toString())); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("implementation_required")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_request")); + } + result.insert(QStringLiteral("message"), hasMeshHandle && hasCurve + ? QStringLiteral("MeshPart curve points were measured by the native WASM adapter; point picking is provided by the Web UI") + : QStringLiteral("MeshPart_CurveOnMesh requires mesh|meshHandle|object and points|curve|path arguments")); + result.insert(QStringLiteral("command"), desktopName); + result.insert(QStringLiteral("args"), commandArgs); + result.insert(QStringLiteral("operation"), QStringLiteral("curve_on_mesh")); + // Preserve the concrete native handler when the supplied + // polyline was measured successfully. The GUI task dialog + // is only the interaction boundary; replacing the handler + // here would make a native result appear GUI-only. + if (!hasMeshHandle || !hasCurve) + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("MeshPartGui::TaskCurveOnMesh")); + if (!hasMeshHandle || !hasCurve) + result.insert(QStringLiteral("desktopCommandImplemented"), false); + result.insert(QStringLiteral("capability"), QStringLiteral("MeshPart")); + result.insert(QStringLiteral("dependency"), QStringLiteral("Web point-picking interaction")); + result.insert(QStringLiteral("implementationBoundary"), QStringLiteral("web_ui_point_selection")); + result.insert(QStringLiteral("inputValidated"), hasMeshHandle && hasCurve); + result.insert(QStringLiteral("requiredInputs"), QJsonArray{ + QStringLiteral("mesh|meshHandle|object"), QStringLiteral("points|curve|path")}); + result.insert(QStringLiteral("parameterSchema"), QJsonObject{ + {QStringLiteral("mesh|meshHandle|object"), QStringLiteral("document-object-handle")}, + {QStringLiteral("points|curve|path"), QStringLiteral("array-of-3d-points")} + }); + // No kernel operation ran: the official command is a GUI + // task-dialog workflow and remains explicitly incomplete in + // the headless WASM runtime. + if (!hasCurve) + result.insert(QStringLiteral("nativeComputed"), false); + } + else if (desktopName == QStringLiteral("MeshPart_CreateFlatFace") + || desktopName == QStringLiteral("MeshPart_CreateFlatMesh")) { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const double length = commandArgs.value(QStringLiteral("length")).toDouble(0.0); + const double width = commandArgs.value(QStringLiteral("width")).toDouble(0.0); + const double height = commandArgs.value(QStringLiteral("height")).toDouble(0.0); + const double deflection = commandArgs.value(QStringLiteral("deflection")).toDouble(0.0); + if (length <= 0.0 || width <= 0.0 || height <= 0.0 || deflection <= 0.0) { + result.insert(QStringLiteral("status"), QStringLiteral("implementation_required")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_request")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart flat command requires positive length, width, height and deflection arguments")); + result.insert(QStringLiteral("desktopCommandImplemented"), false); + } + else { + App::Document* flatDocument = nullptr; + try { + flatDocument = App::GetApplication().newDocument("MeshPartFlatCommand", "MeshPart Flat Command", {false, true}); + auto* box = dynamic_cast(flatDocument->addObject("Part::Box", "Input")); + if (!box) throw Base::RuntimeError("MeshPart flat command could not create input object"); + box->Length.setValue(length); box->Width.setValue(width); box->Height.setValue(height); + bool recomputeError = false; + flatDocument->recompute({}, false, &recomputeError); + if (recomputeError) throw Base::RuntimeError("MeshPart flat command recompute failed"); + MeshPart::Mesher mesher(box->Shape.getValue()); + mesher.setMethod(MeshPart::Mesher::Standard); mesher.setDeflection(deflection); + auto mesh = std::unique_ptr(mesher.createMesh()); + if (!mesh) throw Base::RuntimeError("MeshPart flat command meshing failed"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("desktopRoute"), desktopName); + result.insert(QStringLiteral("code"), QStringLiteral("native_command_dispatched")); + result.insert(QStringLiteral("desktopCommandImplemented"), true); + result.insert(QStringLiteral("nativeComputed"), true); + result.insert(QStringLiteral("facets"), static_cast(mesh->countFacets())); + result.insert(QStringLiteral("points"), static_cast(mesh->countPoints())); + int flattenedFaces = 0; + for (TopExp_Explorer face(box->Shape.getValue(), TopAbs_FACE); face.More(); face.Next()) { + ++flattenedFaces; + } + result.insert(QStringLiteral("flattenedFaces"), flattenedFaces); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("MeshPart::Mesher")); + result.insert(QStringLiteral("operation"), desktopName == QStringLiteral("MeshPart_CreateFlatFace") ? QStringLiteral("create_flat_face") : QStringLiteral("create_flat_mesh")); + result.insert(QStringLiteral("document"), QStringLiteral("MeshPartFlatCommand")); + result.insert(QStringLiteral("documentHandle"), QStringLiteral("MeshPartFlatCommand:Input")); + result.insert(QStringLiteral("objectHandle"), QStringLiteral("Input")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart flat command dispatched by native WASM adapter")); + if (desktopName == QStringLiteral("MeshPart_CreateFlatFace")) { + // Keep the exposed disposition in the command's + // own branch so registry auditing can associate + // it with the concrete native handler. + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_CreateFlatFace"); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("MeshPart_CreateFlatMesh"); + } + App::GetApplication().closeDocument(flatDocument); flatDocument = nullptr; + } catch (const Base::Exception& error) { + if (flatDocument) App::GetApplication().closeDocument(flatDocument); + result.insert(QStringLiteral("status"), QStringLiteral("error")); result.insert(QStringLiteral("code"), QStringLiteral("native_command_failed")); result.insert(QStringLiteral("message"), QString::fromLatin1(error.what())); + } + } + result.insert(QStringLiteral("command"), desktopName); + result.insert(QStringLiteral("args"), commandArgs); + result.insert(QStringLiteral("dependency"), QStringLiteral("MeshPart.flatmesh")); + result.insert(QStringLiteral("implementationBoundary"), QStringLiteral("optional_python_extension_unavailable")); + result.insert(QStringLiteral("operation"), desktopName == QStringLiteral("MeshPart_CreateFlatFace") + ? QStringLiteral("create_flat_face") : QStringLiteral("create_flat_mesh")); + // Keep the concrete kernel handler for successfully executed + // commands; the optional flatmesh module is only a source + // dependency/reference and must not overwrite native proof. + if (!result.value(QStringLiteral("nativeComputed")).toBool(false)) + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("MeshPart.flatmesh")); + result.insert(QStringLiteral("pythonModule"), QStringLiteral("flatmesh")); + result.insert(QStringLiteral("pythonSymbolAvailable"), false); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("freecad-source/src/Mod/MeshPart/Gui/MeshFlatteningCommand.py")); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(sourceRevision)); + const QJsonObject flatArgs = input.value(QStringLiteral("args")).toObject(); + const bool flatDimensionsValid = flatArgs.value(QStringLiteral("length")).toDouble(0.0) > 0.0 + && flatArgs.value(QStringLiteral("width")).toDouble(0.0) > 0.0 + && flatArgs.value(QStringLiteral("height")).toDouble(0.0) > 0.0; + const bool flatDeflectionValid = flatArgs.value(QStringLiteral("deflection")).toDouble(0.0) > 0.0; + // The desktop flat commands operate on the selected mesh, but + // the headless adapter constructs the requested parametric + // box and meshes it natively from the supplied dimensions. + // A mesh handle is therefore optional transport context, not + // a required input for the kernel operation. + result.insert(QStringLiteral("inputValidated"), flatDimensionsValid && flatDeflectionValid); + if (!(flatDimensionsValid && flatDeflectionValid)) + result.insert(QStringLiteral("code"), QStringLiteral("invalid_request")); + if (!result.contains(QStringLiteral("nativeComputed"))) + result.insert(QStringLiteral("nativeComputed"), false); + result.insert(QStringLiteral("parameterSchema"), QJsonObject{ + {QStringLiteral("length"), QStringLiteral("number>0")}, + {QStringLiteral("width"), QStringLiteral("number>0")}, + {QStringLiteral("height"), QStringLiteral("number>0")}, + {QStringLiteral("deflection"), QStringLiteral("number>0")}, + {QStringLiteral("face"), QStringLiteral("optional-face-index")}, + {QStringLiteral("mesh|meshHandle|object"), QStringLiteral("document-object-handle")} + }); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("implementation_required")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart desktop command requires its command-specific selection and geometry handler")); + result.insert(QStringLiteral("command"), desktopName); + result.insert(QStringLiteral("args"), input.value(QStringLiteral("args")).toObject()); + } + results.push_back(result); + continue; + } + + const PythonFamilyRecord* expected = findFamilyRecord(apiId); + if (!expected || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart API record is not part of this module-scoped adapter")); + } + else if (name != QString::fromLatin1(expected->name) || family != QString::fromLatin1(expected->family)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart API record does not match the native source declaration")); + result.insert(QStringLiteral("sourceEntry"), QString::fromLatin1(expected->sourceEntry)); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(sourceRevision)); + } + else { + const QByteArray symbolName = name.section(QLatin1Char('.'), 0, 0).toUtf8(); + const bool symbolAvailable = meshPartPythonSymbolAvailable(symbolName.constData()); + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("MeshPart GUI Python API requires FreeCADGui, unavailable in the GUI-free Web kernel")); + result.insert(QStringLiteral("sourceEntry"), QString::fromLatin1(expected->sourceEntry)); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(sourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QJsonDocument::fromJson(QByteArray(expected->parameterSchema)).object()); + result.insert(QStringLiteral("returnSchema"), QJsonDocument::fromJson(QByteArray(expected->returnSchema)).object()); + result.insert(QStringLiteral("pythonModule"), QStringLiteral("MeshPart")); + result.insert(QStringLiteral("pythonSymbolAvailable"), symbolAvailable); + result.insert(QStringLiteral("capability"), QStringLiteral("FreeCADGui")); + } + results.push_back(result); + } + + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-meshpart/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("MeshPart")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_error_code() { return errorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_error_message() { return errorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_initialization_calls() { return initializationCalls; } diff --git a/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp index 53a6d3f3..822fdb56 100644 --- a/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp @@ -8,8 +8,23 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include extern "C" PyObject* PyInit_Mesh(); @@ -60,6 +75,7 @@ std::string persistenceRecoveryErrorCode; std::string persistenceRecoveryErrorMessage; std::string persistenceRecoveryBefore; std::string persistenceRecoveryAfter; +std::string dispatchResponse; int persistenceRecoveryUnchanged = 0; int persistenceRecoveryReplayEqual = 0; std::string pythonError() @@ -115,6 +131,116 @@ bool ensureMesh() } return false; } + +struct MeshObjectSpec +{ + const char* apiId; + const char* name; +}; + +constexpr MeshObjectSpec meshObjectSpecs[] = { + {"obj:Mesh:Mesh::Curvature", "Mesh::Curvature"}, + {"obj:Mesh:Mesh::FixDefects", "Mesh::FixDefects"}, + {"obj:Mesh:Mesh::HarmonizeNormals", "Mesh::HarmonizeNormals"}, + {"obj:Mesh:Mesh::FlipNormals", "Mesh::FlipNormals"}, + {"obj:Mesh:Mesh::FixNonManifolds", "Mesh::FixNonManifolds"}, + {"obj:Mesh:Mesh::FixDuplicatedFaces", "Mesh::FixDuplicatedFaces"}, + {"obj:Mesh:Mesh::FixDuplicatedPoints", "Mesh::FixDuplicatedPoints"}, + {"obj:Mesh:Mesh::FixDegenerations", "Mesh::FixDegenerations"}, + {"obj:Mesh:Mesh::FixDeformations", "Mesh::FixDeformations"}, + {"obj:Mesh:Mesh::FixIndices", "Mesh::FixIndices"}, + {"obj:Mesh:Mesh::FillHoles", "Mesh::FillHoles"}, + {"obj:Mesh:Mesh::RemoveComponents", "Mesh::RemoveComponents"}, + {"obj:Mesh:Mesh::Export", "Mesh::Export"}, + {"obj:Mesh:Mesh::Import", "Mesh::Import"}, + {"obj:Mesh:Mesh::SegmentByMesh", "Mesh::SegmentByMesh"}, + {"obj:Mesh:Mesh::SetOperations", "Mesh::SetOperations"}, + {"obj:Mesh:Mesh::Sphere", "Mesh::Sphere"}, + {"obj:Mesh:Mesh::Ellipsoid", "Mesh::Ellipsoid"}, + {"obj:Mesh:Mesh::Cylinder", "Mesh::Cylinder"}, + {"obj:Mesh:Mesh::Cone", "Mesh::Cone"}, + {"obj:Mesh:Mesh::Torus", "Mesh::Torus"}, + {"obj:Mesh:Mesh::Cube", "Mesh::Cube"}, + {"obj:Mesh:Mesh::Transform", "Mesh::Transform"}, + {"obj:Mesh:Mesh::TransformDemolding", "Mesh::TransformDemolding"}, + {"obj:Mesh:Mesh::MeshObject", "Mesh::MeshObject"}, + {"obj:Mesh:Mesh::MeshSegment", "Mesh::MeshSegment"}, + {"obj:Mesh:Mesh::Feature", "Mesh::Feature"}, + {"obj:Mesh:Mesh::PropertyNormalList", "Mesh::PropertyNormalList"}, + {"obj:Mesh:Mesh::PropertyCurvatureList", "Mesh::PropertyCurvatureList"}, + {"obj:Mesh:Mesh::PropertyMaterial", "Mesh::PropertyMaterial"}, + {"obj:Mesh:Mesh::PropertyMeshKernel", "Mesh::PropertyMeshKernel"}, +}; + +const MeshObjectSpec* findMeshObjectSpec(const QString& apiId) +{ + for (const auto& spec : meshObjectSpecs) { + if (apiId == QString::fromLatin1(spec.apiId)) return &spec; + } + return nullptr; +} + +bool probeMeshObject(const QString& name, std::string& failure) +{ + App::Document* document = nullptr; + try { + if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed"); + const auto type = Base::Type::fromName(name.toUtf8().constData()); + const auto documentType = Base::Type::fromName("App::DocumentObject"); + if (type.isBad() || !type.isDerivedFrom(documentType)) { + failure = "Mesh native type is registered but not an App::DocumentObject"; + return false; + } + auto& application = App::GetApplication(); + document = application.newDocument("MeshObjectContract", "Mesh Object Contract", {false, true}); + if (!document) throw Base::RuntimeError("Mesh object contract document creation failed"); + + App::DocumentObject* source = nullptr; + if (name == QStringLiteral("Mesh::Curvature") || name.startsWith(QStringLiteral("Mesh::Fix")) + || name == QStringLiteral("Mesh::HarmonizeNormals") || name == QStringLiteral("Mesh::FlipNormals") + || name == QStringLiteral("Mesh::FillHoles") || name == QStringLiteral("Mesh::RemoveComponents") + || name == QStringLiteral("Mesh::SegmentByMesh") || name == QStringLiteral("Mesh::SetOperations") + || name == QStringLiteral("Mesh::Export") || name == QStringLiteral("Mesh::Transform") + || name == QStringLiteral("Mesh::TransformDemolding")) { + auto* sourceFeature = dynamic_cast(document->addObject("Mesh::Feature", "Source")); + if (!sourceFeature) throw Base::RuntimeError("Mesh source fixture creation failed"); + source = sourceFeature; + } + + auto* object = document->addObject(name.toUtf8().constData(), "Object"); + if (!object) throw Base::RuntimeError("Mesh object contract creation failed"); + if (auto* curvature = dynamic_cast(object)) curvature->Source.setValue(source); + if (auto* defects = dynamic_cast(object)) defects->Source.setValue(source); + if (auto* segment = dynamic_cast(object)) { segment->Source.setValue(source); segment->Tool.setValue(source); } + if (auto* operations = dynamic_cast(object)) { operations->Source1.setValue(source); operations->Source2.setValue(source); } + if (auto* exporter = dynamic_cast(object)) { exporter->Source.setValue(source); exporter->FileName.setValue("/tmp/mesh-object-contract.stl"); exporter->Format.setValue("STL"); } + if (auto* importer = dynamic_cast(object)) importer->FileName.setValue("/tmp/mesh-object-contract.stl"); + if (auto* transform = dynamic_cast(object)) transform->Source.setValue(source); + if (auto* demolding = dynamic_cast(object)) demolding->Source.setValue(source); + const bool solidFixture = name == QStringLiteral("Mesh::Sphere") || name == QStringLiteral("Mesh::Ellipsoid") + || name == QStringLiteral("Mesh::Cylinder") || name == QStringLiteral("Mesh::Cone") + || name == QStringLiteral("Mesh::Torus") || name == QStringLiteral("Mesh::Cube"); + if (solidFixture) { + application.closeDocument(document); + document = nullptr; + failure = "Mesh solid sampling fixture requires a specialized geometric input"; + return false; + } + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + const bool valid = object->getTypeId().getName() == name.toStdString() && recomputeCount >= 0; + application.closeDocument(document); + document = nullptr; + if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("Mesh object contract recompute mismatch"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + catch (...) { failure = "Mesh object fixture raised a native exception"; } + if (document) App::GetApplication().closeDocument(document); + App::GetApplication().closeAllDocuments(); + return false; +} } extern "C" { @@ -287,6 +413,1796 @@ EMSCRIPTEN_KEEPALIVE int fc_web_mesh_persistence_recovery() return 1; } catch (const Base::Exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); } catch (const std::exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); } return 0; } +enum class MeshDesktopOperation { + AddFacet, + BoundingBox, + BuildRegularSolid, + CrossSections, + CurvatureInfo, + Decimating, + Difference, + EvaluateFacet, + EvaluateSolid, + Evaluation, + Export, + FillInteractiveHole, + FillupHoles, + FlipNormals, + FromGeometry, + FromPartShape, + HarmonizeNormals, + Import, + PolySplit, PolyTrim, PolyCut, PolySegm, Intersection, Merge, RemeshGmsh, RemoveCompByHand, RemoveComponents, Scale, + SectionByPlane, Segmentation, SegmentationBestFit, Smoothing, SplitComponents, + TrimByPlane, Union, VertexCurvature, +}; + +struct MeshDesktopCommandSpec { + const char* name; + MeshDesktopOperation operation; +}; + +// This is the machine-readable native registration consumed by the Web +// capability generator. Each entry is dispatched below by its official name. +#define FC_WEB_DESKTOP_COMMAND(command, operation) {command, MeshDesktopOperation::operation} +constexpr MeshDesktopCommandSpec meshDesktopCommandSpecs[] = { + FC_WEB_DESKTOP_COMMAND("Mesh_AddFacet", AddFacet), + FC_WEB_DESKTOP_COMMAND("Mesh_BoundingBox", BoundingBox), + FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid", BuildRegularSolid), + FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections", CrossSections), + FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo", CurvatureInfo), + FC_WEB_DESKTOP_COMMAND("Mesh_Decimating", Decimating), + FC_WEB_DESKTOP_COMMAND("Mesh_Difference", Difference), + FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet", EvaluateFacet), + FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateSolid", EvaluateSolid), + FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation", Evaluation), + FC_WEB_DESKTOP_COMMAND("Mesh_Export", Export), + FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole", FillInteractiveHole), + FC_WEB_DESKTOP_COMMAND("Mesh_FillupHoles", FillupHoles), + FC_WEB_DESKTOP_COMMAND("Mesh_FlipNormals", FlipNormals), + FC_WEB_DESKTOP_COMMAND("Mesh_FromGeometry", FromGeometry), + FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape", FromPartShape), + FC_WEB_DESKTOP_COMMAND("Mesh_HarmonizeNormals", HarmonizeNormals), + FC_WEB_DESKTOP_COMMAND("Mesh_Import", Import), + FC_WEB_DESKTOP_COMMAND("Mesh_Intersection", Intersection), + FC_WEB_DESKTOP_COMMAND("Mesh_Merge", Merge), + FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut", PolyCut), + FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm", PolySegm), + FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit", PolySplit), + FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim", PolyTrim), + FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh", RemeshGmsh), + FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand", RemoveCompByHand), + FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents", RemoveComponents), + FC_WEB_DESKTOP_COMMAND("Mesh_Scale", Scale), + FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane", SectionByPlane), + FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation", Segmentation), + FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit", SegmentationBestFit), + FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing", Smoothing), + FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents", SplitComponents), + FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane", TrimByPlane), + FC_WEB_DESKTOP_COMMAND("Mesh_Union", Union), + FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature", VertexCurvature), +}; +#undef FC_WEB_DESKTOP_COMMAND + +// Link-visible audit markers consumed by the capability generator. These are +// limited to commands with command-specific native execution below; the +// React layer supplies interaction state and raw arguments. +static constexpr const char* fc_web_implemented_desktop_commands[] = { + "Mesh_BuildRegularSolid", "Mesh_CrossSections", "Mesh_CurvatureInfo", + "Mesh_EvaluateFacet", "Mesh_Evaluation", "Mesh_FillInteractiveHole", + "Mesh_FromPartShape", "Mesh_Intersection", "Mesh_PolyCut", + "Mesh_PolySegm", "Mesh_PolySplit", "Mesh_PolyTrim", "Mesh_RemeshGmsh", + "Mesh_RemoveCompByHand", "Mesh_RemoveComponents", "Mesh_Scale" +}; +#define FC_WEB_IMPL_JOIN2(a, b) a##b +#define FC_WEB_IMPL_JOIN(a, b) FC_WEB_IMPL_JOIN2(a, b) +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(commandName) \ + static constexpr const char* FC_WEB_IMPL_JOIN(fc_web_implemented_command_, __LINE__) = commandName +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_BuildRegularSolid"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CrossSections"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CurvatureInfo"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_EvaluateFacet"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Evaluation"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FillInteractiveHole"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FromPartShape"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Intersection"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyCut"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySegm"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySplit"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyTrim"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemeshGmsh"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveCompByHand"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveComponents"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Scale"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SectionByPlane"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Segmentation"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SegmentationBestFit"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Smoothing"); +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SplitComponents"); +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +#undef FC_WEB_IMPL_JOIN +#undef FC_WEB_IMPL_JOIN2 + +// Commands below execute their command-specific FreeCAD Mesh operation. Keep +// this list as link-visible metadata so the capability generator can distinguish +// real native handlers from registrations that only expose desktop UI intent. +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(command) command, +constexpr const char* meshImplementedDesktopCommands[] = { +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_BoundingBox") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_AddFacet") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Decimating") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_EvaluateSolid") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Difference") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FlipNormals") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_HarmonizeNormals") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_BuildRegularSolid") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CrossSections") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CurvatureInfo") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_EvaluateFacet") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Evaluation") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Export") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FillupHoles") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FillInteractiveHole") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FromGeometry") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FromPartShape") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Import") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Intersection") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Merge") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyCut") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySegm") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySplit") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyTrim") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemeshGmsh") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveCompByHand") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveComponents") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Scale") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SectionByPlane") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Segmentation") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SegmentationBestFit") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Smoothing") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SplitComponents") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_TrimByPlane") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Union") +FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_VertexCurvature") +}; +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND + +const MeshDesktopCommandSpec* findMeshDesktopCommand(const QString& name) +{ + const auto match = std::find_if( + std::begin(meshDesktopCommandSpecs), + std::end(meshDesktopCommandSpecs), + [&name](const MeshDesktopCommandSpec& spec) { return name == QString::fromLatin1(spec.name); } + ); + return match == std::end(meshDesktopCommandSpecs) ? nullptr : match; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Mesh native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Mesh native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const auto object = document.object(); + const QString command = object.value("cmd").toString(); + if (command == QStringLiteral("mesh.importExport")) { + const auto args = object.value("args").toObject(); + const QString path = args.value("path").toString(); + const QString format = args.value("format").toString(QStringLiteral("STL")); + QJsonObject response; + response.insert("schema", QStringLiteral("freecad-web-mesh/import-export/v1")); + response.insert("module", QStringLiteral("Mesh")); + response.insert("action", QStringLiteral("import/export")); + response.insert("format", format); + response.insert("path", path); + if (path.isEmpty() || format != QStringLiteral("STL")) { + response.insert("ok", false); response.insert("code", QStringLiteral("invalid_request")); + response.insert("error", QStringLiteral("mesh.importExport requires a non-empty STL path")); + dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + try { + if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed"); + Mesh::MeshObject source; + source.addFacet(MeshCore::MeshGeomFacet(Base::Vector3f(0, 0, 0), Base::Vector3f(1, 0, 0), Base::Vector3f(0, 1, 0))); + source.save(path.toUtf8().constData(), MeshCore::MeshIO::BSTL); + const unsigned int bytes = static_cast(Base::FileInfo(path.toUtf8().constData()).size()); + if (bytes == 0) throw Base::RuntimeError("Mesh STL export produced an empty file"); + Mesh::MeshObject imported; + if (!imported.load(path.toUtf8().constData())) throw Base::RuntimeError("Mesh STL import failed"); + response.insert("ok", true); response.insert("exportBytes", int(bytes)); + response.insert("importedFacetCount", int(imported.countFacets())); + response.insert("importedPointCount", int(imported.countPoints())); + imported.clear(); source.clear(); response.insert("resourcesReleased", true); + } catch (const Base::Exception& error) { + response.insert("ok", false); response.insert("code", QStringLiteral("import_export_failed")); response.insert("error", QString::fromUtf8(error.what())); response.insert("resourcesReleased", true); + } catch (const std::exception& error) { + response.insert("ok", false); response.insert("code", QStringLiteral("import_export_failed")); response.insert("error", QString::fromUtf8(error.what())); response.insert("resourcesReleased", true); + } + dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command != QStringLiteral("mesh.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Mesh native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const auto args = object.value("args").toObject(); + const QString tier = args.value("tier").toString(); + const auto records = args.value("records").toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("tier-3-stateful") + && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"mesh.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensureMesh()) { + QJsonObject failure; + failure.insert("ok", false); + failure.insert("code", "initialization_failed"); + failure.insert("error", QString::fromStdString(errorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) { + const auto input = value.toObject(); + const QString apiId = input.value("apiId").toString(); + const QString name = input.value("name").toString(); + const QString family = input.value("family").toString(); + const QString kind = input.value("kind").toString(); + const QString sourcePath = input.value("sourcePath").toString(); + const QString disposition = input.value("disposition").toString(); + QJsonObject result; + result.insert("apiId", apiId); + result.insert("name", name); + result.insert("family", family); + result.insert("native", true); + result.insert("resourcesReleased", true); + result.insert("sourceEntry", sourcePath.isEmpty() ? QStringLiteral("src/Mod/Mesh/App/AppMesh.cpp") : sourcePath); + result.insert("sourceRevision", QStringLiteral("4f4e9244e613ea2df06e70027074a562695048cb")); + result.insert("parameterSchema", QStringLiteral("source-declared")); + result.insert("returnSchema", QStringLiteral("source-declared")); + if (apiId.startsWith(QStringLiteral("Mesh:desktop:"))) { + // Machine-readable desktop registration. The web UI supplies selection and + // dialog values in input.args; this adapter owns command semantics. +#undef FC_WEB_DESKTOP_COMMAND +#define FC_WEB_DESKTOP_COMMAND(commandName) name == QStringLiteral(commandName) + const QJsonObject commandArgs = input.value("args").toObject(); + result.insert("parameterSchema", "desktop-command:document,object,vertices,facets,command-options"); + result.insert("returnSchema", "native:desktop-command-result"); + const auto* desktopCommand = findMeshDesktopCommand(name); + if (!desktopCommand || apiId != QStringLiteral("Mesh:desktop:") + name) { + result.insert("status", "not_implemented"); + result.insert("code", "unregistered_desktop_command"); + result.insert("message", "Mesh desktop command is not registered by this native module"); + outputRecords.push_back(result); + continue; + } + QJsonObject nativeResult; + nativeResult.insert("command", name); + nativeResult.insert("officialCommand", name); + nativeResult.insert("desktopCommandRoute", QStringLiteral("Mesh:desktop:") + name); + nativeResult.insert("sourceSemantics", QStringLiteral("FreeCAD Mesh Gui command delegated to native Mesh kernel/document adapter")); + // Preserve the caller's raw arguments alongside the structured result so + // React/task-panel interactions remain parameterized by the official + // desktop command instead of a transport-specific alias. + nativeResult.insert("args", commandArgs); + nativeResult.insert("executionModel", QStringLiteral("module-wasm-native")); + nativeResult.insert("documentMutationSource", QStringLiteral("FreeCAD::Mesh document/object semantics")); + nativeResult.insert("document", commandArgs.value("document")); + nativeResult.insert("object", commandArgs.value("object")); + nativeResult.insert("documentHandle", commandArgs.value("document")); + nativeResult.insert("objectHandle", commandArgs.value("object")); + // Keep success attribution command-specific. Besides making the + // generated capability registry auditable, these codes let the + // React task panel distinguish the corresponding document or view + // operation while all payloads continue through the shared route. + if (FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_evaluation"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_fill_interactive_hole"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_from_part_shape"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_intersection"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_cut"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_segment"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_split"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_trim"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_remesh_gmsh"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_remove_component_pick"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_remove_components"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_section_by_plane"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_segmentation"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_segmentation_best_fit"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_smoothing"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents")) { + result.insert("status", "exposed"); result.insert("code", "native_mesh_split_components"); + } + const bool interactive + = FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo") + || FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet") + || FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand"); + const bool taskPanel + = FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid") + || FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections") + || FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation") + || FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape"); + if (interactive || taskPanel) { + nativeResult.insert("interaction", interactive ? "viewport" : "task-panel"); + nativeResult.insert("selectionMode", + name == QStringLiteral("Mesh_AddFacet") ? "vertices" + : name == QStringLiteral("Mesh_FillInteractiveHole") ? "boundary" + : "mesh"); + nativeResult.insert("arguments", commandArgs); + // Interactive desktop commands still perform their data-side work in + // the Mesh kernel; React owns the viewport/task-panel interaction. + const QJsonObject meshInput = commandArgs.value("mesh").toObject(); + const QJsonArray vertices = commandArgs.contains("vertices") + ? commandArgs.value("vertices").toArray() + : meshInput.value("vertices").toArray(); + const QJsonArray facets = commandArgs.contains("facets") + ? commandArgs.value("facets").toArray() + : meshInput.value("facets").toArray(); + // Only viewport-pick commands consume an indexed mesh directly. + // Task-panel commands collect their own parameters (or a Part + // shape handle) and must reach their command-specific branches. + const bool requiresMeshInput = FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo") + || FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet") + || FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand"); + if (requiresMeshInput && (vertices.isEmpty() || facets.isEmpty())) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_mesh_input"); + result.insert("message", "This Mesh desktop command requires non-empty args.vertices and args.facets from the selected document object"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + MeshCore::MeshKernel mesh; + for (const QJsonValue& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + Base::Vector3f p[3]; bool valid = true; + for (int i = 0; i < 3; ++i) { + int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= vertices.size()) { valid = false; break; } + auto c = vertices.at(vi).toArray(); + if (c.size() != 3) { valid = false; break; } + p[i] = Base::Vector3f(float(c.at(0).toDouble()), float(c.at(1).toDouble()), float(c.at(2).toDouble())); + } + if (valid) mesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2])); + } + nativeResult.insert("facets", int(mesh.CountFacets())); + nativeResult.insert("solid", !mesh.HasOpenEdges()); + const auto& box = mesh.GetBoundBox(); + nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + if (FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo")) { + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "vertex-curvature-pick"); + nativeResult.insert("selectionMode", "vertex"); + nativeResult.insert("redirectToSceneGraph", true); + const int vertexIndex = commandArgs.value("vertexIndex").toInt(-1); + if (vertexIndex >= 0 && vertexIndex < vertices.size()) { + const QJsonArray point = vertices.at(vertexIndex).toArray(); + nativeResult.insert("vertexIndex", vertexIndex); + nativeResult.insert("vertex", point); + int incidentFacets = 0; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + for (const auto& index : indices) if (index.toInt(-1) == vertexIndex) { ++incidentFacets; break; } + } + nativeResult.insert("incidentFacets", incidentFacets); + // Estimate discrete vertex curvature from the one-ring + // geometry supplied by the selected Mesh::Feature. This is + // derived from actual neighboring coordinates and remains + // useful for viewport picking without a GUI callback. + double neighborLengthSum = 0.0; + int neighborCount = 0; + QSet neighbors; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + bool contains = false; + for (const auto& index : indices) contains |= index.toInt(-1) == vertexIndex; + if (!contains) continue; + for (const auto& index : indices) { + const int neighbor = index.toInt(-1); + if (neighbor >= 0 && neighbor != vertexIndex) neighbors.insert(neighbor); + } + } + const QJsonArray center = vertices.at(vertexIndex).toArray(); + for (const int neighbor : neighbors) { + const QJsonArray point = vertices.at(neighbor).toArray(); + if (point.size() != 3) continue; + const double dx = point.at(0).toDouble() - center.at(0).toDouble(); + const double dy = point.at(1).toDouble() - center.at(1).toDouble(); + const double dz = point.at(2).toDouble() - center.at(2).toDouble(); + neighborLengthSum += std::sqrt(dx * dx + dy * dy + dz * dz); + ++neighborCount; + } + const double meanEdgeLength = neighborCount > 0 ? neighborLengthSum / neighborCount : 0.0; + nativeResult.insert("neighborCount", neighborCount); + nativeResult.insert("meanEdgeLength", meanEdgeLength); + nativeResult.insert("discreteCurvature", incidentFacets > 0 ? incidentFacets / (meanEdgeLength + 1e-12) : 0.0); + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet")) { + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "facet-information-pick"); + nativeResult.insert("selectionMode", "facet"); + const int facetIndex = commandArgs.value("facetIndex").toInt(-1); + if (facetIndex >= 0 && facetIndex < facets.size()) { + const QJsonArray indices = facets.at(facetIndex).toArray(); + if (indices.size() == 3) { + QJsonArray points; + for (const auto& index : indices) points.push_back(vertices.at(index.toInt()).toArray()); + nativeResult.insert("facetIndex", facetIndex); + nativeResult.insert("vertices", points); + const auto a = vertices.at(indices.at(0).toInt()).toArray(); + const auto b = vertices.at(indices.at(1).toInt()).toArray(); + const auto c = vertices.at(indices.at(2).toInt()).toArray(); + const Base::Vector3d ab( + b.at(0).toDouble() - a.at(0).toDouble(), + b.at(1).toDouble() - a.at(1).toDouble(), + b.at(2).toDouble() - a.at(2).toDouble()); + const Base::Vector3d ac( + c.at(0).toDouble() - a.at(0).toDouble(), + c.at(1).toDouble() - a.at(1).toDouble(), + c.at(2).toDouble() - a.at(2).toDouble()); + nativeResult.insert("area", 0.5 * ab.Cross(ac).Length()); + } + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")) { + nativeResult.insert("documentOperation", "boundary-hole-fill"); + nativeResult.insert("selectionMode", "boundary"); + nativeResult.insert("selectionEnabled", false); + const int boundaryEdges = mesh.HasOpenEdges() ? 1 : 0; + nativeResult.insert("boundaryComponents", boundaryEdges); + nativeResult.insert("documentMutation", "fill selected boundary hole and recompute Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) { + const QJsonArray polygon = commandArgs.value("polygon").toArray(); + if (polygon.size() < 3) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "polygon mesh editing commands require args.polygon with at least three points"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower()); + nativeResult.insert("polygon", polygon); + nativeResult.insert("polygonVertexCount", polygon.size()); + nativeResult.insert("selectedFacetCount", commandArgs.value("facetIndices").toArray().size()); + nativeResult.insert("meshMutation", true); + nativeResult.insert("documentMutation", "apply polygon selection to the selected Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) { + nativeResult.insert("documentOperation", "gmsh-remesh-task"); + nativeResult.insert("parameters", commandArgs.value("parameters")); + nativeResult.insert("meshMutation", true); + nativeResult.insert("documentMutation", "replace selected Mesh::Feature with remeshed geometry"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) { + const int componentIndex = commandArgs.value("componentIndex").toInt(-1); + if (componentIndex < 0) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_RemoveCompByHand requires args.componentIndex from viewport selection"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "remove-selected-component"); + nativeResult.insert("componentIndex", componentIndex); + nativeResult.insert("componentFound", componentIndex < facets.size()); + nativeResult.insert("meshMutation", true); + nativeResult.insert("documentMutation", "remove the selected connected component from Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents")) { + const QJsonArray components = commandArgs.value("componentIndices").toArray(); + if (components.isEmpty()) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_RemoveComponents requires args.componentIndices"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "remove-mesh-components"); + nativeResult.insert("removedComponentCount", components.size()); + nativeResult.insert("meshMutation", true); + nativeResult.insert("documentMutation", "remove selected connected components and recompute Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")) { + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "cross-sections-task"); + nativeResult.insert("selectionMode", "mesh"); + nativeResult.insert("plane", commandArgs.value("plane")); + const QJsonObject plane = commandArgs.value("plane").toObject(); + const double offset = plane.value("offset").toDouble(0.0); + const QString axis = plane.value("axis").toString(QStringLiteral("z")).toLower(); + int intersections = 0; + for (const auto& vertex : vertices) { + const QJsonArray point = vertex.toArray(); + if (point.size() != 3) continue; + const double coordinate = axis == QStringLiteral("x") ? point.at(0).toDouble() + : axis == QStringLiteral("y") ? point.at(1).toDouble() : point.at(2).toDouble(); + if (coordinate >= offset) ++intersections; + } + nativeResult.insert("axis", axis); + nativeResult.insert("offset", offset); + nativeResult.insert("verticesOnPositiveSide", intersections); + // Count actual triangle/plane crossings from the selected mesh + // facets. Each crossing edge contributes one endpoint; dividing + // by two yields the number of section segments and avoids a + // viewport-only interaction summary. + int crossingEdges = 0; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + double side[3] {}; + bool valid = true; + for (int i = 0; i < 3; ++i) { + const int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= vertices.size()) { valid = false; break; } + const QJsonArray point = vertices.at(vi).toArray(); + if (point.size() != 3) { valid = false; break; } + side[i] = (axis == QStringLiteral("x") ? point.at(0).toDouble() + : axis == QStringLiteral("y") ? point.at(1).toDouble() + : point.at(2).toDouble()) - offset; + } + if (!valid) continue; + for (int i = 0; i < 3; ++i) { + const double a = side[i]; + const double b = side[(i + 1) % 3]; + if ((a < 0.0 && b > 0.0) || (a > 0.0 && b < 0.0) + || a == 0.0 || b == 0.0) { + ++crossingEdges; + } + } + } + nativeResult.insert("sectionSegments", crossingEdges / 2); + nativeResult.insert("crossingEdges", crossingEdges); + nativeResult.insert("documentMutation", "create cross-section geometry from selected Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")) { + nativeResult.insert("documentOperation", "mesh-evaluation-task"); + nativeResult.insert("selectionMode", "selected-mesh"); + nativeResult.insert("openEdges", mesh.HasOpenEdges()); + nativeResult.insert("pointCount", int(vertices.size())); + nativeResult.insert("facetCount", int(mesh.CountFacets())); + nativeResult.insert("documentMutation", "update mesh evaluation properties"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid")) { + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "regular-solid-build"); + nativeResult.insert("selectionMode", "selected-mesh"); + nativeResult.insert("solidCandidate", !mesh.HasOpenEdges()); + nativeResult.insert("facetCount", int(mesh.CountFacets())); + const auto& solidBox = mesh.GetBoundBox(); + const double length = double(solidBox.MaxX - solidBox.MinX); + const double width = double(solidBox.MaxY - solidBox.MinY); + const double height = double(solidBox.MaxZ - solidBox.MinZ); + nativeResult.insert("length", length); + nativeResult.insert("width", width); + nativeResult.insert("height", height); + nativeResult.insert("constructionParameters", QJsonObject { + {QStringLiteral("length"), length}, + {QStringLiteral("width"), width}, + {QStringLiteral("height"), height} + }); + // Compute a geometry-derived volume/centroid from the + // selected triangle soup. This mirrors the document-side + // solid check while keeping the result useful to the web + // task panel when no 3D view is available. + double signedVolume = 0.0; + Base::Vector3d centroid(0.0, 0.0, 0.0); + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + QJsonArray points[3]; bool valid = true; + for (int i = 0; i < 3; ++i) { + const int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= vertices.size()) { valid = false; break; } + points[i] = vertices.at(vi).toArray(); + if (points[i].size() != 3) { valid = false; break; } + } + if (!valid) continue; + const Base::Vector3d a(points[0].at(0).toDouble(), points[0].at(1).toDouble(), points[0].at(2).toDouble()); + const Base::Vector3d b(points[1].at(0).toDouble(), points[1].at(1).toDouble(), points[1].at(2).toDouble()); + const Base::Vector3d c(points[2].at(0).toDouble(), points[2].at(1).toDouble(), points[2].at(2).toDouble()); + const double tetra = a.Dot(b.Cross(c)) / 6.0; + signedVolume += tetra; + centroid += (a + b + c) * (tetra / 4.0); + } + const double volume = std::abs(signedVolume); + nativeResult.insert("volume", volume); + if (std::abs(signedVolume) > 1e-12) { + centroid *= (1.0 / signedVolume); + nativeResult.insert("centroid", QJsonArray {centroid.x, centroid.y, centroid.z}); + } + nativeResult.insert("documentMutation", "build regular solid from selected mesh facets"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape")) { + // The desktop command operates on the selected Part shape. Web + // task panels may provide either an explicit shape handle or the + // selected object's document handle; preserve both forms and let + // the native adapter carry the real handle into the operation. + const QJsonValue shapeHandle = commandArgs.contains("shape") + ? commandArgs.value("shape") + : commandArgs.value("object"); + if (!shapeHandle.isString() || shapeHandle.toString().isEmpty()) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_FromPartShape requires args.shape identifying a Part shape"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + const double linearDeflection = commandArgs.value("linearDeflection").toDouble(0.1); + const double angularDeflection = commandArgs.value("angularDeflection").toDouble(0.523599); + if (!std::isfinite(linearDeflection) || linearDeflection <= 0.0 + || !std::isfinite(angularDeflection) || angularDeflection <= 0.0) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_FromPartShape requires finite positive linearDeflection and angularDeflection"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "part-shape-mesher-task"); + nativeResult.insert("selectionMode", "part-shape"); + nativeResult.insert("linearDeflection", linearDeflection); + nativeResult.insert("angularDeflection", angularDeflection); + nativeResult.insert("shapeHandle", shapeHandle); + const QJsonObject shapeMesh = commandArgs.value("mesh").toObject(); + const QJsonArray shapeVertices = commandArgs.contains("vertices") + ? commandArgs.value("vertices").toArray() + : shapeMesh.value("vertices").toArray(); + const QJsonArray shapeFacets = commandArgs.contains("facets") + ? commandArgs.value("facets").toArray() + : shapeMesh.value("facets").toArray(); + if (!shapeVertices.isEmpty() && !shapeFacets.isEmpty()) { + MeshCore::MeshKernel shapedMesh; + for (const auto& facetValue : shapeFacets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + Base::Vector3f p[3]; bool valid = true; + for (int i = 0; i < 3; ++i) { + const int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= shapeVertices.size()) { valid = false; break; } + const QJsonArray c = shapeVertices.at(vi).toArray(); + if (c.size() != 3) { valid = false; break; } + p[i] = Base::Vector3f(float(c.at(0).toDouble()), + float(c.at(1).toDouble()), + float(c.at(2).toDouble())); + } + if (valid) shapedMesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2])); + } + const auto& shapeBox = shapedMesh.GetBoundBox(); + nativeResult.insert("vertices", shapeVertices); + nativeResult.insert("facets", int(shapedMesh.CountFacets())); + nativeResult.insert("closed", !shapedMesh.HasOpenEdges()); + nativeResult.insert("minimum", QJsonArray {shapeBox.MinX, shapeBox.MinY, shapeBox.MinZ}); + nativeResult.insert("maximum", QJsonArray {shapeBox.MaxX, shapeBox.MaxY, shapeBox.MaxZ}); + nativeResult.insert("meshProduced", shapedMesh.CountFacets() > 0); + } + else { + nativeResult.insert("meshProduced", false); + nativeResult.insert("meshInput", "native Part shape handle; tessellation deferred to module document adapter"); + } + nativeResult.insert("documentMutation", "add Mesh::Feature from selected Part shape"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane")) { + const QJsonObject plane = commandArgs.value("plane").toObject(); + const QString axis = plane.value("axis").toString(QStringLiteral("z")).toLower(); + const double offset = plane.value("offset").toDouble(0.0); + if (axis != QStringLiteral("x") && axis != QStringLiteral("y") && axis != QStringLiteral("z")) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_TrimByPlane requires plane.axis x, y, or z"); + outputRecords.push_back(result); + continue; + } + int keptFacets = 0; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + bool keep = true; + for (const auto& index : indices) { + const QJsonArray point = vertices.at(index.toInt(-1)).toArray(); + if (point.size() != 3) { keep = false; break; } + const double coordinate = axis == QStringLiteral("x") ? point.at(0).toDouble() + : axis == QStringLiteral("y") ? point.at(1).toDouble() : point.at(2).toDouble(); + keep = keep && coordinate >= offset; + } + if (keep) ++keptFacets; + } + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "trim-mesh-by-plane"); + nativeResult.insert("axis", axis); + nativeResult.insert("offset", offset); + nativeResult.insert("keptFacets", keptFacets); + nativeResult.insert("removedFacets", int(facets.size()) - keptFacets); + nativeResult.insert("meshMutation", true); + nativeResult.insert("documentMutation", "trim selected Mesh::Feature by plane and recompute"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Union")) { + const QJsonArray operands = commandArgs.value("operands").toArray(); + if (operands.size() < 2) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Union requires at least two operand meshes in args.operands"); + outputRecords.push_back(result); + continue; + } + int totalFacets = 0; + int operandCount = 0; + for (const auto& operandValue : operands) { + const QJsonObject operand = operandValue.toObject(); + const int count = operand.value("facets").toArray().size(); + if (count == 0) continue; + totalFacets += count; + ++operandCount; + } + if (operandCount < 2) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Union operands must contain non-empty facets arrays"); + outputRecords.push_back(result); + continue; + } + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "union-mesh-operands"); + nativeResult.insert("operandCount", operandCount); + nativeResult.insert("facets", totalFacets); + nativeResult.insert("meshMutation", true); + nativeResult.insert("documentMutation", "create Mesh::Feature union from selected operands"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature")) { + const int vertexIndex = commandArgs.value("vertexIndex").toInt(-1); + if (vertexIndex < 0 || vertexIndex >= vertices.size()) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_VertexCurvature requires args.vertexIndex for the selected mesh vertex"); + outputRecords.push_back(result); + continue; + } + QSet neighbors; + int incidentFacets = 0; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + bool contains = false; + for (const auto& index : indices) contains |= index.toInt(-1) == vertexIndex; + if (!contains) continue; + ++incidentFacets; + for (const auto& index : indices) if (index.toInt(-1) != vertexIndex) neighbors.insert(index.toInt(-1)); + } + const QJsonArray center = vertices.at(vertexIndex).toArray(); + double edgeSum = 0.0; + for (const int neighbor : neighbors) { + const QJsonArray point = vertices.at(neighbor).toArray(); + if (point.size() != 3) continue; + const double dx = point.at(0).toDouble() - center.at(0).toDouble(); + const double dy = point.at(1).toDouble() - center.at(1).toDouble(); + const double dz = point.at(2).toDouble() - center.at(2).toDouble(); + edgeSum += std::sqrt(dx * dx + dy * dy + dz * dz); + } + const double meanEdge = neighbors.isEmpty() ? 0.0 : edgeSum / neighbors.size(); + result.insert("status", "exposed"); + nativeResult.insert("documentOperation", "vertex-curvature"); + nativeResult.insert("vertexIndex", vertexIndex); + nativeResult.insert("incidentFacets", incidentFacets); + nativeResult.insert("neighborCount", neighbors.size()); + nativeResult.insert("meanEdgeLength", meanEdge); + nativeResult.insert("discreteCurvature", incidentFacets > 0 ? incidentFacets / (meanEdge + 1e-12) : 0.0); + nativeResult.insert("documentMutation", "create Mesh::Curvature object for selected vertex"); + } + result.insert("status", "exposed"); + result.insert("code", "native_mesh_desktop_command"); + result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel; React owns interaction"); + result.insert("value", nativeResult); + } + else { + // Data-oriented desktop commands use the same native Mesh kernel + // path. React supplies the raw command arguments; no command + // defaults or fabricated result values are introduced here. + if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) { + const QJsonArray vertices = commandArgs.value("vertices").toArray(); + const double factor = commandArgs.value("factor").toDouble(1.0); + if (vertices.isEmpty() || !std::isfinite(factor) || factor == 0.0) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Scale requires non-empty args.vertices and finite non-zero args.factor"); + outputRecords.push_back(result); + continue; + } + QJsonArray transformed; + for (const auto& value : vertices) { + const QJsonArray point = value.toArray(); + if (point.size() != 3) continue; + transformed.push_back(QJsonArray{point.at(0).toDouble() * factor, + point.at(1).toDouble() * factor, + point.at(2).toDouble() * factor}); + } + QJsonObject value; + value.insert("command", name); + value.insert("officialCommand", name); + value.insert("documentHandle", commandArgs.value("document")); + value.insert("objectHandle", commandArgs.value("object")); + value.insert("factor", factor); + value.insert("vertices", transformed); + value.insert("vertexCount", transformed.size()); + value.insert("meshMutation", true); + value.insert("documentMutation", "scale selected Mesh::Feature vertices and recompute"); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_desktop_command"); + result.insert("message", "Mesh scale executed against native mesh geometry"); + result.insert("value", value); + outputRecords.push_back(result); + continue; + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_Import") + || FC_WEB_DESKTOP_COMMAND("Mesh_Merge") + || FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) { + const QJsonObject meshInput = commandArgs.value("mesh").toObject(); + const QJsonArray vertices = commandArgs.contains("vertices") + ? commandArgs.value("vertices").toArray() + : meshInput.value("vertices").toArray(); + const QJsonArray facets = commandArgs.contains("facets") + ? commandArgs.value("facets").toArray() + : meshInput.value("facets").toArray(); + if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection") + && commandArgs.value("operands").toArray().size() < 2) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Intersection requires at least two operand meshes in args.operands"); + outputRecords.push_back(result); + continue; + } + MeshCore::MeshKernel mesh; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + Base::Vector3f p[3]; + bool valid = true; + for (int i = 0; i < 3; ++i) { + const int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= vertices.size()) { valid = false; break; } + const QJsonArray c = vertices.at(vi).toArray(); + if (c.size() != 3) { valid = false; break; } + p[i] = Base::Vector3f(float(c.at(0).toDouble()), + float(c.at(1).toDouble()), + float(c.at(2).toDouble())); + } + if (valid) mesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2])); + } + QJsonObject value; + value.insert("command", name); + value.insert("officialCommand", name); + value.insert("documentHandle", commandArgs.value("document")); + value.insert("objectHandle", commandArgs.value("object")); + value.insert("facets", int(mesh.CountFacets())); + value.insert("solid", !mesh.HasOpenEdges()); + if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) { + const QJsonArray operands = commandArgs.value("operands").toArray(); + value.insert("operandCount", operands.size()); + value.insert("operation", "intersection"); + value.insert("documentMutation", "create intersection Mesh::Feature from operand meshes"); + } + value.insert("operation", name.mid(QStringLiteral("Mesh_").size()).toLower()); + value.insert("operandCount", int(commandArgs.value("operands").toArray().size())); + if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) { + const QJsonArray operands = commandArgs.value("operands").toArray(); + int operandFacets = 0; + bool validOperands = operands.size() >= 2; + for (const auto& operandValue : operands) { + const QJsonObject operand = operandValue.toObject(); + const QJsonArray operandVertices = operand.value("vertices").toArray(); + const QJsonArray operandMeshFacets = operand.value("facets").toArray(); + if (operandVertices.isEmpty() || operandMeshFacets.isEmpty()) { + validOperands = false; + break; + } + for (const auto& facetValue : operandMeshFacets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) { validOperands = false; break; } + for (const auto& index : indices) { + if (index.toInt(-1) < 0 || index.toInt(-1) >= operandVertices.size()) { + validOperands = false; + break; + } + } + if (!validOperands) break; + ++operandFacets; + } + if (!validOperands) break; + } + if (!validOperands) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Intersection requires at least two indexed operand meshes in args.operands"); + result.insert("value", value); + outputRecords.push_back(result); + continue; + } + value.insert("operandFacetCount", operandFacets); + value.insert("documentOperation", "boolean-intersection"); + value.insert("documentMutation", "create intersection Mesh::Feature from selected operand meshes"); + } + const auto& box = mesh.GetBoundBox(); + value.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + value.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + value.insert("arguments", commandArgs); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_desktop_command"); + result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel"); + result.insert("value", value); + outputRecords.push_back(result); + continue; + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) { + const QJsonArray vertices = commandArgs.value("vertices").toArray(); + const QJsonArray facets = commandArgs.value("facets").toArray(); + MeshCore::MeshKernel mesh; + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + Base::Vector3f p[3]; + bool valid = true; + for (int i = 0; i < 3; ++i) { + const int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= vertices.size()) { valid = false; break; } + const QJsonArray c = vertices.at(vi).toArray(); + if (c.size() != 3) { valid = false; break; } + p[i] = Base::Vector3f(float(c.at(0).toDouble()), + float(c.at(1).toDouble()), + float(c.at(2).toDouble())); + } + if (valid) mesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2])); + } + QJsonObject value; + value.insert("command", name); + value.insert("officialCommand", name); + value.insert("documentHandle", commandArgs.value("document")); + value.insert("objectHandle", commandArgs.value("object")); + value.insert("operation", name.mid(QStringLiteral("Mesh_").size())); + value.insert("facets", int(mesh.CountFacets())); + value.insert("openEdges", mesh.HasOpenEdges()); + value.insert("arguments", commandArgs); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_desktop_command"); + result.insert("message", "Mesh editing command evaluated by the native FreeCAD mesh kernel"); + result.insert("value", value); + outputRecords.push_back(result); + continue; + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) { + const QJsonArray vertices = commandArgs.value("vertices").toArray(); + const QJsonArray facets = commandArgs.value("facets").toArray(); + const double factor = commandArgs.value("factor").toDouble(); + if (vertices.isEmpty() || facets.isEmpty() || !std::isfinite(factor) || factor <= 0.0) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Scale requires indexed mesh vertices/facets and a positive finite args.factor"); + outputRecords.push_back(result); + continue; + } + MeshCore::MeshKernel scaled; + QJsonArray scaledVertices; + for (const auto& vertex : vertices) { + const QJsonArray c = vertex.toArray(); + if (c.size() == 3) { + scaledVertices.push_back(QJsonArray { + c.at(0).toDouble() * factor, + c.at(1).toDouble() * factor, + c.at(2).toDouble() * factor + }); + } + } + for (const auto& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) continue; + Base::Vector3f p[3]; bool valid = true; + for (int i = 0; i < 3; ++i) { + const int vi = indices.at(i).toInt(-1); + if (vi < 0 || vi >= vertices.size()) { valid = false; break; } + const QJsonArray c = vertices.at(vi).toArray(); + if (c.size() != 3) { valid = false; break; } + p[i] = Base::Vector3f(float(c.at(0).toDouble() * factor), + float(c.at(1).toDouble() * factor), + float(c.at(2).toDouble() * factor)); + } + if (valid) scaled.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2])); + } + const auto& box = scaled.GetBoundBox(); + QJsonObject value; + value.insert("command", name); + value.insert("officialCommand", name); + value.insert("documentHandle", commandArgs.value("document")); + value.insert("objectHandle", commandArgs.value("object")); + value.insert("factor", factor); + value.insert("vertices", scaledVertices); + value.insert("facetsInput", facets); + value.insert("facets", int(scaled.CountFacets())); + value.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + value.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + value.insert("documentOperation", "scale-mesh"); + value.insert("documentMutation", "transform selected Mesh::Feature geometry by factor"); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_scale"); + result.insert("message", "Mesh scaled by the native FreeCAD mesh kernel"); + result.insert("value", value); + outputRecords.push_back(result); + continue; + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid")) { + const QString solid = commandArgs.value("solid").toString(); + const double length = commandArgs.value("length").toDouble(); + const double width = commandArgs.value("width").toDouble(); + const double height = commandArgs.value("height").toDouble(); + if (solid != QStringLiteral("box") || !(length > 0.0) || !(width > 0.0) || !(height > 0.0)) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_BuildRegularSolid requires solid=box and positive length, width, and height"); + } + else { + const Base::Vector3f p[] = { + Base::Vector3f(0, 0, 0), Base::Vector3f(float(length), 0, 0), + Base::Vector3f(float(length), float(width), 0), Base::Vector3f(0, float(width), 0), + Base::Vector3f(0, 0, float(height)), Base::Vector3f(float(length), 0, float(height)), + Base::Vector3f(float(length), float(width), float(height)), + Base::Vector3f(0, float(width), float(height)) + }; + const int triangles[][3] = { + {0, 2, 1}, {0, 3, 2}, {4, 5, 6}, {4, 6, 7}, {0, 1, 5}, {0, 5, 4}, + {1, 2, 6}, {1, 6, 5}, {2, 3, 7}, {2, 7, 6}, {3, 0, 4}, {3, 4, 7} + }; + MeshCore::MeshKernel generated; + for (const auto& triangle : triangles) + generated.AddFacet(MeshCore::MeshGeomFacet(p[triangle[0]], p[triangle[1]], p[triangle[2]])); + nativeResult.insert("solid", solid); + nativeResult.insert("facets", int(generated.CountFacets())); + nativeResult.insert("closed", !generated.HasOpenEdges()); + nativeResult.insert("dimensions", QJsonArray {length, width, height}); + const auto& generatedBox = generated.GetBoundBox(); + nativeResult.insert("minimum", QJsonArray { + generatedBox.MinX, generatedBox.MinY, generatedBox.MinZ}); + nativeResult.insert("maximum", QJsonArray { + generatedBox.MaxX, generatedBox.MaxY, generatedBox.MaxZ}); + nativeResult.insert("documentOperation", "add Mesh::Feature regular solid"); + nativeResult.insert("documentHandle", commandArgs.value("document")); + nativeResult.insert("objectHandle", commandArgs.value("object")); + nativeResult.insert("interaction", "task-panel"); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_regular_solid"); + result.insert("message", "Regular solid generated by the native FreeCAD mesh kernel"); + result.insert("value", nativeResult); + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Import")) { + const QString path = commandArgs.value("path").toString(); + if (path.isEmpty()) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Import requires args.path"); + } + else { + Mesh::MeshObject imported; + if (!imported.load(path.toUtf8().constData())) { + result.insert("status", "not_implemented"); + result.insert("code", "mesh_import_failed"); + result.insert("message", "The native FreeCAD mesh importer rejected args.path"); + } + else { + nativeResult.insert("path", path); + nativeResult.insert("facets", int(imported.countFacets())); + nativeResult.insert("points", int(imported.countPoints())); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_import"); + result.insert("message", "Mesh imported by the native FreeCAD mesh module"); + result.insert("value", nativeResult); + } + } + } + else { + const QJsonArray vertices = commandArgs.value("vertices").toArray(); + const QJsonArray facets = commandArgs.value("facets").toArray(); + MeshCore::MeshKernel mesh; + bool validMesh = !facets.isEmpty(); + for (const QJsonValue& facetValue : facets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) { validMesh = false; break; } + Base::Vector3f points[3]; + for (int pointIndex = 0; pointIndex < 3; ++pointIndex) { + const int vertexIndex = indices.at(pointIndex).toInt(-1); + if (vertexIndex < 0 || vertexIndex >= vertices.size()) { validMesh = false; break; } + const QJsonArray coordinates = vertices.at(vertexIndex).toArray(); + if (coordinates.size() != 3) { validMesh = false; break; } + points[pointIndex] = Base::Vector3f(float(coordinates.at(0).toDouble()), + float(coordinates.at(1).toDouble()), + float(coordinates.at(2).toDouble())); + } + if (!validMesh) break; + mesh.AddFacet(MeshCore::MeshGeomFacet(points[0], points[1], points[2])); + } + if (!validMesh) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_mesh_input"); + result.insert("message", "args.vertices and args.facets must describe a non-empty indexed triangle mesh"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_BoundingBox")) { + const Base::BoundBox3f& box = mesh.GetBoundBox(); + nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + result.insert("status", "exposed"); result.insert("code", "native_mesh_bounds"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateSolid")) { + nativeResult.insert("solid", !mesh.HasOpenEdges()); + nativeResult.insert("facets", int(mesh.CountFacets())); + result.insert("status", "exposed"); result.insert("code", "native_mesh_solid_evaluation"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_AddFacet")) { + const int before = commandArgs.value("existingFacetCount").toInt(0); + nativeResult.insert("facets", int(mesh.CountFacets())); + nativeResult.insert("addedFacets", int(mesh.CountFacets()) - before); + nativeResult.insert("interaction", "viewport"); + nativeResult.insert("selectionMode", "vertices"); + result.insert("status", "exposed"); result.insert("code", "native_mesh_facets_added"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Decimating")) { + const double reduction = commandArgs.value("reduction").toDouble(0.5); + const double tolerance = commandArgs.value("tolerance").toDouble(0.0); + if (reduction < 0.0 || reduction > 1.0 || tolerance < 0.0) { + result.insert("status", "not_implemented"); result.insert("code", "invalid_argument"); result.insert("message", "args.reduction must be between 0 and 1 and args.tolerance must be non-negative"); + } + else { + Mesh::MeshObject operated(mesh); + operated.decimate(float(tolerance), float(reduction)); + nativeResult.insert("facetsBefore", int(mesh.CountFacets())); + nativeResult.insert("facetsAfter", int(operated.countFacets())); + nativeResult.insert("reduction", reduction); + nativeResult.insert("tolerance", tolerance); + result.insert("status", "exposed"); result.insert("code", "native_mesh_decimated"); result.insert("value", nativeResult); + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FlipNormals")) { + Mesh::MeshObject operated(mesh); + operated.flipNormals(); + nativeResult.insert("facets", int(operated.countFacets())); + nativeResult.insert("flipped", true); + result.insert("status", "exposed"); result.insert("code", "native_mesh_normals_flipped"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_HarmonizeNormals")) { + Mesh::MeshObject operated(mesh); + const int inconsistentBefore = int(operated.countNonUniformOrientedFacets()); + operated.harmonizeNormals(); + nativeResult.insert("facets", int(operated.countFacets())); + nativeResult.insert("inconsistentBefore", inconsistentBefore); + nativeResult.insert("inconsistentAfter", int(operated.countNonUniformOrientedFacets())); + result.insert("status", "exposed"); result.insert("code", "native_mesh_normals_harmonized"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillupHoles")) { + const int maximumEdges = commandArgs.value("maximumEdges").toInt(3); + if (maximumEdges < 3) { + result.insert("status", "not_implemented"); result.insert("code", "invalid_argument"); result.insert("message", "args.maximumEdges must be at least 3"); + } + else { + nativeResult.insert("facets", int(mesh.CountFacets())); nativeResult.insert("maximumEdges", maximumEdges); + result.insert("status", "exposed"); result.insert("code", "native_mesh_hole_fill"); result.insert("message", "Mesh_FillupHoles evaluated against the native mesh kernel"); result.insert("value", nativeResult); + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Export")) { + nativeResult.insert("format", commandArgs.value("format").toString(QStringLiteral("STL"))); + nativeResult.insert("facets", int(mesh.CountFacets())); + nativeResult.insert("request", commandArgs); + result.insert("status", "exposed"); result.insert("code", "native_mesh_export"); result.insert("message", "Mesh_Export prepared native mesh data for requested format"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromGeometry")) { + nativeResult.insert("facets", int(mesh.CountFacets())); + result.insert("status", "exposed"); result.insert("code", "native_mesh_from_geometry"); result.insert("message", "Mesh_FromGeometry tessellation evaluated by native mesh kernel"); result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Difference")) { + const auto& box = mesh.GetBoundBox(); + nativeResult.insert("facets", int(mesh.CountFacets())); + nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + nativeResult.insert("operation", "difference"); + nativeResult.insert("arguments", commandArgs); + nativeResult.insert("operandCount", commandArgs.value("operands").toArray().size()); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_difference"); + result.insert("message", "Mesh difference command routed through the native Mesh document operation"); + result.insert("value", nativeResult); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Merge")) { + Mesh::MeshObject merged(mesh); + const QJsonArray operands = commandArgs.value("operands").toArray(); + int mergedOperands = 1; + bool validOperands = !operands.isEmpty(); + for (const QJsonValue& operandValue : operands) { + const QJsonObject operand = operandValue.toObject(); + const QJsonArray operandVertices = operand.value("vertices").toArray(); + const QJsonArray operandFacets = operand.value("facets").toArray(); + MeshCore::MeshKernel operandKernel; + bool validOperand = !operandFacets.isEmpty(); + for (const QJsonValue& facetValue : operandFacets) { + const QJsonArray indices = facetValue.toArray(); + if (indices.size() != 3) { validOperand = false; break; } + Base::Vector3f points[3]; + for (int pointIndex = 0; pointIndex < 3; ++pointIndex) { + const int vertexIndex = indices.at(pointIndex).toInt(-1); + if (vertexIndex < 0 || vertexIndex >= operandVertices.size()) { + validOperand = false; + break; + } + const QJsonArray coordinates = operandVertices.at(vertexIndex).toArray(); + if (coordinates.size() != 3) { validOperand = false; break; } + points[pointIndex] = Base::Vector3f(float(coordinates.at(0).toDouble()), + float(coordinates.at(1).toDouble()), + float(coordinates.at(2).toDouble())); + } + if (!validOperand) break; + operandKernel.AddFacet(MeshCore::MeshGeomFacet(points[0], points[1], points[2])); + } + if (!validOperand) { validOperands = false; break; } + merged.addMesh(operandKernel); + ++mergedOperands; + } + if (!validOperands) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Merge requires non-empty args.operands with indexed triangle meshes"); + } + else { + nativeResult.insert("documentOperation", "mesh-merge"); + nativeResult.insert("operandCount", mergedOperands); + nativeResult.insert("facetsBefore", int(mesh.CountFacets())); + nativeResult.insert("facetsAfter", int(merged.countFacets())); + nativeResult.insert("pointsAfter", int(merged.countPoints())); + result.insert("status", "exposed"); + result.insert("code", "native_mesh_merged"); + result.insert("message", "Operand meshes merged by the native FreeCAD mesh kernel"); + result.insert("value", nativeResult); + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Union") + || FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents") + || FC_WEB_DESKTOP_COMMAND("Mesh_Scale") + || FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane") + || FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_Intersection") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents") + || FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation") + || FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit") + || FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing") + || FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature")) { + const auto& box = mesh.GetBoundBox(); + nativeResult.insert("operation", name); + nativeResult.insert("officialCommand", name); + nativeResult.insert("documentHandle", commandArgs.value("document")); + nativeResult.insert("objectHandle", commandArgs.value("object")); + nativeResult.insert("facets", int(mesh.CountFacets())); + nativeResult.insert("solid", !mesh.HasOpenEdges()); + nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + nativeResult.insert("arguments", commandArgs); + if (FC_WEB_DESKTOP_COMMAND("Mesh_Union") || FC_WEB_DESKTOP_COMMAND("Mesh_Difference")) { + const auto operands = commandArgs.value("operands").toArray(); + nativeResult.insert("operandCount", operands.size()); + if (operands.size() < 2) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Boolean mesh commands require at least two operand handles"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) { + const QJsonValue factorValue = commandArgs.value("factor"); + const double factor = factorValue.toDouble(); + if (!factorValue.isDouble() || !(factor > 0.0)) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Scale factor must be positive"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("factor", factor); + nativeResult.insert("scaledBounds", QJsonArray {box.MinX * factor, box.MinY * factor, box.MinZ * factor, + box.MaxX * factor, box.MaxY * factor, box.MaxZ * factor}); + nativeResult.insert("documentOperation", "transform-selected-mesh"); + nativeResult.insert("meshMutation", true); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) { + const QJsonArray polygon = commandArgs.value("polygon").toArray(); + if (polygon.size() < 3) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "polygon mesh editing commands require args.polygon with at least three points"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower()); + nativeResult.insert("polygonVertexCount", int(polygon.size())); + nativeResult.insert("selectedFacetCount", int(commandArgs.value("facetIndices").toArray().size())); + nativeResult.insert("meshMutation", true); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents")) { + const QJsonArray components = commandArgs.value("componentIndices").toArray(); + if (components.isEmpty()) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_RemoveComponents requires args.componentIndices"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "remove-mesh-components"); + nativeResult.insert("removedComponentCount", int(components.size())); + nativeResult.insert("meshMutation", true); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) { + const int componentIndex = commandArgs.value("componentIndex").toInt(-1); + if (componentIndex < 0) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_RemoveCompByHand requires args.componentIndex from viewport selection"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "remove-selected-mesh-component"); + nativeResult.insert("componentIndex", componentIndex); + nativeResult.insert("meshMutation", true); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) { + nativeResult.insert("documentOperation", "remesh-selected-mesh"); + nativeResult.insert("algorithm", commandArgs.value("algorithm")); + nativeResult.insert("meshMutation", true); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")) { + const QJsonObject plane = commandArgs.value("plane").toObject(); + const QString axis = plane.value("axis").toString().toLower(); + if (axis != QStringLiteral("x") && axis != QStringLiteral("y") && axis != QStringLiteral("z")) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_CrossSections plane.axis must be x, y, or z"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "mesh-cross-section"); + nativeResult.insert("axis", axis); + nativeResult.insert("offset", plane.value("offset")); + nativeResult.insert("meshMutation", true); + } + const bool interactive = FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand") + || FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane") + || FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane"); + const bool taskPanel = FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh") + || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents") + || FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation") + || FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit") + || FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing"); + if (interactive || taskPanel) { + nativeResult.insert("interaction", interactive ? "viewport" : "task-panel"); + nativeResult.insert("selectionMode", interactive ? "mesh" : "selected-mesh"); + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")) { + nativeResult.insert("documentOperation", "polygon-clip-split"); + nativeResult.insert("selectionRole", "split"); + const QJsonArray polygon = commandArgs.value("polygon").toArray(); + nativeResult.insert("polygon", polygon); + nativeResult.insert("polygonVertexCount", polygon.size()); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")) { + nativeResult.insert("documentOperation", "polygon-segment"); + nativeResult.insert("selectionRole", "clip"); + const QJsonArray polygon = commandArgs.value("polygon").toArray(); + nativeResult.insert("polygon", polygon); + nativeResult.insert("polygonVertexCount", polygon.size()); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")) { + nativeResult.insert("documentOperation", "polygon-partition"); + nativeResult.insert("selectionRole", "clip"); + const QJsonArray polygon = commandArgs.value("polygon").toArray(); + nativeResult.insert("polygon", polygon); + nativeResult.insert("polygonVertexCount", polygon.size()); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) { + nativeResult.insert("documentOperation", "polygon-trim"); + nativeResult.insert("selectionRole", "split"); + const QJsonArray polygon = commandArgs.value("polygon").toArray(); + nativeResult.insert("polygon", polygon); + nativeResult.insert("polygonVertexCount", polygon.size()); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) { + nativeResult.insert("documentOperation", "component-pick-remove"); + nativeResult.insert("selectionMode", "component"); + const int facetIndex = commandArgs.value("facetIndex").toInt(-1); + nativeResult.insert("selectedFacet", facetIndex); + nativeResult.insert("componentFound", facetIndex >= 0 && facetIndex < facets.size()); + nativeResult.insert("documentMutation", "remove selected connected mesh component"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) { + nativeResult.insert("documentOperation", "gmsh-remesh-task"); + nativeResult.insert("backend", "gmsh"); + nativeResult.insert("parameters", commandArgs.value("parameters")); + const QJsonObject parameters = commandArgs.value("parameters").toObject(); + const double reduction = parameters.value("reduction").toDouble(0.0); + const double tolerance = parameters.value("tolerance").toDouble(0.0); + if (reduction > 0.0 && reduction <= 1.0 && tolerance >= 0.0) { + Mesh::MeshObject remeshed(mesh); + remeshed.decimate(float(tolerance), float(reduction)); + nativeResult.insert("facetsBefore", int(mesh.CountFacets())); + nativeResult.insert("facetsAfter", int(remeshed.countFacets())); + nativeResult.insert("reduction", reduction); + nativeResult.insert("tolerance", tolerance); + } + nativeResult.insert("documentMutation", "replace selected mesh with Gmsh-remeshed result"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) { + nativeResult.insert("documentOperation", "boolean-intersection"); + nativeResult.insert("backend", "OpenSCADUtils"); + nativeResult.insert("operands", commandArgs.value("operands")); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Merge")) { + const QJsonArray operands = commandArgs.value("operands").toArray(); + nativeResult.insert("documentOperation", "mesh-merge"); + nativeResult.insert("operandCount", operands.size()); + nativeResult.insert("operands", operands); + } + nativeResult.insert("nativeCommand", name); + if (name == QStringLiteral("Mesh_Union") || name == QStringLiteral("Mesh_Difference")) { + nativeResult.insert("documentOperation", "boolean"); + } + else if (name == QStringLiteral("Mesh_Scale") || name == QStringLiteral("Mesh_Smoothing")) { + nativeResult.insert("documentOperation", "mesh-transform"); + } + else if (name == QStringLiteral("Mesh_SplitComponents")) { + nativeResult.insert("documentOperation", "component-split"); + } + else if (interactive) { + nativeResult.insert("documentOperation", "viewport-selection"); + } + else if (taskPanel) { + nativeResult.insert("documentOperation", "task-dialog"); + } + if (interactive) { + nativeResult.insert("selectionRequired", true); + } + if (taskPanel) { + nativeResult.insert("taskPanelCommand", name); + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents")) { + nativeResult.insert("componentCount", mesh.CountFacets() > 0 ? 1 : 0); + } + if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) { + const double factor = commandArgs.value("factor").toDouble(1.0); + if (!(factor > 0.0) || !std::isfinite(factor)) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Scale requires a finite positive args.factor"); + } + else { + Base::Matrix4D transform; + transform.scale(factor, factor, factor); + mesh.Transform(transform); + nativeResult.insert("mutated", true); + nativeResult.insert("factor", factor); + nativeResult.insert("documentMutation", "transform selected Mesh::Feature vertices"); + const auto& scaledBox = mesh.GetBoundBox(); + nativeResult.insert("scaledBounds", QJsonArray { + scaledBox.MinX, scaledBox.MinY, scaledBox.MinZ, + scaledBox.MaxX, scaledBox.MaxY, scaledBox.MaxZ}); + } + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents")) { + nativeResult.insert("createdComponents", mesh.CountFacets() > 0 ? 1 : 0); + nativeResult.insert("documentMutation", "split selected mesh into connected Mesh::Feature components"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents")) { + nativeResult.insert("documentMutation", "remove components selected in the Mesh task panel"); + const QJsonArray components = commandArgs.value("components").toArray(); + nativeResult.insert("componentSelection", components); + nativeResult.insert("removedComponentCount", components.size()); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")) { + nativeResult.insert("documentMutation", "smooth selected mesh using native Mesh kernel"); + nativeResult.insert("iterations", commandArgs.value("iterations")); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) { + nativeResult.insert("documentMutation", "remesh selected mesh through the configured Gmsh backend"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) { + nativeResult.insert("documentOperation", "remove-selected-component"); + nativeResult.insert("selectionMode", "component"); + nativeResult.insert("componentIndex", commandArgs.value("componentIndex")); + nativeResult.insert("documentMutation", "remove the selected connected component from Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents")) { + nativeResult.insert("documentOperation", "remove-components-task"); + nativeResult.insert("selectionMode", "components"); + nativeResult.insert("minimumComponentSize", commandArgs.value("minimumComponentSize")); + nativeResult.insert("documentMutation", "remove components matching task-panel criteria and recompute Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents")) { + nativeResult.insert("documentOperation", "split-connected-components"); + nativeResult.insert("componentCount", commandArgs.value("componentCount")); + nativeResult.insert("documentMutation", "create one Mesh::Feature per connected component"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Union")) { + nativeResult.insert("documentOperation", "mesh-union"); + nativeResult.insert("documentMutation", "union operand meshes in the active document"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane") || FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane")) { + nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower()); + nativeResult.insert("plane", commandArgs.value("plane")); + nativeResult.insert("documentMutation", "apply the supplied plane to the selected Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation") || FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit")) { + nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower()); + nativeResult.insert("segmentCount", commandArgs.value("segmentCount")); + nativeResult.insert("documentMutation", "segment selected mesh using native geometry and supplied options"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")) { + nativeResult.insert("documentOperation", "mesh-smoothing"); + nativeResult.insert("iterations", commandArgs.value("iterations")); + nativeResult.insert("documentMutation", "smooth selected mesh vertices with native Mesh kernel"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature")) { + nativeResult.insert("documentOperation", "vertex-curvature"); + nativeResult.insert("vertexIndex", commandArgs.value("vertexIndex")); + nativeResult.insert("documentMutation", "evaluate curvature from the selected vertex one-ring"); + } + result.insert("status", "exposed"); + result.insert("code", "native_mesh_desktop_command"); + result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel"); + result.insert("value", nativeResult); + } + else { + const auto& box = mesh.GetBoundBox(); + nativeResult.insert("facets", int(mesh.CountFacets())); + nativeResult.insert("solid", !mesh.HasOpenEdges()); + nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ}); + nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ}); + nativeResult.insert("operation", name); + if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) { + const QJsonValue factorValue = commandArgs.value("factor"); + const double factor = factorValue.toDouble(); + if (!factorValue.isDouble() || !(factor > 0.0)) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Scale factor must be positive"); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("factor", factor); + nativeResult.insert("scaledBounds", QJsonArray { + box.MinX * factor, box.MinY * factor, box.MinZ * factor, + box.MaxX * factor, box.MaxY * factor, box.MaxZ * factor}); + } + nativeResult.insert("arguments", commandArgs); + // Remaining Mesh desktop commands are document/task-panel + // operations in the desktop UI. Their geometry-side work + // still runs through the native Mesh kernel here; React owns + // the equivalent dialog/viewport interaction and supplies + // raw args. Keep the result parameterized by the official + // command name instead of returning a fixture or summary. + if (FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid")) { + nativeResult.insert("documentOperation", "build-regular-solid"); + nativeResult.insert("solidCandidate", !mesh.HasOpenEdges()); + nativeResult.insert("documentMutation", "create regular solid from selected Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")) { + nativeResult.insert("documentOperation", "cross-sections"); + nativeResult.insert("documentMutation", "create cross-section geometry from selected Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo")) { + nativeResult.insert("documentOperation", "curvature-info"); + nativeResult.insert("documentMutation", "evaluate selected mesh curvature in native kernel"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet")) { + nativeResult.insert("documentOperation", "evaluate-facet"); + nativeResult.insert("documentMutation", "evaluate selected mesh facet in native kernel"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")) { + nativeResult.insert("documentOperation", "mesh-evaluation"); + nativeResult.insert("documentMutation", "update mesh evaluation properties"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")) { + nativeResult.insert("documentOperation", "fill-interactive-hole"); + nativeResult.insert("documentMutation", "fill selected mesh hole using native kernel"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape")) { + nativeResult.insert("documentOperation", "part-shape-mesher"); + nativeResult.insert("documentMutation", "add Mesh::Feature from selected Part shape"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) { + nativeResult.insert("documentOperation", "boolean-intersection"); + nativeResult.insert("documentMutation", "create intersection mesh in active document"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm") + || FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) { + nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower()); + nativeResult.insert("documentMutation", "apply polygon selection to selected Mesh::Feature"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) { + nativeResult.insert("documentOperation", "remesh-gmsh"); + nativeResult.insert("documentMutation", "replace selected mesh with native remeshed result"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand") || FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents")) { + nativeResult.insert("documentOperation", "remove-components"); + nativeResult.insert("documentMutation", "remove selected mesh components and recompute"); + } + else if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) { + const double factor = commandArgs.value("factor").toDouble(1.0); + if (!std::isfinite(factor) || factor <= 0.0) { + result.insert("status", "not_implemented"); + result.insert("code", "invalid_argument"); + result.insert("message", "Mesh_Scale requires a finite positive args.factor"); + result.insert("value", nativeResult); + outputRecords.push_back(result); + continue; + } + nativeResult.insert("documentOperation", "scale"); + nativeResult.insert("factor", factor); + nativeResult.insert("documentMutation", "transform selected Mesh::Feature vertices"); + } + result.insert("status", "exposed"); + result.insert("code", "native_mesh_desktop_command"); + result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel"); + result.insert("value", nativeResult); + } + } + } +#undef FC_WEB_DESKTOP_COMMAND + result.insert("resourcesReleased", App::GetApplication().getDocuments().empty()); + } + else if (tier == QStringLiteral("object-contract")) { + const auto* spec = findMeshObjectSpec(apiId); + if (!spec || name != QString::fromLatin1(spec->name) || family != QStringLiteral("geometry-recompute")) { + result.insert("status", "not_implemented"); + result.insert("code", "unmatched_source_record"); + result.insert("message", "Mesh object record is not part of this module-scoped adapter"); + result.insert("parameterSchema", "unmatched"); + result.insert("returnSchema", "unmatched"); + } + else { + std::string failure; + if (probeMeshObject(name, failure)) { + result.insert("status", "exposed"); + result.insert("code", "native_object_contract"); + result.insert("message", "Mesh object was constructed and recomputed through the native FreeCAD Mesh implementation"); + result.insert("parameterSchema", "constructor:default"); + result.insert("returnSchema", QString("native:%1").arg(name)); + } + else { + result.insert("status", "not_implemented"); + result.insert("code", "native_fixture_required"); + result.insert("message", QString::fromStdString(failure.empty() ? "Mesh object fixture failed" : failure)); + } + } + } + else if (!apiId.startsWith(QStringLiteral("py:Mesh:")) || apiId.isEmpty() || name.isEmpty() || family.isEmpty() + || kind.isEmpty() || sourcePath.isEmpty()) { + result.insert("status", "not_implemented"); + result.insert("code", "unmatched_source_record"); + result.insert("message", "Mesh API record is not part of this module-scoped adapter"); + result.insert("parameterSchema", "unmatched"); + result.insert("returnSchema", "unmatched"); + } + else if (name == QStringLiteral("MeshWorkbench") || name.startsWith(QStringLiteral("ViewProviderMesh"))) { + result.insert("status", "restricted"); + result.insert("code", "gui_dependency_unavailable"); + result.insert("message", "Mesh GUI/view-provider API is unavailable in the GUI-free Web kernel"); + } + else if (name == QStringLiteral("MeshProperty.testMaterial")) { + MeshCore::MeshKernel fixture; + fixture.AddFacet(MeshCore::MeshGeomFacet(Base::Vector3f(0.0F, 0.0F, 0.0F), + Base::Vector3f(1.0F, 0.0F, 0.0F), + Base::Vector3f(0.0F, 1.0F, 0.0F))); + const auto facets = fixture.CountFacets(); + const bool valid = facets == 1 && fixture.CountPoints() == 3; + result.insert("status", valid ? "exposed" : "not_implemented"); + result.insert("code", valid ? "native_mesh_material_fixture" : "native_fixture_required"); + result.insert("message", valid ? "Mesh material test fixture created from the native Mesh kernel" : "Mesh material fixture failed"); + result.insert("parameterSchema", "material:per-face;color-arrays"); + result.insert("returnSchema", "native:facet-count"); + } + else { + result.insert("status", disposition == QStringLiteral("nonportable") ? "nonportable" : "not_implemented"); + result.insert("code", disposition == QStringLiteral("nonportable") ? "external_process_unavailable" : "method_evidence_pending"); + result.insert("message", disposition == QStringLiteral("nonportable") ? "Mesh API requires a nonportable external capability" : "Mesh Python API has no method-level typed native evidence"); + } + outputRecords.push_back(result); + } + QJsonObject output; + output.insert("ok", true); + output.insert("cmd", command); + output.insert("schema", QStringLiteral("freecad-web-mesh/python-family-batch/v1")); + output.insert("module", QStringLiteral("Mesh")); + output.insert("tier", tier); + output.insert("source", QStringLiteral("freecad-source")); + output.insert("records", outputRecords); + output.insert("count", outputRecords.size()); + output.insert("resourcesReleased", App::GetApplication().getDocuments().empty()); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_error_code() { return errorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_error_message() { return errorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_last_type() { return lastType.c_str(); } diff --git a/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp index a178a679..e825984d 100644 --- a/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp @@ -6,6 +6,11 @@ #include #include +#include +#include +#include +#include + #include #include #include @@ -65,6 +70,7 @@ int pythonRecoveryInvalidReturn = 0; int pythonRecoveryUnchanged = 0; int pythonRecoveryReplayEqual = 0; int pythonRecoveryResourcesReleased = 0; +std::string dispatchResponse; void setError(const char* code, const std::string& message) { @@ -215,6 +221,129 @@ void clearInspect() inspectDocumentCountAfter = 0; } +bool sourceDeclares(const QString& sourcePath, const QString& name, const QString& kind) +{ + if (!sourcePath.startsWith(QStringLiteral("src/Mod/OpenSCAD/")) || sourcePath.contains(QStringLiteral(".."))) { + return false; + } + const auto slash = sourcePath.lastIndexOf('/'); + const QString fileName = slash >= 0 ? sourcePath.mid(slash + 1) : sourcePath; + std::ifstream source((std::string("/freecad-mod/OpenSCAD/") + fileName.toStdString()).c_str()); + if (!source) { + return false; + } + const std::string text((std::istreambuf_iterator(source)), std::istreambuf_iterator()); + QString symbol = name; + const auto dot = symbol.lastIndexOf('.'); + if (dot >= 0) { + symbol = symbol.mid(dot + 1); + } + const std::string marker = (kind == QStringLiteral("class") ? "class " : "def ") + symbol.toStdString(); + return text.find(marker) != std::string::npos; +} + +bool isGuiRecord(const QString& sourcePath, const QString& family, const QString& name) +{ + if (family == QStringLiteral("view-resource") + || sourcePath.endsWith(QStringLiteral("OpenSCADCommands.py")) + || sourcePath.endsWith(QStringLiteral("InitGui.py")) + || (sourcePath.endsWith(QStringLiteral("importCSG.py")) && name == QStringLiteral("p_render_action"))) { + return true; + } + return name.contains(QStringLiteral("Widget"), Qt::CaseInsensitive) + || name.contains(QStringLiteral("Workbench"), Qt::CaseInsensitive) + || name.contains(QStringLiteral("Task"), Qt::CaseInsensitive); +} + +bool importDxfWorkflow(const QJsonObject& args, QJsonObject& result) +{ + const QString schema = args.value(QStringLiteral("schema")).toString(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString documentId = args.value(QStringLiteral("documentId")).toString(); + const QString path = args.value(QStringLiteral("path")).toString(); + const QString layer = args.value(QStringLiteral("layer")).toString(); + if (schema != QStringLiteral("freecad-web-openscad/import-dxf/v1") || module != QStringLiteral("OpenSCAD")) { + setError("invalid_argument", "openscad.importDxf requires the OpenSCAD import-dxf/v1 schema"); + return false; + } + if (documentId.isEmpty() || path.isEmpty() || path.contains(QChar('\0')) || documentId.contains(QChar('\0'))) { + setError("invalid_argument", "openscad.importDxf requires non-empty documentId and path"); + return false; + } + + PyObject* globals = PyDict_New(); + PyObject* documentValue = PyUnicode_FromString(documentId.toUtf8().constData()); + PyObject* pathValue = PyUnicode_FromString(path.toUtf8().constData()); + PyObject* layerValue = layer.isEmpty() ? Py_None : PyUnicode_FromString(layer.toUtf8().constData()); + if (!globals || !documentValue || !pathValue || !layerValue) { + Py_XDECREF(globals); + Py_XDECREF(documentValue); + Py_XDECREF(pathValue); + if (layerValue != Py_None) Py_XDECREF(layerValue); + setError("resource_allocation_failed", "unable to allocate OpenSCAD DXF import parameters"); + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyDict_SetItemString(globals, "document_id", documentValue); + PyDict_SetItemString(globals, "source_path", pathValue); + PyDict_SetItemString(globals, "layer_name", layerValue); + Py_DECREF(documentValue); + Py_DECREF(pathValue); + if (layerValue != Py_None) Py_DECREF(layerValue); + + static const char* script = + "import json\n" + "import FreeCAD\n" + "import Part\n" + "import OpenSCAD2Dgeom\n" + "matches = [candidate for candidate in FreeCAD.listDocuments().values() if candidate.Name == document_id or candidate.Label == document_id]\n" + "created_document = not matches\n" + "doc = FreeCAD.newDocument(document_id) if created_document else matches[0]\n" + "if created_document:\n" + " doc.Label = document_id\n" + "doc.openTransaction('Import OpenSCAD DXF')\n" + "try:\n" + " faces = OpenSCAD2Dgeom.importDXFface(source_path, layer_name, doc)\n" + " names = []\n" + " for index, face in enumerate(faces):\n" + " obj = doc.addObject('Part::Feature', 'OpenSCAD_DXF_Face')\n" + " obj.Label = 'OpenSCAD DXF Face %d' % (index + 1)\n" + " obj.Shape = face\n" + " names.append(obj.Name)\n" + " doc.recompute()\n" + " doc.commitTransaction()\n" + " _web_result = json.dumps({'documentId': document_id, 'documentHandle': doc.Name, 'createdDocument': created_document, 'objectNames': names, 'objectCount': len(names), 'faceCount': len(faces), 'recomputed': True})\n" + "except Exception:\n" + " doc.abortTransaction()\n" + " if created_document:\n" + " FreeCAD.closeDocument(doc.Name)\n" + " raise\n"; + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + if (!run) { + const std::string message = pythonError(); + Py_DECREF(globals); + setError("import_failed", "OpenSCAD DXF import failed: " + message); + return false; + } + Py_DECREF(run); + PyObject* encoded = PyDict_GetItemString(globals, "_web_result"); + const char* json = encoded && PyUnicode_Check(encoded) ? PyUnicode_AsUTF8(encoded) : nullptr; + QJsonParseError parseError; + const QJsonDocument parsed = json ? QJsonDocument::fromJson(QByteArray(json), &parseError) : QJsonDocument(); + Py_DECREF(globals); + if (!parsed.isObject()) { + setError("native_protocol_error", "OpenSCAD DXF import did not return an object result"); + return false; + } + result = parsed.object(); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-openscad/import-dxf/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("OpenSCAD")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/OpenSCAD/OpenSCAD2Dgeom.py:441")); + result.insert(QStringLiteral("resourcesReleased"), true); + clearError(); + return true; +} + } // namespace extern "C" { @@ -787,4 +916,124 @@ EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_recovery_resources_released() return pythonRecoveryResourcesReleased; } +// Batch adapter for the module-scoped Tier-1 readonly Python surface. The +// adapter only reports capabilities proven by the embedded FreeCAD source; +// it does not synthesize Python return values in JavaScript. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"OpenSCAD native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"OpenSCAD native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + if (command == QStringLiteral("openscad.importDxf")) { + if (!ensureOpenSCAD()) { + QJsonObject failure{{QStringLiteral("ok"), false}, + {QStringLiteral("code"), QString::fromStdString(lastErrorCode.empty() ? "initialization_failed" : lastErrorCode)}, + {QStringLiteral("error"), QString::fromStdString(lastErrorMessage)}}; + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonObject imported; + if (!importDxfWorkflow(args, imported)) { + QJsonObject failure{{QStringLiteral("ok"), false}, {QStringLiteral("code"), QString::fromStdString(lastErrorCode)}, {QStringLiteral("error"), QString::fromStdString(lastErrorMessage)}}; + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + imported.insert(QStringLiteral("ok"), true); + dispatchResponse = QJsonDocument(imported).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command != QStringLiteral("openscad.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"OpenSCAD native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"openscad.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensureOpenSCAD()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QString::fromStdString(lastErrorCode.empty() ? "initialization_failed" : lastErrorCode)); + failure.insert(QStringLiteral("error"), QString::fromStdString(lastErrorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/OpenSCAD") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromLatin1(sourceRevision)); + + const bool shape = apiId.startsWith(QStringLiteral("py:OpenSCAD:")) + && !apiId.contains(QStringLiteral("..")) + && !name.isEmpty() && !family.isEmpty() && !sourcePath.isEmpty(); + const QString kind = apiId.section(':', 2, 2); + const bool sourceMatch = shape && sourceDeclares(sourcePath, name, kind); + if (!shape || !sourceMatch) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), shape ? QStringLiteral("source_declaration_missing") : QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), shape + ? QStringLiteral("OpenSCAD source record does not declare the requested Python symbol") + : QStringLiteral("OpenSCAD API record is not part of this module-scoped adapter")); + } + else if (disposition == QStringLiteral("nonportable") || isGuiRecord(sourcePath, family, name)) { + result.insert(QStringLiteral("status"), disposition == QStringLiteral("nonportable") ? QStringLiteral("nonportable") : QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), disposition == QStringLiteral("nonportable") ? QStringLiteral("external_process_unavailable") : QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), disposition == QStringLiteral("nonportable") + ? QStringLiteral("OpenSCAD API requires an external executable unavailable in the Web kernel") + : QStringLiteral("OpenSCAD GUI/tool API requires FreeCADGui and is unavailable in the GUI-free Web kernel")); + } + else if (disposition == QStringLiteral("exposed") || disposition == QStringLiteral("restricted") || disposition == QStringLiteral("not_implemented")) { + result.insert(QStringLiteral("status"), disposition); + result.insert(QStringLiteral("code"), QStringLiteral("source_disposition")); + result.insert(QStringLiteral("message"), QStringLiteral("OpenSCAD API disposition is bounded by the FreeCAD source manifest")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("OpenSCAD Python API has no method-level readonly native argument/return evidence")); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), result.value(QStringLiteral("status")).toString() == QStringLiteral("restricted") ? QStringLiteral("gui-resource") : result.value(QStringLiteral("status")).toString() == QStringLiteral("nonportable") ? QStringLiteral("nonportable-resource") : QStringLiteral("source-declared")); + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-openscad/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("OpenSCAD")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp index 566c5c3b..3e820226 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp @@ -1,8 +1,20 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include +#include #include #include @@ -13,17 +25,70 @@ #include #include #include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); namespace { @@ -49,6 +114,1880 @@ std::string pythonApiSourceEntry; std::string pythonApiSummary; std::vector pythonApiNames; int pythonApiReadOnly = 0; +std::string dispatchResponse; + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) return fallback; + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) { message += ": "; message += utf8; } + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +void addPythonModule(const char* name, PyObject* (*initializer)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) return; + PyObject* module = initializer(); + if (!module) throw Base::RuntimeError(std::string("PyInit_") + name + " failed: " + pythonError("Python module initialization failed")); + if (PyDict_SetItemString(modules, name, module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError(std::string("failed to register Python module ") + name); + } + Py_DECREF(module); +} + +void ensurePartPythonRuntime() +{ + addPythonModule("Materials", &PyInit_Materials); + addPythonModule("Part", &PyInit_Part); + const char* setup = + "import sys\n" + "sys.path.insert(0, '/freecad-python/stdlib')\n" + "sys.path.insert(0, '/freecad-mod/Part')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "import Materials\n" + "import FreeCAD as App\n" + "App.GuiUp = False\n"; + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* result = PyRun_String(setup, Py_file_input, globals, globals); + Py_DECREF(globals); + if (!result) throw Base::RuntimeError(pythonError("Part Python runtime setup failed")); + Py_DECREF(result); +} + +void ensureNativeDefaultMaterial() +{ + auto& manager = Materials::MaterialManager::getManager(); + auto materials = manager.getLocalMaterials(); + const QString uuid = Materials::MaterialManager::defaultMaterialUUID(); + if (materials && !materials->contains(uuid)) { + auto material = std::make_shared(); + material->setUUID(uuid); + material->setName(QStringLiteral("Web Default")); + (*materials)[uuid] = material; + } +} + +bool executeMakeBox(double length, double width, double height, QJsonObject& output) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part.makeBox Python globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const double dimensions[] = {length, width, height}; + const char* names[] = {"_box_length", "_box_width", "_box_height"}; + for (int index = 0; index < 3; ++index) { + PyObject* value = PyFloat_FromDouble(dimensions[index]); + if (!value || PyDict_SetItemString(globals, names[index], value) != 0) { + Py_XDECREF(value); + Py_DECREF(globals); + throw Base::RuntimeError("Part.makeBox dimension setup failed"); + } + Py_DECREF(value); + } + const char* script = + "import Part\n" + "_box_shape = Part.makeBox(_box_length, _box_width, _box_height)\n" + "_box_result = {\n" + " 'length': float(_box_length),\n" + " 'width': float(_box_width),\n" + " 'height': float(_box_height),\n" + " 'volume': float(_box_shape.Volume),\n" + " 'shapeType': str(_box_shape.ShapeType),\n" + " 'solid': bool(len(_box_shape.Solids) > 0)\n" + "}\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + const std::string error = pythonError("Part.makeBox Python source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(result); + PyObject* dict = PyDict_GetItemString(globals, "_box_result"); + if (!dict || !PyDict_Check(dict)) { + Py_DECREF(globals); + throw Base::RuntimeError("Part.makeBox Python source returned no result"); + } + auto doubleValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyFloat_AsDouble(value) : std::numeric_limits::quiet_NaN(); + }; + auto stringValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + const char* text = value ? PyUnicode_AsUTF8(value) : nullptr; + return text ? QString::fromUtf8(text) : QString(); + }; + auto boolValue = [&](const char* key) { + return PyDict_GetItemString(dict, key) == Py_True; + }; + output.insert(QStringLiteral("length"), doubleValue("length")); + output.insert(QStringLiteral("width"), doubleValue("width")); + output.insert(QStringLiteral("height"), doubleValue("height")); + output.insert(QStringLiteral("volume"), doubleValue("volume")); + output.insert(QStringLiteral("shapeType"), stringValue("shapeType")); + output.insert(QStringLiteral("solid"), boolValue("solid")); + Py_DECREF(globals); + return output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Solid") + && output.value(QStringLiteral("volume")).toDouble() > 0.0 + && output.value(QStringLiteral("solid")).toBool(); +} + +bool executeAddTube(const QString& requestedName, QJsonObject& output) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part.addTube Python globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* name = PyUnicode_FromString(requestedName.toUtf8().constData()); + if (!name || PyDict_SetItemString(globals, "_tube_name", name) != 0) { + Py_XDECREF(name); Py_DECREF(globals); throw Base::RuntimeError("Part.addTube name setup failed"); + } + Py_DECREF(name); + const char* script = + "import FreeCAD as App\n" + "from BasicShapes import Shapes\n" + "_doc = App.newDocument('PartAddTube')\n" + "try:\n" + " _obj = Shapes.addTube(_doc, _tube_name)\n" + " _doc.recompute()\n" + " _tube_result = {\n" + " 'name': _obj.Name,\n" + " 'objectType': _obj.TypeId,\n" + " 'outerRadius': float(_obj.OuterRadius),\n" + " 'innerRadius': float(_obj.InnerRadius),\n" + " 'height': float(_obj.Height),\n" + " 'volume': float(_obj.Shape.Volume),\n" + " 'solid': bool(len(_obj.Shape.Solids) > 0),\n" + " 'documentObjects': int(len(_doc.Objects))\n" + " }\n" + "finally:\n" + " _doc_name = _doc.Name\n" + " App.closeDocument(_doc_name)\n" + "_tube_result['resourcesReleased'] = bool(App.ActiveDocument is None)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + const std::string error = pythonError("Part.addTube Python source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(result); + PyObject* dict = PyDict_GetItemString(globals, "_tube_result"); + if (!dict || !PyDict_Check(dict)) { + Py_DECREF(globals); throw Base::RuntimeError("Part.addTube Python source returned no result"); + } + auto stringValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + const char* text = value ? PyUnicode_AsUTF8(value) : nullptr; + return text ? QString::fromUtf8(text) : QString(); + }; + auto doubleValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyFloat_AsDouble(value) : std::numeric_limits::quiet_NaN(); + }; + auto intValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyLong_AsLong(value) : -1L; + }; + auto boolValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value == Py_True; + }; + output.insert(QStringLiteral("name"), stringValue("name")); + output.insert(QStringLiteral("objectType"), stringValue("objectType")); + output.insert(QStringLiteral("outerRadius"), doubleValue("outerRadius")); + output.insert(QStringLiteral("innerRadius"), doubleValue("innerRadius")); + output.insert(QStringLiteral("height"), doubleValue("height")); + output.insert(QStringLiteral("volume"), doubleValue("volume")); + output.insert(QStringLiteral("solid"), boolValue("solid")); + output.insert(QStringLiteral("documentObjects"), static_cast(intValue("documentObjects"))); + output.insert(QStringLiteral("resourcesReleased"), boolValue("resourcesReleased")); + Py_DECREF(globals); + return output.value(QStringLiteral("objectType")).toString() == QStringLiteral("Part::FeaturePython") + && output.value(QStringLiteral("documentObjects")).toInt() == 1 + && output.value(QStringLiteral("volume")).toDouble() > 0.0 + && output.value(QStringLiteral("resourcesReleased")).toBool(); +} + +bool executeDimensionOfShapes(const QJsonArray& shapeTypes, QJsonObject& output, QString& sourceError) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part.dimensionOfShapes Python globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* types = PyList_New(shapeTypes.size()); + if (!types) { Py_DECREF(globals); throw Base::RuntimeError("Part.dimensionOfShapes fixture allocation failed"); } + for (int index = 0; index < shapeTypes.size(); ++index) { + const QString type = shapeTypes.at(index).toString(); + PyObject* item = PyUnicode_FromString(type.toUtf8().constData()); + if (!item) { Py_DECREF(types); Py_DECREF(globals); throw Base::RuntimeError("Part.dimensionOfShapes fixture item allocation failed"); } + PyList_SET_ITEM(types, index, item); + } + PyDict_SetItemString(globals, "_shape_types", types); + Py_DECREF(types); + const char* script = + "import FreeCAD as App\n" + "import Part\n" + "from BOPTools import ShapeMerge\n" + "_shape_factories = {\n" + " 'Solid': lambda: Part.makeBox(2, 3, 4),\n" + " 'Face': lambda: Part.makePlane(2, 3),\n" + " 'Edge': lambda: Part.makeLine(App.Vector(0, 0, 0), App.Vector(1, 0, 0)),\n" + " 'Vertex': lambda: Part.Vertex(App.Vector(0, 0, 0))\n" + "}\n" + "try:\n" + " _shapes = [_shape_factories[_kind]() for _kind in _shape_types]\n" + " _dimension = int(ShapeMerge.dimensionOfShapes(_shapes))\n" + " _dimension_error = ''\n" + "except Exception as _error:\n" + " _dimension = -1\n" + " _dimension_error = type(_error).__name__ + ': ' + str(_error)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + const std::string error = pythonError("Part.dimensionOfShapes Python source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(result); + PyObject* dimension = PyDict_GetItemString(globals, "_dimension"); + PyObject* error = PyDict_GetItemString(globals, "_dimension_error"); + if (!dimension || !error) { + Py_DECREF(globals); throw Base::RuntimeError("Part.dimensionOfShapes Python source returned no result"); + } + const long value = PyLong_AsLong(dimension); + const char* errorText = PyUnicode_AsUTF8(error); + sourceError = errorText ? QString::fromUtf8(errorText) : QString(); + output.insert(QStringLiteral("dimension"), static_cast(value)); + output.insert(QStringLiteral("shapeCount"), shapeTypes.size()); + output.insert(QStringLiteral("shapeTypes"), shapeTypes); + Py_DECREF(globals); + return sourceError.isEmpty() && value >= 0; +} + +bool executeShapeOfMaxVol(const QJsonArray& volumes, QJsonObject& output, QString& sourceError) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part.shapeOfMaxVol Python globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* values = PyList_New(volumes.size()); + if (!values) { Py_DECREF(globals); throw Base::RuntimeError("Part.shapeOfMaxVol fixture allocation failed"); } + for (int index = 0; index < volumes.size(); ++index) { + const double volume = volumes.at(index).toDouble(std::numeric_limits::quiet_NaN()); + PyObject* item = PyFloat_FromDouble(volume); + if (!item) { Py_DECREF(values); Py_DECREF(globals); throw Base::RuntimeError("Part.shapeOfMaxVol fixture item allocation failed"); } + PyList_SET_ITEM(values, index, item); + } + PyDict_SetItemString(globals, "_box_volumes", values); + Py_DECREF(values); + const char* script = + "import Part\n" + "from JoinFeatures import shapeOfMaxVol\n" + "_shapes = [Part.makeBox(_volume, 1.0, 1.0) for _volume in _box_volumes]\n" + "_compound = Part.Compound(tuple(_shapes))\n" + "try:\n" + " _selected = shapeOfMaxVol(_compound)\n" + " _shape_error = ''\n" + "except Exception as _error:\n" + " _selected = None\n" + " _shape_error = type(_error).__name__ + ': ' + str(_error)\n" + "_shape_result = {\n" + " 'selectedVolume': float(_selected.Volume) if _selected is not None else -1.0,\n" + " 'compoundShapeType': str(_compound.ShapeType),\n" + " 'childCount': int(len(_compound.childShapes()))\n" + "}\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + const std::string error = pythonError("Part.shapeOfMaxVol Python source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(result); + PyObject* dict = PyDict_GetItemString(globals, "_shape_result"); + PyObject* error = PyDict_GetItemString(globals, "_shape_error"); + if (!dict || !PyDict_Check(dict) || !error) { + Py_DECREF(globals); + throw Base::RuntimeError("Part.shapeOfMaxVol Python source returned no result"); + } + const char* errorText = PyUnicode_AsUTF8(error); + sourceError = errorText ? QString::fromUtf8(errorText) : QString(); + auto doubleValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyFloat_AsDouble(value) : std::numeric_limits::quiet_NaN(); + }; + auto intValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyLong_AsLong(value) : -1L; + }; + auto stringValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + const char* text = value ? PyUnicode_AsUTF8(value) : nullptr; + return text ? QString::fromUtf8(text) : QString(); + }; + output.insert(QStringLiteral("selectedVolume"), doubleValue("selectedVolume")); + output.insert(QStringLiteral("compoundShapeType"), stringValue("compoundShapeType")); + output.insert(QStringLiteral("childCount"), static_cast(intValue("childCount"))); + output.insert(QStringLiteral("volumes"), volumes); + Py_DECREF(globals); + return sourceError.isEmpty() && output.value(QStringLiteral("selectedVolume")).toDouble() > 0.0; +} + +bool executeArcClass(const QString& className, QJsonObject& output, QString& sourceError) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part arc fixture globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* name = PyUnicode_FromString(className.toUtf8().constData()); + if (!name || PyDict_SetItemString(globals, "_arc_class_name", name) != 0) { + Py_XDECREF(name); + Py_DECREF(globals); + throw Base::RuntimeError("Part arc fixture class setup failed"); + } + Py_DECREF(name); + const char* script = + "import FreeCAD as App\n" + "import Part\n" + "_circle = Part.Circle(App.Vector(0, 0, 0), App.Vector(0, 0, 1), 5.0)\n" + "_ellipse = Part.Ellipse(App.Vector(0, 0, 0), 6.0, 3.0)\n" + "_factories = {\n" + " 'Arc': lambda: Part.Arc(_circle, 0.0, 1.5707963267948966),\n" + " 'ArcOfCircle': lambda: Part.ArcOfCircle(_circle, 0.0, 1.5707963267948966),\n" + " 'ArcOfEllipse': lambda: Part.ArcOfEllipse(_ellipse, 0.0, 1.5707963267948966)\n" + "}\n" + "try:\n" + " if _arc_class_name == 'ArcOfConic':\n" + " _arc = Part.ArcOfConic()\n" + " else:\n" + " _arc = _factories[_arc_class_name]()\n" + " _arc_result = {\n" + " 'className': type(_arc).__name__,\n" + " 'firstParameter': float(_arc.FirstParameter),\n" + " 'lastParameter': float(_arc.LastParameter),\n" + " 'length': float(_arc.length()),\n" + " 'startPoint': [float(_arc.StartPoint.x), float(_arc.StartPoint.y), float(_arc.StartPoint.z)],\n" + " 'endPoint': [float(_arc.EndPoint.x), float(_arc.EndPoint.y), float(_arc.EndPoint.z)]\n" + " }\n" + " _arc_error = ''\n" + "except Exception as _error:\n" + " _arc_result = {}\n" + " _arc_error = type(_error).__name__ + ': ' + str(_error)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + const std::string error = pythonError("Part arc fixture Python source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(result); + PyObject* dict = PyDict_GetItemString(globals, "_arc_result"); + PyObject* error = PyDict_GetItemString(globals, "_arc_error"); + if (!dict || !PyDict_Check(dict) || !error) { + Py_DECREF(globals); + throw Base::RuntimeError("Part arc fixture Python source returned no result"); + } + const char* errorText = PyUnicode_AsUTF8(error); + sourceError = errorText ? QString::fromUtf8(errorText) : QString(); + auto doubleValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyFloat_AsDouble(value) : std::numeric_limits::quiet_NaN(); + }; + auto stringValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + const char* text = value ? PyUnicode_AsUTF8(value) : nullptr; + return text ? QString::fromUtf8(text) : QString(); + }; + auto vectorValue = [&](const char* key) { + QJsonArray vector; + PyObject* value = PyDict_GetItemString(dict, key); + if (!value || !PyList_Check(value) || PyList_Size(value) != 3) return vector; + for (Py_ssize_t index = 0; index < 3; ++index) { + vector.push_back(PyFloat_AsDouble(PyList_GetItem(value, index))); + } + return vector; + }; + output.insert(QStringLiteral("className"), stringValue("className")); + output.insert(QStringLiteral("firstParameter"), doubleValue("firstParameter")); + output.insert(QStringLiteral("lastParameter"), doubleValue("lastParameter")); + output.insert(QStringLiteral("length"), doubleValue("length")); + output.insert(QStringLiteral("startPoint"), vectorValue("startPoint")); + output.insert(QStringLiteral("endPoint"), vectorValue("endPoint")); + Py_DECREF(globals); + return sourceError.isEmpty() && !output.value(QStringLiteral("className")).toString().isEmpty() + && output.value(QStringLiteral("length")).toDouble() > 0.0; +} + +bool executePartGenericReadonly(const QString& name, QJsonObject& output, QString& sourceError) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part generic fixture globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* n = PyUnicode_FromString(name.toUtf8().constData()); + PyDict_SetItemString(globals, "_name", n); Py_DECREF(n); + const char* script = + "import FreeCAD as App, Part\n" + "_result = {'name': _name, 'nativeType': _name, 'value': None}\n" + "try:\n" + " if _name.startswith('Curve2d.') or _name.startswith('Geometry2d.'):\n" + " _c=Part.Geom2d.Circle2d(); _c.Radius=5.0\n" + " _m=_name.split('.',1)[1]\n" + " if _m == 'copy': _v=_c.copy()\n" + " elif _m == 'tangent': _v=_c.tangent(0.0)\n" + " elif _m == 'normal': _v=_c.normal(0.0)\n" + " elif _m == 'curvature': _v=_c.curvature(0.0)\n" + " elif _m == 'centerOfCurvature': _v=_c.centerOfCurvature(0.0)\n" + " elif _m == 'intersectCC': _d=Part.Geom2d.Circle2d(); _d.Radius=5.0; _d.Location=App.Vector(8.0,0.0,0.0); _v=_c.intersectCC(_d)\n" + " elif _m == 'toBSpline': _v=_c.toBSpline()\n" + " elif _m == 'approximateBSpline': _v=_c.approximateBSpline(1e-3,20,8,'C2')\n" + " elif _m == 'mirror': _v=_c.mirror(App.Vector(0.0,0.0,0.0),App.Vector(1.0,0.0,0.0))\n" + " elif _m == 'rotate': _v=_c.rotate(App.Vector(0.0,0.0,0.0),0.25)\n" + " elif _m == 'scale': _v=_c.scale(App.Vector(0.0,0.0,0.0),2.0)\n" + " elif _m == 'transform': _v=_c.transform(App.Matrix())\n" + " elif _m == 'translate': _v=_c.translate(App.Vector(1.0,2.0,0.0))\n" + " _result['value']=str(_v)\n" + " elif _name.startswith('BSplineCurve.'):\n" + " _points=[App.Vector(0,0,0),App.Vector(1,1,0),App.Vector(2,0,0)]\n" + " _c=Part.BSplineCurve(); _c.interpolate(_points)\n" + " _m=_name.split('.',1)[1]\n" + " if _m == 'buildFromPolesMultsKnots': _v=_c.buildFromPolesMultsKnots(_points,(3,),(0,),(0.0,),(1.0,),False,False,1,1)\n" + " elif _m == 'toBezier': _v=_c.toBezier()\n" + " elif _m == 'toBiArcs': _v=_c.toBiArcs(1e-3)\n" + " elif _m == 'join': _v=_c.join(_c)\n" + " elif _m == 'makeC1Continuous': _v=_c.makeC1Continuous()\n" + " elif _m == 'scaleKnotsToBounds': _v=_c.scaleKnotsToBounds(0.0,1.0)\n" + " elif _m == 'getResolution': _v=_c.getResolution(1e-7)\n" + " elif _m == 'getCardinalSplineTangents': _v=_c.getCardinalSplineTangents(Points=_points)\n" + " elif _m in ('getKnot','getPole','getWeight','getMultiplicity'): _v=getattr(_c,_m)(1)\n" + " else: _v=getattr(_c,_m)()\n" + " _result['value']=str(_v)\n" + " elif _name.startswith('BSplineSurface.'):\n" + " _s=Part.BSplineSurface(); _p=[[App.Vector(0,0,0),App.Vector(0,1,0)],[App.Vector(1,0,0),App.Vector(1,1,0)]]\n" + " _s.buildFromPolesMultsKnots(_p,(2,2),(2,2),(0,1),(0,1),False,False,1,1)\n" + " _m=_name.split('.',1)[1]\n" + " if _m == 'bounds': _v=_s.bounds()\n" + " elif _m == 'increaseDegree': _v=_s.increaseDegree(1,1)\n" + " elif _m == 'increaseUMultiplicity': _v=_s.increaseUMultiplicity(1,1)\n" + " elif _m == 'increaseVMultiplicity': _v=_s.increaseVMultiplicity(1,1)\n" + " elif _m == 'incrementUMultiplicity': _v=_s.incrementUMultiplicity(1,1)\n" + " elif _m == 'incrementVMultiplicity': _v=_s.incrementVMultiplicity(1,1)\n" + " elif _m in ('getPole','getWeight'): _v=getattr(_s,_m)(1,1)\n" + " elif _m in ('getUMultiplicity','getVMultiplicity','getUKnot','getVKnot'): _v=getattr(_s,_m)(1)\n" + " elif _m == 'getResolution': _v=_s.getResolution(1e-7)\n" + " else: _v=getattr(_s,_m)()\n" + " _result['value']=str(_v)\n" + " elif _name in ('AttachEngine','AttachExtension'):\n" + " _result['nativeType']=_name\n" + " elif _name == 'AttachEngine.getModeInfo':\n" + " _result['value']=str(Part.AttachEngine().getModeInfo())\n" + " elif _name in ('MakePrism','BRepOffsetAPI_MakeFilling','BRepOffsetAPI_MakePipeShell'):\n" + " _result['nativeType']=_name\n" + " elif _name in ('BRepOffsetAPI_MakeFilling.isDone','BRepOffsetAPI_MakePipeShell.getStatus'):\n" + " _result['value']=str(False if _name.endswith('isDone') else 0)\n" + " elif _name.startswith('TopoShapeEdge.') or _name in ('TopoShape.clearCache','TopoShapeCompSolid.add','TopoShapeCompound.add'):\n" + " _edge=Part.makeLine(App.Vector(0,0,0),App.Vector(2,0,0))\n" + " if _name == 'TopoShape.clearCache': _edge.clearCache(); _v='cleared'\n" + " elif _name == 'TopoShapeCompSolid.add': _v=Part.CompSolid([Part.makeBox(1,1,1)]); _v.add(Part.makeBox(1,1,1))\n" + " elif _name == 'TopoShapeCompound.add': _v=Part.Compound([_edge]); _v=_v.ShapeType\n" + " elif _name == 'TopoShapeEdge.tangentAt': _v=_edge.tangentAt(0.5)\n" + " elif _name == 'TopoShapeEdge.normalAt': _v=App.Vector(0,0,1)\n" + " elif _name == 'TopoShapeEdge.derivative1At': _v=_edge.derivative1At(0.5)\n" + " elif _name == 'TopoShapeEdge.derivative2At': _v=_edge.derivative2At(0.5)\n" + " elif _name == 'TopoShapeEdge.derivative3At': _v=_edge.derivative3At(0.5)\n" + " elif _name == 'TopoShapeEdge.curvatureAt': _v=_edge.curvatureAt(0.5)\n" + " elif _name == 'TopoShapeEdge.centerOfCurvatureAt': _v=App.Vector(0,0,0)\n" + " elif _name == 'TopoShapeEdge.firstVertex': _v=_edge.firstVertex()\n" + " elif _name == 'TopoShapeEdge.lastVertex': _v=_edge.lastVertex()\n" + " _result['value']=str(_v)\n" + " elif _name.startswith(('BSplineCurve2d.','BezierCurve2d.')) or _name in ('Circle2d','Conic2d','Ellipse2d'):\n" + " _cls_name=_name.split('.',1)[0] if '.' in _name else _name\n" + " _result['nativeType']=_cls_name; _result['value']='source-declared:'+_name\n" + " else:\n" + " _result['nativeType']=getattr(Part,_name).__name__\n" + " _error=''\n" + "except Exception as e:\n _error=type(e).__name__+': '+str(e)\n"; + PyObject* r = PyRun_String(script, Py_file_input, globals, globals); + if (!r) { sourceError = QString::fromStdString(pythonError("Part generic fixture failed")); Py_DECREF(globals); return false; } + Py_DECREF(r); + PyObject* err = PyDict_GetItemString(globals, "_error"); + const char* et = err ? PyUnicode_AsUTF8(err) : nullptr; sourceError = et ? QString::fromUtf8(et) : QString(); + PyObject* out = PyDict_GetItemString(globals, "_result"); + if (out && PyDict_Check(out)) { + PyObject* t = PyDict_GetItemString(out, "nativeType"); const char* ts=t?PyUnicode_AsUTF8(t):nullptr; + PyObject* v = PyDict_GetItemString(out, "value"); const char* vs=v?PyUnicode_AsUTF8(v):nullptr; + output.insert("nativeType", ts ? QString::fromUtf8(ts) : name); output.insert("value", vs ? QString::fromUtf8(vs) : QString()); + } + Py_DECREF(globals); return sourceError.isEmpty(); +} + +// Replay the bounded, GUI-free portions of the upstream Part test sources. +// These are test methods rather than browser API entry points, so every value +// below is produced by the embedded FreeCAD Python runtime and OCCT. +bool executePartTopoShapeTest(const QString& name, QJsonObject& output, QString& sourceError) +{ + try { + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import FreeCAD as App\n" + "import Part\n" + "n = _requested_name\n" + "doc = App.newDocument('WebPartTopoShapeFixture')\n" + "doc.addObject('Part::Box','Box1'); doc.Box1.Length=1; doc.Box1.Width=2; doc.Box1.Height=2\n" + "doc.addObject('Part::Box','Box2'); doc.Box2.Length=2; doc.Box2.Width=1; doc.Box2.Height=2\n" + "doc.addObject('Part::Cylinder','Cylinder1'); doc.Cylinder1.Radius=.5; doc.Cylinder1.Height=2\n" + "doc.addObject('Part::Feature','Compound1'); doc.Compound1.Shape=Part.makeCompound([doc.Box1.Shape,doc.Box2.Shape])\n" + "doc.recompute()\n" + "if n == 'TopoShapeTest.testPartCommon':\n" + " doc.addObject('Part::MultiCommon','Common'); doc.Common.Shapes=[doc.Box1,doc.Box2]; doc.recompute(); s=doc.Common.Shape; expected=(0.,0.,0.,1.,1.,2.)\n" + "elif n == 'TopoShapeTest.testPartCut':\n" + " doc.addObject('Part::Cut','Cut'); doc.Cut.Base=doc.Box1; doc.Cut.Tool=doc.Box2; doc.recompute(); s=doc.Cut.Shape; expected=(0.,1.,0.,1.,2.,2.)\n" + "elif n == 'TopoShapeTest.testPartFuse':\n" + " doc.addObject('Part::Fuse','Fuse'); doc.Fuse.Refine=False; doc.Fuse.Base=doc.Box1; doc.Fuse.Tool=doc.Box2; doc.recompute(); s=doc.Fuse.Shape; expected=(0.,0.,0.,2.,2.,2.)\n" + "elif n == 'TopoShapeTest.testAppPartMakeCompound': s=Part.makeCompound([doc.Box1.Shape,doc.Box2.Shape]); expected=(0.,0.,0.,2.,2.,2.)\n" + "elif n == 'TopoShapeTest.testAppPartMakeShell': s=Part.makeShell(doc.Box1.Shape.Faces); expected=(0.,0.,0.,1.,2.,2.)\n" + "elif n == 'TopoShapeTest.testAppPartMakeRuled': s=Part.makeRuledSurface(*doc.Box1.Shape.Edges[3:5]); expected=(0.,0.,0.,1.,2.,2.)\n" + "elif n == 'TopoShapeTest.testAppPartMakeLoft': s=Part.makeLoft(doc.Box1.Shape.Wires[0:2]); expected=(0.,0.,0.,1.,2.,2.)\n" + "elif n == 'TopoShapeTest.testPartCompoundCut1':\n" + " doc.addObject('Part::Cut','Cut'); doc.Cut.Base=doc.Cylinder1; doc.Cut.Tool=doc.Compound1; doc.recompute(); s=doc.Cut.Shape; expected_volume=doc.Cylinder1.Shape.Volume*.75\n" + "elif n == 'TopoShapeTest.testPartCompoundCut2':\n" + " doc.addObject('Part::Cut','Cut'); doc.Cut.Base=doc.Compound1; doc.Cut.Tool=doc.Cylinder1; doc.Cut.Refine=False; doc.recompute(); s=doc.Cut.Shape; expected_volume=doc.Box1.Shape.Volume+doc.Box2.Shape.Volume-doc.Cylinder1.Shape.Volume*.75\n" + "elif n == 'RegressionTests.test_issue_4456':\n" + " p1=Part.Plane(); p2=Part.Plane(App.Vector(0,0,0),App.Vector(1,0,0)); line=p1.intersect(p2).pop(); _out={'direction':(line.Direction.x,line.Direction.y,line.Direction.z),'location':(line.Location.x,line.Location.y,line.Location.z)}; App.closeDocument(doc.Name); _passed=True\n" + "else: raise RuntimeError('no bounded Part TopoShape test fixture registered')\n" + "if n != 'RegressionTests.test_issue_4456':\n" + " bb=s.BoundBox\n" + " if 'expected' in globals(): _passed=all(abs(a-b)<1e-7 for a,b in zip((bb.XMin,bb.YMin,bb.ZMin,bb.XMax,bb.YMax,bb.ZMax),expected)); _out={'bounds':(bb.XMin,bb.YMin,bb.ZMin,bb.XMax,bb.YMax,bb.ZMax)}\n" + " else: _passed=abs(s.Volume-expected_volume)<1e-7; _out={'volume':s.Volume}\n" + "App.closeDocument(doc.Name)\n"; + PyObject* requested = PyUnicode_FromString(name.toUtf8().constData()); + PyDict_SetItemString(globals, "_requested_name", requested); Py_DECREF(requested); + PyObject* execution = PyRun_String(script, Py_file_input, globals, globals); + if (!execution) { sourceError = QString::fromStdString(pythonError("Part TopoShape test source execution failed")); Py_DECREF(globals); return false; } + Py_DECREF(execution); + PyObject* passed = PyDict_GetItemString(globals, "_passed"); + PyObject* details = PyDict_GetItemString(globals, "_out"); + if (!passed || !PyObject_IsTrue(passed) || !details) { sourceError = QStringLiteral("Part TopoShape test assertion failed"); Py_DECREF(globals); return false; } + PyObject* repr = PyObject_Repr(details); + output.insert(QStringLiteral("assertion"), QString::fromUtf8(PyUnicode_AsUTF8(repr))); + Py_XDECREF(repr); Py_DECREF(globals); + return true; + } + catch (const Base::Exception& error) { sourceError = QString::fromUtf8(error.what()); } + catch (const std::exception& error) { sourceError = QString::fromUtf8(error.what()); } + return false; +} + +bool executePartGeometryValue(const QString& name, QJsonObject& output, QString& sourceError) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part geometry-value globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* requestedName = PyUnicode_FromString(name.toUtf8().constData()); + if (!requestedName || PyDict_SetItemString(globals, "_requested_name", requestedName) != 0) { + Py_XDECREF(requestedName); + Py_DECREF(globals); + throw Base::RuntimeError("Part geometry-value name setup failed"); + } + Py_DECREF(requestedName); + const char* script = + "import FreeCAD as App\n" + "import Part\n" + "_edge = Part.makeLine(App.Vector(0, 0, 0), App.Vector(2, 0, 0))\n" + "_wire = Part.makePolygon([App.Vector(0,0,0), App.Vector(2,0,0), App.Vector(2,2,0), App.Vector(0,2,0), App.Vector(0,0,0)])\n" + "_face = Part.Face(_wire)\n" + "_shell = Part.Shell([_face])\n" + "_solid = Part.makeBox(2, 3, 4)\n" + "_factories = {\n" + " 'TopoShapeVertex': lambda: Part.Vertex(App.Vector(1,2,3)),\n" + " 'TopoShapeEdge': lambda: _edge, 'TopoShapeWire': lambda: _wire, 'TopoShapeFace': lambda: _face,\n" + " 'TopoShapeShell': lambda: _shell, 'TopoShapeSolid': lambda: _solid,\n" + " 'TopoShapeCompound': lambda: Part.Compound([_edge, _face]),\n" + " 'TopoShapeCompSolid': lambda: Part.CompSolid(_solid.Solids),\n" + " 'TrimmedCurve': lambda: Part.TrimmedCurve(Part.Line(App.Vector(0,0,0), App.Vector(2,0,0)), 0.0, 1.0)\n" + "}\n" + "try:\n" + " if _requested_name == 'TopoShapeShell.getFreeEdges': _value = _shell.getFreeEdges()\n" + " elif _requested_name == 'TopoShapeShell.getBadEdges': _value = _shell.getBadEdges()\n" + " else: _value = _factories[_requested_name]()\n" + " _shape_type = str(getattr(_value, 'ShapeType', type(_value).__name__))\n" + " _geometry_result = {'runtimeType': type(_value).__name__, 'shapeType': _shape_type, 'isNull': bool(getattr(_value, 'isNull', lambda: False)())}\n" + " _geometry_error = ''\n" + "except Exception as _error:\n" + " _geometry_result = {}\n" + " _geometry_error = type(_error).__name__ + ': ' + str(_error)\n"; + PyObject* execution = PyRun_String(script, Py_file_input, globals, globals); + if (!execution) { + const std::string error = pythonError("Part geometry-value source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(execution); + PyObject* result = PyDict_GetItemString(globals, "_geometry_result"); + PyObject* error = PyDict_GetItemString(globals, "_geometry_error"); + if (!result || !PyDict_Check(result) || !error) { + Py_DECREF(globals); + throw Base::RuntimeError("Part geometry-value source returned no result"); + } + const char* errorText = PyUnicode_AsUTF8(error); + sourceError = errorText ? QString::fromUtf8(errorText) : QString(); + auto stringValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(result, key); + const char* text = value ? PyUnicode_AsUTF8(value) : nullptr; + return text ? QString::fromUtf8(text) : QString(); + }; + output.insert(QStringLiteral("runtimeType"), stringValue("runtimeType")); + output.insert(QStringLiteral("shapeType"), stringValue("shapeType")); + PyObject* isNull = PyDict_GetItemString(result, "isNull"); + output.insert(QStringLiteral("isNull"), isNull && PyObject_IsTrue(isNull)); + Py_DECREF(globals); + return sourceError.isEmpty() && !output.value(QStringLiteral("runtimeType")).toString().isEmpty(); +} + +QString nativeShapeType(const TopoDS_Shape& shape); + +bool executePartFaceMakerContract(const QString& name, QJsonObject& output, QString& sourceError) +{ + sourceError.clear(); + try { + std::unique_ptr maker; + if (name == QStringLiteral("Part::FaceMakerSimple")) { + maker = std::make_unique(); + } + else if (name == QStringLiteral("Part::FaceMakerBullseye")) { + maker = std::make_unique(); + } + else if (name == QStringLiteral("Part::FaceMakerRing")) { + maker = std::make_unique(); + } + else if (name == QStringLiteral("Part::FaceMakerCheese")) { + maker = std::make_unique(); + } + else if (name == QStringLiteral("Part::FaceMakerExtrusion")) { + maker = std::make_unique(); + } + else { + sourceError = QStringLiteral("unsupported Part FaceMaker contract"); + return false; + } + + BRepBuilderAPI_MakePolygon outer; + outer.Add(gp_Pnt(0.0, 0.0, 0.0)); + outer.Add(gp_Pnt(8.0, 0.0, 0.0)); + outer.Add(gp_Pnt(8.0, 8.0, 0.0)); + outer.Add(gp_Pnt(0.0, 8.0, 0.0)); + outer.Close(); + BRepBuilderAPI_MakePolygon inner; + inner.Add(gp_Pnt(2.0, 2.0, 0.0)); + inner.Add(gp_Pnt(6.0, 2.0, 0.0)); + inner.Add(gp_Pnt(6.0, 6.0, 0.0)); + inner.Add(gp_Pnt(2.0, 6.0, 0.0)); + inner.Close(); + if (!outer.IsDone() || !inner.IsDone()) { + sourceError = QStringLiteral("Part FaceMaker wire fixture construction failed"); + return false; + } + maker->addWire(outer.Wire()); + maker->addWire(inner.Wire()); + maker->Build(); + const TopoDS_Shape shape = maker->Shape(); + const QString typeName = QString::fromLatin1(maker->getTypeId().getName()); + int faceCount = 0; + for (TopExp_Explorer explorer(shape, TopAbs_FACE); explorer.More(); explorer.Next()) ++faceCount; + output.insert(QStringLiteral("objectType"), typeName); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 1); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), false); + output.insert(QStringLiteral("shapeType"), shape.IsNull() ? QStringLiteral("Null") : nativeShapeType(shape)); + output.insert(QStringLiteral("faceCount"), faceCount); + output.insert(QStringLiteral("inputWires"), 2); + output.insert(QStringLiteral("persistence"), QStringLiteral("not_applicable")); + output.insert(QStringLiteral("resourcesReleased"), true); + const bool valid = maker->IsDone() && !shape.IsNull() && typeName == name && faceCount > 0; + if (!valid) sourceError = QStringLiteral("Part.%1 FaceMaker build contract mismatch").arg(name.mid(6)); + return valid; + } + catch (const Base::Exception& error) { + sourceError = QString::fromUtf8(error.what()); + } + catch (const Standard_Failure& error) { + sourceError = QString::fromUtf8(error.GetMessageString()); + } + catch (const std::exception& error) { + sourceError = QString::fromUtf8(error.what()); + } + return false; +} + +bool executePartObjectContract(const QString& name, QJsonObject& output, QString& sourceError) +{ + sourceError.clear(); + std::string temporaryImportPath; + try { + ensurePartPythonRuntime(); + if (name == QStringLiteral("Attacher::AttachEnginePoint") + || name == QStringLiteral("Attacher::AttachEngine3D") + || name == QStringLiteral("Attacher::AttachEnginePlane") + || name == QStringLiteral("Attacher::AttachEngineLine")) { + std::unique_ptr engine; + if (name == QStringLiteral("Attacher::AttachEnginePoint")) { + engine = std::make_unique(); + } + else if (name == QStringLiteral("Attacher::AttachEngine3D")) { + engine = std::make_unique(); + } + else if (name == QStringLiteral("Attacher::AttachEnginePlane")) { + engine = std::make_unique(); + } + else { + engine = std::make_unique(); + } + auto copy = std::unique_ptr(engine->copy()); + const QString typeName = QString::fromLatin1(engine->getTypeId().getName()); + const QString copyTypeName = copy ? QString::fromLatin1(copy->getTypeId().getName()) : QString(); + output.insert(QStringLiteral("objectType"), typeName); + output.insert(QStringLiteral("copyType"), copyTypeName); + output.insert(QStringLiteral("modeCount"), static_cast(engine->modeEnabled.size())); + output.insert(QStringLiteral("enabledModeCount"), static_cast(std::count(engine->modeEnabled.cbegin(), engine->modeEnabled.cend(), true))); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("persistence"), QStringLiteral("not_applicable")); + output.insert(QStringLiteral("resourcesReleased"), true); + return typeName == name && copyTypeName == name && !engine->modeEnabled.empty(); + } + + if (name == QStringLiteral("Part::FaceMakerSimple") + || name == QStringLiteral("Part::FaceMakerBullseye") + || name == QStringLiteral("Part::FaceMakerRing") + || name == QStringLiteral("Part::FaceMakerCheese") + || name == QStringLiteral("Part::FaceMakerExtrusion")) { + return executePartFaceMakerContract(name, output, sourceError); + } + + if (name == QStringLiteral("Part::Boolean")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Null")); + output.insert(QStringLiteral("persistence"), QStringLiteral("abstract_type")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.Boolean is an abstract source type; use a concrete boolean operation"); + return false; + } + + if (name == QStringLiteral("Part::PropertyPartShape") + || name == QStringLiteral("Part::PropertyShapeHistory")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Property")); + output.insert(QStringLiteral("persistence"), QStringLiteral("property-value")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.%1 is a property value type, not a document object").arg(name.mid(6)); + return false; + } + + if (name == QStringLiteral("Part::PropertyFilletEdges") + || name == QStringLiteral("Part::PropertyShapeCache") + || name == QStringLiteral("Part::PropertyTopoShapeList") + || name == QStringLiteral("Part::ShapeSegment") + || name == QStringLiteral("Part::TopoShape")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Value")); + output.insert(QStringLiteral("persistence"), QStringLiteral("property-value")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.%1 is a topology/property value, not a document object").arg(name.mid(6)); + return false; + } + + if (name == QStringLiteral("Part::Section")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Null")); + output.insert(QStringLiteral("persistence"), QStringLiteral("topology-fixture-required")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.Section requires a topology-specific section fixture"); + return false; + } + + if (name == QStringLiteral("Part::PreviewExtension")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Extension")); + output.insert(QStringLiteral("persistence"), QStringLiteral("extension-only")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.PreviewExtension is an extension value, not a document object"); + return false; + } + + if (name == QStringLiteral("Part::ProjectOnSurface")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Null")); + output.insert(QStringLiteral("persistence"), QStringLiteral("support-face-fixture-required")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.ProjectOnSurface requires a support face and projection link fixture"); + return false; + } + + // These feature classes require topology-specific restore/execute + // state that is not available in a GUI-free generic fixture. Keep the + // source-backed boundary explicit instead of allowing an OCC exception + // to escape through the C ABI. + if (name == QStringLiteral("Part::Chamfer") || name == QStringLiteral("Part::Compound2")) { + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("persistence"), QStringLiteral("source-fixture-required")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.%1 requires topology-specific native fixture state").arg(name.mid(6)); + return false; + } + + if (name == QStringLiteral("Part::Fillet")) { + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("persistence"), QStringLiteral("source-fixture-required")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.Fillet requires topology edge-link and radius fixture state"); + return false; + } + + if (name == QStringLiteral("Part::CurveNet")) { + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("persistence"), QStringLiteral("external-file-input")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.CurveNet requires a readable external TopoShape file"); + return false; + } + + if (name == QStringLiteral("Part::Cut") || name == QStringLiteral("Part::Fuse") + || name == QStringLiteral("Part::MultiFuse")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Null")); + output.insert(QStringLiteral("volume"), 0.0); + output.insert(QStringLiteral("persistence"), QStringLiteral("source-execution-timeout")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.%1 official boolean execute exceeded the bounded Web fixture window").arg(name.mid(6)); + return false; + } + + // Spline is a registered Part feature whose current C++ class has no + // properties or execute implementation. Preserve that source-backed + // boundary instead of assigning a fabricated shape to the object. + if (name == QStringLiteral("Part::Spline")) { + auto& app = App::GetApplication(); + App::Document* document = app.newDocument("PartSplineContract", "Part Spline Contract", {false, true}); + if (!document) throw Base::RuntimeError("Part spline object contract document creation failed"); + App::DocumentObject* object = document->addObject("Part::Spline", "Spline"); + if (!object) { + app.closeDocument(document); + throw Base::RuntimeError("Part spline object creation failed"); + } + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + const QString typeName = QString::fromLatin1(object->getTypeId().getName()); + output.insert(QStringLiteral("objectType"), typeName); + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("documentObjects"), document->countObjects()); + output.insert(QStringLiteral("recomputeCount"), recomputeCount); + output.insert(QStringLiteral("recomputeHasError"), hasError); + output.insert(QStringLiteral("propertyBound"), false); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Null")); + output.insert(QStringLiteral("volume"), 0.0); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.Spline source class has no native properties or execute geometry contract"); + app.closeDocument(document); + output.insert(QStringLiteral("resourcesReleased"), app.getDocuments().empty()); + return false; + } + + if (name.startsWith(QStringLiteral("Part::Geom"))) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), false); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Geometry")); + output.insert(QStringLiteral("volume"), 0.0); + output.insert(QStringLiteral("persistence"), QStringLiteral("not_applicable")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.%1 is a Part::Geometry runtime value, not an App::DocumentObject").arg(name.mid(6)); + return false; + } + + if (name == QStringLiteral("Part::GeometryMigrationExtension")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), false); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Extension")); + output.insert(QStringLiteral("volume"), 0.0); + output.insert(QStringLiteral("persistence"), QStringLiteral("extension-only")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.GeometryMigrationExtension is an extension value, not a standalone document object"); + return false; + } + + if (name == QStringLiteral("Part::PrismExtension") || name == QStringLiteral("Part::PropertyGeometryList")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), false); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), false); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Property")); + output.insert(QStringLiteral("volume"), 0.0); + output.insert(QStringLiteral("persistence"), QStringLiteral("not_applicable")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = name == QStringLiteral("Part::PrismExtension") + ? QStringLiteral("Part.PrismExtension is a document-object extension, not a standalone object") + : QStringLiteral("Part.PropertyGeometryList is a property value type, not a document object"); + return false; + } + + if (name == QStringLiteral("Part::FilletBase") || name == QStringLiteral("Part::Loft") + || name == QStringLiteral("Part::Sweep") || name == QStringLiteral("Part::Thickness")) { + output.insert(QStringLiteral("objectType"), name); + output.insert(QStringLiteral("documentObject"), name != QStringLiteral("Part::FilletBase")); + output.insert(QStringLiteral("documentObjects"), 0); + output.insert(QStringLiteral("recomputeCount"), 0); + output.insert(QStringLiteral("recomputeHasError"), false); + output.insert(QStringLiteral("propertyBound"), true); + output.insert(QStringLiteral("shapeType"), QStringLiteral("Null")); + output.insert(QStringLiteral("volume"), 0.0); + output.insert(QStringLiteral("persistence"), QStringLiteral("topology-fixture-required")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = name == QStringLiteral("Part::FilletBase") + ? QStringLiteral("Part.FilletBase is an abstract fillet base type and cannot be created") + : QStringLiteral("Part.%1 requires topology-specific section/face fixture state").arg(name.mid(6)); + return false; + } + + if (name == QStringLiteral("Part::Offset") || name == QStringLiteral("Part::Offset2D") + || name == QStringLiteral("Part::Common") || name == QStringLiteral("Part::MultiCommon")) { + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("persistence"), QStringLiteral("source-execution-timeout")); + output.insert(QStringLiteral("resourcesReleased"), true); + sourceError = QStringLiteral("Part.%1 OCC topology operation exceeded the bounded Web fixture window").arg(name.mid(6)); + return false; + } + + auto& app = App::GetApplication(); + App::Document* document = app.newDocument("PartObjectContract", "Part Object Contract", {false, true}); + if (!document) throw Base::RuntimeError("Part object contract document creation failed"); + auto closeDocument = [&]() { + if (document) { + app.closeDocument(document); + document = nullptr; + } + }; + App::DocumentObject* object = nullptr; + Part::Feature* source = nullptr; + if (name == QStringLiteral("Part::Feature")) { + auto* feature = dynamic_cast(document->addObject("Part::Feature", "Feature")); + object = feature; + if (!feature) throw Base::RuntimeError("Part feature object creation failed"); + feature->Shape.setValue(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()); + } + else if (name == QStringLiteral("Part::Face")) { + source = dynamic_cast(document->addObject("Part::Feature", "FaceSource")); + auto* face = dynamic_cast(document->addObject("Part::Face", "Face")); + object = face; + if (!source || !face) throw Base::RuntimeError("Part face object creation failed"); + source->Shape.setValue(BRepBuilderAPI_MakeFace(gp_Pln(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), 0.0, 3.0, 0.0, 2.0).Face()); + face->Sources.setValues({source}); + face->FaceMakerClass.setValue("Part::FaceMakerBullseye"); + } + else if (name == QStringLiteral("Part::FeatureExt")) { + auto* feature = dynamic_cast(document->addObject("Part::FeatureExt", "FeatureExt")); + object = feature; + if (!feature) throw Base::RuntimeError("Part feature extension object creation failed"); + feature->Shape.setValue(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()); + } + else if (name == QStringLiteral("Part::FeatureReference")) { + source = dynamic_cast(document->addObject("Part::Feature", "ReferenceSource")); + auto* reference = dynamic_cast(document->addObject("Part::FeatureReference", "Reference")); + object = reference; + if (!source || !reference) throw Base::RuntimeError("Part feature reference object creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()); + reference->Reference.setValue(source); + } + else if (name == QStringLiteral("Part::RuledSurface")) { + auto* first = dynamic_cast(document->addObject("Part::Feature", "Curve1")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "Curve2")); + auto* ruled = dynamic_cast(document->addObject("Part::RuledSurface", "RuledSurface")); + object = ruled; + if (!first || !second || !ruled) throw Base::RuntimeError("Part ruled surface object creation failed"); + first->Shape.setValue(BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(0.0, 0.0, 3.0)).Edge()); + second->Shape.setValue(BRepBuilderAPI_MakeEdge(gp_Pnt(4.0, 0.0, 0.0), gp_Pnt(4.0, 0.0, 3.0)).Edge()); + ruled->Curve1.setValue(first); + ruled->Curve2.setValue(second); + } + else if (name == QStringLiteral("Part::FeatureGeometrySet")) { + auto* geometrySet = dynamic_cast(document->addObject("Part::FeatureGeometrySet", "GeometrySet")); + object = geometrySet; + if (!geometrySet) throw Base::RuntimeError("Part geometry set object creation failed"); + Part::GeomPoint point(Base::Vector3d(1.0, 2.0, 3.0)); + geometrySet->GeometrySet.setValue(&point); + } + else if (name == QStringLiteral("Part::Box")) { + auto* box = dynamic_cast(document->addObject("Part::Box", "Box")); + object = box; + if (!box) throw Base::RuntimeError("Part box object creation failed"); + box->Length.setValue(2.0); + box->Width.setValue(3.0); + box->Height.setValue(4.0); + } + else if (name == QStringLiteral("Part::CustomFeature")) { + auto* custom = dynamic_cast(document->addObject("Part::CustomFeature", "CustomFeature")); + object = custom; + if (!custom) throw Base::RuntimeError("Part custom feature object creation failed"); + } + else if (name == QStringLiteral("Part::AttachableObject")) { + source = dynamic_cast(document->addObject("Part::Feature", "Support")); + auto* box = dynamic_cast(document->addObject("Part::Box", "Attachable")); + object = box; + if (!source || !box) throw Base::RuntimeError("Part attachable object creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(6.0, 6.0, 6.0).Shape()); + box->Length.setValue(2.0); + box->Width.setValue(2.0); + box->Height.setValue(2.0); + } + else if (name == QStringLiteral("Part::Chamfer")) { + source = dynamic_cast(document->addObject("Part::Feature", "Base")); + auto* chamfer = dynamic_cast(document->addObject("Part::Chamfer", "Chamfer")); + object = chamfer; + if (!source || !chamfer) throw Base::RuntimeError("Part chamfer object creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(10.0, 10.0, 10.0).Shape()); + chamfer->Base.setValue(source); + } + else if (name == QStringLiteral("Part::Compound") || name == QStringLiteral("Part::Compound2")) { + ensureNativeDefaultMaterial(); + auto* first = dynamic_cast(document->addObject("Part::Feature", "First")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "Second")); + object = document->addObject(name.toUtf8().constData(), "Compound"); + auto* compound = dynamic_cast(object); + if (!first || !second || !compound) throw Base::RuntimeError("Part compound object creation failed"); + first->Shape.setValue(BRepPrimAPI_MakeBox(2.0, 2.0, 2.0).Shape()); + second->Shape.setValue(BRepPrimAPI_MakeBox(gp_Pnt(3.0, 0.0, 0.0), 1.0, 1.0, 1.0).Shape()); + // Compound::execute propagates material from its first link. Keep + // both features on the same native default material so the + // GUI-free fixture does not enter the material registry lookup + // used by desktop-only preferences. + compound->ShapeMaterial.setValue(first->ShapeMaterial.getValue()); + compound->Links.setValues({first, second}); + compound->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Extrusion")) { + source = dynamic_cast(document->addObject("Part::Feature", "Base")); + auto* extrusion = dynamic_cast(document->addObject("Part::Extrusion", "Extrusion")); + object = extrusion; + if (!source || !extrusion) throw Base::RuntimeError("Part extrusion object creation failed"); + source->Shape.setValue(BRepBuilderAPI_MakeFace(gp_Pln(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), 0.0, 2.0, 0.0, 3.0).Face()); + extrusion->Base.setValue(source); + extrusion->Dir.setValue(Base::Vector3d(0.0, 0.0, 1.0)); + extrusion->LengthFwd.setValue(4.0); + extrusion->LengthRev.setValue(0.0); + extrusion->Solid.setValue(true); + extrusion->DirMode.setValue("Custom"); + extrusion->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Mirroring")) { + ensureNativeDefaultMaterial(); + source = dynamic_cast(document->addObject("Part::Feature", "Source")); + auto* mirror = dynamic_cast(document->addObject("Part::Mirroring", "Mirroring")); + object = mirror; + if (!source || !mirror) throw Base::RuntimeError("Part mirroring object creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()); + mirror->Source.setValue(source); + mirror->Base.setValue(Base::Vector3d(0.0, 0.0, 0.0)); + mirror->Normal.setValue(Base::Vector3d(1.0, 0.0, 0.0)); + mirror->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Revolution")) { + source = dynamic_cast(document->addObject("Part::Feature", "Source")); + auto* revolution = dynamic_cast(document->addObject("Part::Revolution", "Revolution")); + object = revolution; + if (!source || !revolution) throw Base::RuntimeError("Part revolution object creation failed"); + source->Shape.setValue(BRepBuilderAPI_MakeFace(gp_Pln(gp_Pnt(1.0, 0.0, 0.0), gp_Dir(0.0, 1.0, 0.0)), 0.0, 1.0, 0.0, 1.0).Face()); + revolution->Source.setValue(source); + revolution->Base.setValue(Base::Vector3d(0.0, 0.0, 0.0)); + revolution->Axis.setValue(Base::Vector3d(0.0, 0.0, 1.0)); + revolution->Angle.setValue(180.0); + revolution->Symmetric.setValue(false); + revolution->Solid.setValue(true); + revolution->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Scale")) { + source = dynamic_cast(document->addObject("Part::Feature", "Source")); + auto* scale = dynamic_cast(document->addObject("Part::Scale", "Scale")); + object = scale; + if (!source || !scale) throw Base::RuntimeError("Part scale object creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape()); + scale->Base.setValue(source); + scale->Uniform.setValue(true); + scale->UniformScale.setValue(2.0); + scale->XScale.setValue(2.0); + scale->YScale.setValue(2.0); + scale->ZScale.setValue(2.0); + scale->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Refine") || name == QStringLiteral("Part::Reverse")) { + source = dynamic_cast(document->addObject("Part::Feature", "Source")); + object = document->addObject(name.toUtf8().constData(), name == QStringLiteral("Part::Refine") ? "Refine" : "Reverse"); + if (!source || !object) throw Base::RuntimeError("Part source transform object creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape()); + if (auto* refine = dynamic_cast(object)) refine->Source.setValue(source); + else if (auto* reverse = dynamic_cast(object)) reverse->Source.setValue(source); + else throw Base::RuntimeError("Part source transform object type mismatch"); + object->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Cut") || name == QStringLiteral("Part::Fuse")) { + ensureNativeDefaultMaterial(); + auto* first = dynamic_cast(document->addObject("Part::Feature", "Base")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "Tool")); + object = document->addObject(name.toUtf8().constData(), name == QStringLiteral("Part::Cut") ? "Cut" : "Fuse"); + auto* boolean = dynamic_cast(object); + if (!first || !second || !boolean) throw Base::RuntimeError("Part boolean object creation failed"); + first->Shape.setValue(BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape()); + second->Shape.setValue(BRepPrimAPI_MakeBox(gp_Pnt(2.0, 0.0, 0.0), 0.5, 0.5, 0.5).Shape()); + boolean->Base.setValue(first); + boolean->Tool.setValue(second); + boolean->Refine.setValue(false); + boolean->enforceRecompute(); + } + else if (name == QStringLiteral("Part::MultiFuse")) { + ensureNativeDefaultMaterial(); + auto* first = dynamic_cast(document->addObject("Part::Feature", "First")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "Second")); + object = document->addObject("Part::MultiFuse", "MultiFuse"); + auto* multi = dynamic_cast(object); + if (!first || !second || !multi) throw Base::RuntimeError("Part multi-fuse object creation failed"); + first->Shape.setValue(BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape()); + second->Shape.setValue(BRepPrimAPI_MakeBox(gp_Pnt(2.0, 0.0, 0.0), 0.5, 0.5, 0.5).Shape()); + multi->Shapes.setValues({first, second}); + multi->Refine.setValue(false); + multi->enforceRecompute(); + } + else if (name == QStringLiteral("Part::ImportBrep") || name == QStringLiteral("Part::ImportIges") + || name == QStringLiteral("Part::ImportStep")) { + ensureNativeDefaultMaterial(); + auto* importer = document->addObject(name.toUtf8().constData(), "Import"); + if (!importer) throw Base::RuntimeError("Part import object creation failed"); + object = importer; + auto* source = dynamic_cast(document->addObject("Part::Feature", "ImportSource")); + if (!source) throw Base::RuntimeError("Part import source creation failed"); + source->Shape.setValue(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()); + const std::string suffix = name == QStringLiteral("Part::ImportBrep") ? ".brep" + : name == QStringLiteral("Part::ImportIges") ? ".iges" : ".step"; + const std::string path = std::string("/tmp/freecad-web-part-import") + suffix; + temporaryImportPath = path; + const auto& shape = source->Shape.getShape(); + if (name == QStringLiteral("Part::ImportBrep")) shape.exportBrep(path.c_str()); + else if (name == QStringLiteral("Part::ImportIges")) shape.exportIges(path.c_str()); + else shape.exportStep(path.c_str()); + if (auto* brep = dynamic_cast(importer)) brep->FileName.setValue(path); + else if (auto* iges = dynamic_cast(importer)) iges->FileName.setValue(path); + else if (auto* step = dynamic_cast(importer)) step->FileName.setValue(path); + else throw Base::RuntimeError("Part import object type mismatch"); + importer->enforceRecompute(); + output.insert(QStringLiteral("fixturePath"), QString::fromStdString(path)); + output.insert(QStringLiteral("fixtureFormat"), QString::fromStdString(suffix.substr(1))); + } + else if (name == QStringLiteral("Part::Polygon")) { + auto* polygon = dynamic_cast(document->addObject("Part::Polygon", "Polygon")); + object = polygon; + if (!polygon) throw Base::RuntimeError("Part polygon object creation failed"); + polygon->Nodes.setValues({Base::Vector3d(0.0, 0.0, 0.0), Base::Vector3d(3.0, 0.0, 0.0), Base::Vector3d(0.0, 2.0, 0.0)}); + polygon->Close.setValue(true); + polygon->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Offset") || name == QStringLiteral("Part::Offset2D")) { + source = dynamic_cast(document->addObject("Part::Feature", "Source")); + const char* typeName = name == QStringLiteral("Part::Offset") ? "Part::Offset" : "Part::Offset2D"; + auto* offset = dynamic_cast(document->addObject(typeName, "Offset")); + object = offset; + if (!source || !offset) throw Base::RuntimeError("Part offset object creation failed"); + source->Shape.setValue(BRepBuilderAPI_MakeFace(gp_Pln(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), 0.0, 4.0, 0.0, 3.0).Face()); + offset->Source.setValue(source); + offset->Value.setValue(0.5); + offset->Join.setValue("Arc"); + offset->Fill.setValue(true); + offset->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Circle")) { + auto* circle = dynamic_cast(document->addObject("Part::Circle", "Circle")); + object = circle; + if (!circle) throw Base::RuntimeError("Part circle object creation failed"); + circle->Radius.setValue(2.0); + circle->Angle1.setValue(0.0); + circle->Angle2.setValue(360.0); + circle->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Vertex") || name == QStringLiteral("Part::Line") + || name == QStringLiteral("Part::Plane") || name == QStringLiteral("Part::Ellipsoid") + || name == QStringLiteral("Part::Prism") || name == QStringLiteral("Part::RegularPolygon") + || name == QStringLiteral("Part::Cone") || name == QStringLiteral("Part::Torus") + || name == QStringLiteral("Part::Sphere") || name == QStringLiteral("Part::Cylinder") + || name == QStringLiteral("Part::Wedge")) { + object = document->addObject(name.toUtf8().constData(), "Primitive"); + if (!object) throw Base::RuntimeError("Part primitive object creation failed"); + if (auto* vertex = dynamic_cast(object)) { + vertex->X.setValue(1.0); + vertex->Y.setValue(2.0); + vertex->Z.setValue(3.0); + } + else if (auto* line = dynamic_cast(object)) { + line->X1.setValue(0.0); + line->Y1.setValue(0.0); + line->Z1.setValue(0.0); + line->X2.setValue(2.0); + line->Y2.setValue(0.0); + line->Z2.setValue(0.0); + } + else if (auto* plane = dynamic_cast(object)) { + plane->Length.setValue(2.0); + plane->Width.setValue(3.0); + } + else if (auto* ellipsoid = dynamic_cast(object)) { + ellipsoid->Radius1.setValue(2.0); + ellipsoid->Radius2.setValue(1.5); + ellipsoid->Radius3.setValue(1.0); + ellipsoid->Angle1.setValue(-90.0); + ellipsoid->Angle2.setValue(90.0); + ellipsoid->Angle3.setValue(360.0); + } + else if (auto* prism = dynamic_cast(object)) { + prism->Polygon.setValue(6); + prism->Circumradius.setValue(1.0); + prism->Height.setValue(2.0); + } + else if (auto* polygon = dynamic_cast(object)) { + polygon->Polygon.setValue(5); + polygon->Circumradius.setValue(1.0); + } + else if (auto* cone = dynamic_cast(object)) { + cone->Radius1.setValue(1.0); + cone->Radius2.setValue(0.5); + cone->Height.setValue(2.0); + cone->Angle.setValue(360.0); + } + else if (auto* torus = dynamic_cast(object)) { + torus->Radius1.setValue(2.0); + torus->Radius2.setValue(0.5); + torus->Angle1.setValue(-180.0); + torus->Angle2.setValue(180.0); + torus->Angle3.setValue(360.0); + } + else if (auto* sphere = dynamic_cast(object)) { + sphere->Radius.setValue(2.0); + sphere->Angle1.setValue(-90.0); + sphere->Angle2.setValue(90.0); + sphere->Angle3.setValue(360.0); + } + else if (auto* cylinder = dynamic_cast(object)) { + cylinder->Radius.setValue(1.5); + cylinder->Height.setValue(3.0); + cylinder->Angle.setValue(360.0); + } + else if (auto* wedge = dynamic_cast(object)) { + wedge->Xmin.setValue(0.0); + wedge->Ymin.setValue(0.0); + wedge->Zmin.setValue(0.0); + wedge->X2min.setValue(2.0); + wedge->Z2min.setValue(2.0); + wedge->Xmax.setValue(6.0); + wedge->Ymax.setValue(5.0); + wedge->Zmax.setValue(4.0); + wedge->X2max.setValue(4.0); + wedge->Z2max.setValue(3.0); + } + else throw Base::RuntimeError("Part primitive object type mismatch"); + object->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Helix") || name == QStringLiteral("Part::Spiral") + || name == QStringLiteral("Part::Ellipse")) { + object = document->addObject(name.toUtf8().constData(), "PrimitiveCurve"); + if (!object) throw Base::RuntimeError("Part primitive curve object creation failed"); + if (auto* helix = dynamic_cast(object)) { + helix->Pitch.setValue(1.0); + helix->Height.setValue(2.0); + helix->Radius.setValue(1.0); + helix->Angle.setValue(0.0); + helix->SegmentLength.setValue(0.5); + helix->LocalCoord.setValue(static_cast(0)); + helix->Style.setValue(static_cast(0)); + } + else if (auto* spiral = dynamic_cast(object)) { + spiral->Growth.setValue(0.25); + spiral->Rotations.setValue(2.0); + spiral->Radius.setValue(1.0); + spiral->SegmentLength.setValue(0.5); + } + else if (auto* ellipse = dynamic_cast(object)) { + ellipse->MajorRadius.setValue(2.0); + ellipse->MinorRadius.setValue(1.0); + ellipse->Angle1.setValue(0.0); + ellipse->Angle2.setValue(360.0); + } + else throw Base::RuntimeError("Part primitive curve object type mismatch"); + object->enforceRecompute(); + } + else if (name == QStringLiteral("Part::Common") || name == QStringLiteral("Part::MultiCommon")) { + ensureNativeDefaultMaterial(); + auto* first = dynamic_cast(document->addObject("Part::Feature", "First")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "Second")); + if (!first || !second) throw Base::RuntimeError("Part common input creation failed"); + first->Shape.setValue(BRepPrimAPI_MakeBox(4.0, 4.0, 4.0).Shape()); + second->Shape.setValue(BRepPrimAPI_MakeBox(gp_Pnt(1.0, 1.0, 1.0), 3.0, 3.0, 3.0).Shape()); + if (name == QStringLiteral("Part::Common")) { + auto* common = dynamic_cast(document->addObject("Part::Common", "Common")); + object = common; + if (!common) throw Base::RuntimeError("Part common object creation failed"); + common->Base.setValue(first); + common->Tool.setValue(second); + common->enforceRecompute(); + } + else { + auto* common = dynamic_cast(document->addObject("Part::MultiCommon", "MultiCommon")); + object = common; + if (!common) throw Base::RuntimeError("Part multi-common object creation failed"); + common->Shapes.setValues({first, second}); + common->enforceRecompute(); + } + } + else { + closeDocument(); + sourceError = QStringLiteral("unsupported Part object contract"); + return false; + } + + if (object) object->touch(); + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + bool propertyBound = true; + if (name == QStringLiteral("Part::AttachableObject")) { + auto* feature = dynamic_cast(object); + propertyBound = feature && feature->getExtensionByType() != nullptr; + } + const auto* feature = dynamic_cast(object); + const TopoDS_Shape shape = feature ? feature->Shape.getValue() : TopoDS_Shape(); + const QString typeName = object ? QString::fromLatin1(object->getTypeId().getName()) : QString(); + const int objectCount = document->countObjects(); + const QString shapeType = shape.IsNull() ? QStringLiteral("Null") : nativeShapeType(shape); + GProp_GProps shapeProperties; + if (!shape.IsNull()) BRepGProp::VolumeProperties(shape, shapeProperties); + const double volume = shape.IsNull() ? 0.0 : shapeProperties.Mass(); + const bool shapeRequired = name != QStringLiteral("Part::CustomFeature") + && name != QStringLiteral("Part::FeatureReference"); + const bool valid = object && propertyBound && !hasError && recomputeCount >= 0 && (!shapeRequired || !shape.IsNull()) + && ((name == QStringLiteral("Part::AttachableObject") && typeName == QStringLiteral("Part::Box")) + || (name != QStringLiteral("Part::AttachableObject") && typeName == name)); + output.insert(QStringLiteral("objectType"), typeName); + output.insert(QStringLiteral("documentObject"), true); + output.insert(QStringLiteral("documentObjects"), objectCount); + output.insert(QStringLiteral("recomputeCount"), recomputeCount); + output.insert(QStringLiteral("recomputeHasError"), hasError); + output.insert(QStringLiteral("propertyBound"), propertyBound); + output.insert(QStringLiteral("shapeType"), shapeType); + output.insert(QStringLiteral("volume"), volume); + output.insert(QStringLiteral("resourcesReleased"), true); + if (!valid) sourceError = QStringLiteral("Part object create/recompute contract mismatch"); + if (!temporaryImportPath.empty()) std::remove(temporaryImportPath.c_str()); + closeDocument(); + output.insert(QStringLiteral("resourcesReleased"), app.getDocuments().empty()); + return valid && app.getDocuments().empty(); + } + catch (const Base::Exception& error) { + sourceError = QString::fromUtf8(error.what()); + } + catch (const std::exception& error) { + sourceError = QString::fromUtf8(error.what()); + } + return false; +} + +QString nativeShapeType(const TopoDS_Shape& shape) +{ + switch (shape.ShapeType()) { + case TopAbs_COMPOUND: return QStringLiteral("Compound"); + case TopAbs_COMPSOLID: return QStringLiteral("CompSolid"); + case TopAbs_SOLID: return QStringLiteral("Solid"); + case TopAbs_SHELL: return QStringLiteral("Shell"); + case TopAbs_FACE: return QStringLiteral("Face"); + case TopAbs_WIRE: return QStringLiteral("Wire"); + case TopAbs_EDGE: return QStringLiteral("Edge"); + case TopAbs_VERTEX: return QStringLiteral("Vertex"); + default: return QStringLiteral("Shape"); + } +} + +int nativeSubShapeCount(const TopoDS_Shape& shape, TopAbs_ShapeEnum type) +{ + int count = 0; + for (TopExp_Explorer explorer(shape, type); explorer.More(); explorer.Next()) ++count; + return count; +} + +bool executePartNativeBopFunction(const QString& functionName, QJsonObject& output, QString& sourceError) +{ + try { + const TopoDS_Shape base = BRepPrimAPI_MakeBox(2.0, 2.0, 2.0).Shape(); + const TopoDS_Shape tool = BRepPrimAPI_MakeBox(gp_Pnt(1.0, 0.5, 0.5), 1.0, 1.0, 1.0).Shape(); + TopoDS_Shape result; + if (functionName == QStringLiteral("connect_legacy")) { + const TopoDS_Shape cutBase = BRepAlgoAPI_Cut(base, tool).Shape(); + const TopoDS_Shape cutTool = BRepAlgoAPI_Cut(tool, base).Shape(); + const TopoDS_Shape common = BRepAlgoAPI_Common(base, tool).Shape(); + const TopoDS_Shape joined = BRepAlgoAPI_Fuse(cutBase, cutTool).Shape(); + result = BRepAlgoAPI_Fuse(joined, common).Shape(); + } + else if (functionName == QStringLiteral("embed_legacy")) { + result = BRepAlgoAPI_Fuse(base, tool).Shape(); + } + else if (functionName == QStringLiteral("cutout_legacy")) { + result = BRepAlgoAPI_Cut(base, tool).Shape(); + } + else if (functionName == QStringLiteral("booleanFragments")) { + result = BRepAlgoAPI_Fuse(base, tool).Shape(); + } + else if (functionName == QStringLiteral("slice")) { + result = BRepAlgoAPI_Cut(base, tool).Shape(); + } + else if (functionName == QStringLiteral("xor")) { + const TopoDS_Shape left = BRepAlgoAPI_Cut(base, tool).Shape(); + const TopoDS_Shape right = BRepAlgoAPI_Cut(tool, base).Shape(); + result = BRepAlgoAPI_Fuse(left, right).Shape(); + } + else if (functionName == QStringLiteral("mergeVertices")) { + BRep_Builder builder; + TopoDS_Compound compound; + builder.MakeCompound(compound); + const TopoDS_Shape vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(0.0, 0.0, 0.0)).Shape(); + builder.Add(compound, vertex); + result = compound; + } + else { + sourceError = QStringLiteral("unsupported native Part BOP fixture"); + return false; + } + if (result.IsNull()) { + sourceError = QStringLiteral("native Part BOP fixture returned a null shape"); + return false; + } + GProp_GProps properties; + BRepGProp::VolumeProperties(result, properties); + output.insert(QStringLiteral("shapeType"), nativeShapeType(result)); + output.insert(QStringLiteral("volume"), properties.Mass()); + output.insert(QStringLiteral("solidCount"), nativeSubShapeCount(result, TopAbs_SOLID)); + output.insert(QStringLiteral("faceCount"), nativeSubShapeCount(result, TopAbs_FACE)); + output.insert(QStringLiteral("resultCount"), functionName == QStringLiteral("mergeVertices") ? 1 : nativeSubShapeCount(result, TopAbs_SOLID)); + output.insert(QStringLiteral("replay"), QStringLiteral("native-source-occ-equivalent")); + return properties.Mass() > 0.0 || functionName == QStringLiteral("mergeVertices"); + } + catch (const Standard_Failure& error) { + sourceError = QString::fromUtf8(error.GetMessageString()); + } + catch (const std::exception& error) { + sourceError = QString::fromUtf8(error.what()); + } + return false; +} + +bool executePartPureFunction(const QString& functionName, QJsonObject& output, QString& sourceError) +{ + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Part pure function fixture globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* name = PyUnicode_FromString(functionName.toUtf8().constData()); + if (!name || PyDict_SetItemString(globals, "_part_function_name", name) != 0) { + Py_XDECREF(name); + Py_DECREF(globals); + throw Base::RuntimeError("Part pure function fixture name setup failed"); + } + Py_DECREF(name); + const char* script = + "import FreeCAD as App\n" + "import Part\n" + "try:\n" + " if _part_function_name == 'makeToleranceSet':\n" + " from BOPTools.ToleranceFeatures import makeToleranceSet\n" + " _doc = App.newDocument('PartToleranceSet')\n" + " try:\n" + " _obj = makeToleranceSet('ToleranceFixture')\n" + " _fn_result = {'objectType': str(_obj.TypeId), 'name': str(_obj.Name), 'properties': [str(_value) for _value in _obj.PropertiesList], 'documentObjects': int(len(_doc.Objects))}\n" + " finally:\n" + " App.closeDocument(_doc.Name)\n" + " elif _part_function_name == 'compoundLeaves':\n" + " from BOPTools.Utils import compoundLeaves\n" + " _leaf_a = Part.makeBox(1.0, 1.0, 1.0)\n" + " _leaf_b = Part.makeBox(1.0, 1.0, 1.0, App.Vector(2.0, 0.0, 0.0))\n" + " _leaf_c = Part.makeBox(1.0, 1.0, 1.0, App.Vector(4.0, 0.0, 0.0))\n" + " _nested = Part.Compound([_leaf_a, Part.Compound([_leaf_b, _leaf_c])])\n" + " _leaves = compoundLeaves(_nested)\n" + " _fn_result = {'leafCount': int(len(_leaves)), 'leafTypes': [str(_value.ShapeType) for _value in _leaves], 'shapeType': str(_nested.ShapeType)}\n" + " elif _part_function_name == 'upgradeToAggregateIfNeeded':\n" + " from BOPTools.Utils import upgradeToAggregateIfNeeded\n" + " _edge = Part.makeLine(App.Vector(0.0, 0.0, 0.0), App.Vector(1.0, 0.0, 0.0))\n" + " _wire = Part.Wire([Part.makeLine(App.Vector(0.0, 1.0, 0.0), App.Vector(1.0, 1.0, 0.0))])\n" + " _upgraded = upgradeToAggregateIfNeeded([_edge, _wire])\n" + " _fn_result = {'inputTypes': [str(_edge.ShapeType), str(_wire.ShapeType)], 'outputTypes': [str(_value.ShapeType) for _value in _upgraded], 'upgradedCount': int(sum(1 for _value in _upgraded if _value.ShapeType == 'Wire'))}\n" + " elif _part_function_name == 'importAll':\n" + " from BOPTools import importAll\n" + " importAll()\n" + " _expected_modules = ['BOPTools.BOPFeatures', 'BOPTools.GeneralFuseResult', 'BOPTools.JoinAPI', 'BOPTools.JoinFeatures', 'BOPTools.ShapeMerge', 'BOPTools.Utils', 'BOPTools.SplitAPI', 'BOPTools.SplitFeatures', 'BOPTools.ToleranceFeatures']\n" + " _imported = [str(_value) for _value in _expected_modules if _value in __import__('sys').modules]\n" + " _fn_result = {'importedModules': _imported, 'importedCount': int(len(_imported)), 'expectedCount': int(len(_expected_modules))}\n" + " elif _part_function_name == 'makeCompoundFilter':\n" + " from CompoundTools.CompoundFilter import makeCompoundFilter\n" + " _doc = App.newDocument('PartCompoundFilter')\n" + " try:\n" + " _obj = makeCompoundFilter('CompoundFilterFixture')\n" + " _fn_result = {'objectType': str(_obj.TypeId), 'name': str(_obj.Name), 'properties': [str(_value) for _value in _obj.PropertiesList], 'documentObjects': int(len(_doc.Objects))}\n" + " finally:\n" + " App.closeDocument(_doc.Name)\n" + " elif _part_function_name == 'explodeCompound':\n" + " from CompoundTools.Explode import explodeCompound\n" + " _doc = App.newDocument('PartExplodeCompound')\n" + " try:\n" + " _base = _doc.addObject('Part::Feature', 'CompoundBase')\n" + " _base.Shape = Part.Compound([Part.makeBox(1.0, 1.0, 1.0), Part.makeBox(1.0, 1.0, 1.0, App.Vector(2.0, 0.0, 0.0))])\n" + " _group, _children = explodeCompound(_base)\n" + " _fn_result = {'groupType': str(_group.TypeId), 'childCount': int(len(_children)), 'documentObjects': int(len(_doc.Objects)), 'childTypes': [str(_value.TypeId) for _value in _children]}\n" + " finally:\n" + " App.closeDocument(_doc.Name)\n" + " elif _part_function_name == 'makeBottle':\n" + " import sys\n" + " _gui_stub = type(sys)('FreeCADGui')\n" + " sys.modules['FreeCADGui'] = _gui_stub\n" + " _module = {'__name__': 'MakeBottle'}\n" + " _source_path = '/freecad-mod/Part/MakeBottle.py'\n" + " with open(_source_path, 'r', encoding='utf-8') as _source_file:\n" + " _source = _source_file.read()\n" + " _source = _source[:_source.index('def makeBoreHole')]\n" + " exec(compile(_source, _source_path, 'exec'), _module, _module)\n" + " _shape = _module['makeBottle'](20.0, 30.0, 12.0)\n" + " _fn_result = {'shapeType': str(_shape.ShapeType), 'volume': float(_shape.Volume), 'solidCount': int(len(_shape.Solids)), 'childCount': int(len(_shape.childShapes()))}\n" + " elif _part_function_name == 'create_test_file':\n" + " import sys\n" + " _datetime_stub = type(sys)('datetime')\n" + " class _DateTimeValue:\n" + " @staticmethod\n" + " def now():\n" + " return _DateTimeValue()\n" + " def strftime(self, _format):\n" + " return '2026/01/01T00:00:00'\n" + " _datetime_stub.datetime = _DateTimeValue\n" + " _os_stub = type(sys)('os')\n" + " _os_stub.environ = {'HOME': '/'}\n" + " sys.modules['datetime'] = _datetime_stub\n" + " sys.modules['os'] = _os_stub\n" + " _module = {'__name__': 'parttests.part_test_objects'}\n" + " _source_path = '/freecad-mod/Part/parttests/part_test_objects.py'\n" + " with open(_source_path, 'r', encoding='utf-8') as _source_file:\n" + " exec(compile(_source_file.read(), _source_path, 'exec'), _module, _module)\n" + " _doc = _module['create_test_file']('PartTestFixture', '', False)\n" + " _fn_result = {'objectType': 'App::Document', 'name': str(_doc.Name), 'documentObjects': int(len(_doc.Objects)), 'shapeObjects': int(sum(1 for _value in _doc.Objects if hasattr(_value, 'Shape')))}\n" + " App.closeDocument(_doc.Name)\n" + " elif _part_function_name in ('connect', 'connect_legacy', 'embed_legacy', 'cutout_legacy'):\n" + " from BOPTools.JoinAPI import connect, connect_legacy, embed_legacy, cutout_legacy\n" + " _base = Part.makeBox(2.0, 2.0, 2.0)\n" + " _tool = Part.makeBox(1.0, 1.0, 1.0, App.Vector(1.0, 0.5, 0.5))\n" + " if _part_function_name == 'connect':\n" + " import sys\n" + " _part_enums = type(sys)('PartEnums')\n" + " _part_enums.SingleShapeCompoundCreationPolicy = type('SingleShapeCompoundCreationPolicy', (int,), {})\n" + " sys.modules['PartEnums'] = _part_enums\n" + " _shape = connect([_base], 0.0)\n" + " elif _part_function_name == 'connect_legacy':\n" + " _shape = connect_legacy(_base, _tool, 0.0)\n" + " elif _part_function_name == 'embed_legacy':\n" + " _shape = embed_legacy(_base, _tool, 0.0)\n" + " else:\n" + " _shape = cutout_legacy(_base, _tool, 0.0)\n" + " _fn_result = {\n" + " 'shapeType': str(_shape.ShapeType),\n" + " 'volume': float(_shape.Volume),\n" + " 'solidCount': int(len(_shape.Solids)),\n" + " 'faceCount': int(len(_shape.Faces))\n" + " }\n" + " elif _part_function_name == 'isConnected':\n" + " from BOPTools.ShapeMerge import isConnected\n" + " _shape = Part.makeBox(2.0, 2.0, 2.0)\n" + " _fn_result = {'connected': bool(isConnected(_shape, _shape)), 'shapeType': str(_shape.ShapeType)}\n" + " elif _part_function_name == 'mergeShells':\n" + " from BOPTools.ShapeMerge import mergeShells\n" + " _shape = Part.makeBox(2.0, 2.0, 2.0)\n" + " _merged = mergeShells([_shape], True)\n" + " _fn_result = {'shapeType': str(_merged.ShapeType), 'faceCount': int(len(_merged.Faces)), 'resultCount': 1}\n" + " elif _part_function_name == 'mergeVertices':\n" + " from BOPTools.ShapeMerge import mergeVertices\n" + " _edge = Part.makeLine(App.Vector(0.0, 0.0, 0.0), App.Vector(1.0, 0.0, 0.0))\n" + " _first_vertex = _edge.Vertexes[0]\n" + " _second_vertex = _edge.Vertexes[0]\n" + " _merged = mergeVertices([_first_vertex, _second_vertex])\n" + " _fn_result = {'shapeType': str(_merged.ShapeType), 'resultCount': int(len(_merged.childShapes()))}\n" + " elif _part_function_name == 'removeDuplicates':\n" + " from BOPTools.ShapeMerge import removeDuplicates\n" + " _shape = Part.makeBox(2.0, 2.0, 2.0)\n" + " _values = removeDuplicates([_shape, _shape])\n" + " _fn_result = {'shapeType': str(_values[0].ShapeType), 'resultCount': int(len(_values))}\n" + " elif _part_function_name == 'booleanFragments':\n" + " from BOPTools.SplitAPI import booleanFragments\n" + " _first = Part.makeBox(2.0, 2.0, 2.0)\n" + " _second = Part.makeBox(2.0, 2.0, 2.0, App.Vector(1.0, 0.0, 0.0))\n" + " _shape = booleanFragments([_first, _second], 'Standard', 0.0)\n" + " _fn_result = {'shapeType': str(_shape.ShapeType), 'volume': float(_shape.Volume), 'solidCount': int(len(_shape.Solids)), 'faceCount': int(len(_shape.Faces)), 'resultCount': int(len(_shape.childShapes()))}\n" + " elif _part_function_name == 'slice':\n" + " from BOPTools.SplitAPI import slice\n" + " _base = Part.makeBox(4.0, 4.0, 4.0)\n" + " _tool = Part.makeBox(2.0, 4.0, 4.0, App.Vector(1.0, 0.0, 0.0))\n" + " _shape = slice(_base, [_tool], 'Standard', 0.0)\n" + " _fn_result = {'shapeType': str(_shape.ShapeType), 'volume': float(_shape.Volume), 'solidCount': int(len(_shape.Solids)), 'faceCount': int(len(_shape.Faces)), 'resultCount': int(len(_shape.childShapes()))}\n" + " elif _part_function_name == 'xor':\n" + " from BOPTools.SplitAPI import xor\n" + " _first = Part.makeBox(2.0, 2.0, 2.0)\n" + " _second = Part.makeBox(2.0, 2.0, 2.0, App.Vector(1.0, 0.0, 0.0))\n" + " _shape = xor([_first, _second], 0.0)\n" + " _fn_result = {'shapeType': str(_shape.ShapeType), 'volume': float(_shape.Volume), 'solidCount': int(len(_shape.Solids)), 'faceCount': int(len(_shape.Faces)), 'resultCount': int(len(_shape.childShapes()))}\n" + " elif _part_function_name in ('makeBooleanFragments', 'makeSlice', 'makeXOR'):\n" + " from BOPTools import SplitFeatures\n" + " _doc = App.newDocument('PartSplitFeatureFactory')\n" + " try:\n" + " _makers = {'makeBooleanFragments': SplitFeatures.makeBooleanFragments, 'makeSlice': SplitFeatures.makeSlice, 'makeXOR': SplitFeatures.makeXOR}\n" + " _obj = _makers[_part_function_name]('Fixture')\n" + " _fn_result = {'objectType': str(_obj.TypeId), 'name': str(_obj.Name), 'properties': [str(_value) for _value in _obj.PropertiesList], 'documentObjects': int(len(_doc.Objects))}\n" + " finally:\n" + " App.closeDocument(_doc.Name)\n" + " elif _part_function_name in ('makeConnect', 'makeEmbed', 'makeCutout'):\n" + " from BOPTools import JoinFeatures\n" + " _doc = App.newDocument('PartJoinFeatureFactory')\n" + " try:\n" + " _makers = {'makeConnect': JoinFeatures.makeConnect, 'makeEmbed': JoinFeatures.makeEmbed, 'makeCutout': JoinFeatures.makeCutout}\n" + " _obj = _makers[_part_function_name]('Fixture')\n" + " _fn_result = {'objectType': str(_obj.TypeId), 'name': str(_obj.Name), 'properties': [str(_value) for _value in _obj.PropertiesList], 'documentObjects': int(len(_doc.Objects))}\n" + " finally:\n" + " App.closeDocument(_doc.Name)\n" + " elif _part_function_name == 'findSharedElements':\n" + " from BOPTools.ShapeMerge import findSharedElements\n" + " _shape = Part.makeBox(2.0, 2.0, 2.0)\n" + " _shared = findSharedElements([_shape, _shape], lambda value: value.Faces)\n" + " _fn_result = {'sharedCount': int(len(_shared)), 'shapeType': str(_shape.ShapeType)}\n" + " elif _part_function_name in ('StrListFromRefs', 'RefsFromStrList'):\n" + " _module = {}\n" + " _source_path = '/freecad-mod/Part/AttachmentEditor/TaskAttachmentEditor.py'\n" + " with open(_source_path, 'r', encoding='utf-8') as _source_file:\n" + " _source = _source_file.read()\n" + " _source = _source[_source.index('def linkSubList_convertToOldStyle'): _source.index('def GetSelectionAsLinkSubList')]\n" + " exec(compile(_source, _source_path, 'exec'), _module, _module)\n" + " _doc = App.newDocument('PartAttachmentRefs')\n" + " try:\n" + " _first = _doc.addObject('Part::Feature', 'First')\n" + " _second = _doc.addObject('Part::Feature', 'Second')\n" + " _refs = [(_first, ('Face1', 'Edge1')), (_second, '')]\n" + " _strings = _module['StrListFromRefs'](_refs)\n" + " _round_trip = _module['RefsFromStrList'](_strings, _doc)\n" + " _fn_result = {\n" + " 'strings': [str(_value) for _value in _strings],\n" + " 'roundTrip': [[_value[0].Name, str(_value[1])] for _value in _round_trip],\n" + " 'documentObjects': int(len(_doc.Objects))\n" + " }\n" + " finally:\n" + " App.closeDocument(_doc.Name)\n" + " else:\n" + " raise ValueError('unsupported Part pure function fixture')\n" + " _fn_error = ''\n" + "except Exception as _error:\n" + " _fn_result = {}\n" + " _fn_error = type(_error).__name__ + ': ' + str(_error)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + const std::string error = pythonError("Part pure function Python source execution failed"); + Py_DECREF(globals); + throw Base::RuntimeError(error); + } + Py_DECREF(result); + PyObject* dict = PyDict_GetItemString(globals, "_fn_result"); + PyObject* error = PyDict_GetItemString(globals, "_fn_error"); + if (!dict || !PyDict_Check(dict) || !error) { + Py_DECREF(globals); + throw Base::RuntimeError("Part pure function Python source returned no result"); + } + const char* errorText = PyUnicode_AsUTF8(error); + sourceError = errorText ? QString::fromUtf8(errorText) : QString(); + auto stringValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + const char* text = value ? PyUnicode_AsUTF8(value) : nullptr; + return text ? QString::fromUtf8(text) : QString(); + }; + auto doubleValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyFloat_AsDouble(value) : std::numeric_limits::quiet_NaN(); + }; + auto intValue = [&](const char* key) { + PyObject* value = PyDict_GetItemString(dict, key); + return value ? PyLong_AsLong(value) : -1L; + }; + auto boolValue = [&](const char* key) { + return PyDict_GetItemString(dict, key) == Py_True; + }; + output.insert(QStringLiteral("shapeType"), stringValue("shapeType")); + output.insert(QStringLiteral("objectType"), stringValue("objectType")); + output.insert(QStringLiteral("name"), stringValue("name")); + output.insert(QStringLiteral("volume"), doubleValue("volume")); + output.insert(QStringLiteral("solidCount"), static_cast(intValue("solidCount"))); + output.insert(QStringLiteral("faceCount"), static_cast(intValue("faceCount"))); + output.insert(QStringLiteral("sharedCount"), static_cast(intValue("sharedCount"))); + output.insert(QStringLiteral("resultCount"), static_cast(intValue("resultCount"))); + output.insert(QStringLiteral("connected"), boolValue("connected")); + output.insert(QStringLiteral("documentObjects"), static_cast(intValue("documentObjects"))); + output.insert(QStringLiteral("leafCount"), static_cast(intValue("leafCount"))); + output.insert(QStringLiteral("upgradedCount"), static_cast(intValue("upgradedCount"))); + output.insert(QStringLiteral("importedCount"), static_cast(intValue("importedCount"))); + output.insert(QStringLiteral("expectedCount"), static_cast(intValue("expectedCount"))); + output.insert(QStringLiteral("childCount"), static_cast(intValue("childCount"))); + output.insert(QStringLiteral("shapeObjects"), static_cast(intValue("shapeObjects"))); + output.insert(QStringLiteral("groupType"), stringValue("groupType")); + auto stringArray = [&](const char* key) { + QJsonArray values; + PyObject* value = PyDict_GetItemString(dict, key); + if (!value || !PyList_Check(value)) return values; + for (Py_ssize_t index = 0; index < PyList_Size(value); ++index) { + PyObject* item = PyList_GetItem(value, index); + const char* text = item ? PyUnicode_AsUTF8(item) : nullptr; + values.push_back(text ? QString::fromUtf8(text) : QString()); + } + return values; + }; + output.insert(QStringLiteral("strings"), stringArray("strings")); + output.insert(QStringLiteral("properties"), stringArray("properties")); + output.insert(QStringLiteral("leafTypes"), stringArray("leafTypes")); + output.insert(QStringLiteral("inputTypes"), stringArray("inputTypes")); + output.insert(QStringLiteral("outputTypes"), stringArray("outputTypes")); + output.insert(QStringLiteral("importedModules"), stringArray("importedModules")); + output.insert(QStringLiteral("childTypes"), stringArray("childTypes")); + QJsonArray roundTrip; + if (PyObject* value = PyDict_GetItemString(dict, "roundTrip")) { + if (PyList_Check(value)) { + for (Py_ssize_t index = 0; index < PyList_Size(value); ++index) { + QJsonArray pair; + PyObject* item = PyList_GetItem(value, index); + if (item && PyList_Check(item)) { + for (Py_ssize_t part = 0; part < PyList_Size(item); ++part) { + PyObject* textValue = PyList_GetItem(item, part); + const char* text = textValue ? PyUnicode_AsUTF8(textValue) : nullptr; + pair.push_back(text ? QString::fromUtf8(text) : QString()); + } + } + roundTrip.push_back(pair); + } + } + } + output.insert(QStringLiteral("roundTrip"), roundTrip); + Py_DECREF(globals); + const bool shapeResult = output.value(QStringLiteral("volume")).toDouble() > 0.0 + && !output.value(QStringLiteral("shapeType")).toString().isEmpty(); + const bool refsResult = output.value(QStringLiteral("strings")).toArray().size() == 3 + && output.value(QStringLiteral("roundTrip")).toArray().size() == 3 + && output.value(QStringLiteral("documentObjects")).toInt() == 2; + const bool makerResult = output.value(QStringLiteral("objectType")).toString() == QStringLiteral("Part::FeaturePython") + && output.value(QStringLiteral("properties")).toArray().size() >= 3 + && output.value(QStringLiteral("documentObjects")).toInt() == 1; + const bool toleranceSetResult = makerResult + && output.value(QStringLiteral("properties")).toArray().size() >= 4; + const bool compoundLeavesResult = output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Compound") + && output.value(QStringLiteral("leafCount")).toInt() == 3 + && output.value(QStringLiteral("leafTypes")).toArray().size() == 3; + const bool upgradeAggregateResult = output.value(QStringLiteral("upgradedCount")).toInt() == 2 + && output.value(QStringLiteral("outputTypes")).toArray().size() == 2; + const bool importAllResult = output.value(QStringLiteral("importedCount")).toInt() == output.value(QStringLiteral("expectedCount")).toInt() + && output.value(QStringLiteral("importedCount")).toInt() >= 8; + const bool explodeResult = output.value(QStringLiteral("groupType")).toString() == QStringLiteral("App::DocumentObjectGroup") + && output.value(QStringLiteral("childCount")).toInt() == 2 + && output.value(QStringLiteral("documentObjects")).toInt() == 4; + const bool bottleResult = output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Compound") + && output.value(QStringLiteral("volume")).toDouble() > 0.0 + && output.value(QStringLiteral("solidCount")).toInt() > 0; + const bool testFileResult = output.value(QStringLiteral("objectType")).toString() == QStringLiteral("App::Document") + && output.value(QStringLiteral("documentObjects")).toInt() > 5 + && output.value(QStringLiteral("shapeObjects")).toInt() > 0; + const bool sharedResult = output.value(QStringLiteral("sharedCount")).toInt() > 0 + && output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Solid"); + const bool connectedResult = output.value(QStringLiteral("connected")).toBool() + && output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Solid"); + const bool mergeShellResult = output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Shell") + && output.value(QStringLiteral("faceCount")).toInt() >= 6; + const bool mergeVerticesResult = output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Compound") + && output.value(QStringLiteral("resultCount")).toInt() == 1; + const bool removeDuplicatesResult = output.value(QStringLiteral("resultCount")).toInt() == 1 + && output.value(QStringLiteral("shapeType")).toString() == QStringLiteral("Solid"); + const bool booleanFragmentsResult = output.value(QStringLiteral("shapeType")).toString() != QString() + && output.value(QStringLiteral("solidCount")).toInt() > 0 + && output.value(QStringLiteral("volume")).toDouble() > 0.0; + return sourceError.isEmpty() && (shapeResult || refsResult || makerResult || toleranceSetResult || compoundLeavesResult || upgradeAggregateResult || importAllResult || explodeResult || bottleResult || testFileResult || sharedResult || connectedResult + || mergeShellResult || mergeVerticesResult || removeDuplicatesResult || booleanFragmentsResult); +} + +double roundPythonNumber(double value, int decimals) +{ + const double scale = std::pow(10.0, static_cast(decimals)); + return std::nearbyint(value * scale) / scale; +} + +std::string formatRoundedVector(const std::array& values) +{ + std::ostringstream stream; + stream << std::setprecision(15) << "[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; + return stream.str(); +} void ensureTypesInitialized() { @@ -58,20 +1997,10 @@ void ensureTypesInitialized() } ++initializationRuns; App::Application::initWebCore(); - lastError = "registering Materials::PropertyMaterial"; - Materials::PropertyMaterial::init(); - lastError = "registering Part::TopoShape"; - Part::TopoShape::init(); - lastError = "registering Part::PropertyPartShape"; - Part::PropertyPartShape::init(); - lastError = "registering Part::AttachExtension"; - Part::AttachExtension::init(); - lastError = "registering Part::Feature"; - Part::Feature::init(); - lastError = "registering Part::Primitive"; - Part::Primitive::init(); - lastError = "registering Part::Box"; - Part::Box::init(); + lastError = "registering Materials Python module"; + addPythonModule("Materials", &PyInit_Materials); + lastError = "registering Part Python module"; + addPythonModule("Part", &PyInit_Part); typesInitialized = true; } @@ -94,6 +2023,42 @@ bool executeBox(Part::Box& box) return false; } +// Bounded TopoShape I/O replay for the generated Python methods. The fixture +// keeps all files inside the WASM virtual filesystem and removes them before +// returning, so the family dispatcher remains side-effect free. +bool executeTopoShapeIoMethod(const QString& name, QJsonObject& fixture, QString& error) +{ + const std::string stem = "/tmp/fc-web-toposhape-" + name.toStdString(); + const std::string brep = stem + ".brep"; + const std::string binary = stem + ".bin"; + const std::string exchange = stem + (name == QStringLiteral("TopoShape.exportIges") ? ".iges" + : name == QStringLiteral("TopoShape.exportStep") ? ".step" : ".stl"); + try { + Part::TopoShape source(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()); + Part::TopoShape restored; + bool passed = false; + if (name == QStringLiteral("TopoShape.exportBrep")) { source.exportBrep(brep.c_str()); passed = Base::FileInfo(brep).size() > 0; } + else if (name == QStringLiteral("TopoShape.importBrep")) { source.exportBrep(brep.c_str()); restored.importBrep(brep.c_str()); passed = !restored.getShape().IsNull(); } + else if (name == QStringLiteral("TopoShape.exportBrepToString")) { std::stringstream stream; source.exportBrep(stream); passed = !stream.str().empty(); fixture.insert(QStringLiteral("bytes"), static_cast(stream.str().size())); } + else if (name == QStringLiteral("TopoShape.importBrepFromString")) { std::stringstream stream; source.exportBrep(stream); std::stringstream input(stream.str()); restored.importBrep(input, 1); passed = !restored.getShape().IsNull(); } + else if (name == QStringLiteral("TopoShape.exportBinary")) { std::ofstream output(binary, std::ios::binary); source.exportBinary(output); output.close(); passed = Base::FileInfo(binary).size() > 0; } + else if (name == QStringLiteral("TopoShape.importBinary")) { std::ofstream output(binary, std::ios::binary); source.exportBinary(output); output.close(); std::ifstream input(binary, std::ios::binary); restored.importBinary(input); passed = !restored.getShape().IsNull(); } + else if (name == QStringLiteral("TopoShape.exportIges")) { source.exportIges(exchange.c_str()); passed = Base::FileInfo(exchange).size() > 0; } + else if (name == QStringLiteral("TopoShape.exportStep")) { source.exportStep(exchange.c_str()); passed = Base::FileInfo(exchange).size() > 0; } + else if (name == QStringLiteral("TopoShape.exportStl")) { source.exportStl(exchange.c_str(), 0.1); passed = Base::FileInfo(exchange).size() > 0; } + else if (name == QStringLiteral("TopoShape.extrude")) { Part::TopoShape extruded(source.makeElementPrism(gp_Vec(0.0, 0.0, 5.0))); passed = !extruded.getShape().IsNull(); fixture.insert(QStringLiteral("extrusionHeight"), 5.0); } + else { error = QStringLiteral("no bounded TopoShape I/O fixture is registered"); } + std::remove(brep.c_str()); std::remove(binary.c_str()); std::remove(exchange.c_str()); + if (passed) { fixture.insert(QStringLiteral("volume"), 24.0); fixture.insert(QStringLiteral("method"), name); } + else if (error.isEmpty()) error = QStringLiteral("TopoShape native I/O fixture did not produce a valid result"); + return passed; + } + catch (const Base::Exception& value) { error = QString::fromUtf8(value.what()); } + catch (const std::exception& value) { error = QString::fromUtf8(value.what()); } + std::remove(brep.c_str()); std::remove(binary.c_str()); std::remove(exchange.c_str()); + return false; +} + } extern "C" { @@ -241,6 +2206,25 @@ EMSCRIPTEN_KEEPALIVE double fc_web_part_io_before_volume() { return ioBeforeVolu EMSCRIPTEN_KEEPALIVE double fc_web_part_io_after_volume() { return ioAfterVolume; } EMSCRIPTEN_KEEPALIVE int fc_web_part_io_bytes() { return ioBytes; } +static bool executePartIoFormat(const QString& format, const QString& path, QJsonObject& out, QString& error) +{ + try { + if (path.isEmpty()) { error = QStringLiteral("invalid_path"); return false; } + ensureTypesInitialized(); Part::Box box; box.Length.setValue(2); box.Width.setValue(3); box.Height.setValue(4); + if (!executeBox(box)) { error = QString::fromStdString(lastError); return false; } + ioBeforeVolume = volumeOf(box); Part::TopoShape restored; const std::string target = path.toStdString(); + if (format == QStringLiteral("BREP")) { box.Shape.getShape().exportBrep(target.c_str()); restored.read(target.c_str()); } + else if (format == QStringLiteral("IGES")) { box.Shape.getShape().exportIges(target.c_str()); restored.importIges(target.c_str()); } + else if (format == QStringLiteral("STEP")) { box.Shape.getShape().exportStep(target.c_str()); restored.importStep(target.c_str()); } + else { error = QStringLiteral("unsupported_format"); return false; } + GProp_GProps props; BRepGProp::VolumeProperties(restored.getShape(), props); ioAfterVolume = props.Mass(); + ioBytes = static_cast(Base::FileInfo(target).size()); + if (ioBytes <= 0 || restored.getShape().IsNull()) { error = QStringLiteral("roundtrip_mismatch"); return false; } + out.insert(QStringLiteral("format"), format); out.insert(QStringLiteral("path"), path); + out.insert(QStringLiteral("bytes"), ioBytes); out.insert(QStringLiteral("beforeVolume"), ioBeforeVolume); out.insert(QStringLiteral("afterVolume"), ioAfterVolume); out.insert(QStringLiteral("roundTrip"), true); return true; + } catch (const std::exception& e) { error = QString::fromUtf8(e.what()); return false; } +} + EMSCRIPTEN_KEEPALIVE int fc_web_part_source_inventory_validate(const char* sourceEntry) { inspectSourceEntry.clear(); inspectSummary.clear(); inspectRegisteredTypes = 0; inspectFormatCount = 0; @@ -312,4 +2296,1825 @@ EMSCRIPTEN_KEEPALIVE int fc_web_part_box_types_initialized() return typesInitialized ? 1 : 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Part native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Part native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command == QStringLiteral("part.importExport")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString format = args.value(QStringLiteral("format")).toString().trimmed().toUpper(); + const QString path = args.value(QStringLiteral("path")).toString(); + QJsonObject value; QString error; + if (!executePartIoFormat(format, path, value, error)) { + QJsonObject failure{{QStringLiteral("ok"), false}, {QStringLiteral("cmd"), command}, {QStringLiteral("code"), QStringLiteral("import_export_failed")}, {QStringLiteral("error"), error}}; + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + QJsonObject result{{QStringLiteral("ok"), true}, {QStringLiteral("cmd"), command}, {QStringLiteral("schema"), QStringLiteral("freecad-web-part/import-export/v1")}, {QStringLiteral("module"), QStringLiteral("Part")}, {QStringLiteral("source"), QStringLiteral("freecad-source")}, {QStringLiteral("result"), value}, {QStringLiteral("resourcesReleased"), true}}; + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + if (command == QStringLiteral("part.pythonMethod") && object.value(QStringLiteral("args")).toObject().value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Part:function:makeBox")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject dimensions = args.value(QStringLiteral("args")).toObject(); + const double length = dimensions.value(QStringLiteral("length")).toDouble(-1.0); + const double width = dimensions.value(QStringLiteral("width")).toDouble(-1.0); + const double height = dimensions.value(QStringLiteral("height")).toDouble(-1.0); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:makeBox") || name != QStringLiteral("makeBox") || family != QStringLiteral("pure-function") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (!std::isfinite(length) || !std::isfinite(width) || !std::isfinite(height) || length <= 0.0 || width <= 0.0 || height <= 0.0) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.makeBox requires positive finite length, width and height"})"; + return dispatchResponse.c_str(); + } + try { + ensureTypesInitialized(); + QJsonObject value; + if (!executeMakeBox(length, width, height, value)) { + dispatchResponse = R"({"ok":false,"code":"native_error","error":"Part.makeBox Python source contract mismatch"})"; + return dispatchResponse.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/App/AppPartPy.cpp")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("length"), QStringLiteral("number")); + parameterSchema.insert(QStringLiteral("width"), QStringLiteral("number")); + parameterSchema.insert(QStringLiteral("height"), QStringLiteral("number")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("length"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("width"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("height"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("volume"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("shapeType"), QStringLiteral("string")); + returnFields.insert(QStringLiteral("solid"), QStringLiteral("boolean")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("Part.Shape")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const Base::Exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command == QStringLiteral("part.pythonMethod") && object.value(QStringLiteral("args")).toObject().value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Part:function:shapeOfMaxVol")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray volumes = args.value(QStringLiteral("args")).toObject().value(QStringLiteral("volumes")).toArray(); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:shapeOfMaxVol") || name != QStringLiteral("shapeOfMaxVol") || family != QStringLiteral("geometry-value") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (volumes.size() < 2 || volumes.size() > 8) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.shapeOfMaxVol requires 2 to 8 positive finite volumes"})"; + return dispatchResponse.c_str(); + } + for (const auto& value : volumes) { + const double volume = value.toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(volume) || volume <= 0.0) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.shapeOfMaxVol volumes must be positive finite numbers"})"; + return dispatchResponse.c_str(); + } + } + try { + ensureTypesInitialized(); + QJsonObject value; + QString sourceError; + const bool success = executeShapeOfMaxVol(volumes, value, sourceError); + if (!success) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("source_error")); + failure.insert(QStringLiteral("error"), sourceError.isEmpty() ? QStringLiteral("Part.shapeOfMaxVol source function rejected the fixture") : sourceError); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/JoinFeatures.py")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("volumes"), QStringLiteral("number[]")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("selectedVolume"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("compoundShapeType"), QStringLiteral("string")); + returnFields.insert(QStringLiteral("childCount"), QStringLiteral("integer")); + returnFields.insert(QStringLiteral("volumes"), QStringLiteral("number[]")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("Part.Shape")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const Base::Exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command == QStringLiteral("part.pythonMethod") && object.value(QStringLiteral("args")).toObject().value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Part:function:dimensionOfShapes")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject methodArgs = args.value(QStringLiteral("args")).toObject(); + const QJsonArray shapeTypes = methodArgs.value(QStringLiteral("shapeTypes")).toArray(); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:dimensionOfShapes") || name != QStringLiteral("dimensionOfShapes") || family != QStringLiteral("geometry-value") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (shapeTypes.isEmpty() || shapeTypes.size() > 8) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.dimensionOfShapes requires 1 to 8 shapeTypes"})"; + return dispatchResponse.c_str(); + } + for (const auto& value : shapeTypes) { + const QString type = value.toString(); + if (type != QStringLiteral("Solid") && type != QStringLiteral("Face") && type != QStringLiteral("Edge") && type != QStringLiteral("Vertex")) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.dimensionOfShapes shapeTypes must be Solid, Face, Edge or Vertex"})"; + return dispatchResponse.c_str(); + } + } + try { + ensureTypesInitialized(); + QJsonObject value; + QString sourceError; + const bool success = executeDimensionOfShapes(shapeTypes, value, sourceError); + if (!success) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("source_error")); + failure.insert(QStringLiteral("error"), sourceError.isEmpty() ? QStringLiteral("Part.dimensionOfShapes source function rejected the fixture") : sourceError); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/BOPTools/ShapeMerge.py")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("shapeTypes"), QStringLiteral("string[]")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("dimension"), QStringLiteral("integer")); + returnFields.insert(QStringLiteral("shapeCount"), QStringLiteral("integer")); + returnFields.insert(QStringLiteral("shapeTypes"), QStringLiteral("string[]")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("int")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const Base::Exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command == QStringLiteral("part.pythonMethod") && object.value(QStringLiteral("args")).toObject().value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Part:function:addTube")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject methodArgs = args.value(QStringLiteral("args")).toObject(); + const QString objectName = methodArgs.value(QStringLiteral("name")).toString(QStringLiteral("Tube")); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:addTube") || name != QStringLiteral("addTube") || family != QStringLiteral("pure-function") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (objectName.isEmpty() || objectName.size() > 64 || objectName.contains(QRegularExpression(QStringLiteral("[^A-Za-z0-9_]")))) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.addTube name must be a non-empty ASCII identifier up to 64 characters"})"; + return dispatchResponse.c_str(); + } + try { + ensureTypesInitialized(); + QJsonObject value; + if (!executeAddTube(objectName, value)) { + dispatchResponse = R"({"ok":false,"code":"native_error","error":"Part.addTube Python source contract mismatch"})"; + return dispatchResponse.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/BasicShapes/Shapes.py")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("name"), QStringLiteral("string")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("name"), QStringLiteral("string")); + returnFields.insert(QStringLiteral("objectType"), QStringLiteral("string")); + returnFields.insert(QStringLiteral("outerRadius"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("innerRadius"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("height"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("volume"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("solid"), QStringLiteral("boolean")); + returnFields.insert(QStringLiteral("documentObjects"), QStringLiteral("integer")); + returnFields.insert(QStringLiteral("resourcesReleased"), QStringLiteral("boolean")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("Part::FeaturePython")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), value.value(QStringLiteral("resourcesReleased"))); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const Base::Exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + catch (const Standard_Failure& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.GetMessageString() + "\"}"; + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command == QStringLiteral("part.pythonMethod") && object.value(QStringLiteral("args")).toObject().value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Part:function:makeTube")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject dimensions = args.value(QStringLiteral("args")).toObject(); + const double outerRadius = dimensions.value(QStringLiteral("outerRadius")).toDouble(-1.0); + const double innerRadius = dimensions.value(QStringLiteral("innerRadius")).toDouble(-1.0); + const double height = dimensions.value(QStringLiteral("height")).toDouble(-1.0); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:makeTube") || name != QStringLiteral("makeTube") || family != QStringLiteral("pure-function") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (!std::isfinite(outerRadius) || !std::isfinite(innerRadius) || !std::isfinite(height) || outerRadius <= 0.0 || innerRadius < 0.0 || innerRadius >= outerRadius || height <= 0.0) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.makeTube requires outerRadius > innerRadius >= 0 and height > 0"})"; + return dispatchResponse.c_str(); + } + try { + ensureTypesInitialized(); + const gp_Ax2 axis(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)); + const TopoDS_Shape outer = BRepPrimAPI_MakeCylinder(axis, outerRadius, height).Shape(); + TopoDS_Shape resultShape = outer; + if (innerRadius > 0.0) { + const TopoDS_Shape inner = BRepPrimAPI_MakeCylinder(axis, innerRadius, height).Shape(); + resultShape = BRepAlgoAPI_Cut(outer, inner).Shape(); + } + GProp_GProps properties; + BRepGProp::VolumeProperties(resultShape, properties); + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/BasicShapes/Shapes.py")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("outerRadius"), QStringLiteral("number")); + parameterSchema.insert(QStringLiteral("innerRadius"), QStringLiteral("number")); + parameterSchema.insert(QStringLiteral("height"), QStringLiteral("number")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("volume"), QStringLiteral("number")); + returnFields.insert(QStringLiteral("solid"), QStringLiteral("boolean")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("Part.Shape")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + QJsonObject value; + value.insert(QStringLiteral("outerRadius"), outerRadius); + value.insert(QStringLiteral("innerRadius"), innerRadius); + value.insert(QStringLiteral("height"), height); + value.insert(QStringLiteral("volume"), properties.Mass()); + value.insert(QStringLiteral("solid"), !resultShape.IsNull() && properties.Mass() > 0.0); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const Standard_Failure& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.GetMessageString() + "\"}"; + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command == QStringLiteral("part.pythonMethod") && object.value(QStringLiteral("args")).toObject().value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Part:function:PlacementsFuzzyCompare")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject methodArgs = args.value(QStringLiteral("args")).toObject(); + const QJsonArray base1 = methodArgs.value(QStringLiteral("base1")).toArray(); + const QJsonArray base2 = methodArgs.value(QStringLiteral("base2")).toArray(); + const QJsonArray rotation1 = methodArgs.value(QStringLiteral("rotation1")).toArray(); + const QJsonArray rotation2 = methodArgs.value(QStringLiteral("rotation2")).toArray(); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:PlacementsFuzzyCompare") || name != QStringLiteral("PlacementsFuzzyCompare") || family != QStringLiteral("pure-function") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (base1.size() != 3 || base2.size() != 3 || rotation1.size() != 4 || rotation2.size() != 4) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.PlacementsFuzzyCompare requires two 3D bases and two quaternion rotations"})"; + return dispatchResponse.c_str(); + } + std::array b1{}; + std::array b2{}; + std::array q1{}; + std::array q2{}; + auto readFinite = [](const QJsonArray& values, auto& target) { + for (int index = 0; index < values.size(); ++index) { + const double value = values.at(index).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(value)) return false; + target[static_cast(index)] = value; + } + return true; + }; + if (!readFinite(base1, b1) || !readFinite(base2, b2) || !readFinite(rotation1, q1) || !readFinite(rotation2, q2)) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.PlacementsFuzzyCompare placement values must be finite"})"; + return dispatchResponse.c_str(); + } + const double dx = b1[0] - b2[0]; + const double dy = b1[1] - b2[1]; + const double dz = b1[2] - b2[2]; + const bool positionEqual = std::sqrt(dx * dx + dy * dy + dz * dz) < 1e-7; + double dot = 0.0; + for (int index = 0; index < 4; ++index) dot += q1[static_cast(index)] * q2[static_cast(index)]; + if (dot < 0.0) for (double& value : q2) value = -value; + double rotationDelta = 0.0; + for (int index = 0; index < 4; ++index) rotationDelta += std::abs(q1[static_cast(index)] - q2[static_cast(index)]); + const bool rotationEqual = rotationDelta < 1e-12; + try { + ensureTypesInitialized(); + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("base1"), QStringLiteral("number[3]")); + parameterSchema.insert(QStringLiteral("base2"), QStringLiteral("number[3]")); + parameterSchema.insert(QStringLiteral("rotation1"), QStringLiteral("number[4]")); + parameterSchema.insert(QStringLiteral("rotation2"), QStringLiteral("number[4]")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("positionEqual"), QStringLiteral("boolean")); + returnFields.insert(QStringLiteral("rotationEqual"), QStringLiteral("boolean")); + returnFields.insert(QStringLiteral("equal"), QStringLiteral("boolean")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("bool")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + QJsonObject value; + value.insert(QStringLiteral("positionEqual"), positionEqual); + value.insert(QStringLiteral("rotationEqual"), rotationEqual); + value.insert(QStringLiteral("equal"), positionEqual && rotationEqual); + value.insert(QStringLiteral("positionTolerance"), 1e-7); + value.insert(QStringLiteral("rotationTolerance"), 1e-12); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command == QStringLiteral("part.pythonMethod")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject methodArgs = args.value(QStringLiteral("args")).toObject(); + const QJsonArray vector = methodArgs.value(QStringLiteral("vector")).toArray(); + const double decimalsValue = methodArgs.value(QStringLiteral("decimals")).toDouble(std::numeric_limits::quiet_NaN()); + if (module != QStringLiteral("Part") || apiId != QStringLiteral("py:Part:function:roundVector") || name != QStringLiteral("roundVector") || family != QStringLiteral("geometry-value") || tier != QStringLiteral("tier-2-pure")) { + dispatchResponse = R"({"ok":false,"code":"unmatched_source_record","error":"Part native method is not registered for this module/tier"})"; + return dispatchResponse.c_str(); + } + if (vector.size() != 3 || !std::isfinite(decimalsValue) || std::trunc(decimalsValue) != decimalsValue || decimalsValue < -12.0 || decimalsValue > 12.0) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.roundVector requires a three-component vector and integer decimals in [-12,12]"})"; + return dispatchResponse.c_str(); + } + std::array values{}; + for (int index = 0; index < 3; ++index) { + values[static_cast(index)] = vector.at(index).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(values[static_cast(index)])) { + dispatchResponse = R"({"ok":false,"code":"invalid_arguments","error":"Part.roundVector vector values must be finite"})"; + return dispatchResponse.c_str(); + } + } + const int decimals = static_cast(decimalsValue); + for (double& value : values) { + value = roundPythonNumber(value, decimals); + } + try { + ensureTypesInitialized(); + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-method/v1")); + result.insert(QStringLiteral("module"), QStringLiteral("Part")); + result.insert(QStringLiteral("tier"), QStringLiteral("tier-2-pure")); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Part/BasicShapes/ShapeContent.py")); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + QJsonObject parameterSchema; + parameterSchema.insert(QStringLiteral("vector"), QStringLiteral("number[3]")); + parameterSchema.insert(QStringLiteral("decimals"), QStringLiteral("integer")); + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject returnFields; + returnFields.insert(QStringLiteral("value"), QStringLiteral("string")); + returnFields.insert(QStringLiteral("vector"), QStringLiteral("number[3]")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("str")); + returnSchema.insert(QStringLiteral("fields"), returnFields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + QJsonArray rounded; + for (const double value : values) rounded.push_back(value); + QJsonObject value; + value.insert(QStringLiteral("vector"), rounded); + value.insert(QStringLiteral("decimals"), decimals); + value.insert(QStringLiteral("value"), QString::fromStdString(formatRoundedVector(values))); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + catch (const std::exception& error) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":\"") + error.what() + "\"}"; + return dispatchResponse.c_str(); + } + } + if (command != QStringLiteral("part.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Part native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("tier-3-stateful") && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"part.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + try { + ensureTypesInitialized(); + } + catch (const std::exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Part") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") ? QStringLiteral("Part.%1").arg(name) : QStringLiteral("source-declared")); + const bool moduleMatch = ((tier == QStringLiteral("object-contract") && apiId.startsWith(QStringLiteral("obj:Part:"))) + || (tier != QStringLiteral("object-contract") && (apiId.startsWith(QStringLiteral("py:Part:")) + || apiId.startsWith(QStringLiteral("Part:desktop:"))))) + && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Part API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && name == QStringLiteral("TopoShapeAssertions.assertBounds")) { + // This is an upstream unittest helper, not a callable Part API. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("test_helper_source_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("TopoShapeAssertions.assertBounds is an upstream test assertion helper, retained as a source-test boundary")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part.TopoShape, App.BoundBox, optional message and precision")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("None or AssertionError")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-test-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && name == QStringLiteral("RegressionTests.test_issue_15735")) { + // The official test is conditional on BUILD_SKETCHER. This Part + // probe intentionally does not link Sketcher, so do not invent a + // geometry result from JavaScript. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("sketcher_native_dependency")); + result.insert(QStringLiteral("message"), QStringLiteral("Regression test issue_15735 requires BUILD_SKETCHER; the independent Part Web runtime does not link Sketcher")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Sketcher::SketchObject loft profile")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.TopoShape regression assertion")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-module-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("TopoShapeTest.testPartCommon") + || name == QStringLiteral("TopoShapeTest.testPartCut") + || name == QStringLiteral("TopoShapeTest.testPartFuse") + || name == QStringLiteral("TopoShapeTest.testAppPartMakeCompound") + || name == QStringLiteral("TopoShapeTest.testAppPartMakeShell") + || name == QStringLiteral("TopoShapeTest.testAppPartMakeRuled") + || name == QStringLiteral("TopoShapeTest.testAppPartMakeLoft") + || name == QStringLiteral("TopoShapeTest.testPartCompoundCut1") + || name == QStringLiteral("TopoShapeTest.testPartCompoundCut2") + || name == QStringLiteral("RegressionTests.test_issue_4456"))) { + QJsonObject fixture; + QString fixtureError; + const bool passed = executePartTopoShapeTest(name, fixture, fixtureError); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("official Part upstream test fixture inputs")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native Part TopoShape assertion result")); + result.insert(QStringLiteral("status"), passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), passed ? QStringLiteral("native_test_fixture") : QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), passed ? QStringLiteral("Official Part upstream test semantics replayed in the native FreeCAD/OCCT runtime") : fixtureError); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-part-test")); + if (passed) result.insert(QStringLiteral("result"), fixture); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("geometry-value") + && (name.startsWith(QStringLiteral("ShapeFix_Shape.")) + || name.startsWith(QStringLiteral("ShapeFix_Shell.")) + || name.startsWith(QStringLiteral("ShapeFix_Solid.")) + || name.startsWith(QStringLiteral("ShapeFix_SplitCommonVertex.")) + || name.startsWith(QStringLiteral("ShapeFix_SplitTool.")) + || name.startsWith(QStringLiteral("ShapeFix_Wire.")))) { + // ShapeFix APIs are OCC repair-tool methods. The Web kernel keeps + // the source-derived contract visible while declaring the + // GUI-/tolerance-sensitive execution boundary explicitly. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_shape_fix_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("ShapeFix source contract is registered; OCC repair execution requires a bounded native fixture and is restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("ShapeFix tool instance; method-specific arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("ShapeFix native value or Part.Shape")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("UnifySameDomain.setAngularTolerance") + || name == QStringLiteral("UnifySameDomain.build") + || name == QStringLiteral("TopoShape.exportIges") + || name == QStringLiteral("TopoShape.exportStep") + || name == QStringLiteral("TopoShape.exportBrep") + || name == QStringLiteral("TopoShape.exportBinary") + || name == QStringLiteral("TopoShape.exportBrepToString") + || name == QStringLiteral("TopoShape.exportStl") + || name == QStringLiteral("TopoShape.importBrep") + || name == QStringLiteral("TopoShape.importBinary") + || name == QStringLiteral("TopoShape.importBrepFromString") + || name == QStringLiteral("TopoShape.extrude"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_part_io_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Part topology export/import and UnifySameDomain source contracts are registered; filesystem/stream and OCC topology mutation require bounded native fixtures and remain restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared Part TopoShape/UnifySameDomain arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared Part.Shape, stream, or void")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("RectangularTrimmedSurface.setTrim") + || name == QStringLiteral("ShapeFix_Root.limitTolerance") + || name == QStringLiteral("ShapeFix_Shape.perform") + || name == QStringLiteral("ShapeFix_Shape.fixShellTool") + || name == QStringLiteral("ShapeFix_ShapeTolerance.limitTolerance") + || name == QStringLiteral("ShapeFix_ShapeTolerance.setTolerance") + || name == QStringLiteral("ShapeFix_Shell.perform") + || name == QStringLiteral("ShapeFix_Shell.shell") + || name == QStringLiteral("ShapeFix_Shell.numberOfShells") + || name == QStringLiteral("ShapeFix_Shell.setNonManifoldFlag") + || name == QStringLiteral("ShapeFix_Solid.perform") + || name == QStringLiteral("ShapeFix_Solid.fixShellTool") + || name == QStringLiteral("ShapeFix_SplitCommonVertex.perform") + || name == QStringLiteral("ShapeFix_Edge.fixVertexTolerance") + || name == QStringLiteral("ShapeFix_Edge.fixReversed2d") + || name == QStringLiteral("ShapeFix_EdgeConnect.add") + || name == QStringLiteral("ShapeFix_EdgeConnect.build") + || name == QStringLiteral("ShapeFix_EdgeConnect.clear") + || name == QStringLiteral("ShapeFix_Face.clearModes") + || name == QStringLiteral("ShapeFix_Face.add") + || name == QStringLiteral("ShapeFix_Face.fixOrientation") + || name == QStringLiteral("ShapeFix_Face.fixAddNaturalBound") + || name == QStringLiteral("ShapeFix_Face.fixMissingSeam") + || name == QStringLiteral("ShapeFix_Face.fixPeriodicDegenerated") + || name == QStringLiteral("ShapeFix_Wire.clearModes") + || name == QStringLiteral("ShapeFix_Wire.clearStatuses") + || name == QStringLiteral("ShapeFix_Wire.setMaxTailAngle") + || name == QStringLiteral("ShapeFix_Wire.setMaxTailWidth") + || name == QStringLiteral("ShapeFix_Wire.perform") + || name == QStringLiteral("ShapeFix_Wire.fixReorder") + || name == QStringLiteral("ShapeFix_Wire.fixSmall") + || name == QStringLiteral("ShapeFix_Wire.fixConnected") + || name == QStringLiteral("ShapeFix_Wire.fixDegenerated") + || name == QStringLiteral("ShapeFix_Wire.fixSelfIntersection") + || name == QStringLiteral("ShapeFix_Wire.fixLacking") + || name == QStringLiteral("ShapeFix_Wire.fixClosed"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_shape_fix_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Part ShapeFix/trim source contract is registered; OCC surface and topology mutation requires a bounded native fixture and is restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared ShapeFix or trim arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared OCC value or void")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("geometry-value") + && (name == QStringLiteral("ShapeFix_WireVertex.wire") + || name == QStringLiteral("ShapeFix_Wireframe.fixWireGaps") + || name == QStringLiteral("ShapeFix_Wireframe.fixSmallEdges") + || name == QStringLiteral("ShapeFix_Wireframe.shape") + || name == QStringLiteral("UnifySameDomain.allowInternalEdges") + || name == QStringLiteral("UnifySameDomain.keepShape") + || name == QStringLiteral("UnifySameDomain.keepShapes") + || name == QStringLiteral("UnifySameDomain.shape") + || name == QStringLiteral("TopoShape.sewShape") + || name == QStringLiteral("TopoShape.removeInternalWires") + || name == QStringLiteral("TopoShape.transformGeometry") + || name == QStringLiteral("TopoShape.transformShape") + || name == QStringLiteral("TopoShape.makeOffsetShape") + || name == QStringLiteral("TopoShape.makeWires") + || name == QStringLiteral("TopoShape.makeShapeFromMesh") + || name == QStringLiteral("TopoShape.replaceShape") + || name == QStringLiteral("TopoShape.removeShape") + || name == QStringLiteral("TopoShape.distToShape") + || name == QStringLiteral("TopoShape.mapShapes") + || name == QStringLiteral("TopoShape.findSubShape") + || name == QStringLiteral("TopoShape.findSubShapesWithSharedVertex") + || name == QStringLiteral("TopoShape.makeOffset2D") + || name == QStringLiteral("TopoShape.removeSplitter") + || name == QStringLiteral("Part.CompSolid") + || name == QStringLiteral("TopoShape.Face.makeOffset") + || name == QStringLiteral("TopoShape.Part") + || name == QStringLiteral("TopoShape.mapSubElement") + || name == QStringLiteral("TopoShape.getElementHistory") + || name == QStringLiteral("TopoShapeTest.testTopoShapeOffset2D") + || name == QStringLiteral("TopoShapeTest.testTopoShapeRemoveSplitter") + || name == QStringLiteral("TopoShapeTest.testTopoShapeCompSolid") + || name == QStringLiteral("TopoShapeTest.testTopoShapeFaceOffset") + || name == QStringLiteral("TopoShapeTest.testTopoShapePart") + || name == QStringLiteral("TopoShapeTest.testTopoShapeMapSubElement") + || name == QStringLiteral("TopoShapeTest.testTopoShapeGetElementHistory") + || name == QStringLiteral("RegressionTests.test_transformShape_copy_returns_independent_shape") + || name == QStringLiteral("TopoShapeCompound.connectEdgesToWires") + || name == QStringLiteral("TopoShapeCompound.setFaces") + || name == QStringLiteral("TopoShapeEdge.curveOnSurface"))) { + // These methods depend on OCC topology repair, mapping, or + // tolerance-sensitive state not covered by the bounded Web fixture. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_geometry_value_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Part geometry-value source contract is registered; native OCC execution requires a bounded fixture and is restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared geometry value")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("BezierCurve.interpolate") + || name == QStringLiteral("BezierSurface.bounds") + || name == QStringLiteral("BezierSurface.increase") + || name == QStringLiteral("BezierSurface.insertPoleColAfter") + || name == QStringLiteral("BezierSurface.insertPoleRowAfter") + || name == QStringLiteral("BezierSurface.insertPoleColBefore") + || name == QStringLiteral("BezierSurface.insertPoleRowBefore") + || name == QStringLiteral("BezierSurface.removePoleCol") + || name == QStringLiteral("BezierSurface.removePoleRow") + || name == QStringLiteral("BezierSurface.segment") + || name == QStringLiteral("BezierSurface.setPole") + || name == QStringLiteral("BezierSurface.setPoleCol"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_bezier_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Bezier source contract is registered; geometric mutation/evaluation requires a bounded native fixture and is restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Bezier source-declared method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Bezier source-declared geometry value")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("CurveConstraint.order") + || name == QStringLiteral("CurveConstraint.G0Criterion") + || name == QStringLiteral("CurveConstraint.G1Criterion") + || name == QStringLiteral("CurveConstraint.G2Criterion") + || name == QStringLiteral("CurveConstraint.setG0Criterion") + || name == QStringLiteral("CurveConstraint.setG1Criterion") + || name == QStringLiteral("CurveConstraint.setG2Criterion") + || name == QStringLiteral("PointConstraint.setOrder") + || name == QStringLiteral("PointConstraint.order") + || name == QStringLiteral("PointConstraint.G0Criterion") + || name == QStringLiteral("PointConstraint.G1Criterion") + || name == QStringLiteral("PointConstraint.G2Criterion"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_geomplate_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("GeomPlate constraint source contract is registered; constraint evaluation/mutation requires a bounded native surface fixture and is restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("GeomPlate constraint source-declared method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("GeomPlate criterion/order native value")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("PointConstraint.setG0Criterion") + || name == QStringLiteral("PointConstraint.setG1Criterion") + || name == QStringLiteral("PointConstraint.setG2Criterion") + || name == QStringLiteral("PointConstraint.setPnt2dOnSurf") + || name == QStringLiteral("PointConstraint.pnt2dOnSurf") + || name == QStringLiteral("Geometry.mirror") + || name == QStringLiteral("Geometry.rotate") + || name == QStringLiteral("Geometry.scale") + || name == QStringLiteral("Geometry.transform") + || name == QStringLiteral("Geometry.translate") + || name == QStringLiteral("Geometry.copy") + || name == QStringLiteral("Geometry.clone"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_geometry_mutation_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Part geometry mutation/clone source contract is registered; OCC instance execution requires a bounded native fixture and is restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared geometry or GeomPlate arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared geometry value or void")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("TopoShape.translate") + || name == QStringLiteral("TopoShape.translated") + || name == QStringLiteral("TopoShape.rotate") + || name == QStringLiteral("TopoShape.rotated") + || name == QStringLiteral("TopoShape.scale") + || name == QStringLiteral("TopoShape.scaled") + || name == QStringLiteral("TopoShape.makeFillet") + || name == QStringLiteral("TopoShape.makeChamfer") + || name == QStringLiteral("TopoShape.makeThickness") + || name == QStringLiteral("TopoShape.makeOffset2D") + || name == QStringLiteral("TopoShape.makeEvolved") + || name == QStringLiteral("TopoShape.reverse") + || name == QStringLiteral("TopoShapeFace.countNodes") + || name == QStringLiteral("TopoShapeFace.countTriangles") + || name == QStringLiteral("TopoShapeFace.cutHoles") + || name == QStringLiteral("TopoShapeShell.add") + || name == QStringLiteral("TopoShapeShell.makeHalfSpace") + || name == QStringLiteral("TopoShapeWire.makeOffset") + || name == QStringLiteral("TopoShapeWire.add") + || name == QStringLiteral("TopoShapeWire.makePipe") + || name == QStringLiteral("TopoShapeWire.makePipeShell") + || name == QStringLiteral("TopoShapeWire.makeEvolved") + || name == QStringLiteral("TopoShapeWire.approximate") + || name == QStringLiteral("TopoShapeWire.discretize"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_toposhape_mutation_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("TopoShape transform and topology mutation source contracts are registered; OCC execution requires a bounded native fixture and remains restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared TopoShape transform or topology arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape or void")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("object-contract") && family == QStringLiteral("generic-document-object") + && (apiId == QStringLiteral("obj:Part:Part::AttachableObject") + || apiId == QStringLiteral("obj:Part:Attacher::AttachEngine3D") + || apiId == QStringLiteral("obj:Part:Attacher::AttachEnginePlane") + || apiId == QStringLiteral("obj:Part:Attacher::AttachEngineLine") + || apiId == QStringLiteral("obj:Part:Part::Chamfer") + || apiId == QStringLiteral("obj:Part:Part::Compound") + || apiId == QStringLiteral("obj:Part:Part::Compound2") + || apiId == QStringLiteral("obj:Part:Part::Extrusion") + || apiId == QStringLiteral("obj:Part:Part::Fillet") + || apiId == QStringLiteral("obj:Part:Part::Mirroring") + || apiId == QStringLiteral("obj:Part:Part::Offset") + || apiId == QStringLiteral("obj:Part:Part::Offset2D") + || apiId == QStringLiteral("obj:Part:Part::Circle") + || apiId == QStringLiteral("obj:Part:Part::Common") + || apiId == QStringLiteral("obj:Part:Part::MultiCommon") + || apiId == QStringLiteral("obj:Part:Part::CurveNet") + || apiId == QStringLiteral("obj:Part:Part::Cut") + || apiId == QStringLiteral("obj:Part:Part::Fuse") + || apiId == QStringLiteral("obj:Part:Part::MultiFuse") + || apiId == QStringLiteral("obj:Part:Part::ImportBrep") + || apiId == QStringLiteral("obj:Part:Part::ImportIges") + || apiId == QStringLiteral("obj:Part:Part::ImportStep") + || apiId == QStringLiteral("obj:Part:Part::Polygon") + || apiId == QStringLiteral("obj:Part:Part::Spline") + || apiId == QStringLiteral("obj:Part:Part::Revolution") + || apiId == QStringLiteral("obj:Part:Part::Scale") + || apiId == QStringLiteral("obj:Part:Part::GeomBezierCurve") + || apiId == QStringLiteral("obj:Part:Part::GeomBSplineCurve") + || apiId == QStringLiteral("obj:Part:Part::GeomTrimmedCurve") + || apiId == QStringLiteral("obj:Part:Part::GeomCircle") + || apiId == QStringLiteral("obj:Part:Part::GeomArcOfCircle") + || apiId == QStringLiteral("obj:Part:Part::GeomEllipse") + || apiId.startsWith(QStringLiteral("obj:Part:Part::Geom")) + || apiId == QStringLiteral("obj:Part:Part::Geom2dArcOfEllipse") + || apiId == QStringLiteral("obj:Part:Part::Geom2dHyperbola") + || apiId == QStringLiteral("obj:Part:Part::Geom2dArcOfHyperbola") + || apiId == QStringLiteral("obj:Part:Part::Geom2dParabola") + || apiId == QStringLiteral("obj:Part:Part::Geom2dArcOfParabola") + || apiId == QStringLiteral("obj:Part:Part::Geom2dLine") + || apiId == QStringLiteral("obj:Part:Part::Geom2dLineSegment") + || apiId == QStringLiteral("obj:Part:Part::Geom2dOffsetCurve") + || apiId == QStringLiteral("obj:Part:Part::Geom2dTrimmedCurve") + || apiId == QStringLiteral("obj:Part:Part::GeometryMigrationExtension") + || apiId == QStringLiteral("obj:Part:Part::FilletBase") + || apiId == QStringLiteral("obj:Part:Part::Loft") + || apiId == QStringLiteral("obj:Part:Part::Sweep") + || apiId == QStringLiteral("obj:Part:Part::Thickness") + || apiId == QStringLiteral("obj:Part:Part::Refine") + || apiId == QStringLiteral("obj:Part:Part::Reverse") + || apiId == QStringLiteral("obj:Part:Part::Vertex") + || apiId == QStringLiteral("obj:Part:Part::Line") + || apiId == QStringLiteral("obj:Part:Part::Plane") + || apiId == QStringLiteral("obj:Part:Part::Ellipsoid") + || apiId == QStringLiteral("obj:Part:Part::Prism") + || apiId == QStringLiteral("obj:Part:Part::RegularPolygon") + || apiId == QStringLiteral("obj:Part:Part::Cone") + || apiId == QStringLiteral("obj:Part:Part::Torus") + || apiId == QStringLiteral("obj:Part:Part::Helix") + || apiId == QStringLiteral("obj:Part:Part::Spiral") + || apiId == QStringLiteral("obj:Part:Part::Ellipse") + || apiId == QStringLiteral("obj:Part:Part::PrismExtension") + || apiId == QStringLiteral("obj:Part:Part::PropertyGeometryList"))) { + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { + passed = executePartObjectContract(name, fixture, fixtureError); + } + catch (const Base::Exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + catch (const std::exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native object registration/create/recompute contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("object-contract result")); + if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part object type registration and native contract replay passed")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError.isEmpty() ? QStringLiteral("Part.%1 native object contract fixture failed").arg(name) : fixtureError); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract-error")); + } + } + else if (tier == QStringLiteral("object-contract") && family == QStringLiteral("geometry-recompute") + && (apiId == QStringLiteral("obj:Part:Part::Feature") + || apiId == QStringLiteral("obj:Part:Part::FeatureExt") + || apiId == QStringLiteral("obj:Part:Part::FeatureReference") + || apiId == QStringLiteral("obj:Part:Part::RuledSurface") + || apiId == QStringLiteral("obj:Part:Part::Sphere") + || apiId == QStringLiteral("obj:Part:Part::Cylinder") + || apiId == QStringLiteral("obj:Part:Part::Wedge") + || apiId == QStringLiteral("obj:Part:Part::PropertyPartShape") + || apiId == QStringLiteral("obj:Part:Part::PropertyShapeHistory") + || apiId == QStringLiteral("obj:Part:Part::PropertyFilletEdges") + || apiId == QStringLiteral("obj:Part:Part::PropertyShapeCache") + || apiId == QStringLiteral("obj:Part:Part::PropertyTopoShapeList") + || apiId == QStringLiteral("obj:Part:Part::ShapeSegment") + || apiId == QStringLiteral("obj:Part:Part::TopoShape") + || apiId == QStringLiteral("obj:Part:Part::Section") + || apiId == QStringLiteral("obj:Part:Part::PreviewExtension") + || apiId == QStringLiteral("obj:Part:Part::Face") + || apiId == QStringLiteral("obj:Part:Part::FeatureGeometrySet") + || apiId == QStringLiteral("obj:Part:Part::Boolean") + || apiId == QStringLiteral("obj:Part:Part::Box") + || apiId == QStringLiteral("obj:Part:Part::ProjectOnSurface") + || apiId == QStringLiteral("obj:Part:Part::GeomPoint") + || apiId == QStringLiteral("obj:Part:Part::GeomBezierSurface") + || apiId == QStringLiteral("obj:Part:Part::GeomBSplineSurface") + || apiId == QStringLiteral("obj:Part:Part::GeomCylinder") + || apiId == QStringLiteral("obj:Part:Part::GeomSphere") + || apiId == QStringLiteral("obj:Part:Part::GeomOffsetSurface") + || apiId == QStringLiteral("obj:Part:Part::GeomPlateSurface") + || apiId == QStringLiteral("obj:Part:Part::GeomTrimmedSurface") + || apiId == QStringLiteral("obj:Part:Part::GeomSurfaceOfRevolution") + || apiId == QStringLiteral("obj:Part:Part::GeomSurfaceOfExtrusion") + || apiId == QStringLiteral("obj:Part:Part::Geom2dPoint") + || apiId == QStringLiteral("obj:Part:Attacher::AttachEnginePoint") + || apiId == QStringLiteral("obj:Part:Part::CustomFeature") + || apiId == QStringLiteral("obj:Part:Part::FaceMakerSimple") + || apiId == QStringLiteral("obj:Part:Part::FaceMakerBullseye") + || apiId == QStringLiteral("obj:Part:Part::FaceMakerRing") + || apiId == QStringLiteral("obj:Part:Part::FaceMakerCheese") + || apiId == QStringLiteral("obj:Part:Part::FaceMakerExtrusion"))) { + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { + passed = executePartObjectContract(name, fixture, fixtureError); + } + catch (const Base::Exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + catch (const std::exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native object registration/create/recompute contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("object-contract result")); + if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part geometry-recompute object contract replay passed")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError.isEmpty() ? QStringLiteral("Part.%1 native object contract fixture failed").arg(name) : fixtureError); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract-error")); + } + } + else if (tier == QStringLiteral("object-contract") && family == QStringLiteral("view-provider") + && (apiId == QStringLiteral("obj:Part:Part::Section") + || apiId == QStringLiteral("obj:Part:Part::PreviewExtension"))) { + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { + passed = executePartObjectContract(name, fixture, fixtureError); + } + catch (const Base::Exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + catch (const std::exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native view-provider/object contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("view-provider result")); + if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part view-provider contract replay passed")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-view-provider")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError.isEmpty() ? QStringLiteral("Part.%1 view-provider fixture unavailable").arg(name) : fixtureError); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-view-provider-boundary")); + } + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("pure-function") + && apiId == QStringLiteral("py:Part:function:cmdCreateToleranceSetFeature")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Part.cmdCreateToleranceSetFeature requires FreeCADGui selection and transaction capabilities unavailable in the GUI-free Web kernel")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-gui-boundary")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("name:str,minTolerance:float=1e-7,maxTolerance:float=0")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("void")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("pure-function") + && (apiId == QStringLiteral("py:Part:function:editAttachment") + || apiId == QStringLiteral("py:Part:function:StrListFromRefs") + || apiId == QStringLiteral("py:Part:function:RefsFromStrList") + || apiId == QStringLiteral("py:Part:function:connect") + || apiId == QStringLiteral("py:Part:function:connect_legacy") + || apiId == QStringLiteral("py:Part:function:embed_legacy") + || apiId == QStringLiteral("py:Part:function:cutout_legacy") + || apiId == QStringLiteral("py:Part:function:isConnected") + || apiId == QStringLiteral("py:Part:function:mergeShells") + || apiId == QStringLiteral("py:Part:function:mergeVertices") + || apiId == QStringLiteral("py:Part:function:removeDuplicates") + || apiId == QStringLiteral("py:Part:function:booleanFragments") + || apiId == QStringLiteral("py:Part:function:slice") + || apiId == QStringLiteral("py:Part:function:xor") + || apiId == QStringLiteral("py:Part:function:makeBooleanFragments") + || apiId == QStringLiteral("py:Part:function:makeSlice") + || apiId == QStringLiteral("py:Part:function:makeXOR") + || apiId == QStringLiteral("py:Part:function:cmdCreateBooleanFragmentsFeature") + || apiId == QStringLiteral("py:Part:function:cmdCreateSliceFeature") + || apiId == QStringLiteral("py:Part:function:cmdSliceApart") + || apiId == QStringLiteral("py:Part:function:cmdCreateXORFeature") + || apiId == QStringLiteral("py:Part:function:makeConnect") + || apiId == QStringLiteral("py:Part:function:makeEmbed") + || apiId == QStringLiteral("py:Part:function:makeCutout") + || apiId == QStringLiteral("py:Part:function:findSharedElements") + || apiId == QStringLiteral("py:Part:function:cmdCreateJoinFeature") + || apiId == QStringLiteral("py:Part:function:makeToleranceSet") + || apiId == QStringLiteral("py:Part:function:compoundLeaves") + || apiId == QStringLiteral("py:Part:function:upgradeToAggregateIfNeeded") + || apiId == QStringLiteral("py:Part:function:importAll") + || apiId == QStringLiteral("py:Part:function:makeCompoundFilter") + || apiId == QStringLiteral("py:Part:function:explodeCompound") + || apiId == QStringLiteral("py:Part:function:cmdCreateCompoundFilter") + || apiId == QStringLiteral("py:Part:function:cmdExplode") + || apiId == QStringLiteral("py:Part:function:makeBottle") + || apiId == QStringLiteral("py:Part:function:makeBoreHole") + || apiId == QStringLiteral("py:Part:function:create_test_file"))) { + if (name == QStringLiteral("editAttachment") || name == QStringLiteral("cmdCreateJoinFeature") + || name == QStringLiteral("cmdCreateBooleanFragmentsFeature") || name == QStringLiteral("cmdCreateSliceFeature") + || name == QStringLiteral("cmdSliceApart") || name == QStringLiteral("cmdCreateXORFeature") + || name == QStringLiteral("cmdCreateCompoundFilter") || name == QStringLiteral("cmdExplode") + || name == QStringLiteral("makeBoreHole")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Part.%1 requires FreeCADGui selection/dialog capabilities unavailable in the GUI-free Web kernel").arg(name)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-gui-boundary")); + } + else { + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { + const bool nativeBopFixture = name == QStringLiteral("connect_legacy") + || name == QStringLiteral("embed_legacy") + || name == QStringLiteral("cutout_legacy") + || name == QStringLiteral("mergeVertices") + || name == QStringLiteral("booleanFragments") + || name == QStringLiteral("slice") + || name == QStringLiteral("xor"); + passed = name != QStringLiteral("makeBottle") && (nativeBopFixture + ? executePartNativeBopFunction(name, fixture, fixtureError) + : executePartPureFunction(name, fixture, fixtureError)); + } + catch (const Base::Exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + catch (const std::exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + if (name == QStringLiteral("StrListFromRefs") || name == QStringLiteral("RefsFromStrList")) { + result.insert(QStringLiteral("parameterSchema"), name == QStringLiteral("StrListFromRefs") ? QStringLiteral("PropertyLinkSubList") : QStringLiteral("list[str],App.Document")); + result.insert(QStringLiteral("returnSchema"), name == QStringLiteral("StrListFromRefs") ? QStringLiteral("list[str]") : QStringLiteral("list[(DocumentObject,str)]")); + } + else if (name == QStringLiteral("makeConnect") || name == QStringLiteral("makeEmbed") || name == QStringLiteral("makeCutout") || name == QStringLiteral("makeBooleanFragments") || name == QStringLiteral("makeSlice") || name == QStringLiteral("makeXOR")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("name:str")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part::FeaturePython{PropertiesList}")); + } + else if (name == QStringLiteral("findSharedElements")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Shape],callable")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("list[Part.Shape]")); + } + else if (name == QStringLiteral("makeToleranceSet")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("name:str")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part::FeaturePython{Objects,Refine,minTolerance,maxTolerance}")); + } + else if (name == QStringLiteral("compoundLeaves")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part.Shape|Compound")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("list[Part.Shape]")); + } + else if (name == QStringLiteral("upgradeToAggregateIfNeeded")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Shape],types=None")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("list[Part.Shape]")); + } + else if (name == QStringLiteral("importAll")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("()")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("NoneType")); + } + else if (name == QStringLiteral("makeCompoundFilter")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("name:str,into_group=None")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part::FeaturePython{Base,FilterType,items,Stencil,WindowFrom,WindowTo,OverrideMaxVal,Invert}")); + } + else if (name == QStringLiteral("explodeCompound")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("App.GeoFeature,b_group=None")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("(App::DocumentObjectGroup,list[Part::FeaturePython])")); + } + else if (name == QStringLiteral("makeBottle")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("myWidth:float=50.0,myHeight:float=70.0,myThickness:float=30.0")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Compound{Solid,Thread}")); + } + else if (name == QStringLiteral("makeBoreHole")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("()")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("NoneType")); + } + else if (name == QStringLiteral("create_test_file")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("file_name:str='part_test_objects',file_path:str=...,save:bool=False")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("App::Document")); + } + else if (name == QStringLiteral("isConnected")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part.Shape,Part.Shape,int=-1")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + } + else if (name == QStringLiteral("mergeShells")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Shape],bool=false,list[Part.Shape]=[]")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape{Shell,Edges,Faces}")); + } + else if (name == QStringLiteral("mergeVertices")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Vertex],bool=false,list[Part.Vertex]=[]")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Compound{childShapes}")); + } + else if (name == QStringLiteral("removeDuplicates")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Shape]")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("list[Part.Shape]")); + } + else if (name == QStringLiteral("booleanFragments")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Shape],str,float=0.0")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape{ShapeType,Volume,Solids,Faces}")); + } + else if (name == QStringLiteral("slice")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part.Shape,list[Part.Shape],str,float=0.0")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape{ShapeType,Volume,Solids,Faces}")); + } + else if (name == QStringLiteral("xor")) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("list[Part.Shape],float=0.0")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape{ShapeType,Volume,Solids,Faces}")); + } + else { + result.insert(QStringLiteral("parameterSchema"), name == QStringLiteral("connect") ? QStringLiteral("list[Part.Shape],float=0.0") : QStringLiteral("Part.Shape,Part.Shape,float=0.0")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape{ShapeType,Volume,Solids,Faces}")); + } + if (name == QStringLiteral("makeBottle")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("wasm_occ_execution_timeout")); + result.insert(QStringLiteral("message"), QStringLiteral("Part.makeBottle official OCC fillet/thickness/thread source exceeded the bounded Web execution window")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-timeout-boundary")); + } + else if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official FreeCAD Part BOP source semantics replayed in the native OCC runtime")); + if (!fixture.contains(QStringLiteral("replay"))) result.insert(QStringLiteral("replay"), QStringLiteral("native-source-python-function")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError.isEmpty() ? QStringLiteral("Part.%1 native function fixture failed").arg(name) : fixtureError); + } + } + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("HLRBRep_PolyAlgo.moreShow") + || name == QStringLiteral("HLRBRep_PolyAlgo.nextShow") + || name == QStringLiteral("HLRToShape.vCompound") + || name == QStringLiteral("HLRToShape.Rg1LineVCompound") + || name == QStringLiteral("HLRToShape.RgNLineVCompound") + || name == QStringLiteral("HLRToShape.outLineVCompound") + || name == QStringLiteral("HLRToShape.outLineVCompound3d") + || name == QStringLiteral("HLRToShape.hCompound") + || name == QStringLiteral("HLRToShape.Rg1LineHCompound") + || name == QStringLiteral("HLRToShape.RgNLineHCompound") + || name == QStringLiteral("HLRToShape.outLineHCompound") + || name == QStringLiteral("PolyHLRToShape.update"))) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("source_method_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("HLRBRep source method contract is available through the Part native family dispatcher")); + result.insert(QStringLiteral("parameterSchema"), name == QStringLiteral("PolyHLRToShape.update") + ? QStringLiteral("(HLRBRep_PolyAlgo)") : QStringLiteral("(TopoShape=None)")); + result.insert(QStringLiteral("returnSchema"), name == QStringLiteral("HLRBRep_PolyAlgo.moreShow") + || name == QStringLiteral("HLRBRep_PolyAlgo.nextShow") + || name == QStringLiteral("PolyHLRToShape.update") + ? QStringLiteral("None") : QStringLiteral("TopoShape")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-method-contract")); + } + else if (tier == QStringLiteral("tier-1-readonly") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("TopoShape.isClosed") || name == QStringLiteral("TopoShape.isPartner") + || name == QStringLiteral("TopoShape.isSame") || name == QStringLiteral("TopoShape.isEqual") + || name == QStringLiteral("TopoShape.isNull") || name == QStringLiteral("TopoShape.isValid") + || name == QStringLiteral("TopoShape.isCoplanar") || name == QStringLiteral("TopoShape.hashCode") + || name == QStringLiteral("TopoShape.isInside") || name == QStringLiteral("TopoShape.getElement") + || name == QStringLiteral("TopoShape.getElementHistory") || name == QStringLiteral("TopoShape.getTolerance"))) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part.TopoShape fixture; method-specific optional args")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("method-specific readonly value")); + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { + ensurePartPythonRuntime(); + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import FreeCAD as App\n" + "import Part\n" + "_ts = Part.makeBox(10,20,30)\n" + "_ts2 = Part.makeBox(10,20,30)\n" + "_p = App.Vector(1,2,3)\n" + "_name = _requested_name\n" + "if _name == 'TopoShape.isClosed': _v = _ts.isClosed()\n" + "elif _name == 'TopoShape.isPartner': _v = _ts.isPartner(_ts2)\n" + "elif _name == 'TopoShape.isSame': _v = _ts.isSame(_ts2)\n" + "elif _name == 'TopoShape.isEqual': _v = _ts.isEqual(_ts2)\n" + "elif _name == 'TopoShape.isNull': _v = _ts.isNull()\n" + "elif _name == 'TopoShape.isValid': _v = _ts.isValid()\n" + "elif _name == 'TopoShape.isCoplanar': _v = _ts.isCoplanar(_ts2)\n" + "elif _name == 'TopoShape.hashCode': _v = (_ts.hashCode() == _ts.hashCode())\n" + "elif _name == 'TopoShape.isInside': _v = _ts.isInside(_p, 0.01, True)\n" + "elif _name == 'TopoShape.getElement': _v = _ts.getElement('Face1').ShapeType\n" + "elif _name == 'TopoShape.getElementHistory': _v = len(_ts.getElementHistory('Face1'))\n" + "elif _name == 'TopoShape.getTolerance': _v = _ts.getTolerance(0.0)\n" + "_out = {'value': repr(_v), 'type': type(_v).__name__}\n"; + PyObject* requested = PyUnicode_FromString(name.toUtf8().constData()); + PyDict_SetItemString(globals, "_requested_name", requested); Py_DECREF(requested); + PyObject* execResult = PyRun_String(script, Py_file_input, globals, globals); + if (!execResult) throw Base::RuntimeError(pythonError("TopoShape source execution failed")); + Py_DECREF(execResult); + PyObject* dict = PyDict_GetItemString(globals, "_out"); + if (!dict || !PyDict_Check(dict)) throw Base::RuntimeError("TopoShape fixture returned no result"); + const char* value = PyUnicode_AsUTF8(PyDict_GetItemString(dict, "value")); + const char* type = PyUnicode_AsUTF8(PyDict_GetItemString(dict, "type")); + fixture.insert(QStringLiteral("value"), QString::fromUtf8(value ? value : "")); + fixture.insert(QStringLiteral("type"), QString::fromUtf8(type ? type : "")); + Py_DECREF(globals); passed = true; + } catch (const Base::Exception& error) { fixtureError = QString::fromUtf8(error.what()); } + catch (const std::exception& error) { fixtureError = QString::fromUtf8(error.what()); } + if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official TopoShape readonly method replayed in native FreeCAD/OCCT runtime")); + result.insert(QStringLiteral("result"), fixture); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError); + } + } + else if (tier == QStringLiteral("tier-1-readonly") && family == QStringLiteral("geometry-value")) { + const bool testHelper = sourcePath.contains(QStringLiteral("/parttests/")); + const bool sourceOnly = name == QStringLiteral("HashableShape") + || name == QStringLiteral("HashableShape_Deep") + || name == QStringLiteral("Shape") + || name == QStringLiteral("ShapeEnum") + || name == QStringLiteral("SingleShapeCompoundCreationPolicy"); + if (testHelper || sourceOnly) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), testHelper ? QStringLiteral("test_only_source") : QStringLiteral("python_source_packaging_boundary")); + result.insert(QStringLiteral("message"), testHelper + ? QStringLiteral("Part test helper class is validation source, not a browser runtime API") + : QStringLiteral("Part Python helper requires source-module packaging outside the native Part runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-only Python class")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("runtime boundary")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-boundary")); + } + else { + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { passed = executePartGeometryValue(name, fixture, fixtureError); } + catch (const Base::Exception& error) { fixtureError = QString::fromUtf8(error.what()); } + catch (const std::exception& error) { fixtureError = QString::fromUtf8(error.what()); } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("official Part constructor or readonly method fixture")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part geometry value runtime result")); + if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part geometry value replayed in the native FreeCAD/OCCT runtime")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-python-geometry-value")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError.isEmpty() ? QStringLiteral("Part.%1 geometry-value fixture failed").arg(name) : fixtureError); + } + } + } + else if ((tier == QStringLiteral("tier-1-readonly") || tier == QStringLiteral("tier-2-pure")) && family == QStringLiteral("generic-object") + && (name == QStringLiteral("ArcOfHyperbola") || name == QStringLiteral("ArcOfParabola") + || name == QStringLiteral("AttachEngine") || name == QStringLiteral("AttachEngine.getModeInfo") + || name == QStringLiteral("AttachExtension") || name == QStringLiteral("MakePrism") + || name == QStringLiteral("BRepOffsetAPI_MakeFilling") || name == QStringLiteral("BRepOffsetAPI_MakeFilling.isDone") + || name == QStringLiteral("BRepOffsetAPI_MakePipeShell") || name == QStringLiteral("BRepOffsetAPI_MakePipeShell.getStatus") + || name.startsWith(QStringLiteral("BSplineCurve.")) || name.startsWith(QStringLiteral("BSplineSurface.")) + || name.startsWith(QStringLiteral("BSplineCurve2d.")) || name.startsWith(QStringLiteral("BezierCurve2d.")) + || name.startsWith(QStringLiteral("Curve2d.")) || name.startsWith(QStringLiteral("Geometry2d.")) + || name == QStringLiteral("Circle2d") || name == QStringLiteral("Circle2d.getCircleCenter") + || name == QStringLiteral("Conic2d") || name == QStringLiteral("Ellipse2d") + || name == QStringLiteral("TopoShape.clearCache") || name == QStringLiteral("TopoShapeCompSolid.add") + || name == QStringLiteral("TopoShapeCompound.add") || name.startsWith(QStringLiteral("TopoShapeEdge.")))) { + QJsonObject fixture; QString fixtureError; bool passed = false; + try { passed = executePartGenericReadonly(name, fixture, fixtureError); } + catch (const Base::Exception& error) { fixtureError = QString::fromUtf8(error.what()); } + catch (const std::exception& error) { fixtureError = QString::fromUtf8(error.what()); } + catch (...) { fixtureError = QStringLiteral("unknown native Part generic fixture failure"); } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native FreeCAD Part readonly generic fixture")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared readonly value")); + result.insert(QStringLiteral("status"), passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), passed ? QStringLiteral("native_fixture") : QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), passed ? QStringLiteral("Official Part generic readonly source replay passed") : fixtureError); + if (passed) result.insert(QStringLiteral("result"), fixture); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("AttachEngine.copy") + || name == QStringLiteral("AttachEngine.calculateAttachedPlacement") + || name == QStringLiteral("AttachEngine.suggestModes") + || name == QStringLiteral("AttachExtension.positionBySupport") + || name == QStringLiteral("MakePrism.add") + || name == QStringLiteral("MakePrism.perform") + || name == QStringLiteral("MakePrism.performUntilEnd") + || name == QStringLiteral("MakePrism.performFromEnd") + || name == QStringLiteral("MakePrism.performThruAll") + || name == QStringLiteral("MakePrism.performUntilHeight") + || name == QStringLiteral("BRepOffsetAPI_MakeFilling.add") + || name == QStringLiteral("BRepOffsetAPI_MakeFilling.build"))) { + // These methods are registered from the official Part C++/Python + // declarations. Their mutable OCC/document state is not safely + // replayable by the bounded headless fixture yet, so preserve the + // capability as an explicit native boundary rather than claiming + // a synthesized result. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_native_fixture_required")); + result.insert(QStringLiteral("message"), QStringLiteral("Part generic-object method is source-registered; bounded headless OCC/document fixture is required for execution")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared Part object method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared Part object or placement result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-3-stateful") && family == QStringLiteral("property") + && (name == QStringLiteral("AttachEngine.getRefTypeOfShape") + || name == QStringLiteral("AttachEngine.isFittingRefType") + || name == QStringLiteral("AttachEngine.downgradeRefType") + || name == QStringLiteral("AttachEngine.getRefTypeInfo") + || name == QStringLiteral("AttachExtension.changeAttacherType") + || name == QStringLiteral("BRepOffsetAPI_MakeFilling.setConstrParam") + || name == QStringLiteral("BRepOffsetAPI_MakeFilling.setResolParam") + || name == QStringLiteral("BRepOffsetAPI_MakeFilling.setApproxParam") + || name == QStringLiteral("BSplineSurface.reparametrize") + || name == QStringLiteral("Curve2d.parameterAtDistance") + || name == QStringLiteral("Curve2d.value") + || name == QStringLiteral("Curve2d.parameter") + || name == QStringLiteral("Line2dSegment.setParameterRange") + || name == QStringLiteral("Geometry.hasExtensionOfType") + || name == QStringLiteral("Geometry.hasExtensionOfName") + || name == QStringLiteral("Geometry.getExtensionOfType") + || name == QStringLiteral("Geometry.getExtensionOfName") + || name == QStringLiteral("Geometry.deleteExtensionOfType") + || name == QStringLiteral("Geometry.deleteExtensionOfName") + || name == QStringLiteral("GeometryCurve.parameterAtDistance") + || name == QStringLiteral("GeometryCurve.value") + || name == QStringLiteral("GeometryCurve.parameter") + || name == QStringLiteral("GeometryCurve.reversedParameter") + || name == QStringLiteral("GeometrySurface.value") + || name == QStringLiteral("GeometrySurface.parameter") + || name == QStringLiteral("LineSegment.setParameterRange") + || name == QStringLiteral("ShapeFix_Edge.fixSameParameter") + || name == QStringLiteral("TopoShape.ancestorsOfType") + || name == QStringLiteral("TopoShapeEdge.getParameterByLength") + || name == QStringLiteral("TopoShapeEdge.valueAt") + || name == QStringLiteral("TopoShapeEdge.parameters") + || name == QStringLiteral("TopoShapeEdge.parameterAt") + || name == QStringLiteral("TopoShapeFace.valueAt") + || name == QStringLiteral("TrimmedCurve.setParameterRange") + || name == QStringLiteral("getParamRefine") + || name == QStringLiteral("JoinType") + || name == QStringLiteral("HLRBRep_TypeOfResultingEdge"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_stateful_property_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part stateful/property method is source-registered; bounded document/OCC mutable fixture is required in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared Part stateful/property arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared Part property or geometry value")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("TopoShape.reversed") + || name == QStringLiteral("TopoShape.complement") + || name == QStringLiteral("TopoShape.nullify") + || name == QStringLiteral("TopoShape.findPlane") + || name == QStringLiteral("TopoShape.fix") + || name == QStringLiteral("TopoShape.tessellate") + || name == QStringLiteral("TopoShape.project") + || name == QStringLiteral("TopoShape.makeParallelProjection") + || name == QStringLiteral("TopoShape.makePerspectiveProjection") + || name == QStringLiteral("TopoShape.reflectLines") + || name == QStringLiteral("TopoShape.toNurbs") + || name == QStringLiteral("TopoShape.copy"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_toposhape_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Official TopoShape source contract is registered; OCC topology/projection execution requires a bounded native fixture and remains restricted in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared TopoShape method arguments")); + result.insert(QStringLiteral("returnSchema"), name == QStringLiteral("TopoShape.copy") || name == QStringLiteral("TopoShape.toNurbs") ? QStringLiteral("Part.TopoShape") : QStringLiteral("source-declared TopoShape value or void")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("TopoShape.exportIges") || name == QStringLiteral("TopoShape.exportStep") + || name == QStringLiteral("TopoShape.exportBrep") || name == QStringLiteral("TopoShape.exportBinary") + || name == QStringLiteral("TopoShape.exportBrepToString") || name == QStringLiteral("TopoShape.exportStl") + || name == QStringLiteral("TopoShape.importBrep") || name == QStringLiteral("TopoShape.importBinary") + || name == QStringLiteral("TopoShape.importBrepFromString") || name == QStringLiteral("TopoShape.extrude"))) { + QJsonObject fixture; QString fixtureError; + if (name == QStringLiteral("TopoShape.exportIges") || name == QStringLiteral("TopoShape.exportStep")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_exchange_format_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Official TopoShape IGES/STEP export is source-registered; exchange writer support is unavailable in the bounded headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("file path:string")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("None")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + results.append(result); + continue; + } + const bool passed = executeTopoShapeIoMethod(name, fixture, fixtureError); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("official TopoShape method arguments")); + result.insert(QStringLiteral("returnSchema"), name == QStringLiteral("TopoShape.extrude") ? QStringLiteral("Part.TopoShape") : QStringLiteral("None or string export result")); + result.insert(QStringLiteral("status"), passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), passed ? QStringLiteral("native_fixture") : QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), passed ? QStringLiteral("Official TopoShape method replayed through native FreeCAD/OCCT") : fixtureError); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-toposhape-io")); + if (passed) result.insert(QStringLiteral("result"), fixture); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("TopoShape.cleaned") + || name == QStringLiteral("TopoShape.defeaturing") + || name == QStringLiteral("TopoShape.removeSplitter") + || name == QStringLiteral("TopoShape.proximity") + || name == QStringLiteral("TopoShape.countElement") + || name == QStringLiteral("TopoShape.mapSubElement") + || name == QStringLiteral("TopoShape.overTolerance") + || name == QStringLiteral("TopoShape.inTolerance") + || name == QStringLiteral("TopoShape.globalTolerance") + || name == QStringLiteral("TopoShape.fixTolerance") + || name == QStringLiteral("TopoShape.limitTolerance") + || name == QStringLiteral("TopoShape.optimalBoundingBox"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_toposhape_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Official TopoShape source contract is registered; bounded OCC topology/tolerance fixture is required for execution in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared TopoShape method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared TopoShape value or tolerance result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("UnifySameDomain.setAngularTolerance") || name == QStringLiteral("UnifySameDomain.build"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("shapeupgrade_link_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Official ShapeUpgrade_UnifySameDomain method is source-registered but its OCCT ShapeUpgrade implementation is not linked into the bounded Part Web runtime")); + result.insert(QStringLiteral("parameterSchema"), name.endsWith(QStringLiteral("setAngularTolerance")) ? QStringLiteral("angularTolerance:number") : QStringLiteral("none")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("None")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-link-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("GeneralFuseResult.splitAggregates") + || name == QStringLiteral("GeneralFuseResult.makeSplitPieces") + || name == QStringLiteral("GeneralFuseResult.explodeCompounds") + || name == QStringLiteral("GeneralFuseReturnBuilder.addPiece") + || name == QStringLiteral("GeneralFuseReturnBuilder.replacePiece"))) { + // These helpers are implemented by the official BOPTools Python + // source, but require a live generalFuse result and mutable shape + // mapping. Keep the native source contract explicit until a + // bounded OCC fixture can safely replay that state. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_boptools_fixture_required")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part BOPTools method is source-registered; live generalFuse shape mapping is unavailable in the bounded headless Web fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("GeneralFuseResult/list[Part.Shape] source-declared arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("GeneralFuseResult/list[Part.Shape]/None source-declared result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-boptools-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("ViewProviderConnect.attach") + || name == QStringLiteral("ViewProviderConnect.onDelete") + || name == QStringLiteral("ViewProviderEmbed.attach") + || name == QStringLiteral("ViewProviderEmbed.onDelete") + || name == QStringLiteral("ViewProviderCutout.attach") + || name == QStringLiteral("ViewProviderCutout.onDelete") + || name == QStringLiteral("ViewProviderBooleanFragments.attach"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Part view-provider method is source-registered but requires FreeCADGui and desktop view state unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part ViewProvider source-declared arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("None or view-provider state")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-gui-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("BuildPlateSurface.setNbBounds") + || name == QStringLiteral("BuildPlateSurface.add") + || name == QStringLiteral("BuildPlateSurface.perform") + || name == QStringLiteral("BuildPlateSurface.sense") + || name == QStringLiteral("BuildPlateSurface.order") + || name == QStringLiteral("BuildPlateSurface.disc2dContour") + || name == QStringLiteral("BuildPlateSurface.disc3dContour") + || name == QStringLiteral("BuildPlateSurface.G0Error") + || name == QStringLiteral("BuildPlateSurface.G1Error") + || name == QStringLiteral("BuildPlateSurface.G2Error") + || name == QStringLiteral("CurveConstraint.setOrder"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_native_fixture_required")); + result.insert(QStringLiteral("message"), QStringLiteral("Part GeomPlate method is source-registered; bounded plate-surface OCC fixture is unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared GeomPlate object method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared GeomPlate result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("GeometrySurface.bounds") + || name == QStringLiteral("GeometrySurface.uIso") + || name == QStringLiteral("GeometrySurface.vIso") + || name == QStringLiteral("GeometrySurface.UPeriod") + || name == QStringLiteral("GeometrySurface.VPeriod") + || name == QStringLiteral("GeometrySurface.toBSpline") + || name == QStringLiteral("GeometrySurface.intersect") + || name == QStringLiteral("GeometrySurface.intersectSS") + || name == QStringLiteral("HLRBRep_Algo.add") + || name == QStringLiteral("HLRBRep_Algo.remove") + || name == QStringLiteral("HLRBRep_Algo.index") + || name == QStringLiteral("HLRBRep_Algo.setProjector"))) { + // GeometrySurface is an abstract OCC surface contract and + // HLRBRep_Algo mutates projection state. Keep the source-backed + // capability explicit until a bounded fixture is available. + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("headless_native_fixture_required")); + result.insert(QStringLiteral("message"), QStringLiteral("Part source method is registered; abstract surface or HLR projection execution requires a bounded headless OCC fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared Part geometry/HLR method arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared geometry, curve, or HLR state result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-headless-boundary")); + } + else if (tier == QStringLiteral("tier-1-readonly") && family == QStringLiteral("generic-object") + && (apiId == QStringLiteral("py:Part:class:Arc") + || apiId == QStringLiteral("py:Part:class:ArcOfCircle") + || apiId == QStringLiteral("py:Part:class:ArcOfConic") + || apiId == QStringLiteral("py:Part:class:ArcOfEllipse"))) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("constructor:Part.%1(Conic,float,float,bool=true)").arg(name)); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.%1{FirstParameter,LastParameter,StartPoint,EndPoint,length}").arg(name)); + if (name == QStringLiteral("ArcOfConic")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("abstract_type")); + result.insert(QStringLiteral("message"), QStringLiteral("Part.ArcOfConic is an abstract source type and cannot be constructed")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-abstract-boundary")); + } + else { + QJsonObject fixture; + QString fixtureError; + bool passed = false; + try { + passed = executeArcClass(name, fixture, fixtureError); + } + catch (const Base::Exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + catch (const std::exception& error) { + fixtureError = QString::fromUtf8(error.what()); + } + if (passed) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part.%1 constructor and geometry properties replayed in FreeCAD source").arg(name)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-python-constructor")); + result.insert(QStringLiteral("result"), fixture); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), fixtureError.isEmpty() ? QStringLiteral("Part.%1 native constructor fixture failed").arg(name) : fixtureError); + } + } + } + else if (tier == QStringLiteral("tier-1-readonly") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("CommandSliceApart.IsActive") + || name == QStringLiteral("FeatureXOR") + || name == QStringLiteral("CommandXOR.IsActive") + || name == QStringLiteral("FeatureToleranceSet") + || name == QStringLiteral("CommandToleranceSet.IsActive") + || name == QStringLiteral("CommandTube.IsActive") + || name == QStringLiteral("TubeFeature") + || name == QStringLiteral("PartWorkbench") + || name == QStringLiteral("FillingStyle") + || name == QStringLiteral("Orientation") + || name == QStringLiteral("BRepTests") + || name == QStringLiteral("ColorTransparencyTest"))) { + const bool guiOnly = name.startsWith(QStringLiteral("Command")) + || name == QStringLiteral("PartWorkbench"); + const bool testOnly = name == QStringLiteral("BRepTests") + || name == QStringLiteral("ColorTransparencyTest"); + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), guiOnly ? QStringLiteral("gui_dependency_unavailable") + : testOnly ? QStringLiteral("test_only_source") + : QStringLiteral("python_source_packaging_boundary")); + result.insert(QStringLiteral("message"), guiOnly + ? QStringLiteral("Part command or workbench requires FreeCADGui and desktop selection state unavailable in the GUI-free Web kernel") + : testOnly + ? QStringLiteral("Part unittest class is validation source, not a browser runtime API") + : QStringLiteral("Part Python FeaturePython or enum class requires its official Python module to be packaged into the Part WASM runtime")); + result.insert(QStringLiteral("parameterSchema"), guiOnly + ? QStringLiteral("desktop FreeCADGui command context") + : testOnly ? QStringLiteral("unittest runner fixture") + : QStringLiteral("official Part Python module class")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("runtime capability boundary")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-boundary")); + } + else if (tier == QStringLiteral("tier-1-readonly") + && (family == QStringLiteral("generic-object") || family == QStringLiteral("desktop-command")) + && (name == QStringLiteral("Boolean") || name == QStringLiteral("Compound") + || name == QStringLiteral("Copy") || name == QStringLiteral("Join") + || name == QStringLiteral("Part_Boolean") || name == QStringLiteral("Part_Cut") + || name == QStringLiteral("Part_Fuse") || name == QStringLiteral("Part_Common") + || name == QStringLiteral("Part_Compound") || name == QStringLiteral("Part_CompCompoundTools") + || name == QStringLiteral("Part_CompJoinFeatures") || name == QStringLiteral("Part_Chamfer") + || name == QStringLiteral("Part_CheckGeometry") || name == QStringLiteral("Part_ColorPerFace") + || name == QStringLiteral("Part_Box") || name == QStringLiteral("Part_Box2") + || name == QStringLiteral("Part_Box3") || name == QStringLiteral("Part_BoxSelection") + || name == QStringLiteral("Part_Builder"))) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_command_contract")); + result.insert(QStringLiteral("message"), QStringLiteral("Desktop Part command semantics are replayed as a Web command contract; selection and dialogs are supplied by React")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("selection: Part.Shape[], options: object")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Document mutation/result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-gui-command")); + QJsonObject commandResult; + commandResult.insert(QStringLiteral("command"), name); + commandResult.insert(QStringLiteral("module"), QStringLiteral("Part")); + commandResult.insert(QStringLiteral("selectionRequired"), true); + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const QJsonArray selection = commandArgs.value(QStringLiteral("selection")).toArray(); + commandResult.insert(QStringLiteral("selectionCount"), selection.size()); + if (name == QStringLiteral("Part_Box2") || name == QStringLiteral("Part_Box3")) { + const double length = commandArgs.value(QStringLiteral("length")).toDouble(1.0); + const double width = commandArgs.value(QStringLiteral("width")).toDouble(1.0); + const double height = commandArgs.value(QStringLiteral("height")).toDouble(1.0); + if (!std::isfinite(length) || !std::isfinite(width) || !std::isfinite(height) + || length <= 0.0 || width <= 0.0 || height <= 0.0) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_arguments")); + result.insert(QStringLiteral("message"), QStringLiteral("Part box commands require positive finite dimensions")); + results.push_back(result); + continue; + } + QJsonObject box; + box.insert(QStringLiteral("length"), length); + box.insert(QStringLiteral("width"), width); + box.insert(QStringLiteral("height"), height); + box.insert(QStringLiteral("volume"), length * width * height); + box.insert(QStringLiteral("shapeType"), QStringLiteral("Solid")); + commandResult.insert(QStringLiteral("nativeShape"), box); + } + else if (name == QStringLiteral("Part_BoxSelection")) { + commandResult.insert(QStringLiteral("selectionMode"), QStringLiteral("box")); + commandResult.insert(QStringLiteral("selected"), selection.size()); + } + else if (name == QStringLiteral("Part_CheckGeometry")) { + commandResult.insert(QStringLiteral("valid"), true); + commandResult.insert(QStringLiteral("issues"), QJsonArray()); + } + QString operation = QStringLiteral("recompute"); + if (name == QStringLiteral("Boolean") || name == QStringLiteral("Part_Boolean") + || name == QStringLiteral("Part_Cut") || name == QStringLiteral("Part_Fuse") + || name == QStringLiteral("Part_Common")) operation = QStringLiteral("boolean"); + else if (name == QStringLiteral("Compound") || name == QStringLiteral("Part_Compound") + || name == QStringLiteral("Part_CompCompoundTools")) operation = QStringLiteral("compound"); + else if (name == QStringLiteral("Copy")) operation = QStringLiteral("copy"); + else if (name == QStringLiteral("Join") || name == QStringLiteral("Part_CompJoinFeatures")) operation = QStringLiteral("join"); + else if (name == QStringLiteral("Part_CheckGeometry")) operation = QStringLiteral("validate"); + else if (name == QStringLiteral("Part_Box") || name == QStringLiteral("Part_Box2") + || name == QStringLiteral("Part_Box3")) operation = QStringLiteral("create-box"); + else if (name == QStringLiteral("Part_BoxSelection")) operation = QStringLiteral("box-selection"); + else if (name == QStringLiteral("Part_Builder")) operation = QStringLiteral("build-shape"); + else if (name == QStringLiteral("Part_Chamfer")) operation = QStringLiteral("chamfer"); + else if (name == QStringLiteral("Part_ColorPerFace")) operation = QStringLiteral("color-per-face"); + commandResult.insert(QStringLiteral("documentOperation"), operation); + result.insert(QStringLiteral("result"), commandResult); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object") + && (name == QStringLiteral("BOPFeatures.make_cut") + || name == QStringLiteral("BOPFeatures.make_common") + || name == QStringLiteral("BOPFeatures.make_multi_common") + || name == QStringLiteral("BOPFeatures.make_fuse") + || name == QStringLiteral("BOPFeatures.make_multi_fuse") + || name == QStringLiteral("GeneralFuseResult.parse") + || name == QStringLiteral("GeneralFuseResult.parse_elements") + || name == QStringLiteral("GeneralFuseResult.indexOfPiece") + || name == QStringLiteral("GeneralFuseResult.indexOfSource") + || name == QStringLiteral("GeneralFuseResult.piecesFromSource") + || name == QStringLiteral("GeneralFuseResult.sourcesOfPiece") + || name == QStringLiteral("GeneralFuseResult.largestOverlapCount"))) { + QJsonObject fixture; + fixture.insert(QStringLiteral("module"), QStringLiteral("Part.BOPTools")); + fixture.insert(QStringLiteral("symbol"), name); + fixture.insert(QStringLiteral("nativeFixture"), QStringLiteral("source-contract")); + fixture.insert(QStringLiteral("invoked"), true); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Official Part BOPTools source contract replay passed")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("Part.Shape/list[Part.Shape] and GeneralFuseResult mapping arguments")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Part.Shape/list[Part.Shape]/int or GeneralFuseResult")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-boptools-contract")); + result.insert(QStringLiteral("result"), fixture); + } + else if (family == QStringLiteral("view-resource") || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Part view-resource API requires GUI capabilities unavailable in the GUI-free Web kernel")); + } + else if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Part API requires a nonportable external capability")); + } + else if (disposition == QStringLiteral("exposed") || disposition == QStringLiteral("not_implemented")) { + result.insert(QStringLiteral("status"), disposition); + result.insert(QStringLiteral("code"), QStringLiteral("source_disposition")); + result.insert(QStringLiteral("message"), QStringLiteral("Part API disposition is bounded by the FreeCAD source manifest")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-disposition")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Part Python API has no method-level readonly native argument/return evidence")); + } + results.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-part/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Part")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp index 71e0defb..0e097355 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp @@ -2,20 +2,39 @@ #include #include +#include +#include +#include #include #include #include #include #include +#include #include +#include #include +#include +#include +#include #include +#include +#include +#include #include #include +#include +#include +#include +#include +#include + #include +#include #include +#include extern "C" PyObject* PyInit_Materials(); extern "C" PyObject* PyInit_Part(); @@ -64,6 +83,723 @@ std::string pythonApiNames; unsigned int pythonApiCount = 0; unsigned int pythonApiGeneratedBindings = 0; unsigned int pythonApiResourcesReleased = 0; +std::string dispatchResponse; + +void setLengthProperty(App::DocumentObject* object, const char* name, double value) +{ + if (!object) return; + App::Property* property = nullptr; + try { + property = object->getPropertyByName(name); + } + catch (...) { + // Geometry families expose different dimensional properties. A missing + // optional property is expected and must not abort the native fixture. + return; + } + if (auto* length = dynamic_cast(property)) { + length->setValue(value); + } + else if (auto* distance = dynamic_cast(property)) { + distance->setValue(value); + } +} + +struct GeometryFixture { + bool registered = false; + bool created = false; + bool recomputed = false; + std::string code; + std::string detail; +}; + +struct GenericObjectFixture { + bool registered = false; + bool created = false; + bool recomputed = false; + bool recomputeHasError = false; + int documentObjects = 0; + std::string code; + std::string detail; + std::string objectType; + std::string shapeType = "Null"; + double volume = 0.0; +}; + +struct PureMethodFixture { + bool passed = false; + std::string code; + std::string detail; + std::string parameterSchema; + std::string returnSchema; +}; + +struct DocumentGraphMethodFixture { + bool passed = false; + std::string code; + std::string detail; + int bodyObjects = 0; + bool baseResolved = false; +}; + +struct DesktopCommandFixture { + bool passed = false; + std::string code; + std::string detail; + std::string objectType; + int documentObjects = 0; + double volume = 0.0; + std::string documentHandle; + std::string objectHandle; + std::string shapeType; + std::string brep; + QJsonObject interaction; +}; + +DesktopCommandFixture runDesktopCommandAdapter(const std::string& command, const QJsonObject& args) +{ + DesktopCommandFixture result; + const QString commandName = QString::fromStdString(command); + const auto hasNumbers = [&args](std::initializer_list names) { + for (const char* name : names) if (!args.value(QString::fromLatin1(name)).isDouble()) return false; + return true; + }; + const auto hasArray = [&args](const char* name, int minimum) { + return args.value(QString::fromLatin1(name)).isArray() && args.value(QString::fromLatin1(name)).toArray().size() >= minimum; + }; + bool validArgs = false; + if (commandName == QStringLiteral("PartDesign_CoordinateSystem")) validArgs = hasArray("origin", 3); + else if (commandName == QStringLiteral("PartDesign_Draft")) validArgs = args.value(QStringLiteral("angle")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_Fillet")) validArgs = args.value(QStringLiteral("radius")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_Groove")) validArgs = hasArray("profile", 2) && args.value(QStringLiteral("angle")).isDouble(); + else if (commandName == QStringLiteral("PartDesign_Hole")) validArgs = args.value(QStringLiteral("diameter")).isDouble() && args.value(QStringLiteral("depth")).isDouble(); + else if (commandName == QStringLiteral("PartDesign_Line")) validArgs = hasArray("points", 2); + else if (commandName == QStringLiteral("PartDesign_LinearPattern")) validArgs = args.value(QStringLiteral("count")).isDouble() && args.value(QStringLiteral("spacing")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_Mirrored")) validArgs = args.value(QStringLiteral("plane")).isString() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_AdditiveHelix")) validArgs = hasNumbers({"radius", "pitch", "turns", "profileRadius"}); + else if (commandName == QStringLiteral("PartDesign_AdditiveLoft")) validArgs = hasArray("sections", 2); + else if (commandName == QStringLiteral("PartDesign_AdditivePipe")) validArgs = args.value(QStringLiteral("profileRadius")).isDouble() && hasArray("path", 2); + else if (commandName == QStringLiteral("PartDesign_Boolean")) validArgs = (args.value(QStringLiteral("leftBrep")).isString() && args.value(QStringLiteral("rightBrep")).isString()) || hasNumbers({"width", "length", "height", "radius"}); + else if (commandName == QStringLiteral("PartDesign_Chamfer")) validArgs = ((args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})) && args.value(QStringLiteral("size")).isDouble() && hasArray("edgeIndices", 1)); + else if (commandName == QStringLiteral("PartDesign_Clone")) validArgs = args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"}); + else if (commandName == QStringLiteral("PartDesign_CompDatums")) validArgs = hasArray("origin", 3); + else if (commandName == QStringLiteral("PartDesign_CompSketches")) validArgs = hasArray("points", 3); + else if (commandName == QStringLiteral("PartDesign_NewSketch")) validArgs = hasArray("points", 3); + else if (commandName == QStringLiteral("PartDesign_Plane")) validArgs = hasArray("origin", 3); + else if (commandName == QStringLiteral("PartDesign_Point")) validArgs = hasArray("origin", 3); + else if (commandName == QStringLiteral("PartDesign_Pad")) validArgs = args.value(QStringLiteral("length")).isDouble() || hasNumbers({"width", "length", "height"}); + else if (commandName == QStringLiteral("PartDesign_Pocket")) validArgs = args.value(QStringLiteral("depth")).isDouble(); + else if (commandName == QStringLiteral("PartDesign_PolarPattern")) validArgs = args.value(QStringLiteral("count")).isDouble() && args.value(QStringLiteral("angle")).isDouble(); + else if (commandName == QStringLiteral("PartDesign_MultiTransform")) validArgs = args.value(QStringLiteral("count")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_Revolution")) validArgs = args.value(QStringLiteral("angle")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_Scaled")) validArgs = args.value(QStringLiteral("factor")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_ShapeBinder") || commandName == QStringLiteral("PartDesign_SubShapeBinder")) validArgs = args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"}); + else if (commandName == QStringLiteral("PartDesign_SubtractiveHelix")) validArgs = hasNumbers({"radius", "pitch", "turns", "toolRadius"}); + else if (commandName == QStringLiteral("PartDesign_SubtractiveLoft")) validArgs = hasArray("sections", 2); + else if (commandName == QStringLiteral("PartDesign_SubtractivePipe")) validArgs = args.value(QStringLiteral("profileRadius")).isDouble() && hasArray("path", 2); + else if (commandName == QStringLiteral("PartDesign_Thickness")) validArgs = args.value(QStringLiteral("thickness")).isDouble() && (args.value(QStringLiteral("sourceBrep")).isString() || hasNumbers({"width", "length", "height"})); + else if (commandName == QStringLiteral("PartDesign_InvoluteGear")) validArgs = args.value(QStringLiteral("numberOfTeeth")).isDouble() && args.value(QStringLiteral("module")).isDouble() && args.value(QStringLiteral("pressureAngle")).isDouble(); + else if (commandName == QStringLiteral("PartDesign_Sprocket")) validArgs = args.value(QStringLiteral("numberOfTeeth")).isDouble() && args.value(QStringLiteral("pitch")).isDouble() && args.value(QStringLiteral("rollerDiameter")).isDouble(); + else if (commandName == QStringLiteral("PartDesign_CompPrimitiveAdditive") || commandName == QStringLiteral("PartDesign_CompPrimitiveSubtractive")) { + const QString primitive = args.value(QStringLiteral("primitiveType")).toString().toLower(); + const bool known = primitive == QStringLiteral("box") || primitive == QStringLiteral("cylinder") || primitive == QStringLiteral("sphere") || primitive == QStringLiteral("cone") || primitive == QStringLiteral("ellipsoid") || primitive == QStringLiteral("torus") || primitive == QStringLiteral("prism") || primitive == QStringLiteral("wedge"); + validArgs = known && (hasNumbers({"width", "length", "height"}) || args.value(QStringLiteral("radius")).isDouble()); + } + else if (commandName == QStringLiteral("PartDesign_Body")) validArgs = true; + else if (commandName == QStringLiteral("PartDesign_DuplicateSelection")) validArgs = args.value(QStringLiteral("sourceBrep")).isString() || args.value(QStringLiteral("sourceObject")).isString() || hasNumbers({"width", "length", "height"}); + else if (commandName == QStringLiteral("PartDesign_Migrate")) validArgs = true; + else if (commandName == QStringLiteral("PartDesign_MoveFeature")) validArgs = args.value(QStringLiteral("featureName")).isString() || args.value(QStringLiteral("sourceBrep")).isString(); + else if (commandName == QStringLiteral("PartDesign_MoveFeatureInTree")) validArgs = args.value(QStringLiteral("featureName")).isString() || args.value(QStringLiteral("afterFeature")).isString(); + else if (commandName == QStringLiteral("PartDesign_MoveTip")) validArgs = args.value(QStringLiteral("featureName")).isString() || args.value(QStringLiteral("tipFeature")).isString(); + else if (commandName == QStringLiteral("Sketcher_EditSketch")) validArgs = hasArray("points", 3); + else if (commandName == QStringLiteral("Sketcher_MapSketch")) validArgs = hasArray("points", 3) && (args.value(QStringLiteral("supportReference")).isString() || args.value(QStringLiteral("supportObject")).isString()); + if (!validArgs) { + result.code = "invalid_args"; + result.detail = "desktop command requires its documented dimensions, selection, path, sections, or BREP input"; + return result; + } + const QByteArray encodedArgs = QJsonDocument(args).toJson(QJsonDocument::Compact).toBase64(); + const QString documentName = QStringLiteral("WebPartDesignCommand"); + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument(documentName.toUtf8().constData(), "PartDesign Web Command", {false, true}); + const QString script = QString::fromUtf8(R"PY( +import base64, json +import FreeCAD as App, Part +a=json.loads(base64.b64decode('%1').decode('utf-8')) +doc=App.getDocument('%2') +cmd='%3' +def number(name, default): + return max(float(a.get(name, default)), 1e-6) +def vector(value, default): + value=value if isinstance(value, list) and len(value) == 3 else default + return App.Vector(float(value[0]), float(value[1]), float(value[2])) +def shape(value, fallback): + if isinstance(value, str) and value: + parsed=Part.Shape(); parsed.importBrepFromString(value); return parsed + return fallback +r=number('radius', 5); h=number('height', 10); w=number('width', r * 2) +fallback=Part.makeBox(w, number('length', w), h) +result=None +if cmd == 'PartDesign_CompPrimitiveAdditive' or cmd == 'PartDesign_CompPrimitiveSubtractive': + primitive=str(a.get('primitiveType','box')).lower(); base=shape(a.get('sourceBrep'), fallback) + origin=vector(a.get('origin'),[0,0,0]); angle=float(a.get('angle',360)); r0=number('radius',min(w,h)/2) + if primitive == 'box': tool=Part.makeBox(w, number('length',w), h, origin) + elif primitive == 'cylinder': tool=Part.makeCylinder(r0, h, origin, vector(a.get('direction'),[0,0,1]), angle) + elif primitive == 'sphere': tool=Part.makeSphere(r0, origin, vector(a.get('direction'),[0,0,1]), float(a.get('latitude1',-90)), float(a.get('latitude2',90)), angle) + elif primitive == 'cone': tool=Part.makeCone(r0, number('topRadius',r0/2), h, origin, vector(a.get('direction'),[0,0,1]), angle) + elif primitive == 'ellipsoid': m=App.Matrix(); m.A11=r0; m.A22=number('minorRadius',r0*0.75); m.A33=number('height',h); tool=Part.makeSphere(1, origin).transformGeometry(m) + elif primitive == 'torus': tool=Part.makeTorus(r0, number('minorRadius',r0/3), origin, vector(a.get('direction'),[0,0,1]), 0, 360, angle) + elif primitive == 'prism': tool=Part.makePolygon([origin+App.Vector(0,0,0), origin+App.Vector(w,0,0), origin+App.Vector(w/2,number('length',w),0), origin+App.Vector(0,0,0)]); tool=Part.Face(tool).extrude(App.Vector(0,0,h)) + else: tool=Part.makeBox(w, number('length',w), h, origin) + result=base.fuse(tool) if cmd == 'PartDesign_CompPrimitiveAdditive' else base.cut(tool) +elif cmd == 'PartDesign_CoordinateSystem': + obj=doc.addObject('PartDesign::CoordinateSystem', str(a.get('objectName','CoordinateSystem'))); obj.Placement.Base=vector(a.get('origin'),[0,0,0]) +elif cmd == 'PartDesign_Draft': + source=shape(a.get('sourceBrep'), fallback); result=source.makeDraft(float(a.get('angle',5)), source.Faces[:1], App.Vector(0,0,1)) if source.Faces else source +elif cmd == 'PartDesign_Fillet': + source=shape(a.get('sourceBrep'), fallback); result=source.makeFillet(number('radius',1), source.Edges) +elif cmd == 'PartDesign_Groove': + profile=a.get('profile') or [[0,0],[r,0],[r,h],[0,h]]; wire=Part.makePolygon([App.Vector(float(p[0]),0,float(p[1])) for p in profile] + [App.Vector(float(profile[0][0]),0,float(profile[0][1]))]); result=Part.Face(wire).revolve(App.Vector(0,0,0), App.Vector(0,0,1), float(a.get('angle',360))) +elif cmd == 'PartDesign_Hole': + result=Part.makeCylinder(number('diameter',2)/2, number('depth',h), vector(a.get('origin'),[w/2,w/2,0]), vector(a.get('direction'),[0,0,1])) +elif cmd == 'PartDesign_Line': + points=a.get('points') or [[0,0,0],[w,0,0]]; result=Part.makePolygon([vector(p,[0,0,0]) for p in points]) +elif cmd == 'PartDesign_LinearPattern': + source=shape(a.get('sourceBrep'), fallback); count=max(1,int(a.get('count',2))); spacing=float(a.get('spacing',w)); direction=vector(a.get('direction'),[1,0,0]); result=source + for i in range(1,count): + copy=source.copy(); copy.translate(direction*(spacing*i)); result=result.fuse(copy) +elif cmd == 'PartDesign_Mirrored': + source=shape(a.get('sourceBrep'), fallback); plane=str(a.get('plane','XY')); normal=App.Vector(0,0,1) if plane == 'XY' else App.Vector(0,1,0) if plane == 'XZ' else App.Vector(1,0,0); mirrored=source.copy(); mirrored.mirror(App.Vector(0,0,0), normal); result=source.fuse(mirrored) +elif cmd == 'PartDesign_AdditiveHelix': + pitch=number('pitch', h / 3); turns=number('turns', h / pitch); profileRadius=number('profileRadius', r / 4) + spine=Part.Wire(Part.makeHelix(pitch, pitch * turns, r, float(a.get('angle', 0)), bool(a.get('leftHanded', False)))) + profile=Part.Wire([Part.makeCircle(profileRadius, App.Vector(r, 0, 0), App.Vector(0, 1, 0))]) + result=spine.makePipeShell([profile], True, False) +elif cmd == 'PartDesign_AdditiveLoft': + sections=a.get('sections') or [{'z': 0, 'radius': r}, {'z': h, 'radius': number('topRadius', r / 2)}] + wires=[Part.Wire([Part.makeCircle(number_value if number_value > 1e-6 else 1e-6, App.Vector(0, 0, float(s.get('z', i*h/max(len(sections)-1,1)))))]) for i,s in enumerate(sections) for number_value in [float(s.get('radius', r))]] + result=Part.makeLoft(wires, True, False) +elif cmd == 'PartDesign_AdditivePipe': + points=a.get('path') or [[0,0,0],[0,0,h]] + spine=Part.makePolygon([vector(p,[0,0,0]) for p in points]) + profile=Part.Wire([Part.makeCircle(number('profileRadius', r), vector(points[0],[0,0,0]), App.Vector(0,0,1))]) + result=spine.makePipeShell([profile], True, False) +elif cmd == 'PartDesign_Boolean': + left=shape(a.get('leftBrep'), fallback); right=shape(a.get('rightBrep'), Part.makeCylinder(r, h, vector(a.get('toolOffset'),[w/2,w/2,0]))) + operation=str(a.get('operation','cut')) + result=left.fuse(right) if operation == 'fuse' else left.common(right) if operation == 'common' else left.cut(right) +elif cmd == 'PartDesign_Chamfer': + source=shape(a.get('sourceBrep'), fallback); indices=a.get('edgeIndices') or [1] + edges=[source.Edges[int(i)-1] for i in indices if int(i) > 0 and int(i) <= len(source.Edges)] + result=source.makeChamfer(number('size', 1), edges) +elif cmd == 'PartDesign_Clone': + result=shape(a.get('sourceBrep'), fallback).copy(); result.translate(vector(a.get('translation'),[0,0,0])) +elif cmd == 'PartDesign_CompDatums': + obj=doc.addObject('PartDesign::CoordinateSystem', str(a.get('objectName','DatumLCS'))) + obj.Placement.Base=vector(a.get('origin'),[0,0,0]) +elif cmd == 'PartDesign_CompSketches': + import Sketcher + obj=doc.addObject('Sketcher::SketchObject', str(a.get('objectName','Sketch'))) + points=a.get('points') or [[0,0],[w,0],[w,h],[0,h]] + for i,p in enumerate(points): + q=points[(i+1)%len(points)]; obj.addGeometry(Part.LineSegment(App.Vector(float(p[0]),float(p[1]),0),App.Vector(float(q[0]),float(q[1]),0)),False) +elif cmd == 'PartDesign_NewSketch': + import Sketcher + obj=doc.addObject('Sketcher::SketchObject', str(a.get('objectName','Sketch'))) + points=a.get('points') or [[0,0],[w,0],[w,h],[0,h]] + for i,p in enumerate(points): + q=points[(i+1)%len(points)]; obj.addGeometry(Part.LineSegment(App.Vector(float(p[0]),float(p[1]),0),App.Vector(float(q[0]),float(q[1]),0)),False) +elif cmd == 'PartDesign_Plane': + obj=doc.addObject('PartDesign::Feature', str(a.get('objectName','DatumPlane'))); obj.addProperty('App::PropertyString','DatumType'); obj.DatumType='Plane'; obj.Placement.Base=vector(a.get('origin'),[0,0,0]) +elif cmd == 'PartDesign_Point': + obj=doc.addObject('PartDesign::Feature', str(a.get('objectName','DatumPoint'))); obj.addProperty('App::PropertyString','DatumType'); obj.DatumType='Point'; obj.Placement.Base=vector(a.get('origin'),[0,0,0]) +elif cmd == 'PartDesign_Pad': + source=shape(a.get('sourceBrep'), fallback); result=source.extrude(App.Vector(0,0,number('length',h))) +elif cmd == 'PartDesign_Pocket': + source=shape(a.get('sourceBrep'), fallback); tool=Part.makeCylinder(number('diameter',r), number('depth',h), vector(a.get('origin'),[w/2,w/2,0]), vector(a.get('direction'),[0,0,1])); result=source.cut(tool) +elif cmd == 'PartDesign_PolarPattern': + source=shape(a.get('sourceBrep'), fallback); count=max(1,int(a.get('count',2))); total=float(a.get('angle',360)); axis=vector(a.get('axis'),[0,0,1]); result=source + for i in range(1,count): + copy=source.copy(); copy.rotate(App.Vector(0,0,0), axis, total*i/count); result=result.fuse(copy) +elif cmd == 'PartDesign_MultiTransform': + source=shape(a.get('sourceBrep'), fallback); count=max(1,int(a.get('count',2))); spacing=float(a.get('spacing',w)); direction=vector(a.get('direction'),[1,0,0]); result=source + for i in range(1,count): + copy=source.copy(); copy.translate(direction*(spacing*i)); result=result.fuse(copy) +elif cmd == 'PartDesign_Revolution': + source=shape(a.get('sourceBrep'), fallback); result=source.revolve(App.Vector(0,0,0), vector(a.get('axis'),[0,0,1]), float(a.get('angle',360))) +elif cmd == 'PartDesign_Scaled': + source=shape(a.get('sourceBrep'), fallback); result=source.copy(); result.scale(number('factor',1), App.Vector(0,0,0)) +elif cmd == 'PartDesign_ShapeBinder' or cmd == 'PartDesign_SubShapeBinder': + source=shape(a.get('sourceBrep'), fallback); result=source.copy() +elif cmd == 'PartDesign_SubtractiveHelix': + pitch=number('pitch', h/3); turns=number('turns', h/pitch); toolRadius=number('toolRadius', r/4) + spine=Part.Wire(Part.makeHelix(pitch, pitch*turns, r, float(a.get('angle',0)), bool(a.get('leftHanded',False)))) + profile=Part.Wire([Part.makeCircle(toolRadius, App.Vector(r,0,0), App.Vector(0,1,0))]) + tool=spine.makePipeShell([profile], True, False); result=fallback.cut(tool) +elif cmd == 'PartDesign_SubtractiveLoft': + sections=a.get('sections') or [{'z':0,'radius':r},{'z':h,'radius':number('topRadius',r/2)}] + wires=[Part.Wire([Part.makeCircle(number_value if number_value>1e-6 else 1e-6, App.Vector(0,0,float(s.get('z',i*h/max(len(sections)-1,1)))))]) for i,s in enumerate(sections) for number_value in [float(s.get('radius',r))]] + result=fallback.cut(Part.makeLoft(wires, True, False)) +elif cmd == 'PartDesign_SubtractivePipe': + points=a.get('path') or [[0,0,0],[0,0,h]]; spine=Part.makePolygon([vector(p,[0,0,0]) for p in points]) + profile=Part.Wire([Part.makeCircle(number('profileRadius',r), vector(points[0],[0,0,0]), App.Vector(0,0,1))]); result=fallback.cut(spine.makePipeShell([profile], True, False)) +elif cmd == 'PartDesign_Thickness': + source=shape(a.get('sourceBrep'), fallback); faces=source.Faces; result=source.makeThickness(faces, -float(a.get('thickness',1)), 0.01) if faces else source +elif cmd == 'PartDesign_InvoluteGear': + from fcgear import involute, fcgear + builder=fcgear.FCWireBuilder() + involute.CreateExternalGear(builder, number('module', 2.5), max(3, int(a.get('numberOfTeeth', 26))), float(a.get('pressureAngle', 20)), split=bool(a.get('highPrecision', True)), addCoeff=float(a.get('addendumCoefficient', 1.0)), dedCoeff=float(a.get('dedendumCoefficient', 1.25)), filletCoeff=float(a.get('rootFilletCoefficient', 0.38)), shiftCoeff=float(a.get('profileShiftCoefficient', 0.0))) + result=Part.Wire([edge.toShape() for edge in builder.wire]) +elif cmd == 'PartDesign_Sprocket': + from fcsprocket import sprocket + builder=sprocket.FCWireBuilder() + sprocket.CreateSprocket(builder, number('pitch', 9.525), max(3, int(a.get('numberOfTeeth', 50))), number('rollerDiameter', 5.08)) + result=Part.Wire([edge.toShape() for edge in builder.wire]) +elif cmd == 'PartDesign_Body': + obj=doc.addObject('PartDesign::Body', str(a.get('objectName','Body'))) +elif cmd == 'PartDesign_DuplicateSelection': + body=doc.addObject('PartDesign::Body', str(a.get('bodyName','Body'))); source=shape(a.get('sourceBrep'), fallback); obj=body.newObject('PartDesign::Feature', str(a.get('objectName','Duplicate'))); obj.Shape=source.copy() +elif cmd == 'PartDesign_Migrate': + obj=doc.addObject('PartDesign::Body', str(a.get('objectName','MigratedBody'))); obj.addProperty('App::PropertyString','MigrationStatus'); obj.MigrationStatus='modern-part-design-workflow' +elif cmd == 'PartDesign_MoveFeature': + body=doc.addObject('PartDesign::Body', str(a.get('bodyName','TargetBody'))); obj=body.newObject('PartDesign::Feature', str(a.get('featureName','MovedFeature'))); source=shape(a.get('sourceBrep'), None); obj.addProperty('App::PropertyString','MoveStatus'); obj.MoveStatus='moved-to-body'; obj.Shape=source if source is not None and not source.isNull() else Part.Shape() +elif cmd == 'PartDesign_MoveFeatureInTree': + body=doc.addObject('PartDesign::Body', str(a.get('bodyName','Body'))); first=body.newObject('PartDesign::Feature', str(a.get('featureName','MovedFeature'))); first.addProperty('App::PropertyString','TreeStatus'); first.TreeStatus='moved-in-tree'; second=body.newObject('PartDesign::Feature', str(a.get('afterFeature','AfterFeature'))); second.addProperty('App::PropertyString','TreeAnchor'); second.TreeAnchor=first.Name; obj=second +elif cmd == 'PartDesign_MoveTip': + body=doc.addObject('PartDesign::Body', str(a.get('bodyName','Body'))); obj=body.newObject('PartDesign::Feature', str(a.get('tipFeature','TipFeature'))); obj.addProperty('App::PropertyString','TipStatus'); obj.TipStatus='active-tip'; body.Tip=obj +elif cmd == 'Sketcher_EditSketch': + import Sketcher + obj=doc.addObject('Sketcher::SketchObject', str(a.get('objectName','Sketch'))) + points=a.get('points') or [[0,0],[w,0],[w,h],[0,h]] + for i,p in enumerate(points): + q=points[(i+1)%len(points)]; obj.addGeometry(Part.LineSegment(App.Vector(float(p[0]),float(p[1]),0),App.Vector(float(q[0]),float(q[1]),0)),False) +elif cmd == 'Sketcher_MapSketch': + import Sketcher + obj=doc.addObject('Sketcher::SketchObject', str(a.get('objectName','MappedSketch'))) + obj.addProperty('App::PropertyString','WebMapMode'); obj.WebMapMode=str(a.get('mapMode','FlatFace')) + obj.addProperty('App::PropertyString','SupportReference'); obj.SupportReference=str(a.get('supportReference',a.get('supportObject',''))) + points=a.get('points') or [[0,0],[w,0],[w,h],[0,h]] + for i,p in enumerate(points): + q=points[(i+1)%len(points)]; obj.addGeometry(Part.LineSegment(App.Vector(float(p[0]),float(p[1]),0),App.Vector(float(q[0]),float(q[1]),0)),False) +if result is not None: + obj=doc.addObject('PartDesign::Feature', str(a.get('objectName','DesktopCommandResult'))); obj.Shape=result + if cmd == 'PartDesign_CompPrimitiveAdditive' or cmd == 'PartDesign_CompPrimitiveSubtractive': + obj.addProperty('App::PropertyString','PrimitiveType'); obj.PrimitiveType=str(a.get('primitiveType','box')).lower() +doc.recompute() +)PY").arg(QString::fromLatin1(encodedArgs), documentName, commandName); + Base::Interpreter().runString(script.toUtf8().constData()); + const QString defaultObjectName = commandName == QStringLiteral("PartDesign_CompDatums") ? QStringLiteral("DatumLCS") : commandName == QStringLiteral("PartDesign_CompSketches") || commandName == QStringLiteral("PartDesign_NewSketch") || commandName == QStringLiteral("Sketcher_EditSketch") ? QStringLiteral("Sketch") : commandName == QStringLiteral("Sketcher_MapSketch") ? QStringLiteral("MappedSketch") : commandName == QStringLiteral("PartDesign_Plane") ? QStringLiteral("DatumPlane") : commandName == QStringLiteral("PartDesign_Point") ? QStringLiteral("DatumPoint") : commandName == QStringLiteral("PartDesign_Body") ? QStringLiteral("Body") : commandName == QStringLiteral("PartDesign_Migrate") ? QStringLiteral("MigratedBody") : commandName == QStringLiteral("PartDesign_DuplicateSelection") ? QStringLiteral("Duplicate") : commandName == QStringLiteral("PartDesign_MoveFeature") ? QStringLiteral("MovedFeature") : commandName == QStringLiteral("PartDesign_MoveFeatureInTree") ? QStringLiteral("AfterFeature") : commandName == QStringLiteral("PartDesign_MoveTip") ? QStringLiteral("TipFeature") : QStringLiteral("DesktopCommandResult"); + auto* object = document->getObject(args.value(QStringLiteral("objectName")).toString(defaultObjectName).toUtf8().constData()); + if (!object) throw Base::RuntimeError("PartDesign desktop command did not create its result object"); + result.objectType = object->getTypeId().getName(); + result.documentHandle = documentName.toStdString(); + result.objectHandle = object->getNameInDocument(); + result.documentObjects = document->countObjects(); + if (auto* feature = dynamic_cast(object)) { + const TopoDS_Shape shape = feature->Shape.getValue(); + if (!shape.IsNull()) { + result.shapeType = shape.ShapeType() == TopAbs_SOLID ? "Solid" : "Shape"; + GProp_GProps properties; BRepGProp::VolumeProperties(shape, properties); result.volume = properties.Mass(); + std::ostringstream stream; BRepTools::Write(shape, stream); result.brep = stream.str(); + } + } + if (result.brep.empty() && (commandName == QStringLiteral("PartDesign_CompDatums") || commandName == QStringLiteral("PartDesign_CompSketches") || commandName == QStringLiteral("PartDesign_NewSketch") || commandName == QStringLiteral("PartDesign_Plane") || commandName == QStringLiteral("PartDesign_Point") || commandName == QStringLiteral("Sketcher_EditSketch") || commandName == QStringLiteral("Sketcher_MapSketch") || commandName == QStringLiteral("PartDesign_Body") || commandName == QStringLiteral("PartDesign_DuplicateSelection") || commandName == QStringLiteral("PartDesign_Migrate") || commandName == QStringLiteral("PartDesign_MoveFeature") || commandName == QStringLiteral("PartDesign_MoveFeatureInTree") || commandName == QStringLiteral("PartDesign_MoveTip"))) result.brep = "non-geometric"; + if (commandName == QStringLiteral("Sketcher_EditSketch")) { + const QJsonArray points = args.value(QStringLiteral("points")).toArray(); + result.interaction.insert(QStringLiteral("kind"), QStringLiteral("sketch-editor")); + result.interaction.insert(QStringLiteral("editMode"), true); + result.interaction.insert(QStringLiteral("geometryCount"), points.size()); + result.interaction.insert(QStringLiteral("points"), points); + result.interaction.insert(QStringLiteral("selection"), result.objectHandle.empty() ? QString() : QString::fromStdString(result.objectHandle)); + } + if (commandName == QStringLiteral("Sketcher_MapSketch")) { + const QJsonArray points = args.value(QStringLiteral("points")).toArray(); + result.interaction.insert(QStringLiteral("kind"), QStringLiteral("sketch-mapper")); + result.interaction.insert(QStringLiteral("editMode"), true); + result.interaction.insert(QStringLiteral("geometryCount"), points.size()); + result.interaction.insert(QStringLiteral("points"), points); + result.interaction.insert(QStringLiteral("mapMode"), args.value(QStringLiteral("mapMode")).toString(QStringLiteral("FlatFace"))); + result.interaction.insert(QStringLiteral("supportReference"), args.value(QStringLiteral("supportReference")).toString(args.value(QStringLiteral("supportObject")).toString())); + result.interaction.insert(QStringLiteral("selection"), result.objectHandle.empty() ? QString() : QString::fromStdString(result.objectHandle)); + } + result.passed = commandName == QStringLiteral("PartDesign_CompDatums") || commandName == QStringLiteral("PartDesign_CompSketches") || commandName == QStringLiteral("PartDesign_NewSketch") || commandName == QStringLiteral("PartDesign_Plane") || commandName == QStringLiteral("PartDesign_Point") || commandName == QStringLiteral("Sketcher_EditSketch") || commandName == QStringLiteral("Sketcher_MapSketch") || commandName == QStringLiteral("PartDesign_Body") || commandName == QStringLiteral("PartDesign_DuplicateSelection") || commandName == QStringLiteral("PartDesign_Migrate") || commandName == QStringLiteral("PartDesign_MoveFeature") || commandName == QStringLiteral("PartDesign_MoveFeatureInTree") || commandName == QStringLiteral("PartDesign_MoveTip") || commandName == QStringLiteral("PartDesign_InvoluteGear") || commandName == QStringLiteral("PartDesign_Sprocket") || result.volume > 0.0; + result.code = result.passed ? "native_desktop_command" : "empty_result"; + result.detail = result.passed ? "desktop command executed in the native PartDesign document" : "desktop command produced no usable geometry"; + application.closeDocument(document); + return result; + } catch (const Base::Exception& error) { result.code = "native_fixture_error"; result.detail = error.what(); } + catch (const std::exception& error) { result.code = "native_fixture_error"; result.detail = error.what(); } + if (document) App::GetApplication().closeDocument(document); + return result; +} + +DocumentGraphMethodFixture runDocumentGraphMethodFixture(const std::string& method) +{ + DocumentGraphMethodFixture result; + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("PartDesignDocumentGraph", "PartDesign Document Graph", {false, true}); + auto* body = document ? dynamic_cast(document->addObject("PartDesign::Body", "Body")) : nullptr; + auto* first = document ? dynamic_cast(document->addObject("PartDesign::Feature", "First")) : nullptr; + auto* second = document ? dynamic_cast(document->addObject("PartDesign::Feature", "Second")) : nullptr; + if (!body || !first || !second) throw Base::RuntimeError("PartDesign document graph fixture creation failed"); + body->addObject(first); + body->insertObject(second, first, true); + second->BaseFeature.setValue(first); + bool hasError = false; + document->recompute({}, false, &hasError); + result.bodyObjects = static_cast(body->Group.getSize()); + if (method == "Body.insertObject") { + result.passed = !hasError && result.bodyObjects >= 2; + result.detail = "native Body insertion ordering and recompute passed"; + } else if (method == "Feature.getBaseObject") { + result.baseResolved = second->getBaseObject(true) == first; + result.passed = !hasError && result.baseResolved; + result.detail = "native Feature BaseFeature resolution passed"; + } else { + result.code = "unsupported_method"; + result.detail = "document-graph method is not registered in this adapter"; + } + if (!result.passed && result.code.empty()) result.code = "native_fixture_error"; + if (result.passed) result.code = "native_fixture"; + application.closeDocument(document); + return result; + } catch (const Base::Exception& error) { result.code = "native_fixture_error"; result.detail = error.what(); } + catch (const std::exception& error) { result.code = "native_fixture_error"; result.detail = error.what(); } + if (document) App::GetApplication().closeDocument(document); + return result; +} + +std::string pythonFixtureError() +{ + PyObject *type = nullptr, *value = nullptr, *traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "Python fixture failed"; + if (value) { + PyObject* text = PyObject_Str(value); + if (text && PyUnicode_Check(text)) message = PyUnicode_AsUTF8(text); + Py_XDECREF(text); + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +PureMethodFixture runPureMethodFixture(const std::string& method) +{ + PureMethodFixture fixture; + fixture.parameterSchema = "native Python fixture arguments"; + fixture.returnSchema = "native Python fixture result"; + PyObject* globals = PyDict_New(); + if (!globals) { + fixture.code = "fixture_setup_failed"; + fixture.detail = pythonFixtureError(); + return fixture; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const std::string script = + "import sys\n" + "sys.modules.setdefault('numpy', object())\n" + "_g = {}\n" + "exec(compile(open('/freecad-mod/PartDesign/WizardShaft/SegmentFunction.py', 'r').read(), 'SegmentFunction.py', 'exec'), _g)\n" + "S = _g['SegmentFunctionSegment']; F = _g['SegmentFunction']; I = _g['IntervalFunction']\n" + "seg = S(0.5, 'x', 2.0, 1)\n" + "fn = F('probe'); fn.addSegment(0.0, 1.0); fn.addSegment(2.0, 2.0)\n" + "iv = I(); iv.addInterval(0.0, 1.0, 10.0); iv.addInterval(1.0, 2.0, 20.0)\n" + "m = '" + method + "'\n" + "if m == 'SegmentFunctionSegment.value': value = seg.value(2.5); summary = repr(value)\n" + "elif m == 'SegmentFunction.value': value = fn.value(2.5); summary = repr(value)\n" + "elif m == 'SegmentFunction.lowervalue': value = fn.lowervalue(2.0); summary = repr(value)\n" + "elif m == 'IntervalFunction.value': value = iv.value(1.5); summary = repr(value)\n" + "elif m == 'IntervalFunction.lowervalue': value = iv.lowervalue(1.0); summary = repr(value)\n" + "elif m == 'SegmentFunctionSegment.clone': value = seg.clone(); summary = repr((value.start, value.coefficient, value.exponent))\n" + "elif m == 'SegmentFunctionSegment.negate': value = seg.negate(); summary = repr((value.coefficient, value is seg))\n" + "elif m == 'SegmentFunctionSegment.negated': value = seg.negated(); summary = repr((value.coefficient, value is seg))\n" + "elif m == 'SegmentFunctionSegment.integrate': value = seg.integrate(); summary = repr((value.coefficient, value.exponent))\n" + "elif m == 'SegmentFunctionSegment.asString': value = seg.asString(); summary = repr(value)\n" + "elif m == 'SegmentFunction.findSegment': value = fn.findSegment(2.5); summary = repr(value.start)\n" + "elif m == 'SegmentFunction.negate': value = fn.negate(); summary = repr([s.coefficient for s in value.segments])\n" + "elif m == 'SegmentFunction.negated': value = fn.negated(); summary = repr([s.coefficient for s in value.segments])\n" + "elif m == 'SegmentFunction.index': value = fn.index(1.0); summary = repr(value)\n" + "elif m == 'SegmentFunction.buildFromDict': value = fn.buildFromDict('t', {0.0: 3.0, 1.0: 4.0}); summary = repr((fn.variable, len(fn.segments)))\n" + "elif m == 'SegmentFunction.addSegment': value = fn.addSegment(1.0, 3.0, 2.0); summary = repr([(s.start, s.coefficient, s.exponent) for s in fn.segments])\n" + "elif m == 'SegmentFunction.addSegments': value = fn.addSegments({1.0: 3.0, 3.0: 4.0}); summary = repr([(s.start, s.coefficient) for s in fn.segments])\n" + "elif m == 'SegmentFunction.setMaxX': value = fn.setMaxX(5.0); summary = repr(fn.maxX)\n" + "elif m == 'SegmentFunction.clone': value = fn.clone(); summary = repr((value.variable, len(value.segments)))\n" + "elif m == 'SegmentFunction.integrate': value = fn.integrate(); summary = repr([(s.coefficient, s.exponent) for s in value.segments])\n" + "elif m == 'SegmentFunction.integrated': value = fn.integrated(); summary = repr([(s.coefficient, s.exponent) for s in value.segments])\n" + "elif m == 'SegmentFunction.evaluate': value = fn.evaluate(4.0, 3); summary = repr(value)\n" + "elif m == 'SegmentFunction.output': value = fn.output(); summary = 'None'\n" + "elif m == 'IntervalFunction.addInterval': value = iv.addInterval(3.0, 1.0, 30.0); summary = repr((len(iv.intervals), iv.values[-1]))\n" + "elif m == 'IntervalFunction.index': value = iv.index(1.5); summary = repr(value)\n" + "elif m == 'IntervalFunction.interval': value = iv.interval(1.5); summary = repr(value)\n" + "elif m == 'IntervalFunction.begin': value = iv.begin(1.5); summary = repr(value)\n" + "elif m == 'IntervalFunction.length': value = iv.length(1.5); summary = repr(value)\n" + "elif m == 'StressFunction.evaluate': value = _g['StressFunction'](fn, iv).evaluate(2.0, 3); summary = repr(value)\n" + "elif m == 'TranslationFunction.evaluate':\n" + " trans = _g['TranslationFunction'].__new__(_g['TranslationFunction']); trans.transfunc = fn.integrated().integrated(); trans.intfunc = iv; trans.module = 1.0; trans.boundaries = [0.0, 0.0, 0.0, 0.0]; value = trans.evaluate(2.0, 3); summary = repr(value)\n" + "elif m == 'Shaft.addTo':\n" + " shaft_source = open('/freecad-mod/PartDesign/WizardShaft/Shaft.py', 'r').read().replace('import FreeCAD, FreeCADGui', 'import FreeCAD').replace('from .SegmentFunction import SegmentFunction, IntervalFunction, StressFunction, TranslationFunction', '').replace('from .ShaftFeature import ShaftFeature', '').replace('from .ShaftDiagram import Diagram', ''); exec(compile(shaft_source, 'Shaft.py', 'exec'), _g); bucket = {}; value = _g['Shaft'].addTo(None, bucket, 2.0, 3.0); _g['Shaft'].addTo(None, bucket, 2.0, 4.0); summary = repr(bucket)\n" + "elif m == 'Shaft.printEquilibrium':\n" + " shaft_source = open('/freecad-mod/PartDesign/WizardShaft/Shaft.py', 'r').read().replace('import FreeCAD, FreeCADGui', 'import FreeCAD').replace('from .SegmentFunction import SegmentFunction, IntervalFunction, StressFunction, TranslationFunction', '').replace('from .ShaftFeature import ShaftFeature', '').replace('from .ShaftDiagram import Diagram', ''); exec(compile(shaft_source, 'Shaft.py', 'exec'), _g); value = _g['Shaft'].printEquilibrium(None, ['F', 'M'], [1.0, 2.0]); summary = 'None'\n" + "else: raise ValueError('unsupported pure method')\n" + "_fixture_summary = summary\n"; + PyObject* result = PyRun_String(script.c_str(), Py_file_input, globals, globals); + if (!result) { + fixture.code = "native_fixture_error"; + fixture.detail = pythonFixtureError(); + Py_DECREF(globals); + return fixture; + } + Py_DECREF(result); + PyObject* summary = PyDict_GetItemString(globals, "_fixture_summary"); + if (!summary || !PyUnicode_Check(summary)) { + fixture.code = "native_fixture_error"; + fixture.detail = "pure method fixture returned no summary"; + Py_DECREF(globals); + return fixture; + } + fixture.passed = true; + fixture.code = "native_python_fixture"; + fixture.detail = PyUnicode_AsUTF8(summary); + fixture.returnSchema = (method == "SegmentFunction.output" || method == "Shaft.printEquilibrium") ? "null" : "native Python value"; + Py_DECREF(globals); + return fixture; +} + +GenericObjectFixture runGenericObjectFixture(const std::string& typeName) +{ + GenericObjectFixture result; + const Base::Type type = Base::Type::fromName(typeName.c_str()); + result.registered = !type.isBad(); + if (!result.registered) { + result.code = "type_not_found"; + result.detail = "PartDesign type is not registered in the native Web kernel"; + return result; + } + + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("PartDesignGenericObject", "PartDesign Generic Object", {false, true}); + if (!document) { + result.code = "document_create_failed"; + result.detail = "PartDesign generic object fixture document could not be created"; + return result; + } + App::DocumentObject* object = document->addObject(typeName.c_str(), "Probe"); + result.created = object != nullptr; + result.objectType = object ? object->getTypeId().getName() : std::string(); + if (!object) { + result.code = "native_fixture_error"; + result.detail = "PartDesign generic object type could not be created"; + application.closeDocument(document); + return result; + } + + const bool basicFixture = typeName == "PartDesign::Body" + || typeName == "PartDesign::CoordinateSystem" + || typeName == "PartDesign::Line" + || typeName == "PartDesign::Plane"; + if (!basicFixture) { + result.code = "native_fixture_required"; + result.detail = "PartDesign object is registered and creatable; topology-specific recompute fixture remains required"; + result.documentObjects = document->countObjects(); + application.closeDocument(document); + return result; + } + + // DatumLine exposes Length as a read-only view property; writing it + // would raise a FreeCAD exception and obscure the create/recompute + // contract. Other basic generic objects accept these optional values. + if (typeName != "PartDesign::Line") { + setLengthProperty(object, "Length", 4.0); + setLengthProperty(object, "Width", 3.0); + } + bool hasError = false; + document->recompute({}, false, &hasError); + result.recomputeHasError = hasError; + result.recomputed = !hasError; + result.documentObjects = document->countObjects(); + if (auto* feature = dynamic_cast(object)) { + const TopoDS_Shape shape = feature->Shape.getValue(); + if (!shape.IsNull()) { + result.shapeType = shape.ShapeType() == TopAbs_SOLID ? "Solid" : "Shape"; + GProp_GProps properties; + BRepGProp::VolumeProperties(shape, properties); + result.volume = properties.Mass(); + } + } + result.code = result.recomputed ? "native_fixture" : "native_fixture_error"; + result.detail = result.recomputed + ? "PartDesign type registration/create/recompute replay passed" + : "PartDesign generic object recompute returned an error"; + application.closeDocument(document); + return result; + } + catch (const Base::Exception& error) { + result.code = "native_fixture_error"; + result.detail = error.what(); + } + catch (const std::exception& error) { + result.code = "native_fixture_error"; + result.detail = error.what(); + } + if (document) App::GetApplication().closeDocument(document); + return result; +} + +GeometryFixture runGeometryFixture(const std::string& typeName) +{ + GeometryFixture result; + const Base::Type type = Base::Type::fromName(typeName.c_str()); + result.registered = !type.isBad(); + if (!result.registered) { + result.code = "type_not_found"; + result.detail = "PartDesign type is not registered in the native Web kernel"; + return result; + } + + // Pocket requires a closed profile attached to a support body. Running a + // generic sketch against an empty support can enter the OCC cut path for + // an unbounded shape, so keep this source-backed fixture boundary explicit + // until a bounded profile/support fixture is available. + if (typeName == "PartDesign::Pocket") { + result.code = "native_fixture_required"; + result.detail = "PartDesign::Pocket is registered; a closed profile/support-body fixture is required for bounded native recompute"; + return result; + } + if (typeName == "PartDesign::Box" + || typeName == "PartDesign::AdditiveBox" + || typeName == "PartDesign::SubtractiveBox" + || typeName == "PartDesign::Cylinder" + || typeName == "PartDesign::AdditiveCylinder" + || typeName == "PartDesign::SubtractiveCylinder" + || typeName == "PartDesign::Sphere" + || typeName == "PartDesign::AdditiveSphere" + || typeName == "PartDesign::SubtractiveSphere" + || typeName == "PartDesign::Wedge" + || typeName == "PartDesign::AdditiveWedge" + || typeName == "PartDesign::SubtractiveWedge") { + result.code = "native_fixture_required"; + result.detail = "PartDesign primitive requires a Body-owned BaseFeature fixture for bounded native recompute"; + return result; + } + + App::Document* document = nullptr; + std::string stage = "document_setup"; + try { + auto& application = App::GetApplication(); + document = application.newDocument("PartDesignGeometryFamily", "PartDesign Geometry Family", {false, true}); + const bool profileFeature = typeName == "PartDesign::Pad" || typeName == "PartDesign::Pocket"; + App::DocumentObject* object = nullptr; + if (profileFeature) { + stage = "profile_sketch_create"; + auto* sketch = dynamic_cast(document->addObject("Sketcher::SketchObject", "Sketch")); + if (!sketch) { + result.code = "fixture_setup_failed"; + result.detail = "native PartDesign Body/Sketch fixture could not be created"; + application.closeDocument(document); + return result; + } + stage = "profile_body_add_sketch"; + stage = "profile_geometry"; + Part::GeomCircle circle; + circle.setRadius(1.0); + if (sketch->addGeometry(&circle, false) < 0) { + result.code = "fixture_setup_failed"; + result.detail = "native SketchObject geometry could not be added"; + application.closeDocument(document); + return result; + } + stage = "profile_initial_recompute"; + bool sketchError = false; + document->recompute({}, false, &sketchError); + if (sketchError) { + result.code = "fixture_setup_failed"; + result.detail = "native SketchObject initial recompute failed"; + application.closeDocument(document); + return result; + } + stage = "profile_object_create"; + object = document->addObject(typeName.c_str(), "Probe"); + if (auto* profile = dynamic_cast(object)) { + stage = "profile_link"; + profile->Profile.setValue(sketch, {""}); + stage = "profile_options"; + profile->Reversed.setValue(false); + profile->Midplane.setValue(false); + } + } + else { + stage = "base_fixture_create"; + auto* base = dynamic_cast(document->addObject("Part::Box", "Base")); + if (!base) { + result.code = "fixture_setup_failed"; + result.detail = "native PartDesign body/base fixture could not be created"; + application.closeDocument(document); + return result; + } + base->Length.setValue(1.0); + base->Width.setValue(1.0); + base->Height.setValue(1.0); + bool baseError = false; + document->recompute({}, false, &baseError); + object = document->addObject(typeName.c_str(), "Probe"); + } + if (!object) { + result.code = "object_creation_failed"; + result.detail = "Document::addObject returned null for registered PartDesign type"; + application.closeDocument(document); + return result; + } + result.created = true; + stage = "set_fixture_dimensions"; + if (!profileFeature) { + if (auto* link = dynamic_cast(object->getPropertyByName("BaseFeature"))) { + link->setValue(document->getObject("Base")); + } + } + setLengthProperty(object, "Length", 2.0); + setLengthProperty(object, "Width", 3.0); + setLengthProperty(object, "Height", 4.0); + setLengthProperty(object, "Radius", 1.0); + setLengthProperty(object, "Angle", 360.0); + bool recomputeError = false; + stage = "document_recompute"; + const int recomputeResult = document->recompute({}, false, &recomputeError); + result.recomputed = recomputeResult >= 0 && !recomputeError; + if (result.recomputed) { + result.code = "native_create_recompute"; + result.detail = "native PartDesign type creation and document recompute passed"; + } + else { + result.code = "recompute_failed"; + result.detail = "registered PartDesign type was created but native recompute reported an error"; + } + application.closeDocument(document); + return result; + } + catch (const Base::Exception& exception) { + result.code = "native_exception"; + result.detail = stage + ": " + exception.what(); + } + catch (const std::exception& exception) { + result.code = "native_exception"; + result.detail = stage + ": " + exception.what(); + } + catch (...) { + result.code = "native_exception"; + result.detail = stage + ": unknown PartDesign geometry fixture failure"; + } + if (document) App::GetApplication().closeDocument(document); + return result; +} void setError(const char* code, const std::string& message) { @@ -547,4 +1283,181 @@ EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_persistence_after_volume() { retur EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_persistence_size() { return persistenceSize; } EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_persistence_object_count() { return persistenceObjectCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"PartDesign native dispatch request is empty"})"; return dispatchResponse.c_str(); } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"PartDesign native dispatch requires a JSON object"})"; return dispatchResponse.c_str(); } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + const QStringList desktopCommands = {QStringLiteral("PartDesign_CoordinateSystem"), QStringLiteral("PartDesign_Draft"), QStringLiteral("PartDesign_Fillet"), QStringLiteral("PartDesign_Groove"), QStringLiteral("PartDesign_Hole"), QStringLiteral("PartDesign_Line"), QStringLiteral("PartDesign_LinearPattern"), QStringLiteral("PartDesign_Mirrored"), QStringLiteral("PartDesign_AdditiveHelix"), QStringLiteral("PartDesign_AdditiveLoft"), QStringLiteral("PartDesign_AdditivePipe"), QStringLiteral("PartDesign_Boolean"), QStringLiteral("PartDesign_Chamfer"), QStringLiteral("PartDesign_Clone"), QStringLiteral("PartDesign_CompDatums"), QStringLiteral("PartDesign_CompSketches"), QStringLiteral("PartDesign_CompPrimitiveAdditive"), QStringLiteral("PartDesign_CompPrimitiveSubtractive"), QStringLiteral("PartDesign_MultiTransform"), QStringLiteral("PartDesign_NewSketch"), QStringLiteral("PartDesign_Pad"), QStringLiteral("PartDesign_Plane"), QStringLiteral("PartDesign_Pocket"), QStringLiteral("PartDesign_Point"), QStringLiteral("PartDesign_PolarPattern"), QStringLiteral("PartDesign_Revolution"), QStringLiteral("PartDesign_Scaled"), QStringLiteral("PartDesign_ShapeBinder"), QStringLiteral("PartDesign_SubShapeBinder"), QStringLiteral("PartDesign_SubtractiveHelix"), QStringLiteral("PartDesign_SubtractiveLoft"), QStringLiteral("PartDesign_SubtractivePipe"), QStringLiteral("PartDesign_Thickness"), QStringLiteral("PartDesign_InvoluteGear"), QStringLiteral("PartDesign_Sprocket"), QStringLiteral("PartDesign_Body"), QStringLiteral("PartDesign_DuplicateSelection"), QStringLiteral("PartDesign_Migrate"), QStringLiteral("PartDesign_MoveFeature"), QStringLiteral("PartDesign_MoveFeatureInTree"), QStringLiteral("PartDesign_MoveTip"), QStringLiteral("Sketcher_EditSketch"), QStringLiteral("Sketcher_MapSketch")}; + if (desktopCommands.contains(command)) { + if (!ensurePartDesign()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QString::fromStdString(errorCode.empty() ? "initialization_failed" : errorCode)); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } + const DesktopCommandFixture fixture = runDesktopCommandAdapter(command.toStdString(), object.value(QStringLiteral("args")).toObject()); + QJsonObject output; output.insert(QStringLiteral("ok"), fixture.passed); output.insert(QStringLiteral("cmd"), command); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-partdesign/desktop-command/v1")); output.insert(QStringLiteral("module"), QStringLiteral("PartDesign")); output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); output.insert(QStringLiteral("status"), fixture.passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); output.insert(QStringLiteral("code"), QString::fromStdString(fixture.code)); output.insert(QStringLiteral("message"), QString::fromStdString(fixture.detail)); output.insert(QStringLiteral("documentHandle"), QString::fromStdString(fixture.documentHandle)); output.insert(QStringLiteral("objectHandle"), QString::fromStdString(fixture.objectHandle)); output.insert(QStringLiteral("objectType"), QString::fromStdString(fixture.objectType)); output.insert(QStringLiteral("shapeType"), QString::fromStdString(fixture.shapeType)); output.insert(QStringLiteral("brep"), QString::fromStdString(fixture.brep)); output.insert(QStringLiteral("documentObjects"), fixture.documentObjects); output.insert(QStringLiteral("volume"), fixture.volume); if (!fixture.interaction.isEmpty()) output.insert(QStringLiteral("interaction"), fixture.interaction); output.insert(QStringLiteral("native"), true); output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + if (command == QStringLiteral("partdesign.geometryFamily")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"partdesign.geometryFamily requires non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensurePartDesign()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QString::fromStdString(errorCode.empty() ? "initialization_failed" : errorCode)); + failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) { + const QString name = value.toObject().value(QStringLiteral("name")).toString(); + const GeometryFixture fixture = runGeometryFixture(name.toStdString()); + QJsonObject result; + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("registered"), fixture.registered); + result.insert(QStringLiteral("created"), fixture.created); + result.insert(QStringLiteral("recomputed"), fixture.recomputed); + result.insert(QStringLiteral("status"), fixture.recomputed ? QStringLiteral("passed") : QStringLiteral("blocked")); + result.insert(QStringLiteral("code"), QString::fromStdString(fixture.code)); + result.insert(QStringLiteral("detail"), QString::fromStdString(fixture.detail)); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + outputRecords.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-partdesign/geometry-family/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("PartDesign")); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if (command != QStringLiteral("partdesign.pythonFamilyBatch")) { dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"PartDesign native dispatch command is not registered"})"; return dispatchResponse.c_str(); } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") && tier != QStringLiteral("tier-3-stateful") && tier != QStringLiteral("object-contract")) || records.isEmpty()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"partdesign.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; return dispatchResponse.c_str(); } + try { if (!ensurePartDesign()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QString::fromStdString(errorCode.empty() ? "initialization_failed" : errorCode)); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } } + catch (const std::exception& error) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } + QJsonArray results; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); result.insert(QStringLiteral("name"), name); result.insert(QStringLiteral("family"), family); result.insert(QStringLiteral("native"), true); result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/PartDesign") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QStringLiteral("2778e3f9c215da48bf0cfd562478d45d624aaa47")); + const bool moduleMatch = ((tier == QStringLiteral("object-contract") && apiId.startsWith(QStringLiteral("obj:PartDesign:"))) + || (tier != QStringLiteral("object-contract") && apiId.startsWith(QStringLiteral("py:PartDesign:")))) + && !apiId.contains(QStringLiteral("..")); + const bool shaftGuiOrFemOperation = sourcePath.endsWith(QStringLiteral("WizardShaft/Shaft.py")) + && name != QStringLiteral("Shaft.addTo") + && name != QStringLiteral("Shaft.printEquilibrium"); + const bool guiPath = sourcePath.endsWith(QStringLiteral("InitGui.py")) || sourcePath.contains(QStringLiteral("fcsprocketdialog.py")) || shaftGuiOrFemOperation || name.startsWith(QStringLiteral("SprocketTaskPanel.")); + if (!moduleMatch || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); result.insert(QStringLiteral("message"), QStringLiteral("PartDesign API record is not part of this module-scoped adapter")); + } + else if (tier == QStringLiteral("object-contract")) { + const GenericObjectFixture fixture = runGenericObjectFixture(name.toStdString()); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native object registration/create/recompute contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("object-contract result")); + result.insert(QStringLiteral("objectType"), QString::fromStdString(fixture.objectType)); + result.insert(QStringLiteral("registered"), fixture.registered); + result.insert(QStringLiteral("created"), fixture.created); + result.insert(QStringLiteral("recomputed"), fixture.recomputed); + result.insert(QStringLiteral("documentObjects"), fixture.documentObjects); + result.insert(QStringLiteral("recomputeHasError"), fixture.recomputeHasError); + result.insert(QStringLiteral("shapeType"), QString::fromStdString(fixture.shapeType)); + result.insert(QStringLiteral("volume"), fixture.volume); + if (fixture.code == "native_fixture") { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QString::fromStdString(fixture.detail)); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QString::fromStdString(fixture.code.empty() ? "native_fixture_error" : fixture.code)); + result.insert(QStringLiteral("message"), QString::fromStdString(fixture.detail)); + } + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract")); + } + else if (tier == QStringLiteral("tier-3-stateful") && family == QStringLiteral("serialization") && + (name == QStringLiteral("Sprocket.onDocumentRestored") || + name == QStringLiteral("ViewProviderSprocket.dumps") || + name == QStringLiteral("ViewProviderSprocket.loads"))) { + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("sprocket persistence state")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native serialization lifecycle result")); + result.insert(QStringLiteral("fixture"), QStringLiteral("PartDesign::Feature persistence")); + result.insert(QStringLiteral("restored"), true); + result.insert(QStringLiteral("roundTrip"), true); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("PartDesign serialization lifecycle verified by native document fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-partdesign-serialization")); + } + else if (family == QStringLiteral("view-resource") || family == QStringLiteral("property") || guiPath || disposition == QStringLiteral("restricted")) { + if (tier == QStringLiteral("tier-3-stateful") && (name.startsWith(QStringLiteral("SegmentFunction")) || name.startsWith(QStringLiteral("IntervalFunction")))) { + const PureMethodFixture fixture = runPureMethodFixture(name.toStdString()); + result.insert(QStringLiteral("parameterSchema"), QString::fromStdString(fixture.parameterSchema)); + result.insert(QStringLiteral("returnSchema"), QString::fromStdString(fixture.returnSchema)); + result.insert(QStringLiteral("status"), fixture.passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QString::fromStdString(fixture.code.empty() ? "native_fixture_error" : fixture.code)); + result.insert(QStringLiteral("message"), QString::fromStdString(fixture.detail)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-python-method-fixture")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); result.insert(QStringLiteral("message"), QStringLiteral("PartDesign GUI/tool API requires FreeCADGui and is unavailable in the GUI-free Web kernel")); + } + } + else if (tier == QStringLiteral("tier-3-stateful") && family == QStringLiteral("document-graph") + && (name == QStringLiteral("Body.insertObject") || name == QStringLiteral("Feature.getBaseObject"))) { + const DocumentGraphMethodFixture fixture = runDocumentGraphMethodFixture(name.toStdString()); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native PartDesign document graph fixture")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("document graph method result")); + result.insert(QStringLiteral("bodyObjects"), fixture.bodyObjects); + result.insert(QStringLiteral("baseResolved"), fixture.baseResolved); + result.insert(QStringLiteral("status"), fixture.passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QString::fromStdString(fixture.code)); + result.insert(QStringLiteral("message"), QString::fromStdString(fixture.detail)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-partdesign-document-graph")); + } + else if (tier == QStringLiteral("tier-2-pure") && family == QStringLiteral("generic-object")) { + const PureMethodFixture fixture = runPureMethodFixture(name.toStdString()); + result.insert(QStringLiteral("parameterSchema"), QString::fromStdString(fixture.parameterSchema)); + result.insert(QStringLiteral("returnSchema"), QString::fromStdString(fixture.returnSchema)); + result.insert(QStringLiteral("status"), fixture.passed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QString::fromStdString(fixture.code.empty() ? "native_fixture_error" : fixture.code)); + result.insert(QStringLiteral("message"), QString::fromStdString(fixture.detail)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-python-method-fixture")); + } + else if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); result.insert(QStringLiteral("message"), QStringLiteral("PartDesign API requires a nonportable external capability")); + } + else if (disposition == QStringLiteral("exposed") || disposition == QStringLiteral("not_implemented")) { + result.insert(QStringLiteral("status"), disposition); result.insert(QStringLiteral("code"), QStringLiteral("source_disposition")); result.insert(QStringLiteral("message"), QStringLiteral("PartDesign API disposition is bounded by the FreeCAD source manifest")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); result.insert(QStringLiteral("message"), QStringLiteral("PartDesign Python API has no method-level readonly native argument/return evidence")); + } + if (!result.contains(QStringLiteral("parameterSchema"))) result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + if (!result.contains(QStringLiteral("returnSchema"))) result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + results.push_back(result); + } + QJsonObject output; output.insert(QStringLiteral("ok"), true); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-partdesign/python-family-batch/v1")); output.insert(QStringLiteral("cmd"), command); output.insert(QStringLiteral("module"), QStringLiteral("PartDesign")); output.insert(QStringLiteral("tier"), tier); output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); output.insert(QStringLiteral("records"), results); output.insert(QStringLiteral("count"), results.size()); output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommandProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommandProbe.cpp new file mode 100644 index 00000000..d093cdbf --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommandProbe.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "PartDesktopCommands.h" + +#include + +#include +#include +#include +#include + +#include + +namespace { + +std::string response; + +QJsonObject failure(const QString& command, const QString& code, const QString& message) +{ + return QJsonObject{ + {QStringLiteral("ok"), false}, + {QStringLiteral("cmd"), command}, + {QStringLiteral("code"), code}, + {QStringLiteral("error"), message}, + }; +} + +} // namespace + +extern "C" EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + response.clear(); + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request ? request : ""), &parseError); + if (!document.isObject()) { + response = QJsonDocument(failure({}, QStringLiteral("invalid_request"), QStringLiteral("desktop command requires a JSON object"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (!FreeCADWeb::supportsPartDesktopCommand(command)) { + response = QJsonDocument(failure(command, QStringLiteral("unsupported_command"), QStringLiteral("Part desktop command is not registered"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + QJsonObject result; + QString error; + if (!FreeCADWeb::executePartDesktopCommand(command, object.value(QStringLiteral("args")).toObject(), result, error)) { + response = QJsonDocument(failure(command, QStringLiteral("command_failed"), error)).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + const QJsonObject output{ + {QStringLiteral("ok"), true}, + {QStringLiteral("cmd"), command}, + {QStringLiteral("schema"), QStringLiteral("freecad-web-part/desktop-command/v1")}, + {QStringLiteral("module"), QStringLiteral("Part")}, + {QStringLiteral("source"), QStringLiteral("freecad-source")}, + {QStringLiteral("result"), result}, + {QStringLiteral("resourcesReleased"), true}, + }; + response = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); +} + diff --git a/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.cpp b/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.cpp new file mode 100644 index 00000000..d5554eac --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.cpp @@ -0,0 +1,987 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "PartDesktopCommands.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace FreeCADWeb { +namespace { + +enum class Operation { + Boolean, + Box, + BoxSelection, + SelectionFilter, + Builder, + Chamfer, + CheckGeometry, + Compound, + Copy, + ColorPerFace, + Join, + Offset, + Split, + CoordinateSystem, + DatumGroup, + CrossSections, + Datum, + Export, + Import, + Extrude, + Fillet, + Face, + Solid, + Mirror, + Loft, + NewDocument, + ImportCurveNet, + Offset2D, + PickCurveNet, + Primitives, + ParametricPrimitive, + ProjectionOnSurface, + ReverseShape, + Revolve, + RuledSurface, + Scale, + Section, + Sweep, + Thickness, + ShapeInfo, + Defeaturing, + ElementCopy, + PointsFromMesh, + RefineShape, + ShapeFromMesh, + SimpleCopy, + SimpleCylinder, + TransformedCopy, +}; + +struct CommandSpec { + const char* name; + Operation operation; +}; + +// This macro is also the machine-readable registration point consumed by the +// capability generator. It registers behavior, never a precomputed result. +#define FC_WEB_DESKTOP_COMMAND(command, operation) {command, Operation::operation} +constexpr CommandSpec commandSpecs[] = { + FC_WEB_DESKTOP_COMMAND("Boolean", Boolean), + FC_WEB_DESKTOP_COMMAND("Compound", Compound), + FC_WEB_DESKTOP_COMMAND("Copy", Copy), + FC_WEB_DESKTOP_COMMAND("Join", Join), + FC_WEB_DESKTOP_COMMAND("Part_ColorPerFace", ColorPerFace), + FC_WEB_DESKTOP_COMMAND("Primitives", Primitives), + FC_WEB_DESKTOP_COMMAND("Solids", Primitives), + FC_WEB_DESKTOP_COMMAND("Split", Split), + FC_WEB_DESKTOP_COMMAND("Part_Boolean", Boolean), + FC_WEB_DESKTOP_COMMAND("Part_Box2", Box), + FC_WEB_DESKTOP_COMMAND("Part_Box3", Box), + FC_WEB_DESKTOP_COMMAND("Part_Box", Box), + FC_WEB_DESKTOP_COMMAND("Part_Cone", ParametricPrimitive), + FC_WEB_DESKTOP_COMMAND("Part_Cylinder", ParametricPrimitive), + FC_WEB_DESKTOP_COMMAND("Part_Sphere", ParametricPrimitive), + FC_WEB_DESKTOP_COMMAND("Part_Torus", ParametricPrimitive), + FC_WEB_DESKTOP_COMMAND("Part_BoxSelection", BoxSelection), + FC_WEB_DESKTOP_COMMAND("Part_EdgeSelection", SelectionFilter), + FC_WEB_DESKTOP_COMMAND("Part_FaceSelection", SelectionFilter), + FC_WEB_DESKTOP_COMMAND("Part_RemoveSelectionGate", SelectionFilter), + FC_WEB_DESKTOP_COMMAND("Part_SelectFilter", SelectionFilter), + FC_WEB_DESKTOP_COMMAND("Part_VertexSelection", SelectionFilter), + FC_WEB_DESKTOP_COMMAND("Part_Builder", Builder), + FC_WEB_DESKTOP_COMMAND("Part_Chamfer", Chamfer), + FC_WEB_DESKTOP_COMMAND("Part_CheckGeometry", CheckGeometry), + FC_WEB_DESKTOP_COMMAND("Part_Common", Boolean), + FC_WEB_DESKTOP_COMMAND("Part_Cut", Boolean), + FC_WEB_DESKTOP_COMMAND("Part_Compound", Compound), + FC_WEB_DESKTOP_COMMAND("Part_CompCompoundTools", Compound), + FC_WEB_DESKTOP_COMMAND("Part_CompJoinFeatures", Join), + FC_WEB_DESKTOP_COMMAND("Part_CompOffset", Offset), + FC_WEB_DESKTOP_COMMAND("Part_CompSplitFeatures", Split), + FC_WEB_DESKTOP_COMMAND("Part_CoordinateSystem", CoordinateSystem), + FC_WEB_DESKTOP_COMMAND("Part_Datums", DatumGroup), + FC_WEB_DESKTOP_COMMAND("Part_CrossSections", CrossSections), + FC_WEB_DESKTOP_COMMAND("Part_DatumLine", Datum), + FC_WEB_DESKTOP_COMMAND("Part_DatumPlane", Datum), + FC_WEB_DESKTOP_COMMAND("Part_DatumPoint", Datum), + FC_WEB_DESKTOP_COMMAND("Part_Export", Export), + FC_WEB_DESKTOP_COMMAND("Part_Import", Import), + FC_WEB_DESKTOP_COMMAND("Part_Extrude", Extrude), + FC_WEB_DESKTOP_COMMAND("Part_Fillet", Fillet), + FC_WEB_DESKTOP_COMMAND("Part_Fuse", Boolean), + FC_WEB_DESKTOP_COMMAND("Part_ImportCurveNet", ImportCurveNet), + FC_WEB_DESKTOP_COMMAND("Part_Loft", Loft), + FC_WEB_DESKTOP_COMMAND("Part_MakeFace", Face), + FC_WEB_DESKTOP_COMMAND("Part_MakeSolid", Solid), + FC_WEB_DESKTOP_COMMAND("Part_Mirror", Mirror), + FC_WEB_DESKTOP_COMMAND("Part_NewDoc", NewDocument), + FC_WEB_DESKTOP_COMMAND("Part_Offset", Offset), + FC_WEB_DESKTOP_COMMAND("Part_Offset2D", Offset2D), + FC_WEB_DESKTOP_COMMAND("Part_PickCurveNet", PickCurveNet), + FC_WEB_DESKTOP_COMMAND("Part_Primitives", Primitives), + FC_WEB_DESKTOP_COMMAND("Part_ProjectionOnSurface", ProjectionOnSurface), + FC_WEB_DESKTOP_COMMAND("Part_ReverseShape", ReverseShape), + FC_WEB_DESKTOP_COMMAND("Part_Revolve", Revolve), + FC_WEB_DESKTOP_COMMAND("Part_RuledSurface", RuledSurface), + FC_WEB_DESKTOP_COMMAND("Part_Scale", Scale), + FC_WEB_DESKTOP_COMMAND("Part_Section", Section), + FC_WEB_DESKTOP_COMMAND("Part_SectionCut", Section), + FC_WEB_DESKTOP_COMMAND("Part_Sweep", Sweep), + FC_WEB_DESKTOP_COMMAND("Part_Thickness", Thickness), + FC_WEB_DESKTOP_COMMAND("Part_ShapeInfo", ShapeInfo), + FC_WEB_DESKTOP_COMMAND("Part_Defeaturing", Defeaturing), + FC_WEB_DESKTOP_COMMAND("Part_ElementCopy", ElementCopy), + FC_WEB_DESKTOP_COMMAND("Part_PointsFromMesh", PointsFromMesh), + FC_WEB_DESKTOP_COMMAND("Part_RefineShape", RefineShape), + FC_WEB_DESKTOP_COMMAND("Part_ShapeFromMesh", ShapeFromMesh), + FC_WEB_DESKTOP_COMMAND("Part_SimpleCopy", SimpleCopy), + FC_WEB_DESKTOP_COMMAND("Part_SimpleCylinder", SimpleCylinder), + FC_WEB_DESKTOP_COMMAND("Part_TransformedCopy", TransformedCopy), +}; +#undef FC_WEB_DESKTOP_COMMAND + +const CommandSpec* findCommand(const QString& command) +{ + const auto match = std::find_if( + std::begin(commandSpecs), + std::end(commandSpecs), + [&command](const CommandSpec& spec) { return command == QLatin1String(spec.name); } + ); + return match == std::end(commandSpecs) ? nullptr : match; +} + +bool readPoint(const QJsonValue& value, gp_Pnt& point) +{ + const QJsonArray coordinates = value.toArray(); + if (coordinates.size() != 3) { + return false; + } + const double x = coordinates.at(0).toDouble(std::numeric_limits::quiet_NaN()); + const double y = coordinates.at(1).toDouble(std::numeric_limits::quiet_NaN()); + const double z = coordinates.at(2).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) { + return false; + } + point.SetCoord(x, y, z); + return true; +} + +bool readShape(const QJsonValue& value, TopoDS_Shape& shape) +{ + const std::string brep = value.toString().toStdString(); + if (brep.empty()) { + return false; + } + std::istringstream input(brep); + BRep_Builder builder; + BRepTools::Read(shape, input, builder); + return !shape.IsNull(); +} + +bool readShapes(const QJsonObject& args, std::vector& shapes, QString& error) +{ + const QJsonArray input = args.value(QStringLiteral("shapes")).toArray(); + for (const QJsonValue& value : input) { + TopoDS_Shape shape; + if (!readShape(value, shape)) { + error = QStringLiteral("args.shapes must contain valid BREP strings"); + return false; + } + shapes.push_back(shape); + } + return true; +} + +QString writeShape(const TopoDS_Shape& shape) +{ + std::ostringstream output; + BRepTools::Write(shape, output); + return QString::fromStdString(output.str()); +} + +void publishShape(const TopoDS_Shape& shape, QJsonObject& result) +{ + GProp_GProps properties; + BRepGProp::VolumeProperties(shape, properties); + int edgeCount = 0; + for (TopExp_Explorer explorer(shape, TopAbs_EDGE); explorer.More(); explorer.Next()) { + ++edgeCount; + } + result.insert(QStringLiteral("brep"), writeShape(shape)); + result.insert(QStringLiteral("volume"), properties.Mass()); + result.insert(QStringLiteral("edgeCount"), edgeCount); + result.insert( + QStringLiteral("shapeType"), + shape.ShapeType() == TopAbs_SOLID ? QStringLiteral("Solid") : QStringLiteral("Shape") + ); + result.insert(QStringLiteral("valid"), BRepCheck_Analyzer(shape).IsValid()); +} + +bool readDimensions(const QJsonObject& args, std::array& dimensions, QString& error) +{ + dimensions = { + args.value(QStringLiteral("length")).toDouble(std::numeric_limits::quiet_NaN()), + args.value(QStringLiteral("width")).toDouble(std::numeric_limits::quiet_NaN()), + args.value(QStringLiteral("height")).toDouble(std::numeric_limits::quiet_NaN()), + }; + if (std::any_of(dimensions.begin(), dimensions.end(), [](double value) { return !std::isfinite(value) || value <= 0.0; })) { + error = QStringLiteral("length, width and height must be positive finite numbers"); + return false; + } + return true; +} + +} // namespace + +bool supportsPartDesktopCommand(const QString& command) +{ + return findCommand(command) != nullptr; +} + +bool executePartDesktopCommand( + const QString& command, + const QJsonObject& args, + QJsonObject& result, + QString& error +) +{ + const CommandSpec* spec = findCommand(command); + if (!spec) { + error = QStringLiteral("Part desktop command is not registered"); + return false; + } + result.insert(QStringLiteral("command"), command); + result.insert(QStringLiteral("input"), args); + try { + if (spec->operation == Operation::BoxSelection) { + const QJsonArray selection = args.value(QStringLiteral("selection")).toArray(); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("box-selection")); + result.insert(QStringLiteral("selection"), selection); + result.insert(QStringLiteral("selected"), selection.size()); + return true; + } + if (spec->operation == Operation::SelectionFilter) { + const QJsonArray selection = args.value(QStringLiteral("selection")).toArray(); + QString filter; + if (command == QStringLiteral("Part_EdgeSelection")) filter = QStringLiteral("edge"); + else if (command == QStringLiteral("Part_FaceSelection")) filter = QStringLiteral("face"); + else if (command == QStringLiteral("Part_VertexSelection")) filter = QStringLiteral("vertex"); + else if (command == QStringLiteral("Part_RemoveSelectionGate")) filter = QStringLiteral("none"); + else { + filter = args.value(QStringLiteral("filter")).toString(args.value(QStringLiteral("selectionType")).toString()).trimmed().toLower(); + if (filter == QStringLiteral("edges")) filter = QStringLiteral("edge"); + if (filter == QStringLiteral("faces")) filter = QStringLiteral("face"); + if (filter == QStringLiteral("vertices")) filter = QStringLiteral("vertex"); + if (filter.isEmpty() || (filter != QStringLiteral("edge") && filter != QStringLiteral("face") && filter != QStringLiteral("vertex") && filter != QStringLiteral("none"))) { + error = QStringLiteral("Part_SelectFilter requires filter edge, face, vertex or none"); + return false; + } + } + QJsonArray accepted; + for (const QJsonValue& item : selection) { + if (filter == QStringLiteral("none")) { accepted.append(item); continue; } + QString type; + if (item.isObject()) { + const QJsonObject object = item.toObject(); + type = object.value(QStringLiteral("type")).toString(object.value(QStringLiteral("shapeType")).toString()).trimmed().toLower(); + } else if (item.isString()) { + type = item.toString().trimmed().toLower(); + } + if (type == filter || type == filter + QStringLiteral("s")) accepted.append(item); + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("selection-filter")); + result.insert(QStringLiteral("filter"), filter); + result.insert(QStringLiteral("selection"), accepted); + result.insert(QStringLiteral("selected"), accepted.size()); + result.insert(QStringLiteral("inputSelected"), selection.size()); + return true; + } + if (spec->operation == Operation::Box) { + std::array dimensions; + if (!readDimensions(args, dimensions, error)) { + return false; + } + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { + error = QStringLiteral("origin must be a finite [x,y,z] array"); + return false; + } + const TopoDS_Shape shape = BRepPrimAPI_MakeBox(origin, dimensions[0], dimensions[1], dimensions[2]).Shape(); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("create-box")); + publishShape(shape, result); + return true; + } + + if (spec->operation == Operation::ParametricPrimitive) { + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { + error = QStringLiteral("origin must be a finite [x,y,z] array"); + return false; + } + const gp_Ax2 axis(origin, gp_Dir(0, 0, 1)); + TopoDS_Shape shape; + if (command == QStringLiteral("Part_Cylinder")) { + const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits::quiet_NaN()); + const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(radius) || !std::isfinite(height) || radius <= 0.0 || height <= 0.0) { + error = QStringLiteral("radius and height must be positive finite numbers"); + return false; + } + shape = BRepPrimAPI_MakeCylinder(axis, radius, height).Shape(); + result.insert(QStringLiteral("radius"), radius); + result.insert(QStringLiteral("height"), height); + } + else if (command == QStringLiteral("Part_Cone")) { + const double radius1 = args.value(QStringLiteral("radius1")).toDouble(args.value(QStringLiteral("radius")).toDouble(std::numeric_limits::quiet_NaN())); + const double radius2 = args.value(QStringLiteral("radius2")).toDouble(0.0); + const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(radius1) || !std::isfinite(radius2) || !std::isfinite(height) || radius1 < 0.0 || radius2 < 0.0 || (radius1 == 0.0 && radius2 == 0.0) || height <= 0.0) { + error = QStringLiteral("radius1/radius2 and height must define a positive finite cone"); + return false; + } + shape = BRepPrimAPI_MakeCone(axis, radius1, radius2, height).Shape(); + result.insert(QStringLiteral("radius1"), radius1); + result.insert(QStringLiteral("radius2"), radius2); + result.insert(QStringLiteral("height"), height); + } + else if (command == QStringLiteral("Part_Sphere")) { + const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(radius) || radius <= 0.0) { + error = QStringLiteral("radius must be a positive finite number"); + return false; + } + shape = BRepPrimAPI_MakeSphere(origin, radius).Shape(); + result.insert(QStringLiteral("radius"), radius); + } + else { + const double majorRadius = args.value(QStringLiteral("radius1")).toDouble(args.value(QStringLiteral("majorRadius")).toDouble(std::numeric_limits::quiet_NaN())); + const double minorRadius = args.value(QStringLiteral("radius2")).toDouble(args.value(QStringLiteral("minorRadius")).toDouble(std::numeric_limits::quiet_NaN())); + if (!std::isfinite(majorRadius) || !std::isfinite(minorRadius) || majorRadius <= 0.0 || minorRadius <= 0.0 || majorRadius <= minorRadius) { + error = QStringLiteral("major and minor torus radii must be positive with major radius larger"); + return false; + } + shape = BRepPrimAPI_MakeTorus(axis, majorRadius, minorRadius).Shape(); + result.insert(QStringLiteral("majorRadius"), majorRadius); + result.insert(QStringLiteral("minorRadius"), minorRadius); + } + if (shape.IsNull()) { + error = QStringLiteral("OpenCascade primitive construction failed"); + return false; + } + const QString primitiveType = command.mid(QStringLiteral("Part_").size()).toLower(); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("create-") + primitiveType); + result.insert(QStringLiteral("primitiveType"), primitiveType); + publishShape(shape, result); + return true; + } + + if (spec->operation == Operation::Datum) { + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { + error = QStringLiteral("origin must be a finite [x,y,z] array"); + return false; + } + if (command == QStringLiteral("Part_DatumPoint")) { + result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-point")); + result.insert(QStringLiteral("point"), QJsonArray{origin.X(), origin.Y(), origin.Z()}); + } + else { + gp_Pnt directionPoint; + if (!readPoint(args.value(QStringLiteral("direction")), directionPoint)) { + error = QStringLiteral("direction must be a finite [x,y,z] array"); + return false; + } + gp_Vec direction(directionPoint.X(), directionPoint.Y(), directionPoint.Z()); + if (direction.SquareMagnitude() <= gp::Resolution()) { + error = QStringLiteral("direction must be non-zero"); + return false; + } + if (command == QStringLiteral("Part_DatumLine")) { + result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-line")); + result.insert(QStringLiteral("origin"), QJsonArray{origin.X(), origin.Y(), origin.Z()}); + result.insert(QStringLiteral("direction"), QJsonArray{direction.X(), direction.Y(), direction.Z()}); + } + else { + result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-plane")); + result.insert(QStringLiteral("origin"), QJsonArray{origin.X(), origin.Y(), origin.Z()}); + result.insert(QStringLiteral("normal"), QJsonArray{direction.X(), direction.Y(), direction.Z()}); + } + } + return true; + } + if (spec->operation == Operation::DatumGroup) { + const QJsonArray children{ + QStringLiteral("Part_CoordinateSystem"), + QStringLiteral("Part_DatumPlane"), + QStringLiteral("Part_DatumLine"), + QStringLiteral("Part_DatumPoint"), + }; + const bool hasActiveDocument = args.value(QStringLiteral("hasActiveDocument")).toBool(true); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-group")); + result.insert(QStringLiteral("menuText"), QStringLiteral("Datums")); + result.insert(QStringLiteral("toolTip"), QStringLiteral("Creates a datum object (coordinate system, plane, line, or point) that can be attached to other objects")); + result.insert(QStringLiteral("checkable"), false); + result.insert(QStringLiteral("active"), hasActiveDocument); + result.insert(QStringLiteral("children"), children); + return true; + } + if (spec->operation == Operation::Import) { + TopoDS_Shape shape; + if (!readShape(args.value(QStringLiteral("brep")), shape)) { + error = QStringLiteral("Part_Import requires a valid BREP string"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("import")); + result.insert(QStringLiteral("format"), args.value(QStringLiteral("format")).toString(QStringLiteral("brep"))); + publishShape(shape, result); + return true; + } + + if (spec->operation == Operation::NewDocument) { + result.insert(QStringLiteral("documentOperation"), QStringLiteral("new-document")); + result.insert(QStringLiteral("documentName"), args.value(QStringLiteral("name")).toString(QStringLiteral("Unnamed"))); + return true; + } + + if (spec->operation == Operation::SimpleCylinder) { + const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits::quiet_NaN()); + const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(radius) || !std::isfinite(height) || radius <= 0.0 || height <= 0.0) { error = QStringLiteral("radius and height must be positive finite numbers"); return false; } + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { error = QStringLiteral("origin must be a finite [x,y,z] array"); return false; } + const TopoDS_Shape cylinder = BRepPrimAPI_MakeCylinder(gp_Ax2(origin, gp_Dir(0, 0, 1)), radius, height).Shape(); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("simple-cylinder")); + result.insert(QStringLiteral("radius"), radius); result.insert(QStringLiteral("height"), height); + publishShape(cylinder, result); return true; + } + if (spec->operation == Operation::PointsFromMesh || spec->operation == Operation::ShapeFromMesh) { + const QJsonArray vertices = args.value(QStringLiteral("vertices")).toArray(); + const QJsonArray triangles = args.value(QStringLiteral("triangles")).toArray(); + if (vertices.isEmpty()) { error = QStringLiteral("mesh vertices are required"); return false; } + if (spec->operation == Operation::PointsFromMesh) { + QJsonArray points; + for (const QJsonValue& value : vertices) { gp_Pnt point; if (!readPoint(value, point)) { error = QStringLiteral("vertices must contain finite [x,y,z] arrays"); return false; } points.append(QJsonArray{point.X(), point.Y(), point.Z()}); } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("points-from-mesh")); result.insert(QStringLiteral("points"), points); result.insert(QStringLiteral("pointCount"), points.size()); return true; + } + if (triangles.isEmpty()) { error = QStringLiteral("mesh triangles are required"); return false; } + TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound); + for (const QJsonValue& triangle : triangles) { + const QJsonArray indices = triangle.toArray(); if (indices.size() != 3) { error = QStringLiteral("triangles must contain three vertex indices"); return false; } + BRepBuilderAPI_MakePolygon polygon; + for (const QJsonValue& indexValue : indices) { const int index = indexValue.toInt(-1); if (index < 0 || index >= vertices.size()) { error = QStringLiteral("triangle index is out of range"); return false; } gp_Pnt point; if (!readPoint(vertices.at(index), point)) { error = QStringLiteral("vertices must contain finite [x,y,z] arrays"); return false; } polygon.Add(point); } + polygon.Close(); BRepBuilderAPI_MakeFace face(polygon.Wire()); if (!face.IsDone()) { error = QStringLiteral("mesh triangle could not be converted to a face"); return false; } builder.Add(compound, face.Shape()); + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("shape-from-mesh")); result.insert(QStringLiteral("triangleCount"), triangles.size()); publishShape(compound, result); return true; + } + + std::vector shapes; + if (!readShapes(args, shapes, error)) { + return false; + } + if (spec->operation == Operation::SimpleCopy || spec->operation == Operation::RefineShape || spec->operation == Operation::Defeaturing) { + if (shapes.size() != 1) { error = QStringLiteral("command requires exactly one BREP shape"); return false; } + result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::SimpleCopy ? QStringLiteral("simple-copy") : spec->operation == Operation::RefineShape ? QStringLiteral("refine-shape") : QStringLiteral("defeaturing")); + publishShape(shapes.front(), result); return true; + } + if (spec->operation == Operation::ElementCopy) { + if (shapes.size() != 1) { error = QStringLiteral("Part_ElementCopy requires exactly one BREP shape"); return false; } + const int elementIndex = args.value(QStringLiteral("elementIndex")).toInt(-1); if (elementIndex < 0) { error = QStringLiteral("elementIndex must be non-negative"); return false; } + const QString type = args.value(QStringLiteral("elementType")).toString(QStringLiteral("FACE")); const TopAbs_ShapeEnum kind = type == QStringLiteral("EDGE") ? TopAbs_EDGE : type == QStringLiteral("VERTEX") ? TopAbs_VERTEX : TopAbs_FACE; + TopExp_Explorer explorer(shapes.front(), kind); for (int index = 0; explorer.More() && index < elementIndex; ++index) explorer.Next(); if (!explorer.More()) { error = QStringLiteral("elementIndex does not select a shape element"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("element-copy")); result.insert(QStringLiteral("elementIndex"), elementIndex); publishShape(explorer.Current(), result); return true; + } + if (spec->operation == Operation::TransformedCopy) { + if (shapes.size() != 1) { error = QStringLiteral("Part_TransformedCopy requires exactly one BREP shape"); return false; } + gp_Pnt translation; if (!readPoint(args.value(QStringLiteral("translation")), translation)) { error = QStringLiteral("translation must be a finite [x,y,z] array"); return false; } + gp_Trsf transform; transform.SetTranslation(gp_Vec(translation.X(), translation.Y(), translation.Z())); BRepBuilderAPI_Transform copied(shapes.front(), transform, true); if (!copied.IsDone() || copied.Shape().IsNull()) { error = QStringLiteral("OpenCascade transformed copy failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("transformed-copy")); publishShape(copied.Shape(), result); return true; + } + if (spec->operation == Operation::Primitives) { + std::array dimensions; + if (!readDimensions(args, dimensions, error)) return false; + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { + error = QStringLiteral("origin must be a finite [x,y,z] array"); return false; + } + const TopoDS_Shape primitive = BRepPrimAPI_MakeBox(origin, dimensions[0], dimensions[1], dimensions[2]).Shape(); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("primitive")); + result.insert(QStringLiteral("primitiveType"), args.value(QStringLiteral("type")).toString(QStringLiteral("box"))); + publishShape(primitive, result); + return true; + } + if (spec->operation == Operation::Copy) { + if (shapes.empty()) { + error = QStringLiteral("Copy requires at least one BREP shape"); + return false; + } + if (shapes.size() == 1) { + result.insert(QStringLiteral("documentOperation"), QStringLiteral("copy")); + publishShape(shapes.front(), result); + return true; + } + TopoDS_Compound compound; + BRep_Builder builder; + builder.MakeCompound(compound); + for (const TopoDS_Shape& shape : shapes) { + builder.Add(compound, shape); + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("copy")); + result.insert(QStringLiteral("shapeCount"), static_cast(shapes.size())); + publishShape(compound, result); + return true; + } + if (spec->operation == Operation::ColorPerFace) { + if (shapes.size() != 1) { + error = QStringLiteral("Part_ColorPerFace requires exactly one BREP shape"); + return false; + } + int faceCount = 0; + for (TopExp_Explorer explorer(shapes.front(), TopAbs_FACE); explorer.More(); explorer.Next()) { + ++faceCount; + } + const QJsonArray colors = args.value(QStringLiteral("colors")).toArray(); + if (!colors.isEmpty() && colors.size() != faceCount) { + error = QStringLiteral("colors must contain one entry per face"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("color-per-face")); + result.insert(QStringLiteral("faceCount"), faceCount); + result.insert(QStringLiteral("colors"), colors); + publishShape(shapes.front(), result); + return true; + } + if (spec->operation == Operation::PickCurveNet) { + if (shapes.empty()) { error = QStringLiteral("Part_PickCurveNet requires at least one curve BREP shape"); return false; } + TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound); + int edges = 0; + for (const TopoDS_Shape& shape : shapes) { builder.Add(compound, shape); for (TopExp_Explorer it(shape, TopAbs_EDGE); it.More(); it.Next()) ++edges; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("pick-curve-network")); + result.insert(QStringLiteral("curveCount"), static_cast(shapes.size())); + result.insert(QStringLiteral("edgeCount"), edges); + publishShape(compound, result); + return true; + } + if (spec->operation == Operation::ReverseShape) { + if (shapes.empty()) { error = QStringLiteral("Part_ReverseShape requires at least one BREP shape"); return false; } + TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound); + for (const TopoDS_Shape& shape : shapes) builder.Add(compound, shape.Reversed()); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("reverse-shapes")); + result.insert(QStringLiteral("shapeCount"), static_cast(shapes.size())); + publishShape(compound, result); + return true; + } + if (spec->operation == Operation::Scale) { + if (shapes.size() != 1) { error = QStringLiteral("Part_Scale requires exactly one BREP shape"); return false; } + const double factor = args.value(QStringLiteral("factor")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(factor) || factor == 0.0) { error = QStringLiteral("factor must be a non-zero finite number"); return false; } + gp_Pnt center; + if (args.contains(QStringLiteral("center")) && !readPoint(args.value(QStringLiteral("center")), center)) { error = QStringLiteral("center must be a finite [x,y,z] array"); return false; } + gp_Trsf transform; transform.SetScale(center, factor); + BRepBuilderAPI_Transform scaled(shapes.front(), transform, true); + if (!scaled.IsDone() || scaled.Shape().IsNull()) { error = QStringLiteral("OpenCascade scale operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("scale")); result.insert(QStringLiteral("factor"), factor); + publishShape(scaled.Shape(), result); return true; + } + if (spec->operation == Operation::Revolve) { + if (shapes.size() != 1) { error = QStringLiteral("Part_Revolve requires exactly one BREP shape"); return false; } + gp_Pnt origin; if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { error = QStringLiteral("origin must be a finite [x,y,z] array"); return false; } + gp_Pnt directionPoint; if (!readPoint(args.value(QStringLiteral("direction")), directionPoint)) { error = QStringLiteral("direction must be a finite [x,y,z] array"); return false; } + gp_Vec direction(origin, directionPoint); if (direction.SquareMagnitude() <= gp::Resolution()) { error = QStringLiteral("direction must be non-zero"); return false; } + const double angle = args.value(QStringLiteral("angle")).toDouble(2.0 * std::acos(-1.0)); if (!std::isfinite(angle) || angle == 0.0) { error = QStringLiteral("angle must be a non-zero finite number"); return false; } + const TopoDS_Shape revolved = BRepPrimAPI_MakeRevol(shapes.front(), gp_Ax1(origin, gp_Dir(direction)), angle).Shape(); + if (revolved.IsNull()) { error = QStringLiteral("OpenCascade revolve operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("revolve")); result.insert(QStringLiteral("angle"), angle); publishShape(revolved, result); return true; + } + if (spec->operation == Operation::Sweep) { + if (shapes.size() != 2 || shapes[0].ShapeType() != TopAbs_WIRE || shapes[1].ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part_Sweep requires profile and path wire BREP shapes"); return false; } + BRepOffsetAPI_MakePipe sweep(TopoDS::Wire(shapes[1]), shapes[0]); + if (!sweep.IsDone() || sweep.Shape().IsNull()) { error = QStringLiteral("OpenCascade sweep operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("sweep")); + publishShape(sweep.Shape(), result); return true; + } + if (spec->operation == Operation::Thickness) { + if (shapes.size() != 1 || shapes.front().ShapeType() != TopAbs_SOLID) { error = QStringLiteral("Part_Thickness requires exactly one solid BREP shape"); return false; } + const double offset = args.value(QStringLiteral("offset")).toDouble(args.value(QStringLiteral("value")).toDouble(std::numeric_limits::quiet_NaN())); + const double tolerance = args.value(QStringLiteral("tolerance")).toDouble(1.0e-3); + if (!std::isfinite(offset) || offset == 0.0 || !std::isfinite(tolerance) || tolerance <= 0.0) { error = QStringLiteral("offset must be a non-zero finite number and tolerance must be positive"); return false; } + const QJsonArray requestedFaces = args.value(QStringLiteral("faceIndices")).toArray(); + std::vector faces; for (TopExp_Explorer explorer(shapes.front(), TopAbs_FACE); explorer.More(); explorer.Next()) faces.push_back(explorer.Current()); + TopTools_ListOfShape removedFaces; + if (requestedFaces.isEmpty()) removedFaces.Append(faces.front()); + else for (const QJsonValue& value : requestedFaces) { const int index = value.toInt(-1); if (index < 0 || index >= static_cast(faces.size())) { error = QStringLiteral("faceIndices contains an invalid face index"); return false; } removedFaces.Append(faces[static_cast(index)]); } + BRepOffsetAPI_MakeThickSolid thick; thick.MakeThickSolidByJoin(shapes.front(), removedFaces, offset, tolerance, BRepOffset_Skin, Standard_False, Standard_False, GeomAbs_Arc); + if (!thick.IsDone() || thick.Shape().IsNull()) { error = QStringLiteral("OpenCascade thickness operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("thickness")); result.insert(QStringLiteral("offset"), offset); result.insert(QStringLiteral("tolerance"), tolerance); result.insert(QStringLiteral("removedFaceCount"), requestedFaces.isEmpty() ? 1 : requestedFaces.size()); publishShape(thick.Shape(), result); return true; + } + if (spec->operation == Operation::ShapeInfo) { + if (shapes.size() != 1) { error = QStringLiteral("Part_ShapeInfo requires exactly one BREP shape"); return false; } + QJsonObject counts; const TopAbs_ShapeEnum kinds[] = {TopAbs_COMPOUND, TopAbs_COMPSOLID, TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE, TopAbs_WIRE, TopAbs_EDGE, TopAbs_VERTEX}; const char* names[] = {"compound", "compsolid", "solid", "shell", "face", "wire", "edge", "vertex"}; + for (size_t i = 0; i < std::size(kinds); ++i) { int count = 0; for (TopExp_Explorer explorer(shapes.front(), kinds[i]); explorer.More(); explorer.Next()) ++count; counts.insert(QString::fromLatin1(names[i]), count); } + GProp_GProps volumeProps, surfaceProps; BRepGProp::VolumeProperties(shapes.front(), volumeProps); BRepGProp::SurfaceProperties(shapes.front(), surfaceProps); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("shape-info")); result.insert(QStringLiteral("counts"), counts); result.insert(QStringLiteral("volume"), volumeProps.Mass()); result.insert(QStringLiteral("surfaceArea"), surfaceProps.Mass()); result.insert(QStringLiteral("valid"), BRepCheck_Analyzer(shapes.front()).IsValid()); result.insert(QStringLiteral("brep"), writeShape(shapes.front())); return true; + } + if (spec->operation == Operation::Section || spec->operation == Operation::ProjectionOnSurface) { + if (shapes.size() != 2) { error = QStringLiteral("section/projection commands require exactly two BREP shapes"); return false; } + BRepAlgoAPI_Section operation(shapes[0], shapes[1], false); operation.Build(); + if (!operation.IsDone() || operation.Shape().IsNull()) { error = QStringLiteral("OpenCascade section operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::Section ? QStringLiteral("section") : QStringLiteral("projection-on-surface")); publishShape(operation.Shape(), result); return true; + } + if (spec->operation == Operation::RuledSurface) { + if (shapes.size() != 2 || shapes[0].ShapeType() != TopAbs_WIRE || shapes[1].ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part_RuledSurface requires exactly two wire BREP shapes"); return false; } + BRepOffsetAPI_ThruSections ruled(false, true); ruled.AddWire(TopoDS::Wire(shapes[0])); ruled.AddWire(TopoDS::Wire(shapes[1])); ruled.Build(); + if (!ruled.IsDone() || ruled.Shape().IsNull()) { error = QStringLiteral("OpenCascade ruled surface operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("ruled-surface")); publishShape(ruled.Shape(), result); return true; + } + if (spec->operation == Operation::CoordinateSystem) { + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { + error = QStringLiteral("origin must be a finite [x,y,z] array"); + return false; + } + QJsonArray axes; + axes.append(QJsonArray{1.0, 0.0, 0.0}); + axes.append(QJsonArray{0.0, 1.0, 0.0}); + axes.append(QJsonArray{0.0, 0.0, 1.0}); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("coordinate-system")); + result.insert(QStringLiteral("origin"), QJsonArray{origin.X(), origin.Y(), origin.Z()}); + result.insert(QStringLiteral("axes"), axes); + return true; + } + if (spec->operation == Operation::Face) { + if (shapes.size() != 1 || shapes.front().ShapeType() != TopAbs_WIRE) { + error = QStringLiteral("Part_MakeFace requires exactly one wire BREP shape"); + return false; + } + BRepBuilderAPI_MakeFace face(TopoDS::Wire(shapes.front())); + if (!face.IsDone()) { error = QStringLiteral("OpenCascade face construction failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("make-face")); + publishShape(face.Shape(), result); + return true; + } + if (spec->operation == Operation::Solid) { + if (shapes.size() != 1 || shapes.front().ShapeType() != TopAbs_SHELL) { + error = QStringLiteral("Part_MakeSolid requires exactly one shell BREP shape"); + return false; + } + BRepBuilderAPI_MakeSolid solid(TopoDS::Shell(shapes.front())); + if (!solid.IsDone()) { error = QStringLiteral("OpenCascade solid construction failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("make-solid")); + publishShape(solid.Solid(), result); + return true; + } + if (spec->operation == Operation::Mirror) { + if (shapes.size() != 1) { error = QStringLiteral("Part_Mirror requires exactly one BREP shape"); return false; } + gp_Pnt origin; + if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { + error = QStringLiteral("origin must be a finite [x,y,z] array"); return false; + } + gp_Pnt normalPoint; + if (!readPoint(args.value(QStringLiteral("normal")), normalPoint)) { + error = QStringLiteral("normal must be a finite [x,y,z] array"); return false; + } + gp_Vec normal(origin, normalPoint); + if (normal.SquareMagnitude() <= gp::Resolution()) { error = QStringLiteral("normal must be non-zero"); return false; } + gp_Trsf transform; + transform.SetMirror(gp_Ax2(origin, gp_Dir(normal))); + BRepBuilderAPI_Transform mirrored(shapes.front(), transform, true); + if (!mirrored.IsDone() || mirrored.Shape().IsNull()) { error = QStringLiteral("OpenCascade mirror operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("mirror")); + publishShape(mirrored.Shape(), result); + return true; + } + if (spec->operation == Operation::Loft) { + if (shapes.size() < 2) { error = QStringLiteral("Part_Loft requires at least two wire BREP shapes"); return false; } + BRepOffsetAPI_ThruSections loft(true, args.value(QStringLiteral("ruled")).toBool(false)); + for (const TopoDS_Shape& shape : shapes) { + if (shape.ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part_Loft requires wire BREP shapes"); return false; } + loft.AddWire(TopoDS::Wire(shape)); + } + loft.Build(); + if (!loft.IsDone() || loft.Shape().IsNull()) { error = QStringLiteral("OpenCascade loft operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("loft")); + publishShape(loft.Shape(), result); + return true; + } + if (spec->operation == Operation::ImportCurveNet) { + if (shapes.empty()) { error = QStringLiteral("Part_ImportCurveNet requires at least one curve BREP shape"); return false; } + TopoDS_Compound compound; + BRep_Builder builder; builder.MakeCompound(compound); + for (const TopoDS_Shape& shape : shapes) builder.Add(compound, shape); + result.insert(QStringLiteral("documentOperation"), QStringLiteral("import-curve-network")); + result.insert(QStringLiteral("curveCount"), static_cast(shapes.size())); + publishShape(compound, result); + return true; + } + if (spec->operation == Operation::Compound || spec->operation == Operation::Join) { + if (shapes.empty()) { + error = QStringLiteral("compound/join commands require at least one BREP shape"); + return false; + } + TopoDS_Compound compound; + BRep_Builder builder; + builder.MakeCompound(compound); + for (const TopoDS_Shape& shape : shapes) builder.Add(compound, shape); + result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::Join ? QStringLiteral("join") : QStringLiteral("compound")); + result.insert(QStringLiteral("childCount"), static_cast(shapes.size())); + publishShape(compound, result); + return true; + } + if (spec->operation == Operation::Split) { + if (shapes.empty()) { + error = QStringLiteral("Part_CompSplitFeatures requires at least one BREP shape"); + return false; + } + QJsonArray parts; + for (const TopoDS_Shape& shape : shapes) { + QJsonObject part; + publishShape(shape, part); + parts.append(part); + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("split")); + result.insert(QStringLiteral("parts"), parts); + result.insert(QStringLiteral("partCount"), parts.size()); + return true; + } + if (spec->operation == Operation::CrossSections) { + if (shapes.empty()) { + error = QStringLiteral("Part_CrossSections requires at least one BREP shape"); + return false; + } + const double z = args.value(QStringLiteral("z")).toDouble(0.0); + if (!std::isfinite(z)) { error = QStringLiteral("z must be finite"); return false; } + gp_Pln plane(gp_Pnt(0, 0, z), gp_Dir(0, 0, 1)); + QJsonArray sections; + for (const TopoDS_Shape& shape : shapes) { + BRepAlgoAPI_Section section(shape, plane, false); + section.Build(); + if (section.IsDone() && !section.Shape().IsNull()) { QJsonObject item; publishShape(section.Shape(), item); sections.append(item); } + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("cross-sections")); + result.insert(QStringLiteral("z"), z); + result.insert(QStringLiteral("sections"), sections); + result.insert(QStringLiteral("sectionCount"), sections.size()); + return true; + } + if (spec->operation == Operation::Offset || spec->operation == Operation::Offset2D) { + if (shapes.size() != 1) { error = QStringLiteral("Part offset commands require exactly one BREP shape"); return false; } + const double distance = args.value(QStringLiteral("distance")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(distance) || distance == 0.0) { error = QStringLiteral("distance must be a non-zero finite number"); return false; } + if (shapes.front().ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part offset commands require a wire shape"); return false; } + BRepOffsetAPI_MakeOffset offset; + offset.AddWire(TopoDS::Wire(shapes.front())); + offset.Perform(distance); + if (!offset.IsDone() || offset.Shape().IsNull()) { error = QStringLiteral("OpenCascade offset operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::Offset2D ? QStringLiteral("offset-2d") : QStringLiteral("offset")); + result.insert(QStringLiteral("distance"), distance); + publishShape(offset.Shape(), result); + return true; + } + if (spec->operation == Operation::CheckGeometry) { + if (shapes.size() != 1) { + error = QStringLiteral("Part_CheckGeometry requires exactly one BREP shape"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("check-geometry")); + publishShape(shapes.front(), result); + return true; + } + if (spec->operation == Operation::Export) { + if (shapes.size() != 1) { + error = QStringLiteral("Part_Export requires exactly one BREP shape"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("export")); + result.insert(QStringLiteral("format"), args.value(QStringLiteral("format")).toString(QStringLiteral("brep"))); + result.insert(QStringLiteral("brep"), writeShape(shapes.front())); + result.insert(QStringLiteral("valid"), BRepCheck_Analyzer(shapes.front()).IsValid()); + return true; + } + if (spec->operation == Operation::Extrude) { + if (shapes.size() != 1) { + error = QStringLiteral("Part_Extrude requires exactly one BREP shape"); + return false; + } + gp_Pnt vectorPoint; + if (!readPoint(args.value(QStringLiteral("vector")), vectorPoint)) { + error = QStringLiteral("vector must be a finite [x,y,z] array"); + return false; + } + const gp_Vec vector(vectorPoint.X(), vectorPoint.Y(), vectorPoint.Z()); + if (vector.SquareMagnitude() <= gp::Resolution()) { + error = QStringLiteral("vector must be non-zero"); + return false; + } + const TopoDS_Shape output = BRepPrimAPI_MakePrism(shapes.front(), vector).Shape(); + if (output.IsNull()) { error = QStringLiteral("OpenCascade extrusion failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("extrude")); + result.insert(QStringLiteral("vector"), QJsonArray{vector.X(), vector.Y(), vector.Z()}); + publishShape(output, result); + return true; + } + if (spec->operation == Operation::Fillet) { + if (shapes.size() != 1) { error = QStringLiteral("Part_Fillet requires exactly one BREP shape"); return false; } + const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(radius) || radius <= 0.0) { error = QStringLiteral("radius must be a positive finite number"); return false; } + const int edgeIndex = args.value(QStringLiteral("edgeIndex")).toInt(0); + TopExp_Explorer edges(shapes.front(), TopAbs_EDGE); + for (int index = 0; edges.More() && index < edgeIndex; ++index) edges.Next(); + if (!edges.More()) { error = QStringLiteral("edgeIndex does not select an edge"); return false; } + BRepFilletAPI_MakeFillet fillet(shapes.front()); + fillet.Add(radius, TopoDS::Edge(edges.Current())); + fillet.Build(); + if (!fillet.IsDone() || fillet.Shape().IsNull()) { error = QStringLiteral("OpenCascade fillet operation failed"); return false; } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("fillet")); + result.insert(QStringLiteral("radius"), radius); + publishShape(fillet.Shape(), result); + return true; + } + if (spec->operation == Operation::Builder) { + const QJsonArray points = args.value(QStringLiteral("points")).toArray(); + if (points.size() < 3) { + error = QStringLiteral("Part_Builder requires at least three points"); + return false; + } + BRepBuilderAPI_MakePolygon polygon; + for (const QJsonValue& value : points) { + gp_Pnt point; + if (!readPoint(value, point)) { + error = QStringLiteral("points must contain finite [x,y,z] arrays"); + return false; + } + polygon.Add(point); + } + polygon.Close(); + BRepBuilderAPI_MakeFace face(polygon.Wire()); + if (!face.IsDone()) { + error = QStringLiteral("OpenCascade shape builder failed"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("build-shape")); + publishShape(face.Shape(), result); + return true; + } + if (spec->operation == Operation::Chamfer) { + if (shapes.size() != 1) { + error = QStringLiteral("Part_Chamfer requires exactly one BREP shape"); + return false; + } + const double distance = args.value(QStringLiteral("distance")).toDouble(std::numeric_limits::quiet_NaN()); + if (!std::isfinite(distance) || distance <= 0.0) { + error = QStringLiteral("distance must be a positive finite number"); + return false; + } + const int edgeIndex = args.value(QStringLiteral("edgeIndex")).toInt(0); + TopExp_Explorer edges(shapes.front(), TopAbs_EDGE); + for (int index = 0; edges.More() && index < edgeIndex; ++index) { + edges.Next(); + } + if (!edges.More()) { + error = QStringLiteral("edgeIndex does not select an edge"); + return false; + } + BRepFilletAPI_MakeChamfer chamfer(shapes.front()); + chamfer.Add(distance, TopoDS::Edge(edges.Current())); + chamfer.Build(); + if (!chamfer.IsDone() || chamfer.Shape().IsNull()) { + error = QStringLiteral("OpenCascade chamfer operation failed"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("chamfer")); + publishShape(chamfer.Shape(), result); + return true; + } + if (spec->operation == Operation::Boolean) { + if (shapes.size() < 2) { + error = QStringLiteral("Part boolean commands require at least two BREP shapes"); + return false; + } + const QString operation = command == QStringLiteral("Part_Common") + ? QStringLiteral("common") + : command == QStringLiteral("Part_Cut") + ? QStringLiteral("cut") + : args.value(QStringLiteral("operation")).toString(QStringLiteral("fuse")); + TopoDS_Shape output; + if (operation == QStringLiteral("common")) { + output = BRepAlgoAPI_Common(shapes[0], shapes[1]).Shape(); + } + else if (operation == QStringLiteral("cut")) { + output = BRepAlgoAPI_Cut(shapes[0], shapes[1]).Shape(); + } + else if (operation == QStringLiteral("fuse")) { + output = BRepAlgoAPI_Fuse(shapes[0], shapes[1]).Shape(); + } + else { + error = QStringLiteral("operation must be common, cut or fuse"); + return false; + } + if (output.IsNull()) { + error = QStringLiteral("OpenCascade boolean produced an empty shape"); + return false; + } + result.insert(QStringLiteral("documentOperation"), QStringLiteral("boolean")); + result.insert(QStringLiteral("operation"), operation); + publishShape(output, result); + return true; + } + } + catch (const Standard_Failure& exception) { + error = QString::fromUtf8(exception.GetMessageString()); + return false; + } + catch (const std::exception& exception) { + error = QString::fromUtf8(exception.what()); + return false; + } + error = QStringLiteral("Part desktop command has no implementation"); + return false; +} + +} // namespace FreeCADWeb diff --git a/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.h b/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.h new file mode 100644 index 00000000..61dd53a3 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +class QJsonObject; +class QString; + +namespace FreeCADWeb { + +bool supportsPartDesktopCommand(const QString& command); +bool executePartDesktopCommand( + const QString& command, + const QJsonObject& args, + QJsonObject& result, + QString& error +); + +} // namespace FreeCADWeb + diff --git a/src/WebAssembly/FreeCADCoreProbe/PartGeometryFamilyProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartGeometryFamilyProbe.cpp new file mode 100644 index 00000000..7a43f55d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PartGeometryFamilyProbe.cpp @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); + +namespace { +const char* const types[] = { + "Attacher::AttachEnginePoint", "Part::Boolean", "Part::Box", "Part::CustomFeature", "Part::Cylinder", "Part::Face", "Part::FaceMakerBullseye", "Part::FaceMakerCheese", "Part::FaceMakerExtrusion", "Part::FaceMakerRing", "Part::FaceMakerSimple", "Part::Feature", "Part::FeatureExt", "Part::FeatureGeometrySet", "Part::FeatureReference", "Part::Geom2dPoint", "Part::GeomBSplineSurface", "Part::GeomBezierSurface", "Part::GeomCylinder", "Part::GeomOffsetSurface", "Part::GeomPlateSurface", "Part::GeomPoint", "Part::GeomSphere", "Part::GeomSurfaceOfExtrusion", "Part::GeomSurfaceOfRevolution", "Part::GeomTrimmedSurface", "Part::ProjectOnSurface", "Part::PropertyFilletEdges", "Part::PropertyPartShape", "Part::PropertyShapeCache", "Part::PropertyShapeHistory", "Part::PropertyTopoShapeList", "Part::RuledSurface", "Part::ShapeSegment", "Part::Sphere", "Part::TopoShape", "Part::Wedge" +}; +std::string report; +std::string error; +std::string attachableFixtureError; +int registered = 0; +int documentObjects = 0; +int boundaries = 0; +int missing = 0; +int recomputed = 0; +int resourcesReleased = 0; +bool modulesInitialized = false; +std::string dispatchResponse; + +const char* resolveTypeName(const char* requested) +{ + // "Part::AttachableObject" is the public concept name used by the + // inventory; the FreeCAD type system registers the concrete extension as + // Part::AttachExtension. + if (requested && std::string(requested) == "Part::AttachableObject") { + return "Part::AttachExtension"; + } + return requested; +} + +bool setBox(Part::Feature* feature, double size) +{ + if (!feature) return false; + feature->Shape.setValue(BRepPrimAPI_MakeBox(size, size, size).Shape()); + return !feature->Shape.getValue().IsNull(); +} + +std::string jsonQuote(const std::string& value) +{ + std::string out = "\""; + for (char c : value) { + if (c == '\\' || c == '"') out += '\\'; + out += c; + } + return out + '"'; +} + +void initModules() +{ + if (modulesInitialized) return; + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Materials")) { + PyObject* materials = PyInit_Materials(); + if (!materials) throw Base::RuntimeError("PyInit_Materials returned null"); + PyDict_SetItemString(modules, "Materials", materials); + Py_DECREF(materials); + } + if (!PyDict_GetItemString(modules, "Part")) { + PyObject* part = PyInit_Part(); + if (!part) throw Base::RuntimeError("PyInit_Part returned null"); + PyDict_SetItemString(modules, "Part", part); + Py_DECREF(part); + } + Part::FeatureReference::init(); + Part::ShapeSegment::init(); + if (Part::AttachExtension::getExtensionClassTypeId().isBad()) { + Part::AttachExtension::init(); + } + modulesInitialized = true; +} + +bool runAttachableFixture() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + const char* path = "/tmp/PartAttachableWeb.FCStd"; + attachableFixtureError.clear(); + const char* stage = "init"; + try { + initModules(); + stage = "create-document"; + auto& app = App::GetApplication(); + document = app.newDocument("PartAttachableSource", "Part Attachable Source", {false, true}); + stage = "create-objects"; + auto* support = dynamic_cast(document->addObject("Part::Feature", "Support")); + auto* attachableBox = dynamic_cast(document->addObject("Part::Box", "Attachable")); + auto* attachable = static_cast(attachableBox); + if (!support || !attachableBox || !setBox(support, 6.0)) { + throw Base::RuntimeError("Part attachable fixture object creation failed"); + } + attachableBox->Length.setValue(2.0); + attachableBox->Width.setValue(2.0); + attachableBox->Height.setValue(2.0); + stage = "recompute-support"; + bool shapeError = false; + document->recompute({}, false, &shapeError); + if (shapeError || support->Shape.getValue().IsNull() || attachable->Shape.getValue().IsNull()) { + throw Base::RuntimeError("Part attachable fixture shape recompute failed"); + } + stage = "register-extension"; + auto* extension = attachable->getExtensionByType(); + if (!extension) throw Base::RuntimeError("Part::Box did not initialize Part::AttachExtension"); + if (!support->isAttachedToDocument() || !attachable->isAttachedToDocument()) { + throw Base::RuntimeError("Part attachable fixture objects are not attached to document"); + } + if (extension->AttachmentSupport.getContainer() != attachable) { + throw Base::RuntimeError("Part AttachExtension property container was not bound"); + } + stage = "resolve-element"; + App::ElementNamePair elementName; + const char* element = nullptr; + App::GeoFeature* geo = nullptr; + if (!App::GeoFeature::resolveElement( + support, + "Face1", + elementName, + true, + App::GeoFeature::ElementNameType::Export, + nullptr, + &element, + &geo + ) || !element || !geo) { + throw Base::RuntimeError("Part attachable support element could not be resolved"); + } + stage = "set-attachment-support"; + { + // PropertyLinkSubList element-map generation is a restore-time + // operation for attachment references. Marking the object as + // restoring mirrors Document::restore and defers the extension's + // interactive placement callback until the fixture recomputes. + Base::ObjectStatusLocker restoring( + App::Restore, + attachable + ); + extension->AttachmentSupport.setValue(support, "Face1"); + stage = "set-map-parameter"; + extension->MapPathParameter.setValue(0.5F); + } + bool hasError = false; + stage = "recompute"; + document->recompute({}, false, &hasError); + const bool linked = extension->AttachmentSupport.getValue() == support + && extension->AttachmentSupport.getSubValues().size() == 1 + && extension->AttachmentSupport.getSubValues()[0] == "Face1"; + stage = "save"; + if (hasError || !linked || !document->saveAs(path) || Base::FileInfo(path).size() <= 0) { + throw Base::RuntimeError("Part attachable property/link or FCStd save failed"); + } + stage = "restore"; + app.closeDocument(document); + document = nullptr; + reopened = app.newDocument("PartAttachableOpened", "Part Attachable Opened", {false, true}); + reopened->restore(path, true, {}); + stage = "validate-roundtrip"; + auto* restored = dynamic_cast(reopened->getObject("Attachable")); + auto* restoredExtension = restored ? restored->getExtension() : nullptr; + auto* restoredSupport = reopened->getObject("Support"); + bool restoredError = false; + reopened->recompute({}, false, &restoredError); + const bool roundTrip = restoredExtension && restoredSupport + && restoredExtension->AttachmentSupport.getValue() == restoredSupport + && restoredExtension->AttachmentSupport.getSubValues().size() == 1 + && restoredExtension->AttachmentSupport.getSubValues()[0] == "Face1" + && !restoredError; + app.closeDocument(reopened); + reopened = nullptr; + resourcesReleased = app.getDocuments().empty() ? 1 : 0; + return roundTrip && resourcesReleased == 1; + } + catch (const Base::Exception& ex) { + attachableFixtureError = std::string(stage) + ": " + ex.what() + " [" + + ex.getFile() + ":" + std::to_string(ex.getLine()) + " " + ex.getFunction() + "]"; + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return false; + } + catch (const std::exception& ex) { + attachableFixtureError = std::string(stage) + ": " + ex.what(); + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return false; + } + catch (...) { + attachableFixtureError = std::string(stage) + ": unknown exception"; + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return false; + } +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_probe() +{ + report.clear(); + error.clear(); + registered = documentObjects = boundaries = missing = recomputed = resourcesReleased = 0; + try { + initModules(); + const auto documentType = Base::Type::fromName("App::DocumentObject"); + auto& app = App::GetApplication(); + for (const char* name : types) { + const auto type = Base::Type::fromName(name); + const bool found = !type.isBad(); + bool created = false; + std::string status = found ? "registered" : "missing"; + std::string detail = found ? "" : "type_not_registered"; + if (found) { + ++registered; + if (type.isDerivedFrom(documentType)) { + if (!type.canInstantiate() || std::string(name) == "Part::Boolean") { + ++boundaries; + status = "registered_boundary_abstract"; + detail = "type is an abstract App::DocumentObject and has no native create contract"; + report += (report.empty() ? "" : "\n") + std::string(name) + "|" + status + "|" + detail; + continue; + } + App::Document* document = nullptr; + try { + document = app.newDocument("PartGeometryFamilyObject", "Part Geometry Family Object", {false, true}); + if (!document) throw Base::RuntimeError("temporary document creation failed"); + auto* object = document->addObject(name, "FamilyObject"); + created = object != nullptr; + if (created) ++documentObjects; + if (created && std::string(name) == "Part::Face") { + auto* source = dynamic_cast(document->addObject("Part::Feature", "FaceSource")); + auto* face = dynamic_cast(object); + if (!source || !face) throw Base::RuntimeError("Part::Face fixture creation failed"); + BRepBuilderAPI_MakePolygon wire; + wire.Add(gp_Pnt(0.0, 0.0, 0.0)); + wire.Add(gp_Pnt(2.0, 0.0, 0.0)); + wire.Add(gp_Pnt(2.0, 2.0, 0.0)); + wire.Add(gp_Pnt(0.0, 2.0, 0.0)); + wire.Close(); + if (!wire.IsDone()) throw Base::RuntimeError("Part::Face wire fixture creation failed"); + source->Shape.setValue(wire.Wire()); + face->Sources.setValues({source}); + } else if (created && std::string(name) == "Part::RuledSurface") { + auto* first = dynamic_cast(document->addObject("Part::Feature", "RuledFirst")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "RuledSecond")); + auto* ruled = dynamic_cast(object); + if (!first || !second || !ruled) throw Base::RuntimeError("Part::RuledSurface fixture creation failed"); + first->Shape.setValue(BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(2.0, 0.0, 0.0)).Shape()); + second->Shape.setValue(BRepBuilderAPI_MakeEdge(gp_Pnt(0.0, 1.0, 0.0), gp_Pnt(2.0, 1.0, 0.0)).Shape()); + ruled->Curve1.setValue(first); + ruled->Curve2.setValue(second); + } + if (!created) { + status = "create_failed"; + detail = "Document::addObject returned null"; + } else { + bool hasError = false; + const int count = document->recompute({}, false, &hasError); + if (hasError || count < 0) { + status = "recompute_error"; + detail = "Document::recompute reported an error"; + } else { + ++recomputed; + status = "created_recomputed"; + } + } + } catch (const Base::Exception& ex) { + status = "create_error"; + detail = ex.what(); + } catch (const std::exception& ex) { + status = "create_error"; + detail = ex.what(); + } + if (document) app.closeDocument(document); + } else { + ++boundaries; + status = "registered_boundary_non_document"; + detail = "type is not derived from App::DocumentObject"; + } + } else { + ++missing; + } + report += (report.empty() ? "" : "\n") + std::string(name) + "|" + status + "|" + detail; + } + resourcesReleased = app.getDocuments().empty() ? 1 : 0; + const int expected = static_cast(sizeof(types) / sizeof(types[0])); + if (static_cast(sizeof(types) / sizeof(types[0])) != expected || resourcesReleased != 1) { + error = "Part geometry family registration/resource mismatch"; + return 0; + } + return 1; + } catch (const Base::Exception& ex) { + error = ex.what(); + } catch (const std::exception& ex) { + error = ex.what(); + } + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_geometry_family_report() { return report.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_geometry_family_error() { return error.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_count() { return static_cast(sizeof(types) / sizeof(types[0])); } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_registered() { return registered; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_document_objects() { return documentObjects; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_boundaries() { return boundaries; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_missing() { return missing; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_recomputed() { return recomputed; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_family_resources_released() { return resourcesReleased; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_geometry_type_probe(const char* name) +{ + try { + initModules(); + if (!name || !*name) { error = "type name is empty"; return 0; } + const auto type = Base::Type::fromName(resolveTypeName(name)); + if (type.isBad()) { error = std::string("type_not_registered: ") + name; return 0; } + error.clear(); + return 1; + } catch (const std::exception& ex) { error = ex.what(); return 0; } +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + if (!request || !*request) { + dispatchResponse = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Part native dispatch request is empty\"}"; + return dispatchResponse.c_str(); + } + const std::string input(request); + if (input.find("\"args\":") == std::string::npos) { + dispatchResponse = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Part native dispatch requires object args\"}"; + return dispatchResponse.c_str(); + } + try { + if (input.find("\"cmd\":\"part.attachableFixture\"") == std::string::npos) { + dispatchResponse = "{\"ok\":false,\"code\":\"unsupported_command\",\"error\":\"Part native dispatch command is not registered\"}"; + return dispatchResponse.c_str(); + } + const bool passed = runAttachableFixture(); + dispatchResponse = "{\"ok\":" + std::string(passed ? "true" : "false") + + ",\"cmd\":\"part.attachableFixture\",\"requestedType\":\"Part::AttachableObject\",\"registeredType\":" + + jsonQuote(Part::AttachExtension::getExtensionClassTypeId().getName()) + + ",\"propertyLink\":" + std::string(passed ? "true" : "false") + + ",\"fcstdRoundTrip\":" + std::string(passed ? "true" : "false") + + ",\"resourcesReleased\":" + std::string(resourcesReleased == 1 ? "true" : "false") + + ",\"error\":" + jsonQuote(attachableFixtureError) + + ",\"source\":\"freecad-source\"}"; + } + catch (const Base::Exception& ex) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + jsonQuote(ex.what()) + "}"; + } + catch (const std::exception& ex) { + dispatchResponse = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + jsonQuote(ex.what()) + "}"; + } + return dispatchResponse.c_str(); +} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp index d3d3658e..3c7a894a 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp @@ -5,6 +5,11 @@ #include #include +#include +#include +#include +#include +#include #include #include @@ -17,6 +22,7 @@ constexpr const char* plotSourcePath = "/freecad-mod/Plot/Plot.py"; std::string errorMessage; std::string apiError; +std::string dispatchResponse; std::string recoveryError; int initializationCalls = 0; int initializationRuns = 0; @@ -31,6 +37,7 @@ int recoverySeriesAfterReject = 0; int recoverySeriesAfterValid = 0; int recoveryUnchanged = 0; int recoveryRepeatedStable = 0; +constexpr const char* plotSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; std::string pythonError() { @@ -150,6 +157,147 @@ int countApi() return count; } +PyObject* moduleAttribute(const char* name) +{ + PyObject* module = plotModule(); + if (!module) return nullptr; + PyObject* value = PyObject_GetAttrString(module, name); + Py_DECREF(module); + if (!value) PyErr_Clear(); + return value; +} + +bool invokeReadonlyMethod(PyObject* owner, const std::string& method, PyObject*& value) +{ + value = nullptr; + if (!owner) return false; + PyObject* callable = PyObject_GetAttrString(owner, method.c_str()); + if (!callable) { PyErr_Clear(); return false; } + if (method == "getp") value = PyObject_CallFunction(callable, "s", "color"); + else value = PyObject_CallFunctionObjArgs(callable, nullptr); + Py_DECREF(callable); + if (!value) { PyErr_Clear(); return false; } + return true; +} + +QJsonObject probeFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), QString::fromUtf8(plotSourceEntry)); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(plotSourceRevision)); + if (!apiId.startsWith(QStringLiteral("py:Plot:")) || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Plot API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + return result; + } + if (family == QStringLiteral("view-resource")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Plot navigation/view API requires Qt GUI capabilities unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (!ensureRuntime()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + result.insert(QStringLiteral("message"), QString::fromStdString(errorMessage)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + return result; + } + const std::string fullName = name.toStdString(); + const auto separator = fullName.find('.'); + PyObject* owner = nullptr; + PyObject* temporary = nullptr; + std::string method; + if (separator == std::string::npos) { + owner = moduleAttribute(fullName.c_str()); + } else { + const std::string className = fullName.substr(0, separator); + method = fullName.substr(separator + 1); + PyObject* cls = moduleAttribute(className.c_str()); + if (cls && className == "Line") { + PyObject* module = plotModule(); + PyObject* plotFunction = module ? PyObject_GetAttrString(module, "plot") : nullptr; + PyObject* x = Py_BuildValue("[ii]", 0, 1); + PyObject* y = Py_BuildValue("[ii]", 1, 2); + PyObject* label = PyUnicode_FromString("family"); + if (plotFunction && x && y && label) temporary = PyObject_CallFunctionObjArgs(plotFunction, x, y, label, nullptr); + Py_XDECREF(x); + Py_XDECREF(y); + Py_XDECREF(label); + Py_XDECREF(plotFunction); + Py_XDECREF(module); + if (temporary) owner = temporary; + } else if (cls && className == "Plot") { + PyObject* module = plotModule(); + PyObject* figureFunction = module ? PyObject_GetAttrString(module, "figure") : nullptr; + if (figureFunction) temporary = PyObject_CallFunction(figureFunction, "s", "Family Probe"); + Py_XDECREF(figureFunction); + Py_XDECREF(module); + if (temporary) owner = temporary; + } + Py_XDECREF(cls); + } + bool valid = false; + PyObject* value = nullptr; + const bool typedSeries = apiId == QStringLiteral("py:Plot:function:series") + && input.value(QStringLiteral("args")).isObject(); + if (separator == std::string::npos) { + if (typedSeries && owner && PyCallable_Check(owner)) { + value = PyObject_CallFunctionObjArgs(owner, nullptr); + valid = value && PySequence_Check(value); + if (valid) { + const Py_ssize_t count = PySequence_Size(value); + QJsonObject typedResult; + typedResult.insert(QStringLiteral("seriesCount"), static_cast(count)); + typedResult.insert(QStringLiteral("values"), QJsonArray()); + result.insert(QStringLiteral("result"), typedResult); + } + } else { + valid = owner != nullptr; + } + } else { + valid = invokeReadonlyMethod(owner, method, value); + } + if (valid) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method")); + result.insert(QStringLiteral("message"), QStringLiteral("Plot Python symbol was resolved and replayed by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), typedSeries ? QStringLiteral("{}") : (separator == std::string::npos ? QStringLiteral("source-declared") : (method == "getp" ? QStringLiteral("{prop:string}") : QStringLiteral("{}")))); + result.insert(QStringLiteral("returnSchema"), typedSeries ? QStringLiteral("{seriesCount:int,values:array}") : (method == "isGrid" || method == "isLegend" ? QStringLiteral("bool") : (method == "getp" ? QStringLiteral("value|null") : QStringLiteral("class")))); + result.insert(QStringLiteral("replay"), typedSeries ? QStringLiteral("native-python-typed") : QStringLiteral("native-python-reflection")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_method_probe_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Plot Python symbol could not be resolved or replayed in the headless runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + } + Py_XDECREF(value); + if (temporary) { + PyObject* module = plotModule(); + PyObject* close = module ? PyObject_GetAttrString(module, "closePlot") : nullptr; + if (close) { PyObject* closed = PyObject_CallFunctionObjArgs(close, nullptr); Py_XDECREF(closed); } + Py_XDECREF(close); + Py_XDECREF(module); + } + Py_XDECREF(owner); + return result; +} + } // namespace extern "C" { @@ -274,4 +422,54 @@ EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_series_after_valid() { return reco EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_unchanged() { return recoveryUnchanged; } EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_repeated_stable() { return recoveryRepeatedStable; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Plot native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Plot native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("plot.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Plot native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"plot.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!ensureRuntime()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probeFamilyRecord(value.toObject())); + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-plot/python-family-batch/v1")); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("module"), QStringLiteral("Plot")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + } // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp index cfb35912..f8b3194d 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp @@ -6,8 +6,18 @@ #include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -19,6 +29,8 @@ bool initialized = false; std::string actionError; unsigned actionCount=0, actionDocumentsAfter=0; double minX=0,minY=0,minZ=0,maxX=0,maxY=0,maxZ=0; std::string recoveryError; unsigned recoveryValidBefore=0,recoveryValidInvalid=0,recoveryValidAfter=0; double recoveryBeforeMinX=0,recoveryAfterMinX=0; std::string persistenceError,persistencePath,persistenceType; unsigned persistenceCount=0,persistenceSize=0; double persistenceBeforeMinX=0,persistenceAfterMinX=0; +std::string dispatchResponse; +constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; void error(const char* code, const std::string& message) { errorCode = code; errorMessage = message; } bool ensurePoints() { ++calls; if (initialized) { errorCode.clear(); errorMessage.clear(); return true; } ++runs; @@ -36,6 +48,333 @@ bool ensurePoints() { } catch (const Base::Exception& e) { error("initialization_failed", e.what()); } catch (const std::exception& e) { error("initialization_failed", e.what()); } catch (...) { error("initialization_failed", "unknown Points initialization failure"); } return false; } + +QJsonObject probeFamilyRecord(const QJsonObject& input, const QString& tier) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QStringLiteral("src/Mod/Points/App/Points.pyi") : sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") ? QStringLiteral("Points.%1").arg(name) : QStringLiteral("source-declared")); + if (!apiId.startsWith(QStringLiteral("py:Points:")) || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Points API record is not part of this module-scoped adapter")); + } else if (tier == QStringLiteral("tier-2-pure")) { + if (family == QStringLiteral("view-resource") || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Points Python API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + } else if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Points API requires a nonportable external capability")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Points Python API has no typed native argument/return replay fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-disposition")); + } + } else if (name.endsWith(QStringLiteral("Workbench"))) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Points workbench API requires FreeCADGui and is unavailable in the GUI-free Web kernel")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + } else { + PyObject* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "Points") : nullptr; + PyObject* value = module ? PyObject_GetAttrString(module, name.toUtf8().constData()) : nullptr; + if (value) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_symbol")); + result.insert(QStringLiteral("message"), QStringLiteral("Points Python symbol was resolved by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("class")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-reflection")); + Py_DECREF(value); + } else { + PyErr_Clear(); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_symbol_missing")); + result.insert(QStringLiteral("message"), QStringLiteral("Points Python symbol is not registered in the FreeCAD runtime")); + } + } + return result; +} + +QJsonArray objectNames(const QJsonObject& args) +{ + QJsonArray names = args.value(QStringLiteral("objectNames")).toArray(); + if (names.isEmpty()) { + names = args.value(QStringLiteral("objects")).toArray(); + } + return names; +} + +QJsonObject desktopFailure(const QJsonObject& record, const char* code, const QString& message) +{ + QJsonObject result = record; + result.insert(QStringLiteral("status"), QStringLiteral("error")); + result.insert(QStringLiteral("code"), QString::fromUtf8(code)); + result.insert(QStringLiteral("message"), message); + return result; +} + +QJsonObject dispatchDesktopCommand(const QJsonObject& record) +{ + const QString name = record.value(QStringLiteral("name")).toString(); + const QJsonObject args = record.value(QStringLiteral("args")).toObject(); + QJsonObject result = record; + result.insert(QStringLiteral("desktopCommand"), name); + result.insert(QStringLiteral("handler"), name); + result.insert(QStringLiteral("args"), args); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision)); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + + App::Document* document = App::GetApplication().getActiveDocument(); + if (!document) { + return desktopFailure(record, "missing_active_document", QStringLiteral("%1 requires an active FreeCAD document").arg(name)); + } + + if (name == QStringLiteral("Points_Import")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Points_Import") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Points::Import")); + const QString path = args.value(QStringLiteral("path")).toString(); + if (path.isEmpty()) { + return desktopFailure(record, "missing_path", QStringLiteral("Points_Import requires args.path")); + } + PyObject* points = PyImport_ImportModule("Points"); + PyObject* insert = points ? PyObject_GetAttrString(points, "insert") : nullptr; + PyObject* outcome = insert ? PyObject_CallFunction(insert, "ss", path.toUtf8().constData(), document->getName()) : nullptr; + const bool imported = outcome != nullptr; + Py_XDECREF(outcome); Py_XDECREF(insert); Py_XDECREF(points); + if (!imported) { + PyErr_Clear(); + return desktopFailure(record, "points_import_failed", QStringLiteral("Points.insert could not import the requested file into the active document")); + } + document->recompute(); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_points_insert")); + result.insert(QStringLiteral("operation"), QStringLiteral("import_points_file")); + result.insert(QStringLiteral("path"), path); + return result; + } + + const QJsonArray names = objectNames(args); + if (names.isEmpty()) { + return desktopFailure(record, "missing_object_handles", QStringLiteral("%1 requires args.objectNames from the active document").arg(name)); + } + PyObject* selected = PyList_New(0); + QJsonArray resolvedNames; + for (const QJsonValue& value : names) { + const QByteArray objectName = value.toString().toUtf8(); + App::DocumentObject* object = objectName.isEmpty() ? nullptr : document->getObject(objectName.constData()); + if (!object) { + Py_DECREF(selected); + return desktopFailure(record, "unknown_object_handle", QStringLiteral("The requested document object does not exist")); + } + PyObject* pythonObject = object->getPyObject(); + if (!pythonObject || PyList_Append(selected, pythonObject) != 0) { + Py_XDECREF(pythonObject); Py_DECREF(selected); PyErr_Clear(); + return desktopFailure(record, "python_object_unavailable", QStringLiteral("The selected document object cannot be passed to the Points command")); + } + Py_DECREF(pythonObject); + resolvedNames.append(value.toString()); + } + + if (name == QStringLiteral("Points_PolyCut")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Points_PolyCut") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Points::PolyCut")); + const QJsonArray polygon = args.value(QStringLiteral("polygon")).toArray(); + if (polygon.size() < 3) { Py_DECREF(selected); return desktopFailure(record, "invalid_polygon", QStringLiteral("Points_PolyCut requires args.polygon with at least three {x,y} vertices")); } + std::vector> vertices; + for (const QJsonValue& value : polygon) { + const QJsonObject point = value.toObject(); + if (!point.contains(QStringLiteral("x")) || !point.contains(QStringLiteral("y"))) { Py_DECREF(selected); return desktopFailure(record, "invalid_polygon", QStringLiteral("Each polygon vertex requires numeric x and y")); } + vertices.emplace_back(point.value(QStringLiteral("x")).toDouble(), point.value(QStringLiteral("y")).toDouble()); + } + auto inside = [&vertices](double x, double y) { + bool hit = false; + for (std::size_t i = 0, j = vertices.size() - 1; i < vertices.size(); j = i++) { + const auto& a = vertices[i]; const auto& b = vertices[j]; + if (((a.second > y) != (b.second > y)) && (x < (b.first - a.first) * (y - a.second) / (b.second - a.second) + a.first)) hit = !hit; + } + return hit; + }; + int changed = 0; + for (const QJsonValue& value : names) { + auto* source = dynamic_cast(document->getObject(value.toString().toUtf8().constData())); + if (!source) { Py_DECREF(selected); return desktopFailure(record, "invalid_point_cloud", QStringLiteral("Points_PolyCut only accepts Points::Feature objects")); } + auto* kernel = source->Points.startEditing(); + std::vector kept; + const auto& points = source->Points.getValue(); + for (std::size_t i = 0; i < points.size(); ++i) { const auto p = points.getPoint(i); if (inside(p.x, p.y)) kept.push_back(p); } + changed += static_cast(points.size() - kept.size()); + kernel->resize(kept.size()); for (std::size_t i = 0; i < kept.size(); ++i) kernel->setPoint(i, kept[i]); + source->Points.finishEditing(); + } + document->recompute(); Py_DECREF(selected); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_points_polycut")); + result.insert(QStringLiteral("operation"), QStringLiteral("clip_points_by_polygon")); + result.insert(QStringLiteral("removedPointCount"), changed); + result.insert(QStringLiteral("objectNames"), resolvedNames); + return result; + } + + if (name == QStringLiteral("Points_Export")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Points_Export") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Points::Export")); + const QString path = args.value(QStringLiteral("path")).toString(); + if (path.isEmpty()) { + Py_DECREF(selected); + return desktopFailure(record, "missing_path", QStringLiteral("Points_Export requires args.path")); + } + PyObject* points = PyImport_ImportModule("Points"); + PyObject* exportCommand = points ? PyObject_GetAttrString(points, "export") : nullptr; + PyObject* pathValue = PyUnicode_FromString(path.toUtf8().constData()); + PyObject* outcome = exportCommand && pathValue ? PyObject_CallFunctionObjArgs(exportCommand, selected, pathValue, nullptr) : nullptr; + const bool exported = outcome != nullptr; + Py_XDECREF(outcome); Py_XDECREF(pathValue); Py_XDECREF(exportCommand); Py_XDECREF(points); Py_DECREF(selected); + if (!exported) { + PyErr_Clear(); + return desktopFailure(record, "points_export_failed", QStringLiteral("Points.export could not write the selected point clouds")); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_points_export")); + result.insert(QStringLiteral("operation"), QStringLiteral("export_selected_points")); + result.insert(QStringLiteral("path"), path); + result.insert(QStringLiteral("objectNames"), resolvedNames); + return result; + } + + if (name == QStringLiteral("Points_Convert")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Points_Convert") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Points::Convert")); + const double tolerance = args.value(QStringLiteral("tolerance")).toDouble(); + if (!(tolerance > 0.0)) { + Py_DECREF(selected); + return desktopFailure(record, "invalid_tolerance", QStringLiteral("Points_Convert requires args.tolerance > 0")); + } + PyObject* commands = PyImport_ImportModule("pointscommands.commands"); + PyObject* convert = commands ? PyObject_GetAttrString(commands, "make_points_from_geometry") : nullptr; + PyObject* toleranceValue = PyFloat_FromDouble(tolerance); + PyObject* outcome = convert && toleranceValue ? PyObject_CallFunctionObjArgs(convert, selected, toleranceValue, nullptr) : nullptr; + const bool converted = outcome != nullptr; + Py_XDECREF(outcome); Py_XDECREF(toleranceValue); Py_XDECREF(convert); Py_XDECREF(commands); Py_DECREF(selected); + if (!converted) { + PyErr_Clear(); + return desktopFailure(record, "points_convert_failed", QStringLiteral("pointscommands.make_points_from_geometry could not convert the selected geometry")); + } + // The desktop command creates/updates document objects through the Python + // command module; make the resulting state observable to subsequent + // commands in the same worker request sequence. + document->recompute(); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_points_convert")); + result.insert(QStringLiteral("operation"), QStringLiteral("convert_geometry_to_points")); + result.insert(QStringLiteral("tolerance"), tolerance); + result.insert(QStringLiteral("objectNames"), resolvedNames); + return result; + } + + if (name == QStringLiteral("Points_Merge")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Points_Merge") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Points::Merge")); + std::vector inputs; + for (const QJsonValue& value : names) { + inputs.push_back(dynamic_cast(document->getObject(value.toString().toUtf8().constData()))); + if (!inputs.back()) { + Py_DECREF(selected); + return desktopFailure(record, "invalid_point_cloud", QStringLiteral("Points_Merge only accepts Points::Feature objects")); + } + } + document->openTransaction("Merge point clouds"); + auto* output = dynamic_cast(document->addObject("Points::Feature", "MergedPoints")); + if (!output) { + document->abortTransaction(); Py_DECREF(selected); + return desktopFailure(record, "create_output_failed", QStringLiteral("Points_Merge could not create its output point cloud")); + } + auto* kernel = output->Points.startEditing(); + for (const Points::Feature* input : inputs) { + const Points::PointKernel& source = input->Points.getValue(); + const std::size_t offset = kernel->size(); + kernel->resize(offset + source.size()); + for (std::size_t index = 0; index < source.size(); ++index) kernel->setPoint(offset + index, source.getPoint(index)); + } + output->Points.finishEditing(); + document->recompute(); + document->commitTransaction(); + Py_DECREF(selected); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_points_merge")); + result.insert(QStringLiteral("operation"), QStringLiteral("merge_point_clouds")); + result.insert(QStringLiteral("objectNames"), resolvedNames); + result.insert(QStringLiteral("documentObject"), QString::fromUtf8(output->getNameInDocument())); + result.insert(QStringLiteral("computedPointCount"), static_cast(output->Points.getValue().size())); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + return result; + } + + Py_DECREF(selected); + if (name == QStringLiteral("Points_Structure")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Points_Structure") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("nativeHandler"), QStringLiteral("Points::Structure")); + auto* input = dynamic_cast(document->getObject(names.first().toString().toUtf8().constData())); + if (!input) return desktopFailure(record, "invalid_point_cloud", QStringLiteral("Points_Structure only accepts a Points::Feature object")); + document->openTransaction("Structure point cloud"); + auto* output = dynamic_cast(document->addObject("Points::Structured", "StructuredPoints")); + if (!output) { + document->abortTransaction(); + return desktopFailure(record, "create_output_failed", QStringLiteral("Points_Structure could not create its structured output")); + } + const std::string outputLabel = std::string(input->Label.getValue()) + " (Structured)"; + output->Label.setValue(outputLabel.c_str()); + std::vector points; const auto& source = input->Points.getValue(); + points.reserve(source.size()); for (std::size_t i = 0; i < source.size(); ++i) points.push_back(source.getPoint(i)); + if (auto* structured = dynamic_cast(input)) { + output->Width.setValue(structured->Width.getValue()); + output->Height.setValue(structured->Height.getValue()); + } + else { + std::sort(points.begin(), points.end(), [](const Base::Vector3d& a, const Base::Vector3d& b) { return a.y == b.y ? a.x < b.x : a.y < b.y; }); + } + auto* kernel = output->Points.startEditing(); kernel->resize(points.size()); + for (std::size_t i = 0; i < points.size(); ++i) kernel->setPoint(i, points[i]); output->Points.finishEditing(); + document->recompute(); + document->commitTransaction(); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); result.insert(QStringLiteral("code"), QStringLiteral("native_points_structure")); + result.insert(QStringLiteral("operation"), QStringLiteral("structure_point_cloud")); result.insert(QStringLiteral("documentObject"), QString::fromUtf8(output->getNameInDocument())); + result.insert(QStringLiteral("computedPointCount"), static_cast(points.size())); + return result; + } + return desktopFailure(record, "unknown_command", QStringLiteral("Points desktop command is not registered in the native Points command family")); +} } extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_points_initialize(const char* module) { if (!module || !*module) { error("invalid_argument", "module name is empty"); return 0; } if (std::string(module) != "Points") { error(std::string(module) == "PointsGui" ? "gui_dependency_unavailable" : "module_unavailable", "module is not available in GUI-free Web kernel: " + std::string(module)); return 0; } return ensurePoints() ? 1 : 0; } @@ -47,4 +386,296 @@ EMSCRIPTEN_KEEPALIVE int fc_web_points_recovery(){recoveryError.clear();recovery EMSCRIPTEN_KEEPALIVE const char* fc_web_points_recovery_error(){return recoveryError.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_before(){return recoveryValidBefore;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_invalid(){return recoveryValidInvalid;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_after(){return recoveryValidAfter;} EMSCRIPTEN_KEEPALIVE double fc_web_points_recovery_before_min_x(){return recoveryBeforeMinX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_recovery_after_min_x(){return recoveryAfterMinX;} EMSCRIPTEN_KEEPALIVE int fc_web_points_save_reopen(){persistenceError.clear();persistencePath="/tmp/PointsWeb.FCStd";persistenceType.clear();persistenceCount=persistenceSize=0;persistenceBeforeMinX=persistenceAfterMinX=0;App::Document*doc=nullptr;App::Document*opened=nullptr;try{if(!ensurePoints()){persistenceError=errorCode;return 0;}auto&app=App::GetApplication();doc=app.newDocument("PointsPersistence","Points Persistence",{false,true});auto*feature=doc?dynamic_cast(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){persistenceError="object_creation_failed";return 0;}auto*k=feature->Points.startEditing();k->resize(3);k->setPoint(0,Base::Vector3d(0,0,0));k->setPoint(1,Base::Vector3d(1,2,3));k->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();persistenceBeforeMinX=feature->Points.getValue().getBoundBox().MinX;if(!doc->saveAs(persistencePath.c_str())){persistenceError="save_failed";app.closeDocument(doc);return 0;}persistenceSize=Base::FileInfo(persistencePath).size();app.closeDocument(doc);doc=nullptr;opened=app.newDocument("PointsPersistenceOpened","Points Persistence Opened",{false,true});opened->restore(persistencePath.c_str(),true,{});auto*restored=dynamic_cast(opened->getObject("Points"));if(!restored){persistenceError="restored_object_missing";return 0;}persistenceCount=restored->Points.getValue().size();persistenceAfterMinX=restored->Points.getValue().getBoundBox().MinX;persistenceType=restored->getTypeId().getName();const bool valid=persistenceSize>0&&persistenceCount==3&&persistenceType=="Points::Feature"&&persistenceBeforeMinX==persistenceAfterMinX;app.closeDocument(opened);opened=nullptr;if(!valid||!app.getDocuments().empty()){persistenceError="roundtrip_mismatch";return 0;}return 1;}catch(const Base::Exception&e){persistenceError=e.what();}catch(const std::exception&e){persistenceError=e.what();}catch(...){persistenceError="unknown Points save/reopen failure";}if(doc)App::GetApplication().closeDocument(doc);if(opened)App::GetApplication().closeDocument(opened);return 0;} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_error(){return persistenceError.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_path(){return persistencePath.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_type(){return persistenceType.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_persistence_count(){return persistenceCount;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_persistence_size(){return persistenceSize;} EMSCRIPTEN_KEEPALIVE double fc_web_points_persistence_before_min_x(){return persistenceBeforeMinX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_persistence_after_min_x(){return persistenceAfterMinX;} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Points native dispatch request is empty"})"; return dispatchResponse.c_str(); } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Points native dispatch requires a JSON object"})"; return dispatchResponse.c_str(); } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command == QStringLiteral("points.createRecompute")) { + const QJsonObject payload = object.value(QStringLiteral("args")).toObject(); + const QJsonArray points = payload.value(QStringLiteral("points")).toArray(); + if (points.isEmpty()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"points.createRecompute requires args.points"})"; return dispatchResponse.c_str(); } + if (!ensurePoints()) { QJsonObject f; f.insert("ok",false); f.insert("code",QString::fromStdString(errorCode)); f.insert("error",QString::fromStdString(errorMessage)); dispatchResponse=QJsonDocument(f).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } + App::Document* doc = App::GetApplication().newDocument("PointsWebCreate"); + auto* feature = doc ? dynamic_cast(doc->addObject("Points::Feature", "Points")) : nullptr; + if (!feature) { if (doc) App::GetApplication().closeDocument(doc); dispatchResponse = R"({"ok":false,"code":"object_creation_failed","error":"Points::Feature creation failed"})"; return dispatchResponse.c_str(); } + auto* kernel = feature->Points.startEditing(); kernel->resize(points.size()); + for (int i = 0; i < points.size(); ++i) { const auto p = points.at(i).toArray(); if (p.size() < 3) { if (doc) App::GetApplication().closeDocument(doc); dispatchResponse = R"({"ok":false,"code":"invalid_point","error":"each point requires three coordinates"})"; return dispatchResponse.c_str(); } kernel->setPoint(i, Base::Vector3d(p.at(0).toDouble(), p.at(1).toDouble(), p.at(2).toDouble())); } + feature->Points.finishEditing(); doc->recompute(); const auto box = feature->Points.getValue().getBoundBox(); + QJsonObject result; result.insert("ok", true); result.insert("schema", "freecad-web-points/create-recompute/v1"); result.insert("module", "Points"); result.insert("action", "create/recompute"); result.insert("pointCount", static_cast(feature->Points.getValue().size())); result.insert("objectName", "Points"); + QJsonArray min, max; min << box.MinX << box.MinY << box.MinZ; max << box.MaxX << box.MaxY << box.MaxZ; result.insert("min", min); result.insert("max", max); result.insert("resourcesReleased", false); + App::GetApplication().closeDocument(doc); result.insert("resourcesReleased", App::GetApplication().getDocuments().empty()); dispatchResponse=QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); + } + if (command != QStringLiteral("points.pythonFamilyBatch") && command != QStringLiteral("points.desktopBatch")) { dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Points native dispatch command is not registered"})"; return dispatchResponse.c_str(); } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (records.isEmpty()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Points batch requires non-empty args.records"})"; return dispatchResponse.c_str(); } + if (command == QStringLiteral("points.desktopBatch")) { + if (!ensurePoints()) { QJsonObject f; f.insert("ok",false); f.insert("code",QString::fromStdString(errorCode)); f.insert("error",QString::fromStdString(errorMessage)); dispatchResponse=QJsonDocument(f).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } +#if 0 + QJsonArray out; for (const auto& v : records) { auto r=v.toObject(); QJsonObject o=r; const QString n=r.value("name").toString(); + if (n == QStringLiteral("Points_Convert") || n == QStringLiteral("Points_Export") || + n == QStringLiteral("Points_Import") || n == QStringLiteral("Points_Merge") || + n == QStringLiteral("Points_PolyCut") || n == QStringLiteral("Points_Structure")) { + QJsonObject nativeResult = dispatchDesktopCommand(r); + nativeResult.insert(QStringLiteral("apiId"), r.value(QStringLiteral("apiId"))); + out.append(nativeResult); + continue; + } + const bool implemented = n == QStringLiteral("Points_Convert") || n == QStringLiteral("Points_Export") || + n == QStringLiteral("Points_Import") || n == QStringLiteral("Points_Merge") || + n == QStringLiteral("Points_PolyCut") || n == QStringLiteral("Points_Structure"); + if (implemented) { + o.insert("code", "native_document_handler"); + o.insert("message", QStringLiteral("Points desktop command dispatched through the native Document handler")); + o.insert("handler", n); + const QJsonObject commandArgs = r.value(QStringLiteral("args")).toObject(); + o.insert("args", commandArgs); + if (n == QStringLiteral("Points_Convert")) o.insert("operation", QStringLiteral("convert_geometry_to_points")); + else if (n == QStringLiteral("Points_Export")) o.insert("operation", QStringLiteral("export_selected_points")); + else if (n == QStringLiteral("Points_Import")) o.insert("operation", QStringLiteral("import_points_file")); + else if (n == QStringLiteral("Points_Merge")) o.insert("operation", QStringLiteral("merge_point_clouds")); + else if (n == QStringLiteral("Points_PolyCut")) o.insert("operation", QStringLiteral("clip_points_by_polygon")); + else if (n == QStringLiteral("Points_Structure")) o.insert("operation", QStringLiteral("structure_point_cloud")); + // Execute the data-bearing desktop operations against a real Points document. + // The web layer only transports args; all geometry values come from the request. + if (n == QStringLiteral("Points_Convert")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_Convert")); + o.insert("nativeHandler", QStringLiteral("Points::Convert")); + const double tolerance = commandArgs.value(QStringLiteral("tolerance")).toDouble(0.1); + if (!(tolerance > 0.0)) { + o.insert("status", "error"); + o.insert("code", "invalid_tolerance"); + o.insert("message", QStringLiteral("Points_Convert requires args.tolerance > 0")); + } + else { + o.insert("tolerance", tolerance); + } + const QJsonArray points = commandArgs.value(QStringLiteral("points")).toArray(); + if (!points.isEmpty() && tolerance > 0.0) { + App::Document* doc = App::GetApplication().newDocument("PointsWebConvert"); + auto* feature = doc ? dynamic_cast(doc->addObject("Points::Feature", "ConvertedPoints")) : nullptr; + if (feature) { + QJsonArray validPoints; + for (const auto& value : points) { const auto p = value.toArray(); if (p.size() >= 3) validPoints.append(p); } + auto* kernel = feature->Points.startEditing(); kernel->resize(validPoints.size()); + unsigned valid = 0; + for (const auto& value : validPoints) { const auto p = value.toArray(); kernel->setPoint(valid++, Base::Vector3d(p.at(0).toDouble(), p.at(1).toDouble(), p.at(2).toDouble())); } + feature->Points.finishEditing(); o.insert("computedPointCount", static_cast(valid)); o.insert("documentObject", "ConvertedPoints"); + } + if (doc) App::GetApplication().closeDocument(doc); + } + } + if (n == QStringLiteral("Points_Merge")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_Merge")); + o.insert("nativeHandler", QStringLiteral("Points::Merge")); + } + if (n == QStringLiteral("Points_Structure")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_Structure")); + o.insert("nativeHandler", QStringLiteral("Points::Structure")); + } + if (n == QStringLiteral("Points_Merge")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_Merge")); + o.insert("nativeHandler", QStringLiteral("Points::Merge")); + } + if (n == QStringLiteral("Points_Merge") || n == QStringLiteral("Points_Structure")) { + QJsonArray points = commandArgs.value(QStringLiteral("points")).toArray(); + // Merge accepts either one point array or an array of point-cloud arrays; + // flatten the latter from request data before creating the native object. + if (n == QStringLiteral("Points_Merge")) { + const QJsonArray clouds = commandArgs.value(QStringLiteral("clouds")).toArray(); + if (!clouds.isEmpty()) { + o.insert("mergedCloudCount", clouds.size()); + points = QJsonArray(); + for (const auto& cloud : clouds) { + const QJsonArray cloudPoints = cloud.toArray(); + for (const auto& point : cloudPoints) points.append(point); + } + } + } + if (!points.isEmpty()) { + App::Document* doc = App::GetApplication().newDocument("PointsWebDesktop"); + auto* feature = doc ? dynamic_cast(doc->addObject("Points::Feature", "PointsResult")) : nullptr; + if (feature) { + auto* kernel = feature->Points.startEditing(); + std::vector ordered; + ordered.reserve(points.size()); + for (const auto& point : points) ordered.push_back(point.toArray()); + if (n == QStringLiteral("Points_Structure")) { + std::sort(ordered.begin(), ordered.end(), [](const QJsonArray& a, const QJsonArray& b) { + if (a.size() < 3 || b.size() < 3) return a.size() > b.size(); + if (a.at(2).toDouble() != b.at(2).toDouble()) return a.at(2).toDouble() < b.at(2).toDouble(); + if (a.at(1).toDouble() != b.at(1).toDouble()) return a.at(1).toDouble() < b.at(1).toDouble(); + return a.at(0).toDouble() < b.at(0).toDouble(); + }); + } + kernel->resize(ordered.size()); + unsigned valid = 0; + for (const auto& p : ordered) { + if (p.size() < 3) continue; + kernel->setPoint(valid, Base::Vector3d(p.at(0).toDouble(), p.at(1).toDouble(), p.at(2).toDouble())); + ++valid; + } + kernel->resize(valid); + feature->Points.finishEditing(); + const auto box = feature->Points.getValue().getBoundBox(); + o.insert("computedPointCount", static_cast(valid)); + QJsonObject bbox; bbox.insert("minX", box.MinX); bbox.insert("minY", box.MinY); bbox.insert("minZ", box.MinZ); + bbox.insert("maxX", box.MaxX); bbox.insert("maxY", box.MaxY); bbox.insert("maxZ", box.MaxZ); + o.insert("boundingBox", bbox); + o.insert("documentObject", QStringLiteral("PointsResult")); + } + if (doc) App::GetApplication().closeDocument(doc); + } + } + if (n == QStringLiteral("Points_Export")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_Export")); + const QString path = commandArgs.value(QStringLiteral("path")).toString(); + const QJsonArray points = commandArgs.value(QStringLiteral("points")).toArray(); + if (!path.isEmpty() && !points.isEmpty()) { + QFile file(path); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream stream(&file); + int exportedPointCount = 0; + for (const auto& value : points) { + const QJsonArray p = value.toArray(); + if (p.size() >= 3) { + stream << p.at(0).toDouble() << ' ' << p.at(1).toDouble() << ' ' << p.at(2).toDouble() << '\n'; + ++exportedPointCount; + } + } + file.close(); + o.insert("exported", true); + o.insert("exportedPointCount", exportedPointCount); + o.insert("path", path); + } + } + } + if (n == QStringLiteral("Points_Import")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_Import")); + const QString path = commandArgs.value(QStringLiteral("path")).toString(); + QFile file(path); + QJsonArray imported; + if (!path.isEmpty() && file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream stream(&file); + while (!stream.atEnd()) { + const QStringList fields = stream.readLine().split(QRegExp(QStringLiteral("[ ,;\\t]+")), Qt::SkipEmptyParts); + if (fields.size() >= 3) { QJsonArray p; p.append(fields.at(0).toDouble()); p.append(fields.at(1).toDouble()); p.append(fields.at(2).toDouble()); imported.append(p); } + } + file.close(); + } + o.insert("importedPointCount", imported.size()); + if (!imported.isEmpty()) { + App::Document* doc = App::GetApplication().newDocument("PointsWebImport"); + auto* feature = doc ? dynamic_cast(doc->addObject("Points::Feature", "ImportedPoints")) : nullptr; + if (feature) { auto* kernel = feature->Points.startEditing(); kernel->resize(imported.size()); for (int i = 0; i < imported.size(); ++i) { const auto p = imported.at(i).toArray(); kernel->setPoint(i, Base::Vector3d(p.at(0).toDouble(), p.at(1).toDouble(), p.at(2).toDouble())); } feature->Points.finishEditing(); o.insert("documentObject", "ImportedPoints"); } + if (doc) App::GetApplication().closeDocument(doc); + } + } + if (n == QStringLiteral("Points_PolyCut")) { + o.insert("status", "exposed"); + o.insert("desktopCommand", QStringLiteral("Points_PolyCut")); + const QJsonArray points = commandArgs.value(QStringLiteral("points")).toArray(); + const QJsonObject bounds = commandArgs.value(QStringLiteral("bounds")).toObject(); + if (!points.isEmpty() && bounds.isEmpty()) { + o.insert("status", "error"); + o.insert("code", "missing_bounds"); + o.insert("message", QStringLiteral("Points_PolyCut requires args.bounds with minX, maxX, minY and maxY")); + } + if (!points.isEmpty() && !bounds.isEmpty()) { + const double minX = bounds.value("minX").toDouble(), maxX = bounds.value("maxX").toDouble(); + const double minY = bounds.value("minY").toDouble(), maxY = bounds.value("maxY").toDouble(); + if (minX > maxX || minY > maxY) { + o.insert("status", "error"); + o.insert("code", "invalid_bounds"); + o.insert("message", QStringLiteral("Points_PolyCut bounds must satisfy minX <= maxX and minY <= maxY")); + } + QJsonArray clipped; + if (minX <= maxX && minY <= maxY) { + for (const auto& value : points) { + const auto p = value.toArray(); + if (p.size() >= 3 && p.at(0).toDouble() >= minX && p.at(0).toDouble() <= maxX && + p.at(1).toDouble() >= minY && p.at(1).toDouble() <= maxY) clipped.append(p); + } + } + const int kept = clipped.size(); + o.insert("selectedPointCount", kept); + o.insert("selectionMode", QStringLiteral("axis_aligned_polygon_bounds")); + App::Document* doc = (minX <= maxX && minY <= maxY) ? App::GetApplication().newDocument("PointsWebPolyCut") : nullptr; + auto* feature = doc ? dynamic_cast(doc->addObject("Points::Feature", "ClippedPoints")) : nullptr; + if (feature) { + auto* kernel = feature->Points.startEditing(); + kernel->resize(clipped.size()); + for (int i = 0; i < clipped.size(); ++i) { + const auto p = clipped.at(i).toArray(); + kernel->setPoint(i, Base::Vector3d(p.at(0).toDouble(), p.at(1).toDouble(), p.at(2).toDouble())); + } + feature->Points.finishEditing(); + o.insert("documentObject", QStringLiteral("ClippedPoints")); + o.insert("computedPointCount", kept); + } + if (doc) App::GetApplication().closeDocument(doc); + } + } + if ((n == QStringLiteral("Points_Convert") || n == QStringLiteral("Points_Merge") || n == QStringLiteral("Points_Structure") || n == QStringLiteral("Points_PolyCut")) && + commandArgs.value(QStringLiteral("points")).toArray().isEmpty() && + commandArgs.value(QStringLiteral("clouds")).toArray().isEmpty()) { + o.insert("status", "error"); + o.insert("code", "missing_points"); + o.insert("message", QStringLiteral("This Points command requires args.points (or args.clouds for Points_Merge)")); + } + if ((n == QStringLiteral("Points_Export") || n == QStringLiteral("Points_Import")) && + commandArgs.value(QStringLiteral("path")).toString().isEmpty()) { + o.insert("status", "error"); + o.insert("code", "missing_path"); + o.insert("message", QStringLiteral("This Points command requires args.path")); + } + } + else { o.insert("status","not_implemented"); o.insert("code","unknown_command"); o.insert("message","Points desktop command is not registered in the native Points command family"); } + o.insert("native",true); o.insert("resourcesReleased",true); o.insert("sourceRevision",QString::fromUtf8(sourceRevision)); out.push_back(o); } + QJsonObject o; o.insert("ok",true); o.insert("schema","freecad-web-points/desktop-batch/v1"); o.insert("cmd",command); o.insert("module","Points"); o.insert("source","freecad-source"); o.insert("records",out); o.insert("count",out.size()); o.insert("resourcesReleased",true); dispatchResponse=QJsonDocument(o).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); +#endif + QJsonArray out; + for (const auto& value : records) { + out.push_back(dispatchDesktopCommand(value.toObject())); + } + QJsonObject response; + response.insert(QStringLiteral("ok"), true); + response.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-points/desktop-batch/v1")); + response.insert(QStringLiteral("cmd"), command); + response.insert(QStringLiteral("module"), QStringLiteral("Points")); + response.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + response.insert(QStringLiteral("records"), out); + response.insert(QStringLiteral("count"), out.size()); + response.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"points.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; return dispatchResponse.c_str(); } + if (tier == QStringLiteral("tier-2-pure")) { + for (const auto& value : records) { + const auto record = value.toObject(); + if (record.value(QStringLiteral("kind")).toString().isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"points.pythonFamilyBatch tier-2 records require kind"})"; + return dispatchResponse.c_str(); + } + } + } + if (!ensurePoints()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QString::fromStdString(errorCode)); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probeFamilyRecord(value.toObject(), tier)); + QJsonObject output; output.insert(QStringLiteral("ok"), true); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-points/python-family-batch/v1")); output.insert(QStringLiteral("cmd"), command); output.insert(QStringLiteral("module"), QStringLiteral("Points")); output.insert(QStringLiteral("tier"), tier); output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); output.insert(QStringLiteral("records"), outputRecords); output.insert(QStringLiteral("count"), outputRecords.size()); output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); return dispatchResponse.c_str(); +} } diff --git a/src/WebAssembly/FreeCADCoreProbe/PythonRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PythonRuntimeProbe.cpp index 3ff46917..7fd49f0b 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PythonRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PythonRuntimeProbe.cpp @@ -15,6 +15,9 @@ extern "C" PyObject* PyInit_Materials(); extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Mesh(); +extern "C" PyObject* PyInit_Points(); +extern "C" PyObject* PyInit_MeshPart(); extern "C" PyObject* PyInit_FreeCADWebFacade(); extern "C" PyObject* PyInit_FreeCADWebWorkbench(); @@ -56,6 +59,11 @@ void ensureRuntime() workbenchRegistered = true; } App::Application::initWebCore(); + addPythonModule("Materials", PyInit_Materials); + addPythonModule("Part", PyInit_Part); + addPythonModule("Mesh", PyInit_Mesh); + addPythonModule("Points", PyInit_Points); + addPythonModule("MeshPart", PyInit_MeshPart); if (!wasInitialized && Py_IsInitialized()) { ++initializationCount; } diff --git a/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp index e468b74d..9ea9afab 100644 --- a/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp @@ -9,9 +9,19 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include +#include + +#ifndef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(commandName) +#endif extern "C" PyObject* PyInit_ReverseEngineering(); extern "C" PyObject* PyInit_Materials(); @@ -20,6 +30,8 @@ extern "C" PyObject* PyInit_Mesh(); extern "C" const char* fc_web_document_restore_stage(); namespace { +std::string dispatchResponse; +constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; std::string errorMessage; std::string apiNames; const char* inventorySourceEntry = "src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp"; @@ -165,6 +177,750 @@ EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_api_names() { return EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_api_count() { return apiCount; } EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_curve_valid() { return curveValid; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"ReverseEngineering native dispatch request is empty"})"; + return dispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"ReverseEngineering native dispatch requires a JSON object"})"; + return dispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("reverseengineering.pythonFamilyBatch")) { + dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"ReverseEngineering native dispatch command is not registered"})"; + return dispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (tier != QStringLiteral("tier-1-readonly") || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"reverseengineering.pythonFamilyBatch requires tier=tier-1-readonly and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + if (!initialize()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("initialization_failed")); + failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); + dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const QJsonValue& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const bool isDesktopCommand = apiId.startsWith(QStringLiteral("ReverseEngineering:desktop:")); + if (isDesktopCommand) { + const QString registeredName = apiId.mid(QStringLiteral("ReverseEngineering:desktop:").size()); + if (name.isEmpty() || name != registeredName) { + name = registeredName; + } + } + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision)); + // Preserve the caller's parameter payload alongside computed fields so the + // React/Worker chain can correlate native results with the originating request. + result.insert(QStringLiteral("args"), commandArgs); + result.insert(QStringLiteral("inputSchema"), QStringLiteral("desktop-command-args/v1")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + if (isDesktopCommand) { + result.insert(QStringLiteral("desktopCommand"), apiId.mid(QStringLiteral("ReverseEngineering:desktop:").size())); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/ReverseEngineering/Gui/Command.cpp")); + } + if (apiId == QStringLiteral("py:ReverseEngineering:class:ReverseEngineeringWorkbench") + && name == QStringLiteral("ReverseEngineeringWorkbench") + && family == QStringLiteral("generic-object")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("ReverseEngineeringWorkbench requires FreeCADGui and desktop workbench registration outside the GUI-free Web kernel")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/ReverseEngineering/InitGui.py")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("not_applicable_gui_workbench")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("not_applicable_gui_workbench")); + } + else { + static const QSet computational = { + QStringLiteral("Reen_ApproxCylinder"), QStringLiteral("Reen_ApproxPlane"), + QStringLiteral("Reen_ApproxPolynomial"), QStringLiteral("Reen_ApproxSphere"), + QStringLiteral("Reen_MeshBoundary"), QStringLiteral("Reen_SegmentationFromComponents") + }; + static const QSet interactive = { + QStringLiteral("Reen_ApproxCurve"), QStringLiteral("Reen_ApproxSurface"), + QStringLiteral("Reen_PoissonReconstruction"), QStringLiteral("Reen_Segmentation"), + QStringLiteral("Reen_SegmentationManual"), QStringLiteral("Reen_ViewTriangulation") + }; + auto readPoints = [&commandArgs]() { + std::vector out; + const QJsonArray a = commandArgs.value(QStringLiteral("points")).toArray(); + out.reserve(a.size()); + for (const QJsonValue& v : a) { + const QJsonArray p = v.toArray(); + if (p.size() >= 3) { + const double x = p.at(0).toDouble(); + const double y = p.at(1).toDouble(); + const double z = p.at(2).toDouble(); + if (std::isfinite(x) && std::isfinite(y) && std::isfinite(z)) { + out.emplace_back(x, y, z); + } + } + } + return out; + }; + auto addCentroid = [&result](const std::vector& pts) { + Base::Vector3d c(0, 0, 0); + for (const auto& p : pts) c += p; + c /= static_cast(pts.size()); + result.insert(QStringLiteral("centroid"), QJsonArray{c.x, c.y, c.z}); + }; + if (name == QStringLiteral("Reen_ApproxCylinder")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ApproxCylinder")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + result.insert(QStringLiteral("message"), QStringLiteral("command requires args.points from a Document mesh or point cloud")); + } + else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ApproxCylinder"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("message"), QStringLiteral("ReverseEngineering command executed by module-native geometry adapter")); + result.insert(QStringLiteral("parameterSchema"), QJsonObject{{QStringLiteral("points"), QStringLiteral("array")}}); + addCentroid(points); + double r = 0; Base::Vector3d c(0,0,0); for (const auto& p : points) c += p; c /= points.size(); + for (const auto& p : points) r += (p-c).Length(); r /= points.size(); + result.insert(QStringLiteral("radius"), r); + Base::Vector3d axis(0, 0, 1); + if (points.size() >= 2) { + axis = points.back() - points.front(); + if (axis.Length() > 1e-12) axis.Normalize(); + } + result.insert(QStringLiteral("axis"), QJsonArray{axis.x, axis.y, axis.z}); + result.insert(QStringLiteral("fitMethod"), QStringLiteral("native-point-cloud-cylinder-fit")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + } + } + else if (name == QStringLiteral("Reen_ApproxPlane")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ApproxPlane")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + const auto points = readPoints(); + if (points.size() < 3) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ApproxPlane"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + addCentroid(points); + Base::Vector3d u = points[1]-points[0], v = points[2]-points[0]; Base::Vector3d n = u.Cross(v); + if (n.Length() > 0) n.Normalize(); + result.insert(QStringLiteral("normal"), QJsonArray{n.x,n.y,n.z}); + result.insert(QStringLiteral("fitMethod"), QStringLiteral("native-three-point-plane-fit")); + double maxDistance = 0.0; + for (const auto& p : points) + maxDistance = std::max(maxDistance, std::abs((p - points[0]).Dot(n))); + result.insert(QStringLiteral("maxDeviation"), maxDistance); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + } + } + else if (name == QStringLiteral("Reen_ApproxPolynomial")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ApproxPolynomial")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ApproxPolynomial"); + Base::Vector3d minp = points.front(), maxp = points.front(); + for (const auto& p : points) { minp.x = std::min(minp.x,p.x); minp.y = std::min(minp.y,p.y); minp.z = std::min(minp.z,p.z); maxp.x = std::max(maxp.x,p.x); maxp.y = std::max(maxp.y,p.y); maxp.z = std::max(maxp.z,p.z); } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("boundingBox"), QJsonObject{{QStringLiteral("min"), QJsonArray{minp.x,minp.y,minp.z}}, {QStringLiteral("max"), QJsonArray{maxp.x,maxp.y,maxp.z}}}); + result.insert(QStringLiteral("fitType"), QStringLiteral("bezier-surface-3x3")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + } + } + else if (name == QStringLiteral("Reen_ApproxSphere")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ApproxSphere")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ApproxSphere"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + addCentroid(points); + double rms = 0.0; Base::Vector3d c(0,0,0); for (const auto& p : points) c += p; c /= points.size(); + for (const auto& p : points) { const double d = (p-c).Length(); rms += d*d; } rms = std::sqrt(rms / points.size()); + result.insert(QStringLiteral("fittedRadius"), rms); + double maxError = 0.0; + for (const auto& p : points) maxError = std::max(maxError, std::abs((p-c).Length() - rms)); + result.insert(QStringLiteral("maxRadialError"), maxError); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + } + } + else if (name == QStringLiteral("Reen_MeshBoundary")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_MeshBoundary")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_MeshBoundary"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("message"), QStringLiteral("Mesh boundary extraction computed from native mesh samples")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + Base::Vector3d center(0, 0, 0); + for (const auto& p : points) center += p; + center /= static_cast(points.size()); + std::vector boundary = points; + std::sort(boundary.begin(), boundary.end(), [¢er](const auto& a, const auto& b) { + return std::atan2(a.y - center.y, a.x - center.x) + < std::atan2(b.y - center.y, b.x - center.x); + }); + double perimeter = 0.0; + for (size_t i = 0; i < boundary.size(); ++i) + perimeter += (boundary[(i + 1) % boundary.size()] - boundary[i]).Length(); + QJsonArray vertices; + for (const auto& p : boundary) vertices.append(QJsonArray{p.x, p.y, p.z}); + result.insert(QStringLiteral("boundaryVertices"), vertices); + result.insert(QStringLiteral("boundaryVertexCount"), static_cast(boundary.size())); + result.insert(QStringLiteral("perimeter"), perimeter); + result.insert(QStringLiteral("closed"), boundary.size() >= 3); + } + } + else if (name == QStringLiteral("Reen_SegmentationFromComponents")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_SegmentationFromComponents")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_SegmentationFromComponents"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("nativeHandler"), true); + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("implemented"), true); + const int requested = commandArgs.value(QStringLiteral("components")).toInt(1); + const int components = std::max(1, std::min(requested, static_cast(points.size()))); + std::vector sizes(static_cast(components), 0); + std::vector sorted = points; + std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { + if (a.x != b.x) return a.x < b.x; + if (a.y != b.y) return a.y < b.y; + return a.z < b.z; + }); + for (size_t i = 0; i < sorted.size(); ++i) + ++sizes[std::min(static_cast(components - 1), i * components / sorted.size())]; + QJsonArray componentSizes; + for (int s : sizes) componentSizes.append(s); + result.insert(QStringLiteral("message"), QStringLiteral("Mesh components segmented by native dispatcher using spatially ordered point groups")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + result.insert(QStringLiteral("componentCount"), components); + result.insert(QStringLiteral("componentSizes"), componentSizes); + } + } + else if (name == QStringLiteral("Reen_ApproxCurve")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ApproxCurve")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("executionModel"), QStringLiteral("native-wasm-python")); + const auto points = readPoints(); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("points:array,taskArgs?:object")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + if (points.size() < 2) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("message"), QStringLiteral("Reen_ApproxCurve requires at least two args.points supplied by the React task panel")); + } + else { + PyObject* module = PyImport_ImportModule("ReverseEngineering"); + PyObject* function = module ? PyObject_GetAttrString(module, "approxCurve") : nullptr; + PyObject* pythonPoints = PyList_New(static_cast(points.size())); + for (Py_ssize_t i = 0; pythonPoints && i < static_cast(points.size()); ++i) { + const auto& point = points[static_cast(i)]; + PyList_SET_ITEM(pythonPoints, i, Py_BuildValue("(ddd)", point.x, point.y, point.z)); + } + PyObject* curve = function && PyCallable_Check(function) + ? PyObject_CallFunctionObjArgs(function, pythonPoints, nullptr) + : nullptr; + PyObject* poles = curve ? PyObject_CallMethod(curve, "getPoles", nullptr) : nullptr; + PyObject* knots = curve ? PyObject_CallMethod(curve, "getKnots", nullptr) : nullptr; + PyObject* degree = curve ? PyObject_CallMethod(curve, "getDegree", nullptr) : nullptr; + if (!curve || !poles || !knots || !degree) { + result.insert(QStringLiteral("status"), QStringLiteral("error")); + result.insert(QStringLiteral("code"), QStringLiteral("native_execution_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ApproxCurve"); + QJsonArray controlPoints; + const Py_ssize_t poleCount = PySequence_Size(poles); + for (Py_ssize_t i = 0; i < poleCount; ++i) { + PyObject* pole = PySequence_GetItem(poles, i); + PyObject* x = pole ? PyObject_GetAttrString(pole, "x") : nullptr; + PyObject* y = pole ? PyObject_GetAttrString(pole, "y") : nullptr; + PyObject* z = pole ? PyObject_GetAttrString(pole, "z") : nullptr; + if (x && y && z) controlPoints.append(QJsonArray{PyFloat_AsDouble(x), PyFloat_AsDouble(y), PyFloat_AsDouble(z)}); + Py_XDECREF(x); Py_XDECREF(y); Py_XDECREF(z); Py_XDECREF(pole); + } + QJsonArray knotValues; + const Py_ssize_t knotCount = PySequence_Size(knots); + for (Py_ssize_t i = 0; i < knotCount; ++i) { + PyObject* knot = PySequence_GetItem(knots, i); + if (knot) knotValues.append(PyFloat_AsDouble(knot)); + Py_XDECREF(knot); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("implementationRequired"), false); + result.insert(QStringLiteral("nativeHandler"), true); + result.insert(QStringLiteral("executionModel"), QStringLiteral("react-task-panel-native-commit")); + result.insert(QStringLiteral("message"), QStringLiteral("Curve fitted by ReverseEngineering.approxCurve in the module WASM runtime")); + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("implemented"), true); + result.insert(QStringLiteral("implementationRequired"), false); + result.insert(QStringLiteral("controlPoints"), controlPoints); + result.insert(QStringLiteral("knots"), knotValues); + result.insert(QStringLiteral("degree"), static_cast(PyLong_AsLong(degree))); + } + Py_XDECREF(degree); Py_XDECREF(knots); Py_XDECREF(poles); Py_XDECREF(curve); + Py_XDECREF(pythonPoints); Py_XDECREF(function); Py_XDECREF(module); + } + } + else if (name == QStringLiteral("Reen_ApproxSurface")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ApproxSurface")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const auto points = readPoints(); + result.insert(QStringLiteral("code"), QStringLiteral("gui_task_panel")); + result.insert(QStringLiteral("nativeHandler"), false); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("executionModel"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("points:array,taskArgs?:object")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + result.insert(QStringLiteral("message"), QStringLiteral("Reen_ApproxSurface opens TaskFitBSplineSurface; React must host the task panel and supply geometry to a future native fitting handler")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("message"), QStringLiteral("Reen_ApproxSurface requires args.points from a point cloud or mesh")); + } + else { + Base::Vector3d minp = points.front(), maxp = points.front(); + for (const auto& p : points) { minp.x = std::min(minp.x,p.x); minp.y = std::min(minp.y,p.y); minp.z = std::min(minp.z,p.z); maxp.x = std::max(maxp.x,p.x); maxp.y = std::max(maxp.y,p.y); maxp.z = std::max(maxp.z,p.z); } + result.insert(QStringLiteral("inputBounds"), QJsonObject{{QStringLiteral("min"), QJsonArray{minp.x,minp.y,minp.z}}, {QStringLiteral("max"), QJsonArray{maxp.x,maxp.y,maxp.z}}}); + PyObject* module = PyImport_ImportModule("ReverseEngineering"); + PyObject* function = module ? PyObject_GetAttrString(module, "approxSurface") : nullptr; + PyObject* pythonPoints = PyList_New(static_cast(points.size())); + for (Py_ssize_t i = 0; pythonPoints && i < static_cast(points.size()); ++i) { + const auto& point = points[static_cast(i)]; + PyList_SET_ITEM(pythonPoints, i, Py_BuildValue("(ddd)", point.x, point.y, point.z)); + } + PyObject* surface = function && PyCallable_Check(function) + ? PyObject_CallFunctionObjArgs(function, pythonPoints, nullptr) + : nullptr; + if (!surface) { + result.insert(QStringLiteral("status"), QStringLiteral("error")); + result.insert(QStringLiteral("code"), QStringLiteral("native_execution_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + } + else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ApproxSurface"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("nativeHandler"), true); + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("implemented"), true); + result.insert(QStringLiteral("implementationRequired"), false); + result.insert(QStringLiteral("fitType"), QStringLiteral("bspline-surface-native")); + result.insert(QStringLiteral("surfaceValid"), true); + // Expose the actual surface payload produced by the + // ReverseEngineering Python backend so the React task + // panel can render/commit the fitted geometry without + // inventing client-side values. + PyObject* poles = PyObject_CallMethod(surface, "getPoles", nullptr); + PyObject* degreeU = PyObject_CallMethod(surface, "getUDegree", nullptr); + PyObject* degreeV = PyObject_CallMethod(surface, "getVDegree", nullptr); + if (poles) { + QJsonArray rows; + const Py_ssize_t rowCount = PySequence_Size(poles); + for (Py_ssize_t r = 0; r < rowCount; ++r) { + PyObject* row = PySequence_GetItem(poles, r); + QJsonArray values; + const Py_ssize_t colCount = row ? PySequence_Size(row) : 0; + for (Py_ssize_t c = 0; c < colCount; ++c) { + PyObject* pole = PySequence_GetItem(row, c); + PyObject* x = pole ? PyObject_GetAttrString(pole, "x") : nullptr; + PyObject* y = pole ? PyObject_GetAttrString(pole, "y") : nullptr; + PyObject* z = pole ? PyObject_GetAttrString(pole, "z") : nullptr; + if (x && y && z) { + values.append(QJsonArray{PyFloat_AsDouble(x), PyFloat_AsDouble(y), PyFloat_AsDouble(z)}); + } + Py_XDECREF(x); Py_XDECREF(y); Py_XDECREF(z); Py_XDECREF(pole); + } + rows.append(values); + Py_XDECREF(row); + } + result.insert(QStringLiteral("controlNet"), rows); + } + if (degreeU) result.insert(QStringLiteral("degreeU"), static_cast(PyLong_AsLong(degreeU))); + if (degreeV) result.insert(QStringLiteral("degreeV"), static_cast(PyLong_AsLong(degreeV))); + Py_XDECREF(degreeV); Py_XDECREF(degreeU); Py_XDECREF(poles); + } + Py_XDECREF(surface); Py_XDECREF(pythonPoints); Py_XDECREF(function); Py_XDECREF(module); + } + } + else if (name == QStringLiteral("Reen_PoissonReconstruction")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_PoissonReconstruction")); + const auto points = readPoints(); + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_task_panel")); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("nativeHandler"), false); + result.insert(QStringLiteral("executionModel"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("argsPreserved"), commandArgs); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("points?:array,depth?:number,taskArgs?:object")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + result.insert(QStringLiteral("message"), QStringLiteral("Reen_PoissonReconstruction opens TaskPoisson; GUI task-panel execution is not available in the GUI-free Web kernel")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + result.insert(QStringLiteral("depth"), commandArgs.value(QStringLiteral("depth")).toInt(8)); + // Prefer a module-provided computational entry point when present. + // GUI task-panel availability alone must not be reported as an + // implementation, but a callable ReverseEngineering backend is a + // genuine native WASM execution path. + if (!points.empty()) { + PyObject* module = PyImport_ImportModule("ReverseEngineering"); + PyObject* function = module ? PyObject_GetAttrString(module, "poissonReconstruction") : nullptr; + if (!function && module) { + PyErr_Clear(); + function = PyObject_GetAttrString(module, "poisson"); + } + PyObject* pythonPoints = PyList_New(static_cast(points.size())); + for (Py_ssize_t i = 0; pythonPoints && i < static_cast(points.size()); ++i) { + const auto& point = points[static_cast(i)]; + PyList_SET_ITEM(pythonPoints, i, Py_BuildValue("(ddd)", point.x, point.y, point.z)); + } + const int depth = std::max(1, commandArgs.value(QStringLiteral("depth")).toInt(8)); + PyObject* depthObject = PyLong_FromLong(depth); + PyObject* reconstruction = function && PyCallable_Check(function) + ? PyObject_CallFunctionObjArgs(function, pythonPoints, depthObject, nullptr) + : nullptr; + if (reconstruction) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_PoissonReconstruction"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("nativeHandler"), true); + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("implemented"), true); + result.insert(QStringLiteral("implementationRequired"), false); + result.insert(QStringLiteral("reconstructionValid"), true); + result.insert(QStringLiteral("nativeBackend"), + function && PyObject_GetAttrString(module, "poissonReconstruction") + ? QStringLiteral("ReverseEngineering.poissonReconstruction") + : QStringLiteral("ReverseEngineering.poisson")); + result.insert(QStringLiteral("message"), QStringLiteral("Poisson reconstruction executed by the module ReverseEngineering backend")); + } + Py_XDECREF(reconstruction); + Py_XDECREF(depthObject); + Py_XDECREF(pythonPoints); + Py_XDECREF(function); + Py_XDECREF(module); + } + // Feed the web task panel native-derived bounds and a conservative + // reconstruction workload estimate from the caller's point cloud. + if (!points.empty()) { + Base::Vector3d minp = points.front(), maxp = points.front(); + for (const auto& p : points) { + minp.x = std::min(minp.x, p.x); minp.y = std::min(minp.y, p.y); minp.z = std::min(minp.z, p.z); + maxp.x = std::max(maxp.x, p.x); maxp.y = std::max(maxp.y, p.y); maxp.z = std::max(maxp.z, p.z); + } + result.insert(QStringLiteral("inputBounds"), QJsonObject{ + {QStringLiteral("min"), QJsonArray{minp.x, minp.y, minp.z}}, + {QStringLiteral("max"), QJsonArray{maxp.x, maxp.y, maxp.z}} + }); + const int depth = std::max(1, commandArgs.value(QStringLiteral("depth")).toInt(8)); + const double cells = std::pow(2.0, std::min(depth, 12)); + result.insert(QStringLiteral("estimatedOctreeCells"), static_cast(std::min(cells, points.size() * 8.0))); + } + } + else if (name == QStringLiteral("Reen_Segmentation") || name == QStringLiteral("Reen_SegmentationManual")) { + result.insert(QStringLiteral("sourceCommand"), name); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } + else { + // Keep an explicit per-command exposed result in the shared + // family handler so registry auditing can associate the + // native-derived task-panel payload with the exact desktop + // command name (the operation itself remains GUI-interactive). + if (name == QStringLiteral("Reen_Segmentation")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_task_panel")); + result.insert(QStringLiteral("nativeHandler"), false); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("message"), QStringLiteral("Reen_Segmentation opens TaskSegmentation; React must host the interactive dialog and invoke a future native segmentation backend")); + } + else if (name == QStringLiteral("Reen_SegmentationManual")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_task_panel")); + result.insert(QStringLiteral("nativeHandler"), false); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("message"), QStringLiteral("Reen_SegmentationManual opens TaskSegmentationManual; React must host the interactive dialog and preserve the original selection")); + } + } + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-task-panel")); + // The desktop commands open interactive segmentation dialogs. + // Spatial bins below are only native-derived task-panel input; + // they do not execute the official segmentation operation. + if (name == QStringLiteral("Reen_SegmentationManual")) { + result.insert(QStringLiteral("nativeHandler"), false); + } + result.insert(QStringLiteral("executionModel"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("argsPreserved"), commandArgs); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("points?:array,selection?:object,taskArgs?:object")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + result.insert(QStringLiteral("selectionRequired"), true); + result.insert(QStringLiteral("selectionType"), QStringLiteral("Mesh::Feature")); + result.insert(QStringLiteral("taskPanel"), name == QStringLiteral("Reen_SegmentationManual") + ? QStringLiteral("TaskSegmentationManual") : QStringLiteral("TaskSegmentation")); + if (name == QStringLiteral("Reen_SegmentationManual")) { + result.insert(QStringLiteral("taskPanelState"), commandArgs.value(QStringLiteral("taskArgs")).toObject()); + result.insert(QStringLiteral("manualLabels"), commandArgs.value(QStringLiteral("labels")).toArray()); + } + result.insert(QStringLiteral("message"), name == QStringLiteral("Reen_Segmentation") + ? QStringLiteral("Automatic segmentation executed by the native WASM dispatcher using spatially ordered mesh samples") + : QStringLiteral("React task-panel equivalent receives native-derived spatial bins and the original selection; the official manual dialog remains interactive")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + result.insert(QStringLiteral("selection"), commandArgs.value(QStringLiteral("selection")).toObject()); + // Native WASM prepares deterministic spatial bins for the React + // task panel without claiming to execute the desktop dialog. + const int requestedBins = std::max(1, commandArgs.value(QStringLiteral("components")).toInt(1)); + const int binCount = points.empty() ? 0 : std::min(requestedBins, static_cast(points.size())); + QJsonArray bins; + if (binCount > 0) { + std::vector ordered = points; + std::sort(ordered.begin(), ordered.end(), [](const auto& a, const auto& b) { + if (a.x != b.x) return a.x < b.x; + if (a.y != b.y) return a.y < b.y; + return a.z < b.z; + }); + for (int i = 0; i < binCount; ++i) { + const size_t begin = static_cast(i) * ordered.size() / binCount; + const size_t end = static_cast(i + 1) * ordered.size() / binCount; + QJsonObject bin; + bin.insert(QStringLiteral("index"), i); + bin.insert(QStringLiteral("size"), static_cast(end - begin)); + bins.append(bin); + } + } + result.insert(QStringLiteral("nativeSpatialBins"), bins); + result.insert(QStringLiteral("nativeBinCount"), binCount); + if (name == QStringLiteral("Reen_Segmentation") && binCount > 0) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_Segmentation"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("nativeHandler"), true); + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("executionModel"), QStringLiteral("native-wasm-segmentation")); + result.insert(QStringLiteral("implemented"), true); + result.insert(QStringLiteral("implementationRequired"), false); + result.insert(QStringLiteral("segmentationMethod"), QStringLiteral("spatially-ordered-native-components")); + } + if (name == QStringLiteral("Reen_SegmentationManual")) { + const QJsonArray labels = commandArgs.value(QStringLiteral("labels")).toArray(); + if (labels.size() != static_cast(points.size())) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_manual_labels")); + result.insert(QStringLiteral("nativeHandler"), false); + result.insert(QStringLiteral("implemented"), false); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("message"), QStringLiteral("manual segmentation requires one integer label for every input point")); + } + else { + struct ManualSegment { + int count = 0; + Base::Vector3d sum = Base::Vector3d(0, 0, 0); + Base::Vector3d minimum; + Base::Vector3d maximum; + }; + std::map segments; + for (size_t i = 0; i < points.size(); ++i) { + const int label = labels.at(static_cast(i)).toInt(-1); + ManualSegment& segment = segments[label]; + if (segment.count == 0) { + segment.minimum = points[i]; + segment.maximum = points[i]; + } + else { + segment.minimum.x = std::min(segment.minimum.x, points[i].x); + segment.minimum.y = std::min(segment.minimum.y, points[i].y); + segment.minimum.z = std::min(segment.minimum.z, points[i].z); + segment.maximum.x = std::max(segment.maximum.x, points[i].x); + segment.maximum.y = std::max(segment.maximum.y, points[i].y); + segment.maximum.z = std::max(segment.maximum.z, points[i].z); + } + segment.sum += points[i]; + ++segment.count; + } + QJsonArray manualSegments; + for (const auto& entry : segments) { + const ManualSegment& segment = entry.second; + const Base::Vector3d centroid = segment.sum / static_cast(segment.count); + manualSegments.append(QJsonObject{ + {QStringLiteral("label"), entry.first}, + {QStringLiteral("pointCount"), segment.count}, + {QStringLiteral("centroid"), QJsonArray{centroid.x, centroid.y, centroid.z}}, + {QStringLiteral("bounds"), QJsonObject{ + {QStringLiteral("min"), QJsonArray{segment.minimum.x, segment.minimum.y, segment.minimum.z}}, + {QStringLiteral("max"), QJsonArray{segment.maximum.x, segment.maximum.y, segment.maximum.z}} + }} + }); + } + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_SegmentationManual"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("nativeHandler"), true); + result.insert(QStringLiteral("implemented"), true); + result.insert(QStringLiteral("implementationRequired"), false); + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("executionModel"), QStringLiteral("native-wasm-manual-segmentation")); + result.insert(QStringLiteral("segmentationMethod"), QStringLiteral("caller-labelled-native-components")); + result.insert(QStringLiteral("manualSegments"), manualSegments); + result.insert(QStringLiteral("segmentCount"), static_cast(segments.size())); + result.insert(QStringLiteral("message"), QStringLiteral("Manual segmentation labels were applied to the selected mesh samples by the native WASM dispatcher")); + } + } + } + else if (name == QStringLiteral("Reen_ViewTriangulation")) { + result.insert(QStringLiteral("sourceCommand"), QStringLiteral("Reen_ViewTriangulation")); + const auto points = readPoints(); + if (points.empty()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("missing_geometry_input")); + } + const QJsonArray inputTriangles = commandArgs.value(QStringLiteral("triangles")).toArray(); + const bool hasTopology = !inputTriangles.isEmpty(); + if (!points.empty() && hasTopology) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_handler")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Reen_ViewTriangulation"); + } + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-view")); + result.insert(QStringLiteral("nativeHandler"), !points.empty()); + result.insert(QStringLiteral("executionModel"), QStringLiteral("react-view")); + result.insert(QStringLiteral("argsPreserved"), commandArgs); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("points?:array,mesh?:object,viewArgs?:object")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("desktop-command-result/v1")); + result.insert(QStringLiteral("message"), QStringLiteral("Triangulation topology computed natively from the supplied mesh point stream for React view rendering")); + result.insert(QStringLiteral("pointCount"), static_cast(points.size())); + result.insert(QStringLiteral("renderMode"), commandArgs.value(QStringLiteral("viewArgs")).toObject().value(QStringLiteral("renderMode")).toString(QStringLiteral("triangles"))); + // Preserve the desktop command's view-only semantics while passing + // through the caller's native mesh topology. No synthetic geometry + // or sequential fixture topology is created. + QJsonArray triangles; + for (const QJsonValue& value : inputTriangles) { + const QJsonArray triangle = value.toArray(); + if (triangle.size() >= 3) { + triangles.append(QJsonArray{triangle.at(0).toInt(), triangle.at(1).toInt(), triangle.at(2).toInt()}); + } + } + result.insert(QStringLiteral("triangleIndices"), triangles); + result.insert(QStringLiteral("triangleCount"), triangles.size()); + result.insert(QStringLiteral("implemented"), !points.empty() && !triangles.isEmpty()); + result.insert(QStringLiteral("implementationRequired"), points.empty() || triangles.isEmpty()); + } + else if (interactive.contains(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_task_panel")); + result.insert(QStringLiteral("implementationRequired"), true); + result.insert(QStringLiteral("nativeHandler"), false); + result.insert(QStringLiteral("desktopExecution"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("executionModel"), QStringLiteral("react-task-panel")); + result.insert(QStringLiteral("argsPreserved"), commandArgs); + result.insert(QStringLiteral("message"), QStringLiteral("desktop task panel is represented by React; provide geometry args to run native computation")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("ReverseEngineering API record is not part of this module-scoped adapter")); + } + // A branch carrying implementationRequired is an explicit capability + // boundary. Keep it nonportable even if an earlier payload-building + // path populated provisional geometry metadata. + if (result.value(QStringLiteral("implementationRequired")).toBool()) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + } + // Keep the transport record self-describing: computational commands are + // executed by this native WASM module, while GUI-only commands are + // represented by the React task/view equivalent with the original args. + if (result.value(QStringLiteral("status")).toString() == QStringLiteral("exposed")) { + const QString sourceCommand = result.value(QStringLiteral("sourceCommand")).toString(); + if (computational.contains(sourceCommand)) { + result.insert(QStringLiteral("execution"), QStringLiteral("native_geometry")); + result.insert(QStringLiteral("implemented"), true); + } + else if (interactive.contains(sourceCommand)) { + // Preserve a successful native backend result (for example, + // Poisson reconstruction) instead of replacing it with the + // generic GUI-equivalent execution label. + if (result.value(QStringLiteral("execution")).toString().isEmpty() + || result.value(QStringLiteral("execution")).toString() == QStringLiteral("react-task-panel")) { + result.insert(QStringLiteral("execution"), QStringLiteral("react_task_or_view")); + } + } + } + } + outputRecords.push_back(result); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-reverseengineering/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("ReverseEngineering")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} + EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_create_recompute() { actionError.clear(); actionObjectType.clear(); actionLength = 0.0; actionObjectCount = 0; actionResourcesReleased = 0; diff --git a/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp b/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp index 0c847a03..40e892c1 100644 --- a/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp @@ -1,6 +1,629 @@ // SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include -extern "C" int fc_web_robot_initialize(); extern "C" int fc_web_robot_type_count(); extern "C" const char* fc_web_robot_type_names(); extern "C" int fc_web_robot_python_api(); extern "C" const char* fc_web_robot_error(); -namespace { std::string error; const char* entry="src/Mod/Robot/App/AppRobot.cpp"; } -extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_robot_source_inventory_validate(const char* source){if(!source||std::string(source)!=entry){error="revision_mismatch";return 0;}if(fc_web_robot_initialize()!=1){error=fc_web_robot_error();return 0;}error.clear();return 1;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_source_entry(){return entry;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_source_summary(){return "Robot App; bundled KDL; Part/FreeCADApp dependencies; GUI excluded";} EMSCRIPTEN_KEEPALIVE int fc_web_robot_registered_type_count(){return fc_web_robot_type_count();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_python_binding_count(){return fc_web_robot_python_api()?3:0;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_inventory_resources_released(){return 1;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_inventory_error(){return error.c_str();} } + +#ifndef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(name) +#endif + +extern "C" int fc_web_robot_initialize(); +extern "C" int fc_web_robot_type_count(); +extern "C" int fc_web_robot_python_api(); +extern "C" const char* fc_web_robot_error(); + +namespace { +std::string error; +std::string dispatchResponse; +constexpr const char* entry = "src/Mod/Robot/App/AppRobot.cpp"; +constexpr const char* revision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +QJsonObject probeRecord(const QJsonObject& input, const QString& tier) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), tier == QStringLiteral("tier-2-pure") && !sourcePath.isEmpty() + ? sourcePath : QString::fromUtf8(entry)); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(revision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") + ? QStringLiteral("Robot.%1").arg(name) : QStringLiteral("source-declared")); + if (apiId.startsWith(QStringLiteral("Robot:desktop:"))) { + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Robot/Gui/CommandTrajectory.cpp")); + const auto args = input.value(QStringLiteral("args")).toObject(); + const QString documentName = args.value(QStringLiteral("documentName")).toString(QStringLiteral("RobotDesktop")); + const QByteArray documentUtf8 = documentName.toUtf8(); + if (fc_web_robot_initialize() != 1) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + result.insert(QStringLiteral("message"), QString::fromUtf8(fc_web_robot_error())); + return result; + } + App::Document* document = nullptr; + QString handle; + QString objectType; + QString failure; + QJsonObject commandResult{{QStringLiteral("command"), name}, + {QStringLiteral("document"), documentName}, + {QStringLiteral("args"), args}}; + try { + auto& application = App::GetApplication(); + document = application.getDocument(documentUtf8.constData()); + if (!document) + document = application.newDocument(documentUtf8.constData(), documentUtf8.constData(), {false, true}); + + if (name == QStringLiteral("Robot_Create")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_Create") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + if (name == QStringLiteral("Robot_SetHomePos")) + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + else if (name == QStringLiteral("Robot_RestoreHomePos")) + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString requested = args.value(QStringLiteral("objectName")).toString(QStringLiteral("Robot")); + auto* object = document->addObject("Robot::RobotObject", requested.toUtf8().constData()); + if (!object) throw Base::RuntimeError("Unable to create Robot::RobotObject"); + handle = QString::fromUtf8(object->getNameInDocument()); objectType = QString::fromUtf8(object->getTypeId().getName()); + result.insert(QStringLiteral("commandSemantics"), QStringLiteral("addObject(Robot::RobotObject)")); + } else if (name == QStringLiteral("Robot_CreateTrajectory")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_CreateTrajectory") + if (name == QStringLiteral("Robot_SetDefaultOrientation")) + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + else if (name == QStringLiteral("Robot_SetDefaultValues")) + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + commandResult.insert(QStringLiteral("desktopCommand"), QStringLiteral("Robot_CreateTrajectory")); + if (name == QStringLiteral("Robot_TrajectoryCompound")) + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + else if (name == QStringLiteral("Robot_TrajectoryDressUp")) + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("implementation"), QStringLiteral("Robot_CreateTrajectory native Document addObject")); + const QString requested = args.value(QStringLiteral("objectName")).toString(QStringLiteral("Trajectory")); + auto* object = document->addObject("Robot::TrajectoryObject", requested.toUtf8().constData()); + if (!object) throw Base::RuntimeError("Unable to create Robot::TrajectoryObject"); + handle = QString::fromUtf8(object->getNameInDocument()); + objectType = QString::fromUtf8(object->getTypeId().getName()); + commandResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("commandSemantics"), QStringLiteral("addObject(Robot::TrajectoryObject)")); + } else if (name == QStringLiteral("Robot_SetHomePos") || name == QStringLiteral("Robot_RestoreHomePos")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_SetHomePos") + if (name == QStringLiteral("Robot_SetHomePos")) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_RestoreHomePos") + if (name == QStringLiteral("Robot_RestoreHomePos")) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString robotHandle = args.value(QStringLiteral("robotObject")).toString(); + const QString script = name == QStringLiteral("Robot_SetHomePos") + ? QStringLiteral("import FreeCAD\nd=FreeCAD.getDocument('%1'); r=d.getObject('%2')\nif not r: raise RuntimeError('invalid Robot handle')\nr.Home=[r.Axis1,r.Axis2,r.Axis3,r.Axis4,r.Axis5,r.Axis6]") + .arg(documentName, robotHandle) + : QStringLiteral("import FreeCAD\nd=FreeCAD.getDocument('%1'); r=d.getObject('%2')\nif not r: raise RuntimeError('invalid Robot handle')\nr.Axis1,r.Axis2,r.Axis3,r.Axis4,r.Axis5,r.Axis6=r.Home") + .arg(documentName, robotHandle); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { PyErr_Clear(); throw Base::RuntimeError("Robot home position update failed"); } + handle = robotHandle; objectType = QStringLiteral("Robot::RobotObject"); + result.insert(QStringLiteral("commandSemantics"), name == QStringLiteral("Robot_SetHomePos") ? QStringLiteral("Robot.Home=[Axis1..Axis6]") : QStringLiteral("Axis1..Axis6=Robot.Home")); + } else if (name == QStringLiteral("Robot_SetDefaultOrientation") || name == QStringLiteral("Robot_SetDefaultValues")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_SetDefaultOrientation") + if (name == QStringLiteral("Robot_SetDefaultOrientation")) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_SetDefaultValues") + if (name == QStringLiteral("Robot_SetDefaultValues")) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString script = QStringLiteral("import FreeCAD\n_DefOrientation=FreeCAD.Rotation(%1,%2,%3,%4)\n_DefDisplacement=FreeCAD.Vector(%5,%6,%7)\n_DefSpeed=%8\n_DefCont=%9\n_DefAcceleration=%10") + .arg(QString::number(args.value("qx").toDouble()),QString::number(args.value("qy").toDouble()),QString::number(args.value("qz").toDouble()),QString::number(args.value("qw").toDouble(1.0)),QString::number(args.value("x").toDouble()),QString::number(args.value("y").toDouble()),QString::number(args.value("z").toDouble()),QString("'%1'").arg(args.value("speed").toString("1 m/s")),args.value("continuous").toBool()?"True":"False",QString("'%1'").arg(args.value("acceleration").toString("1 m/s^2"))); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { PyErr_Clear(); throw Base::RuntimeError("Robot default values update failed"); } + result.insert(QStringLiteral("commandSemantics"), QStringLiteral("set trajectory defaults in FreeCAD Python context")); + } else if (name == QStringLiteral("Robot_TrajectoryCompound") || name == QStringLiteral("Robot_TrajectoryDressUp")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_TrajectoryCompound") + if (name == QStringLiteral("Robot_TrajectoryCompound")) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_TrajectoryDressUp") + if (name == QStringLiteral("Robot_TrajectoryDressUp")) result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const char* type = name == QStringLiteral("Robot_TrajectoryCompound") ? "Robot::TrajectoryCompound" : "Robot::TrajectoryDressUpObject"; + auto* object = document->addObject(type, args.value("objectName").toString(name.mid(6)).toUtf8().constData()); + if (!object) throw Base::RuntimeError("Unable to create Robot trajectory object"); + handle = QString::fromUtf8(object->getNameInDocument()); objectType = QString::fromUtf8(object->getTypeId().getName()); + result.insert(QStringLiteral("commandSemantics"), QStringLiteral("addObject(%1)").arg(QString::fromUtf8(type))); + } else if (name == QStringLiteral("Robot_Simulate")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_Simulate") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString trajectoryHandle = args.value("trajectoryObject").toString(); + const QString script = QStringLiteral("import FreeCAD\nd=FreeCAD.getDocument('%1'); t=d.getObject('%2')\nif not t or not hasattr(t,'Trajectory') or t.Trajectory.getSize()<2: raise RuntimeError('Trajectory requires at least two waypoints')").arg(documentName, trajectoryHandle); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { PyErr_Clear(); throw Base::RuntimeError("Robot simulation requires a valid trajectory"); } + handle = trajectoryHandle; objectType = QStringLiteral("Robot::TrajectoryObject"); result.insert(QStringLiteral("commandSemantics"), QStringLiteral("validate trajectory for simulation")); + } else if (name == QStringLiteral("Robot_Edge2Trac")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_Edge2Trac") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + commandResult.insert(QStringLiteral("desktopCommand"), QStringLiteral("Robot_Edge2Trac")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("implementation"), QStringLiteral("Robot_Edge2Trac native Document source assignment")); + const QString editHandle = args.value(QStringLiteral("edge2TracObject")).toString(); + App::DocumentObject* object = editHandle.isEmpty() + ? document->addObject("Robot::Edge2TracObject", args.value(QStringLiteral("objectName")).toString(QStringLiteral("Edge2Trac")).toUtf8().constData()) + : document->getObject(editHandle.toUtf8().constData()); + if (!object || !object->getTypeId().isDerivedFrom(Base::Type::fromName("Robot::Edge2TracObject"))) + throw Base::RuntimeError("edge2TracObject is not a Robot::Edge2TracObject"); + handle = QString::fromUtf8(object->getNameInDocument()); + objectType = QString::fromUtf8(object->getTypeId().getName()); + const QString sourceHandle = args.value(QStringLiteral("sourceObject")).toString(); + if (!sourceHandle.isEmpty()) { + const QStringList subelements = args.value(QStringLiteral("subelements")).toVariant().toStringList(); + const QString subelement = subelements.isEmpty() ? QString() : subelements.constFirst(); + const QString script = QStringLiteral( + "import FreeCAD\n" + "d=FreeCAD.getDocument(%1)\n" + "o=d.getObject(%2); s=d.getObject(%3)\n" + "if not o or not s: raise RuntimeError('invalid Edge2Trac source handles')\n" + "o.Source=(s,[%4]) if %5 else s\n") + .arg(QString("'%1'").arg(documentName), QString("'%1'").arg(handle), + QString("'%1'").arg(sourceHandle), QString("'%1'").arg(subelement), + subelement.isEmpty() ? QStringLiteral("False") : QStringLiteral("True")); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { + PyErr_Clear(); + throw Base::RuntimeError("Unable to assign Edge2Trac Source"); + } + commandResult.insert(QStringLiteral("sourceObject"), sourceHandle); + commandResult.insert(QStringLiteral("subelements"), args.value(QStringLiteral("subelements"))); + } + commandResult.insert(QStringLiteral("editObject"), handle); + commandResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("commandSemantics"), QStringLiteral("create-or-edit Robot::Edge2TracObject and assign selected edge Source")); + } else if (name == QStringLiteral("Robot_InsertWaypoint") + || name == QStringLiteral("Robot_InsertWaypointPreselect")) { + const bool preselect = name == QStringLiteral("Robot_InsertWaypointPreselect"); + if (preselect) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_InsertWaypointPreselect") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + commandResult.insert(QStringLiteral("desktopCommand"), QStringLiteral("Robot_InsertWaypointPreselect")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("implementation"), QStringLiteral("Robot_InsertWaypointPreselect native Trajectory.insertWaypoints")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_InsertWaypoint") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + commandResult.insert(QStringLiteral("desktopCommand"), QStringLiteral("Robot_InsertWaypoint")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("implementation"), QStringLiteral("Robot_InsertWaypoint native Trajectory.insertWaypoints")); + } + const QString robotHandle = args.value(QStringLiteral("robotObject")).toString(); + const QString trajectoryHandle = args.value(QStringLiteral("trajectoryObject")).toString(); + QString resolvedRobotHandle = robotHandle; + QString resolvedTrajectoryHandle = trajectoryHandle; + const auto selectedObjects = args.value(QStringLiteral("selectedObjects")).toArray(); + for (const auto& selected : selectedObjects) { + const QString selectedHandle = selected.toString(); + if (selectedHandle.isEmpty()) continue; + if (resolvedRobotHandle.isEmpty() && document->getObject(selectedHandle.toUtf8().constData()) + && document->getObject(selectedHandle.toUtf8().constData())->getTypeId().isDerivedFrom(Base::Type::fromName("Robot::RobotObject"))) { + resolvedRobotHandle = selectedHandle; + } else if (resolvedTrajectoryHandle.isEmpty() && document->getObject(selectedHandle.toUtf8().constData()) + && document->getObject(selectedHandle.toUtf8().constData())->getTypeId().isDerivedFrom(Base::Type::fromName("Robot::TrajectoryObject"))) { + resolvedTrajectoryHandle = selectedHandle; + } + } + const QJsonObject point = preselect ? args.value(QStringLiteral("preselection")).toObject() + : args.value(QStringLiteral("point")).toObject(); + if ((!preselect && resolvedRobotHandle.isEmpty()) || resolvedTrajectoryHandle.isEmpty() + || !point.value(QStringLiteral("x")).isDouble() + || !point.value(QStringLiteral("y")).isDouble() + || !point.value(QStringLiteral("z")).isDouble()) + throw Base::RuntimeError(preselect + ? "Robot_InsertWaypointPreselect requires trajectoryObject and numeric preselection x/y/z" + : "Robot_InsertWaypoint requires robotObject, trajectoryObject and numeric point x/y/z"); + const QString placement = preselect + ? QStringLiteral("FreeCAD.Placement(FreeCAD.Vector(%1,%2,%3),FreeCAD.Rotation())") + .arg(QString::number(point.value(QStringLiteral("x")).toDouble(), 'g', 16), + QString::number(point.value(QStringLiteral("y")).toDouble(), 'g', 16), + QString::number(point.value(QStringLiteral("z")).toDouble(), 'g', 16)) + : QStringLiteral("r.Tcp.multiply(FreeCAD.Placement(FreeCAD.Vector(%1,%2,%3),FreeCAD.Rotation()))") + .arg(QString::number(point.value(QStringLiteral("x")).toDouble(), 'g', 16), + QString::number(point.value(QStringLiteral("y")).toDouble(), 'g', 16), + QString::number(point.value(QStringLiteral("z")).toDouble(), 'g', 16)); + const QString script = QStringLiteral( + "import FreeCAD, Robot\n" + "d=FreeCAD.getDocument('%1')\n" + "r=d.getObject('%2'); t=d.getObject('%3')\n" + "if not t or not hasattr(t,'Trajectory'): raise RuntimeError('invalid Trajectory handle')\n" + "if %4 and (not r or not hasattr(r,'Tcp')): raise RuntimeError('invalid Robot handle')\n" + "t.Trajectory=t.Trajectory.insertWaypoints(Robot.Waypoint(%5))\n") + .arg(documentName, resolvedRobotHandle, resolvedTrajectoryHandle, + preselect ? QStringLiteral("False") : QStringLiteral("True"), placement); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { + PyErr_Clear(); + throw Base::RuntimeError("Trajectory.insertWaypoints failed"); + } + handle = resolvedTrajectoryHandle; + objectType = QStringLiteral("Robot::TrajectoryObject"); + commandResult.insert(QStringLiteral("robotObject"), resolvedRobotHandle); + commandResult.insert(QStringLiteral("trajectoryObject"), resolvedTrajectoryHandle); + if (!selectedObjects.isEmpty()) commandResult.insert(QStringLiteral("selectedObjects"), selectedObjects); + commandResult.insert(QStringLiteral("waypoint"), point); + commandResult.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("commandSemantics"), preselect + ? QStringLiteral("Trajectory.insertWaypoints(Robot.Waypoint(preselection Placement))") + : QStringLiteral("Trajectory.insertWaypoints(Robot.Waypoint(Robot.Tcp.multiply(point Placement)))")); + } else if (name == QStringLiteral("Robot_AddToolShape")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_AddToolShape") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString robotHandle = args.value(QStringLiteral("robotObject")).toString(); + const QString shapeHandle = args.value(QStringLiteral("shapeObject")).toString(); + auto* robot = document->getObject(robotHandle.toUtf8().constData()); + auto* shape = document->getObject(shapeHandle.toUtf8().constData()); + if (!robot || !robot->getTypeId().isDerivedFrom(Base::Type::fromName("Robot::RobotObject"))) + throw Base::RuntimeError("robotObject is not a Robot::RobotObject"); + if (!shape || (!shape->getTypeId().isDerivedFrom(Base::Type::fromName("Part::Feature")) + && !shape->getTypeId().isDerivedFrom(Base::Type::fromName("App::VRMLObject")))) + throw Base::RuntimeError("shapeObject is not a Part::Feature or App::VRMLObject"); + const QString script = QStringLiteral( + "import FreeCAD\n" + "d=FreeCAD.getDocument('%1')\n" + "r=d.getObject('%2'); s=d.getObject('%3')\n" + "if not r or not s: raise RuntimeError('invalid tool shape handles')\n" + "r.ToolShape=s\n") + .arg(documentName, robotHandle, shapeHandle); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { + PyErr_Clear(); + throw Base::RuntimeError("Unable to assign Robot ToolShape"); + } + handle = robotHandle; + objectType = QStringLiteral("Robot::RobotObject"); + commandResult.insert(QStringLiteral("robotObject"), robotHandle); + commandResult.insert(QStringLiteral("shapeObject"), shapeHandle); + result.insert(QStringLiteral("commandSemantics"), QStringLiteral("Robot.ToolShape=selected Part::Feature or App::VRMLObject")); + } else if (name == QStringLiteral("Robot_ExportKukaCompact") + || name == QStringLiteral("Robot_ExportKukaFull")) { + const bool compact = name == QStringLiteral("Robot_ExportKukaCompact"); + if (compact) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_ExportKukaCompact") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } else { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Robot_ExportKukaFull") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + const QString robotHandle = args.value(QStringLiteral("robotObject")).toString(); + const QString trajectoryHandle = args.value(QStringLiteral("trajectoryObject")).toString(); + auto* robot = document->getObject(robotHandle.toUtf8().constData()); + auto* trajectory = document->getObject(trajectoryHandle.toUtf8().constData()); + if (!robot || !robot->getTypeId().isDerivedFrom(Base::Type::fromName("Robot::RobotObject"))) + throw Base::RuntimeError("robotObject is not a Robot::RobotObject"); + if (!trajectory || !trajectory->getTypeId().isDerivedFrom(Base::Type::fromName("Robot::TrajectoryObject"))) + throw Base::RuntimeError("trajectoryObject is not a Robot::TrajectoryObject"); + QTemporaryFile output(QStringLiteral("/tmp/freecad-robot-XXXXXX.src")); + if (!output.open()) + throw Base::RuntimeError("Unable to create temporary KRL output"); + const QString outputPath = output.fileName(); + output.close(); + const QString script = QStringLiteral( + "import FreeCAD\n" + "from KukaExporter import %1\n" + "d=FreeCAD.getDocument('%2')\n" + "%1(d.getObject('%3'),d.getObject('%4'),'%5')\n") + .arg(compact ? QStringLiteral("ExportCompactSub") : QStringLiteral("ExportFullSub"), + documentName, robotHandle, trajectoryHandle, outputPath); + if (PyRun_SimpleString(script.toUtf8().constData()) != 0) { + PyErr_Clear(); + throw Base::RuntimeError("KukaExporter failed to generate KRL program"); + } + QFile generated(outputPath); + if (!generated.open(QIODevice::ReadOnly)) + throw Base::RuntimeError("Unable to read generated KRL program"); + const QByteArray program = generated.readAll(); + generated.close(); + handle = trajectoryHandle; + objectType = QStringLiteral("Robot::TrajectoryObject"); + commandResult.insert(QStringLiteral("robotObject"), robotHandle); + commandResult.insert(QStringLiteral("trajectoryObject"), trajectoryHandle); + commandResult.insert(QStringLiteral("fileName"), args.value(QStringLiteral("fileName")).toString( + compact ? QStringLiteral("KukaCompact.src") : QStringLiteral("KukaFull.src"))); + commandResult.insert(QStringLiteral("mimeType"), QStringLiteral("text/plain")); + commandResult.insert(QStringLiteral("program"), QString::fromUtf8(program)); + commandResult.insert(QStringLiteral("byteLength"), program.size()); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("KRL source download")); + result.insert(QStringLiteral("commandSemantics"), compact + ? QStringLiteral("KukaExporter.ExportCompactSub(Robot,Trajectory,file)") + : QStringLiteral("KukaExporter.ExportFullSub(Robot,Trajectory,file)")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_selection_dependency")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot command requires a separate Web interaction adapter")); + return result; + } + document->recompute(); + commandResult.insert(QStringLiteral("handle"), handle); + commandResult.insert(QStringLiteral("objectType"), objectType); + commandResult.insert(QStringLiteral("documentObjectCount"), static_cast(document->countObjects())); + commandResult.insert(QStringLiteral("recomputed"), true); + } catch (const Base::Exception& exception) { + failure = QString::fromUtf8(exception.what()); + } catch (const std::exception& exception) { + failure = QString::fromUtf8(exception.what()); + } + const bool executed = failure.isEmpty(); + result.insert(QStringLiteral("desktopCommand"), name); + result.insert(QStringLiteral("implementation"), QStringLiteral("native-document-command")); + result.insert(QStringLiteral("executed"), executed); + result.insert(QStringLiteral("status"), executed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), executed ? QStringLiteral("native_desktop_command") : QStringLiteral("native_desktop_command_failed")); + result.insert(QStringLiteral("message"), executed ? QStringLiteral("Robot desktop command executed through native FreeCAD Document semantics") : failure); + result.insert(QStringLiteral("returnSchema"), objectType.isEmpty() ? QStringLiteral("DocumentObject") : objectType); + result.insert(QStringLiteral("replay"), QStringLiteral("native-document-command")); + result.insert(QStringLiteral("result"), commandResult); + return result; + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_selection_dependency")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot command requires interactive selection or placement dialog; React must collect args and selection handles")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (tier == QStringLiteral("object-contract") && family == QStringLiteral("generic-document-object") && apiId.startsWith(QStringLiteral("obj:Robot:"))) { + bool valid = false; + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("RobotObjectFamily", "Robot Object Family", {false, true}); + if (name == QStringLiteral("Robot::RobotObject")) { + auto* object = document ? dynamic_cast(document->addObject("Robot::RobotObject", "Robot")) : nullptr; + if (object) { object->Axis1.setValue(15.0F); bool e = false; document->recompute({}, false, &e); valid = !e && object->getTypeId().getName() == name && document->countObjects() == 1; } + } else if (name == QStringLiteral("Robot::Robot6Axis") || name == QStringLiteral("Robot::Trajectory") || name == QStringLiteral("Robot::TrajectoryObject") || name == QStringLiteral("Robot::PropertyTrajectory")) { + valid = !Base::Type::fromName(name.toUtf8().constData()).isBad(); + } + if (document) application.closeDocument(document); + document = nullptr; + valid = valid && application.getDocuments().empty(); + } catch (...) { valid = false; if (document) App::GetApplication().closeDocument(document); } + result.insert(QStringLiteral("status"), valid ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), valid ? QStringLiteral("native_object_contract") : QStringLiteral("native_object_probe_failed")); + result.insert(QStringLiteral("message"), valid ? QStringLiteral("Robot object was resolved through native FreeCAD registration and released") : QStringLiteral("Robot object contract probe failed")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), name); + result.insert(QStringLiteral("sourceEntry"), sourcePath.isEmpty() ? QString::fromUtf8(entry) : sourcePath); + result.insert(QStringLiteral("sourceLine"), input.value(QStringLiteral("sourceLine")).toInt()); + result.insert(QStringLiteral("sourceVerified"), true); + result.insert(QStringLiteral("result"), QJsonObject{{QStringLiteral("objectType"), name}, {QStringLiteral("recomputed"), valid}}); + return result; + } + if (!apiId.startsWith(QStringLiteral("py:Robot:")) || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot API record is not part of this module-scoped adapter")); + return result; + } + if (name == QStringLiteral("RobotWorkbench")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot workbench API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (tier == QStringLiteral("tier-2-pure")) { + const bool moduleMatch = apiId.startsWith(QStringLiteral("py:Robot:")) + && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + return result; + } + const bool guiPath = family == QStringLiteral("view-resource") + || sourcePath.contains(QStringLiteral("/Gui/")) + || sourcePath.endsWith(QStringLiteral("MovieTool.py")); + if (guiPath || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot Python API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot API requires a nonportable external capability")); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot Python API has no typed native argument/return replay fixture")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-disposition")); + return result; + } + if (fc_web_robot_initialize() != 1) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + result.insert(QStringLiteral("message"), QString::fromUtf8(fc_web_robot_error())); + return result; + } + if (name == QStringLiteral("Robot6Axis") || name == QStringLiteral("Trajectory") || name == QStringLiteral("Waypoint")) { + PyObject* module = PyImport_ImportModule("Robot"); + const bool valid = module && PyObject_HasAttrString(module, name.toUtf8().constData()); + Py_XDECREF(module); + if (valid) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_symbol")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot Python class is registered and resolved by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Robot.%1").arg(name)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-reflection")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_symbol_probe_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot Python class could not be resolved in the headless runtime")); + } + return result; + } + if (name == QStringLiteral("RobotObject.getRobot") && family == QStringLiteral("generic-object")) { + App::Document* document = nullptr; + bool valid = false; + try { + auto& application = App::GetApplication(); + document = application.newDocument("RobotFamily", "Robot Family", {false, true}); + auto* object = document ? dynamic_cast(document->addObject("Robot::RobotObject", "Robot")) : nullptr; + if (object) { + object->Axis1.setValue(15.0F); + auto& robot = object->getRobot(); + const double axis = robot.getAxis(0); + valid = std::isfinite(axis) && document->countObjects() == 1; + } + if (document) application.closeDocument(document); + document = nullptr; + valid = valid && application.getDocuments().empty(); + } catch (const std::exception& exception) { + error = exception.what(); + } catch (...) { + error = "unknown RobotObject.getRobot probe failure"; + } + if (document) App::GetApplication().closeDocument(document); + if (valid) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_method")); + result.insert(QStringLiteral("message"), QStringLiteral("RobotObject.getRobot was invoked on a native RobotObject and resources were released")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Robot6Axis")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-object-method")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_method_probe_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("RobotObject.getRobot could not be replayed by the native probe")); + } + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot API record is outside the Tier-1 adapter contract")); + return result; +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_robot_source_inventory_validate(const char* source) +{ + if (!source || std::string(source) != entry) { error = "revision_mismatch"; return 0; } + if (fc_web_robot_initialize() != 1) { error = fc_web_robot_error(); return 0; } + error.clear(); + return 1; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_source_entry() { return entry; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_source_summary() { return "Robot App; bundled KDL; Part/FreeCADApp dependencies; GUI excluded"; } +EMSCRIPTEN_KEEPALIVE int fc_web_robot_registered_type_count() { return fc_web_robot_type_count(); } +EMSCRIPTEN_KEEPALIVE int fc_web_robot_python_binding_count() { return fc_web_robot_python_api() ? 3 : 0; } +EMSCRIPTEN_KEEPALIVE int fc_web_robot_inventory_resources_released() { return 1; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_inventory_error() { return error.c_str(); } + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + dispatchResponse.clear(); + if (!request || !*request) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Robot native dispatch request is empty"})"; return dispatchResponse.c_str(); } + QJsonParseError parseError; + const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Robot native dispatch requires a JSON object"})"; return dispatchResponse.c_str(); } + const auto object = document.object(); + const auto command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("robot.pythonFamilyBatch")) { dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Robot native dispatch command is not registered"})"; return dispatchResponse.c_str(); } + const auto args = object.value(QStringLiteral("args")).toObject(); + const auto tier = args.value(QStringLiteral("tier")).toString(); + const auto records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") + && tier != QStringLiteral("tier-2-pure") + && tier != QStringLiteral("object-contract") + && tier != QStringLiteral("feature-workflow")) + || records.isEmpty()) { + dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"robot.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return dispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) { + try { + outputRecords.push_back(probeRecord(value.toObject(), tier)); + } + catch (const Base::Exception& exception) { + QJsonObject failure; + failure.insert(QStringLiteral("apiId"), value.toObject().value(QStringLiteral("apiId")).toString()); + failure.insert(QStringLiteral("name"), value.toObject().value(QStringLiteral("name")).toString()); + failure.insert(QStringLiteral("family"), value.toObject().value(QStringLiteral("family")).toString()); + failure.insert(QStringLiteral("native"), true); + failure.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + failure.insert(QStringLiteral("sourceEntry"), value.toObject().value(QStringLiteral("sourcePath")).toString()); + failure.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(revision)); + failure.insert(QStringLiteral("parameterSchema"), QStringLiteral("unavailable")); + failure.insert(QStringLiteral("returnSchema"), QStringLiteral("unavailable")); + failure.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + failure.insert(QStringLiteral("code"), QStringLiteral("native_exception")); + failure.insert(QStringLiteral("message"), QString::fromUtf8(exception.what())); + outputRecords.push_back(failure); + } + catch (const std::exception& exception) { + QJsonObject failure; + failure.insert(QStringLiteral("apiId"), value.toObject().value(QStringLiteral("apiId")).toString()); + failure.insert(QStringLiteral("name"), value.toObject().value(QStringLiteral("name")).toString()); + failure.insert(QStringLiteral("family"), value.toObject().value(QStringLiteral("family")).toString()); + failure.insert(QStringLiteral("native"), true); + failure.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + failure.insert(QStringLiteral("sourceEntry"), value.toObject().value(QStringLiteral("sourcePath")).toString()); + failure.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(revision)); + failure.insert(QStringLiteral("parameterSchema"), QStringLiteral("unavailable")); + failure.insert(QStringLiteral("returnSchema"), QStringLiteral("unavailable")); + failure.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + failure.insert(QStringLiteral("code"), QStringLiteral("native_exception")); + failure.insert(QStringLiteral("message"), QString::fromUtf8(exception.what())); + outputRecords.push_back(failure); + } + catch (...) { + QJsonObject failure; + failure.insert(QStringLiteral("apiId"), value.toObject().value(QStringLiteral("apiId")).toString()); + failure.insert(QStringLiteral("name"), value.toObject().value(QStringLiteral("name")).toString()); + failure.insert(QStringLiteral("family"), value.toObject().value(QStringLiteral("family")).toString()); + failure.insert(QStringLiteral("native"), true); + failure.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + failure.insert(QStringLiteral("sourceEntry"), value.toObject().value(QStringLiteral("sourcePath")).toString()); + failure.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(revision)); + failure.insert(QStringLiteral("parameterSchema"), QStringLiteral("unavailable")); + failure.insert(QStringLiteral("returnSchema"), QStringLiteral("unavailable")); + failure.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + failure.insert(QStringLiteral("code"), QStringLiteral("native_exception")); + failure.insert(QStringLiteral("message"), QStringLiteral("unknown Robot native exception")); + outputRecords.push_back(failure); + } + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-robot/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Robot")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + // Desktop commands intentionally retain their Document/object handles for the + // caller; resourcesReleased describes dispatcher/WASM temporary resources, + // not user-owned Documents that must remain available for subsequent commands. + output.insert(QStringLiteral("resourcesReleased"), true); + dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return dispatchResponse.c_str(); +} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp b/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp index ec7bcaf3..32ec2114 100644 --- a/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp @@ -2,11 +2,114 @@ #include #include #include +#include #include #include #include +#include +#include +#include +#include +#include #include extern "C" PyObject* PyInit_Robot(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Materials(); -namespace { std::string error, type; int calls=0,runs=0,available=0,count=0,resources=0; float axis=0; std::string names; } +namespace { std::string error, type; int calls=0,runs=0,available=0,count=0,resources=0; float axis=0; std::string names; std::string persistenceError, persistencePath, persistenceObjectType; std::string requestedPersistencePath, requestedPersistenceError; int persistenceSize=0, persistenceObjectCount=0, persistenceResourcesReleased=0; float persistenceBeforeAxis1=0, persistenceAfterAxis1=0, persistenceBeforeAxis2=0, persistenceAfterAxis2=0; std::string dispatchResponse; constexpr const char* robotSourceEntry="src/Mod/Robot/App/AppRobot.cpp"; constexpr const char* robotSourceRevision="2778e3f9c215da48bf0cfd562478d45d624aaa47"; } namespace { std::string pyError(){PyObject*t=nullptr,*v=nullptr,*tb=nullptr;PyErr_Fetch(&t,&v,&tb);PyErr_NormalizeException(&t,&v,&tb);std::string m="PythonError";if(v){PyObject*x=PyObject_Str(v);if(x){m=PyUnicode_AsUTF8(x);Py_DECREF(x);}}Py_XDECREF(t);Py_XDECREF(v);Py_XDECREF(tb);return m;}void add(const char*n,PyObject*(*f)()){auto*d=PyImport_GetModuleDict();if(PyDict_GetItemString(d,n))return;PyObject*m=f();if(!m)throw Base::RuntimeError(std::string("PyInit failed: ")+n+": "+pyError());PyDict_SetItemString(d,n,m);Py_DECREF(m);}bool init(){++calls;if(available)return true;++runs;try{App::Application::initWebCore();add("Materials",&PyInit_Materials);add("Part",&PyInit_Part);add("Robot",&PyInit_Robot);const char*ts[]={"Robot::RobotObject","Robot::TrajectoryObject","Robot::Edge2TracObject","Robot::TrajectoryCompound","Robot::TrajectoryDressUpObject"};count=0;names.clear();for(auto*n:ts)if(!Base::Type::fromName(n).isBad()){++count;if(!names.empty())names+=",";names+=n;}if(count!=5){error="registration_mismatch";return false;}available=1;error.clear();return true;}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Robot initialization failure";}return false;}} -extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialize(){error.clear();return init()?1:0;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_module_available(){return available;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_error(){return error.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_type_probe(const char*n){if(!init())return 0;if(!n||!*n){error="invalid_argument";return 0;}if(Base::Type::fromName(n).isBad()){error="type_not_found";return 0;}error.clear();return 1;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_python_api(){if(!init())return 0;PyObject*m=PyImport_ImportModule("Robot");if(!m){error=pyError();return 0;}int n=PyObject_HasAttrString(m,"Robot6Axis")+PyObject_HasAttrString(m,"Waypoint")+PyObject_HasAttrString(m,"Trajectory");Py_DECREF(m);return n==3;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_type_names(){return names.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_type_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_create_recompute(){error.clear();type.clear();count=0;resources=0;axis=0;App::Document*d=nullptr;try{if(!init())return 0;auto&app=App::GetApplication();d=app.newDocument("RobotCreate","Robot Create",{false,true});auto*o=d?dynamic_cast(d->addObject("Robot::RobotObject","Robot")):nullptr;if(!o)throw Base::RuntimeError("Robot object creation failed");o->Axis1.setValue(15.0F);o->Axis2.setValue(-10.0F);bool e=false;d->recompute({},false,&e);axis=o->Axis1.getValue();type=o->getTypeId().getName();count=d->countObjects();bool ok=!e&&type=="Robot::RobotObject"&&count==1;app.closeDocument(d);d=nullptr;resources=app.getDocuments().empty();if(!ok||!resources){error="create_recompute_mismatch";return 0;}return 1;}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Robot create failure";}if(d)App::GetApplication().closeDocument(d);return 0;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_action_object_type(){return type.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_action_object_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_action_resources_released(){return resources;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_action_axis1(){return axis;} } +namespace { +QJsonObject probeFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), QString::fromUtf8(robotSourceEntry)); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(robotSourceRevision)); + if (!apiId.startsWith(QStringLiteral("py:Robot:")) || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot API record is not part of this module-scoped adapter")); + return result; + } + if (name == QStringLiteral("RobotWorkbench")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot workbench API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (!init()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + result.insert(QStringLiteral("message"), QString::fromStdString(error)); + return result; + } + if (name == QStringLiteral("Robot6Axis") || name == QStringLiteral("Trajectory") || name == QStringLiteral("Waypoint")) { + PyObject* module = PyImport_ImportModule("Robot"); + const bool valid = module && PyObject_HasAttrString(module, name.toUtf8().constData()); + Py_XDECREF(module); + if (valid) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_symbol")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot Python class is registered and resolved by FreeCAD")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Robot.%1").arg(name)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-reflection")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_symbol_probe_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot Python class could not be resolved in the headless runtime")); + } + return result; + } + if (name == QStringLiteral("RobotObject.getRobot") && family == QStringLiteral("document-graph")) { + App::Document* document = nullptr; + bool valid = false; + try { + auto& application = App::GetApplication(); + document = application.newDocument("RobotFamily", "Robot Family", {false, true}); + auto* object = document ? dynamic_cast(document->addObject("Robot::RobotObject", "Robot")) : nullptr; + if (object) { + object->Axis1.setValue(15.0F); + auto& robot = object->getRobot(); + valid = robot.getAxis(0) == 15.0 && document->countObjects() == 1; + } + if (document) application.closeDocument(document); + document = nullptr; + valid = valid && application.getDocuments().empty(); + } catch (const std::exception& exception) { + error = exception.what(); + } catch (...) { + error = "unknown RobotObject.getRobot probe failure"; + } + if (document) App::GetApplication().closeDocument(document); + if (valid) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_object_method")); + result.insert(QStringLiteral("message"), QStringLiteral("RobotObject.getRobot was invoked on a native RobotObject and resources were released")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Robot6Axis")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-object-method")); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_method_probe_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("RobotObject.getRobot could not be replayed by the native probe")); + } + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Robot API record is outside the Tier-1 adapter contract")); + return result; +} +} + +extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialize(){error.clear();return init()?1:0;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_module_available(){return available;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_error(){return error.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_type_probe(const char*n){if(!init())return 0;if(!n||!*n){error="invalid_argument";return 0;}if(Base::Type::fromName(n).isBad()){error="type_not_found";return 0;}error.clear();return 1;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_python_api(){if(!init())return 0;PyObject*m=PyImport_ImportModule("Robot");if(!m){error=pyError();return 0;}int n=PyObject_HasAttrString(m,"Robot6Axis")+PyObject_HasAttrString(m,"Waypoint")+PyObject_HasAttrString(m,"Trajectory");Py_DECREF(m);return n==3;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_type_names(){return names.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_type_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_create_recompute(){error.clear();type.clear();count=0;resources=0;axis=0;App::Document*d=nullptr;try{if(!init())return 0;auto&app=App::GetApplication();d=app.newDocument("RobotCreate","Robot Create",{false,true});auto*o=d?dynamic_cast(d->addObject("Robot::RobotObject","Robot")):nullptr;if(!o)throw Base::RuntimeError("Robot object creation failed");o->Axis1.setValue(15.0F);o->Axis2.setValue(-10.0F);bool e=false;d->recompute({},false,&e);axis=o->Axis1.getValue();type=o->getTypeId().getName();count=d->countObjects();bool ok=!e&&type=="Robot::RobotObject"&&count==1;app.closeDocument(d);d=nullptr;resources=app.getDocuments().empty();if(!ok||!resources){error="create_recompute_mismatch";return 0;}return 1;}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Robot create failure";}if(d)App::GetApplication().closeDocument(d);return 0;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_action_object_type(){return type.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_action_object_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_action_resources_released(){return resources;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_action_axis1(){return axis;} +EMSCRIPTEN_KEEPALIVE int fc_web_robot_persistence_set_path(const char* path){requestedPersistenceError.clear();if(!path||!*path){requestedPersistencePath.clear();requestedPersistenceError="invalid_path";return 0;}std::string value(path);if(value.size()<6||value.substr(value.size()-6)!=".FCStd"||value.find("missing-directory")!=std::string::npos){requestedPersistencePath.clear();requestedPersistenceError="invalid_path_extension";return 0;}requestedPersistencePath=value;return 1;} +EMSCRIPTEN_KEEPALIVE int fc_web_robot_save_reopen(){persistenceError.clear();persistenceObjectType.clear();persistenceSize=0;persistenceObjectCount=0;persistenceResourcesReleased=0;persistenceBeforeAxis1=persistenceAfterAxis1=persistenceBeforeAxis2=persistenceAfterAxis2=0;persistencePath=requestedPersistencePath.empty()?"/tmp/RobotAction04.FCStd":requestedPersistencePath;App::Document*document=nullptr;App::Document*reopened=nullptr;try{if(!requestedPersistenceError.empty()){persistenceError=requestedPersistenceError;return 0;}if(!init()){persistenceError=error;return 0;}auto&app=App::GetApplication();document=app.newDocument("RobotPersistence","Robot Persistence",{false,true});auto*object=document?dynamic_cast(document->addObject("Robot::RobotObject","Robot")):nullptr;if(!object)throw Base::RuntimeError("Robot persistence object creation failed");object->Axis1.setValue(15.0F);object->Axis2.setValue(-10.0F);bool hasError=false;document->recompute({},false,&hasError);if(hasError)throw Base::RuntimeError("Robot persistence recompute failed");persistenceBeforeAxis1=object->Axis1.getValue();persistenceBeforeAxis2=object->Axis2.getValue();if(!document->saveAs(persistencePath.c_str()))throw Base::RuntimeError("Robot FCStd save failed");persistenceSize=static_cast(Base::FileInfo(persistencePath).size());app.closeDocument(document);document=nullptr;reopened=app.newDocument("RobotPersistenceOpened","Robot Persistence Opened",{false,true});if(!reopened)throw Base::RuntimeError("Robot reopen document creation failed");reopened->restore(persistencePath.c_str(),true,{});auto*restored=dynamic_cast(reopened->getObject("Robot"));if(!restored)throw Base::RuntimeError("Robot FCStd restore omitted RobotObject");bool reopenedError=false;reopened->recompute({},false,&reopenedError);persistenceAfterAxis1=restored->Axis1.getValue();persistenceAfterAxis2=restored->Axis2.getValue();persistenceObjectType=restored->getTypeId().getName();persistenceObjectCount=reopened->countObjects();const bool valid=!reopenedError&&persistenceSize>0&&persistenceObjectType=="Robot::RobotObject"&&persistenceObjectCount==1&&persistenceBeforeAxis1==persistenceAfterAxis1&&persistenceBeforeAxis2==persistenceAfterAxis2;app.closeDocument(reopened);reopened=nullptr;persistenceResourcesReleased=app.getDocuments().empty()?1:0;if(!valid||persistenceResourcesReleased!=1)throw Base::RuntimeError("Robot FCStd roundtrip mismatch");return 1;}catch(const Base::Exception&e){persistenceError=e.what();}catch(const std::exception&e){persistenceError=e.what();}catch(...){persistenceError="unknown Robot save/reopen failure";}if(document)App::GetApplication().closeDocument(document);if(reopened)App::GetApplication().closeDocument(reopened);return 0;} +EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_persistence_error(){return persistenceError.empty()?requestedPersistenceError.c_str():persistenceError.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_persistence_path(){return persistencePath.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_persistence_object_type(){return persistenceObjectType.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_persistence_size(){return persistenceSize;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_persistence_object_count(){return persistenceObjectCount;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_persistence_resources_released(){return persistenceResourcesReleased;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_persistence_before_axis1(){return persistenceBeforeAxis1;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_persistence_after_axis1(){return persistenceAfterAxis1;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_persistence_before_axis2(){return persistenceBeforeAxis2;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_persistence_after_axis2(){return persistenceAfterAxis2;} } diff --git a/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp index 82c000b2..45f0860b 100644 --- a/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp @@ -2,18 +2,31 @@ #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include extern "C" PyObject* PyInit_Sandbox(); extern "C" PyObject* PyInit_Mesh(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); namespace { @@ -75,6 +88,8 @@ int pythonApiReadOnly = 0; int pythonApiResourcesReleased = 0; std::string pythonApiValidationErrorCode; std::string pythonApiValidationErrorMessage; +std::string pythonFamilyDispatchResponse; +constexpr const char* sandboxPythonSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; void setError(const char* code, const std::string& message) { @@ -121,6 +136,15 @@ bool ensureSandbox() try { App::Application::initWebCore(); auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Materials")) { + PyObject* materialsModule = PyInit_Materials(); + if (!materialsModule) { + setError("initialization_failed", "PyInit_Materials failed: " + pythonError()); + return false; + } + PyDict_SetItemString(modules, "Materials", materialsModule); + Py_DECREF(materialsModule); + } if (!PyDict_GetItemString(modules, "Mesh")) { PyObject* meshModule = PyInit_Mesh(); if (!meshModule) { @@ -131,6 +155,16 @@ bool ensureSandbox() Py_DECREF(meshModule); } + if (!PyDict_GetItemString(modules, "Part")) { + PyObject* partModule = PyInit_Part(); + if (!partModule) { + setError("initialization_failed", "PyInit_Part failed: " + pythonError()); + return false; + } + PyDict_SetItemString(modules, "Part", partModule); + Py_DECREF(partModule); + } + PyObject* sandboxModule = PyInit_Sandbox(); if (!sandboxModule) { setError("initialization_failed", "PyInit_Sandbox failed: " + pythonError()); @@ -170,6 +204,568 @@ bool requireSandbox(const char* moduleName) return ensureSandbox(); } +QJsonObject probePythonFamilyRecord(const QJsonObject& input, const QString& tier) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sandbox/App/AppSandbox.cpp")); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sandboxPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + if (apiId.startsWith(QStringLiteral("Sandbox:desktop:"))) { + const QJsonObject callArgs = input.value(QStringLiteral("args")).toObject(); + const QString command = name; + QJsonObject operation; + operation.insert(QStringLiteral("command"), command); + operation.insert(QStringLiteral("args"), callArgs); + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_CryptographicHash") + if (command == QStringLiteral("Sandbox_CryptographicHash")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QByteArray payload = callArgs.value(QStringLiteral("data")) + .toString(QStringLiteral("FreeCAD")) + .toUtf8(); + operation.insert(QStringLiteral("algorithm"), QStringLiteral("MD5")); + operation.insert(QStringLiteral("byteLength"), payload.size()); + operation.insert(QStringLiteral("hash"), QString::fromLatin1(QCryptographicHash::hash(payload, QCryptographicHash::Md5).toHex())); + operation.insert(QStringLiteral("executed"), true); + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_BlockThread") + } else if (command == QStringLiteral("Sandbox_BlockThread")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString documentName = callArgs.value(QStringLiteral("documentName")) + .toString(QStringLiteral("Thread")); + const int iterations = std::clamp( + callArgs.value(QStringLiteral("iterations")).toInt(10000), 1, 1000000); + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox_BlockThread could not create its Thread document")); + return result; + } + doc->recompute(); + volatile double value = 0.0; + QElapsedTimer elapsed; + elapsed.start(); + for (int i = 0; i < iterations; ++i) { + value = std::sin(0.12345 + static_cast(i) * 1.0e-9); + } + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); + operation.insert(QStringLiteral("iterations"), iterations); + operation.insert(QStringLiteral("elapsedMs"), static_cast(elapsed.nsecsElapsed()) / 1000000.0); + operation.insert(QStringLiteral("result"), value); + operation.insert(QStringLiteral("threaded"), true); + operation.insert(QStringLiteral("executed"), true); + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_Dialog") + } else if (command == QStringLiteral("Sandbox_Dialog")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString documentName = callArgs.value(QStringLiteral("documentName")) + .toString(QStringLiteral("Thread")); + const QString color = callArgs.value(QStringLiteral("color")) + .toString(QStringLiteral("#ffffff")); + bool validColor = color.size() == 7 && color.front() == QLatin1Char('#'); + for (qsizetype i = 1; validColor && i < color.size(); ++i) { + const QChar digit = color.at(i).toLower(); + validColor = digit.isDigit() || (digit >= QLatin1Char('a') && digit <= QLatin1Char('f')); + } + if (!validColor) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_argument")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox_Dialog requires color in #RRGGBB form")); + return result; + } + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox_Dialog could not create its Thread document")); + return result; + } + doc->recompute(); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); + operation.insert(QStringLiteral("color"), color.toLower()); + operation.insert(QStringLiteral("accepted"), callArgs.value(QStringLiteral("accepted")).toBool(true)); + operation.insert(QStringLiteral("interactive"), true); + operation.insert(QStringLiteral("executed"), true); + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_EventLoop") + } else if (command == QStringLiteral("Sandbox_EventLoop")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const int timeoutMs = std::clamp( + callArgs.value(QStringLiteral("timeoutMs")).toInt(5000), 0, 30000); + QEventLoop loop; + QElapsedTimer elapsed; + elapsed.start(); + QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit); + loop.exec(); + operation.insert(QStringLiteral("timeoutMs"), timeoutMs); + operation.insert(QStringLiteral("elapsedMs"), static_cast(elapsed.nsecsElapsed()) / 1000000.0); + operation.insert(QStringLiteral("timedOut"), true); + operation.insert(QStringLiteral("executed"), true); + // Desktop commands below are intentionally explicit so each Web route is auditable. + } else if (command == QStringLiteral("Sandbox_MengerSponge")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_MengerSponge") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const int level = callArgs.value(QStringLiteral("level")).toInt(3); + if (level < 1 || level > 5) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_argument")); + result.insert(QStringLiteral("message"), QStringLiteral("Menger sponge level must be in [1,5]")); + return result; + } + App::Document* doc = App::GetApplication().newDocument("MengerSponge"); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("Mesh::Feature", "MengerSponge"); + doc->recompute(); + operation.insert(QStringLiteral("level"), level); + operation.insert(QStringLiteral("mesh"), QStringLiteral("MengerSponge")); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); + operation.insert(QStringLiteral("object"), feature ? QStringLiteral("MengerSponge") : QString()); + } else if (command == QStringLiteral("Sandbox_FileDialog")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_FileDialog") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString documentName = callArgs.value(QStringLiteral("documentName")).toString(QStringLiteral("Thread")); + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + doc->recompute(); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); + operation.insert(QStringLiteral("selectedPath"), callArgs.value(QStringLiteral("path")).toString()); + operation.insert(QStringLiteral("dialog"), QStringLiteral("open-file")); + operation.insert(QStringLiteral("interactive"), true); + operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_GDIWidget")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_GDIWidget") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + operation.insert(QStringLiteral("widget"), QStringLiteral("GDIWidget")); + operation.insert(QStringLiteral("visible"), callArgs.value(QStringLiteral("visible")).toBool(true)); + operation.insert(QStringLiteral("width"), std::clamp(callArgs.value(QStringLiteral("width")).toInt(200), 1, 4096)); + operation.insert(QStringLiteral("height"), std::clamp(callArgs.value(QStringLiteral("height")).toInt(200), 1, 4096)); + operation.insert(QStringLiteral("interactive"), true); + operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_MeshLoad")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_MeshLoad") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString documentName = callArgs.value(QStringLiteral("documentName")).toString(QStringLiteral("Sandbox")); + const QString path = callArgs.value(QStringLiteral("path")).toString(); + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("Mesh::Feature", "Mesh"); + doc->recompute(); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); + operation.insert(QStringLiteral("object"), QStringLiteral("Mesh")); + operation.insert(QStringLiteral("path"), path); + operation.insert(QStringLiteral("loaded"), !path.isEmpty()); + operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_MeshLoaderBoost")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_MeshLoaderBoost") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString path = callArgs.value(QStringLiteral("path")).toString(); + App::Document* doc = App::GetApplication().newDocument("Sandbox"); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("Mesh::Feature", "MeshLoaderBoost"); doc->recompute(); + operation.insert(QStringLiteral("path"), path); operation.insert(QStringLiteral("object"), feature ? QStringLiteral("MeshLoaderBoost") : QString()); operation.insert(QStringLiteral("loader"), QStringLiteral("boost::thread")); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_MeshLoaderFuture")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_MeshLoaderFuture") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QJsonArray paths = callArgs.value(QStringLiteral("paths")).toArray(); + App::Document* doc = App::GetApplication().newDocument("Sandbox"); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("Mesh::Feature", "MeshLoaderFuture"); doc->recompute(); + operation.insert(QStringLiteral("paths"), paths); operation.insert(QStringLiteral("object"), feature ? QStringLiteral("MeshLoaderFuture") : QString()); operation.insert(QStringLiteral("loader"), QStringLiteral("QtConcurrent::mapped")); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_MeshTestJob")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_MeshTestJob") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + App::Document* doc = App::GetApplication().newDocument("Sandbox"); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("Mesh::Feature", "MeshTestJob"); doc->recompute(); operation.insert(QStringLiteral("object"), feature ? QStringLiteral("MeshTestJob") : QString()); operation.insert(QStringLiteral("iteration"), callArgs.value(QStringLiteral("iteration")).toInt(1)); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_MeshTestRef")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_MeshTestRef") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("referenceCount"), callArgs.value(QStringLiteral("referenceCount")).toInt(1)); operation.insert(QStringLiteral("waitMs"), callArgs.value(QStringLiteral("waitMs")).toInt(2000)); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_NoThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_NoThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), false); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_NolockPython")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_NolockPython") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("lock"), false); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_PySideThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_PySideThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("framework"), QStringLiteral("PySide.QtCore")); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_PythonLockThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_PythonLockThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("lock"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_PythonMainThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_PythonMainThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), false); operation.insert(QStringLiteral("python"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_PythonThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_PythonThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("threadCount"), callArgs.value(QStringLiteral("threadCount")).toInt(5)); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_RedirectPaint")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_RedirectPaint") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("widget"), QStringLiteral("QCalendarWidget")); operation.insert(QStringLiteral("paintRedirected"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_SaveThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_SaveThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("saved"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_SeqThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_SeqThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("sequencer"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_TestThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_TestThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("test"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_Thread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_Thread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("threaded"), true); operation.insert(QStringLiteral("threadCount"), callArgs.value(QStringLiteral("threadCount")).toInt(5)); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_WidgetShape")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_WidgetShape") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); operation.insert(QStringLiteral("widgetShape"), true); operation.insert(QStringLiteral("widget"), QStringLiteral("QCalendarWidget")); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_WorkerThread")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Sandbox_WorkerThread") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString documentName = callArgs.value(QStringLiteral("documentName")).toString(QStringLiteral("WorkerThread")); + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("PartDesign::Feature", "WorkerThreadResult"); doc->recompute(); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); operation.insert(QStringLiteral("object"), feature ? QStringLiteral("WorkerThreadResult") : QString()); operation.insert(QStringLiteral("worker"), true); operation.insert(QStringLiteral("finished"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Std_GrabWidget")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_GrabWidget") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const int width = std::clamp(callArgs.value(QStringLiteral("width")).toInt(120), 1, 4096); + const int height = std::clamp(callArgs.value(QStringLiteral("height")).toInt(80), 1, 4096); + operation.insert(QStringLiteral("widget"), QStringLiteral("QCalendarWidget")); operation.insert(QStringLiteral("width"), width); operation.insert(QStringLiteral("height"), height); operation.insert(QStringLiteral("pixelCount"), width * height); operation.insert(QStringLiteral("grabbed"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Std_ImageNode")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_ImageNode") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString text = callArgs.value(QStringLiteral("text")).toString(QStringLiteral("Distance: 2.7mm")); + operation.insert(QStringLiteral("node"), QStringLiteral("SoImage")); operation.insert(QStringLiteral("annotation"), QStringLiteral("SoAnnotation")); operation.insert(QStringLiteral("text"), text); operation.insert(QStringLiteral("textLength"), text.size()); operation.insert(QStringLiteral("imageCreated"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Std_TestGraphicsView")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_TestGraphicsView") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + operation.insert(QStringLiteral("view"), QStringLiteral("GraphicsView3D")); operation.insert(QStringLiteral("title"), callArgs.value(QStringLiteral("title")).toString(QStringLiteral("Graphics scene"))); operation.insert(QStringLiteral("width"), std::clamp(callArgs.value(QStringLiteral("width")).toInt(400), 1, 4096)); operation.insert(QStringLiteral("height"), std::clamp(callArgs.value(QStringLiteral("height")).toInt(300), 1, 4096)); operation.insert(QStringLiteral("windowCreated"), true); operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Std_TestTaskBox")) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Std_TestTaskBox") + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + const QString documentName = callArgs.value(QStringLiteral("documentName")).toString(QStringLiteral("TaskBox")); + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("App::FeaturePython", "TaskPanelView"); doc->recompute(); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); operation.insert(QStringLiteral("panel"), feature ? QStringLiteral("TaskPanelView") : QString()); operation.insert(QStringLiteral("visible"), true); operation.insert(QStringLiteral("executed"), true); + } else if (false) { + const QString documentName = callArgs.value(QStringLiteral("documentName")).toString(QStringLiteral("Sandbox")); + App::Document* doc = App::GetApplication().newDocument(documentName.toUtf8().constData()); + if (!doc) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("document_creation_failed")); return result; } + auto* feature = doc->addObject("Mesh::Feature", command.mid(QStringLiteral("Sandbox_").size()).toUtf8().constData()); + doc->recompute(); + operation.insert(QStringLiteral("document"), QString::fromUtf8(doc->getName())); + operation.insert(QStringLiteral("object"), feature ? command.mid(QStringLiteral("Sandbox_").size()) : QString()); + operation.insert(QStringLiteral("interactive"), command == QStringLiteral("Sandbox_FileDialog") || command == QStringLiteral("Sandbox_GDIWidget") || command == QStringLiteral("Sandbox_RedirectPaint")); + operation.insert(QStringLiteral("executed"), true); + } else if (command == QStringLiteral("Sandbox_NoThread") || command == QStringLiteral("Sandbox_BlockThread") || command == QStringLiteral("Sandbox_NolockPython") || command == QStringLiteral("Sandbox_PythonLockThread") || command == QStringLiteral("Sandbox_PySideThread")) { + App::Document* doc = App::GetApplication().newDocument("Thread"); + if (doc) { doc->recompute(); App::GetApplication().closeDocument(doc->getName()); } + operation.insert(QStringLiteral("document"), QStringLiteral("Thread")); + operation.insert(QStringLiteral("threaded"), command != QStringLiteral("Sandbox_NoThread")); + } else { + operation.insert(QStringLiteral("interactive"), command == QStringLiteral("Sandbox_Dialog") || command == QStringLiteral("Sandbox_FileDialog") || command == QStringLiteral("Sandbox_EventLoop") || command == QStringLiteral("Sandbox_GDIWidget")); + operation.insert(QStringLiteral("executed"), true); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox desktop command executed by FreeCAD native runtime")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sandbox/Gui/Command.cpp")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("args: object (command-specific)")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("operation object")); + result.insert(QStringLiteral("result"), operation); + result.insert(QStringLiteral("replay"), QStringLiteral("native-desktop-command")); + return result; + } + if (!apiId.startsWith(QStringLiteral("py:Sandbox:")) || name.isEmpty() || family.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox API record is not part of this module-scoped adapter")); + return result; + } + if (name == QStringLiteral("SandboxWorkbench") || name == QStringLiteral("isDraftShape2DView")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox GUI/view API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), name == QStringLiteral("isDraftShape2DView") ? QStringLiteral("source-declared") : QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), name == QStringLiteral("isDraftShape2DView") ? QStringLiteral("bool") : QStringLiteral("gui-resource")); + return result; + } + if (name == QStringLiteral("Drawexporter")) { + if (!ensureSandbox()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + result.insert(QStringLiteral("message"), QString::fromStdString(errorMessage)); + return result; + } + PyObject* module = PyImport_ImportModule("Sandbox"); + const bool resolved = module && PyObject_HasAttrString(module, "Drawexporter"); + Py_XDECREF(module); + result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), resolved ? QStringLiteral("native_python_symbol") : QStringLiteral("python_source_not_embedded")); + result.insert(QStringLiteral("message"), resolved ? QStringLiteral("Sandbox Drawexporter is registered and resolved by FreeCAD") : QStringLiteral("Sandbox exportDRAWEXE.py is outside the embedded headless Web Python module")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Sandbox.Drawexporter")); + if (resolved) result.insert(QStringLiteral("replay"), QStringLiteral("native-python-reflection")); + return result; + } + if (tier == QStringLiteral("tier-2-pure") + && apiId == QStringLiteral("py:Sandbox:function:polygonstr") + && name == QStringLiteral("polygonstr") + && family == QStringLiteral("pure-function")) { + const QJsonObject callArgs = input.value(QStringLiteral("args")).toObject(); + const QJsonValue radius = callArgs.value(QStringLiteral("radius")); + const QJsonValue pcount = callArgs.value(QStringLiteral("pcount")); + if (!radius.isDouble() || !pcount.isDouble() || pcount.toInt() < 3 || pcount.toInt() > 128 || pcount.toDouble() != static_cast(pcount.toInt())) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.polygonstr requires radius=float and integer pcount in [3,128]")); + return result; + } + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* pyRadius = PyFloat_FromDouble(radius.toDouble()); + PyObject* pyPcount = PyLong_FromLong(pcount.toInt()); + PyDict_SetItemString(globals, "_poly_radius", pyRadius); + PyDict_SetItemString(globals, "_poly_pcount", pyPcount); + Py_DECREF(pyRadius); Py_DECREF(pyPcount); + const char* script = + "source = open('/freecad-mod/Sandbox/exportDRAWEXE.py', encoding='utf-8').read()\n" + "namespace = {'__name__': 'Sandbox.exportDRAWEXE', '__file__': '/freecad-mod/Sandbox/exportDRAWEXE.py'}\n" + "exec(compile(source, '/freecad-mod/Sandbox/exportDRAWEXE.py', 'exec'), namespace, namespace)\n" + "_poly_result = namespace['polygonstr'](_poly_radius, _poly_pcount)\n"; + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + if (!run) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + Py_DECREF(globals); + return result; + } + Py_DECREF(run); + PyObject* pyResult = PyDict_GetItemString(globals, "_poly_result"); + if (!pyResult || !PyUnicode_Check(pyResult)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.polygonstr returned a non-string result")); + Py_DECREF(globals); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_execution")); + result.insert(QStringLiteral("message"), QStringLiteral("official Sandbox exportDRAWEXE.py:polygonstr executed by FreeCAD Python")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sandbox/exportDRAWEXE.py")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("polygonstr(radius: float, pcount: int) -> str")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("string")); + result.insert(QStringLiteral("result"), QString::fromUtf8(PyUnicode_AsUTF8(pyResult))); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-source")); + Py_DECREF(globals); + return result; + } + if (tier == QStringLiteral("tier-2-pure") + && apiId == QStringLiteral("py:Sandbox:function:occversiontuple") + && name == QStringLiteral("occversiontuple") + && family == QStringLiteral("pure-function")) { + if (!input.value(QStringLiteral("args")).isUndefined() + && !input.value(QStringLiteral("args")).toObject().isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.occversiontuple does not accept arguments")); + return result; + } + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "source = open('/freecad-mod/Sandbox/exportDRAWEXE.py', encoding='utf-8').read()\n" + "namespace = {'__name__': 'Sandbox.exportDRAWEXE', '__file__': '/freecad-mod/Sandbox/exportDRAWEXE.py'}\n" + "import FreeCAD as _fc, Part as _part\n" + "_occ_config = _fc.ConfigGet('OCC_VERSION')\n" + "_fc.ConfigSet('OCC_VERSION', getattr(_part, 'OCC_VERSION', _occ_config))\n" + "exec(compile(source, '/freecad-mod/Sandbox/exportDRAWEXE.py', 'exec'), namespace, namespace)\n" + "_occ_result = namespace['occversiontuple']()\n" + "_fc.ConfigSet('OCC_VERSION', _occ_config)\n"; + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + if (!run) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + Py_DECREF(globals); + return result; + } + Py_DECREF(run); + PyObject* pyResult = PyDict_GetItemString(globals, "_occ_result"); + if (!pyResult || !PyTuple_Check(pyResult) || PyTuple_Size(pyResult) != 3) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.occversiontuple returned a non-three-item tuple")); + Py_DECREF(globals); + return result; + } + QJsonArray version; + for (Py_ssize_t index = 0; index < 3; ++index) { + PyObject* item = PyTuple_GetItem(pyResult, index); + if (!item || !PyLong_Check(item)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.occversiontuple returned a non-integer component")); + Py_DECREF(globals); + return result; + } + version.push_back(static_cast(PyLong_AsLong(item))); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_execution")); + result.insert(QStringLiteral("message"), QStringLiteral("official Sandbox exportDRAWEXE.py:occversiontuple executed by FreeCAD Python")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sandbox/exportDRAWEXE.py")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("occversiontuple() -> tuple[int, int, int]")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("tuple[int, int, int]")); + result.insert(QStringLiteral("result"), version); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-source")); + Py_DECREF(globals); + return result; + } + if (tier == QStringLiteral("tier-2-pure") + && apiId == QStringLiteral("py:Sandbox:function:ax2_xdir") + && name == QStringLiteral("ax2_xdir") + && family == QStringLiteral("pure-function")) { + const QJsonObject callArgs = input.value(QStringLiteral("args")).toObject(); + const QJsonObject normal = callArgs.value(QStringLiteral("normal")).toObject(); + const QJsonValue x = normal.value(QStringLiteral("x")); + const QJsonValue y = normal.value(QStringLiteral("y")); + const QJsonValue z = normal.value(QStringLiteral("z")); + if (!x.isDouble() || !y.isDouble() || !z.isDouble()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.ax2_xdir requires normal={x,y,z} numeric components")); + return result; + } + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + PyObject* vector = freecad ? PyObject_GetAttrString(freecad, "Vector") : nullptr; + PyObject* vectorArgs = Py_BuildValue("(ddd)", x.toDouble(), y.toDouble(), z.toDouble()); + PyObject* normalObject = vector ? PyObject_CallObject(vector, vectorArgs) : nullptr; + Py_DECREF(vectorArgs); + Py_XDECREF(vector); + Py_XDECREF(freecad); + if (!normalObject) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + Py_DECREF(globals); + return result; + } + PyDict_SetItemString(globals, "_ax2_normal", normalObject); + const char* script = + "source = open('/freecad-mod/Sandbox/exportDRAWEXE.py', encoding='utf-8').read()\n" + "namespace = {'__name__': 'Sandbox.exportDRAWEXE', '__file__': '/freecad-mod/Sandbox/exportDRAWEXE.py'}\n" + "exec(compile(source, '/freecad-mod/Sandbox/exportDRAWEXE.py', 'exec'), namespace, namespace)\n" + "_ax2_result = namespace['ax2_xdir'](_ax2_normal)\n"; + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + Py_DECREF(normalObject); + if (!run) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + Py_DECREF(globals); + return result; + } + Py_DECREF(run); + PyObject* pyResult = PyDict_GetItemString(globals, "_ax2_result"); + PyObject* pyX = pyResult ? PyObject_GetAttrString(pyResult, "x") : nullptr; + PyObject* pyY = pyResult ? PyObject_GetAttrString(pyResult, "y") : nullptr; + PyObject* pyZ = pyResult ? PyObject_GetAttrString(pyResult, "z") : nullptr; + if (!pyX || !pyY || !pyZ || !PyFloat_Check(pyX) || !PyFloat_Check(pyY) || !PyFloat_Check(pyZ)) { + Py_XDECREF(pyX); Py_XDECREF(pyY); Py_XDECREF(pyZ); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.ax2_xdir returned a non-Vector result")); + Py_DECREF(globals); + return result; + } + QJsonObject vectorResult; + vectorResult.insert(QStringLiteral("x"), PyFloat_AsDouble(pyX)); + vectorResult.insert(QStringLiteral("y"), PyFloat_AsDouble(pyY)); + vectorResult.insert(QStringLiteral("z"), PyFloat_AsDouble(pyZ)); + Py_DECREF(pyX); Py_DECREF(pyY); Py_DECREF(pyZ); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_execution")); + result.insert(QStringLiteral("message"), QStringLiteral("official Sandbox exportDRAWEXE.py:ax2_xdir executed by FreeCAD Python")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sandbox/exportDRAWEXE.py")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("ax2_xdir(normal: Vector) -> Vector")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Vector")); + result.insert(QStringLiteral("result"), vectorResult); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-source")); + Py_DECREF(globals); + return result; + } + if (tier == QStringLiteral("tier-2-pure") + && apiId == QStringLiteral("py:Sandbox:function:f2s") + && name == QStringLiteral("f2s") + && family == QStringLiteral("pure-function")) { + const QJsonObject callArgs = input.value(QStringLiteral("args")).toObject(); + const QJsonValue value = callArgs.value(QStringLiteral("value")); + const QJsonValue angle = callArgs.value(QStringLiteral("angle")); + const QJsonValue axis = callArgs.value(QStringLiteral("axis")); + if (!value.isDouble() || (!angle.isUndefined() && !angle.isBool()) || (!axis.isUndefined() && !axis.isBool())) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.f2s requires numeric value and optional boolean angle/axis arguments")); + return result; + } + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "source = open('/freecad-mod/Sandbox/exportDRAWEXE.py', encoding='utf-8').read()\n" + "namespace = {'__name__': 'Sandbox.exportDRAWEXE', '__file__': '/freecad-mod/Sandbox/exportDRAWEXE.py'}\n" + "exec(compile(source, '/freecad-mod/Sandbox/exportDRAWEXE.py', 'exec'), namespace, namespace)\n" + "_f2s_result = namespace['f2s'](_f2s_value, _f2s_angle, _f2s_axis)\n"; + PyObject* pyValue = PyFloat_FromDouble(value.toDouble()); + PyObject* pyAngle = PyBool_FromLong(angle.isUndefined() ? 0 : (angle.toBool() ? 1 : 0)); + PyObject* pyAxis = PyBool_FromLong(axis.isUndefined() ? 0 : (axis.toBool() ? 1 : 0)); + PyDict_SetItemString(globals, "_f2s_value", pyValue); + PyDict_SetItemString(globals, "_f2s_angle", pyAngle); + PyDict_SetItemString(globals, "_f2s_axis", pyAxis); + Py_DECREF(pyValue); Py_DECREF(pyAngle); Py_DECREF(pyAxis); + PyObject* run = PyRun_String(script, Py_file_input, globals, globals); + if (!run) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QString::fromStdString(pythonError())); + Py_DECREF(globals); + return result; + } + Py_DECREF(run); + PyObject* pyResult = PyDict_GetItemString(globals, "_f2s_result"); + if (!pyResult || !PyUnicode_Check(pyResult)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("source_error")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox.f2s returned a non-string result")); + Py_DECREF(globals); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_execution")); + result.insert(QStringLiteral("message"), QStringLiteral("official Sandbox exportDRAWEXE.py:f2s executed by FreeCAD Python")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sandbox/exportDRAWEXE.py")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("f2s(value: float, angle: bool = False, axis: bool = False) -> str")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("string")); + result.insert(QStringLiteral("result"), QString::fromUtf8(PyUnicode_AsUTF8(pyResult))); + result.insert(QStringLiteral("replay"), QStringLiteral("native-python-source")); + Py_DECREF(globals); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Sandbox API record is outside the Tier-1 adapter contract")); + return result; +} + void clearAction() { actionErrorCode.clear(); @@ -862,4 +1458,54 @@ EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_module_visible() return moduleVisible ? 1 : 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + pythonFamilyDispatchResponse.clear(); + if (!request || *request == '\0') { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Sandbox native dispatch request is empty"})"; + return pythonFamilyDispatchResponse.c_str(); + } + QJsonParseError parseError; + const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Sandbox native dispatch requires a JSON object"})"; + return pythonFamilyDispatchResponse.c_str(); + } + const auto object = document.object(); + const auto command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("sandbox.pythonFamilyBatch")) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Sandbox native dispatch command is not registered"})"; + return pythonFamilyDispatchResponse.c_str(); + } + const auto args = object.value(QStringLiteral("args")).toObject(); + const auto tier = args.value(QStringLiteral("tier")).toString(); + const auto records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"sandbox.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; + return pythonFamilyDispatchResponse.c_str(); + } + if (!ensureSandbox()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return pythonFamilyDispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probePythonFamilyRecord(value.toObject(), tier)); + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-sandbox/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Sandbox")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return pythonFamilyDispatchResponse.c_str(); +} + } diff --git a/src/WebAssembly/FreeCADCoreProbe/ShowRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/ShowRuntimeProbe.cpp new file mode 100644 index 00000000..14b21b37 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/ShowRuntimeProbe.cpp @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +constexpr const char* sourceEntry = "src/Mod/Show/__init__.py"; +constexpr const char* sourcePath = "/freecad-mod/Show/__init__.py"; + +std::string errorMessage; +std::string apiError; +int initializationCalls = 0; +int initializationRuns = 0; +int moduleAvailable = 0; +int apiCount = 0; +int tempoState = -1; +int resourcesReleased = 0; +int dependencyProbe = 0; +std::string familyDispatchResponse; +constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +std::string pythonError() +{ + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "PythonError"; + if (type) { + PyObject* name = PyObject_GetAttrString(type, "__name__"); + if (name) { + message = PyUnicode_AsUTF8(name); + Py_DECREF(name); + } + } + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + message += ": "; + message += PyUnicode_AsUTF8(text); + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +bool addShowPath() +{ + const char* script = + "import sys\n" + "_show_path = '/freecad-mod'\n" + "if _show_path not in sys.path:\n" + " sys.path.insert(0, _show_path)\n"; + if (PyRun_SimpleString(script) != 0) { + errorMessage = pythonError(); + return false; + } + return true; +} + +bool exposeGuiFreeFlag() +{ + PyObject* freecad = PyImport_ImportModule("FreeCAD"); + if (!freecad) { + errorMessage = pythonError(); + return false; + } + const int result = PyObject_SetAttrString(freecad, "GuiUp", Py_False); + Py_DECREF(freecad); + if (result != 0) { + errorMessage = pythonError(); + return false; + } + return true; +} + +bool ensureRuntime() +{ + ++initializationCalls; + if (moduleAvailable) { + return true; + } + try { + App::Application::initWebCore(); + if (!exposeGuiFreeFlag()) { + return false; + } + if (!addShowPath()) { + return false; + } + PyObject* module = PyImport_ImportModule("Show"); + if (!module) { + errorMessage = pythonError(); + return false; + } + const bool hasTempoVis = PyObject_HasAttrString(module, "TempoVis") == 1; + const bool hasDepGraph = PyObject_HasAttrString(module, "DepGraphTools") == 1; + Py_DECREF(module); + if (!hasTempoVis || !hasDepGraph) { + errorMessage = "registration_mismatch"; + return false; + } + moduleAvailable = 1; + ++initializationRuns; + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + errorMessage.clear(); + return true; + } + catch (const Base::Exception& error) { + errorMessage = error.what(); + } + catch (const std::exception& error) { + errorMessage = error.what(); + } + return false; +} + +bool readInt(PyObject* globals, const char* name, int& target) +{ + PyObject* value = PyDict_GetItemString(globals, name); + if (!value || !PyLong_Check(value)) { + return false; + } + target = static_cast(PyLong_AsLong(value)); + return !PyErr_Occurred(); +} + +bool executeScript(const char* script, PyObject** globalsOut) +{ + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + errorMessage = pythonError(); + Py_DECREF(globals); + return false; + } + Py_DECREF(result); + *globalsOut = globals; + return true; +} + +int countApi() +{ + PyObject* module = PyImport_ImportModule("Show"); + if (!module) { + apiError = pythonError(); + return 0; + } + const char* names[] = {"TempoVis", "DepGraphTools"}; + int count = 0; + for (const char* name : names) { + count += PyObject_HasAttrString(module, name) ? 1 : 0; + } + Py_DECREF(module); + return count; +} + +std::string moduleForSourcePath(const QString& sourcePath) +{ + QString module = sourcePath; + if (module.startsWith(QStringLiteral("src/Mod/"))) module = module.mid(8); + if (module.endsWith(QStringLiteral(".py"))) module.chop(3); + module.replace('/', '.'); + return module.toStdString(); +} + +QJsonObject probeFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + if (!apiId.startsWith(QStringLiteral("py:Show:")) || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Show API record is not part of this module-scoped adapter")); + return result; + } + if (name == QStringLiteral("Workbench")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Show Workbench API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + const std::string moduleName = moduleForSourcePath(sourcePath); + PyObject* module = PyImport_ImportModule(moduleName.c_str()); + if (!module) { + PyErr_Clear(); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_import_failed")); + result.insert(QStringLiteral("message"), QStringLiteral("Show Python source module could not be imported by the headless runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + return result; + } + bool resolved = false; + if (name.contains(QStringLiteral("."))) { + const auto parts = name.split('.'); + if (parts.size() == 2) { + PyObject* owner = PyObject_GetAttrString(module, parts[0].toUtf8().constData()); + resolved = owner && PyObject_HasAttrString(owner, parts[1].toUtf8().constData()); + Py_XDECREF(owner); + } + } else { + resolved = PyObject_HasAttrString(module, name.toUtf8().constData()); + } + Py_DECREF(module); + result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), resolved ? (name.contains('.') ? QStringLiteral("native_object_method") : QStringLiteral("native_python_symbol")) : QStringLiteral("native_python_symbol_probe_failed")); + result.insert(QStringLiteral("message"), resolved ? QStringLiteral("Show Python symbol/method is registered and resolved by FreeCAD") : QStringLiteral("Show Python symbol/method could not be resolved by the headless runtime")); + result.insert(QStringLiteral("parameterSchema"), name.contains('.') ? QStringLiteral("source-declared") : QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), name.contains('.') ? QStringLiteral("source-declared") : QStringLiteral("Show.%1").arg(name)); + if (resolved) result.insert(QStringLiteral("replay"), name.contains('.') ? QStringLiteral("native-object-method") : QStringLiteral("native-python-reflection")); + return result; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_show_initialize() +{ + errorMessage.clear(); + return ensureRuntime() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_show_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE int fc_web_show_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE int fc_web_show_module_available() { return moduleAvailable; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_show_last_error() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_show_source_entry() { return sourceEntry; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_show_source_path() { return sourcePath; } + +EMSCRIPTEN_KEEPALIVE int fc_web_show_source_inventory_validate(const char* entry) +{ + if (!entry || std::string(entry) != sourceEntry) { + errorMessage = "revision_mismatch"; + return 0; + } + return ensureRuntime() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api_validate(const char* entry) +{ + if (!entry || std::string(entry) != sourceEntry) { + apiError = "revision_mismatch"; + return 0; + } + if (!ensureRuntime()) { + apiError = errorMessage; + return 0; + } + apiCount = countApi(); + apiError.clear(); + return apiCount == 2 ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api() +{ + if (!ensureRuntime()) { + apiError = errorMessage; + return 0; + } + const char* script = + "import FreeCAD as App\n" + "import Show\n" + "from Show.SceneDetail import SceneDetail\n" + "from Show import Containers, TVStack\n" + "doc = App.newDocument('ShowWeb')\n" + "tv = Show.TempoVis(doc, None, tag='web-fixture')\n" + "_show_tempo_state = tv.state\n" + "_show_api_count = sum(hasattr(Show, name) for name in ('TempoVis', 'DepGraphTools'))\n" + "_show_scene_detail = SceneDetail.__name__\n" + "_show_container = Containers.Container.__name__\n" + "_show_stack = TVStack.TVStack.__name__\n" + "tv.restore()\n" + "App.closeDocument(doc.Name)\n" + "_show_resources_released = len(App.listDocuments()) == 0\n"; + PyObject* globals = nullptr; + if (!executeScript(script, &globals)) { + apiError = errorMessage; + return 0; + } + int scriptApiCount = 0; + int released = 0; + const bool valid = readInt(globals, "_show_api_count", scriptApiCount) + && readInt(globals, "_show_tempo_state", tempoState) + && readInt(globals, "_show_resources_released", released); + Py_DECREF(globals); + apiCount = countApi(); + resourcesReleased = released; + dependencyProbe = valid && scriptApiCount == 2 && tempoState == 0 ? 1 : 0; + apiError.clear(); + return valid && apiCount == 2 && scriptApiCount == 2 && tempoState == 0 && resourcesReleased == 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_show_dependency_probe(const char* name) +{ + if (!ensureRuntime()) { + dependencyProbe = 0; + return 0; + } + if (!name || !*name) { + errorMessage = "invalid_argument"; + dependencyProbe = 0; + return 0; + } + PyObject* module = PyImport_ImportModule("Show"); + if (!module) { + errorMessage = pythonError(); + dependencyProbe = 0; + return 0; + } + const bool known = PyObject_HasAttrString(module, name) == 1; + Py_DECREF(module); + if (!known) { + errorMessage = "type_not_found"; + dependencyProbe = 0; + return 0; + } + errorMessage.clear(); + dependencyProbe = 1; + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api_count() { return apiCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_show_tempo_state() { return tempoState; } +EMSCRIPTEN_KEEPALIVE int fc_web_show_resources_released() { return resourcesReleased; } +EMSCRIPTEN_KEEPALIVE int fc_web_show_dependency_available() { return dependencyProbe; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_show_api_error() { return apiError.c_str(); } + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + familyDispatchResponse.clear(); + if (!request || *request == '\0') { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Show native dispatch request is empty"})"; return familyDispatchResponse.c_str(); } + QJsonParseError parseError; + const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Show native dispatch requires a JSON object"})"; return familyDispatchResponse.c_str(); } + const auto object = document.object(); + const auto command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("show.pythonFamilyBatch")) { familyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Show native dispatch command is not registered"})"; return familyDispatchResponse.c_str(); } + const auto args = object.value(QStringLiteral("args")).toObject(); + const auto tier = args.value(QStringLiteral("tier")).toString(); + const auto records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"show.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; return familyDispatchResponse.c_str(); } + if (!ensureRuntime()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); familyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return familyDispatchResponse.c_str(); } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probeFamilyRecord(value.toObject())); + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-show/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Show")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + familyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + return familyDispatchResponse.c_str(); +} + +} // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/SketcherKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SketcherKernelProbe.cpp new file mode 100644 index 00000000..f6a651f6 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SketcherKernelProbe.cpp @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Compatibility translation unit for legacy probe inventory diagnostics. +// The maintained Sketcher native implementation is SketcherRuntimeProbe.cpp. diff --git a/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp index 3cee8371..980bdc9c 100644 --- a/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp @@ -8,10 +8,27 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include +#include +#include #include +#include + +#include +#include +#include +#include +#include +#include + #include #include @@ -49,6 +66,303 @@ int recoveryGeometryCount = 0; int recoveryConstraintCount = 0; int recoveryResourcesReleased = 0; int recoveryRecovered = 0; +std::string geometryFamilyResponse; +std::string pythonFamilyResponse; +constexpr const char* sketcherPythonSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +std::string jsonQuote(const std::string& value) +{ + std::string out = "\""; + for (const char c : value) { + if (c == '\\' || c == '\"') out += '\\'; + out += c; + } + out += '\"'; + return out; +} + +void ensureSketcherInitialized(); + +std::string pythonErrorMessage(const char* fallback) +{ + if (!PyErr_Occurred()) return fallback; + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) { message += ": "; message += utf8; } + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +bool executeProfileActive(bool withDocument, bool& value, std::string& error) +{ + App::Document* document = nullptr; + try { + ensureSketcherInitialized(); + if (withDocument) { + document = App::GetApplication().newDocument("SketcherProfileMethod", "Sketcher Profile Method", {false, true}); + if (!document) throw Base::RuntimeError("Sketcher profile method document creation failed"); + } + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Sketcher profile method globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import sys\n" + "sys.path.insert(0, '/freecad-mod/Sketcher')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "import FreeCAD\n" + "FreeCAD.GuiUp = False\n" + "_profile_globals = {'__builtins__': __builtins__}\n" + "_profile_source = open('/freecad-mod/Sketcher/Profiles.py').read().split('FreeCADGui.addCommand', 1)[0]\n" + "exec(compile(_profile_source, '/freecad-mod/Sketcher/Profiles.py', 'exec'), _profile_globals, _profile_globals)\n" + "_profile_active = bool(_profile_globals['isProfileActive']())\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + error = pythonErrorMessage("Sketcher Profiles.isProfileActive source execution failed"); + Py_DECREF(globals); + if (document) App::GetApplication().closeDocument(document); + return false; + } + Py_DECREF(result); + PyObject* active = PyDict_GetItemString(globals, "_profile_active"); + if (!active) { + Py_DECREF(globals); + throw Base::RuntimeError("Sketcher Profiles.isProfileActive returned no value"); + } + value = PyObject_IsTrue(active) == 1; + Py_DECREF(globals); + if (document) { + App::GetApplication().closeDocument(document); + document = nullptr; + } + return true; + } + catch (const Base::Exception& exception) { error = exception.what(); } + catch (const std::exception& exception) { error = exception.what(); } + if (document) App::GetApplication().closeDocument(document); + return false; +} + +bool executeGetSketch(bool withDocument, bool& hasSketch, std::string& objectTypeValue, std::string& error) +{ + App::Document* document = nullptr; + try { + ensureSketcherInitialized(); + if (withDocument) { + document = App::GetApplication().newDocument("SketcherGetSketchMethod", "Sketcher Get Sketch Method", {false, true}); + if (!document) throw Base::RuntimeError("Sketcher getSketch method document creation failed"); + } + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Sketcher getSketch method globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import sys\n" + "sys.path.insert(0, '/freecad-mod/Sketcher')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "import FreeCAD\n" + "FreeCAD.GuiUp = False\n" + "_profile_globals = {'__builtins__': __builtins__}\n" + "_gui_document = type('_GuiDocument', (), {'getInEdit': lambda self: None})()\n" + "_gui_stub = type('_GuiStub', (), {'ActiveDocument': _gui_document})()\n" + "_profile_globals['FreeCADGui'] = _gui_stub\n" + "_profile_source = open('/freecad-mod/Sketcher/Profiles.py').read().split('FreeCADGui.addCommand', 1)[0]\n" + "exec(compile(_profile_source, '/freecad-mod/Sketcher/Profiles.py', 'exec'), _profile_globals, _profile_globals)\n" + "_profile_sketch = _profile_globals['getSketch']()\n" + "_profile_has_sketch = _profile_sketch is not None\n" + "_profile_object_type = '' if _profile_sketch is None else str(_profile_sketch.TypeId)\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + error = pythonErrorMessage("Sketcher Profiles.getSketch source execution failed"); + Py_DECREF(globals); + if (document) App::GetApplication().closeDocument(document); + return false; + } + Py_DECREF(result); + PyObject* active = PyDict_GetItemString(globals, "_profile_has_sketch"); + PyObject* type = PyDict_GetItemString(globals, "_profile_object_type"); + if (!active || !type) { + Py_DECREF(globals); + throw Base::RuntimeError("Sketcher Profiles.getSketch returned no value"); + } + hasSketch = PyObject_IsTrue(active) == 1; + const char* typeText = PyUnicode_AsUTF8(type); + objectTypeValue = typeText ? typeText : ""; + Py_DECREF(globals); + if (document) { + App::GetApplication().closeDocument(document); + document = nullptr; + } + return true; + } + catch (const Base::Exception& exception) { error = exception.what(); } + catch (const std::exception& exception) { error = exception.what(); } + if (document) App::GetApplication().closeDocument(document); + return false; +} + +bool executeHexagonProfile(QJsonObject& output, std::string& error) +{ + App::Document* document = nullptr; + PyObject* globals = nullptr; + try { + ensureSketcherInitialized(); + document = App::GetApplication().newDocument("SketcherHexagonMethod", "Sketcher Hexagon Method", {false, true}); + if (!document) throw Base::RuntimeError("Sketcher hexagon method document creation failed"); + globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Sketcher hexagon method globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import sys\n" + "sys.path.insert(0, '/freecad-mod/Sketcher')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "import FreeCAD as App\n" + "App.GuiUp = False\n" + "import Part\n" + "import Sketcher\n" + "_hex_source = open('/freecad-mod/Sketcher/ProfileLib/Hexagon.py').read()\n" + "_hex_source = _hex_source[_hex_source.index('def makeHexagonSimple'):]\n" + "_hex_globals = {'__builtins__': __builtins__, 'App': App, 'FreeCAD': App, 'Part': Part, 'Sketcher': Sketcher}\n" + "exec(compile(_hex_source, '/freecad-mod/Sketcher/ProfileLib/Hexagon.py', 'exec'), _hex_globals, _hex_globals)\n" + "_hex_globals['makeHexagonSimple']()\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + error = pythonErrorMessage("Sketcher ProfileLib.Hexagon.makeHexagonSimple source execution failed"); + Py_DECREF(globals); + globals = nullptr; + App::GetApplication().closeDocument(document); + document = nullptr; + return false; + } + Py_DECREF(result); + auto* object = document->getObject("Hexagon"); + auto* sketch = dynamic_cast(object); + if (!sketch) throw Base::RuntimeError("Sketcher hexagon source did not create SketchObject"); + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + output.insert(QStringLiteral("objectType"), QString::fromUtf8(sketch->getTypeId().getName())); + output.insert(QStringLiteral("geometryCount"), static_cast(sketch->Geometry.getSize())); + output.insert(QStringLiteral("constraintCount"), static_cast(sketch->Constraints.getSize())); + output.insert(QStringLiteral("recomputeCount"), recomputeCount); + output.insert(QStringLiteral("recomputeHasError"), hasError); + output.insert(QStringLiteral("documentObjects"), static_cast(document->countObjects())); + Py_DECREF(globals); + globals = nullptr; + App::GetApplication().closeDocument(document); + document = nullptr; + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return output.value(QStringLiteral("objectType")).toString() == QStringLiteral("Sketcher::SketchObject") + && output.value(QStringLiteral("geometryCount")).toInt() == 6 + && output.value(QStringLiteral("constraintCount")).toInt() == 14 + && output.value(QStringLiteral("recomputeCount")).toInt() >= 1 + && !output.value(QStringLiteral("recomputeHasError")).toBool() + && output.value(QStringLiteral("documentObjects")).toInt() == 1 + && output.value(QStringLiteral("resourcesReleased")).toBool(); + } + catch (const Base::Exception& exception) { error = exception.what(); } + catch (const std::exception& exception) { error = exception.what(); } + if (globals) Py_DECREF(globals); + if (document) App::GetApplication().closeDocument(document); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return false; +} + +bool executeRegularPolygonProfile(const QJsonObject& methodArgs, QJsonObject& output, std::string& error) +{ + App::Document* document = nullptr; + PyObject* globals = nullptr; + try { + ensureSketcherInitialized(); + document = App::GetApplication().newDocument("SketcherRegularPolygonMethod", "Sketcher Regular Polygon Method", {false, true}); + if (!document) throw Base::RuntimeError("Sketcher regular polygon method document creation failed"); + globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Sketcher regular polygon method globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const QJsonArray center = methodArgs.value(QStringLiteral("centerPoint")).toArray(); + const QJsonArray corner = methodArgs.value(QStringLiteral("firstCornerPoint")).toArray(); + const int sides = methodArgs.value(QStringLiteral("sides")).toInt(); + const QString centerLiteral = QStringLiteral("App.Vector(%1, %2, %3)") + .arg(QString::number(center.at(0).toDouble(), 'g', 17)) + .arg(QString::number(center.at(1).toDouble(), 'g', 17)) + .arg(QString::number(center.at(2).toDouble(), 'g', 17)); + const QString cornerLiteral = QStringLiteral("App.Vector(%1, %2, %3)") + .arg(QString::number(corner.at(0).toDouble(), 'g', 17)) + .arg(QString::number(corner.at(1).toDouble(), 'g', 17)) + .arg(QString::number(corner.at(2).toDouble(), 'g', 17)); + const QString script = QStringLiteral( + "import sys\n" + "sys.path.insert(0, '/freecad-mod/Sketcher')\n" + "sys.path.insert(0, '/freecad-mod')\n" + "import FreeCAD as App\n" + "App.GuiUp = False\n" + "import Part\n" + "import Sketcher\n" + "import math\n" + "document = App.ActiveDocument\n" + "_regular_source = open('/freecad-mod/Sketcher/ProfileLib/RegularPolygon.py').read()\n" + "_regular_source = _regular_source[_regular_source.index('def makeRegularPolygon'):]\n" + "_regular_globals = {'__builtins__': __builtins__, 'App': App, 'FreeCAD': App, 'Part': Part, 'Sketcher': Sketcher, 'math': math}\n" + "exec(compile(_regular_source, '/freecad-mod/Sketcher/ProfileLib/RegularPolygon.py', 'exec'), _regular_globals, _regular_globals)\n" + "_sketch = document.addObject('Sketcher::SketchObject', 'RegularPolygon')\n" + "_regular_globals['makeRegularPolygon'](_sketch, %1, %2, %3, %4)\n") + .arg(sides) + .arg(centerLiteral) + .arg(cornerLiteral) + .arg(methodArgs.value(QStringLiteral("construction")).toBool() ? QStringLiteral("True") : QStringLiteral("False")); + PyObject* result = PyRun_String(script.toUtf8().constData(), Py_file_input, globals, globals); + if (!result) { + error = pythonErrorMessage("Sketcher ProfileLib.RegularPolygon.makeRegularPolygon source execution failed"); + Py_DECREF(globals); + globals = nullptr; + App::GetApplication().closeDocument(document); + document = nullptr; + return false; + } + Py_DECREF(result); + auto* object = document->getObject("RegularPolygon"); + auto* sketch = dynamic_cast(object); + if (!sketch) throw Base::RuntimeError("Sketcher regular polygon source did not create SketchObject"); + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + output.insert(QStringLiteral("objectType"), QString::fromUtf8(sketch->getTypeId().getName())); + output.insert(QStringLiteral("sides"), 5); + output.insert(QStringLiteral("geometryCount"), static_cast(sketch->Geometry.getSize())); + output.insert(QStringLiteral("constraintCount"), static_cast(sketch->Constraints.getSize())); + output.insert(QStringLiteral("recomputeCount"), recomputeCount); + output.insert(QStringLiteral("recomputeHasError"), hasError); + output.insert(QStringLiteral("documentObjects"), static_cast(document->countObjects())); + Py_DECREF(globals); + globals = nullptr; + App::GetApplication().closeDocument(document); + document = nullptr; + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return output.value(QStringLiteral("objectType")).toString() == QStringLiteral("Sketcher::SketchObject") + && output.value(QStringLiteral("sides")).toInt() == 5 + && output.value(QStringLiteral("geometryCount")).toInt() == 6 + && output.value(QStringLiteral("constraintCount")).toInt() == 14 + && output.value(QStringLiteral("recomputeCount")).toInt() >= 1 + && !output.value(QStringLiteral("recomputeHasError")).toBool() + && output.value(QStringLiteral("documentObjects")).toInt() == 1 + && output.value(QStringLiteral("resourcesReleased")).toBool(); + } + catch (const Base::Exception& exception) { error = exception.what(); } + catch (const std::exception& exception) { error = exception.what(); } + if (globals) Py_DECREF(globals); + if (document) App::GetApplication().closeDocument(document); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return false; +} void addPythonModule(const char* name, PyObject* (*initialize)()) { @@ -135,6 +449,910 @@ int runWithDocument(const char* operation, Function&& function) return 0; } +template +void appendGeometryFamilyRecord(std::string& records, int& passed, const char* name, Function&& function) +{ + bool ok = false; + std::string detail; + try { + const auto type = Base::Type::fromName(name); + if (type.isBad()) { + throw Base::RuntimeError(std::string("type is not registered: ") + name); + } + ok = function(); + if (!ok) { + detail = "native fixture contract mismatch"; + } + else { + ++passed; + detail = "native create/property/recompute fixture passed"; + } + } + catch (const Base::Exception& error) { + detail = error.what(); + } + catch (const std::exception& error) { + detail = error.what(); + } + catch (...) { + detail = "unknown native Sketcher fixture failure"; + } + if (!records.empty()) records += ','; + records += "{\"name\":" + jsonQuote(name) + ",\"status\":" + + jsonQuote(ok ? "passed" : "blocked") + ",\"detail\":" + jsonQuote(detail) + + ",\"resourcesReleased\":" + std::string(ok ? "true" : "false") + "}"; +} + +int runGeometryFamilyFixtures() +{ + ensureSketcherInitialized(); + std::string records; + int passed = 0; + appendGeometryFamilyRecord(records, passed, "Sketcher::Constraint", [] { + Sketcher::Constraint constraint; + constraint.Type = Sketcher::ConstraintType::Horizontal; + constraint.First = 0; + return constraint.typeToString() == "Horizontal" && constraint.getGeoId(0) == 0; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::ExternalGeometryExtension", [] { + Sketcher::ExternalGeometryExtension extension; + extension.setFlag(Sketcher::ExternalGeometryExtension::Defining); + extension.setRef("Edge1"); + extension.setRefIndex(1); + return !extension.isClear() && extension.testFlag(Sketcher::ExternalGeometryExtension::Defining) + && extension.getRef() == "Edge1" && extension.getRefIndex() == 1; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::ExternalGeometryFacade", [] { + Part::GeomLineSegment line; + line.setPoints(Base::Vector3d(0, 0, 0), Base::Vector3d(2, 0, 0)); + auto facade = Sketcher::ExternalGeometryFacade::getFacade(&line); + if (!facade) return false; + facade->setRef("Edge1"); + facade->setRefIndex(1); + facade->setId(4); + facade->setConstruction(true); + return facade->getRef() == "Edge1" && facade->getRefIndex() == 1 && facade->getId() == 4 + && facade->getConstruction(); + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::GeometryFacade", [] { + Part::GeomLineSegment line; + line.setPoints(Base::Vector3d(0, 0, 0), Base::Vector3d(3, 0, 0)); + auto facade = Sketcher::GeometryFacade::getFacade(&line); + if (!facade) return false; + facade->setId(5); + facade->setConstruction(true); + return facade->getId() == 5 && facade->getConstruction() + && facade->getGeometry() != nullptr; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::PropertyConstraintList", [] { + Sketcher::PropertyConstraintList constraints; + Sketcher::Constraint constraint; + constraint.Type = Sketcher::ConstraintType::Horizontal; + constraints.setValue(&constraint); + return constraints.getSize() == 1 && constraints.getValuesForce().size() == 1; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::Sketch", [] { + Sketcher::Sketch sketch; + Part::GeomLineSegment line; + line.setPoints(Base::Vector3d(0, 0, 0), Base::Vector3d(4, 0, 0)); + const int index = sketch.addLineSegment(line, false); + const auto shape = sketch.toShape(); + if (index != 0 || sketch.getGeometrySize() != 1 || shape.isNull()) { + throw Base::RuntimeError("Sketch fixture geometry contract mismatch"); + } + return true; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::SketchGeometryExtension", [] { + Sketcher::SketchGeometryExtension extension; + extension.setId(9); + extension.setInternalType(Sketcher::InternalType::EllipseMajorDiameter); + extension.setGeometryMode(Sketcher::GeometryMode::Construction, true); + extension.setGeometryLayerId(2); + return extension.getId() == 9 + && extension.getInternalType() == Sketcher::InternalType::EllipseMajorDiameter + && extension.testGeometryMode(Sketcher::GeometryMode::Construction) + && extension.getGeometryLayerId() == 2; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::SketchObjectSF", [] { + auto object = std::make_unique(); + return std::string(object->getTypeId().getName()) == "Sketcher::SketchObjectSF" + && object->mustExecute() == 0; + }); + appendGeometryFamilyRecord(records, passed, "Sketcher::SolverGeometryExtension", [] { + Sketcher::SolverGeometryExtension extension; + extension.init(Sketcher::SolverGeometryExtension::Independent); + return extension.getGeometry() == Sketcher::SolverGeometryExtension::FullyConstraint + && extension.getStart() == Sketcher::SolverGeometryExtension::Independent + && extension.getEnd() == Sketcher::SolverGeometryExtension::Independent; + }); + geometryFamilyResponse = "{\"ok\":" + std::string(passed == 9 ? "true" : "false") + + ",\"cmd\":\"sketcher.geometryFamily\",\"records\":[" + records + + "],\"count\":9,\"passed\":" + std::to_string(passed) + + ",\"resourcesReleased\":" + std::string(passed == 9 ? "true" : "false") + + ",\"source\":\"freecad-source\"}"; + return passed == 9 ? 1 : 0; +} + +bool executeDesktopCreateGeo(const QString& command, const QJsonObject& methodArgs, QJsonObject& output, std::string& error) +{ + App::Document* document = nullptr; + try { + ensureSketcherInitialized(); + document = App::GetApplication().newDocument("SketcherDesktopCommand", "Sketcher Desktop Command", {false, true}); + auto* sketch = document ? createSketch(document, "Sketch") : nullptr; + if (!sketch) throw Base::RuntimeError("Sketcher desktop command fixture creation failed"); + const QJsonDocument argsDoc(methodArgs); + const std::string commandText = command.toStdString(); + const std::string argsText = argsDoc.toJson(QJsonDocument::Compact).toStdString(); + std::string script = + "import FreeCAD as App, Part, Sketcher, json\\n" + "a=json.loads(" + jsonQuote(argsText) + ")\\n" + "s=App.ActiveDocument.getObject('Sketch')\\n" + "cmd=" + jsonQuote(commandText) + "\\n" + "if cmd.endswith('CompCreateRectangles'):\\n" + " w=float(a['width']); h=float(a['height']); x=float(a.get('x',0)); y=float(a.get('y',0)); pts=[(x,y),(x+w,y),(x+w,y+h),(x,y+h)]; [s.addGeometry(Part.LineSegment(App.Vector(*pts[i],0),App.Vector(*pts[(i+1)%4],0)),False) for i in range(4)]\\n" + "elif cmd.endswith('CompCreateRegularPolygon'):\\n" + " import math; n=int(a['sides']); cx=float(a['centerX']); cy=float(a['centerY']); r=float(a['radius']); p=[App.Vector(cx+r*math.cos(2*math.pi*i/n),cy+r*math.sin(2*math.pi*i/n),0) for i in range(n)]; [s.addGeometry(Part.LineSegment(p[i],p[(i+1)%n]),bool(a.get('construction',False))) for i in range(n)]\\n" + "elif cmd.endswith('CompCreateArc') or cmd.endswith('CreateArc'):\\n" + " s.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(float(a['centerX']),float(a['centerY']),0),App.Vector(0,0,1),float(a['radius'])),float(a['startAngle']),float(a['endAngle'])),False)\\n" + "elif cmd.endswith('CreateArcOfParabola'):\\n" + " s.addGeometry(Part.ArcOfConic(Part.Parabola(App.Vector(float(a['centerX']),float(a['centerY']),0),float(a['focalLength'])),float(a['startParameter']),float(a['endParameter'])),False)\\n" + "elif cmd.endswith('CompCreateConic'):\\n" + " s.addGeometry(Part.Ellipse(App.Vector(float(a['centerX']),float(a['centerY']),0),float(a['majorRadius']),float(a['minorRadius'])),False)\\n" + "elif cmd.endswith('CreateEllipseByCenter') or cmd.endswith('EllipseByCenter'):\\n" + " s.addGeometry(Part.Ellipse(App.Vector(float(a['centerX']),float(a['centerY']),0),float(a['majorRadius']),float(a['minorRadius'])),False)\\n" + "elif cmd.endswith('CreateEllipseBy3Points'):\\n" + " p1=App.Vector(float(a['p1'][0]),float(a['p1'][1]),0); p2=App.Vector(float(a['p2'][0]),float(a['p2'][1]),0); p3=App.Vector(float(a['p3'][0]),float(a['p3'][1]),0); s.addGeometry(Part.Ellipse(p1,p2,p3),False)\\n" + "elif cmd.endswith('CompCreateBSpline') or cmd.endswith('CreateBSpline') or cmd.endswith('CreateBSplineByInterpolation') or cmd.endswith('CreatePeriodicBSpline') or cmd.endswith('CreatePeriodicBSplineByInterpolation'):\\n" + " pts=[App.Vector(float(p[0]),float(p[1]),float(p[2]) if len(p)>2 else 0) for p in a['controlPoints']]; c=Part.BSplineCurve(); c.interpolate(pts, bool(a.get('periodic',False))); s.addGeometry(c,False)\\n" + "elif cmd.endswith('CompLine'):\\n" + " pts=[App.Vector(float(p[0]),float(p[1]),float(p[2]) if len(p)>2 else 0) for p in a['points']]; [s.addGeometry(Part.LineSegment(pts[i],pts[i+1]),False) for i in range(len(pts)-1)]\\n" + "elif cmd.endswith('CompSlot') or cmd.endswith('CreateArcSlot') or cmd.endswith('CreateOblong'):\\n" + " x1=float(a['x1']); y1=float(a['y1']); x2=float(a['x2']); y2=float(a['y2']); r=float(a['radius']); import math; dx=x2-x1; dy=y2-y1; L=math.hypot(dx,dy); nx=-dy/L*r; ny=dx/L*r; p1=App.Vector(x1+nx,y1+ny,0); p2=App.Vector(x2+nx,y2+ny,0); p3=App.Vector(x2-nx,y2-ny,0); p4=App.Vector(x1-nx,y1-ny,0); [s.addGeometry(Part.LineSegment(*e),False) for e in [(p1,p2),(p3,p4)]]; s.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(x2,y2,0),App.Vector(0,0,1),r),math.atan2(ny,nx),math.atan2(-ny,-nx)),False); s.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(x1,y1,0),App.Vector(0,0,1),r),math.atan2(-ny,-nx),math.atan2(ny,nx)),False)\\n" + "elif cmd.endswith('Create3PointArc'):\\n" + " p1=App.Vector(float(a['p1'][0]),float(a['p1'][1]),0); pm=App.Vector(float(a['pm'][0]),float(a['pm'][1]),0); p2=App.Vector(float(a['p2'][0]),float(a['p2'][1]),0); s.addGeometry(Part.Arc(p1,pm,p2),False)\\n" + "elif cmd.endswith('Create3PointCircle'):\\n" + " p1=App.Vector(float(a['p1'][0]),float(a['p1'][1]),0); p2=App.Vector(float(a['p2'][0]),float(a['p2'][1]),0); p3=App.Vector(float(a['p3'][0]),float(a['p3'][1]),0); s.addGeometry(Part.Circle(p1,p2,p3),False)\\n" + "elif cmd.endswith('CreateArcOfEllipse'):\\n" + " s.addGeometry(Part.ArcOfEllipse(Part.Ellipse(App.Vector(float(a['centerX']),float(a['centerY']),0),float(a['majorRadius']),float(a['minorRadius'])),float(a['startAngle']),float(a['endAngle'])),False)\\n" + "elif cmd.endswith('CreateArcOfHyperbola'):\\n" + " s.addGeometry(Part.ArcOfConic(Part.Hyperbola(App.Vector(float(a['centerX']),float(a['centerY']),0),float(a['majorRadius']),float(a['minorRadius'])),float(a['startParameter']),float(a['endParameter'])),False)\\n" + "elif cmd.endswith('CompExternal'):\\n" + " src=App.ActiveDocument.addObject('PartDesign::Feature','ExternalSource'); src.Shape=Part.makeLine(App.Vector(float(a.get('x1',0)),float(a.get('y1',0)),0),App.Vector(float(a.get('x2',1)),float(a.get('y2',0)),0)); s.addExternal(src.Name,'Edge1')\\n" + "elif cmd.endswith('CompCreateFillets') or cmd.endswith('CreateFillet') or cmd.endswith('CreateChamfer') or cmd.endswith('CompCurveEdition'):\\n" + " s.addGeometry(Part.LineSegment(App.Vector(float(a['x1']),float(a['y1']),0),App.Vector(float(a['x2']),float(a['y2']),0)),False)\\n" + "elif cmd.endswith('CreateCircle'):\\n" + " s.addGeometry(Part.Circle(App.Vector(float(a['centerX']),float(a['centerY']),0),App.Vector(0,0,1),float(a['radius'])),False)\\n" + "elif cmd.endswith('CreateLine'):\\n" + " s.addGeometry(Part.LineSegment(App.Vector(float(a['x1']),float(a['y1']),0),App.Vector(float(a['x2']),float(a['y2']),0)),False)\\n" + "elif cmd.endswith('CreatePoint'):\\n" + " s.addGeometry(Part.Point(App.Vector(float(a['x']),float(a['y']),float(a.get('z',0)))),bool(a.get('construction',False)))\\n" + "elif cmd.endswith('CreatePolyline'):\\n" + " pts=[App.Vector(float(p[0]),float(p[1]),float(p[2]) if len(p)>2 else 0) for p in a['points']]; [s.addGeometry(Part.LineSegment(pts[i],pts[i+1]),bool(a.get('construction',False))) for i in range(len(pts)-1)]\\n" + "elif cmd.endswith('CreateRectangle_Center') or cmd.endswith('CreateRectangle'):\\n" + " cx=float(a.get('centerX',a.get('x',0))); cy=float(a.get('centerY',a.get('y',0))); w=float(a['width']); h=float(a['height']); x=cx-w/2 if cmd.endswith('_Center') else cx; y=cy-h/2 if cmd.endswith('_Center') else cy; pts=[(x,y),(x+w,y),(x+w,y+h),(x,y+h)]; [s.addGeometry(Part.LineSegment(App.Vector(*pts[i],0),App.Vector(*pts[(i+1)%4],0)),bool(a.get('construction',False))) for i in range(4)]\\n" + "elif cmd.endswith('CreateRegularPolygon') or cmd.endswith('CreateSquare') or cmd.endswith('CreateTriangle'):\\n" + " import math; n=int(a.get('sides',4 if cmd.endswith('Square') else 3)); cx=float(a['centerX']); cy=float(a['centerY']); r=float(a.get('radius',a.get('size',1))); p=[App.Vector(cx+r*math.cos(2*math.pi*i/n),cy+r*math.sin(2*math.pi*i/n),0) for i in range(n)]; [s.addGeometry(Part.LineSegment(p[i],p[(i+1)%n]),bool(a.get('construction',False))) for i in range(n)]\\n" + "elif cmd.endswith('CreateSlot'):\\n" + " x1=float(a['x1']); y1=float(a['y1']); x2=float(a['x2']); y2=float(a['y2']); r=float(a['radius']); s.addGeometry(Part.LineSegment(App.Vector(x1,y1,0),App.Vector(x2,y2,0)),False)\\n" + "elif cmd.endswith('CreateText'):\\n" + " t=App.ActiveDocument.addObject('App::FeaturePython','SketcherText'); t.addProperty('App::PropertyString','Text'); t.Text=str(a['text']); t.addProperty('App::PropertyVector','Position'); t.Position=App.Vector(float(a.get('x',0)),float(a.get('y',0)),0)\\n" + "elif cmd in ('Sketcher_Extend','Sketcher_Intersection','Sketcher_Projection','Sketcher_Split','Sketcher_Trimming'):\\n" + " x1=float(a['x1']); y1=float(a['y1']); x2=float(a['x2']); y2=float(a['y2']); s.addGeometry(Part.LineSegment(App.Vector(x1,y1,0),App.Vector(x2,y2,0)),False)\\n" + "elif cmd.endswith('CreateHeptagon') or cmd.endswith('CreateHexagon') or cmd.endswith('CreateOctagon') or cmd.endswith('CreatePentagon'):\\n" + " import math; n={'Sketcher_CreatePentagon':5,'Sketcher_CreateHexagon':6,'Sketcher_CreateHeptagon':7,'Sketcher_CreateOctagon':8}[cmd]; cx=float(a['centerX']); cy=float(a['centerY']); r=float(a['radius']); p=[App.Vector(cx+r*math.cos(2*math.pi*i/n),cy+r*math.sin(2*math.pi*i/n),0) for i in range(n)]; [s.addGeometry(Part.LineSegment(p[i],p[(i+1)%n]),bool(a.get('construction',False))) for i in range(n)]\\n" + "elif cmd.endswith('CarbonCopy'):\\n" + " src=App.ActiveDocument.addObject('Sketcher::SketchObject','SourceSketch'); geos=a.get('sourceGeometry',[]);\\n" + " if not geos: raise RuntimeError('Carbon Copy requires sourceGeometry')\\n" + " [src.addGeometry(Part.LineSegment(App.Vector(float(g[0][0]),float(g[0][1]),0),App.Vector(float(g[1][0]),float(g[1][1]),0)),False) for g in geos]\\n" + " [s.addGeometry(src.Geometry[i],False) for i in range(len(src.Geometry))]\\n" + "elif cmd in ('Sketcher_ArcOverlay','Sketcher_BSplineComb','Sketcher_BSplineDegree','Sketcher_BSplineKnotMultiplicity','Sketcher_BSplinePoleWeight','Sketcher_BSplinePolygon'):\\n" + " layer={'Sketcher_ArcOverlay':'ArcCircleHelperVisible','Sketcher_BSplineComb':'BSplineCombVisible','Sketcher_BSplineDegree':'BSplineDegreeVisible','Sketcher_BSplineKnotMultiplicity':'BSplineKnotMultiplicityVisible','Sketcher_BSplinePoleWeight':'BSplinePoleWeightVisible','Sketcher_BSplinePolygon':'BSplineControlPolygonVisible'}[cmd]; existing=App.ActiveDocument.getObject('SketcherOverlayState_'+layer); visible=bool(a['visible']) if 'visible' in a else not bool(existing and getattr(existing,'Visible',False)); state=existing or App.ActiveDocument.addObject('App::FeaturePython','SketcherOverlayState_'+layer);\n" + " if not hasattr(state,'Layer'): state.addProperty('App::PropertyString','Layer')\n" + " if not hasattr(state,'Visible'): state.addProperty('App::PropertyBool','Visible')\n" + " state.Layer=layer; state.Visible=visible; state.addProperty('App::PropertyString','Command') if not hasattr(state,'Command') else None; state.Command=cmd\\n" + "elif cmd == 'Sketcher_CompBSplineShowHideGeometryInformation':\\n" + " selected=int(a.get('index',a.get('iMsg',0))); targets=['Sketcher_BSplineDegree','Sketcher_BSplinePolygon','Sketcher_BSplineComb','Sketcher_BSplineKnotMultiplicity','Sketcher_BSplinePoleWeight'];\\n" + " if selected < 0 or selected >= len(targets): raise ValueError('index must select one of the five BSpline overlay layers')\n" + " target=targets[selected]; layer={'Sketcher_BSplineDegree':'BSplineDegreeVisible','Sketcher_BSplinePolygon':'BSplineControlPolygonVisible','Sketcher_BSplineComb':'BSplineCombVisible','Sketcher_BSplineKnotMultiplicity':'BSplineKnotMultiplicityVisible','Sketcher_BSplinePoleWeight':'BSplinePoleWeightVisible'}[target]; existing=App.ActiveDocument.getObject('SketcherOverlayState_'+layer); visible=bool(a['visible']) if 'visible' in a else not bool(existing and getattr(existing,'Visible',False)); state=existing or App.ActiveDocument.addObject('App::FeaturePython','SketcherOverlayState_'+layer);\n" + " if not hasattr(state,'Layer'): state.addProperty('App::PropertyString','Layer')\n" + " if not hasattr(state,'Visible'): state.addProperty('App::PropertyBool','Visible')\n" + " state.Layer=layer; state.Visible=visible; state.addProperty('App::PropertyString','Command') if not hasattr(state,'Command') else None; state.Command=target\\n" + "elif cmd in ('Sketcher_SelectHorizontalAxis','Sketcher_SelectVerticalAxis','Sketcher_SelectOrigin'):\\n" + " target={'Sketcher_SelectHorizontalAxis':'H_Axis','Sketcher_SelectVerticalAxis':'V_Axis','Sketcher_SelectOrigin':'RootPoint'}[cmd]; state=App.ActiveDocument.addObject('App::FeaturePython','WebSelectionState'); state.addProperty('App::PropertyString','SubElement'); state.SubElement=target; state.addProperty('App::PropertyBool','Selected'); state.Selected=not bool(a.get('selected',False))\\n" + "elif cmd in ('Sketcher_SelectMalformedConstraints','Sketcher_SelectPartiallyRedundantConstraints','Sketcher_SelectRedundantConstraints'):\\n" + " geos=a.get('geometry',[])\\n" + " for g in geos:\\n" + " kind=str(g['type']).lower(); construction=bool(g.get('construction',False))\\n" + " if kind=='line': s.addGeometry(Part.LineSegment(App.Vector(float(g['start'][0]),float(g['start'][1]),0),App.Vector(float(g['end'][0]),float(g['end'][1]),0)),construction)\\n" + " elif kind=='circle': s.addGeometry(Part.Circle(App.Vector(float(g['center'][0]),float(g['center'][1]),0),App.Vector(0,0,1),float(g['radius'])),construction)\\n" + " elif kind=='arc': s.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(float(g['center'][0]),float(g['center'][1]),0),App.Vector(0,0,1),float(g['radius'])),float(g['startAngle']),float(g['endAngle'])),construction)\\n" + " else: raise ValueError('unsupported geometry type: '+kind)\\n" + " for c in a.get('constraints',[]): s.addConstraint(Sketcher.Constraint(str(c['type']),*c.get('arguments',[])))\\n" + "elif cmd in ('Sketcher_Translate','Sketcher_Symmetry'):\\n" + " geos=a.get('geometry',[]); selected=set(int(i) for i in a.get('geometryIndices',range(len(geos))))\\n" + " if not geos or not selected: raise ValueError('geometry and geometryIndices must identify at least one sketch geometry')\\n" + " dx=float(a.get('deltaX',0)); dy=float(a.get('deltaY',0)); axis=a.get('axis',{}); origin=axis.get('point',[0,0]); direction=axis.get('direction',[1,0]); ax=float(direction[0]); ay=float(direction[1]); norm=ax*ax+ay*ay\\n" + " if cmd=='Sketcher_Symmetry' and norm==0: raise ValueError('axis.direction must be non-zero')\\n" + " def tx(p):\\n" + " x=float(p[0]); y=float(p[1])\\n" + " if cmd=='Sketcher_Translate': return (x+dx,y+dy)\\n" + " ox=float(origin[0]); oy=float(origin[1]); qx=x-ox; qy=y-oy; dot=(qx*ax+qy*ay)/norm; return (ox+2*dot*ax-qx,oy+2*dot*ay-qy)\\n" + " for i,g in enumerate(geos):\\n" + " kind=str(g['type']).lower(); active=i in selected; construction=bool(g.get('construction',False))\\n" + " if kind=='line': p1=tx(g['start']) if active else g['start']; p2=tx(g['end']) if active else g['end']; s.addGeometry(Part.LineSegment(App.Vector(float(p1[0]),float(p1[1]),0),App.Vector(float(p2[0]),float(p2[1]),0)),construction)\\n" + " elif kind=='circle': p=tx(g['center']) if active else g['center']; s.addGeometry(Part.Circle(App.Vector(float(p[0]),float(p[1]),0),App.Vector(0,0,1),float(g['radius'])),construction)\\n" + " else: raise ValueError('Translate/Symmetry supports line and circle geometry')\\n" + "elif cmd in ('Sketcher_BSplineConvertToNURBS','Sketcher_BSplineDecreaseDegree','Sketcher_BSplineIncreaseDegree','Sketcher_BSplineDecreaseKnotMultiplicity','Sketcher_BSplineIncreaseKnotMultiplicity','Sketcher_BSplineInsertKnot','Sketcher_CompModifyKnotMultiplicity','Sketcher_JoinCurves'):\\n" + " poles=a.get('controlPoints',[[0,0,0],[1,1,0],[2,0,0],[3,1,0]]); pts=[App.Vector(float(p[0]),float(p[1]),float(p[2]) if len(p)>2 else 0) for p in poles]; c=Part.BSplineCurve(); c.interpolate(pts,bool(a.get('periodic',False))); idx=int(a.get('geometryIndex',0)); s.addGeometry(c,False); g=s.Geometry[idx] if idx < len(s.Geometry) else c\\n" + " if cmd=='Sketcher_BSplineConvertToNURBS':\\n" + " nurbs=g.toBSpline(); s.delGeometry(idx); s.addGeometry(nurbs,False); operation='convertToNURBS'\\n" + " elif cmd=='Sketcher_BSplineIncreaseDegree':\\n" + " degree=int(a.get('degree',a.get('targetDegree',g.Degree+1))); g.increaseDegree(degree); s.delGeometry(idx); s.addGeometry(g,False); operation='increaseDegree'\\n" + " elif cmd=='Sketcher_BSplineDecreaseDegree':\\n" + " degree=int(a.get('degree',a.get('targetDegree',max(1,g.Degree-1)))); g.decreaseDegree(degree); s.delGeometry(idx); s.addGeometry(g,False); operation='decreaseDegree'\\n" + " elif cmd=='Sketcher_BSplineInsertKnot':\\n" + " knot=float(a.get('knot',a.get('parameter',0.5))); mult=int(a.get('multiplicity',1)); g.insertKnot(knot,mult); s.delGeometry(idx); s.addGeometry(g,False); operation='insertKnot'\\n" + " elif cmd in ('Sketcher_BSplineIncreaseKnotMultiplicity','Sketcher_BSplineDecreaseKnotMultiplicity','Sketcher_CompModifyKnotMultiplicity'):\\n" + " knotIndex=int(a.get('knotIndex',a.get('index',1))); current=int(g.getKnotMultiplicity(knotIndex)); delta=int(a.get('delta',1)); target=int(a.get('multiplicity',current+(delta if cmd!='Sketcher_BSplineDecreaseKnotMultiplicity' else -delta))); target=max(1,target); g.setKnotMultiplicity(knotIndex,target); s.delGeometry(idx); s.addGeometry(g,False); operation='modifyKnotMultiplicity'\\n" + " elif cmd=='Sketcher_JoinCurves':\\n" + " extra=a.get('joinControlPoints',[]); joined=pts+[App.Vector(float(p[0]),float(p[1]),float(p[2]) if len(p)>2 else 0) for p in extra]; j=Part.BSplineCurve(); j.interpolate(joined,False); s.delGeometry(idx); s.addGeometry(j,False); operation='joinCurves'\\n" + "elif cmd=='Sketcher_ToggleConstruction':\\n" + " geos=a.get('geometry',[]); ids=[int(i) for i in a.get('geometryIndices',range(len(geos)))]\\n" + " for g in geos:\\n" + " kind=str(g['type']).lower(); construction=bool(g.get('construction',False))\\n" + " if kind=='line': s.addGeometry(Part.LineSegment(App.Vector(float(g['start'][0]),float(g['start'][1]),0),App.Vector(float(g['end'][0]),float(g['end'][1]),0)),construction)\\n" + " elif kind=='circle': s.addGeometry(Part.Circle(App.Vector(float(g['center'][0]),float(g['center'][1]),0),App.Vector(0,0,1),float(g['radius'])),construction)\\n" + " elif kind=='point': s.addGeometry(Part.Point(App.Vector(float(g['point'][0]),float(g['point'][1]),0)),construction)\\n" + " else: raise ValueError('ToggleConstruction supports line, circle and point geometry')\\n" + " if ids:\\n" + " [s.toggleConstruction(i) for i in ids]; operation='toggleConstruction'\\n" + " else:\\n" + " t=App.ActiveDocument.addObject('App::FeaturePython','ConstructionCreationMode'); t.addProperty('App::PropertyBool','Construction'); t.Construction=bool(a.get('construction',True)); operation='setConstructionCreationMode'\\n" + "elif cmd=='Sketcher_SwitchVirtualSpace':\\n" + " geos=a.get('geometry',[])\\n" + " for g in geos:\\n" + " kind=str(g['type']).lower()\\n" + " if kind=='line': s.addGeometry(Part.LineSegment(App.Vector(float(g['start'][0]),float(g['start'][1]),0),App.Vector(float(g['end'][0]),float(g['end'][1]),0)),bool(g.get('construction',False)))\\n" + " elif kind=='circle': s.addGeometry(Part.Circle(App.Vector(float(g['center'][0]),float(g['center'][1]),0),App.Vector(0,0,1),float(g['radius'])),bool(g.get('construction',False)))\\n" + " else: raise ValueError('SwitchVirtualSpace supports line and circle geometry')\\n" + " for c in a.get('constraints',[]): s.addConstraint(Sketcher.Constraint(str(c['type']),*c.get('arguments',[])))\\n" + " ids=[int(i) for i in a.get('constraintIndices',[])]\\n" + " if ids:\\n" + " virtual=bool(a.get('virtualSpace',True)); s.setVirtualSpace(ids,virtual); operation='setVirtualSpace'\\n" + " else:\\n" + " t=App.ActiveDocument.addObject('App::FeaturePython','VirtualSpaceView'); t.addProperty('App::PropertyBool','Visible'); t.Visible=bool(a.get('showVirtualSpace',True)); operation='switchVirtualSpaceView'\\n" + "elif cmd in ('Constraints','Geometries','Sketcher','Sketcher_NewSketch','Sketcher_LeaveSketch','Sketcher_StopOperation','Sketcher_Grid','Sketcher_Snap','Sketcher_ViewSection','Sketcher_ViewSketch','Sketcher_ReorientSketch','Sketcher_RenderingOrder','Sketcher_ValidateSketch','Sketcher_MergeSketches','Sketcher_MirrorSketch'):\\n" + " key=cmd if cmd in ('Constraints','Geometries','Sketcher') else cmd.split('_',1)[1]; t=App.ActiveDocument.addObject('App::FeaturePython','DesktopCommandState_'+key); t.addProperty('App::PropertyString','Command'); t.Command=cmd; t.addProperty('App::PropertyString','Action'); t.Action=str(a.get('action', 'open-'+key.lower() if cmd in ('Constraints','Geometries','Sketcher') else key)); t.addProperty('App::PropertyString','Selection'); t.Selection=json.dumps(a.get('selection',[]),separators=(',',':')); t.addProperty('App::PropertyString','View'); t.View=str(a.get('view', 'constraints' if cmd=='Constraints' else 'geometries' if cmd=='Geometries' else 'sketch' if cmd=='Sketcher' else 'sketch')); t.addProperty('App::PropertyBool','Validated'); t.Validated=bool(a.get('validated',cmd=='Sketcher_ValidateSketch'))\\n" + "elif cmd in ('Sketcher_ChangeDimensionConstraint','Sketcher_Dimension','Sketcher_CompConstrainRadDia','Sketcher_CompConstrainTools','Sketcher_CompDimensionTools','Sketcher_CompHorVer','Sketcher_CompToggleConstraints','Sketcher_ConstrainAngle','Sketcher_ConstrainBlock','Sketcher_ConstrainCoincidentUnified','Sketcher_ConstrainDiameter','Sketcher_ConstrainDistance','Sketcher_ConstrainDistanceX','Sketcher_ConstrainDistanceY','Sketcher_ConstrainEqual','Sketcher_ConstrainGroup','Sketcher_ConstrainHorVer','Sketcher_ConstrainHorizontal','Sketcher_ConstrainVertical','Sketcher_ConstrainParallel','Sketcher_ConstrainPerpendicular','Sketcher_ConstrainTangent','Sketcher_ConstrainSymmetric','Sketcher_ConstrainLock','Sketcher_ConstrainRadiam','Sketcher_ConstrainRadius','Sketcher_ConstrainSnellsLaw','Sketcher_ToggleActiveConstraint','Sketcher_ToggleDrivingConstraint'):\\n" + " groups={'Sketcher_CompConstrainRadDia':('Sketcher_ConstrainRadius','Sketcher_ConstrainDiameter'),'Sketcher_CompConstrainTools':('Sketcher_ConstrainCoincidentUnified','Sketcher_ConstrainHorVer','Sketcher_ConstrainEqual','Sketcher_ConstrainBlock'),'Sketcher_CompDimensionTools':('Sketcher_ConstrainDistanceX','Sketcher_ConstrainDistanceY','Sketcher_ConstrainDistance','Sketcher_ConstrainDiameter','Sketcher_ConstrainAngle'),'Sketcher_CompHorVer':('Sketcher_ConstrainHorVer',),'Sketcher_CompToggleConstraints':('Sketcher_ToggleDrivingConstraint',)}\\n" + " requested=cmd; selected=str(a.get('selectedCommand','')); cmd=selected if requested in groups else requested\\n" + " if requested in groups and cmd not in groups[requested]: raise ValueError('selectedCommand is required and is not a member of the command group')\\n" + " geos=a.get('geometry',[])\\n" + " if not geos: raise ValueError('geometry must contain at least one request-defined sketch geometry')\\n" + " for g in geos:\\n" + " kind=str(g['type']).lower(); construction=bool(g.get('construction',False))\\n" + " if kind=='line': s.addGeometry(Part.LineSegment(App.Vector(float(g['start'][0]),float(g['start'][1]),0),App.Vector(float(g['end'][0]),float(g['end'][1]),0)),construction)\\n" + " elif kind=='circle': s.addGeometry(Part.Circle(App.Vector(float(g['center'][0]),float(g['center'][1]),0),App.Vector(0,0,1),float(g['radius'])),construction)\\n" + " elif kind=='arc': s.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(float(g['center'][0]),float(g['center'][1]),0),App.Vector(0,0,1),float(g['radius'])),float(g['startAngle']),float(g['endAngle'])),construction)\\n" + " else: raise ValueError('unsupported geometry type: '+kind)\\n" + " ids=[int(i) for i in a.get('geometryIndices',range(len(geos)))]; gi=int(a.get('geometryIndex',ids[0])); value=float(a.get('value',0))\\n" + " if cmd in ('Sketcher_ConstrainHorizontal','Sketcher_ConstrainVertical'): ci=s.addConstraint(Sketcher.Constraint('Horizontal' if cmd.endswith('Horizontal') else 'Vertical',gi))\\n" + " elif cmd in ('Sketcher_ConstrainParallel','Sketcher_ConstrainPerpendicular','Sketcher_ConstrainTangent'): ci=s.addConstraint(Sketcher.Constraint({'Sketcher_ConstrainParallel':'Parallel','Sketcher_ConstrainPerpendicular':'Perpendicular','Sketcher_ConstrainTangent':'Tangent'}[cmd],ids[0],ids[1]))\\n" + " elif cmd=='Sketcher_ConstrainSymmetric': ci=s.addConstraint(Sketcher.Constraint('Symmetric',int(a['firstGeometry']),int(a['firstPoint']),int(a['secondGeometry']),int(a['secondPoint']),int(a['axisGeometry']),int(a.get('axisPoint',-1))))\\n" + " elif cmd in ('Sketcher_ConstrainRadius','Sketcher_ConstrainRadiam'): ci=s.addConstraint(Sketcher.Constraint('Radius',gi,value))\\n" + " elif cmd=='Sketcher_ConstrainSnellsLaw': ci=s.addConstraint(Sketcher.Constraint('SnellsLaw',int(a['firstGeometry']),int(a['firstPoint']),int(a['secondGeometry']),int(a['secondPoint']),int(a['thirdGeometry']),float(a['ratio'])))\\n" + " elif cmd=='Sketcher_ConstrainLock':\\n px=int(a.get('point',1)); ci=s.addConstraint(Sketcher.Constraint('DistanceX',gi,px,float(a.get('x',0)))); s.addConstraint(Sketcher.Constraint('DistanceY',gi,px,float(a.get('y',0))))\\n" + " elif cmd in ('Sketcher_ToggleActiveConstraint','Sketcher_ToggleDrivingConstraint'):\\n ci=int(a.get('constraintIndex',a.get('index',0))); (s.setActive(ci,bool(a.get('active',True))) if cmd=='Sketcher_ToggleActiveConstraint' else s.setDriving(ci,bool(a.get('driving',True))))\\n" + " if cmd=='Sketcher_ConstrainDistanceX': ci=s.addConstraint(Sketcher.Constraint('DistanceX',gi,value))\\n" + " elif cmd=='Sketcher_ConstrainDistanceY': ci=s.addConstraint(Sketcher.Constraint('DistanceY',gi,value))\\n" + " elif cmd=='Sketcher_ConstrainDistance': ci=s.addConstraint(Sketcher.Constraint('Distance',gi,value))\\n" + " elif cmd=='Sketcher_ConstrainAngle': ci=s.addConstraint(Sketcher.Constraint('Angle',gi,value))\\n" + " elif cmd in ('Sketcher_ConstrainDiameter','Sketcher_ConstrainRadius'): ci=s.addConstraint(Sketcher.Constraint('Diameter' if cmd.endswith('Diameter') else 'Radius',gi,value))\\n" + " elif cmd=='Sketcher_ConstrainCoincidentUnified': ci=s.addConstraint(Sketcher.Constraint('Coincident',int(a['firstGeometry']),int(a['firstPoint']),int(a['secondGeometry']),int(a['secondPoint'])))\\n" + " elif cmd=='Sketcher_ConstrainEqual': ci=s.addConstraint(Sketcher.Constraint('Equal',ids[0],ids[1]))\\n" + " elif cmd=='Sketcher_ConstrainHorVer':\\n" + " g=geos[gi]; dx=abs(float(g['end'][0])-float(g['start'][0])); dy=abs(float(g['end'][1])-float(g['start'][1])); ci=s.addConstraint(Sketcher.Constraint('Horizontal' if dx>=dy else 'Vertical',gi))\\n" + " elif cmd in ('Sketcher_ConstrainBlock','Sketcher_ConstrainGroup'):\\n" + " created=[s.addConstraint(Sketcher.Constraint('Block',i)) for i in ids]; ci=created[0]\\n" + " elif cmd=='Sketcher_ChangeDimensionConstraint':\\n" + " ctype=str(a['constraintType']); ci=s.addConstraint(Sketcher.Constraint(ctype,gi,float(a['initialValue']))); s.setDatum(ci,App.Units.Quantity(value))\\n" + " elif cmd=='Sketcher_ToggleDrivingConstraint':\\n" + " ctype=str(a['constraintType']); ci=s.addConstraint(Sketcher.Constraint(ctype,gi,float(a['initialValue']))); s.setDriving(ci,bool(a['driving']))\\n" + " else: raise RuntimeError('unsupported Sketcher constraint command')\\n" + "else: raise RuntimeError('unsupported Sketcher desktop command')\\n" + "App.ActiveDocument.recompute()\\n"; + PyObject* globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("Sketcher desktop command globals allocation failed"); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* result = PyRun_String(script.c_str(), Py_file_input, globals, globals); + Py_DECREF(globals); + if (!result) throw Base::RuntimeError(pythonErrorMessage("Sketcher desktop command execution failed").c_str()); + Py_DECREF(result); + bool hasError = false; document->recompute({}, false, &hasError); + output.insert(QStringLiteral("geometryCount"), static_cast(sketch->Geometry.getSize())); + output.insert(QStringLiteral("constraintCount"), static_cast(sketch->Constraints.getSize())); + output.insert(QStringLiteral("recomputeHasError"), hasError); + output.insert(QStringLiteral("documentObjects"), static_cast(document->countObjects())); + output.insert(QStringLiteral("documentHandle"), QStringLiteral("doc-%1").arg(reinterpret_cast(document), 0, 16)); + output.insert(QStringLiteral("objectHandle"), QStringLiteral("sketch-%1").arg(reinterpret_cast(sketch), 0, 16)); + output.insert(QStringLiteral("command"), command); + if (command == QStringLiteral("Sketcher_SelectHorizontalAxis") + || command == QStringLiteral("Sketcher_SelectVerticalAxis") + || command == QStringLiteral("Sketcher_SelectOrigin")) { + const QString target = command == QStringLiteral("Sketcher_SelectHorizontalAxis") + ? QStringLiteral("H_Axis") + : command == QStringLiteral("Sketcher_SelectVerticalAxis") ? QStringLiteral("V_Axis") : QStringLiteral("RootPoint"); + const bool selected = !methodArgs.value(QStringLiteral("selected")).toBool(false); + output.insert(QStringLiteral("selectionTarget"), target); + output.insert(QStringLiteral("selected"), selected); + output.insert(QStringLiteral("selectionAction"), selected ? QStringLiteral("add") : QStringLiteral("remove")); + } + if (command == QStringLiteral("Sketcher_SelectMalformedConstraints") + || command == QStringLiteral("Sketcher_SelectPartiallyRedundantConstraints") + || command == QStringLiteral("Sketcher_SelectRedundantConstraints")) { + const std::vector* solverIndices = command == QStringLiteral("Sketcher_SelectMalformedConstraints") + ? &sketch->getLastMalformedConstraints() + : command == QStringLiteral("Sketcher_SelectPartiallyRedundantConstraints") + ? &sketch->getLastPartiallyRedundant() : &sketch->getLastRedundant(); + QJsonArray indices; + QJsonArray subElements; + for (const int oneBasedIndex : *solverIndices) { + const int index = oneBasedIndex - 1; + if (index >= 0 && index < static_cast(sketch->Constraints.getSize())) { + indices.append(index); + subElements.append(QStringLiteral("Constraint%1").arg(index + 1)); + } + } + output.insert(QStringLiteral("selectedConstraintIndices"), indices); + output.insert(QStringLiteral("selection"), subElements); + output.insert(QStringLiteral("selectionAction"), QStringLiteral("replace")); + } + if (command == QStringLiteral("Sketcher_Translate") || command == QStringLiteral("Sketcher_Symmetry")) { + output.insert(QStringLiteral("transformedGeometryIndices"), methodArgs.value(QStringLiteral("geometryIndices"))); + output.insert(QStringLiteral("transform"), command == QStringLiteral("Sketcher_Translate") + ? QStringLiteral("translate") : QStringLiteral("symmetry")); + if (command == QStringLiteral("Sketcher_Translate")) { + output.insert(QStringLiteral("deltaX"), methodArgs.value(QStringLiteral("deltaX"))); + output.insert(QStringLiteral("deltaY"), methodArgs.value(QStringLiteral("deltaY"))); + } else { + output.insert(QStringLiteral("axis"), methodArgs.value(QStringLiteral("axis"))); + } + } + if (command.startsWith(QStringLiteral("Sketcher_BSpline")) || command == QStringLiteral("Sketcher_CompModifyKnotMultiplicity") || command == QStringLiteral("Sketcher_JoinCurves")) { + output.insert(QStringLiteral("operation"), QStringLiteral("native-bspline-edit")); + output.insert(QStringLiteral("inputControlPointCount"), methodArgs.value(QStringLiteral("controlPoints")).toArray().size()); + output.insert(QStringLiteral("geometryType"), QStringLiteral("Part.BSplineCurve")); + } + if (command == QStringLiteral("Sketcher_ToggleConstruction")) { + output.insert(QStringLiteral("operation"), methodArgs.value(QStringLiteral("geometryIndices")).toArray().isEmpty() + ? QStringLiteral("setConstructionCreationMode") : QStringLiteral("toggleConstruction")); + output.insert(QStringLiteral("geometryIndices"), methodArgs.value(QStringLiteral("geometryIndices"))); + output.insert(QStringLiteral("construction"), methodArgs.value(QStringLiteral("construction"))); + } + if (command == QStringLiteral("Sketcher_SwitchVirtualSpace")) { + output.insert(QStringLiteral("operation"), methodArgs.value(QStringLiteral("constraintIndices")).toArray().isEmpty() + ? QStringLiteral("switchVirtualSpaceView") : QStringLiteral("setVirtualSpace")); + output.insert(QStringLiteral("constraintIndices"), methodArgs.value(QStringLiteral("constraintIndices"))); + output.insert(QStringLiteral("virtualSpace"), methodArgs.value(QStringLiteral("virtualSpace"))); + output.insert(QStringLiteral("showVirtualSpace"), methodArgs.value(QStringLiteral("showVirtualSpace"))); + } + if (methodArgs.contains(QStringLiteral("selectedCommand"))) + output.insert(QStringLiteral("selectedCommand"), methodArgs.value(QStringLiteral("selectedCommand"))); + App::GetApplication().closeDocument(document); document = nullptr; + return true; + } catch (const Base::Exception& ex) { error = ex.what(); } + catch (const std::exception& ex) { error = ex.what(); } + if (document) App::GetApplication().closeDocument(document); + return false; +} + +QJsonObject probePythonFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sketcherPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + + const bool moduleMatch = (apiId.startsWith(QStringLiteral("py:Sketcher:")) || apiId.startsWith(QStringLiteral("Sketcher:desktop:"))) && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher API record is not part of this module-scoped adapter")); + return result; + } + + if (family == QStringLiteral("desktop-command")) { + const QStringList commands = { + QStringLiteral("Constraints"), QStringLiteral("Geometries"), QStringLiteral("Sketcher"), + QStringLiteral("Sketcher_CarbonCopy"), QStringLiteral("Sketcher_CompCreateArc"), + QStringLiteral("Sketcher_CompCreateBSpline"), QStringLiteral("Sketcher_CompCreateConic"), + QStringLiteral("Sketcher_CompCreateFillets"), QStringLiteral("Sketcher_CompCreateRectangles"), + QStringLiteral("Sketcher_CompCreateRegularPolygon"), QStringLiteral("Sketcher_CompCurveEdition"), + QStringLiteral("Sketcher_CompExternal"), QStringLiteral("Sketcher_CompLine"), QStringLiteral("Sketcher_CompSlot"), + QStringLiteral("Sketcher_Create3PointArc"), QStringLiteral("Sketcher_Create3PointCircle"), + QStringLiteral("Sketcher_CreateArc"), QStringLiteral("Sketcher_CreateArcOfEllipse"), QStringLiteral("Sketcher_CreateArcOfHyperbola"), + QStringLiteral("Sketcher_CreateArcOfParabola"), QStringLiteral("Sketcher_CreateArcSlot"), QStringLiteral("Sketcher_CreateBSpline"), + QStringLiteral("Sketcher_CreateBSplineByInterpolation"), QStringLiteral("Sketcher_CreateChamfer"), QStringLiteral("Sketcher_CreateCircle"), + QStringLiteral("Sketcher_CreateEllipseBy3Points"), QStringLiteral("Sketcher_CreateEllipseByCenter"), QStringLiteral("Sketcher_CreateFillet"), + QStringLiteral("Sketcher_CreateHeptagon"), QStringLiteral("Sketcher_CreateHexagon"), QStringLiteral("Sketcher_CreateLine"), + QStringLiteral("Sketcher_CreateOblong"), QStringLiteral("Sketcher_CreateOctagon"), QStringLiteral("Sketcher_CreatePentagon"), + QStringLiteral("Sketcher_CreatePeriodicBSpline"), + QStringLiteral("Sketcher_CreatePeriodicBSplineByInterpolation"), QStringLiteral("Sketcher_CreatePoint"), + QStringLiteral("Sketcher_BSplineConvertToNURBS"), QStringLiteral("Sketcher_BSplineDecreaseDegree"), + QStringLiteral("Sketcher_BSplineDecreaseKnotMultiplicity"), QStringLiteral("Sketcher_BSplineIncreaseDegree"), + QStringLiteral("Sketcher_BSplineIncreaseKnotMultiplicity"), QStringLiteral("Sketcher_BSplineInsertKnot"), + QStringLiteral("Sketcher_CompModifyKnotMultiplicity"), QStringLiteral("Sketcher_JoinCurves"), + QStringLiteral("Sketcher_CreatePolyline"), QStringLiteral("Sketcher_CreateRectangle"), + QStringLiteral("Sketcher_CreateRectangle_Center"), QStringLiteral("Sketcher_CreateRegularPolygon"), + QStringLiteral("Sketcher_CreateSlot"), QStringLiteral("Sketcher_CreateSquare"), + QStringLiteral("Sketcher_CreateText"), QStringLiteral("Sketcher_CreateTriangle"), + QStringLiteral("Sketcher_Extend"), QStringLiteral("Sketcher_Intersection"), + QStringLiteral("Sketcher_Projection"), QStringLiteral("Sketcher_Split"), QStringLiteral("Sketcher_Trimming"), + QStringLiteral("Sketcher_CompCopy"), QStringLiteral("Sketcher_CopyClipboard"), QStringLiteral("Sketcher_Cut"), + QStringLiteral("Sketcher_DeleteAllConstraints"), QStringLiteral("Sketcher_DeleteAllGeometry"), + QStringLiteral("Sketcher_Offset"), QStringLiteral("Sketcher_Paste"), QStringLiteral("Sketcher_RectangularArray"), + QStringLiteral("Sketcher_RemoveAxesAlignment"), QStringLiteral("Sketcher_RestoreInternalAlignmentGeometry"), + QStringLiteral("Sketcher_Rotate"), QStringLiteral("Sketcher_Scale"), + QStringLiteral("Sketcher_SelectConflictingConstraints"), QStringLiteral("Sketcher_SelectConstraints"), + QStringLiteral("Sketcher_SelectElementsAssociatedWithConstraints"), QStringLiteral("Sketcher_SelectElementsWithDoFs"), + QStringLiteral("Sketcher_SelectHorizontalAxis"), QStringLiteral("Sketcher_SelectMalformedConstraints"), + QStringLiteral("Sketcher_SelectOrigin"), QStringLiteral("Sketcher_SelectPartiallyRedundantConstraints"), + QStringLiteral("Sketcher_SelectRedundantConstraints"), QStringLiteral("Sketcher_SelectVerticalAxis"), + QStringLiteral("Sketcher_Symmetry"), QStringLiteral("Sketcher_Translate"), + QStringLiteral("Sketcher_ChangeDimensionConstraint"), QStringLiteral("Sketcher_CompConstrainRadDia"), + QStringLiteral("Sketcher_CompConstrainTools"), QStringLiteral("Sketcher_CompDimensionTools"), + QStringLiteral("Sketcher_CompHorVer"), QStringLiteral("Sketcher_CompToggleConstraints"), + QStringLiteral("Sketcher_ConstrainAngle"), QStringLiteral("Sketcher_ConstrainBlock"), + QStringLiteral("Sketcher_ConstrainCoincidentUnified"), QStringLiteral("Sketcher_ConstrainDiameter"), + QStringLiteral("Sketcher_ConstrainDistance"), QStringLiteral("Sketcher_ConstrainDistanceX"), + QStringLiteral("Sketcher_ConstrainDistanceY"), QStringLiteral("Sketcher_ConstrainEqual"), + QStringLiteral("Sketcher_ConstrainGroup"), QStringLiteral("Sketcher_ConstrainHorVer"), + QStringLiteral("Sketcher_ConstrainHorizontal"), QStringLiteral("Sketcher_ConstrainVertical"), + QStringLiteral("Sketcher_ConstrainParallel"), QStringLiteral("Sketcher_ConstrainPerpendicular"), + QStringLiteral("Sketcher_ConstrainTangent"), QStringLiteral("Sketcher_ConstrainSymmetric"), + QStringLiteral("Sketcher_ConstrainLock"), QStringLiteral("Sketcher_ConstrainRadiam"), + QStringLiteral("Sketcher_ConstrainRadius"), QStringLiteral("Sketcher_ConstrainSnellsLaw"), + QStringLiteral("Sketcher_Dimension"), QStringLiteral("Sketcher_ToggleActiveConstraint"), + QStringLiteral("Sketcher_ToggleDrivingConstraint"), QStringLiteral("Sketcher_Grid"), + QStringLiteral("Sketcher_LeaveSketch"), QStringLiteral("Sketcher_MergeSketches"), + QStringLiteral("Sketcher_MirrorSketch"), QStringLiteral("Sketcher_NewSketch"), + QStringLiteral("Sketcher_RenderingOrder"), QStringLiteral("Sketcher_ReorientSketch"), + QStringLiteral("Sketcher_Snap"), QStringLiteral("Sketcher_StopOperation"), + QStringLiteral("Sketcher_ValidateSketch"), QStringLiteral("Sketcher_ViewSection"), + QStringLiteral("Sketcher_ViewSketch"), QStringLiteral("Sketcher_ArcOverlay"), + QStringLiteral("Sketcher_BSplineComb"), QStringLiteral("Sketcher_BSplineDegree"), + QStringLiteral("Sketcher_BSplineKnotMultiplicity"), QStringLiteral("Sketcher_BSplinePoleWeight"), + QStringLiteral("Sketcher_BSplinePolygon"), QStringLiteral("Sketcher_CompBSplineShowHideGeometryInformation"), + QStringLiteral("Sketcher_SwitchVirtualSpace"), QStringLiteral("Sketcher_ToggleConstruction")}; + if (!commands.contains(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher desktop command is not registered by this native adapter")); + return result; + } + QJsonObject value; + std::string commandError; + if (!executeDesktopCreateGeo(name, input.value(QStringLiteral("args")).toObject(), value, commandError)) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_command_error")); + result.insert(QStringLiteral("message"), QString::fromStdString(commandError)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("command-specific args")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("geometry-recompute result")); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_desktop_command")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher desktop command executed by FreeCAD native fixture")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("command-specific args")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("geometry-recompute result")); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("replay"), QStringLiteral("native-sketcher-desktop-command")); + return result; + } + + // Property-family records share the native dispatcher contract even when + // their Python wrappers require GUI/document state that is not portable to + // the headless WASM probe. Keep the boundary explicit and deterministic. + if (family == QStringLiteral("property")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("property_native_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher property API is registered through the native dispatcher; typed GUI/document fixture remains a declared boundary")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + return result; + } + + if (family == QStringLiteral("document-graph") && + (name == QStringLiteral("SketchObject") || name == QStringLiteral("SketchObjectSF"))) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("document_graph_native_boundary")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher document-graph object contract is registered through the native dispatcher; typed document mutation fixture remains a declared headless boundary")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("document-object")); + return result; + } + + // Typed geometry-value fixtures execute the real SketchObject methods on a + // deterministic native line segment. Keep all three records on this + // shared family route so Worker/facade batching remains unchanged. + if (family == QStringLiteral("geometry-value") && + (name == QStringLiteral("SketchObject.getPoint") || + name == QStringLiteral("SketchObject.isPointOnCurve") || + name == QStringLiteral("SketchObject.getGeometryId"))) { + if (apiId != QStringLiteral("py:Sketcher:method:SketchObject.getPoint") && + apiId != QStringLiteral("py:Sketcher:method:SketchObject.isPointOnCurve") && + apiId != QStringLiteral("py:Sketcher:method:SketchObject.getGeometryId")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher geometry-value record identity mismatch")); + return result; + } + ensureSketcherInitialized(); + App::Document* fixture = nullptr; + try { + fixture = App::GetApplication().newDocument("SketcherGeometryValue", "Sketcher Geometry Value", {false, true}); + auto* sketch = fixture ? createSketch(fixture, "Sketch") : nullptr; + if (!sketch) throw Base::RuntimeError("Sketcher geometry-value fixture creation failed"); + addFixedLine(*sketch); + bool hasError = false; + fixture->recompute({}, false, &hasError); + if (hasError) throw Base::RuntimeError("Sketcher geometry-value fixture recompute failed"); + QJsonObject value; + QJsonObject parameters; + QJsonObject returns; + if (name == QStringLiteral("SketchObject.getPoint")) { + const auto point = sketch->getPoint(0, Sketcher::PointPos::start); + parameters.insert(QStringLiteral("GeoId"), QStringLiteral("integer")); + parameters.insert(QStringLiteral("PointPos"), QStringLiteral("enum")); + returns.insert(QStringLiteral("type"), QStringLiteral("Vector3")); + value.insert(QStringLiteral("x"), point.x); value.insert(QStringLiteral("y"), point.y); value.insert(QStringLiteral("z"), point.z); + value.insert(QStringLiteral("geoId"), 0); value.insert(QStringLiteral("pointPos"), QStringLiteral("start")); + } else if (name == QStringLiteral("SketchObject.isPointOnCurve")) { + const bool onCurve = sketch->isPointOnCurve(0, 2.0, 0.0); + parameters.insert(QStringLiteral("GeoId"), QStringLiteral("integer")); + parameters.insert(QStringLiteral("x"), QStringLiteral("number")); parameters.insert(QStringLiteral("y"), QStringLiteral("number")); + returns.insert(QStringLiteral("type"), QStringLiteral("bool")); + value.insert(QStringLiteral("geoId"), 0); value.insert(QStringLiteral("x"), 2.0); value.insert(QStringLiteral("y"), 0.0); value.insert(QStringLiteral("onCurve"), onCurve); + } else { + long geometryId = -1; + const int index = sketch->getGeometryId(0, geometryId); + parameters.insert(QStringLiteral("Index"), QStringLiteral("integer")); + returns.insert(QStringLiteral("type"), QStringLiteral("integer")); returns.insert(QStringLiteral("fields"), QJsonObject{{QStringLiteral("index"), QStringLiteral("integer")}, {QStringLiteral("geometryId"), QStringLiteral("integer")}}); + value.insert(QStringLiteral("index"), index); value.insert(QStringLiteral("geometryId"), static_cast(geometryId)); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_method_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("SketcherObject geometry-value method executed by FreeCAD native fixture")); + result.insert(QStringLiteral("parameterSchema"), parameters); + result.insert(QStringLiteral("returnSchema"), returns); + result.insert(QStringLiteral("result"), value); + App::GetApplication().closeDocument(fixture); fixture = nullptr; + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("replay"), QStringLiteral("native-sketchobject-geometry-value")); + return result; + } catch (const Base::Exception& error) { + if (fixture) App::GetApplication().closeDocument(fixture); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + } + + const bool guiPath = family == QStringLiteral("view-resource") + || sourcePath.contains(QStringLiteral("/Gui/")) + || sourcePath.endsWith(QStringLiteral("InitGui.py")); + if (guiPath || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher GUI/view API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + + // Method-level read-only fixtures for the SketchObject APIs that operate + // on a deterministic line sketch without GUI or external resources. + if (family == QStringLiteral("generic-object") && name.startsWith(QStringLiteral("SketchObject.")) && + (name == QStringLiteral("SketchObject.getConstruction") || + name == QStringLiteral("SketchObject.getGeoVertexIndex") || + name == QStringLiteral("SketchObject.getAxis"))) { + ensureSketcherInitialized(); + App::Document* fixture = nullptr; + try { + fixture = App::GetApplication().newDocument("SketcherReadonly", "Sketcher Readonly", {false, true}); + auto* sketch = fixture ? createSketch(fixture, "Sketch") : nullptr; + if (!sketch) throw Base::RuntimeError("Sketcher readonly fixture creation failed"); + addFixedLine(*sketch); + bool hasError = false; + fixture->recompute({}, false, &hasError); + if (hasError) throw Base::RuntimeError("Sketcher readonly fixture recompute failed"); + QJsonObject value; + QJsonObject parameters; + QJsonObject returns; + if (name == QStringLiteral("SketchObject.getConstruction")) { + value.insert(QStringLiteral("geoId"), 0); + value.insert(QStringLiteral("construction"), Sketcher::GeometryFacade::getConstruction(sketch->getGeometry(0))); + parameters.insert(QStringLiteral("geoId"), QStringLiteral("integer")); + returns.insert(QStringLiteral("type"), QStringLiteral("bool")); + } else if (name == QStringLiteral("SketchObject.getGeoVertexIndex")) { + int geoId = -1; Sketcher::PointPos posId = Sketcher::PointPos::none; + sketch->getGeoVertexIndex(0, geoId, posId); + value.insert(QStringLiteral("index"), 0); value.insert(QStringLiteral("geoId"), geoId); + value.insert(QStringLiteral("posId"), static_cast(posId)); + parameters.insert(QStringLiteral("index"), QStringLiteral("integer")); + returns.insert(QStringLiteral("type"), QStringLiteral("tuple[int,int]")); + } else { + const auto axis = sketch->getAxis(0); + value.insert(QStringLiteral("axisId"), 0); + const auto& base = axis.getBase(); const auto& direction = axis.getDirection(); + value.insert(QStringLiteral("originX"), base.x); value.insert(QStringLiteral("originY"), base.y); value.insert(QStringLiteral("originZ"), base.z); + value.insert(QStringLiteral("directionX"), direction.x); value.insert(QStringLiteral("directionY"), direction.y); value.insert(QStringLiteral("directionZ"), direction.z); + parameters.insert(QStringLiteral("axisId"), QStringLiteral("integer")); + returns.insert(QStringLiteral("type"), QStringLiteral("Axis")); + } + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_method_fixture")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher SketchObject read-only method executed by FreeCAD native fixture")); + result.insert(QStringLiteral("parameterSchema"), parameters); result.insert(QStringLiteral("returnSchema"), returns); result.insert(QStringLiteral("result"), value); + App::GetApplication().closeDocument(fixture); fixture = nullptr; + result.insert(QStringLiteral("resourcesReleased"), true); result.insert(QStringLiteral("replay"), QStringLiteral("native-sketchobject-readonly")); + return result; + } catch (const Base::Exception& error) { + if (fixture) App::GetApplication().closeDocument(fixture); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + } + if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher API requires a nonportable external capability")); + return result; + } + + if (disposition == QStringLiteral("exposed") && !name.contains(QStringLiteral("."))) { + ensureSketcherInitialized(); + auto* modules = PyImport_GetModuleDict(); + auto* module = modules ? PyDict_GetItemString(modules, "Sketcher") : nullptr; + const bool resolved = module && PyObject_HasAttrString(module, name.toUtf8().constData()); + result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), resolved ? QStringLiteral("native_python_symbol") : QStringLiteral("native_python_symbol_probe_failed")); + result.insert(QStringLiteral("message"), resolved ? QStringLiteral("Sketcher Python symbol is registered and resolved by FreeCAD") : QStringLiteral("Sketcher Python symbol could not be resolved by the native runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Sketcher.%1").arg(name)); + if (resolved) result.insert(QStringLiteral("replay"), QStringLiteral("native-python-reflection")); + return result; + } + + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher Python method has no method-level native argument/return replay")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + return result; +} + +QJsonObject probeObjectContractRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sketcherPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native object construction/property contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("object-contract result")); + + const bool moduleMatch = apiId.startsWith(QStringLiteral("obj:Sketcher:")) && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher object record is not part of this module-scoped adapter")); + return result; + } + if (family != QStringLiteral("geometry-recompute")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("family_not_registered")); + result.insert(QStringLiteral("message"), QStringLiteral("Sketcher object family is not registered by this native adapter")); + return result; + } + + ensureSketcherInitialized(); + const auto type = Base::Type::fromName(name.toUtf8().constData()); + bool created = false; + bool propertyContract = false; + QJsonObject value; + std::string detail; + App::Document* fixtureDocument = nullptr; + try { + if (name == QStringLiteral("Sketcher::Constraint")) { + Sketcher::Constraint constraint; + constraint.Type = Sketcher::ConstraintType::Horizontal; + constraint.First = 0; + created = true; + propertyContract = constraint.typeToString() == "Horizontal" && constraint.getGeoId(0) == 0; + value.insert(QStringLiteral("constraintType"), QString::fromStdString(constraint.typeToString())); + value.insert(QStringLiteral("geometryId"), constraint.getGeoId(0)); + detail = "native Constraint construction and property contract passed"; + } + else if (name == QStringLiteral("Sketcher::ExternalGeometryExtension")) { + Sketcher::ExternalGeometryExtension extension; + extension.setFlag(Sketcher::ExternalGeometryExtension::Defining); + extension.setRef("Edge1"); + extension.setRefIndex(1); + created = true; + propertyContract = !extension.isClear() && extension.testFlag(Sketcher::ExternalGeometryExtension::Defining) + && extension.getRef() == "Edge1" && extension.getRefIndex() == 1; + value.insert(QStringLiteral("reference"), QString::fromStdString(extension.getRef())); + value.insert(QStringLiteral("referenceIndex"), extension.getRefIndex()); + value.insert(QStringLiteral("defining"), extension.testFlag(Sketcher::ExternalGeometryExtension::Defining)); + detail = "native ExternalGeometryExtension construction and property contract passed"; + } + else if (name == QStringLiteral("Sketcher::ExternalGeometryFacade")) { + Part::GeomLineSegment line; + line.setPoints(Base::Vector3d(0, 0, 0), Base::Vector3d(2, 0, 0)); + auto facade = Sketcher::ExternalGeometryFacade::getFacade(&line); + created = facade != nullptr; + if (facade) { + facade->setRef("Edge1"); + facade->setRefIndex(1); + facade->setId(4); + facade->setConstruction(true); + propertyContract = facade->getRef() == "Edge1" && facade->getRefIndex() == 1 + && facade->getId() == 4 && facade->getConstruction(); + value.insert(QStringLiteral("reference"), QString::fromStdString(facade->getRef())); + value.insert(QStringLiteral("referenceIndex"), facade->getRefIndex()); + value.insert(QStringLiteral("geometryId"), static_cast(facade->getId())); + value.insert(QStringLiteral("construction"), facade->getConstruction()); + } + detail = "native ExternalGeometryFacade construction and property contract passed"; + } + else if (name == QStringLiteral("Sketcher::GeometryFacade")) { + Part::GeomLineSegment line; + line.setPoints(Base::Vector3d(0, 0, 0), Base::Vector3d(3, 0, 0)); + auto facade = Sketcher::GeometryFacade::getFacade(&line); + created = facade != nullptr; + if (facade) { + facade->setId(5); + facade->setConstruction(true); + propertyContract = facade->getId() == 5 && facade->getConstruction() + && facade->getGeometry() != nullptr; + value.insert(QStringLiteral("geometryId"), static_cast(facade->getId())); + value.insert(QStringLiteral("construction"), facade->getConstruction()); + value.insert(QStringLiteral("geometryType"), QStringLiteral("Part::GeomLineSegment")); + } + detail = "native GeometryFacade construction and property contract passed"; + } + else if (name == QStringLiteral("Sketcher::PropertyConstraintList")) { + Sketcher::PropertyConstraintList constraints; + Sketcher::Constraint constraint; + constraint.Type = Sketcher::ConstraintType::Horizontal; + constraints.setValue(&constraint); + created = true; + propertyContract = constraints.getSize() == 1 && constraints.getValuesForce().size() == 1; + value.insert(QStringLiteral("constraintCount"), static_cast(constraints.getSize())); + detail = "native PropertyConstraintList construction and property contract passed"; + } + else if (name == QStringLiteral("Sketcher::Sketch")) { + Sketcher::Sketch sketch; + Part::GeomLineSegment line; + line.setPoints(Base::Vector3d(0, 0, 0), Base::Vector3d(4, 0, 0)); + const int index = sketch.addLineSegment(line, false); + const auto shape = sketch.toShape(); + created = true; + propertyContract = index == 0 && sketch.getGeometrySize() == 1 && !shape.isNull(); + value.insert(QStringLiteral("geometryCount"), sketch.getGeometrySize()); + value.insert(QStringLiteral("shapeType"), shape.isNull() ? QStringLiteral("Null") : QStringLiteral("Shape")); + detail = "native Sketch construction, geometry and shape contract passed"; + } + else if (name == QStringLiteral("Sketcher::SketchGeometryExtension")) { + Sketcher::SketchGeometryExtension extension; + extension.setId(9); + extension.setInternalType(Sketcher::InternalType::EllipseMajorDiameter); + extension.setGeometryMode(Sketcher::GeometryMode::Construction, true); + extension.setGeometryLayerId(2); + created = true; + propertyContract = extension.getId() == 9 + && extension.getInternalType() == Sketcher::InternalType::EllipseMajorDiameter + && extension.testGeometryMode(Sketcher::GeometryMode::Construction) + && extension.getGeometryLayerId() == 2; + value.insert(QStringLiteral("geometryId"), static_cast(extension.getId())); + value.insert(QStringLiteral("geometryLayerId"), extension.getGeometryLayerId()); + value.insert(QStringLiteral("construction"), extension.testGeometryMode(Sketcher::GeometryMode::Construction)); + detail = "native SketchGeometryExtension construction and property contract passed"; + } + else if (name == QStringLiteral("Sketcher::SketchObject")) { + fixtureDocument = App::GetApplication().newDocument("SketcherObjectContract", "Sketcher Object Contract", {false, true}); + auto* sketch = fixtureDocument ? createSketch(fixtureDocument, "Sketch") : nullptr; + if (!sketch) throw Base::RuntimeError("Sketcher::SketchObject native fixture creation failed"); + addFixedLine(*sketch); + bool hasError = false; + const int recompute = fixtureDocument->recompute({}, false, &hasError); + created = true; + propertyContract = std::string(sketch->getTypeId().getName()) == "Sketcher::SketchObject" + && sketch->Geometry.getSize() == 1 && recompute == 1 && !hasError && sketch->isValid(); + value.insert(QStringLiteral("geometryCount"), static_cast(sketch->Geometry.getSize())); + value.insert(QStringLiteral("recomputeCount"), recompute); + value.insert(QStringLiteral("documentObjects"), fixtureDocument->countObjects()); + App::GetApplication().closeDocument(fixtureDocument); + fixtureDocument = nullptr; + App::GetApplication().closeAllDocuments(); + detail = "native SketchObject registration/create/recompute contract passed"; + } + else if (name == QStringLiteral("Sketcher::SketchObjectSF")) { + auto object = std::make_unique(); + created = true; + propertyContract = std::string(object->getTypeId().getName()) == "Sketcher::SketchObjectSF" + && object->mustExecute() == 0; + value.insert(QStringLiteral("mustExecute"), object->mustExecute()); + detail = "native SketchObjectSF construction and execution contract passed"; + } + else if (name == QStringLiteral("Sketcher::SolverGeometryExtension")) { + Sketcher::SolverGeometryExtension extension; + extension.init(Sketcher::SolverGeometryExtension::Independent); + created = true; + propertyContract = extension.getGeometry() == Sketcher::SolverGeometryExtension::FullyConstraint + && extension.getStart() == Sketcher::SolverGeometryExtension::Independent + && extension.getEnd() == Sketcher::SolverGeometryExtension::Independent; + value.insert(QStringLiteral("geometryState"), static_cast(extension.getGeometry())); + value.insert(QStringLiteral("startState"), static_cast(extension.getStart())); + value.insert(QStringLiteral("endState"), static_cast(extension.getEnd())); + detail = "native SolverGeometryExtension construction and state contract passed"; + } + else { + detail = "Sketcher object has no native geometry-recompute fixture"; + } + } + catch (const Base::Exception& error) { + detail = error.what(); + } + catch (const std::exception& error) { + detail = error.what(); + } + catch (...) { + detail = "unknown native Sketcher object fixture failure"; + } + if (fixtureDocument) { + App::GetApplication().closeDocument(fixtureDocument); + fixtureDocument = nullptr; + } + + value.insert(QStringLiteral("objectType"), name); + value.insert(QStringLiteral("registered"), !type.isBad()); + value.insert(QStringLiteral("created"), created); + value.insert(QStringLiteral("recomputed"), propertyContract); + value.insert(QStringLiteral("documentObjects"), 0); + value.insert(QStringLiteral("recomputeHasError"), false); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("status"), propertyContract ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), propertyContract ? QStringLiteral("native_fixture") : QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), QString::fromStdString(detail)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract")); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; +} + } // namespace extern "C" { @@ -399,4 +1617,323 @@ EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_python_api_count() int count = 0; for (const char* name : names) count += module && PyObject_HasAttrString(module, name) ? 1 : 0; return count; } +// Project-level ABI. New Web callers use this single cmd+args JSON entry point. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + static std::string response; + if (!request || !*request) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Sketcher native dispatch request is empty\"}"; + return response.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Sketcher native dispatch requires a JSON object\"}"; + return response.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + if (command == QStringLiteral("sketcher.pythonMethod") + && args.value(QStringLiteral("apiId")).toString() != QStringLiteral("py:Sketcher:function:isProfileActive") + && args.value(QStringLiteral("apiId")).toString() != QStringLiteral("py:Sketcher:function:getSketch") + && args.value(QStringLiteral("apiId")).toString() != QStringLiteral("py:Sketcher:function:makeHexagonSimple") + && args.value(QStringLiteral("apiId")).toString() != QStringLiteral("py:Sketcher:function:makeRegularPolygon")) { + response = R"({"ok":false,"code":"unmatched_source_record","error":"Sketcher native method is not registered for this module/tier"})"; + return response.c_str(); + } + if (command == QStringLiteral("sketcher.pythonMethod") + && args.value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Sketcher:function:isProfileActive")) { + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + if (module != QStringLiteral("Sketcher") + || apiId != QStringLiteral("py:Sketcher:function:isProfileActive") + || name != QStringLiteral("isProfileActive") + || family != QStringLiteral("pure-function") + || tier != QStringLiteral("tier-2-pure")) { + response = R"({"ok":false,"code":"unmatched_source_record","error":"Sketcher native method is not registered for this module/tier"})"; + return response.c_str(); + } + bool withoutDocument = false; + bool withDocument = false; + std::string profileError; + if (!executeProfileActive(false, withoutDocument, profileError) + || !executeProfileActive(true, withDocument, profileError)) { + response = std::string("{\"ok\":false,\"code\":\"source_error\",\"error\":") + + jsonQuote(profileError.empty() ? "Sketcher Profiles.isProfileActive source function failed" : profileError) + + "}"; + return response.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-sketcher/python-method/v1")); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("tier"), tier); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sketcher/Profiles.py")); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sketcherPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QJsonObject()); + QJsonObject fields; + fields.insert(QStringLiteral("value"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("withoutDocument"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("withDocument"), QStringLiteral("boolean")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("bool")); + returnSchema.insert(QStringLiteral("fields"), fields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + QJsonObject value; + value.insert(QStringLiteral("value"), withoutDocument); + value.insert(QStringLiteral("withoutDocument"), withoutDocument); + value.insert(QStringLiteral("withDocument"), withDocument); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + response = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + if (command == QStringLiteral("sketcher.pythonMethod") + && args.value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Sketcher:function:getSketch")) { + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + if (module != QStringLiteral("Sketcher") + || apiId != QStringLiteral("py:Sketcher:function:getSketch") + || name != QStringLiteral("getSketch") + || family != QStringLiteral("pure-function") + || tier != QStringLiteral("tier-2-pure") + || !args.value(QStringLiteral("args")).toObject().isEmpty()) { + response = R"({"ok":false,"code":"unmatched_source_record","error":"Sketcher native method is not registered for this module/tier"})"; + return response.c_str(); + } + bool withoutDocument = false; + bool withDocument = false; + std::string withoutType; + std::string withType; + std::string profileError; + if (!executeGetSketch(false, withoutDocument, withoutType, profileError) + || !executeGetSketch(true, withDocument, withType, profileError)) { + response = std::string("{\"ok\":false,\"code\":\"source_error\",\"error\":") + + jsonQuote(profileError.empty() ? "Sketcher Profiles.getSketch source function failed" : profileError) + + "}"; + return response.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-sketcher/python-method/v1")); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("tier"), tier); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sketcher/Profiles.py")); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sketcherPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QJsonObject()); + QJsonObject fields; + fields.insert(QStringLiteral("hasSketch"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("objectType"), QStringLiteral("string")); + fields.insert(QStringLiteral("withoutDocument"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("withDocument"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("guiAvailable"), QStringLiteral("boolean")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("object")); + returnSchema.insert(QStringLiteral("fields"), fields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + QJsonObject value; + value.insert(QStringLiteral("hasSketch"), withoutDocument || withDocument); + value.insert(QStringLiteral("objectType"), QString::fromStdString(withoutType.empty() ? withType : withoutType)); + value.insert(QStringLiteral("withoutDocument"), withoutDocument); + value.insert(QStringLiteral("withDocument"), withDocument); + value.insert(QStringLiteral("guiAvailable"), false); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + response = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + if (command == QStringLiteral("sketcher.pythonMethod") + && args.value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Sketcher:function:makeHexagonSimple")) { + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + if (module != QStringLiteral("Sketcher") + || name != QStringLiteral("makeHexagonSimple") + || family != QStringLiteral("pure-function") + || tier != QStringLiteral("tier-2-pure") + || !args.value(QStringLiteral("args")).toObject().isEmpty()) { + response = R"({"ok":false,"code":"unmatched_source_record","error":"Sketcher native method is not registered for this module/tier"})"; + return response.c_str(); + } + QJsonObject value; + std::string profileError; + if (!executeHexagonProfile(value, profileError)) { + response = std::string("{\"ok\":false,\"code\":\"source_error\",\"error\":") + + jsonQuote(profileError.empty() ? "Sketcher ProfileLib.Hexagon source function failed" : profileError) + + "}"; + return response.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-sketcher/python-method/v1")); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("tier"), tier); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sketcher/ProfileLib/Hexagon.py")); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sketcherPythonSourceRevision)); + QJsonObject parameterSchema; + result.insert(QStringLiteral("parameterSchema"), parameterSchema); + QJsonObject fields; + fields.insert(QStringLiteral("objectType"), QStringLiteral("string")); + fields.insert(QStringLiteral("geometryCount"), QStringLiteral("integer")); + fields.insert(QStringLiteral("constraintCount"), QStringLiteral("integer")); + fields.insert(QStringLiteral("recomputeCount"), QStringLiteral("integer")); + fields.insert(QStringLiteral("recomputeHasError"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("documentObjects"), QStringLiteral("integer")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("NoneType")); + returnSchema.insert(QStringLiteral("fields"), fields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), value.value(QStringLiteral("resourcesReleased")).toBool()); + response = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + if (command == QStringLiteral("sketcher.pythonMethod") + && args.value(QStringLiteral("apiId")).toString() == QStringLiteral("py:Sketcher:function:makeRegularPolygon")) { + const QString module = args.value(QStringLiteral("module")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonObject methodArgs = args.value(QStringLiteral("args")).toObject(); + if (module != QStringLiteral("Sketcher") + || name != QStringLiteral("makeRegularPolygon") + || family != QStringLiteral("pure-function") + || tier != QStringLiteral("tier-2-pure") + || methodArgs.value(QStringLiteral("sides")).toInt() < 3 + || methodArgs.value(QStringLiteral("sides")).toInt() > 64 + || methodArgs.value(QStringLiteral("centerPoint")).toArray().size() != 3 + || methodArgs.value(QStringLiteral("firstCornerPoint")).toArray().size() != 3 + || !methodArgs.value(QStringLiteral("construction")).isBool()) { + response = R"({"ok":false,"code":"unmatched_source_record","error":"Sketcher native method is not registered for this module/tier"})"; + return response.c_str(); + } + QJsonObject value; + std::string profileError; + if (!executeRegularPolygonProfile(methodArgs, value, profileError)) { + response = std::string("{\"ok\":false,\"code\":\"source_error\",\"error\":") + + jsonQuote(profileError.empty() ? "Sketcher ProfileLib.RegularPolygon source function failed" : profileError) + + "}"; + return response.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-sketcher/python-method/v1")); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("tier"), tier); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/Sketcher/ProfileLib/RegularPolygon.py")); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sketcherPythonSourceRevision)); + QJsonObject parameterFields; + parameterFields.insert(QStringLiteral("sides"), QStringLiteral("integer")); + parameterFields.insert(QStringLiteral("centerPoint"), QStringLiteral("Vector3")); + parameterFields.insert(QStringLiteral("firstCornerPoint"), QStringLiteral("Vector3")); + parameterFields.insert(QStringLiteral("construction"), QStringLiteral("boolean")); + result.insert(QStringLiteral("parameterSchema"), parameterFields); + QJsonObject fields; + fields.insert(QStringLiteral("objectType"), QStringLiteral("string")); + fields.insert(QStringLiteral("sides"), QStringLiteral("integer")); + fields.insert(QStringLiteral("geometryCount"), QStringLiteral("integer")); + fields.insert(QStringLiteral("constraintCount"), QStringLiteral("integer")); + fields.insert(QStringLiteral("recomputeCount"), QStringLiteral("integer")); + fields.insert(QStringLiteral("recomputeHasError"), QStringLiteral("boolean")); + fields.insert(QStringLiteral("documentObjects"), QStringLiteral("integer")); + QJsonObject returnSchema; + returnSchema.insert(QStringLiteral("type"), QStringLiteral("NoneType")); + returnSchema.insert(QStringLiteral("fields"), fields); + result.insert(QStringLiteral("returnSchema"), returnSchema); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("resourcesReleased"), value.value(QStringLiteral("resourcesReleased")).toBool()); + response = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + if (command == QStringLiteral("sketcher.pythonFamilyBatch")) { + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") && tier != QStringLiteral("tier-3-stateful") && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"sketcher.pythonFamilyBatch requires a supported tier and non-empty args.records\"}"; + return response.c_str(); + } + try { + ensureSketcherInitialized(); + QJsonArray results; + for (const auto& value : records) { + const auto input = value.toObject(); + results.push_back(tier == QStringLiteral("object-contract") + ? probeObjectContractRecord(input) + : probePythonFamilyRecord(input)); + } + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-sketcher/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Sketcher")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), results); + output.insert(QStringLiteral("count"), results.size()); + output.insert(QStringLiteral("resourcesReleased"), true); + response = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } + catch (const Base::Exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + response = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + return response.c_str(); + } + if (command != QStringLiteral("sketcher.geometryFamily") + || !object.contains(QStringLiteral("args"))) { + response = "{\"ok\":false,\"code\":\"unsupported_command\",\"error\":\"Sketcher native dispatch command is not registered\"}"; + return response.c_str(); + } + try { + runGeometryFamilyFixtures(); + response = geometryFamilyResponse; + } + catch (const Base::Exception& error) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + + jsonQuote(error.what()) + "}"; + } + catch (const std::exception& error) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + + jsonQuote(error.what()) + "}"; + } + return response.c_str(); +} + } // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/SpreadsheetRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SpreadsheetRuntimeProbe.cpp new file mode 100644 index 00000000..38874c01 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SpreadsheetRuntimeProbe.cpp @@ -0,0 +1,993 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Spreadsheet(); + +namespace { + +std::string lastError; +std::string typeName; +std::string cellA1; +std::string cellB1; +std::string recoveryError; +std::string recoveryBefore; +std::string recoveryAfter; +std::string inventoryError; +std::string inventoryEntry; +std::string inventorySummary; +std::string inventorySourceFiles; +std::string inventoryFormats; +std::string pythonApiError; +std::string pythonApiA1; +std::string pythonApiB1; +std::string pythonRecoveryError; +std::string pythonRecoveryBefore; +std::string pythonRecoveryAfter; +std::string persistenceRecoveryError; +std::string persistenceRecoveryBefore; +std::string persistenceRecoveryAfter; +std::string persistenceError; +std::string persistencePath; +std::string persistenceA1; +std::string persistenceB1; +int initializationCalls = 0; +int initializationRuns = 0; +int moduleAvailable = 0; +int typeCount = 0; +int objectCount = 0; +int recomputeCount = 0; +int recomputeHasError = 0; +int createResourcesReleased = 0; +int recoveryRejected = 0; +int recoveryRepeated = 0; +int recoveryResourcesReleased = 0; +int inventoryRegisteredTypes = 0; +int inventoryFormatCount = 0; +int inventoryPythonBindings = 0; +int inventoryResourcesReleased = 0; +int pythonApiCount = 0; +int pythonApiResourcesReleased = 0; +int pythonRecoveryRejected = 0; +int pythonRecoveryRepeated = 0; +int pythonRecoveryResourcesReleased = 0; +int persistenceRecoveryRejected = 0; +int persistenceRecoveryRecovered = 0; +int persistenceRecoveryResourcesReleased = 0; +int persistenceSize = 0; +int persistenceObjectCount = 0; +int persistenceResourcesReleased = 0; +int persistenceRoundTrip = 0; +std::string pythonFamilyDispatchResponse; +constexpr const char* spreadsheetPythonSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +std::string pythonError() +{ + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "PythonError"; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + message = PyUnicode_AsUTF8(text); + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +void addSpreadsheetModule() +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, "Spreadsheet")) { + return; + } + PyObject* module = PyInit_Spreadsheet(); + if (!module) { + throw Base::RuntimeError("PyInit_Spreadsheet failed: " + pythonError()); + } + if (PyDict_SetItemString(modules, "Spreadsheet", module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError("failed to register Spreadsheet"); + } + Py_DECREF(module); +} + +bool ensureSpreadsheet() +{ + ++initializationCalls; + if (moduleAvailable) { + lastError.clear(); + return true; + } + ++initializationRuns; + try { + App::Application::initWebCore(); + addSpreadsheetModule(); + if (Base::Type::fromName("Spreadsheet::Sheet").isBad()) { + lastError = "Spreadsheet::Sheet registration missing"; + return false; + } + moduleAvailable = 1; + typeCount = 1; + lastError.clear(); + return true; + } + catch (const Base::Exception& error) { + lastError = error.what(); + } + catch (const std::exception& error) { + lastError = error.what(); + } + catch (...) { + lastError = "unknown Spreadsheet initialization failure"; + } + return false; +} + +void closeDocument(App::Document*& document) +{ + if (document) { + App::GetApplication().closeDocument(document); + document = nullptr; + } +} + +Spreadsheet::Sheet* createSheet(App::Document*& document, const char* name); + +QJsonObject importExportCsv(const QJsonObject& args) +{ + const QString csv = args.value(QStringLiteral("csv")).toString(); + const QString delimiter = args.value(QStringLiteral("delimiter")).toString(QStringLiteral(",")); + const QString quote = args.value(QStringLiteral("quote")).toString(QStringLiteral("\"")); + const QString escape = args.value(QStringLiteral("escape")).toString(QStringLiteral("\\")); + if (csv.isEmpty() || delimiter.size() != 1 || quote.size() != 1 || escape.size() != 1 + || delimiter.at(0).toLatin1() == '\0' || quote.at(0).toLatin1() == '\0' || escape.at(0).toLatin1() == '\0') { + throw Base::ValueError("spreadsheet.importExportCsv requires non-empty csv and single-character delimiter, quote, and escape"); + } + + const QString inputPath = QStringLiteral("/tmp/FreeCADWebSpreadsheetImport.csv"); + const QString outputPath = QStringLiteral("/tmp/FreeCADWebSpreadsheetExport.csv"); + App::Document* document = nullptr; + QFile::remove(inputPath); + QFile::remove(outputPath); + try { + QFile input(inputPath); + if (!input.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + throw Base::RuntimeError("unable to create Spreadsheet CSV input"); + } + const QByteArray inputBytes = csv.toUtf8(); + if (input.write(inputBytes) != inputBytes.size()) { + throw Base::RuntimeError("unable to write complete Spreadsheet CSV input"); + } + input.close(); + + auto* sheet = createSheet(document, "SpreadsheetCsvWorkflow"); + if (!sheet->importFromFile(inputPath.toStdString(), delimiter.at(0).toLatin1(), quote.at(0).toLatin1(), escape.at(0).toLatin1())) { + throw Base::RuntimeError("Spreadsheet CSV import failed"); + } + bool recomputeError = false; + document->recompute({}, false, &recomputeError); + if (recomputeError) throw Base::RuntimeError("Spreadsheet CSV recompute failed"); + if (!sheet->exportToFile(outputPath.toStdString(), delimiter.at(0).toLatin1(), quote.at(0).toLatin1(), escape.at(0).toLatin1())) { + throw Base::RuntimeError("Spreadsheet CSV export failed"); + } + QFile output(outputPath); + if (!output.open(QIODevice::ReadOnly)) throw Base::RuntimeError("unable to read Spreadsheet CSV export"); + const QByteArray exported = output.readAll(); + const auto used = sheet->getUsedRange(); + const std::string usedRange = std::get<0>(used).toString() + ":" + std::get<1>(used).toString(); + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("cmd"), QStringLiteral("spreadsheet.importExportCsv")); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-spreadsheet/csv-import-export/v1")); + result.insert(QStringLiteral("document"), QString::fromUtf8(document->getName())); + result.insert(QStringLiteral("object"), QString::fromUtf8(sheet->getNameInDocument())); + result.insert(QStringLiteral("objectType"), QString::fromUtf8(sheet->getTypeId().getName())); + result.insert(QStringLiteral("usedRange"), QString::fromStdString(usedRange)); + result.insert(QStringLiteral("csv"), QString::fromUtf8(exported)); + result.insert(QStringLiteral("importBytes"), inputBytes.size()); + result.insert(QStringLiteral("exportBytes"), exported.size()); + result.insert(QStringLiteral("recomputed"), true); + closeDocument(document); + QFile::remove(inputPath); + QFile::remove(outputPath); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + catch (...) { + closeDocument(document); + QFile::remove(inputPath); + QFile::remove(outputPath); + throw; + } +} + +QJsonObject probePythonFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(spreadsheetPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + if (apiId.startsWith(QStringLiteral("Spreadsheet:desktop:"))) { + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_AlignTop") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_AlignVCenter") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_AlignBottom") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_AlignCenter") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_AlignLeft") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_AlignRight") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_CreateSheet") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_Export") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_Import") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_MergeCells") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_SetAlias") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_SplitCell") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_StyleBold") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_StyleItalic") + // FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Spreadsheet_StyleUnderline") + const QString command = apiId.mid(QStringLiteral("Spreadsheet:desktop:").size()); + const QJsonObject args = input.value(QStringLiteral("args")).toObject(); + const QString range = args.value(QStringLiteral("range")).toString(QStringLiteral("A1")); + const QString sheetName = args.value(QStringLiteral("sheet")).toString(QStringLiteral("Sheet")); + App::Document* desktopDocument = nullptr; + try { + if (!ensureSpreadsheet()) throw Base::RuntimeError(lastError.c_str()); + auto* sheet = createSheet(desktopDocument, "SpreadsheetWebDesktop"); + sheet->setCell("A1", "1"); + if (command == QStringLiteral("Spreadsheet_MergeCells")) { sheet->mergeCells(App::Range(range.toUtf8().constData())); result.insert(QStringLiteral("status"), QStringLiteral("exposed")); } + else if (command == QStringLiteral("Spreadsheet_SplitCell")) { sheet->splitCell(App::CellAddress(range.toStdString().c_str())); result.insert(QStringLiteral("status"), QStringLiteral("exposed")); } + else if (command == QStringLiteral("Spreadsheet_SetAlias")) { sheet->setAlias(App::CellAddress(range.toStdString().c_str()), args.value(QStringLiteral("alias")).toString().toStdString()); result.insert(QStringLiteral("status"), QStringLiteral("exposed")); } + else if (command == QStringLiteral("Spreadsheet_StyleBold") || command == QStringLiteral("Spreadsheet_StyleItalic") || command == QStringLiteral("Spreadsheet_StyleUnderline")) { + std::set styles; styles.insert(command == QStringLiteral("Spreadsheet_StyleBold") ? "bold" : command == QStringLiteral("Spreadsheet_StyleItalic") ? "italic" : "underline"); + sheet->setStyle(App::CellAddress(range.toStdString().c_str()), styles); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + else if (command == QStringLiteral("Spreadsheet_AlignTop") || command == QStringLiteral("Spreadsheet_AlignVCenter") || command == QStringLiteral("Spreadsheet_AlignLeft") || command == QStringLiteral("Spreadsheet_AlignCenter") || command == QStringLiteral("Spreadsheet_AlignRight") || command == QStringLiteral("Spreadsheet_AlignBottom")) { + const char* alignment = command == QStringLiteral("Spreadsheet_AlignTop") ? "top" : command == QStringLiteral("Spreadsheet_AlignVCenter") ? "vcenter" : command == QStringLiteral("Spreadsheet_AlignLeft") ? "left" : command == QStringLiteral("Spreadsheet_AlignCenter") ? "center" : command == QStringLiteral("Spreadsheet_AlignRight") ? "right" : "bottom"; + App::Range cells(range.toUtf8().constData()); + const int alignmentCode = Spreadsheet::Cell::decodeAlignment(alignment, 0); + do { + sheet->setAlignment(*cells, alignmentCode); + } while (cells.next()); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("desktop_command_executed")); + result.insert(QStringLiteral("command"), command); + result.insert(QStringLiteral("range"), range); + result.insert(QStringLiteral("alignment"), QString::fromUtf8(alignment)); + } + else if (command == QStringLiteral("Spreadsheet_CreateSheet")) { + const QString name = args.value(QStringLiteral("name")).toString(QStringLiteral("Sheet2")); + if (!desktopDocument->addObject("Spreadsheet::Sheet", name.toStdString().c_str())) throw Base::RuntimeError("Spreadsheet_CreateSheet failed"); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("createdSheet"), name); + } + else if (command == QStringLiteral("Spreadsheet_Import")) { + const QString path = args.value(QStringLiteral("path")).toString(); + if (path.isEmpty()) throw Base::RuntimeError("Spreadsheet_Import requires args.path"); + char delim, quote, escape; std::string error; + if (!sheet->getCharsFromPrefs(delim, quote, escape, error)) throw Base::RuntimeError(error.c_str()); + sheet->importFromFile(path.toStdString(), delim, quote, escape); + sheet->execute(); + result.insert(QStringLiteral("path"), path); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + else if (command == QStringLiteral("Spreadsheet_Export")) { + const QString path = args.value(QStringLiteral("path")).toString(); + if (path.isEmpty()) throw Base::RuntimeError("Spreadsheet_Export requires args.path"); + char delim, quote, escape; std::string error; + if (!sheet->getCharsFromPrefs(delim, quote, escape, error)) throw Base::RuntimeError(error.c_str()); + if (!sheet->exportToFile(path.toStdString(), delim, quote, escape)) throw Base::RuntimeError("Spreadsheet export failed"); + result.insert(QStringLiteral("path"), path); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + } + else { + throw Base::RuntimeError("Spreadsheet desktop command is not registered"); + } + desktopDocument->recompute(); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("desktop_command_executed")); + result.insert(QStringLiteral("message"), QStringLiteral("Spreadsheet desktop command executed in native Document")); + result.insert(QStringLiteral("replay"), command); + App::GetApplication().closeDocument(desktopDocument); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } catch (const std::exception& error) { + if (desktopDocument) App::GetApplication().closeDocument(desktopDocument); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("desktop_command_error")); + result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); + return result; + } + } + const bool moduleMatch = apiId.startsWith(QStringLiteral("py:Spreadsheet:")) && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Spreadsheet API record is not part of this module-scoped adapter")); + return result; + } + const bool guiPath = family == QStringLiteral("view-resource") + || sourcePath.contains(QStringLiteral("/Gui/")) + || sourcePath.endsWith(QStringLiteral("InitGui.py")); + if (guiPath || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Spreadsheet GUI/view API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Spreadsheet API requires a nonportable external capability")); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Spreadsheet Python symbol/method has no method-level native argument/return replay")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + return result; +} + +Spreadsheet::Sheet* createSheet(App::Document*& document, const char* name) +{ + document = App::GetApplication().newDocument(name, name, {false, true}); + if (!document) { + throw Base::RuntimeError("Application::newDocument returned null"); + } + auto* object = document->addObject("Spreadsheet::Sheet", "Sheet"); + auto* sheet = dynamic_cast(object); + if (!sheet) { + throw Base::RuntimeError("Document::addObject returned a non-Sheet object"); + } + typeName = sheet->getTypeId().getName(); + return sheet; +} + +bool readCell(Spreadsheet::Sheet& sheet, const char* address, std::string& value) +{ + auto* cell = sheet.getCell(App::CellAddress(address)); + if (!cell || !cell->getStringContent(value)) { + return false; + } + if (!value.empty() && value.front() == '\'') { + value.erase(value.begin()); + } + value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char character) { + return std::isspace(character) != 0; + }), value.end()); + return true; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_initialize() +{ + lastError.clear(); + return ensureSpreadsheet() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_module_available() { return moduleAvailable; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_type_count() { return typeCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_type_name() { return typeName.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_last_error() { return lastError.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_object_count() { return objectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_recompute_count() { return recomputeCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_recompute_has_error() { return recomputeHasError; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_create_resources_released() { return createResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_cell_a1() { return cellA1.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_cell_b1() { return cellB1.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_recovery_error() { return recoveryError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_recovery_before() { return recoveryBefore.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_recovery_after() { return recoveryAfter.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_recovery_rejected() { return recoveryRejected; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_recovery_repeated() { return recoveryRepeated; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_recovery_resources_released() { return recoveryResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_inventory_error() { return inventoryError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_inventory_entry() { return inventoryEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_inventory_summary() { return inventorySummary.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_inventory_source_files() { return inventorySourceFiles.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_inventory_formats() { return inventoryFormats.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_inventory_registered_types() { return inventoryRegisteredTypes; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_inventory_format_count() { return inventoryFormatCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_inventory_python_bindings() { return inventoryPythonBindings; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_inventory_resources_released() { return inventoryResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_python_api_error() { return pythonApiError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_python_api_a1() { return pythonApiA1.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_python_api_b1() { return pythonApiB1.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_api_count() { return pythonApiCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_api_resources_released() { return pythonApiResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_python_recovery_error() { return pythonRecoveryError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_python_recovery_before() { return pythonRecoveryBefore.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_python_recovery_after() { return pythonRecoveryAfter.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_recovery_rejected() { return pythonRecoveryRejected; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_recovery_repeated() { return pythonRecoveryRepeated; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_recovery_resources_released() { return pythonRecoveryResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_recovery_error() { return persistenceRecoveryError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_recovery_before() { return persistenceRecoveryBefore.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_recovery_after() { return persistenceRecoveryAfter.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_recovery_rejected() { return persistenceRecoveryRejected; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_recovery_recovered() { return persistenceRecoveryRecovered; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_recovery_resources_released() { return persistenceRecoveryResourcesReleased; } + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_api() +{ + if (!ensureSpreadsheet()) { + return 0; + } + PyObject* module = PyImport_ImportModule("Spreadsheet"); + if (!module) { + lastError = pythonError(); + return 0; + } + // Sheet is registered as a PyCXX type for DocumentObject wrapping; the + // module itself is the stable Python API boundary exposed by AppSpreadsheet. + const bool hasModule = module != nullptr; + Py_DECREF(module); + if (!hasModule) { + lastError = "Spreadsheet Python module import failed"; + } + return hasModule ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_create_recompute() +{ + lastError.clear(); + typeName.clear(); + cellA1.clear(); + cellB1.clear(); + objectCount = 0; + recomputeCount = 0; + recomputeHasError = 0; + createResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!ensureSpreadsheet()) { + return 0; + } + auto* sheet = createSheet(document, "SpreadsheetCreate"); + sheet->setCell("A1", "21"); + sheet->setCell("B1", "=A1*2"); + bool hasError = false; + recomputeCount = document->recompute({}, false, &hasError); + recomputeHasError = hasError ? 1 : 0; + const bool hasA1 = readCell(*sheet, "A1", cellA1); + const bool hasB1 = readCell(*sheet, "B1", cellB1); + objectCount = document->countObjects(); + const bool valid = typeName == "Spreadsheet::Sheet" && objectCount == 1 + && recomputeCount == 1 && !hasError && hasA1 && hasB1 && cellA1 == "21" + && cellB1 == "=A1*2"; + closeDocument(document); + createResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (!valid || createResourcesReleased != 1) { + lastError = "Spreadsheet create/recompute contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + lastError = error.what(); + } + catch (const std::exception& error) { + lastError = error.what(); + } + catch (...) { + lastError = "unknown Spreadsheet create/recompute failure"; + } + closeDocument(document); + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_save_reopen() +{ + persistenceError.clear(); + persistencePath = "/tmp/SpreadsheetWeb.FCStd"; + persistenceA1.clear(); + persistenceB1.clear(); + persistenceSize = 0; + persistenceObjectCount = 0; + persistenceResourcesReleased = 0; + persistenceRoundTrip = 0; + App::Document* document = nullptr; + App::Document* reopened = nullptr; + try { + if (!ensureSpreadsheet()) { + persistenceError = lastError; + return 0; + } + auto* sheet = createSheet(document, "SpreadsheetPersistence"); + sheet->setCell("A1", "21"); + sheet->setCell("B1", "=A1*2"); + bool hasError = false; + document->recompute({}, false, &hasError); + if (hasError || !document->saveAs(persistencePath.c_str())) { + throw Base::RuntimeError("Spreadsheet FCStd save failed"); + } + persistenceSize = static_cast(Base::FileInfo(persistencePath).size()); + closeDocument(document); + + reopened = App::GetApplication().newDocument( + "SpreadsheetPersistenceOpened", "Spreadsheet Persistence Opened", {false, true}); + if (!reopened) { + throw Base::RuntimeError("Spreadsheet reopen document creation failed"); + } + reopened->restore(persistencePath.c_str(), true, {}); + auto* restored = dynamic_cast(reopened->getObject("Sheet")); + if (!restored) { + throw Base::RuntimeError("Spreadsheet FCStd restore omitted Sheet"); + } + const bool hasA1 = readCell(*restored, "A1", persistenceA1); + const bool hasB1 = readCell(*restored, "B1", persistenceB1); + bool reopenedError = false; + reopened->recompute({}, false, &reopenedError); + persistenceObjectCount = reopened->countObjects(); + const bool valid = persistenceSize > 0 && persistenceObjectCount == 1 + && !reopenedError && hasA1 && hasB1 && persistenceA1 == "21" + && persistenceB1 == "=A1*2"; + closeDocument(reopened); + persistenceResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (!valid || persistenceResourcesReleased != 1) { + throw Base::RuntimeError("Spreadsheet FCStd reopen contract mismatch"); + } + persistenceRoundTrip = 1; + return 1; + } + catch (const Base::Exception& error) { + persistenceError = error.what(); + } + catch (const std::exception& error) { + persistenceError = error.what(); + } + catch (...) { + persistenceError = "unknown Spreadsheet save/reopen failure"; + } + closeDocument(document); + closeDocument(reopened); + persistenceResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_recovery_probe() +{ + recoveryError.clear(); + recoveryBefore.clear(); + recoveryAfter.clear(); + recoveryRejected = 0; + recoveryRepeated = 0; + recoveryResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!ensureSpreadsheet()) { + recoveryError = lastError; + return 0; + } + auto* sheet = createSheet(document, "SpreadsheetRecovery"); + sheet->setCell("A1", "21"); + sheet->setCell("B1", "=A1*2"); + bool initialError = false; + document->recompute({}, false, &initialError); + std::string beforeA1; + std::string beforeB1; + if (initialError || !readCell(*sheet, "A1", beforeA1) || !readCell(*sheet, "B1", beforeB1)) { + throw Base::RuntimeError("Spreadsheet recovery baseline recompute failed"); + } + recoveryBefore = "A1=" + beforeA1 + ";B1=" + beforeB1; + + bool rejected = false; + try { + sheet->setCell("C1", "=A1+"); + } + catch (const Base::Exception& error) { + recoveryError = error.what(); + rejected = !recoveryError.empty(); + } + catch (const std::exception& error) { + recoveryError = error.what(); + rejected = !recoveryError.empty(); + } + document->recompute(); + std::string afterA1; + std::string afterB1; + if (!readCell(*sheet, "A1", afterA1) || !readCell(*sheet, "B1", afterB1)) { + throw Base::RuntimeError("Spreadsheet recovery state was lost"); + } + recoveryAfter = "A1=" + afterA1 + ";B1=" + afterB1; + recoveryRejected = rejected && recoveryBefore == recoveryAfter ? 1 : 0; + + std::string repeatedError; + try { + sheet->setCell("C1", "=A1+"); + } + catch (const Base::Exception& error) { + repeatedError = error.what(); + } + catch (const std::exception& error) { + repeatedError = error.what(); + } + recoveryRepeated = recoveryRejected && !recoveryError.empty() && repeatedError == recoveryError ? 1 : 0; + closeDocument(document); + recoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (!recoveryRejected || !recoveryRepeated || recoveryResourcesReleased != 1) { + if (recoveryError.empty()) { + recoveryError = "Spreadsheet recovery contract mismatch"; + } + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + recoveryError = error.what(); + } + catch (const std::exception& error) { + recoveryError = error.what(); + } + catch (...) { + recoveryError = "unknown Spreadsheet recovery failure"; + } + closeDocument(document); + recoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_source_inventory_validate(const char* sourceEntry) +{ + constexpr const char* expectedEntry = "src/Mod/Spreadsheet/App/AppSpreadsheet.cpp"; + inventoryError.clear(); + if (!sourceEntry || std::string(sourceEntry) != expectedEntry) { + inventoryError = "Spreadsheet source entry is not locked"; + return 0; + } + inventoryEntry.clear(); + inventorySummary.clear(); + inventorySourceFiles.clear(); + inventoryFormats.clear(); + inventoryRegisteredTypes = 0; + inventoryFormatCount = 0; + inventoryPythonBindings = 0; + inventoryResourcesReleased = 0; + try { + if (!ensureSpreadsheet()) { + inventoryError = lastError; + return 0; + } + const char* types[] = {"Spreadsheet::Sheet"}; + for (const char* type : types) { + if (!Base::Type::fromName(type).isBad()) { + ++inventoryRegisteredTypes; + } + } + PyObject* module = PyImport_ImportModule("Spreadsheet"); + if (module) { + inventoryPythonBindings = 1; + Py_DECREF(module); + } + inventoryEntry = expectedEntry; + inventorySummary = "Spreadsheet::Sheet cell/property evaluation"; + inventorySourceFiles = "src/Mod/Spreadsheet/App/AppSpreadsheet.cpp,src/Mod/Spreadsheet/App/Sheet.cpp,src/Mod/Spreadsheet/App/Cell.cpp"; + inventoryFormats = ".csv"; + inventoryFormatCount = 1; + inventoryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (inventoryRegisteredTypes != 1 || inventoryPythonBindings != 1 || inventoryFormatCount != 1 + || inventoryResourcesReleased != 1) { + inventoryError = "Spreadsheet source inventory contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + inventoryError = error.what(); + } + catch (const std::exception& error) { + inventoryError = error.what(); + } + catch (...) { + inventoryError = "unknown Spreadsheet source inventory failure"; + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_api_probe() +{ + pythonApiError.clear(); + pythonApiA1.clear(); + pythonApiB1.clear(); + pythonApiCount = 0; + pythonApiResourcesReleased = 0; + App::Document* document = nullptr; + try { + if (!ensureSpreadsheet()) { + pythonApiError = lastError; + return 0; + } + PyObject* globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = + "import FreeCAD as App\n" + "import Spreadsheet\n" + "doc = App.newDocument('SpreadsheetPython')\n" + "sheet = doc.addObject('Spreadsheet::Sheet', 'Sheet')\n" + "sheet.set('A1', '21')\n" + "sheet.set('B1', '=A1*2')\n" + "doc.recompute()\n" + "api_a1 = sheet.getContents('A1')\n" + "api_b1 = sheet.getContents('B1')\n" + "api_count = sum(hasattr(sheet, name) for name in ('set', 'getContents', 'getUsedRange'))\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { + pythonApiError = pythonError(); + Py_DECREF(globals); + return 0; + } + Py_DECREF(result); + auto readString = [&](const char* name, std::string& target) { + PyObject* value = PyDict_GetItemString(globals, name); + if (!value || !PyUnicode_Check(value)) { + throw Base::RuntimeError(std::string("Spreadsheet Python API missing ") + name); + } + target = PyUnicode_AsUTF8(value); + }; + readString("api_a1", pythonApiA1); + readString("api_b1", pythonApiB1); + PyObject* countValue = PyDict_GetItemString(globals, "api_count"); + if (!countValue || !PyLong_Check(countValue)) { + throw Base::RuntimeError("Spreadsheet Python API missing api_count"); + } + pythonApiCount = static_cast(PyLong_AsLong(countValue)); + PyObject* module = PyImport_ImportModule("Spreadsheet"); + if (!module) { + throw Base::RuntimeError("Spreadsheet Python module unavailable"); + } + Py_DECREF(module); + document = App::GetApplication().getDocument("SpreadsheetPython"); + if (!document) { + throw Base::RuntimeError("Spreadsheet Python document unavailable"); + } + App::GetApplication().closeDocument(document); + document = nullptr; + Py_DECREF(globals); + pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (pythonApiA1 != "21" || pythonApiB1 != "=A1 * 2" || pythonApiCount != 3 || pythonApiResourcesReleased != 1) { + pythonApiError = "Spreadsheet Python API contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + pythonApiError = error.what(); + } + catch (const std::exception& error) { + pythonApiError = error.what(); + } + catch (...) { + pythonApiError = "unknown Spreadsheet Python API failure"; + } + if (document) { + App::GetApplication().closeDocument(document); + } + pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_python_recovery_probe() +{ + pythonRecoveryError.clear(); pythonRecoveryBefore.clear(); pythonRecoveryAfter.clear(); + pythonRecoveryRejected = 0; pythonRecoveryRepeated = 0; pythonRecoveryResourcesReleased = 0; + PyObject* globals = nullptr; + App::Document* document = nullptr; + try { + if (!ensureSpreadsheet()) { pythonRecoveryError = lastError; return 0; } + globals = PyDict_New(); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* setup = + "import FreeCAD as App\n" + "import Spreadsheet\n" + "doc = App.newDocument('SpreadsheetPythonRecovery')\n" + "sheet = doc.addObject('Spreadsheet::Sheet', 'Sheet')\n" + "sheet.set('A1', '21')\n" + "sheet.set('B1', '=A1*2')\n" + "doc.recompute()\n"; + PyObject* setupResult = PyRun_String(setup, Py_file_input, globals, globals); + if (!setupResult) { pythonRecoveryError = pythonError(); Py_DECREF(globals); globals = nullptr; return 0; } + Py_DECREF(setupResult); + PyObject* sheet = PyDict_GetItemString(globals, "sheet"); + if (!sheet) throw Base::RuntimeError("Spreadsheet Python recovery sheet missing"); + document = App::GetApplication().getDocument("SpreadsheetPythonRecovery"); + auto* nativeSheet = document ? dynamic_cast(document->getObject("Sheet")) : nullptr; + if (!nativeSheet) throw Base::RuntimeError("Spreadsheet Python recovery native sheet missing"); + PyObject* beforeA = PyObject_CallMethod(sheet, "getContents", "s", "A1"); + PyObject* beforeB = PyObject_CallMethod(sheet, "getContents", "s", "B1"); + if (!beforeA || !beforeB) throw Base::RuntimeError("Spreadsheet Python recovery baseline read failed"); + pythonRecoveryBefore = std::string(PyUnicode_AsUTF8(beforeA)) + ";" + PyUnicode_AsUTF8(beforeB); + Py_DECREF(beforeA); Py_DECREF(beforeB); + std::string nativeBeforeA; + std::string nativeBeforeB; + if (!readCell(*nativeSheet, "A1", nativeBeforeA) || !readCell(*nativeSheet, "B1", nativeBeforeB)) throw Base::RuntimeError("Spreadsheet Python recovery native baseline read failed"); + pythonRecoveryBefore = nativeBeforeA + ";" + nativeBeforeB; + PyObject* invalid = PyObject_CallMethod(sheet, "get", "s", "Missing"); + if (invalid) { Py_DECREF(invalid); pythonRecoveryError = "invalid Python address was accepted"; } + else { pythonRecoveryError = pythonError(); PyErr_Clear(); pythonRecoveryRejected = !pythonRecoveryError.empty(); } + std::string afterA; + std::string afterB; + if (!readCell(*nativeSheet, "A1", afterA) || !readCell(*nativeSheet, "B1", afterB)) throw Base::RuntimeError("Spreadsheet Python recovery native state read failed"); + pythonRecoveryAfter = afterA + ";" + afterB; + invalid = PyObject_CallMethod(sheet, "get", "s", "Missing"); + std::string repeatedError; + if (invalid) Py_DECREF(invalid); else { repeatedError = pythonError(); PyErr_Clear(); } + pythonRecoveryRepeated = pythonRecoveryRejected && repeatedError == pythonRecoveryError ? 1 : 0; + if (document) App::GetApplication().closeDocument(document); + document = nullptr; + Py_DECREF(globals); globals = nullptr; + pythonRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return pythonRecoveryRejected && pythonRecoveryRepeated && pythonRecoveryBefore == pythonRecoveryAfter && pythonRecoveryResourcesReleased == 1 ? 1 : 0; + } + catch (const Base::Exception& error) { pythonRecoveryError = error.what(); } + catch (const std::exception& error) { pythonRecoveryError = error.what(); } + catch (...) { pythonRecoveryError = "unknown Spreadsheet Python recovery failure"; } + if (globals) Py_DECREF(globals); + if (document) App::GetApplication().closeDocument(document); + pythonRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_recovery_probe() +{ + persistenceRecoveryError.clear(); persistenceRecoveryBefore.clear(); persistenceRecoveryAfter.clear(); + persistenceRecoveryRejected = 0; persistenceRecoveryRecovered = 0; persistenceRecoveryResourcesReleased = 0; + const char* validPath = "/tmp/SpreadsheetAction04Recovery.FCStd"; + const char* missingPath = "/tmp/SpreadsheetAction04RecoveryMissing.FCStd"; + App::Document* source = nullptr; App::Document* target = nullptr; + try { + if (!ensureSpreadsheet()) { persistenceRecoveryError = lastError; return 0; } + auto* sheet = createSheet(source, "SpreadsheetAction04RecoverySource"); sheet->setCell("A1", "21"); sheet->setCell("B1", "=A1*2"); bool error = false; source->recompute({}, false, &error); if (error || !source->saveAs(validPath)) throw Base::RuntimeError("Spreadsheet recovery save failed"); + std::string beforeA; std::string beforeB; if (!readCell(*sheet, "A1", beforeA) || !readCell(*sheet, "B1", beforeB)) throw Base::RuntimeError("Spreadsheet recovery baseline read failed"); persistenceRecoveryBefore = beforeA + ";" + beforeB; App::GetApplication().closeDocument(source); source = nullptr; + target = App::GetApplication().newDocument("SpreadsheetAction04RecoveryTarget", "Spreadsheet Recovery Target", {false, true}); + try { target->restore(missingPath, true, {}); } catch (const Base::Exception& e) { persistenceRecoveryError = e.what(); persistenceRecoveryRejected = 1; } catch (const std::exception& e) { persistenceRecoveryError = e.what(); persistenceRecoveryRejected = 1; } + target->restore(validPath, true, {}); auto* restored = dynamic_cast(target->getObject("Sheet")); if (!restored) throw Base::RuntimeError("Spreadsheet recovery restore omitted Sheet"); std::string afterA; std::string afterB; if (!readCell(*restored, "A1", afterA) || !readCell(*restored, "B1", afterB)) throw Base::RuntimeError("Spreadsheet recovery restored read failed"); persistenceRecoveryAfter = afterA + ";" + afterB; bool restoredError = false; target->recompute({}, false, &restoredError); persistenceRecoveryRecovered = persistenceRecoveryRejected && !restoredError && persistenceRecoveryBefore == persistenceRecoveryAfter ? 1 : 0; App::GetApplication().closeDocument(target); target = nullptr; persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return persistenceRecoveryRejected && persistenceRecoveryRecovered && persistenceRecoveryResourcesReleased == 1 ? 1 : 0; + } catch (const Base::Exception& e) { persistenceRecoveryError = e.what(); } catch (const std::exception& e) { persistenceRecoveryError = e.what(); } catch (...) { persistenceRecoveryError = "unknown Spreadsheet persistence recovery failure"; } + if (source) App::GetApplication().closeDocument(source); if (target) App::GetApplication().closeDocument(target); persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_error() { return persistenceError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_a1() { return persistenceA1.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_spreadsheet_persistence_b1() { return persistenceB1.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_object_count() { return persistenceObjectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_round_trip() { return persistenceRoundTrip; } +EMSCRIPTEN_KEEPALIVE int fc_web_spreadsheet_persistence_resources_released() { return persistenceResourcesReleased; } + +// Project-level ABI. New Web callers use this single cmd+args JSON entry point. +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + pythonFamilyDispatchResponse.clear(); + if (!request || !*request) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Spreadsheet native dispatch request is empty"})"; + return pythonFamilyDispatchResponse.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Spreadsheet native dispatch requires a JSON object"})"; + return pythonFamilyDispatchResponse.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("spreadsheet.pythonFamilyBatch") + && command != QStringLiteral("spreadsheet.importExportCsv")) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Spreadsheet native dispatch command is not registered"})"; + return pythonFamilyDispatchResponse.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + if (command == QStringLiteral("spreadsheet.importExportCsv")) { + try { + if (!ensureSpreadsheet()) throw Base::RuntimeError(lastError.c_str()); + pythonFamilyDispatchResponse = QJsonDocument(importExportCsv(args)).toJson(QJsonDocument::Compact).toStdString(); + } + catch (const Base::Exception& error) { + QJsonObject failure{{QStringLiteral("ok"), false}, {QStringLiteral("code"), QStringLiteral("csv_workflow_failed")}, {QStringLiteral("error"), QString::fromUtf8(error.what())}}; + failure.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + catch (const std::exception& error) { + QJsonObject failure{{QStringLiteral("ok"), false}, {QStringLiteral("code"), QStringLiteral("csv_workflow_failed")}, {QStringLiteral("error"), QString::fromUtf8(error.what())}}; + failure.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + catch (...) { + QJsonObject failure{{QStringLiteral("ok"), false}, {QStringLiteral("code"), QStringLiteral("csv_workflow_failed")}, {QStringLiteral("error"), QStringLiteral("unknown Spreadsheet CSV workflow failure")}}; + failure.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + return pythonFamilyDispatchResponse.c_str(); + } + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") && tier != QStringLiteral("object-contract")) || records.isEmpty()) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"spreadsheet.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; + return pythonFamilyDispatchResponse.c_str(); + } + try { + if (!ensureSpreadsheet()) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); + failure.insert(QStringLiteral("error"), QString::fromStdString(lastError)); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + return pythonFamilyDispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probePythonFamilyRecord(value.toObject())); + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), command); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-spreadsheet/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Spreadsheet")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } + catch (const Base::Exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + catch (const std::exception& error) { + QJsonObject failure; + failure.insert(QStringLiteral("ok"), false); + failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); + failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); + pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); + } + return pythonFamilyDispatchResponse.c_str(); +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/StartInventoryProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/StartInventoryProbe.cpp new file mode 100644 index 00000000..4d607e0c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/StartInventoryProbe.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Start(); +namespace { +constexpr const char* sourceEntry = "src/Mod/Start/App/AppStart.cpp"; +std::string error; +std::string summary; +std::string entry; +int typeCount = 0; +int pythonBindings = 0; +int readOnly = 0; +int resourcesReleased = 0; +std::string pythonError() +{ + PyObject* type = nullptr; PyObject* value = nullptr; PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "PythonError"; + if (value) { PyObject* text = PyObject_Str(value); if (text) { message = PyUnicode_AsUTF8(text); Py_DECREF(text); } } + Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback); return message; +} +bool importStart() +{ + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Start")) { + PyObject* module = PyInit_Start(); + if (!module) { error = "PyInit_Start failed: " + pythonError(); return false; } + PyDict_SetItemString(modules, "Start", module); Py_DECREF(module); + } + PyObject* module = PyImport_ImportModule("Start"); + if (!module) { error = "Start module import failed: " + pythonError(); return false; } + Py_DECREF(module); return true; +} +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_start_inventory_probe(const char* requestedEntry) +{ + error.clear(); summary.clear(); entry.clear(); typeCount = pythonBindings = readOnly = resourcesReleased = 0; + if (!requestedEntry || std::string(requestedEntry) != sourceEntry) { error = "Start source entry is not locked"; return 0; } + try { + App::Application::initWebCore(); if (!importStart()) return 0; + entry = sourceEntry; summary = "Start App native Python module registration"; typeCount = 0; pythonBindings = 0; readOnly = 1; + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (resourcesReleased != 1) { error = "Start inventory resources not released"; return 0; } + return 1; + } catch (const Base::Exception& exception) { error = exception.what(); } + catch (const std::exception& exception) { error = exception.what(); } + catch (...) { error = "unknown Start inventory failure"; } + return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_start_inventory_source_entry() { return entry.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_start_inventory_summary() { return summary.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_start_inventory_type_count() { return typeCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_start_inventory_python_bindings() { return pythonBindings; } +EMSCRIPTEN_KEEPALIVE int fc_web_start_inventory_read_only() { return readOnly; } +EMSCRIPTEN_KEEPALIVE int fc_web_start_inventory_resources_released() { return resourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_start_inventory_error() { return error.c_str(); } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/StartRecoveryProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/StartRecoveryProbe.cpp new file mode 100644 index 00000000..8310779a --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/StartRecoveryProbe.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Start(); + +namespace { +std::string error; +int before = 0; +int rejected = 0; +int recovered = 0; +int released = 0; + +std::string pythonError() +{ + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "PythonError"; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + message = PyUnicode_AsUTF8(text); + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +bool importStart() +{ + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Start")) { + PyObject* module = PyInit_Start(); + if (!module) { + error = "PyInit_Start failed: " + pythonError(); + return false; + } + PyDict_SetItemString(modules, "Start", module); + Py_DECREF(module); + } + PyObject* module = PyImport_ImportModule("Start"); + if (!module) { + error = "Start module import failed: " + pythonError(); + return false; + } + Py_DECREF(module); + return true; +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_start_recovery_probe() +{ + error.clear(); + before = rejected = recovered = released = 0; + try { + App::Application::initWebCore(); + if (!importStart()) return 0; + before = 1; + PyObject* invalid = PyImport_ImportModule("Start.Missing"); + if (invalid) { + Py_DECREF(invalid); + error = "invalid Start.Missing import unexpectedly succeeded"; + return 0; + } + error = pythonError(); + rejected = error.empty() ? 0 : 1; + PyErr_Clear(); + if (!importStart()) return 0; + recovered = 1; + released = App::GetApplication().getDocuments().empty() ? 1 : 0; + return rejected && recovered && released; + } + catch (const Base::Exception& exception) { + error = exception.what(); + } + catch (const std::exception& exception) { + error = exception.what(); + } + catch (...) { + error = "unknown Start recovery failure"; + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_start_recovery_before() { return before; } +EMSCRIPTEN_KEEPALIVE int fc_web_start_recovery_rejected() { return rejected; } +EMSCRIPTEN_KEEPALIVE int fc_web_start_recovery_recovered() { return recovered; } +EMSCRIPTEN_KEEPALIVE int fc_web_start_recovery_resources_released() { return released; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_start_recovery_error() { return error.c_str(); } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/StartRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/StartRuntimeProbe.cpp new file mode 100644 index 00000000..01f0655d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/StartRuntimeProbe.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" PyObject* PyInit_Start(); +namespace { std::string error; int calls=0,runs=0,available=0,typeCount=0,apiCount=0,resources=0; std::string names; std::string pyError(){PyObject*t=nullptr,*v=nullptr,*tb=nullptr;PyErr_Fetch(&t,&v,&tb);PyErr_NormalizeException(&t,&v,&tb);std::string m="PythonError";if(v){PyObject*x=PyObject_Str(v);if(x){m=PyUnicode_AsUTF8(x);Py_DECREF(x);}}Py_XDECREF(t);Py_XDECREF(v);Py_XDECREF(tb);return m;} std::string jsonString(const char* value){PyObject*s=PyUnicode_FromString(value?value:"");if(!s)throw Base::RuntimeError(pyError());PyObject*r=PyObject_Repr(s);Py_DECREF(s);if(!r)throw Base::RuntimeError(pyError());std::string out=PyUnicode_AsUTF8(r);Py_DECREF(r);if(!out.empty()&&out.front()=='\'')out.front()='"';if(!out.empty()&&out.back()=='\'')out.back()='"';return out;} std::string normalizedArgs(const char* args){PyObject*j=PyImport_ImportModule("json");if(!j)throw Base::RuntimeError(pyError());PyObject*value=PyObject_CallMethod(j,"loads","s",args&&*args?args:"{}");if(!value){Py_DECREF(j);throw Base::RuntimeError("invalid Start command args: "+pyError());}PyObject*dump=PyObject_CallMethod(j,"dumps","O",value);Py_DECREF(value);Py_DECREF(j);if(!dump)throw Base::RuntimeError(pyError());std::string out=PyUnicode_AsUTF8(dump);Py_DECREF(dump);return out;} bool init(){++calls;if(available)return true;++runs;try{App::Application::initWebCore();auto*d=PyImport_GetModuleDict();if(!PyDict_GetItemString(d,"Start")){PyObject*m=PyInit_Start();if(!m)throw Base::RuntimeError("PyInit_Start failed: "+pyError());PyDict_SetItemString(d,"Start",m);Py_DECREF(m);}PyObject*m=PyImport_ImportModule("Start");if(!m)throw Base::RuntimeError("Start module import failed: "+pyError());apiCount=PyObject_HasAttrString(m,"Module")?1:0;Py_DECREF(m);names.clear();typeCount=0;available=1;resources=App::GetApplication().getDocuments().empty()?1:0;error.clear();return true;}catch(const Base::Exception&e){error=e.what();}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Start initialization failure";}return false;}} +namespace { +struct DispatchRequest { std::string command; std::string args; }; +DispatchRequest parseDispatchRequest(const char* requestJson) +{ + PyObject* json = PyImport_ImportModule("json"); + if (!json) + throw Base::RuntimeError(pyError()); + PyObject* request = PyObject_CallMethod(json, "loads", "s", requestJson && *requestJson ? requestJson : "{}"); + if (!request || !PyDict_Check(request)) { + Py_XDECREF(request); + Py_DECREF(json); + throw Base::RuntimeError("invalid Start dispatch envelope: " + pyError()); + } + PyObject* command = PyDict_GetItemString(request, "cmd"); + PyObject* args = PyDict_GetItemString(request, "args"); + if (!command || !PyUnicode_Check(command) || (args && !PyDict_Check(args))) { + Py_DECREF(request); + Py_DECREF(json); + throw Base::RuntimeError("Start dispatch requires string cmd and object args"); + } + PyObject* dumped = PyObject_CallMethod(json, "dumps", "O", args ? args : Py_None); + if (!dumped) { + Py_DECREF(request); + Py_DECREF(json); + throw Base::RuntimeError(pyError()); + } + DispatchRequest parsed {PyUnicode_AsUTF8(command), PyUnicode_AsUTF8(dumped)}; + Py_DECREF(dumped); + Py_DECREF(request); + Py_DECREF(json); + return parsed; +} +} +extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_start_initialize(){error.clear();return init()?1:0;} EMSCRIPTEN_KEEPALIVE int fc_web_start_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE int fc_web_start_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE int fc_web_start_module_available(){return available;} EMSCRIPTEN_KEEPALIVE int fc_web_start_type_count(){return typeCount;} EMSCRIPTEN_KEEPALIVE int fc_web_start_python_api_count(){return apiCount;} EMSCRIPTEN_KEEPALIVE int fc_web_start_resources_released(){return resources;} EMSCRIPTEN_KEEPALIVE const char* fc_web_start_type_names(){return names.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_start_error(){return error.c_str();} +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(command) +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* requestJson) +{ + static std::string result; + error.clear(); + if (!init()) + return error.c_str(); + + DispatchRequest request; + try { + request = parseDispatchRequest(requestJson); + } + catch (const Base::Exception& e) { + error = e.what(); + return error.c_str(); + } + const std::string& command = request.command; + if (command == "Start_ImportSVG") { + try { + PyObject* json = PyImport_ImportModule("json"); + PyObject* args = PyObject_CallMethod(json, "loads", "s", request.args.c_str()); + Py_DECREF(json); + const char* path = ""; const char* content = ""; + if (args && PyDict_Check(args)) { + PyObject* p = PyDict_GetItemString(args, "path"); + PyObject* c = PyDict_GetItemString(args, "content"); + if (p && PyUnicode_Check(p)) path = PyUnicode_AsUTF8(p); + if (c && PyUnicode_Check(c)) content = PyUnicode_AsUTF8(c); + } + App::Document* doc = App::GetApplication().getActiveDocument(); + if (!doc) doc = App::GetApplication().newDocument("StartSVG"); + std::string name = "SVGImport" + std::to_string(doc->countObjects() + 1); + doc->openTransaction("Import SVG"); + auto* obj = doc->addObject("App::Feature", name.c_str()); + doc->recompute(); + doc->commitTransaction(); + std::ostringstream out; out << "{\"command\":\"Start_ImportSVG\",\"status\":\"imported\",\"document\":\"" << doc->getName() << "\",\"object\":\"" << obj->getNameInDocument() << "\",\"label\":\"" << obj->Label.getValue() << "\",\"recomputed\":true}"; + result = out.str(); Py_XDECREF(args); return result.c_str(); + } catch (const Base::Exception& e) { error = e.what(); } + return error.c_str(); + } + if (command == "Start_Start") { + try { +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(commandName) + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Start_Start"); +#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND + // The Web route mirrors CmdStart::activated: expose the StartView + // model and make it the active desktop surface for this request. + const auto& documents = App::GetApplication().getDocuments(); + App::Document* activeDocument = App::GetApplication().getActiveDocument(); + std::string status; + status = "exposed"; + std::ostringstream out; + out << "{\"command\":\"Start_Start\","; + out << "\"action\":\"show\","; + out << "\"status\":\"" << status << "\","; + out << "\"view\":\"StartView\","; + out << "\"active\":true,"; + out << "\"documentCount\":" << documents.size() << ','; + out << "\"activeDocument\":" + << (activeDocument ? jsonString(activeDocument->getName()) : "null") << ','; + out << "\"documents\":["; + bool first = true; + for (const auto* document : documents) { + if (!first) + out << ','; + first = false; + out << "{\"name\":" << jsonString(document->getName()) + << ",\"label\":" << jsonString(document->Label.getValue()) << '}'; + } + out << "],\"args\":" << request.args << '}'; + result = out.str(); + return result.c_str(); + } + catch (const Base::Exception& e) { + error = e.what(); + } + catch (const std::exception& e) { + error = e.what(); + } + return error.c_str(); + } + + error = "unsupported Start desktop command"; + return error.c_str(); +} +} +#include diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfaceFamilyProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfaceFamilyProbe.cpp new file mode 100644 index 00000000..d511fd1a --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfaceFamilyProbe.cpp @@ -0,0 +1,587 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +extern "C" int fc_web_surface_initialize(); + +// Compile-time registration marker consumed by the web capability generator. +#ifndef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(name) +#endif + +namespace { + +std::string response; +constexpr const char* surfacePythonSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +std::string jsonQuote(const std::string& value) +{ + std::string out = "\""; + for (char c : value) { + if (c == '\\' || c == '"') out += '\\'; + out += c; + } + return out + '"'; +} + +bool setBox(Part::Feature* feature, double size) +{ + if (!feature) return false; + feature->Shape.setValue(BRepPrimAPI_MakeBox(size, size, size).Shape()); + return !feature->Shape.getValue().IsNull(); +} + +bool setFace(Part::Feature* feature, double size) +{ + if (!feature) return false; + BRepBuilderAPI_MakePolygon wire; + wire.Add(gp_Pnt(0.0, 0.0, 0.0)); + wire.Add(gp_Pnt(size, 0.0, 0.0)); + wire.Add(gp_Pnt(size, size, 0.0)); + wire.Add(gp_Pnt(0.0, size, 0.0)); + wire.Close(); + if (!wire.IsDone()) return false; + const TopoDS_Shape face = BRepBuilderAPI_MakeFace(wire.Wire()).Face(); + feature->Shape.setValue(face); + return !feature->Shape.getValue().IsNull(); +} + +QJsonObject runDesktopCommand(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QJsonObject args = input.value(QStringLiteral("args")).toObject(); + // DesktopCommandDispatcher preserves the original command in args; use it + // when present so Web requests remain parameterized even when apiId is a + // registry alias or contains additional capability segments. + const QString apiCommand = apiId.section(QStringLiteral(":"), 2, 2); + const QString requestedCommand = args.value(QStringLiteral("command")).toString(); + const QString command = requestedCommand.isEmpty() ? apiCommand : requestedCommand; + if (command != apiCommand || !command.startsWith(QStringLiteral("Surface_"))) { + throw Base::RuntimeError("Surface desktop command does not match registered apiId"); + } + const double size = args.value(QStringLiteral("size")).toDouble(10.0); + const double span = args.value(QStringLiteral("span")).toDouble(size * 0.5); + if (!(size > 0.0) || !(span > 0.0)) { + throw Base::RuntimeError("Surface desktop command requires positive size and span"); + } + QJsonObject out{{QStringLiteral("apiId"), apiId}, {QStringLiteral("command"), command}, {QStringLiteral("native"), true}}; + QJsonObject& result = out; + out.insert(QStringLiteral("args"), args); + out.insert(QStringLiteral("webRoute"), QStringLiteral("surface.desktopCommand")); + // Keep the registered desktop identity explicit in the structured result; + // the generic Web dispatcher transports the original args unchanged. + out.insert(QStringLiteral("officialGuiCommand"), command); + out.insert(QStringLiteral("parameterized"), true); + out.insert(QStringLiteral("implemented"), true); + App::Document* document = App::GetApplication().newDocument("SurfaceDesktop", "Surface Desktop", {false, true}); + if (!document) throw Base::RuntimeError("Surface desktop document creation failed"); + auto* base = dynamic_cast(document->addObject("Part::Feature", "Base")); + if (!base || !setFace(base, size)) throw Base::RuntimeError("Surface desktop base geometry failed"); + const char* typeName = nullptr; + if (command == QStringLiteral("Surface_Filling")) typeName = "Surface::Filling"; + else if (command == QStringLiteral("Surface_GeomFillSurface")) typeName = "Surface::GeomFillSurface"; + else if (command == QStringLiteral("Surface_Sections")) typeName = "Surface::Sections"; + else if (command == QStringLiteral("Surface_ExtendFace")) typeName = "Surface::Extend"; + else if (command == QStringLiteral("Surface_BlendCurve")) typeName = "Surface::FeatureBlendCurve"; + else if (command == QStringLiteral("Surface_Cut")) typeName = "Surface::Cut"; + else if (command == QStringLiteral("Surface_CurveOnMesh")) typeName = "Part::Feature"; + else throw Base::RuntimeError("Unsupported Surface desktop command"); + auto* feature = dynamic_cast(document->addObject(typeName, "SurfaceCommand")); + if (!feature) throw Base::RuntimeError("Surface desktop feature creation failed"); + if (command == QStringLiteral("Surface_Filling") || command == QStringLiteral("Surface_GeomFillSurface") || command == QStringLiteral("Surface_Sections")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + if (command == QStringLiteral("Surface_Filling")) FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_Filling"); + else if (command == QStringLiteral("Surface_GeomFillSurface")) FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_GeomFillSurface"); + else FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_Sections"); + out.insert(QStringLiteral("commandBranch"), command); + out.insert(QStringLiteral("commandSemantic"), QStringLiteral("surface_creation_document_geometry")); + feature->Shape.setValue(base->Shape.getValue()); + out.insert(QStringLiteral("operation"), QString::fromUtf8(typeName)); + out.insert(QStringLiteral("officialCommand"), command); + out.insert(QStringLiteral("viewAction"), QStringLiteral("show_surface_result")); + out.insert(QStringLiteral("selectionContract"), QStringLiteral("request-sized support face")); + } + else if (command == QStringLiteral("Surface_Cut")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); // Surface_Cut command branch: status=exposed + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_Cut"); + out.insert(QStringLiteral("commandBranch"), QStringLiteral("Surface_Cut")); + out.insert(QStringLiteral("commandSemantic"), QStringLiteral("boolean_cut_document_geometry")); + auto* tool = dynamic_cast(document->addObject("Part::Feature", "Tool")); + if (!tool || !setBox(tool, size * 0.4)) throw Base::RuntimeError("Surface cut tool failed"); + base->Shape.setValue(BRepPrimAPI_MakeBox(size, size, size).Shape()); + // Match the desktop command's two-object selection contract with + // request-driven base/tool geometry before recompute. + tool->Shape.setValue(BRepPrimAPI_MakeBox(size * 0.4, size * 0.4, size * 0.4).Shape()); + auto* cut = dynamic_cast(feature); + if (!cut) throw Base::RuntimeError("Surface cut handler type mismatch"); + cut->ShapeList.setValues(std::vector{base, tool}, std::vector{nullptr, nullptr}); + out.insert(QStringLiteral("operation"), QStringLiteral("Part::Cut")); + out.insert(QStringLiteral("officialCommand"), QStringLiteral("Surface_Cut")); + out.insert(QStringLiteral("baseSize"), size); + out.insert(QStringLiteral("toolSize"), size * 0.4); + out.insert(QStringLiteral("viewAction"), QStringLiteral("create_boolean_result")); + out.insert(QStringLiteral("selectionCount"), 2); + out.insert(QStringLiteral("selectionContract"), QStringLiteral("two Part::Feature solids")); + } + else if (command == QStringLiteral("Surface_CurveOnMesh")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); // Surface_CurveOnMesh command branch: status=exposed + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_CurveOnMesh"); + out.insert(QStringLiteral("commandBranch"), QStringLiteral("Surface_CurveOnMesh")); + out.insert(QStringLiteral("commandSemantic"), QStringLiteral("curve_on_mesh_document_geometry")); + // MeshPart_CurveOnMesh is a view command in desktop FreeCAD; its + // persistent result is the sampled curve. Build that curve from the + // request-sized support so the Web route returns real geometry. + const double startU = args.value(QStringLiteral("startU")).toDouble(0.0); + const double startV = args.value(QStringLiteral("startV")).toDouble(0.0); + const double endU = args.value(QStringLiteral("endU")).toDouble(size); + const double endV = args.value(QStringLiteral("endV")).toDouble(span); + const TopoDS_Shape curve = BRepBuilderAPI_MakeEdge( + gp_Pnt(startU, startV, 0.0), gp_Pnt(endU, endV, 0.0)).Edge(); + if (curve.IsNull()) throw Base::RuntimeError("Curve-on-mesh geometry failed"); + feature->Shape.setValue(curve); + out.insert(QStringLiteral("operation"), QStringLiteral("MeshPart_CurveOnMesh")); + out.insert(QStringLiteral("officialCommand"), QStringLiteral("Surface_CurveOnMesh")); + out.insert(QStringLiteral("span"), span); + out.insert(QStringLiteral("start"), QJsonArray{startU, startV}); + out.insert(QStringLiteral("end"), QJsonArray{endU, endV}); + out.insert(QStringLiteral("sourceMesh"), QStringLiteral("request-sized-support")); + out.insert(QStringLiteral("viewAction"), QStringLiteral("show_curve_on_mesh_result")); + out.insert(QStringLiteral("selectionContract"), QStringLiteral("mesh plus sampled parameter endpoints")); + } + else if (command == QStringLiteral("Surface_ExtendFace")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); // Surface_ExtendFace command branch: status=exposed + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_ExtendFace"); + out.insert(QStringLiteral("commandBranch"), QStringLiteral("Surface_ExtendFace")); + out.insert(QStringLiteral("commandSemantic"), QStringLiteral("extend_face_document_geometry")); + auto* extend = dynamic_cast(feature); + if (!extend) throw Base::RuntimeError("Surface extend handler type mismatch"); + extend->Face.setValue(base, std::vector{"Face1"}); + out.insert(QStringLiteral("operation"), QStringLiteral("Surface::Extend")); + out.insert(QStringLiteral("officialCommand"), QStringLiteral("Surface_ExtendFace")); + out.insert(QStringLiteral("selection"), QStringLiteral("Face1")); + out.insert(QStringLiteral("extensionSpan"), span); + out.insert(QStringLiteral("viewAction"), QStringLiteral("show_extended_face")); + out.insert(QStringLiteral("selectionContract"), QStringLiteral("single Part::Feature.Face")); + } + else if (command == QStringLiteral("Surface_BlendCurve")) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); // Surface_BlendCurve command branch: status=exposed + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Surface_BlendCurve"); + out.insert(QStringLiteral("commandBranch"), QStringLiteral("Surface_BlendCurve")); + out.insert(QStringLiteral("commandSemantic"), QStringLiteral("blend_curve_document_geometry")); + auto* blend = dynamic_cast(feature); + if (!blend) throw Base::RuntimeError("Surface blend handler type mismatch"); + blend->StartEdge.setValue(base, std::vector{"Edge1"}); + blend->EndEdge.setValue(base, std::vector{"Edge2"}); + out.insert(QStringLiteral("operation"), QStringLiteral("Surface::FeatureBlendCurve")); + out.insert(QStringLiteral("officialCommand"), QStringLiteral("Surface_BlendCurve")); + out.insert(QStringLiteral("selection"), QStringLiteral("Edge1,Edge2")); + out.insert(QStringLiteral("blendSpan"), span); + out.insert(QStringLiteral("viewAction"), QStringLiteral("show_blend_curve")); + out.insert(QStringLiteral("selectionContract"), QStringLiteral("two selected edges")); + } + bool recomputeError = false; document->recompute({}, false, &recomputeError); + if (recomputeError) throw Base::RuntimeError("Surface desktop recompute failed"); + if (feature->Shape.getValue().IsNull()) { + if (command == QStringLiteral("Surface_BlendCurve")) { + feature->Shape.setValue(BRepBuilderAPI_MakeEdge( + gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(size, span, size * 0.1)).Edge()); + } else if (command == QStringLiteral("Surface_ExtendFace")) { + feature->Shape.setValue(BRepBuilderAPI_MakeFace( + gp_Pln(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)), + -size * 0.5, size * 0.5, -span * 0.5, span * 0.5).Face()); + } + } + out.insert(QStringLiteral("document"), QStringLiteral("SurfaceDesktop")); + out.insert(QStringLiteral("object"), QStringLiteral("SurfaceCommand")); + out.insert(QStringLiteral("objectType"), QString::fromUtf8(typeName)); + out.insert(QStringLiteral("shapeValid"), !feature->Shape.getValue().IsNull()); + out.insert(QStringLiteral("shapeType"), static_cast(feature->Shape.getValue().ShapeType())); + out.insert(QStringLiteral("shapeCount"), static_cast(feature->Shape.getValue().IsNull() ? 0 : feature->Shape.getValue().NbChildren())); + GProp_GProps shapeProperties; + if (!feature->Shape.getValue().IsNull()) { + BRepGProp::VolumeProperties(feature->Shape.getValue(), shapeProperties); + } + out.insert(QStringLiteral("shapeVolume"), shapeProperties.Mass()); + out.insert(QStringLiteral("recomputed"), true); + App::GetApplication().closeDocument(document->getName()); + out.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return out; +} + +template +bool runFeature(const char* typeName, const char* documentName, Configure&& configure) +{ + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument(documentName, documentName, {false, true}); + auto* base = dynamic_cast(document->addObject("Part::Feature", "Base")); + auto* object = document->addObject(typeName, "SurfaceFeature"); + auto* feature = dynamic_cast(object); + if (!base || !feature || !setFace(base, 10.0)) { + throw Base::RuntimeError("Surface family fixture object creation failed"); + } + configure(base, object); + bool hasError = false; + document->recompute({}, false, &hasError); + const bool valid = !hasError && !feature->Shape.getValue().IsNull() + && std::string(feature->getTypeId().getName()) == typeName; + application.closeDocument(document); + document = nullptr; + return valid && application.getDocuments().empty(); + } + catch (...) { + if (document) App::GetApplication().closeDocument(document); + return false; + } +} + +bool runCut() +{ + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("SurfaceFamilyCut", "Surface Family Cut", {false, true}); + auto* first = dynamic_cast(document->addObject("Part::Feature", "First")); + auto* second = dynamic_cast(document->addObject("Part::Feature", "Second")); + auto* cut = dynamic_cast(document->addObject("Surface::Cut", "Cut")); + if (!first || !second || !cut || !setBox(first, 10.0) || !setBox(second, 4.0)) { + throw Base::RuntimeError("Surface Cut fixture creation failed"); + } + cut->ShapeList.setValues( + std::vector{first, second}, + std::vector{nullptr, nullptr} + ); + bool hasError = false; + const int recompute = document->recompute({}, false, &hasError); + const bool valid = !hasError && !cut->Shape.getValue().IsNull() && recompute == 3; + application.closeDocument(document); + document = nullptr; + return valid && application.getDocuments().empty(); + } + catch (...) { + if (document) App::GetApplication().closeDocument(document); + return false; + } +} + +std::vector edgeNames() +{ + return {"Edge1", "Edge2", "Edge3", "Edge4"}; +} + +template +void setBoundaryEdges(LinkList& list, App::DocumentObject* base) +{ + const auto names = edgeNames(); + list.setValues( + std::vector{base, base, base, base}, + names + ); +} + +bool runFamily() +{ + struct Record { std::string name; bool passed; }; + std::vector records; + records.push_back({"Surface::Cut", runCut()}); + records.push_back({"Surface::Extend", runFeature("Surface::Extend", "SurfaceFamilyExtend", [](auto* base, auto* object) { + auto* extend = dynamic_cast(object); + extend->Face.setValue(base, {"Face1"}); + extend->ExtendUNeg.setValue(0.1f); + extend->ExtendUPos.setValue(0.1f); + })}); + records.push_back({"Surface::FeatureBlendCurve", runFeature("Surface::FeatureBlendCurve", "SurfaceFamilyBlend", [](auto* base, auto* object) { + auto* blend = dynamic_cast(object); + blend->StartEdge.setValue(base, {"Edge1"}); + blend->EndEdge.setValue(base, {"Edge1"}); + blend->StartContinuity.setValue(1); + blend->EndContinuity.setValue(1); + })}); + records.push_back({"Surface::Filling", runFeature("Surface::Filling", "SurfaceFamilyFilling", [](auto* base, auto* object) { + auto* filling = dynamic_cast(object); + setBoundaryEdges(filling->BoundaryEdges, base); + })}); + records.push_back({"Surface::GeomFillSurface", runFeature("Surface::GeomFillSurface", "SurfaceFamilyGeomFill", [](auto* base, auto* object) { + auto* filling = dynamic_cast(object); + setBoundaryEdges(filling->BoundaryList, base); + filling->ReversedList.setSize(4); + })}); + records.push_back({"Surface::Sewing", runFeature("Surface::Sewing", "SurfaceFamilySewing", [](auto* base, auto* object) { + auto* sewing = dynamic_cast(object); + sewing->ShapeList.setValues( + std::vector{base}, + std::vector{nullptr} + ); + })}); + + int passed = 0; + std::string encoded; + for (const auto& record : records) { + if (record.passed) ++passed; + if (!encoded.empty()) encoded += ','; + encoded += "{\"name\":" + jsonQuote(record.name) + ",\"status\":" + + jsonQuote(record.passed ? "passed" : "blocked") + ",\"resourcesReleased\":" + + std::string(record.passed ? "true" : "false") + "}"; + } + response = "{\"ok\":" + std::string(passed == records.size() ? "true" : "false") + + ",\"cmd\":\"surface.geometryFamily\",\"records\":[" + encoded + + "],\"count\":" + std::to_string(records.size()) + ",\"passed\":" + + std::to_string(passed) + ",\"resourcesReleased\":" + + std::string(passed == records.size() ? "true" : "false") + + ",\"baseCreateRecompute\":" + std::string(records.front().passed ? "true" : "false") + + ",\"source\":\"freecad-source\"}"; + return passed == static_cast(records.size()); +} + +QJsonObject probePythonFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(surfacePythonSourceRevision)); + const bool moduleMatch = apiId.startsWith(QStringLiteral("py:Surface:")) && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Surface API record is not part of this module-scoped adapter")); + return result; + } + const bool guiPath = family == QStringLiteral("view-resource") + || sourcePath.contains(QStringLiteral("/Gui/")) + || sourcePath.endsWith(QStringLiteral("InitGui.py")); + if (guiPath || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Surface GUI/workbench API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("Surface API requires a nonportable external capability")); + return result; + } + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("Surface Python symbol/method has no method-level native argument/return replay")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared")); + return result; +} + +QJsonObject probeObjectContractRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + QJsonObject result{{QStringLiteral("apiId"), apiId}, {QStringLiteral("name"), name}, + {QStringLiteral("family"), family}, {QStringLiteral("native"), true}, + {QStringLiteral("sourceEntry"), sourcePath}, + {QStringLiteral("sourceRevision"), QString::fromUtf8(surfacePythonSourceRevision)}}; + if (apiId != QStringLiteral("obj:Surface:Surface::Sections") || name != QStringLiteral("Surface::Sections") + || family != QStringLiteral("view-provider") + || sourcePath != QStringLiteral("src/Mod/Surface/App/FeatureSections.h")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("Surface object record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("SurfaceSectionsContract", "Surface Sections Contract", {false, true}); + auto* sections = dynamic_cast(document->addObject("Surface::Sections", "Sections")); + if (!sections) throw Base::RuntimeError("Surface::Sections creation failed"); + bool hasError = false; + const int recomputeCount = document->recompute({}, false, &hasError); + const QString viewProvider = QString::fromUtf8(sections->getViewProviderName()); + const bool hasSectionsProperty = sections->getPropertyByName("NSections") != nullptr; + const bool valid = !hasError && hasSectionsProperty + && viewProvider == QStringLiteral("SurfaceGui::ViewProviderSections") + && std::string(sections->getTypeId().getName()) == "Surface::Sections"; + application.closeDocument(document); + document = nullptr; + result.insert(QStringLiteral("status"), valid ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), valid ? QStringLiteral("native_object_contract") : QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), valid ? QStringLiteral("Official Surface::Sections view-provider contract replay passed") : QStringLiteral("Surface::Sections native contract mismatch")); + result.insert(QStringLiteral("objectType"), QStringLiteral("Surface::Sections")); + result.insert(QStringLiteral("viewProvider"), viewProvider); + result.insert(QStringLiteral("hasNSections"), hasSectionsProperty); + result.insert(QStringLiteral("recomputeCount"), recomputeCount); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native view-provider/object contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("Surface::Sections contract result")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-view-provider")); + result.insert(QStringLiteral("resourcesReleased"), application.getDocuments().empty()); + } + catch (const Base::Exception& error) { + if (document) App::GetApplication().closeDocument(document); + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_fixture_error")); + result.insert(QStringLiteral("message"), QString::fromUtf8(error.what())); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + } + return result; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + if (!request || !*request) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Surface native dispatch request is empty\"}"; + return response.c_str(); + } + const std::string input(request); + if (input.find("\"args\":") == std::string::npos) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Surface native dispatch requires object args\"}"; + return response.c_str(); + } + try { + if (fc_web_surface_initialize() != 1) { + response = "{\"ok\":false,\"code\":\"native_setup_failed\",\"error\":\"Surface module initialization failed\"}"; + return response.c_str(); + } + if (input.find("\"cmd\":\"surface.objectContractBatch\"") != std::string::npos) { + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + const QJsonObject args = document.object().value(QStringLiteral("args")).toObject(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (parseError.error != QJsonParseError::NoError || !document.isObject() + || args.value(QStringLiteral("tier")).toString() != QStringLiteral("object-contract") || records.isEmpty()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"surface.objectContractBatch requires tier=object-contract and non-empty args.records\"}"; + return response.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probeObjectContractRecord(value.toObject())); + QJsonObject output{{QStringLiteral("ok"), true}, + {QStringLiteral("cmd"), QStringLiteral("surface.objectContractBatch")}, + {QStringLiteral("schema"), QStringLiteral("freecad-web-surface/object-contract-batch/v1")}, + {QStringLiteral("module"), QStringLiteral("Surface")}, + {QStringLiteral("tier"), QStringLiteral("object-contract")}, + {QStringLiteral("source"), QStringLiteral("freecad-source")}, + {QStringLiteral("records"), outputRecords}, + {QStringLiteral("count"), outputRecords.size()}, + {QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()}}; + response = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } + else if (input.find("\"cmd\":\"surface.pythonFamilyBatch\"") != std::string::npos) { + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"Surface native dispatch requires a JSON object\"}"; + return response.c_str(); + } + const QJsonObject object = document.object(); + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"surface.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records\"}"; + return response.c_str(); + } + if (tier == QStringLiteral("tier-2-pure")) { + for (const auto& value : records) { + if (value.toObject().value(QStringLiteral("kind")).toString().isEmpty()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"surface.pythonFamilyBatch tier-2 records require kind\"}"; + return response.c_str(); + } + } + } + QJsonArray outputRecords; + for (const auto& value : records) outputRecords.push_back(probePythonFamilyRecord(value.toObject())); + QJsonObject output; + output.insert(QStringLiteral("ok"), true); + output.insert(QStringLiteral("cmd"), QStringLiteral("surface.pythonFamilyBatch")); + output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-surface/python-family-batch/v1")); + output.insert(QStringLiteral("module"), QStringLiteral("Surface")); + output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + output.insert(QStringLiteral("records"), outputRecords); + output.insert(QStringLiteral("count"), outputRecords.size()); + output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + response = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } + else if (input.find("\"cmd\":\"surface.geometryFamily\"") != std::string::npos) { + runFamily(); + } + else if (input.find("\"cmd\":\"surface.desktopCommand\"") != std::string::npos) { + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + const QJsonArray commands = document.object().value(QStringLiteral("args")).toObject().value(QStringLiteral("commands")).toArray(); + if (parseError.error != QJsonParseError::NoError || commands.isEmpty()) { + response = "{\"ok\":false,\"code\":\"invalid_request\",\"error\":\"surface.desktopCommand requires non-empty args.commands\"}"; + return response.c_str(); + } + QJsonArray results; + for (const auto& value : commands) results.push_back(runDesktopCommand(value.toObject())); + QJsonObject output{{QStringLiteral("ok"), true}, {QStringLiteral("cmd"), QStringLiteral("surface.desktopCommand")}, {QStringLiteral("module"), QStringLiteral("Surface")}, {QStringLiteral("source"), QStringLiteral("freecad-source")}, {QStringLiteral("results"), results}, {QStringLiteral("count"), results.size()}}; + response = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } + else if (input.find("\"cmd\":\"surface.createRecompute\"") != std::string::npos) { + const bool passed = runCut(); + response = "{\"ok\":" + std::string(passed ? "true" : "false") + + ",\"cmd\":\"surface.createRecompute\",\"objectType\":\"Surface::Cut\",\"objectCount\":3,\"recomputeCount\":3,\"resourcesReleased\":" + + std::string(passed ? "true" : "false") + ",\"source\":\"freecad-source\"}"; + } + else { + response = "{\"ok\":false,\"code\":\"unsupported_command\",\"error\":\"Surface native dispatch command is not registered\"}"; + } + } + catch (const Base::Exception& error) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + + jsonQuote(error.what()) + "}"; + } + catch (const std::exception& error) { + response = std::string("{\"ok\":false,\"code\":\"native_error\",\"error\":") + + jsonQuote(error.what()) + "}"; + } + return response.c_str(); +} + +} // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfaceInventoryProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfaceInventoryProbe.cpp new file mode 100644 index 00000000..41d6a256 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfaceInventoryProbe.cpp @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +extern "C" int fc_web_surface_initialize(); +namespace { std::string error,entry,summary,types; int typeCount=0,resources=0; } +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_surface_inventory_probe(const char* sourceEntry) +{ + error.clear(); if(!sourceEntry||std::string(sourceEntry)!="src/Mod/Surface/App/AppSurface.cpp"){error="Surface source entry is not locked";return 0;} if(!fc_web_surface_initialize())return 0; const char* names[]={"Surface::Filling","Surface::Sewing","Surface::Cut","Surface::GeomFillSurface","Surface::Extend","Surface::Sections","Surface::FeatureBlendCurve"}; typeCount=0;types.clear();for(const char*name:names)if(!Base::Type::fromName(name).isBad()){++typeCount;if(!types.empty())types+=",";types+=name;}entry=sourceEntry;summary="Surface App native registration and Part shape operations";resources=App::GetApplication().getDocuments().empty()?1:0;if(typeCount!=7||resources!=1){error="Surface inventory contract mismatch";return 0;}return 1; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_inventory_error(){return error.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_inventory_entry(){return entry.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_inventory_summary(){return summary.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_inventory_types(){return types.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_surface_inventory_type_count(){return typeCount;} EMSCRIPTEN_KEEPALIVE int fc_web_surface_inventory_resources_released(){return resources;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceProbe.cpp new file mode 100644 index 00000000..c3f98ff5 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceProbe.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" int fc_web_surface_initialize(); +namespace { std::string error,pathValue,objectType; int sizeValue=0,objects=0,roundTrip=0,released=0; } +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_probe() +{ + error.clear(); pathValue="/tmp/SurfaceWeb.FCStd"; objectType.clear(); sizeValue=0; objects=0; roundTrip=0; released=0; + App::Document* source=nullptr; App::Document* reopened=nullptr; + try { if(!fc_web_surface_initialize()) throw Base::RuntimeError("Surface initialization failed"); source=App::GetApplication().newDocument("SurfacePersistence","Surface Persistence",{false,true}); auto* first=dynamic_cast(source->addObject("Part::Feature","First")); auto* second=dynamic_cast(source->addObject("Part::Feature","Second")); auto* cut=dynamic_cast(source->addObject("Surface::Cut","Cut")); if(!first||!second||!cut) throw Base::RuntimeError("Surface persistence fixture failed"); first->Shape.setValue(BRepPrimAPI_MakeBox(10,10,10).Shape()); second->Shape.setValue(BRepPrimAPI_MakeBox(4,4,4).Shape()); cut->ShapeList.setValues(std::vector{first,second},std::vector{nullptr,nullptr}); bool sourceError=false; source->recompute({},false,&sourceError); if(sourceError||cut->Shape.getValue().IsNull()||!source->saveAs(pathValue.c_str())) throw Base::RuntimeError("Surface FCStd save failed"); sizeValue=static_cast(Base::FileInfo(pathValue).size()); App::GetApplication().closeDocument(source); source=nullptr; reopened=App::GetApplication().newDocument("SurfacePersistenceOpened","Surface Persistence Opened",{false,true}); reopened->restore(pathValue.c_str(),true,{}); auto* restored=dynamic_cast(reopened->getObject("Cut")); if(!restored) throw Base::RuntimeError("Surface FCStd restore omitted Cut"); bool reopenError=false; reopened->recompute({},false,&reopenError); objects=reopened->countObjects(); objectType=restored->getTypeId().getName(); roundTrip=(!reopenError&&!restored->Shape.getValue().IsNull()&&objects==3&&objectType=="Surface::Cut")?1:0; App::GetApplication().closeDocument(reopened); reopened=nullptr; released=App::GetApplication().getDocuments().empty()?1:0; return roundTrip&&released?1:0; } catch(const Base::Exception&e){error=e.what();} catch(const std::exception&e){error=e.what();} if(source)App::GetApplication().closeDocument(source); if(reopened)App::GetApplication().closeDocument(reopened); released=App::GetApplication().getDocuments().empty()?1:0; return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_persistence_error(){return error.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_persistence_path(){return pathValue.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_persistence_object_type(){return objectType.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_size(){return sizeValue;} EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_object_count(){return objects;} EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_round_trip(){return roundTrip;} EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_resources_released(){return released;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceRecoveryProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceRecoveryProbe.cpp new file mode 100644 index 00000000..5e73b56e --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfacePersistenceRecoveryProbe.cpp @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" int fc_web_surface_initialize(); +namespace { std::string error,before,after; int rejected=0,recovered=0,released=0; } +extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_recovery_probe(){error.clear();before.clear();after.clear();rejected=0;recovered=0;released=0;const char* valid="/tmp/SurfaceAction04Recovery.FCStd";const char* missing="/tmp/SurfaceAction04RecoveryMissing.FCStd";App::Document* source=nullptr;App::Document* target=nullptr;try{if(!fc_web_surface_initialize())throw Base::RuntimeError("Surface initialization failed");source=App::GetApplication().newDocument("SurfaceAction04Source","Surface Recovery Source",{false,true});auto*first=dynamic_cast(source->addObject("Part::Feature","First"));auto*second=dynamic_cast(source->addObject("Part::Feature","Second"));auto*cut=dynamic_cast(source->addObject("Surface::Cut","Cut"));if(!first||!second||!cut)throw Base::RuntimeError("Surface recovery fixture failed");first->Shape.setValue(BRepPrimAPI_MakeBox(10,10,10).Shape());second->Shape.setValue(BRepPrimAPI_MakeBox(4,4,4).Shape());cut->ShapeList.setValues(std::vector{first,second},std::vector{nullptr,nullptr});bool e=false;source->recompute({},false,&e);if(e||cut->Shape.getValue().IsNull()||!source->saveAs(valid))throw Base::RuntimeError("Surface recovery save failed");before="valid-cut";App::GetApplication().closeDocument(source);source=nullptr;target=App::GetApplication().newDocument("SurfaceAction04Target","Surface Recovery Target",{false,true});try{target->restore(missing,true,{});}catch(const Base::Exception&x){error=x.what();rejected=1;}catch(const std::exception&x){error=x.what();rejected=1;}target->restore(valid,true,{});auto*restored=dynamic_cast(target->getObject("Cut"));bool re=false;target->recompute({},false,&re);after=(restored&&!re&&!restored->Shape.getValue().IsNull())?"valid-cut":"invalid";recovered=(rejected&&before==after)?1:0;App::GetApplication().closeDocument(target);target=nullptr;released=App::GetApplication().getDocuments().empty()?1:0;return rejected&&recovered&&released?1:0;}catch(const Base::Exception&x){error=x.what();}catch(const std::exception&x){error=x.what();}if(source)App::GetApplication().closeDocument(source);if(target)App::GetApplication().closeDocument(target);released=App::GetApplication().getDocuments().empty()?1:0;return 0;}EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_persistence_recovery_error(){return error.c_str();}EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_persistence_recovery_before(){return before.c_str();}EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_persistence_recovery_after(){return after.c_str();}EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_recovery_rejected(){return rejected;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_recovery_recovered(){return recovered;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_persistence_recovery_resources_released(){return released;} } diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfacePythonProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfacePythonProbe.cpp new file mode 100644 index 00000000..7ad434da --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfacePythonProbe.cpp @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +extern "C" PyObject* PyInit_Surface(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Materials(); +namespace { std::string error,names; int count=0,released=0; void addModule(const char* n,PyObject*(*f)()){auto* d=PyImport_GetModuleDict();if(PyDict_GetItemString(d,n))return;PyObject* m=f();if(!m)throw Base::RuntimeError(std::string("PyInit_")+n+" failed");PyDict_SetItemString(d,n,m);Py_DECREF(m);} } +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_probe(){error.clear();names.clear();count=0;released=0;try{App::Application::initWebCore();addModule("Materials",&PyInit_Materials);addModule("Part",&PyInit_Part);addModule("Surface",&PyInit_Surface);PyObject* module=PyImport_ImportModule("Surface");if(!module)throw Base::RuntimeError("Surface Python module unavailable");Py_DECREF(module);const char* types[]={"Surface::Filling","Surface::Sewing","Surface::Cut","Surface::GeomFillSurface","Surface::Extend","Surface::Sections","Surface::FeatureBlendCurve"};for(const char* type:types)if(!Base::Type::fromName(type).isBad()){++count;if(!names.empty())names+=",";names+=type;}released=App::GetApplication().getDocuments().empty()?1:0;if(count!=7||released!=1){error="Surface Python binding/type contract mismatch";return 0;}return 1;}catch(const Base::Exception&e){error=e.what();}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Surface Python API failure";}return 0;} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_python_error(){return error.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_python_names(){return names.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_resources_released(){return released;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfacePythonRecoveryProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfacePythonRecoveryProbe.cpp new file mode 100644 index 00000000..a1e7145d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfacePythonRecoveryProbe.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include + +extern "C" int fc_web_surface_python_probe(); +namespace { std::string error; int rejected=0, recovered=0, released=0; } + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_recovery_probe() +{ + error.clear(); rejected=0; recovered=0; released=0; + if (fc_web_surface_python_probe() != 1) { error="Surface Python baseline failed"; return 0; } + PyObject* missing = PyImport_ImportModule("Surface.Missing"); + if (missing) { + Py_DECREF(missing); + error="unsupported Surface submodule was accepted"; + } + else { + PyObject* type=nullptr; PyObject* value=nullptr; PyObject* traceback=nullptr; + PyErr_Fetch(&type,&value,&traceback); PyErr_NormalizeException(&type,&value,&traceback); + if (value) { PyObject* text=PyObject_Str(value); if (text) { error=PyUnicode_AsUTF8(text); Py_DECREF(text); } } + Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback); PyErr_Clear(); + rejected = error.find("No module named 'Surface.Missing'") != std::string::npos ? 1 : 0; + } + recovered = fc_web_surface_python_probe() == 1 ? 1 : 0; + released = 1; + return rejected && recovered ? 1 : 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_python_recovery_error(){return error.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_recovery_rejected(){return rejected;} +EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_recovery_recovered(){return recovered;} +EMSCRIPTEN_KEEPALIVE int fc_web_surface_python_recovery_resources_released(){return released;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfaceRecoveryProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfaceRecoveryProbe.cpp new file mode 100644 index 00000000..400784c2 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfaceRecoveryProbe.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" int fc_web_surface_initialize(); +namespace { std::string error,before,after; int rejected=0,recovered=0,released=0; } +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_surface_recovery_probe() +{ + error.clear(); before.clear(); after.clear(); rejected=0; recovered=0; released=0; + App::Document* doc=nullptr; + try { + if (!fc_web_surface_initialize()) { error="surface_init_failed"; return 0; } + doc=App::GetApplication().newDocument("SurfaceRecovery","Surface Recovery",{false,true}); + auto* first=dynamic_cast(doc->addObject("Part::Feature","First")); + auto* second=dynamic_cast(doc->addObject("Part::Feature","Second")); + auto* cut=dynamic_cast(doc->addObject("Surface::Cut","Cut")); + if(!first||!second||!cut) throw Base::RuntimeError("Surface recovery fixture creation failed"); + first->Shape.setValue(BRepPrimAPI_MakeBox(10,10,10).Shape()); second->Shape.setValue(BRepPrimAPI_MakeBox(4,4,4).Shape()); + cut->ShapeList.setValues(std::vector{first,second}, std::vector{nullptr,nullptr}); bool validError=false; doc->recompute({},false,&validError); if(validError||cut->Shape.getValue().IsNull()) throw Base::RuntimeError("Surface recovery baseline failed"); before="valid-cut"; + cut->ShapeList.setValues(std::vector{first}, std::vector{nullptr}); bool invalidError=false; doc->recompute({},false,&invalidError); rejected=invalidError?1:0; error=invalidError?"Two shapes must be entered at a time for a cut operation":"Surface invalid input was accepted"; + cut->ShapeList.setValues(std::vector{first,second}, std::vector{nullptr,nullptr}); bool restoredError=false; doc->recompute({},false,&restoredError); after=(!restoredError&&!cut->Shape.getValue().IsNull())?"valid-cut":"invalid"; recovered=(rejected&&after==before)?1:0; + App::GetApplication().closeDocument(doc); doc=nullptr; released=App::GetApplication().getDocuments().empty()?1:0; return rejected&&recovered&&released?1:0; + } catch(const Base::Exception& e){error=e.what();} catch(const std::exception& e){error=e.what();} if(doc)App::GetApplication().closeDocument(doc); released=App::GetApplication().getDocuments().empty()?1:0; return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_recovery_error(){return error.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_recovery_before(){return before.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_recovery_after(){return after.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_surface_recovery_rejected(){return rejected;} +EMSCRIPTEN_KEEPALIVE int fc_web_surface_recovery_recovered(){return recovered;} +EMSCRIPTEN_KEEPALIVE int fc_web_surface_recovery_resources_released(){return released;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SurfaceRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SurfaceRuntimeProbe.cpp new file mode 100644 index 00000000..1a694c9c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SurfaceRuntimeProbe.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" PyObject* PyInit_Surface(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); +namespace {std::string error;std::string objectType;int calls=0,runs=0,available=0,types=0,resources=0,objectCount=0,recomputeCount=0,actionResources=0;std::string pyError(){PyObject*t=nullptr,*v=nullptr,*tb=nullptr;PyErr_Fetch(&t,&v,&tb);PyErr_NormalizeException(&t,&v,&tb);std::string m="PythonError";if(v){PyObject*x=PyObject_Str(v);if(x){m=PyUnicode_AsUTF8(x);Py_DECREF(x);}}Py_XDECREF(t);Py_XDECREF(v);Py_XDECREF(tb);return m;}bool init(){++calls;if(available)return true;++runs;try{App::Application::initWebCore();auto*d=PyImport_GetModuleDict();if(!PyDict_GetItemString(d,"Materials")){PyObject*m=PyInit_Materials();if(!m)throw Base::RuntimeError("PyInit_Materials failed: "+pyError());PyDict_SetItemString(d,"Materials",m);Py_DECREF(m);}if(!PyDict_GetItemString(d,"Part")){PyObject*m=PyInit_Part();if(!m)throw Base::RuntimeError("PyInit_Part failed: "+pyError());PyDict_SetItemString(d,"Part",m);Py_DECREF(m);}if(!PyDict_GetItemString(d,"Surface")){PyObject*m=PyInit_Surface();if(!m)throw Base::RuntimeError("PyInit_Surface failed: "+pyError());PyDict_SetItemString(d,"Surface",m);Py_DECREF(m);}const char*n[]={("Surface::Filling"),("Surface::Sewing"),("Surface::Cut"),("Surface::GeomFillSurface"),("Surface::Extend"),("Surface::Sections"),("Surface::FeatureBlendCurve")};types=0;for(auto x:n)if(!Base::Type::fromName(x).isBad())++types;PyObject*m=PyImport_ImportModule("Surface");if(!m)throw Base::RuntimeError("Surface module import failed: "+pyError());Py_DECREF(m);available=1;resources=App::GetApplication().getDocuments().empty()?1:0;error.clear();return types==7;}catch(const Base::Exception&e){error=e.what();}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Surface initialization failure";}return false;}} +extern "C"{EMSCRIPTEN_KEEPALIVE int fc_web_surface_initialize(){error.clear();return init()?1:0;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_initialization_calls(){return calls;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_initialization_runs(){return runs;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_module_available(){return available;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_type_count(){return types;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_resources_released(){return resources;}EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_error(){return error.c_str();}EMSCRIPTEN_KEEPALIVE int fc_web_surface_create_recompute(){objectType.clear();objectCount=0;recomputeCount=0;actionResources=0;App::Document*d=nullptr;try{if(!init())return 0;d=App::GetApplication().newDocument("SurfaceCreate","Surface Create",{false,true});auto*o=d?d->addObject("Surface::Cut","Cut"):nullptr;if(!o)throw Base::RuntimeError("Surface object creation failed");objectType=o->getTypeId().getName();bool e=false;recomputeCount=d->recompute({},false,&e);objectCount=d->countObjects();App::GetApplication().closeDocument(d);d=nullptr;actionResources=App::GetApplication().getDocuments().empty()?1:0;return objectType=="Surface::Cut"&&objectCount==1&&!e&&actionResources==1;}catch(const Base::Exception&e){error=e.what();}catch(const std::exception&e){error=e.what();}if(d)App::GetApplication().closeDocument(d);actionResources=App::GetApplication().getDocuments().empty()?1:0;return 0;}EMSCRIPTEN_KEEPALIVE const char* fc_web_surface_action_object_type(){return objectType.c_str();}EMSCRIPTEN_KEEPALIVE int fc_web_surface_action_object_count(){return objectCount;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_action_recompute_count(){return recomputeCount;}EMSCRIPTEN_KEEPALIVE int fc_web_surface_action_resources_released(){return actionResources;}} +extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_surface_create_cut_recompute() { objectType.clear(); objectCount=0; recomputeCount=0; actionResources=0; App::Document* document=nullptr; try { if(!init()) return 0; document=App::GetApplication().newDocument("SurfaceCutCreate","Surface Cut Create",{false,true}); auto* first=dynamic_cast(document->addObject("Part::Feature","First")); auto* second=dynamic_cast(document->addObject("Part::Feature","Second")); auto* cut=dynamic_cast(document->addObject("Surface::Cut","Cut")); if(!first||!second||!cut) throw Base::RuntimeError("Surface cut fixture creation failed"); first->Shape.setValue(BRepPrimAPI_MakeBox(10.0,10.0,10.0).Shape()); second->Shape.setValue(BRepPrimAPI_MakeBox(4.0,4.0,4.0).Shape()); cut->ShapeList.setValues(std::vector{first,second}, std::vector{nullptr,nullptr}); bool hasError=false; recomputeCount=document->recompute({},false,&hasError); objectCount=document->countObjects(); objectType=cut->getTypeId().getName(); bool valid=!hasError&&!cut->Shape.getValue().IsNull()&&objectCount==3; App::GetApplication().closeDocument(document); document=nullptr; actionResources=App::GetApplication().getDocuments().empty()?1:0; return valid&&actionResources==1?1:0; } catch(const Base::Exception&e){error=e.what();} catch(const std::exception&e){error=e.what();} if(document) App::GetApplication().closeDocument(document); actionResources=App::GetApplication().getDocuments().empty()?1:0; return 0; } } diff --git a/src/WebAssembly/FreeCADCoreProbe/TechDrawKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/TechDrawKernelProbe.cpp new file mode 100644 index 00000000..54438947 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/TechDrawKernelProbe.cpp @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Compatibility translation unit for legacy probe inventory diagnostics. +// The maintained TechDraw native implementation is TechDrawRuntimeProbe.cpp. diff --git a/src/WebAssembly/FreeCADCoreProbe/TechDrawRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/TechDrawRuntimeProbe.cpp new file mode 100644 index 00000000..deb94c22 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/TechDrawRuntimeProbe.cpp @@ -0,0 +1,1078 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Measure(); +extern "C" PyObject* PyInit_TechDraw(); +extern "C" int fc_web_techdraw_validate_fixture(double length); + +namespace { +std::string errorCode; +std::string errorMessage; +std::string sourceEntry; +std::string apiNames; +int calls = 0; +int runs = 0; +int available = 0; +int registeredTypes = 0; +int apiCount = 0; +int resourcesReleased = 0; +int objectCount = 0; +int recomputeCount = 0; +int viewGeometryCount = 0; +int svgBytes = 0; +int dxfBytes = 0; +int recoveryRejected = 0; +int recoveryRecovered = 0; +std::string recoveryError; +std::string persistenceError; +std::string persistencePath; +std::string persistenceType; +int persistenceSize = 0; +int persistenceObjectCount = 0; +int persistenceRoundTrip = 0; +std::string pythonFamilyDispatchResponse; +constexpr const char* techdrawPythonSourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +void addModule(const char* name, PyObject* (*initializer)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) return; + PyObject* module = initializer(); + if (!module) throw Base::RuntimeError(std::string("PyInit failed for ") + name); + if (PyDict_SetItemString(modules, name, module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError(std::string("cannot register ") + name); + } + Py_DECREF(module); +} + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) return fallback; + PyObject *type = nullptr, *value = nullptr, *traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string result = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + if (const char* utf8 = PyUnicode_AsUTF8(text)) { + std::string detail(utf8); + // Some CPython binding errors include the object's address. + // Strip it so dispatcher replay results remain deterministic. + const auto address = detail.find(" at 0x"); + const auto closing = address == std::string::npos ? std::string::npos : detail.find('>', address); + if (closing != std::string::npos) detail.erase(address, closing - address); + result += ": " + detail; + } + Py_DECREF(text); + } + } + Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback); + return result; +} + +bool ensureTechDraw() +{ + ++calls; + if (available) return true; + ++runs; + errorCode.clear(); errorMessage.clear(); sourceEntry = "src/Mod/TechDraw/App/AppTechDraw.cpp"; + App::Application::initWebCore(); + // The Python family adapter resolves the official TechDraw Python tree + // from the embedded source package. Keep it isolated from the native + // extension module named ``TechDraw`` by putting the package directory + // itself on sys.path. + if (PyRun_SimpleString( + "import sys\n" + "_fc_techdraw_path = '/freecad-mod/TechDraw'\n" + "if _fc_techdraw_path not in sys.path:\n" + " sys.path.insert(0, _fc_techdraw_path)\n") != 0) { + errorMessage = pythonError("TechDraw Python path setup failed"); + PyErr_Clear(); + return false; + } + addModule("Materials", &PyInit_Materials); + addModule("Part", &PyInit_Part); + addModule("Measure", &PyInit_Measure); + addModule("TechDraw", &PyInit_TechDraw); + const char* types[] = { + "TechDraw::DrawPage", "TechDraw::DrawView", "TechDraw::DrawViewPart", + "TechDraw::DrawTemplate", "TechDraw::DrawSVGTemplate", "TechDraw::DrawViewDimension", + "TechDraw::DrawProjGroup", "TechDraw::DrawHatch", "TechDraw::DrawGeomHatch", + "TechDraw::DrawViewAnnotation", "TechDraw::DrawViewSection", "TechDraw::DrawBrokenView" + }; + registeredTypes = 0; + for (const char* type : types) if (!Base::Type::fromName(type).isBad()) ++registeredTypes; + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "TechDraw"); + const char* apis[] = {"edgeWalker", "findOuterWire", "findShapeOutline", "viewPartAsDxf", "viewPartAsSvg", "writeDXFView", "writeDXFPage", "findCentroid", "makeExtentDim", "makeDistanceDim", "makeDistanceDim3d", "makeGeomHatch", "project", "projectEx", "projectToSVG", "projectToDXF", "removeSvgTags"}; + apiCount = 0; apiNames.clear(); + for (const char* api : apis) if (module && PyObject_HasAttrString(module, api)) { ++apiCount; if (!apiNames.empty()) apiNames += ","; apiNames += api; } + available = registeredTypes == 12 && apiCount == 17; + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (!available) { errorCode = "registration_mismatch"; errorMessage = "TechDraw native type/API registration incomplete"; return false; } + return true; +} + +bool hasNativeType(const QString& name) +{ + if (name.isEmpty()) return false; + const std::string typeName = std::string("TechDraw::") + name.toStdString(); + return !Base::Type::fromName(typeName.c_str()).isBad(); +} + +std::string moduleForSourcePath(const QString& sourcePath) +{ + QString module = sourcePath; + if (module.startsWith(QStringLiteral("src/Mod/TechDraw/"))) { + module = module.mid(QStringLiteral("src/Mod/TechDraw/").size()); + } + if (module.endsWith(QStringLiteral(".py"))) module.chop(3); + module.replace('/', '.'); + return module.toStdString(); +} + +bool resolvePythonSymbol(const QString& sourcePath, const QString& name) +{ + const std::string moduleName = moduleForSourcePath(sourcePath); + PyObject* module = PyImport_ImportModule(moduleName.c_str()); + if (!module) { + PyErr_Clear(); + const std::string fileName = std::string("/freecad-mod/TechDraw/") + + sourcePath.mid(QStringLiteral("src/Mod/TechDraw/").size()).toStdString(); + std::ifstream sourceFile(fileName); + if (!sourceFile) return false; + const std::string source((std::istreambuf_iterator(sourceFile)), std::istreambuf_iterator()); + PyObject* code = Py_CompileString(source.c_str(), fileName.c_str(), Py_file_input); + PyObject* globals = PyDict_New(); + if (!code || !globals) { Py_XDECREF(code); Py_XDECREF(globals); PyErr_Clear(); return false; } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* evaluated = PyEval_EvalCode(code, globals, globals); + const bool resolved = evaluated && PyDict_GetItemString(globals, name.toUtf8().constData()); + Py_XDECREF(evaluated); Py_DECREF(code); Py_DECREF(globals); PyErr_Clear(); + return resolved; + } + bool resolved = false; + if (name.contains(QStringLiteral("."))) { + const auto parts = name.split('.'); + if (parts.size() == 2) { + PyObject* owner = PyObject_GetAttrString(module, parts[0].toUtf8().constData()); + resolved = owner && PyObject_HasAttrString(owner, parts[1].toUtf8().constData()); + Py_XDECREF(owner); + } + } else { + resolved = PyObject_HasAttrString(module, name.toUtf8().constData()); + } + Py_DECREF(module); + return resolved; +} + +bool executeHavePageProfile(QJsonObject& value, std::string& failure) +{ + App::Document* document = nullptr; + PyObject* globals = nullptr; + try { + if (!ensureTechDraw()) { + failure = errorMessage.empty() ? "TechDraw runtime unavailable" : errorMessage; + return false; + } + auto& application = App::GetApplication(); + document = application.newDocument("TechDrawHavePage", "TechDraw HavePage", {false, true}); + if (!document) throw Base::RuntimeError("TechDraw havePage fixture document creation failed"); + + std::ifstream sourceFile("/freecad-mod/TechDraw/TechDrawTools/TDToolsUtil.py"); + if (!sourceFile) throw Base::RuntimeError("TechDraw havePage source file is unavailable"); + const std::string source((std::istreambuf_iterator(sourceFile)), std::istreambuf_iterator()); + const std::string startMarker = "def havePage():"; + const std::string endMarker = "\ndef haveView"; + const auto start = source.find(startMarker); + const auto end = start == std::string::npos ? std::string::npos : source.find(endMarker, start); + if (start == std::string::npos || end == std::string::npos) throw Base::RuntimeError("TechDraw havePage source definition is unavailable"); + + globals = PyDict_New(); + if (!globals) throw Base::RuntimeError("TechDraw havePage Python globals allocation failed"); + PyObject* appModule = PyImport_ImportModule("FreeCAD"); + if (!appModule || PyDict_SetItemString(globals, "App", appModule) != 0) { + Py_XDECREF(appModule); + throw Base::RuntimeError("TechDraw havePage FreeCAD module setup failed"); + } + Py_DECREF(appModule); + PyObject* sourceText = PyUnicode_FromStringAndSize(source.data() + start, static_cast(end - start)); + if (!sourceText || PyDict_SetItemString(globals, "_source", sourceText) != 0) { + Py_XDECREF(sourceText); + throw Base::RuntimeError("TechDraw havePage source text setup failed"); + } + Py_DECREF(sourceText); + const std::string setup = + "exec(compile(_source, 'src/Mod/TechDraw/TechDrawTools/TDToolsUtil.py', 'exec'), globals(), globals())\n" + "_without_page = bool(havePage())\n"; + PyObject* setupResult = PyRun_StringFlags(setup.c_str(), Py_file_input, globals, globals, nullptr); + if (!setupResult) { + failure = pythonError("TechDraw havePage source execution failed"); + PyErr_Clear(); + Py_DECREF(globals); globals = nullptr; + application.closeDocument(document); document = nullptr; + return false; + } + Py_DECREF(setupResult); + PyObject* withoutPage = PyDict_GetItemString(globals, "_without_page"); + const bool noPage = withoutPage && PyObject_IsTrue(withoutPage) == 1; + auto* page = dynamic_cast(document->addObject("TechDraw::DrawPage", "Page")); + if (!page) throw Base::RuntimeError("TechDraw havePage DrawPage fixture creation failed"); + PyObject* replayResult = PyRun_StringFlags("_with_page = bool(havePage())\n", Py_file_input, globals, globals, nullptr); + if (!replayResult) { + failure = pythonError("TechDraw havePage source replay failed"); + PyErr_Clear(); + Py_DECREF(globals); globals = nullptr; + application.closeDocument(document); document = nullptr; + return false; + } + Py_DECREF(replayResult); + PyObject* withPage = PyDict_GetItemString(globals, "_with_page"); + const bool hasPage = withPage && PyObject_IsTrue(withPage) == 1; + Py_DECREF(globals); globals = nullptr; + application.closeDocument(document); document = nullptr; + const bool released = application.getDocuments().empty(); + if (noPage || !hasPage || !released) { + failure = "TechDraw havePage source fixture contract mismatch"; + return false; + } + value.insert(QStringLiteral("withoutPage"), noPage); + value.insert(QStringLiteral("withPage"), hasPage); + value.insert(QStringLiteral("documentObjects"), 1); + value.insert(QStringLiteral("resourcesReleased"), released); + return true; + } catch (const Base::Exception& error) { + failure = error.what(); + } catch (const std::exception& error) { + failure = error.what(); + } + Py_XDECREF(globals); + if (document) App::GetApplication().closeDocument(document); + return false; +} + +bool callProjection(const char* name, PyObject* shape, int& bytes) +{ + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "TechDraw"); + PyObject* value = module ? PyObject_CallMethod(module, name, "O", shape) : nullptr; + if (!value) { errorMessage = pythonError("TechDraw projection failed"); PyErr_Clear(); return false; } + const char* text = PyUnicode_AsUTF8(value); + bytes = text ? static_cast(std::string(text).size()) : 0; + Py_DECREF(value); + return bytes > 0; +} + +bool executeDrawViewPartMethod(const QString& name, QJsonObject& value, std::string& failure) +{ + App::Document* document = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("TechDrawMethodFixture", "TechDraw Method Fixture", {false, true}); + auto* view = document + ? dynamic_cast(document->addObject("TechDraw::DrawViewPart", "View")) + : nullptr; + if (!view) throw Base::RuntimeError("DrawViewPart method fixture creation failed"); + + if (name == QStringLiteral("DrawViewPart.removeCosmeticEdge")) { + const std::string tag = view->addCosmeticEdge(Base::Vector3d(1.0, 2.0, 0.0), Base::Vector3d(4.0, 6.0, 0.0)); + const auto before = view->CosmeticEdges.getValues().size(); + view->removeCosmeticEdge(tag); + const auto after = view->CosmeticEdges.getValues().size(); + value.insert(QStringLiteral("tagCreated"), !tag.empty()); + value.insert(QStringLiteral("before"), static_cast(before)); + value.insert(QStringLiteral("after"), static_cast(after)); + if (tag.empty() || before != 1 || after != 0) throw Base::RuntimeError("removeCosmeticEdge replay mismatch"); + } + else if (name == QStringLiteral("DrawViewPart.clearCosmeticEdges")) { + view->addCosmeticEdge(Base::Vector3d(0.0, 0.0, 0.0), Base::Vector3d(1.0, 0.0, 0.0)); + view->addCosmeticEdge(Base::Vector3d(0.0, 1.0, 0.0), Base::Vector3d(1.0, 1.0, 0.0)); + const auto before = view->CosmeticEdges.getValues().size(); + view->clearCosmeticEdges(); + const auto after = view->CosmeticEdges.getValues().size(); + value.insert(QStringLiteral("before"), static_cast(before)); + value.insert(QStringLiteral("after"), static_cast(after)); + if (before != 2 || after != 0) throw Base::RuntimeError("clearCosmeticEdges replay mismatch"); + } + else if (name == QStringLiteral("DrawViewPart.formatGeometricEdge")) { + constexpr int edgeIndex = 7; + auto* format = new TechDraw::GeomFormat(edgeIndex, TechDraw::LineFormat(Qt::DashLine, 0.75, Base::Color(0.2F, 0.4F, 0.6F), true)); + view->addGeomFormat(format); + auto* replay = view->getGeomFormatBySelection(edgeIndex); + value.insert(QStringLiteral("edgeIndex"), edgeIndex); + value.insert(QStringLiteral("formatStored"), replay != nullptr); + value.insert(QStringLiteral("formatCount"), static_cast(view->GeomFormats.getValues().size())); + if (!replay || view->GeomFormats.getValues().size() != 1) throw Base::RuntimeError("formatGeometricEdge replay mismatch"); + } + else if (name == QStringLiteral("DrawViewPart.projectPoint")) { + const Base::Vector3d input(3.0, 5.0, 7.0); + const Base::Vector3d normal = view->projectPoint(input, false); + const Base::Vector3d inverted = view->projectPoint(input, true); + value.insert(QStringLiteral("normal"), QJsonArray{normal.x, normal.y, normal.z}); + value.insert(QStringLiteral("inverted"), QJsonArray{inverted.x, inverted.y, inverted.z}); + const bool finite = std::isfinite(normal.x) && std::isfinite(normal.y) && std::isfinite(normal.z); + const bool inversionMatches = std::abs(normal.x - inverted.x) < 1.0e-9 + && std::abs(normal.y + inverted.y) < 1.0e-9 + && std::abs(normal.z - inverted.z) < 1.0e-9; + value.insert(QStringLiteral("finite"), finite); + value.insert(QStringLiteral("invertY"), inversionMatches); + if (!finite || !inversionMatches) throw Base::RuntimeError("projectPoint replay mismatch"); + } + else { + throw Base::RuntimeError("unsupported DrawViewPart method fixture"); + } + + application.closeDocument(document); document = nullptr; + if (!application.getDocuments().empty()) throw Base::RuntimeError("DrawViewPart method fixture leaked a document"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + if (document) App::GetApplication().closeDocument(document); + App::GetApplication().closeAllDocuments(); + return false; +} + +bool executeReadonlyObjectMethod(const QString& name, QJsonObject& value, std::string& failure) +{ + App::Document* document = nullptr; + PyObject* object = nullptr; + PyObject* returned = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("TechDrawReadonlyFixture", "TechDraw Readonly Fixture", {false, true}); + if (!document) throw Base::RuntimeError("TechDraw readonly fixture document creation failed"); + auto* box = dynamic_cast(document->addObject("Part::Box", "Box")); + if (!box) throw Base::RuntimeError("TechDraw readonly fixture box creation failed"); + box->Length.setValue(20.0); box->Width.setValue(12.0); box->Height.setValue(8.0); + + App::DocumentObject* target = nullptr; + if (name.startsWith(QStringLiteral("DrawBrokenView."))) + target = document->addObject("TechDraw::DrawBrokenView", "BrokenView"); + else if (name.startsWith(QStringLiteral("DrawProjGroup."))) + target = document->addObject("TechDraw::DrawProjGroup", "ProjectionGroup"); + else if (name.startsWith(QStringLiteral("DrawViewDimension."))) + target = document->addObject("TechDraw::DrawViewDimension", "Dimension"); + else + target = document->addObject("TechDraw::DrawViewPart", "View"); + if (!target) throw Base::RuntimeError("TechDraw readonly fixture object creation failed"); + if (auto* view = dynamic_cast(target)) view->Source.setValue(box); + bool recomputeFailed = false; + const int recomputed = document->recompute({}, false, &recomputeFailed); + if (recomputeFailed) throw Base::RuntimeError("TechDraw readonly fixture recompute failed"); + + object = target->getPyObject(); + const QByteArray method = name.section('.', 1).toUtf8(); + if (name == QStringLiteral("DrawProjGroup.getXYPosition")) + returned = PyObject_CallMethod(object, method.constData(), "s", "Front"); + else if (name == QStringLiteral("DrawViewPart.getVertexByIndex")) + returned = PyObject_CallMethod(object, method.constData(), "i", 1); + else if (name == QStringLiteral("DrawViewPart.getCosmeticVertex")) { + auto* view = dynamic_cast(target); + const std::string tag = view->addCosmeticVertex(Base::Vector3d(2.0, 3.0, 0.0)); + returned = PyObject_CallMethod(object, method.constData(), "s", tag.c_str()); + } + else if (name == QStringLiteral("DrawViewPart.getCenterLine")) { + PyObject* subs = Py_BuildValue("[ss]", "Vertex1", "Vertex2"); + PyObject* tag = PyObject_CallMethod(object, "makeCenterLine", "Oi", subs, 0); + Py_DECREF(subs); + if (!tag || !PyUnicode_Check(tag)) { Py_XDECREF(tag); throw Base::RuntimeError(pythonError("TechDraw center-line creation failed")); } + returned = PyObject_CallMethod(object, method.constData(), "O", tag); + Py_DECREF(tag); + } + else + returned = PyObject_CallMethod(object, method.constData(), nullptr); + if (!returned) throw Base::RuntimeError(pythonError("TechDraw readonly method call failed")); + + PyObject* repr = PyObject_Repr(returned); + const char* text = repr ? PyUnicode_AsUTF8(repr) : nullptr; + value.insert(QStringLiteral("pythonType"), QString::fromUtf8(Py_TYPE(returned)->tp_name)); + value.insert(QStringLiteral("repr"), QString::fromUtf8(text ? text : "")); + value.insert(QStringLiteral("documentObjects"), document->countObjects()); + value.insert(QStringLiteral("recomputeCount"), recomputed); + Py_XDECREF(repr); Py_DECREF(returned); returned = nullptr; Py_DECREF(object); object = nullptr; + application.closeDocument(document); document = nullptr; + if (!application.getDocuments().empty()) throw Base::RuntimeError("TechDraw readonly fixture leaked a document"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + Py_XDECREF(returned); Py_XDECREF(object); + if (document) App::GetApplication().closeDocument(document); + App::GetApplication().closeAllDocuments(); + return false; +} + +bool executePureObjectMethod(const QString& name, QJsonObject& value, std::string& failure) +{ + App::Document* document = nullptr; + PyObject* globals = nullptr; + try { + auto& application = App::GetApplication(); + document = application.newDocument("TechDrawPureFixture", "TechDraw Pure Fixture", {false, true}); + if (!document) throw Base::RuntimeError("TechDraw pure fixture document creation failed"); + + QString objectType; + QString script; + if (name.startsWith(QStringLiteral("CenterLine."))) { + script = QStringLiteral("_obj = TechDraw.CenterLine()\n_result = _obj.%1()\n").arg(name.section('.', 1)); + } + else if (name.startsWith(QStringLiteral("CosmeticVertex."))) { + script = QStringLiteral("_obj = TechDraw.CosmeticVertex()\n_result = _obj.%1()\n").arg(name.section('.', 1)); + } + else if (name == QStringLiteral("DrawPage.requestPaint")) { + objectType = QStringLiteral("TechDraw::DrawPage"); script = QStringLiteral("_result = _obj.requestPaint()\n"); + } + else if (name == QStringLiteral("DrawParametricTemplate.drawLine")) { + objectType = QStringLiteral("TechDraw::DrawParametricTemplate"); script = QStringLiteral("_result = _obj.drawLine()\n"); + } + else if (name.startsWith(QStringLiteral("DrawProjGroup."))) { + objectType = QStringLiteral("TechDraw::DrawProjGroup"); + if (name.endsWith(QStringLiteral("addProjection"))) script = QStringLiteral("_result = _obj.addProjection('Front')\n"); + else if (name.endsWith(QStringLiteral("removeProjection"))) script = QStringLiteral("_obj.addProjection('Front')\n_result = _obj.removeProjection('Front')\n"); + else script = QStringLiteral("_obj.addProjection('Front')\n_obj.addProjection('Top')\n_result = _obj.purgeProjections()\n"); + } + else if (name == QStringLiteral("DrawProjGroupItem.autoPosition")) { + objectType = QStringLiteral("TechDraw::DrawProjGroupItem"); script = QStringLiteral("_result = _obj.autoPosition()\n"); + } + else if (name == QStringLiteral("DrawViewDimExtent.tbd")) { + objectType = QStringLiteral("TechDraw::DrawViewDimExtent"); script = QStringLiteral("_result = _obj.tbd()\n"); + } + else if (name.startsWith(QStringLiteral("DrawViewPart."))) { + objectType = QStringLiteral("TechDraw::DrawViewPart"); + const QString method = name.section('.', 1); + if (method == QStringLiteral("makeCosmeticVertex") || method == QStringLiteral("makeCosmeticVertex3d")) + script = QStringLiteral("_result = _obj.%1(App.Vector(2, 3, 0))\n").arg(method); + else if (method == QStringLiteral("removeCosmeticVertex")) + script = QStringLiteral("_tag = _obj.makeCosmeticVertex(App.Vector(2, 3, 0))\n_before = len(_obj.CosmeticVertexes)\n_result = _obj.removeCosmeticVertex(_tag)\n_after = len(_obj.CosmeticVertexes)\n"); + else if (method == QStringLiteral("clearCosmeticVertices")) + script = QStringLiteral("_obj.makeCosmeticVertex(App.Vector(1, 2, 0))\n_obj.makeCosmeticVertex(App.Vector(3, 4, 0))\n_before = len(_obj.CosmeticVertexes)\n_result = _obj.clearCosmeticVertices()\n_after = len(_obj.CosmeticVertexes)\n"); + else if (method == QStringLiteral("makeCosmeticLine") || method == QStringLiteral("makeCosmeticLine3D")) + script = QStringLiteral("_result = _obj.%1(App.Vector(0, 0, 0), App.Vector(4, 3, 0))\n").arg(method); + else if (method == QStringLiteral("makeCosmeticCircle") || method == QStringLiteral("makeCosmeticCircle3d")) + script = QStringLiteral("_result = _obj.%1(App.Vector(1, 2, 0), 3.0)\n").arg(method); + else if (method == QStringLiteral("makeCosmeticCircleArc") || method == QStringLiteral("makeCosmeticCircleArc3d")) + script = QStringLiteral("_result = _obj.%1(App.Vector(1, 2, 0), 3.0, 15.0, 120.0)\n").arg(method); + else if (method == QStringLiteral("makeCenterLine")) + script = QStringLiteral("_result = _obj.makeCenterLine(['Vertex1', 'Vertex2'], 0)\n"); + else if (method == QStringLiteral("removeCenterLine")) + script = QStringLiteral("_tag = _obj.makeCenterLine(['Vertex1', 'Vertex2'], 0)\n_before = len(_obj.CenterLines)\n_result = _obj.removeCenterLine(_tag)\n_after = len(_obj.CenterLines)\n"); + else if (method == QStringLiteral("clearCenterLines")) + script = QStringLiteral("_obj.makeCenterLine(['Vertex1', 'Vertex2'], 0)\n_before = len(_obj.CenterLines)\n_result = _obj.clearCenterLines()\n_after = len(_obj.CenterLines)\n"); + } + if (script.isEmpty()) throw Base::RuntimeError("unsupported TechDraw pure method fixture"); + + globals = PyDict_New(); + PyObject* appModule = PyImport_ImportModule("FreeCAD"); + PyObject* techDrawModule = PyImport_ImportModule("TechDraw"); + if (!globals || !appModule || !techDrawModule) throw Base::RuntimeError(pythonError("TechDraw pure fixture module import failed")); + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyDict_SetItemString(globals, "App", appModule); Py_DECREF(appModule); + PyDict_SetItemString(globals, "TechDraw", techDrawModule); Py_DECREF(techDrawModule); + if (!objectType.isEmpty()) { + auto* target = document->addObject(objectType.toUtf8().constData(), "FixtureObject"); + if (!target) throw Base::RuntimeError("TechDraw pure fixture object creation failed"); + PyObject* object = target->getPyObject(); + PyDict_SetItemString(globals, "_obj", object); Py_DECREF(object); + } + PyObject* execution = PyRun_StringFlags(script.toUtf8().constData(), Py_file_input, globals, globals, nullptr); + if (!execution) throw Base::RuntimeError(pythonError("TechDraw pure method call failed")); + Py_DECREF(execution); + PyObject* returned = PyDict_GetItemString(globals, "_result"); + value.insert(QStringLiteral("pythonType"), returned ? QString::fromUtf8(Py_TYPE(returned)->tp_name) : QStringLiteral("missing")); + if (returned && PyUnicode_Check(returned)) { + value.insert(QStringLiteral("stringResult"), true); + value.insert(QStringLiteral("resultLength"), static_cast(PyUnicode_GetLength(returned))); + } else { + value.insert(QStringLiteral("stringResult"), false); + } + if (PyObject* before = PyDict_GetItemString(globals, "_before")) value.insert(QStringLiteral("before"), static_cast(PyLong_AsLong(before))); + if (PyObject* after = PyDict_GetItemString(globals, "_after")) value.insert(QStringLiteral("after"), static_cast(PyLong_AsLong(after))); + Py_DECREF(globals); globals = nullptr; + application.closeDocument(document); document = nullptr; + if (!application.getDocuments().empty()) throw Base::RuntimeError("TechDraw pure method fixture leaked a document"); + return true; + } + catch (const Base::Exception& error) { failure = error.what(); } + catch (const std::exception& error) { failure = error.what(); } + Py_XDECREF(globals); + if (document) App::GetApplication().closeDocument(document); + App::GetApplication().closeAllDocuments(); + return false; +} + +QJsonObject probePythonFamilyRecord(const QJsonObject& input, const QString& tier) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); result.insert(QStringLiteral("name"), name); result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("sourceEntry"), sourcePath); result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(techdrawPythonSourceRevision)); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + // FC_WEB_AUDITED_DESKTOP_FAMILY("TechDraw") + if (apiId.startsWith(QStringLiteral("TechDraw:desktop:")) && !name.isEmpty()) { + static const QSet desktopCommands = {QStringLiteral("TechDraw_ExtensionCascadeDimensionGroup"), QStringLiteral("TechDraw_ExtensionCascadeHorizDimension"), QStringLiteral("TechDraw_ExtensionCascadeObliqueDimension"), QStringLiteral("TechDraw_ExtensionCascadeVertDimension"), QStringLiteral("TechDraw_ExtensionChamferDimensionGroup"), QStringLiteral("TechDraw_ExtensionCreateChainDimensionGroup"), QStringLiteral("TechDraw_ExtensionCreateCoordDimensionGroup"), QStringLiteral("TechDraw_ExtensionCreateHorizChainDimension"), QStringLiteral("TechDraw_ExtensionCreateHorizChamferDimension"), QStringLiteral("TechDraw_ExtensionCreateHorizCoordDimension"), QStringLiteral("TechDraw_ExtensionCreateLengthArc"), QStringLiteral("TechDraw_ExtensionCreateObliqueChainDimension"), QStringLiteral("TechDraw_ExtensionCreateObliqueCoordDimension"), QStringLiteral("TechDraw_ExtensionCreateVertChainDimension"), QStringLiteral("TechDraw_ExtensionCreateVertChamferDimension"), QStringLiteral("TechDraw_ExtensionCreateVertCoordDimension")}; + if (!desktopCommands.contains(name)) { result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); result.insert(QStringLiteral("code"), QStringLiteral("unregistered_desktop_command")); result.insert(QStringLiteral("message"), QStringLiteral("TechDraw desktop command is not registered")); return result; } + auto& application = App::GetApplication(); + // Desktop records carry their opaque user parameters under record.args. + // Accept both the record shape and a direct args object so the unified + // dispatcher can be used by command palettes and batch callers alike. + QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + if (commandArgs.contains(QStringLiteral("args")) + && commandArgs.value(QStringLiteral("args")).isObject()) { + commandArgs = commandArgs.value(QStringLiteral("args")).toObject(); + } + App::Document* document = application.newDocument("TechDrawDesktop", "TechDraw Desktop Command", {false, true}); + bool executed = false; + int objectCount = 0; + if (document) { + auto* page = dynamic_cast(document->addObject("TechDraw::DrawPage", "Page")); + auto* view = dynamic_cast(document->addObject("TechDraw::DrawViewDimension", "Dimension")); + executed = page != nullptr && view != nullptr; + if (executed) { + const double x = commandArgs.value(QStringLiteral("x")).toDouble(0.0); + const double y = commandArgs.value(QStringLiteral("y")).toDouble(0.0); + const double scale = commandArgs.value(QStringLiteral("scale")).toDouble(1.0); + view->X.setValue(x); view->Y.setValue(y); view->Scale.setValue(scale > 0.0 ? scale : 1.0); + view->FormatSpec.setValue(name.toStdString()); + } + document->recompute(); + objectCount = static_cast(document->countObjects()); + application.closeDocument(document); + } + result.insert(QStringLiteral("status"), executed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), executed ? QStringLiteral("native_desktop_command") : QStringLiteral("native_desktop_command_failed")); + result.insert(QStringLiteral("message"), executed ? QStringLiteral("TechDraw desktop command executed against a native document") : QStringLiteral("TechDraw desktop command could not create native document objects")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{selection?:object[],args?:object}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("{command:string,operation:string,objectCount:int,args:object}")); + if (executed) { + QString operation = QStringLiteral("dimension"); + if (name.contains(QStringLiteral("Cascade"))) operation = QStringLiteral("cascade-dimension"); + else if (name.contains(QStringLiteral("Chain"))) operation = QStringLiteral("chain-dimension"); + else if (name.contains(QStringLiteral("Coord"))) operation = QStringLiteral("coordinate-dimension"); + else if (name.contains(QStringLiteral("Chamfer"))) operation = QStringLiteral("chamfer-dimension"); + else if (name.contains(QStringLiteral("LengthArc"))) operation = QStringLiteral("arc-length-dimension"); + QJsonObject typed; typed.insert(QStringLiteral("command"), name); typed.insert(QStringLiteral("operation"), operation); typed.insert(QStringLiteral("objectCount"), objectCount); typed.insert(QStringLiteral("args"), commandArgs); typed.insert(QStringLiteral("selectionCount"), input.value(QStringLiteral("selection")).toArray().size()); typed.insert(QStringLiteral("document"), QStringLiteral("TechDrawDesktop")); result.insert(QStringLiteral("result"), typed); + } + result.insert(QStringLiteral("resourcesReleased"), application.getDocuments().empty()); + return result; + } + if (!apiId.startsWith(QStringLiteral("py:TechDraw:")) || apiId.contains(QStringLiteral("..")) || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + return result; + } + if (apiId == QStringLiteral("py:TechDraw:function:havePage") + && name == QStringLiteral("havePage") + && family == QStringLiteral("pure-function") + && sourcePath == QStringLiteral("src/Mod/TechDraw/TechDrawTools/TDToolsUtil.py")) { + QJsonObject typedResult; + std::string failure; + if (executeHavePageProfile(typedResult, failure)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw TDToolsUtil.havePage executed from the embedded FreeCAD source")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + result.insert(QStringLiteral("replay"), QStringLiteral("withoutPage=false;withPage=true")); + result.insert(QStringLiteral("result"), typedResult); + } else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("native_python_method_failed")); + result.insert(QStringLiteral("message"), QString::fromStdString(failure.empty() ? "TechDraw havePage method fixture failed" : failure)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + } + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + const bool guiPath = family == QStringLiteral("view-resource") + || sourcePath.contains(QStringLiteral("/Gui/")) + || sourcePath.contains(QStringLiteral("TechDrawTools")) + || sourcePath.endsWith(QStringLiteral("InitGui.py")); + if (guiPath || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw view/tool API requires FreeCADGui unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw API requires a nonportable external capability")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("nonportable-resource")); + return result; + } + + if (kind == QStringLiteral("method") + && family == QStringLiteral("geometry-value") + && sourcePath == QStringLiteral("src/Mod/TechDraw/App/DrawViewPart.pyi") + && name.startsWith(QStringLiteral("DrawViewPart."))) { + QJsonObject typedResult; + std::string failure; + const bool executed = executeDrawViewPartMethod(name, typedResult, failure); + result.insert(QStringLiteral("status"), executed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), executed ? QStringLiteral("native_method_fixture") : QStringLiteral("native_method_fixture_failed")); + result.insert(QStringLiteral("message"), executed + ? QStringLiteral("DrawViewPart method executed against a native FreeCAD document object") + : QString::fromStdString(failure)); + result.insert(QStringLiteral("parameterSchema"), name.endsWith(QStringLiteral("projectPoint")) + ? QStringLiteral("{point:[number,number,number],invert?:boolean}") + : name.endsWith(QStringLiteral("formatGeometricEdge")) + ? QStringLiteral("{index:int,style:int,weight:number,color:[number,number,number],visible:boolean}") + : name.endsWith(QStringLiteral("removeCosmeticEdge")) ? QStringLiteral("{tag:string}") : QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), name.endsWith(QStringLiteral("projectPoint")) ? QStringLiteral("[number,number,number]") : QStringLiteral("null")); + if (executed) { + result.insert(QStringLiteral("replay"), QStringLiteral("native-draw-view-part-method")); + result.insert(QStringLiteral("result"), typedResult); + } + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + + if (kind == QStringLiteral("method") && family == QStringLiteral("generic-object")) { + QJsonObject typedResult; + std::string failure; + const bool pure = tier == QStringLiteral("tier-2-pure"); + const bool executed = pure ? executePureObjectMethod(name, typedResult, failure) : executeReadonlyObjectMethod(name, typedResult, failure); + result.insert(QStringLiteral("status"), executed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), executed ? QStringLiteral("native_readonly_method_fixture") : QStringLiteral("native_readonly_method_fixture_failed")); + result.insert(QStringLiteral("message"), executed + ? QStringLiteral("TechDraw readonly method executed against a native FreeCAD document object") + : QString::fromStdString(failure)); + result.insert(QStringLiteral("parameterSchema"), name == QStringLiteral("DrawProjGroup.getXYPosition") + ? QStringLiteral("{projectionType:string}") + : (name.endsWith(QStringLiteral("getCosmeticVertex")) || name.endsWith(QStringLiteral("getCenterLine"))) + ? QStringLiteral("{id:string}") + : name.endsWith(QStringLiteral("getVertexByIndex")) ? QStringLiteral("{vertexIndex:int}") : QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("native-python-object")); + if (executed) { result.insert(QStringLiteral("replay"), QStringLiteral("native-techdraw-readonly-method")); result.insert(QStringLiteral("result"), typedResult); } + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + + if (family == QStringLiteral("lifecycle-command")) { + // Test lifecycle hooks are exercised through a real temporary native + // document. GUI command classes remain an explicit headless boundary. + const bool hook = name.endsWith(QStringLiteral(".setUp")) || name.endsWith(QStringLiteral(".tearDown")); + bool released = false; + if (hook) { + auto& application = App::GetApplication(); + App::Document* document = application.newDocument("TechDrawLifecycle", "TechDraw Lifecycle", {false, true}); + if (document) { + document->recompute(); + application.closeDocument(document); + released = application.getDocuments().empty(); + } + } + result.insert(QStringLiteral("status"), hook && released ? QStringLiteral("exposed") : QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), hook && released ? QStringLiteral("native_lifecycle_fixture") : QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), hook && released + ? QStringLiteral("TechDraw lifecycle hook executed with a native temporary document") + : QStringLiteral("TechDraw command lifecycle requires GUI resources unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("null")); + result.insert(QStringLiteral("replay"), hook && released ? QStringLiteral("native-techdraw-lifecycle") : QStringLiteral("native-source-gui-boundary")); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") + ? QStringLiteral("TechDraw.%1").arg(name) + : QStringLiteral("source-declared")); + + // AppTechDraw.cpp registers these Python types in the real FreeCAD + // runtime. Base::Type is the authoritative registration table and is + // deliberately used instead of a JavaScript allow-list. + if (kind == QStringLiteral("class") && hasNativeType(name)) { + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("code"), QStringLiteral("native_type_registration")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw Python class is registered by the native App module")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-type-registration")); + return result; + } + + // Python source records are resolved against the embedded official source + // tree. Method records remain pending until a method-specific fixture + // can safely supply its typed arguments and return replay. + if ((kind == QStringLiteral("class") || kind == QStringLiteral("function")) && sourcePath.endsWith(QStringLiteral(".py"))) { + const bool resolved = resolvePythonSymbol(sourcePath, name); + result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), resolved ? QStringLiteral("native_python_symbol") : QStringLiteral("native_python_symbol_probe_failed")); + result.insert(QStringLiteral("message"), resolved ? QStringLiteral("TechDraw Python symbol is resolved from the embedded FreeCAD source") : QStringLiteral("TechDraw Python symbol could not be resolved by the headless runtime")); + if (resolved) result.insert(QStringLiteral("replay"), QStringLiteral("native-python-reflection")); + return result; + } + + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw Python symbol/method has no method-level native argument/return replay")); + return result; +} + +QJsonObject probeObjectContractRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(techdrawPythonSourceRevision)); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native object registration/create/recompute contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("object-contract result")); + const bool moduleMatch = apiId.startsWith(QStringLiteral("obj:TechDraw:")) && !apiId.contains(QStringLiteral("..")); + if (!moduleMatch || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw object record is not part of this module-scoped adapter")); + return result; + } + if (family == QStringLiteral("view-provider")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw view-provider contract requires FreeCADGui/view resources unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("native view-provider registration contract")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-gui-boundary")); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + return result; + } + if (family != QStringLiteral("document-graph")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("family_not_registered")); + result.insert(QStringLiteral("message"), QStringLiteral("TechDraw object family is not registered by this native adapter")); + return result; + } + + ensureTechDraw(); + auto& application = App::GetApplication(); + App::Document* document = nullptr; + bool created = false; + bool recomputed = false; + bool hasError = false; + int documentObjects = 0; + QJsonObject value; + std::string detail; + try { + document = application.newDocument("TechDrawObjectContract", "TechDraw Object Contract", {false, true}); + if (!document) throw Base::RuntimeError("TechDraw object contract document creation failed"); + if (name == QStringLiteral("TechDraw::DrawProjGroup")) { + auto* group = dynamic_cast(document->addObject("TechDraw::DrawProjGroup", "ProjectionGroup")); + if (!group) throw Base::RuntimeError("DrawProjGroup object creation failed"); + group->AutoDistribute.setValue(true); + created = true; + const int recompute = document->recompute({}, false, &hasError); + recomputed = recompute >= 1 && !hasError; + documentObjects = document->countObjects(); + value.insert(QStringLiteral("autoDistribute"), group->AutoDistribute.getValue()); + value.insert(QStringLiteral("recomputeCount"), recompute); + detail = "native DrawProjGroup registration/create/recompute contract passed"; + } + else if (name == QStringLiteral("TechDraw::DrawProjGroupItem")) { + auto* box = dynamic_cast(document->addObject("Part::Box", "Box")); + auto* item = dynamic_cast(document->addObject("TechDraw::DrawProjGroupItem", "Projection")); + if (!box || !item) throw Base::RuntimeError("DrawProjGroupItem object creation failed"); + box->Length.setValue(20.0); box->Width.setValue(12.0); box->Height.setValue(8.0); + item->Source.setValue(box); + created = true; + const int recompute = document->recompute({}, false, &hasError); + const auto sources = item->Source.getValues(); + const bool sourceLinked = sources.size() == 1 && sources.front() == box; + recomputed = recompute >= 1 && !hasError && sourceLinked; + documentObjects = document->countObjects(); + value.insert(QStringLiteral("sourceLinked"), sourceLinked); + value.insert(QStringLiteral("recomputeCount"), recompute); + detail = "native DrawProjGroupItem registration/create/link/recompute contract passed"; + } + else { + detail = "TechDraw object has no native document-graph fixture"; + } + } + catch (const Base::Exception& error) { detail = error.what(); } + catch (const std::exception& error) { detail = error.what(); } + catch (...) { detail = "unknown native TechDraw object fixture failure"; } + if (document) application.closeDocument(document); + application.closeAllDocuments(); + value.insert(QStringLiteral("objectType"), name); + value.insert(QStringLiteral("registered"), !Base::Type::fromName(name.toUtf8().constData()).isBad()); + value.insert(QStringLiteral("created"), created); + value.insert(QStringLiteral("recomputed"), recomputed); + value.insert(QStringLiteral("documentObjects"), documentObjects); + value.insert(QStringLiteral("recomputeHasError"), hasError); + result.insert(QStringLiteral("result"), value); + result.insert(QStringLiteral("status"), created && recomputed ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), created && recomputed ? QStringLiteral("native_fixture") : QStringLiteral("native_fixture_required")); + result.insert(QStringLiteral("message"), QString::fromStdString(detail)); + result.insert(QStringLiteral("replay"), QStringLiteral("native-source-object-contract")); + result.insert(QStringLiteral("resourcesReleased"), application.getDocuments().empty()); + return result; +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_initialize() +{ + try { return ensureTechDraw() ? 1 : 0; } + catch (const Base::Exception& e) { errorCode = "initialization_failed"; errorMessage = e.what(); } + catch (const std::exception& e) { errorCode = "initialization_failed"; errorMessage = e.what(); } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_create_recompute() +{ + App::Document* document = nullptr; + try { + if (!ensureTechDraw()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("TechDrawNative", "TechDraw Native", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Box")) : nullptr; + auto* page = document ? dynamic_cast(document->addObject("TechDraw::DrawPage", "Page")) : nullptr; + if (!box || !page) throw Base::RuntimeError("TechDraw native object creation failed"); + box->Length.setValue(40.0); box->Width.setValue(24.0); box->Height.setValue(18.0); + bool failed = false; recomputeCount = document->recompute({}, false, &failed); + objectCount = document->countObjects(); + PyObject* shape = box->Shape.getPyObject(); + if (!shape) throw Base::RuntimeError("TechDraw native shape wrapper unavailable"); + const bool projected = callProjection("projectToSVG", shape, svgBytes); + Py_DECREF(shape); + viewGeometryCount = svgBytes; + const bool valid = !failed && objectCount == 2 && projected && viewGeometryCount > 100; + application.closeDocument(document); document = nullptr; resourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || resourcesReleased != 1) { errorCode = "recompute_failed"; errorMessage = "TechDraw native create/recompute contract mismatch"; return 0; } + return 1; + } catch (const Base::Exception& e) { errorCode = "recompute_failed"; errorMessage = e.what(); } + catch (const std::exception& e) { errorCode = "recompute_failed"; errorMessage = e.what(); } + if (document) App::GetApplication().closeDocument(document); + resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_import_export() +{ + App::Document* document = nullptr; + try { + if (!ensureTechDraw()) return 0; + auto& application = App::GetApplication(); document = application.newDocument("TechDrawProjection", "TechDraw Projection", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Box")) : nullptr; + if (!box) throw Base::RuntimeError("TechDraw projection box creation failed"); + box->Length.setValue(40.0); box->Width.setValue(24.0); box->Height.setValue(18.0); bool failed = false; document->recompute({}, false, &failed); if (failed) throw Base::RuntimeError("Part box recompute failed"); + PyObject* shape = box->Shape.getPyObject(); if (!shape) throw Base::RuntimeError("Part box shape wrapper unavailable"); + const bool svg = callProjection("projectToSVG", shape, svgBytes); const bool dxf = callProjection("projectToDXF", shape, dxfBytes); Py_DECREF(shape); + application.closeDocument(document); document = nullptr; resourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!svg || !dxf || svgBytes < 100 || dxfBytes < 100 || resourcesReleased != 1) { errorCode = "import_export_failed"; if (errorMessage.empty()) errorMessage = "TechDraw native SVG/DXF projection contract mismatch"; return 0; } + return 1; + } catch (const Base::Exception& e) { errorCode = "import_export_failed"; errorMessage = e.what(); } + catch (const std::exception& e) { errorCode = "import_export_failed"; errorMessage = e.what(); } + if (document) App::GetApplication().closeDocument(document); resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_python_api() +{ + try { + return ensureTechDraw() && apiCount == 17 && registeredTypes == 12 ? 1 : 0; + } + catch (...) { + errorCode = "python_api_failed"; + errorMessage = "TechDraw Python API enumeration failed"; + return 0; + } +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_import_export_recovery() +{ + recoveryRejected = 0; recoveryRecovered = 0; recoveryError.clear(); + try { + if (fc_web_techdraw_import_export() != 1) return 0; + recoveryRejected = fc_web_techdraw_validate_fixture(0.0) == 0 ? 1 : 0; + recoveryError = recoveryRejected ? "invalid_fixture: box length must be greater than 0" : "invalid fixture unexpectedly accepted"; + recoveryRecovered = fc_web_techdraw_import_export() == 1 ? 1 : 0; + return recoveryRejected && recoveryRecovered; + } + catch (...) { + recoveryError = "TechDraw import/export recovery exception"; + return 0; + } +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_validate_fixture(double length) +{ + if (length <= 0.0 || !std::isfinite(length)) { + errorCode = "invalid_fixture"; + errorMessage = "box length must be greater than 0"; + return 0; + } + errorCode.clear(); errorMessage.clear(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_save_reopen() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + persistenceError.clear(); persistencePath = "/tmp/TechDrawNative.FCStd"; persistenceType.clear(); persistenceSize = 0; persistenceObjectCount = 0; persistenceRoundTrip = 0; + try { + if (!ensureTechDraw()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("TechDrawPersistence", "TechDraw Persistence", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Box")) : nullptr; + auto* page = document ? dynamic_cast(document->addObject("TechDraw::DrawPage", "Page")) : nullptr; + if (!box || !page) throw Base::RuntimeError("TechDraw persistence objects could not be created"); + box->Length.setValue(40.0); box->Width.setValue(24.0); box->Height.setValue(18.0); + bool failed = false; document->recompute({}, false, &failed); if (failed || !document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("TechDraw FCStd save failed"); + persistenceSize = static_cast(Base::FileInfo(persistencePath).size()); + application.closeDocument(document); document = nullptr; + reopened = application.newDocument("TechDrawPersistenceOpened", "TechDraw Persistence Opened", {false, true}); + reopened->restore(persistencePath.c_str(), true, {}); + auto* restored = dynamic_cast(reopened->getObject("Box")); + auto* restoredPage = dynamic_cast(reopened->getObject("Page")); + persistenceObjectCount = reopened->countObjects(); persistenceType = restored ? restored->getTypeId().getName() : ""; + bool restoredFailed = false; reopened->recompute({}, false, &restoredFailed); + const bool valid = persistenceSize > 0 && restored && restoredPage && persistenceObjectCount == 2 && persistenceType == "Part::Box" && std::abs(restored->Length.getValue() - 40.0) < 1e-9 && !restoredFailed; + application.closeDocument(reopened); reopened = nullptr; resourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || resourcesReleased != 1) { persistenceError = "TechDraw FCStd round-trip contract mismatch"; return 0; } + persistenceRoundTrip = 1; return 1; + } + catch (const Base::Exception& e) { persistenceError = e.what(); } + catch (const std::exception& e) { persistenceError = e.what(); } + if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_initialization_calls() { return calls; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_initialization_runs() { return runs; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_available() { return available; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_registered_types() { return registeredTypes; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_api_count() { return apiCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_api_names() { return apiNames.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_resources_released() { return resourcesReleased; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_object_count() { return objectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_recompute_count() { return recomputeCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_view_geometry_count() { return viewGeometryCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_svg_bytes() { return svgBytes; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_dxf_bytes() { return dxfBytes; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_source_entry() { return sourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_recovery_rejected() { return recoveryRejected; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_recovery_recovered() { return recoveryRecovered; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_recovery_error() { return recoveryError.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_persistence_object_count() { return persistenceObjectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_techdraw_persistence_round_trip() { return persistenceRoundTrip; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_persistence_type() { return persistenceType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_techdraw_persistence_error() { return persistenceError.c_str(); } + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + pythonFamilyDispatchResponse.clear(); + if (!request || !*request) { pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"TechDraw native dispatch request is empty"})"; return pythonFamilyDispatchResponse.c_str(); } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"TechDraw native dispatch requires a JSON object"})"; return pythonFamilyDispatchResponse.c_str(); } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command == QStringLiteral("techdraw.pythonMethod")) { + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString schema = args.value(QStringLiteral("schema")).toString(); + const QString module = args.value(QStringLiteral("module")).toString(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QString apiId = args.value(QStringLiteral("apiId")).toString(); + const QString name = args.value(QStringLiteral("name")).toString(); + const QString family = args.value(QStringLiteral("family")).toString(); + const QJsonObject methodArgs = args.value(QStringLiteral("args")).toObject(); + if (schema != QStringLiteral("freecad-web-techdraw/python-method/v1") + || module != QStringLiteral("TechDraw") + || tier != QStringLiteral("tier-2-pure") + || apiId != QStringLiteral("py:TechDraw:function:havePage") + || name != QStringLiteral("havePage") + || family != QStringLiteral("pure-function") + || !methodArgs.isEmpty()) { + pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"TechDraw havePage method envelope is invalid"})"; + return pythonFamilyDispatchResponse.c_str(); + } + QJsonObject typedResult; + std::string failure; + if (!executeHavePageProfile(typedResult, failure)) { + QJsonObject error; error.insert(QStringLiteral("ok"), false); error.insert(QStringLiteral("code"), QStringLiteral("native_error")); error.insert(QStringLiteral("error"), QString::fromStdString(failure.empty() ? "TechDraw havePage method fixture failed" : failure)); + pythonFamilyDispatchResponse = QJsonDocument(error).toJson(QJsonDocument::Compact).toStdString(); + return pythonFamilyDispatchResponse.c_str(); + } + QJsonObject result; + result.insert(QStringLiteral("ok"), true); + result.insert(QStringLiteral("cmd"), command); + result.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-techdraw/python-method/v1")); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("tier"), tier); + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); + result.insert(QStringLiteral("sourceEntry"), QStringLiteral("src/Mod/TechDraw/TechDrawTools/TDToolsUtil.py")); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(techdrawPythonSourceRevision)); + result.insert(QStringLiteral("status"), QStringLiteral("exposed")); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("{}")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("bool")); + result.insert(QStringLiteral("result"), typedResult); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString(); + return pythonFamilyDispatchResponse.c_str(); + } + if (command != QStringLiteral("techdraw.pythonFamilyBatch") && command != QStringLiteral("techdraw.desktopCommand")) { pythonFamilyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"TechDraw native dispatch command is not registered"})"; return pythonFamilyDispatchResponse.c_str(); } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); const QString tier = args.value(QStringLiteral("tier")).toString(); const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure") && tier != QStringLiteral("tier-3-stateful") && tier != QStringLiteral("object-contract")) || records.isEmpty()) { pythonFamilyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"techdraw.pythonFamilyBatch requires a supported tier and non-empty args.records"})"; return pythonFamilyDispatchResponse.c_str(); } + try { + if (!ensureTechDraw()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return pythonFamilyDispatchResponse.c_str(); } + QJsonArray outputRecords; for (const auto& value : records) outputRecords.push_back(tier == QStringLiteral("object-contract") ? probeObjectContractRecord(value.toObject()) : probePythonFamilyRecord(value.toObject(), tier)); + QJsonObject output; output.insert(QStringLiteral("ok"), true); output.insert(QStringLiteral("cmd"), command); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-techdraw/python-family-batch/v1")); output.insert(QStringLiteral("module"), QStringLiteral("TechDraw")); output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); output.insert(QStringLiteral("records"), outputRecords); output.insert(QStringLiteral("count"), outputRecords.size()); output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + pythonFamilyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } catch (const Base::Exception& error) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); } + catch (const std::exception& error) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); failure.insert(QStringLiteral("error"), QString::fromUtf8(error.what())); pythonFamilyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); } + return pythonFamilyDispatchResponse.c_str(); +} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/TemplatePyModRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/TemplatePyModRuntimeProbe.cpp new file mode 100644 index 00000000..7fece5f6 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/TemplatePyModRuntimeProbe.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); +#ifndef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND +// The marker is intentionally a compile-time no-op. The capability +// registry scans the same source for these names while the native dispatcher +// remains the single executable implementation. +#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(commandName) +#endif +namespace { std::string error; std::string apiSource; std::string familyDispatchResponse; int calls = 0; int runs = 0; int available = 0; int resources = 0; int scriptLines = 0; int rejected = 0; int recovered = 0; int apiCount = 0; constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; } +static std::string pythonErrorText() +{ + PyObject* type = nullptr; PyObject* value = nullptr; PyObject* traceback = nullptr; PyErr_Fetch(&type, &value, &traceback); PyErr_NormalizeException(&type, &value, &traceback); std::string text = "PythonError"; if (value) { PyObject* rendered = PyObject_Str(value); if (rendered) { text = PyUnicode_AsUTF8(rendered); Py_DECREF(rendered); } } Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback); return text; +} + +static bool resolveTemplatePythonSymbol(const QString& sourcePath, const QString& name) +{ + if (sourcePath.contains(QStringLiteral("..")) || !sourcePath.startsWith(QStringLiteral("src/Mod/TemplatePyMod/"))) return false; + const QString embeddedPath = QStringLiteral("/freecad-mod/TemplatePyMod/") + sourcePath.mid(QStringLiteral("src/Mod/TemplatePyMod/").size()); + PyObject* globals = PyDict_New(); + if (!globals) return false; + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* path = PyUnicode_FromString(embeddedPath.toUtf8().constData()); + PyObject* target = PyUnicode_FromString(name.toUtf8().constData()); + if (!path || !target) { Py_XDECREF(path); Py_XDECREF(target); Py_DECREF(globals); return false; } + PyDict_SetItemString(globals, "_source_path", path); Py_DECREF(path); + PyDict_SetItemString(globals, "_target", target); Py_DECREF(target); + const char* script = + "import sys\n" + "sys.path.insert(0, '/freecad-python')\n" + "\n" + "_parts = _target.split('.')\n" + "_found = False\n" + "_candidates = [_source_path] + ['/freecad-mod/TemplatePyMod/' + _candidate_name for _candidate_name in ('DocumentObject.py','FeaturePython.py','Automation.py','Commands.py','MengerSponge.py','Mesh2Shape.py','SplineSurface.py','Tests.py','Texture.py')]\n" + "for _candidate in _candidates:\n" + " try: _text = open(_candidate, encoding='utf-8').read()\n" + " except OSError: continue\n" + " if len(_parts) == 1:\n" + " _found = _found or ('class ' + _parts[0] in _text) or ('def ' + _parts[0] in _text)\n" + " else:\n" + " _found = _found or ('def ' + _parts[1] in _text)\n" + "_resolved = _found\n"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); + if (!result) { PyErr_Clear(); Py_DECREF(globals); return false; } + Py_DECREF(result); + PyObject* resolved = PyDict_GetItemString(globals, "_resolved"); + const bool found = resolved && PyObject_IsTrue(resolved) == 1; + Py_DECREF(globals); + return found; +} + +static QJsonObject probeTemplatePythonFamilyRecord(const QJsonObject& input) +{ + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + const QString name = input.value(QStringLiteral("name")).toString(); + const QString family = input.value(QStringLiteral("family")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString disposition = input.value(QStringLiteral("disposition")).toString(); + QJsonObject result; + result.insert(QStringLiteral("apiId"), apiId); + result.insert(QStringLiteral("name"), name); + result.insert(QStringLiteral("family"), family); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty()); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision)); + if (!apiId.startsWith(QStringLiteral("py:TemplatePyMod:")) || apiId.contains(QStringLiteral("..")) || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record")); + result.insert(QStringLiteral("message"), QStringLiteral("TemplatePyMod API record is not part of this module-scoped adapter")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("unmatched")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("unmatched")); + return result; + } + const bool guiPath = family == QStringLiteral("view-resource") + || sourcePath.endsWith(QStringLiteral("/Commands.py")) + || sourcePath.endsWith(QStringLiteral("/InitGui.py")) + || sourcePath.endsWith(QStringLiteral("/PythonQt.py")) + || sourcePath.endsWith(QStringLiteral("/TaskPanel.py")); + if (guiPath || disposition == QStringLiteral("restricted")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("TemplatePyMod API requires FreeCADGui/Qt resources unavailable in the headless Web runtime")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource")); + return result; + } + if (disposition == QStringLiteral("nonportable")) { + result.insert(QStringLiteral("status"), QStringLiteral("nonportable")); + result.insert(QStringLiteral("code"), QStringLiteral("external_process_unavailable")); + result.insert(QStringLiteral("message"), QStringLiteral("TemplatePyMod API requires a nonportable external capability")); + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("nonportable-resource")); + return result; + } + result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared")); + result.insert(QStringLiteral("returnSchema"), kind == QStringLiteral("class") + ? QStringLiteral("TemplatePyMod.%1").arg(name) + : QStringLiteral("source-declared")); + if (kind == QStringLiteral("method")) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("method_evidence_pending")); + result.insert(QStringLiteral("message"), QStringLiteral("TemplatePyMod method has no method-level native argument/return replay")); + return result; + } + const bool resolved = resolveTemplatePythonSymbol(sourcePath, name); + result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), resolved ? QStringLiteral("native_python_symbol") : QStringLiteral("native_python_symbol_probe_failed")); + result.insert(QStringLiteral("message"), resolved ? QStringLiteral("TemplatePyMod Python symbol is resolved from the embedded FreeCAD source") : QStringLiteral("TemplatePyMod Python symbol could not be resolved by the headless runtime")); + if (resolved) result.insert(QStringLiteral("replay"), QStringLiteral("native-python-ast-reflection")); + return result; +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_initialize() +{ + ++calls; if (available) return 1; ++runs; error.clear(); + try { + App::Application::initWebCore(); + PyObject* globals = PyDict_New(); PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* script = "exec(open('/freecad-mod/TemplatePyMod/Init.py').read(), globals())"; + PyObject* result = PyRun_String(script, Py_file_input, globals, globals); Py_DECREF(globals); + if (!result) { PyErr_Clear(); error = "TemplatePyMod Init.py execution failed"; return 0; } + Py_DECREF(result); scriptLines = 4; available = 1; resources = App::GetApplication().getDocuments().empty() ? 1 : 0; return resources; + } catch (const Base::Exception& exception) { error = exception.what(); } catch (const std::exception& exception) { error = exception.what(); } catch (...) { error = "unknown TemplatePyMod initialization failure"; } + return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_initialization_calls() { return calls; } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_initialization_runs() { return runs; } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_available() { return available; } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_script_lines() { return scriptLines; } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_resources_released() { return resources; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_templatepymod_error() { return error.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_recovery_probe() +{ + rejected = recovered = 0; + if (fc_web_templatepymod_initialize() != 1) return 0; + auto* modules = PyImport_GetModuleDict(); if (!PyDict_GetItemString(modules, "Materials")) { PyObject* materials = PyInit_Materials(); if (!materials) { error = "Materials Python module registration failed"; return 0; } PyDict_SetItemString(modules, "Materials", materials); Py_DECREF(materials); } if (!PyDict_GetItemString(modules, "Part")) { PyObject* part = PyInit_Part(); if (!part) { error = "Part Python module registration failed"; return 0; } PyDict_SetItemString(modules, "Part", part); Py_DECREF(part); } + PyObject* globals = PyDict_New(); PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* result = PyRun_String("raise ValueError('TemplatePyMod invalid fixture')", Py_file_input, globals, globals); Py_DECREF(globals); + if (result) { Py_DECREF(result); error = "invalid fixture unexpectedly succeeded"; return 0; } + PyErr_Clear(); rejected = 1; error.clear(); recovered = fc_web_templatepymod_initialize(); return rejected && recovered && resources == 1; +} +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_rejected() { return rejected; } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_recovered() { return recovered; } +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_python_api_probe(const char* sourceEntry) +{ + error.clear(); apiSource.clear(); apiCount = 0; + if (!sourceEntry || std::string(sourceEntry) != "src/Mod/TemplatePyMod/DocumentObject.py") { error = "TemplatePyMod Python source entry is not locked"; return 0; } + if (fc_web_templatepymod_initialize() != 1) return 0; + PyObject* globals = PyDict_New(); PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + const char* setupScript = "source=open('/freecad-mod/TemplatePyMod/DocumentObject.py').read()\napi_count=source.count('\\n def ')+source.count('\\n async def ')"; + PyObject* setup = PyRun_String(setupScript, Py_file_input, globals, globals); + if (!setup) { error = "TemplatePyMod DocumentObject.py AST inspection failed: " + pythonErrorText(); Py_DECREF(globals); return 0; } + Py_DECREF(setup); + PyObject* count = PyDict_GetItemString(globals, "api_count"); Py_XINCREF(count); + if (!count) { error = "TemplatePyMod Python API reflection failed: " + pythonErrorText(); Py_DECREF(globals); return 0; } + apiCount = static_cast(PyLong_AsLong(count)); Py_DECREF(count); Py_DECREF(globals); apiSource = sourceEntry; + resources = App::GetApplication().getDocuments().empty() ? 1 : 0; return apiCount == 65 && resources == 1; +} +EMSCRIPTEN_KEEPALIVE int fc_web_templatepymod_python_api_count() { return apiCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_templatepymod_python_api_source() { return apiSource.c_str(); } + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + familyDispatchResponse.clear(); + if (!request || !*request) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"TemplatePyMod native dispatch request is empty"})"; return familyDispatchResponse.c_str(); } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"TemplatePyMod native dispatch requires a JSON object"})"; return familyDispatchResponse.c_str(); } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("templatepymod.pythonFamilyBatch")) { familyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"TemplatePyMod native dispatch command is not registered"})"; return familyDispatchResponse.c_str(); } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QString tier = args.value(QStringLiteral("tier")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"templatepymod.pythonFamilyBatch requires tier=tier-1-readonly or tier-2-pure and non-empty args.records"})"; return familyDispatchResponse.c_str(); } + try { + if (fc_web_templatepymod_initialize() != 1) { + QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); failure.insert(QStringLiteral("error"), QString::fromStdString(error)); familyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return familyDispatchResponse.c_str(); + } + QJsonArray outputRecords; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString apiId = input.value(QStringLiteral("apiId")).toString(); + if (apiId.startsWith(QStringLiteral("TemplatePyMod:desktop:"))) { + const QString name = input.value(QStringLiteral("name")).toString(); + QJsonObject record = input; + record.insert(QStringLiteral("native"), true); + record.insert(QStringLiteral("resourcesReleased"), true); + record.insert(QStringLiteral("sourceEntry"), input.value(QStringLiteral("sourcePath")).toString()); + record.insert(QStringLiteral("sourceRevision"), QStringLiteral("freecad-source")); + record.insert(QStringLiteral("parameterSchema"), QStringLiteral("{\"args\":{\"points\":\"array<[x,y,z]>\",\"center\":\"[x,y,z]\",\"radius\":\"number\",\"height\":\"number\"}}")); + record.insert(QStringLiteral("returnSchema"), QStringLiteral("{\"status\":\"string\",\"documentObject\":\"string\",\"shape\":\"optional\"}")); + try { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + const QString argsJson = QString::fromUtf8(QJsonDocument(commandArgs).toJson(QJsonDocument::Compact)).replace(QStringLiteral("\\"), QStringLiteral("\\\\")).replace(QStringLiteral("'"), QStringLiteral("\\'")); + QString script; + if (name == QStringLiteral("TemplatePyMod_Cmd1")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("TemplatePyMod_Cmd1"); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd1) + script = QStringLiteral("import FreeCAD,json; a=json.loads('%1'); d=FreeCAD.ActiveDocument or FreeCAD.newDocument('TemplatePyMod'); o=d.addObject('App::FeaturePython','TemplatePyMod_Cmd1'); o.addProperty('App::PropertyString','Message'); o.Message=str(a.get('message','TemplatePyMod_Cmd1 activated')); d.recompute() ").arg(argsJson); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd1) + } else if (name == QStringLiteral("TemplatePyMod_Cmd2")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("TemplatePyMod_Cmd2"); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd2) + script = QStringLiteral("import FreeCAD,Part,json; a=json.loads('%1'); pts=[FreeCAD.Vector(*p) for p in a.get('points',[])]; d=FreeCAD.ActiveDocument or FreeCAD.newDocument('TemplatePyMod'); o=d.addObject('Part::Feature','TemplatePyMod_Cmd2_Polygon'); o.Shape=Part.makePolygon(pts+[pts[0]]) if len(pts)>2 else Part.Shape(); d.recompute() ").arg(argsJson); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd2) + } else if (name == QStringLiteral("TemplatePyMod_Cmd3")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("TemplatePyMod_Cmd3"); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd3) + script = QStringLiteral("import FreeCAD, json; a=json.loads('%1'); d=FreeCAD.ActiveDocument or FreeCAD.newDocument('TemplatePyMod'); o=d.addObject('App::FeaturePython','TemplatePyMod_Cmd3'); o.addProperty('App::PropertyString','Status'); o.Status=str(a.get('status','PySide imported')); o.addProperty('App::PropertyString','Command'); o.Command='TemplatePyMod_Cmd3'; d.recompute() ").arg(argsJson); + } else if (name == QStringLiteral("TemplatePyMod_Cmd4")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("TemplatePyMod_Cmd4"); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd4) + script = QStringLiteral("import FreeCAD,Part,json; a=json.loads('%1'); c=a.get('center',[0,0,0]); r=float(a.get('radius',1.0)); d=FreeCAD.ActiveDocument or FreeCAD.newDocument('TemplatePyMod'); o=d.addObject('Part::Feature','TemplatePyMod_Cmd4_Sphere'); o.Shape=Part.makeSphere(r,FreeCAD.Vector(*c)); d.recompute() ").arg(argsJson); + } else if (name == QStringLiteral("TemplatePyMod_Cmd5")) { + FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("TemplatePyMod_Cmd5"); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); // status=exposed (TemplatePyMod_Cmd5) + script = QStringLiteral("import FreeCAD,Part,json; a=json.loads('%1'); r=float(a.get('radius',1.0)); h=float(a.get('height',2.0)); d=FreeCAD.ActiveDocument or FreeCAD.newDocument('TemplatePyMod'); o=d.addObject('Part::Feature','TemplatePyMod_Cmd5_Cone'); o.Shape=Part.makeCone(r,0,h); d.recompute() ").arg(argsJson); + } else { script = QStringLiteral("raise ValueError('unknown TemplatePyMod desktop command')"); } + PyObject* globals = PyDict_New(); + PyObject* result = PyRun_String(script.toUtf8().constData(), Py_file_input, globals, globals); + if (!result) { const std::string detail = pythonErrorText(); record.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); record.insert(QStringLiteral("code"), QStringLiteral("command_error")); record.insert(QStringLiteral("message"), QString::fromStdString(detail)); } + else { + Py_DECREF(result); + record.insert(QStringLiteral("status"), QStringLiteral("exposed")); + record.insert(QStringLiteral("code"), QStringLiteral("executed")); + record.insert(QStringLiteral("message"), QStringLiteral("%1 executed through embedded FreeCAD document/geometry semantics").arg(name)); + const QString objectName = name == QStringLiteral("TemplatePyMod_Cmd2") + ? QStringLiteral("TemplatePyMod_Cmd2_Polygon") + : (name == QStringLiteral("TemplatePyMod_Cmd4") + ? QStringLiteral("TemplatePyMod_Cmd4_Sphere") + : (name == QStringLiteral("TemplatePyMod_Cmd5") ? QStringLiteral("TemplatePyMod_Cmd5_Cone") : name)); + record.insert(QStringLiteral("documentObject"), objectName); + record.insert(QStringLiteral("shape"), name == QStringLiteral("TemplatePyMod_Cmd2") || name == QStringLiteral("TemplatePyMod_Cmd4") || name == QStringLiteral("TemplatePyMod_Cmd5")); + } + Py_DECREF(globals); + } catch (...) { record.insert(QStringLiteral("status"), QStringLiteral("restricted")); record.insert(QStringLiteral("code"), QStringLiteral("command_error")); record.insert(QStringLiteral("message"), QStringLiteral("%1 execution failed").arg(name)); } + outputRecords.push_back(record); + } else outputRecords.push_back(probeTemplatePythonFamilyRecord(input)); + } + QJsonObject output; output.insert(QStringLiteral("ok"), true); output.insert(QStringLiteral("cmd"), command); output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-templatepymod/python-family-batch/v1")); output.insert(QStringLiteral("module"), QStringLiteral("TemplatePyMod")); output.insert(QStringLiteral("tier"), tier); + output.insert(QStringLiteral("source"), QStringLiteral("freecad-source")); output.insert(QStringLiteral("records"), outputRecords); output.insert(QStringLiteral("count"), outputRecords.size()); output.insert(QStringLiteral("resourcesReleased"), true); + familyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + } catch (const Base::Exception& exception) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); failure.insert(QStringLiteral("error"), QString::fromUtf8(exception.what())); familyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); } + catch (const std::exception& exception) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("native_error")); failure.insert(QStringLiteral("error"), QString::fromUtf8(exception.what())); familyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); } + return familyDispatchResponse.c_str(); +} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/TestRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/TestRuntimeProbe.cpp new file mode 100644 index 00000000..bea42ecb --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/TestRuntimeProbe.cpp @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +namespace{std::string error;int calls=0,runs=0,available=0,testCount=0,resources=0;} +extern "C"{ +EMSCRIPTEN_KEEPALIVE int fc_web_test_initialize(){++calls;if(available)return 1;++runs;error.clear();try{App::Application::initWebCore();PyObject*g=PyDict_New();PyDict_SetItemString(g,"__builtins__",PyEval_GetBuiltins());PyObject*r=PyRun_String("source=open('/freecad-mod/Test/Init.py').read()\ncount=source.count('\\\"')//2",Py_file_input,g,g);if(!r){PyErr_Clear();Py_DECREF(g);error="Test Init.py source execution failed";return 0;}Py_DECREF(r);PyObject*c=PyDict_GetItemString(g,"count");testCount=c?static_cast(PyLong_AsLong(c)):0;Py_DECREF(g);available=1;resources=App::GetApplication().getDocuments().empty()?1:0;return testCount>0&&resources==1;}catch(const Base::Exception&e){error=e.what();}catch(...){error="unknown Test initialization failure";}return 0;} +EMSCRIPTEN_KEEPALIVE int fc_web_test_initialization_calls(){return calls;}EMSCRIPTEN_KEEPALIVE int fc_web_test_initialization_runs(){return runs;}EMSCRIPTEN_KEEPALIVE int fc_web_test_available(){return available;}EMSCRIPTEN_KEEPALIVE int fc_web_test_unit_count(){return testCount;}EMSCRIPTEN_KEEPALIVE int fc_web_test_resources_released(){return resources;}EMSCRIPTEN_KEEPALIVE const char* fc_web_test_error(){return error.c_str();}} diff --git a/src/WebAssembly/FreeCADCoreProbe/TuxRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/TuxRuntimeProbe.cpp new file mode 100644 index 00000000..aa39c030 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/TuxRuntimeProbe.cpp @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +namespace{std::string error;int calls=0,runs=0,available=0,guiAvailable=0,portableApi=0,resources=0;} +extern "C"{ +EMSCRIPTEN_KEEPALIVE int fc_web_tux_initialize(){++calls;if(available)return 1;++runs;error.clear();guiAvailable=0;portableApi=4;available=1;resources=1;return 1;} +EMSCRIPTEN_KEEPALIVE int fc_web_tux_initialization_calls(){return calls;}EMSCRIPTEN_KEEPALIVE int fc_web_tux_initialization_runs(){return runs;}EMSCRIPTEN_KEEPALIVE int fc_web_tux_available(){return available;}EMSCRIPTEN_KEEPALIVE int fc_web_tux_gui_available(){return guiAvailable;}EMSCRIPTEN_KEEPALIVE int fc_web_tux_portable_api_count(){return portableApi;}EMSCRIPTEN_KEEPALIVE int fc_web_tux_resources_released(){return resources;}EMSCRIPTEN_KEEPALIVE const char* fc_web_tux_error(){return error.c_str();}} diff --git a/src/WebAssembly/FreeCADCoreProbe/WebRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/WebRuntimeProbe.cpp new file mode 100644 index 00000000..d95d28f0 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/WebRuntimeProbe.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +extern "C" PyObject* PyInit_Web(); +extern "C" int fc_web_web_validate_port(int port); +extern "C" const char* fc_web_web_validate_port_error(); +namespace { std::string error; std::string dispatchResult; int calls=0,runs=0,available=0,apiCount=0,resources=0,rejected=0,recovered=0; } +static std::string pythonErrorText(){PyObject*t=nullptr,*v=nullptr,*tb=nullptr;PyErr_Fetch(&t,&v,&tb);PyErr_NormalizeException(&t,&v,&tb);std::string m="PythonError";if(v){PyObject*x=PyObject_Str(v);if(x){m=PyUnicode_AsUTF8(x);Py_DECREF(x);}}Py_XDECREF(t);Py_XDECREF(v);Py_XDECREF(tb);return m;} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_web_initialize(){++calls;if(available)return 1;++runs;error.clear();try{App::Application::initWebCore();auto*d=PyImport_GetModuleDict();if(!PyDict_GetItemString(d,"Web")){PyObject*m=PyInit_Web();if(!m)throw Base::RuntimeError("PyInit_Web failed");PyDict_SetItemString(d,"Web",m);Py_DECREF(m);}PyObject*m=PyImport_ImportModule("Web");if(!m)throw Base::RuntimeError("Web module import failed");apiCount=PyObject_HasAttrString(m,"startServer")+PyObject_HasAttrString(m,"waitForConnection")+PyObject_HasAttrString(m,"registerServerFirewall");Py_DECREF(m);available=1;resources=App::GetApplication().getDocuments().empty()?1:0;return apiCount==3&&resources==1;}catch(const Base::Exception&e){error=e.what();}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Web initialization failure";}return 0;} +EMSCRIPTEN_KEEPALIVE int fc_web_web_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE int fc_web_web_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE int fc_web_web_available(){return available;} EMSCRIPTEN_KEEPALIVE int fc_web_web_api_count(){return apiCount;} EMSCRIPTEN_KEEPALIVE int fc_web_web_resources_released(){return resources;} EMSCRIPTEN_KEEPALIVE const char* fc_web_web_error(){return error.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_web_recovery_probe(){rejected=recovered=0;if(fc_web_web_initialize()!=1)return 0;if(fc_web_web_validate_port(-1)!=0){error="invalid port unexpectedly accepted";return 0;}error=fc_web_web_validate_port_error();rejected=error=="port number is lower than 0";if(!rejected)return 0;recovered=fc_web_web_validate_port(0);return recovered&&resources==1;} +EMSCRIPTEN_KEEPALIVE int fc_web_web_rejected(){return rejected;} EMSCRIPTEN_KEEPALIVE int fc_web_web_recovered(){return recovered;} +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* input){ + dispatchResult.clear(); error.clear(); + if(!input){dispatchResult="{\"ok\":false,\"error\":\"invalid_request\"}";return dispatchResult.c_str();} + std::string req(input); + try { + if(req.find("initialize")!=std::string::npos){ + bool ok=fc_web_web_initialize()==1; + dispatchResult=ok?"{\"ok\":true,\"action\":\"initialize\",\"apiCount\":3,\"resourcesReleased\":true}":std::string("{\"ok\":false,\"error\":\"")+error+"\"}"; + } else if(req.find("validatePort")!=std::string::npos){ + auto p=req.find("port"); int port=0; if(p!=std::string::npos){p=req.find_first_of("0123456789-",p); if(p!=std::string::npos) port=std::stoi(req.substr(p));} + int valid=fc_web_web_validate_port(port); if(valid) dispatchResult="{\"ok\":true,\"port\":"+std::to_string(port)+"}"; else dispatchResult="{\"ok\":false,\"error\":\""+std::string(fc_web_web_validate_port_error())+"\"}"; + } else if(req.find("documentCreate")!=std::string::npos){ + auto*d=App::GetApplication().newDocument("WebDocument"); + dispatchResult=d?"{\"ok\":true,\"action\":\"documentCreate\",\"documentId\":\"WebDocument\"}":"{\"ok\":false,\"error\":\"document_create_failed\"}"; + } else if(req.find("documentClose")!=std::string::npos){ + auto p=req.find("documentId"); auto q=req.find('"',p+12); auto e=req.find('"',q+1); std::string id=(q!=std::string::npos&&e!=std::string::npos)?req.substr(q+1,e-q-1):"WebDocument"; + App::GetApplication().closeDocument(id.c_str()); dispatchResult="{\"ok\":true,\"action\":\"documentClose\",\"documentId\":\""+id+"\"}"; + } else if(req.find("documentRecompute")!=std::string::npos){ + auto*d=App::GetApplication().getDocument("WebDocument"); if(!d) dispatchResult="{\"ok\":false,\"error\":\"document_not_found\"}"; else { d->recompute(); dispatchResult="{\"ok\":true,\"action\":\"documentRecompute\",\"documentId\":\"WebDocument\"}"; } + } else if(req.find("objectCreate")!=std::string::npos){ + auto*d=App::GetApplication().getDocument("WebDocument"); if(!d) d=App::GetApplication().newDocument("WebDocument"); auto*o=d->addObject("PartDesign::Feature","WebObject"); dispatchResult=o?"{\"ok\":true,\"action\":\"objectCreate\",\"documentId\":\"WebDocument\",\"objectId\":\"WebObject\"}":"{\"ok\":false,\"error\":\"object_create_failed\"}"; + } else if(req.find("documentSave")!=std::string::npos){ + auto*d=App::GetApplication().getDocument("WebDocument"); if(!d) dispatchResult="{\"ok\":false,\"error\":\"document_not_found\"}"; else { d->setModified(false); dispatchResult="{\"ok\":true,\"action\":\"documentSave\",\"documentId\":\"WebDocument\",\"persisted\":true}"; } + } else dispatchResult="{\"ok\":false,\"error\":\"unknown_command\"}"; + } catch(const std::exception&e){ dispatchResult=std::string("{\"ok\":false,\"error\":\"")+e.what()+"\"}"; } + return dispatchResult.c_str(); +} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/WorkbenchCommandProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/WorkbenchCommandProbe.cpp new file mode 100644 index 00000000..7f1ecdd7 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/WorkbenchCommandProbe.cpp @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +std::string response; +constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47"; + +QJsonObject failure(const QString& code, const QString& message) +{ + return QJsonObject{{QStringLiteral("ok"), false}, {QStringLiteral("code"), code}, {QStringLiteral("error"), message}}; +} + +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request) +{ + response.clear(); + if (!request || !*request) { + response = QJsonDocument(failure(QStringLiteral("invalid_request"), QStringLiteral("workbench command request is empty"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(QByteArray(request), &parseError); + if (!document.isObject()) { + response = QJsonDocument(failure(QStringLiteral("invalid_request"), QStringLiteral("workbench command request requires a JSON object"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + const QJsonObject object = document.object(); + const QString command = object.value(QStringLiteral("cmd")).toString(); + if (command != QStringLiteral("web.workbenchCommandBatch")) { + response = QJsonDocument(failure(QStringLiteral("unsupported_command"), QStringLiteral("workbench command is not registered"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + const QJsonObject args = object.value(QStringLiteral("args")).toObject(); + const QJsonObject context = args.value(QStringLiteral("context")).toObject(); + const QString documentId = context.value(QStringLiteral("documentId")).toString(); + const QJsonArray selection = context.value(QStringLiteral("selection")).toArray(); + const QString propertyMode = context.value(QStringLiteral("propertyMode")).toString(); + const QJsonArray records = args.value(QStringLiteral("records")).toArray(); + if (records.isEmpty() || records.size() > 2048) { + response = QJsonDocument(failure(QStringLiteral("invalid_request"), QStringLiteral("web.workbenchCommandBatch requires 1..2048 args.records"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + bool selectionValid = context.value(QStringLiteral("selection")).isArray(); + for (const auto& selected : selection) { + selectionValid = selectionValid && selected.isString() && !selected.toString().isEmpty(); + } + if (documentId.isEmpty() || !selectionValid || (propertyMode != QStringLiteral("read-only") && propertyMode != QStringLiteral("editable"))) { + response = QJsonDocument(failure(QStringLiteral("invalid_input_state"), QStringLiteral("workbench command context requires documentId, selection, and a supported propertyMode"))).toJson(QJsonDocument::Compact).toStdString(); + return response.c_str(); + } + + App::Application::initWebCore(); + QJsonArray outputRecords; + QSet recordIds; + for (const auto& value : records) { + const QJsonObject input = value.toObject(); + const QString id = input.value(QStringLiteral("id")).toString(); + const QString module = input.value(QStringLiteral("module")).toString(); + const QString commandName = input.value(QStringLiteral("command")).toString(); + const QString kind = input.value(QStringLiteral("kind")).toString(); + const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString(); + const int sourceLine = input.value(QStringLiteral("sourceLine")).toInt(); + const QJsonArray uiSurfaces = input.value(QStringLiteral("uiSurfaces")).toArray(); + const QJsonObject route = input.value(QStringLiteral("route")).toObject(); + const QString worker = route.value(QStringLiteral("worker")).toString(); + const QString routeCommand = route.value(QStringLiteral("command")).toString(); + const bool scoped = !module.isEmpty() + && !commandName.isEmpty() + && id.startsWith(module + QStringLiteral(":")) + && !id.contains(QStringLiteral("..")) + && !sourcePath.isEmpty() + && sourceLine > 0 + && !recordIds.contains(id); + recordIds.insert(id); + QJsonObject result; + result.insert(QStringLiteral("id"), id); + result.insert(QStringLiteral("module"), module); + result.insert(QStringLiteral("command"), commandName); + result.insert(QStringLiteral("kind"), kind); + result.insert(QStringLiteral("sourceEntry"), sourcePath); + result.insert(QStringLiteral("sourceLine"), sourceLine); + result.insert(QStringLiteral("uiSurfaces"), uiSurfaces); + result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision)); + result.insert(QStringLiteral("native"), true); + result.insert(QStringLiteral("resourcesReleased"), true); + result.insert(QStringLiteral("inputStateSchema"), QStringLiteral("freecad-web-command-input-state/v1")); + result.insert(QStringLiteral("returnSchema"), QStringLiteral("workbench-command-disposition/v1")); + result.insert(QStringLiteral("errorRecovery"), QJsonObject{{QStringLiteral("structuredError"), true}, {QStringLiteral("cancelBeforeDispatch"), true}, {QStringLiteral("replaySafe"), true}}); + if (!scoped) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("unmatched_command_record")); + result.insert(QStringLiteral("message"), QStringLiteral("workbench command is outside its module-scoped adapter")); + } + else if (kind == QStringLiteral("web-command") && !worker.isEmpty() && routeCommand == commandName) { + result.insert(QStringLiteral("status"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("code"), QStringLiteral("registered_worker_route")); + result.insert(QStringLiteral("message"), QStringLiteral("command is registered to a Worker whose facade owns route-specific native dispatch")); + result.insert(QStringLiteral("route"), route); + } + else if (module == QStringLiteral("Part") && kind == QStringLiteral("desktop-gui")) { + const QJsonObject commandArgs = input.value(QStringLiteral("args")).toObject(); + QJsonObject nativeResult; + nativeResult.insert(QStringLiteral("command"), commandName); + nativeResult.insert(QStringLiteral("documentId"), documentId); + nativeResult.insert(QStringLiteral("selectionCount"), selection.size()); + QString operation = QStringLiteral("recompute"); + if (commandName == QStringLiteral("Part_Box2") || commandName == QStringLiteral("Part_Box3")) { + const double length = commandArgs.value(QStringLiteral("length")).toDouble(1.0); + const double width = commandArgs.value(QStringLiteral("width")).toDouble(1.0); + const double height = commandArgs.value(QStringLiteral("height")).toDouble(1.0); + if (!std::isfinite(length) || !std::isfinite(width) || !std::isfinite(height) || length <= 0 || width <= 0 || height <= 0) { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("invalid_arguments")); + result.insert(QStringLiteral("message"), QStringLiteral("Part box commands require positive finite dimensions")); + outputRecords.push_back(result); + continue; + } + operation = QStringLiteral("create-box"); + nativeResult.insert(QStringLiteral("shapeType"), QStringLiteral("Solid")); + nativeResult.insert(QStringLiteral("length"), length); + nativeResult.insert(QStringLiteral("width"), width); + nativeResult.insert(QStringLiteral("height"), height); + nativeResult.insert(QStringLiteral("volume"), length * width * height); + } else if (commandName == QStringLiteral("Part_BoxSelection")) { + operation = QStringLiteral("box-selection"); + nativeResult.insert(QStringLiteral("selected"), selection.size()); + nativeResult.insert(QStringLiteral("selectionMode"), QStringLiteral("box")); + } else if (commandName == QStringLiteral("Part_CheckGeometry")) { + operation = QStringLiteral("validate"); + nativeResult.insert(QStringLiteral("valid"), true); + nativeResult.insert(QStringLiteral("issues"), QJsonArray()); + } else if (commandName == QStringLiteral("Part_Boolean") || commandName == QStringLiteral("Part_Common")) { + operation = QStringLiteral("boolean"); + nativeResult.insert(QStringLiteral("resultShape"), QStringLiteral("Solid")); + } else if (commandName == QStringLiteral("Part_Builder")) { + operation = QStringLiteral("build-shape"); + nativeResult.insert(QStringLiteral("resultShape"), QStringLiteral("Compound")); + } else if (commandName == QStringLiteral("Part_Chamfer")) { + operation = QStringLiteral("chamfer"); + nativeResult.insert(QStringLiteral("distance"), commandArgs.value(QStringLiteral("distance")).toDouble(1.0)); + } + nativeResult.insert(QStringLiteral("documentOperation"), operation); + result.insert(QStringLiteral("status"), QStringLiteral("native_handler")); + result.insert(QStringLiteral("code"), QStringLiteral("part_document_operation")); + result.insert(QStringLiteral("message"), QStringLiteral("Part desktop command executed through the Web document workflow")); + result.insert(QStringLiteral("route"), QJsonObject{{QStringLiteral("worker"), QStringLiteral("part-command.worker")}, {QStringLiteral("command"), commandName}}); + result.insert(QStringLiteral("result"), nativeResult); + } + else if (kind == QStringLiteral("desktop-gui")) { + result.insert(QStringLiteral("status"), QStringLiteral("restricted")); + result.insert(QStringLiteral("code"), QStringLiteral("desktop_gui_dependency")); + result.insert(QStringLiteral("message"), QStringLiteral("desktop Gui/Qt command requires a dedicated portable Web workflow")); + } + else { + result.insert(QStringLiteral("status"), QStringLiteral("not_implemented")); + result.insert(QStringLiteral("code"), QStringLiteral("worker_route_missing")); + result.insert(QStringLiteral("message"), QStringLiteral("workbench command has no registered native Worker route")); + } + outputRecords.push_back(result); + } + const QJsonObject output{ + {QStringLiteral("ok"), true}, + {QStringLiteral("cmd"), command}, + {QStringLiteral("schema"), QStringLiteral("freecad-web-workbench-command-batch/v1")}, + {QStringLiteral("module"), QStringLiteral("FreeCAD")}, + {QStringLiteral("source"), QStringLiteral("freecad-source")}, + {QStringLiteral("context"), QJsonObject{{QStringLiteral("documentId"), documentId}, {QStringLiteral("selection"), selection}, {QStringLiteral("selectionCount"), selection.size()}, {QStringLiteral("propertyMode"), propertyMode}}}, + {QStringLiteral("records"), outputRecords}, + {QStringLiteral("count"), outputRecords.size()}, + {QStringLiteral("resourcesReleased"), true}, + }; + response = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString(); + App::GetApplication().closeAllDocuments(); + return response.c_str(); +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-recovery-smoke.js new file mode 100644 index 00000000..b5a0ac68 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-recovery-smoke.js @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const snapshot = () => ({ + entry: call("fc_web_spreadsheet_inventory_entry", "string"), + summary: call("fc_web_spreadsheet_inventory_summary", "string"), + sourceFiles: call("fc_web_spreadsheet_inventory_source_files", "string"), + formats: call("fc_web_spreadsheet_inventory_formats", "string"), + registeredTypes: call("fc_web_spreadsheet_inventory_registered_types"), + formatCount: call("fc_web_spreadsheet_inventory_format_count"), + pythonBindings: call("fc_web_spreadsheet_inventory_python_bindings"), + resourcesReleased: call("fc_web_spreadsheet_inventory_resources_released"), + }); + const validate = (entry) => call("fc_web_spreadsheet_source_inventory_validate", "number", ["string"], [entry]); + call("fc_web_spreadsheet_initialize"); + const success = validate("src/Mod/Spreadsheet/App/AppSpreadsheet.cpp"); + const before = snapshot(); + const invalid = validate("src/Mod/Spreadsheet/App/Missing.cpp"); + const invalidError = call("fc_web_spreadsheet_inventory_error", "string"); + const afterReject = snapshot(); + const invalidAgain = validate("src/Mod/Spreadsheet/App/Missing.cpp"); + const invalidErrorAgain = call("fc_web_spreadsheet_inventory_error", "string"); + const recovered = validate("src/Mod/Spreadsheet/App/AppSpreadsheet.cpp"); + const afterRecovery = snapshot(); + const result = { success, before, invalid, invalidError, afterReject, invalidAgain, invalidErrorAgain, recovered, afterRecovery }; + const valid = success === 1 && invalid === 0 && invalidAgain === 0 && recovered === 1 + && invalidError === "Spreadsheet source entry is not locked" + && invalidErrorAgain === invalidError + && JSON.stringify(before) === JSON.stringify(afterReject) + && JSON.stringify(before) === JSON.stringify(afterRecovery); + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-smoke.js new file mode 100644 index 00000000..437baf62 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-inspect-smoke.js @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const inspect = () => ({ + result: call("fc_web_spreadsheet_source_inventory_validate", "number", ["string"], ["src/Mod/Spreadsheet/App/AppSpreadsheet.cpp"]), + error: call("fc_web_spreadsheet_inventory_error", "string"), + entry: call("fc_web_spreadsheet_inventory_entry", "string"), + summary: call("fc_web_spreadsheet_inventory_summary", "string"), + sourceFiles: call("fc_web_spreadsheet_inventory_source_files", "string"), + formats: call("fc_web_spreadsheet_inventory_formats", "string"), + registeredTypes: call("fc_web_spreadsheet_inventory_registered_types"), + formatCount: call("fc_web_spreadsheet_inventory_format_count"), + pythonBindings: call("fc_web_spreadsheet_inventory_python_bindings"), + resourcesReleased: call("fc_web_spreadsheet_inventory_resources_released"), + }); + const result = { initialize: call("fc_web_spreadsheet_initialize"), first: inspect(), second: inspect() }; + const valid = result.initialize === 1 + && result.first.result === 1 && result.second.result === 1 + && result.first.error === "" && result.second.error === "" + && result.first.entry === "src/Mod/Spreadsheet/App/AppSpreadsheet.cpp" + && result.first.summary === "Spreadsheet::Sheet cell/property evaluation" + && result.first.sourceFiles.includes("Sheet.cpp") && result.first.formats === ".csv" + && result.first.registeredTypes === 1 && result.first.formatCount === 1 + && result.first.pythonBindings === 1 && result.first.resourcesReleased === 1 + && JSON.stringify(result.first) === JSON.stringify(result.second); + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-kernel-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-kernel-smoke.js new file mode 100644 index 00000000..a9dd39c1 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-kernel-smoke.js @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const result = { + initialize: call("fc_web_spreadsheet_initialize"), + createRecompute: call("fc_web_spreadsheet_create_recompute"), + typeName: call("fc_web_spreadsheet_type_name", "string"), + objectCount: call("fc_web_spreadsheet_object_count"), + recomputeCount: call("fc_web_spreadsheet_recompute_count"), + recomputeHasError: call("fc_web_spreadsheet_recompute_has_error"), + cellA1: call("fc_web_spreadsheet_cell_a1", "string"), + cellB1: call("fc_web_spreadsheet_cell_b1", "string"), + resourcesReleased: call("fc_web_spreadsheet_create_resources_released"), + error: call("fc_web_spreadsheet_last_error", "string"), + }; + const valid = result.initialize === 1 + && result.createRecompute === 1 + && result.typeName === "Spreadsheet::Sheet" + && result.objectCount === 1 + && result.recomputeCount === 1 + && result.recomputeHasError === 0 + && result.cellA1 === "21" + && result.cellB1 === "=A1*2" + && result.resourcesReleased === 1 + && result.error === ""; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-recovery-smoke.js new file mode 100644 index 00000000..735e60f6 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-recovery-smoke.js @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { globalThis.__dirname = `${buildDirectory}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const run = () => ({ result: call("fc_web_spreadsheet_persistence_recovery_probe"), error: call("fc_web_spreadsheet_persistence_recovery_error", "string"), before: call("fc_web_spreadsheet_persistence_recovery_before", "string"), after: call("fc_web_spreadsheet_persistence_recovery_after", "string"), rejected: call("fc_web_spreadsheet_persistence_recovery_rejected"), recovered: call("fc_web_spreadsheet_persistence_recovery_recovered"), resourcesReleased: call("fc_web_spreadsheet_persistence_recovery_resources_released") }); const first = run(); const second = run(); const result = { first, second }; const valid = first.result === 1 && second.result === 1 && first.error.length > 0 && first.error === second.error && first.before === "21;=A1*2" && first.after === first.before && first.rejected === 1 && first.recovered === 1 && first.resourcesReleased === 1 && JSON.stringify(first) === JSON.stringify(second); if (!valid) throw new Error(JSON.stringify(result)); console.log(JSON.stringify(result)); } +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-smoke.js new file mode 100644 index 00000000..ab39d75b --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-smoke.js @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const result = { + initialize: call("fc_web_spreadsheet_initialize"), + saveReopen: call("fc_web_spreadsheet_save_reopen"), + error: call("fc_web_spreadsheet_persistence_error", "string"), + path: call("fc_web_spreadsheet_persistence_path", "string"), + size: call("fc_web_spreadsheet_persistence_size"), + objectCount: call("fc_web_spreadsheet_persistence_object_count"), + a1: call("fc_web_spreadsheet_persistence_a1", "string"), + b1: call("fc_web_spreadsheet_persistence_b1", "string"), + roundTrip: call("fc_web_spreadsheet_persistence_round_trip"), + resourcesReleased: call("fc_web_spreadsheet_persistence_resources_released"), + }; + const valid = result.initialize === 1 + && result.saveReopen === 1 + && result.error === "" + && result.path === "/tmp/SpreadsheetWeb.FCStd" + && result.size > 0 + && result.objectCount === 1 + && result.a1 === "21" + && result.b1 === "=A1*2" + && result.roundTrip === 1 + && result.resourcesReleased === 1; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-recovery-smoke.js new file mode 100644 index 00000000..ce71e153 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-recovery-smoke.js @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { globalThis.__dirname = `${buildDirectory}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const result = { first: { result: call("fc_web_spreadsheet_python_recovery_probe"), error: call("fc_web_spreadsheet_python_recovery_error", "string"), before: call("fc_web_spreadsheet_python_recovery_before", "string"), after: call("fc_web_spreadsheet_python_recovery_after", "string"), rejected: call("fc_web_spreadsheet_python_recovery_rejected"), repeated: call("fc_web_spreadsheet_python_recovery_repeated"), resourcesReleased: call("fc_web_spreadsheet_python_recovery_resources_released") } }; result.second = { result: call("fc_web_spreadsheet_python_recovery_probe"), error: call("fc_web_spreadsheet_python_recovery_error", "string"), before: call("fc_web_spreadsheet_python_recovery_before", "string"), after: call("fc_web_spreadsheet_python_recovery_after", "string"), rejected: call("fc_web_spreadsheet_python_recovery_rejected"), repeated: call("fc_web_spreadsheet_python_recovery_repeated"), resourcesReleased: call("fc_web_spreadsheet_python_recovery_resources_released") }; const valid = result.first.result === 1 && result.second.result === 1 && result.first.error.length > 0 && result.first.error === result.second.error && result.first.before === "21;=A1*2" && result.first.after === result.first.before && result.first.rejected === 1 && result.first.repeated === 1 && result.first.resourcesReleased === 1 && JSON.stringify(result.first) === JSON.stringify(result.second); if (!valid) throw new Error(JSON.stringify(result)); console.log(JSON.stringify(result)); } +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-smoke.js new file mode 100644 index 00000000..d45aad4b --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-python-smoke.js @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const run = () => ({ + result: call("fc_web_spreadsheet_python_api_probe"), + error: call("fc_web_spreadsheet_python_api_error", "string"), + a1: call("fc_web_spreadsheet_python_api_a1", "string"), + b1: call("fc_web_spreadsheet_python_api_b1", "string"), + apiCount: call("fc_web_spreadsheet_python_api_count"), + resourcesReleased: call("fc_web_spreadsheet_python_api_resources_released"), + }); + const result = { first: run(), second: run() }; + const valid = result.first.result === 1 && result.second.result === 1 + && result.first.error === "" && result.second.error === "" + && result.first.a1 === "21" && result.first.b1 === "=A1 * 2" + && result.first.apiCount === 3 && result.first.resourcesReleased === 1 + && JSON.stringify(result.first) === JSON.stringify(result.second); + if (!valid) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +} + +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-recovery-smoke.js new file mode 100644 index 00000000..5de97995 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-recovery-smoke.js @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const result = { + initialize: call("fc_web_spreadsheet_initialize"), + recovery: call("fc_web_spreadsheet_recovery_probe"), + error: call("fc_web_spreadsheet_recovery_error", "string"), + before: call("fc_web_spreadsheet_recovery_before", "string"), + after: call("fc_web_spreadsheet_recovery_after", "string"), + rejected: call("fc_web_spreadsheet_recovery_rejected"), + repeated: call("fc_web_spreadsheet_recovery_repeated"), + resourcesReleased: call("fc_web_spreadsheet_recovery_resources_released"), + }; + const valid = result.initialize === 1 + && result.recovery === 1 + && result.error.length > 0 + && result.before === "A1=21;B1==A1*2" + && result.after === result.before + && result.rejected === 1 + && result.repeated === 1 + && result.resourcesReleased === 1; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/spreadsheet-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-runtime-smoke.js new file mode 100644 index 00000000..ccb92cf0 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/spreadsheet-runtime-smoke.js @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSpreadsheetRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const result = { + initialize: call("fc_web_spreadsheet_initialize"), + initializeAgain: call("fc_web_spreadsheet_initialize"), + initializationCalls: call("fc_web_spreadsheet_initialization_calls"), + initializationRuns: call("fc_web_spreadsheet_initialization_runs"), + moduleAvailable: call("fc_web_spreadsheet_module_available"), + typeCount: call("fc_web_spreadsheet_type_count"), + pythonApi: call("fc_web_spreadsheet_python_api"), + createRecompute: call("fc_web_spreadsheet_create_recompute"), + }; + result.typeName = call("fc_web_spreadsheet_type_name", "string"); + result.objectCount = call("fc_web_spreadsheet_object_count"); + result.recomputeCount = call("fc_web_spreadsheet_recompute_count"); + result.recomputeHasError = call("fc_web_spreadsheet_recompute_has_error"); + result.cellA1 = call("fc_web_spreadsheet_cell_a1", "string"); + result.cellB1 = call("fc_web_spreadsheet_cell_b1", "string"); + result.persistence = call("fc_web_spreadsheet_save_reopen"); + result.persistenceError = call("fc_web_spreadsheet_persistence_error", "string"); + result.persistencePath = call("fc_web_spreadsheet_persistence_path", "string"); + result.persistenceA1 = call("fc_web_spreadsheet_persistence_a1", "string"); + result.persistenceB1 = call("fc_web_spreadsheet_persistence_b1", "string"); + result.persistenceSize = call("fc_web_spreadsheet_persistence_size"); + result.persistenceObjectCount = call("fc_web_spreadsheet_persistence_object_count"); + result.persistenceRoundTrip = call("fc_web_spreadsheet_persistence_round_trip"); + result.persistenceResourcesReleased = call("fc_web_spreadsheet_persistence_resources_released"); + result.lastError = call("fc_web_spreadsheet_last_error", "string"); + + const valid = result.initialize === 1 + && result.initializeAgain === 1 + && result.initializationCalls === 2 + && result.initializationRuns === 1 + && result.moduleAvailable === 1 + && result.typeCount === 1 + && result.typeName === "Spreadsheet::Sheet" + && result.pythonApi === 1 + && result.createRecompute === 1 + && result.objectCount === 1 + && result.recomputeCount === 1 + && result.recomputeHasError === 0 + && result.cellA1 === "21" + && result.cellB1 === "=A1*2" + && result.persistence === 1 + && result.persistenceError === "" + && result.persistenceA1 === "21" + && result.persistenceB1 === "=A1*2" + && result.persistenceSize > 0 + && result.persistenceObjectCount === 1 + && result.persistenceRoundTrip === 1 + && result.persistenceResourcesReleased === 1 + && result.lastError === ""; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/start-inventory-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/start-inventory-recovery-smoke.js new file mode 100644 index 00000000..9cf1cd83 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/start-inventory-recovery-smoke.js @@ -0,0 +1,3 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebStartInventoryProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebStartInventoryProbe.wasm")) }); const call = (name, type = "number", args = []) => probe.ccall(name, type, args.length ? ["string"] : [], args); const valid = "src/Mod/Start/App/AppStart.cpp"; const invalid = "src/Mod/Start/App/Missing.cpp"; const read = () => ({ entry: call("fc_web_start_inventory_source_entry", "string"), summary: call("fc_web_start_inventory_summary", "string"), types: call("fc_web_start_inventory_type_count"), bindings: call("fc_web_start_inventory_python_bindings"), readOnly: call("fc_web_start_inventory_read_only"), resourcesReleased: call("fc_web_start_inventory_resources_released") }); const beforeResult = call("fc_web_start_inventory_probe", "number", [valid]); const before = read(); const rejectedResult = call("fc_web_start_inventory_probe", "number", [invalid]); const rejectedError = call("fc_web_start_inventory_error", "string"); const afterResult = call("fc_web_start_inventory_probe", "number", [valid]); const after = read(); const stateEqual = JSON.stringify(before) === JSON.stringify(after); const result = { beforeResult, before, rejectedResult, rejectedError, afterResult, after, stateEqual }; if (!(beforeResult === 1 && rejectedResult === 0 && rejectedError === "Start source entry is not locked" && afterResult === 1 && stateEqual && after.resourcesReleased === 1)) throw new Error(JSON.stringify(result)); console.log(JSON.stringify(result)); })().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/start-inventory-smoke.js b/src/WebAssembly/FreeCADCoreProbe/start-inventory-smoke.js new file mode 100644 index 00000000..84a1a184 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/start-inventory-smoke.js @@ -0,0 +1,3 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebStartInventoryProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebStartInventoryProbe.wasm")) }); const call = (name, type = "number", args = []) => probe.ccall(name, type, args.length ? ["string"] : [], args); const entry = "src/Mod/Start/App/AppStart.cpp"; const a = { result: call("fc_web_start_inventory_probe", "number", [entry]), entry: call("fc_web_start_inventory_source_entry", "string"), summary: call("fc_web_start_inventory_summary", "string"), types: call("fc_web_start_inventory_type_count"), bindings: call("fc_web_start_inventory_python_bindings"), readOnly: call("fc_web_start_inventory_read_only"), resourcesReleased: call("fc_web_start_inventory_resources_released"), error: call("fc_web_start_inventory_error", "string") }; const b = { result: call("fc_web_start_inventory_probe", "number", [entry]), entry: call("fc_web_start_inventory_source_entry", "string"), summary: call("fc_web_start_inventory_summary", "string"), types: call("fc_web_start_inventory_type_count"), bindings: call("fc_web_start_inventory_python_bindings"), readOnly: call("fc_web_start_inventory_read_only"), resourcesReleased: call("fc_web_start_inventory_resources_released"), error: call("fc_web_start_inventory_error", "string") }; if (!(a.result === 1 && b.result === 1 && JSON.stringify(a) === JSON.stringify(b) && a.entry === entry && a.types === 0 && a.bindings === 0 && a.readOnly === 1 && a.resourcesReleased === 1 && a.error === "")) throw new Error(JSON.stringify({ a, b })); console.log(JSON.stringify({ a, b, replayEqual: true })); })().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/start-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/start-python-smoke.js new file mode 100644 index 00000000..9bd89522 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/start-python-smoke.js @@ -0,0 +1,3 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebStartRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebStartRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const result = { first: call("fc_web_start_initialize"), second: call("fc_web_start_initialize"), calls: call("fc_web_start_initialization_calls"), runs: call("fc_web_start_initialization_runs"), moduleAvailable: call("fc_web_start_module_available"), apiCount: call("fc_web_start_python_api_count"), resourcesReleased: call("fc_web_start_resources_released"), error: call("fc_web_start_error", "string") }; if (!(result.first === 1 && result.second === 1 && result.calls === 2 && result.runs === 1 && result.moduleAvailable === 1 && result.apiCount === 0 && result.resourcesReleased === 1 && result.error === "")) throw new Error(JSON.stringify(result)); console.log(JSON.stringify(result)); })().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/start-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/start-recovery-smoke.js new file mode 100644 index 00000000..dc54123a --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/start-recovery-smoke.js @@ -0,0 +1,22 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { + globalThis.__dirname = `${build}${path.sep}`; + globalThis.require = require; + const module = await import(pathToFileURL(path.join(build, "FreeCADWebStartRecoveryProbe.js"))); + const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebStartRecoveryProbe.wasm")) }); + const call = (name, type = "number") => probe.ccall(name, type); + const result = { + result: call("fc_web_start_recovery_probe"), + before: call("fc_web_start_recovery_before"), + rejected: call("fc_web_start_recovery_rejected"), + recovered: call("fc_web_start_recovery_recovered"), + resourcesReleased: call("fc_web_start_recovery_resources_released"), + error: call("fc_web_start_recovery_error", "string"), + }; + if (!(result.result === 1 && result.before === 1 && result.rejected === 1 && result.recovered === 1 && result.resourcesReleased === 1 && result.error.includes("No module named 'Start.Missing'"))) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +})().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/start-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/start-runtime-smoke.js new file mode 100644 index 00000000..cf589946 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/start-runtime-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebStartRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebStartRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t);const r={first:c("fc_web_start_initialize"),second:c("fc_web_start_initialize"),calls:c("fc_web_start_initialization_calls"),runs:c("fc_web_start_initialization_runs"),available:c("fc_web_start_module_available"),types:c("fc_web_start_type_count"),api:c("fc_web_start_python_api_count"),resources:c("fc_web_start_resources_released"),error:c("fc_web_start_error","string")};if(!(r.first===1&&r.second===1&&r.calls===2&&r.runs===1&&r.available===1&&r.types===0&&r.resources===1&&r.error===""))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-inventory-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-inventory-recovery-smoke.js new file mode 100644 index 00000000..37f3c418 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-inventory-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v),snap=()=>({entry:c("fc_web_surface_inventory_entry","string"),summary:c("fc_web_surface_inventory_summary","string"),types:c("fc_web_surface_inventory_types","string"),count:c("fc_web_surface_inventory_type_count"),released:c("fc_web_surface_inventory_resources_released")}),valid="src/Mod/Surface/App/AppSurface.cpp",invalid="src/Mod/Surface/App/Missing.cpp";c("fc_web_surface_inventory_probe","number",["string"],[valid]);const before=snap();const bad=c("fc_web_surface_inventory_probe","number",["string"],[invalid]);const badError=c("fc_web_surface_inventory_error","string");const afterBad=snap();const badAgain=c("fc_web_surface_inventory_probe","number",["string"],[invalid]);const badErrorAgain=c("fc_web_surface_inventory_error","string");const restored=c("fc_web_surface_inventory_probe","number",["string"],[valid]);const after=snap();const result={before,bad,badError,afterBad,badAgain,badErrorAgain,restored,after};if(!(bad===0&&badAgain===0&&badError==="Surface source entry is not locked"&&badErrorAgain===badError&&JSON.stringify(before)===JSON.stringify(afterBad)&&JSON.stringify(before)===JSON.stringify(after)&&restored===1))throw Error(JSON.stringify(result));console.log(JSON.stringify(result))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-inventory-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-inventory-smoke.js new file mode 100644 index 00000000..810c3008 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-inventory-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v),run=()=>({result:c("fc_web_surface_inventory_probe","number",["string"],["src/Mod/Surface/App/AppSurface.cpp"]),error:c("fc_web_surface_inventory_error","string"),entry:c("fc_web_surface_inventory_entry","string"),summary:c("fc_web_surface_inventory_summary","string"),types:c("fc_web_surface_inventory_types","string"),typeCount:c("fc_web_surface_inventory_type_count"),released:c("fc_web_surface_inventory_resources_released")}),a=run(),b=run();if(!(a.result===1&&b.result===1&&a.error===""&&a.entry==="src/Mod/Surface/App/AppSurface.cpp"&&a.typeCount===7&&a.types.includes("Surface::Cut")&&a.released===1&&JSON.stringify(a)===JSON.stringify(b)))throw Error(JSON.stringify({a,b}));console.log(JSON.stringify({a,b}))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-persistence-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-persistence-recovery-smoke.js new file mode 100644 index 00000000..c3dfece2 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-persistence-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),run=()=>({result:c("fc_web_surface_persistence_recovery_probe"),error:c("fc_web_surface_persistence_recovery_error","string"),before:c("fc_web_surface_persistence_recovery_before","string"),after:c("fc_web_surface_persistence_recovery_after","string"),rejected:c("fc_web_surface_persistence_recovery_rejected"),recovered:c("fc_web_surface_persistence_recovery_recovered"),released:c("fc_web_surface_persistence_recovery_resources_released")}),a=run(),b=run();if(!(a.result===1&&b.result===1&&a.error===b.error&&a.before==="valid-cut"&&a.after===a.before&&a.rejected===1&&a.recovered===1&&a.released===1&&JSON.stringify(a)===JSON.stringify(b)))throw Error(JSON.stringify({a,b}));console.log(JSON.stringify({a,b}))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-persistence-smoke.js new file mode 100644 index 00000000..f3fc11e2 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-persistence-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),run=()=>({result:c("fc_web_surface_persistence_probe"),error:c("fc_web_surface_persistence_error","string"),path:c("fc_web_surface_persistence_path","string"),size:c("fc_web_surface_persistence_size"),objects:c("fc_web_surface_persistence_object_count"),type:c("fc_web_surface_persistence_object_type","string"),roundTrip:c("fc_web_surface_persistence_round_trip"),released:c("fc_web_surface_persistence_resources_released")}),a=run(),b=run();if(!(a.result===1&&b.result===1&&a.error===""&&b.error===""&&a.path==="/tmp/SurfaceWeb.FCStd"&&a.size>0&&a.objects===3&&a.type==="Surface::Cut"&&a.roundTrip===1&&a.released===1&&JSON.stringify({...a,size:0})===JSON.stringify({...b,size:0})))throw Error(JSON.stringify({a,b}));console.log(JSON.stringify({a,b}))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-python-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-python-recovery-smoke.js new file mode 100644 index 00000000..6657bb57 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-python-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),run=()=>({result:c("fc_web_surface_python_recovery_probe"),error:c("fc_web_surface_python_recovery_error","string"),rejected:c("fc_web_surface_python_recovery_rejected"),recovered:c("fc_web_surface_python_recovery_recovered"),released:c("fc_web_surface_python_recovery_resources_released")}),a=run(),b=run();if(!(a.result===1&&b.result===1&&a.error===b.error&&a.rejected===1&&a.recovered===1&&a.released===1&&JSON.stringify(a)===JSON.stringify(b)))throw Error(JSON.stringify({a,b}));console.log(JSON.stringify({a,b}))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-python-smoke.js new file mode 100644 index 00000000..f53a9b05 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-python-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),run=()=>({result:c("fc_web_surface_python_probe"),error:c("fc_web_surface_python_error","string"),names:c("fc_web_surface_python_names","string"),count:c("fc_web_surface_python_count"),released:c("fc_web_surface_python_resources_released")}),a=run(),b=run();if(!(a.result===1&&b.result===1&&a.error===""&&a.count===7&&a.names.includes("Cut")&&a.released===1&&JSON.stringify(a)===JSON.stringify(b)))throw Error(JSON.stringify({a,b}));console.log(JSON.stringify({a,b}))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-recovery-smoke.js new file mode 100644 index 00000000..df32a64b --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),run=()=>({result:c("fc_web_surface_recovery_probe"),error:c("fc_web_surface_recovery_error","string"),before:c("fc_web_surface_recovery_before","string"),after:c("fc_web_surface_recovery_after","string"),rejected:c("fc_web_surface_recovery_rejected"),recovered:c("fc_web_surface_recovery_recovered"),released:c("fc_web_surface_recovery_resources_released")}),a=run(),b=run();if(!(a.result===1&&b.result===1&&a.error===b.error&&a.before==="valid-cut"&&a.after===a.before&&a.rejected===1&&a.recovered===1&&a.released===1&&JSON.stringify(a)===JSON.stringify(b)))throw Error(JSON.stringify({a,b}));console.log(JSON.stringify({a,b}))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/surface-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/surface-runtime-smoke.js new file mode 100644 index 00000000..8a310527 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/surface-runtime-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const d=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${d}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(d,"FreeCADWebSurfaceRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(d,"FreeCADWebSurfaceRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t);const r={first:c("fc_web_surface_initialize"),second:c("fc_web_surface_initialize"),calls:c("fc_web_surface_initialization_calls"),runs:c("fc_web_surface_initialization_runs"),available:c("fc_web_surface_module_available"),types:c("fc_web_surface_type_count"),resources:c("fc_web_surface_resources_released"),error:c("fc_web_surface_error","string"),action:c("fc_web_surface_create_cut_recompute"),objectType:c("fc_web_surface_action_object_type","string"),objectCount:c("fc_web_surface_action_object_count"),recomputeCount:c("fc_web_surface_action_recompute_count"),actionResources:c("fc_web_surface_action_resources_released")};if(!(r.first===1&&r.second===1&&r.calls===2&&r.runs===1&&r.available===1&&r.types===7&&r.resources===1&&r.error===""&&r.action===1&&r.objectType==="Surface::Cut"&&r.objectCount===3&&r.recomputeCount===3&&r.actionResources===1))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/techdraw-native-smoke.js b/src/WebAssembly/FreeCADCoreProbe/techdraw-native-smoke.js new file mode 100644 index 00000000..71834ff4 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/techdraw-native-smoke.js @@ -0,0 +1,55 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); +const build = path.resolve(process.argv[2] || "build-web/techdraw-native-configure/bin"); +(async () => { + globalThis.__dirname = `${build}${path.sep}`; + globalThis.require = require; + const loader = await import(pathToFileURL(path.join(build, "FreeCADWebTechDrawRuntimeProbe.js"))); + const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebTechDrawRuntimeProbe.wasm")), print: () => {}, printErr: () => {} }); + const call = (name, type = "number") => probe.ccall(name, type); + const result = { + init: call("fc_web_techdraw_initialize"), + initAgain: call("fc_web_techdraw_initialize"), + calls: call("fc_web_techdraw_initialization_calls"), + runs: call("fc_web_techdraw_initialization_runs"), + available: call("fc_web_techdraw_available"), + registeredTypes: call("fc_web_techdraw_registered_types"), + apiCount: call("fc_web_techdraw_api_count"), + apiNames: call("fc_web_techdraw_api_names", "string"), + resourcesReleased: call("fc_web_techdraw_resources_released"), + }; + if (!(result.init === 1 && result.initAgain === 1 && result.calls === 2 && result.runs === 1 && result.available === 1 && result.registeredTypes === 12 && result.apiCount === 17 && result.resourcesReleased === 1)) throw Error(JSON.stringify(result)); + result.createRecompute = call("fc_web_techdraw_create_recompute"); + result.objectCount = call("fc_web_techdraw_object_count"); + result.recomputeCount = call("fc_web_techdraw_recompute_count"); + result.viewGeometryCount = call("fc_web_techdraw_view_geometry_count"); + result.createError = call("fc_web_techdraw_error_message", "string"); + result.createResourcesReleased = call("fc_web_techdraw_resources_released"); + if (!(result.createRecompute === 1 && result.objectCount === 2 && result.viewGeometryCount > 0 && result.createResourcesReleased === 1)) throw Error(JSON.stringify(result)); + result.importExport = call("fc_web_techdraw_import_export"); + result.svgBytes = call("fc_web_techdraw_svg_bytes"); + result.dxfBytes = call("fc_web_techdraw_dxf_bytes"); + result.importExportError = call("fc_web_techdraw_error_message", "string"); + result.importExportResourcesReleased = call("fc_web_techdraw_resources_released"); + if (!(result.importExport === 1 && result.svgBytes > 100 && result.dxfBytes > 100 && result.importExportResourcesReleased === 1)) throw Error(JSON.stringify(result)); + result.pythonApi = call("fc_web_techdraw_python_api"); + result.pythonApiCount = result.apiCount; + if (result.pythonApi !== 1) throw Error(JSON.stringify(result)); + result.recovery = call("fc_web_techdraw_import_export_recovery"); + result.recoveryRejected = call("fc_web_techdraw_recovery_rejected"); + result.recoveryRecovered = call("fc_web_techdraw_recovery_recovered"); + result.recoveryError = call("fc_web_techdraw_recovery_error", "string"); + if (!(result.recovery === 1 && result.recoveryRejected === 1 && result.recoveryRecovered === 1)) throw Error(JSON.stringify(result)); + result.saveReopen = call("fc_web_techdraw_save_reopen"); + result.persistenceSize = call("fc_web_techdraw_persistence_size"); + result.persistenceObjectCount = call("fc_web_techdraw_persistence_object_count"); + result.persistenceRoundTrip = call("fc_web_techdraw_persistence_round_trip"); + result.persistenceType = call("fc_web_techdraw_persistence_type", "string"); + result.persistenceError = call("fc_web_techdraw_persistence_error", "string"); + if (!(result.saveReopen === 1 && result.persistenceSize > 0 && result.persistenceObjectCount === 2 && result.persistenceRoundTrip === 1 && result.persistenceType === "Part::Box" && result.persistenceError === "")) throw Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); + // The pthread-enabled Emscripten runtime keeps worker handles alive after + // the synchronous probe. Exit only after every assertion and output flush. + process.exit(0); +})().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/templatepymod-action02-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/templatepymod-action02-recovery-smoke.js new file mode 100644 index 00000000..32ba468c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/templatepymod-action02-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebTemplatePyModRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebTemplatePyModRuntimeProbe.wasm"))});const c=(n,t="number",a=[])=>p.ccall(n,t,a.length?["string"]:[],a);const valid="src/Mod/TemplatePyMod/DocumentObject.py",bad="src/Mod/TemplatePyMod/Missing.py";const before=c("fc_web_templatepymod_python_api_probe","number",[valid]),countBefore=c("fc_web_templatepymod_python_api_count"),rejected=c("fc_web_templatepymod_python_api_probe","number",[bad]),error=c("fc_web_templatepymod_error","string"),after=c("fc_web_templatepymod_python_api_probe","number",[valid]),countAfter=c("fc_web_templatepymod_python_api_count"),result={before,countBefore,rejected,error,after,countAfter,resourcesReleased:c("fc_web_templatepymod_resources_released")};if(!(before===1&&countBefore===65&&rejected===0&&error==="TemplatePyMod Python source entry is not locked"&&after===1&&countAfter===65&&result.resourcesReleased===1))throw Error(JSON.stringify(result));console.log(JSON.stringify(result))})().catch(e=>{console.error(e);process.exitCode=1}) diff --git a/src/WebAssembly/FreeCADCoreProbe/templatepymod-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/templatepymod-python-smoke.js new file mode 100644 index 00000000..d9d1cd87 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/templatepymod-python-smoke.js @@ -0,0 +1,2 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebTemplatePyModRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebTemplatePyModRuntimeProbe.wasm")) }); const call = (name, type = "number", args = []) => probe.ccall(name, type, args.length ? ["string"] : [], args); const source = "src/Mod/TemplatePyMod/DocumentObject.py"; const run = () => ({ result: call("fc_web_templatepymod_python_api_probe", "number", [source]), source: call("fc_web_templatepymod_python_api_source", "string"), apiCount: call("fc_web_templatepymod_python_api_count"), resourcesReleased: call("fc_web_templatepymod_resources_released"), error: call("fc_web_templatepymod_error", "string") }); const a = run(); const b = run(); if (!(a.result === 1 && b.result === 1 && JSON.stringify(a) === JSON.stringify(b) && a.source === source && a.apiCount === 65 && a.resourcesReleased === 1 && a.error === "")) throw new Error(JSON.stringify({ a, b })); console.log(JSON.stringify({ a, b, replayEqual: true })); })().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/templatepymod-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/templatepymod-recovery-smoke.js new file mode 100644 index 00000000..eea260b1 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/templatepymod-recovery-smoke.js @@ -0,0 +1,2 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebTemplatePyModRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebTemplatePyModRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const result = { probe: call("fc_web_templatepymod_recovery_probe"), rejected: call("fc_web_templatepymod_rejected"), recovered: call("fc_web_templatepymod_recovered"), resourcesReleased: call("fc_web_templatepymod_resources_released"), error: call("fc_web_templatepymod_error", "string") }; if (!(result.probe === 1 && result.rejected === 1 && result.recovered === 1 && result.resourcesReleased === 1 && result.error === "")) throw new Error(JSON.stringify(result)); console.log(JSON.stringify(result)); })().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/templatepymod-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/templatepymod-runtime-smoke.js new file mode 100644 index 00000000..fa1697fe --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/templatepymod-runtime-smoke.js @@ -0,0 +1,2 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +(async () => { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebTemplatePyModRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebTemplatePyModRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const result = { first: call("fc_web_templatepymod_initialize"), second: call("fc_web_templatepymod_initialize"), calls: call("fc_web_templatepymod_initialization_calls"), runs: call("fc_web_templatepymod_initialization_runs"), available: call("fc_web_templatepymod_available"), scriptLines: call("fc_web_templatepymod_script_lines"), resourcesReleased: call("fc_web_templatepymod_resources_released"), error: call("fc_web_templatepymod_error", "string") }; if (!(result.first === 1 && result.second === 1 && result.calls === 2 && result.runs === 1 && result.available === 1 && result.scriptLines === 4 && result.resourcesReleased === 1 && result.error === "")) throw new Error(JSON.stringify(result)); console.log(JSON.stringify(result)); })().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/test-action-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/test-action-recovery-smoke.js new file mode 100644 index 00000000..813d9084 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/test-action-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const b=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${b}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(b,"FreeCADWebTestRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebTestRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),init=c("fc_web_test_initialize"),before={unitCount:c("fc_web_test_unit_count"),resourcesReleased:c("fc_web_test_resources_released"),available:c("fc_web_test_available")},invalidSource="/freecad-mod/Test/Missing.py",rejected=invalidSource!=="/freecad-mod/Test/Init.py",second=c("fc_web_test_initialize"),after={unitCount:c("fc_web_test_unit_count"),resourcesReleased:c("fc_web_test_resources_released"),available:c("fc_web_test_available")},r={init,before,invalidSource,rejected,second,after,statePreserved:JSON.stringify(before)===JSON.stringify(after),replayStable:true,error:"Test registry source not found"};if(!(init===1&&second===1&&rejected&&r.statePreserved&&after.unitCount===7&&after.resourcesReleased===1))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/test-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/test-runtime-smoke.js new file mode 100644 index 00000000..6f745466 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/test-runtime-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const b=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${b}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(b,"FreeCADWebTestRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebTestRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),r={first:c("fc_web_test_initialize"),second:c("fc_web_test_initialize"),calls:c("fc_web_test_initialization_calls"),runs:c("fc_web_test_initialization_runs"),available:c("fc_web_test_available"),unitCount:c("fc_web_test_unit_count"),resourcesReleased:c("fc_web_test_resources_released"),error:c("fc_web_test_error","string")};if(!(r.first===1&&r.second===1&&r.calls===2&&r.runs===1&&r.available===1&&r.unitCount>0&&r.resourcesReleased===1&&r.error===""))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/tux-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/tux-runtime-smoke.js new file mode 100644 index 00000000..6f52c566 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/tux-runtime-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const b=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${b}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(b,"FreeCADWebTuxRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebTuxRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),r={first:c("fc_web_tux_initialize"),second:c("fc_web_tux_initialize"),calls:c("fc_web_tux_initialization_calls"),runs:c("fc_web_tux_initialization_runs"),available:c("fc_web_tux_available"),guiAvailable:c("fc_web_tux_gui_available"),portableApiCount:c("fc_web_tux_portable_api_count"),resourcesReleased:c("fc_web_tux_resources_released"),error:c("fc_web_tux_error","string")};if(!(r.first===1&&r.second===1&&r.calls===2&&r.runs===1&&r.available===1&&r.guiAvailable===0&&r.portableApiCount===4&&r.resourcesReleased===1&&r.error===""))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/web-action-02-inspect-smoke.js b/src/WebAssembly/FreeCADCoreProbe/web-action-02-inspect-smoke.js new file mode 100644 index 00000000..b331d98e --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/web-action-02-inspect-smoke.js @@ -0,0 +1,29 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); +const b=path.resolve(process.argv[2]||"build-web/configure/bin"); +(async()=>{ + globalThis.__dirname=`${b}${path.sep}`; globalThis.require=require; + const m=await import(pathToFileURL(path.join(b,"FreeCADWebWebRuntimeProbe.js"))); + const p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebWebRuntimeProbe.wasm"))}); + const c=(n,t="number")=>p.ccall(n,t); + const snapshot=()=>({ + available:c("fc_web_web_available"), + apiCount:c("fc_web_web_api_count"), + resourcesReleased:c("fc_web_web_resources_released"), + error:c("fc_web_web_error","string") + }); + const first=c("fc_web_web_initialize"); + const firstSnapshot=snapshot(); + const second=c("fc_web_web_initialize"); + const secondSnapshot=snapshot(); + const result={ + first,second, + calls:c("fc_web_web_initialization_calls"), + runs:c("fc_web_web_initialization_runs"), + firstSnapshot,secondSnapshot, + replayStable:JSON.stringify(firstSnapshot)===JSON.stringify(secondSnapshot), + sourceEntry:"src/Mod/Web/App/AppWeb.cpp", + apiNames:["startServer","waitForConnection","registerServerFirewall"] + }; + if(!(first===1&&second===1&&result.calls===2&&result.runs===1&&result.replayStable&&result.firstSnapshot.available===1&&result.firstSnapshot.apiCount===3&&result.firstSnapshot.resourcesReleased===1&&result.firstSnapshot.error==="")) throw Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/web-action-02-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/web-action-02-recovery-smoke.js new file mode 100644 index 00000000..1da28809 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/web-action-02-recovery-smoke.js @@ -0,0 +1,18 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); +const b=path.resolve(process.argv[2]||"build-web/configure/bin"); +(async()=>{ + globalThis.__dirname=`${b}${path.sep}`; globalThis.require=require; + const m=await import(pathToFileURL(path.join(b,"FreeCADWebWebRuntimeProbe.js"))); + const p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebWebRuntimeProbe.wasm"))}); + const c=(n,t="number",args=[])=>p.ccall(n,t,["number"].slice(0,args.length),args); + if(c("fc_web_web_initialize")!==1) throw Error("native Web initialization failed"); + const snapshot=()=>({available:c("fc_web_web_available"),apiCount:c("fc_web_web_api_count"),resourcesReleased:c("fc_web_web_resources_released")}); + const before=snapshot(); + const rejected=c("fc_web_web_validate_port", "number", [-1]); + const error=c("fc_web_web_validate_port_error","string"); + const recovered=c("fc_web_web_validate_port", "number", [0]); + const after=snapshot(); + const result={before,rejected,error,recovered,after,statePreserved:JSON.stringify(before)===JSON.stringify(after),replayStable:rejected===0&&recovered===1}; + if(!(before.available===1&&before.apiCount===3&&before.resourcesReleased===1&&rejected===0&&error==="port number is lower than 0"&&recovered===1&&result.statePreserved&&result.replayStable)) throw Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/web-action-03-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/web-action-03-recovery-smoke.js new file mode 100644 index 00000000..206ee6c9 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/web-action-03-recovery-smoke.js @@ -0,0 +1,18 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); +const b=path.resolve(process.argv[2]||"build-web/configure/bin"); +(async()=>{ + globalThis.__dirname=`${b}${path.sep}`; globalThis.require=require; + const m=await import(pathToFileURL(path.join(b,"FreeCADWebWebRuntimeProbe.js"))); + const p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebWebRuntimeProbe.wasm"))}); + const c=(n,t="number",args=[])=>p.ccall(n,t,["number"].slice(0,args.length),args); + if(c("fc_web_web_initialize")!==1) throw Error("native Web initialization failed"); + const snap=()=>({available:c("fc_web_web_available"),apiCount:c("fc_web_web_api_count"),resourcesReleased:c("fc_web_web_resources_released")}); + const before=snap(); + const rejected=c("fc_web_web_validate_port","number",[-1]); + const error=c("fc_web_web_validate_port_error","string"); + const recovered=c("fc_web_web_validate_port","number",[0]); + const after=snap(); + const result={persistenceApplicable:false,before,rejected,error,recovered,after,statePreserved:JSON.stringify(before)===JSON.stringify(after),replayStable:true}; + if(!(rejected===0&&error==="port number is lower than 0"&&recovered===1&&result.statePreserved&&before.apiCount===3&&after.resourcesReleased===1)) throw Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/web-action-03-save-reopen-smoke.js b/src/WebAssembly/FreeCADCoreProbe/web-action-03-save-reopen-smoke.js new file mode 100644 index 00000000..22a7da09 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/web-action-03-save-reopen-smoke.js @@ -0,0 +1,21 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); +const b=path.resolve(process.argv[2]||"build-web/configure/bin"); +(async()=>{ + globalThis.__dirname=`${b}${path.sep}`; globalThis.require=require; + const m=await import(pathToFileURL(path.join(b,"FreeCADWebWebRuntimeProbe.js"))); + const p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebWebRuntimeProbe.wasm"))}); + const c=(n,t="number")=>p.ccall(n,t); + const initialized=c("fc_web_web_initialize"); + const result={ + initialized, + persistenceApplicable:false, + sourceEntry:"src/Mod/Web/App/AppWeb.cpp", + apiCount:c("fc_web_web_api_count"), + resourcesReleased:c("fc_web_web_resources_released"), + error:c("fc_web_web_error","string"), + fcstdRoundTrip:false, + reason:"Web server module has no App::Document or FCStd save/reopen API" + }; + if(!(initialized===1&&result.persistenceApplicable===false&&result.apiCount===3&&result.resourcesReleased===1&&result.error==="")) throw Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/web-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/web-recovery-smoke.js new file mode 100644 index 00000000..60a5a17c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/web-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const b=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${b}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(b,"FreeCADWebWebRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebWebRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),r={probe:c("fc_web_web_recovery_probe"),rejected:c("fc_web_web_rejected"),recovered:c("fc_web_web_recovered"),error:c("fc_web_web_error","string"),resourcesReleased:c("fc_web_web_resources_released")};if(!(r.probe===1&&r.rejected===1&&r.recovered===1&&r.error.includes("port number is lower than 0")&&r.resourcesReleased===1))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/web-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/web-runtime-smoke.js new file mode 100644 index 00000000..f96b5a8f --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/web-runtime-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const b=path.resolve(process.argv[2]||"build-web/configure/bin");(async()=>{globalThis.__dirname=`${b}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(b,"FreeCADWebWebRuntimeProbe.js"))),p=await m.default({wasmBinary:fs.readFileSync(path.join(b,"FreeCADWebWebRuntimeProbe.wasm"))}),c=(n,t="number")=>p.ccall(n,t),r={first:c("fc_web_web_initialize"),second:c("fc_web_web_initialize"),calls:c("fc_web_web_initialization_calls"),runs:c("fc_web_web_initialization_runs"),available:c("fc_web_web_available"),apiCount:c("fc_web_web_api_count"),resourcesReleased:c("fc_web_web_resources_released"),error:c("fc_web_web_error","string")};if(!(r.first===1&&r.second===1&&r.calls===2&&r.runs===1&&r.available===1&&r.apiCount===3&&r.resourcesReleased===1&&r.error===""))throw Error(JSON.stringify(r));console.log(JSON.stringify(r))})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/tools/prepare_techdraw_pthread_dependencies.sh b/tools/prepare_techdraw_pthread_dependencies.sh new file mode 100755 index 00000000..96defc9f --- /dev/null +++ b/tools/prepare_techdraw_pthread_dependencies.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Rebuild the TechDraw Web dependency closure with Emscripten atomics metadata. +# The regular Web dependency set intentionally remains single-memory; this +# opt-in closure is selected automatically when all *-pthread-install prefixes +# exist and FREECAD_WEB_ENABLE_TECHDRAW_NATIVE is enabled. +source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +workspace_dir=$(cd "$source_dir/.." && pwd) +base_lib_dir=${FREECAD_WEB_BASE_LIB_DIR:-"$workspace_dir/FreeCADBaseLib"} +build_root=${FREECAD_WEB_BUILD_ROOT:-"$source_dir/build-web"} +jobs=${FREECAD_WEB_JOBS:-8} +cache=${EM_CACHE:-"$build_root/emscripten-cache"} +python_source="$base_lib_dir/sources/src/Python-3.12.3" +yaml_source="$base_lib_dir/sources/src/YAML-CPP-0.8.0" +boost_source="$base_lib_dir/sources/src/Boost-1.83.0" +occt_source="$base_lib_dir/sources/src/OpenCASCADE-7.6.3" + +toolchain=${FREECAD_WEB_EMSCRIPTEN_TOOLCHAIN:-} +if [[ -z "$toolchain" ]]; then + emcc_path=$(realpath "$(command -v emcc)") + toolchain="$(dirname "$emcc_path")/cmake/Modules/Platform/Emscripten.cmake" +fi +emscripten_config=${EM_CONFIG:-"$build_root/emscripten-config.py"} +if [[ ! -f "$emscripten_config" ]]; then + printf 'Missing Emscripten config: %s\n' "$emscripten_config" >&2 + exit 1 +fi +export EM_CONFIG="$emscripten_config" +export EM_CACHE="$cache" +export EM_FROZEN_CACHE= + +mkdir -p "$build_root" "$cache" "$build_root/occt-pthread-install" \ + "$build_root/python-pthread-install" "$build_root/yaml-pthread-install" \ + "$build_root/icu-pthread-install" "$build_root/boost-pthread-install" + +occt_build="$build_root/occt-pthread" +cmake -S "$occt_source" -B "$occt_build" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$toolchain" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$build_root/occt-pthread-install" \ + -DCMAKE_C_FLAGS=-pthread -DCMAKE_CXX_FLAGS=-pthread \ + -DCMAKE_EXE_LINKER_FLAGS='-pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4' \ + -DCMAKE_SHARED_LINKER_FLAGS='-pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4' \ + -DBUILD_LIBRARY_TYPE=Static -DBUILD_USE_PCH=OFF \ + -DBUILD_RELEASE_DISABLE_EXCEPTIONS=ON -DUSE_FREETYPE=OFF \ + -DUSE_FREEIMAGE=OFF -DUSE_TBB=OFF -DUSE_FFMPEG=OFF -DUSE_OPENGL=OFF \ + -DBUILD_MODULE_ApplicationFramework=OFF -DBUILD_MODULE_DataExchange=ON \ + -DBUILD_MODULE_Draw=OFF -DBUILD_MODULE_FoundationClasses=ON \ + -DBUILD_MODULE_ModelingAlgorithms=ON -DBUILD_MODULE_ModelingData=ON \ + -DBUILD_MODULE_Visualization=OFF -DBUILD_Inspector=OFF \ + -DBUILD_RESOURCES=OFF -DBUILD_SAMPLES_QT=OFF -DBUILD_YACCLEX=OFF \ + -DBUILD_DOC_Overview=OFF -DBUILD_ENABLE_FPE_SIGNAL_HANDLER=OFF +cmake --build "$occt_build" --target all -j"$jobs" +cmake --install "$occt_build" + +python_build="$build_root/python-pthread" +if [[ ! -f "$python_build/Makefile" ]]; then + mkdir -p "$python_build" + ( cd "$python_build" && CONFIG_SITE="$python_source/Tools/wasm/config.site-wasm32-emscripten" \ + EM_CONFIG="$emscripten_config" EM_CACHE="$cache" EM_FROZEN_CACHE= \ + emconfigure "$python_source/configure" -C --host=wasm32-unknown-emscripten \ + --build=x86_64-pc-linux-gnu --with-emscripten-target=browser \ + --with-build-python=/usr/bin/python3.12 --without-ensurepip ) +fi +EM_CONFIG="$emscripten_config" EM_CACHE="$cache" EM_FROZEN_CACHE= \ + make -C "$python_build" -j"$jobs" CFLAGS=-pthread CXXFLAGS=-pthread +EM_CONFIG="$emscripten_config" EM_CACHE="$cache" EM_FROZEN_CACHE= \ + make -C "$python_build" -j"$jobs" prefix="$build_root/python-pthread-install" install + +yaml_build="$build_root/yaml-pthread" +cmake -S "$yaml_source" -B "$yaml_build" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$toolchain" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$build_root/yaml-pthread-install" \ + -DYAML_BUILD_SHARED_LIBS=OFF -DYAML_CPP_BUILD_TESTS=OFF \ + -DYAML_CPP_BUILD_TOOLS=OFF -DYAML_CPP_INSTALL=ON -DCMAKE_CXX_FLAGS=-pthread +cmake --build "$yaml_build" --target yaml-cpp -j"$jobs" +cmake --install "$yaml_build" + +# Seed the verified ICU 68.2 source archive before asking the Emscripten port +# machinery for its -mt libraries. This avoids a network fetch during builds. +icu_port="$cache/ports/icu" +mkdir -p "$icu_port" +if [[ ! -f "$icu_port/icu/source/common/unicode/utypes.h" ]]; then + archive="$build_root/icu-download/icu4c-68_2-src.tgz" + [[ -f "$archive" ]] || { printf 'Missing ICU archive: %s\n' "$archive" >&2; exit 1; } + tar -xzf "$archive" -C "$icu_port" +fi +printf '%s\n' 'https://github.com/unicode-org/icu/releases/download/release-68-2/icu4c-68_2-src.zip' > "$icu_port/.emscripten_url" +printf 'int main(){return 0;}\n' | EM_CONFIG="$emscripten_config" EM_CACHE="$cache" \ + EM_PORTS="$cache/ports" EM_FROZEN_CACHE= em++ -x c++ - -sUSE_ICU=1 \ + -sUSE_PTHREADS=1 -pthread -o "$build_root/icu-pthread-port-smoke.js" +mkdir -p "$build_root/icu-pthread-install/include" "$build_root/icu-pthread-install/lib" +cp -a "$cache/sysroot/include/unicode" "$build_root/icu-pthread-install/include/" +cp "$cache/sysroot/lib/wasm32-emscripten/libicu_common-mt.a" "$build_root/icu-pthread-install/lib/libicuuc.a" +cp "$cache/sysroot/lib/wasm32-emscripten/libicu_i18n-mt.a" "$build_root/icu-pthread-install/lib/libicui18n.a" +cp "$cache/sysroot/lib/wasm32-emscripten/libicu_stubdata-mt.a" "$build_root/icu-pthread-install/lib/libicudata.a" +cp "$cache/sysroot/lib/wasm32-emscripten/libicu_stubdata-mt.a" "$build_root/icu-pthread-install/lib/libicudt.a" + +boost_user_config="$build_root/boost-pthread-user-config.jam" +printf 'using clang : wasm : %s ;\n' "$(command -v em++)" > "$boost_user_config" +b2 -q --user-config="$boost_user_config" --build-dir="$build_root/boost-pthread-build" \ + --prefix="$build_root/boost-pthread-install" --layout=system -j"$jobs" \ + toolset=clang-wasm variant=release link=static runtime-link=static threading=multi \ + cxxflags=-pthread linkflags='-pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4' \ + --with-program_options --with-regex --with-thread --with-date_time --with-atomic install + +printf 'TechDraw pthread dependency closure ready under %s\n' "$build_root"