Compare commits

..

28 Commits

Author SHA1 Message Date
forbes
7168c402b8 fix(build): fix DlgSettingsGeneral::applyMenuIconSize visibility and namespace
- Move applyMenuIconSize to public access so StartupProcess can call it
- Add Dialog:: namespace qualifier in StartupProcess.cpp
2026-02-08 17:01:41 -06:00
forbes
af3137dc6c chore: update silo submodule to latest main
New features:
- SSE reconnect with exponential backoff and terminal state handling
- Silo_Diag connection diagnostics command
- Server mode UI reflection in client
- Silo-aware start panel
- Database Activity pane with comments, interaction, and badges
2026-02-08 16:25:18 -06:00
forbes
173046846f fix(theme): Report View defaults, Help rebranding, hyperlink color, checkbox consistency
Report View (fixes #41):
- Enable error and log message recording by default
- Auto-show report view on errors and warnings

Help page rebranding (fixes #42):
- Remove disabled GitHub (online) option
- Remove PySide Web components disclaimer
- Rebrand 'In a FreeCAD tab' to 'In an application tab'
- Clean up FreeCAD-specific references in tooltips

Hyperlink color (fixes #43):
- Add QLabel[haslink] rule to set QPalette::Link to Catppuccin Lavender (#b4befe)
- Applied across all three stylesheet copies

Checkbox/indicator consistency (fixes #44):
- Add QGroupBox::indicator styling to match QCheckBox::indicator
- Fills checkable group boxes (e.g. Notification Area) with the same
  themed filled-square style instead of OS default checkmark
2026-02-08 15:42:16 -06:00
b6ab14cc6f Merge branch 'main' into fix/ui-appearance-polish
Some checks failed
Build and Test / build (pull_request) Failing after 17m32s
2026-02-08 21:36:51 +00:00
da46073d68 Merge pull request 'feat: expose version to Python and add update checker (#28, #29)' (#45) from feat/update-checker into main
Some checks failed
Build and Test / build (push) Has been cancelled
Reviewed-on: #45
2026-02-08 21:36:40 +00:00
forbes
262dfa583d fix(defaults): tune Document, Selection, and Notification preference defaults
Some checks failed
Build and Test / build (pull_request) Failing after 12m28s
Document (fixes #37):
- Increase max undo/redo steps from 20 to 50
- Reduce auto-save interval from 15 to 5 minutes
- Increase max backup files from 1 to 3
- Set license to Other with blank URL (removes Wikipedia link)

Selection (fixes #38):
- Enable preselect-on-tree-hover (PreSelection)
- Enable auto-switch to 3D view (SyncView)
- Enable auto-expand tree item (SyncSelection)

Notification Area (fixes #40):
- Reduce max notification list from 1000 to 100
- Reduce max concurrent popups from 15 to 3
- Reduce bubble width from 800px to 400px
- Reduce max popup duration from 20s to 10s
- Reduce min popup duration from 5s to 3s
2026-02-08 13:36:57 -06:00
forbes
cc5ba638d1 fix(gui): UI appearance polish — Wayland scaling, menu icon size pref, dialog cleanup
Some checks failed
Build and Test / build (pull_request) Failing after 13m13s
- Fix hazy text on Wayland by setting QT_SCALE_FACTOR_ROUNDING_POLICY=RoundPreferFloor
  and QT_ENABLE_HIGHDPI_SCALING=1 in deb and AppImage launchers; remove forced
  QT_QPA_PLATFORM=xcb from AppImage to allow native Wayland (fixes #33)

- Add menu icon size preference selector (Small/Medium/Large/Extra large) in
  General preferences, applied via QMenu stylesheet at startup and on save (fixes #46)

- Replace verbose theme customization banner with concise 'Kindred Create Theme'
  label in UI preferences; split Overlay group into 'Panel Visibility' and
  'Overlay Interaction' sections (fixes #35)

- Remove Addon Manager branding label from General preferences, change recent
  file list default from 4 to 10 (fixes #36)
2026-02-08 13:27:22 -06:00
forbes
35302154ae feat: expose version to Python and add update checker (#28, #29)
All checks were successful
Build and Test / build (pull_request) Successful in 1h11m28s
Issue #28: Add version.py.in CMake template that injects
KINDRED_CREATE_VERSION at build time, making the Kindred Create
version available to Python code via 'from version import VERSION'.

Issue #29: Add update_checker.py that queries the Gitea releases
API on startup (10s deferred) to check for newer versions. Uses
stdlib urllib only, 5s timeout, never blocks the UI. Respects
user preferences for check interval, enable/disable, and skipped
versions. Logs results to Console for now — UI notification will
follow in issue #30.

Closes #28
Closes #29
2026-02-08 13:11:08 -06:00
forbes
be7f1d9221 fix: update silo submodule — fix origin registration import
All checks were successful
Build and Test / build (push) Successful in 1h7m24s
Silo origin was not appearing in the origin selector because
silo_origin.py used a relative import (from .silo_commands) which
fails since the directory is not a Python package. Updated submodule
includes the fix to use absolute imports.
2026-02-08 10:36:14 -06:00
forbes
10b5c9d584 fix(gui): wire Manage Origins dialog to Silo_Settings command
Some checks failed
Build and Test / build (push) Has been cancelled
Replace stub QMessageBox in Add Silo and Edit Origin buttons with
actual calls to the Silo_Settings command, so users can configure
their Silo connection from the origin manager dialog.
2026-02-08 10:31:41 -06:00
forbes
ee839c23b8 fix(ci): add error handling to release creation API calls
Some checks failed
Build and Test / build (push) Has been cancelled
Release Build / build-linux (push) Successful in 1h26m59s
Release Build / publish-release (push) Has been cancelled
Print HTTP status and response body on failure instead of crashing
with a cryptic KeyError when the Gitea API returns an error.
2026-02-08 10:22:26 -06:00
forbes
67f825c305 fix(ci): filter dereferenced tag refs from git ls-remote output
Some checks failed
Build and Test / build (push) Successful in 1h7m46s
Release Build / build-linux (push) Successful in 1h25m16s
Release Build / publish-release (push) Failing after 5m17s
2026-02-07 22:22:55 -06:00
440df2a9be Merge pull request 'fix(gui): merge Silo toolbar into File toolbar via origin system (#65)' (#27) from fix/merge-silo-toolbar into main
Some checks failed
Build and Test / build (push) Failing after 1m33s
Release Build / build-linux (push) Failing after 1m58s
Release Build / publish-release (push) Has been skipped
2026-02-08 03:29:24 +00:00
forbes
1750949afd fix(gui): merge Silo toolbar into File toolbar via origin system (#65)
Some checks failed
Build and Test / build (pull_request) Failing after 1m41s
- Remove separate Silo workbench toolbar (now redundant)
- Remove SiloMenuManipulator (Std commands already delegate to origins)
- Remove Silo_ToggleMode (origin selector handles mode switching)
- Register Silo origin at startup via Create module
- Update docs to reflect unified origin architecture
2026-02-07 21:29:05 -06:00
b2c6fc2ebc Merge pull request 'fix(silo): workbench bug fixes and submodule updates' (#26) from fix/silo-workbench-bugs into main
Some checks failed
Build and Test / build (push) Failing after 1m52s
2026-02-08 03:11:07 +00:00
forbes
d0c67455dc chore: update ztools and OndselSolver submodules
Some checks failed
Build and Test / build (pull_request) Failing after 3m25s
2026-02-07 21:01:00 -06:00
forbes
62906b0269 test(gui): add unit tests for OriginManager and LocalFileOrigin
Some checks failed
Build and Test / build (push) Has been cancelled
Add 20 GTest cases covering the Kindred Origin system:

- LocalFileOriginTest: identity methods and capability flags
- OriginManagerTest: registration lifecycle, current origin
  selection, signal emissions, and fallback behavior
- OriginManagerDocTest: document ownership via SiloItemId
  property detection, origin association, and null safety

Uses MockOrigin stub for testing OriginManager without real
PLM backends.
2026-02-07 20:56:08 -06:00
99bc7629e7 Merge pull request 'fix(gui): widen origin selector widget and update silo submodule' (#25) from fix/silo-workbench-bugs into main
All checks were successful
Build and Test / build (push) Successful in 1h3m36s
Reviewed-on: #25
2026-02-07 20:34:56 +00:00
forbes
d95c850b7b fix(gui): widen origin selector widget and update silo submodule
Some checks failed
Build and Test / build (pull_request) Has been cancelled
- Increase OriginSelectorWidget max width from 120 to 160px for longer
  origin names
- Set explicit SizePolicy::Preferred for better toolbar layout
- Update silo submodule to fix/silo-workbench-bugs with auth and menu fixes
2026-02-07 14:34:23 -06:00
1f7dae4f11 Merge pull request 'art: update kindred icon set' (#24) from art/update-kindred-icons into main
All checks were successful
Build and Test / build (push) Successful in 1h5m8s
Reviewed-on: #24
2026-02-07 16:45:30 +00:00
1d7e4e2eef Merge pull request 'fix(ci): fetch only latest tag, add patchelf dep, update docs' (#23) from docs/update-ci-and-overview into main
Some checks failed
Build and Test / build (push) Has been cancelled
Reviewed-on: #23
2026-02-07 16:45:20 +00:00
forbes
fe50b1595e art: update kindred icon set
Some checks failed
Build and Test / build (pull_request) Has been cancelled
Update Document, DrawStyle, Link, PartDesign, and document-* SVGs.
Remove DrawStyleHiddenLine.svg.
2026-02-07 10:44:24 -06:00
forbes
0e5a259d14 fix(ci): fetch only latest tag and add patchelf build dep
Some checks failed
Build and Test / build (pull_request) Has been cancelled
- Change both build.yml and release.yml to fetch only the latest v* tag
  via git ls-remote instead of fetching all tags
- Add patchelf as a Linux build dependency in recipe.yaml to fix
  rattler-build packaging failure
2026-02-07 10:42:48 -06:00
forbes
bfb2728f8d docs: update OVERVIEW.md and CI_CD.md to match current repo state
- Fix stale submodule URLs (gitea.kindred.internal -> git.kindred-systems.com)
- Update silo submodule name and commit, add note about repo split
- Document actual release mechanism (curl API, not release-action)
- Mark macOS/Windows builds as disabled in platform matrix
- Update ccache docs: date-based key rotation, rattler-build env forwarding
- Document disk cleanup steps and runner cleanup daemon
- Update appimagetool extraction note (FUSE unavailable in containers)
2026-02-07 09:34:41 -06:00
forbes
7bec3d5c3b fix(ci): fix release creation and artifact collection
All checks were successful
Build and Test / build (push) Successful in 1h5m11s
- Exclude appimagetool from artifact upload (FreeCAD_*.AppImage glob)
- Build release JSON payload in Python to fix false/False type error
  (shell 'false' is not valid Python; use bool with re.search instead)
2026-02-07 08:20:24 -06:00
forbes
145c29d7d6 fix(build): pin icu<76 in build deps to match host
The build environment (via qt6-main) was pulling ICU 78 headers while
the host/runtime environment had ICU 75 pinned. The compiled binary
contained icu_78 mangled symbols that fail to resolve against ICU 75
shared libs at runtime. Pin icu>=75,<76 in build deps to match.
2026-02-07 08:18:46 -06:00
forbes
8ba7b73aa8 fix(ci): fix ccache strategy and add runner cleanup
- Cache key: replace run_id (unique per build) with date-based key
  so entries are reused within the same day and rotate daily
- Skip cache save on exact hit (act_runner can't overwrite keys)
- build.sh: set CCACHE_DIR/CCACHE_BASEDIR inside rattler-build's
  isolated env so release builds actually use the cached directory
- build.sh: print ccache stats at end for diagnostics
- Add disk space cleanup step to build.yml (matching release.yml)
- Remove cross-build cache fallback in release.yml (different -O flags)
- Add runner cleanup daemon (.gitea/runner/) with systemd timer
  to purge stale cache entries, Docker data, and old workspaces
2026-02-07 08:18:00 -06:00
forbes
1e4deea130 fix(ci): add disk space cleanup steps to release workflow
Some checks failed
Build and Test / build (push) Failing after 15m24s
The runner host disk is at 95% capacity. Add cleanup steps:
- Remove pre-installed bloat (dotnet, android, boost) at start
- Clear rattler package cache and pixi build dirs between
  AppImage and .deb build steps
2026-02-06 20:01:33 -06:00
59 changed files with 2140 additions and 356 deletions

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Kindred Create CI runner disk cleanup
After=docker.service
[Service]
Type=oneshot
ExecStart=/opt/runner/cleanup.sh
Environment=CLEANUP_THRESHOLD=85
Environment=CACHE_MAX_AGE_DAYS=7
StandardOutput=append:/var/log/runner-cleanup.log
StandardError=append:/var/log/runner-cleanup.log

220
.gitea/runner/cleanup.sh Executable file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env bash
# Runner disk cleanup script for Kindred Create CI/CD
#
# Designed to run as a cron job on the pubworker host:
# */30 * * * * /path/to/cleanup.sh >> /var/log/runner-cleanup.log 2>&1
#
# Or install the systemd timer (see cleanup.timer / cleanup.service).
#
# What it cleans:
# 1. Docker: stopped containers, dangling images, build cache
# 2. act_runner action cache: keeps only the newest entry per key prefix
# 3. act_runner workspaces: removes leftover build workspaces
# 4. System: apt cache, old logs
#
# What it preserves:
# - The current runner container and its image
# - The most recent cache entry per prefix (so ccache hits still work)
# - Everything outside of known CI paths
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration -- adjust these to match your runner setup
# ---------------------------------------------------------------------------
# Disk usage threshold (percent) -- only run aggressive cleanup above this
THRESHOLD=${CLEANUP_THRESHOLD:-85}
# act_runner cache directory (default location)
CACHE_DIR=${CACHE_DIR:-/root/.cache/actcache}
# act_runner workspace directories
WORKSPACES=(
"/root/.cache/act"
"/workspace"
)
# Maximum age (days) for cache entries before unconditional deletion
CACHE_MAX_AGE_DAYS=${CACHE_MAX_AGE_DAYS:-7}
# Maximum age (days) for Docker images not used by running containers
DOCKER_IMAGE_MAX_AGE=${DOCKER_IMAGE_MAX_AGE:-48h}
# Log prefix
LOG_PREFIX="[runner-cleanup]"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') ${LOG_PREFIX} $*"; }
disk_usage_pct() {
df --output=pcent / | tail -1 | tr -dc '0-9'
}
bytes_to_human() {
numfmt --to=iec-i --suffix=B "$1" 2>/dev/null || echo "${1}B"
}
# ---------------------------------------------------------------------------
# Phase 1: Check if cleanup is needed
# ---------------------------------------------------------------------------
usage=$(disk_usage_pct)
log "Disk usage: ${usage}% (threshold: ${THRESHOLD}%)"
if [ "$usage" -lt "$THRESHOLD" ]; then
log "Below threshold, running light cleanup only"
AGGRESSIVE=false
else
log "Above threshold, running aggressive cleanup"
AGGRESSIVE=true
fi
# ---------------------------------------------------------------------------
# Phase 2: Docker cleanup (always runs, safe)
# ---------------------------------------------------------------------------
log "--- Docker cleanup ---"
# Remove stopped containers
stopped=$(docker ps -aq --filter status=exited --filter status=dead 2>/dev/null | wc -l)
if [ "$stopped" -gt 0 ]; then
docker rm $(docker ps -aq --filter status=exited --filter status=dead) 2>/dev/null || true
log "Removed ${stopped} stopped containers"
fi
# Remove dangling images (untagged layers)
dangling=$(docker images -q --filter dangling=true 2>/dev/null | wc -l)
if [ "$dangling" -gt 0 ]; then
docker rmi $(docker images -q --filter dangling=true) 2>/dev/null || true
log "Removed ${dangling} dangling images"
fi
# Prune build cache
docker builder prune -f --filter "until=${DOCKER_IMAGE_MAX_AGE}" 2>/dev/null || true
log "Pruned Docker build cache older than ${DOCKER_IMAGE_MAX_AGE}"
if [ "$AGGRESSIVE" = true ]; then
# Remove all images not used by running containers
running_images=$(docker ps -q 2>/dev/null | xargs -r docker inspect --format='{{.Image}}' | sort -u)
all_images=$(docker images -q 2>/dev/null | sort -u)
for img in $all_images; do
if ! echo "$running_images" | grep -q "$img"; then
docker rmi -f "$img" 2>/dev/null || true
fi
done
log "Removed unused Docker images (aggressive)"
# Prune volumes
docker volume prune -f 2>/dev/null || true
log "Pruned unused Docker volumes"
fi
# ---------------------------------------------------------------------------
# Phase 3: act_runner action cache cleanup
# ---------------------------------------------------------------------------
log "--- Action cache cleanup ---"
if [ -d "$CACHE_DIR" ]; then
before=$(du -sb "$CACHE_DIR" 2>/dev/null | cut -f1)
# Delete cache entries older than max age
find "$CACHE_DIR" -type f -mtime "+${CACHE_MAX_AGE_DAYS}" -delete 2>/dev/null || true
find "$CACHE_DIR" -type d -empty -delete 2>/dev/null || true
after=$(du -sb "$CACHE_DIR" 2>/dev/null | cut -f1)
freed=$((before - after))
log "Cache cleanup freed $(bytes_to_human $freed) (entries older than ${CACHE_MAX_AGE_DAYS}d)"
else
log "Cache directory not found: ${CACHE_DIR}"
# Try common alternative locations
for alt in /var/lib/act_runner/.cache/actcache /home/*/.cache/actcache; do
if [ -d "$alt" ]; then
log "Found cache at: $alt (update CACHE_DIR config)"
CACHE_DIR="$alt"
find "$CACHE_DIR" -type f -mtime "+${CACHE_MAX_AGE_DAYS}" -delete 2>/dev/null || true
find "$CACHE_DIR" -type d -empty -delete 2>/dev/null || true
break
fi
done
fi
# ---------------------------------------------------------------------------
# Phase 4: Workspace cleanup
# ---------------------------------------------------------------------------
log "--- Workspace cleanup ---"
for ws in "${WORKSPACES[@]}"; do
if [ -d "$ws" ]; then
# Remove workspace dirs not modified in the last 2 hours
# (active builds should be touching files continuously)
before=$(du -sb "$ws" 2>/dev/null | cut -f1)
find "$ws" -mindepth 1 -maxdepth 1 -type d -mmin +120 -exec rm -rf {} + 2>/dev/null || true
after=$(du -sb "$ws" 2>/dev/null | cut -f1)
freed=$((before - after))
if [ "$freed" -gt 0 ]; then
log "Workspace $ws: freed $(bytes_to_human $freed)"
fi
fi
done
# ---------------------------------------------------------------------------
# Phase 5: System cleanup
# ---------------------------------------------------------------------------
log "--- System cleanup ---"
# apt cache
apt-get clean 2>/dev/null || true
# Truncate large log files (keep last 1000 lines)
for logfile in /var/log/syslog /var/log/daemon.log /var/log/kern.log; do
if [ -f "$logfile" ] && [ "$(stat -c%s "$logfile" 2>/dev/null)" -gt 104857600 ]; then
tail -1000 "$logfile" > "${logfile}.tmp" && mv "${logfile}.tmp" "$logfile"
log "Truncated $logfile (was >100MB)"
fi
done
# Journal logs older than 3 days
journalctl --vacuum-time=3d 2>/dev/null || true
# ---------------------------------------------------------------------------
# Phase 6: Emergency cleanup (only if still critical)
# ---------------------------------------------------------------------------
usage=$(disk_usage_pct)
if [ "$usage" -gt 95 ]; then
log "CRITICAL: Still at ${usage}% after cleanup"
# Nuclear option: remove ALL docker data except running containers
docker system prune -af --volumes 2>/dev/null || true
log "Ran docker system prune -af --volumes"
# Clear entire action cache
if [ -d "$CACHE_DIR" ]; then
rm -rf "${CACHE_DIR:?}/"*
log "Cleared entire action cache"
fi
usage=$(disk_usage_pct)
log "After emergency cleanup: ${usage}%"
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
usage=$(disk_usage_pct)
log "Cleanup complete. Disk usage: ${usage}%"
# Report top space consumers for diagnostics
log "Top 10 directories under /var:"
du -sh /var/*/ 2>/dev/null | sort -rh | head -10 | while read -r line; do
log " $line"
done

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Run CI runner cleanup every 30 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -22,6 +22,17 @@ jobs:
DEBIAN_FRONTEND: noninteractive
steps:
- name: Free disk space
run: |
echo "=== Disk usage before cleanup ==="
df -h /
rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache 2>/dev/null || true
rm -rf /usr/local/share/boost /usr/share/swift 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
apt-get clean 2>/dev/null || true
echo "=== Disk usage after cleanup ==="
df -h /
- name: Install system prerequisites
run: |
apt-get update -qq
@@ -36,8 +47,12 @@ jobs:
submodules: recursive
fetch-depth: 1
- name: Fetch tags (for git describe)
run: git fetch --no-recurse-submodules --force --depth=1 origin '+refs/tags/*:refs/tags/*'
- name: Fetch latest tag (for git describe)
run: |
latest_tag=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | grep -v '\^{}' | head -n1 | awk '{print $2}')
if [ -n "$latest_tag" ]; then
git fetch --no-recurse-submodules --force --depth=1 origin "+${latest_tag}:${latest_tag}"
fi
- name: Install pixi
run: |
@@ -46,12 +61,16 @@ jobs:
export PATH="$HOME/.pixi/bin:$PATH"
pixi --version
- name: Compute cache date key
id: cache-date
run: echo "date=$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT
- name: Restore ccache
id: ccache-restore
uses: https://github.com/actions/cache/restore@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-build-${{ github.ref_name }}-${{ github.run_id }}
key: ccache-build-${{ github.ref_name }}-${{ steps.cache-date.outputs.date }}
restore-keys: |
ccache-build-${{ github.ref_name }}-
ccache-build-main-
@@ -60,6 +79,7 @@ jobs:
run: |
mkdir -p $CCACHE_DIR
pixi run ccache -z
pixi run ccache -p
- name: Configure (CMake)
run: pixi run cmake --preset conda-linux-release
@@ -71,11 +91,11 @@ jobs:
run: pixi run ccache -s
- name: Save ccache
if: always()
if: always() && steps.ccache-restore.outputs.cache-hit != 'true'
uses: https://github.com/actions/cache/save@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-build-${{ github.ref_name }}-${{ github.run_id }}
key: ccache-build-${{ github.ref_name }}-${{ steps.cache-date.outputs.date }}
- name: Run C++ unit tests
continue-on-error: true

View File

@@ -31,6 +31,18 @@ jobs:
DEBIAN_FRONTEND: noninteractive
steps:
- name: Free disk space
run: |
echo "=== Disk usage before cleanup ==="
df -h /
# Remove pre-installed bloat common in runner images
rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache 2>/dev/null || true
rm -rf /usr/local/share/boost /usr/share/swift 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
apt-get clean 2>/dev/null || true
echo "=== Disk usage after cleanup ==="
df -h /
- name: Install system prerequisites
run: |
apt-get update -qq
@@ -45,8 +57,12 @@ jobs:
submodules: recursive
fetch-depth: 1
- name: Fetch tags
run: git fetch --tags --force --no-recurse-submodules origin
- name: Fetch latest tag (for git describe)
run: |
latest_tag=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | grep -v '\^{}' | head -n1 | awk '{print $2}')
if [ -n "$latest_tag" ]; then
git fetch --no-recurse-submodules --force --depth=1 origin "+${latest_tag}:${latest_tag}"
fi
- name: Install pixi
run: |
@@ -55,20 +71,26 @@ jobs:
export PATH="$HOME/.pixi/bin:$PATH"
pixi --version
- name: Compute cache date key
id: cache-date
run: echo "date=$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT
- name: Restore ccache
id: ccache-restore
uses: https://github.com/actions/cache/restore@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-release-linux-${{ github.run_id }}
key: ccache-release-linux-${{ steps.cache-date.outputs.date }}
restore-keys: |
ccache-release-linux-
ccache-build-main-
- name: Prepare ccache
run: |
mkdir -p $CCACHE_DIR
# Ensure ccache is accessible to rattler-build's subprocess
export PATH="$(pixi run bash -c 'echo $PATH')"
pixi run ccache -z
pixi run ccache -p
- name: Build release package (AppImage)
working-directory: package/rattler-build
@@ -80,11 +102,19 @@ jobs:
run: pixi run ccache -s
- name: Save ccache
if: always()
if: always() && steps.ccache-restore.outputs.cache-hit != 'true'
uses: https://github.com/actions/cache/save@v4
with:
path: /tmp/ccache-kindred-create
key: ccache-release-linux-${{ github.run_id }}
key: ccache-release-linux-${{ steps.cache-date.outputs.date }}
- name: Clean up intermediate build files
run: |
# Remove pixi package cache and build work dirs to free space for .deb
rm -rf package/rattler-build/.pixi/build 2>/dev/null || true
find /root/.cache/rattler -type f -delete 2>/dev/null || true
echo "=== Disk usage after cleanup ==="
df -h /
- name: Build .deb package
run: |
@@ -105,7 +135,7 @@ jobs:
with:
name: release-linux
path: |
package/rattler-build/linux/*.AppImage
package/rattler-build/linux/FreeCAD_*.AppImage
package/rattler-build/linux/*.deb
package/rattler-build/linux/*-SHA256.txt
package/rattler-build/linux/*.sha256
@@ -321,12 +351,13 @@ jobs:
REPO: ${{ github.repository }}
run: |
TAG="${BUILD_TAG}"
PRERELEASE=false
if echo "$TAG" | grep -qE '(rc|beta|alpha)'; then
PRERELEASE=true
fi
BODY="## Kindred Create ${TAG}
# Build JSON payload entirely in Python to avoid shell/Python type mismatches
PAYLOAD=$(python3 -c "
import json, re
tag = '${TAG}'
prerelease = bool(re.search(r'(rc|beta|alpha)', tag))
body = '''## Kindred Create {tag}
### Downloads
@@ -337,7 +368,14 @@ jobs:
*macOS and Windows builds are not yet available.*
SHA256 checksums are provided alongside each artifact."
SHA256 checksums are provided alongside each artifact.'''.format(tag=tag)
print(json.dumps({
'tag_name': tag,
'name': f'Kindred Create {tag}',
'body': body,
'prerelease': prerelease,
}))
")
# Delete existing release for this tag (if any) so we can recreate
existing=$(curl -s -o /dev/null -w "%{http_code}" \
@@ -355,26 +393,34 @@ jobs:
fi
# Create release
release_id=$(curl -s -X POST \
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${TAG}',
'name': 'Kindred Create ${TAG}',
'body': '''${BODY}''',
'prerelease': ${PRERELEASE}
}))")" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
-d "$PAYLOAD" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases")
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "::error::Failed to create release (HTTP ${http_code}): ${body}"
exit 1
fi
release_id=$(echo "$body" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Created release ${release_id}"
# Upload assets
for file in release/*; do
filename=$(basename "$file")
echo "Uploading ${filename}..."
curl -s -X POST \
upload_resp=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${file}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${release_id}/assets?name=${filename}"
echo " done."
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${release_id}/assets?name=${filename}")
upload_code=$(echo "$upload_resp" | tail -1)
if [ "$upload_code" -lt 200 ] || [ "$upload_code" -ge 300 ]; then
echo "::warning::Failed to upload ${filename} (HTTP ${upload_code})"
else
echo " done."
fi
done

View File

@@ -17,8 +17,8 @@ FreeCAD startup
│ └─ exec(mods/silo/freecad/InitGui.py)
│ └─ registers SiloWorkbench
└─ Deferred setup (QTimer):
├─ 1500ms: _setup_silo_auth_panel() → "Database Auth" dock
├─ 2000ms: _setup_silo_menu() → SiloMenuManipulator
├─ 1500ms: _register_silo_origin() registers Silo FileOrigin
├─ 2000ms: _setup_silo_auth_panel() → "Database Auth" dock
├─ 3000ms: _check_silo_first_start() → settings prompt
└─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock
```

View File

@@ -7,7 +7,7 @@ Kindred Create uses Gitea Actions for continuous integration and release builds.
| Workflow | Trigger | Purpose | Artifacts |
|----------|---------|---------|-----------|
| `build.yml` | Push to `main`, pull requests | Build + test | Linux tarball |
| `release.yml` | Tags matching `v*` | Multi-platform release | AppImage, .deb, .dmg, .exe, .7z |
| `release.yml` | Tags matching `v*` or `latest` | Release build | AppImage, .deb |
All builds run on public runners in dockerized mode. No host-mode or internal infrastructure is required.
@@ -34,14 +34,16 @@ Runs on every push to `main` and on pull requests. Builds the project in an Ubun
### Caching
ccache is persisted between builds using `actions/cache`. Cache keys are scoped by branch and commit SHA, with fallback to the branch key then `main`.
ccache is persisted between builds using `actions/cache`. Cache keys use a date suffix so entries rotate daily (one save per day per branch). Saves are skipped when the exact key already exists, preventing duplicate entries that fill runner storage.
```
Key: ccache-build-{branch}-{sha}
Key: ccache-build-{branch}-{YYYYMMDD}
Fallback: ccache-build-{branch}-
Fallback: ccache-build-main-
```
Release builds use a separate key namespace (`ccache-release-linux-{YYYYMMDD}`) because they compile with different optimization flags (`-O3`). The rattler-build script (`build.sh`) explicitly sets `CCACHE_DIR` and `CCACHE_BASEDIR` since rattler-build does not forward environment variables from the parent process.
ccache configuration: 4 GB max, zlib compression level 6, sloppy mode for include timestamps and PCH.
---
@@ -63,17 +65,19 @@ Tags containing `rc`, `beta`, or `alpha` are marked as pre-releases.
### Platform matrix
| Job | Runner | Container | Preset | Output |
|-----|--------|-----------|--------|--------|
| `build-linux` | `ubuntu-latest` | `ubuntu:24.04` | `conda-linux-release` | AppImage, .deb |
| `build-macos` (Intel) | `macos-13` | native | `conda-macos-release` | .dmg (x86_64) |
| `build-macos` (Apple Silicon) | `macos-14` | native | `conda-macos-release` | .dmg (arm64) |
| `build-windows` | `windows-latest` | native | `conda-windows-release` | .exe (NSIS), .7z |
| Job | Runner | Container | Preset | Output | Status |
|-----|--------|-----------|--------|--------|--------|
| `build-linux` | `ubuntu-latest` | `ubuntu:24.04` | `conda-linux-release` | AppImage, .deb | Active |
| `build-macos` (Intel) | `macos-13` | native | `conda-macos-release` | .dmg (x86_64) | Disabled |
| `build-macos` (Apple Silicon) | `macos-14` | native | `conda-macos-release` | .dmg (arm64) | Disabled |
| `build-windows` | `windows-latest` | native | `conda-windows-release` | .exe (NSIS), .7z | Disabled |
All four jobs run concurrently. After all succeed, `publish-release` collects artifacts and creates the Gitea release.
Only the Linux build is currently active. macOS and Windows jobs are defined but commented out pending runner availability or cross-compilation support. After `build-linux` succeeds, `publish-release` collects artifacts and creates the Gitea release.
### Linux build
Both workflows start with a disk cleanup step that removes pre-installed bloat (dotnet, Android SDK, etc.) to free space for the build.
Uses the rattler-build packaging pipeline:
1. `pixi install` in `package/rattler-build/`
@@ -81,9 +85,10 @@ Uses the rattler-build packaging pipeline:
3. The bundle script:
- Copies the pixi conda environment to an AppDir
- Strips unnecessary files (includes, static libs, cmake files, `__pycache__`)
- Downloads `appimagetool` and creates a squashfs AppImage (zstd compressed)
- Generates SHA256 checksums
4. `package/debian/build-deb.sh` builds a .deb from the AppDir
- Downloads `appimagetool`, extracts it with `--appimage-extract` (FUSE unavailable in containers), and runs via `squashfs-root/AppRun`
- Creates a squashfs AppImage (zstd compressed) with SHA256 checksums
4. Intermediate build files are cleaned up to free space for the .deb step
5. `package/debian/build-deb.sh` builds a .deb from the AppDir
- Installs to `/opt/kindred-create/` with wrapper scripts in `/usr/bin/`
- Sets up LD_LIBRARY_PATH, QT_PLUGIN_PATH, PYTHONPATH in wrappers
- Creates desktop entry, MIME types, AppStream metainfo
@@ -123,10 +128,15 @@ Builds natively on Windows runner:
`publish-release` runs after all platform builds succeed:
1. Downloads all artifacts from `build-linux`, `build-macos`, `build-windows`
2. Collects release files (AppImage, .deb, .dmg, .7z, .exe, checksums)
3. Creates a Gitea release via `gitea.com/actions/release-action`
4. Requires `RELEASE_TOKEN` secret with repository write permissions
1. Downloads all artifacts from completed build jobs
2. Collects release files (AppImage, .deb, checksums) into a `release/` directory
3. Deletes any existing Gitea release for the same tag (allows re-running)
4. Creates a new Gitea release via the REST API (`/api/v1/repos/{owner}/{repo}/releases`)
5. Uploads each artifact as a release attachment via the API
The release payload (tag name, body, prerelease flag) is built entirely in Python to avoid shell/Python type mismatches. Tags containing `rc`, `beta`, or `alpha` are automatically marked as pre-releases.
Requires `RELEASE_TOKEN` secret with repository write permissions.
---
@@ -174,6 +184,27 @@ container:
network: bridge
```
### Runner cleanup daemon
A cleanup script at `.gitea/runner/cleanup.sh` prevents disk exhaustion on self-hosted runners. It uses a tiered approach based on disk usage thresholds:
| Threshold | Action |
|-----------|--------|
| 70% | Docker cleanup (stopped containers, dangling images, build cache) |
| 80% | Purge act_runner cache entries older than 7 days, clean inactive workspaces |
| 90% | System cleanup (apt cache, old logs, journal vacuum to 100 MB) |
| 95% | Emergency: remove all act_runner cache entries and Docker images |
Install via the provided systemd units (`.gitea/runner/cleanup.service` and `.gitea/runner/cleanup.timer`) to run every 30 minutes:
```bash
sudo cp .gitea/runner/cleanup.sh /usr/local/bin/runner-cleanup.sh
sudo cp .gitea/runner/cleanup.service /etc/systemd/system/
sudo cp .gitea/runner/cleanup.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer
```
---
## Secrets
@@ -214,11 +245,12 @@ Defined in `CMakePresets.json`. Release builds use:
### ccache
Compiler cache is used across all builds to speed up incremental compilation. Cache is persisted between CI runs via `actions/cache`. Configuration:
Compiler cache is used across all builds to speed up incremental compilation. Cache is persisted between CI runs via `actions/cache` with date-based key rotation. Configuration:
- Max size: 4 GB
- Compression: zlib level 6
- Sloppy mode: include timestamps, PCH defines, time macros
- `CCACHE_BASEDIR`: set to workspace root (build workflow) or `$SRC_DIR` (rattler-build) for path normalization across runs
---
@@ -243,9 +275,12 @@ The Docker container installs only minimal dependencies. If a new dependency is
ccache misses spike when:
- The compiler version changes (pixi update)
- CMake presets change configuration flags
- The cache key doesn't match (new branch, force-pushed SHA)
- First build of the day (date-based key rotates daily)
- New branch without a prior cache (falls back to `main` cache)
Check `pixi run ccache -s` output for hit/miss ratios.
For release builds, ensure `build.sh` is correctly setting `CCACHE_DIR=/tmp/ccache-kindred-create` -- rattler-build does not forward environment variables from the workflow, so ccache config must be set inside the script.
Check `pixi run ccache -s` output (printed in the "Show ccache statistics" step) for hit/miss ratios. The "Prepare ccache" step also prints the full ccache configuration via `ccache -p`.
### Submodule checkout fails

View File

@@ -62,12 +62,12 @@ These appear in the File menu and "Origin Tools" toolbar across all workbenches
| `Silo_Rollback` | Rollback to previous revision |
| `Silo_SetStatus` | Set revision status (draft/review/released/obsolete) |
| `Silo_Settings` | Configure API URL, projects dir, SSL certificates |
| `Silo_ToggleMode` | Swap Ctrl+O/S/N between FreeCAD and Silo commands |
| `Silo_Auth` | Login/logout authentication panel |
**Global integration** via `SiloMenuManipulator` in `src/Mod/Create/InitGui.py`:
- File menu: Silo_New, Silo_Open, Silo_Save, Silo_Commit, Silo_Pull, Silo_Push, Silo_BOM
- File toolbar: Silo_ToggleMode button
**Global integration** via the origin system in `src/Gui/`:
- File toolbar: `Std_Origin` selector widget + `Std_New`/`Std_Open`/`Std_Save` (delegate to current origin)
- Origin Tools toolbar: `Origin_Commit`/`Origin_Pull`/`Origin_Push`/`Origin_Info`/`Origin_BOM` (auto-disable by capability)
- Silo origin registered at startup by `src/Mod/Create/InitGui.py`
**Server architecture:** Go REST API (38+ routes) + PostgreSQL + MinIO S3. Authentication via local (bcrypt), LDAP, or OIDC backends. See `mods/silo/docs/` for server documentation.

View File

@@ -58,11 +58,11 @@ The Python API provides everything needed for feature creation, command registra
The Create module is a thin Python loader that:
- Adds `mods/` addon paths to `sys.path` and executes their `Init.py`/`InitGui.py` files
- Installs `SiloMenuManipulator` for global File menu/toolbar injection
- Registers the Silo FileOrigin so the origin selector can offer it at startup
- Sets up deferred Silo dock panels (auth, activity) via `QTimer`
- Handles first-start configuration
This layer does not contain C++ code. It uses FreeCAD's `WorkbenchManipulator` API for menu/toolbar injection.
This layer does not contain C++ code.
### Layer 4: Kindred Workbenches -- `mods/`
@@ -116,9 +116,9 @@ Pure Python workbenches following FreeCAD's addon pattern. Self-contained with `
**Goal:** Silo commands available globally, not just in the Silo workbench.
**Implementation:** `SiloMenuManipulator` in `src/Mod/Create/InitGui.py` uses `FreeCADGui.addWorkbenchManipulator()` to inject Silo commands into the File menu and toolbar across all workbenches. `Silo_ToggleMode` provides a one-click swap of Ctrl+O/S/N between standard FreeCAD and Silo file commands.
**Implementation:** The unified origin system (`FileOrigin`, `OriginManager`, `OriginSelectorWidget`) in `src/Gui/` delegates all file operations (New/Open/Save) to the selected origin. Standard commands (`Std_New`, `Std_Open`, `Std_Save`) and origin commands (`Origin_Commit`, `Origin_Pull`, `Origin_Push`, `Origin_Info`, `Origin_BOM`) are built into the File toolbar and menu. The Silo workbench no longer has its own toolbar — it only provides a menu with admin/management commands.
**Dock panels:** Database Auth (1500ms) and Database Activity (4000ms) panels are created via deferred QTimers and docked in the right panel area.
**Dock panels:** Database Auth (2000ms) and Database Activity (4000ms) panels are created via deferred QTimers and docked in the right panel area.
### Phase 6: Build system integration -- PARTIAL

View File

@@ -8,7 +8,7 @@
2. **WorkbenchManipulator timing.** The `_ZToolsPartDesignManipulator` appends commands by name. If ZToolsWorkbench hasn't been activated when the user switches to PartDesign, the commands may not be registered. The manipulator API tolerates missing commands silently, but buttons won't appear.
3. **Silo shortcut persistence.** `Silo_ToggleMode` stores original shortcuts in a module-level dict. If FreeCAD crashes with Silo mode on, original shortcuts are lost on next launch.
3. ~~**Silo shortcut persistence.**~~ Resolved. `Silo_ToggleMode` removed; file operations now delegate to the selected origin via the unified origin system.
### High

View File

@@ -1,7 +1,7 @@
# Kindred Create
**Last updated:** 2026-02-06
**Branch:** main @ `c858706d480`
**Last updated:** 2026-02-07
**Branch:** main @ `7bec3d5c3b2`
**Kindred Create:** v0.1.0
**FreeCAD base:** v1.0.0
@@ -19,11 +19,13 @@
| Submodule | Path | Source | Pinned commit |
|-----------|------|--------|---------------|
| ztools | `mods/ztools` | `gitea.kindred.internal/kindred/ztools-0065` | `d2f94c3` |
| silo | `mods/silo` | `gitea.kindred.internal/kindred/silo-0062` | `27e112e` |
| OndselSolver | `src/3rdParty/OndselSolver` | `gitea.kindred.internal/kindred/ondsel` | `5d1988b` |
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` | `d2f94c3` |
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` | `bf0b843` |
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` | `5d1988b` |
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` | `756c91a` |
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | `01e242e` |
| googletest | `tests/lib` | `github.com/google/googletest` | `56efe39` |
The silo submodule was split from a monorepo into three repos: `silo-client` (shared Python API client), `silo-mod` (FreeCAD workbench, used as Create's submodule), and `silo-calc` (LibreOffice Calc extension). The `silo-mod` repo includes `silo-client` as its own submodule.
OndselSolver is forked from `github.com/FreeCAD/OndselSolver` to carry a Newton-Raphson convergence fix (see [KNOWN_ISSUES.md](KNOWN_ISSUES.md#12)).

View File

@@ -1,8 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<path d="M8 6 L20 6 L24 10 L24 26 L8 26 Z" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<path d="M20 6 L20 10 L24 10" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<line x1="11" y1="14" x2="21" y2="14" stroke="#74c7ec" stroke-width="1.5"/>
<line x1="11" y1="18" x2="21" y2="18" stroke="#74c7ec" stroke-width="1.5"/>
<line x1="11" y1="22" x2="17" y2="22" stroke="#74c7ec" stroke-width="1.5"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg4"
sodipodi:docname="Document.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect5"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.8970359,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect4"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,2.25625,0,1 @ F,0,0,1,0,1.411235,0,1 @ F,0,0,1,0,1.2732549,0,1 @ F,0,0,1,0,2.25625,0,1 @ F,0,0,1,0,1.875,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="10.90625"
inkscape:cy="12.21875"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<path
d="m 10.246497,6 h 8.332515 a 3.4070227,3.4070227 22.5 0 1 2.409129,0.9978938 l 2.101779,2.101779 a 3.0739092,3.0739092 67.5 0 1 0.900327,2.1735822 l 0,12.470495 A 2.25625,2.25625 135 0 1 21.733997,26 H 9.8652468 a 1.875,1.875 45 0 1 -1.875,-1.875 V 8.25625 A 2.25625,2.25625 135 0 1 10.246497,6 Z"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="path1"
inkscape:path-effect="#path-effect4"
inkscape:original-d="M 7.9902468,6 H 19.990247 l 4,4 V 26 H 7.9902468 Z" />
<path
d="m 19.707388,5.8623613 v 3.4443418 a 0.8970359,0.8970359 45 0 0 0.897036,0.8970359 h 3.385823"
fill="none"
stroke="#74c7ec"
stroke-width="1.61701"
id="path2"
style="stroke:#89b4fa;stroke-width:1.5;stroke-dasharray:none;stroke-opacity:1"
inkscape:path-effect="#path-effect5"
inkscape:original-d="m 19.707388,5.8623613 v 4.3413777 h 4.282859" />
<line
x1="11"
y1="14"
x2="21"
y2="14"
stroke="#74c7ec"
stroke-width="1.5"
id="line2"
style="stroke-linecap:round" />
<line
x1="11"
y1="18"
x2="21"
y2="18"
stroke="#74c7ec"
stroke-width="1.5"
id="line3"
style="stroke-linecap:round" />
<line
x1="11"
y1="22"
x2="17"
y2="22"
stroke="#74c7ec"
stroke-width="1.5"
id="line4"
style="stroke-linecap:round" />
</svg>

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<ellipse cx="16" cy="16" rx="10" ry="6" fill="none" stroke="#f9e2af" stroke-width="2"/>
<circle cx="16" cy="16" r="3" fill="#fab387"/>
<ellipse cx="16" cy="16" rx="10" ry="6" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="16" cy="16" r="3" fill="none" stroke="#cdd6f4" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 344 B

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<path d="M8 12 L16 8 L24 12 L24 22 L16 26 L8 22 Z" fill="#f9e2af" fill-opacity="0.6" stroke="#fab387" stroke-width="1.5"/>
<path d="M8 12 L16 16 L24 12 M16 16 L16 26" fill="none" stroke="#fab387" stroke-width="1.5"/>
<path d="M5.5 9.5 16 4l10.5 5.5v13L16 28 5.5 22.5z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 9.5 16 15l10.5-5.5M16 15v13" fill="none" stroke="#89b4fa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<path d="M8 12 L16 8 L24 12 L24 22 L16 26 L8 22 Z" fill="none" stroke="#f9e2af" stroke-width="1.5"/>
<path d="M8 12 L16 16 L24 12 M16 16 L16 26" fill="none" stroke="#fab387" stroke-width="1.5" stroke-dasharray="2,2"/>
</svg>

Before

Width:  |  Height:  |  Size: 345 B

View File

@@ -1,7 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<rect x="8" y="10" width="16" height="12" fill="none" stroke="#f9e2af" stroke-width="2"/>
<line x1="8" y1="10" x2="12" y2="6" stroke="#fab387" stroke-width="1.5"/>
<line x1="24" y1="10" x2="28" y2="6" stroke="#fab387" stroke-width="1.5"/>
<line x1="12" y1="6" x2="28" y2="6" stroke="#fab387" stroke-width="1.5"/>
<path d="M5.5 9.5 16 4l10.5 5.5v13L16 28 5.5 22.5z" fill="#585b70" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 9.5 16 15l10.5-5.5M16 15v13" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -1,10 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<circle cx="8" cy="12" r="2" fill="#f9e2af"/>
<circle cx="16" cy="8" r="2" fill="#f9e2af"/>
<circle cx="24" cy="12" r="2" fill="#f9e2af"/>
<circle cx="8" cy="22" r="2" fill="#fab387"/>
<circle cx="16" cy="26" r="2" fill="#fab387"/>
<circle cx="24" cy="22" r="2" fill="#fab387"/>
<circle cx="16" cy="16" r="2" fill="#f9e2af"/>
<circle cx="16" cy="4" r="1.5" fill="#cdd6f4"/>
<circle cx="5.5" cy="9.5" r="1.5" fill="#cdd6f4"/>
<circle cx="26.5" cy="9.5" r="1.5" fill="#cdd6f4"/>
<circle cx="16" cy="15" r="1.5" fill="#cdd6f4"/>
<circle cx="5.5" cy="22.5" r="1.5" fill="#cdd6f4"/>
<circle cx="26.5" cy="22.5" r="1.5" fill="#cdd6f4"/>
<circle cx="16" cy="28" r="1.5" fill="#cdd6f4"/>
</svg>

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 491 B

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<path d="M8 12 L16 8 L24 12 L24 22 L16 26 L8 22 Z" fill="#f9e2af" stroke="#fab387" stroke-width="1.5"/>
<path d="M8 12 L16 16 L24 12 M16 16 L16 26" fill="none" stroke="#313244" stroke-width="1"/>
<path d="M5.5 9.5 16 4l10.5 5.5v13L16 28 5.5 22.5z" fill="#45475a" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 9.5 16 15l10.5-5.5M16 15v13" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<path d="M8 12 L16 8 L24 12 L24 22 L16 26 L8 22 Z" fill="none" stroke="#f9e2af" stroke-width="1.5"/>
<path d="M8 12 L16 16 L24 12 M16 16 L16 26" fill="none" stroke="#fab387" stroke-width="1.5"/>
<path d="M5.5 9.5 16 4l10.5 5.5v13L16 28 5.5 22.5z" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 9.5 16 15l10.5-5.5M16 15v13" fill="none" stroke="#cdd6f4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -1,9 +1,62 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<circle cx="10" cy="10" r="4" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<circle cx="22" cy="10" r="4" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<circle cx="10" cy="22" r="4" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<circle cx="22" cy="22" r="4" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<line x1="14" y1="10" x2="18" y2="10" stroke="#89b4fa" stroke-width="1.5"/>
<line x1="10" y1="14" x2="10" y2="18" stroke="#74c7ec" stroke-width="1.5"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<circle
cx="10"
cy="10"
r="4"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle1" />
<circle
cx="22"
cy="10"
r="4"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle2" />
<circle
cx="10"
cy="22"
r="4"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle3" />
<circle
cx="22"
cy="22"
r="4"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle4" />
<line
x1="14"
y1="10"
x2="18"
y2="10"
stroke="#cba6f7"
stroke-width="1.5"
id="line1" />
<line
x1="10"
y1="14"
x2="10"
y2="18"
stroke="#cba6f7"
stroke-width="1.5"
id="line2" />
</svg>

Before

Width:  |  Height:  |  Size: 607 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,7 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<rect x="6" y="6" width="10" height="10" rx="1" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<rect x="10" y="10" width="10" height="10" rx="1" fill="#313244" stroke="#74c7ec" stroke-width="1.5"/>
<rect x="14" y="14" width="10" height="10" rx="1" fill="#313244" stroke="#89b4fa" stroke-width="1.5"/>
<circle cx="24" cy="8" r="4" fill="none" stroke="#a6e3a1" stroke-width="1.5"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<rect
x="6"
y="6"
width="10"
height="10"
rx="1.5"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="rect2" />
<rect
x="10"
y="10"
width="10"
height="10"
rx="1.5"
fill="#313244"
stroke="#cba6f7"
stroke-width="1.5"
id="rect3" />
<rect
x="14"
y="14"
width="10"
height="10"
rx="1.5"
fill="#313244"
stroke="#89b4fa"
stroke-width="1.5"
id="rect4" />
<circle
cx="24"
cy="8"
r="4"
fill="none"
stroke="#a6e3a1"
stroke-width="1.5"
id="circle1" />
</svg>

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 910 B

View File

@@ -1,6 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<rect x="8" y="8" width="10" height="10" rx="1" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<path d="M18 18 L24 24" stroke="#74c7ec" stroke-width="2"/>
<circle cx="24" cy="24" r="3" fill="#74c7ec"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<rect
x="8"
y="8"
width="10"
height="10"
rx="1.5"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="rect2" />
<path
d="M 18,18 24,24"
stroke="#89b4fa"
stroke-width="1.5"
id="path1" />
<circle
cx="24"
cy="24"
r="3"
fill="#cba6f7"
id="circle1" />
</svg>

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -1,7 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<rect x="6" y="10" width="20" height="14" rx="2" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<circle cx="12" cy="17" r="3" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<circle cx="20" cy="17" r="3" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<line x1="15" y1="17" x2="17" y2="17" stroke="#89b4fa" stroke-width="1.5"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<rect
x="6"
y="10"
width="20"
height="14"
rx="2"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="rect2" />
<circle
cx="12"
cy="17"
r="3"
fill="none"
stroke="#cba6f7"
stroke-width="1.5"
id="circle1" />
<circle
cx="20"
cy="17"
r="3"
fill="none"
stroke="#cba6f7"
stroke-width="1.5"
id="circle2" />
<line
x1="15"
y1="17"
x2="17"
y2="17"
stroke="#89b4fa"
stroke-width="1.5"
id="line1" />
</svg>

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 838 B

View File

@@ -1,7 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<circle cx="10" cy="10" r="3" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<circle cx="22" cy="10" r="3" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<circle cx="10" cy="20" r="3" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<path d="M24 18 L24 26 L18 22 Z" fill="#a6e3a1"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<circle
cx="10"
cy="10"
r="3"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle1" />
<circle
cx="22"
cy="10"
r="3"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle2" />
<circle
cx="10"
cy="20"
r="3"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle3" />
<path
d="M 24,18 24,26 18,22 Z"
fill="#a6e3a1"
id="path1" />
</svg>

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 764 B

View File

@@ -1,5 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<rect x="6" y="6" width="12" height="12" rx="1" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<rect x="14" y="14" width="12" height="12" rx="1" fill="#313244" stroke="#74c7ec" stroke-width="1.5"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<rect
x="6"
y="6"
width="12"
height="12"
rx="1.5"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="rect2" />
<rect
x="14"
y="14"
width="12"
height="12"
rx="1.5"
fill="#313244"
stroke="#cba6f7"
stroke-width="1.5"
id="rect3" />
</svg>

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 616 B

View File

@@ -1,7 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<circle cx="10" cy="16" r="5" fill="none" stroke="#89b4fa" stroke-width="1.5"/>
<circle cx="22" cy="16" r="5" fill="none" stroke="#74c7ec" stroke-width="1.5"/>
<path d="M13 12 L19 12 M19 12 L17 10 M19 12 L17 14" stroke="#a6e3a1" stroke-width="1.5"/>
<path d="M19 20 L13 20 M13 20 L15 18 M13 20 L15 22" stroke="#f38ba8" stroke-width="1.5"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<circle
cx="10"
cy="16"
r="5"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle1" />
<circle
cx="22"
cy="16"
r="5"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="circle2" />
<path
d="M 13,12 19,12 M 19,12 17,10 M 19,12 17,14"
stroke="#a6e3a1"
stroke-width="1.5"
fill="none"
id="path1" />
<path
d="M 19,20 13,20 M 13,20 15,18 M 13,20 15,22"
stroke="#f38ba8"
stroke-width="1.5"
fill="none"
id="path2" />
</svg>

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 837 B

View File

@@ -1,14 +1,62 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
x="2"
y="2"
width="28"
height="28"
rx="4"
fill="#313244"
id="rect1" />
<!-- Base sketch profile -->
<path d="M6 24 L6 20 L14 18 L26 20 L26 24 L14 26 Z" fill="#45475a" stroke="#6c7086" stroke-width="1"/>
<path
d="M 6,24 6,20 14,18 26,20 26,24 14,26 Z"
fill="#45475a"
stroke="#6c7086"
stroke-width="1"
id="path1" />
<!-- Extruded body -->
<path d="M6 20 L6 10 L14 8 L26 10 L26 20 L14 22 Z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
<path d="M6 10 L14 12 L26 10" stroke="#89b4fa" stroke-width="1.5" fill="none"/>
<path d="M14 12 L14 22" stroke="#89b4fa" stroke-width="1.5"/>
<path
d="M 6,20 6,10 14,8 26,10 26,20 14,22 Z"
fill="#45475a"
stroke="#89b4fa"
stroke-width="1.5"
id="path2" />
<path
d="M 6,10 14,12 26,10"
stroke="#89b4fa"
stroke-width="1.5"
fill="none"
id="path3" />
<path
d="M 14,12 V 22"
stroke="#89b4fa"
stroke-width="1.5"
id="path4" />
<!-- Top face -->
<path d="M6 10 L14 8 L26 10 L14 12 Z" fill="#74c7ec" fill-opacity="0.4"/>
<path
d="M 6,10 14,8 26,10 14,12 Z"
fill="#74c7ec"
fill-opacity="0.4"
id="path5" />
<!-- Extrude arrow -->
<path d="M20 18 L20 6" stroke="#a6e3a1" stroke-width="2" stroke-linecap="round"/>
<path d="M17 9 L20 5 L23 9" stroke="#a6e3a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path
d="M 20,18 V 6"
stroke="#a6e3a1"
stroke-width="2"
stroke-linecap="round"
id="path6" />
<path
d="M 17,9 20,5 23,9"
stroke="#a6e3a1"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
id="path7" />
</svg>

Before

Width:  |  Height:  |  Size: 878 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,14 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
x="2"
y="2"
width="28"
height="28"
rx="4"
fill="#313244"
id="rect1" />
<!-- Solid block -->
<path d="M4 22 L4 10 L16 6 L28 10 L28 22 L16 26 Z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
<path d="M4 10 L16 14 L28 10" stroke="#89b4fa" stroke-width="1.5" fill="none"/>
<path d="M16 14 L16 26" stroke="#89b4fa" stroke-width="1.5"/>
<path
d="M 4,22 4,10 16,6 28,10 28,22 16,26 Z"
fill="#45475a"
stroke="#89b4fa"
stroke-width="1.5"
id="path1" />
<path
d="M 4,10 16,14 28,10"
stroke="#89b4fa"
stroke-width="1.5"
fill="none"
id="path2" />
<path
d="M 16,14 V 26"
stroke="#89b4fa"
stroke-width="1.5"
id="path3" />
<!-- Pocket cut-out -->
<path d="M10 12 L16 10 L22 12 L22 18 L16 20 L10 18 Z" fill="#1e1e2e" stroke="#f38ba8" stroke-width="1.5"/>
<path d="M10 12 L16 14 L22 12" stroke="#f38ba8" stroke-width="1" fill="none"/>
<path d="M16 14 L16 20" stroke="#f38ba8" stroke-width="1"/>
<path
d="M 10,12 16,10 22,12 22,18 16,20 10,18 Z"
fill="#1e1e2e"
stroke="#f38ba8"
stroke-width="1.5"
id="path4" />
<path
d="M 10,12 16,14 22,12"
stroke="#f38ba8"
stroke-width="1"
fill="none"
id="path5" />
<path
d="M 16,14 V 20"
stroke="#f38ba8"
stroke-width="1"
id="path6" />
<!-- Cut arrow -->
<path d="M16 8 L16 16" stroke="#f38ba8" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 13 L16 17 L18 13" stroke="#f38ba8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path
d="M 16,8 V 16"
stroke="#f38ba8"
stroke-width="1.5"
stroke-linecap="round"
id="path7" />
<path
d="M 14,13 16,17 18,13"
stroke="#f38ba8"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
id="path8" />
</svg>

Before

Width:  |  Height:  |  Size: 925 B

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,6 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#313244"/>
<path d="M12 20 L8 20 A4 4 0 0 1 8 12 L12 12" fill="none" stroke="#585b70" stroke-width="2"/>
<path d="M20 12 L24 12 A4 4 0 0 1 24 20 L20 20" fill="none" stroke="#585b70" stroke-width="2"/>
<line x1="6" y1="26" x2="26" y2="6" stroke="#f38ba8" stroke-width="2"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
width="32"
height="32"
rx="4"
fill="#313244"
id="rect1" />
<path
d="M 12,20 H 8 A 4,4 0 0 1 8,12 h 4"
fill="none"
stroke="#585b70"
stroke-width="1.5"
id="path1" />
<path
d="M 20,12 h 4 A 4,4 0 0 1 24,20 h -4"
fill="none"
stroke="#585b70"
stroke-width="1.5"
id="path2" />
<line
x1="6"
y1="26"
x2="26"
y2="6"
stroke="#f38ba8"
stroke-width="2"
id="line1" />
</svg>

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 680 B

View File

@@ -1,10 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Document shape -->
<path d="M9 5 L9 27 L23 27 L23 11 L17 5 Z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
<!-- Folded corner -->
<path d="M17 5 L17 11 L23 11" fill="#313244" stroke="#89b4fa" stroke-width="1.5" stroke-linejoin="round"/>
<!-- Plus sign for "new" -->
<circle cx="22" cy="22" r="6" fill="#a6e3a1"/>
<path d="M22 19 L22 25 M19 22 L25 22" stroke="#1e1e2e" stroke-width="2" stroke-linecap="round"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
x="2"
y="2"
width="28"
height="28"
rx="4"
fill="#313244"
id="rect1" />
<path
d="M 10,6 H 18 L 24,12 V 26 H 10 Z"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="path1"
style="stroke-linejoin:round" />
<path
d="M 18,6 V 12 H 24"
fill="none"
stroke="#89b4fa"
stroke-width="1.5"
id="path2"
style="stroke-linejoin:round" />
<line
x1="12"
y1="16"
x2="20"
y2="16"
stroke="#74c7ec"
stroke-width="1.5"
id="line1"
style="stroke-linecap:round" />
<line
x1="12"
y1="20"
x2="20"
y2="20"
stroke="#74c7ec"
stroke-width="1.5"
id="line2"
style="stroke-linecap:round" />
<circle
cx="24"
cy="24"
r="5"
fill="#a6e3a1"
id="circle1" />
<path
d="M 24,21.5 V 26.5 M 21.5,24 H 26.5"
stroke="#1e1e2e"
stroke-width="1.5"
stroke-linecap="round"
id="path3" />
</svg>

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,14 +1,75 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
x="2"
y="2"
width="28"
height="28"
rx="4"
fill="#313244"
id="rect1" />
<!-- Printer body -->
<rect x="5" y="12" width="22" height="10" rx="2" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
<rect
x="5"
y="12"
width="22"
height="10"
rx="2"
fill="#45475a"
stroke="#89b4fa"
stroke-width="1.5"
id="rect2" />
<!-- Paper input (top) -->
<rect x="9" y="6" width="14" height="8" rx="1" fill="#cdd6f4" stroke="#6c7086" stroke-width="1"/>
<rect
x="9"
y="6"
width="14"
height="8"
rx="1"
fill="#cdd6f4"
stroke="#6c7086"
stroke-width="1"
id="rect3" />
<!-- Paper output (bottom) -->
<rect x="9" y="20" width="14" height="8" rx="1" fill="#cdd6f4" stroke="#6c7086" stroke-width="1"/>
<rect
x="9"
y="20"
width="14"
height="8"
rx="1"
fill="#cdd6f4"
stroke="#6c7086"
stroke-width="1"
id="rect4" />
<!-- Paper lines -->
<line x1="11" y1="23" x2="21" y2="23" stroke="#585b70" stroke-width="1" stroke-linecap="round"/>
<line x1="11" y1="25" x2="18" y2="25" stroke="#585b70" stroke-width="1" stroke-linecap="round"/>
<line
x1="11"
y1="23"
x2="21"
y2="23"
stroke="#585b70"
stroke-width="1"
stroke-linecap="round"
id="line1" />
<line
x1="11"
y1="25.5"
x2="18"
y2="25.5"
stroke="#585b70"
stroke-width="1"
stroke-linecap="round"
id="line2" />
<!-- Printer status light -->
<circle cx="23" cy="15" r="1.5" fill="#a6e3a1"/>
<circle
cx="23"
cy="15"
r="1.5"
fill="#a6e3a1"
id="circle1" />
</svg>

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,15 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
x="2"
y="2"
width="28"
height="28"
rx="4"
fill="#313244"
id="rect1" />
<!-- Floppy disk body -->
<path d="M7 6 L7 26 L25 26 L25 10 L21 6 Z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
<path
d="M 7,6 H 21 L 25,10 V 26 H 7 Z"
fill="#45475a"
stroke="#89b4fa"
stroke-width="1.5"
id="path1"
style="stroke-linejoin:round" />
<!-- Metal slider -->
<rect x="10" y="6" width="10" height="8" rx="1" fill="#1e1e2e" stroke="#6c7086" stroke-width="1"/>
<rect
x="10"
y="6"
width="10"
height="8"
rx="1"
fill="#1e1e2e"
stroke="#6c7086"
stroke-width="1"
id="rect2" />
<!-- Slider notch -->
<rect x="17" y="7" width="2" height="6" fill="#313244"/>
<rect
x="17"
y="7"
width="2"
height="6"
fill="#313244"
id="rect3" />
<!-- Label area -->
<rect x="9" y="17" width="14" height="7" rx="1" fill="#cdd6f4"/>
<!-- Pencil icon for "save as" -->
<g transform="translate(18, 14)">
<path d="M6 2 L8 0 L10 2 L4 8 L2 8 L2 6 Z" fill="#f9e2af" stroke="#fab387" stroke-width="0.5"/>
</g>
<rect
x="9"
y="17"
width="14"
height="7"
rx="1"
fill="#cdd6f4"
id="rect4" />
<!-- Pencil badge for "save as" -->
<circle
cx="24"
cy="24"
r="5"
fill="#f9e2af"
id="circle1" />
<path
d="M 22,26 22,24 26,20 28,22 24,26 Z"
fill="#1e1e2e"
id="path2" />
</svg>

Before

Width:  |  Height:  |  Size: 738 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,14 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 32"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<rect
x="2"
y="2"
width="28"
height="28"
rx="4"
fill="#313244"
id="rect1" />
<!-- Floppy disk body -->
<path d="M7 6 L7 26 L25 26 L25 10 L21 6 Z" fill="#45475a" stroke="#89b4fa" stroke-width="1.5"/>
<path
d="M 7,6 H 21 L 25,10 V 26 H 7 Z"
fill="#45475a"
stroke="#89b4fa"
stroke-width="1.5"
id="path1"
style="stroke-linejoin:round" />
<!-- Metal slider -->
<rect x="10" y="6" width="10" height="8" rx="1" fill="#1e1e2e" stroke="#6c7086" stroke-width="1"/>
<rect
x="10"
y="6"
width="10"
height="8"
rx="1"
fill="#1e1e2e"
stroke="#6c7086"
stroke-width="1"
id="rect2" />
<!-- Slider notch -->
<rect x="17" y="7" width="2" height="6" fill="#313244"/>
<rect
x="17"
y="7"
width="2"
height="6"
fill="#313244"
id="rect3" />
<!-- Label area -->
<rect x="9" y="17" width="14" height="7" rx="1" fill="#cdd6f4"/>
<rect
x="9"
y="17"
width="14"
height="7"
rx="1"
fill="#cdd6f4"
id="rect4" />
<!-- Label lines -->
<line x1="11" y1="19" x2="21" y2="19" stroke="#313244" stroke-width="1" stroke-linecap="round"/>
<line x1="11" y1="22" x2="17" y2="22" stroke="#313244" stroke-width="1" stroke-linecap="round"/>
<line
x1="11"
y1="19.5"
x2="21"
y2="19.5"
stroke="#313244"
stroke-width="1"
stroke-linecap="round"
id="line1" />
<line
x1="11"
y1="22"
x2="17"
y2="22"
stroke="#313244"
stroke-width="1"
stroke-linecap="round"
id="line2" />
</svg>

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -112,6 +112,10 @@ export XKB_CONFIG_ROOT="${KINDRED_CREATE_HOME}/share/X11/xkb"
export FONTCONFIG_FILE="${KINDRED_CREATE_HOME}/etc/fonts/fonts.conf"
export FONTCONFIG_PATH="${KINDRED_CREATE_HOME}/etc/fonts"
# Qt Wayland fractional scaling — force integer rounding to avoid blurry text
export QT_SCALE_FACTOR_ROUNDING_POLICY=RoundPreferFloor
export QT_ENABLE_HIGHDPI_SCALING=1
# Use system CA certificates so bundled Python trusts internal CAs (e.g. FreeIPA)
# The bundled openssl has a hardcoded cafile from the build environment which
# does not exist on the target system.

View File

@@ -1,3 +1,15 @@
# Configure ccache to use a shared cache directory that persists across CI runs.
# The workflow caches /tmp/ccache-kindred-create between builds.
export CCACHE_DIR="${CCACHE_DIR:-/tmp/ccache-kindred-create}"
export CCACHE_BASEDIR="${SRC_DIR:-$(pwd)}"
export CCACHE_COMPRESS="${CCACHE_COMPRESS:-true}"
export CCACHE_COMPRESSLEVEL="${CCACHE_COMPRESSLEVEL:-6}"
export CCACHE_MAXSIZE="${CCACHE_MAXSIZE:-4G}"
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:-include_file_ctime,include_file_mtime,pch_defines,time_macros}"
mkdir -p "$CCACHE_DIR"
echo "ccache config: CCACHE_DIR=$CCACHE_DIR CCACHE_BASEDIR=$CCACHE_BASEDIR"
ccache -z || true
if [[ ${HOST} =~ .*linux.* ]]; then
CMAKE_PRESET=conda-linux-release
fi
@@ -46,3 +58,6 @@ cmake --install build
mv ${PREFIX}/bin/FreeCAD ${PREFIX}/bin/freecad || true
mv ${PREFIX}/bin/FreeCADCmd ${PREFIX}/bin/freecadcmd || true
echo "=== ccache statistics ==="
ccache -s || true

View File

@@ -9,8 +9,9 @@ export PATH_TO_FREECAD_LIBDIR=${HERE}/usr/lib
export FONTCONFIG_FILE=/etc/fonts/fonts.conf
export FONTCONFIG_PATH=/etc/fonts
# Fix: Use X to run on Wayland
export QT_QPA_PLATFORM=xcb
# Qt HiDPI scaling — force integer rounding to avoid blurry text on fractional scales
export QT_SCALE_FACTOR_ROUNDING_POLICY=RoundPreferFloor
export QT_ENABLE_HIGHDPI_SCALING=1
# Show packages info if DEBUG env variable is set
if [ "$DEBUG" = 1 ]; then

View File

@@ -18,12 +18,17 @@ requirements:
- cmake
- compilers>=1.10,<1.11
- doxygen
- icu>=75,<76
- ninja
- noqt5
- python>=3.11,<3.12
- qt6-main>=6.8,<6.9
- swig >=4.0,<4.4
- if: linux
then:
- patchelf
- if: linux and x86_64
then:
- clang

View File

@@ -25,6 +25,10 @@
<FCUInt Name="colorLogging" Value="2497893887"/>
<FCUInt Name="colorWarning" Value="4192382975"/>
<FCUInt Name="colorError" Value="4086016255"/>
<FCBool Name="checkError" Value="1"/>
<FCBool Name="checkLogging" Value="1"/>
<FCBool Name="checkShowReportViewOnError" Value="1"/>
<FCBool Name="checkShowReportViewOnWarning" Value="1"/>
</FCParamGroup>
<FCParamGroup Name="View">
<FCUInt Name="BackgroundColor" Value="505294591"/>
@@ -72,9 +76,26 @@
<FCUInt Name="BacklightColor" Value="1162304255"/>
<FCFloat Name="BacklightIntensity" Value="0.30"/>
</FCParamGroup>
<FCParamGroup Name="Document">
<FCInt Name="MaxUndoSize" Value="50"/>
<FCInt Name="AutoSaveTimeout" Value="5"/>
<FCInt Name="CountBackupFiles" Value="3"/>
<FCInt Name="prefLicenseType" Value="19"/>
<FCText Name="prefLicenseUrl"></FCText>
</FCParamGroup>
<FCParamGroup Name="TreeView">
<FCUInt Name="TreeEditColor" Value="3416717311"/>
<FCUInt Name="TreeActiveColor" Value="2799935999"/>
<FCBool Name="PreSelection" Value="1"/>
<FCBool Name="SyncView" Value="1"/>
<FCBool Name="SyncSelection" Value="1"/>
</FCParamGroup>
<FCParamGroup Name="NotificationArea">
<FCInt Name="MaxWidgetMessages" Value="100"/>
<FCInt Name="MaxOpenNotifications" Value="3"/>
<FCInt Name="NotificiationWidth" Value="400"/>
<FCInt Name="NotificationTime" Value="10"/>
<FCInt Name="MinimumOnScreenTime" Value="3"/>
</FCParamGroup>
<FCParamGroup Name="General">
<FCText Name="AutoloadModule">ZToolsWorkbench</FCText>

View File

@@ -744,6 +744,33 @@ QGroupBox::title {
background-color: #1e1e2e;
}
QGroupBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #585b70;
border-radius: 4px;
background-color: #313244;
}
QGroupBox::indicator:hover {
border-color: #cba6f7;
}
QGroupBox::indicator:checked {
background-color: #cba6f7;
border-color: #cba6f7;
}
QGroupBox::indicator:checked:disabled {
background-color: #6c7086;
border-color: #6c7086;
}
QGroupBox::indicator:disabled {
background-color: #181825;
border-color: #45475a;
}
/* =============================================================================
Tree View
============================================================================= */
@@ -961,6 +988,11 @@ QLabel:disabled {
color: #6c7086;
}
/* Link color — picked up by FreeCAD to set QPalette::Link */
QLabel[haslink="true"] {
color: #b4befe; /* Catppuccin Lavender */
}
/* =============================================================================
Frames
============================================================================= */

View File

@@ -31,6 +31,8 @@
#include "OriginManager.h"
#include "FileOrigin.h"
#include "BitmapFactory.h"
#include "Application.h"
#include "Command.h"
namespace Gui {
@@ -176,10 +178,7 @@ FileOrigin* OriginManagerDialog::selectedOrigin() const
void OriginManagerDialog::onAddSilo()
{
// TODO: Open SiloConfigDialog for adding new instance
QMessageBox::information(this, tr("Add Silo"),
tr("Silo configuration dialog not yet implemented.\n\n"
"To add a Silo instance, configure it in the Silo workbench preferences."));
Application::Instance->commandManager().runCommandByName("Silo_Settings");
}
void OriginManagerDialog::onEditOrigin()
@@ -189,10 +188,7 @@ void OriginManagerDialog::onEditOrigin()
return;
}
// TODO: Open SiloConfigDialog for editing
QMessageBox::information(this, tr("Edit Origin"),
tr("Origin editing not yet implemented.\n\n"
"To edit this origin, modify settings in the Silo workbench preferences."));
Application::Instance->commandManager().runCommandByName("Silo_Settings");
}
void OriginManagerDialog::onRemoveOrigin()

View File

@@ -54,8 +54,9 @@ void OriginSelectorWidget::setupUi()
{
setPopupMode(QToolButton::InstantPopup);
setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setMinimumWidth(70);
setMaximumWidth(120);
setMinimumWidth(80);
setMaximumWidth(160);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
// Create menu
m_menu = new QMenu(this);

View File

@@ -744,6 +744,33 @@ QGroupBox::title {
background-color: #1e1e2e;
}
QGroupBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #585b70;
border-radius: 4px;
background-color: #313244;
}
QGroupBox::indicator:hover {
border-color: #cba6f7;
}
QGroupBox::indicator:checked {
background-color: #cba6f7;
border-color: #cba6f7;
}
QGroupBox::indicator:checked:disabled {
background-color: #6c7086;
border-color: #6c7086;
}
QGroupBox::indicator:disabled {
background-color: #181825;
border-color: #45475a;
}
/* =============================================================================
Tree View
============================================================================= */
@@ -964,6 +991,11 @@ QLabel:disabled {
color: #6c7086;
}
/* Link color — picked up by FreeCAD to set QPalette::Link */
QLabel[haslink="true"] {
color: #b4befe; /* Catppuccin Lavender */
}
/* =============================================================================
Frames
============================================================================= */

View File

@@ -28,6 +28,7 @@
#include <QApplication>
#include <QFileDialog>
#include <QLocale>
#include <QMenu>
#include <QMessageBox>
#include <QString>
#include <algorithm>
@@ -96,7 +97,6 @@ DlgSettingsGeneral::DlgSettingsGeneral(QWidget* parent)
ui->SaveNewPreferencePack->setEnabled(false);
ui->ManagePreferencePacks->setEnabled(false);
ui->themesCombobox->setEnabled(false);
ui->moreThemesLabel->setEnabled(false);
}
}
}
@@ -121,7 +121,6 @@ DlgSettingsGeneral::DlgSettingsGeneral(QWidget* parent)
this,
&DlgSettingsGeneral::onThemeChanged
);
connect(ui->moreThemesLabel, &QLabel::linkActivated, this, &DlgSettingsGeneral::onLinkActivated);
}
// If there are any saved config file backs, show the revert button, otherwise hide it:
@@ -147,9 +146,6 @@ DlgSettingsGeneral::DlgSettingsGeneral(QWidget* parent)
const auto visible = UnitsApi::isMultiUnitLength();
ui->comboBox_FracInch->setVisible(visible);
ui->fractionalInchLabel->setVisible(visible);
ui->moreThemesLabel->setEnabled(
Application::Instance->commandManager().getCommandByName("Std_AddonMgr") != nullptr
);
}
/**
@@ -166,7 +162,7 @@ void DlgSettingsGeneral::setRecentFileSize()
auto recent = getMainWindow()->findChild<RecentFilesAction*>(QLatin1String("recentFiles"));
if (recent) {
ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("RecentFiles");
recent->resizeList(hGrp->GetInt("RecentFiles", 4));
recent->resizeList(hGrp->GetInt("RecentFiles", 10));
}
}
@@ -264,6 +260,11 @@ void DlgSettingsGeneral::saveSettings()
hGrp->SetInt("ToolbarIconSize", pixel);
getMainWindow()->setIconSize(QSize(pixel, pixel));
QVariant menuSize = ui->menuIconSize->itemData(ui->menuIconSize->currentIndex());
int menuPixel = menuSize.toInt();
hGrp->SetInt("MenuIconSize", menuPixel);
applyMenuIconSize(menuPixel);
int blinkTime {hGrp->GetBool("EnableCursorBlinking", true) ? -1 : 0};
qApp->setCursorFlashTime(blinkTime);
@@ -348,6 +349,7 @@ void DlgSettingsGeneral::loadSettings()
}
addIconSizes(getCurrentIconSize());
addMenuIconSizes(getCurrentMenuIconSize());
// TreeMode combobox setup.
loadDockWindowVisibility();
@@ -395,8 +397,9 @@ void DlgSettingsGeneral::resetSettingsToDefaults()
hGrp = WindowParameter::getDefaultParameter()->GetGroup("General");
// reset "Language" parameter
hGrp->RemoveASCII("Language");
// reset "ToolbarIconSize" parameter
// reset "ToolbarIconSize" and "MenuIconSize" parameters
hGrp->RemoveInt("ToolbarIconSize");
hGrp->RemoveInt("MenuIconSize");
// finally reset all the parameters associated to Gui::Pref* widgets
PreferencePage::resetSettingsToDefaults();
@@ -534,6 +537,63 @@ void DlgSettingsGeneral::translateIconSizes()
}
}
int DlgSettingsGeneral::getCurrentMenuIconSize() const
{
ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General");
return hGrp->GetInt("MenuIconSize", 24);
}
void DlgSettingsGeneral::addMenuIconSizes(int current)
{
ui->menuIconSize->clear();
QList<int> sizes {16, 20, 24, 28};
if (!sizes.contains(current)) {
sizes.append(current);
}
for (int size : sizes) {
ui->menuIconSize->addItem(QString(), QVariant(size));
}
int index = ui->menuIconSize->findData(QVariant(current));
ui->menuIconSize->setCurrentIndex(index);
translateMenuIconSizes();
}
void DlgSettingsGeneral::translateMenuIconSizes()
{
auto getSize = [this](int index) {
return ui->menuIconSize->itemData(index).toInt();
};
QStringList sizes;
sizes << tr("Small (%1px)").arg(getSize(0));
sizes << tr("Medium (%1px)").arg(getSize(1));
sizes << tr("Large (%1px)").arg(getSize(2));
sizes << tr("Extra large (%1px)").arg(getSize(3));
if (ui->menuIconSize->count() > 4) {
sizes << tr("Custom (%1px)").arg(getSize(4));
}
for (int index = 0; index < sizes.size(); index++) {
ui->menuIconSize->setItemText(index, sizes[index]);
}
}
void DlgSettingsGeneral::applyMenuIconSize(int pixel)
{
// Apply menu icon size via stylesheet override on all QMenu widgets
QString rule = QStringLiteral("QMenu::icon { width: %1px; height: %1px; }").arg(pixel);
for (auto* widget : qApp->allWidgets()) {
if (auto* menu = qobject_cast<QMenu*>(widget)) {
menu->setStyleSheet(rule);
}
}
// Store the rule so new menus pick it up via the main window
getMainWindow()->setProperty("_menuIconSizeRule", rule);
}
void DlgSettingsGeneral::retranslateUnits()
{
auto setItem = [&, index {0}](const std::string& item) mutable {
@@ -547,6 +607,7 @@ void DlgSettingsGeneral::changeEvent(QEvent* event)
{
if (event->type() == QEvent::LanguageChange) {
translateIconSizes();
translateMenuIconSizes();
retranslateUnits();
int index = ui->UseLocaleFormatting->currentIndex();
ui->retranslateUi(this);
@@ -823,24 +884,6 @@ void DlgSettingsGeneral::onThemeChanged(int index)
themeChanged = true;
}
void DlgSettingsGeneral::onLinkActivated(const QString& link)
{
auto const addonManagerLink = QStringLiteral("freecad:Std_AddonMgr");
if (link != addonManagerLink) {
return;
}
// Set the user preferences to include only preference packs.
// This is a quick and dirty way to open Addon Manager with only themes.
auto pref = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Addons"
);
pref->SetInt("PackageTypeSelection", 3); // 3 stands for Preference Packs
pref->SetInt("StatusSelection", 0); // 0 stands for any installation status
Gui::Application::Instance->commandManager().runCommandByName("Std_AddonMgr");
}
///////////////////////////////////////////////////////////
namespace

View File

@@ -72,7 +72,6 @@ protected Q_SLOTS:
void onManagePreferencePacksClicked();
void onImportConfigClicked();
void onThemeChanged(int index);
void onLinkActivated(const QString& link);
public Q_SLOTS:
void onUnitSystemIndexChanged(int index);
@@ -91,6 +90,12 @@ private:
int getCurrentIconSize() const;
void addIconSizes(int current);
void translateIconSizes();
int getCurrentMenuIconSize() const;
void addMenuIconSizes(int current);
void translateMenuIconSizes();
public:
static void applyMenuIconSize(int pixel);
private:
int localeIndex;

View File

@@ -219,35 +219,35 @@ dot/period will always be printed</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="moreThemesLabel">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>Looking for more themes? You can obtain them using the &lt;a href=&quot;freecad:Std_AddonMgr&quot;&gt;Addon Manager&lt;/a&gt;.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="1" column="0">
<widget class="QLabel" name="iconSizeLabel">
<property name="text">
<string>Size of toolbar icons</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="1" column="1">
<widget class="QComboBox" name="toolbarIconSize">
<property name="toolTip">
<string>Icon size in the toolbar</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="menuIconSizeLabel">
<property name="text">
<string>Size of menu icons</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="menuIconSize">
<property name="toolTip">
<string>Icon size in context menus and dropdown menus</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="treeModeLabel">
<property name="text">
@@ -278,7 +278,7 @@ dot/period will always be printed</string>
<string>How many files should be listed in recent files list</string>
</property>
<property name="value">
<number>4</number>
<number>10</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>RecentFiles</cstring>

View File

@@ -23,7 +23,7 @@
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Customize the current theme. The offered settings are optional for theme developers so they may or may not have an effect in the current theme.</string>
<string>Kindred Create Theme</string>
</property>
<property name="wordWrap">
<bool>true</bool>
@@ -392,10 +392,10 @@
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Overlay</string>
<string>Panel Visibility</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="0">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="hideTabBarCheckBox">
<property name="toolTip">
<string>Hide tab bar in dock overlay</string>
@@ -414,23 +414,7 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="Gui::PrefCheckBox" name="hintShowTabBarCheckBox">
<property name="toolTip">
<string>Show tab bar on mouse over when auto hide</string>
</property>
<property name="text">
<string>Hint show tab bar</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>DockOverlayHintTabBar</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>View</cstring>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="1" column="0">
<widget class="Gui::PrefCheckBox" name="hidePropertyViewScrollBarCheckBox">
<property name="toolTip">
<string>Hide property view scroll bar in dock overlay</string>
@@ -446,7 +430,7 @@
</property>
</widget>
</item>
<item row="5" column="0">
<item row="2" column="0">
<widget class="Gui::PrefCheckBox" name="overlayAutoHideCheckBox">
<property name="toolTip">
<string>Automatically hide overlaid dock panels when in non 3D view (e.g. TechDraw or Spreadsheet)</string>
@@ -465,13 +449,22 @@
</property>
</widget>
</item>
<item row="6" column="0">
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3b">
<property name="title">
<string>Overlay Interaction</string>
</property>
<layout class="QGridLayout" name="gridLayout_2b">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="mouseClickPassThroughCheckBox">
<property name="toolTip">
<string>Auto mouse click through transparent part of dock overlay.</string>
<string>Auto mouse click through transparent part of dock overlay</string>
</property>
<property name="text">
<string>Automatically pass through of the mouse cursor</string>
<string>Automatically pass through mouse cursor</string>
</property>
<property name="checked">
<bool>true</bool>
@@ -484,13 +477,13 @@
</property>
</widget>
</item>
<item row="7" column="0">
<item row="1" column="0">
<widget class="Gui::PrefCheckBox" name="mouseWheelPassThroughCheckBox">
<property name="toolTip">
<string>Automatically passes mouse wheel events through the transparent areas of an overlay panel</string>
</property>
<property name="text">
<string>Automatically pass through of the mouse wheel</string>
<string>Automatically pass through mouse wheel</string>
</property>
<property name="checked">
<bool>true</bool>
@@ -503,6 +496,22 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="Gui::PrefCheckBox" name="hintShowTabBarCheckBox">
<property name="toolTip">
<string>Show tab bar on mouse over when auto hide</string>
</property>
<property name="text">
<string>Hint show tab bar</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>DockOverlayHintTabBar</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>View</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -56,6 +56,7 @@
#include "Language/Translator.h"
#include "Dialogs/DlgVersionMigrator.h"
#include "FreeCADStyle.h"
#include "PreferencePages/DlgSettingsGeneral.h"
#include <App/Application.h>
#include <Base/Console.h>
@@ -226,6 +227,7 @@ void StartupPostProcess::execute()
setProcessMessages();
setAutoSaving();
setToolBarIconSize();
setMenuIconSize();
setWheelEventFilter();
setLocale();
setCursorFlashing();
@@ -281,6 +283,15 @@ void StartupPostProcess::setToolBarIconSize()
}
}
void StartupPostProcess::setMenuIconSize()
{
ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General");
int size = int(hGrp->GetInt("MenuIconSize", 0));
if (size >= 16) {
Dialog::DlgSettingsGeneral::applyMenuIconSize(size);
}
}
void StartupPostProcess::setWheelEventFilter()
{
// filter wheel events for combo boxes

View File

@@ -64,6 +64,7 @@ private:
void setProcessMessages();
void setAutoSaving();
void setToolBarIconSize();
void setMenuIconSize();
void setWheelEventFilter();
void setLocale();
void setCursorFlashing();

View File

@@ -744,6 +744,33 @@ QGroupBox::title {
background-color: #1e1e2e;
}
QGroupBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #585b70;
border-radius: 4px;
background-color: #313244;
}
QGroupBox::indicator:hover {
border-color: #cba6f7;
}
QGroupBox::indicator:checked {
background-color: #cba6f7;
border-color: #cba6f7;
}
QGroupBox::indicator:checked:disabled {
background-color: #6c7086;
border-color: #6c7086;
}
QGroupBox::indicator:disabled {
background-color: #181825;
border-color: #45475a;
}
/* =============================================================================
Tree View
============================================================================= */
@@ -985,6 +1012,11 @@ QLabel:disabled {
color: #6c7086;
}
/* Link color — picked up by FreeCAD to set QPalette::Link */
QLabel[haslink="true"] {
color: #b4befe; /* Catppuccin Lavender */
}
/* =============================================================================
Frames
============================================================================= */

View File

@@ -1,11 +1,20 @@
# 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
)

View File

@@ -65,34 +65,15 @@ def _check_silo_first_start():
FreeCAD.Console.PrintLog(f"Create: Silo first-start check skipped: {e}\n")
def _setup_silo_menu():
"""Inject Silo commands into the File menu and toolbar across all workbenches."""
def _register_silo_origin():
"""Register Silo as a file origin so the origin selector can offer it."""
try:
# Import silo_commands eagerly so commands are registered before the
# manipulator tries to add them to toolbars/menus.
import silo_commands # noqa: F401
import silo_commands # noqa: F401 - registers Silo commands
import silo_origin
class SiloMenuManipulator:
def modifyMenuBar(self):
return [
{"insert": "Silo_New", "menuItem": "Std_New", "after": ""},
{"insert": "Silo_Open", "menuItem": "Std_Open", "after": ""},
{"insert": "Silo_Save", "menuItem": "Std_Save", "after": ""},
{"insert": "Silo_Commit", "menuItem": "Silo_Save", "after": ""},
{"insert": "Silo_Pull", "menuItem": "Silo_Commit", "after": ""},
{"insert": "Silo_Push", "menuItem": "Silo_Pull", "after": ""},
{"insert": "Silo_BOM", "menuItem": "Silo_Push", "after": ""},
]
def modifyToolBars(self):
return [
{"append": "Silo_ToggleMode", "toolBar": "File"},
]
FreeCADGui.addWorkbenchManipulator(SiloMenuManipulator())
FreeCAD.Console.PrintLog("Create: Silo menu manipulator installed\n")
silo_origin.register_silo_origin()
except Exception as e:
FreeCAD.Console.PrintLog(f"Create: Silo menu setup skipped: {e}\n")
FreeCAD.Console.PrintLog(f"Create: Silo origin registration skipped: {e}\n")
def _setup_silo_auth_panel():
@@ -167,13 +148,24 @@ def _setup_silo_activity_panel():
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, _setup_silo_auth_panel)
QTimer.singleShot(2000, _setup_silo_menu)
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@"

View File

@@ -20,34 +20,13 @@
<string>Source</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="Gui::PrefRadioButton" name="radioButton_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Fetches the documentation from pages rendered on GitHub.
This is currently not available.</string>
</property>
<property name="text">
<string>GitHub (online)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>optionGithub</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Help</cstring>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="Gui::PrefRadioButton" name="radioOffline">
<property name="toolTip">
<string>Set this to a custom URL or the folder where the help files are located.
You can easily download the documentation for offline use by using the Addon
Manager and installing the &quot;offline-documentation&quot; addon. If this
field is left blank, FreeCAD will automatically search for the help files at
the default location ($USERAPPDATADIR/Mod/offline-documentation).</string>
If this field is left blank, the application will automatically search for
the help files at the default location ($USERAPPDATADIR/Mod/offline-documentation).</string>
</property>
<property name="text">
<string>Custom location</string>
@@ -84,10 +63,8 @@ to get French translation of the documentation.</string>
<widget class="Gui::PrefFileChooser" name="fileChooser">
<property name="toolTip">
<string>Set this to a custom URL or the folder where the help files are located.
Documentation can be downloaded for offline use via the Addon Manager and installing the
&quot;offline-documentation&quot; addon. If this field is left blank, FreeCAD will
automatically search for the help files at the default location
($USERAPPDATADIR/Mod/offline-documentation).</string>
If this field is left blank, the application will automatically search for
the help files at the default location ($USERAPPDATADIR/Mod/offline-documentation).</string>
</property>
<property name="mode">
<enum>Gui::FileChooser::Directory</enum>
@@ -116,8 +93,8 @@ automatically search for the help files at the default location
<item row="0" column="0">
<widget class="Gui::PrefRadioButton" name="radioButton">
<property name="toolTip">
<string>The documentation pages will be fetched from the official
FreeCADwiki at https://wiki.freecad.org</string>
<string>The documentation pages will be fetched from the
FreeCAD wiki at https://wiki.freecad.org</string>
</property>
<property name="text">
<string>FreeCAD Wiki (online)</string>
@@ -137,9 +114,9 @@ FreeCADwiki at https://wiki.freecad.org</string>
<widget class="Gui::PrefRadioButton" name="radioOnline">
<property name="toolTip">
<string>The documentation pages will be fetched from an automatic Markdown conversion
of the FreeCAD wiki,hosted on FreeCAD's GitHub account. This can be styled with a
custom stylesheet below and can look nicer than the wiki option. The 'Markdown' or
'Pandoc' Python module should be installed for optimal results.</string>
of the FreeCAD wiki. This can be styled with a custom stylesheet below and can
look nicer than the wiki option. The 'Markdown' or 'Pandoc' Python module should
be installed for optimal results.</string>
</property>
<property name="text">
<string>Markdown version (online)</string>
@@ -164,16 +141,7 @@ custom stylesheet below and can look nicer than the wiki option. The 'Markdown'
<string>Display</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Note: if PySide Web components are not found on the system, help pages will open in the default web browser regardless of the options below.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefRadioButton" name="radioBrowser">
<property name="toolTip">
@@ -196,10 +164,10 @@ custom stylesheet below and can look nicer than the wiki option. The 'Markdown'
<item>
<widget class="Gui::PrefRadioButton" name="radioTab">
<property name="toolTip">
<string>The documentation will open in a new tab inside the FreeCAD interface. This requires the PySide QtWebengineWidgets component.</string>
<string>The documentation will open in a new tab inside the application. This requires the PySide QtWebEngineWidgets component.</string>
</property>
<property name="text">
<string>In a FreeCAD tab</string>
<string>In an application tab</string>
</property>
<property name="checked">
<bool>false</bool>
@@ -218,8 +186,8 @@ custom stylesheet below and can look nicer than the wiki option. The 'Markdown'
<bool>true</bool>
</property>
<property name="toolTip">
<string>Documentation opens in a dockable dialog within FreeCAD, allowing simultaneous work in the 3D view.
Requires the PySide QtWebengineWidgets component.</string>
<string>Documentation opens in a dockable dialog, allowing simultaneous work in the 3D view.
Requires the PySide QtWebEngineWidgets component.</string>
</property>
<property name="text">
<string>In a separate, embeddable dialog</string>

View File

@@ -4,6 +4,7 @@
add_executable(Gui_tests_run
Assistant.cpp
Camera.cpp
OriginManager.cpp
StyleParameters/StyleParametersApplicationTest.cpp
StyleParameters/ParserTest.cpp
StyleParameters/ParameterManagerTest.cpp

View File

@@ -0,0 +1,382 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <gtest/gtest.h>
#include <string>
#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/PropertyStandard.h>
#include <Gui/FileOrigin.h>
#include <Gui/OriginManager.h>
#include <src/App/InitApplication.h>
namespace
{
/**
* Minimal FileOrigin implementation for testing OriginManager.
* All document operations are stubs -- we only need identity,
* capability, and ownership methods to test the manager.
*/
class MockOrigin : public Gui::FileOrigin
{
public:
explicit MockOrigin(std::string originId,
Gui::OriginType originType = Gui::OriginType::PLM,
bool ownsAll = false)
: _id(std::move(originId))
, _type(originType)
, _ownsAll(ownsAll)
{}
// Identity
std::string id() const override { return _id; }
std::string name() const override { return _id + " Name"; }
std::string nickname() const override { return _id; }
QIcon icon() const override { return {}; }
Gui::OriginType type() const override { return _type; }
// Characteristics
bool tracksExternally() const override { return _type == Gui::OriginType::PLM; }
bool requiresAuthentication() const override { return _type == Gui::OriginType::PLM; }
// Capabilities
bool supportsRevisions() const override { return _supportsRevisions; }
bool supportsBOM() const override { return _supportsBOM; }
bool supportsPartNumbers() const override { return _supportsPartNumbers; }
// Document identity
std::string documentIdentity(App::Document* /*doc*/) const override { return {}; }
std::string documentDisplayId(App::Document* /*doc*/) const override { return {}; }
bool ownsDocument(App::Document* doc) const override
{
if (!doc) {
return false;
}
return _ownsAll;
}
// Document operations (stubs)
App::Document* newDocument(const std::string& /*name*/) override { return nullptr; }
App::Document* openDocument(const std::string& /*identity*/) override { return nullptr; }
App::Document* openDocumentInteractive() override { return nullptr; }
bool saveDocument(App::Document* /*doc*/) override { return false; }
bool saveDocumentAs(App::Document* /*doc*/, const std::string& /*id*/) override
{
return false;
}
bool saveDocumentAsInteractive(App::Document* /*doc*/) override { return false; }
// Test controls
bool _supportsRevisions = true;
bool _supportsBOM = true;
bool _supportsPartNumbers = true;
private:
std::string _id;
Gui::OriginType _type;
bool _ownsAll;
};
} // namespace
// =========================================================================
// LocalFileOrigin identity tests
// =========================================================================
class LocalFileOriginTest : public ::testing::Test
{
protected:
static void SetUpTestSuite()
{
tests::initApplication();
}
void SetUp() override
{
_origin = std::make_unique<Gui::LocalFileOrigin>();
}
Gui::LocalFileOrigin* origin() { return _origin.get(); }
private:
std::unique_ptr<Gui::LocalFileOrigin> _origin;
};
TEST_F(LocalFileOriginTest, LocalOriginId)
{
EXPECT_EQ(origin()->id(), "local");
}
TEST_F(LocalFileOriginTest, LocalOriginName)
{
EXPECT_EQ(origin()->name(), "Local Files");
}
TEST_F(LocalFileOriginTest, LocalOriginNickname)
{
EXPECT_EQ(origin()->nickname(), "Local");
}
TEST_F(LocalFileOriginTest, LocalOriginType)
{
EXPECT_EQ(origin()->type(), Gui::OriginType::Local);
}
TEST_F(LocalFileOriginTest, LocalOriginCapabilities)
{
EXPECT_FALSE(origin()->tracksExternally());
EXPECT_FALSE(origin()->requiresAuthentication());
EXPECT_FALSE(origin()->supportsRevisions());
EXPECT_FALSE(origin()->supportsBOM());
EXPECT_FALSE(origin()->supportsPartNumbers());
EXPECT_FALSE(origin()->supportsAssemblies());
}
// =========================================================================
// OriginManager tests
// =========================================================================
class OriginManagerTest : public ::testing::Test
{
protected:
static void SetUpTestSuite()
{
tests::initApplication();
}
void SetUp() override
{
// Ensure clean singleton state for each test
Gui::OriginManager::destruct();
_mgr = Gui::OriginManager::instance();
}
void TearDown() override
{
Gui::OriginManager::destruct();
}
Gui::OriginManager* mgr() { return _mgr; }
private:
Gui::OriginManager* _mgr = nullptr;
};
// --- Registration ---
TEST_F(OriginManagerTest, LocalOriginAlwaysPresent)
{
auto* local = mgr()->getOrigin("local");
ASSERT_NE(local, nullptr);
EXPECT_EQ(local->id(), "local");
}
TEST_F(OriginManagerTest, RegisterCustomOrigin)
{
auto* raw = new MockOrigin("test-plm");
EXPECT_TRUE(mgr()->registerOrigin(raw));
EXPECT_EQ(mgr()->getOrigin("test-plm"), raw);
}
TEST_F(OriginManagerTest, RejectDuplicateId)
{
mgr()->registerOrigin(new MockOrigin("dup"));
// Second registration with same ID should fail
EXPECT_FALSE(mgr()->registerOrigin(new MockOrigin("dup")));
}
TEST_F(OriginManagerTest, RejectNullOrigin)
{
EXPECT_FALSE(mgr()->registerOrigin(nullptr));
}
TEST_F(OriginManagerTest, OriginIdsIncludesAll)
{
mgr()->registerOrigin(new MockOrigin("alpha"));
mgr()->registerOrigin(new MockOrigin("beta"));
auto ids = mgr()->originIds();
EXPECT_EQ(ids.size(), 3u); // local + alpha + beta
auto has = [&](const std::string& id) {
return std::find(ids.begin(), ids.end(), id) != ids.end();
};
EXPECT_TRUE(has("local"));
EXPECT_TRUE(has("alpha"));
EXPECT_TRUE(has("beta"));
}
// --- Unregistration ---
TEST_F(OriginManagerTest, UnregisterCustomOrigin)
{
mgr()->registerOrigin(new MockOrigin("removable"));
EXPECT_TRUE(mgr()->unregisterOrigin("removable"));
EXPECT_EQ(mgr()->getOrigin("removable"), nullptr);
}
TEST_F(OriginManagerTest, CannotUnregisterLocal)
{
EXPECT_FALSE(mgr()->unregisterOrigin("local"));
EXPECT_NE(mgr()->getOrigin("local"), nullptr);
}
TEST_F(OriginManagerTest, UnregisterCurrentSwitchesToLocal)
{
mgr()->registerOrigin(new MockOrigin("ephemeral"));
mgr()->setCurrentOrigin("ephemeral");
EXPECT_EQ(mgr()->currentOriginId(), "ephemeral");
mgr()->unregisterOrigin("ephemeral");
EXPECT_EQ(mgr()->currentOriginId(), "local");
}
// --- Current origin selection ---
TEST_F(OriginManagerTest, DefaultCurrentIsLocal)
{
EXPECT_EQ(mgr()->currentOriginId(), "local");
EXPECT_NE(mgr()->currentOrigin(), nullptr);
}
TEST_F(OriginManagerTest, SetCurrentOrigin)
{
mgr()->registerOrigin(new MockOrigin("plm1"));
std::string notified;
auto conn = mgr()->signalCurrentOriginChanged.connect([&](const std::string& id) {
notified = id;
});
EXPECT_TRUE(mgr()->setCurrentOrigin("plm1"));
EXPECT_EQ(mgr()->currentOriginId(), "plm1");
EXPECT_EQ(notified, "plm1");
}
TEST_F(OriginManagerTest, SetCurrentRejectsUnknown)
{
EXPECT_FALSE(mgr()->setCurrentOrigin("nonexistent"));
EXPECT_EQ(mgr()->currentOriginId(), "local");
}
TEST_F(OriginManagerTest, SetCurrentSameIdNoSignal)
{
int signalCount = 0;
auto conn = mgr()->signalCurrentOriginChanged.connect([&](const std::string&) {
signalCount++;
});
mgr()->setCurrentOrigin("local"); // already current
EXPECT_EQ(signalCount, 0);
}
// --- Document ownership ---
class OriginManagerDocTest : public ::testing::Test
{
protected:
static void SetUpTestSuite()
{
tests::initApplication();
}
void SetUp() override
{
Gui::OriginManager::destruct();
_mgr = Gui::OriginManager::instance();
_docName = App::GetApplication().getUniqueDocumentName("test");
_doc = App::GetApplication().newDocument(_docName.c_str(), "testUser");
}
void TearDown() override
{
App::GetApplication().closeDocument(_docName.c_str());
Gui::OriginManager::destruct();
}
Gui::OriginManager* mgr() { return _mgr; }
App::Document* doc() { return _doc; }
private:
Gui::OriginManager* _mgr = nullptr;
std::string _docName;
App::Document* _doc = nullptr;
};
TEST_F(OriginManagerDocTest, LocalOwnsPlainDocument)
{
// A document with no SiloItemId property should be owned by local
auto* local = mgr()->getOrigin("local");
ASSERT_NE(local, nullptr);
EXPECT_TRUE(local->ownsDocument(doc()));
}
TEST_F(OriginManagerDocTest, LocalDisownsTrackedDocument)
{
// Add an object with a SiloItemId property -- local should reject ownership
auto* obj = doc()->addObject("App::FeaturePython", "TrackedPart");
ASSERT_NE(obj, nullptr);
obj->addDynamicProperty("App::PropertyString", "SiloItemId");
auto* prop = dynamic_cast<App::PropertyString*>(obj->getPropertyByName("SiloItemId"));
ASSERT_NE(prop, nullptr);
prop->setValue("some-uuid");
auto* local = mgr()->getOrigin("local");
EXPECT_FALSE(local->ownsDocument(doc()));
}
TEST_F(OriginManagerDocTest, FindOwningOriginPrefersNonLocal)
{
// Register a PLM mock that claims ownership of everything
auto* plm = new MockOrigin("test-plm", Gui::OriginType::PLM, /*ownsAll=*/true);
mgr()->registerOrigin(plm);
auto* owner = mgr()->findOwningOrigin(doc());
ASSERT_NE(owner, nullptr);
EXPECT_EQ(owner->id(), "test-plm");
}
TEST_F(OriginManagerDocTest, FindOwningOriginFallsBackToLocal)
{
// No PLM origins registered -- should fall back to local
auto* owner = mgr()->findOwningOrigin(doc());
ASSERT_NE(owner, nullptr);
EXPECT_EQ(owner->id(), "local");
}
TEST_F(OriginManagerDocTest, OriginForNewDocumentReturnsCurrent)
{
mgr()->registerOrigin(new MockOrigin("plm2"));
mgr()->setCurrentOrigin("plm2");
auto* origin = mgr()->originForNewDocument();
ASSERT_NE(origin, nullptr);
EXPECT_EQ(origin->id(), "plm2");
}
TEST_F(OriginManagerDocTest, SetAndClearDocumentOrigin)
{
auto* local = mgr()->getOrigin("local");
mgr()->setDocumentOrigin(doc(), local);
EXPECT_EQ(mgr()->originForDocument(doc()), local);
mgr()->clearDocumentOrigin(doc());
// After clearing, originForDocument falls back to ownership detection
auto* resolved = mgr()->originForDocument(doc());
ASSERT_NE(resolved, nullptr);
EXPECT_EQ(resolved->id(), "local");
}
TEST_F(OriginManagerDocTest, NullDocumentHandling)
{
EXPECT_EQ(mgr()->findOwningOrigin(nullptr), nullptr);
EXPECT_EQ(mgr()->originForDocument(nullptr), nullptr);
}