Improve animation frame generation in workflow #42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| - 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: Generate assembly.png and cnclayout.svg | |
| run: | | |
| cd LifeTrac-v25/mechanical_design | |
| # Generate assembly.png (1024x768 matching CEB-Press example) | |
| echo "Generating assembly.png..." | |
| openscad -o assembly.png \ | |
| --camera=3000,3000,2000,0,0,500 \ | |
| --imgsize=1024,768 \ | |
| --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 | |
| # Main assembly view | |
| echo "Rendering main assembly..." | |
| openscad -o output/renders/lifetrac_v25_main.png \ | |
| --camera=3000,3000,2000,0,0,500 \ | |
| --imgsize=1920,1080 \ | |
| --projection=p \ | |
| --colorscheme=Nature \ | |
| openscad/lifetrac_v25.scad | |
| # Front view | |
| echo "Rendering front view..." | |
| openscad -o output/renders/lifetrac_v25_front.png \ | |
| --camera=0,3000,1000,0,0,500 \ | |
| --imgsize=1920,1080 \ | |
| --projection=p \ | |
| --colorscheme=Nature \ | |
| openscad/lifetrac_v25.scad | |
| # Side view | |
| echo "Rendering side view..." | |
| openscad -o output/renders/lifetrac_v25_side.png \ | |
| --camera=3000,0,1000,0,0,500 \ | |
| --imgsize=1920,1080 \ | |
| --projection=p \ | |
| --colorscheme=Nature \ | |
| openscad/lifetrac_v25.scad | |
| # Top view | |
| echo "Rendering top view..." | |
| openscad -o output/renders/lifetrac_v25_top.png \ | |
| --camera=0,0,3000,0,0,500 \ | |
| --imgsize=1920,1080 \ | |
| --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 | |
| echo "Generating animation frames..." | |
| mkdir -p output/animations/frames | |
| # Generate 36 frames for animation (every 10 degrees) | |
| 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=3000,3000,2000,0,0,500 \ | |
| --imgsize=1280,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 (1280x720 PNG) | |
| - 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 | |
| }); |