Compare commits
75 Commits
v0.1.2
...
docs/mdboo
| Author | SHA1 | Date | |
|---|---|---|---|
| 64bde8d97a | |||
| a9de667914 | |||
|
|
353efe51e2 | ||
| 6d828a6459 | |||
|
|
b7374d7b1f | ||
| 7ef7ce1dfc | |||
| 7411508b36 | |||
| 766f9b491c | |||
| ad34cffcd6 | |||
| cecd4988a8 | |||
|
|
977fa3c934 | ||
|
|
f0f4c068d2 | ||
|
|
5f43b534d9 | ||
| 599d4b23a8 | |||
| d136ee3ebe | |||
| 561d634934 | |||
|
|
eb57f80e72 | ||
|
|
098f0233c4 | ||
| 68690f3b22 | |||
| 087fe99324 | |||
| 7033744656 | |||
|
|
84c1776f92 | ||
| f391c526cd | |||
| ac4f01c5c0 | |||
| 1d2a11572d | |||
|
|
b3a6c9d925 | ||
| 7c84eeb68d | |||
|
|
590cb9f6c3 | ||
|
|
0a0ac547ef | ||
|
|
6773ca0dfd | ||
|
|
82e7362a56 | ||
| cf523f1d87 | |||
|
|
4bf74cf339 | ||
| 3b07a0f99b | |||
|
|
9cabb29824 | ||
| 793e6bdd49 | |||
| b6ab14cc6f | |||
| da46073d68 | |||
|
|
262dfa583d | ||
|
|
cc5ba638d1 | ||
|
|
35302154ae | ||
|
|
be7f1d9221 | ||
|
|
10b5c9d584 | ||
|
|
ee839c23b8 | ||
|
|
67f825c305 | ||
| 440df2a9be | |||
|
|
1750949afd | ||
| b2c6fc2ebc | |||
|
|
d0c67455dc | ||
|
|
62906b0269 | ||
| 99bc7629e7 | |||
|
|
d95c850b7b | ||
| 1f7dae4f11 | |||
| 1d7e4e2eef | |||
|
|
fe50b1595e | ||
|
|
0e5a259d14 | ||
|
|
bfb2728f8d | ||
|
|
7bec3d5c3b | ||
|
|
145c29d7d6 | ||
|
|
8ba7b73aa8 | ||
|
|
1e4deea130 | ||
|
|
3923e2e4b9 | ||
|
|
9e29b76fbc | ||
|
|
88e025f1c6 | ||
|
|
772d3b3288 | ||
|
|
dfa2b73966 | ||
|
|
056b015e78 | ||
| 6649372f7b | |||
| 5db68dab25 | |||
|
|
c59c704da3 | ||
|
|
a2200b4042 | ||
|
|
c858706d48 | ||
|
|
724440dcb7 | ||
|
|
2f594dac0a | ||
|
|
939b81385e |
11
.gitea/runner/cleanup.service
Normal 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
@@ -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
|
||||
10
.gitea/runner/cleanup.timer
Normal 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
|
||||
@@ -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
|
||||
|
||||
22
.gitea/workflows/docs.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: docs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build mdBook
|
||||
run: mdbook build docs/
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
rm -rf /opt/git/docs/book/*
|
||||
cp -r docs/book/* /opt/git/docs/book/
|
||||
@@ -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
|
||||
@@ -315,22 +345,82 @@ jobs:
|
||||
ls -lah release/
|
||||
|
||||
- name: Create release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: release/*
|
||||
title: "Kindred Create ${{ env.BUILD_TAG }}"
|
||||
body: |
|
||||
## Kindred Create ${{ env.BUILD_TAG }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITEA_URL: ${{ github.server_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
TAG="${BUILD_TAG}"
|
||||
|
||||
### Downloads
|
||||
# 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}
|
||||
|
||||
| Platform | File |
|
||||
|----------|------|
|
||||
| Linux (AppImage) | `KindredCreate-*-Linux-x86_64.AppImage` |
|
||||
| Linux (Debian/Ubuntu) | `kindred-create_*.deb` |
|
||||
### Downloads
|
||||
|
||||
*macOS and Windows builds are not yet available.*
|
||||
| Platform | File |
|
||||
|----------|------|
|
||||
| Linux (AppImage) | \`KindredCreate-*-Linux-x86_64.AppImage\` |
|
||||
| Linux (Debian/Ubuntu) | \`kindred-create_*.deb\` |
|
||||
|
||||
SHA256 checksums are provided alongside each artifact.
|
||||
prerelease: ${{ contains(github.ref_name, 'rc') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') }}
|
||||
api_key: ${{ secrets.RELEASE_TOKEN }}
|
||||
*macOS and Windows builds are not yet available.*
|
||||
|
||||
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}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}")
|
||||
if [ "$existing" = "200" ]; then
|
||||
release_id=$(curl -s \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" | \
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${release_id}"
|
||||
echo "Deleted existing release ${release_id} for tag ${TAG}"
|
||||
fi
|
||||
|
||||
# Create release
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-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}..."
|
||||
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}")
|
||||
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
|
||||
|
||||
3
.gitignore
vendored
@@ -71,3 +71,6 @@ files_to_translate.txt
|
||||
# pixi environments
|
||||
.pixi
|
||||
*.egg-info
|
||||
|
||||
# mdBook build output
|
||||
docs/book/
|
||||
|
||||
2
.gitmodules
vendored
@@ -15,4 +15,4 @@
|
||||
url = https://git.kindred-systems.com/forbes/ztools.git
|
||||
[submodule "mods/silo"]
|
||||
path = mods/silo
|
||||
url = https://git.kindred-systems.com/kindred/silo.git
|
||||
url = https://git.kindred-systems.com/kindred/silo-mod.git
|
||||
|
||||
200
CONTRIBUTING.md
@@ -1,111 +1,131 @@
|
||||
# FreeCAD Contribution Process (FCP)
|
||||
# Contributing to Kindred Create
|
||||
|
||||
FreeCAD's contribution process is inspired by the Collective Code Construction Contract which itself is an evolution of the github.com Fork and Pull Model.
|
||||
Kindred Create is maintained at [git.kindred-systems.com/kindred/create](https://git.kindred-systems.com/kindred/create). Contributions are submitted as pull requests against the `main` branch.
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
|
||||
cd create
|
||||
pixi run configure
|
||||
pixi run build
|
||||
pixi run freecad
|
||||
```
|
||||
|
||||
## 0. Status
|
||||
See the [README](README.md) for full build instructions.
|
||||
|
||||
FreeCAD is in a transition period. The following are to be regarded as GUIDELINES for contribution submission and acceptance. For historical reasons, the actual process MAY diverge from this process during the transition. Such deviations SHOULD be noted and discussed whenever possible.
|
||||
## Branch and PR workflow
|
||||
|
||||
## 1. Goals
|
||||
1. Create a feature branch from `main`:
|
||||
```bash
|
||||
git checkout -b feat/my-feature main
|
||||
```
|
||||
2. Make your changes, commit with conventional commit messages (see below).
|
||||
3. Push and open a pull request against `main`.
|
||||
4. CI builds and tests run automatically on all PRs.
|
||||
|
||||
The FreeCAD Contribution Process is expressed here with the following specific goals in mind:
|
||||
## Commit messages
|
||||
|
||||
1. To provide transparency and fairness in the contribution process.
|
||||
2. To allow contributions to be included as quickly as possible.
|
||||
3. To preserve and improve the code quality while encouraging appropriate experimentation and risk-taking.
|
||||
4. To minimize dependence on individual Contributors by encouraging a large pool of active Contributors.
|
||||
5. To be inclusive of many viewpoints and to harness a diverse set of skills.
|
||||
6. To provide an encouraging environment where Contributors learn and improve their skills.
|
||||
7. To protect the free and open nature of the FreeCAD project.
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
## 2. Fundamentals
|
||||
| Prefix | Purpose |
|
||||
|--------|---------|
|
||||
| `feat:` | New feature |
|
||||
| `fix:` | Bug fix |
|
||||
| `chore:` | Maintenance, dependencies |
|
||||
| `docs:` | Documentation only |
|
||||
| `art:` | Icons, theme, visual assets |
|
||||
|
||||
1. FreeCAD uses the git distributed revision control system.
|
||||
2. Source code for the main application and related subprojects is hosted on github.com in the FreeCAD organization.
|
||||
3. Problems are discrete, well-defined limitations or bugs.
|
||||
4. FreeCAD uses GitHub's issue-tracking system to track problems and contributions. For help requests and general discussions, use the project forum.
|
||||
5. Contributions are sets of code changes that resolve a single problem.
|
||||
6. FreeCAD uses the Pull Request workflow for evaluating and accepting contributions.
|
||||
Examples:
|
||||
- `feat: add datum point creation mode`
|
||||
- `fix(gui): correct menu icon size on Wayland`
|
||||
- `chore: update silo submodule`
|
||||
|
||||
## 3. Roles
|
||||
1. "User": A member of the wider FreeCAD community who uses the software.
|
||||
2. "Contributor": A person who submits a contribution that resolves a previously identified problem. Contributors do not have commit access to the repository unless they are also Maintainers. Everyone, without distinction or discrimination, SHALL have an equal right to become a Contributor.
|
||||
3. "Maintainer": A person who merges contributions. Maintainers may or may not be Contributors. Their role is to enforce the process. Maintainers have commit access to the repository.
|
||||
4. "Administrator": Administrators have additional authority to maintain the list of designated Maintainers.
|
||||
## Code style
|
||||
|
||||
## 4. Licensing, Ownership, and Credit
|
||||
1. FreeCAD is distributed under the Lesser General Public License, version 2, or superior (LGPL2+). Additional details can be found in the LICENSE file.
|
||||
2. All contributions to FreeCAD MUST use a compatible license.
|
||||
3. All contributions are owned by their authors unless assigned to another.
|
||||
4. FreeCAD does not have a mandatory copyright assignment policy.
|
||||
5. A Contributor who wishes to be identified in the Credits section of the application "About" dialog is responsible for identifying themselves. They should modify the Contributors file and submit a PR with a single commit for this modification only. The contributors file is found at https://github.com/FreeCAD/FreeCAD/blob/main/src/Doc/CONTRIBUTORS
|
||||
6. A contributor who does not wish to assume the copyright of their contribution MAY choose to assign it to the [FreeCAD project association](https://fpa.freecad.org) by mentioning **Copyright (c) 2022 The FreeCAD project association <fpa@freecad.org>** in the file's license code block.
|
||||
### C/C++
|
||||
|
||||
## 5. Contribution Requirements
|
||||
Formatted with **clang-format** (config in `.clang-format`). Static analysis via **clang-tidy** (config in `.clang-tidy`).
|
||||
|
||||
1. Contributions are submitted in the form of Pull Requests (PR).
|
||||
2. Maintainers and Contributors MUST have a GitHub account and SHOULD use their real names or a well-known alias.
|
||||
3. If the GitHub username differs from the username on the FreeCAD Forum, effort SHOULD be taken to avoid confusion.
|
||||
4. A PR SHOULD be a minimal and accurate answer to exactly one identified and agreed-on problem.
|
||||
5. A PR SHOULD refrain from adding additional dependencies to the FreeCAD project unless no other option is available.
|
||||
6. Code submissions MUST adhere to the code style guidelines of the project if these are defined.
|
||||
7. If a PR contains multiple commits, each commit MUST compile cleanly when merged with all previous commits of the same PR. Each commit SHOULD add value to the history of the project. Checkpoint commits SHOULD be squashed.
|
||||
8. A PR SHALL NOT include non-trivial code from other projects unless the Contributor is the original author of that code.
|
||||
9. A PR MUST compile cleanly and pass project self-tests on all target platforms.
|
||||
10. Changes that break python API used by extensions SHALL be avoided. If it is not possible to avoid breaking changes, the amount of them MUST be minimized and PR MUST clearly describe all breaking changes with clear description on how to replace no longer working solution with newer one. Contributor SHOULD search for addons that will be broken and list them in the PR.
|
||||
11. Each commit message in a PR MUST succinctly explain what the commit achieves. The commit message SHALL follow the suggestions in the `git commit --help` documentation, section DISCUSSION.
|
||||
12. The PR Title MUST succinctly explain what the PR achieves. The Body MAY be as detailed as needed. If a PR changes the user interface (UI), the body of the text MUST include a presentation of these UI changes, preferably with screenshots of the previous and revised state.
|
||||
13. If a PR contains the work of another author (for example, if it is cherry-picked from a fork by someone other than the PR-submitter):
|
||||
1. the PR description MUST contain proper attribution as the first line, for example: "This is work of XYZ cherry-picked from <link>";
|
||||
2. all commits MUST have proper authorship, i.e. be authored by the original author and committed by the author of the PR;
|
||||
3. if changes to cherry-picked commits are necessary they SHOULD be done as follow-up commits. If it is not possible to do so, then the modified commits MUST contain a `Co-Authored-By` trailer in their commit message.
|
||||
14. A “Valid PR” is one which satisfies the above requirements.
|
||||
### Python
|
||||
|
||||
## 6. Process
|
||||
Formatted with **black** (100-character line length). Linted with **pylint** (config in `.pylintrc`).
|
||||
|
||||
1. Change on the project follows the pattern of accurately identifying problems and applying minimal, accurate solutions to these problems.
|
||||
2. To request changes, a User logs an issue on the project GitHub issue tracker.
|
||||
3. The User or Contributor SHOULD write the issue by describing the problem they face or observe. Links to the forum or other resources are permitted but the issue SHOULD be complete and accurate and SHOULD NOT require the reader to visit the forum or any other platform to understand what is being described.
|
||||
4. Issue authors SHOULD strive to describe the minimum acceptable condition.
|
||||
5. Issue authors SHOULD focus on User tasks and avoid comparisons to other software solutions.
|
||||
6. The User or Contributor SHOULD seek consensus on the accuracy of their observation and the value of solving the problem.
|
||||
7. To submit a solution to a problem, a Contributor SHALL create a pull request back to the project.
|
||||
8. Contributors and Maintainers SHALL NOT commit changes directly to the target branch.
|
||||
9. To discuss a proposed solution, Users MAY comment on the Pull Request in GitHub. Forum conversations regarding the solution SHOULD be discouraged and conversation redirected to the Pull Request or the related issue.
|
||||
10. To accept or reject a Pull Request, a Maintainer SHALL use GitHub's interface.
|
||||
11. Maintainers SHOULD NOT merge their own PRs except:
|
||||
1. in exceptional cases, such as non-responsiveness from other Maintainers for an extended period.
|
||||
2. If the Maintainer is also the primary developer of the workbench or subsystem.
|
||||
### Pre-commit hooks
|
||||
|
||||
12. Maintainers SHALL merge valid PRs from other Contributors rapidly.
|
||||
13. Maintainers MAY, at their discretion merge PRs that have not met all criteria to be considered valid to:
|
||||
1. end fruitless discussions
|
||||
2. capture toxic contributions in the historical record
|
||||
3. engage with the Contributor on improving their contribution quality.
|
||||
14. Maintainers SHALL NOT make value judgments on correct contributions.
|
||||
15. If a PR requires significant further work before merging, the PR SHOULD be moved to draft status.
|
||||
16. If a PR is complete, but should not be merged yet (for example, because it depends on another in-process PR), the "On hold" label SHOULD be applied.
|
||||
17. Any Contributor who has value judgments on a PR SHOULD express these via their own PR.
|
||||
18. The User who created an issue SHOULD close the issue after checking the PR is successful.
|
||||
19. Maintainers SHOULD close issues that are left open without action or update for an unreasonable period.
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## 7. Branches and Releases
|
||||
This runs clang-format, black, and pylint automatically on staged files.
|
||||
|
||||
1. The project SHALL have one branch (“main”) that always holds the latest in-progress version and SHOULD always build.
|
||||
2. The project SHALL NOT use topic branches for any reason. Personal forks MAY use topic branches.
|
||||
3. To make a stable release a Maintainer SHALL tag the repository. Stable releases SHALL always be released from the repository main branch.
|
||||
## Submodules
|
||||
|
||||
## 8. Project Administration
|
||||
Kindred Create uses git submodules for addon workbenches:
|
||||
|
||||
1. Project Administrators are those individuals who are members of the FreeCAD Github organization and have the role of 'owner'. They have the task of administering the organization including adding and removing individuals from various teams.
|
||||
2. Project Administrator is a technical role necessitated by the GitHub platform. Except for the specific exceptions listed below, the Project Administrators do not make the decision about individual team members. Rather, they carry out the collective wishes of the Maintainers team. Project Administrators will be selected from the Maintainers team by the Maintainers themselves.
|
||||
3. To ensure continuity there SHALL be at least four Project Administrators at all times.
|
||||
4. The project Administrators will manage the set of project Maintainers. They SHALL maintain a sufficiently large pool of Maintainers to ensure their succession and permit timely review of contributions. If the pool of Maintainers is insufficient, the Project Administrators will request that the Maintainers select additional individuals to add.
|
||||
5. Contributors who have a history of successful PRs and have demonstrated continued professionalism should be invited to be Maintainers.
|
||||
6. Administrators SHOULD remove Maintainers who are inactive for an extended period, or who repeatedly fail to apply this process accurately.
|
||||
7. The list of Maintainers SHALL be publicly accessible and reflective of current activity on the project.
|
||||
8. Administrators SHALL act expediently to protect the FreeCAD infrastructure and resources.
|
||||
9. Administrators SHOULD block or ban “bad actors” who cause stress, animosity, or confusion to others in the project. This SHOULD be done after public discussion, with a chance for all parties to speak. A bad actor is someone who repeatedly ignores the rules and culture of the project, who is hostile or offensive, who impedes the productive exchange of information, and who is unable to self-correct their behavior when asked to do so by others.
|
||||
| Submodule | Path | Repository |
|
||||
|-----------|------|------------|
|
||||
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` |
|
||||
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` |
|
||||
|
||||
To update a submodule:
|
||||
|
||||
```bash
|
||||
cd mods/silo
|
||||
git checkout main && git pull
|
||||
cd ../..
|
||||
git add mods/silo
|
||||
git commit -m "chore: update silo submodule"
|
||||
```
|
||||
|
||||
If you cloned without `--recursive`, initialize submodules with:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## Theme and QSS changes
|
||||
|
||||
The Catppuccin Mocha theme has **three QSS copies** that must be kept in sync:
|
||||
|
||||
1. `resources/preferences/KindredCreate/KindredCreate.qss` (canonical)
|
||||
2. `src/Gui/Stylesheets/KindredCreate.qss`
|
||||
3. `src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`
|
||||
|
||||
When modifying the theme, apply changes to all three files. Note that the copies have intentional differences (e.g., tree branch rendering style), so do not blindly copy between them -- apply edits individually.
|
||||
|
||||
See [KNOWN_ISSUES.md](docs/KNOWN_ISSUES.md) for the planned QSS consolidation.
|
||||
|
||||
## Preference pack
|
||||
|
||||
Default preferences are defined in `resources/preferences/KindredCreate/KindredCreate.cfg`. This XML file uses FreeCAD's parameter format:
|
||||
|
||||
```xml
|
||||
<FCParamGroup Name="GroupName">
|
||||
<FCBool Name="Setting" Value="1"/>
|
||||
<FCInt Name="Setting" Value="42"/>
|
||||
<FCText Name="Setting">value</FCText>
|
||||
</FCParamGroup>
|
||||
```
|
||||
|
||||
Changes here affect the out-of-box experience for all users.
|
||||
|
||||
## CI/CD
|
||||
|
||||
- **Build workflow** (`build.yml`): Runs on every push to `main` and on PRs. Builds in Ubuntu 24.04 container, runs C++ and Python tests.
|
||||
- **Release workflow** (`release.yml`): Triggered by `v*` tags. Builds AppImage and .deb packages.
|
||||
|
||||
See [docs/CI_CD.md](docs/CI_CD.md) for full details.
|
||||
|
||||
## Architecture
|
||||
|
||||
For an overview of the codebase structure, bootstrap flow, and design decisions, see:
|
||||
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) -- Bootstrap flow and source layout
|
||||
- [docs/COMPONENTS.md](docs/COMPONENTS.md) -- Feature inventory
|
||||
- [docs/INTEGRATION_PLAN.md](docs/INTEGRATION_PLAN.md) -- Architecture layers and roadmap
|
||||
|
||||
## License
|
||||
|
||||
All contributions must be compatible with [LGPL-2.1-or-later](LICENSE).
|
||||
|
||||
61
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Architecture
|
||||
|
||||
## Bootstrap flow
|
||||
|
||||
```
|
||||
FreeCAD startup
|
||||
└─ src/Mod/Create/Init.py
|
||||
└─ setup_kindred_addons()
|
||||
├─ exec(mods/ztools/ztools/Init.py)
|
||||
└─ exec(mods/silo/freecad/Init.py)
|
||||
|
||||
└─ src/Mod/Create/InitGui.py
|
||||
├─ setup_kindred_workbenches()
|
||||
│ ├─ exec(mods/ztools/ztools/InitGui.py)
|
||||
│ │ ├─ registers ZToolsWorkbench
|
||||
│ │ └─ installs _ZToolsPartDesignManipulator (global)
|
||||
│ └─ exec(mods/silo/freecad/InitGui.py)
|
||||
│ └─ registers SiloWorkbench
|
||||
└─ Deferred setup (QTimer):
|
||||
├─ 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 (SSE)
|
||||
└─ 10000ms: _check_for_updates() → update checker (Gitea API)
|
||||
```
|
||||
|
||||
## Key source layout
|
||||
|
||||
```
|
||||
src/Mod/Create/ Kindred bootstrap module (Python)
|
||||
├── Init.py Adds mods/ addon paths, loads Init.py files
|
||||
├── InitGui.py Loads workbenches, installs Silo manipulators
|
||||
├── version.py.in CMake template → version.py (build-time)
|
||||
└── update_checker.py Checks Gitea releases API for updates
|
||||
|
||||
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin
|
||||
src/Gui/CommandOrigin.cpp Origin_Commit/Pull/Push/Info/BOM commands
|
||||
src/Gui/OriginManager.h/.cpp Origin lifecycle management
|
||||
src/Gui/OriginSelectorWidget.h/.cpp UI for origin selection
|
||||
|
||||
mods/ztools/ [submodule] ztools workbench
|
||||
├── ztools/InitGui.py ZToolsWorkbench + PartDesign manipulator
|
||||
├── ztools/ztools/
|
||||
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
|
||||
│ ├── datums/core.py Datum creation via Part::AttachExtension
|
||||
│ └── resources/ Icons, theme utilities
|
||||
└── CatppuccinMocha/ Theme preference pack (QSS)
|
||||
|
||||
mods/silo/ [submodule -> silo-mod.git] FreeCAD workbench
|
||||
├── silo-client/ [submodule -> silo-client.git] shared API client
|
||||
│ └── silo_client/ SiloClient, SiloSettings, CATEGORY_NAMES
|
||||
└── freecad/ FreeCAD workbench (Python)
|
||||
├── InitGui.py SiloWorkbench
|
||||
├── silo_commands.py Commands + FreeCADSiloSettings adapter
|
||||
└── silo_origin.py FileOrigin backend for Silo
|
||||
|
||||
src/Gui/Stylesheets/ QSS themes and SVG assets
|
||||
src/Gui/PreferencePacks/ KindredCreate preference pack (cfg + build-time QSS)
|
||||
```
|
||||
|
||||
See [INTEGRATION_PLAN.md](INTEGRATION_PLAN.md) for architecture layers and phase status.
|
||||
@@ -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
|
||||
|
||||
|
||||
116
docs/COMPONENTS.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Components
|
||||
|
||||
## ztools workbench
|
||||
|
||||
**Registered commands (9):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) |
|
||||
| `ZTools_DatumManager` | Manage existing datum objects |
|
||||
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) |
|
||||
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation |
|
||||
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly |
|
||||
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis |
|
||||
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles |
|
||||
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment |
|
||||
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation |
|
||||
|
||||
**PartDesign integration** via `_ZToolsPartDesignManipulator`:
|
||||
- `ZTools_DatumCreator`, `ZTools_DatumManager` → "Part Design Helper Features" toolbar
|
||||
- `ZTools_EnhancedPocket` → "Part Design Modeling Features" toolbar
|
||||
- `ZTools_RotatedLinearPattern` → "Part Design Transformation Features" toolbar
|
||||
- Same commands inserted into Part Design menu after `PartDesign_Boolean`
|
||||
|
||||
**Datum types (7):** offset_from_face, offset_from_plane, midplane, 3_points, normal_to_edge, angled, tangent_to_cylinder. All except tangent_to_cylinder use `Part::AttachExtension` for automatic parametric updates.
|
||||
|
||||
---
|
||||
|
||||
## Origin commands (C++)
|
||||
|
||||
The Origin abstraction (`src/Gui/FileOrigin.h`) provides a backend-agnostic interface for document storage. Commands delegate to the active `FileOrigin` implementation (currently `LocalFileOrigin` for local files, `SiloOrigin` via `mods/silo/freecad/silo_origin.py` for Silo-tracked documents).
|
||||
|
||||
**Registered commands (5):**
|
||||
|
||||
| Command | Function | Icon |
|
||||
|---------|----------|------|
|
||||
| `Origin_Commit` | Commit changes as a new revision | `silo-commit` |
|
||||
| `Origin_Pull` | Pull a specific revision from the origin | `silo-pull` |
|
||||
| `Origin_Push` | Push local changes to the origin | `silo-push` |
|
||||
| `Origin_Info` | Show document information from origin | `silo-info` |
|
||||
| `Origin_BOM` | Show Bill of Materials for this document | `silo-bom` |
|
||||
|
||||
These appear in the File menu and "Origin Tools" toolbar across all workbenches (see `src/Gui/Workbench.cpp`).
|
||||
|
||||
---
|
||||
|
||||
## Silo workbench
|
||||
|
||||
**Registered commands (14):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `Silo_New` | Create new Silo-tracked document |
|
||||
| `Silo_Open` | Open file from Silo database |
|
||||
| `Silo_Save` | Save to Silo (create revision) |
|
||||
| `Silo_Commit` | Commit current revision |
|
||||
| `Silo_Pull` | Pull latest revision from server |
|
||||
| `Silo_Push` | Push local changes to server |
|
||||
| `Silo_Info` | View item metadata and history |
|
||||
| `Silo_BOM` | Bill of materials dialog (BOM + Where Used) |
|
||||
| `Silo_TagProjects` | Assign project tags |
|
||||
| `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_Auth` | Login/logout authentication panel |
|
||||
|
||||
**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`
|
||||
|
||||
**Dock panels:**
|
||||
- Database Auth (2000ms) -- Login/logout and API token management
|
||||
- Database Activity (4000ms) -- Real-time server event feed via SSE (Server-Sent Events) with automatic reconnection and exponential backoff
|
||||
- Start Panel -- In-viewport landing page with recent files and Silo integration
|
||||
|
||||
**Server architecture:** Go REST API (38+ routes) + PostgreSQL + MinIO S3. Authentication via local (bcrypt), LDAP, or OIDC backends. SSE endpoint for real-time event streaming. See `mods/silo/docs/` for server documentation.
|
||||
|
||||
**LibreOffice Calc extension** ([silo-calc](https://git.kindred-systems.com/kindred/silo-calc.git)): BOM management, item creation, and AI-assisted descriptions via OpenRouter API. Shares the same Silo REST API and auth token system via the shared [silo-client](https://git.kindred-systems.com/kindred/silo-client.git) package.
|
||||
|
||||
---
|
||||
|
||||
## Theme
|
||||
|
||||
**Canonical source:** `src/Gui/Stylesheets/KindredCreate.qss`
|
||||
|
||||
The PreferencePacks copy (`src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`) is generated at build time via `configure_file()` in `src/Gui/PreferencePacks/CMakeLists.txt`. Only the Stylesheets copy needs to be maintained.
|
||||
|
||||
Notable theme customizations beyond standard Catppuccin colors:
|
||||
- `QGroupBox::indicator` styling to match `QCheckBox::indicator` (consistent checkbox appearance)
|
||||
- `QLabel[haslink="true"]` link color (`#b4befe` Catppuccin Lavender) -- picked up by FreeCAD to set `QPalette::Link`
|
||||
- Spanning-line tree branch indicators
|
||||
|
||||
---
|
||||
|
||||
## Icon infrastructure
|
||||
|
||||
### Qt resource icons (`src/Gui/Icons/`)
|
||||
|
||||
5 `silo-*` SVGs registered in `resource.qrc`, used by C++ Origin commands:
|
||||
|
||||
`silo-bom.svg`, `silo-commit.svg`, `silo-info.svg`, `silo-pull.svg`, `silo-push.svg`
|
||||
|
||||
### Silo module icons (`mods/silo/freecad/resources/icons/`)
|
||||
|
||||
10 SVGs loaded at runtime by the `_icon()` function in `silo_commands.py`:
|
||||
|
||||
`silo-auth.svg`, `silo-bom.svg`, `silo-commit.svg`, `silo-info.svg`, `silo-new.svg`, `silo-open.svg`, `silo-pull.svg`, `silo-push.svg`, `silo-save.svg`, `silo.svg`
|
||||
|
||||
### Missing icons
|
||||
|
||||
3 command icon names have no corresponding SVG file: `silo-tag`, `silo-rollback`, `silo-status`. The `_icon()` function returns an empty string for these, so `Silo_TagProjects`, `Silo_Rollback`, and `Silo_SetStatus` render without toolbar icons.
|
||||
|
||||
### Palette
|
||||
|
||||
All silo-* icons use the Catppuccin Mocha color scheme. See `kindred-icons/README.md` for palette specification and icon design standards.
|
||||
84
docs/KNOWN_ISSUES.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Known Issues
|
||||
|
||||
## Issues
|
||||
|
||||
### Critical
|
||||
|
||||
1. ~~**QSS duplication.**~~ Resolved. The canonical QSS lives in `src/Gui/Stylesheets/KindredCreate.qss`. The PreferencePacks copy is now generated at build time via `configure_file()` in `src/Gui/PreferencePacks/CMakeLists.txt`. The unused `resources/preferences/KindredCreate/` directory has been removed.
|
||||
|
||||
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.**~~ Resolved. `Silo_ToggleMode` removed; file operations now delegate to the selected origin via the unified origin system.
|
||||
|
||||
### High
|
||||
|
||||
4. **Silo authentication not production-hardened.** Local auth (bcrypt) works end-to-end. LDAP (FreeIPA) and OIDC (Keycloak) backends are coded but depend on infrastructure not yet deployed. FreeCAD client has `Silo_Auth` dock panel for login and API token management. Server has session middleware (`alexedwards/scs`), CSRF protection (`nosurf`), and role-based access control (admin/editor/viewer). Migration `009_auth.sql` adds users, api_tokens, and sessions tables.
|
||||
|
||||
5. **No unit tests.** Zero test coverage for ztools and Silo FreeCAD commands. Silo Go backend also lacks tests.
|
||||
|
||||
6. **Assembly solver datum handling is minimal.** The `findPlacement()` fix in `src/Mod/Assembly/UtilsAssembly.py` extracts placement from `obj.Shape.Faces[0]` for `PartDesign::Plane` and from shape vertex for `PartDesign::Point`. Does not handle empty shapes or non-planar datum objects.
|
||||
|
||||
### Medium
|
||||
|
||||
7. **`Silo_BOM` requires Silo-tracked document.** Depends on `SiloPartNumber` property. Unregistered documents show a warning with no registration path.
|
||||
|
||||
8. **PartDesign menu insertion fragility.** `_ZToolsPartDesignManipulator.modifyMenuBar()` inserts after `PartDesign_Boolean`. If upstream renames this command, insertions silently fail.
|
||||
|
||||
9. **tangent_to_cylinder falls back to manual placement.** TangentPlane MapMode requires a vertex reference not collected by the current UI.
|
||||
|
||||
10. **`delete_bom_entry()` bypasses error normalization.** Uses raw `urllib.request` instead of `SiloClient._request()`.
|
||||
|
||||
11. **Missing Silo icons.** Three commands reference icons that don't exist: `silo-tag.svg` (`Silo_TagProjects`), `silo-rollback.svg` (`Silo_Rollback`), `silo-status.svg` (`Silo_SetStatus`). The `_icon()` function returns an empty string, so these commands render without toolbar icons.
|
||||
|
||||
### Fixed (retain for reference)
|
||||
|
||||
12. **OndselSolver Newton-Raphson convergence.** `NewtonRaphson::isConvergedToNumericalLimit()` compared `dxNorms->at(iterNo)` to itself instead of `dxNorms->at(iterNo - 1)`. This prevented convergence detection on complex assemblies, causing solver exhaustion and "grounded object moved" warnings. Fixed in Kindred fork (`src/3rdParty/OndselSolver`). Needs upstreaming to `FreeCAD/OndselSolver`.
|
||||
|
||||
13. **Assembly solver crash on document restore.** `AssemblyObject::onChanged()` called `updateSolveStatus()` when the Group property changed during document restore, triggering the solver while child objects were still deserializing (SIGSEGV). Fixed with `isRestoring()` and `isPerformingTransaction()` guards at `src/Mod/Assembly/App/AssemblyObject.cpp:143`.
|
||||
|
||||
14. **`DlgSettingsGeneral::applyMenuIconSize` visibility.** The method was `private` but called from `StartupProcess.cpp`. Fixed by moving to `public` (PR #49). Also required `Dialog::` namespace qualifier in `StartupProcess.cpp`.
|
||||
|
||||
---
|
||||
|
||||
## Incomplete features
|
||||
|
||||
### Silo
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Authentication | Local auth complete | LDAP/OIDC backends coded, pending infrastructure. Auth dock panel available. |
|
||||
| CSRF protection | Implemented | `nosurf` library on web form routes |
|
||||
| File locking | Not implemented | Needed to prevent concurrent edits |
|
||||
| Odoo ERP integration | Stub only | Returns "not yet implemented" |
|
||||
| Part number date segments | Broken | `formatDate()` returns error |
|
||||
| Location/inventory APIs | Tables exist, no handlers | |
|
||||
| CSV import rollback | Not implemented | `bom_handlers.go` |
|
||||
| SSE event streaming | Implemented | Reconnect logic with exponential backoff |
|
||||
| Database Activity panel | Implemented | Dock panel showing real-time server events |
|
||||
| Start panel | Implemented | In-viewport start page with recent files and Silo integration |
|
||||
|
||||
### ztools
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Tangent-to-cylinder attachment | Manual fallback | No vertex ref in UI |
|
||||
| Angled datum live editing | Incomplete | AttachmentOffset not updated in panel |
|
||||
| Assembly pattern undo | Not implemented | |
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
1. **Authentication hardening** -- Deploy FreeIPA and Keycloak infrastructure. End-to-end test LDAP and OIDC flows. Harden token rotation and session expiry.
|
||||
|
||||
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save.
|
||||
|
||||
3. **File locking** -- Pessimistic locks on `Silo_Open` to prevent concurrent edits. Requires server-side lock table and client-side lock display.
|
||||
|
||||
4. **Build system** -- CMake install rules for `mods/` submodules so packages include ztools and Silo without manual steps.
|
||||
|
||||
5. **Test coverage** -- Unit tests for ztools datum creation, Silo FreeCAD commands, and Go API endpoints.
|
||||
|
||||
6. **QSS consolidation** -- Eliminate the 3-copy QSS duplication via build-time copy or symlinks. The canonical source is `resources/preferences/KindredCreate/KindredCreate.qss`.
|
||||
|
||||
7. **Update notification UI** -- Display in-app notification when a new release is available (issue #30). The update checker backend is already implemented.
|
||||
31
docs/OVERVIEW.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Kindred Create
|
||||
|
||||
**Last updated:** 2026-02-08
|
||||
**Branch:** main @ `cf523f1d87a`
|
||||
**Kindred Create:** v0.1.0
|
||||
**FreeCAD base:** v1.0.0
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Contents |
|
||||
|----------|----------|
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Bootstrap flow, source layout, submodules |
|
||||
| [COMPONENTS.md](COMPONENTS.md) | ztools, Silo, Origin commands, theme, icons |
|
||||
| [KNOWN_ISSUES.md](KNOWN_ISSUES.md) | Bugs, incomplete features, next steps |
|
||||
| [INTEGRATION_PLAN.md](INTEGRATION_PLAN.md) | Architecture layers, integration phases |
|
||||
| [CI_CD.md](CI_CD.md) | Build and release workflows |
|
||||
|
||||
## Submodules
|
||||
|
||||
| Submodule | Path | Source | Pinned commit |
|
||||
|-----------|------|--------|---------------|
|
||||
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` | `3298d1c` |
|
||||
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` | `f9924d3` |
|
||||
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` | `fe41fa3` |
|
||||
| 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)).
|
||||
@@ -1,215 +0,0 @@
|
||||
# Repository State
|
||||
|
||||
**Last updated:** 2026-02-03
|
||||
**Branch:** main @ `0ef9ffcf51`
|
||||
**Kindred Create:** v0.1.0
|
||||
**FreeCAD base:** v1.0.0
|
||||
|
||||
## Submodules
|
||||
|
||||
| Submodule | Path | Source | Pinned commit |
|
||||
|-----------|------|--------|---------------|
|
||||
| ztools | `mods/ztools` | `gitea.kindred.internal/kindred/ztools-0065` | `d2f94c3` |
|
||||
| silo | `mods/silo` | `gitea.kindred.internal/kindred/silo-0062` | `17a10ab` |
|
||||
| OndselSolver | `src/3rdParty/OndselSolver` | `gitea.kindred.internal/kindred/ondsel` | `e32c9cd` |
|
||||
| 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` |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Bootstrap flow
|
||||
|
||||
```
|
||||
FreeCAD startup
|
||||
└─ src/Mod/Create/Init.py
|
||||
└─ setup_kindred_addons()
|
||||
├─ exec(mods/ztools/ztools/Init.py)
|
||||
└─ exec(mods/silo/pkg/freecad/Init.py)
|
||||
|
||||
└─ src/Mod/Create/InitGui.py
|
||||
├─ setup_kindred_workbenches()
|
||||
│ ├─ exec(mods/ztools/ztools/InitGui.py)
|
||||
│ │ ├─ registers ZToolsWorkbench
|
||||
│ │ └─ installs _ZToolsPartDesignManipulator (global)
|
||||
│ └─ exec(mods/silo/pkg/freecad/InitGui.py)
|
||||
│ └─ registers SiloWorkbench
|
||||
└─ Deferred setup (QTimer):
|
||||
├─ 1500ms: _setup_silo_auth_panel() → "Database Auth" dock
|
||||
├─ 2000ms: _setup_silo_menu() → SiloMenuManipulator
|
||||
├─ 3000ms: _check_silo_first_start() → settings prompt
|
||||
└─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock
|
||||
```
|
||||
|
||||
### Key source layout
|
||||
|
||||
```
|
||||
src/Mod/Create/ Kindred bootstrap module (Python)
|
||||
├── Init.py Adds mods/ addon paths, loads Init.py files
|
||||
└── InitGui.py Loads workbenches, installs Silo manipulators
|
||||
|
||||
mods/ztools/ [submodule] ztools workbench
|
||||
├── ztools/InitGui.py ZToolsWorkbench + PartDesign manipulator
|
||||
├── ztools/ztools/
|
||||
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
|
||||
│ ├── datums/core.py Datum creation via Part::AttachExtension
|
||||
│ └── resources/ Icons, theme utilities
|
||||
└── CatppuccinMocha/ Theme preference pack (QSS)
|
||||
|
||||
mods/silo/ [submodule] Silo parts database
|
||||
├── cmd/ Go server entry points
|
||||
├── internal/ Go API, database, storage packages
|
||||
├── pkg/freecad/ FreeCAD workbench (Python)
|
||||
│ ├── InitGui.py SiloWorkbench
|
||||
│ └── silo_commands.py Commands + SiloClient API
|
||||
├── deployments/ Docker compose configuration
|
||||
└── migrations/ PostgreSQL schema migrations
|
||||
|
||||
src/Gui/Stylesheets/ QSS themes and SVG assets
|
||||
resources/preferences/ Canonical preference pack (KindredCreate)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component status
|
||||
|
||||
### ztools workbench
|
||||
|
||||
**Registered commands (9):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) |
|
||||
| `ZTools_DatumManager` | Manage existing datum objects |
|
||||
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) |
|
||||
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation |
|
||||
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly |
|
||||
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis |
|
||||
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles |
|
||||
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment |
|
||||
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation |
|
||||
|
||||
**PartDesign integration** via `_ZToolsPartDesignManipulator`:
|
||||
- `ZTools_DatumCreator`, `ZTools_DatumManager` → "Part Design Helper Features" toolbar
|
||||
- `ZTools_EnhancedPocket` → "Part Design Modeling Features" toolbar
|
||||
- `ZTools_RotatedLinearPattern` → "Part Design Transformation Features" toolbar
|
||||
- Same commands inserted into Part Design menu after `PartDesign_Boolean`
|
||||
|
||||
**Datum types (7):** offset_from_face, offset_from_plane, midplane, 3_points, normal_to_edge, angled, tangent_to_cylinder. All except tangent_to_cylinder use `Part::AttachExtension` for automatic parametric updates.
|
||||
|
||||
### Silo workbench
|
||||
|
||||
**Registered commands (13):**
|
||||
|
||||
| Command | Function |
|
||||
|---------|----------|
|
||||
| `Silo_New` | Create new Silo-tracked document |
|
||||
| `Silo_Open` | Open file from Silo database |
|
||||
| `Silo_Save` | Save to Silo (create revision) |
|
||||
| `Silo_Commit` | Commit current revision |
|
||||
| `Silo_Pull` | Pull latest revision from server |
|
||||
| `Silo_Push` | Push local changes to server |
|
||||
| `Silo_Info` | View item metadata and history |
|
||||
| `Silo_BOM` | Bill of materials dialog (BOM + Where Used) |
|
||||
| `Silo_TagProjects` | Assign project tags |
|
||||
| `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 |
|
||||
|
||||
**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
|
||||
|
||||
**Server architecture:** Go REST API (38 routes) + PostgreSQL + MinIO. See `mods/silo/docs/REPOSITORY_STATUS.md` for route details.
|
||||
|
||||
### Theme
|
||||
|
||||
**Canonical source:** `resources/preferences/KindredCreate/KindredCreate.qss`
|
||||
|
||||
Four copies must stay in sync:
|
||||
1. `resources/preferences/KindredCreate/KindredCreate.qss` (canonical)
|
||||
2. `src/Gui/Stylesheets/KindredCreate.qss`
|
||||
3. `src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss`
|
||||
4. `mods/ztools/CatppuccinMocha/CatppuccinMocha.qss`
|
||||
|
||||
---
|
||||
|
||||
## Known issues
|
||||
|
||||
### Critical
|
||||
|
||||
1. **QSS duplication.** Four copies of the stylesheet must be kept in sync manually. A build step or symlinks should eliminate this.
|
||||
|
||||
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.
|
||||
|
||||
### High
|
||||
|
||||
4. **No authentication on Silo server.** All API endpoints are publicly accessible. Required before multi-user deployment.
|
||||
|
||||
5. **No unit tests.** Zero test coverage for ztools and Silo FreeCAD commands. Silo Go backend also lacks tests.
|
||||
|
||||
6. **Assembly solver datum handling is minimal.** The `findPlacement()` fix extracts placement from `obj.Shape.Faces[0]` for `PartDesign::Plane`. Does not handle empty shapes or non-planar datum objects.
|
||||
|
||||
### Medium
|
||||
|
||||
7. **`Silo_BOM` requires Silo-tracked document.** Depends on `SiloPartNumber` property. Unregistered documents show a warning with no registration path.
|
||||
|
||||
8. **PartDesign menu insertion fragility.** `_ZToolsPartDesignManipulator.modifyMenuBar()` inserts after `PartDesign_Boolean`. If upstream renames this command, insertions silently fail.
|
||||
|
||||
9. **tangent_to_cylinder falls back to manual placement.** TangentPlane MapMode requires a vertex reference not collected by the current UI.
|
||||
|
||||
10. **`delete_bom_entry()` bypasses error normalization.** Uses raw `urllib.request` instead of `SiloClient._request()`.
|
||||
|
||||
---
|
||||
|
||||
## Incomplete features
|
||||
|
||||
### Silo
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Authentication/authorization | Not implemented | Required for multi-user |
|
||||
| File locking | Not implemented | Needed to prevent concurrent edits |
|
||||
| Odoo ERP integration | Stub only | Returns "not yet implemented" |
|
||||
| Part number date segments | Broken | `formatDate()` returns error |
|
||||
| Location/inventory APIs | Tables exist, no handlers | |
|
||||
| CSRF protection | Not implemented | Web UI only |
|
||||
| CSV import rollback | Not implemented | `bom_handlers.go` |
|
||||
|
||||
### ztools
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Tangent-to-cylinder attachment | Manual fallback | No vertex ref in UI |
|
||||
| Angled datum live editing | Incomplete | AttachmentOffset not updated in panel |
|
||||
| Assembly pattern undo | Not implemented | |
|
||||
|
||||
### Integration plan
|
||||
|
||||
| Phase | Feature | Status |
|
||||
|-------|---------|--------|
|
||||
| 1 | Addon auto-loading | Done |
|
||||
| 2 | Enhanced Pocket as C++ feature | Not started |
|
||||
| 3 | Datum C++ helpers | Not started (Python approach used) |
|
||||
| 4 | Theme moved to Create module | Partial (QSS synced, not relocated) |
|
||||
| 5 | Silo deep integration | Done |
|
||||
| 6 | Build system install rules for mods/ | Partial (CI/CD done, CMake install rules pending) |
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
1. **Authentication** -- LDAP/FreeIPA integration for Silo multi-user deployment. Server needs auth middleware; FreeCAD client needs credential storage.
|
||||
|
||||
2. **BOM-Assembly bridge** -- Auto-populate Silo BOM from Assembly component links on save.
|
||||
|
||||
3. **File locking** -- Pessimistic locks on `Silo_Open` to prevent concurrent edits. Requires server-side lock table and client-side lock display.
|
||||
|
||||
4. **Build system** -- CMake install rules for `mods/` submodules so packages include ztools and Silo without manual steps.
|
||||
|
||||
5. **Test coverage** -- Unit tests for ztools datum creation, Silo FreeCAD commands, and Go API endpoints.
|
||||
17
docs/book.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[book]
|
||||
title = "Kindred Create Documentation"
|
||||
authors = ["Kindred Systems LLC"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
|
||||
[build]
|
||||
build-dir = "book"
|
||||
|
||||
[output.html]
|
||||
default-theme = "coal"
|
||||
preferred-dark-theme = "coal"
|
||||
git-repository-url = "https://git.kindred-systems.com/kindred/create"
|
||||
git-repository-icon = "fa-code-branch"
|
||||
additional-css = ["theme/kindred.css"]
|
||||
no-section-label = false
|
||||
33
docs/src/SUMMARY.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](./introduction.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
- [Getting Started](./guide/getting-started.md)
|
||||
- [Installation](./guide/installation.md)
|
||||
- [Building from Source](./guide/building.md)
|
||||
- [Workbenches](./guide/workbenches.md)
|
||||
- [ztools](./guide/ztools.md)
|
||||
- [Silo](./guide/silo.md)
|
||||
|
||||
# Architecture
|
||||
|
||||
- [Overview](./architecture/overview.md)
|
||||
- [Python as Source of Truth](./architecture/python-source-of-truth.md)
|
||||
- [Silo Server](./architecture/silo-server.md)
|
||||
- [OndselSolver](./architecture/ondsel-solver.md)
|
||||
|
||||
# Development
|
||||
|
||||
- [Contributing](./development/contributing.md)
|
||||
- [Code Quality](./development/code-quality.md)
|
||||
- [Repository Structure](./development/repo-structure.md)
|
||||
- [Build System](./development/build-system.md)
|
||||
|
||||
# Reference
|
||||
|
||||
- [Configuration](./reference/configuration.md)
|
||||
- [Glossary](./reference/glossary.md)
|
||||
27
docs/src/architecture/ondsel-solver.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# OndselSolver
|
||||
|
||||
OndselSolver is the assembly constraint solver used by FreeCAD's Assembly workbench. Kindred Create vendors a fork of the solver as a git submodule.
|
||||
|
||||
- **Path:** `src/3rdParty/OndselSolver/`
|
||||
- **Source:** `git.kindred-systems.com/kindred/solver` (Kindred fork)
|
||||
|
||||
## How it works
|
||||
|
||||
The solver uses a **Lagrangian constraint formulation** to resolve assembly constraints (mates, joints, fixed positions). Given a set of parts with geometric constraints between them, it computes positions and orientations that satisfy all constraints simultaneously.
|
||||
|
||||
The Assembly workbench (`src/Mod/Assembly/`) calls the solver whenever constraints are added or modified. Kindred Create has patches to `Assembly/` that extend `findPlacement()` for better datum and origin handling.
|
||||
|
||||
## Why a fork
|
||||
|
||||
The solver is forked from the upstream Ondsel project for:
|
||||
- **Pinned stability** — the submodule is pinned to a known-good commit
|
||||
- **Potential modifications** — the fork allows Kindred-specific patches if needed
|
||||
- **Availability** — hosted on Kindred's Gitea instance for reliable access
|
||||
|
||||
## Future: GNN solver
|
||||
|
||||
There are plans to explore a Graph Neural Network (GNN) approach to constraint solving that could complement or supplement the Lagrangian solver for specific use cases. This is not yet implemented.
|
||||
|
||||
## Related: GSL
|
||||
|
||||
The `src/3rdParty/GSL/` submodule is Microsoft's Guidelines Support Library (`github.com/microsoft/GSL`), providing C++ core guidelines utilities like `gsl::span` and `gsl::not_null`. It is a build dependency, not related to the constraint solver.
|
||||
78
docs/src/architecture/overview.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Architecture Overview
|
||||
|
||||
Kindred Create is structured as a thin integration layer on top of FreeCAD. The design follows three principles:
|
||||
|
||||
1. **Minimal core modifications** — prefer submodule addons over patching FreeCAD internals
|
||||
2. **Graceful degradation** — Create runs without ztools or Silo if submodules are missing
|
||||
3. **Pure Python addons** — workbenches follow FreeCAD's standard addon pattern
|
||||
|
||||
## Three-layer model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ FreeCAD Documents (.FCStd) │ Python source of truth
|
||||
│ Workbench logic (Python) │
|
||||
├─────────────────────────────────┤
|
||||
│ PostgreSQL │ Silo metadata, revisions, BOM
|
||||
├─────────────────────────────────┤
|
||||
│ MinIO (S3-compatible) │ Binary file storage cache
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
FreeCAD documents are the authoritative representation of CAD data. Silo's PostgreSQL database stores metadata (part numbers, revisions, BOM relationships) and MinIO stores the binary `.FCStd` files. The FreeCAD workbench synchronizes between local files and the server.
|
||||
|
||||
## Source layout
|
||||
|
||||
```
|
||||
create/
|
||||
├── src/App/ # Core application (C++)
|
||||
├── src/Base/ # Foundation classes (C++)
|
||||
├── src/Gui/ # GUI framework (C++ + Qt6 + QSS)
|
||||
│ ├── Stylesheets/ # KindredCreate.qss theme
|
||||
│ ├── PreferencePacks/ # Theme preference pack
|
||||
│ ├── Icons/ # silo-*.svg origin icons
|
||||
│ ├── FileOrigin.* # Abstract file origin interface
|
||||
│ └── OriginManager.* # Origin lifecycle management
|
||||
├── src/Mod/ # ~37 FreeCAD modules
|
||||
│ ├── Create/ # Kindred bootstrap module
|
||||
│ ├── Assembly/ # Assembly workbench (Kindred patches)
|
||||
│ ├── PartDesign/ # Part Design (stock + ztools injection)
|
||||
│ └── ... # Other stock FreeCAD modules
|
||||
├── mods/
|
||||
│ ├── ztools/ # Datum/pattern/pocket workbench (submodule)
|
||||
│ └── silo/ # Parts database workbench (submodule)
|
||||
└── src/3rdParty/
|
||||
├── OndselSolver/ # Assembly solver (submodule)
|
||||
└── GSL/ # Guidelines Support Library (submodule)
|
||||
```
|
||||
|
||||
## Bootstrap sequence
|
||||
|
||||
1. FreeCAD core initializes, discovers `src/Mod/Create/`
|
||||
2. `Init.py` runs `setup_kindred_addons()` — adds `mods/ztools/ztools` and `mods/silo/freecad` to `sys.path`, executes their `Init.py`
|
||||
3. GUI phase: `InitGui.py` runs `setup_kindred_workbenches()` — executes addon `InitGui.py` files to register workbenches
|
||||
4. Deferred QTimer cascade:
|
||||
- **1500ms** — Register Silo as a file origin
|
||||
- **2000ms** — Dock the Silo auth panel
|
||||
- **3000ms** — Check for Silo first-start configuration
|
||||
- **4000ms** — Dock the Silo activity panel
|
||||
- **10000ms** — Check for application updates
|
||||
|
||||
The QTimer cascade exists because FreeCAD's startup is not fully synchronous — Silo registration must wait for the GUI framework to be ready.
|
||||
|
||||
## Origin system
|
||||
|
||||
The origin system is Kindred's primary addition to FreeCAD's GUI layer:
|
||||
|
||||
- **`FileOrigin`** — abstract C++ interface for file backends
|
||||
- **`LocalFileOrigin`** — default implementation (local filesystem)
|
||||
- **`SiloOrigin`** — Silo database backend (registered by the Python addon)
|
||||
- **`OriginManager`** — manages origin lifecycle, switching, capability queries
|
||||
- **`OriginSelectorWidget`** — dropdown in the File toolbar
|
||||
- **`CommandOrigin.cpp`** — Commit / Pull / Push / Info / BOM commands that delegate to the active origin
|
||||
|
||||
## Module interaction
|
||||
|
||||
- **ztools** injects commands into PartDesign via `_ZToolsPartDesignManipulator`
|
||||
- **Silo** registers as a `FileOrigin` backend via `silo_origin.register_silo_origin()`
|
||||
- **Create module** is glue only — no feature code, just bootstrap and version management
|
||||
26
docs/src/architecture/python-source-of-truth.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Python as Source of Truth
|
||||
|
||||
In Kindred Create's architecture, FreeCAD documents (`.FCStd` files) are the authoritative representation of all CAD data. The Silo database and MinIO storage are caches and metadata indexes — they do not define the model.
|
||||
|
||||
## How it works
|
||||
|
||||
An `.FCStd` file is a ZIP archive containing:
|
||||
- XML documents describing the parametric model tree
|
||||
- BREP geometry files for each shape
|
||||
- Thumbnail images
|
||||
- Embedded spreadsheets and metadata
|
||||
|
||||
When a user runs **Commit**, the workbench uploads the entire `.FCStd` file to MinIO and records metadata (part number, revision, timestamp, commit message) in PostgreSQL. When a user runs **Pull**, the workbench downloads the `.FCStd` from MinIO and opens it locally.
|
||||
|
||||
## Why this design
|
||||
|
||||
- **No data loss** — the complete model is always in the `.FCStd` file, never split across systems
|
||||
- **Offline capability** — engineers can work without a server connection
|
||||
- **FreeCAD compatibility** — files are standard FreeCAD documents, openable in stock FreeCAD
|
||||
- **Simple sync model** — the unit of transfer is always a whole file, avoiding merge conflicts in binary geometry
|
||||
|
||||
## Trade-offs
|
||||
|
||||
- **Large files** — `.FCStd` files can grow large with complex assemblies; every commit stores the full file
|
||||
- **No partial sync** — you cannot pull a single feature or component; it is all or nothing
|
||||
- **Conflict resolution** — two users editing the same file must resolve conflicts manually (last-commit-wins by default)
|
||||
50
docs/src/architecture/silo-server.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Silo Server
|
||||
|
||||
The Silo server is a Go REST API that provides the backend for the Silo workbench. It manages part numbers, revisions, bills of materials, and file storage for engineering teams.
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
silo/
|
||||
├── cmd/
|
||||
│ ├── silo/ # CLI tool
|
||||
│ └── silod/ # API server
|
||||
├── internal/
|
||||
│ ├── api/ # HTTP handlers, routes, templates
|
||||
│ ├── config/ # Configuration loading
|
||||
│ ├── db/ # PostgreSQL access
|
||||
│ ├── migration/ # Property migration utilities
|
||||
│ ├── partnum/ # Part number generation
|
||||
│ ├── schema/ # YAML schema parsing
|
||||
│ └── storage/ # MinIO file storage
|
||||
├── migrations/ # Database migration SQL scripts
|
||||
├── schemas/ # Part numbering schema definitions (YAML)
|
||||
└── deployments/ # Docker Compose and systemd configs
|
||||
```
|
||||
|
||||
## Stack
|
||||
|
||||
- **Go** REST API with 38+ routes
|
||||
- **PostgreSQL** for metadata, revisions, BOM relationships
|
||||
- **MinIO** (S3-compatible) for binary `.FCStd` file storage
|
||||
- **LDAP / OIDC** for authentication
|
||||
- **SSE** (Server-Sent Events) for real-time activity feed
|
||||
|
||||
## Key features
|
||||
|
||||
- **Configurable part number generation** via YAML schemas
|
||||
- **Revision tracking** with append-only history
|
||||
- **BOM management** with reference designators and alternates
|
||||
- **Physical inventory** tracking with hierarchical locations
|
||||
|
||||
## Database migrations
|
||||
|
||||
Migrations live in `migrations/` as numbered SQL scripts (e.g., `001_initial.sql`). Apply them sequentially against the database:
|
||||
|
||||
```bash
|
||||
psql -h psql.kindred.internal -U silo -d silo -f migrations/001_initial.sql
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
See `mods/silo/deployments/` for Docker Compose and systemd configurations. A typical deployment runs the Go server alongside PostgreSQL and MinIO containers.
|
||||
72
docs/src/development/build-system.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Build System
|
||||
|
||||
Kindred Create uses **CMake** for build configuration, **pixi** (conda-based) for dependency management and task running, and **ccache** for compilation caching.
|
||||
|
||||
## Overview
|
||||
|
||||
- **CMake** >= 3.22.0
|
||||
- **Ninja** generator (via conda presets)
|
||||
- **pixi** manages all dependencies — compilers, Qt6, OpenCASCADE, Python, etc.
|
||||
- **ccache** with 4 GB max, zlib compression level 6, sloppy mode
|
||||
- **mold** linker on Linux for faster link times
|
||||
|
||||
## CMake configuration
|
||||
|
||||
The root `CMakeLists.txt` defines:
|
||||
- **Kindred Create version:** `0.1.0` (via `KINDRED_CREATE_VERSION`)
|
||||
- **FreeCAD base version:** `1.0.0` (via `FREECAD_VERSION`)
|
||||
- CMake policy settings for compatibility
|
||||
- ccache auto-detection
|
||||
- Submodule dependency checks
|
||||
- Library setup: yaml-cpp, fmt, ICU
|
||||
|
||||
### Version injection
|
||||
|
||||
The version flows from CMake to Python via `configure_file()`:
|
||||
|
||||
```
|
||||
CMakeLists.txt (KINDRED_CREATE_VERSION = "0.1.0")
|
||||
→ src/Mod/Create/version.py.in (template)
|
||||
→ build/*/Mod/Create/version.py (generated)
|
||||
→ update_checker.py (imports VERSION)
|
||||
```
|
||||
|
||||
## CMake presets
|
||||
|
||||
Defined in `CMakePresets.json`:
|
||||
|
||||
| Preset | Platform | Build type |
|
||||
|--------|----------|------------|
|
||||
| `conda-linux-debug` | Linux | Debug |
|
||||
| `conda-linux-release` | Linux | Release |
|
||||
| `conda-macos-debug` | macOS | Debug |
|
||||
| `conda-macos-release` | macOS | Release |
|
||||
| `conda-windows-debug` | Windows | Debug |
|
||||
| `conda-windows-release` | Windows | Release |
|
||||
|
||||
All presets inherit from a hidden `common` base and a hidden `conda` base (Ninja generator, conda toolchain).
|
||||
|
||||
## cMake/ helper modules
|
||||
|
||||
The `cMake/` directory contains CMake helper macros inherited from FreeCAD:
|
||||
- **FreeCAD_Helpers** — macros for building FreeCAD modules
|
||||
- Platform detection modules
|
||||
- Dependency finding modules (Find*.cmake)
|
||||
|
||||
## Dependencies
|
||||
|
||||
Core dependencies managed by pixi (from `pixi.toml`):
|
||||
|
||||
| Category | Packages |
|
||||
|----------|----------|
|
||||
| Build | cmake, ninja, swig, compilers (clang/gcc) |
|
||||
| CAD kernel | occt 7.8, coin3d, opencamlib, pythonocc-core |
|
||||
| UI | Qt6 6.8, PySide6, pyside6 |
|
||||
| Math | eigen, numpy, scipy, sympy |
|
||||
| Data | hdf5, vtk, smesh, ifcopenshell |
|
||||
| Python | 3.11 (< 3.12), pip, freecad-stubs |
|
||||
|
||||
Platform-specific extras:
|
||||
- **Linux:** clang, kernel-headers, mesa, X11, libspnav
|
||||
- **macOS:** sed
|
||||
- **Windows:** pthreads-win32
|
||||
40
docs/src/development/code-quality.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Code Quality
|
||||
|
||||
## Formatting and linting
|
||||
|
||||
### C/C++
|
||||
|
||||
- **Formatter:** clang-format (config in `.clang-format`)
|
||||
- **Static analysis:** clang-tidy (config in `.clang-tidy`)
|
||||
|
||||
### Python
|
||||
|
||||
- **Formatter:** black with 100-character line length
|
||||
- **Linter:** pylint (config in `.pylintrc`)
|
||||
|
||||
## Pre-commit hooks
|
||||
|
||||
The repository uses [pre-commit](https://pre-commit.com/) to run formatters and linters automatically on staged files:
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
Configured hooks (`.pre-commit-config.yaml`):
|
||||
- `trailing-whitespace` — remove trailing whitespace
|
||||
- `end-of-file-fixer` — ensure files end with a newline
|
||||
- `check-yaml` — validate YAML syntax
|
||||
- `check-added-large-files` — prevent accidental large file commits
|
||||
- `mixed-line-ending` — normalize line endings
|
||||
- `black` — Python formatting (100 char lines)
|
||||
- `clang-format` — C/C++ formatting
|
||||
|
||||
## Scope
|
||||
|
||||
Pre-commit hooks are configured to run on specific directories:
|
||||
- `src/Base/`, `src/Gui/`, `src/Main/`, `src/Tools/`
|
||||
- `src/Mod/Assembly/`, `src/Mod/BIM/`, `src/Mod/CAM/`, `src/Mod/Draft/`, `src/Mod/Fem/`, and other stock modules
|
||||
- `tests/src/`
|
||||
|
||||
Excluded: generated files, vendored libraries (`QSint/`, `Quarter/`, `3Dconnexion/navlib`), and binary formats.
|
||||
52
docs/src/development/contributing.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
|
||||
Kindred Create is maintained at [git.kindred-systems.com/kindred/create](https://git.kindred-systems.com/kindred/create). Contributions are submitted as pull requests against the `main` branch.
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
|
||||
cd create
|
||||
pixi run configure
|
||||
pixi run build
|
||||
pixi run freecad
|
||||
```
|
||||
|
||||
See [Building from Source](../guide/building.md) for the full development setup.
|
||||
|
||||
## Branch and PR workflow
|
||||
|
||||
1. Create a feature branch from `main`:
|
||||
```bash
|
||||
git checkout -b feat/my-feature main
|
||||
```
|
||||
2. Make your changes, commit with conventional commit messages (see below).
|
||||
3. Push and open a pull request against `main`.
|
||||
4. CI builds and tests run automatically on all PRs.
|
||||
|
||||
## Commit messages
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
| Prefix | Purpose |
|
||||
|--------|---------|
|
||||
| `feat:` | New feature |
|
||||
| `fix:` | Bug fix |
|
||||
| `chore:` | Maintenance, dependencies |
|
||||
| `docs:` | Documentation only |
|
||||
| `art:` | Icons, theme, visual assets |
|
||||
|
||||
Scope is optional but encouraged:
|
||||
- `feat(ztools): add datum point creation mode`
|
||||
- `fix(gui): correct menu icon size on Wayland`
|
||||
- `chore: update silo submodule`
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Report issues at the [issue tracker](https://git.kindred-systems.com/kindred/create/issues). When reporting:
|
||||
|
||||
1. Note whether the issue involves Kindred Create additions (ztools, Silo, theme) or base FreeCAD
|
||||
2. Include version info from **Help > About FreeCAD > Copy to clipboard**
|
||||
3. Provide reproduction steps and attach example files (FCStd as ZIP) if applicable
|
||||
|
||||
For base FreeCAD issues, also check the [FreeCAD issue tracker](https://github.com/FreeCAD/FreeCAD/issues).
|
||||
73
docs/src/development/repo-structure.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Repository Structure
|
||||
|
||||
```
|
||||
create/
|
||||
├── src/
|
||||
│ ├── App/ # Core application (C++)
|
||||
│ ├── Base/ # Base classes (C++)
|
||||
│ ├── Gui/ # GUI framework and stylesheets (C++)
|
||||
│ ├── Main/ # Application entry points
|
||||
│ ├── Mod/ # FreeCAD modules (~37)
|
||||
│ │ ├── Create/ # Kindred bootstrap module
|
||||
│ │ ├── Assembly/ # Assembly workbench (Kindred patches)
|
||||
│ │ ├── PartDesign/ # Part Design workbench
|
||||
│ │ ├── Sketcher/ # Sketcher workbench
|
||||
│ │ ├── AddonManager/ # Addon manager (submodule)
|
||||
│ │ └── ... # Other stock FreeCAD modules
|
||||
│ └── 3rdParty/
|
||||
│ ├── OndselSolver/ # Assembly solver (submodule)
|
||||
│ └── GSL/ # Guidelines Support Library (submodule)
|
||||
├── mods/ # Kindred addon workbenches (submodules)
|
||||
│ ├── ztools/ # ztools workbench
|
||||
│ └── silo/ # Silo parts database
|
||||
├── kindred-icons/ # SVG icon library (~200 icons)
|
||||
├── resources/ # Branding, desktop integration
|
||||
│ ├── branding/ # Logo, splash, icon generation scripts
|
||||
│ └── icons/ # Platform icons (.ico, .icns, hicolor)
|
||||
├── package/ # Packaging scripts
|
||||
│ ├── debian/ # Debian package
|
||||
│ ├── ubuntu/ # Ubuntu-specific
|
||||
│ ├── fedora/ # RPM package
|
||||
│ ├── rattler-build/ # Cross-platform bundles (AppImage, DMG, NSIS)
|
||||
│ └── WindowsInstaller/ # NSIS installer definition
|
||||
├── .gitea/workflows/ # CI/CD pipelines
|
||||
│ ├── build.yml # Build + test on push/PR
|
||||
│ └── release.yml # Release on tag push
|
||||
├── tests/ # Test suite
|
||||
│ ├── src/ # C++ test sources
|
||||
│ └── lib/ # Google Test framework (submodule)
|
||||
├── cMake/ # CMake helper modules
|
||||
├── docs/ # Documentation (this book)
|
||||
├── tools/ # Dev utilities (build, lint, profile)
|
||||
├── contrib/ # IDE configs (VSCode, CLion, debugger)
|
||||
├── data/ # Example and test data
|
||||
├── CMakeLists.txt # Root build configuration
|
||||
├── CMakePresets.json # Platform build presets
|
||||
├── pixi.toml # Pixi environment and tasks
|
||||
├── CONTRIBUTING.md # Contribution guide
|
||||
├── README.md # Project overview
|
||||
├── LICENSE # LGPL-2.1-or-later
|
||||
└── .pre-commit-config.yaml # Code quality hooks
|
||||
```
|
||||
|
||||
## Git submodules
|
||||
|
||||
| Submodule | Path | Source | Purpose |
|
||||
|-----------|------|--------|---------|
|
||||
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` | Unified workbench |
|
||||
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` | Parts database |
|
||||
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` | Assembly solver |
|
||||
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` | C++ guidelines library |
|
||||
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | Extension manager |
|
||||
| googletest | `tests/lib` | `github.com/google/googletest` | Test framework |
|
||||
|
||||
## Key files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/Mod/Create/Init.py` | Console-phase bootstrap — loads addons |
|
||||
| `src/Mod/Create/InitGui.py` | GUI-phase bootstrap — registers workbenches, deferred setup |
|
||||
| `src/Gui/FileOrigin.h` | Abstract file origin interface (Kindred addition) |
|
||||
| `src/Gui/Stylesheets/KindredCreate.qss` | Catppuccin Mocha theme |
|
||||
| `pixi.toml` | Build tasks and dependencies |
|
||||
| `CMakeLists.txt` | Root CMake configuration |
|
||||
107
docs/src/guide/building.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Building from Source
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **git** with submodule support
|
||||
- **[pixi](https://pixi.sh)** — conda-based dependency manager and task runner
|
||||
|
||||
Pixi handles all other dependencies (CMake, compilers, Qt6, OpenCASCADE, etc.).
|
||||
|
||||
## Clone
|
||||
|
||||
```bash
|
||||
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
|
||||
cd create
|
||||
```
|
||||
|
||||
If cloned without `--recursive`:
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
The repository includes six submodules:
|
||||
|
||||
| Submodule | Path | Source |
|
||||
|-----------|------|--------|
|
||||
| ztools | `mods/ztools` | `git.kindred-systems.com/forbes/ztools` |
|
||||
| silo-mod | `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` |
|
||||
| OndselSolver | `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` |
|
||||
| GSL | `src/3rdParty/GSL` | `github.com/microsoft/GSL` |
|
||||
| AddonManager | `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` |
|
||||
| googletest | `tests/lib` | `github.com/google/googletest` |
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pixi run configure
|
||||
pixi run build
|
||||
pixi run install
|
||||
pixi run freecad
|
||||
```
|
||||
|
||||
By default these target the **debug** configuration. For release builds:
|
||||
|
||||
```bash
|
||||
pixi run configure-release
|
||||
pixi run build-release
|
||||
pixi run install-release
|
||||
pixi run freecad-release
|
||||
```
|
||||
|
||||
## All pixi tasks
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| `initialize` | `git submodule update --init --recursive` |
|
||||
| `configure` | CMake configure (debug) |
|
||||
| `configure-debug` | CMake configure with debug preset |
|
||||
| `configure-release` | CMake configure with release preset |
|
||||
| `build` | Build (debug) |
|
||||
| `build-debug` | `cmake --build build/debug` |
|
||||
| `build-release` | `cmake --build build/release` |
|
||||
| `install` | Install (debug) |
|
||||
| `install-debug` | `cmake --install build/debug` |
|
||||
| `install-release` | `cmake --install build/release` |
|
||||
| `test` | Run tests (debug) |
|
||||
| `test-debug` | `ctest --test-dir build/debug` |
|
||||
| `test-release` | `ctest --test-dir build/release` |
|
||||
| `freecad` | Launch FreeCAD (debug) |
|
||||
| `freecad-debug` | `build/debug/bin/FreeCAD` |
|
||||
| `freecad-release` | `build/release/bin/FreeCAD` |
|
||||
|
||||
## CMake presets
|
||||
|
||||
The project provides presets in `CMakePresets.json` for each platform and build type:
|
||||
|
||||
- `conda-linux-debug` / `conda-linux-release`
|
||||
- `conda-macos-debug` / `conda-macos-release`
|
||||
- `conda-windows-debug` / `conda-windows-release`
|
||||
|
||||
All presets inherit from a `common` base that enables `CMAKE_EXPORT_COMPILE_COMMANDS` and configures job pools. The `conda` presets use the Ninja generator and pick up compiler paths from the pixi environment.
|
||||
|
||||
## Platform notes
|
||||
|
||||
**Linux:** Uses clang from conda-forge. Requires kernel-headers, mesa, X11, and libspnav (all provided by pixi). Uses the mold linker for faster link times.
|
||||
|
||||
**macOS:** Minimal extra dependencies — pixi provides nearly everything. Tested on both Intel and Apple Silicon.
|
||||
|
||||
**Windows:** Requires pthreads-win32 and MSVC. The conda preset configures the MSVC toolchain automatically when run inside a pixi shell.
|
||||
|
||||
## Caching
|
||||
|
||||
The build uses **ccache** for compilation caching:
|
||||
- Maximum cache size: 4 GB
|
||||
- Compression: zlib level 6
|
||||
- Sloppiness mode enabled for faster cache hits
|
||||
|
||||
ccache is auto-detected by CMake at configure time.
|
||||
|
||||
## Common problems
|
||||
|
||||
**Submodules not initialized:** If you see missing file errors for ztools or Silo, run `pixi run initialize` or `git submodule update --init --recursive`.
|
||||
|
||||
**Pixi not found:** Install pixi from <https://pixi.sh>.
|
||||
|
||||
**ccache full:** Clear with `ccache -C` or increase the limit in your ccache config.
|
||||
|
||||
**Preset not found:** Ensure you are running CMake from within a pixi shell (`pixi shell`) so that conda environment variables are set.
|
||||
41
docs/src/guide/getting-started.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Getting Started
|
||||
|
||||
Kindred Create can be installed from prebuilt packages or built from source. This section covers both paths.
|
||||
|
||||
## Quick start (prebuilt)
|
||||
|
||||
Download the latest release from the [releases page](https://git.kindred-systems.com/kindred/create/releases).
|
||||
|
||||
**Debian/Ubuntu:**
|
||||
```bash
|
||||
sudo apt install ./kindred-create_*.deb
|
||||
```
|
||||
|
||||
**AppImage:**
|
||||
```bash
|
||||
chmod +x KindredCreate-*.AppImage
|
||||
./KindredCreate-*.AppImage
|
||||
```
|
||||
|
||||
## Quick start (from source)
|
||||
|
||||
```bash
|
||||
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
|
||||
cd create
|
||||
pixi run configure
|
||||
pixi run build
|
||||
pixi run freecad
|
||||
```
|
||||
|
||||
See [Installation](./installation.md) for prebuilt package details and [Building from Source](./building.md) for the full development setup.
|
||||
|
||||
## First run
|
||||
|
||||
On first launch, Kindred Create:
|
||||
|
||||
1. Loads the **ztools** and **Silo** workbenches automatically via the Create bootstrap module
|
||||
2. Opens the **ZToolsWorkbench** as the default workbench
|
||||
3. Prompts for Silo server configuration if not yet set up
|
||||
4. Checks for application updates in the background (after ~10 seconds)
|
||||
|
||||
If the Silo server is not available, Kindred Create operates normally with local file operations. The Silo features activate once a server is configured.
|
||||
49
docs/src/guide/installation.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Installation
|
||||
|
||||
## Prebuilt packages
|
||||
|
||||
Download the latest release from the [releases page](https://git.kindred-systems.com/kindred/create/releases).
|
||||
|
||||
### Debian / Ubuntu
|
||||
|
||||
```bash
|
||||
sudo apt install ./kindred-create_*.deb
|
||||
```
|
||||
|
||||
### AppImage (any Linux)
|
||||
|
||||
```bash
|
||||
chmod +x KindredCreate-*.AppImage
|
||||
./KindredCreate-*.AppImage
|
||||
```
|
||||
|
||||
The AppImage is a self-contained bundle using squashfs with zstd compression. No installation required.
|
||||
|
||||
### macOS
|
||||
|
||||
> macOS builds are planned but not yet available in CI. Build from source for now.
|
||||
|
||||
### Windows
|
||||
|
||||
> Windows builds are planned but not yet available in CI. Build from source for now.
|
||||
|
||||
## Verifying your installation
|
||||
|
||||
Launch Kindred Create and check the console output (View > Report View) for:
|
||||
|
||||
```
|
||||
Create: Loaded ztools Init.py
|
||||
Create: Loaded silo Init.py
|
||||
Create module initialized
|
||||
```
|
||||
|
||||
This confirms the bootstrap module loaded both workbenches. If Silo is not configured, you will see a settings prompt on first launch.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
**Debian/Ubuntu:**
|
||||
```bash
|
||||
sudo apt remove kindred-create
|
||||
```
|
||||
|
||||
**AppImage:** Delete the `.AppImage` file. No system files are modified.
|
||||
67
docs/src/guide/silo.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Silo
|
||||
|
||||
Silo is an item database and part management system for Kindred Create. It provides revision-controlled storage for CAD files, part number generation, BOM management, and team collaboration.
|
||||
|
||||
- **Submodule path:** `mods/silo/`
|
||||
- **Source:** `git.kindred-systems.com/kindred/silo-mod`
|
||||
|
||||
## Architecture
|
||||
|
||||
Silo has three components:
|
||||
|
||||
- **Go REST API server** (`cmd/silod/`) — 38+ routes, backed by PostgreSQL and MinIO
|
||||
- **FreeCAD workbench** (`freecad/`) — Python commands integrated into Kindred Create
|
||||
- **Shared API client** (`silo-client/`) — Python library used by the workbench (nested submodule)
|
||||
|
||||
The silo-mod repository 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).
|
||||
|
||||
## Workbench commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| New | Create a new item in the database |
|
||||
| Open | Open an item by part number |
|
||||
| Save | Save the current document locally |
|
||||
| Commit | Save current state as a new revision with comment |
|
||||
| Pull | Download an item by part number |
|
||||
| Push | Batch upload modified files |
|
||||
| Info | View revision history and metadata |
|
||||
| BOM | View or edit the bill of materials |
|
||||
| TagProjects | Tag items with project identifiers |
|
||||
| Rollback | Revert to a previous revision |
|
||||
| SetStatus | Change item lifecycle status |
|
||||
| Settings | Configure Silo server connection |
|
||||
| Auth | Authenticate with the Silo server |
|
||||
|
||||
## Origin integration
|
||||
|
||||
Silo registers as a **file origin** via the `FileOrigin` interface in `src/Gui/`. This makes Silo commands (Commit, Pull, Push, Info, BOM) available in the File menu and Origin Tools toolbar across all workbenches.
|
||||
|
||||
The registration happens via a deferred QTimer (1500ms after startup) in `src/Mod/Create/InitGui.py`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The FreeCAD workbench reads configuration from:
|
||||
|
||||
- `SILO_API_URL` — Server API endpoint (default: `http://localhost:8080/api`)
|
||||
- `SILO_PROJECTS_DIR` — Local projects directory (default: `~/projects`)
|
||||
|
||||
On first launch, Kindred Create prompts for Silo server configuration via the Settings command.
|
||||
|
||||
## Server setup
|
||||
|
||||
See `mods/silo/README.md` for full server deployment instructions. Quick start:
|
||||
|
||||
```bash
|
||||
# Database setup
|
||||
psql -h psql.kindred.internal -U silo -d silo -f migrations/001_initial.sql
|
||||
|
||||
# Configure
|
||||
cp config.example.yaml config.yaml
|
||||
# Edit config.yaml with your settings
|
||||
|
||||
# Run server
|
||||
go run ./cmd/silod
|
||||
```
|
||||
|
||||
The server supports LDAP and OIDC authentication, and provides a real-time activity feed via Server-Sent Events (SSE).
|
||||
23
docs/src/guide/workbenches.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Workbenches
|
||||
|
||||
Kindred Create ships two custom workbenches on top of FreeCAD's standard set.
|
||||
|
||||
## ztools
|
||||
|
||||
A unified workbench that consolidates part design, assembly, and sketcher tools into a single interface. It is the **default workbench** when Kindred Create launches.
|
||||
|
||||
ztools commands are also injected into the PartDesign workbench menus and toolbars via a manipulator mechanism, so they are accessible even when working in stock PartDesign.
|
||||
|
||||
See the [ztools guide](./ztools.md) for details.
|
||||
|
||||
## Silo
|
||||
|
||||
A parts database workbench for managing CAD files, part numbers, revisions, and bills of materials across teams. Silo commands (New, Open, Save, Commit, Pull, Push, Info, BOM) are integrated into the File menu and toolbar across **all** workbenches via the origin system.
|
||||
|
||||
Silo requires a running server instance. On first launch, Kindred Create prompts for server configuration.
|
||||
|
||||
See the [Silo guide](./silo.md) for details.
|
||||
|
||||
## Stock FreeCAD workbenches
|
||||
|
||||
All standard FreeCAD workbenches are available: PartDesign, Sketcher, Assembly, TechDraw, Draft, BIM, CAM, FEM, Mesh, Spreadsheet, and others. Kindred Create does not remove or disable any stock functionality.
|
||||
48
docs/src/guide/ztools.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# ztools
|
||||
|
||||
ztools is a pure-Python FreeCAD workbench that consolidates part design, assembly, and sketcher tools into a single unified interface.
|
||||
|
||||
- **Submodule path:** `mods/ztools/`
|
||||
- **Source:** `git.kindred-systems.com/forbes/ztools`
|
||||
|
||||
## Features
|
||||
|
||||
### Datum Creator
|
||||
|
||||
Nine commands for creating datum geometry (planes, axes, points) with 16 creation modes. Modes include offset from face, through three points, normal to edge, and more. These are accessed from the ztools toolbar and are also injected into the PartDesign workbench.
|
||||
|
||||
### Enhanced Pocket
|
||||
|
||||
Extends FreeCAD's Pocket feature with flip-side cutting — the ability to cut outside the sketch profile rather than inside.
|
||||
|
||||
### Assembly Patterns
|
||||
|
||||
Linear and polar patterning tools for assemblies. Create regular arrangements of components without manually placing each one.
|
||||
|
||||
### Spreadsheet Formatting
|
||||
|
||||
Commands for formatting spreadsheet cells: bold, italic, underline, text alignment, and cell colors. Accessible from the ztools toolbar when a spreadsheet is active.
|
||||
|
||||
## PartDesign injection
|
||||
|
||||
ztools registers a `_ZToolsPartDesignManipulator` that hooks into the PartDesign workbench at startup. This injects ztools commands into PartDesign's menus and toolbars so they are available without switching workbenches.
|
||||
|
||||
The registration happens in `mods/ztools/ztools/ztools/InitGui.py` when the Create bootstrap module loads addon workbenches.
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
mods/ztools/
|
||||
├── ztools/ztools/
|
||||
│ ├── InitGui.py # Workbench registration + manipulator
|
||||
│ ├── Init.py # Console initialization
|
||||
│ ├── commands/ # 9 command implementations
|
||||
│ ├── datums/core.py # Datum creation (16 modes)
|
||||
│ └── resources/ # Icons and theme
|
||||
└── CatppuccinMocha/ # Theme preference pack
|
||||
```
|
||||
|
||||
## Further reading
|
||||
|
||||
- `mods/ztools/KINDRED_INTEGRATION.md` — integration architecture notes
|
||||
- `mods/ztools/ROADMAP.md` — planned features
|
||||
25
docs/src/introduction.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Kindred Create
|
||||
|
||||
Kindred Create is a fork of [FreeCAD](https://www.freecad.org) 1.0+ that adds integrated tooling for professional engineering workflows. It ships custom workbenches and a dark theme on top of FreeCAD's parametric modeling core.
|
||||
|
||||
- **License:** LGPL 2.1+
|
||||
- **Organization:** [Kindred Systems LLC](https://www.kindred-systems.com)
|
||||
- **Build system:** CMake + [pixi](https://pixi.sh)
|
||||
- **Current version:** Kindred Create v0.1.0 (FreeCAD base v1.0.0)
|
||||
|
||||
## Key features
|
||||
|
||||
**[ztools](./guide/ztools.md)** — A unified workbench that consolidates part design, assembly, and sketcher tools into a single interface. Adds custom datum creation (planes, axes, points with 16 creation modes), pattern tools for assemblies, an enhanced pocket with flip-side cutting, and spreadsheet formatting commands.
|
||||
|
||||
**[Silo](./guide/silo.md)** — A parts database system for managing CAD files, part numbers, revisions, and bills of materials across teams. Includes a Go REST API server backed by PostgreSQL and MinIO, with FreeCAD commands for opening, saving, and syncing files directly from the application.
|
||||
|
||||
**Catppuccin Mocha theme** — A dark theme applied across the entire application, including the 3D viewport, sketch editor, spreadsheet view, and tree view.
|
||||
|
||||
**Update checker** — On startup, Kindred Create checks the Gitea releases API for newer versions and logs the result.
|
||||
|
||||
## Links
|
||||
|
||||
- **Source:** <https://git.kindred-systems.com/kindred/create>
|
||||
- **Downloads:** <https://git.kindred-systems.com/kindred/create/releases>
|
||||
- **Issue tracker:** <https://git.kindred-systems.com/kindred/create/issues>
|
||||
- **Website:** <https://www.kindred-systems.com/create>
|
||||
45
docs/src/reference/configuration.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Configuration
|
||||
|
||||
## Silo workbench
|
||||
|
||||
The Silo workbench stores its configuration in FreeCAD's parameter system under `User parameter:BaseApp/Preferences/Mod/KindredSilo`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `SILO_API_URL` | `http://localhost:8080/api` | Silo server API endpoint |
|
||||
| `SILO_PROJECTS_DIR` | `~/projects` | Local directory for checked-out files |
|
||||
|
||||
### FreeCAD parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `ApiUrl` | String | (empty) | Silo server URL |
|
||||
| `FirstStartChecked` | Bool | false | Whether the first-start prompt has been shown |
|
||||
|
||||
## Update checker
|
||||
|
||||
Configuration lives in `User parameter:BaseApp/Preferences/Mod/KindredCreate/Update`.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `CheckEnabled` | Bool | true | Enable/disable update checks |
|
||||
| `CheckIntervalDays` | Int | 1 | Minimum days between checks |
|
||||
| `LastCheckTimestamp` | String | (empty) | ISO 8601 timestamp of last check |
|
||||
| `SkippedVersion` | String | (empty) | Version the user chose to skip |
|
||||
|
||||
The update checker queries the Gitea releases API at:
|
||||
```
|
||||
https://git.kindred-systems.com/api/v1/repos/kindred/create/releases
|
||||
```
|
||||
|
||||
## Theme
|
||||
|
||||
The default theme is **Catppuccin Mocha** (`KindredCreate.qss`). It is applied via the KindredCreate preference pack at startup.
|
||||
|
||||
To switch themes, go to **Edit > Preferences > General > Stylesheet** and select a different QSS file.
|
||||
|
||||
## Build presets
|
||||
|
||||
See `CMakePresets.json` for available build configurations. The pixi tasks (`configure`, `build`, etc.) use these presets automatically.
|
||||
53
docs/src/reference/glossary.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Glossary
|
||||
|
||||
## Terms
|
||||
|
||||
**BOM** — Bill of Materials. A structured list of components in an assembly with part numbers, quantities, and reference designators.
|
||||
|
||||
**Catppuccin Mocha** — A dark color palette used for Kindred Create's theme. Part of the [Catppuccin](https://github.com/catppuccin/catppuccin) color scheme family.
|
||||
|
||||
**Datum** — Reference geometry (plane, axis, or point) used as a construction aid for modeling. Not a physical shape — used to position features relative to abstract references.
|
||||
|
||||
**FCStd** — FreeCAD's standard document format. A ZIP archive containing XML model trees, BREP geometry, thumbnails, and embedded data.
|
||||
|
||||
**FileOrigin** — Abstract C++ interface in `src/Gui/` that defines a pluggable file backend. Implementations: `LocalFileOrigin` (filesystem) and `SiloOrigin` (database).
|
||||
|
||||
**FreeCAD** — Open-source parametric 3D CAD platform. Kindred Create is based on FreeCAD v1.0.0. Website: <https://www.freecad.org>
|
||||
|
||||
**Kindred Create** — The full application: a FreeCAD fork plus Kindred's addon workbenches, theme, and tooling.
|
||||
|
||||
**Manipulator** — FreeCAD mechanism for injecting commands from one workbench into another's menus and toolbars. Used by ztools to add commands to PartDesign.
|
||||
|
||||
**MinIO** — S3-compatible object storage server. Used by Silo to store binary `.FCStd` files.
|
||||
|
||||
**OndselSolver** — Lagrangian constraint solver for the Assembly workbench. Vendored as a submodule from a Kindred fork.
|
||||
|
||||
**Origin** — In Kindred Create context: the pluggable file backend system. In FreeCAD context: the coordinate system origin (X/Y/Z axes and planes) of a Part or Body.
|
||||
|
||||
**pixi** — Conda-based dependency manager and task runner. Used for all build operations. Website: <https://pixi.sh>
|
||||
|
||||
**Preference pack** — FreeCAD mechanism for bundling theme settings, preferences, and stylesheets into an installable package.
|
||||
|
||||
**QSS** — Qt Style Sheet. A CSS-like language for styling Qt widgets. Kindred Create's theme is defined in `KindredCreate.qss`.
|
||||
|
||||
**rattler-build** — Cross-platform package build tool from the conda ecosystem. Used to create AppImage, DMG, and NSIS installer bundles.
|
||||
|
||||
**Silo** — Kindred's parts database system. Consists of a Go server, FreeCAD workbench, and shared Python client library.
|
||||
|
||||
**SSE** — Server-Sent Events. HTTP-based protocol for real-time server-to-client notifications. Used by Silo for the activity feed.
|
||||
|
||||
**Workbench** — FreeCAD's plugin/module system. Each workbench provides a set of tools, menus, and toolbars for a specific task domain.
|
||||
|
||||
**ztools** — Kindred's unified workbench combining Part Design, Assembly, and Sketcher tools with custom datum creation and assembly patterns.
|
||||
|
||||
## Repository URLs
|
||||
|
||||
| Repository | URL |
|
||||
|------------|-----|
|
||||
| Kindred Create | <https://git.kindred-systems.com/kindred/create> |
|
||||
| ztools | <https://git.kindred-systems.com/forbes/ztools> |
|
||||
| silo-mod | <https://git.kindred-systems.com/kindred/silo-mod> |
|
||||
| OndselSolver | <https://git.kindred-systems.com/kindred/solver> |
|
||||
| GSL | <https://github.com/microsoft/GSL> |
|
||||
| AddonManager | <https://github.com/FreeCAD/AddonManager> |
|
||||
| googletest | <https://github.com/google/googletest> |
|
||||
8
docs/theme/kindred.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/* Kindred Create docs - minor overrides for coal theme */
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
}
|
||||
|
||||
.sidebar .sidebar-scrollbox {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,6 +59,10 @@ sed -i "1s/.*/\nLIST OF PACKAGES:/" AppDir/packages.txt
|
||||
curl -LO https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$(uname -m).AppImage
|
||||
chmod a+x appimagetool-$(uname -m).AppImage
|
||||
|
||||
# Extract appimagetool so it works in containers without FUSE
|
||||
./appimagetool-$(uname -m).AppImage --appimage-extract > /dev/null 2>&1
|
||||
APPIMAGETOOL=squashfs-root/AppRun
|
||||
|
||||
if [ "${UPLOAD_RELEASE}" == "true" ]; then
|
||||
case "${BUILD_TAG}" in
|
||||
*weekly*)
|
||||
@@ -76,7 +80,7 @@ fi
|
||||
echo -e "\nCreate the appimage"
|
||||
# export GPG_TTY=$(tty)
|
||||
chmod a+x ./AppDir/AppRun
|
||||
./appimagetool-$(uname -m).AppImage \
|
||||
${APPIMAGETOOL} \
|
||||
--comp zstd \
|
||||
--mksquashfs-opt -Xcompression-level \
|
||||
--mksquashfs-opt 22 \
|
||||
|
||||
@@ -18,11 +18,16 @@ 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
|
||||
- swig >=4.0,<4.4
|
||||
|
||||
- if: linux
|
||||
then:
|
||||
- patchelf
|
||||
|
||||
- if: linux and x86_64
|
||||
then:
|
||||
@@ -102,6 +107,7 @@ requirements:
|
||||
- fmt
|
||||
- freetype
|
||||
- hdf5
|
||||
- icu>=75,<76
|
||||
- lark
|
||||
- libboost-devel
|
||||
- matplotlib-base
|
||||
|
||||
@@ -25,6 +25,7 @@ freetype = "*"
|
||||
git = "*"
|
||||
graphviz = "*"
|
||||
hdf5 = "*"
|
||||
icu = ">=75,<76"
|
||||
ifcopenshell = "*"
|
||||
lark = "*"
|
||||
libboost-devel = "*"
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<FCParameters>
|
||||
<FCParamGroup Name="Root">
|
||||
<FCParamGroup Name="BaseApp">
|
||||
<FCParamGroup Name="Preferences">
|
||||
<FCParamGroup Name="Editor">
|
||||
<FCUInt Name="Text" Value="3453416703"/>
|
||||
<FCUInt Name="Bookmark" Value="3032415999"/>
|
||||
<FCUInt Name="Breakpoint" Value="4086016255"/>
|
||||
<FCUInt Name="Keyword" Value="3416717311"/>
|
||||
<FCUInt Name="Comment" Value="2139095295"/>
|
||||
<FCUInt Name="Block comment" Value="2139095295"/>
|
||||
<FCUInt Name="Number" Value="4206069759"/>
|
||||
<FCUInt Name="String" Value="2799935999"/>
|
||||
<FCUInt Name="Character" Value="4073902335"/>
|
||||
<FCUInt Name="Class name" Value="2310339327"/>
|
||||
<FCUInt Name="Define name" Value="2310339327"/>
|
||||
<FCUInt Name="Operator" Value="2312199935"/>
|
||||
<FCUInt Name="Python output" Value="2796290303"/>
|
||||
<FCUInt Name="Python error" Value="4086016255"/>
|
||||
<FCUInt Name="Current line highlight" Value="1162304255"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="OutputWindow">
|
||||
<FCUInt Name="colorText" Value="3453416703"/>
|
||||
<FCUInt Name="colorLogging" Value="2497893887"/>
|
||||
<FCUInt Name="colorWarning" Value="4192382975"/>
|
||||
<FCUInt Name="colorError" Value="4086016255"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="View">
|
||||
<FCUInt Name="BackgroundColor" Value="505294591"/>
|
||||
<FCUInt Name="BackgroundColor2" Value="286333951"/>
|
||||
<FCUInt Name="BackgroundColor3" Value="404235775"/>
|
||||
<FCUInt Name="BackgroundColor4" Value="825378047"/>
|
||||
<FCBool Name="Simple" Value="0"/>
|
||||
<FCBool Name="Gradient" Value="1"/>
|
||||
<FCBool Name="UseBackgroundColorMid" Value="0"/>
|
||||
<FCUInt Name="HighlightColor" Value="3416717311"/>
|
||||
<FCUInt Name="SelectionColor" Value="3032415999"/>
|
||||
<FCUInt Name="PreselectColor" Value="2497893887"/>
|
||||
<FCUInt Name="DefaultShapeColor" Value="1482387711"/>
|
||||
<FCBool Name="RandomColor" Value="0"/>
|
||||
<FCUInt Name="DefaultShapeLineColor" Value="2470768383"/>
|
||||
<FCUInt Name="DefaultShapeVertexColor" Value="2470768383"/>
|
||||
<FCUInt Name="BoundingBoxColor" Value="1819509759"/>
|
||||
<FCUInt Name="AnnotationTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="SketchEdgeColor" Value="3453416703"/>
|
||||
<FCUInt Name="SketchVertexColor" Value="3453416703"/>
|
||||
<FCUInt Name="EditedEdgeColor" Value="3416717311"/>
|
||||
<FCUInt Name="EditedVertexColor" Value="4123402495"/>
|
||||
<FCUInt Name="ConstructionColor" Value="4206069759"/>
|
||||
<FCUInt Name="ExternalColor" Value="4192382975"/>
|
||||
<FCUInt Name="FullyConstrainedColor" Value="2799935999"/>
|
||||
<FCUInt Name="InternalAlignedGeoColor" Value="1959907071"/>
|
||||
<FCUInt Name="FullyConstraintElementColor" Value="2799935999"/>
|
||||
<FCUInt Name="FullyConstraintConstructionElementColor" Value="2497893887"/>
|
||||
<FCUInt Name="FullyConstraintInternalAlignmentColor" Value="2312199935"/>
|
||||
<FCUInt Name="FullyConstraintConstructionPointColor" Value="2799935999"/>
|
||||
<FCUInt Name="ConstrainedIcoColor" Value="2310339327"/>
|
||||
<FCUInt Name="NonDrivingConstrDimColor" Value="2139095295"/>
|
||||
<FCUInt Name="ConstrainedDimColor" Value="3416717311"/>
|
||||
<FCUInt Name="ExprBasedConstrDimColor" Value="4206069759"/>
|
||||
<FCUInt Name="DeactivatedConstrDimColor" Value="1819509759"/>
|
||||
<FCUInt Name="CursorTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="CursorCrosshairColor" Value="3416717311"/>
|
||||
<FCUInt Name="CreateLineColor" Value="2799935999"/>
|
||||
<FCUInt Name="ShadowLightColor" Value="2470768128"/>
|
||||
<FCUInt Name="ShadowGroundColor" Value="286333952"/>
|
||||
<FCUInt Name="HiddenLineColor" Value="825378047"/>
|
||||
<FCUInt Name="HiddenLineFaceColor" Value="505294591"/>
|
||||
<FCUInt Name="HiddenLineBackground" Value="505294591"/>
|
||||
<FCBool Name="EnableBacklight" Value="1"/>
|
||||
<FCUInt Name="BacklightColor" Value="1162304255"/>
|
||||
<FCFloat Name="BacklightIntensity" Value="0.30"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="TreeView">
|
||||
<FCUInt Name="TreeEditColor" Value="3416717311"/>
|
||||
<FCUInt Name="TreeActiveColor" Value="2799935999"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="General">
|
||||
<FCText Name="AutoloadModule">ZToolsWorkbench</FCText>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="MainWindow">
|
||||
<FCText Name="StyleSheet">KindredCreate.qss</FCText>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Mod">
|
||||
<FCParamGroup Name="Start">
|
||||
<FCUInt Name="BackgroundColor1" Value="404235775"/>
|
||||
<FCUInt Name="BackgroundTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="PageColor" Value="505294591"/>
|
||||
<FCUInt Name="PageTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="BoxColor" Value="825378047"/>
|
||||
<FCUInt Name="LinkColor" Value="2310339327"/>
|
||||
<FCUInt Name="BackgroundColor2" Value="286333951"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Part">
|
||||
<FCUInt Name="VertexColor" Value="3032415999"/>
|
||||
<FCUInt Name="EdgeColor" Value="2310339327"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="PartDesign">
|
||||
<FCUInt Name="DefaultDatumColor" Value="3416717311"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Draft">
|
||||
<FCUInt Name="snapcolor" Value="2799935999"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Sketcher">
|
||||
<FCUInt Name="GridLineColor" Value="1162304255"/>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParameters>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
|
||||
<name>Kindred Create Preference Packs</name>
|
||||
<description>Default preference packs for Kindred Create, featuring the Catppuccin Mocha color theme.</description>
|
||||
<version>0.1.0</version>
|
||||
<maintainer email="support@kindredsystems.net">Kindred Systems LLC</maintainer>
|
||||
<license>LGPL-2.1-or-later</license>
|
||||
<url type="website">https://kindredsystems.net</url>
|
||||
|
||||
<content>
|
||||
<preferencepack>
|
||||
<name>KindredCreate</name>
|
||||
<description>The default Kindred Create theme based on Catppuccin Mocha - a soothing dark color palette.</description>
|
||||
</preferencepack>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
2
src/3rdParty/OndselSolver
vendored
@@ -674,14 +674,15 @@ void StdCmdNew::activated(int iMsg)
|
||||
}
|
||||
|
||||
// Set default view orientation for the new document
|
||||
Gui::Document* guiDoc = Application::Instance->getDocument(doc);
|
||||
if (guiDoc) {
|
||||
auto views = guiDoc->getMDIViewsOfType(View3DInventor::getClassTypeId());
|
||||
for (auto* view : views) {
|
||||
auto view3d = static_cast<View3DInventor*>(view);
|
||||
view3d->getViewer()->viewDefaultOrientation();
|
||||
}
|
||||
}
|
||||
auto hGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/View"
|
||||
);
|
||||
std::string default_view = hGrp->GetASCII("NewDocumentCameraOrientation", "Top");
|
||||
doCommand(
|
||||
Command::Gui,
|
||||
"Gui.activeDocument().activeView().viewDefaultOrientation('%s',0)",
|
||||
default_view.c_str()
|
||||
);
|
||||
|
||||
ParameterGrp::handle hViewGrp = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/View"
|
||||
|
||||
@@ -185,6 +185,11 @@
|
||||
<file>sel-bbox.svg</file>
|
||||
<file>sel-forward.svg</file>
|
||||
<file>sel-instance.svg</file>
|
||||
<file>silo-bom.svg</file>
|
||||
<file>silo-commit.svg</file>
|
||||
<file>silo-info.svg</file>
|
||||
<file>silo-pull.svg</file>
|
||||
<file>silo-push.svg</file>
|
||||
<file>spaceball_button.svg</file>
|
||||
<file>SpNav-PanLR.svg</file>
|
||||
<file>SpNav-PanUD.svg</file>
|
||||
|
||||
12
src/Gui/Icons/silo-bom.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Outer box -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" fill="#313244"/>
|
||||
<!-- List lines (BOM rows) -->
|
||||
<line x1="8" y1="8" x2="18" y2="8" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<line x1="8" y1="12" x2="18" y2="12" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<line x1="8" y1="16" x2="18" y2="16" stroke="#89dceb" stroke-width="1.5"/>
|
||||
<!-- Hierarchy dots -->
|
||||
<circle cx="6" cy="8" r="1" fill="#cba6f7"/>
|
||||
<circle cx="6" cy="12" r="1" fill="#cba6f7"/>
|
||||
<circle cx="6" cy="16" r="1" fill="#cba6f7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
8
src/Gui/Icons/silo-commit.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Git commit style -->
|
||||
<circle cx="12" cy="12" r="4" fill="#313244" stroke="#a6e3a1"/>
|
||||
<line x1="12" y1="2" x2="12" y2="8" stroke="#cba6f7"/>
|
||||
<line x1="12" y1="16" x2="12" y2="22" stroke="#cba6f7"/>
|
||||
<!-- Checkmark inside -->
|
||||
<polyline points="9.5 12 11 13.5 14.5 10" stroke="#a6e3a1" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
6
src/Gui/Icons/silo-info.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Info circle -->
|
||||
<circle cx="12" cy="12" r="10" fill="#313244"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12" stroke="#89dceb" stroke-width="2"/>
|
||||
<circle cx="12" cy="8" r="0.5" fill="#89dceb" stroke="#89dceb"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
7
src/Gui/Icons/silo-pull.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Cloud -->
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||
<!-- Download arrow -->
|
||||
<path d="M12 13v5m0 0l-2-2m2 2l2-2" stroke="#89b4fa" stroke-width="2"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13" stroke="#89b4fa" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
7
src/Gui/Icons/silo-push.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cba6f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Cloud -->
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" fill="#313244"/>
|
||||
<!-- Upload arrow -->
|
||||
<path d="M12 18v-5m0 0l-2 2m2-2l2 2" stroke="#a6e3a1" stroke-width="2"/>
|
||||
<line x1="12" y1="13" x2="12" y2="9" stroke="#a6e3a1" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -124,7 +124,7 @@ bool OriginManager::registerOrigin(FileOrigin* origin)
|
||||
}
|
||||
|
||||
_origins[originId] = std::unique_ptr<FileOrigin>(origin);
|
||||
Base::Console().Log("OriginManager: Registered origin '%s'\n", originId.c_str());
|
||||
Base::Console().log("OriginManager: Registered origin '%s'\n", originId.c_str());
|
||||
|
||||
signalOriginRegistered(originId);
|
||||
return true;
|
||||
@@ -150,7 +150,7 @@ bool OriginManager::unregisterOrigin(const std::string& id)
|
||||
}
|
||||
|
||||
_origins.erase(it);
|
||||
Base::Console().Log("OriginManager: Unregistered origin '%s'\n", id.c_str());
|
||||
Base::Console().log("OriginManager: Unregistered origin '%s'\n", id.c_str());
|
||||
|
||||
signalOriginUnregistered(id);
|
||||
return true;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
@@ -244,7 +245,7 @@ QIcon OriginSelectorWidget::iconForOrigin(FileOrigin* origin) const
|
||||
QPixmap overlay = BitmapFactory().pixmapFromSvg(
|
||||
"dagViewFail", QSizeF(8, 8));
|
||||
if (!overlay.isNull()) {
|
||||
baseIcon = BitmapFactory::mergePixmap(
|
||||
baseIcon = BitmapFactoryInst::mergePixmap(
|
||||
baseIcon, overlay, BitmapFactoryInst::BottomRight);
|
||||
}
|
||||
}
|
||||
@@ -256,7 +257,7 @@ QIcon OriginSelectorWidget::iconForOrigin(FileOrigin* origin) const
|
||||
QPixmap overlay = BitmapFactory().pixmapFromSvg(
|
||||
"Warning", QSizeF(8, 8));
|
||||
if (!overlay.isNull()) {
|
||||
baseIcon = BitmapFactory::mergePixmap(
|
||||
baseIcon = BitmapFactoryInst::mergePixmap(
|
||||
baseIcon, overlay, BitmapFactoryInst::BottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ ADD_CUSTOM_TARGET(PreferencePacks_data ALL
|
||||
|
||||
FILE(COPY ${PreferencePacks_Files} ${PreferencePacks_Directories} DESTINATION "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks")
|
||||
|
||||
# Copy KindredCreate.qss from Stylesheets into the PreferencePacks build directory.
|
||||
# The canonical QSS lives in src/Gui/Stylesheets/; this avoids maintaining a duplicate. (#51)
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../Stylesheets/KindredCreate.qss
|
||||
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks/KindredCreate/KindredCreate.qss
|
||||
COPYONLY
|
||||
)
|
||||
|
||||
fc_copy_sources(PreferencePacks_data "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks"
|
||||
${PreferencePacks_Files}
|
||||
${PreferencePacks_Directories})
|
||||
@@ -23,9 +31,10 @@ INSTALL(
|
||||
${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks
|
||||
)
|
||||
|
||||
# Install from build directory so the generated QSS copy is included
|
||||
INSTALL(
|
||||
DIRECTORY
|
||||
${PreferencePacks_Directories}
|
||||
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks/KindredCreate
|
||||
DESTINATION
|
||||
${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks
|
||||
)
|
||||
|
||||
@@ -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" />
|
||||
@@ -81,9 +85,29 @@
|
||||
<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>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="MainWindow">
|
||||
<FCText Name="StyleSheet">KindredCreate.qss</FCText>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <a href="freecad:Std_AddonMgr">Addon Manager</a>.</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,6 +64,7 @@ private:
|
||||
void setProcessMessages();
|
||||
void setAutoSaving();
|
||||
void setToolBarIconSize();
|
||||
void setMenuIconSize();
|
||||
void setWheelEventFilter();
|
||||
void setLocale();
|
||||
void setCursorFlashing();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* Hyperlinks — sets QPalette::Link via Application.cpp haslink mechanism */
|
||||
QLabel[haslink="true"] {
|
||||
color: #b4befe;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
Frames
|
||||
============================================================================= */
|
||||
|
||||
@@ -49,7 +49,9 @@ def activePartOrAssembly():
|
||||
|
||||
def activeAssembly():
|
||||
active_assembly = activePartOrAssembly()
|
||||
if active_assembly is not None and active_assembly.isDerivedFrom("Assembly::AssemblyObject"):
|
||||
if active_assembly is not None and active_assembly.isDerivedFrom(
|
||||
"Assembly::AssemblyObject"
|
||||
):
|
||||
if active_assembly.ViewObject.isInEditMode():
|
||||
return active_assembly
|
||||
|
||||
@@ -59,7 +61,9 @@ def activeAssembly():
|
||||
def activePart():
|
||||
active_part = activePartOrAssembly()
|
||||
|
||||
if active_part is not None and not active_part.isDerivedFrom("Assembly::AssemblyObject"):
|
||||
if active_part is not None and not active_part.isDerivedFrom(
|
||||
"Assembly::AssemblyObject"
|
||||
):
|
||||
return active_part
|
||||
|
||||
return None
|
||||
@@ -120,7 +124,9 @@ def number_of_components_in(assembly):
|
||||
def isLink(obj):
|
||||
# If element count is not 0, then its a link group in which case the Link
|
||||
# is a container and it's the LinkElement that is linking to external doc.
|
||||
return (obj.TypeId == "App::Link" and obj.ElementCount == 0) or obj.TypeId == "App::LinkElement"
|
||||
return (
|
||||
obj.TypeId == "App::Link" and obj.ElementCount == 0
|
||||
) or obj.TypeId == "App::LinkElement"
|
||||
|
||||
|
||||
def isLinkGroup(obj):
|
||||
@@ -375,7 +381,9 @@ def getGlobalPlacement(ref, targetObj=None):
|
||||
if not isRefValid(ref, 1):
|
||||
return App.Placement()
|
||||
|
||||
if targetObj is None: # If no targetObj is given, we consider it's the getObject(ref)
|
||||
if (
|
||||
targetObj is None
|
||||
): # If no targetObj is given, we consider it's the getObject(ref)
|
||||
targetObj = getObject(ref)
|
||||
if targetObj is None:
|
||||
return App.Placement()
|
||||
@@ -520,11 +528,17 @@ def findElementClosestVertex(ref, mousePos):
|
||||
|
||||
for i, edge in enumerate(edges):
|
||||
curve = edge.Curve
|
||||
if curve.TypeId == "Part::GeomCircle" or curve.TypeId == "Part::GeomEllipse":
|
||||
if (
|
||||
curve.TypeId == "Part::GeomCircle"
|
||||
or curve.TypeId == "Part::GeomEllipse"
|
||||
):
|
||||
center_points.append(curve.Location)
|
||||
center_points_edge_indexes.append(i)
|
||||
|
||||
elif _type == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve":
|
||||
elif (
|
||||
_type == "Part::GeomCylinder"
|
||||
and curve.TypeId == "Part::GeomBSplineCurve"
|
||||
):
|
||||
# handle special case of 2 cylinder intersecting.
|
||||
for j, facej in enumerate(obj.Shape.Faces):
|
||||
surfacej = facej.Surface
|
||||
@@ -553,7 +567,9 @@ def findElementClosestVertex(ref, mousePos):
|
||||
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
|
||||
centerOfG = face.CenterOfGravity - surface.Center
|
||||
centerPoint = surface.Center + centerOfG
|
||||
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis)
|
||||
centerPoint = centerPoint + App.Vector().projectToLine(
|
||||
centerOfG, surface.Axis
|
||||
)
|
||||
face_points.append(centerPoint)
|
||||
else:
|
||||
face_points.append(face.CenterOfGravity)
|
||||
@@ -623,7 +639,8 @@ def color_from_unsigned(c):
|
||||
|
||||
def getJointsOfType(asm, jointTypes):
|
||||
if not (
|
||||
asm.isDerivedFrom("Assembly::AssemblyObject") or asm.isDerivedFrom("Assembly::AssemblyLink")
|
||||
asm.isDerivedFrom("Assembly::AssemblyObject")
|
||||
or asm.isDerivedFrom("Assembly::AssemblyLink")
|
||||
):
|
||||
return []
|
||||
|
||||
@@ -763,7 +780,9 @@ def getSubMovingParts(obj, partsAsSolid):
|
||||
|
||||
if isLink(obj):
|
||||
linked_obj = obj.getLinkedObject()
|
||||
if linked_obj.isDerivedFrom("App::Part") or linked_obj.isDerivedFrom("Part::Feature"):
|
||||
if linked_obj.isDerivedFrom("App::Part") or linked_obj.isDerivedFrom(
|
||||
"Part::Feature"
|
||||
):
|
||||
return [obj]
|
||||
|
||||
return []
|
||||
@@ -996,7 +1015,7 @@ def findPlacement(ref, ignoreVertex=False):
|
||||
vtx = getElementName(ref[1][1])
|
||||
|
||||
if not elt or not vtx:
|
||||
# case of whole parts such as PartDesign::Body or App/PartDesign::CordinateSystem/Point/Line/Plane.
|
||||
# Origin objects (App::Line, App::Plane, App::Point)
|
||||
if obj.TypeId == "App::Line":
|
||||
if obj.Role == "X_Axis":
|
||||
return App.Placement(App.Vector(), App.Rotation(0.5, 0.5, 0.5, 0.5))
|
||||
@@ -1005,9 +1024,25 @@ def findPlacement(ref, ignoreVertex=False):
|
||||
if obj.Role == "Z_Axis":
|
||||
return App.Placement(App.Vector(), App.Rotation(-0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
# PartDesign datum planes (including ZTools datums like ZPlane_Mid, ZPlane_Offset)
|
||||
if obj.TypeId == "App::Plane":
|
||||
if obj.Role == "XY_Plane":
|
||||
return App.Placement()
|
||||
if obj.Role == "XZ_Plane":
|
||||
return App.Placement(
|
||||
App.Vector(), App.Rotation(App.Vector(1, 0, 0), -90)
|
||||
)
|
||||
if obj.Role == "YZ_Plane":
|
||||
return App.Placement(
|
||||
App.Vector(), App.Rotation(App.Vector(0, 1, 0), 90)
|
||||
)
|
||||
return App.Placement()
|
||||
|
||||
if obj.TypeId == "App::Point":
|
||||
return App.Placement()
|
||||
|
||||
# PartDesign datum planes
|
||||
if obj.isDerivedFrom("PartDesign::Plane"):
|
||||
if hasattr(obj, "Shape") and obj.Shape.Faces:
|
||||
if hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.Faces:
|
||||
face = obj.Shape.Faces[0]
|
||||
surface = face.Surface
|
||||
plc = App.Placement()
|
||||
@@ -1015,9 +1050,28 @@ def findPlacement(ref, ignoreVertex=False):
|
||||
if hasattr(surface, "Rotation") and surface.Rotation is not None:
|
||||
plc.Rotation = App.Rotation(surface.Rotation)
|
||||
return obj.Placement.inverse() * plc
|
||||
return obj.Placement
|
||||
|
||||
# PartDesign datum lines
|
||||
if obj.isDerivedFrom("PartDesign::Line"):
|
||||
if hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.Edges:
|
||||
edge = obj.Shape.Edges[0]
|
||||
points = getPointsFromVertexes(edge.Vertexes)
|
||||
mid = (points[0] + points[1]) * 0.5
|
||||
direction = round_vector(edge.Curve.Direction)
|
||||
plane = Part.Plane(App.Vector(), direction)
|
||||
plc = App.Placement()
|
||||
plc.Base = mid
|
||||
plc.Rotation = App.Rotation(plane.Rotation)
|
||||
return obj.Placement.inverse() * plc
|
||||
return obj.Placement
|
||||
|
||||
# PartDesign datum points
|
||||
if obj.isDerivedFrom("PartDesign::Point"):
|
||||
if hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.Vertexes:
|
||||
plc = App.Placement()
|
||||
plc.Base = obj.Shape.Vertexes[0].Point
|
||||
return obj.Placement.inverse() * plc
|
||||
return obj.Placement
|
||||
|
||||
return App.Placement()
|
||||
@@ -1080,9 +1134,14 @@ def findPlacement(ref, ignoreVertex=False):
|
||||
if surface.TypeId == "Part::GeomCylinder":
|
||||
centerOfG = face.CenterOfGravity - surface.Center
|
||||
centerPoint = surface.Center + centerOfG
|
||||
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis)
|
||||
centerPoint = centerPoint + App.Vector().projectToLine(
|
||||
centerOfG, surface.Axis
|
||||
)
|
||||
plc.Base = centerPoint
|
||||
elif surface.TypeId == "Part::GeomTorus" or surface.TypeId == "Part::GeomSphere":
|
||||
elif (
|
||||
surface.TypeId == "Part::GeomTorus"
|
||||
or surface.TypeId == "Part::GeomSphere"
|
||||
):
|
||||
plc.Base = surface.Center
|
||||
elif surface.TypeId == "Part::GeomCone":
|
||||
plc.Base = surface.Apex
|
||||
@@ -1100,7 +1159,8 @@ def findPlacement(ref, ignoreVertex=False):
|
||||
plc.Base = (center_point.x, center_point.y, center_point.z)
|
||||
|
||||
elif (
|
||||
surface.TypeId == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve"
|
||||
surface.TypeId == "Part::GeomCylinder"
|
||||
and curve.TypeId == "Part::GeomBSplineCurve"
|
||||
):
|
||||
# handle special case of 2 cylinder intersecting.
|
||||
plc.Base = findCylindersIntersection(obj, surface, edge, elt_index)
|
||||
@@ -1394,13 +1454,16 @@ def generatePropertySettings(documentObject):
|
||||
commands.append(f"obj.{propertyName} = {propertyValue:.5f}")
|
||||
elif propertyType == "App::PropertyInt" or propertyType == "App::PropertyBool":
|
||||
commands.append(f"obj.{propertyName} = {propertyValue}")
|
||||
elif propertyType == "App::PropertyString" or propertyType == "App::PropertyEnumeration":
|
||||
elif (
|
||||
propertyType == "App::PropertyString"
|
||||
or propertyType == "App::PropertyEnumeration"
|
||||
):
|
||||
commands.append(f'obj.{propertyName} = "{propertyValue}"')
|
||||
elif propertyType == "App::PropertyPlacement":
|
||||
commands.append(
|
||||
f"obj.{propertyName} = App.Placement("
|
||||
f"App.Vector({propertyValue.Base.x:.5f},{propertyValue.Base.y:.5f},{propertyValue.Base.z:.5f}),"
|
||||
f"App.Rotation(*{[round(n,5) for n in propertyValue.Rotation.getYawPitchRoll()]}))"
|
||||
f"App.Rotation(*{[round(n, 5) for n in propertyValue.Rotation.getYawPitchRoll()]}))"
|
||||
)
|
||||
elif propertyType == "App::PropertyXLinkSubHidden":
|
||||
commands.append(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -33,7 +42,13 @@ install(
|
||||
# Install Silo addon
|
||||
install(
|
||||
DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}/mods/silo/pkg/freecad/
|
||||
${CMAKE_SOURCE_DIR}/mods/silo/freecad/
|
||||
DESTINATION
|
||||
mods/silo/pkg/freecad
|
||||
mods/silo/freecad
|
||||
)
|
||||
install(
|
||||
DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}/mods/silo/silo-client/
|
||||
DESTINATION
|
||||
mods/silo/silo-client
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ def setup_kindred_addons():
|
||||
# Define built-in addons with their paths relative to mods/
|
||||
addons = [
|
||||
("ztools", "ztools/ztools"), # mods/ztools/ztools/
|
||||
("silo", "silo/pkg/freecad"), # mods/silo/pkg/freecad/
|
||||
("silo", "silo/freecad"), # mods/silo/freecad/
|
||||
]
|
||||
|
||||
for name, subpath in addons:
|
||||
|
||||
@@ -15,7 +15,7 @@ def setup_kindred_workbenches():
|
||||
|
||||
addons = [
|
||||
("ztools", "ztools/ztools"),
|
||||
("silo", "silo/pkg/freecad"),
|
||||
("silo", "silo/freecad"),
|
||||
]
|
||||
|
||||
for name, subpath in addons:
|
||||
@@ -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
|
||||
|
||||
165
src/Mod/Create/update_checker.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Kindred Create update checker.
|
||||
|
||||
Queries the Gitea releases API to determine if a newer version is
|
||||
available. Designed to run in the background on startup without
|
||||
blocking the UI.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import FreeCAD
|
||||
|
||||
_RELEASES_URL = "https://git.kindred-systems.com/api/v1/repos/kindred/create/releases"
|
||||
_PREF_PATH = "User parameter:BaseApp/Preferences/Mod/KindredCreate/Update"
|
||||
_TIMEOUT = 5
|
||||
|
||||
|
||||
def _parse_version(tag):
|
||||
"""Parse a version tag like 'v0.1.3' into a comparable tuple.
|
||||
|
||||
Returns None if the tag doesn't match the expected pattern.
|
||||
"""
|
||||
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)$", tag)
|
||||
if not m:
|
||||
return None
|
||||
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
||||
|
||||
|
||||
def check_for_update(current_version):
|
||||
"""Check if a newer release is available on Gitea.
|
||||
|
||||
Args:
|
||||
current_version: Version string like "0.1.3".
|
||||
|
||||
Returns:
|
||||
Dict with update info if a newer version exists, None otherwise.
|
||||
Dict keys: version, tag, release_url, assets, body.
|
||||
"""
|
||||
current = _parse_version(current_version)
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{_RELEASES_URL}?limit=10",
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
|
||||
releases = json.loads(resp.read())
|
||||
|
||||
best = None
|
||||
best_version = current
|
||||
|
||||
for release in releases:
|
||||
if release.get("draft"):
|
||||
continue
|
||||
if release.get("prerelease"):
|
||||
continue
|
||||
|
||||
tag = release.get("tag_name", "")
|
||||
# Skip the rolling 'latest' tag
|
||||
if tag == "latest":
|
||||
continue
|
||||
|
||||
ver = _parse_version(tag)
|
||||
if ver is None:
|
||||
continue
|
||||
|
||||
if ver > best_version:
|
||||
best_version = ver
|
||||
best = release
|
||||
|
||||
if best is None:
|
||||
return None
|
||||
|
||||
assets = []
|
||||
for asset in best.get("assets", []):
|
||||
assets.append(
|
||||
{
|
||||
"name": asset.get("name", ""),
|
||||
"url": asset.get("browser_download_url", ""),
|
||||
"size": asset.get("size", 0),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"version": ".".join(str(x) for x in best_version),
|
||||
"tag": best["tag_name"],
|
||||
"release_url": best.get("html_url", ""),
|
||||
"assets": assets,
|
||||
"body": best.get("body", ""),
|
||||
}
|
||||
|
||||
|
||||
def _should_check(param):
|
||||
"""Determine whether an update check should run now.
|
||||
|
||||
Args:
|
||||
param: FreeCAD parameter group for update preferences.
|
||||
|
||||
Returns:
|
||||
True if a check should be performed.
|
||||
"""
|
||||
if not param.GetBool("CheckEnabled", True):
|
||||
return False
|
||||
|
||||
last_check = param.GetString("LastCheckTimestamp", "")
|
||||
if not last_check:
|
||||
return True
|
||||
|
||||
interval_days = param.GetInt("CheckIntervalDays", 1)
|
||||
if interval_days <= 0:
|
||||
return True
|
||||
|
||||
try:
|
||||
last_dt = datetime.fromisoformat(last_check)
|
||||
now = datetime.now(timezone.utc)
|
||||
elapsed = (now - last_dt).total_seconds()
|
||||
return elapsed >= interval_days * 86400
|
||||
except (ValueError, TypeError):
|
||||
return True
|
||||
|
||||
|
||||
def _run_update_check():
|
||||
"""Entry point called from the deferred startup timer."""
|
||||
param = FreeCAD.ParamGet(_PREF_PATH)
|
||||
|
||||
if not _should_check(param):
|
||||
return
|
||||
|
||||
try:
|
||||
from version import VERSION
|
||||
except ImportError:
|
||||
FreeCAD.Console.PrintLog(
|
||||
"Create: update check skipped — version module not available\n"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
result = check_for_update(VERSION)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintLog(f"Create: update check failed: {e}\n")
|
||||
return
|
||||
|
||||
# Record that we checked
|
||||
param.SetString(
|
||||
"LastCheckTimestamp",
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
)
|
||||
|
||||
if result is None:
|
||||
FreeCAD.Console.PrintLog("Create: application is up to date\n")
|
||||
return
|
||||
|
||||
skipped = param.GetString("SkippedVersion", "")
|
||||
if result["version"] == skipped:
|
||||
FreeCAD.Console.PrintLog(
|
||||
f"Create: update {result['version']} available but skipped by user\n"
|
||||
)
|
||||
return
|
||||
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Kindred Create {result['version']} is available (current: {VERSION})\n"
|
||||
)
|
||||
1
src/Mod/Create/version.py.in
Normal file
@@ -0,0 +1 @@
|
||||
VERSION = "@KINDRED_CREATE_VERSION@"
|
||||
@@ -4,6 +4,7 @@
|
||||
add_executable(Gui_tests_run
|
||||
Assistant.cpp
|
||||
Camera.cpp
|
||||
OriginManager.cpp
|
||||
StyleParameters/StyleParametersApplicationTest.cpp
|
||||
StyleParameters/ParserTest.cpp
|
||||
StyleParameters/ParameterManagerTest.cpp
|
||||
|
||||
382
tests/src/Gui/OriginManager.cpp
Normal 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);
|
||||
}
|
||||