feat: .kc file format — Layer 1 (format registration) #186
Submodule mods/silo updated: be8783bf0a...fed72676bc
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
59
src/Mod/Create/kc_format.py
Normal file
59
src/Mod/Create/kc_format.py
Normal 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())
|
||||
Reference in New Issue
Block a user