Skip to content

Fix documentation: update animation frame size to 720x720 #74

Fix documentation: update animation frame size to 720x720

Fix documentation: update animation frame size to 720x720 #74

name: OpenSCAD Render and Export
on:
push:
paths:
- 'LifeTrac-v25/mechanical_design/**/*.scad'
- '.github/workflows/openscad-render.yml'
pull_request:
paths:
- 'LifeTrac-v25/mechanical_design/**/*.scad'
workflow_dispatch:
jobs:
render:
runs-on: ubuntu-latest
name: Generate Renders and Outputs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for --force-with-lease to work correctly
- name: Install OpenSCAD
run: |
sudo apt-get update
sudo apt-get install -y openscad xvfb x11-utils
openscad --version
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x16 &
XVFB_PID=$!
echo "XVFB_PID=$XVFB_PID" >> $GITHUB_ENV
# Wait for Xvfb to be ready (max 10 seconds)
for i in {1..10}; do
if xdpyinfo -display :99 >/dev/null 2>&1; then
echo "Xvfb is ready on display :99"
break
fi
if [ $i -eq 10 ]; then
echo "Timeout waiting for Xvfb to start"
exit 1
fi
sleep 1
done
echo "DISPLAY=:99" >> $GITHUB_ENV
- name: Create output directories
run: |
cd LifeTrac-v25/mechanical_design
mkdir -p output/{dxf,renders,animations}
- name: Calculate camera position
run: |
cd LifeTrac-v25/mechanical_design
# Calculate camera position dynamically based on machine dimensions
# Extract parameters from OpenSCAD file
MACHINE_HEIGHT=$(grep -E "^MACHINE_HEIGHT = " openscad/lifetrac_v25.scad | grep -oE "[0-9]+\.?[0-9]*" || echo "1000")
WHEEL_BASE=$(grep -E "^WHEEL_BASE = " openscad/lifetrac_v25.scad | grep -oE "[0-9]+\.?[0-9]*" || echo "1400")
GROUND_CLEARANCE=$(grep -E "^GROUND_CLEARANCE = " openscad/lifetrac_v25.scad | grep -oE "[0-9]+\.?[0-9]*" || echo "150")
ARM_MAX_ANGLE=$(grep -E "^ARM_MAX_ANGLE = " openscad/lifetrac_v25.scad | grep -oE "[0-9]+\.?[0-9]*" || echo "60")
BUCKET_HEIGHT=$(grep -E "^BUCKET_HEIGHT = " openscad/lifetrac_v25.scad | grep -oE "[0-9]+\.?[0-9]*" || echo "450")
# Validate all extracted parameters
if [ -z "$MACHINE_HEIGHT" ] || [ -z "$WHEEL_BASE" ] || [ -z "$GROUND_CLEARANCE" ] || [ -z "$ARM_MAX_ANGLE" ] || [ -z "$BUCKET_HEIGHT" ]; then
echo "ERROR: Failed to extract required parameters from OpenSCAD file"
echo " MACHINE_HEIGHT: $MACHINE_HEIGHT"
echo " WHEEL_BASE: $WHEEL_BASE"
echo " GROUND_CLEARANCE: $GROUND_CLEARANCE"
echo " ARM_MAX_ANGLE: $ARM_MAX_ANGLE"
echo " BUCKET_HEIGHT: $BUCKET_HEIGHT"
exit 1
fi
# Calculate key dimensions
FRAME_Z_OFFSET=$GROUND_CLEARANCE
ARM_PIVOT_Z=$((FRAME_Z_OFFSET + MACHINE_HEIGHT - 50))
WHEEL_MIDPOINT_Y=$((WHEEL_BASE / 2))
# Calculate maximum height when arms are raised to ARM_MAX_ANGLE
# ARM_LENGTH_APPROX: Approximation of arm length based on design geometry
# This is derived from the OpenSCAD formula: ARM_LENGTH = ceil(sqrt(pow(ARM_PIVOT_Z, 2) + pow(MIN_HORIZONTAL_REACH, 2)))
# For the current machine dimensions (ARM_PIVOT_Z=1100mm, MIN_HORIZONTAL_REACH≈1200mm), this yields approximately 1600mm
# If machine geometry changes significantly, this value should be recalculated or extracted from OpenSCAD
ARM_LENGTH_APPROX=1600
# Max height = ARM_PIVOT_Z + ARM_LENGTH * sin(ARM_MAX_ANGLE) + BUCKET_HEIGHT
MAX_ARM_TIP_HEIGHT=$(awk "BEGIN {pi=3.14159; print int($ARM_PIVOT_Z + $ARM_LENGTH_APPROX * sin($ARM_MAX_ANGLE * pi / 180) + $BUCKET_HEIGHT)}")
# Calculate camera position to frame the scene properly
# Bottom margin: wheels at ground (Z=0) should be near bottom with small margin
# Top margin: max arm height should reach near top with small margin
# Height range to capture: 0 to MAX_ARM_TIP_HEIGHT
SCENE_HEIGHT=$MAX_ARM_TIP_HEIGHT
MARGIN_FACTOR="1.15" # 15% margin on top and bottom
# Center point: halfway between ground and max height, at wheel midpoint Y
CENTER_Y=$WHEEL_MIDPOINT_Y
CENTER_Z=$(awk "BEGIN {print int($SCENE_HEIGHT / 2)}")
# Camera distance: calculate to fit scene in square 720x720 frame
# Using 45-degree isometric view angle (sqrt(2) factor for diagonal)
CAMERA_DISTANCE=$(awk "BEGIN {print int($SCENE_HEIGHT * $MARGIN_FACTOR * 1.414)}")
echo "=== Camera Calculation ==="
echo "Machine dimensions:"
echo " MACHINE_HEIGHT: $MACHINE_HEIGHT mm"
echo " WHEEL_BASE: $WHEEL_BASE mm"
echo " GROUND_CLEARANCE: $GROUND_CLEARANCE mm"
echo " ARM_MAX_ANGLE: $ARM_MAX_ANGLE degrees"
echo " BUCKET_HEIGHT: $BUCKET_HEIGHT mm"
echo "Calculated values:"
echo " ARM_PIVOT_Z: $ARM_PIVOT_Z mm"
echo " ARM_LENGTH_APPROX: $ARM_LENGTH_APPROX mm (see comment for derivation)"
echo " MAX_ARM_TIP_HEIGHT: $MAX_ARM_TIP_HEIGHT mm"
echo " SCENE_HEIGHT: $SCENE_HEIGHT mm"
echo " CAMERA_DISTANCE: $CAMERA_DISTANCE mm"
echo " CENTER_Y: $CENTER_Y mm"
echo " CENTER_Z: $CENTER_Z mm"
echo "Camera position: --camera=$CAMERA_DISTANCE,$CAMERA_DISTANCE,$CAMERA_DISTANCE,0,$CENTER_Y,$CENTER_Z"
echo "=========================="
# Export calculated values as environment variables for subsequent steps
echo "CAMERA_DISTANCE=$CAMERA_DISTANCE" >> $GITHUB_ENV
echo "CENTER_Y=$CENTER_Y" >> $GITHUB_ENV
echo "CENTER_Z=$CENTER_Z" >> $GITHUB_ENV
- name: Generate assembly.png and cnclayout.svg
run: |
cd LifeTrac-v25/mechanical_design
# Generate assembly.png (720x720 square as requested)
echo "Generating assembly.png..."
openscad -o assembly.png \
--camera=$CAMERA_DISTANCE,$CAMERA_DISTANCE,$CAMERA_DISTANCE,0,$CENTER_Y,$CENTER_Z \
--imgsize=720,720 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Generate cnclayout.svg (2D projection for CNC cutting)
echo "Generating cnclayout.svg..."
openscad -o cnclayout.svg \
--projection=o \
--viewall \
cnclayout.scad
echo "✓ assembly.png and cnclayout.svg generated successfully"
ls -lh assembly.png cnclayout.svg
- name: Render preview images
run: |
cd LifeTrac-v25/mechanical_design
# Use camera parameters calculated in previous step (from environment variables)
# Main assembly view (720x720 square)
echo "Rendering main assembly..."
openscad -o output/renders/lifetrac_v25_main.png \
--camera=$CAMERA_DISTANCE,$CAMERA_DISTANCE,$CAMERA_DISTANCE,0,$CENTER_Y,$CENTER_Z \
--imgsize=720,720 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Front view (720x720 square, uses calculated camera position)
echo "Rendering front view..."
openscad -o output/renders/lifetrac_v25_front.png \
--camera=0,$CAMERA_DISTANCE,$CENTER_Z,0,$CENTER_Y,$CENTER_Z \
--imgsize=720,720 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Side view (720x720 square, uses calculated camera position)
echo "Rendering side view..."
openscad -o output/renders/lifetrac_v25_side.png \
--camera=$CAMERA_DISTANCE,$CENTER_Y,$CENTER_Z,0,$CENTER_Y,$CENTER_Z \
--imgsize=720,720 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Top view (720x720 square, uses calculated camera position)
echo "Rendering top view..."
openscad -o output/renders/lifetrac_v25_top.png \
--camera=0,$CENTER_Y,$CAMERA_DISTANCE,0,$CENTER_Y,$CENTER_Z \
--imgsize=720,720 \
--projection=o \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
echo "Preview images rendered successfully"
- name: Render module examples
run: |
cd LifeTrac-v25/mechanical_design
# Render each module's example
for module in modules/*.scad; do
filename=$(basename "$module" .scad)
echo "Rendering $filename examples..."
openscad -o "output/renders/${filename}_examples.png" \
--camera=500,500,500,0,0,0 \
--imgsize=1600,900 \
--projection=p \
--colorscheme=Nature \
"$module"
done
- name: Generate animation frames
run: |
cd LifeTrac-v25/mechanical_design
# Use camera parameters calculated in previous step (from environment variables)
echo "Generating animation frames with calculated camera position..."
echo "Camera: $CAMERA_DISTANCE,$CAMERA_DISTANCE,$CAMERA_DISTANCE,0,$CENTER_Y,$CENTER_Z"
mkdir -p output/animations/frames
# Generate 36 frames for animation (every 10 degrees)
# Using dynamically calculated camera position that adapts to machine dimensions
total_frames=36
for i in $(seq 0 $((total_frames-1))); do
# Use total_frames-1 so last frame lands exactly at t=1.0
t=$(awk "BEGIN {print $i/($total_frames-1)}")
printf "Frame %d (t=%.4f)\n" "$i" "$t"
openscad -o "output/animations/frames/frame_$(printf %03d $i).png" \
--camera=$CAMERA_DISTANCE,$CAMERA_DISTANCE,$CAMERA_DISTANCE,0,$CENTER_Y,$CENTER_Z \
--imgsize=720,720 \
--projection=p \
--colorscheme=Nature \
-D "animation_time=$t" \
-D "\$t=$t" \
openscad/lifetrac_v25.scad
done
echo "Animation frames generated"
- name: Create animation GIF
run: |
sudo apt-get install -y imagemagick
cd LifeTrac-v25/mechanical_design/output/animations
echo "Creating animation GIF of arm movement..."
convert -delay 10 -loop 0 frames/frame_*.png lifetrac_v25_animation.gif
# Copy to parent directory for easy access
cp lifetrac_v25_animation.gif ../../lifetrac_v25_animation.gif
echo "✓ Animation GIF created successfully"
ls -lh ../../lifetrac_v25_animation.gif
- name: Generate summary report
run: |
cd LifeTrac-v25/mechanical_design
cat > output/RENDER_REPORT.md << 'EOF'
# OpenSCAD Render Report
**Generated:** $(date)
**Commit:** ${{ github.sha }}
**Branch:** ${{ github.ref_name }}
## Files Generated
### Preview Renders
- Main assembly view
- Front view
- Side view
- Top view
- Module examples (5 modules)
### Animations
- 36 animation frames (720x720 PNG square format)
- Animated GIF showing arm movement (lifetrac_v25_animation.gif)
### CNC Files
- assembly.png (3D render of complete assembly)
- cnclayout.svg (2D layout for CNC cutting)
## Next Steps
1. Review rendered images in the artifacts
2. View lifetrac_v25_animation.gif to see arm movement
3. Use cnclayout.svg for CNC plasma cutting
4. Use animation frames to create videos
5. Export DXF files for individual parts using export_all_cnc_parts.sh
6. Check the Structural Analysis workflow for design validation results
---
Generated by GitHub Actions
EOF
- name: Upload render artifacts
uses: actions/upload-artifact@v4
with:
name: openscad-renders
path: |
LifeTrac-v25/mechanical_design/output/renders/
LifeTrac-v25/mechanical_design/output/RENDER_REPORT.md
retention-days: 90
- name: Upload animation artifacts
uses: actions/upload-artifact@v4
with:
name: openscad-animations
path: LifeTrac-v25/mechanical_design/output/animations/
retention-days: 90
- name: Extract specifications and update README
run: |
cd LifeTrac-v25
echo "Extracting specifications from OpenSCAD file..."
# Create Python script to extract specs
cat > /tmp/extract_specs.py << 'PYTHON_EOF'
import re
import sys
# Read the OpenSCAD file
try:
with open('mechanical_design/openscad/lifetrac_v25.scad', 'r') as f:
content = f.read()
except FileNotFoundError:
print("ERROR: OpenSCAD file not found", file=sys.stderr)
sys.exit(1)
# Extract key dimensions with error handling
def extract_value(pattern, content, name):
try:
match = re.search(pattern, content)
if match:
return float(match.group(1))
print(f"WARNING: Could not find {name} in OpenSCAD file", file=sys.stderr)
return None
except (ValueError, AttributeError) as e:
print(f"ERROR: Failed to extract {name}: {e}", file=sys.stderr)
return None
MACHINE_WIDTH = extract_value(r'MACHINE_WIDTH\s*=\s*(\d+)', content, 'MACHINE_WIDTH')
MACHINE_LENGTH = extract_value(r'MACHINE_LENGTH\s*=\s*(\d+)', content, 'MACHINE_LENGTH')
MACHINE_HEIGHT = extract_value(r'MACHINE_HEIGHT\s*=\s*(\d+)', content, 'MACHINE_HEIGHT')
WHEEL_DIAMETER = extract_value(r'WHEEL_DIAMETER\s*=\s*(\d+)', content, 'WHEEL_DIAMETER')
GROUND_CLEARANCE = extract_value(r'GROUND_CLEARANCE\s*=\s*(\d+)', content, 'GROUND_CLEARANCE')
HYDRAULIC_PRESSURE_PSI = extract_value(r'HYDRAULIC_PRESSURE_PSI\s*=\s*(\d+)', content, 'HYDRAULIC_PRESSURE_PSI')
LIFT_CYLINDER_BORE = extract_value(r'LIFT_CYLINDER_BORE\s*=\s*([\d.]+)', content, 'LIFT_CYLINDER_BORE')
# Validate that all required values were found
required_values = {
'MACHINE_WIDTH': MACHINE_WIDTH,
'MACHINE_LENGTH': MACHINE_LENGTH,
'MACHINE_HEIGHT': MACHINE_HEIGHT,
'WHEEL_DIAMETER': WHEEL_DIAMETER,
'GROUND_CLEARANCE': GROUND_CLEARANCE,
'HYDRAULIC_PRESSURE_PSI': HYDRAULIC_PRESSURE_PSI,
'LIFT_CYLINDER_BORE': LIFT_CYLINDER_BORE
}
missing_values = [k for k, v in required_values.items() if v is None]
if missing_values:
print(f"ERROR: Missing required values: {', '.join(missing_values)}", file=sys.stderr)
print("The following patterns failed to match:", file=sys.stderr)
for val in missing_values:
print(f" - {val}", file=sys.stderr)
print("Skipping README update to avoid breaking existing content", file=sys.stderr)
print("If OpenSCAD file structure changed, update the regex patterns in this workflow", file=sys.stderr)
sys.exit(0) # Exit gracefully without updating
# Conservative lift capacity estimate based on typical loader geometry
# NOTE: This is a simplified estimate. The actual lift capacity calculation in the
# OpenSCAD file uses complex lever arm geometry and moment calculations which would
# require additional extraction logic. For now, we use a conservative estimate.
# If design changes significantly, this value should be reviewed and updated.
ESTIMATED_LIFT_CAPACITY_KG = 1200
ESTIMATED_LIFT_CAPACITY_LBS = ESTIMATED_LIFT_CAPACITY_KG * 2.205
# Read current README
with open('README.md', 'r') as f:
readme = f.read()
# Build the specifications table
specs_table = f"""| Specification | Value (mm) | Value (inches) | Value (meters) |
|--------------|------------|----------------|----------------|
| **Overall Width** | {int(MACHINE_WIDTH):,} mm | {MACHINE_WIDTH/25.4:.1f}" | {MACHINE_WIDTH/1000:.2f} m |
| **Overall Length** | {int(MACHINE_LENGTH):,} mm | {MACHINE_LENGTH/25.4:.1f}" | {MACHINE_LENGTH/1000:.2f} m |
| **Height to Frame Top** | {int(MACHINE_HEIGHT):,} mm | {MACHINE_HEIGHT/25.4:.1f}" | {MACHINE_HEIGHT/1000:.2f} m |
| **Ground Clearance** | {int(GROUND_CLEARANCE)} mm | {GROUND_CLEARANCE/25.4:.1f}" | {GROUND_CLEARANCE/1000:.2f} m |
| **Wheel Diameter** | {int(WHEEL_DIAMETER)} mm | {WHEEL_DIAMETER/25.4:.1f}" | {WHEEL_DIAMETER/1000:.2f} m |"""
perf_table = f"""| Specification | Value |
|--------------|-------|
| **Hydraulic System Pressure** | {int(HYDRAULIC_PRESSURE_PSI):,} PSI |
| **Estimated Lift Capacity** | ~{int(ESTIMATED_LIFT_CAPACITY_KG):,} kg (~{int(ESTIMATED_LIFT_CAPACITY_LBS):,} lbs) |
| **Drive Configuration** | All-wheel drive (4 hydraulic motors) |
| **Cylinder Bore (Lift)** | {LIFT_CYLINDER_BORE} mm ({LIFT_CYLINDER_BORE/25.4:.1f}") |"""
# Update the specs in README using regex with validation
original_readme = readme
# Update technical specifications table
updated_readme = re.sub(
r'(\| Specification \| Value \(mm\) \| Value \(inches\) \| Value \(meters\) \|.*?\n)(\| \*\*Wheel Diameter\*\* \|[^\n]+)',
specs_table,
readme,
count=1,
flags=re.DOTALL
)
if updated_readme == readme:
print("WARNING: Technical specifications table pattern not found, trying alternative pattern", file=sys.stderr)
# Try simpler pattern that looks for the table header
pattern_start = readme.find('### Technical Specifications')
if pattern_start != -1:
# Find the end of the table
table_start = readme.find('| Specification | Value (mm)', pattern_start)
if table_start != -1:
# Find next section header or blank line after table
table_end = readme.find('\n\n###', table_start)
if table_end == -1:
table_end = readme.find('\n\n| Specification | Value |', table_start)
if table_end != -1:
# Extract table and replace
old_table = readme[table_start:readme.find('\n', readme.rfind('|', table_start, table_end))+1]
updated_readme = readme.replace(old_table, specs_table + '\n', 1)
readme = updated_readme
# Update performance specifications table
updated_readme = re.sub(
r'(### Performance Specifications\s*\n\n\| Specification \| Value \|.*?\n)(\| \*\*Cylinder Bore \(Lift\)\*\* \|[^\n]+)',
f'### Performance Specifications\n\n{perf_table}',
readme,
count=1,
flags=re.DOTALL
)
if updated_readme == readme:
print("WARNING: Performance specifications table pattern not found", file=sys.stderr)
else:
readme = updated_readme
# Only write if changes were made
if readme != original_readme:
try:
with open('README.md', 'w') as f:
f.write(readme)
print("✓ README specifications updated successfully")
except IOError as e:
print(f"ERROR: Failed to write README: {e}", file=sys.stderr)
sys.exit(1)
else:
print("INFO: No changes needed to README specifications")
print(f" Width: {int(MACHINE_WIDTH)} mm")
print(f" Length: {int(MACHINE_LENGTH)} mm")
print(f" Height: {int(MACHINE_HEIGHT)} mm")
print(f" Lift Capacity: ~{int(ESTIMATED_LIFT_CAPACITY_KG)} kg")
PYTHON_EOF
python3 /tmp/extract_specs.py
echo "README specifications updated"
- name: Commit and push rendered outputs
run: |
cd LifeTrac-v25
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
# Add the generated files (mechanical_design images and updated README)
git add mechanical_design/assembly.png mechanical_design/cnclayout.svg mechanical_design/lifetrac_v25_animation.gif README.md
# Check if there are changes
if ! git diff --staged --quiet; then
git commit -m "Update rendered outputs and specifications [skip ci]"
git push --force-with-lease origin HEAD || {
echo "INFO: git push failed. This is expected on PRs from forks."
echo "The files are available as workflow artifacts."
}
else
echo "No changes to rendered output files or README"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Cleanup Xvfb
if: always()
run: |
if [ -n "$XVFB_PID" ]; then
echo "Stopping Xvfb process (PID: $XVFB_PID)"
# Try graceful shutdown first
kill -TERM $XVFB_PID 2>/dev/null || true
# Wait up to 5 seconds for process to terminate
for i in {1..5}; do
if ! kill -0 $XVFB_PID 2>/dev/null; then
echo "Xvfb stopped gracefully"
break
fi
sleep 1
done
# Force kill if still running
kill -KILL $XVFB_PID 2>/dev/null || true
echo "Xvfb force stopped"
fi
- name: Comment on PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const comment = `## OpenSCAD Render Results 🎨
✅ Rendering completed successfully!
### Generated Outputs
- 📸 Preview renders (main, front, side, top views)
- 🎬 Animation frames (36 frames)
- 🎞️ Animated GIF of arm movement (lifetrac_v25_animation.gif)
- 🔧 Module examples
- 🖼️ Assembly image and CNC layout (assembly.png, cnclayout.svg)
### Artifacts
Download the generated files from the workflow artifacts:
- \`openscad-renders\` - Preview images
- \`openscad-animations\` - Animation frames and GIF
### Committed Files
The following files have been committed to the repository:
- \`assembly.png\` - 3D assembly render
- \`cnclayout.svg\` - 2D CNC cutting layout
- \`lifetrac_v25_animation.gif\` - Animation showing arm movement
_Note: Check the **OpenSCAD Structural Analysis** workflow for design validation and structural analysis results._
**Workflow Run:** [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});