From 9809ff852cc2ddeeacb1718dc1881bccae8fd150 Mon Sep 17 00:00:00 2001 From: forbes Date: Fri, 13 Feb 2026 14:10:36 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20.kc=20file=20format=20=E2=80=94=20Layer?= =?UTF-8?q?=201=20(format=20registration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked from feat/kc-file-format-layer1 (723a8c98d5b). - Document.cpp: checkFileName() accepts .kc extension - FreeCADInit.py: register .kc import type - Dialog filters: Save As, Save Copy, Merge, Project Utility - kc_format.py: DocumentObserver preserves silo/ entries across saves - InitGui.py: register kc_format observer on startup --- src/App/Document.cpp | 2 +- src/App/FreeCADInit.py | 2 +- src/Gui/CommandDoc.cpp | 2 +- src/Gui/Dialogs/DlgProjectUtility.cpp | 2 +- src/Gui/Document.cpp | 4 +- src/Mod/Create/InitGui.py | 11 +++++ src/Mod/Create/kc_format.py | 59 +++++++++++++++++++++++++++ 7 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/Mod/Create/kc_format.py diff --git a/src/App/Document.cpp b/src/App/Document.cpp index 71ed12bc2e..2cad92de03 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -1728,7 +1728,7 @@ static std::string checkFileName(const char* file) .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") ->GetBool("CheckExtension", true)) { const char* ext = strrchr(file, '.'); - if ((ext == nullptr) || !boost::iequals(ext + 1, "fcstd")) { + if ((ext == nullptr) || (!boost::iequals(ext + 1, "fcstd") && !boost::iequals(ext + 1, "kc"))) { if (ext && ext[1] == 0) { fn += "FCStd"; } diff --git a/src/App/FreeCADInit.py b/src/App/FreeCADInit.py index b997fb89e5..52ae425b45 100644 --- a/src/App/FreeCADInit.py +++ b/src/App/FreeCADInit.py @@ -420,7 +420,7 @@ App.__cmake__ = globals().get("cmake", []) # store unit test names App.__unit_test__ = [] -App.addImportType("FreeCAD document (*.FCStd)", "FreeCAD") +App.addImportType("FreeCAD document (*.FCStd *.kc)", "FreeCAD") # set to no gui, is overwritten by InitGui App.GuiUp = 0 diff --git a/src/Gui/CommandDoc.cpp b/src/Gui/CommandDoc.cpp index 75b1ca31b4..a57621ca04 100644 --- a/src/Gui/CommandDoc.cpp +++ b/src/Gui/CommandDoc.cpp @@ -529,7 +529,7 @@ void StdCmdMergeProjects::activated(int iMsg) Gui::getMainWindow(), QString::fromUtf8(QT_TR_NOOP("Merge Document")), FileDialog::getWorkingDirectory(), - QString::fromUtf8(QT_TR_NOOP("%1 document (*.FCStd)")).arg(exe) + QString::fromUtf8(QT_TR_NOOP("%1 document (*.FCStd *.kc)")).arg(exe) ); if (!project.isEmpty()) { FileDialog::setWorkingDirectory(project); diff --git a/src/Gui/Dialogs/DlgProjectUtility.cpp b/src/Gui/Dialogs/DlgProjectUtility.cpp index 513a590d62..07e824b605 100644 --- a/src/Gui/Dialogs/DlgProjectUtility.cpp +++ b/src/Gui/Dialogs/DlgProjectUtility.cpp @@ -46,7 +46,7 @@ DlgProjectUtility::DlgProjectUtility(QWidget* parent, Qt::WindowFlags fl) ui->setupUi(this); connect(ui->extractButton, &QPushButton::clicked, this, &DlgProjectUtility::extractButton); connect(ui->createButton, &QPushButton::clicked, this, &DlgProjectUtility::createButton); - ui->extractSource->setFilter(QStringLiteral("%1 (*.FCStd)").arg(tr("Project file"))); + ui->extractSource->setFilter(QStringLiteral("%1 (*.FCStd *.kc)").arg(tr("Project file"))); } /** diff --git a/src/Gui/Document.cpp b/src/Gui/Document.cpp index eb86c30c10..be172910da 100644 --- a/src/Gui/Document.cpp +++ b/src/Gui/Document.cpp @@ -1616,7 +1616,7 @@ bool Document::saveAs() getMainWindow(), QObject::tr("Save %1 Document").arg(exe), name, - QStringLiteral("%1 %2 (*.FCStd)").arg(exe, QObject::tr("Document")) + QStringLiteral("%1 %2 (*.FCStd *.kc)").arg(exe, QObject::tr("Document")) ); if (!fn.isEmpty()) { @@ -1739,7 +1739,7 @@ bool Document::saveCopy() getMainWindow(), QObject::tr("Save %1 Document").arg(exe), QString::fromUtf8(getDocument()->FileName.getValue()), - QObject::tr("%1 document (*.FCStd)").arg(exe) + QObject::tr("%1 document (*.FCStd *.kc)").arg(exe) ); if (!fn.isEmpty()) { const char* DocName = App::GetApplication().getDocumentName(getDocument()); diff --git a/src/Mod/Create/InitGui.py b/src/Mod/Create/InitGui.py index bdf0eed086..4283812f0c 100644 --- a/src/Mod/Create/InitGui.py +++ b/src/Mod/Create/InitGui.py @@ -48,6 +48,16 @@ setup_kindred_workbenches() FreeCAD.Console.PrintLog("Create GUI module initialized\n") +def _register_kc_format(): + """Register .kc file format round-trip preservation.""" + try: + import kc_format + + kc_format.register() + except Exception as e: + FreeCAD.Console.PrintLog(f"Create: kc_format registration skipped: {e}\n") + + # --------------------------------------------------------------------------- # Silo integration enhancements # --------------------------------------------------------------------------- @@ -162,6 +172,7 @@ def _check_for_updates(): try: from PySide.QtCore import QTimer + QTimer.singleShot(500, _register_kc_format) QTimer.singleShot(1500, _register_silo_origin) QTimer.singleShot(2000, _setup_silo_auth_panel) QTimer.singleShot(3000, _check_silo_first_start) diff --git a/src/Mod/Create/kc_format.py b/src/Mod/Create/kc_format.py new file mode 100644 index 0000000000..974c4a7076 --- /dev/null +++ b/src/Mod/Create/kc_format.py @@ -0,0 +1,59 @@ +""" +kc_format.py — .kc file format round-trip preservation. + +Caches silo/ ZIP entries before FreeCAD's C++ save rewrites the ZIP +from scratch, then re-injects them after save completes. +""" + +import os +import zipfile + +import FreeCAD + +# Cache: filepath -> {entry_name: bytes} +_silo_cache = {} + + +class _KcFormatObserver: + """Document observer that preserves silo/ entries across saves.""" + + def slotStartSaveDocument(self, doc, filename): + """Before save: cache silo/ entries from the existing file.""" + if not filename.lower().endswith(".kc"): + return + if not os.path.isfile(filename): + return + try: + with zipfile.ZipFile(filename, "r") as zf: + entries = {} + for name in zf.namelist(): + if name.startswith("silo/"): + entries[name] = zf.read(name) + if entries: + _silo_cache[filename] = entries + except Exception: + pass + + def slotFinishSaveDocument(self, doc, filename): + """After save: re-inject cached silo/ entries into the .kc ZIP.""" + if not filename.lower().endswith(".kc"): + _silo_cache.pop(filename, None) + return + entries = _silo_cache.pop(filename, None) + if not entries: + return + try: + with zipfile.ZipFile(filename, "a") as zf: + existing = set(zf.namelist()) + for name, data in entries.items(): + if name not in existing: + zf.writestr(name, data) + except Exception as e: + FreeCAD.Console.PrintWarning( + f"kc_format: failed to preserve silo/ entries: {e}\n" + ) + + +def register(): + """Connect to application-level save signals.""" + FreeCAD.addDocumentObserver(_KcFormatObserver())