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

Merged
forbes merged 1 commits from feat/kc-file-format-layer1 into main 2026-02-13 19:42:40 +00:00
8 changed files with 77 additions and 7 deletions

View File

@@ -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";
}

View File

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

View File

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

View File

@@ -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")));
}
/**

View File

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

View File

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

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