1081 lines
39 KiB
Markdown
1081 lines
39 KiB
Markdown
---
|
||
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)
|