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:
54
src/Mod/Create/CMakeLists.txt
Normal file
54
src/Mod/Create/CMakeLists.txt
Normal 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
48
src/Mod/Create/Init.py
Normal 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
171
src/Mod/Create/InitGui.py
Normal 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
|
||||
165
src/Mod/Create/update_checker.py
Normal file
165
src/Mod/Create/update_checker.py
Normal 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"
|
||||
)
|
||||
1
src/Mod/Create/version.py.in
Normal file
1
src/Mod/Create/version.py.in
Normal file
@@ -0,0 +1 @@
|
||||
VERSION = "@KINDRED_CREATE_VERSION@"
|
||||
Reference in New Issue
Block a user