feat: .kc Layer 1 — manifest auto-creation and platform file associations
Some checks failed
Build and Test / build (pull_request) Has been cancelled

kc_format.py:
- Auto-create silo/manifest.json with default fields (UUID, timestamps,
  username) when saving a .kc file that lacks one
- Update modified_at timestamp on each save
- KC_VERSION = 1.0

Platform integration:
- kindred-create.desktop: add application/x-kindred-create MIME type
- kindred-create.xml: register .kc glob patterns with dedicated MIME type
  (application/x-kindred-create), separate from .fcstd type
This commit is contained in:
forbes
2026-02-14 10:57:46 -06:00
parent 0b3d4b1274
commit 04835c3629
3 changed files with 63 additions and 13 deletions

View File

@@ -7,7 +7,7 @@ Icon=kindred-create
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Graphics;Science;Engineering; Categories=Graphics;Science;Engineering;
MimeType=application/x-extension-fcstd;x-scheme-handler/kindred; MimeType=application/x-extension-fcstd;application/x-kindred-create;x-scheme-handler/kindred;
Keywords=CAD;3D;modeling;engineering;design;parametric; Keywords=CAD;3D;modeling;engineering;design;parametric;
StartupNotify=true StartupNotify=true
StartupWMClass=KindredCreate StartupWMClass=KindredCreate

View File

@@ -1,11 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info"> <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-extension-fcstd"> <mime-type type="application/x-extension-fcstd">
<comment>Kindred Create Document</comment> <comment>FreeCAD Document</comment>
<comment xml:lang="en">Kindred Create Document</comment> <comment xml:lang="en">FreeCAD Document</comment>
<icon name="kindred-create"/> <icon name="kindred-create"/>
<glob pattern="*.FCStd"/> <glob pattern="*.FCStd"/>
<glob pattern="*.fcstd"/> <glob pattern="*.fcstd"/>
<glob pattern="*.FCSTD"/> <glob pattern="*.FCSTD"/>
</mime-type> </mime-type>
<mime-type type="application/x-kindred-create">
<comment>Kindred Create Document</comment>
<comment xml:lang="en">Kindred Create Document</comment>
<icon name="kindred-create"/>
<glob pattern="*.kc"/>
<glob pattern="*.KC"/>
</mime-type>
</mime-info> </mime-info>

View File

@@ -1,18 +1,40 @@
""" """
kc_format.py — .kc file format round-trip preservation. kc_format.py — .kc file format support.
Caches silo/ ZIP entries before FreeCAD's C++ save rewrites the ZIP Handles two responsibilities:
from scratch, then re-injects them after save completes. 1. Round-trip preservation: caches silo/ ZIP entries before FreeCAD's C++
save rewrites the ZIP from scratch, then re-injects them after save.
2. Manifest auto-creation: ensures every .kc file has silo/manifest.json.
""" """
import json
import os import os
import uuid
import zipfile import zipfile
from datetime import datetime, timezone
import FreeCAD import FreeCAD
# Cache: filepath -> {entry_name: bytes} # Cache: filepath -> {entry_name: bytes}
_silo_cache = {} _silo_cache = {}
KC_VERSION = "1.0"
def _default_manifest():
"""Generate a default silo/manifest.json for new .kc files."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
username = os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
return {
"kc_version": KC_VERSION,
"silo_instance": None,
"part_uuid": str(uuid.uuid4()),
"revision_hash": None,
"created_at": now,
"modified_at": now,
"created_by": username,
}
class _KcFormatObserver: class _KcFormatObserver:
"""Document observer that preserves silo/ entries across saves.""" """Document observer that preserves silo/ entries across saves."""
@@ -35,22 +57,43 @@ class _KcFormatObserver:
pass pass
def slotFinishSaveDocument(self, doc, filename): def slotFinishSaveDocument(self, doc, filename):
"""After save: re-inject cached silo/ entries into the .kc ZIP.""" """After save: re-inject cached silo/ entries and ensure manifest."""
if not filename.lower().endswith(".kc"): if not filename.lower().endswith(".kc"):
_silo_cache.pop(filename, None) _silo_cache.pop(filename, None)
return return
entries = _silo_cache.pop(filename, None) entries = _silo_cache.pop(filename, None)
if not entries:
return
try: try:
with zipfile.ZipFile(filename, "a") as zf: with zipfile.ZipFile(filename, "a") as zf:
existing = set(zf.namelist()) existing = set(zf.namelist())
for name, data in entries.items(): # Re-inject cached silo/ entries
if name not in existing: if entries:
zf.writestr(name, data) for name, data in entries.items():
if name not in existing:
zf.writestr(name, data)
existing.add(name)
# Ensure silo/manifest.json exists
if "silo/manifest.json" not in existing:
manifest = _default_manifest()
zf.writestr(
"silo/manifest.json",
json.dumps(manifest, indent=2) + "\n",
)
else:
# Update modified_at timestamp
raw = zf.read("silo/manifest.json")
manifest = json.loads(raw)
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
if manifest.get("modified_at") != now:
manifest["modified_at"] = now
# ZipFile append mode can't overwrite; write new entry
# (last duplicate wins in most ZIP readers)
zf.writestr(
"silo/manifest.json",
json.dumps(manifest, indent=2) + "\n",
)
except Exception as e: except Exception as e:
FreeCAD.Console.PrintWarning( FreeCAD.Console.PrintWarning(
f"kc_format: failed to preserve silo/ entries: {e}\n" f"kc_format: failed to update .kc silo/ entries: {e}\n"
) )