phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)

Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
This commit is contained in:
forbes
2026-02-13 14:03:58 -06:00
parent 5d81f8ac16
commit 87a0af0b0f
1566 changed files with 32071 additions and 6155 deletions

View File

@@ -0,0 +1,54 @@
# Kindred Create core module
# Handles auto-loading of ztools and Silo addons
# Generate version.py from template with Kindred Create version
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/version.py.in
${CMAKE_CURRENT_BINARY_DIR}/version.py
@ONLY
)
# Install Python init files
install(
FILES
Init.py
InitGui.py
update_checker.py
${CMAKE_CURRENT_BINARY_DIR}/version.py
DESTINATION
Mod/Create
)
# Install ztools addon
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/ztools/ztools
DESTINATION
mods/ztools
)
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/ztools/CatppuccinMocha
DESTINATION
mods/ztools
)
install(
FILES
${CMAKE_SOURCE_DIR}/mods/ztools/package.xml
DESTINATION
mods/ztools
)
# Install Silo addon
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/silo/freecad/
DESTINATION
mods/silo/freecad
)
install(
DIRECTORY
${CMAKE_SOURCE_DIR}/mods/silo/silo-client/
DESTINATION
mods/silo/silo-client
)

48
src/Mod/Create/Init.py Normal file
View File

@@ -0,0 +1,48 @@
# Kindred Create - Core Module
# Console initialization - loads ztools and Silo addons
import os
import sys
import FreeCAD
def setup_kindred_addons():
"""Add Kindred Create addon paths and load their Init.py files."""
# Get the FreeCAD home directory (where src/Mod/Create is installed)
home = FreeCAD.getHomePath()
mods_dir = os.path.join(home, "mods")
# Define built-in addons with their paths relative to mods/
addons = [
("ztools", "ztools/ztools"), # mods/ztools/ztools/
("silo", "silo/freecad"), # mods/silo/freecad/
]
for name, subpath in addons:
addon_path = os.path.join(mods_dir, subpath)
if os.path.isdir(addon_path):
# Add to sys.path if not already present
if addon_path not in sys.path:
sys.path.insert(0, addon_path)
# Execute Init.py if it exists
init_file = os.path.join(addon_path, "Init.py")
if os.path.isfile(init_file):
try:
with open(init_file) as f:
exec_globals = globals().copy()
exec_globals["__file__"] = init_file
exec_globals["__name__"] = name
exec(compile(f.read(), init_file, "exec"), exec_globals)
FreeCAD.Console.PrintLog(f"Create: Loaded {name} Init.py\n")
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Create: Failed to load {name}: {e}\n"
)
else:
FreeCAD.Console.PrintLog(f"Create: Addon path not found: {addon_path}\n")
setup_kindred_addons()
FreeCAD.Console.PrintLog("Create module initialized\n")

171
src/Mod/Create/InitGui.py Normal file
View File

@@ -0,0 +1,171 @@
# Kindred Create - Core Module
# GUI initialization - loads ztools and Silo workbenches
import os
import sys
import FreeCAD
import FreeCADGui
def setup_kindred_workbenches():
"""Load Kindred Create addon workbenches."""
home = FreeCAD.getHomePath()
mods_dir = os.path.join(home, "mods")
addons = [
("ztools", "ztools/ztools"),
("silo", "silo/freecad"),
]
for name, subpath in addons:
addon_path = os.path.join(mods_dir, subpath)
if os.path.isdir(addon_path):
# Ensure path is in sys.path
if addon_path not in sys.path:
sys.path.insert(0, addon_path)
# Execute InitGui.py if it exists
init_gui_file = os.path.join(addon_path, "InitGui.py")
if os.path.isfile(init_gui_file):
try:
with open(init_gui_file) as f:
exec_globals = globals().copy()
exec_globals["__file__"] = init_gui_file
exec_globals["__name__"] = name
exec(
compile(f.read(), init_gui_file, "exec"),
exec_globals,
)
FreeCAD.Console.PrintLog(f"Create: Loaded {name} workbench\n")
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Create: Failed to load {name} GUI: {e}\n"
)
setup_kindred_workbenches()
FreeCAD.Console.PrintLog("Create GUI module initialized\n")
# ---------------------------------------------------------------------------
# Silo integration enhancements
# ---------------------------------------------------------------------------
def _check_silo_first_start():
"""Show Silo settings dialog on first startup if not yet configured."""
try:
param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/KindredSilo")
if not param.GetBool("FirstStartChecked", False):
param.SetBool("FirstStartChecked", True)
if not param.GetString("ApiUrl", ""):
FreeCADGui.runCommand("Silo_Settings")
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo first-start check skipped: {e}\n")
def _register_silo_origin():
"""Register Silo as a file origin so the origin selector can offer it."""
try:
import silo_commands # noqa: F401 - registers Silo commands
import silo_origin
silo_origin.register_silo_origin()
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo origin registration skipped: {e}\n")
def _setup_silo_auth_panel():
"""Dock the Silo authentication panel in the right-hand side panel."""
try:
from PySide import QtCore, QtWidgets
mw = FreeCADGui.getMainWindow()
if mw is None:
return
# Don't create duplicate panels
if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseAuth"):
return
import silo_commands
auth = silo_commands.SiloAuthDockWidget()
panel = QtWidgets.QDockWidget("Database Auth", mw)
panel.setObjectName("SiloDatabaseAuth")
panel.setWidget(auth.widget)
# Keep the auth object alive so its QTimer isn't destroyed while running
panel._auth = auth
mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel)
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo auth panel skipped: {e}\n")
def _setup_silo_activity_panel():
"""Show a dock widget with recent Silo database activity."""
try:
from PySide import QtCore, QtWidgets
mw = FreeCADGui.getMainWindow()
if mw is None:
return
# Don't create duplicate panels
if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseActivity"):
return
panel = QtWidgets.QDockWidget("Database Activity", mw)
panel.setObjectName("SiloDatabaseActivity")
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
activity_list = QtWidgets.QListWidget()
layout.addWidget(activity_list)
try:
import silo_commands
items = silo_commands._client.list_items()
if isinstance(items, list):
for item in items[:20]:
pn = item.get("part_number", "")
desc = item.get("description", "")
updated = item.get("updated_at", "")
if updated:
updated = updated[:10]
activity_list.addItem(f"{pn} - {desc} - {updated}")
if activity_list.count() == 0:
activity_list.addItem("(No items in database)")
except Exception:
activity_list.addItem("(Unable to connect to Silo database)")
panel.setWidget(widget)
mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel)
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo activity panel skipped: {e}\n")
def _check_for_updates():
"""Check for application updates in the background."""
try:
from update_checker import _run_update_check
_run_update_check()
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Update check skipped: {e}\n")
# Defer enhancements until the GUI event loop is running
try:
from PySide.QtCore import QTimer
QTimer.singleShot(1500, _register_silo_origin)
QTimer.singleShot(2000, _setup_silo_auth_panel)
QTimer.singleShot(3000, _check_silo_first_start)
QTimer.singleShot(4000, _setup_silo_activity_panel)
QTimer.singleShot(10000, _check_for_updates)
except Exception:
pass

View File

@@ -0,0 +1,165 @@
"""Kindred Create update checker.
Queries the Gitea releases API to determine if a newer version is
available. Designed to run in the background on startup without
blocking the UI.
"""
import json
import re
import urllib.request
from datetime import datetime, timezone
import FreeCAD
_RELEASES_URL = "https://git.kindred-systems.com/api/v1/repos/kindred/create/releases"
_PREF_PATH = "User parameter:BaseApp/Preferences/Mod/KindredCreate/Update"
_TIMEOUT = 5
def _parse_version(tag):
"""Parse a version tag like 'v0.1.3' into a comparable tuple.
Returns None if the tag doesn't match the expected pattern.
"""
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)$", tag)
if not m:
return None
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
def check_for_update(current_version):
"""Check if a newer release is available on Gitea.
Args:
current_version: Version string like "0.1.3".
Returns:
Dict with update info if a newer version exists, None otherwise.
Dict keys: version, tag, release_url, assets, body.
"""
current = _parse_version(current_version)
if current is None:
return None
req = urllib.request.Request(
f"{_RELEASES_URL}?limit=10",
headers={"Accept": "application/json"},
)
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
releases = json.loads(resp.read())
best = None
best_version = current
for release in releases:
if release.get("draft"):
continue
if release.get("prerelease"):
continue
tag = release.get("tag_name", "")
# Skip the rolling 'latest' tag
if tag == "latest":
continue
ver = _parse_version(tag)
if ver is None:
continue
if ver > best_version:
best_version = ver
best = release
if best is None:
return None
assets = []
for asset in best.get("assets", []):
assets.append(
{
"name": asset.get("name", ""),
"url": asset.get("browser_download_url", ""),
"size": asset.get("size", 0),
}
)
return {
"version": ".".join(str(x) for x in best_version),
"tag": best["tag_name"],
"release_url": best.get("html_url", ""),
"assets": assets,
"body": best.get("body", ""),
}
def _should_check(param):
"""Determine whether an update check should run now.
Args:
param: FreeCAD parameter group for update preferences.
Returns:
True if a check should be performed.
"""
if not param.GetBool("CheckEnabled", True):
return False
last_check = param.GetString("LastCheckTimestamp", "")
if not last_check:
return True
interval_days = param.GetInt("CheckIntervalDays", 1)
if interval_days <= 0:
return True
try:
last_dt = datetime.fromisoformat(last_check)
now = datetime.now(timezone.utc)
elapsed = (now - last_dt).total_seconds()
return elapsed >= interval_days * 86400
except (ValueError, TypeError):
return True
def _run_update_check():
"""Entry point called from the deferred startup timer."""
param = FreeCAD.ParamGet(_PREF_PATH)
if not _should_check(param):
return
try:
from version import VERSION
except ImportError:
FreeCAD.Console.PrintLog(
"Create: update check skipped — version module not available\n"
)
return
try:
result = check_for_update(VERSION)
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: update check failed: {e}\n")
return
# Record that we checked
param.SetString(
"LastCheckTimestamp",
datetime.now(timezone.utc).isoformat(),
)
if result is None:
FreeCAD.Console.PrintLog("Create: application is up to date\n")
return
skipped = param.GetString("SkippedVersion", "")
if result["version"] == skipped:
FreeCAD.Console.PrintLog(
f"Create: update {result['version']} available but skipped by user\n"
)
return
FreeCAD.Console.PrintMessage(
f"Kindred Create {result['version']} is available (current: {VERSION})\n"
)

View File

@@ -0,0 +1 @@
VERSION = "@KINDRED_CREATE_VERSION@"