From 723a8c98d5bb7f2ba24047d5bf3e075a044d09b7 Mon Sep 17 00:00:00 2001 From: forbes Date: Fri, 13 Feb 2026 13:39:42 -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 Register .kc as a recognized file type alongside .FCStd: C++ changes: - Document.cpp: checkFileName() accepts .kc extension - Gui/Document.cpp: Save As and Save Copy dialogs include *.kc - CommandDoc.cpp: Merge document dialog includes *.kc - DlgProjectUtility.cpp: Project utility dialog includes *.kc Python changes: - FreeCADInit.py: register *.kc in import type system - kc_format.py: DocumentObserver that preserves silo/ ZIP entries across saves (caches before save, re-injects after save) - InitGui.py: register kc_format observer on startup Silo integration: - get_cad_file_path() generates .kc paths for new files - find_file_by_part_number() finds both .kc and .FCStd, preferring .kc - search_local_files() lists both .kc and .FCStd files The .kc format is a superset of .FCStd with a silo/ directory containing Kindred platform metadata. See docs/KC_SPECIFICATION.md. --- mods/silo | 2 +- 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 +++++++++++++++++++++++++++ 8 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/Mod/Create/kc_format.py diff --git a/mods/silo b/mods/silo index be8783bf0a..fed72676bc 160000 --- a/mods/silo +++ b/mods/silo @@ -1 +1 @@ -Subproject commit be8783bf0a98fca9bc89f6bb02bdafc2c3911e4e +Subproject commit fed72676bc52ed63cc95095acba7f6f4eae3ac6a diff --git a/src/App/Document.cpp b/src/App/Document.cpp index 0e63148238..ed98c0f499 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -1680,7 +1680,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 c79ddeda74..8a00a52742 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()) -- 2.49.1