--- 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 = { '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): Promise { 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 { 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 ( ); } ``` ## 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)