Files
Messe-Lotse/plan/3d-booth-generation.md
T
2026-09-29 09:40:32 +00:00

39 KiB
Raw Blame History

goal, version, date_created, status, tags
goal version date_created status tags
Automated 3D Booth Generation via SketchUp + Aluvision + MCP 1.0 2026-06-10 Planned 3d, sketchup, aluvision, mcp, automation

Automated 3D Booth Generation — SketchUp + Aluvision + MCP

Overview

Automatically generate 3D booth models in SketchUp using Aluvision components, driven by job data from the 24HRS Messe-Lotse application. An MCP server bridges the Pimcore backend to a SketchUp Ruby extension, orchestrating component placement, BOM generation, and model export.

Reference implementation: The Expolinc Booth Designer (../sketchup-booth-designer/) for FreeCAD — identical architecture, adapted for SketchUp/Aluvision.

Architecture

┌──────────────────────────────────────────────────────────┐
│  Pimcore Backend (PHP)                                    │
│  POST /api/messebau/jobs/{id}/generate-3d                 │
│  Collects: standType, standSize, standHeight, materials,  │
│            flooring, furniture, lead magnets               │
│  Sends structured payload to MCP server                    │
└──────────────────────┬───────────────────────────────────┘
                       │  MCP protocol (stdio JSON-RPC)
                       ▼
┌──────────────────────────────────────────────────────────┐
│  MCP Server (Node.js/TypeScript)                          │
│                                                           │
│  ┌─────────────────────────────────────────────────────┐ │
│  │ 12 custom MCP tools (see Tool Catalog below)         │ │
│  │ aluvision_info, aluvision_build_booth,               │ │
│  │ aluvision_create_wall, aluvision_create_corner,       │ │
│  │ aluvision_create_storage, aluvision_create_door,      │ │
│  │ aluvision_add_backlit, aluvision_arrange_layout,      │ │
│  │ aluvision_export_bom, aluvision_export_model,         │ │
│  │ aluvision_export_drawing, aluvision_get_component_info│ │
│  └─────────────────────────────────────────────────────┘ │
│                                                           │
│  ┌─────────────────────────────────────────────────────┐ │
│  │ Layout Engine (layout_engine.ts)                     │ │
│  │ Maps standType → wall positions, rotations           │ │
│  │ Computes component placement coordinates             │ │
│  │ Handles corner alignment and grid snapping           │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                           │
│  ┌─────────────────────────────────────────────────────┐ │
│  │ BOM Generator (bom_generator.ts)                     │ │
│  │ Component count, Aluvision part numbers             │ │
│  │ Weight totals, transport case recommendations       │ │
│  │ CSV / JSON / spreadsheet output                     │ │
│  └─────────────────────────────────────────────────────┘ │
└──────────────────────┬───────────────────────────────────┘
                       │  HTTP to localhost:9494
                       ▼
┌──────────────────────────────────────────────────────────┐
│  SketchUp Ruby Extension (WEBrick HTTP server)            │
│                                                           │
│  Started automatically when SketchUp launches.            │
│  Listens on port 9494.                                    │
│                                                           │
│  Endpoints:                                               │
│  POST /booth/generate  →  Creates full booth assembly    │
│  POST /booth/export    →  Exports .skp / PNG / .glb     │
│  POST /booth/bom       →  Enumerates placed components   │
│  GET  /component/list   →  Lists Aluvision library parts  │
│  GET  /health           →  Status + SketchUp version      │
└──────────────────────────────────────────────────────────┘

Why This Architecture Works

  • SketchUp Ruby API runs exclusively inside SketchUp — cannot be called externally. The WEBrick HTTP server bridges this gap.
  • Aluvision components already exist in the SketchUp library — no need to rebuild parametric geometry. The Ruby extension loads, places, and scales existing components.
  • MCP server is stateless — each tool call is independent. The MCP server translates structured requests into HTTP calls to the SketchUp bridge.
  • Same pattern as Expolinc Booth Designer — proven architecture with FreeCAD, adapted for SketchUp.

Why Not Direct SketchUp Automation (Alternatives Considered)

Approach Problem
Ruby console via CLI arguments No structured request/response. Hard to return results.
C SDK (.skp file generation) Extremely complex. Would need to reimplement Aluvision components from scratch.
Trimble Connect API Cloud file management only — no geometry creation API.
SketchUp Viewer API Read-only. No model creation.

Data Flow: Booth Creation

User opens job detail in 24HRS → clicks "3D Booth generieren"
         │
         ▼
Pimcore backend collects job parameters:
  - standType: "Eckstand"
  - standSize: "6x4"
  - standHeight: 2.5
  - hall: "Halle 5.1 / Stand C12"
  - flooring material (from Tab 3)
  - furniture list (from Tab 3)
  - lead magnets (from Tab 3)
         │
         ▼
POST /api/messebau/jobs/{id}/generate-3d
  → invokes MCP tool: aluvision_build_booth
         │
         ▼
MCP Server (Node.js):
  1. Layout Engine resolves standType → wall topology
     "Eckstand" → corner layout: 2 walls at 90°, 2 open sides
     standSize "6x4" → wall_A=6000mm, wall_B=4000mm
  2. Computes wall positions:
     Wall 0: position (0,0,0), rotation 0° (6000mm along X)
     Wall 1: position (6000,0,0), rotation 90° (4000mm along Y)
     Corner connector at (6000,0,0)
  3. Sends HTTP POST to SketchUp bridge:
     POST http://localhost:9494/booth/generate
     Body: { walls: [...], connectors: [...], flooring: {...}, furniture: [...] }
         │
         ▼
SketchUp Ruby Extension:
  1. Opens new model or clears active model
  2. For each wall: loads Aluvision profile component, places at position with rotation
  3. For each connector: loads connector component, places at junction
  4. For flooring: creates floor plane with material from library
  5. For furniture: loads and places furniture components
  6. Saves model to temp directory
  7. Returns: { status: "generated", model_path: "...", component_count: 12 }
         │
         ▼
MCP Server receives response, generates BOM preview
         │
         ▼
Pimcore backend:
  - Renders PNG preview → shows in UI
  - Stores .skp file path → links to job
  - Stores BOM JSON on job
  - User can regenerate with tweaked params

Stand Type → Layout Mapping

24HRS standType Layout Wall Count Open Sides Description
Systemstand inline_closed 3 (back + 2 sides) 1 (front) Standard inline with 3 closed walls
Reihstand inline 1 (back) 3 Row booth with only back wall
Eckstand corner 2 at 90° 2 Corner booth, two aisles
Kopfstand peninsula 3 (U-shape) 1 (front) End-of-row, U-shaped
Blockstand island_closed 4 (rectangle) 0 Enclosed block with door
Inselstand island 4 (rectangle) 0 (open perimeter) Freestanding, all sides accessible

Wall Placement Math

Coordinate system: X = width, Y = depth, Z = height

          Wall 2 (front)
    (0,D) ┌──────────────┐ (W,D)
          │              │
 Wall 3  │    Booth     │  Wall 1
 (left)  │    W × D     │  (right)
          │              │
    (0,0) └──────────────┘ (W,0)
          Wall 0 (back)

Corner layout (Eckstand):
  Wall 0: back wall,  width=W, position=(0,0,0),       rotation=0°
  Wall 1: side wall,  width=D, position=(W,0,0),       rotation=90°
  Connector: 90° corner at (W,0,0)

Island layout (Inselstand):
  Wall 0: back,       width=W, position=(0,0,0),       rotation=0°
  Wall 1: right,      width=D, position=(W,0,0),       rotation=90°
  Wall 2: front,      width=W, position=(W,D,0),       rotation=180°
  Wall 3: left,       width=D, position=(0,D,0),       rotation=270° (-90°)
  4× 90° corner connectors at each junction

Tool Catalog

aluvision_info

Returns available Aluvision systems, profile types, sizes, finishes, and accessories.

Parameters: None

Response:

{
  "systems": {
    "aluminum_profiles": {
      "available_widths_mm": [500, 950, 1950, 2950, 3950, 4950],
      "available_heights_mm": [2500, 3000, 3500],
      "finishes": ["silver_anodized", "white", "black", "champagne"],
      "profile_types": ["standard", "heavy_duty", "curved"]
    }
  },
  "connectors": ["90_degree", "180_inline", "T_connector", "45_degree", "adjustable"],
  "accessories": ["storage_room", "door", "shelf", "counter", "brochure_rack", "monitor_mount"],
  "flooring": ["carpet", "laminate", "raised_floor", "vinyl"],
  "transport_cases": ["standard_case", "xl_case", "graphic_bag", "connector_box"]
}

aluvision_build_booth (Primary Tool)

Builds a complete trade fair booth from a layout specification.

Parameters:

Parameter Type Required Default Description
layout string yes — "inline", "inline_closed", "corner", "peninsula", "island", "island_closed"
width number yes — Booth width in mm
depth number yes — Booth depth in mm
height number no 2500 Wall height in mm
finish string no "silver_anodized" Profile finish
flooring object no — { type: "carpet", color: "#888888" }
furniture object[] no [] [{ type: "counter", position: "wall_0", offset: 0.5 }]
lead_magnets object[] no [] [{ type: "brochure_rack", wall_index: 1 }]
storage_rooms object[] no [] [{ wall_index: 0, width: 1950, depth: 1000 }]
custom_walls object[] no — Custom wall definitions (for manual layout)
job_id string no — 24HRS job ID for filename
customer_name string no — For model metadata
fair_name string no — For model metadata

Example call:

{
  "layout": "corner",
  "width": 6000,
  "depth": 4000,
  "height": 2500,
  "finish": "silver_anodized",
  "flooring": { "type": "carpet", "color": "#333333" },
  "furniture": [
    { "type": "counter", "position": "wall_0", "offset": 0.5 },
    { "type": "stool", "count": 2, "position": "counter" }
  ],
  "storage_rooms": [
    { "wall_index": 1, "width": 1950, "depth": 1000 }
  ],
  "lead_magnets": [
    { "type": "brochure_rack", "wall_index": 0 },
    { "type": "monitor_mount", "wall_index": 0, "size": 55 }
  ],
  "job_id": "42",
  "customer_name": "Brevo",
  "fair_name": "OMR 2026"
}

Response:

{
  "success": true,
  "model_path": "/tmp/booth_job_42_2026-06-10.skp",
  "preview_path": "/tmp/booth_job_42_2026-06-10.png",
  "layout": "corner",
  "dimensions": { "width_mm": 6000, "depth_mm": 4000, "height_mm": 2500 },
  "walls": [
    { "id": "wall_00", "width": 6000, "height": 2500, "finish": "silver_anodized" },
    { "id": "wall_01", "width": 4000, "height": 2500, "finish": "silver_anodized" }
  ],
  "connectors": 1,
  "bom_preview": {
    "component_count": 8,
    "profile_length_m": 10.0,
    "connector_count": 1,
    "flooring_area_m2": 24.0
  }
}

aluvision_create_wall

Creates a single Aluvision wall at a specified position.

Parameters:

Parameter Type Required Default Description
width number yes — Wall width in mm
height number yes — Wall height in mm
position object no {"x":0,"y":0,"z":0} Wall position (mm)
rotation number no 0 Rotation about Z axis in degrees
finish string no "silver_anodized" Profile finish
has_graphic boolean no true Include SEG graphic panel

aluvision_create_corner

Creates a corner connector between two walls.

Parameters:

Parameter Type Required Default Description
angle number no 90 Connector angle in degrees
position object no {"x":0,"y":0,"z":0} Position (mm)
type string no "standard" "standard", "adjustable"

aluvision_create_storage

Creates a storage room attached to a wall.

Parameters:

Parameter Type Required Default Description
width number yes — Room width in mm
depth number yes — Room depth in mm
height number no 2500 Room height in mm
has_door boolean no true Include door
door_type string no "hinged" "hinged", "sliding"
has_shelving boolean no false Include interior shelves

aluvision_create_door

Adds a door to an existing wall.

Parameters: wall_id, width, height, type ("hinged", "sliding"), position (along wall)

aluvision_arrange_layout

Adjusts positions and orientations of existing booth components.

Parameters: positions[] — array of { component_id, position: {x,y,z}, rotation }

aluvision_add_backlit

Converts a wall section to backlit (if Aluvision supports LED frame integrations).

Parameters: wall_id, brightness, color_temperature

aluvision_export_bom

Exports the bill of materials for the current booth design.

Parameters:

Parameter Type Required Default Description
format string no "json" "json", "csv", "spreadsheet"
filepath string no — Output file path

Response (JSON):

{
  "project": "Booth_Brevo_OMR2026",
  "job_id": "42",
  "customer": "Brevo",
  "fair": "OMR 2026",
  "dimensions": { "width_mm": 6000, "depth_mm": 4000, "height_mm": 2500 },
  "components": [
    {
      "part_number": "AV-PROFILE-4950-SL",
      "name": "Aluvision Standard Profile 4950mm, Silver",
      "category": "profile",
      "qty": 2,
      "length_mm": 4950,
      "notes": "Wall 0"
    },
    {
      "part_number": "AV-CONN-90-STD",
      "name": "90° Corner Connector, Standard",
      "category": "connector",
      "qty": 1
    },
    {
      "part_number": "AV-FLOOR-CARPET",
      "name": "Exhibition Carpet, Anthracite",
      "category": "flooring",
      "qty": 1,
      "area_m2": 24.0
    }
  ],
  "totals": {
    "component_count": 8,
    "profile_length_m": 10.0,
    "connector_count": 1
  }
}

aluvision_export_model

Exports the booth model in various formats.

Parameters:

Parameter Type Required Default Description
format string no "skp" "skp", "glb", "dae", "png"
filepath string yes — Output file path
width number no 1920 PNG render width
height number no 1080 PNG render height

aluvision_export_drawing

Generates dimensioned views (top, front, side, isometric).

Parameters: format ("pdf", "png"), filepath, views[] ("top", "front", "right", "isometric"), include_dimensions (boolean)

aluvision_get_component_info

Returns metadata for a placed component (dimensions, part number, finish).

Parameters: component_id

SketchUp Ruby Bridge Extension

Structure

sketchup-aluvision-bridge/
├── aluvision_bridge.rb          # Main extension, starts WEBrick server
├── aluvision_bridge/
│   ├── server.rb                # HTTP endpoint handlers
│   ├── component_loader.rb      # Load Aluvision components from library
│   ├── booth_builder.rb         # Booth assembly logic
│   ├── wall_builder.rb          # Single wall creation
│   ├── connector_builder.rb     # Connector placement
│   ├── storage_builder.rb       # Storage room construction
│   ├── furniture_placer.rb      # Furniture component placement
│   ├── flooring_builder.rb      # Floor plane creation
│   ├── bom_extractor.rb         # Component enumeration for BOM
│   ├── model_exporter.rb        # Export to .skp, .glb, .dae, PNG
│   └── aluvision_registry.rb    # Map of Aluvision component names → library paths
└── aluvision_bridge.rb          # Extension entry point

Extension Entry Point (aluvision_bridge.rb)

require 'sketchup'
require 'webrick'
require 'json'

module AluvisionBridge
  VERSION = '1.0.0'
  PORT = 9494

  unless file_loaded?(__FILE__)
    # Register extension
    ext = SketchupExtension.new('Aluvision Bridge', 'aluvision_bridge/loader.rb')
    ext.version = VERSION
    ext.description = 'HTTP bridge for automated booth generation via MCP'
    ext.creator = '24HRS Team'
    Sketchup.register_extension(ext, true)

    # Start server in a background thread when SketchUp is ready
    UI.start_timer(0, false) do
      Thread.new { start_server }
    end

    file_loaded(__FILE__)
  end

  def self.start_server
    server = WEBrick::HTTPServer.new(
      Port: PORT,
      Logger: WEBrick::Log.new(File.join(ENV['TEMP'] || '/tmp', 'aluvision_bridge.log')),
      AccessLog: []
    )

    # Health check
    server.mount_proc '/health' do |req, res|
      res.body = {
        status: 'ok',
        sketchup_version: Sketchup.version,
        bridge_version: VERSION,
        active_model: !Sketchup.active_model.nil?
      }.to_json
    end

    # List Aluvision components available in the model's library
    server.mount_proc '/component/list' do |req, res|
      components = AluvisionRegistry.list_available
      res.body = { components: components }.to_json
    end

    # Generate full booth
    server.mount_proc '/booth/generate' do |req, res|
      params = JSON.parse(req.body)
      result = BoothBuilder.build(params)
      res.body = result.to_json
    end

    # Export model
    server.mount_proc '/booth/export' do |req, res|
      params = JSON.parse(req.body)
      path = ModelExporter.export(params['format'], params['filepath'])
      res.body = { path: path }.to_json
    end

    # Generate BOM
    server.mount_proc '/booth/bom' do |req, res|
      bom = BomExtractor.extract
      res.body = bom.to_json
    end

    puts "[Aluvision Bridge] Server started on port #{PORT}"
    server.start
  end
end

Aluvision Registry Pattern

The registry maps logical component names to their location in the SketchUp component library:

module AluvisionRegistry
  COMPONENTS = {
    'profile_standard'    => { path: 'Aluvision/Profiles/Standard',      params: [:width, :height, :finish] },
    'profile_heavy'       => { path: 'Aluvision/Profiles/Heavy Duty',     params: [:width, :height, :finish] },
    'connector_90'        => { path: 'Aluvision/Connectors/90 Degree',   params: [:finish] },
    'connector_180'       => { path: 'Aluvision/Connectors/180 Inline',  params: [:finish] },
    'connector_T'         => { path: 'Aluvision/Connectors/T Connector', params: [:finish] },
    'storage_room'        => { path: 'Aluvision/Accessories/Storage Room', params: [:width, :depth, :height] },
    'door_hinged'         => { path: 'Aluvision/Accessories/Door Hinged', params: [:width, :height] },
    'counter_standard'    => { path: 'Aluvision/Furniture/Counter',      params: [:width] },
    'stool'               => { path: 'Aluvision/Furniture/Stool',        params: [] },
    'brochure_rack'       => { path: 'Aluvision/Accessories/Brochure Rack', params: [] },
    'monitor_mount'       => { path: 'Aluvision/Accessories/Monitor Mount', params: [:size] },
    'floor_carpet'        => { path: 'Aluvision/Flooring/Carpet',        params: [:width, :depth, :color] },
    'floor_laminate'      => { path: 'Aluvision/Flooring/Laminate',      params: [:width, :depth, :color] },
  }.freeze

  def self.list_available
    COMPONENTS.keys
  end

  def self.get(name)
    COMPONENTS[name]
  end
end

Booth Builder Core Logic

module BoothBuilder
  # Maps 24HRS standType → wall topology
  LAYOUT_WALLS = {
    'inline'          => [ { side: :back,   angle: 0   } ],
    'inline_closed'   => [ { side: :back,   angle: 0   },
                           { side: :left,   angle: 270 },
                           { side: :right,  angle: 90  } ],
    'corner'          => [ { side: :back,   angle: 0   },
                           { side: :right,  angle: 90  } ],
    'peninsula'       => [ { side: :back,   angle: 0   },
                           { side: :left,   angle: 270 },
                           { side: :right,  angle: 90  } ],
    'island'          => [ { side: :back,   angle: 0   },
                           { side: :right,  angle: 90  },
                           { side: :front,  angle: 180 },
                           { side: :left,   angle: 270 } ],
    'island_closed'   => [ { side: :back,   angle: 0   },
                           { side: :right,  angle: 90  },
                           { side: :front,  angle: 180 },
                           { side: :left,   angle: 270 } ],
  }.freeze

  # Wall position calculation per layout
  WALL_POSITIONS = {
    back:   ->(w, d, h) { [0,     0,       0] },
    right:  ->(w, d, h) { [w,     0,       0] },
    front:  ->(w, d, h) { [w,     d,       0] },
    left:   ->(w, d, h) { [0,     d,       0] },
  }.freeze

  WALL_SIZES = {
    back:   ->(w, d, h) { [w, h] },
    right:  ->(w, d, h) { [d, h] },
    front:  ->(w, d, h) { [w, h] },
    left:   ->(w, d, h) { [d, h] },
  }.freeze

  def self.build(params)
    model = Sketchup.active_model
    model.start_operation('Generate Booth', true)

    layout = params['layout']
    width = params['width'].to_f
    depth = params['depth'].to_f
    height = params['height'].to_f || 2500.0
    finish = params['finish'] || 'silver'

    # Clear existing geometry
    model.entities.clear!

    # Place walls
    walls = {}
    LAYOUT_WALLS[layout].each_with_index do |wall_spec, index|
      size = WALL_SIZES[wall_spec[:side]].call(width, depth, height)
      pos  = WALL_POSITIONS[wall_spec[:side]].call(width, depth, height)

      wall = WallBuilder.create(
        width:  size[0],
        height: size[1],
        position: pos,
        rotation: wall_spec[:angle],
        finish: finish
      )
      walls["wall_#{format('%02d', index)}"] = wall
    end

    # Place connectors at wall junctions
    connectors = []
    wall_list = LAYOUT_WALLS[layout]
    wall_list.each_with_index do |wall_spec, i|
      next_wall = wall_list[(i + 1) % wall_list.length]
      pos = WALL_POSITIONS[wall_spec[:side]].call(width, depth, height)

      connector = ConnectorBuilder.create(
        type: '90_degree',
        position: pos,
        finish: finish
      )
      connectors << connector
    end

    # Place flooring
    if params['flooring']
      FlooringBuilder.create(
        width: width,
        depth: depth,
        type: params['flooring']['type'] || 'carpet',
        color: params['flooring']['color'] || '#888888'
      )
    end

    # Place furniture
    (params['furniture'] || []).each do |item|
      FurniturePlacer.place(item)
    end

    # Place lead magnets
    (params['lead_magnets'] || []).each do |item|
      FurniturePlacer.place(item)
    end

    model.commit_operation

    # Zoom to fit
    model.active_view.zoom_extents

    {
      success: true,
      wall_count: walls.length,
      connector_count: connectors.length,
      dimensions: { width_mm: width, depth_mm: depth, height_mm: height }
    }
  rescue => e
    model.abort_operation
    { success: false, error: e.message, backtrace: e.backtrace.first(5) }
  end
end

MCP Server Implementation

Project Structure

aluvision-mcp-server/
├── src/
│   ├── server.ts              # MCP server entry point, tool registration
│   ├── aluvision_tools.ts     # Tool definitions with parameter schemas
│   ├── layout_engine.ts       # Booth layout logic (standType → walls)
│   ├── bom_generator.ts       # BOM parsing from SketchUp response
│   ├── sketchup_client.ts     # HTTP client for SketchUp bridge
│   └── types.ts               # TypeScript interfaces
├── package.json
└── tsconfig.json

MCP Server Entry Point (server.ts)

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { registerAluvisionTools } from './aluvision_tools.js';

const server = new Server(
  { name: 'aluvision-booth-designer', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

registerAluvisionTools(server);

const transport = new StdioServerTransport();
await server.connect(transport);

Tool Registration Pattern (aluvision_tools.ts)

import { SketchUpClient } from './sketchup_client.js';
import { LayoutEngine } from './layout_engine.js';

const su = new SketchUpClient('http://localhost:9494');

export function registerAluvisionTools(server: Server) {
  server.setRequestHandler('tools/list', async () => ({
    tools: [
      {
        name: 'aluvision_info',
        description: 'List available Aluvision systems, profiles, and components',
        inputSchema: { type: 'object', properties: {} }
      },
      {
        name: 'aluvision_build_booth',
        description: 'Build a complete trade fair booth from layout specification',
        inputSchema: {
          type: 'object',
          properties: {
            layout: {
              type: 'string',
              enum: ['inline', 'inline_closed', 'corner', 'peninsula', 'island', 'island_closed'],
              description: 'Booth layout type (mapped from 24HRS standType)'
            },
            width:  { type: 'number', description: 'Booth width in mm' },
            depth:  { type: 'number', description: 'Booth depth in mm' },
            height: { type: 'number', description: 'Wall height in mm', default: 2500 },
            finish: { type: 'string', description: 'Profile finish' },
            flooring: { type: 'object', description: 'Flooring configuration' },
            furniture: { type: 'array',  description: 'Furniture items' },
            lead_magnets: { type: 'array', description: 'Lead magnet items' },
            storage_rooms: { type: 'array', description: 'Storage room configs' },
            job_id: { type: 'string', description: '24HRS job ID' },
            customer_name: { type: 'string', description: 'Customer name for metadata' },
            fair_name: { type: 'string', description: 'Fair name for metadata' }
          },
          required: ['layout', 'width', 'depth']
        }
      },
      // ... remaining 10 tools follow the same pattern
    ]
  }));

  server.setRequestHandler('tools/call', async (request) => {
    const { name, arguments: args } = request.params;

    switch (name) {
      case 'aluvision_info':
        return await su.get('/component/list');

      case 'aluvision_build_booth': {
        // Layout engine resolves standType
        const layoutPlan = LayoutEngine.resolve(args);
        return await su.post('/booth/generate', layoutPlan);
      }

      case 'aluvision_export_bom':
        return await su.get('/booth/bom', { format: args.format });

      case 'aluvision_export_model':
        return await su.post('/booth/export', args);

      // ... remaining tools
    }
  });
}

Layout Engine (layout_engine.ts)

// Maps 24HRS standType strings to layout types
const STAND_TYPE_TO_LAYOUT: Record<string, string> = {
  'Systemstand':  'inline_closed',
  'Reihstand':    'inline',
  'Eckstand':     'corner',
  'Kopfstand':    'peninsula',
  'Blockstand':   'island_closed',
  'Inselstand':   'island',
};

// Parses "6x4" → { width: 6000, depth: 4000 }
function parseStandSize(size: string): { width: number; depth: number } {
  const parts = size.toLowerCase().split('x').map(s => parseFloat(s.trim()));
  return {
    width: (parts[0] || 3) * 1000,
    depth: (parts[1] || 3) * 1000,
  };
}

export class LayoutEngine {
  static resolve(job: {
    standType: string;
    standSize: string;
    standHeight?: number;
  }): BoothSpec {
    const layout = STAND_TYPE_TO_LAYOUT[job.standType] || 'island';
    const { width, depth } = parseStandSize(job.standSize);

    return {
      layout,
      width,
      depth,
      height: (job.standHeight || 2.5) * 1000,
      finish: 'silver_anodized',
    };
  }
}

HTTP Client for SketchUp Bridge (sketchup_client.ts)

export class SketchUpClient {
  constructor(private baseUrl: string) {}

  async get(path: string, params?: Record<string, string>): Promise<any> {
    const url = new URL(path, this.baseUrl);
    if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    const res = await fetch(url.toString());
    return res.json();
  }

  async post(path: string, body: any): Promise<any> {
    const res = await fetch(new URL(path, this.baseUrl).toString(), {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    return res.json();
  }
}

Integration into 24HRS Workflow

Pimcore Backend Integration

// bundles/MessebauBundle/Controller/Api/Booth3dController.php
class Booth3dController extends AbstractController
{
    #[Route('/api/messebau/jobs/{id}/generate-3d', methods: ['POST'])]
    public function generate3d(Job $job, Booth3dService $service): JsonResponse
    {
        $params = $service->buildParams($job); // collects standType, size, height, materials, etc.
        $result = $service->sendToMcp($params); // invokes aluvision_build_booth via MCP

        // Store result on job
        $job->set3dModelPath($result['model_path']);
        $job->set3dPreviewPath($result['preview_path']);
        $job->set3dBom($result['bom_preview']);
        $job->save();

        return $this->json($result);
    }
}

Parameter Builder — Job Fields to 3D Parameters

class Booth3dService
{
    public function buildParams(Job $job): array
    {
        return [
            'layout'    => $this->mapStandType($job->getStandType()),
            'width'     => $this->parseWidth($job->getStandSize()) * 1000,
            'depth'     => $this->parseDepth($job->getStandSize()) * 1000,
            'height'    => ($job->getStandHeight() ?? 2.5) * 1000,
            'flooring'  => $this->getFlooring($job),
            'furniture' => $this->getFurniture($job),
            'lead_magnets' => $this->getLeadMagnets($job),
            'storage_rooms' => $this->getStorageRooms($job),
            'job_id'    => (string) $job->getId(),
            'customer_name' => $job->getCustomer()?->getCompany(),
            'fair_name' => $job->getFair()?->getName(),
        ];
    }

    private function mapStandType(string $type): string
    {
        return match($type) {
            'Systemstand' => 'inline_closed',
            'Reihstand'   => 'inline',
            'Eckstand'    => 'corner',
            'Kopfstand'   => 'peninsula',
            'Blockstand'  => 'island_closed',
            'Inselstand'  => 'island',
            default       => 'island',
        };
    }

    private function getFlooring(Job $job): array
    {
        $materials = $job->getMaterials();
        foreach ($materials as $m) {
            if ($m->getType() === 'flooring') {
                return ['type' => $m->getName(), 'color' => '#888888'];
            }
        }
        return ['type' => 'carpet', 'color' => '#888888'];
    }

    private function getFurniture(Job $job): array
    {
        $items = [];
        foreach ($job->getMaterials() as $m) {
            if ($m->getType() === 'furniture') {
                $items[] = ['type' => $m->getName(), 'count' => $m->getQuantity()];
            }
        }
        return $items;
    }

    private function getLeadMagnets(Job $job): array
    {
        // Read lead magnet checkboxes from job
        $items = [];
        $mapping = [
            'lead-flyer'    => 'brochure_rack',
            'lead-scanner'  => 'badge_scanner',
            'lead-tablet'   => 'tablet_stand',
            'lead-photo-booth' => 'photo_booth',
        ];
        foreach ($mapping as $field => $component) {
            if ($job->get($field)) {
                $items[] = ['type' => $component];
            }
        }
        return $items;
    }
}

React Frontend Integration

// In DetailHeader.tsx or MasterDataTab.tsx
function Generate3dButton({ jobId }: { jobId: string }) {
  const mutation = useMutation({
    mutationFn: () => api.post(`/jobs/${jobId}/generate-3d`),
    onSuccess: (data) => {
      showToast('3D Booth generated', 'success');
      setPreviewUrl(data.preview_path);
    },
  });

  return (
    <Button onClick={() => mutation.mutate()} loading={mutation.isPending}>
      <Cube /> 3D Booth generieren
    </Button>
  );
}

MCP Configuration

For AI-Assisted Design (Claude Desktop / Claude Code)

{
  "mcpServers": {
    "aluvision": {
      "command": "node",
      "args": [
        "/srv/dockerBuilds/aluvision-mcp-server/dist/server.js"
      ],
      "env": {
        "SKETCHUP_BRIDGE_URL": "http://localhost:9494",
        "ALUVISION_LIB_PATH": "Aluvision"
      }
    }
  }
}

For 24HRS Backend (Programmatic)

The Pimcore backend calls the MCP server directly (not via AI assistant) — the MCP server is used as a structured API. The PHP backend communicates with the MCP server via stdio or HTTP.

// Direct HTTP to SketchUp bridge (skip MCP for programmatic calls)
// OR via MCP server if AI interaction is desired
class McpClient
{
    private string $mcpUrl;

    public function callTool(string $toolName, array $params): array
    {
        // Send MCP JSON-RPC message via HTTP or spawn process
        $payload = [
            'jsonrpc' => '2.0',
            'id' => uniqid(),
            'method' => 'tools/call',
            'params' => [
                'name' => $toolName,
                'arguments' => $params,
            ],
        ];

        // ...
    }
}

Recommendation: For the initial implementation, have Pimcore call the SketchUp bridge directly via HTTP (skip the MCP layer). Add the MCP server only when AI-assisted conversational booth design is needed (e.g. "design a booth with these specs" via natural language).

Implementation Phases

Phase 1: SketchUp Ruby Bridge (~3 days)

  • Audit Aluvision 3D Library component naming and structure
  • Build AluvisionRegistry — map logical names to library paths
  • Implement WallBuilder, ConnectorBuilder — load and place components
  • Implement BoothBuilder — layout engine for all 6 stand types
  • Implement FlooringBuilder, FurniturePlacer — materials and accessories
  • Implement BomExtractor — enumerate placed components
  • Implement ModelExporter — export .skp, render PNG
  • Wire up WEBrick server with all endpoints
  • Test manually: POST JSON → booth appears in SketchUp

Phase 2: MCP Server (~2 days)

  • Scaffold Node.js/TypeScript project with MCP SDK
  • Implement SketchUpClient HTTP client
  • Implement LayoutEngine — 24HRS standType → wall topology
  • Implement all 12 tool definitions
  • Implement tool call handlers → HTTP to SketchUp bridge
  • Test end-to-end: Claude → MCP → SketchUp

Phase 3: Pimcore Integration (~2 days)

  • Create Booth3dController with generate/export/status endpoints
  • Create Booth3dService — job parameter builder
  • Add 3D model fields to Job data object (3dModelPath, 3dPreviewPath, 3dBom)
  • Store generated .skp and PNG in Pimcore DAM
  • API endpoint for polling generation status

Phase 4: Frontend Integration (~2 days)

  • Add "3D Booth generieren" button on Tab 0 / DetailHeader
  • Display generated PNG preview in a modal or dedicated section
  • Add "Regenerate" button for parameter tweaks
  • Download .skp file link
  • Show BOM summary alongside preview

Phase 5: AI-Assisted Design (Future)

  • Wire MCP server to AI assistant for conversational design
  • AI skill: Booth3dIdeator — suggest booth configurations from briefing docs
  • Natural language booth modification ("add a counter on wall 1", "make the back wall backlit")

Dependencies

Dependency Status Notes
SketchUp Pro (desktop) Required Ruby API available in all editions
Aluvision 3D Library Required Already installed in SketchUp
Node.js ≥ 18 + npm ≥ 9 Required For MCP server
@modelcontextprotocol/sdk (TypeScript) Package MCP protocol implementation
ruby/webrick Bundled HTTP server inside SketchUp
SketchUp Ruby API Bundled sketchup.rb, geom.rb, ui.rb

Risks

Risk Mitigation
Aluvision component naming not standardized Audit library first. Build mapping table in AluvisionRegistry.
SketchUp must be running for generation Acceptable for design workflow. For batch, use headless SketchUp on a dedicated machine.
Ruby WEBrick blocking main thread Run server in background Thread. Use UI.start_timer for SketchUp API calls.
Large booth models slow to generate Generate incrementally. Return preview immediately, background full export.
Aluvision library version changes Version-lock the AluvisionRegistry. Add version check on bridge startup.
  • plan/architecture-rewrite-1.0.md — main 24HRS rewrite plan
  • plan/ai-document-skills.md — AI document processing skills
  • ../sketchup-booth-designer/ — Expolinc reference implementation (FreeCAD)