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