39 KiB
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
SketchUpClientHTTP 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
Booth3dControllerwith 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 planplan/ai-document-skills.md— AI document processing skills../sketchup-booth-designer/— Expolinc reference implementation (FreeCAD)