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

1081 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
goal: Automated 3D Booth Generation via SketchUp + Aluvision + MCP
version: 1.0
date_created: 2026-06-10
status: Planned
tags: 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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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):**
```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`)
```ruby
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:
```ruby
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
```ruby
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`)
```typescript
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`)
```typescript
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`)
```typescript
// 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`)
```typescript
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
```php
// 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
```php
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
```tsx
// 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)
```json
{
"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.
```php
// 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. |
## Related Documents
- `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)