feat: Estúdio de Cenas em Camadas - IA desmonta imagem em elementos editáveis individualmente no canvas

This commit is contained in:
2026-06-08 10:34:41 +00:00
parent f16c1dc286
commit 837bda8c88
5 changed files with 484 additions and 7 deletions
+192
View File
@@ -7601,6 +7601,198 @@ document.addEventListener('DOMContentLoaded', () => {
let dragStartWidth, dragStartHeight;
let activeAssetTab = 'postit';
// --- ESTÚDIO DE CENAS EM CAMADAS ---
const btnDecomposeScene = document.getElementById('btnDecomposeScene');
const sceneDecomposePrompt = document.getElementById('sceneDecomposePrompt');
const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader');
const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus');
const sceneLayersList = document.getElementById('sceneLayersList');
const sceneLayersItems = document.getElementById('sceneLayersItems');
// Cria um item de camada de imagem direto no canvas (background ou sticker)
function addSceneLayerToCanvas(layerData) {
if (!canvasModeActive || !cartazPrintableArea) return;
const item = document.createElement('div');
item.className = 'canvas-item canvas-scene-layer';
item.setAttribute('data-layer-name', layerData.name || '');
item.setAttribute('data-rotation', '0');
item.style.position = 'absolute';
item.style.left = (layerData.left || 10) + '%';
item.style.top = (layerData.top || 10) + '%';
item.style.width = (layerData.width || 35) + '%';
item.style.height = (layerData.height || 35) + '%';
item.style.zIndex = layerData.isBackground ? '1' : String(10 + (layerData.zOffset || 0));
item.style.background = 'transparent';
if (layerData.isBackground) {
// Fundo: cobrindo 100% e não selecionável como item de controle
item.style.left = '0';
item.style.top = '0';
item.style.width = '100%';
item.style.height = '100%';
item.style.zIndex = '0';
item.style.pointerEvents = 'none'; // Por padrão, o fundo não captura eventos para facilitar a seleção dos elementos acima
item.innerHTML = `<img src="${layerData.url}"
style="width:100%; height:100%; object-fit:cover; pointer-events:none; display:block;">`;
item.title = '🖼️ Cenário de Fundo';
} else {
item.innerHTML = `<img src="${layerData.url}"
style="width:100%; height:100%; object-fit:contain; pointer-events:none;">`;
item.title = '🎬 ' + (layerData.name || 'Elemento');
}
const handle = document.createElement('div');
handle.className = 'canvas-resize-handle';
item.appendChild(handle);
cartazPrintableArea.appendChild(item);
if (!layerData.isBackground) {
setupCanvasItemEvents(item);
// Permitir que o fundo seja selecionável após clicar nele diretamente
} else {
// para o fundo, adicionar capacidade de clicar para habilitar arrastar
item.style.pointerEvents = 'auto';
setupCanvasItemEvents(item);
}
return item;
}
// Renderiza a lista de camadas no painel lateral
function renderSceneLayers(layers) {
if (!sceneLayersItems) return;
sceneLayersItems.innerHTML = '';
const allLayers = [{ name: '🖼️ Cenário de Fundo', isBackground: true }, ...layers.map(l => ({ ...l }))];
allLayers.forEach((layer, index) => {
const chip = document.createElement('div');
chip.style.cssText = `display:flex; align-items:center; gap:8px; padding:5px 8px;
background:rgba(255,255,255,0.05); border:1px solid rgba(139,92,246,0.2);
border-radius:6px; font-size:0.78rem; color:var(--text-primary); cursor:pointer;`;
chip.innerHTML = `
<span style="font-size:1rem;">${layer.isBackground ? '🌄' : '✨'}</span>
<span style="flex:1;">${layer.name}</span>
<span style="font-size:0.68rem; color:var(--text-secondary);">Camada ${index}</span>
`;
chip.title = `${layer.isBackground ? 'Fundo da cena' : 'Elemento: ' + layer.name}`;
sceneLayersItems.appendChild(chip);
});
}
if (btnDecomposeScene) {
btnDecomposeScene.addEventListener('click', async () => {
const prompt = sceneDecomposePrompt ? sceneDecomposePrompt.value.trim() : '';
if (!prompt) {
showToast('Descreva a cena antes de gerar!', 'warning');
return;
}
// Garantir que o modo canvas está ativo
if (!canvasModeActive) {
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
btnEditCartazToggle && btnEditCartazToggle.click();
}
enterCanvasMode();
}
// Mostrar loading
btnDecomposeScene.disabled = true;
btnDecomposeScene.style.opacity = '0.6';
if (sceneDecomposeLoader) {
sceneDecomposeLoader.style.display = 'flex';
}
if (sceneLayersList) sceneLayersList.style.display = 'none';
const statusSteps = [
'Planejando a cena com IA...',
'Identificando elementos e cenário...',
'Gerando imagem de fundo...',
'Gerando elementos individuais...',
'Montando as camadas no canvas...'
];
let stepIdx = 0;
const statusInterval = setInterval(() => {
if (sceneDecomposeStatus && stepIdx < statusSteps.length) {
sceneDecomposeStatus.textContent = statusSteps[stepIdx++];
}
}, 3500);
try {
const titulo = activeCartazData?.titulo || '';
const response = await fetch('/api/cartazes/decompose-scene', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ tema: prompt, titulo })
});
clearInterval(statusInterval);
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.error || 'Erro na geração da cena.');
}
const data = await response.json();
if (!data.success) throw new Error('Resposta inválida do servidor.');
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...';
// Limpar canvas antes de montar (remover outros items de cena se houver)
const oldLayers = cartazPrintableArea.querySelectorAll('.canvas-scene-layer');
oldLayers.forEach(l => l.remove());
// Adicionar background
if (data.backgroundUrl) {
addSceneLayerToCanvas({ url: data.backgroundUrl, name: 'Cenário de Fundo', isBackground: true });
}
// Adicionar cada elemento com posição escalonada
if (Array.isArray(data.layers)) {
data.layers.forEach((layer, idx) => {
if (layer.url) {
const spread = [
{ left: 10, top: 30 },
{ left: 38, top: 35 },
{ left: 62, top: 28 },
{ left: 25, top: 55 }
];
const pos = spread[idx % spread.length];
addSceneLayerToCanvas({
url: layer.url,
name: layer.name || `Elemento ${idx + 1}`,
isBackground: false,
left: pos.left,
top: pos.top,
width: 28,
height: 28,
zOffset: idx + 1
});
}
});
// Renderizar lista de camadas no painel
renderSceneLayers(data.layers);
if (sceneLayersList) sceneLayersList.style.display = 'flex';
}
showToast('🎬 Cena montada em camadas! Clique nos elementos para mover, girar e redimensionar.', 'success');
} catch (err) {
clearInterval(statusInterval);
console.error('[Decompose Scene]', err);
showToast('Erro ao criar a cena em camadas: ' + err.message, 'error');
} finally {
btnDecomposeScene.disabled = false;
btnDecomposeScene.style.opacity = '1';
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none';
}
});
}
function enterCanvasMode() {
canvasModeActive = true;
if (btnCanvasModeToggle) {