subagent transcript
session 17f09f89 · workflow wf_77616857
delegated task
GOAL: Produce a CONCRETE roadmap to FULL FreeCAD desktop feature parity for the FreeCAD->WebAssembly port. Be evidence-based: grep/read the actual sou
▸ Magik
GOAL: Produce a CONCRETE roadmap to FULL FreeCAD desktop feature parity for the FreeCAD->WebAssembly port. Be evidence-based: grep/read the actual sources, don't reason from general FreeCAD knowledge alone. Cite file paths.
CURRENT STATE (verified working in the browser build):
- FreeCAD 26.3.0dev compiled to wasm. Toolchain: emscripten 4.0.12, ASYNCIFY, JS-exceptions, **single-thread (NO pthreads, QT_FEATURE_thread=-1)**, static CPython 3.14, static Qt 6.11.1 (asyncify), static OCCT, static Coin3D. **NO dlopen** — every C-extension is registered via PyImport_AppendInittab in generated src/Main/WasmInittab*.cpp. **NO subprocess/QProcess, NO threads, NO native OS dialogs.** Browser file I/O via upload/download (deploy/index.html). FreeCAD.wasm ~243MB.
- 3D viewport RENDERS (offscreen FBO + QPainter blit; Coin3D/OpenGL via WebGL).
- **PySide6 FULLY WORKS**: QtCore+QtGui+QtWidgets bindings + shiboken runtime + signals/slots; `from PySide import QtGui` compat shim works; `Gui.getMainWindow()` returns a real PySide6 QMainWindow (shiboken wrapInstance); Python macro scripting drives geometry + GUI.
- Workbenches LIVE (7): Part, PartDesign, Sketcher, Surface, Spreadsheet, Material (+None/Start). Measure + Import modules built-in.
- Built-in C-extensions (inittab): Import, ImportGui, Materials, MatGui, Measure, MeasureGui, Part, PartGui, PartDesign, PartDesignGui, Sketcher, SketcherGui, Spreadsheet, SpreadsheetGui, StartGui, Surface, SurfaceGui.
- MISSING modules (present in src/Mod but NOT built/registered): Draft, BIM, CAM, Fem, Mesh, MeshPart, Points, TechDraw, Assembly, Plot, Show, OpenSCAD, Web, Inspection, ReverseEngineering, Robot, JtReader, Help, AddonManager, Test, Tux, TemplatePyMod.
PATHS:
- FreeCAD source: /home/magik6k/lcad-wasm/freecad-port/FreeCAD (src/Mod/<WB>, src/Gui, src/App). Each module has CMakeLists.txt, App/ (C++), Gui/ (C++), and often a Python workbench tree (Init.py/InitGui.py + *.py).
- wasm build tree: /opt/toolchains/src/freecad-gui-build (CMakeCache.txt, build.ninja, src/Main/WasmInittab*.cpp).
- deploy: /home/magik6k/lcad-wasm/freecad-port/deploy (index.html = browser file I/O + boot; *.data = packaged FS).
- The module-enablement pipeline already used for the live modules: add C-ext to inittab (RegName=CInitSuffix form via FREECAD_WASM_EXTRA_PY_MODULES + the SHARED->STATIC rewrite in FreeCAD/CMakeLists.txt:91-107), package the module's Python tree onto the wasm FS via file_packager (freecad-mods.data), drop unbuildable OCCT deps, fix dependency gates.
- External-tool deps that are a problem in wasm: Fem (calculix/gmsh solvers via subprocess), BIM (ifcopenshell, needs building for wasm + network for downloads), CAM (post-processors), AddonManager (git/network), OpenSCAD (external openscad binary), Web (QtWebEngine — almost certainly not portable).
For each thing you assess, classify wasm-viability: WORKS (already), EASY (module-enable pipeline, no hard blocker), MEDIUM (needs a dep built for wasm or a moderate workaround), HARD (needs a wasm-native re-architecture e.g. web worker/fetch/browser API), BLOCKED (fundamentally not portable — say why + what a degraded fallback looks like). Give a rough effort (S/M/L/XL) and cite evidence (CMakeLists deps, Init.py imports, subprocess/QProcess/network usage grep).
DIMENSION 4 — File I/O & interchange formats + persistence. Determine which import/export formats work now and which need a module: STEP/IGES/BREP (OCCT/Import — likely work), STL/OBJ/PLY/OFF (Mesh), DXF (Draft importDXF/legacy), SVG (Draft/TechDraw), IFC (BIM/ifcopenshell), glTF/OBJ (Import), DAE/Collada, images, PDF export (TechDraw/printing). Read src/Mod/Import and src/Mod/Mesh for the format handlers. Also assess: the browser open/save mechanism (deploy/index.html), FCStd save/load (works?), and **preferences/parameter persistence** across browser reloads (FreeCAD uses a user.cfg param file — where does it live in wasm MEMFS, does it survive reload? IDBFS?). Recompute-on-load, recent files. Give per-format viability + the persistence gap.
Fable
thinking
Now I'm turning my attention to Dimension 4, which covers file I/O, interchange formats, and persistence—let me dig into what the sources say about this.
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/ 2>/dev/null; echo \"---MESH---\"; ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Mesh/App/ 2>/dev/null | head -50", "description": "List Import and Me…
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/deploy/ 2>/dev/null; echo \"===\"; wc -l /home/magik6k/lcad-wasm/freecad-port/deploy/index.html 2>/dev/null", "description": "List deploy dir"}
Fable
thinking
I'm examining the import formats and how the index.html handles file I/O, along with the persistence layer using user.cfg and IDBFS.
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\\|importFormat\\|exportFormat\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/AppImport.cpp | head -60", "description": "Import registered formats"}
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Mesh/App/AppMesh.cpp", "description": "Mesh registered formats"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/deploy/index.html"}
Fable
thinking
The grep output is getting cluttered with zsh noise, so I need to try a different approach. The first two grep attempts came back empty, which is odd—the pattern might not be matching what I expect. Let me rerun the grep and check what's actually in the AppImport.cpp file to see what I'm working with.
Fable
⚙ Bash {"command": "grep -n \"addImportType\\|addExportType\\|ImportType\\|ExportType\\|InitApplication\\|\\.step\\|\\.stp\\|\\.igs\\|\\.iges\\|\\.gltf\\|\\.glb\\|\\.obj\\|\\.brep\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/A…
Fable
thinking
The search didn't turn up anything, so the registration must be defined in the Init.py files where addImportType is typically set up.
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/ 2>&1 | grep -v HISTDB", "description": "grep import type registration in Import mod"}
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/*.py /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/*.py 2>&1 | grep -v HISTDB", "description": "list Import python files"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/Init.py"}
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Part/ 2>&1 | grep -v HISTDB", "description": "grep Part type registration"}
Fable
thinking
I'm noticing that STEP imports colors through the Import module in Part/Init.py, and ImportGui is registered in the initialization table. The glTF importer uses ImportGui, but when I look at how STEP and IGES imports work in the index.html, they're calling the non-GUI App-level functions like Import.insert and Part.insert, which function correctly. I need to trace through what's happening with the registered GUI components to understand the discrepancy.
Fable
⚙ Bash {"command": "sed -n '32,60p' /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Part/Init.py 2>&1 | grep -v HISTDB", "description": "Part Init tail"}
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\\|addTranslatable\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Mesh/Init.py /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Points/Init.py 2>&1 | grep -v HISTDB", "descrip…
Fable
thinking
I'm checking the file format support across the codebase—looking at DXF and SVG imports in Draft, TechDraw's export capabilities for PDF and SVG, and how parameters persist through user.cfg and IDBFS storage.
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\\|addTranslatable\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Draft/Init.py 2>&1 | grep -v HISTDB | head -40", "description": "Draft type registration (DXF/SVG/DAE etc)"}
Fable
⚙ Bash {"command": "grep -rln \"addImportType\\|addExportType\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/*/Init.py 2>&1 | grep -v HISTDB", "description": "which modules register formats"}
Fable
thinking
I'm checking the dependencies for various file format importers in FreeCAD—specifically DXF, IFC, and DAE/Collada support—and noting that the DXF importer has a notable quirk where it downloads a library from the network on first use.
Fable
⚙ Bash {"command": "grep -rn \"download\\|urllib\\|urlopen\\|http\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Draft/importDXF.py 2>&1 | grep -v HISTDB | head -20", "description": "DXF network download check"}
Fable
⚙ Bash {"command": "grep -rn \"addImportType\\|addExportType\\|ifcopenshell\\|import ifc\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM/Init.py 2>&1 | grep -v HISTDB | head -30", "description": "BIM IFC registration"}
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Draft/ 2>&1 | grep -i -E \"dae|collada|dxf|svg|import\" | grep -v HISTDB", "description": "Draft importer files"}
Fable
thinking
Looking at the DXF import setup, there's a C++ DXF reader in the Import/App/dxf folder that serves as the modern importer, while importDXF.py maintains a legacy path with a preference flag to switch between them—the new default uses the internal C++ implementation.
DWG support is blocked since it requires the external ODA converter binary. Now I need to investigate how persistence works in the WASM environment—specifically where the user.cfg parameter file is stored and whether IDBFS is mounted during the build and boot process.
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/dxf/ 2>&1 | grep -v HISTDB | head; echo \"===\"; grep -rn \"dxf\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/CMakeLists.txt 2>&1 | grep -v …
Fable
⚙ Bash {"command": "grep -rn \"IDBFS\\|idbfs\\|persist\\|syncfs\\|FS.mount\" /home/magik6k/lcad-wasm/freecad-port/deploy/index.html /home/magik6k/lcad-wasm/freecad-port/deploy/boot.py 2>&1 | grep -v HISTDB", "description": "persistence mechanism c…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/deploy/boot.py"}
Fable
thinking
I'm noticing that IDBFS isn't configured, which means user.cfg is stored in memory and gets wiped on reload since HOME points to /tmp. I need to trace where FreeCAD actually writes the config file and check the default DXF importer setting, then look into how the C++ DXF native importer is registered and exposed through the Import module.
Fable
⚙ Bash {"command": "grep -rn \"user.cfg\\|system.cfg\\|getUserAppDataDir\\|SaveUserParameter\\|saveUser\\|mConfig\\|UserParameter\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp 2>&1 | grep -v HISTDB | grep -i \"cfg\\|SaveUs…
Fable
⚙ Bash {"command": "grep -rn \"dxfUseLegacyImporter\\|readDXF\\|Import.readDXF\\|import Import\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Draft/importDXF.py 2>&1 | grep -v HISTDB | head", "description": "DXF new importer path"}
Fable
⚙ Bash {"command": "grep -rn \"addExportType\\|addImportType\\|\\.pdf\\|\\.svg\\|\\.dxf\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/TechDraw/Init.py 2>&1 | grep -v HISTDB | head", "description": "TechDraw export types"}
Fable
thinking
So the DXF importer has an interesting architecture—the low-level C++ `Import.readDXF` function is built into the core Import module and available regardless of whether Draft is packaged, but the high-level registration that makes it accessible through the GUI happens in Draft's Init.py, which isn't included in the build.
I need to verify that `readDXF` is actually exported from the Import module, check where TechDraw registers its export types (the grep didn't find anything obvious), and confirm how the config path resolution works in the WASM environment—particularly that `/tmp` is memory-backed and gets wiped on reload, which explains the persistence issue.
Fable
⚙ Bash {"command": "grep -rn \"readDXF\\|writeDXF\\|readDGN\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Import/App/AppImportPy.cpp 2>&1 | grep -v HISTDB | head", "description": "Import module DXF python exports"}
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/TechDraw/*.py 2>&1 | grep -v HISTDB; echo \"===\"; grep -rn \"addExportType\\|addImportType\\|PageFileDialog\\|\\.pdf\\|\\.svg\" /home/magik6k/lcad-wasm/freecad-port/FreeC…
Fable
⚙ Bash {"command": "grep -rn \"UserConfigPath\\|GetHomePath\\|getUserAppDataDir\\|\\.FreeCAD\\|/.local\\|HOME\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp 2>&1 | grep -v HISTDB | grep -i \"UserConfigPath\\s*=\\|getenv\\|H…
Fable
thinking
I'm checking how UserConfigPath resolves in the WebAssembly environment, specifically looking at whether it points to the temporary filesystem under HOME=/tmp.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp", "offset": 3388, "limit": 40}
Fable
⚙ Bash {"command": "grep -rn \"SaveDocument\\|saveToFile\\|writeFile\\|SetSerializer\\|SaveUserParameter\\|_pcUserParamMngr->Save\\|->SaveDocument\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp 2>&1 | grep -v HISTDB | grep …
Fable
thinking
The config path resolves to a temporary in-memory filesystem under HOME, so preferences written via SaveParameter on exit don't persist across reloads since there's no IDBFS mount and the memory gets wiped. However, FCStd save and load operations do work through the standard save/load mechanism that uses a zip format.
Now I'm checking what export formats are actually available — STL, OBJ, PLY, and OFF aren't supported right now because the Mesh module isn't built, though glTF import/export does work since the Import module is included and handles those readers and writers. The Mesh module itself has minimal OCCT dependencies and is mostly self-contained, so enabling it should be straightfor …
Fable
⚙ Bash {"command": "grep -rn \"target_link_libraries\\|E57\\|e57\\|zlib\\|3mf\\|lib3mf\\|Zip\\|smf\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Mesh/App/CMakeLists.txt 2>&1 | grep -v HISTDB | head", "description": "Mesh link deps"}
Fable
⚙ Bash {"command": "grep -rn \"target_link_libraries\\|E57\\|e57\\|pcl\\|PCL\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Points/App/CMakeLists.txt 2>&1 | grep -v HISTDB | head; echo \"=== gltf reader deps ===\"; grep -rn \"gltf\\|tinygl…
Fable
thinking
Looking at what the Mesh module depends on — it's mostly self-contained with its own geometry handling, linking against FreeCADApp and possibly Part. I'm checking how different file formats are handled, particularly 3MF which uses zip compression, while STL, OBJ, PLY, and OFF are simpler formats, and glTF...
Actually, I should verify whether the glTF reader uses OCCT's built-in RWGltf module or a separate JSON library. Let me examine the ReaderGltf implementation and trace through the Mesh_LIBS dependencies to get a complete picture.
Fable
⚙ Bash {"command": "grep -rn \"Mesh_LIBS\\b\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Mesh/App/CMakeLists.txt 2>&1 | grep -v HISTDB | head; echo \"=== gltf includes ===\"; grep -n \"#include\\|RWGltf\\|TDocStd\\|XCAF\" /home/magik6k/l…
Fable
thinking
The glTF import/export is working now that OCCT is built statically, with the reader registered through the import initialization. I'm checking the mesh library configuration in those specific lines.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Mesh/App/CMakeLists.txt", "offset": 25, "limit": 15}
Fable
thinking
Mesh depends on FreeCADBase, FreeCADApp, and QtConcurrent, which could be problematic for single-threaded WebAssembly builds. QtConcurrent might still compile as a header-only library, but with thread support disabled via QT_FEATURE_thread=-1, it likely won't be available—that's a medium concern since Mesh relies on QtConcurrent for parallel algorithms, though I could potentially stub those out if needed.
I'm now compiling the comprehensive evidence into structured output. I've verified that 3MF is handled through Mesh using zipios, and recompute-on-load works as expected since openDocument triggers recomputation. Recent files are stored in user.cfg and won't persist across sessions, which …
Fable
I have gathered sufficient evidence. Let me compile the assessment.
Fable
⚙ StructuredOutput {"dimension": "File I/O & interchange formats + persistence", "worksNow": ["STEP import/export: native OCCT reader/writer (src/Mod/Import/App/ReaderStep.cpp, WriterStep.cpp), Python exposed as Import.readDXF-style API; registered via src/Mo…
Fable
thinking
I included a "requirements_note" field that isn't allowed in the schema, so I need to remove it and keep only "blockers" which is valid. Let me resubmit with just the correct fields.
Fable
⚙ StructuredOutput {"dimension": "File I/O & interchange formats + persistence", "worksNow": ["STEP import/export: native OCCT reader/writer (src/Mod/Import/App/ReaderStep.cpp, WriterStep.cpp); registered via src/Mod/Part/Init.py:38 (STEP with colors -> Impor…