Import CAD projects and cad-router v1
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "text-to-cad",
|
||||
"owner": {
|
||||
"name": "earthtojake"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "CAD, robotics, fabrication, and local review plugins."
|
||||
},
|
||||
"description": "CAD, robotics, fabrication, and local review plugins.",
|
||||
"version": "0.3.9",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "cad",
|
||||
"source": "./plugins/cad",
|
||||
"description": "CAD, robotics, fabrication, and local review skills.",
|
||||
"version": "0.3.9",
|
||||
"author": {
|
||||
"name": "earthtojake"
|
||||
},
|
||||
"homepage": "https://www.cadskills.xyz",
|
||||
"repository": "https://github.com/earthtojake/text-to-cad",
|
||||
"license": "MIT",
|
||||
"category": "productivity",
|
||||
"tags": [
|
||||
"cad",
|
||||
"robotics",
|
||||
"fabrication",
|
||||
"skills"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "text-to-cad",
|
||||
"interface": {
|
||||
"displayName": "Text to CAD"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "cad",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/cad"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Keep CAD exchange files and generated render/topology assets out of normal git object history.
|
||||
assets/** filter=lfs diff=lfs merge=lfs -text
|
||||
benchmarks/** filter=lfs diff=lfs merge=lfs -text
|
||||
# Keep benchmark/spec markdown in normal git so reviews and diffs stay readable.
|
||||
assets/**/*.md !filter !diff !merge text
|
||||
benchmarks/**/*.md !filter !diff !merge text
|
||||
*.step filter=lfs diff=lfs merge=lfs -text
|
||||
*.stp filter=lfs diff=lfs merge=lfs -text
|
||||
*.stl filter=lfs diff=lfs merge=lfs -text
|
||||
*.3mf filter=lfs diff=lfs merge=lfs -text
|
||||
*.gcode filter=lfs diff=lfs merge=lfs -text
|
||||
*.gcode.3mf filter=lfs diff=lfs merge=lfs -text
|
||||
*.dxf filter=lfs diff=lfs merge=lfs -text
|
||||
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||
*.gltf filter=lfs diff=lfs merge=lfs -text
|
||||
*.obj filter=lfs diff=lfs merge=lfs -text
|
||||
*.ply filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
docs/public/hero/**/*.glb !filter !diff !merge -text
|
||||
.*.step.glb filter=lfs diff=lfs merge=lfs -text
|
||||
.*.stp.glb filter=lfs diff=lfs merge=lfs -text
|
||||
**/.*.step/**/*.glb filter=lfs diff=lfs merge=lfs -text
|
||||
**/.*.step/topology.bin filter=lfs diff=lfs merge=lfs -text
|
||||
**/.*.step/topology.json filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; }
|
||||
git lfs post-checkout "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-commit.\n"; exit 2; }
|
||||
git lfs post-commit "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-merge.\n"; exit 2; }
|
||||
git lfs post-merge "$@"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
exec "$REPO_ROOT/scripts/git-hooks/pre-commit" "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
|
||||
git lfs pre-push "$@"
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Set up dependencies
|
||||
description: >-
|
||||
Set up Python and Node.js with dependency caching, then install the repo
|
||||
Python and Node dependencies used by tests and production bundling.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: requirements-dev.txt
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
packages/cadjs/package-lock.json
|
||||
packages/implicitjs/package-lock.json
|
||||
viewer/package-lock.json
|
||||
docs/package-lock.json
|
||||
|
||||
- name: Install Python dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements-dev.txt
|
||||
|
||||
- name: Install Node dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
npm ci --prefix packages/cadjs
|
||||
npm ci --prefix packages/implicitjs
|
||||
npm ci --prefix viewer
|
||||
npm ci --prefix docs
|
||||
@@ -0,0 +1,29 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- github-actions[bot]
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
- breaking-change
|
||||
- semver-major
|
||||
- title: Features
|
||||
labels:
|
||||
- enhancement
|
||||
- feature
|
||||
- semver-minor
|
||||
- title: Fixes
|
||||
labels:
|
||||
- bug
|
||||
- fix
|
||||
- semver-patch
|
||||
- title: Maintenance
|
||||
labels:
|
||||
- dependencies
|
||||
- documentation
|
||||
- maintenance
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Production-layout ref to deploy, normally main or a publish commit.
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: Production-layout ref to deploy.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-docs
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy docs app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out deploy ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@50.28.0
|
||||
|
||||
- name: Deploy docs app to Vercel production
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_DOCS_PROJECT_ID: ${{ secrets.VERCEL_DOCS_PROJECT_ID }}
|
||||
run: |
|
||||
for name in VERCEL_TOKEN VERCEL_ORG_ID VERCEL_DOCS_PROJECT_ID; do
|
||||
if [ -z "${!name:-}" ]; then
|
||||
echo "Missing GitHub Actions secret: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
scripts/github-workflows/deploy-vercel-app.sh \
|
||||
--label "Docs app" \
|
||||
--project-id "$VERCEL_DOCS_PROJECT_ID" \
|
||||
--public-urls "https://www.cadskills.xyz https://cadskills.xyz https://www.cad.fun https://cad.fun"
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Deploy Viewer
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Production-layout ref to deploy, normally main or a publish commit.
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: Production-layout ref to deploy.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-viewer
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy demo viewer app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out deploy ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@50.28.0
|
||||
|
||||
- name: Deploy demo viewer app to Vercel production
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_VIEWER_PROJECT_ID: ${{ secrets.VERCEL_VIEWER_PROJECT_ID }}
|
||||
run: |
|
||||
for name in VERCEL_TOKEN VERCEL_ORG_ID VERCEL_VIEWER_PROJECT_ID; do
|
||||
if [ -z "${!name:-}" ]; then
|
||||
echo "Missing GitHub Actions secret: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
scripts/github-workflows/deploy-vercel-app.sh \
|
||||
--label "Demo viewer app" \
|
||||
--project-id "$VERCEL_VIEWER_PROJECT_ID" \
|
||||
--public-urls "https://demo.cadskills.xyz https://demo.cad.fun"
|
||||
@@ -0,0 +1,512 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: Semver part to bump when set_version is empty.
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
set_version:
|
||||
description: Exact X.Y.Z version to release instead of bumping. Set to the current version to resume a failed publish.
|
||||
required: false
|
||||
type: string
|
||||
base_branch:
|
||||
description: Branch to release from and merge the release PR into.
|
||||
required: true
|
||||
default: develop
|
||||
type: string
|
||||
target_branch:
|
||||
description: Publish target. Use main for releases; use build-test only to test CI/CD or build changes.
|
||||
required: true
|
||||
default: main
|
||||
type: choice
|
||||
options:
|
||||
- main
|
||||
- build-test
|
||||
dry_run:
|
||||
description: Show the release version changes without pushing, merging, or publishing.
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
publish:
|
||||
description: Publish the GitHub Release. Set to false to leave it as a draft.
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
create_release:
|
||||
description: Create the GitHub Release after pushing the tag.
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
auto_merge:
|
||||
description: Merge the release PR immediately, then publish. Set to false to only prepare the PR.
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ inputs.base_branch }}-${{ inputs.target_branch }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-pr:
|
||||
name: Release PR
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
source_sha: ${{ steps.publish_source.outputs.sha }}
|
||||
|
||||
steps:
|
||||
- name: Check out base branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.base_branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Configure release credentials
|
||||
env:
|
||||
RELEASE_ORCHESTRATOR_TOKEN: ${{ secrets.RELEASE_ORCHESTRATOR_TOKEN }}
|
||||
PREPARE_RELEASE_TOKEN: ${{ secrets.PREPARE_RELEASE_TOKEN }}
|
||||
PUBLISH_PUSH_TOKEN: ${{ secrets.PUBLISH_PUSH_TOKEN }}
|
||||
BUILD_TEST_PUSH_TOKEN: ${{ secrets.BUILD_TEST_PUSH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
release_token="${RELEASE_ORCHESTRATOR_TOKEN:-${PREPARE_RELEASE_TOKEN:-${PUBLISH_PUSH_TOKEN:-${BUILD_TEST_PUSH_TOKEN:-$GITHUB_TOKEN}}}}"
|
||||
if [ -n "${RELEASE_ORCHESTRATOR_TOKEN:-}" ]; then
|
||||
echo "Using RELEASE_ORCHESTRATOR_TOKEN for release orchestration."
|
||||
elif [ -n "${PREPARE_RELEASE_TOKEN:-}" ]; then
|
||||
echo "Using PREPARE_RELEASE_TOKEN for release orchestration."
|
||||
elif [ -n "${PUBLISH_PUSH_TOKEN:-}" ]; then
|
||||
echo "Using PUBLISH_PUSH_TOKEN for release orchestration."
|
||||
elif [ -n "${BUILD_TEST_PUSH_TOKEN:-}" ]; then
|
||||
echo "Using BUILD_TEST_PUSH_TOKEN for release orchestration."
|
||||
else
|
||||
echo "Using GITHUB_TOKEN for release orchestration."
|
||||
fi
|
||||
|
||||
git remote set-url origin "https://x-access-token:${release_token}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
{
|
||||
echo "GH_TOKEN=$release_token"
|
||||
echo "RELEASE_TOKEN=$release_token"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare canonical version
|
||||
id: version
|
||||
env:
|
||||
BUMP: ${{ inputs.bump }}
|
||||
SET_VERSION: ${{ inputs.set_version }}
|
||||
run: |
|
||||
if [ -n "$SET_VERSION" ]; then
|
||||
scripts/release/bump-version.sh --set-version "$SET_VERSION" --no-commit
|
||||
else
|
||||
scripts/release/bump-version.sh "$BUMP" --no-commit
|
||||
fi
|
||||
node scripts/release/sync-version.mjs
|
||||
version="$(tr -d '[:space:]' < plugins/cad/VERSION)"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
if git diff --quiet; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Base branch already contains release version $version; skipping the release PR."
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Prepared release version: $version"
|
||||
fi
|
||||
|
||||
- name: Check release version metadata
|
||||
env:
|
||||
CHANGED: ${{ steps.version.outputs.changed }}
|
||||
run: |
|
||||
node scripts/release/sync-version.mjs --check
|
||||
if [ "$CHANGED" = "true" ]; then
|
||||
scripts/release/check-version.sh --incremented-from origin/main
|
||||
latest_tag="$(git tag --list '[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1 || true)"
|
||||
if [ -n "$latest_tag" ]; then
|
||||
scripts/release/check-version.sh --incremented-from "refs/tags/$latest_tag"
|
||||
fi
|
||||
else
|
||||
scripts/release/check-version.sh
|
||||
echo "No version metadata changes; the publish gate decides whether this version still needs to ship."
|
||||
fi
|
||||
|
||||
- name: Check development symlink layout
|
||||
run: scripts/dev/setup-symlinks.sh --check
|
||||
|
||||
- name: Show dry-run diff
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
git diff --stat
|
||||
git diff -- plugins/cad/VERSION
|
||||
|
||||
- name: Stop after dry run
|
||||
if: inputs.dry_run
|
||||
run: echo "Dry run requested; skipping release branch, PR merge, and publish."
|
||||
|
||||
- name: Create or update release pull request
|
||||
if: ${{ !inputs.dry_run && steps.version.outputs.changed == 'true' }}
|
||||
id: release_pr
|
||||
env:
|
||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
branch="release/$VERSION"
|
||||
body="Bumps plugins/cad/VERSION and derived package/plugin metadata to $VERSION. This PR was created by the one-button Release workflow, which merges it into $BASE_BRANCH immediately; the publish job revalidates the full production bundle before anything ships to the target branch."
|
||||
|
||||
git checkout -B "$branch"
|
||||
git add -A
|
||||
git commit -m "Release $VERSION"
|
||||
git push --force-with-lease origin "$branch"
|
||||
|
||||
if pr_json="$(gh pr view "$branch" --repo "$GITHUB_REPOSITORY" --json number,url 2>/dev/null)"; then
|
||||
pr_number="$(printf '%s\n' "$pr_json" | jq -r '.number')"
|
||||
pr_url="$(printf '%s\n' "$pr_json" | jq -r '.url')"
|
||||
gh pr edit "$pr_number" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--title "Release $VERSION" \
|
||||
--body "$body"
|
||||
else
|
||||
pr_url="$(gh pr create \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$branch" \
|
||||
--title "Release $VERSION" \
|
||||
--body "$body")"
|
||||
pr_number="$(gh pr view "$pr_url" --repo "$GITHUB_REPOSITORY" --json number --jq '.number')"
|
||||
fi
|
||||
|
||||
echo "created=true" >> "$GITHUB_OUTPUT"
|
||||
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
echo "url=$pr_url" >> "$GITHUB_OUTPUT"
|
||||
echo "Release PR: $pr_url"
|
||||
|
||||
- name: Merge release pull request
|
||||
if: ${{ !inputs.dry_run && inputs.auto_merge && steps.release_pr.outputs.created == 'true' }}
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.release_pr.outputs.number }}
|
||||
run: |
|
||||
pr_json="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json headRefOid)"
|
||||
head_sha="$(printf '%s\n' "$pr_json" | jq -r '.headRefOid')"
|
||||
merged=false
|
||||
for attempt in $(seq 1 6); do
|
||||
if gh api \
|
||||
--method PUT \
|
||||
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/merge" \
|
||||
-f merge_method=merge \
|
||||
-f sha="$head_sha"; then
|
||||
merged=true
|
||||
break
|
||||
fi
|
||||
echo "Merge attempt $attempt failed; retrying while GitHub computes mergeability..."
|
||||
sleep 10
|
||||
done
|
||||
if [ "$merged" != "true" ]; then
|
||||
echo "Could not merge release PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Stop before publish when auto-merge is disabled
|
||||
if: ${{ !inputs.dry_run && !inputs.auto_merge }}
|
||||
run: echo "auto_merge=false; the release PR was prepared but the publish jobs were skipped."
|
||||
|
||||
- name: Resolve publish source
|
||||
if: ${{ !inputs.dry_run && inputs.auto_merge }}
|
||||
id: publish_source
|
||||
env:
|
||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||
run: |
|
||||
git fetch --no-tags origin "+$BASE_BRANCH:refs/remotes/origin/$BASE_BRANCH"
|
||||
source_sha="$(git rev-parse "origin/$BASE_BRANCH^{commit}")"
|
||||
echo "sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing $BASE_BRANCH at $source_sha."
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
needs: release-pr
|
||||
if: ${{ !inputs.dry_run && inputs.auto_merge }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
published: ${{ steps.push.outputs.published }}
|
||||
publish_sha: ${{ steps.commit.outputs.publish_sha }}
|
||||
|
||||
steps:
|
||||
- name: Check out publish source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-pr.outputs.source_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure publish credentials
|
||||
env:
|
||||
PUBLISH_PUSH_TOKEN: ${{ secrets.PUBLISH_PUSH_TOKEN }}
|
||||
BUILD_TEST_PUSH_TOKEN: ${{ secrets.BUILD_TEST_PUSH_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ -n "$PUBLISH_PUSH_TOKEN" ]; then
|
||||
git remote set-url origin "https://x-access-token:${PUBLISH_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
echo "Using PUBLISH_PUSH_TOKEN for publish push."
|
||||
elif [ -n "$BUILD_TEST_PUSH_TOKEN" ]; then
|
||||
git remote set-url origin "https://x-access-token:${BUILD_TEST_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
echo "Using BUILD_TEST_PUSH_TOKEN for publish push."
|
||||
else
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
echo "Using GITHUB_TOKEN for publish push."
|
||||
fi
|
||||
|
||||
- name: Check canonical release version
|
||||
run: scripts/release/check-version.sh
|
||||
|
||||
- name: Evaluate release gate
|
||||
id: gate
|
||||
env:
|
||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
source_sha="$(git rev-parse HEAD)"
|
||||
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "$TARGET_BRANCH" != "main" ]; then
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Build-test publish requested for $source_sha (CI/CD or build testing only)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$BASE_BRANCH" != "develop" ]; then
|
||||
echo "Real releases must publish develop to main; got base_branch=$BASE_BRANCH." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
current_version="$(tr -d '[:space:]' < plugins/cad/VERSION)"
|
||||
latest_tag="$(git tag --list '[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1 || true)"
|
||||
if ! scripts/release/check-version.sh --incremented-from origin/main >/tmp/publish-main-version-check.log 2>&1; then
|
||||
main_version="$(git show origin/main:plugins/cad/VERSION | tr -d '[:space:]')"
|
||||
if [ "$main_version" = "$current_version" ] && ! git rev-parse --verify --quiet "refs/tags/$current_version" >/dev/null; then
|
||||
echo "Release version already reached origin/main, but tag $current_version is missing; resuming publish."
|
||||
else
|
||||
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping publish: version has not advanced past origin/main."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [ -n "$latest_tag" ] && ! scripts/release/check-version.sh --incremented-from "refs/tags/$latest_tag" >/tmp/publish-tag-version-check.log 2>&1; then
|
||||
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping publish: version has not advanced past latest tag $latest_tag."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Release version advanced; publishing $source_sha to $TARGET_BRANCH."
|
||||
|
||||
- name: Skip non-release publish
|
||||
if: steps.gate.outputs.should_publish != 'true'
|
||||
run: echo "Publish only updates main when the source version is newer than main and the latest release tag."
|
||||
|
||||
- name: Validate publish target
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
env:
|
||||
SOURCE_SHA: ${{ steps.gate.outputs.source_sha }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
scripts/release/check-publish-source.sh --source-ref "$SOURCE_SHA" --target-ref origin/main
|
||||
if [ "$TARGET_BRANCH" = "main" ]; then
|
||||
echo "Validated main publish target."
|
||||
else
|
||||
echo "Validated build-test target against origin/main."
|
||||
fi
|
||||
|
||||
- name: Set up dependencies
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
uses: ./.github/actions/setup-deps
|
||||
|
||||
- name: Check development symlink layout
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
run: scripts/dev/setup-symlinks.sh --check
|
||||
|
||||
- name: Bundle production outputs
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
run: scripts/bundle/bundle.sh --clean
|
||||
|
||||
- name: Validate production bundle layout
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
run: scripts/github-workflows/check-builds.sh --skip-bundle-check
|
||||
|
||||
- name: Run documentation checks
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
run: scripts/test/test-docs.sh
|
||||
|
||||
- name: Run code tests
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
run: scripts/test/test.sh
|
||||
|
||||
- name: Remove models from publish tree
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
run: |
|
||||
rm -rf models
|
||||
if [ -e models ]; then
|
||||
echo "Failed to remove models from the publish tree." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Commit publish result
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
id: commit
|
||||
env:
|
||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||
SOURCE_SHA: ${{ steps.gate.outputs.source_sha }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
if [ -n "$(git ls-files -- models)" ]; then
|
||||
echo "models/ must not be present in the publish commit." >&2
|
||||
git ls-files -- models >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="$(tr -d '[:space:]' < plugins/cad/VERSION)"
|
||||
if target_base_sha="$(git rev-parse --verify "origin/$TARGET_BRANCH^{commit}" 2>/dev/null)"; then
|
||||
echo "Using existing $TARGET_BRANCH as publish parent: $target_base_sha"
|
||||
else
|
||||
target_base_sha="$(git rev-parse --verify "origin/main^{commit}")"
|
||||
echo "Target branch $TARGET_BRANCH does not exist; using origin/main as publish parent: $target_base_sha"
|
||||
fi
|
||||
release_base_sha="$(git rev-parse --verify "origin/main^{commit}")"
|
||||
previous_source_sha="$(scripts/release/check-publish-source.sh --target-ref origin/main --print-previous-source)"
|
||||
included_commits="$(git log --oneline --no-decorate "$previous_source_sha..$SOURCE_SHA" || true)"
|
||||
|
||||
message_file="$(mktemp)"
|
||||
{
|
||||
echo "Publish $version from $BASE_BRANCH to $TARGET_BRANCH"
|
||||
echo
|
||||
echo "Source ref: $BASE_BRANCH"
|
||||
echo "Source commit: $SOURCE_SHA"
|
||||
echo "Target branch: $TARGET_BRANCH"
|
||||
echo "Previous target: $target_base_sha"
|
||||
echo "Release base: $release_base_sha"
|
||||
echo "Previous source: $previous_source_sha"
|
||||
echo
|
||||
echo "Included commits since previous source:"
|
||||
if [ -n "$included_commits" ]; then
|
||||
echo "$included_commits"
|
||||
else
|
||||
echo "(no source commits beyond previous published source)"
|
||||
fi
|
||||
} > "$message_file"
|
||||
|
||||
tree_sha="$(git write-tree)"
|
||||
commit_parents=(-p "$target_base_sha")
|
||||
if [ "$SOURCE_SHA" != "$target_base_sha" ]; then
|
||||
commit_parents+=(-p "$SOURCE_SHA")
|
||||
fi
|
||||
publish_commit="$(git commit-tree "$tree_sha" "${commit_parents[@]}" -F "$message_file")"
|
||||
git reset --hard "$publish_commit"
|
||||
echo "publish_sha=$publish_commit" >> "$GITHUB_OUTPUT"
|
||||
echo "Created publish commit: $publish_commit"
|
||||
|
||||
- name: Push target branch
|
||||
if: steps.gate.outputs.should_publish == 'true'
|
||||
id: push
|
||||
env:
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
if [ "$TARGET_BRANCH" = "main" ]; then
|
||||
git push origin HEAD:main
|
||||
else
|
||||
git push --force-with-lease origin "HEAD:$TARGET_BRANCH"
|
||||
fi
|
||||
echo "published=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
upload-models:
|
||||
name: Upload Models
|
||||
needs:
|
||||
- release-pr
|
||||
- publish
|
||||
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
||||
uses: ./.github/workflows/upload-models.yml
|
||||
with:
|
||||
ref: ${{ needs.release-pr.outputs.source_sha }}
|
||||
secrets: inherit
|
||||
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
needs: publish
|
||||
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
||||
uses: ./.github/workflows/deploy-docs.yml
|
||||
with:
|
||||
ref: ${{ needs.publish.outputs.publish_sha }}
|
||||
secrets: inherit
|
||||
|
||||
deploy-viewer:
|
||||
name: Deploy Viewer
|
||||
needs: publish
|
||||
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
||||
uses: ./.github/workflows/deploy-viewer.yml
|
||||
with:
|
||||
ref: ${{ needs.publish.outputs.publish_sha }}
|
||||
secrets: inherit
|
||||
|
||||
tag-release:
|
||||
name: Tag and GitHub Release
|
||||
needs:
|
||||
- publish
|
||||
- upload-models
|
||||
- deploy-docs
|
||||
- deploy-viewer
|
||||
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out publish commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.publish.outputs.publish_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure tag push credentials
|
||||
env:
|
||||
PUBLISH_PUSH_TOKEN: ${{ secrets.PUBLISH_PUSH_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ -n "$PUBLISH_PUSH_TOKEN" ]; then
|
||||
git remote set-url origin "https://x-access-token:${PUBLISH_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
echo "Using PUBLISH_PUSH_TOKEN for tag push."
|
||||
else
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
echo "Using GITHUB_TOKEN for tag push."
|
||||
fi
|
||||
|
||||
- name: Create release tag and GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
INPUT_PUBLISH: ${{ inputs.publish }}
|
||||
INPUT_CREATE_RELEASE: ${{ inputs.create_release }}
|
||||
run: |
|
||||
args=(--target HEAD)
|
||||
if [ "$INPUT_PUBLISH" = "true" ]; then
|
||||
args+=(--publish)
|
||||
fi
|
||||
if [ "$INPUT_CREATE_RELEASE" != "true" ]; then
|
||||
args+=(--skip-release)
|
||||
fi
|
||||
scripts/release/publish-github-release.sh "${args[@]}"
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
concurrency:
|
||||
group: test-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Version Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Check release version
|
||||
run: |
|
||||
scripts/release/check-version.sh
|
||||
node scripts/release/sync-version.mjs --check
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check development layout
|
||||
run: scripts/github-workflows/check-symlinks.sh
|
||||
|
||||
- name: Set up dependencies
|
||||
uses: ./.github/actions/setup-deps
|
||||
|
||||
- name: Bundle production outputs
|
||||
run: scripts/bundle/bundle.sh --clean
|
||||
|
||||
- name: Check production bundle layout
|
||||
run: scripts/github-workflows/check-builds.sh --skip-bundle-check
|
||||
|
||||
- name: Run documentation checks
|
||||
run: scripts/test/test-docs.sh
|
||||
|
||||
- name: Run code tests
|
||||
run: scripts/test/test.sh
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Upload Models
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Branch, tag, or SHA to upload models from. Use a source branch; main does not contain models/.
|
||||
required: true
|
||||
default: develop
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: Branch, tag, or SHA to upload models from.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: upload-models
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload models catalog
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out upload ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: viewer/package-lock.json
|
||||
|
||||
- name: Install viewer dependencies
|
||||
run: npm ci --prefix viewer
|
||||
|
||||
- name: Upload viewer models catalog
|
||||
env:
|
||||
VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN: ${{ secrets.VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN }}
|
||||
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
|
||||
run: |
|
||||
if [ ! -d models ]; then
|
||||
echo "No models directory at this ref; upload from a source branch such as develop." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN:-}" ] && [ -z "${BLOB_READ_WRITE_TOKEN:-}" ]; then
|
||||
echo "Missing VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN or BLOB_READ_WRITE_TOKEN secret." >&2
|
||||
exit 1
|
||||
fi
|
||||
scripts/viewer/upload-viewer-models-catalog.sh "$PWD/models" --skip-existing --fetch-missing-lfs
|
||||
@@ -0,0 +1,39 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
.python-version
|
||||
|
||||
# Local environments and caches
|
||||
.venv
|
||||
.cache
|
||||
.agents/
|
||||
.vite
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
|
||||
# Production CAD Viewer runtime dist is committed on main. On dev,
|
||||
# skills/cad-viewer/scripts/viewer must stay a symlink to viewer/.
|
||||
!skills/cad-viewer/scripts/viewer/dist/
|
||||
!skills/cad-viewer/scripts/viewer/dist/**
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.swp
|
||||
*.step.glb.lock
|
||||
*.stp.glb.lock
|
||||
tmp/
|
||||
|
||||
# Local printer credentials
|
||||
/bambu-printers.json
|
||||
@@ -0,0 +1,2 @@
|
||||
[lfs]
|
||||
fetchexclude = assets/**,benchmarks/**,models/**,.agents/skills/**/assets/*.gif
|
||||
@@ -0,0 +1,188 @@
|
||||
# AGENTS.md
|
||||
|
||||
This repo is a workbench for CAD-related agent skills. Treat `skills/` as the
|
||||
product and `models/` as the shared fixture/artifact area.
|
||||
|
||||
## Branch And Layout First
|
||||
|
||||
Before changing code, branch from `develop`, not `main`; PRs should target `develop`.
|
||||
Do not start development work from `main`. The `develop` branch intentionally uses
|
||||
symlinks across generated runtime, viewer-local package, and plugin package
|
||||
paths. When a path is symlinked, follow the link and edit the source target.
|
||||
Use `main` as the production clone/release branch only. `main` is publish-only:
|
||||
do not open PRs to `main` or push it directly.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
Do not bump the canonical release version in `plugins/cad/VERSION` during
|
||||
normal development work. Ship releases only through the single `Release`
|
||||
GitHub Actions workflow, which handles the version bump, release PR, publish
|
||||
commit to `main`, models upload, web-app deploys, semver tag, and GitHub
|
||||
Release in one run.
|
||||
|
||||
When asked to publish, make, or ship a release, dispatch `Release` with its
|
||||
defaults: build from `develop` (`base_branch=develop`), publish to `main`
|
||||
(`target_branch=main`), and publish the GitHub Release (`publish=true`, not a
|
||||
draft). Never pick the semver bump yourself: if the request does not name
|
||||
patch, minor, major, or an exact version, ask which one before dispatching.
|
||||
Use `target_branch=build-test` only when the user explicitly asks to test
|
||||
CI/CD or build-pipeline changes — never by default and never as part of a
|
||||
requested release. Rerun `Release` with `set_version` pinned to the current
|
||||
version to resume a failed publish.
|
||||
|
||||
The standalone `Deploy Docs` and `Deploy Viewer` workflows redeploy the
|
||||
individual web apps from `main`, and the standalone `Upload Models` workflow
|
||||
uploads the `models/` catalog to Vercel Blob from `develop`, all without
|
||||
running a release. `main` is publish-only; pushing `develop` runs tests but
|
||||
never publishes. See the Releases section in `CONTRIBUTING.md` for the full
|
||||
flow, CI/CD-testing and resume options, and local/manual fallbacks.
|
||||
|
||||
## Repo Map
|
||||
|
||||
- `skills/`: agent skills and their references/scripts.
|
||||
- `plugins/`: versioned agent plugin packages that bundle repo skills.
|
||||
- `models/`: sample and durable CAD/robot-description fixtures.
|
||||
- `viewer/`: editable CAD Viewer source app.
|
||||
- `packages/cadjs`: shared JS CAD/render/runtime code, UI-framework agnostic.
|
||||
- `packages/implicitjs`: standalone JS implicit CAD model, shader render,
|
||||
snapshot, mesh sampling, and export runtime.
|
||||
- `packages/cadpy`: shared Python STEP/GLB/topology artifact code.
|
||||
- `packages/cadpy_metadata`: dependency-free Python metadata helpers vendored
|
||||
into generated URDF/SRDF/SDF skill runtimes.
|
||||
- `docs/`: documentation site.
|
||||
- `tests/`: root-owned test suites for skills, packages, viewer services, and
|
||||
repo-wide policy.
|
||||
- `scripts/`: durable repo commands grouped by purpose.
|
||||
|
||||
## Repo Rules
|
||||
|
||||
- Keep root guidance short. Put domain workflows, CLI details, and validation
|
||||
policy in the relevant `skills/<skill>/SKILL.md` or `references/` file.
|
||||
- Keep relevant Markdown docs current when changing behavior, commands, or repo
|
||||
layout, but do not bloat `AGENTS.md`; use it only for durable repo-level
|
||||
rules and pointers.
|
||||
- Read `CONTRIBUTING.md` before committing, rebasing, resolving generated-file
|
||||
conflicts, or bumping release versions.
|
||||
- Keep the primary local `develop` checkout in symlink layout with
|
||||
`scripts/dev/setup-symlinks.sh`. Do not auto-repair that layout from
|
||||
Codex or Claude Code startup hooks in linked worktrees.
|
||||
- Each skill must be self-contained and independent at runtime. A skill must
|
||||
not refer to or import or depend on code from another skill, from `skills/`
|
||||
root, or from repository-root modules. Do not add `skills/`, the repository
|
||||
root, or sibling skill directories to `sys.path`, `PYTHONPATH`, `NODE_PATH`,
|
||||
or similar runtime lookup paths. Shared runtime helpers must live under
|
||||
`packages/` as the source of truth and be vendored/generated from there into
|
||||
each consuming skill runtime; do not keep shared helper modules directly under
|
||||
`skills/`.
|
||||
- Edit the source reached by the `develop` symlink layout first, then regenerate
|
||||
explicit derived outputs when a production-output task requires it.
|
||||
- Write all test, sample, permanent, and generated CAD/robot-description
|
||||
artifacts under `models/`, including STEP/STP, STL, GLB, DXF, URDF, SRDF,
|
||||
SDF, and G-code outputs. Do not create ad hoc artifact directories elsewhere.
|
||||
- Reserve `scripts/` for durable repo commands. Do not write temporary,
|
||||
one-off, or local-only helper scripts there; use `tmp/` or `/tmp` instead.
|
||||
- Development symlinks mark generated or copied paths. If a file is under a
|
||||
symlinked runtime, viewer package, or plugin package path, edit the symlink
|
||||
target/source path instead of treating the copy as independent.
|
||||
- When source changes affect generated runtimes or plugin packages, refresh or
|
||||
check them with the master bundle wrapper, `scripts/bundle/bundle.sh`. Use
|
||||
lower-level bundle scripts only when debugging the wrapper itself.
|
||||
- `packages/cadjs` must stay reusable/non-React; app UI and workflow state
|
||||
belong in `viewer/`.
|
||||
- `packages/implicitjs` must stay reusable/non-React and independent of
|
||||
`packages/cadjs`; CAD Viewer and snapshot tools should consume its shared
|
||||
render/export APIs instead of duplicating implicit CAD logic.
|
||||
- `packages/cadpy` owns reusable Python artifact generation; skills should use
|
||||
bundled package code, not sibling skill imports.
|
||||
- Create lightweight shared Python packages under `packages/cadpy_*` when a
|
||||
helper should not inherit heavier package dependencies.
|
||||
- Use path-targeted search, validation, and `git status`; avoid broad scans over
|
||||
generated CAD/LFS artifacts unless the task requires them.
|
||||
- Treat `plugins/cad/VERSION` as the canonical release version. Do not hand-edit
|
||||
duplicate package, plugin, lockfile, or Python `pyproject.toml` versions;
|
||||
release preparation and `scripts/bundle/bundle.sh` stamp them from the
|
||||
canonical version.
|
||||
|
||||
## Environments
|
||||
|
||||
- Prefer `./.venv/bin/python` for CAD Python work.
|
||||
- Keep new branch checkouts and git worktrees lightweight by default. Do not
|
||||
copy `.venv/` or `models/` through `.worktreeinclude`; recreate `.venv/`
|
||||
inside the worktree only when Python dependencies are needed for the workflow.
|
||||
- In Codex or Claude Code worktrees, prefer the skill instructions and scripts
|
||||
under the current worktree's `skills/` directory over globally installed
|
||||
skill symlinks from another checkout.
|
||||
- If a worktree explicitly needs the development symlink layout, run
|
||||
`scripts/dev/setup-symlinks.sh --check` and then
|
||||
`scripts/dev/setup-symlinks.sh` intentionally in that worktree.
|
||||
- Hydrate `models/` only when the user asks for it or when the task targets
|
||||
specific files under `models/`. In a new worktree, make the relevant model
|
||||
paths real before using them, preferring the local Git LFS cache with
|
||||
`git lfs checkout <path>` or `git lfs checkout models`. Download missing LFS
|
||||
objects only when explicitly requested or required after confirming the local
|
||||
cache is missing them.
|
||||
- Install dependencies only for the workflow being changed.
|
||||
- Do not commit `.venv/`, `node_modules/`, caches, `tmp/`, local credentials, or
|
||||
printer config.
|
||||
|
||||
## Checks
|
||||
|
||||
Run the smallest path-targeted check that covers the change. Use broad wrappers
|
||||
when touching shared surfaces or before handoff:
|
||||
|
||||
- Code tests: `scripts/test/test.sh`
|
||||
- In GitHub Actions, `test.yml` checks the canonical release version as a
|
||||
separate non-blocking job; its test job verifies the `develop` symlink
|
||||
layout, bundles temporary production outputs, and runs docs and code tests
|
||||
against that bundle. `main` writes are validated by the `Release`
|
||||
workflow's publish job; GitHub branch settings should block PRs and direct
|
||||
pushes to `main`.
|
||||
- Focused test runners: `scripts/test/test-js.sh`,
|
||||
`scripts/test/test-docs.sh`, `scripts/test/test-python.sh`,
|
||||
`scripts/test/test-global.sh`
|
||||
- Development symlink layout: `scripts/dev/setup-symlinks.sh --check`
|
||||
- Canonical release version: `scripts/release/check-version.sh`
|
||||
- Generated runtime and plugin freshness: `scripts/bundle/bundle.sh --check`
|
||||
- CAD Viewer, `packages/cadjs`, or `packages/implicitjs`:
|
||||
`npm --prefix packages/cadjs test`, `npm --prefix packages/implicitjs test`,
|
||||
`npm --prefix viewer run test`, `npm --prefix viewer run build`
|
||||
- Docs site: `npm --prefix docs run check`
|
||||
- Targeted Python tests: `./.venv/bin/python -m unittest <changed test paths>`
|
||||
|
||||
When a task intentionally writes production outputs locally, run
|
||||
`scripts/bundle/bundle.sh`, rerun `scripts/bundle/bundle.sh --check`, and restore
|
||||
the development symlink layout afterward if you are continuing on `develop`.
|
||||
|
||||
## CAD Viewer
|
||||
|
||||
When reviewing repo fixtures in CAD Viewer, point the Viewer at the repo
|
||||
`models/` directory with an absolute `?dir=` path; keep any permanent or
|
||||
generated CAD/robot-description files in `models/` so the viewer catalog and
|
||||
artifacts stay in one place.
|
||||
|
||||
Start or reuse the Viewer through the `cad-viewer` skill launcher and use the
|
||||
base URL it prints. The launcher owns port selection, reuses a compatible live
|
||||
Viewer for the same worktree/branch, and uses the source app in Vite dev mode
|
||||
when the skill viewer path is a development symlink.
|
||||
|
||||
Run from `skills/cad-viewer`:
|
||||
|
||||
```bash
|
||||
npm --prefix scripts/viewer run agent:start -- --host 127.0.0.1 --shutdown-after 12h
|
||||
```
|
||||
|
||||
Every returned Viewer URL must include `?dir=<absolute-model-root>`, commonly
|
||||
`<repo>/models`, and `file=<path>` values must be relative to `?dir=`. Do not
|
||||
manually choose or increment ports, do not rely on session-storage `?dir=`
|
||||
fallbacks, and do not stop an existing Viewer server unless the user asks.
|
||||
|
||||
Packaged Viewer runtime and handoff details belong in the `cad-viewer` skill
|
||||
instructions. Treat packaged Viewer checks as generated-output checks and use
|
||||
the master bundle wrapper unless you are debugging a lower-level script.
|
||||
|
||||
## Git And LFS
|
||||
|
||||
CAD exchange files, generated render/topology assets, `assets/**`, and
|
||||
`benchmarks/**` may be LFS-tracked. Never disable LFS filters for `git add`,
|
||||
commits, or other object-writing operations. Local hooks live in `.githooks` and
|
||||
delegate build checks through `scripts/git-hooks/pre-commit`.
|
||||
@@ -0,0 +1 @@
|
||||
@AGENTS.md
|
||||
@@ -0,0 +1,343 @@
|
||||
# Contributing
|
||||
|
||||
This repository is a local workbench for CAD-related agent skills. Treat
|
||||
`skills/` as the product under test and `models/` as the shared
|
||||
fixture/artifact area.
|
||||
|
||||
## Local Checkout
|
||||
|
||||
For development, branch from `develop` and open PRs back to `develop`:
|
||||
|
||||
```bash
|
||||
git clone --branch develop https://github.com/earthtojake/text-to-cad.git
|
||||
cd text-to-cad
|
||||
git switch -c my-change
|
||||
```
|
||||
|
||||
Create the repo-local Python development environment:
|
||||
|
||||
```bash
|
||||
python3.12 -m venv .venv
|
||||
./.venv/bin/python -m pip install --upgrade pip
|
||||
./.venv/bin/python -m pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
`requirements-dev.txt` installs the source packages from `packages/` and the
|
||||
small set of Python extras mirrored from skill runtime requirements. This is
|
||||
the default Python environment for broad repo checks and source-checkout
|
||||
development. Skill-specific environments may install generated, skill-local
|
||||
package copies so they match production, but on `develop` you should still edit the
|
||||
source package under `packages/*`.
|
||||
|
||||
For CAD Viewer development:
|
||||
|
||||
```bash
|
||||
npm --prefix viewer install
|
||||
```
|
||||
|
||||
When running a tool manually, use that skill's interpreter:
|
||||
|
||||
```bash
|
||||
.venv/skills/cad/bin/python skills/cad/scripts/step --help
|
||||
.venv/skills/urdf/bin/python skills/urdf/scripts/urdf --help
|
||||
```
|
||||
|
||||
## Link Skills Into Your Agent
|
||||
|
||||
For local development, symlink this checkout's supported skill directories into
|
||||
your agent. Do not copy skill directories into your agent: symlinks keep edits
|
||||
in this checkout visible immediately.
|
||||
|
||||
Use the installer from the repository root:
|
||||
|
||||
```bash
|
||||
scripts/install/install-skills.sh --agent codex
|
||||
```
|
||||
|
||||
To see supported agents and resolved destination directories:
|
||||
|
||||
```bash
|
||||
scripts/install/install-skills.sh --list-agents
|
||||
```
|
||||
|
||||
The installer discovers each directory under `skills/` that contains
|
||||
`SKILL.md`, creates one symlink per skill, and leaves existing non-symlink paths
|
||||
untouched.
|
||||
|
||||
Supported local-development agent destinations:
|
||||
|
||||
| Agent flag | Destination |
|
||||
| ----------- | ------------------------------------------------- |
|
||||
| `codex` | `${CODEX_HOME:-$HOME/.codex}/skills` |
|
||||
| `claude` | `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills` |
|
||||
| `gemini` | `$HOME/.gemini/skills` |
|
||||
| `universal` | `${XDG_CONFIG_HOME:-$HOME/.config}/agents/skills` |
|
||||
| `project` | `.agents/skills` in this repository |
|
||||
|
||||
`claude-code`, `gemini-cli`, `agents`, and `repo` are accepted aliases. Use
|
||||
`--all` to install into every destination above, or repeat `--agent` for a
|
||||
smaller set:
|
||||
|
||||
```bash
|
||||
scripts/install/install-skills.sh --agent codex --agent claude
|
||||
```
|
||||
|
||||
Restart or reload the agent after linking so it rescans available skills.
|
||||
|
||||
To remove this checkout's skill links while testing provider behavior:
|
||||
|
||||
```bash
|
||||
scripts/install/uninstall-skills.sh --agent codex
|
||||
```
|
||||
|
||||
The uninstaller removes only symlinks that point back at this checkout and
|
||||
prunes empty destination directories unless `--keep-empty-dirs` is passed.
|
||||
|
||||
## Test From This Repository
|
||||
|
||||
Run development and test prompts from inside this repository instead of a
|
||||
separate project checkout. The skills assume this workbench layout while you are
|
||||
iterating: `models/` contains fixtures and generated CAD artifacts, `viewer/`
|
||||
contains the editable CAD Viewer source, and repo-relative validation commands
|
||||
live under `scripts/`.
|
||||
|
||||
Write test, sample, and durable CAD/robot-description artifacts under `models/`;
|
||||
do not create ad hoc artifact directories elsewhere. When you need a scratch
|
||||
project, create it under this checkout, for example:
|
||||
|
||||
```bash
|
||||
mkdir -p models/experiments/my-test
|
||||
```
|
||||
|
||||
Then start your agent with `/path/to/text-to-cad` as the working directory and
|
||||
ask it to write files under that scratch path. This keeps skill scripts,
|
||||
fixtures, generated sidecars, and Viewer links using the same repo-relative
|
||||
paths that CI and local checks expect.
|
||||
|
||||
## Source Boundaries
|
||||
|
||||
Each skill must be self-contained and independent when it is installed from a
|
||||
production branch: it must not import or depend on code from another skill or
|
||||
from repository-root modules at runtime.
|
||||
|
||||
The `develop` branch uses symlinks as a checkout layout convenience. Those symlinks
|
||||
point generated-output paths back to the canonical sources so contributors can
|
||||
edit one copy of shared code. They do not relax the runtime self-containment
|
||||
rule: production branches must be able to replace the symlinks with real copies
|
||||
that still run without `skills/`, the repository root, or sibling skill
|
||||
directories on `sys.path`, `PYTHONPATH`, `NODE_PATH`, or similar lookup paths.
|
||||
|
||||
Canonical source directories are:
|
||||
|
||||
- `skills/*` for skill instructions, references, and skill-owned scripts.
|
||||
- `viewer/` for CAD Viewer app and server source.
|
||||
- `packages/*` for shared runtime helpers that are copied into consuming skills
|
||||
for production.
|
||||
|
||||
On `develop`, paths such as `skills/cad-viewer/scripts/viewer`,
|
||||
`skills/*/scripts/packages/*`, `viewer/packages/*`, and `plugins/cad/skills/*`
|
||||
should be symlinks when they mirror root sources. Treat those paths as
|
||||
generated-output aliases, not separate source roots. Edit the canonical source
|
||||
path instead.
|
||||
|
||||
Production-output checks are intentionally centralized. Normal development
|
||||
should stay in the symlinked `develop` layout. When you specifically need to inspect
|
||||
production outputs locally, use a temporary checkout or rerun
|
||||
`scripts/dev/setup-symlinks.sh` afterward, then run:
|
||||
|
||||
```bash
|
||||
scripts/bundle/bundle.sh --clean
|
||||
scripts/bundle/bundle.sh --check
|
||||
```
|
||||
|
||||
Do not run lower-level bundle scripts as part of routine iteration; use the
|
||||
script-specific details in `scripts/README.md` only when you are debugging a
|
||||
production-output check.
|
||||
|
||||
## Branch Layouts
|
||||
|
||||
Open development PRs against `develop`, not `main`. The `develop` branch keeps
|
||||
generated copy targets as symlinks so the editable source remains under
|
||||
`skills/`, `viewer/`, and `packages/`:
|
||||
|
||||
```bash
|
||||
scripts/dev/setup-symlinks.sh
|
||||
scripts/dev/setup-symlinks.sh --check
|
||||
```
|
||||
|
||||
The `main` production branch must be installable from a plain checkout, so it
|
||||
contains generated production outputs instead of symlinks. `main` is
|
||||
publish-only: do not open PRs to `main` or push it directly. The `Test`
|
||||
workflow runs on `develop` and PRs to `develop`: it starts from the symlink
|
||||
layout, runs `scripts/bundle/bundle.sh --clean`, checks the production layout
|
||||
without rebuilding it, runs documentation checks, and runs the code tests
|
||||
against that generated output.
|
||||
|
||||
## Releases
|
||||
|
||||
Normal development PRs should not bump `plugins/cad/VERSION`; release versions
|
||||
are reserved for release PRs so the canonical repo version, Git tag, and GitHub
|
||||
Release describe the same production commit. PRs that do touch release state
|
||||
must keep `plugins/cad/VERSION` and derived version metadata valid; the `Test`
|
||||
workflow checks that metadata in a separate job so code tests still run when it
|
||||
is wrong.
|
||||
|
||||
### Shipping a release
|
||||
|
||||
Run the `Release` GitHub Actions workflow. Its defaults are the real-release
|
||||
settings — build from `develop` (`base_branch=develop`), publish to `main`
|
||||
(`target_branch=main`), and publish the GitHub Release (`publish=true`, not a
|
||||
draft) — and the input descriptions in `.github/workflows/release.yml` are
|
||||
authoritative. Choose the semver bump (`patch`, `minor`, or `major`) or an
|
||||
exact `set_version` deliberately for every release; if a release request does
|
||||
not specify one, confirm it rather than assuming:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml --ref develop -f bump=patch
|
||||
```
|
||||
|
||||
One run bumps `plugins/cad/VERSION` plus derived metadata on a
|
||||
`release/<version>` branch, opens a release PR, merges it into `develop`
|
||||
immediately, and then runs the publish, models-upload, web-app deploy, and
|
||||
tag/GitHub Release jobs in the same run. The release PR does not wait for its own CI checks; the
|
||||
publish job repeats the full bundle and test validation against exactly what
|
||||
ships. The publish job ships to `main` only when the
|
||||
source version is newer than `main` and the latest semver tag, and refuses
|
||||
sources that do not contain the previous publish source commit. It writes a
|
||||
generated production merge commit on top of the previous publish target with
|
||||
the release source as the second parent, which keeps `main` fast-forwardable
|
||||
while preserving source commits for release notes and contributor attribution.
|
||||
The GitHub Release is published immediately by default; set `publish=false` to
|
||||
review it as a draft first. Treat generated outputs as CI products, not edit
|
||||
targets.
|
||||
|
||||
### Testing CI/CD and build changes
|
||||
|
||||
Use `target_branch=build-test` only when explicitly testing changes to the
|
||||
CI/CD pipeline or production build outputs; it is never part of a normal
|
||||
release and should never be chosen by default. It rehearses the full publish
|
||||
flow without touching `main`, deploying, or creating a tag/release. Use
|
||||
`dry_run=true` to preview the version changes only, and `auto_merge=false` to
|
||||
stop after preparing the release PR.
|
||||
|
||||
### Resuming a failed publish
|
||||
|
||||
If a run fails partway — including after `main` has moved but before the semver
|
||||
tag exists — rerun `Release` with `set_version` pinned to the current version.
|
||||
When `develop` already contains that version, the workflow skips the release PR
|
||||
and proceeds straight to the publish jobs.
|
||||
|
||||
### Redeploying the web apps
|
||||
|
||||
The standalone `Deploy Docs` and `Deploy Viewer` workflows redeploy the
|
||||
individual web apps to Vercel production without running a release. They
|
||||
default to deploying `main` and expect a production-layout ref:
|
||||
|
||||
```bash
|
||||
gh workflow run deploy-docs.yml -f ref=main
|
||||
gh workflow run deploy-viewer.yml -f ref=main
|
||||
```
|
||||
|
||||
### Uploading new models
|
||||
|
||||
The standalone `Upload Models` workflow uploads the `models/` catalog and CAD
|
||||
Viewer assets to Vercel Blob without running a release or redeploying the
|
||||
viewer. It skips assets that already match the remote catalog and fetches only
|
||||
the missing Git LFS objects. Upload from a source ref — `main` does not
|
||||
contain `models/`:
|
||||
|
||||
```bash
|
||||
gh workflow run upload-models.yml -f ref=develop
|
||||
```
|
||||
|
||||
### Local and manual fallbacks
|
||||
|
||||
For local release preparation, use the same scripts the workflow calls:
|
||||
|
||||
```bash
|
||||
git fetch origin develop
|
||||
git fetch --tags origin
|
||||
scripts/release/bump-version.sh patch --no-commit
|
||||
node scripts/release/sync-version.mjs
|
||||
scripts/release/check-version.sh --incremented-from origin/main
|
||||
node scripts/release/sync-version.mjs --check
|
||||
```
|
||||
|
||||
`scripts/release/publish-github-release.sh` is the manual fallback for the tag
|
||||
and GitHub Release step. Unlike the `Release` workflow, the script creates a
|
||||
draft release unless `--publish` is passed.
|
||||
|
||||
### Repository settings
|
||||
|
||||
Configure GitHub branch settings/rulesets so `main` rejects PRs and direct
|
||||
pushes, leaving the `Release` workflow's publish job as the only writer. Enable
|
||||
repository tag rulesets for `[0-9]*.[0-9]*.[0-9]*` before publishing from
|
||||
`main`, and enable immutable releases once the production flow is trusted.
|
||||
|
||||
Production users should continue cloning `main`; developers should treat
|
||||
`develop` plus the `Release` workflow as the only route to `main`.
|
||||
|
||||
## Iteration Loop
|
||||
|
||||
1. Edit the relevant skill under `skills/<skill-name>/`.
|
||||
2. Keep skill instructions narrow and executable: say when the skill applies,
|
||||
what inputs it expects, what it produces, and how to validate the work.
|
||||
3. Prefer small files in `references/` and reusable scripts in `scripts/` over
|
||||
long inline instructions.
|
||||
4. Add or update focused fixtures, tests, or benchmark cases when skill behavior
|
||||
changes so regressions are measurable.
|
||||
5. Validate with the smallest relevant check before broad repo checks.
|
||||
|
||||
Generated artifacts should not become skill logic unless they are intentional
|
||||
fixtures. Prefer source files plus deterministic regeneration.
|
||||
|
||||
## Common Dev Checks
|
||||
|
||||
Use path-targeted validation. Common checks from the repo root:
|
||||
|
||||
```bash
|
||||
scripts/test/test.sh
|
||||
scripts/dev/setup-symlinks.sh --check
|
||||
scripts/release/check-version.sh
|
||||
npm --prefix viewer run test
|
||||
npm --prefix docs run check
|
||||
```
|
||||
|
||||
Use `AGENTS.md` or `scripts/README.md` for path-specific validation when you are
|
||||
working in a particular package, skill, plugin, docs site, or production-output
|
||||
path.
|
||||
|
||||
For targeted Python skill-script tests, run the relevant unittest files with the
|
||||
repo-local Python runtime, for example:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python -m unittest tests/python/skills/urdf/test_cli.py
|
||||
```
|
||||
|
||||
Python tests live under `tests/python/`, grouped by tested surface:
|
||||
`skills/<skill>`, `packages/<package>`, `viewer/<service>`, and `global`.
|
||||
|
||||
For fast CAD Viewer source iteration, run the root viewer app in dev mode. Do
|
||||
not run the generated viewer from the cad-viewer skill while modifying Viewer
|
||||
behavior:
|
||||
|
||||
```bash
|
||||
npm --prefix viewer run dev -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
Use the printed URL with an absolute `?dir=/path/to/root` and any absolute
|
||||
`?file=/path/to/model.step`. Do not assume a fixed dev port unless you pass
|
||||
Vite's standard `--port` flag. Packaged Viewer runtime checks are
|
||||
production-output checks; use `scripts/README.md` when you specifically need
|
||||
that path.
|
||||
|
||||
## Git Hygiene
|
||||
|
||||
Do not commit local environments, dependency folders, caches, or temp files such
|
||||
as `.venv/`, `node_modules/`, `.vite/`, `dist/`, `tmp/`, or local credentials.
|
||||
Generated runtime changes should come from the production-output workflow, not
|
||||
manual edits inside generated runtime folders.
|
||||
|
||||
CAD exchange files, generated render/topology assets, `assets/**`, and
|
||||
`benchmarks/**` may be LFS-tracked. Never disable LFS filters for `git add`,
|
||||
commits, or other object-writing operations.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 earthtojake
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,206 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="assets/text-to-cad-demo.gif" alt="Demo of the CAD skill generating and previewing CAD geometry" width="100%">
|
||||
|
||||
<br>
|
||||
|
||||
<pre>
|
||||
██████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
|
||||
██╔════╝██╔══██╗██╔══██╗ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
|
||||
██║ ███████║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
|
||||
██║ ██╔══██║██║ ██║ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
|
||||
╚██████╗██║ ██║██████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
|
||||
╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝
|
||||
</pre>
|
||||
|
||||
A skills library for CAD, robotics, and hardware design agents
|
||||
|
||||
[Docs](https://www.cadskills.xyz) | [Demo](https://demo.cadskills.xyz)
|
||||
|
||||
[](https://discord.gg/5FGB9DwJYU)
|
||||
[](https://github.com/earthtojake/text-to-cad/stargazers)
|
||||
[](LICENSE)
|
||||
[](https://x.com/earthtojake)
|
||||
[](skills/cad/requirements.txt)
|
||||
[](skills/cad/SKILL.md)
|
||||
[](skills/cad/SKILL.md)
|
||||
[](skills/cad/SKILL.md)
|
||||
[](skills/urdf/SKILL.md)
|
||||
[](skills/sdf/SKILL.md)
|
||||
[](skills/srdf/SKILL.md)
|
||||
|
||||
</div>
|
||||
|
||||
# CAD Skills
|
||||
|
||||
CAD Skills is a library of agent skills for generating, inspecting, sourcing,
|
||||
slicing, and handing off CAD and robot-description artifacts from local project
|
||||
files.
|
||||
|
||||
## 🧰 Skills
|
||||
|
||||
Install the library to give agents focused workflows for CAD, fabrication,
|
||||
robot description files, simulation, and local review.
|
||||
|
||||
| Skill | Summary | Source |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| CAD Router | Combines text-to-cad, SimpleCADAPI, CADAM, and the CADDesigner workflow, then enforces shared validation and a task-scoped Viewer handoff. | [skills/cad-router](skills/cad-router/SKILL.md) |
|
||||
| CAD | Creates and edits CAD models from plain-language or image requests, with STEP as the main output along with options to export to STL, 3MF and GLB. | [skills/cad](skills/cad/SKILL.md) |
|
||||
| CAD Viewer | Shows local browser previews for CAD, G-code, and robot files. | [skills/cad-viewer](skills/cad-viewer/SKILL.md) |
|
||||
| step.parts | Finds off-the-shelf STEP parts like screws, bearings, motors, and connectors. | [skills/step-parts](skills/step-parts/SKILL.md) |
|
||||
| DXF | Creates 2D DXF drawings like profiles, templates, gaskets, and cut layouts from Python sources or CAD geometry. | [skills/dxf](skills/dxf/SKILL.md) |
|
||||
| URDF | Writes robot structure files with links, joints, limits, inertials, and meshes. | [skills/urdf](skills/urdf/SKILL.md) |
|
||||
| SRDF | Adds MoveIt planning groups, end effectors, poses, and collision rules to a URDF. | [skills/srdf](skills/srdf/SKILL.md) |
|
||||
| SDF | Creates simulator models and worlds with frames, physics, sensors, and lights. | [skills/sdf](skills/sdf/SKILL.md) |
|
||||
| SendCutSend | Checks DXF and STEP files before upload to SendCutSend. | [skills/sendcutsend](skills/sendcutsend/SKILL.md) |
|
||||
| G-code | Slices supported mesh files into validated, printer-profiled FDM `.gcode` with real slicer CLIs. | [skills/gcode](skills/gcode/SKILL.md) |
|
||||
| Bambu Labs | Dry-runs, uploads, and cautiously starts local Bambu Lab print jobs from validated `.gcode`. | [skills/bambu-labs](skills/bambu-labs/SKILL.md) |
|
||||
| Implicit CAD | Creates browser-native implicit CAD models using GLSL signed-distance fields and CAD Viewer raymarch rendering. Experimental. | [skills/implicit-cad](skills/implicit-cad/SKILL.md) |
|
||||
|
||||
## 💻 Installation
|
||||
|
||||
For production use, install or clone from `main`; that branch contains the
|
||||
generated skill/plugin outputs needed by provider installers.
|
||||
|
||||
### Skills
|
||||
|
||||
Install CAD Skills with the Skills CLI:
|
||||
|
||||
```bash
|
||||
npx skills install earthtojake/text-to-cad
|
||||
```
|
||||
|
||||
This is the preferred installation path. It installs the individual skills
|
||||
directly for supported agents.
|
||||
|
||||
### Plugins
|
||||
|
||||
Provider-native plugin installs are also available for Codex and Claude Code:
|
||||
|
||||
```bash
|
||||
# Codex
|
||||
codex plugin marketplace add earthtojake/text-to-cad
|
||||
codex plugin add cad@text-to-cad
|
||||
```
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude plugin marketplace add earthtojake/text-to-cad
|
||||
claude plugin install cad@text-to-cad
|
||||
```
|
||||
|
||||
Restart your agent if newly installed skills do not appear. For local
|
||||
development, branch from `develop`, open PRs against `develop`, and use the symlink
|
||||
workflow in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%">
|
||||
<a href="./assets/text-to-cad-demo.gif">
|
||||
<img src="./assets/text-to-cad-demo.gif" alt="CAD skill demo showing generated geometry in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/cad/SKILL.md"><strong>CAD</strong></a>
|
||||
</td>
|
||||
<td width="33%">
|
||||
<a href="./assets/urdf-demo.gif">
|
||||
<img src="./assets/urdf-demo.gif" alt="URDF skill demo showing robot description output in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/urdf/SKILL.md"><strong>URDF</strong></a>
|
||||
</td>
|
||||
<td width="33%">
|
||||
<a href="./assets/srdf-moveit2-demo.gif">
|
||||
<img src="./assets/srdf-moveit2-demo.gif" alt="SRDF MoveIt2 skill demo showing inverse kinematics in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/srdf/SKILL.md"><strong>SRDF / MoveIt2</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🧪 Benchmarks
|
||||
|
||||
The repo stores heavyweight assets in `assets/**` and `benchmarks/**` through Git LFS and excludes those trees from default LFS pulls so lightweight clones do not fetch GIF assets. Benchmark markdown remains normal Git for readable diffs. To hydrate only the benchmark assets locally, run:
|
||||
|
||||
```bash
|
||||
git lfs pull --include="benchmarks/**"
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Target</th>
|
||||
<th>Prompt</th>
|
||||
<th>Output</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td><a href="benchmarks/01-rectangular-calibration-block.md">Rectangular calibration block with four holes</a></td>
|
||||
<td>Create a centered 100 x 60 x 20 mm block with four 8 mm vertical through-holes. Add only a 2 mm chamfer on the top outer perimeter.</td>
|
||||
<td><img src="benchmarks/benchmark_01_rectangular_calibration_block.gif" alt="Rectangular calibration block orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td><a href="benchmarks/02-circular-flange.md">Circular flange with bolt-hole pattern</a></td>
|
||||
<td>Create an 80 mm diameter, 10 mm thick circular flange with a 30 mm central through-bore. Add six 6 mm through-holes on a 60 mm bolt circle and fillet the outside circular edges.</td>
|
||||
<td><img src="benchmarks/benchmark_02_circular_flange.gif" alt="Circular flange orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td><a href="benchmarks/03-l-bracket.md">L-bracket with gussets and two hole directions</a></td>
|
||||
<td>Create an L-bracket from a base plate and rear vertical plate. Add vertical base holes, horizontal back-plate holes, two triangular gussets, and a filleted base/back transition.</td>
|
||||
<td><img src="benchmarks/benchmark_03_l_bracket.gif" alt="L-bracket orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td><a href="benchmarks/04-stepped-shaft-keyway.md">Stepped shaft with keyway</a></td>
|
||||
<td>Create a 120 mm shaft along X with 20/30/20 mm diameter stepped sections. Add end chamfers and a shallow rectangular keyway on top of the middle section.</td>
|
||||
<td><img src="benchmarks/benchmark_04_stepped_shaft_keyway.gif" alt="Stepped shaft orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td><a href="benchmarks/05-open-top-electronics-enclosure.md">Open-top electronics enclosure with bosses</a></td>
|
||||
<td>Create a hollow open-top enclosure with 3 mm walls and floor. Add four internal standoffs with centered blind holes and 2 mm outside vertical corner fillets.</td>
|
||||
<td><img src="benchmarks/benchmark_05_open_top_electronics_enclosure.gif" alt="Open-top electronics enclosure orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td><a href="benchmarks/06-clevis-bracket-lightening-cutouts.md">Aerospace-style clevis bracket with lightening cutouts</a></td>
|
||||
<td>Create a symmetric clevis bracket with a base plate, two rounded lugs, base mounting holes, and a horizontal lug bore. Add triangular lightening cutouts, reinforcing ribs, and rounded transitions.</td>
|
||||
<td><img src="benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.gif" alt="Clevis bracket orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td><a href="benchmarks/07-radial-engine-cylinder.md">Radial-engine-style cylinder with cooling fins</a></td>
|
||||
<td>Create a vertical engine-cylinder form with a central barrel, 12 cooling fins, a base flange, and a top cap. Add a 35 degree angled spark-plug boss with a coaxial through-hole.</td>
|
||||
<td><img src="benchmarks/benchmark_07_radial_engine_cylinder.gif" alt="Radial-engine-style cylinder orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td><a href="benchmarks/08-centrifugal-impeller.md">Centrifugal impeller with backward-curved blades</a></td>
|
||||
<td>Create a centrifugal impeller with a backplate, hub, and through-bore. Add 12 fused backward-curved blades sweeping about 45 degrees from root to tip.</td>
|
||||
<td><img src="benchmarks/benchmark_08_centrifugal_impeller.gif" alt="Centrifugal impeller orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td><a href="benchmarks/09-spiral-staircase.md">Spiral staircase with helical handrail</a></td>
|
||||
<td>Create a miniature spiral staircase with a central column, base disk, and 20 rising wedge treads. Add a one-revolution helical handrail and vertical balusters at the tread outer ends.</td>
|
||||
<td><img src="benchmarks/benchmark_09_spiral_staircase.gif" alt="Spiral staircase orbit gif" width="220"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td><a href="benchmarks/10-planetary-gear-stage.md">Simplified planetary gear stage</a></td>
|
||||
<td>Create a flat planetary gear assembly with separate sun, planet, ring, carrier, and pin bodies. Use simplified trapezoidal teeth and place three planets around the sun on a 42 mm radius circle.</td>
|
||||
<td><img src="benchmarks/benchmark_10_planetary_gear_stage.gif" alt="Planetary gear stage orbit gif" width="220"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 🛠️ Contributing
|
||||
|
||||
Development happens from the `develop` branch; open PRs against `develop`, not `main`.
|
||||
For local contribution workflow, skill linking, and validation guidance, see
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
@@ -0,0 +1,29 @@
|
||||
# 1. Rectangular Calibration Block With Four Holes
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid STEP model in millimeters. The part is a rectangular block, 100 mm long in X, 60 mm wide in Y, and 20 mm tall in Z. Center the block on the XY origin, with the bottom face at Z = 0.
|
||||
|
||||
Add four vertical through-holes, each 8 mm in diameter, located at X = +/-35 mm and Y = +/-20 mm.
|
||||
|
||||
Add a 2 mm chamfer to the top perimeter edges only. Do not chamfer the holes.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | Exactly one watertight solid |
|
||||
| Bounding box | 100 x 60 x 20 mm |
|
||||
| Block placement | Centered on XY origin; bottom at Z = 0 |
|
||||
| Hole count | Four through-holes |
|
||||
| Hole diameter | 8 mm each |
|
||||
| Hole axes | Parallel to Z |
|
||||
| Hole locations | X = +/-35 mm, Y = +/-20 mm |
|
||||
| Hole depth | Fully through the 20 mm block |
|
||||
| Chamfer | 2 mm chamfer on top outer perimeter only |
|
||||
| Negative check | No chamfers on holes; no extra bosses, slots, text, or decorative geometry |
|
||||
@@ -0,0 +1,32 @@
|
||||
# 2. Circular Flange With Bolt-Hole Pattern
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid circular flange as a STEP model in millimeters. The flange is a cylinder with an outside diameter of 80 mm and a thickness of 10 mm. Its axis is vertical along Z, with the bottom face at Z = 0 and the center at X = 0, Y = 0.
|
||||
|
||||
Add a central vertical through-bore with diameter 30 mm.
|
||||
|
||||
Add six equally spaced vertical through-holes, each 6 mm in diameter, on a 60 mm bolt-circle diameter.
|
||||
|
||||
Add a 1.5 mm fillet to the top and bottom outside circular edges.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | Exactly one watertight solid |
|
||||
| Outer diameter | 80 mm |
|
||||
| Thickness | 10 mm |
|
||||
| Central bore | Diameter 30 mm vertical through-hole |
|
||||
| Coaxiality | Central bore coaxial with flange outer cylinder |
|
||||
| Bolt-hole count | Six |
|
||||
| Bolt-hole diameter | 6 mm each |
|
||||
| Bolt circle | Hole centers lie on 60 mm diameter circle |
|
||||
| Angular spacing | 60 degrees between adjacent bolt holes |
|
||||
| Fillets | 1.5 mm fillets on top and bottom outside circular edges |
|
||||
| Negative check | No missing bolt holes; no misplaced non-circular holes; no extra patterns |
|
||||
@@ -0,0 +1,42 @@
|
||||
# 3. L-Bracket With Gussets and Two Hole Directions
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid L-bracket STEP model in millimeters.
|
||||
|
||||
The bracket has a horizontal base plate 80 mm long in X, 50 mm wide in Y, and 8 mm thick in Z. Center the base plate on the XY origin, with its bottom at Z = 0.
|
||||
|
||||
Add a vertical back plate along the rear long edge of the base. The back plate is 80 mm long in X, 8 mm thick in Y, and 50 mm tall in Z, rising from the top of the base plate. The back plate should sit along the rear edge at positive Y.
|
||||
|
||||
Add two vertical through-holes in the base plate, each 6 mm in diameter, located at X = +/-25 mm and Y = -10 mm.
|
||||
|
||||
Add two horizontal through-holes in the vertical plate, each 6 mm in diameter, located at X = +/-25 mm and Z = 30 mm, passing through the 8 mm thickness of the vertical plate.
|
||||
|
||||
Add two triangular gussets, each 8 mm thick in X, located at X = +/-20 mm. Each gusset should connect the base plate to the back plate with a right-triangle side profile 30 mm tall and 30 mm deep.
|
||||
|
||||
Add 2 mm fillets to the outside corner where the base and back plate meet.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | Exactly one fused watertight solid |
|
||||
| Base dimensions | 80 x 50 x 8 mm |
|
||||
| Back plate dimensions | 80 x 8 x 50 mm |
|
||||
| Back plate placement | Along positive-Y rear edge of base |
|
||||
| Base hole count | Two |
|
||||
| Base hole geometry | 6 mm diameter vertical through-holes |
|
||||
| Base hole locations | X = +/-25 mm, Y = -10 mm |
|
||||
| Back hole count | Two |
|
||||
| Back hole geometry | 6 mm diameter horizontal through-holes through Y thickness |
|
||||
| Back hole locations | X = +/-25 mm, Z = 30 mm |
|
||||
| Gusset count | Two |
|
||||
| Gusset geometry | Right-triangle side profile, approximately 30 x 30 mm |
|
||||
| Gusset placement | At X = +/-20 mm |
|
||||
| Fillets | 2 mm fillets at base/back outside transition |
|
||||
| Negative check | No floating gussets; no unfused plates; holes must actually cut through |
|
||||
@@ -0,0 +1,36 @@
|
||||
# 4. Stepped Shaft With Keyway
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid stepped shaft STEP model in millimeters.
|
||||
|
||||
The shaft axis runs along X. The total length is 120 mm. The left end center is at X = 0, Y = 0, Z = 0.
|
||||
|
||||
From X = 0 to X = 30, the shaft diameter is 20 mm. From X = 30 to X = 90, the shaft diameter is 30 mm. From X = 90 to X = 120, the shaft diameter is 20 mm.
|
||||
|
||||
Add a 1 mm chamfer to both end edges.
|
||||
|
||||
Add a rectangular keyway slot on the top of the 30 mm diameter middle section. The keyway is 6 mm wide in Y, 3 mm deep in Z, and runs from X = 40 to X = 80.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | Exactly one watertight solid |
|
||||
| Shaft axis | Along X, not Z |
|
||||
| Total length | 120 mm |
|
||||
| Left section | Diameter 20 mm, length 30 mm |
|
||||
| Middle section | Diameter 30 mm, length 60 mm |
|
||||
| Right section | Diameter 20 mm, length 30 mm |
|
||||
| Coaxiality | All three cylindrical sections share same X-axis |
|
||||
| End chamfers | 1 mm chamfer at both ends |
|
||||
| Keyway location | On top of middle section |
|
||||
| Keyway length | From X = 40 to X = 80 |
|
||||
| Keyway width | 6 mm in Y |
|
||||
| Keyway depth | 3 mm downward into shaft |
|
||||
| Negative check | Keyway should not pass through the shaft; shaft should not be vertical |
|
||||
@@ -0,0 +1,41 @@
|
||||
# 5. Open-Top Electronics Enclosure With Bosses
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid open-top electronics enclosure base as a STEP model in millimeters.
|
||||
|
||||
The outer shape is a rectangular box 100 mm long in X, 70 mm wide in Y, and 30 mm tall in Z. Center it on the XY origin, with the bottom face at Z = 0.
|
||||
|
||||
The enclosure is open at the top. The wall thickness is 3 mm and the bottom floor thickness is 3 mm.
|
||||
|
||||
Add four internal cylindrical standoffs rising from the inside floor. Each standoff has an outside diameter of 10 mm and a height of 12 mm above the inside floor. Place the standoffs at X = +/-35 mm and Y = +/-25 mm.
|
||||
|
||||
Add a centered blind hole in each standoff, 3 mm in diameter and 8 mm deep from the top of the standoff.
|
||||
|
||||
Add 2 mm radius fillets to the four outside vertical corners of the enclosure.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | One solid body representing the enclosure material |
|
||||
| Outer dimensions | 100 x 70 x 30 mm |
|
||||
| Placement | Centered on XY origin; bottom at Z = 0 |
|
||||
| Top condition | Open top, not capped |
|
||||
| Wall thickness | 3 mm |
|
||||
| Floor thickness | 3 mm |
|
||||
| Interior cavity | Correctly hollowed, with continuous walls and floor |
|
||||
| Standoff count | Four |
|
||||
| Standoff diameter | Diameter 10 mm |
|
||||
| Standoff height | 12 mm above inside floor |
|
||||
| Standoff locations | X = +/-35 mm, Y = +/-25 mm |
|
||||
| Blind hole count | Four |
|
||||
| Blind hole diameter | Diameter 3 mm |
|
||||
| Blind hole depth | 8 mm from standoff top |
|
||||
| Corner fillets | 2 mm radius on four outside vertical corners |
|
||||
| Negative check | Blind holes must not break through floor; standoffs must not float |
|
||||
@@ -0,0 +1,50 @@
|
||||
# 6. Aerospace-Style Clevis Bracket With Lightening Cutouts
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid aerospace-style clevis bracket as a STEP model in millimeters.
|
||||
|
||||
The part is symmetric about the XZ plane.
|
||||
|
||||
Start with a base plate 120 mm long in X, 60 mm wide in Y, and 10 mm thick in Z, centered on the XY origin, with bottom face at Z = 0.
|
||||
|
||||
Add two vertical clevis lugs rising from the top of the base near the center. Each lug is 18 mm thick in Y, 42 mm tall above the base, and extends 36 mm along X. The two lugs are separated by a 16 mm central gap in Y. The top of each lug has a semicircular rounded profile with radius 18 mm when viewed from the side.
|
||||
|
||||
Add a horizontal through-hole of diameter 14 mm through both lugs along the Y direction, centered at X = 0 and Z = 34 mm.
|
||||
|
||||
Add four base mounting holes, diameter 7 mm, through the base plate, located at X = +/-45 mm and Y = +/-20 mm.
|
||||
|
||||
Add two triangular lightening cutouts through the base web, one on each side of the clevis, each with rounded corners of radius 3 mm.
|
||||
|
||||
Add two diagonal reinforcing ribs from the base to the outer faces of the lugs, one on each side, thickness 6 mm.
|
||||
|
||||
Add 3 mm fillets to the base perimeter and 2 mm fillets at lug-to-base transitions.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | One watertight fused solid |
|
||||
| Symmetry | Symmetric about XZ plane |
|
||||
| Base dimensions | 120 x 60 x 10 mm |
|
||||
| Lug count | Two |
|
||||
| Lug thickness | 18 mm in Y each |
|
||||
| Lug height | 42 mm above base |
|
||||
| Lug X extent | 36 mm |
|
||||
| Lug gap | 16 mm central gap in Y |
|
||||
| Lug top profile | Semicircular/rounded top, radius approximately 18 mm |
|
||||
| Clevis hole | Diameter 14 mm horizontal through-hole |
|
||||
| Clevis hole axis | Along Y |
|
||||
| Clevis hole location | Centered at X = 0, Z = 34 mm |
|
||||
| Base mounting holes | Four 7 mm diameter vertical through-holes |
|
||||
| Base hole locations | X = +/-45 mm, Y = +/-20 mm |
|
||||
| Lightening cutouts | Two triangular cutouts through base/web region |
|
||||
| Cutout corner radius | Approximately 3 mm rounded corners |
|
||||
| Reinforcing ribs | Two diagonal ribs, 6 mm thick |
|
||||
| Fillets | 3 mm base perimeter; 2 mm lug-to-base |
|
||||
| Negative check | Clevis gap must be open; lugs/ribs must not be detached |
|
||||
@@ -0,0 +1,51 @@
|
||||
# 7. Radial-Engine-Style Cylinder With Cooling Fins
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid radial-engine-style cylinder as a STEP model in millimeters.
|
||||
|
||||
The main cylinder axis is vertical along Z and centered at the origin.
|
||||
|
||||
Create a central barrel with diameter 36 mm and height 70 mm, bottom at Z = 0.
|
||||
|
||||
Around the barrel, add 12 horizontal circular cooling fins. Each fin is 2 mm thick in Z, has outside diameter 62 mm, and is spaced every 5 mm from Z = 10 mm to Z = 65 mm.
|
||||
|
||||
Add a thicker base flange at the bottom, outside diameter 70 mm and thickness 8 mm, with six vertical mounting holes of diameter 5 mm on a 56 mm bolt circle.
|
||||
|
||||
Add a top cap cylinder, diameter 44 mm and height 8 mm, from Z = 70 mm to Z = 78 mm.
|
||||
|
||||
Add an angled spark-plug boss protruding from the side of the top cap. The boss is a cylinder of diameter 12 mm and length 24 mm, angled upward at 35 degrees from horizontal, with its axis pointing outward in the positive X direction.
|
||||
|
||||
Add a 5 mm diameter hole through the boss along its own axis.
|
||||
|
||||
Add small 1 mm fillets to the outer fin edges and base flange edges.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | One fused watertight solid |
|
||||
| Main axis | Vertical along Z |
|
||||
| Barrel | Diameter 36 mm, 70 mm tall |
|
||||
| Barrel placement | Centered at origin; bottom at Z = 0 |
|
||||
| Cooling fin count | 12 |
|
||||
| Fin diameter | Diameter 62 mm |
|
||||
| Fin thickness | 2 mm each |
|
||||
| Fin spacing | Nominally 5 mm from Z = 10 to Z = 65 |
|
||||
| Base flange | Diameter 70 mm, 8 mm thick |
|
||||
| Mounting hole count | Six |
|
||||
| Mounting hole diameter | Diameter 5 mm |
|
||||
| Mounting bolt circle | Diameter 56 mm |
|
||||
| Mounting hole spacing | 60 degrees between holes |
|
||||
| Top cap | Diameter 44 mm, 8 mm tall, Z = 70 to 78 mm |
|
||||
| Spark-plug boss | Diameter 12 mm cylinder, 24 mm long |
|
||||
| Boss orientation | Points outward in +X direction |
|
||||
| Boss angle | 35 degrees upward from horizontal |
|
||||
| Boss bore | Diameter 5 mm through-hole along boss axis |
|
||||
| Fillets | 1 mm outer fin-edge and flange-edge fillets |
|
||||
| Negative check | Boss bore must be coaxial with angled boss; fins must not be missing or merged into one thick cylinder |
|
||||
@@ -0,0 +1,43 @@
|
||||
# 8. Centrifugal Impeller With Backward-Curved Blades
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single solid centrifugal impeller as a STEP model in millimeters.
|
||||
|
||||
The impeller axis is vertical along Z and centered at the origin.
|
||||
|
||||
Add a circular backplate disk with outside diameter 90 mm and thickness 6 mm, with its bottom face at Z = 0.
|
||||
|
||||
Add a central hub cylinder on top of the backplate, diameter 26 mm and height 22 mm above the backplate.
|
||||
|
||||
Add a vertical through-bore of diameter 8 mm through the entire part.
|
||||
|
||||
Add 12 identical backward-curved blades on top of the backplate, equally spaced around the hub. Each blade begins at radius 18 mm and ends at radius 43 mm. Each blade is 3 mm thick, 16 mm tall above the backplate, and curves backward by approximately 45 degrees from root to tip. The blade tips should lean opposite the direction of rotation when viewed from above.
|
||||
|
||||
Add 1 mm fillets at the blade roots where they meet the backplate and hub. Add a 1.5 mm fillet to the top and bottom outer circular edges of the backplate.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Solid count | One fused watertight solid |
|
||||
| Backplate diameter | Diameter 90 mm |
|
||||
| Backplate thickness | 6 mm |
|
||||
| Hub diameter | Diameter 26 mm |
|
||||
| Hub height | 22 mm above backplate |
|
||||
| Through-bore | Diameter 8 mm through entire part |
|
||||
| Coaxiality | Backplate, hub, and bore share Z-axis |
|
||||
| Blade count | 12 |
|
||||
| Blade spacing | 30 degrees between adjacent blades |
|
||||
| Blade radial span | Approximately R18 to R43 |
|
||||
| Blade height | 16 mm above backplate |
|
||||
| Blade thickness | Approximately 3 mm |
|
||||
| Blade curvature | Approximately 45 degrees backward from root to tip |
|
||||
| Blade fusion | Blades joined to backplate and hub |
|
||||
| Fillets | 1 mm blade-root fillets; 1.5 mm backplate edge fillets |
|
||||
| Negative check | No straight radial-only blades unless curvature requirement is otherwise approximated; no floating blades |
|
||||
@@ -0,0 +1,46 @@
|
||||
# 9. Spiral Staircase With Helical Handrail
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a single STEP model of a miniature spiral staircase in millimeters.
|
||||
|
||||
The staircase is centered on the origin and rises along Z.
|
||||
|
||||
Add a central vertical column, diameter 14 mm and height 140 mm, with its bottom at Z = 0.
|
||||
|
||||
Add 20 identical wedge-shaped stair treads arranged helically around the column. Each tread is 4 mm thick, has an inner radius of 10 mm, an outer radius of 62 mm, and subtends 24 degrees in plan view. The first tread is at Z = 4 mm, and each subsequent tread rises by 6 mm and rotates by 18 degrees around Z.
|
||||
|
||||
Add a helical outer handrail tube of diameter 5 mm following radius 66 mm, starting at Z = 14 mm and ending at Z = 130 mm, making one full revolution around the staircase.
|
||||
|
||||
Add 20 vertical balusters, each diameter 3 mm, connecting the outer end of each tread to the handrail.
|
||||
|
||||
Add a circular base disk, diameter 90 mm and thickness 5 mm.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Main structure | Valid solid bodies or valid fused assembly |
|
||||
| Central column | Diameter 14 mm, 140 mm tall |
|
||||
| Column placement | Centered on Z-axis; bottom at Z = 0 |
|
||||
| Base disk | Diameter 90 mm, 5 mm thick |
|
||||
| Tread count | 20 |
|
||||
| Tread thickness | 4 mm |
|
||||
| Tread inner radius | R10 mm |
|
||||
| Tread outer radius | R62 mm |
|
||||
| Tread angular width | 24 degrees |
|
||||
| Tread rise | 6 mm per step |
|
||||
| Tread rotation | 18 degrees per step |
|
||||
| Helical progression | Treads rise and rotate together, not merely radial-patterned flat |
|
||||
| Handrail | Diameter 5 mm tube at R66 mm |
|
||||
| Handrail vertical range | Z = 14 mm to Z = 130 mm |
|
||||
| Handrail revolution | Approximately one full 360-degree turn |
|
||||
| Baluster count | 20 |
|
||||
| Baluster diameter | Diameter 3 mm |
|
||||
| Baluster relation | Each connects tread outer region to handrail |
|
||||
| Negative check | No missing rail; no flat circular stair pattern with no rise |
|
||||
@@ -0,0 +1,48 @@
|
||||
# 10. Simplified Planetary Gear Stage
|
||||
|
||||

|
||||
|
||||
## Prompt
|
||||
|
||||
Create a visually clear simplified planetary gear assembly as a STEP model in millimeters.
|
||||
|
||||
The assembly lies flat in the XY plane with gear axes along Z. Use separate solid bodies for the sun gear, three planet gears, ring gear, carrier plate, and three planet pins.
|
||||
|
||||
All gears are 8 mm thick. Use simplified straight-sided trapezoidal teeth rather than true involute teeth.
|
||||
|
||||
The sun gear has 24 external teeth, pitch diameter 48 mm, root diameter 42 mm, and outside diameter 54 mm.
|
||||
|
||||
The three planet gears each have 18 external teeth, pitch diameter 36 mm, root diameter 31 mm, and outside diameter 41 mm. Place the planet gear centers on a 42 mm radius circle, equally spaced every 120 degrees.
|
||||
|
||||
The ring gear is concentric with the sun gear, has 60 internal teeth, internal pitch diameter 120 mm, internal root diameter 126 mm, internal tooth-tip diameter 114 mm, and outside diameter 140 mm.
|
||||
|
||||
Add a thin circular carrier plate below the gears, diameter 105 mm and thickness 4 mm, located from Z = -5 mm to Z = -1 mm.
|
||||
|
||||
Add three vertical planet pins, each diameter 6 mm and height 14 mm, centered under the planet gears.
|
||||
|
||||
Add a central sun bore of diameter 10 mm.
|
||||
|
||||
Export as a STEP file.
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Expected result |
|
||||
| --- | --- |
|
||||
| STEP import | File imports successfully |
|
||||
| Body count | Separate bodies for sun, 3 planets, ring, carrier, and 3 pins |
|
||||
| Gear axes | All gear axes parallel to Z |
|
||||
| Gear thickness | 8 mm for sun, planet, and ring gears |
|
||||
| Sun tooth count | 24 external teeth |
|
||||
| Sun diameters | Pitch approximately 48 mm diameter; root 42 mm diameter; outside 54 mm diameter |
|
||||
| Sun bore | 10 mm diameter central through-bore |
|
||||
| Planet count | Three planet gears |
|
||||
| Planet tooth count | 18 teeth each |
|
||||
| Planet diameters | Pitch approximately 36 mm diameter; root 31 mm diameter; outside 41 mm diameter |
|
||||
| Planet placement | Centers on R42 mm circle |
|
||||
| Planet spacing | 120 degrees apart |
|
||||
| Ring gear | 140 mm outside diameter |
|
||||
| Ring internal teeth | 60 internal teeth |
|
||||
| Ring diameters | Internal pitch approximately 120 mm diameter; internal root 126 mm diameter; tooth-tip 114 mm diameter |
|
||||
| Carrier plate | Diameter 105 mm, 4 mm thick, Z = -5 to -1 mm |
|
||||
| Pins | Three 6 mm diameter pins aligned with planet centers |
|
||||
| Negative check | Ring teeth must face inward; planet gears should not be fused into sun or ring |
|
||||
@@ -0,0 +1,14 @@
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build and cache output
|
||||
dist/
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Local env
|
||||
.env*.local
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
".agents/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"node_modules/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { NextConfig } from "next";
|
||||
import path from "node:path";
|
||||
|
||||
const repoRoot = path.resolve(process.cwd(), "..");
|
||||
const cadJsPackageRoot = path.join(repoRoot, "packages/cadjs/src");
|
||||
const docsThreeRoot = "./node_modules/three";
|
||||
const docsThreeExamplesRoot = "./node_modules/three/examples";
|
||||
const docsGltfLoader =
|
||||
"./node_modules/three/examples/jsm/loaders/GLTFLoader.js";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
allowedDevOrigins: ["127.0.0.1"],
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
hostname: "www.skills.sh",
|
||||
protocol: "https",
|
||||
},
|
||||
],
|
||||
},
|
||||
turbopack: {
|
||||
root: repoRoot,
|
||||
resolveAlias: {
|
||||
"cadjs": cadJsPackageRoot,
|
||||
three: docsThreeRoot,
|
||||
"three/examples": docsThreeExamplesRoot,
|
||||
"three/examples/jsm/loaders/GLTFLoader.js": docsGltfLoader,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "cad-skills-docs",
|
||||
"version": "0.3.9",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"check": "node scripts/check-hero-step-assets.mjs && npm run lint && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next": "16.2.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.7.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"three": "^0.160.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": "^8.5.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/three": "^0.160.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 470 KiB |
@@ -0,0 +1,204 @@
|
||||
const Z_AXIS = [0, 0, 1];
|
||||
const ORIGIN = [0, 0, 0];
|
||||
const PLANETS = ["planet1", "planet2", "planet3"];
|
||||
const PINS = ["pin1", "pin2", "pin3"];
|
||||
const SUN_TEETH = 24;
|
||||
const PLANET_TEETH = 18;
|
||||
const RING_TEETH = 60;
|
||||
// Fixed-ring planetary kinematics: carrier follows the sun by Ns / (Ns + Nr).
|
||||
const CARRIER_RATIO_FIXED_RING = SUN_TEETH / (SUN_TEETH + RING_TEETH);
|
||||
// 3.5 sun revolutions returns the carrier to 360 deg and each gear to an equivalent tooth phase.
|
||||
const FULL_MESH_CYCLE_DEG = 1260;
|
||||
|
||||
let orbitGuide = null;
|
||||
|
||||
function finite(value, fallback = 0) {
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : fallback;
|
||||
}
|
||||
|
||||
function radialUnit(feature) {
|
||||
const center = Array.isArray(feature?.center) ? feature.center : ORIGIN;
|
||||
const length = Math.hypot(center[0] || 0, center[1] || 0);
|
||||
if (length <= 1e-6) {
|
||||
return [1, 0, 0];
|
||||
}
|
||||
return [(center[0] || 0) / length, (center[1] || 0) / length, 0];
|
||||
}
|
||||
|
||||
function scaled(vector, scale) {
|
||||
return vector.map((value) => value * scale);
|
||||
}
|
||||
|
||||
function createOrbitGuide(THREE) {
|
||||
const radius = 42;
|
||||
const points = [];
|
||||
for (let index = 0; index < 144; index += 1) {
|
||||
const angle = (index / 144) * Math.PI * 2;
|
||||
points.push(new THREE.Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius, 4));
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: 0x94a3b8,
|
||||
transparent: true,
|
||||
opacity: 0.34,
|
||||
depthWrite: false
|
||||
});
|
||||
const line = new THREE.LineLoop(geometry, material);
|
||||
line.name = "planetary-gear-orbit-guide";
|
||||
line.renderOrder = 30;
|
||||
return line;
|
||||
}
|
||||
|
||||
const planetaryGearAssemblyStep = {
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
units: {
|
||||
length: "mm",
|
||||
angle: "deg",
|
||||
time: "s"
|
||||
},
|
||||
features: {
|
||||
carrier: { ref: "#o1.1.1", label: "Carrier plate", description: "The plate that carries planet pins around the sun axis." },
|
||||
ring: { ref: "#o1.2.1", label: "Ring gear", description: "Stationary internal gear, visually faded or hidden by controls." },
|
||||
sun: { ref: "#o1.3.1", label: "Sun gear", description: "Central driven gear." },
|
||||
planet1: { ref: "#o1.4.1", label: "Planet gear 1" },
|
||||
pin1: { ref: "#o1.5.1", label: "Planet pin 1" },
|
||||
planet2: { ref: "#o1.6.1", label: "Planet gear 2" },
|
||||
pin2: { ref: "#o1.7.1", label: "Planet pin 2" },
|
||||
planet3: { ref: "#o1.8.1", label: "Planet gear 3" },
|
||||
pin3: { ref: "#o1.9.1", label: "Planet pin 3" }
|
||||
},
|
||||
parameters: {
|
||||
drive: { type: "number", label: "Drive", description: "Sun gear input angle across one closed mesh cycle.", default: 0, min: 0, max: FULL_MESH_CYCLE_DEG, step: 1, unit: "deg" },
|
||||
explode: { type: "number", label: "Explode", description: "Radially separates meshing features for inspection.", default: 0, min: 0, max: 1, step: 0.01 },
|
||||
ringVisible: { type: "boolean", label: "Ring gear", default: true },
|
||||
orbitGuides: { type: "boolean", label: "Orbit guide", default: false },
|
||||
highlightMeshing: { type: "boolean", label: "Mesh highlight", default: false },
|
||||
viewMode: {
|
||||
type: "select",
|
||||
label: "View",
|
||||
default: "mesh",
|
||||
options: [
|
||||
{ value: "mesh", label: "Mesh study" },
|
||||
{ value: "cutaway", label: "Cutaway" },
|
||||
{ value: "carrier", label: "Carrier focus" }
|
||||
]
|
||||
}
|
||||
},
|
||||
animations: {
|
||||
meshCycle: {
|
||||
label: "Mesh cycle",
|
||||
duration: 6,
|
||||
loop: true,
|
||||
update({ cycle, set }) {
|
||||
set("drive", (cycle % 1) * FULL_MESH_CYCLE_DEG);
|
||||
}
|
||||
},
|
||||
inspectExplode: {
|
||||
label: "Explode inspect",
|
||||
duration: 5,
|
||||
loop: true,
|
||||
update({ progress, set }) {
|
||||
set("drive", progress * FULL_MESH_CYCLE_DEG);
|
||||
set("explode", Math.sin(progress * Math.PI));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup({ THREE, modelGroup, cleanup }) {
|
||||
if (!THREE || !modelGroup) {
|
||||
return;
|
||||
}
|
||||
orbitGuide = createOrbitGuide(THREE);
|
||||
modelGroup.add(orbitGuide);
|
||||
cleanup(() => {
|
||||
modelGroup.remove(orbitGuide);
|
||||
orbitGuide.geometry?.dispose?.();
|
||||
orbitGuide.material?.dispose?.();
|
||||
orbitGuide = null;
|
||||
});
|
||||
},
|
||||
|
||||
update({ params, features, effects, time }) {
|
||||
const drive = finite(params.drive);
|
||||
const explode = finite(params.explode);
|
||||
const sunAngle = drive;
|
||||
const carrierAngle = sunAngle * CARRIER_RATIO_FIXED_RING;
|
||||
const planetSpinRelativeToCarrier = -(SUN_TEETH / PLANET_TEETH) * (sunAngle - carrierAngle);
|
||||
const lift = explode * 7;
|
||||
const cutaway = params.viewMode === "cutaway";
|
||||
const carrierFocus = params.viewMode === "carrier";
|
||||
const highlight = params.highlightMeshing === true &&
|
||||
(!time.playing || (time.progress > 0.45 && time.progress < 0.6));
|
||||
|
||||
if (orbitGuide) {
|
||||
orbitGuide.visible = params.orbitGuides !== false;
|
||||
orbitGuide.rotation.z = (carrierAngle * Math.PI) / 180;
|
||||
}
|
||||
|
||||
effects.visible("ring", params.ringVisible !== false);
|
||||
effects.style("ring", {
|
||||
opacity: cutaway ? 0.22 : 1,
|
||||
edgeOpacity: cutaway ? 0.38 : 1
|
||||
});
|
||||
effects.style("sun", {
|
||||
emissive: highlight ? "#7c2d12" : "",
|
||||
emissiveIntensity: highlight ? 0.32 : 0
|
||||
});
|
||||
effects.style(PLANETS, {
|
||||
emissive: highlight ? "#075985" : "",
|
||||
emissiveIntensity: highlight ? 0.22 : 0
|
||||
});
|
||||
effects.style(PINS, {
|
||||
opacity: 1
|
||||
});
|
||||
effects.style("carrier", {
|
||||
opacity: 1,
|
||||
emissive: carrierFocus ? "#14532d" : "",
|
||||
emissiveIntensity: carrierFocus ? 0.2 : 0
|
||||
});
|
||||
|
||||
if (highlight) {
|
||||
effects.highlight(["sun", ...PLANETS], true);
|
||||
}
|
||||
|
||||
effects.transform("sun", {
|
||||
transforms: [
|
||||
{ rotate: { axis: Z_AXIS, origin: features.sun.center, angleDeg: sunAngle } },
|
||||
{ translate: [0, 0, lift] }
|
||||
]
|
||||
});
|
||||
effects.transform("carrier", {
|
||||
transforms: [
|
||||
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } },
|
||||
{ translate: [0, 0, -explode * 4] }
|
||||
]
|
||||
});
|
||||
|
||||
for (const planetId of PLANETS) {
|
||||
const radial = radialUnit(features[planetId]);
|
||||
effects.transform(planetId, {
|
||||
transforms: [
|
||||
{ rotate: { axis: Z_AXIS, origin: features[planetId].center, angleDeg: planetSpinRelativeToCarrier } },
|
||||
{ translate: scaled(radial, explode * 16) },
|
||||
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
for (const pinId of PINS) {
|
||||
const radial = radialUnit(features[pinId]);
|
||||
effects.transform(pinId, {
|
||||
transforms: [
|
||||
{ translate: scaled(radial, explode * 16) },
|
||||
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } },
|
||||
{ translate: [0, 0, -explode * 4] }
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default planetaryGearAssemblyStep;
|
||||
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 308 KiB |
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "workbench-transparent",
|
||||
"settings": {
|
||||
"materials": {
|
||||
"defaultColor": "#d7dce0",
|
||||
"fillColors": ["#d7dce0", "#cdd3d8", "#e4e7ea", "#bfc7ce"],
|
||||
"overrideSourceColors": true,
|
||||
"tintMode": "blend",
|
||||
"tintStrength": 0,
|
||||
"saturation": 0,
|
||||
"contrast": 1,
|
||||
"brightness": 1,
|
||||
"roughness": 0.92,
|
||||
"metalness": 0,
|
||||
"clearcoat": 0,
|
||||
"clearcoatRoughness": 0.9,
|
||||
"opacity": 1,
|
||||
"envMapIntensity": 0,
|
||||
"emissiveIntensity": 0
|
||||
},
|
||||
"edges": {
|
||||
"enabled": true,
|
||||
"contrastMode": "manual",
|
||||
"source": "topology",
|
||||
"topologyFilter": "feature",
|
||||
"topologyMinRelevance": 1,
|
||||
"color": "#111827",
|
||||
"opacity": 0.96,
|
||||
"thickness": 1,
|
||||
"silhouette": true,
|
||||
"silhouetteScale": 0.0025
|
||||
},
|
||||
"background": {
|
||||
"type": "transparent",
|
||||
"solidColor": "#f7f7f7",
|
||||
"linearStart": "#f7f7f7",
|
||||
"linearEnd": "#eeeeee",
|
||||
"linearAngle": 0,
|
||||
"radialInner": "#ffffff",
|
||||
"radialOuter": "#eeeeee"
|
||||
},
|
||||
"floor": {
|
||||
"mode": "none",
|
||||
"color": "#f7f7f7",
|
||||
"roughness": 1,
|
||||
"reflectivity": 0,
|
||||
"shadowOpacity": 0,
|
||||
"horizonBlend": 0
|
||||
},
|
||||
"environment": {
|
||||
"enabled": false,
|
||||
"presetId": "studio-hdri-43",
|
||||
"intensity": 0,
|
||||
"rotationY": 0,
|
||||
"useAsBackground": false
|
||||
},
|
||||
"lighting": {
|
||||
"toneMappingExposure": 1,
|
||||
"directional": {
|
||||
"enabled": true,
|
||||
"color": "#ffffff",
|
||||
"intensity": 0.75,
|
||||
"position": {
|
||||
"x": -160,
|
||||
"y": 220,
|
||||
"z": 260
|
||||
}
|
||||
},
|
||||
"spot": {
|
||||
"enabled": false,
|
||||
"color": "#ffffff",
|
||||
"intensity": 0,
|
||||
"angle": 0.74,
|
||||
"distance": 0,
|
||||
"position": {
|
||||
"x": 190,
|
||||
"y": 210,
|
||||
"z": 170
|
||||
}
|
||||
},
|
||||
"point": {
|
||||
"enabled": false,
|
||||
"color": "#ffffff",
|
||||
"intensity": 0,
|
||||
"distance": 0,
|
||||
"position": {
|
||||
"x": -240,
|
||||
"y": 110,
|
||||
"z": -210
|
||||
}
|
||||
},
|
||||
"ambient": {
|
||||
"enabled": true,
|
||||
"color": "#ffffff",
|
||||
"intensity": 0.82
|
||||
},
|
||||
"hemisphere": {
|
||||
"enabled": true,
|
||||
"skyColor": "#ffffff",
|
||||
"groundColor": "#e5e7eb",
|
||||
"intensity": 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { STEP_TOPOLOGY_SCHEMA_VERSION } from "../../packages/cadjs/src/common/stepTopology.mjs";
|
||||
|
||||
const docsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const heroGlbPath = path.join(docsRoot, "public/hero/planetary_gear_assembly.step.glb");
|
||||
|
||||
function readGlbJson(filePath) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
assert.equal(buffer.toString("ascii", 0, 4), "glTF", `${filePath} is not a GLB`);
|
||||
const jsonLength = buffer.readUInt32LE(12);
|
||||
const jsonChunkType = buffer.toString("ascii", 16, 20);
|
||||
assert.equal(jsonChunkType, "JSON", `${filePath} is missing its JSON chunk`);
|
||||
return JSON.parse(buffer.subarray(20, 20 + jsonLength).toString("utf8"));
|
||||
}
|
||||
|
||||
const gltf = readGlbJson(heroGlbPath);
|
||||
const topology = gltf.extensions?.STEP_topology;
|
||||
|
||||
assert.ok(topology, "Hero STEP GLB is missing the STEP_topology extension");
|
||||
assert.equal(
|
||||
topology.schemaVersion,
|
||||
STEP_TOPOLOGY_SCHEMA_VERSION,
|
||||
`Hero STEP GLB uses STEP_topology schemaVersion ${topology.schemaVersion}; expected ${STEP_TOPOLOGY_SCHEMA_VERSION}`
|
||||
);
|
||||
|
||||
console.log("Hero STEP asset schema is current.");
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function GET(request: Request) {
|
||||
return NextResponse.redirect(
|
||||
new URL("/hero/planetary_gear_assembly.step.js", request.url),
|
||||
307
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function GET(request: Request) {
|
||||
return NextResponse.redirect(
|
||||
new URL("/hero/planetary_gear_assembly.step.glb", request.url),
|
||||
307
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap");
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--background: hsl(218 27% 94%);
|
||||
--foreground: hsl(220 16% 22%);
|
||||
--card: hsl(218 27% 89%);
|
||||
--card-foreground: hsl(220 16% 22%);
|
||||
--popover: hsl(218 27% 89%);
|
||||
--popover-foreground: hsl(220 16% 22%);
|
||||
--primary: hsl(213 32% 40%);
|
||||
--primary-foreground: hsl(0 0% 100%);
|
||||
--secondary: hsl(219 28% 84%);
|
||||
--secondary-foreground: hsl(220 16% 22%);
|
||||
--muted: hsl(219 28% 84%);
|
||||
--muted-foreground: hsl(220 17% 28%);
|
||||
--accent: hsl(213 20% 86%);
|
||||
--accent-foreground: hsl(213 32% 40%);
|
||||
--destructive: hsl(355 60% 45%);
|
||||
--destructive-foreground: hsl(0 0% 100%);
|
||||
--border: hsl(219 18% 72%);
|
||||
--input: hsl(219 18% 72%);
|
||||
--ring: hsl(213 32% 40%);
|
||||
--chart-1: hsl(213 32% 44%);
|
||||
--chart-2: hsl(210 34% 42%);
|
||||
--chart-3: hsl(92 30% 36%);
|
||||
--chart-4: hsl(40 70% 35%);
|
||||
--chart-5: hsl(311 28% 42%);
|
||||
--sidebar-background: hsl(218 27% 89%);
|
||||
--sidebar: var(--sidebar-background);
|
||||
--sidebar-foreground: hsl(220 16% 22%);
|
||||
--sidebar-primary: hsl(213 32% 40%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(213 20% 86%);
|
||||
--sidebar-accent-foreground: hsl(213 32% 40%);
|
||||
--sidebar-border: hsl(219 18% 72%);
|
||||
--sidebar-ring: hsl(213 32% 40%);
|
||||
|
||||
--smui-frost-1: 176 30% 35%;
|
||||
--smui-frost-2: 193 40% 38%;
|
||||
--smui-frost-3: 210 34% 40%;
|
||||
--smui-frost-4: 213 32% 36%;
|
||||
--smui-red: 355 55% 44%;
|
||||
--smui-orange: 14 55% 44%;
|
||||
--smui-yellow: 40 70% 34%;
|
||||
--smui-green: 92 35% 34%;
|
||||
--smui-purple: 311 28% 40%;
|
||||
--smui-surface-0: 218 27% 94%;
|
||||
--smui-surface-1: 218 27% 89%;
|
||||
--smui-surface-2: 219 28% 84%;
|
||||
--smui-surface-3: 219 20% 76%;
|
||||
--smui-border-hover: 220 17% 50%;
|
||||
|
||||
--font-sans: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-serif: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-mono: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-heading: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 0;
|
||||
--shadow-blur: 0;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-2xs: none;
|
||||
--shadow-xs: none;
|
||||
--shadow-sm: none;
|
||||
--shadow: none;
|
||||
--shadow-md: none;
|
||||
--shadow-lg: none;
|
||||
--shadow-xl: none;
|
||||
--shadow-2xl: none;
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--text-label: 11px;
|
||||
--text-ui: 13px;
|
||||
--text-heading: 22px;
|
||||
--text-stat: 26px;
|
||||
--text-hero: 42px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--background: hsl(213 16% 12%);
|
||||
--foreground: hsl(213 27% 88%);
|
||||
--card: hsl(217 16% 15.5%);
|
||||
--card-foreground: hsl(213 27% 88%);
|
||||
--popover: hsl(217 16% 15.5%);
|
||||
--popover-foreground: hsl(213 27% 88%);
|
||||
--primary: hsl(193 44% 67%);
|
||||
--primary-foreground: hsl(213 16% 12%);
|
||||
--secondary: hsl(216 15% 19%);
|
||||
--secondary-foreground: hsl(213 27% 88%);
|
||||
--muted: hsl(216 15% 19%);
|
||||
--muted-foreground: hsl(213 14% 65%);
|
||||
--accent: hsl(193 10% 16%);
|
||||
--accent-foreground: hsl(193 44% 67%);
|
||||
--destructive: hsl(355 52% 64%);
|
||||
--destructive-foreground: hsl(219 28% 94%);
|
||||
--border: hsl(217 17% 28%);
|
||||
--input: hsl(217 17% 28%);
|
||||
--ring: hsl(193 44% 67%);
|
||||
--chart-1: hsl(193 44% 67%);
|
||||
--chart-2: hsl(213 32% 52%);
|
||||
--chart-3: hsl(92 28% 65%);
|
||||
--chart-4: hsl(40 71% 73%);
|
||||
--chart-5: hsl(311 24% 63%);
|
||||
--sidebar-background: hsl(217 16% 15.5%);
|
||||
--sidebar: var(--sidebar-background);
|
||||
--sidebar-foreground: hsl(213 27% 88%);
|
||||
--sidebar-primary: hsl(193 44% 67%);
|
||||
--sidebar-primary-foreground: hsl(213 16% 12%);
|
||||
--sidebar-accent: hsl(193 10% 16%);
|
||||
--sidebar-accent-foreground: hsl(193 44% 67%);
|
||||
--sidebar-border: hsl(217 17% 28%);
|
||||
--sidebar-ring: hsl(193 44% 67%);
|
||||
|
||||
--smui-frost-1: 176 25% 65%;
|
||||
--smui-frost-2: 193 44% 67%;
|
||||
--smui-frost-3: 210 34% 63%;
|
||||
--smui-frost-4: 213 32% 52%;
|
||||
--smui-red: 355 52% 64%;
|
||||
--smui-orange: 14 51% 63%;
|
||||
--smui-yellow: 40 71% 73%;
|
||||
--smui-green: 92 28% 65%;
|
||||
--smui-purple: 311 24% 63%;
|
||||
--smui-surface-0: 213 16% 12%;
|
||||
--smui-surface-1: 217 16% 15.5%;
|
||||
--smui-surface-2: 216 15% 19%;
|
||||
--smui-surface-3: 215 14% 22%;
|
||||
--smui-border-hover: 216 12% 37%;
|
||||
|
||||
--font-sans: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-serif: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-mono: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-heading: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 0;
|
||||
--shadow-blur: 0;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-2xs: none;
|
||||
--shadow-xs: none;
|
||||
--shadow-sm: none;
|
||||
--shadow: none;
|
||||
--shadow-md: none;
|
||||
--shadow-lg: none;
|
||||
--shadow-xl: none;
|
||||
--shadow-2xl: none;
|
||||
--text-label: 11px;
|
||||
--text-ui: 13px;
|
||||
--text-heading: 22px;
|
||||
--text-stat: 26px;
|
||||
--text-hero: 42px;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar-background: var(--sidebar-background);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-mono: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-serif: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-heading: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--radius-sm: var(--radius);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: var(--radius);
|
||||
|
||||
--shadow-2xs: none;
|
||||
--shadow-xs: none;
|
||||
--shadow-sm: none;
|
||||
--shadow: none;
|
||||
--shadow-md: none;
|
||||
--shadow-lg: none;
|
||||
--shadow-xl: none;
|
||||
--shadow-2xl: none;
|
||||
|
||||
--text-label: 11px;
|
||||
--text-ui: 13px;
|
||||
--text-heading: 22px;
|
||||
--text-stat: 26px;
|
||||
--text-hero: 42px;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-smui-frost-1: hsl(var(--smui-frost-1));
|
||||
--color-smui-frost-2: hsl(var(--smui-frost-2));
|
||||
--color-smui-frost-3: hsl(var(--smui-frost-3));
|
||||
--color-smui-frost-4: hsl(var(--smui-frost-4));
|
||||
--color-smui-red: hsl(var(--smui-red));
|
||||
--color-smui-orange: hsl(var(--smui-orange));
|
||||
--color-smui-yellow: hsl(var(--smui-yellow));
|
||||
--color-smui-green: hsl(var(--smui-green));
|
||||
--color-smui-purple: hsl(var(--smui-purple));
|
||||
--color-smui-surface-0: hsl(var(--smui-surface-0));
|
||||
--color-smui-surface-1: hsl(var(--smui-surface-1));
|
||||
--color-smui-surface-2: hsl(var(--smui-surface-2));
|
||||
--color-smui-surface-3: hsl(var(--smui-surface-3));
|
||||
--color-smui-border-hover: hsl(var(--smui-border-hover));
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply min-h-full scroll-smooth font-sans;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-background text-foreground antialiased;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: hsl(var(--smui-frost-2) / 0.2);
|
||||
color: hsl(var(--smui-frost-2));
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@keyframes ascii-terminal-reveal {
|
||||
0% {
|
||||
clip-path: inset(0 100% 0 0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
82% {
|
||||
clip-path: inset(0 0 0 0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
clip-path: inset(0 0 0 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ascii-terminal-sheen {
|
||||
0% {
|
||||
transform: translateX(-115%);
|
||||
}
|
||||
|
||||
72% {
|
||||
transform: translateX(115%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(115%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-carousel {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(calc(-50% - 0.25rem));
|
||||
}
|
||||
}
|
||||
|
||||
.ascii-terminal-reveal {
|
||||
animation: ascii-terminal-reveal 1.05s steps(16, end) both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ascii-terminal-reveal::after {
|
||||
animation: ascii-terminal-sheen 1.05s ease-out both;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--primary) 8%, transparent) 34%,
|
||||
var(--primary) 50%,
|
||||
color-mix(in srgb, var(--foreground) 45%, transparent) 62%,
|
||||
transparent 100%
|
||||
);
|
||||
content: "";
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.agent-carousel-track {
|
||||
animation: agent-carousel 72s linear infinite;
|
||||
}
|
||||
|
||||
.agent-carousel:hover .agent-carousel-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ascii-terminal-reveal,
|
||||
.ascii-terminal-reveal::after,
|
||||
.agent-carousel-track {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.agent-carousel {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.card-glow:hover {
|
||||
border-color: hsl(var(--smui-border-hover));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { siteConfig } from "@/lib/site";
|
||||
import "./globals.css";
|
||||
|
||||
const socialPreview = {
|
||||
url: "/social-preview-gear.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "CAD Skills homepage showing a planetary gear CAD model",
|
||||
};
|
||||
|
||||
const themeScript = `
|
||||
(() => {
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const storageKey = "cad-skills-theme-v2";
|
||||
const applyTheme = (theme) => {
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
};
|
||||
const getStoredTheme = () => {
|
||||
try {
|
||||
const storedTheme = window.localStorage.getItem(storageKey);
|
||||
return storedTheme === "dark" || storedTheme === "light"
|
||||
? storedTheme
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
applyTheme(getStoredTheme() ?? "dark");
|
||||
|
||||
const handleSystemThemeChange = () => {
|
||||
if (!getStoredTheme()) {
|
||||
applyTheme("dark");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
media.addEventListener("change", handleSystemThemeChange);
|
||||
} catch {
|
||||
media.addListener(handleSystemThemeChange);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteConfig.origin),
|
||||
applicationName: siteConfig.name,
|
||||
title: {
|
||||
default: siteConfig.title,
|
||||
template: `%s | ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
keywords: siteConfig.keywords,
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
},
|
||||
openGraph: {
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
url: "/",
|
||||
siteName: siteConfig.name,
|
||||
images: [socialPreview],
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
images: [socialPreview],
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.ico?v=planetary-gear-workbench", type: "image/x-icon" },
|
||||
],
|
||||
shortcut: [
|
||||
{ url: "/favicon.ico?v=planetary-gear-workbench", type: "image/x-icon" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/favicon.png?v=planetary-gear-workbench", type: "image/png" },
|
||||
],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
"max-video-preview": -1,
|
||||
},
|
||||
},
|
||||
category: "engineering",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body>
|
||||
<Script
|
||||
id="theme-script"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{ __html: themeScript }}
|
||||
/>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import Image from "next/image";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { CopyButton } from "@/components/copy-button";
|
||||
import { HeroSection } from "@/components/hero-section";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
|
||||
const skillsInstallCommand = "npx skills install earthtojake/text-to-cad";
|
||||
|
||||
const pluginInstallCommands = [
|
||||
{
|
||||
agent: "Codex",
|
||||
command:
|
||||
"codex plugin marketplace add earthtojake/text-to-cad\ncodex plugin add cad@text-to-cad",
|
||||
},
|
||||
{
|
||||
agent: "Claude Code",
|
||||
command:
|
||||
"claude plugin marketplace add earthtojake/text-to-cad\nclaude plugin install cad@text-to-cad",
|
||||
},
|
||||
];
|
||||
|
||||
const supportedAgents = [
|
||||
{ name: "Claude Code", slug: "claude-code", icon: "claude-code.svg" },
|
||||
{ name: "Codex", slug: "codex", icon: "codex.svg" },
|
||||
];
|
||||
|
||||
const skillGroups = [
|
||||
{
|
||||
name: "CAD Router",
|
||||
path: "skills/cad-router",
|
||||
summary:
|
||||
"Combines text-to-cad, SimpleCADAPI, CADAM, and the CADDesigner workflow with shared validation and a task-scoped Viewer handoff.",
|
||||
},
|
||||
{
|
||||
name: "CAD",
|
||||
path: "skills/cad",
|
||||
summary:
|
||||
"Creates and edits CAD models from plain-language or image requests, with STEP as the main output along with options to export to STL, 3MF and GLB.",
|
||||
},
|
||||
{
|
||||
name: "CAD Viewer",
|
||||
path: "skills/cad-viewer",
|
||||
summary: "Shows local browser previews for CAD, G-code, and robot files.",
|
||||
},
|
||||
{
|
||||
name: "step.parts",
|
||||
path: "skills/step-parts",
|
||||
summary:
|
||||
"Finds off-the-shelf STEP parts like screws, bearings, motors, and connectors.",
|
||||
},
|
||||
{
|
||||
name: "DXF",
|
||||
path: "skills/dxf",
|
||||
summary:
|
||||
"Creates 2D DXF drawings like profiles, templates, gaskets, and cut layouts from Python sources or CAD geometry.",
|
||||
},
|
||||
{
|
||||
name: "URDF",
|
||||
path: "skills/urdf",
|
||||
summary:
|
||||
"Writes robot structure files with links, joints, limits, inertials, and meshes.",
|
||||
},
|
||||
{
|
||||
name: "SRDF",
|
||||
path: "skills/srdf",
|
||||
summary:
|
||||
"Adds MoveIt planning groups, end effectors, poses, and collision rules to a URDF.",
|
||||
},
|
||||
{
|
||||
name: "SDF",
|
||||
path: "skills/sdf",
|
||||
summary:
|
||||
"Creates simulator models and worlds with frames, physics, sensors, and lights.",
|
||||
},
|
||||
{
|
||||
name: "SendCutSend",
|
||||
path: "skills/sendcutsend",
|
||||
summary: "Checks DXF and STEP files before upload to SendCutSend.",
|
||||
},
|
||||
{
|
||||
name: "G-code",
|
||||
path: "skills/gcode",
|
||||
summary:
|
||||
"Slices supported mesh files into validated, printer-profiled FDM .gcode with real slicer CLIs.",
|
||||
},
|
||||
{
|
||||
name: "Bambu Labs",
|
||||
path: "skills/bambu-labs",
|
||||
summary:
|
||||
"Dry-runs, uploads, and cautiously starts local Bambu Lab print jobs from validated .gcode.",
|
||||
},
|
||||
{
|
||||
name: "Implicit CAD",
|
||||
path: "skills/implicit-cad",
|
||||
summary:
|
||||
"Creates browser-native implicit CAD models using GLSL signed-distance fields and CAD Viewer raymarch rendering. Experimental.",
|
||||
},
|
||||
];
|
||||
|
||||
function InstallCommand({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof pluginInstallCommands)[number];
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0 max-w-full overflow-hidden border border-border bg-card">
|
||||
<div className="border-b border-border px-3 py-2 text-label uppercase tracking-[1.5px] text-muted-foreground">
|
||||
{item.agent}
|
||||
</div>
|
||||
<div className="flex min-h-[54px] min-w-0 max-w-full items-stretch">
|
||||
<code className="flex min-w-0 flex-1 items-center overflow-x-auto whitespace-pre px-3 py-2 text-sm leading-6 text-foreground">
|
||||
{item.command}
|
||||
</code>
|
||||
<CopyButton
|
||||
text={item.command}
|
||||
label={`Copy ${item.agent} install command`}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InstallCommands() {
|
||||
return (
|
||||
<div className="grid min-w-0 gap-2">
|
||||
{pluginInstallCommands.map((item) => (
|
||||
<InstallCommand key={item.agent} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillsInstallCommand() {
|
||||
return (
|
||||
<div className="min-w-0 max-w-full overflow-hidden border border-border bg-card">
|
||||
<div className="flex min-h-[54px] min-w-0 max-w-full items-stretch">
|
||||
<code className="flex min-w-0 flex-1 items-center overflow-x-auto whitespace-pre px-3 py-2 text-sm leading-6 text-foreground">
|
||||
{skillsInstallCommand}
|
||||
</code>
|
||||
<CopyButton
|
||||
text={skillsInstallCommand}
|
||||
label="Copy Skills CLI install command"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentTile({
|
||||
agent,
|
||||
hidden = false,
|
||||
}: {
|
||||
agent: (typeof supportedAgents)[number];
|
||||
hidden?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
aria-hidden={hidden ? "true" : undefined}
|
||||
aria-label={`Skills for ${agent.name}`}
|
||||
className="group flex h-[54px] w-[150px] shrink-0 items-center gap-2.5 border border-border bg-card px-2.5 text-muted-foreground transition hover:bg-secondary hover:text-foreground sm:w-[168px]"
|
||||
href={`https://www.skills.sh/agent/${agent.slug}`}
|
||||
rel="noreferrer"
|
||||
tabIndex={hidden ? -1 : undefined}
|
||||
target="_blank"
|
||||
>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center">
|
||||
<Image
|
||||
alt=""
|
||||
src={`https://www.skills.sh/agents/${agent.icon}`}
|
||||
width={44}
|
||||
height={44}
|
||||
unoptimized
|
||||
className="size-5 object-contain opacity-70 grayscale transition group-hover:opacity-100 group-hover:grayscale-0 dark:invert"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-label uppercase tracking-[1.3px]">
|
||||
{agent.name}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCarousel() {
|
||||
return (
|
||||
<div className="agent-carousel relative min-h-[54px] min-w-0 overflow-hidden">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-12 bg-gradient-to-r from-background to-transparent sm:w-20" />
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-12 bg-gradient-to-l from-background to-transparent sm:w-20" />
|
||||
<div className="agent-carousel-track flex w-max gap-2">
|
||||
{supportedAgents.map((agent) => (
|
||||
<AgentTile key={agent.slug} agent={agent} />
|
||||
))}
|
||||
{supportedAgents.map((agent) => (
|
||||
<AgentTile key={`${agent.slug}-duplicate`} agent={agent} hidden />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionIntro({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
id?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2
|
||||
id={id}
|
||||
className="text-heading font-medium tracking-normal text-foreground"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillLink({ skill }: { skill: (typeof skillGroups)[number] }) {
|
||||
return (
|
||||
<a
|
||||
className="inline-flex min-w-0 items-center gap-1.5 text-label uppercase tracking-[1.5px] text-primary transition hover:text-primary/80"
|
||||
href={`https://github.com/earthtojake/text-to-cad/blob/main/${skill.path}/SKILL.md`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className="truncate">{skill.path}</span>
|
||||
<ExternalLink className="size-3 shrink-0" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background text-foreground">
|
||||
<SiteHeader />
|
||||
|
||||
<div className="mx-auto w-full max-w-[1200px] px-4 py-4 sm:px-6">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<HeroSection />
|
||||
|
||||
<section
|
||||
aria-label="Install CAD Skills with supported agents"
|
||||
className="grid gap-5 py-6 lg:grid-cols-2 lg:items-center lg:gap-12"
|
||||
>
|
||||
<div className="min-w-0 space-y-3">
|
||||
<h2 className="text-sm font-medium uppercase tracking-[1.5px] text-foreground">
|
||||
Try It Now
|
||||
</h2>
|
||||
<SkillsInstallCommand />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-3">
|
||||
<h2 className="text-sm font-medium uppercase tracking-[1.5px] text-foreground">
|
||||
Supported Agents
|
||||
</h2>
|
||||
<AgentCarousel />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="skills"
|
||||
aria-labelledby="skills-title"
|
||||
className="scroll-mt-20 space-y-3 py-6"
|
||||
>
|
||||
<SectionIntro
|
||||
id="skills-title"
|
||||
title="SKILLS"
|
||||
description="Install the library to give agents focused workflows for CAD, fabrication, robot description files, simulation, and local review."
|
||||
/>
|
||||
|
||||
<div className="border border-border bg-card">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)] border-b border-border px-3.5 py-2.5 text-xs uppercase tracking-[1.5px] text-muted-foreground md:grid-cols-[minmax(9rem,12rem)_minmax(0,1fr)_max-content] md:gap-5 md:pl-0 md:pr-3.5">
|
||||
<span className="md:pl-3.5">skill</span>
|
||||
<span className="hidden md:block">summary</span>
|
||||
<span className="hidden text-right md:block">source</span>
|
||||
</div>
|
||||
<ul className="divide-y divide-border">
|
||||
{skillGroups.map((skill) => (
|
||||
<li
|
||||
key={skill.name}
|
||||
className="card-glow grid gap-3 px-3.5 py-3 hover:bg-secondary/60 md:grid-cols-[minmax(9rem,12rem)_minmax(0,1fr)_max-content] md:items-center md:gap-5 md:pl-0 md:pr-3.5"
|
||||
>
|
||||
<div className="flex min-w-0 items-center md:pl-3.5">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{skill.name}
|
||||
</h3>
|
||||
<p className="mt-0.5 text-label uppercase tracking-wider text-muted-foreground md:hidden">
|
||||
{skill.path}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 text-sm leading-6 text-muted-foreground">
|
||||
<p>{skill.summary}</p>
|
||||
</div>
|
||||
<div className="min-w-0 md:justify-self-end md:pt-0.5 md:text-right">
|
||||
<SkillLink skill={skill} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="installation"
|
||||
aria-labelledby="installation-title"
|
||||
className="scroll-mt-20 space-y-3 py-6"
|
||||
>
|
||||
<SectionIntro
|
||||
id="installation-title"
|
||||
title="INSTALL"
|
||||
description="Install CAD Skills with the Skills CLI. Provider-native plugin installs are available as a secondary path."
|
||||
/>
|
||||
|
||||
<div className="max-w-3xl space-y-3">
|
||||
<SkillsInstallCommand />
|
||||
<div className="pt-3">
|
||||
<h3 className="mb-3 text-sm font-medium uppercase tracking-[1.5px] text-foreground">
|
||||
Plugin Installs
|
||||
</h3>
|
||||
<InstallCommands />
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
Skills CLI installation is preferred for regular use. Restart
|
||||
your agent if newly installed skills do not appear.
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
Local development symlink guidance lives in{" "}
|
||||
<a
|
||||
className="inline-flex items-center gap-1 text-primary transition hover:text-primary/80"
|
||||
href="https://github.com/earthtojake/text-to-cad/blob/main/CONTRIBUTING.md"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
CONTRIBUTING.md
|
||||
<ExternalLink className="size-3" aria-hidden="true" />
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SiteFooter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { absoluteUrl } from "@/lib/site";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: absoluteUrl("/sitemap.xml"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { absoluteUrl } from "@/lib/site";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: absoluteUrl("/"),
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type CopyButtonProps = {
|
||||
text: string;
|
||||
label?: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
type CopyStatus = "idle" | "copied" | "error";
|
||||
|
||||
export function CopyButton({
|
||||
text,
|
||||
label = "Copy command",
|
||||
compact = false,
|
||||
}: CopyButtonProps) {
|
||||
const [status, setStatus] = useState<CopyStatus>("idle");
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "idle") {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = window.setTimeout(() => setStatus("idle"), 1600);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [status]);
|
||||
|
||||
const copyText = async () => {
|
||||
setStatus("copied");
|
||||
|
||||
try {
|
||||
await window.navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
setStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const isCopied = status === "copied";
|
||||
const isError = status === "error";
|
||||
const buttonLabel = isCopied ? "Copied" : isError ? "Copy failed" : label;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`min-w-[4.25rem] shrink-0 cursor-pointer border-l border-border py-[7px] text-xs uppercase leading-none tracking-wider transition-colors ${
|
||||
isCopied
|
||||
? "bg-primary text-primary-foreground hover:text-primary-foreground"
|
||||
: isError
|
||||
? "bg-destructive text-destructive-foreground hover:text-destructive-foreground"
|
||||
: "bg-secondary text-muted-foreground hover:text-foreground"
|
||||
} ${
|
||||
compact ? "px-2.5" : "px-3"
|
||||
}`}
|
||||
onClick={copyText}
|
||||
aria-label={buttonLabel}
|
||||
title={buttonLabel}
|
||||
aria-live="polite"
|
||||
>
|
||||
{isCopied ? "copied" : isError ? "failed" : "copy"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { HeroStepRender } from "@/components/hero-step-render";
|
||||
|
||||
const cadSkillsAscii = String.raw` ██████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
|
||||
██╔════╝██╔══██╗██╔══██╗ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
|
||||
██║ ███████║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
|
||||
██║ ██╔══██║██║ ██║ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
|
||||
╚██████╗██║ ██║██████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
|
||||
╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝`;
|
||||
|
||||
const ASCII_MEASURE_FONT_SIZE = 10;
|
||||
const ASCII_MIN_FONT_SIZE = 7;
|
||||
const ASCII_MAX_FONT_SIZE = 18;
|
||||
const ASCII_MAX_HEIGHT = 132;
|
||||
const ASCII_WIDTH_FILL = 0.995;
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function AsciiWord({ ascii }: { ascii: string }) {
|
||||
const frameRef = useRef<HTMLDivElement>(null);
|
||||
const measureRef = useRef<HTMLPreElement>(null);
|
||||
const [fontSize, setFontSize] = useState(10.5);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const frame = frameRef.current;
|
||||
const measure = measureRef.current;
|
||||
|
||||
if (!frame || !measure) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateFontSize = () => {
|
||||
const measuredWidth = measure.scrollWidth;
|
||||
const measuredHeight = measure.scrollHeight;
|
||||
const availableWidth = frame.clientWidth * ASCII_WIDTH_FILL;
|
||||
|
||||
if (!measuredWidth || !measuredHeight || !availableWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widthSize =
|
||||
(availableWidth / measuredWidth) * ASCII_MEASURE_FONT_SIZE;
|
||||
const heightSize =
|
||||
(ASCII_MAX_HEIGHT / measuredHeight) * ASCII_MEASURE_FONT_SIZE;
|
||||
const nextFontSize = clamp(
|
||||
Math.min(widthSize, heightSize),
|
||||
ASCII_MIN_FONT_SIZE,
|
||||
ASCII_MAX_FONT_SIZE
|
||||
);
|
||||
|
||||
setFontSize((current) =>
|
||||
Math.abs(current - nextFontSize) > 0.05 ? nextFontSize : current
|
||||
);
|
||||
};
|
||||
|
||||
updateFontSize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateFontSize);
|
||||
resizeObserver.observe(frame);
|
||||
|
||||
void document.fonts?.ready.then(updateFontSize);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [ascii]);
|
||||
|
||||
const visibleClassName =
|
||||
"inline-block select-none whitespace-pre font-mono leading-[108%]";
|
||||
const measureClassName =
|
||||
"pointer-events-none invisible absolute left-0 top-0 inline-block whitespace-pre font-mono text-[10px] leading-[108%]";
|
||||
const asciiStyle = { fontSize: `${fontSize}px` } as CSSProperties;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={frameRef}
|
||||
className="relative max-h-[132px] w-full overflow-hidden"
|
||||
>
|
||||
<pre ref={measureRef} className={measureClassName} aria-hidden="true">
|
||||
{ascii}
|
||||
</pre>
|
||||
<pre
|
||||
className={`${visibleClassName} text-muted-foreground/55`}
|
||||
style={asciiStyle}
|
||||
>
|
||||
{ascii}
|
||||
</pre>
|
||||
<pre
|
||||
className={`ascii-terminal-reveal absolute left-0 top-0 ${visibleClassName} text-foreground`}
|
||||
style={asciiStyle}
|
||||
>
|
||||
{ascii}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroAsciiHeading() {
|
||||
return (
|
||||
<div aria-hidden="true" className="w-full max-w-full overflow-hidden">
|
||||
<AsciiWord ascii={cadSkillsAscii} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroSubtitle({ className = "" }: { className?: string }) {
|
||||
return (
|
||||
<p
|
||||
className={`text-[17px] font-medium leading-7 text-foreground sm:text-[20px] sm:leading-8 lg:text-[22px] lg:leading-9 ${className}`}
|
||||
>
|
||||
Generate CAD in AI agents like Codex and Claude Code.{" "}
|
||||
<span className="text-primary">
|
||||
100% open source, runs locally, free forever.
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="border border-border bg-card">
|
||||
<div className="grid min-w-0 lg:grid-cols-2">
|
||||
<div className="flex min-w-0 items-center px-4 py-4 sm:px-5 lg:min-h-[156px] lg:px-6 lg:py-5">
|
||||
<h1 className="sr-only">CAD Skills</h1>
|
||||
<HeroAsciiHeading />
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 items-center border-t border-border px-4 py-4 sm:px-5 lg:min-h-[156px] lg:border-l lg:border-t-0 lg:px-6 lg:py-5">
|
||||
<HeroSubtitle className="w-full lg:max-w-[34rem]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[260px] min-h-0 border-t border-border bg-background sm:h-[300px] lg:h-[340px]">
|
||||
<HeroStepRender />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { Line2 } from "three/examples/jsm/lines/Line2.js";
|
||||
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
|
||||
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
|
||||
import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
|
||||
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
|
||||
import {
|
||||
CAD_SCENE_SCALE,
|
||||
buildModel,
|
||||
} from "cadjs/common/cadScene.js";
|
||||
import { renderModel } from "cadjs/common/renderModel.js";
|
||||
import { loadSource } from "cadjs/common/source.js";
|
||||
import {
|
||||
normalizeParameterValue,
|
||||
normalizeStepModuleParameterValues as normalizeStepParameterValues,
|
||||
} from "cadjs/common/stepModule.js";
|
||||
import { cloneThemeSettings } from "cadjs/common/themeSettings.js";
|
||||
|
||||
const HERO_STEP_URL = "/hero/planetary_gear_assembly.step.glb";
|
||||
const HERO_STEP_PARAMETER_URL = "/hero/planetary_gear_assembly.step.js";
|
||||
const HERO_STEP_CAD_PATH = "models/fun/planetary_gear_assembly.step";
|
||||
const HERO_STEP_DEMO_URL =
|
||||
"https://demo.cadskills.xyz/?file=fun%2Fplanetary_gear_assembly.step";
|
||||
const HERO_STEP_LABEL = "PLANETARY_GEAR_ASSEMBLY.STEP";
|
||||
const GEAR_MESH_ANIMATION_SPEED = 0.14;
|
||||
const HERO_STEP_PARAMETER_VALUES = {
|
||||
drive: 0,
|
||||
explode: 0,
|
||||
highlightMeshing: false,
|
||||
orbitGuides: false,
|
||||
ringVisible: true,
|
||||
viewMode: "mesh",
|
||||
};
|
||||
|
||||
type PreviewScheme = "dark" | "light";
|
||||
type StepParameterDefinition = NonNullable<
|
||||
Awaited<ReturnType<typeof loadSource>>["stepParameterSource"]
|
||||
>["definition"];
|
||||
type StepParameterAnimation = StepParameterDefinition["animations"][number];
|
||||
type StepParameterRuntime = {
|
||||
animation: StepParameterAnimation | null;
|
||||
animationElapsedSec: number;
|
||||
animationState: {
|
||||
activeId: string;
|
||||
duration: number;
|
||||
elapsedSec: number;
|
||||
loop: boolean;
|
||||
playing: boolean;
|
||||
speed: number;
|
||||
};
|
||||
cadPath: string;
|
||||
definition: StepParameterDefinition;
|
||||
parameterValues: Record<string, unknown>;
|
||||
selectorRuntime: null;
|
||||
sourceUrl: string;
|
||||
};
|
||||
|
||||
const STEP_PREVIEW_PALETTES = {
|
||||
dark: {
|
||||
background: "#111820",
|
||||
border: "#3b4553",
|
||||
fill: ["#c7d0d8", "#aeb9c3", "#d9dee3", "#8f9ba7"],
|
||||
headerBackground: "rgba(17, 24, 32, 0.9)",
|
||||
headerText: "#c9d3df",
|
||||
keyLight: "#f6f8fb",
|
||||
keyLightIntensity: 2.5,
|
||||
fillLight: "#7f95ad",
|
||||
fillLightIntensity: 0.8,
|
||||
ambientLight: "#eef4fa",
|
||||
ambientLightIntensity: 1.85,
|
||||
},
|
||||
light: {
|
||||
background: "#eef1f5",
|
||||
border: "#c9cfda",
|
||||
fill: ["#d7dce0", "#cdd3d8", "#e4e7ea", "#bfc7ce"],
|
||||
headerBackground: "rgba(238, 241, 245, 0.9)",
|
||||
headerText: "#4c566a",
|
||||
keyLight: "#ffffff",
|
||||
keyLightIntensity: 2.6,
|
||||
fillLight: "#cfd8e3",
|
||||
fillLightIntensity: 0.9,
|
||||
ambientLight: "#ffffff",
|
||||
ambientLightIntensity: 2.2,
|
||||
},
|
||||
} as const;
|
||||
|
||||
function currentPreviewScheme(): PreviewScheme {
|
||||
if (typeof document === "undefined") {
|
||||
return "dark";
|
||||
}
|
||||
|
||||
return document.documentElement.classList.contains("dark") ? "dark" : "light";
|
||||
}
|
||||
|
||||
function buildWorkbenchTheme(scheme: PreviewScheme) {
|
||||
const palette = STEP_PREVIEW_PALETTES[scheme];
|
||||
const theme = cloneThemeSettings("workbench");
|
||||
const materials =
|
||||
theme.materials && typeof theme.materials === "object"
|
||||
? theme.materials
|
||||
: {};
|
||||
|
||||
return {
|
||||
...theme,
|
||||
materials: {
|
||||
...materials,
|
||||
defaultColor: palette.fill[0],
|
||||
fillColors: [...palette.fill],
|
||||
overrideSourceColors: false,
|
||||
saturation: 1,
|
||||
contrast: 1,
|
||||
brightness: 1,
|
||||
tintStrength: 0,
|
||||
roughness: 0.92,
|
||||
metalness: 0,
|
||||
clearcoat: 0,
|
||||
envMapIntensity: 0,
|
||||
},
|
||||
edges: {
|
||||
...(theme.edges || {}),
|
||||
enabled: true,
|
||||
color: scheme === "dark" ? "#202b38" : "#2f3a4b",
|
||||
contrastMode: "manual",
|
||||
opacity: 1,
|
||||
silhouette: true,
|
||||
thickness: 1,
|
||||
},
|
||||
background: {
|
||||
type: "solid",
|
||||
solidColor: palette.background,
|
||||
},
|
||||
lighting: {
|
||||
ambient: {
|
||||
color: palette.ambientLight,
|
||||
enabled: true,
|
||||
intensity: palette.ambientLightIntensity,
|
||||
},
|
||||
directionalLights: [
|
||||
{
|
||||
color: palette.keyLight,
|
||||
enabled: true,
|
||||
intensity: palette.keyLightIntensity,
|
||||
position: { x: -120, y: 180, z: 240 },
|
||||
},
|
||||
{
|
||||
color: palette.fillLight,
|
||||
enabled: true,
|
||||
intensity: palette.fillLightIntensity,
|
||||
position: { x: 180, y: -120, z: 120 },
|
||||
},
|
||||
],
|
||||
hemisphere: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHeroStepParameterRuntime(
|
||||
definition: StepParameterDefinition,
|
||||
parameterUrl: string
|
||||
): StepParameterRuntime {
|
||||
const animation =
|
||||
definition.animations.find((item) => item.id === "meshCycle") ??
|
||||
definition.animations[0] ??
|
||||
null;
|
||||
const duration = Math.max(Number(animation?.duration) || 6, 0.001);
|
||||
|
||||
return {
|
||||
animation,
|
||||
animationElapsedSec: 0,
|
||||
animationState: {
|
||||
activeId: animation?.id ?? "",
|
||||
duration,
|
||||
elapsedSec: 0,
|
||||
loop: animation?.loop !== false,
|
||||
playing: false,
|
||||
speed: GEAR_MESH_ANIMATION_SPEED,
|
||||
},
|
||||
cadPath: HERO_STEP_CAD_PATH,
|
||||
definition,
|
||||
parameterValues: normalizeStepParameterValues(
|
||||
definition,
|
||||
HERO_STEP_PARAMETER_VALUES
|
||||
),
|
||||
selectorRuntime: null,
|
||||
sourceUrl: parameterUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function advanceStepParameterRuntime(
|
||||
runtime: StepParameterRuntime,
|
||||
deltaSeconds: number
|
||||
) {
|
||||
const animation = runtime.animation;
|
||||
if (!animation?.update) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = Math.max(Number(animation.duration) || 6, 0.001);
|
||||
const nextElapsed =
|
||||
(runtime.animationElapsedSec +
|
||||
Math.max(deltaSeconds, 0) * GEAR_MESH_ANIMATION_SPEED) %
|
||||
duration;
|
||||
const progress = nextElapsed / duration;
|
||||
const currentValues = normalizeStepParameterValues(
|
||||
runtime.definition,
|
||||
runtime.parameterValues
|
||||
);
|
||||
const nextValues = { ...currentValues };
|
||||
|
||||
const set = (parameterId: string, value: unknown) => {
|
||||
const id = String(parameterId || "").trim();
|
||||
const parameter = runtime.definition.parameterMap?.[id];
|
||||
if (!parameter) {
|
||||
return;
|
||||
}
|
||||
nextValues[id] = normalizeParameterValue(parameter, value);
|
||||
};
|
||||
|
||||
animation.update({
|
||||
cycle: nextElapsed / duration,
|
||||
duration,
|
||||
elapsed: nextElapsed,
|
||||
elapsedSec: nextElapsed,
|
||||
loop: animation.loop !== false,
|
||||
params: currentValues,
|
||||
progress,
|
||||
set,
|
||||
});
|
||||
|
||||
runtime.animationElapsedSec = nextElapsed;
|
||||
// The hero drives sidecar parameter values directly; keep time.playing false so
|
||||
// sidecar-only inspection highlights do not override native STEP colors.
|
||||
runtime.animationState = {
|
||||
activeId: animation.id,
|
||||
duration,
|
||||
elapsedSec: nextElapsed,
|
||||
loop: animation.loop !== false,
|
||||
playing: false,
|
||||
speed: GEAR_MESH_ANIMATION_SPEED,
|
||||
};
|
||||
runtime.parameterValues = nextValues;
|
||||
}
|
||||
|
||||
export function HeroStepRender() {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const [scheme, setScheme] = useState<PreviewScheme>("dark");
|
||||
const [status, setStatus] = useState("loading step");
|
||||
const palette = STEP_PREVIEW_PALETTES[scheme];
|
||||
|
||||
useEffect(() => {
|
||||
const syncScheme = () => {
|
||||
setScheme(currentPreviewScheme());
|
||||
};
|
||||
|
||||
syncScheme();
|
||||
|
||||
const observer = new MutationObserver(syncScheme);
|
||||
observer.observe(document.documentElement, {
|
||||
attributeFilter: ["class"],
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let disposed = false;
|
||||
let cadModel: ReturnType<typeof buildModel> | null = null;
|
||||
let viewport: ReturnType<typeof renderModel> | null = null;
|
||||
let stepParameterRuntime: StepParameterRuntime | null = null;
|
||||
const dragState = {
|
||||
active: false,
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
pitch: 0.18,
|
||||
pointerId: -1,
|
||||
yaw: -0.18,
|
||||
};
|
||||
|
||||
const beforeRender = ({ deltaSeconds }: { deltaSeconds: number }) => {
|
||||
if (disposed || !cadModel) {
|
||||
return;
|
||||
}
|
||||
if (stepParameterRuntime) {
|
||||
advanceStepParameterRuntime(stepParameterRuntime, deltaSeconds);
|
||||
cadModel.update({ stepParameters: stepParameterRuntime });
|
||||
}
|
||||
|
||||
if (!dragState.active) {
|
||||
dragState.yaw += 0.0018;
|
||||
}
|
||||
cadModel.root.rotation.x = dragState.pitch;
|
||||
cadModel.root.rotation.z = dragState.yaw;
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
dragState.active = true;
|
||||
dragState.lastX = event.clientX;
|
||||
dragState.lastY = event.clientY;
|
||||
dragState.pointerId = event.pointerId;
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
if (!dragState.active || event.pointerId !== dragState.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dx = event.clientX - dragState.lastX;
|
||||
const dy = event.clientY - dragState.lastY;
|
||||
dragState.lastX = event.clientX;
|
||||
dragState.lastY = event.clientY;
|
||||
dragState.yaw += dx * 0.008;
|
||||
dragState.pitch = Math.max(
|
||||
-0.85,
|
||||
Math.min(0.85, dragState.pitch + dy * 0.006)
|
||||
);
|
||||
};
|
||||
|
||||
const handlePointerEnd = (event: PointerEvent) => {
|
||||
if (event.pointerId !== dragState.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragState.active = false;
|
||||
dragState.pointerId = -1;
|
||||
if (canvas.hasPointerCapture(event.pointerId)) {
|
||||
canvas.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
setStatus("loading step");
|
||||
const parameterUrl = `${HERO_STEP_PARAMETER_URL}?v=${Date.now()}`;
|
||||
const source = await loadSource({
|
||||
kind: "step",
|
||||
glbUrl: HERO_STEP_URL,
|
||||
stepParameterUrl: parameterUrl,
|
||||
cadPath: HERO_STEP_CAD_PATH,
|
||||
stepParameters: HERO_STEP_PARAMETER_VALUES,
|
||||
});
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
if (!source.stepParameterSource?.definition) {
|
||||
throw new Error("missing STEP parameter sidecar");
|
||||
}
|
||||
stepParameterRuntime = createHeroStepParameterRuntime(
|
||||
source.stepParameterSource.definition,
|
||||
parameterUrl
|
||||
);
|
||||
|
||||
cadModel = buildModel(THREE, source, {
|
||||
theme: buildWorkbenchTheme(scheme),
|
||||
displayMode: "solid",
|
||||
stepParameters: stepParameterRuntime,
|
||||
scale: CAD_SCENE_SCALE.CAD,
|
||||
selection: {
|
||||
showEdges: true,
|
||||
},
|
||||
edgeRendering: {
|
||||
mode: "screen-space",
|
||||
Line2,
|
||||
LineGeometry,
|
||||
LineMaterial,
|
||||
LineSegments2,
|
||||
LineSegmentsGeometry,
|
||||
},
|
||||
});
|
||||
cadModel.root.rotation.x = dragState.pitch;
|
||||
cadModel.root.rotation.z = dragState.yaw;
|
||||
viewport = renderModel(THREE, cadModel, {
|
||||
autoStart: true,
|
||||
beforeRender,
|
||||
canvas,
|
||||
direction: [1, -1, 0.65],
|
||||
hostElement: viewportRef.current ?? canvas,
|
||||
lockedHalfHeightScale: 0.86,
|
||||
padding: 0.1,
|
||||
scale: CAD_SCENE_SCALE.CAD,
|
||||
theme: buildWorkbenchTheme(scheme),
|
||||
});
|
||||
setStatus(HERO_STEP_LABEL);
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "render failed");
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener("pointerdown", handlePointerDown);
|
||||
canvas.addEventListener("pointermove", handlePointerMove);
|
||||
canvas.addEventListener("pointerup", handlePointerEnd);
|
||||
canvas.addEventListener("pointercancel", handlePointerEnd);
|
||||
void load();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
canvas.removeEventListener("pointerdown", handlePointerDown);
|
||||
canvas.removeEventListener("pointermove", handlePointerMove);
|
||||
canvas.removeEventListener("pointerup", handlePointerEnd);
|
||||
canvas.removeEventListener("pointercancel", handlePointerEnd);
|
||||
viewport?.dispose();
|
||||
};
|
||||
}, [palette, scheme]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden"
|
||||
style={{ backgroundColor: palette.background }}
|
||||
>
|
||||
<div ref={viewportRef} className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-label="Light 3D render of a sample STEP planetary gear assembly"
|
||||
className="absolute inset-0 h-full w-full cursor-grab touch-none active:cursor-grabbing"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex min-h-8 shrink-0 items-center justify-between gap-3 border-t px-3 py-[7px] text-label uppercase leading-none tracking-[1.5px]"
|
||||
style={{
|
||||
backgroundColor: palette.headerBackground,
|
||||
borderColor: palette.border,
|
||||
color: palette.headerText,
|
||||
}}
|
||||
>
|
||||
{status === HERO_STEP_LABEL ? (
|
||||
<a
|
||||
className="min-w-0 truncate transition hover:text-primary"
|
||||
href={HERO_STEP_DEMO_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Open planetary gear assembly in the CAD Skills demo"
|
||||
>
|
||||
{status}
|
||||
</a>
|
||||
) : (
|
||||
<span className="min-w-0 truncate">{status}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t border-border py-6">
|
||||
<div className="mx-auto flex w-full max-w-[1200px] justify-end px-4 text-label uppercase tracking-[1.5px] text-muted-foreground sm:px-6">
|
||||
<a
|
||||
className="transition hover:text-primary"
|
||||
href="https://x.com/earthtojake"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Made by @earthtojake
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const GITHUB_REPO_URL = "https://github.com/earthtojake/text-to-cad";
|
||||
|
||||
function DiscordLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M20.32 4.37a19.8 19.8 0 0 0-4.89-1.51.07.07 0 0 0-.07.03c-.21.38-.44.86-.61 1.25a18.27 18.27 0 0 0-5.49 0 12.64 12.64 0 0 0-.62-1.25.08.08 0 0 0-.07-.03 19.74 19.74 0 0 0-4.89 1.51.07.07 0 0 0-.03.03C.53 9.05-.32 13.58.1 18.06a.08.08 0 0 0 .03.06 19.9 19.9 0 0 0 5.99 3.03.08.08 0 0 0 .08-.03c.46-.63.87-1.3 1.23-1.99a.08.08 0 0 0-.04-.11 13.1 13.1 0 0 1-1.87-.89.08.08 0 0 1-.01-.13c.13-.09.25-.19.37-.29a.07.07 0 0 1 .08-.01c3.93 1.79 8.18 1.79 12.06 0a.07.07 0 0 1 .08.01c.12.1.25.2.37.29a.08.08 0 0 1-.01.13 12.3 12.3 0 0 1-1.87.89.08.08 0 0 0-.04.11c.36.7.77 1.36 1.23 1.99a.08.08 0 0 0 .08.03 19.84 19.84 0 0 0 6-3.03.08.08 0 0 0 .03-.05c.5-5.18-.84-9.67-3.55-13.66a.06.06 0 0 0-.02-.04ZM8.02 15.33c-1.18 0-2.16-1.09-2.16-2.42s.96-2.42 2.16-2.42c1.21 0 2.18 1.1 2.16 2.42 0 1.33-.96 2.42-2.16 2.42Zm7.98 0c-1.18 0-2.16-1.09-2.16-2.42s.96-2.42 2.16-2.42c1.21 0 2.18 1.1 2.16 2.42 0 1.33-.95 2.42-2.16 2.42Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.3-1.2-1.6-1.2-1.6-1-.7.1-.7.1-.7 1.1.1 1.7 1.2 1.7 1.2 1 1.7 2.6 1.2 3.3.9.1-.7.4-1.2.7-1.5-2.7-.3-5.5-1.3-5.5-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.6.1-3.2 0 0 1-.3 3.3 1.2a11.2 11.2 0 0 1 6 0C17 4.7 18 5 18 5c.7 1.6.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function formatGitHubStars(stars: number) {
|
||||
return new Intl.NumberFormat("en-US").format(stars);
|
||||
}
|
||||
|
||||
function VersionLink({ version }: { version: string }) {
|
||||
const normalizedVersion = version.trim();
|
||||
|
||||
if (!normalizedVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="hidden px-2.5 py-1.5 text-ui text-muted-foreground transition hover:bg-secondary hover:text-foreground md:inline-flex"
|
||||
href={`${GITHUB_REPO_URL}/releases`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Open GitHub releases for version ${normalizedVersion}`}
|
||||
title={`Open GitHub releases for version ${normalizedVersion}`}
|
||||
>
|
||||
{normalizedVersion}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function SiteHeaderClient({
|
||||
githubStars,
|
||||
discordUrl,
|
||||
version,
|
||||
}: {
|
||||
githubStars: number | null;
|
||||
discordUrl: string;
|
||||
version: string;
|
||||
}) {
|
||||
const githubLabel =
|
||||
githubStars === null
|
||||
? "Open text-to-cad on GitHub"
|
||||
: `Open text-to-cad on GitHub, ${new Intl.NumberFormat("en-US").format(
|
||||
githubStars
|
||||
)} stars`;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 h-14 shrink-0 overflow-hidden border-b border-border bg-background">
|
||||
<div className="mx-auto flex h-full w-full max-w-[1200px] items-center gap-3 px-4 sm:px-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex min-w-0 items-center text-foreground transition hover:text-primary"
|
||||
>
|
||||
<span className="min-w-0 truncate text-sm font-medium">
|
||||
CAD Skills
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav
|
||||
aria-label="Primary"
|
||||
className="ml-auto hidden items-center gap-1 sm:flex"
|
||||
>
|
||||
<a
|
||||
className="px-2.5 py-1.5 text-ui text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
||||
href="#skills"
|
||||
>
|
||||
SKILLS
|
||||
</a>
|
||||
<a
|
||||
className="px-2.5 py-1.5 text-ui text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
||||
href="#installation"
|
||||
>
|
||||
INSTALL
|
||||
</a>
|
||||
<VersionLink version={version} />
|
||||
</nav>
|
||||
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1 sm:ml-0">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="card-glow h-8 border-border bg-card px-2 text-ui text-foreground hover:bg-secondary hover:text-primary"
|
||||
>
|
||||
<a
|
||||
href="https://demo.cadskills.xyz"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Open demo in a new tab"
|
||||
>
|
||||
DEMO
|
||||
</a>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="card-glow h-8 w-8 border-border bg-card text-foreground hover:bg-secondary hover:text-primary"
|
||||
>
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Join the CAD Skills Discord"
|
||||
>
|
||||
<DiscordLogo className="size-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Discord</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="card-glow h-8 border-border bg-card px-2 text-foreground hover:bg-secondary hover:text-primary"
|
||||
>
|
||||
<a
|
||||
href={GITHUB_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={githubLabel}
|
||||
>
|
||||
<GitHubLogo className="size-3.5" />
|
||||
{githubStars !== null ? (
|
||||
<span className="text-label font-medium tabular-nums tracking-wider">
|
||||
{formatGitHubStars(githubStars)}
|
||||
</span>
|
||||
) : null}
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">GitHub</TooltipContent>
|
||||
</Tooltip>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { SiteHeaderClient } from "@/components/site-header-client";
|
||||
import packageJson from "../../package.json";
|
||||
|
||||
const GITHUB_REPO_API_URL =
|
||||
"https://api.github.com/repos/earthtojake/text-to-cad";
|
||||
const DEFAULT_DISCORD_URL = "https://discord.gg/5FGB9DwJYU";
|
||||
|
||||
function normalizeDiscordUrl(value: string | undefined) {
|
||||
const rawValue = String(value || "").trim();
|
||||
if (!rawValue) {
|
||||
return DEFAULT_DISCORD_URL;
|
||||
}
|
||||
|
||||
const urlValue = /^[a-z][a-z\d+.-]*:\/\//i.test(rawValue)
|
||||
? rawValue
|
||||
: `https://${rawValue.replace(/^\/+/, "")}`;
|
||||
|
||||
try {
|
||||
const url = new URL(urlValue);
|
||||
return ["http:", "https:"].includes(url.protocol)
|
||||
? url.href
|
||||
: DEFAULT_DISCORD_URL;
|
||||
} catch {
|
||||
return DEFAULT_DISCORD_URL;
|
||||
}
|
||||
}
|
||||
|
||||
async function getGitHubStars() {
|
||||
try {
|
||||
const response = await fetch(GITHUB_REPO_API_URL, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "cad-skills-docs",
|
||||
},
|
||||
next: { revalidate: 60 * 60 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { stargazers_count?: unknown };
|
||||
const stars = Number(data.stargazers_count);
|
||||
|
||||
return Number.isFinite(stars) ? stars : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function SiteHeader() {
|
||||
const githubStars = await getGitHubStars();
|
||||
return (
|
||||
<SiteHeaderClient
|
||||
githubStars={githubStars}
|
||||
discordUrl={normalizeDiscordUrl(process.env.DISCORD_URL)}
|
||||
version={packageJson.version}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const THEME_STORAGE_KEY = "cad-skills-theme-v2";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const toggleTheme = () => {
|
||||
const root = document.documentElement;
|
||||
const isDark = !root.classList.contains("dark");
|
||||
const theme = isDark ? "dark" : "light";
|
||||
|
||||
root.classList.toggle("dark", isDark);
|
||||
root.style.colorScheme = theme;
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
} catch {
|
||||
// The class change still applies when storage is unavailable.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="card-glow h-8 w-8 border-border bg-card text-foreground hover:bg-secondary hover:text-primary"
|
||||
onClick={toggleTheme}
|
||||
aria-label="Toggle light and dark mode"
|
||||
>
|
||||
<Sun className="size-4 dark:hidden" />
|
||||
<Moon className="hidden size-4 dark:block" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Light / dark</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
@@ -0,0 +1,35 @@
|
||||
const DEFAULT_SITE_ORIGIN = "https://www.cadskills.xyz";
|
||||
const SITE_DESCRIPTION =
|
||||
"A skills library for CAD, robotics, and hardware design agents";
|
||||
const SITE_TITLE = `CAD Skills | ${SITE_DESCRIPTION}`;
|
||||
|
||||
function normalizeOrigin(value: string | undefined, fallback: string) {
|
||||
const candidate = value?.trim() || fallback;
|
||||
|
||||
try {
|
||||
return new URL(candidate).origin;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export const siteConfig = {
|
||||
name: "CAD Skills",
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
keywords: [
|
||||
"CAD Skills",
|
||||
"text-to-cad",
|
||||
"CAD agents",
|
||||
"CAD routing",
|
||||
"agent skills",
|
||||
"step.parts",
|
||||
"STEP parts",
|
||||
"DXF",
|
||||
],
|
||||
origin: normalizeOrigin(process.env.NEXT_PUBLIC_SITE_URL, DEFAULT_SITE_ORIGIN),
|
||||
};
|
||||
|
||||
export function absoluteUrl(path: string) {
|
||||
return new URL(path, `${siteConfig.origin}/`).toString();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { extendTailwindMerge } from "tailwind-merge"
|
||||
|
||||
const twMerge = extendTailwindMerge({
|
||||
extend: {
|
||||
classGroups: {
|
||||
"font-size": [
|
||||
"text-label",
|
||||
"text-ui",
|
||||
"text-heading",
|
||||
"text-stat",
|
||||
"text-hero",
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
declare module "cadjs/common/cadScene.js" {
|
||||
import type { Group, Vector3 } from "three";
|
||||
|
||||
type CadBounds = {
|
||||
min: number[];
|
||||
max: number[];
|
||||
};
|
||||
|
||||
export const CAD_SCENE_SCALE: {
|
||||
CAD: string;
|
||||
URDF: string;
|
||||
};
|
||||
|
||||
export type CadSceneApi = {
|
||||
root: Group;
|
||||
modelGroup: Group;
|
||||
edgesGroup: Group;
|
||||
displayRecords: unknown[];
|
||||
records: unknown[];
|
||||
bounds: CadBounds;
|
||||
radius: number;
|
||||
runtime: unknown;
|
||||
dispose: () => void;
|
||||
update: (settings?: Record<string, unknown>) => CadSceneApi;
|
||||
};
|
||||
|
||||
export function buildModel(
|
||||
THREE: unknown,
|
||||
source: unknown,
|
||||
settings?: Record<string, unknown>
|
||||
): CadSceneApi;
|
||||
|
||||
export function fitCameraToModel(
|
||||
THREE: unknown,
|
||||
camera: unknown,
|
||||
bounds: CadBounds,
|
||||
options?: Record<string, unknown>
|
||||
): {
|
||||
center: Vector3;
|
||||
radius: number;
|
||||
halfHeight: number;
|
||||
distance: number;
|
||||
};
|
||||
}
|
||||
|
||||
declare module "cadjs/common/renderModel.js" {
|
||||
export type RenderViewportApi = {
|
||||
renderer: unknown;
|
||||
scene: unknown;
|
||||
camera: unknown;
|
||||
ready: Promise<void>;
|
||||
resize: () => unknown;
|
||||
render: () => void;
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
capturePng: () => string;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export function renderModel(
|
||||
THREE: unknown,
|
||||
model: unknown,
|
||||
options?: Record<string, unknown>
|
||||
): RenderViewportApi;
|
||||
}
|
||||
|
||||
declare module "cadjs/common/source.js" {
|
||||
export type RenderSource = {
|
||||
kind: string;
|
||||
meshData: unknown;
|
||||
stepParameterSource: {
|
||||
definition: import("cadjs/common/stepModule.js").StepModuleDefinition;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export function loadSource(
|
||||
input: unknown,
|
||||
options?: Record<string, unknown>
|
||||
): Promise<RenderSource>;
|
||||
}
|
||||
|
||||
declare module "cadjs/common/themeSettings.js" {
|
||||
export function cloneThemeSettings(themeId: string): Record<
|
||||
string,
|
||||
unknown
|
||||
> & {
|
||||
materials?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
declare module "cadjs/common/stepModule.js" {
|
||||
export type StepModuleParameter = {
|
||||
id: string;
|
||||
type: string;
|
||||
defaultValue: unknown;
|
||||
};
|
||||
|
||||
export type StepModuleAnimation = {
|
||||
id: string;
|
||||
duration: number;
|
||||
loop: boolean;
|
||||
update?: (context: {
|
||||
cycle: number;
|
||||
duration: number;
|
||||
elapsed: number;
|
||||
elapsedSec: number;
|
||||
loop: boolean;
|
||||
params: Record<string, unknown>;
|
||||
progress: number;
|
||||
set: (parameterId: string, value: unknown) => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type StepModuleDefinition = {
|
||||
animations: StepModuleAnimation[];
|
||||
parameterMap: Record<string, StepModuleParameter>;
|
||||
};
|
||||
|
||||
export function loadStepModuleDefinition(
|
||||
url: string,
|
||||
options?: Record<string, unknown>
|
||||
): Promise<StepModuleDefinition>;
|
||||
|
||||
export function normalizeParameterValue(
|
||||
definition: StepModuleParameter,
|
||||
value: unknown
|
||||
): unknown;
|
||||
|
||||
export function normalizeStepModuleParameterValues(
|
||||
definition: StepModuleDefinition,
|
||||
values?: Record<string, unknown>
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"cadjs/*": ["../packages/cadjs/src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"git": {
|
||||
"deploymentEnabled": false
|
||||
},
|
||||
"buildCommand": "npm run build"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Demo Models
|
||||
|
||||
Curated model fixtures and generator assets for text-to-cad workflows.
|
||||
|
||||
This tree is intended to be committed with Git LFS for large CAD, mesh, robot,
|
||||
and G-code artifacts. Source generators and concise documentation remain normal
|
||||
text files.
|
||||
|
||||
## Directory Map
|
||||
|
||||
- [benchmarks/](benchmarks/README.md): build123d benchmark generators plus STEP
|
||||
and mesh outputs.
|
||||
- [fun/](fun/README.md): standalone generated CAD examples and printable
|
||||
outputs.
|
||||
- [gcode/](gcode/README.md): small slicer input/output fixtures.
|
||||
- [implicits/](implicits/README.md): browser-native implicit CAD examples.
|
||||
- [mechanisms/](mechanisms/README.md): flattened mechanism STEP demos and
|
||||
generated render sidecars.
|
||||
- [robots/](robots/README.md): URDF/SRDF robot fixtures, meshes, printable
|
||||
outputs, and selected robot STEP sources.
|
||||
- [simple/](simple/README.md): compact build123d generators and STEP outputs
|
||||
for basic parts.
|
||||
|
||||
The larger `mechbench/` and `mechbench2/` external datasets are intentionally
|
||||
not included in this committed fixture tree.
|
||||
|
||||
## Git LFS Fetching
|
||||
|
||||
Repository LFS config excludes `models/**` from default LFS fetches so ordinary
|
||||
checkout and publish jobs can avoid downloading every model blob. Fetch the
|
||||
model artifacts explicitly when you need local bytes:
|
||||
|
||||
```bash
|
||||
git lfs pull --include="models/**" --exclude=""
|
||||
```
|
||||
|
||||
## Cleanup Policy
|
||||
|
||||
- Keep canonical sources (`*.py`, `*.implicit.js`, `*.urdf`, `*.srdf`, and docs)
|
||||
readable in normal Git.
|
||||
- Keep durable generated fixtures (`*.step`, `*.stl`, `*.3mf`, `*.glb`,
|
||||
`*.gcode`, and `*.dxf`) in Git LFS.
|
||||
- Do not commit supplementary media or sidecar metadata such as `*.png`,
|
||||
`*.mp4`, `*.gif`, or `*.json` unless a future workflow defines them as a
|
||||
required model artifact.
|
||||
- Do not commit local runtime debris such as `.DS_Store`, `__pycache__/`,
|
||||
`.cache/`, logs, or one-off timestamped review snapshots.
|
||||
- Put temporary scratch artifacts under ignored local paths, not in this tree.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Benchmark Models
|
||||
|
||||
Benchmark CAD fixtures for repeatable geometry generation, import/export, and
|
||||
viewer behavior checks. Each benchmark is intentionally more structured than the
|
||||
small examples in `../simple/` and is meant to exercise a distinct modeling
|
||||
surface.
|
||||
|
||||
## Contents
|
||||
|
||||
- `benchmark_*.py`: build123d generator source for each benchmark.
|
||||
- `benchmark_*.step`: canonical generated STEP output for each benchmark.
|
||||
- `.benchmark_*.step.glb`: generated render/selector sidecar paired with the
|
||||
STEP file.
|
||||
- `benchmark_common.py`: shared helper functions for benchmark generators.
|
||||
- `validate_benchmark.py`: local benchmark validation helper.
|
||||
|
||||
## Coverage
|
||||
|
||||
The benchmark set covers calibration blocks, flanges, brackets, shafts,
|
||||
enclosures, clevises, engine-like fins, impellers, spiral stairs, and planetary
|
||||
gear stages.
|
||||
|
||||
Keep benchmark examples here when they are intended as regression fixtures or
|
||||
represent a richer version of a simpler shape.
|
||||
@@ -0,0 +1,31 @@
|
||||
from build123d import Align, Box, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import line_edges_at_z, safe_chamfer
|
||||
|
||||
|
||||
LENGTH = 100.0
|
||||
WIDTH = 60.0
|
||||
HEIGHT = 20.0
|
||||
HOLE_DIAMETER = 8.0
|
||||
HOLE_LOCATIONS = ((-35.0, -20.0), (-35.0, 20.0), (35.0, -20.0), (35.0, 20.0))
|
||||
TOP_CHAMFER = 2.0
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the rectangular calibration block benchmark model in millimeters."""
|
||||
with BuildPart() as block:
|
||||
Box(LENGTH, WIDTH, HEIGHT, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
||||
|
||||
for x_pos, y_pos in HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=HOLE_DIAMETER / 2.0,
|
||||
height=HEIGHT + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = block.part
|
||||
part = safe_chamfer(part, line_edges_at_z(part, HEIGHT), length=TOP_CHAMFER)
|
||||
part.label = "benchmark_01_rectangular_calibration_block"
|
||||
return part
|
||||
@@ -0,0 +1,50 @@
|
||||
from math import tau
|
||||
|
||||
from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import circular_edges, polar_point, safe_fillet
|
||||
|
||||
|
||||
OUTER_DIAMETER = 80.0
|
||||
THICKNESS = 10.0
|
||||
CENTRAL_BORE_DIAMETER = 30.0
|
||||
BOLT_HOLE_COUNT = 6
|
||||
BOLT_HOLE_DIAMETER = 6.0
|
||||
BOLT_CIRCLE_DIAMETER = 60.0
|
||||
OUTER_FILLET = 1.5
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the circular flange benchmark model in millimeters."""
|
||||
with BuildPart() as flange:
|
||||
Cylinder(
|
||||
radius=OUTER_DIAMETER / 2.0,
|
||||
height=THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
with Locations(Location((0.0, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=CENTRAL_BORE_DIAMETER / 2.0,
|
||||
height=THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for index in range(BOLT_HOLE_COUNT):
|
||||
x_pos, y_pos = polar_point(BOLT_CIRCLE_DIAMETER / 2.0, tau * index / BOLT_HOLE_COUNT)
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=BOLT_HOLE_DIAMETER / 2.0,
|
||||
height=THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = flange.part
|
||||
edges = []
|
||||
edges.extend(circular_edges(part, radius=OUTER_DIAMETER / 2.0, axis="z", coordinate=0.0))
|
||||
edges.extend(circular_edges(part, radius=OUTER_DIAMETER / 2.0, axis="z", coordinate=THICKNESS))
|
||||
part = safe_fillet(part, edges, radius=OUTER_FILLET)
|
||||
part.label = "benchmark_02_circular_flange_bolt_pattern"
|
||||
return part
|
||||
@@ -0,0 +1,113 @@
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
add,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import safe_fillet
|
||||
|
||||
|
||||
BASE_LENGTH = 80.0
|
||||
BASE_WIDTH = 50.0
|
||||
BASE_THICKNESS = 8.0
|
||||
|
||||
BACK_LENGTH = 80.0
|
||||
BACK_THICKNESS = 8.0
|
||||
BACK_HEIGHT = 50.0
|
||||
BACK_CENTER_Y = BASE_WIDTH / 2.0 - BACK_THICKNESS / 2.0
|
||||
|
||||
HOLE_DIAMETER = 6.0
|
||||
BASE_HOLE_LOCATIONS = ((-25.0, -10.0), (25.0, -10.0))
|
||||
BACK_HOLE_LOCATIONS = ((-25.0, 30.0), (25.0, 30.0))
|
||||
|
||||
GUSSET_X_THICKNESS = 8.0
|
||||
GUSSET_X_LOCATIONS = (-20.0, 20.0)
|
||||
GUSSET_DEPTH = 30.0
|
||||
GUSSET_HEIGHT = 30.0
|
||||
TRANSITION_FILLET = 2.0
|
||||
|
||||
|
||||
def _make_gusset(x_center: float):
|
||||
back_front_y = BACK_CENTER_Y - BACK_THICKNESS / 2.0
|
||||
front_y = back_front_y - GUSSET_DEPTH
|
||||
top_z = BASE_THICKNESS + GUSSET_HEIGHT
|
||||
|
||||
with BuildPart() as gusset:
|
||||
with BuildSketch(Plane.YZ):
|
||||
Polygon(
|
||||
[
|
||||
(back_front_y, BASE_THICKNESS),
|
||||
(front_y, BASE_THICKNESS),
|
||||
(back_front_y, top_z),
|
||||
],
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=GUSSET_X_THICKNESS)
|
||||
|
||||
part = gusset.part.moved(Location((x_center - GUSSET_X_THICKNESS / 2.0, 0.0, 0.0)))
|
||||
part.label = f"gusset_x_{x_center:.0f}"
|
||||
return part
|
||||
|
||||
|
||||
def _transition_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
if str(edge.geom_type).endswith("LINE"):
|
||||
if abs(bbox.min.Z - BASE_THICKNESS) < 0.05 and abs(bbox.max.Z - BASE_THICKNESS) < 0.05:
|
||||
center_y = bbox.center().Y
|
||||
if BACK_CENTER_Y - BACK_THICKNESS / 2.0 - 0.05 <= center_y <= BASE_WIDTH / 2.0 + 0.05:
|
||||
if bbox.size.X > 20.0:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the L-bracket benchmark model in millimeters."""
|
||||
with BuildPart() as bracket:
|
||||
Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
||||
|
||||
with Locations(Location((0.0, BACK_CENTER_Y, BASE_THICKNESS))):
|
||||
Box(
|
||||
BACK_LENGTH,
|
||||
BACK_THICKNESS,
|
||||
BACK_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
for x_pos, y_pos in BASE_HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=HOLE_DIAMETER / 2.0,
|
||||
height=BASE_THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_pos, z_pos in BACK_HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, BACK_CENTER_Y, z_pos))):
|
||||
Cylinder(
|
||||
radius=HOLE_DIAMETER / 2.0,
|
||||
height=BACK_THICKNESS + 2.0,
|
||||
rotation=(90.0, 0.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_center in GUSSET_X_LOCATIONS:
|
||||
add(_make_gusset(x_center), mode=Mode.ADD)
|
||||
|
||||
part = bracket.part
|
||||
part = safe_fillet(part, _transition_edges(part), radius=TRANSITION_FILLET)
|
||||
part.label = "benchmark_03_l_bracket_gussets_two_hole_directions"
|
||||
return part
|
||||
@@ -0,0 +1,53 @@
|
||||
from build123d import Align, Box, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import circular_edges, safe_chamfer
|
||||
|
||||
|
||||
TOTAL_LENGTH = 120.0
|
||||
LEFT_LENGTH = 30.0
|
||||
MIDDLE_LENGTH = 60.0
|
||||
RIGHT_LENGTH = 30.0
|
||||
SMALL_DIAMETER = 20.0
|
||||
MIDDLE_DIAMETER = 30.0
|
||||
END_CHAMFER = 1.0
|
||||
|
||||
KEYWAY_X_START = 40.0
|
||||
KEYWAY_X_END = 80.0
|
||||
KEYWAY_WIDTH = 6.0
|
||||
KEYWAY_DEPTH = 3.0
|
||||
|
||||
|
||||
def _add_shaft_section(x_start: float, x_end: float, diameter: float):
|
||||
with Locations(Location(((x_start + x_end) / 2.0, 0.0, 0.0))):
|
||||
Cylinder(
|
||||
radius=diameter / 2.0,
|
||||
height=x_end - x_start,
|
||||
rotation=(0.0, 90.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the stepped shaft with keyway benchmark model in millimeters."""
|
||||
with BuildPart() as shaft:
|
||||
_add_shaft_section(0.0, LEFT_LENGTH, SMALL_DIAMETER)
|
||||
_add_shaft_section(LEFT_LENGTH, LEFT_LENGTH + MIDDLE_LENGTH, MIDDLE_DIAMETER)
|
||||
_add_shaft_section(LEFT_LENGTH + MIDDLE_LENGTH, TOTAL_LENGTH, SMALL_DIAMETER)
|
||||
|
||||
with Locations(Location(((KEYWAY_X_START + KEYWAY_X_END) / 2.0, 0.0, MIDDLE_DIAMETER / 2.0 - KEYWAY_DEPTH))):
|
||||
Box(
|
||||
KEYWAY_X_END - KEYWAY_X_START,
|
||||
KEYWAY_WIDTH,
|
||||
KEYWAY_DEPTH + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = shaft.part
|
||||
end_edges = []
|
||||
end_edges.extend(circular_edges(part, radius=SMALL_DIAMETER / 2.0, axis="x", coordinate=0.0))
|
||||
end_edges.extend(circular_edges(part, radius=SMALL_DIAMETER / 2.0, axis="x", coordinate=TOTAL_LENGTH))
|
||||
part = safe_chamfer(part, end_edges, length=END_CHAMFER)
|
||||
part.label = "benchmark_04_stepped_shaft_keyway"
|
||||
return part
|
||||
@@ -0,0 +1,75 @@
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
RectangleRounded,
|
||||
extrude,
|
||||
)
|
||||
|
||||
|
||||
OUTER_LENGTH = 100.0
|
||||
OUTER_WIDTH = 70.0
|
||||
OUTER_HEIGHT = 30.0
|
||||
WALL_THICKNESS = 3.0
|
||||
FLOOR_THICKNESS = 3.0
|
||||
OUTER_CORNER_RADIUS = 2.0
|
||||
|
||||
STANDOFF_DIAMETER = 10.0
|
||||
STANDOFF_HEIGHT = 12.0
|
||||
STANDOFF_HOLE_DIAMETER = 3.0
|
||||
STANDOFF_HOLE_DEPTH = 8.0
|
||||
STANDOFF_LOCATIONS = ((-35.0, -25.0), (-35.0, 25.0), (35.0, -25.0), (35.0, 25.0))
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the open-top electronics enclosure benchmark model in millimeters."""
|
||||
inner_length = OUTER_LENGTH - 2.0 * WALL_THICKNESS
|
||||
inner_width = OUTER_WIDTH - 2.0 * WALL_THICKNESS
|
||||
cavity_height = OUTER_HEIGHT - FLOOR_THICKNESS
|
||||
standoff_top_z = FLOOR_THICKNESS + STANDOFF_HEIGHT
|
||||
|
||||
with BuildPart() as enclosure:
|
||||
with BuildSketch(Plane.XY):
|
||||
RectangleRounded(
|
||||
OUTER_LENGTH,
|
||||
OUTER_WIDTH,
|
||||
OUTER_CORNER_RADIUS,
|
||||
align=(Align.CENTER, Align.CENTER),
|
||||
)
|
||||
extrude(amount=OUTER_HEIGHT)
|
||||
|
||||
with Locations(Location((0.0, 0.0, FLOOR_THICKNESS))):
|
||||
Box(
|
||||
inner_length,
|
||||
inner_width,
|
||||
cavity_height,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_pos, y_pos in STANDOFF_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, FLOOR_THICKNESS))):
|
||||
Cylinder(
|
||||
radius=STANDOFF_DIAMETER / 2.0,
|
||||
height=STANDOFF_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
with Locations(Location((x_pos, y_pos, standoff_top_z))):
|
||||
Cylinder(
|
||||
radius=STANDOFF_HOLE_DIAMETER / 2.0,
|
||||
height=STANDOFF_HOLE_DEPTH,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MAX),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = enclosure.part
|
||||
part.label = "benchmark_05_open_top_electronics_enclosure_bosses"
|
||||
return part
|
||||
@@ -0,0 +1,164 @@
|
||||
from math import cos, radians, sin
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
add,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import rounded_triangle_points, safe_fillet
|
||||
|
||||
|
||||
BASE_LENGTH = 120.0
|
||||
BASE_WIDTH = 60.0
|
||||
BASE_THICKNESS = 10.0
|
||||
BASE_FILLET = 3.0
|
||||
|
||||
LUG_X_EXTENT = 36.0
|
||||
LUG_RADIUS = 18.0
|
||||
LUG_HEIGHT_ABOVE_BASE = 42.0
|
||||
LUG_THICKNESS_Y = 18.0
|
||||
LUG_GAP_Y = 16.0
|
||||
LUG_FILLET = 2.0
|
||||
|
||||
CLEVIS_HOLE_DIAMETER = 14.0
|
||||
CLEVIS_HOLE_CENTER_Z = 34.0
|
||||
|
||||
BASE_HOLE_DIAMETER = 7.0
|
||||
BASE_HOLE_LOCATIONS = ((-45.0, -20.0), (-45.0, 20.0), (45.0, -20.0), (45.0, 20.0))
|
||||
|
||||
CUTOUT_CORNER_RADIUS = 3.0
|
||||
RIB_THICKNESS_X = 6.0
|
||||
|
||||
|
||||
def _lug_profile_points() -> list[tuple[float, float]]:
|
||||
half_x = LUG_X_EXTENT / 2.0
|
||||
arc_center_z = BASE_THICKNESS + LUG_HEIGHT_ABOVE_BASE - LUG_RADIUS
|
||||
points = [(-half_x, BASE_THICKNESS), (half_x, BASE_THICKNESS), (half_x, arc_center_z)]
|
||||
|
||||
for step in range(1, 12):
|
||||
angle = radians(180.0 * step / 12.0)
|
||||
points.append((LUG_RADIUS * cos(angle), arc_center_z + LUG_RADIUS * sin(angle)))
|
||||
|
||||
points.append((-half_x, arc_center_z))
|
||||
return points
|
||||
|
||||
|
||||
def _make_lug(y_min: float, label: str):
|
||||
with BuildPart() as lug:
|
||||
with BuildSketch(Plane.XZ):
|
||||
Polygon(_lug_profile_points(), align=None)
|
||||
extrude(amount=LUG_THICKNESS_Y)
|
||||
|
||||
part = lug.part.moved(Location((0.0, y_min, 0.0)))
|
||||
part.label = label
|
||||
return part
|
||||
|
||||
|
||||
def _make_rib(sign: float):
|
||||
outer_y = sign * (LUG_GAP_Y / 2.0 + LUG_THICKNESS_Y)
|
||||
foot_y = sign * (BASE_WIDTH / 2.0)
|
||||
top_z = BASE_THICKNESS + 28.0
|
||||
|
||||
with BuildPart() as rib:
|
||||
with BuildSketch(Plane.YZ):
|
||||
Polygon(
|
||||
[
|
||||
(outer_y, BASE_THICKNESS),
|
||||
(foot_y, BASE_THICKNESS),
|
||||
(outer_y, top_z),
|
||||
],
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=RIB_THICKNESS_X)
|
||||
|
||||
part = rib.part.moved(Location((-RIB_THICKNESS_X / 2.0, 0.0, 0.0)))
|
||||
part.label = "positive_y_diagonal_rib" if sign > 0.0 else "negative_y_diagonal_rib"
|
||||
return part
|
||||
|
||||
|
||||
def _outer_base_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
center = bbox.center()
|
||||
on_outer_x = abs(abs(center.X) - BASE_LENGTH / 2.0) < 0.05
|
||||
on_outer_y = abs(abs(center.Y) - BASE_WIDTH / 2.0) < 0.05
|
||||
on_top_or_bottom = abs(center.Z - 0.0) < 0.05 or abs(center.Z - BASE_THICKNESS) < 0.05
|
||||
if (on_outer_x or on_outer_y) and on_top_or_bottom:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def _lug_base_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
center = bbox.center()
|
||||
if abs(center.Z - BASE_THICKNESS) > 0.05:
|
||||
continue
|
||||
if abs(abs(center.Y) - LUG_GAP_Y / 2.0) < 0.05 and bbox.size.X > LUG_X_EXTENT - 1.0:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def _subtract_rounded_triangle(vertices: list[tuple[float, float]]):
|
||||
with BuildSketch(Plane.XY) as cutout:
|
||||
Polygon(
|
||||
rounded_triangle_points(vertices, radius=CUTOUT_CORNER_RADIUS, segments=5),
|
||||
align=None,
|
||||
)
|
||||
extrude(cutout.sketch, amount=BASE_THICKNESS + 2.0, mode=Mode.SUBTRACT)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the clevis bracket benchmark model in millimeters."""
|
||||
positive_lug_y_min = LUG_GAP_Y / 2.0 + LUG_THICKNESS_Y
|
||||
negative_lug_y_min = -LUG_GAP_Y / 2.0
|
||||
|
||||
with BuildPart() as bracket:
|
||||
Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
||||
|
||||
base = safe_fillet(bracket.part, _outer_base_edges(bracket.part), radius=BASE_FILLET)
|
||||
|
||||
with BuildPart() as bracket:
|
||||
add(base, mode=Mode.ADD)
|
||||
add(_make_lug(positive_lug_y_min, "positive_y_clevis_lug"), mode=Mode.ADD)
|
||||
add(_make_lug(negative_lug_y_min, "negative_y_clevis_lug"), mode=Mode.ADD)
|
||||
add(_make_rib(1.0), mode=Mode.ADD)
|
||||
add(_make_rib(-1.0), mode=Mode.ADD)
|
||||
|
||||
with Locations(Location((0.0, 0.0, CLEVIS_HOLE_CENTER_Z))):
|
||||
Cylinder(
|
||||
radius=CLEVIS_HOLE_DIAMETER / 2.0,
|
||||
height=BASE_WIDTH + 8.0,
|
||||
rotation=(90.0, 0.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_pos, y_pos in BASE_HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=BASE_HOLE_DIAMETER / 2.0,
|
||||
height=BASE_THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
_subtract_rounded_triangle([(-54.0, -13.0), (-25.0, 0.0), (-54.0, 13.0)])
|
||||
_subtract_rounded_triangle([(54.0, -13.0), (25.0, 0.0), (54.0, 13.0)])
|
||||
|
||||
part = bracket.part
|
||||
part = safe_fillet(part, _lug_base_edges(part), radius=LUG_FILLET)
|
||||
part.label = "benchmark_06_aerospace_clevis_bracket_lightening_cutouts"
|
||||
return part
|
||||
@@ -0,0 +1,122 @@
|
||||
from math import cos, radians, sin, tau
|
||||
|
||||
from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import circular_edges, polar_point, safe_fillet
|
||||
|
||||
|
||||
BARREL_DIAMETER = 36.0
|
||||
BARREL_HEIGHT = 70.0
|
||||
|
||||
FIN_COUNT = 12
|
||||
FIN_DIAMETER = 62.0
|
||||
FIN_THICKNESS = 2.0
|
||||
FIN_Z_VALUES = tuple(10.0 + 5.0 * index for index in range(FIN_COUNT))
|
||||
|
||||
BASE_FLANGE_DIAMETER = 70.0
|
||||
BASE_FLANGE_THICKNESS = 8.0
|
||||
MOUNT_HOLE_COUNT = 6
|
||||
MOUNT_HOLE_DIAMETER = 5.0
|
||||
MOUNT_BOLT_CIRCLE_DIAMETER = 56.0
|
||||
|
||||
TOP_CAP_DIAMETER = 44.0
|
||||
TOP_CAP_BOTTOM_Z = 70.0
|
||||
TOP_CAP_HEIGHT = 8.0
|
||||
|
||||
BOSS_DIAMETER = 12.0
|
||||
BOSS_LENGTH = 24.0
|
||||
BOSS_ANGLE_DEGREES = 35.0
|
||||
BOSS_BORE_DIAMETER = 5.0
|
||||
|
||||
EDGE_FILLET = 0.95
|
||||
|
||||
|
||||
def _boss_center() -> tuple[float, float, float]:
|
||||
angle = radians(BOSS_ANGLE_DEGREES)
|
||||
start = (TOP_CAP_DIAMETER / 2.0 - 1.0, 0.0, TOP_CAP_BOTTOM_Z + TOP_CAP_HEIGHT / 2.0)
|
||||
direction = (cos(angle), 0.0, sin(angle))
|
||||
return (
|
||||
start[0] + direction[0] * BOSS_LENGTH / 2.0,
|
||||
0.0,
|
||||
start[2] + direction[2] * BOSS_LENGTH / 2.0,
|
||||
)
|
||||
|
||||
|
||||
def _edge_fillets(part):
|
||||
edges = []
|
||||
for z_pos in FIN_Z_VALUES:
|
||||
edges.extend(circular_edges(part, radius=FIN_DIAMETER / 2.0, axis="z", coordinate=z_pos))
|
||||
edges.extend(circular_edges(part, radius=FIN_DIAMETER / 2.0, axis="z", coordinate=z_pos + FIN_THICKNESS))
|
||||
edges.extend(circular_edges(part, radius=BASE_FLANGE_DIAMETER / 2.0, axis="z", coordinate=0.0))
|
||||
edges.extend(circular_edges(part, radius=BASE_FLANGE_DIAMETER / 2.0, axis="z", coordinate=BASE_FLANGE_THICKNESS))
|
||||
return edges
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the radial-engine-style cylinder benchmark model in millimeters."""
|
||||
boss_rotation_y = 90.0 - BOSS_ANGLE_DEGREES
|
||||
boss_center = _boss_center()
|
||||
|
||||
with BuildPart() as cylinder:
|
||||
Cylinder(
|
||||
radius=BARREL_DIAMETER / 2.0,
|
||||
height=BARREL_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
Cylinder(
|
||||
radius=BASE_FLANGE_DIAMETER / 2.0,
|
||||
height=BASE_FLANGE_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
for z_pos in FIN_Z_VALUES:
|
||||
with Locations(Location((0.0, 0.0, z_pos))):
|
||||
Cylinder(
|
||||
radius=FIN_DIAMETER / 2.0,
|
||||
height=FIN_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
with Locations(Location((0.0, 0.0, TOP_CAP_BOTTOM_Z))):
|
||||
Cylinder(
|
||||
radius=TOP_CAP_DIAMETER / 2.0,
|
||||
height=TOP_CAP_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
with Locations(Location(boss_center)):
|
||||
Cylinder(
|
||||
radius=BOSS_DIAMETER / 2.0,
|
||||
height=BOSS_LENGTH,
|
||||
rotation=(0.0, boss_rotation_y, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
for index in range(MOUNT_HOLE_COUNT):
|
||||
x_pos, y_pos = polar_point(MOUNT_BOLT_CIRCLE_DIAMETER / 2.0, tau * index / MOUNT_HOLE_COUNT)
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=MOUNT_HOLE_DIAMETER / 2.0,
|
||||
height=BASE_FLANGE_THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
with Locations(Location(boss_center)):
|
||||
Cylinder(
|
||||
radius=BOSS_BORE_DIAMETER / 2.0,
|
||||
height=BOSS_LENGTH + 6.0,
|
||||
rotation=(0.0, boss_rotation_y, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = cylinder.part
|
||||
part = safe_fillet(part, _edge_fillets(part), radius=EDGE_FILLET)
|
||||
part.label = "benchmark_07_radial_engine_cylinder_cooling_fins"
|
||||
return part
|
||||
@@ -0,0 +1,152 @@
|
||||
from math import asin, cos, hypot, radians, sin, tau
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
add,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import circular_edges, polar_point, safe_fillet
|
||||
|
||||
|
||||
BACKPLATE_DIAMETER = 90.0
|
||||
BACKPLATE_THICKNESS = 6.0
|
||||
BACKPLATE_FILLET = 1.5
|
||||
HUB_DIAMETER = 26.0
|
||||
HUB_HEIGHT = 22.0
|
||||
BORE_DIAMETER = 8.0
|
||||
|
||||
BLADE_COUNT = 12
|
||||
BLADE_ROOT_RADIUS = 18.0
|
||||
BLADE_TIP_RADIUS = 43.0
|
||||
BLADE_THICKNESS = 3.0
|
||||
BLADE_HEIGHT = 16.0
|
||||
BLADE_BACKWARD_SWEEP = radians(45.0)
|
||||
BLADE_SEGMENTS = 5
|
||||
BLADE_ROOT_FILLET = 1.0
|
||||
|
||||
|
||||
def _clamp_radius(point: tuple[float, float]) -> tuple[float, float]:
|
||||
radius = hypot(point[0], point[1])
|
||||
if radius < BLADE_ROOT_RADIUS:
|
||||
scale = BLADE_ROOT_RADIUS / radius
|
||||
return (point[0] * scale, point[1] * scale)
|
||||
if radius > BLADE_TIP_RADIUS:
|
||||
scale = BLADE_TIP_RADIUS / radius
|
||||
return (point[0] * scale, point[1] * scale)
|
||||
return point
|
||||
|
||||
|
||||
def _blade_center(base_angle: float, t: float) -> tuple[float, float]:
|
||||
radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t
|
||||
angle = base_angle - BLADE_BACKWARD_SWEEP * t
|
||||
return polar_point(radius, angle)
|
||||
|
||||
|
||||
def _blade_normal(base_angle: float, t: float) -> tuple[float, float]:
|
||||
radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t
|
||||
dr_dt = BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS
|
||||
dtheta_dt = -BLADE_BACKWARD_SWEEP
|
||||
angle = base_angle + dtheta_dt * t
|
||||
dx_dt = dr_dt * cos(angle) - radius * sin(angle) * dtheta_dt
|
||||
dy_dt = dr_dt * sin(angle) + radius * cos(angle) * dtheta_dt
|
||||
length = hypot(dx_dt, dy_dt)
|
||||
return (-dy_dt / length, dx_dt / length)
|
||||
|
||||
|
||||
def _blade_end_points(base_angle: float, t: float) -> tuple[tuple[float, float], tuple[float, float]]:
|
||||
radius = BLADE_ROOT_RADIUS if t == 0.0 else BLADE_TIP_RADIUS
|
||||
angle = base_angle - BLADE_BACKWARD_SWEEP * t
|
||||
half_angle = asin((BLADE_THICKNESS / 2.0) / radius)
|
||||
return (polar_point(radius, angle + half_angle), polar_point(radius, angle - half_angle))
|
||||
|
||||
|
||||
def _blade_outline(base_angle: float) -> list[tuple[float, float]]:
|
||||
left_side = []
|
||||
right_side = []
|
||||
half_thickness = BLADE_THICKNESS / 2.0
|
||||
|
||||
for index in range(BLADE_SEGMENTS + 1):
|
||||
t = index / BLADE_SEGMENTS
|
||||
if index == 0 or index == BLADE_SEGMENTS:
|
||||
left, right = _blade_end_points(base_angle, t)
|
||||
else:
|
||||
center = _blade_center(base_angle, t)
|
||||
normal = _blade_normal(base_angle, t)
|
||||
left = _clamp_radius((center[0] + normal[0] * half_thickness, center[1] + normal[1] * half_thickness))
|
||||
right = _clamp_radius((center[0] - normal[0] * half_thickness, center[1] - normal[1] * half_thickness))
|
||||
left_side.append(left)
|
||||
right_side.append(right)
|
||||
|
||||
return left_side + list(reversed(right_side))
|
||||
|
||||
|
||||
def _make_blade(index: int):
|
||||
base_angle = tau * index / BLADE_COUNT
|
||||
with BuildPart() as blade:
|
||||
with BuildSketch(Plane.XY):
|
||||
Polygon(_blade_outline(base_angle), align=None)
|
||||
extrude(amount=BLADE_HEIGHT)
|
||||
|
||||
part = blade.part.moved(Location((0.0, 0.0, BACKPLATE_THICKNESS)))
|
||||
part.label = f"backward_curved_blade_{index + 1:02d}"
|
||||
return part
|
||||
|
||||
|
||||
def _blade_root_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
center = bbox.center()
|
||||
if abs(center.Z - BACKPLATE_THICKNESS) > 0.05:
|
||||
continue
|
||||
radial_distance = hypot(center.X, center.Y)
|
||||
if BLADE_ROOT_RADIUS - 3.0 <= radial_distance <= 30.0:
|
||||
if str(edge.geom_type).endswith("LINE"):
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the centrifugal impeller benchmark model in millimeters."""
|
||||
total_height = BACKPLATE_THICKNESS + HUB_HEIGHT
|
||||
|
||||
with BuildPart() as impeller:
|
||||
Cylinder(
|
||||
radius=BACKPLATE_DIAMETER / 2.0,
|
||||
height=BACKPLATE_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
for blade_index in range(BLADE_COUNT):
|
||||
add(_make_blade(blade_index), mode=Mode.ADD)
|
||||
with Locations(Location((0.0, 0.0, BACKPLATE_THICKNESS))):
|
||||
Cylinder(
|
||||
radius=HUB_DIAMETER / 2.0,
|
||||
height=HUB_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
with Locations(Location((0.0, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=BORE_DIAMETER / 2.0,
|
||||
height=total_height + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = impeller.part
|
||||
backplate_edges = []
|
||||
backplate_edges.extend(circular_edges(part, radius=BACKPLATE_DIAMETER / 2.0, axis="z", coordinate=0.0))
|
||||
backplate_edges.extend(circular_edges(part, radius=BACKPLATE_DIAMETER / 2.0, axis="z", coordinate=BACKPLATE_THICKNESS))
|
||||
part = safe_fillet(part, backplate_edges, radius=BACKPLATE_FILLET)
|
||||
part = safe_fillet(part, _blade_root_edges(part), radius=BLADE_ROOT_FILLET)
|
||||
part.label = "benchmark_08_centrifugal_impeller_backward_curved_blades"
|
||||
return part
|
||||
@@ -0,0 +1,149 @@
|
||||
from math import cos, radians, sin
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
BuildLine,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
CenterArc,
|
||||
Circle,
|
||||
Color,
|
||||
Compound,
|
||||
Cylinder,
|
||||
Edge,
|
||||
Line,
|
||||
Location,
|
||||
Plane,
|
||||
extrude,
|
||||
make_face,
|
||||
sweep,
|
||||
)
|
||||
|
||||
|
||||
BASE_DIAMETER = 90.0
|
||||
BASE_THICKNESS = 5.0
|
||||
COLUMN_DIAMETER = 14.0
|
||||
COLUMN_HEIGHT = 140.0
|
||||
|
||||
TREAD_COUNT = 20
|
||||
TREAD_THICKNESS = 4.0
|
||||
TREAD_INNER_RADIUS = 10.0
|
||||
TREAD_OUTER_RADIUS = 62.0
|
||||
TREAD_SWEEP_DEGREES = 24.0
|
||||
TREAD_FIRST_BOTTOM_Z = 4.0
|
||||
TREAD_RISE = 6.0
|
||||
TREAD_ROTATION_DEGREES = 18.0
|
||||
|
||||
HANDRAIL_RADIUS = 66.0
|
||||
HANDRAIL_DIAMETER = 5.0
|
||||
HANDRAIL_START_Z = 14.0
|
||||
HANDRAIL_END_Z = 130.0
|
||||
BALUSTER_DIAMETER = 3.0
|
||||
|
||||
|
||||
def _polar_point(radius: float, angle_degrees: float) -> tuple[float, float]:
|
||||
angle = radians(angle_degrees)
|
||||
return (radius * cos(angle), radius * sin(angle))
|
||||
|
||||
|
||||
def _make_base():
|
||||
with BuildPart() as base:
|
||||
Cylinder(
|
||||
radius=BASE_DIAMETER / 2.0,
|
||||
height=BASE_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = base.part
|
||||
part.label = "base_disk"
|
||||
part.color = Color(0.42, 0.43, 0.45, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_column():
|
||||
with BuildPart() as column:
|
||||
Cylinder(
|
||||
radius=COLUMN_DIAMETER / 2.0,
|
||||
height=COLUMN_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = column.part
|
||||
part.label = "central_column"
|
||||
part.color = Color(0.70, 0.72, 0.74, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_tread_profile():
|
||||
half_sweep = TREAD_SWEEP_DEGREES / 2.0
|
||||
with BuildPart() as tread:
|
||||
with BuildSketch(Plane.XY):
|
||||
with BuildLine():
|
||||
CenterArc((0.0, 0.0), TREAD_OUTER_RADIUS, -half_sweep, TREAD_SWEEP_DEGREES)
|
||||
Line(_polar_point(TREAD_OUTER_RADIUS, half_sweep), _polar_point(TREAD_INNER_RADIUS, half_sweep))
|
||||
CenterArc((0.0, 0.0), TREAD_INNER_RADIUS, half_sweep, -TREAD_SWEEP_DEGREES)
|
||||
Line(_polar_point(TREAD_INNER_RADIUS, -half_sweep), _polar_point(TREAD_OUTER_RADIUS, -half_sweep))
|
||||
make_face()
|
||||
extrude(amount=TREAD_THICKNESS)
|
||||
return tread.part
|
||||
|
||||
|
||||
def _make_tread(index: int):
|
||||
angle = index * TREAD_ROTATION_DEGREES
|
||||
bottom_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE
|
||||
part = _make_tread_profile().moved(Location((0.0, 0.0, bottom_z), (0.0, 0.0, angle)))
|
||||
part.label = f"helical_tread_{index + 1:02d}"
|
||||
part.color = Color(0.78, 0.58, 0.35, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _handrail_center_z(angle_degrees: float) -> float:
|
||||
return HANDRAIL_START_Z + (HANDRAIL_END_Z - HANDRAIL_START_Z) * angle_degrees / 360.0
|
||||
|
||||
|
||||
def _make_handrail():
|
||||
rail_height = HANDRAIL_END_Z - HANDRAIL_START_Z
|
||||
path = Edge.make_helix(
|
||||
pitch=rail_height,
|
||||
height=rail_height,
|
||||
radius=HANDRAIL_RADIUS,
|
||||
center=(0.0, 0.0, HANDRAIL_START_Z),
|
||||
)
|
||||
profile_plane = Plane(
|
||||
origin=(HANDRAIL_RADIUS, 0.0, HANDRAIL_START_Z),
|
||||
x_dir=(1.0, 0.0, 0.0),
|
||||
z_dir=path.tangent_at(0.0),
|
||||
)
|
||||
|
||||
with BuildPart() as rail:
|
||||
with BuildSketch(profile_plane):
|
||||
Circle(HANDRAIL_DIAMETER / 2.0)
|
||||
sweep(path=path, is_frenet=True)
|
||||
part = rail.part
|
||||
part.label = "helical_handrail"
|
||||
part.color = Color(0.22, 0.28, 0.34, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_baluster(index: int):
|
||||
angle = index * TREAD_ROTATION_DEGREES
|
||||
tread_top_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE + TREAD_THICKNESS
|
||||
rail_center_z = _handrail_center_z(angle)
|
||||
x_pos, y_pos = _polar_point(TREAD_OUTER_RADIUS, angle)
|
||||
with BuildPart() as baluster:
|
||||
Cylinder(
|
||||
radius=BALUSTER_DIAMETER / 2.0,
|
||||
height=rail_center_z - tread_top_z,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = baluster.part.moved(Location((x_pos, y_pos, tread_top_z)))
|
||||
part.label = f"baluster_{index + 1:02d}"
|
||||
part.color = Color(0.30, 0.33, 0.36, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the miniature spiral staircase benchmark model in millimeters."""
|
||||
parts = [_make_base(), _make_column()]
|
||||
parts.extend(_make_tread(index) for index in range(TREAD_COUNT))
|
||||
parts.append(_make_handrail())
|
||||
parts.extend(_make_baluster(index) for index in range(TREAD_COUNT))
|
||||
return Compound(obj=parts, children=parts, label="benchmark_09_spiral_staircase_helical_handrail")
|
||||
@@ -0,0 +1,181 @@
|
||||
from math import tau
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Color,
|
||||
Compound,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import polar_point, trapezoid_tooth_profile
|
||||
|
||||
|
||||
GEAR_THICKNESS = 8.0
|
||||
SUN_TEETH = 24
|
||||
SUN_PITCH_DIAMETER = 48.0
|
||||
SUN_ROOT_DIAMETER = 42.0
|
||||
SUN_OUTSIDE_DIAMETER = 54.0
|
||||
SUN_BORE_DIAMETER = 10.0
|
||||
|
||||
PLANET_COUNT = 3
|
||||
PLANET_TEETH = 18
|
||||
PLANET_PITCH_DIAMETER = 36.0
|
||||
PLANET_ROOT_DIAMETER = 31.0
|
||||
PLANET_OUTSIDE_DIAMETER = 41.0
|
||||
PLANET_CENTER_RADIUS = 42.0
|
||||
PLANET_BORE_DIAMETER = 6.8
|
||||
|
||||
RING_TEETH = 60
|
||||
RING_INTERNAL_PITCH_DIAMETER = 120.0
|
||||
RING_INTERNAL_ROOT_DIAMETER = 126.0
|
||||
RING_INTERNAL_TIP_DIAMETER = 114.0
|
||||
RING_OUTSIDE_DIAMETER = 140.0
|
||||
|
||||
CARRIER_DIAMETER = 105.0
|
||||
CARRIER_BOTTOM_Z = -5.0
|
||||
CARRIER_TOP_Z = -1.0
|
||||
CARRIER_THICKNESS = CARRIER_TOP_Z - CARRIER_BOTTOM_Z
|
||||
|
||||
PIN_DIAMETER = 6.0
|
||||
PIN_HEIGHT = 14.0
|
||||
PIN_BOTTOM_Z = -5.0
|
||||
|
||||
|
||||
def _make_external_gear(
|
||||
*,
|
||||
label: str,
|
||||
teeth: int,
|
||||
root_diameter: float,
|
||||
outside_diameter: float,
|
||||
phase: float,
|
||||
center: tuple[float, float] = (0.0, 0.0),
|
||||
bore_diameter: float | None = None,
|
||||
color: Color | None = None,
|
||||
):
|
||||
with BuildPart() as gear:
|
||||
with BuildSketch(Plane.XY):
|
||||
Polygon(
|
||||
trapezoid_tooth_profile(
|
||||
teeth=teeth,
|
||||
root_radius=root_diameter / 2.0,
|
||||
tip_radius=outside_diameter / 2.0,
|
||||
phase=phase,
|
||||
),
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=GEAR_THICKNESS)
|
||||
|
||||
if bore_diameter is not None:
|
||||
with Locations(Location((0.0, 0.0, -0.5))):
|
||||
Cylinder(
|
||||
radius=bore_diameter / 2.0,
|
||||
height=GEAR_THICKNESS + 1.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = gear.part.moved(Location((center[0], center[1], 0.0)))
|
||||
part.label = label
|
||||
part.color = color
|
||||
return part
|
||||
|
||||
|
||||
def _make_internal_ring_gear():
|
||||
with BuildPart() as ring:
|
||||
Cylinder(
|
||||
radius=RING_OUTSIDE_DIAMETER / 2.0,
|
||||
height=GEAR_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
with BuildSketch(Plane.XY):
|
||||
Polygon(
|
||||
trapezoid_tooth_profile(
|
||||
teeth=RING_TEETH,
|
||||
root_radius=RING_INTERNAL_ROOT_DIAMETER / 2.0,
|
||||
tip_radius=RING_INTERNAL_TIP_DIAMETER / 2.0,
|
||||
phase=-tau / RING_TEETH / 2.0,
|
||||
root_span_fraction=0.68,
|
||||
tip_span_fraction=0.34,
|
||||
),
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=GEAR_THICKNESS, mode=Mode.SUBTRACT)
|
||||
part = ring.part
|
||||
part.label = "ring_gear_60_internal_teeth"
|
||||
part.color = Color(0.66, 0.68, 0.70, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_carrier_plate():
|
||||
with BuildPart() as carrier:
|
||||
with Locations(Location((0.0, 0.0, CARRIER_BOTTOM_Z))):
|
||||
Cylinder(
|
||||
radius=CARRIER_DIAMETER / 2.0,
|
||||
height=CARRIER_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = carrier.part
|
||||
part.label = "carrier_plate"
|
||||
part.color = Color(0.52, 0.62, 0.58, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_pin(label: str, center: tuple[float, float]):
|
||||
with BuildPart() as pin:
|
||||
with Locations(Location((center[0], center[1], PIN_BOTTOM_Z))):
|
||||
Cylinder(
|
||||
radius=PIN_DIAMETER / 2.0,
|
||||
height=PIN_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = pin.part
|
||||
part.label = label
|
||||
part.color = Color(0.22, 0.25, 0.29, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _planet_center(index: int) -> tuple[float, float]:
|
||||
return polar_point(PLANET_CENTER_RADIUS, tau * index / PLANET_COUNT)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the simplified planetary gear benchmark assembly in millimeters."""
|
||||
parts = [
|
||||
_make_carrier_plate(),
|
||||
_make_internal_ring_gear(),
|
||||
_make_external_gear(
|
||||
label="sun_gear_24_external_teeth",
|
||||
teeth=SUN_TEETH,
|
||||
root_diameter=SUN_ROOT_DIAMETER,
|
||||
outside_diameter=SUN_OUTSIDE_DIAMETER,
|
||||
phase=-tau / SUN_TEETH / 2.0,
|
||||
bore_diameter=SUN_BORE_DIAMETER,
|
||||
color=Color(0.94, 0.60, 0.22, 1.0),
|
||||
),
|
||||
]
|
||||
|
||||
for index in range(PLANET_COUNT):
|
||||
center = _planet_center(index)
|
||||
parts.append(
|
||||
_make_external_gear(
|
||||
label=f"planet_gear_{index + 1}_18_external_teeth",
|
||||
teeth=PLANET_TEETH,
|
||||
root_diameter=PLANET_ROOT_DIAMETER,
|
||||
outside_diameter=PLANET_OUTSIDE_DIAMETER,
|
||||
phase=tau * index / PLANET_COUNT,
|
||||
center=center,
|
||||
bore_diameter=PLANET_BORE_DIAMETER,
|
||||
color=Color(0.24, 0.51, 0.76, 1.0),
|
||||
)
|
||||
)
|
||||
parts.append(_make_pin(f"planet_pin_{index + 1}", center))
|
||||
|
||||
return Compound(obj=parts, children=parts, label="benchmark_10_simplified_planetary_gear_stage")
|
||||
@@ -0,0 +1,177 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import cos, hypot, sin, tau
|
||||
from typing import Iterable
|
||||
|
||||
from build123d import Axis, BuildPart, Color, Location, Mode, chamfer, fillet
|
||||
|
||||
|
||||
TOLERANCE = 1e-6
|
||||
|
||||
|
||||
def polar_point(radius: float, angle: float) -> tuple[float, float]:
|
||||
return (radius * cos(angle), radius * sin(angle))
|
||||
|
||||
|
||||
def safe_fillet(part, edges: Iterable, radius: float):
|
||||
selected = list(edges)
|
||||
if not selected:
|
||||
return part
|
||||
try:
|
||||
return fillet(selected, radius=radius)
|
||||
except Exception:
|
||||
return part
|
||||
|
||||
|
||||
def safe_chamfer(part, edges: Iterable, length: float):
|
||||
selected = list(edges)
|
||||
if not selected:
|
||||
return part
|
||||
try:
|
||||
return chamfer(selected, length=length)
|
||||
except Exception:
|
||||
return part
|
||||
|
||||
|
||||
def line_edges_at_z(part, z_value: float, *, tol: float = 0.02):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
if abs(bbox.min.Z - z_value) <= tol and abs(bbox.max.Z - z_value) <= tol:
|
||||
if str(edge.geom_type).endswith("LINE"):
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def circular_edges(
|
||||
part,
|
||||
*,
|
||||
radius: float | None = None,
|
||||
axis: str | None = None,
|
||||
coordinate: float | None = None,
|
||||
tol: float = 0.05,
|
||||
):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
try:
|
||||
edge_radius = edge.radius
|
||||
edge_center = edge.arc_center
|
||||
edge_normal = edge.normal()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if radius is not None and abs(edge_radius - radius) > tol:
|
||||
continue
|
||||
|
||||
if axis is not None:
|
||||
components = {
|
||||
"x": abs(edge_normal.X),
|
||||
"y": abs(edge_normal.Y),
|
||||
"z": abs(edge_normal.Z),
|
||||
}
|
||||
if components[axis] < 1.0 - 1e-4:
|
||||
continue
|
||||
|
||||
if coordinate is not None:
|
||||
coord = {"x": edge_center.X, "y": edge_center.Y, "z": edge_center.Z}[axis or "z"]
|
||||
if abs(coord - coordinate) > tol:
|
||||
continue
|
||||
|
||||
edges.append(edge)
|
||||
|
||||
return edges
|
||||
|
||||
|
||||
def cylinder_axis_x(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD):
|
||||
from build123d import Align, Cylinder
|
||||
|
||||
return Cylinder(
|
||||
radius=radius,
|
||||
height=length,
|
||||
rotation=(0.0, 90.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=mode,
|
||||
).locate(Location(center))
|
||||
|
||||
|
||||
def cylinder_axis_y(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD):
|
||||
from build123d import Align, Cylinder
|
||||
|
||||
return Cylinder(
|
||||
radius=radius,
|
||||
height=length,
|
||||
rotation=(90.0, 0.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=mode,
|
||||
).locate(Location(center))
|
||||
|
||||
|
||||
def rounded_triangle_points(
|
||||
vertices: list[tuple[float, float]],
|
||||
*,
|
||||
radius: float,
|
||||
segments: int = 5,
|
||||
) -> list[tuple[float, float]]:
|
||||
"""Approximate a rounded triangular loop with quadratic corner arcs."""
|
||||
points: list[tuple[float, float]] = []
|
||||
count = len(vertices)
|
||||
|
||||
for index, vertex in enumerate(vertices):
|
||||
previous = vertices[(index - 1) % count]
|
||||
next_vertex = vertices[(index + 1) % count]
|
||||
|
||||
to_previous = (previous[0] - vertex[0], previous[1] - vertex[1])
|
||||
to_next = (next_vertex[0] - vertex[0], next_vertex[1] - vertex[1])
|
||||
previous_length = hypot(*to_previous)
|
||||
next_length = hypot(*to_next)
|
||||
offset = min(radius, previous_length * 0.35, next_length * 0.35)
|
||||
|
||||
start = (
|
||||
vertex[0] + to_previous[0] / previous_length * offset,
|
||||
vertex[1] + to_previous[1] / previous_length * offset,
|
||||
)
|
||||
end = (
|
||||
vertex[0] + to_next[0] / next_length * offset,
|
||||
vertex[1] + to_next[1] / next_length * offset,
|
||||
)
|
||||
|
||||
for step in range(segments + 1):
|
||||
t = step / segments
|
||||
x_pos = (1.0 - t) ** 2 * start[0] + 2.0 * (1.0 - t) * t * vertex[0] + t**2 * end[0]
|
||||
y_pos = (1.0 - t) ** 2 * start[1] + 2.0 * (1.0 - t) * t * vertex[1] + t**2 * end[1]
|
||||
points.append((x_pos, y_pos))
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def trapezoid_tooth_profile(
|
||||
*,
|
||||
teeth: int,
|
||||
root_radius: float,
|
||||
tip_radius: float,
|
||||
phase: float,
|
||||
root_span_fraction: float = 0.72,
|
||||
tip_span_fraction: float = 0.38,
|
||||
) -> list[tuple[float, float]]:
|
||||
points: list[tuple[float, float]] = []
|
||||
pitch_angle = tau / teeth
|
||||
|
||||
for tooth_index in range(teeth):
|
||||
center_angle = phase + tooth_index * pitch_angle
|
||||
points.extend(
|
||||
(
|
||||
polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0),
|
||||
polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0),
|
||||
polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0),
|
||||
polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0),
|
||||
)
|
||||
)
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def label(part, name: str, color: Color | None = None):
|
||||
part.label = name
|
||||
if color is not None:
|
||||
part.color = color
|
||||
return part
|
||||
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import atan2, degrees, hypot
|
||||
from pathlib import Path
|
||||
|
||||
from build123d import import_step
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BASIC_TOL = 0.2
|
||||
COMPLEX_TOL = 0.5
|
||||
ANGLE_TOL = 2.0
|
||||
|
||||
|
||||
class BenchmarkFailure(AssertionError):
|
||||
pass
|
||||
|
||||
|
||||
def close(actual: float, expected: float, tol: float = BASIC_TOL) -> bool:
|
||||
return abs(actual - expected) <= tol
|
||||
|
||||
|
||||
def check(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise BenchmarkFailure(message)
|
||||
|
||||
|
||||
def bbox(shape):
|
||||
box = shape.bounding_box()
|
||||
return box.min, box.max, (
|
||||
box.max.X - box.min.X,
|
||||
box.max.Y - box.min.Y,
|
||||
box.max.Z - box.min.Z,
|
||||
)
|
||||
|
||||
|
||||
def assert_bbox(shape, expected_size, expected_min=None, tol: float = BASIC_TOL):
|
||||
min_v, max_v, size = bbox(shape)
|
||||
for actual, expected, axis in zip(size, expected_size, "xyz"):
|
||||
check(close(actual, expected, tol), f"bbox {axis} size {actual:.3f} != {expected:.3f}")
|
||||
if expected_min is not None:
|
||||
for actual, expected, axis in zip((min_v.X, min_v.Y, min_v.Z), expected_min, "xyz"):
|
||||
check(close(actual, expected, tol), f"bbox min {axis} {actual:.3f} != {expected:.3f}")
|
||||
|
||||
|
||||
def axis_name(direction) -> str:
|
||||
components = {"x": abs(direction.X), "y": abs(direction.Y), "z": abs(direction.Z)}
|
||||
return max(components, key=components.get)
|
||||
|
||||
|
||||
def cyl_faces(shape, *, radius: float | None = None, axis: str | None = None, tol: float = BASIC_TOL):
|
||||
faces = []
|
||||
for face in shape.faces():
|
||||
if "CYLINDER" not in str(face.geom_type):
|
||||
continue
|
||||
if radius is not None and not close(face.radius, radius, tol):
|
||||
continue
|
||||
if axis is not None and axis_name(face.axis_of_rotation.direction) != axis:
|
||||
continue
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def torus_count(shape) -> int:
|
||||
return sum(1 for face in shape.faces() if "TORUS" in str(face.geom_type))
|
||||
|
||||
|
||||
def cone_count(shape) -> int:
|
||||
return sum(1 for face in shape.faces() if "CONE" in str(face.geom_type))
|
||||
|
||||
|
||||
def cylinder_centers(shape, *, radius: float, axis: str):
|
||||
centers = []
|
||||
for face in cyl_faces(shape, radius=radius, axis=axis):
|
||||
pos = face.axis_of_rotation.position
|
||||
centers.append((pos.X, pos.Y, pos.Z))
|
||||
return centers
|
||||
|
||||
|
||||
def expect_cylinder_at(shape, *, radius: float, axis: str, x=None, y=None, z=None, tol: float = BASIC_TOL):
|
||||
for center in cylinder_centers(shape, radius=radius, axis=axis):
|
||||
if x is not None and not close(center[0], x, tol):
|
||||
continue
|
||||
if y is not None and not close(center[1], y, tol):
|
||||
continue
|
||||
if z is not None and not close(center[2], z, tol):
|
||||
continue
|
||||
return
|
||||
raise BenchmarkFailure(f"missing cylinder radius {radius} axis {axis} at x={x} y={y} z={z}")
|
||||
|
||||
|
||||
def plane_faces_at(shape, *, axis: str, coordinate: float, tol: float = BASIC_TOL):
|
||||
index = "xyz".index(axis)
|
||||
faces = []
|
||||
for face in shape.faces():
|
||||
if "PLANE" not in str(face.geom_type):
|
||||
continue
|
||||
box = face.bounding_box()
|
||||
mins = (box.min.X, box.min.Y, box.min.Z)
|
||||
maxs = (box.max.X, box.max.Y, box.max.Z)
|
||||
if close(mins[index], coordinate, tol) and close(maxs[index], coordinate, tol):
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def assert_common(shape, *, solids: int | None = None, min_solids: int | None = None):
|
||||
check(shape.is_valid, "STEP imports but imported B-rep is invalid")
|
||||
check(shape.is_manifold, "STEP imports but B-rep is non-manifold")
|
||||
if solids is not None:
|
||||
check(len(shape.solids()) == solids, f"solid count {len(shape.solids())} != {solids}")
|
||||
if min_solids is not None:
|
||||
check(len(shape.solids()) >= min_solids, f"solid count {len(shape.solids())} < {min_solids}")
|
||||
|
||||
|
||||
def validate_01(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (100.0, 60.0, 20.0), (-50.0, -30.0, 0.0))
|
||||
for x in (-35.0, 35.0):
|
||||
for y in (-20.0, 20.0):
|
||||
expect_cylinder_at(shape, radius=4.0, axis="z", x=x, y=y)
|
||||
check(len(cyl_faces(shape, radius=4.0, axis="z")) == 4, "expected four 8 mm through holes")
|
||||
top_faces = plane_faces_at(shape, axis="z", coordinate=20.0)
|
||||
check(any(close(face.bounding_box().size.X, 96.0) and close(face.bounding_box().size.Y, 56.0) for face in top_faces), "top chamfered face is not inset by 2 mm")
|
||||
check(cone_count(shape) == 0 and torus_count(shape) == 0, "unexpected hole chamfers/roundovers")
|
||||
|
||||
|
||||
def validate_02(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (80.0, 80.0, 10.0), (-40.0, -40.0, 0.0))
|
||||
expect_cylinder_at(shape, radius=15.0, axis="z", x=0.0, y=0.0)
|
||||
bolt_centers = cylinder_centers(shape, radius=3.0, axis="z")
|
||||
check(len(bolt_centers) == 6, "expected six bolt holes")
|
||||
angles = sorted(degrees(atan2(y, x)) % 360.0 for x, y, _ in bolt_centers)
|
||||
for x, y, _ in bolt_centers:
|
||||
check(close(hypot(x, y), 30.0, COMPLEX_TOL), "bolt hole is off 60 mm bolt circle")
|
||||
gaps = [(angles[(i + 1) % 6] - angles[i]) % 360.0 for i in range(6)]
|
||||
check(all(close(gap, 60.0, ANGLE_TOL) for gap in gaps), "bolt holes are not spaced 60 degrees apart")
|
||||
check(torus_count(shape) >= 2, "missing top/bottom outer edge fillets")
|
||||
|
||||
|
||||
def validate_03(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (80.0, 50.0, 58.0), (-40.0, -25.0, 0.0))
|
||||
for x in (-25.0, 25.0):
|
||||
expect_cylinder_at(shape, radius=3.0, axis="z", x=x, y=-10.0)
|
||||
expect_cylinder_at(shape, radius=3.0, axis="y", x=x, z=30.0)
|
||||
check(len(cyl_faces(shape, radius=3.0, axis="z")) == 2, "base hole count is not two")
|
||||
check(len(cyl_faces(shape, radius=3.0, axis="y")) == 2, "back plate hole count is not two")
|
||||
check(len(shape.faces()) >= 20, "missing gusset/fillet detail")
|
||||
|
||||
|
||||
def validate_04(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (120.0, 30.0, 30.0), (0.0, -15.0, -15.0))
|
||||
check(len(cyl_faces(shape, radius=10.0, axis="x")) == 2, "expected two 20 mm shaft sections")
|
||||
check(len(cyl_faces(shape, radius=15.0, axis="x")) == 1, "expected one 30 mm shaft section")
|
||||
keyway_planes = plane_faces_at(shape, axis="z", coordinate=12.0)
|
||||
check(any(close(face.bounding_box().size.X, 40.0) and close(face.bounding_box().size.Y, 6.0) for face in keyway_planes), "keyway floor is not 40 x 6 mm at 3 mm depth")
|
||||
check(cone_count(shape) >= 2, "missing 1 mm end chamfers")
|
||||
|
||||
|
||||
def validate_05(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (100.0, 70.0, 30.0), (-50.0, -35.0, 0.0))
|
||||
check(any(close(face.bounding_box().size.X, 94.0) and close(face.bounding_box().size.Y, 64.0) for face in plane_faces_at(shape, axis="z", coordinate=3.0)), "inside floor/wall thickness check failed")
|
||||
for x in (-35.0, 35.0):
|
||||
for y in (-25.0, 25.0):
|
||||
expect_cylinder_at(shape, radius=5.0, axis="z", x=x, y=y)
|
||||
expect_cylinder_at(shape, radius=1.5, axis="z", x=x, y=y)
|
||||
check(len(cyl_faces(shape, radius=5.0, axis="z")) == 4, "standoff count is not four")
|
||||
check(len(cyl_faces(shape, radius=1.5, axis="z")) == 4, "blind hole count is not four")
|
||||
check(len(cyl_faces(shape, radius=2.0, axis="z")) >= 4, "missing 2 mm outside vertical corner fillets")
|
||||
|
||||
|
||||
def validate_06(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (120.0, 60.0, 52.0), (-60.0, -30.0, 0.0))
|
||||
expect_cylinder_at(shape, radius=7.0, axis="y", x=0.0, z=34.0)
|
||||
for x in (-45.0, 45.0):
|
||||
for y in (-20.0, 20.0):
|
||||
expect_cylinder_at(shape, radius=3.5, axis="z", x=x, y=y)
|
||||
check(len(cyl_faces(shape, radius=7.0, axis="y")) == 2, "clevis through-hole should pass through two separate lugs")
|
||||
check(len(cyl_faces(shape, radius=3.5, axis="z")) == 4, "base mounting hole count is not four")
|
||||
check(len(cyl_faces(shape, radius=2.0, axis="x")) >= 2, "missing lug-to-base radius detail")
|
||||
check(len(shape.faces()) >= 90, "missing cutout/rib/lug detail")
|
||||
|
||||
|
||||
def validate_07(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (79.1, 70.0, 92.7), (-35.0, -35.0, 0.0), tol=COMPLEX_TOL)
|
||||
check(len(cyl_faces(shape, radius=31.0, axis="z")) == 12, "cooling fin count is not 12")
|
||||
check(len(cyl_faces(shape, radius=2.5, axis="z")) == 6, "mounting hole count is not six")
|
||||
check(len(cyl_faces(shape, radius=18.0, axis="z")) >= 1, "missing 36 mm barrel")
|
||||
check(len(cyl_faces(shape, radius=22.0, axis="z")) == 1, "missing 44 mm top cap")
|
||||
boss = cyl_faces(shape, radius=6.0)
|
||||
bore = [face for face in cyl_faces(shape, radius=2.5) if axis_name(face.axis_of_rotation.direction) == "x"]
|
||||
check(len(boss) == 1 and len(bore) == 1, "missing angled boss or boss bore")
|
||||
for face in (boss[0], bore[0]):
|
||||
direction = face.axis_of_rotation.direction
|
||||
angle = degrees(atan2(abs(direction.Z), abs(direction.X)))
|
||||
check(close(angle, 35.0, ANGLE_TOL), "boss axis is not 35 degrees upward")
|
||||
check(torus_count(shape) >= 24, "missing fin/flange edge fillets")
|
||||
|
||||
|
||||
def validate_08(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (90.0, 90.0, 28.0), (-45.0, -45.0, 0.0), tol=COMPLEX_TOL)
|
||||
expect_cylinder_at(shape, radius=4.0, axis="z", x=0.0, y=0.0)
|
||||
check(len(cyl_faces(shape, radius=13.0, axis="z")) == 1, "missing 26 mm hub")
|
||||
check(len(shape.faces()) >= 220, "missing repeated curved blade detail")
|
||||
check(torus_count(shape) >= 2, "missing backplate edge fillets")
|
||||
|
||||
|
||||
def validate_09(shape):
|
||||
assert_common(shape, min_solids=43)
|
||||
assert_bbox(shape, (137.0, 137.0, 140.0), (-68.5, -68.5, 0.0), tol=COMPLEX_TOL)
|
||||
check(len(cyl_faces(shape, radius=7.0, axis="z")) == 1, "missing central column")
|
||||
check(len(cyl_faces(shape, radius=45.0, axis="z")) == 1, "missing base disk")
|
||||
check(len(cyl_faces(shape, radius=62.0, axis="z")) == 20, "tread outer radius/count failed")
|
||||
check(len(cyl_faces(shape, radius=10.0, axis="z")) == 20, "tread inner radius/count failed")
|
||||
check(len(cyl_faces(shape, radius=1.5, axis="z")) == 20, "baluster count failed")
|
||||
check(len(shape.solids()) == 43, "staircase assembly should have 43 solids")
|
||||
|
||||
|
||||
def validate_10(shape):
|
||||
assert_common(shape, solids=9)
|
||||
assert_bbox(shape, (140.0, 140.0, 14.0), (-70.0, -70.0, -5.0), tol=COMPLEX_TOL)
|
||||
check(len(cyl_faces(shape, radius=3.0, axis="z")) == 3, "planet pin count failed")
|
||||
check(len(cyl_faces(shape, radius=5.0, axis="z")) == 1, "sun bore failed")
|
||||
check(len(shape.faces()) >= 550 and len(shape.edges()) >= 1600, "gear tooth detail is missing")
|
||||
|
||||
|
||||
VALIDATORS = [
|
||||
("1. Rectangular calibration block with four holes", "benchmark_01_rectangular_calibration_block.step", validate_01),
|
||||
("2. Circular flange with bolt-hole pattern", "benchmark_02_circular_flange.step", validate_02),
|
||||
("3. L-bracket with gussets and two hole directions", "benchmark_03_l_bracket.step", validate_03),
|
||||
("4. Stepped shaft with keyway", "benchmark_04_stepped_shaft_keyway.step", validate_04),
|
||||
("5. Open-top electronics enclosure with bosses", "benchmark_05_open_top_electronics_enclosure.step", validate_05),
|
||||
("6. Aerospace-style clevis bracket with lightening cutouts", "benchmark_06_clevis_bracket_lightening_cutouts.step", validate_06),
|
||||
("7. Radial-engine-style cylinder with cooling fins", "benchmark_07_radial_engine_cylinder.step", validate_07),
|
||||
("8. Centrifugal impeller with backward-curved blades", "benchmark_08_centrifugal_impeller.step", validate_08),
|
||||
("9. Spiral staircase with helical handrail", "benchmark_09_spiral_staircase.step", validate_09),
|
||||
("10. Simplified planetary gear stage", "benchmark_10_planetary_gear_stage.step", validate_10),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures = []
|
||||
for title, filename, validator in VALIDATORS:
|
||||
step_path = ROOT / filename
|
||||
try:
|
||||
shape = import_step(step_path)
|
||||
validator(shape)
|
||||
except Exception as exc:
|
||||
failures.append((title, exc))
|
||||
print(f"FAIL {title}: {exc}")
|
||||
else:
|
||||
print(f"PASS {title}")
|
||||
|
||||
if failures:
|
||||
print(f"\n{len(failures)} benchmark(s) failed.")
|
||||
return 1
|
||||
|
||||
print("\nAll benchmark checks passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"request": "生成中心距120mm、大头孔36mm、小头孔20mm、厚14mm的发动机连杆,导出STEP和STL,后续可以修改参数",
|
||||
"route": {
|
||||
"selected_backend": "build123d",
|
||||
"runner_skill": "cad",
|
||||
"confidence": "high",
|
||||
"fallback_order": [
|
||||
"simplecadapi",
|
||||
"cadam"
|
||||
],
|
||||
"decision_path": "route-decision.json"
|
||||
},
|
||||
"source": {
|
||||
"path": "connecting_rod.py",
|
||||
"format": "python",
|
||||
"backend": "build123d"
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"path": "connecting_rod.step",
|
||||
"role": "primary"
|
||||
},
|
||||
{
|
||||
"path": "connecting_rod.stl",
|
||||
"role": "secondary"
|
||||
},
|
||||
{
|
||||
"path": "connecting_rod_20260722T071141Z.png",
|
||||
"role": "verification_snapshot"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"center_distance_mm": 120.0,
|
||||
"thickness_mm": 14.0,
|
||||
"big_end_outer_diameter_mm": 56.0,
|
||||
"big_end_bore_diameter_mm": 36.0,
|
||||
"small_end_outer_diameter_mm": 36.0,
|
||||
"small_end_bore_diameter_mm": 20.0,
|
||||
"web_width_mm": 24.0,
|
||||
"pocket_depth_each_side_mm": 3.0
|
||||
},
|
||||
"assumptions": [
|
||||
"This V1 sample demonstrates routing and editability rather than a production-rated engine component.",
|
||||
"Material, fatigue loads, bearing fits, lubrication, fasteners, tolerances, and split big-end construction are intentionally out of scope."
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"check": "closed solid count",
|
||||
"result": 1,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"check": "bounding box millimeters",
|
||||
"result": [
|
||||
166.0,
|
||||
56.0,
|
||||
14.0
|
||||
],
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"check": "volume cubic millimeters",
|
||||
"result": 50729.813,
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"check": "bore radii millimeters",
|
||||
"result": [
|
||||
10.0,
|
||||
18.0
|
||||
],
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"check": "STEP topology",
|
||||
"result": {
|
||||
"shape_count": 1,
|
||||
"face_count": 18,
|
||||
"edge_count": 42,
|
||||
"warnings": []
|
||||
},
|
||||
"status": "pass"
|
||||
},
|
||||
{
|
||||
"check": "isometric snapshot review",
|
||||
"result": "Both bores, the connected web, and the top pocket are visibly present; no disconnected body is visible.",
|
||||
"status": "pass"
|
||||
}
|
||||
],
|
||||
"viewer_links": [
|
||||
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1&file=connecting_rod.step"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"request": "Generate a parameterized perforated vase for 3D printing",
|
||||
"route": {
|
||||
"selected_backend": "cadam",
|
||||
"project": "CADAM",
|
||||
"runner_skill": "cad-router",
|
||||
"adapter": "skills/cad-router/scripts/cadam_compile.mjs",
|
||||
"fallback_order": [
|
||||
"build123d",
|
||||
"simplecadapi"
|
||||
],
|
||||
"workflow_profiles": [
|
||||
"caddesigner_ecip"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"path": "cadam_parametric_vase.scad",
|
||||
"format": "scad",
|
||||
"backend": "cadam"
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"path": "cadam_parametric_vase.stl",
|
||||
"role": "primary"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"height_mm": 80,
|
||||
"base_radius_mm": 30,
|
||||
"top_radius_mm": 24,
|
||||
"wall_mm": 2.4,
|
||||
"base_mm": 3
|
||||
},
|
||||
"assumptions": [
|
||||
"The repeated perforations are decorative and are not load-bearing features."
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"check": "CADAM OpenSCAD WASM compile",
|
||||
"status": "passed",
|
||||
"details": "exit code 0; manifold backend reported NoError; 3816 vertices and 7868 facets"
|
||||
},
|
||||
{
|
||||
"check": "CAD Viewer review",
|
||||
"status": "passed",
|
||||
"details": "STL loaded from the task-scoped cad-router-v1 directory"
|
||||
}
|
||||
],
|
||||
"project_contributions": {
|
||||
"text-to-cad": "router, artifact contract, validation, and CAD Viewer",
|
||||
"SimpleCADAPI": "available specialist mechanical fallback",
|
||||
"CADAM": "SCAD source and OpenSCAD-WASM mesh execution",
|
||||
"CADDesigner-Code": "ECIP brief and bounded execution/visual-repair workflow"
|
||||
},
|
||||
"viewer_links": [
|
||||
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1&file=cadam_parametric_vase.stl"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// CADAM/OpenSCAD-WASM route sample for CAD Router V1.
|
||||
$fn = 96;
|
||||
|
||||
height = 80;
|
||||
base_radius = 30;
|
||||
top_radius = 24;
|
||||
wall = 2.4;
|
||||
base = 3;
|
||||
|
||||
difference() {
|
||||
cylinder(h = height, r1 = base_radius, r2 = top_radius);
|
||||
translate([0, 0, base])
|
||||
cylinder(
|
||||
h = height,
|
||||
r1 = base_radius - wall,
|
||||
r2 = top_radius - wall
|
||||
);
|
||||
|
||||
for (z = [14 : 14 : 70]) {
|
||||
for (angle = [0 : 30 : 330]) {
|
||||
rotate([0, 0, angle + z * 0.75])
|
||||
translate([base_radius - 1.0 - z * 0.07, 0, z])
|
||||
rotate([0, 90, 0])
|
||||
cylinder(h = wall + 5, r = 2.2, center = true, $fn = 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Parametric V1 connecting-rod sample routed to the build123d backend."""
|
||||
|
||||
from build123d import Align, Box, BuildPart, BuildSketch, Cylinder, Location, Locations, Mode, Plane, SlotOverall, extrude
|
||||
|
||||
|
||||
CENTER_DISTANCE = 120.0
|
||||
THICKNESS = 14.0
|
||||
|
||||
BIG_END_OUTER_DIAMETER = 56.0
|
||||
BIG_END_BORE_DIAMETER = 36.0
|
||||
SMALL_END_OUTER_DIAMETER = 36.0
|
||||
SMALL_END_BORE_DIAMETER = 20.0
|
||||
|
||||
WEB_WIDTH = 24.0
|
||||
POCKET_LENGTH = 72.0
|
||||
POCKET_WIDTH = 12.0
|
||||
POCKET_DEPTH = 3.0
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return a closed, STEP-ready connecting-rod solid in millimeters."""
|
||||
half_distance = CENTER_DISTANCE / 2.0
|
||||
|
||||
with BuildPart() as rod:
|
||||
with Locations(Location((-half_distance, 0.0, 0.0))):
|
||||
Cylinder(
|
||||
radius=BIG_END_OUTER_DIAMETER / 2.0,
|
||||
height=THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
with Locations(Location((half_distance, 0.0, 0.0))):
|
||||
Cylinder(
|
||||
radius=SMALL_END_OUTER_DIAMETER / 2.0,
|
||||
height=THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
Box(
|
||||
CENTER_DISTANCE,
|
||||
WEB_WIDTH,
|
||||
THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
with Locations(Location((-half_distance, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=BIG_END_BORE_DIAMETER / 2.0,
|
||||
height=THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
with Locations(Location((half_distance, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=SMALL_END_BORE_DIAMETER / 2.0,
|
||||
height=THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
with BuildSketch(Plane.XY):
|
||||
SlotOverall(width=POCKET_LENGTH, height=POCKET_WIDTH)
|
||||
extrude(amount=POCKET_DEPTH, mode=Mode.SUBTRACT)
|
||||
|
||||
with BuildSketch(Plane.XY.offset(THICKNESS - POCKET_DEPTH)):
|
||||
SlotOverall(width=POCKET_LENGTH, height=POCKET_WIDTH)
|
||||
extrude(amount=POCKET_DEPTH, mode=Mode.SUBTRACT)
|
||||
|
||||
result = rod.part
|
||||
result.label = "cad_router_v1_connecting_rod"
|
||||
return result
|
||||
|
After Width: | Height: | Size: 410 KiB |
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"request": "参考图片生成一个8孔圆法兰,中心孔直径20毫米,外径50毫米,厚度4毫米,螺丝孔使用M3",
|
||||
"route": {
|
||||
"selected_backend": "build123d",
|
||||
"runner_skill": "cad",
|
||||
"project": "text-to-cad",
|
||||
"fallback_order": [
|
||||
"simplecadapi",
|
||||
"cadam"
|
||||
],
|
||||
"workflow_profiles": [
|
||||
"caddesigner_ecip",
|
||||
"requirement_refinement",
|
||||
"visual_repair"
|
||||
],
|
||||
"downstream_skills": [
|
||||
"cad-viewer"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"path": "flange_8x_m3.py",
|
||||
"format": "python",
|
||||
"backend": "build123d"
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"path": "flange_8x_m3.step",
|
||||
"role": "primary"
|
||||
},
|
||||
{
|
||||
"path": "flange_8x_m3.stl",
|
||||
"role": "secondary"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"outer_diameter_mm": 50.0,
|
||||
"inner_diameter_mm": 20.0,
|
||||
"thickness_mm": 4.0,
|
||||
"bolt_hole_count": 8,
|
||||
"bolt_hole_diameter_mm": 3.4,
|
||||
"bolt_circle_diameter_mm": 40.0,
|
||||
"angular_spacing_deg": 45.0
|
||||
},
|
||||
"assumptions": [
|
||||
"M3螺丝孔按普通间隙通孔处理,孔径采用3.4毫米,不是M3攻丝底孔。",
|
||||
"未指定分度圆,采用40毫米分度圆,以保持孔到外圆约3.3毫米的最小径向边距。",
|
||||
"参考图中的车削纹理作为外观参考,不生成削弱4毫米薄法兰的深槽。"
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"path": "validation.json",
|
||||
"status": "passed",
|
||||
"checks": {
|
||||
"step_valid": true,
|
||||
"solid_count": 1,
|
||||
"exact_size_mm": [50.0, 50.0, 4.0],
|
||||
"m3_clearance_hole_count": 8,
|
||||
"m3_clearance_hole_diameter_mm": 3.4,
|
||||
"bolt_circle_diameter_mm": 40.0,
|
||||
"angular_spacing_deg": 45.0,
|
||||
"visual_review": "passed_iso_opposite_top_front"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewer_links": [
|
||||
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fflange_8x_m3&file=flange_8x_m3.step"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Parametric eight-hole circular flange generated with build123d."""
|
||||
|
||||
from math import cos, radians, sin
|
||||
|
||||
from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
|
||||
OUTER_DIAMETER_MM = 50.0
|
||||
INNER_DIAMETER_MM = 20.0
|
||||
THICKNESS_MM = 4.0
|
||||
BOLT_HOLE_COUNT = 8
|
||||
M3_CLEARANCE_DIAMETER_MM = 3.4
|
||||
BOLT_CIRCLE_DIAMETER_MM = 40.0
|
||||
|
||||
|
||||
def polar_point(radius: float, angle_degrees: float) -> tuple[float, float]:
|
||||
angle = radians(angle_degrees)
|
||||
return radius * cos(angle), radius * sin(angle)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return one STEP-ready flange solid in millimeters."""
|
||||
with BuildPart() as flange:
|
||||
Cylinder(
|
||||
radius=OUTER_DIAMETER_MM / 2.0,
|
||||
height=THICKNESS_MM,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
with Locations(Location((0.0, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=INNER_DIAMETER_MM / 2.0,
|
||||
height=THICKNESS_MM + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for index in range(BOLT_HOLE_COUNT):
|
||||
angle_degrees = 360.0 * index / BOLT_HOLE_COUNT
|
||||
x_pos, y_pos = polar_point(BOLT_CIRCLE_DIAMETER_MM / 2.0, angle_degrees)
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=M3_CLEARANCE_DIAMETER_MM / 2.0,
|
||||
height=THICKNESS_MM + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = flange.part
|
||||
part.label = "flange_8x_m3_clearance"
|
||||
return part
|
||||
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 255 KiB |