← back to Dw War Room
src/showroom/FigmaTransformer.ts
129 lines
// FigmaTransformer.ts — Converts Figma design context into Three.js scene graph config
// Maps Figma frames → positions, rectangles → geometry, fills → materials
import type { SceneConfig, WingConfig, WallConfig, LightConfig, FurnitureConfig } from './types';
/**
* Transform Figma design JSON into Three.js scene configuration.
* Accepts output from Figma MCP `get_design_context` or raw Figma REST API.
*/
export function transformFigmaToScene(figmaData: any): SceneConfig {
const root = figmaData.document || figmaData;
const config: SceneConfig = {
corridor: { width: 40, depth: 20, height: 12, wallMaterial: 'brick', floorMaterial: 'polished-concrete' },
wings: [],
walls: [],
lights: [],
furniture: [],
entry: { position: [0, 0, 10], signText: 'DESIGNER WALLCOVERINGS' },
};
// Traverse Figma node tree
function traverse(node: any, depth = 0) {
if (!node) return;
// Map FRAME nodes to wings or rooms based on name
if (node.type === 'FRAME' && node.name?.toLowerCase().includes('wing')) {
const bbox = node.absoluteBoundingBox || {};
config.wings.push({
index: config.wings.length,
vendor: node.name.replace(/wing[-_\s]*/i, '').trim(),
position: [bbox.x / 10 || config.wings.length * 4, 0, bbox.y / 10 || 0],
rotation: 0,
panelCount: (node.children || []).filter((c: any) => c.type === 'RECTANGLE').length || 10,
accentColor: extractFillColor(node) || '#c0c0c0',
});
}
// Map RECTANGLE nodes with IMAGE fills to wall sample panels
if (node.type === 'RECTANGLE' && node.fills?.some((f: any) => f.type === 'IMAGE')) {
const bbox = node.absoluteBoundingBox || {};
config.walls.push({
position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 8],
material: 'brick',
hasBookshelf: false,
hasSamplePanels: true,
sampleCount: 1,
});
}
// Map circles/ellipses to spotlights
if (node.type === 'ELLIPSE' && node.name?.toLowerCase().includes('light')) {
const bbox = node.absoluteBoundingBox || {};
config.lights.push({
type: 'spot',
position: [bbox.x / 10 || 0, 10, bbox.y / 10 || 0],
target: [bbox.x / 10 || 0, 0, bbox.y / 10 || 0],
color: extractFillColor(node) || '#ffffff',
intensity: 1.5,
angle: Math.PI / 6,
penumbra: 0.5,
});
}
// Map FRAME named 'furniture' or 'table' to furniture
if (node.type === 'FRAME' && /table|desk|furniture/i.test(node.name || '')) {
const bbox = node.absoluteBoundingBox || {};
config.furniture.push({
type: 'display-table',
position: [bbox.x / 10 || 0, 0.45, bbox.y / 10 || 0],
dimensions: [bbox.width / 10 || 3, 0.9, (bbox.height || 150) / 10],
});
}
// Map FRAME named 'entry' or 'entrance' to entry
if (node.type === 'FRAME' && /entry|entrance|door/i.test(node.name || '')) {
const bbox = node.absoluteBoundingBox || {};
config.entry.position = [bbox.x / 10 || 0, 0, bbox.y / 10 || 10];
// Look for text child as sign text
const textChild = (node.children || []).find((c: any) => c.type === 'TEXT');
if (textChild?.characters) {
config.entry.signText = textChild.characters;
}
}
// Map RECTANGLE with SOLID fills named 'bookshelf' or 'shelf'
if (node.type === 'RECTANGLE' && /bookshelf|shelf|book/i.test(node.name || '')) {
const bbox = node.absoluteBoundingBox || {};
config.walls.push({
position: [bbox.x / 10 || 0, bbox.height / 20 || 2, bbox.y / 10 || 0],
dimensions: [bbox.width / 10 || 4, bbox.height / 10 || 6],
material: 'wood',
hasBookshelf: true,
hasSamplePanels: false,
sampleCount: 0,
});
}
if (node.children) node.children.forEach((c: any) => traverse(c, depth + 1));
}
traverse(root);
// Add default track lighting if none extracted
if (config.lights.length === 0) {
for (let i = 0; i < 8; i++) {
config.lights.push({
type: 'spot',
position: [i * 5 - 17.5, 11, 0],
target: [i * 5 - 17.5, 0, 0],
color: '#f5e6d0',
intensity: 1.2,
angle: Math.PI / 5,
penumbra: 0.4,
});
}
}
return config;
}
function extractFillColor(node: any): string | null {
const fill = (node.fills || []).find((f: any) => f.type === 'SOLID' && f.visible !== false);
if (!fill?.color) return null;
const r = Math.round(fill.color.r * 255);
const g = Math.round(fill.color.g * 255);
const b = Math.round(fill.color.b * 255);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}