Thumbnail Catalog Generation

Create visual catalogs of CAD parts with auto-generated thumbnails.

Workflow Overview

CAD Library
Parts Collection
Processing
Translate to SVF2
Extract Thumbnails
Extract Metadata
Catalog Generation
Create Thumbnail Grid
Build Index Page
Generate Search Data
Catalog Output
HTML Gallery
JSON Index
PDF Catalog
InputProcessGenerateOutput

CLI Approach

Step 1: Generate Thumbnails for All Parts

mkdir -p ./catalog/thumbnails

raps object list cad-library --output json | jq -r '.[].key' | while read key; do
  URN=$(raps object urn cad-library "$key" --output plain)

  # Check if translation exists and is complete
  STATUS=$(raps translate manifest "$URN" 2>/dev/null | jq -r '.status // "none"')

  if [ "$STATUS" = "success" ]; then
    # Generate safe filename
    SAFE_NAME=$(echo "$key" | tr '/' '_' | sed 's/\.[^.]*$//')

    # Download thumbnail
    raps derivative thumbnail "$URN" --output "./catalog/thumbnails/${SAFE_NAME}.png"
    echo "Generated: ${SAFE_NAME}.png"
  else
    echo "Skipping (not translated): $key"
  fi
done

Step 2: Extract Part Metadata

echo "[]" > ./catalog/parts.json

raps object list cad-library --output json | jq -r '.[].key' | while read key; do
  URN=$(raps object urn cad-library "$key" --output plain)

  STATUS=$(raps translate manifest "$URN" 2>/dev/null | jq -r '.status // "none"')

  if [ "$STATUS" = "success" ]; then
    SAFE_NAME=$(echo "$key" | tr '/' '_' | sed 's/\.[^.]*$//')

    # Get properties
    PROPS=$(raps derivative properties "$URN" --output json 2>/dev/null | jq '.[0].properties // {}')

    # Add to catalog
    jq --arg key "$key" \
       --arg name "$SAFE_NAME" \
       --argjson props "$PROPS" \
       '. += [{key: $key, name: $name, thumbnail: ($name + ".png"), properties: $props}]' \
       ./catalog/parts.json > ./catalog/parts.tmp.json

    mv ./catalog/parts.tmp.json ./catalog/parts.json
  fi
done

Step 3: Generate HTML Catalog

cat > ./catalog/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
  <title>Parts Catalog</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
    .part { border: 1px solid #ddd; padding: 10px; text-align: center; border-radius: 8px; }
    .part img { max-width: 100%; height: 150px; object-fit: contain; }
    .part h3 { font-size: 14px; margin: 10px 0 5px; }
    .part p { font-size: 12px; color: #666; margin: 0; }
    .search { margin-bottom: 20px; padding: 10px; width: 100%; font-size: 16px; }
  </style>
</head>
<body>
  <h1>Parts Catalog</h1>
  <input type="text" class="search" placeholder="Search parts..." onkeyup="filterParts(this.value)">
  <div class="grid" id="catalog"></div>

  <script>
    let parts = [];

    fetch('parts.json')
      .then(r => r.json())
      .then(data => {
        parts = data;
        renderParts(parts);
      });

    function renderParts(data) {
      const grid = document.getElementById('catalog');
      grid.innerHTML = data.map(p => `
        <div class="part">
          <img src="thumbnails/${p.thumbnail}" alt="${p.name}">
          <h3>${p.name}</h3>
          <p>${p.properties['Part Number'] || ''}</p>
          <p>${p.properties['Material'] || ''}</p>
        </div>
      `).join('');
    }

    function filterParts(query) {
      const filtered = parts.filter(p =>
        p.name.toLowerCase().includes(query.toLowerCase()) ||
        JSON.stringify(p.properties).toLowerCase().includes(query.toLowerCase())
      );
      renderParts(filtered);
    }
  </script>
</body>
</html>
EOF

echo "Catalog generated at ./catalog/index.html"

CI/CD Pipeline

# .github/workflows/catalog-generation.yml
name: Parts Catalog Generation

on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM
  workflow_dispatch:

jobs:
  generate-catalog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install RAPS
        run: cargo install raps

      - name: Generate thumbnails
        env:
          APS_CLIENT_ID: ${{ secrets.APS_CLIENT_ID }}
          APS_CLIENT_SECRET: ${{ secrets.APS_CLIENT_SECRET }}
        run: |
          mkdir -p ./catalog/thumbnails

          raps object list cad-library --output json | jq -r '.[].key' | while read key; do
            URN=$(raps object urn cad-library "$key" --output plain)
            STATUS=$(raps translate manifest "$URN" 2>/dev/null | jq -r '.status // "none"')

            if [ "$STATUS" = "success" ]; then
              SAFE_NAME=$(echo "$key" | tr '/' '_' | sed 's/\.[^.]*$//')
              raps derivative thumbnail "$URN" --output "./catalog/thumbnails/${SAFE_NAME}.png" 2>/dev/null || true
            fi
          done

      - name: Generate metadata
        env:
          APS_CLIENT_ID: ${{ secrets.APS_CLIENT_ID }}
          APS_CLIENT_SECRET: ${{ secrets.APS_CLIENT_SECRET }}
        run: |
          echo "[]" > ./catalog/parts.json

          raps object list cad-library --output json | jq -r '.[].key' | while read key; do
            URN=$(raps object urn cad-library "$key" --output plain)
            STATUS=$(raps translate manifest "$URN" 2>/dev/null | jq -r '.status // "none"')

            if [ "$STATUS" = "success" ]; then
              SAFE_NAME=$(echo "$key" | tr '/' '_' | sed 's/\.[^.]*$//')
              PROPS=$(raps derivative properties "$URN" --output json 2>/dev/null | jq '.[0].properties // {}' || echo '{}')

              jq --arg key "$key" --arg name "$SAFE_NAME" --argjson props "$PROPS" \
                '. += [{key: $key, name: $name, thumbnail: ($name + ".png"), properties: $props}]' \
                ./catalog/parts.json > ./catalog/parts.tmp.json
              mv ./catalog/parts.tmp.json ./catalog/parts.json
            fi
          done

      - name: Generate HTML
        run: |
          # Generate index.html (see CLI section for template)
          cat > ./catalog/index.html << 'HTMLEOF'
          <!-- HTML template here -->
          HTMLEOF

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./catalog

Pipeline Flow

Schedule/Manual
Daily 2 AM
Generation
List all parts
Generate thumbnails
Extract metadata
Build HTML
Deploy
GitHub Pages

MCP Integration

Generate Catalog

User: "Create a visual catalog of all parts in the CAD library"
User
AI Assistant
RAPS MCP
APS Cloud
User
Create parts catalog
AI
aps_object_list(bucket=“cad-library”)
MCP
List objects
APS
Object list
loop: For each part
AI
aps_derivative_thumbnail(urn)
MCP
Get thumbnail
APS
PNG data
AI
aps_derivative_properties(urn)
MCP
Get properties
APS
Properties JSON
AI
Generate HTML catalog
AI
Catalog ready with link

AI Response:

Generated parts catalog with 45 items:

Catalog Statistics:

  • Total parts: 45
  • With thumbnails: 43
  • With metadata: 45

Categories:

  • Brackets: 12
  • Housings: 8
  • Shafts: 10
  • Gears: 15

View Catalog

Search Catalog

User: "Find all aluminum parts in the catalog"

AI Response:

Found 18 aluminum parts:

PartMaterialWeight
bracket-aAluminum 60610.45 kg
housing-mainAluminum 70752.3 kg
cover-plateAluminum 60610.12 kg

Advanced Features

Category Organization

# Organize by part type
cat ./catalog/parts.json | jq '
  group_by(.properties["Part Type"] // "Uncategorized") |
  map({
    category: .[0].properties["Part Type"] // "Uncategorized",
    count: length,
    parts: .
  })'

Generate PDF Catalog

# Using wkhtmltopdf
wkhtmltopdf \
  --enable-local-file-access \
  --print-media-type \
  ./catalog/index.html \
  ./catalog/parts-catalog.pdf

Incremental Updates

# Only update changed parts
LAST_RUN=$(cat .catalog-last-run 2>/dev/null || echo "1970-01-01")

raps object list cad-library --output json | \
  jq --arg since "$LAST_RUN" '.[] | select(.lastModified > $since)' | \
  jq -r '.key' | while read key; do
    echo "Updating: $key"
    # Generate thumbnail and metadata for this part
  done

date -Iseconds > .catalog-last-run