feat: .kc file format — Layer 1 (format registration)

Cherry-picked from feat/kc-file-format-layer1 (723a8c98d5).
- 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
This commit is contained in:
forbes
2026-02-13 14:10:36 -06:00
parent 54031edede
commit 9809ff852c
7 changed files with 76 additions and 6 deletions

View File

@@ -1728,7 +1728,7 @@ static std::string checkFileName(const char* file)
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetBool("CheckExtension", true)) { ->GetBool("CheckExtension", true)) {
const char* ext = strrchr(file, '.'); 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) { if (ext && ext[1] == 0) {
fn += "FCStd"; fn += "FCStd";
} }

View File

@@ -420,7 +420,7 @@ App.__cmake__ = globals().get("cmake", [])
# store unit test names # store unit test names
App.__unit_test__ = [] App.__unit_test__ = []
App.addImportType("FreeCAD document (*.FCStd)", "FreeCAD") App.addImportType("FreeCAD document (*.FCStd *.kc)", "FreeCAD")
# set to no gui, is overwritten by InitGui # set to no gui, is overwritten by InitGui
App.GuiUp = 0 App.GuiUp = 0

View File

@@ -529,7 +529,7 @@ void StdCmdMergeProjects::activated(int iMsg)
Gui::getMainWindow(), Gui::getMainWindow(),
QString::fromUtf8(QT_TR_NOOP("Merge Document")), QString::fromUtf8(QT_TR_NOOP("Merge Document")),
FileDialog::getWorkingDirectory(), 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()) { if (!project.isEmpty()) {
FileDialog::setWorkingDirectory(project); FileDialog::setWorkingDirectory(project);

View File

@@ -46,7 +46,7 @@ DlgProjectUtility::DlgProjectUtility(QWidget* parent, Qt::WindowFlags fl)
ui->setupUi(this); ui->setupUi(this);
connect(ui->extractButton, &QPushButton::clicked, this, &DlgProjectUtility::extractButton); connect(ui->extractButton, &QPushButton::clicked, this, &DlgProjectUtility::extractButton);
connect(ui->createButton, &QPushButton::clicked, this, &DlgProjectUtility::createButton); 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")));
} }
/** /**

View File

@@ -1616,7 +1616,7 @@ bool Document::saveAs()
getMainWindow(), getMainWindow(),
QObject::tr("Save %1 Document").arg(exe), QObject::tr("Save %1 Document").arg(exe),
name, name,
QStringLiteral("%1 %2 (*.FCStd)").arg(exe, QObject::tr("Document")) QStringLiteral("%1 %2 (*.FCStd *.kc)").arg(exe, QObject::tr("Document"))
); );
if (!fn.isEmpty()) { if (!fn.isEmpty()) {
@@ -1739,7 +1739,7 @@ bool Document::saveCopy()
getMainWindow(), getMainWindow(),
QObject::tr("Save %1 Document").arg(exe), QObject::tr("Save %1 Document").arg(exe),
QString::fromUtf8(getDocument()->FileName.getValue()), QString::fromUtf8(getDocument()->FileName.getValue()),
QObject::tr("%1 document (*.FCStd)").arg(exe) QObject::tr("%1 document (*.FCStd *.kc)").arg(exe)
); );
if (!fn.isEmpty()) { if (!fn.isEmpty()) {
const char* DocName = App::GetApplication().getDocumentName(getDocument()); const char* DocName = App::GetApplication().getDocumentName(getDocument());

View File

@@ -48,6 +48,16 @@ setup_kindred_workbenches()
FreeCAD.Console.PrintLog("Create GUI module initialized\n") 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 # Silo integration enhancements
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -162,6 +172,7 @@ def _check_for_updates():
try: try:
from PySide.QtCore import QTimer from PySide.QtCore import QTimer
QTimer.singleShot(500, _register_kc_format)
QTimer.singleShot(1500, _register_silo_origin) QTimer.singleShot(1500, _register_silo_origin)
QTimer.singleShot(2000, _setup_silo_auth_panel) QTimer.singleShot(2000, _setup_silo_auth_panel)
QTimer.singleShot(3000, _check_silo_first_start) QTimer.singleShot(3000, _check_silo_first_start)

View File

@@ -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())