diff --git a/qml/MaterialEditorWindow.qml b/qml/MaterialEditorWindow.qml index ee941e7..fe2e1d6 100644 --- a/qml/MaterialEditorWindow.qml +++ b/qml/MaterialEditorWindow.qml @@ -50,14 +50,14 @@ ApplicationWindow { statusText.text = "AI generating material..." statusText.color = "orange" } - + function onAiGenerationCompleted(generatedScript) { aiStatusIndicator.isGenerating = false aiStatusIndicator.hasError = false - + // Update the text area with the new script materialTextArea.text = generatedScript - + // Validate the script first if (MaterialEditorQML.validateMaterialScript(generatedScript)) { // Auto-apply if valid @@ -70,7 +70,7 @@ ApplicationWindow { statusText.color = "orange" } } - + function onAiGenerationError(error) { aiStatusIndicator.isGenerating = false aiStatusIndicator.hasError = true @@ -78,6 +78,13 @@ ApplicationWindow { statusText.text = "AI error: " + error statusText.color = "red" } + + function onSdPendingForMaterialChanged() { + if (MaterialEditorQML.sdPendingForMaterial) { + statusText.text = "Generating texture..." + statusText.color = "orange" + } + } } // Simplified Button component @@ -718,9 +725,9 @@ ApplicationWindow { } MenuItem { - text: "PBR Material" + text: "Normal Map Material" onTriggered: { - materialTextArea.text = "material PBRMaterial\n{\n technique\n {\n pass\n {\n ambient 0.1 0.1 0.1 1.0\n diffuse 0.8 0.8 0.8 1.0\n specular 0.04 0.04 0.04 1.0\n \n texture_unit // Albedo\n {\n texture albedo.png\n }\n \n texture_unit // Normal\n {\n texture normal.png\n }\n \n texture_unit // Roughness\n {\n texture roughness.png\n }\n }\n }\n}" + materialTextArea.text = "material NormalMapMaterial\n{\n technique\n {\n pass\n {\n ambient 0.2 0.2 0.2 1.0\n diffuse 0.8 0.8 0.8 1.0\n specular 0.5 0.5 0.5 32.0\n \n texture_unit\n {\n texture diffuse.png\n }\n \n texture_unit normal_map\n {\n texture normal.png\n }\n }\n }\n}" } } } @@ -865,7 +872,7 @@ ApplicationWindow { ThemedTextField { id: aiPromptInput Layout.fillWidth: true - placeholderText: "Type a command like: 'add texture glow.png', 'make it transparent', 'create PBR material'" + placeholderText: "Describe the material (e.g., 'rusty metal', 'shiny glass', 'oak wood with normal map')" enabled: !aiStatusIndicator.isGenerating && LLMManager.modelLoaded background: Rectangle { @@ -903,10 +910,10 @@ ApplicationWindow { } } - // Progress indicator for local LLM + // Combined progress indicator for LLM + SD RowLayout { Layout.fillWidth: true - visible: aiStatusIndicator.isGenerating && LLMManager.modelLoaded + visible: (aiStatusIndicator.isGenerating || MaterialEditorQML.sdPendingForMaterial || MaterialEditorQML.sdIsGenerating) && LLMManager.modelLoaded spacing: 8 ProgressBar { @@ -914,11 +921,17 @@ ApplicationWindow { Layout.fillWidth: true from: 0 to: 1 - value: MaterialEditorQML.llmGenerationProgress + value: MaterialEditorQML.sdPendingForMaterial || MaterialEditorQML.sdIsGenerating ? + MaterialEditorQML.sdGenerationProgress : + MaterialEditorQML.llmGenerationProgress } Text { - text: Math.round(MaterialEditorQML.llmGenerationProgress * 100) + "%" + property real displayProgress: MaterialEditorQML.sdPendingForMaterial || MaterialEditorQML.sdIsGenerating ? + MaterialEditorQML.sdGenerationProgress : + MaterialEditorQML.llmGenerationProgress + text: (MaterialEditorQML.sdPendingForMaterial || MaterialEditorQML.sdIsGenerating ? "Texture: " : "LLM: ") + + Math.round(displayProgress * 100) + "%" font.pointSize: 9 color: textColor } diff --git a/qml/PassPropertiesPanel.qml b/qml/PassPropertiesPanel.qml index 94f0776..3823a0e 100644 --- a/qml/PassPropertiesPanel.qml +++ b/qml/PassPropertiesPanel.qml @@ -48,6 +48,16 @@ GroupBox { // Lighting and Depth Settings GroupBox { title: "Lighting & Depth" + background: Rectangle { + color: MaterialEditorQML.panelColor + border.color: MaterialEditorQML.borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } Layout.fillWidth: true ColumnLayout { @@ -60,19 +70,61 @@ GroupBox { spacing: 15 CheckBox { + text: "Lighting" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.lightingEnabled onCheckedChanged: MaterialEditorQML.setLightingEnabled(checked) } CheckBox { + text: "Depth Write" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.depthWriteEnabled onCheckedChanged: MaterialEditorQML.setDepthWriteEnabled(checked) } CheckBox { + text: "Depth Check" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.depthCheckEnabled onCheckedChanged: MaterialEditorQML.setDepthCheckEnabled(checked) } @@ -118,6 +170,16 @@ GroupBox { // Colors GroupBox { title: "Colors" + background: Rectangle { + color: MaterialEditorQML.panelColor + border.color: MaterialEditorQML.borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } Layout.fillWidth: true GridLayout { @@ -149,7 +211,21 @@ GroupBox { } } CheckBox { + text: "Use Vertex Color" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.useVertexColorToAmbient onCheckedChanged: MaterialEditorQML.setUseVertexColorToAmbient(checked) } @@ -177,7 +253,21 @@ GroupBox { } } CheckBox { + text: "Use Vertex Color" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.useVertexColorToDiffuse onCheckedChanged: MaterialEditorQML.setUseVertexColorToDiffuse(checked) } @@ -205,7 +295,21 @@ GroupBox { } } CheckBox { + text: "Use Vertex Color" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.useVertexColorToSpecular onCheckedChanged: MaterialEditorQML.setUseVertexColorToSpecular(checked) } @@ -233,7 +337,21 @@ GroupBox { } } CheckBox { + text: "Use Vertex Color" + + contentItem: Text { + + text: parent.text + + color: textColor + + leftPadding: parent.indicator.width + parent.spacing + + verticalAlignment: Text.AlignVCenter + + } + checked: MaterialEditorQML.useVertexColorToEmissive onCheckedChanged: MaterialEditorQML.setUseVertexColorToEmissive(checked) } @@ -243,6 +361,16 @@ GroupBox { // Alpha and Material Properties GroupBox { title: "Alpha & Material" + background: Rectangle { + color: MaterialEditorQML.panelColor + border.color: MaterialEditorQML.borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } Layout.fillWidth: true GridLayout { @@ -339,6 +467,16 @@ GroupBox { // Blending GroupBox { title: "Blending" + background: Rectangle { + color: MaterialEditorQML.panelColor + border.color: MaterialEditorQML.borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } Layout.fillWidth: true GridLayout { diff --git a/qml/TexturePropertiesPanel.qml b/qml/TexturePropertiesPanel.qml index 2b9ca0b..5248e3e 100644 --- a/qml/TexturePropertiesPanel.qml +++ b/qml/TexturePropertiesPanel.qml @@ -49,7 +49,17 @@ GroupBox { GroupBox { title: "Texture Selection" Layout.fillWidth: true - + background: Rectangle { + color: panelColor + border.color: borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } + ColumnLayout { anchors.fill: parent spacing: 10 @@ -125,6 +135,16 @@ GroupBox { title: "Preview" Layout.fillWidth: true Layout.preferredHeight: 200 + background: Rectangle { + color: panelColor + border.color: borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } Rectangle { anchors.fill: parent @@ -139,13 +159,30 @@ GroupBox { width: Math.min(parent.width - 20, sourceSize.width) height: Math.min(parent.height - 20, sourceSize.height) fillMode: Image.PreserveAspectFit - source: MaterialEditorQML.getTexturePreviewPath() + source: MaterialEditorQML.getTexturePreviewPath() !== "" ? MaterialEditorQML.getTexturePreviewPath() + "?v=0" : "" + // Cache-bust counter for forcing image reload + property int cacheBuster: 0 + + function reloadPreview() { + var path = MaterialEditorQML.getTexturePreviewPath() + if (path !== "") { + cacheBuster++ + texturePreview.source = path + "?v=" + cacheBuster + } else { + texturePreview.source = "" + } + } + // Update source when texture name changes Connections { target: MaterialEditorQML function onTextureNameChanged() { - texturePreview.source = MaterialEditorQML.getTexturePreviewPath() + texturePreview.reloadPreview() + } + function onSdTextureGenerated(filePath) { + // Force reload after SD generation (same filename, new content) + texturePreview.reloadPreview() } } @@ -643,119 +680,6 @@ GroupBox { } } - // AI Texture Edit (img2img) - GroupBox { - title: "AI Texture Edit" - visible: MaterialEditorQML.stableDiffusionEnabled - Layout.fillWidth: true - - background: Rectangle { - color: MaterialEditorQML.panelColor - border.color: MaterialEditorQML.borderColor - border.width: 1 - radius: 4 - } - label: ThemedLabel { - text: parent.title - font.bold: true - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 8 - - ThemedLabel { - text: "Edit the current texture with AI" - font.pointSize: 9 - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - ThemedTextField { - id: sdEditPromptField - Layout.fillWidth: true - placeholderText: "Describe the change (e.g., 'change eyes to blue')..." - enabled: MaterialEditorQML.sdModelLoaded && !MaterialEditorQML.sdIsGenerating - - Keys.onReturnPressed: { - if (text.length > 0 && MaterialEditorQML.sdModelLoaded && !MaterialEditorQML.sdIsGenerating) { - MaterialEditorQML.editTextureFromPrompt(text, sdStrengthSlider.value / 100.0) - } - } - } - - ThemedButton { - text: MaterialEditorQML.sdIsGenerating ? "Stop" : "Edit" - enabled: MaterialEditorQML.sdModelLoaded && - (MaterialEditorQML.sdIsGenerating || sdEditPromptField.text.length > 0) - onClicked: { - if (MaterialEditorQML.sdIsGenerating) { - MaterialEditorQML.stopTextureGeneration() - } else { - MaterialEditorQML.editTextureFromPrompt(sdEditPromptField.text, sdStrengthSlider.value / 100.0) - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - ThemedLabel { text: "Strength:" } - Slider { - id: sdStrengthSlider - Layout.fillWidth: true - from: 10 - to: 100 - stepSize: 5 - value: 50 - - background: Rectangle { - x: sdStrengthSlider.leftPadding - y: sdStrengthSlider.topPadding + sdStrengthSlider.availableHeight / 2 - height / 2 - implicitWidth: 200 - implicitHeight: 4 - width: sdStrengthSlider.availableWidth - height: implicitHeight - radius: 2 - color: MaterialEditorQML.borderColor - } - handle: Rectangle { - x: sdStrengthSlider.leftPadding + sdStrengthSlider.visualPosition * (sdStrengthSlider.availableWidth - width) - y: sdStrengthSlider.topPadding + sdStrengthSlider.availableHeight / 2 - height / 2 - implicitWidth: 16 - implicitHeight: 16 - radius: 8 - color: MaterialEditorQML.accentColor - border.color: MaterialEditorQML.borderColor - } - } - ThemedLabel { - text: Math.round(sdStrengthSlider.value) + "%" - Layout.minimumWidth: 35 - } - } - - ThemedLabel { - text: "Low = subtle changes, High = major changes" - font.pointSize: 8 - opacity: 0.6 - } - - ProgressBar { - Layout.fillWidth: true - from: 0 - to: 1 - value: MaterialEditorQML.sdGenerationProgress - visible: MaterialEditorQML.sdIsGenerating - } - } - } - // AI Texture Generation GroupBox { title: "AI Texture Generation" @@ -867,32 +791,39 @@ GroupBox { GroupBox { title: "Information" Layout.fillWidth: true - + background: Rectangle { + color: panelColor + border.color: borderColor + border.width: 1 + radius: 4 + } + label: ThemedLabel { + text: parent.title + font.bold: true + } + ColumnLayout { anchors.fill: parent spacing: 5 - - Text { + + ThemedLabel { text: "Texture: " + (MaterialEditorQML.textureName || "None") font.pointSize: 10 - color: textColor } - - Text { - text: texturePreview.source != "" && texturePreview.status === Image.Ready ? + + ThemedLabel { + text: texturePreview.source != "" && texturePreview.status === Image.Ready ? "Size: " + texturePreview.sourceSize.width + " x " + texturePreview.sourceSize.height : "Size: Unknown" font.pointSize: 10 - color: textColor } - - Text { - text: "Animation: " + - (MaterialEditorQML.scrollAnimUSpeed != 0.0 || MaterialEditorQML.scrollAnimVSpeed != 0.0 ? - "Enabled (" + MaterialEditorQML.scrollAnimUSpeed.toFixed(2) + ", " + MaterialEditorQML.scrollAnimVSpeed.toFixed(2) + ")" : + + ThemedLabel { + text: "Animation: " + + (MaterialEditorQML.scrollAnimUSpeed != 0.0 || MaterialEditorQML.scrollAnimVSpeed != 0.0 ? + "Enabled (" + MaterialEditorQML.scrollAnimUSpeed.toFixed(2) + ", " + MaterialEditorQML.scrollAnimVSpeed.toFixed(2) + ")" : "Disabled") font.pointSize: 10 - color: textColor } } } diff --git a/qml/ThemedTextField.qml b/qml/ThemedTextField.qml index f43f95a..6651bbe 100644 --- a/qml/ThemedTextField.qml +++ b/qml/ThemedTextField.qml @@ -5,7 +5,7 @@ TextField { color: MaterialEditorQML.textColor selectionColor: Qt.rgba(0.4, 0.4, 0.6, 1.0) selectedTextColor: MaterialEditorQML.textColor - placeholderTextColor: Qt.rgba(0.6, 0.6, 0.6, 1.0) + placeholderTextColor: MaterialEditorQML.disabledTextColor background: Rectangle { implicitWidth: 200 diff --git a/src/LLMManager.cpp b/src/LLMManager.cpp index 59db3fb..4ae3a6f 100644 --- a/src/LLMManager.cpp +++ b/src/LLMManager.cpp @@ -239,8 +239,9 @@ QString LLMManager::getOgre3DSystemPrompt() 1. Output ONLY the material script - no markdown, no explanations 2. Keep the same material name if modifying an existing material 3. All color values must be NUMBERS between 0.0 and 1.0 -4. Do NOT add texture_unit unless specifically asked for textures -5. For simple color changes, only modify ambient/diffuse/specular/emissive values +4. When the material needs a realistic look (e.g., wood, metal, stone, fabric) AND available textures are listed below, add a texture_unit with a descriptive filename like "oak_wood.png" or "rusty_metal.png" +5. Do NOT add texture_unit if no available textures are listed — the system may not support texture generation +6. For simple color changes, only modify ambient/diffuse/specular/emissive values BASIC STRUCTURE: material NAME diff --git a/src/MaterialEditorQML.cpp b/src/MaterialEditorQML.cpp index 8254951..b8380cb 100644 --- a/src/MaterialEditorQML.cpp +++ b/src/MaterialEditorQML.cpp @@ -89,7 +89,23 @@ MaterialEditorQML::MaterialEditorQML(QObject *parent) connect(sdManager, &SDManager::generationProgressChanged, this, &MaterialEditorQML::onSDGenerationProgress); connect(sdManager, &SDManager::generationCompleted, this, &MaterialEditorQML::onSDGenerationCompleted); connect(sdManager, &SDManager::generationError, this, &MaterialEditorQML::onSDGenerationError); + connect(sdManager, &SDManager::generationStopped, this, &MaterialEditorQML::onSDGenerationStopped); connect(sdManager, &SDManager::modelLoadedChanged, this, &MaterialEditorQML::onSDModelLoadedChanged); + + // Register generated textures directory as Ogre resource location at startup + // so textures from previous sessions are found when materials reference them + if (isOgreAvailable()) { + QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QString genTexDir = QDir(dataPath).filePath("generated_textures"); + if (QDir(genTexDir).exists()) { + try { + auto &rgm = Ogre::ResourceGroupManager::getSingleton(); + rgm.addResourceLocation(genTexDir.toStdString(), "FileSystem", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + rgm.initialiseResourceGroup(Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + } catch (...) {} + } + } #endif } @@ -2774,27 +2790,21 @@ void MaterialEditorQML::onLLMGenerationCompleted(const QString &generatedText) // Update the material text with the AI-generated script setMaterialText(cleanedText); - emit aiGenerationCompleted(cleanedText); - // LCOV_EXCL_START — SD auto-trigger requires SD enabled with loaded model - // Auto-trigger SD texture generation if the material references textures - // and SD is available with a loaded model + // Check if SD needs to generate a texture before we apply the material + bool sdTriggered = false; #ifdef ENABLE_STABLE_DIFFUSION SDManager *sdManager = SDManager::instance(); if (sdManager->isModelLoaded() && !sdManager->isGenerating()) { - // Look for texture_unit blocks with texture references that look like AI placeholders QRegularExpression texRegex(R"(texture\s+["\']?([^"'\s]+)["\']?)"); QRegularExpressionMatchIterator it = texRegex.globalMatch(cleanedText); while (it.hasNext()) { QRegularExpressionMatch match = it.next(); QString texName = match.captured(1).trimmed(); - // Skip if the texture file already exists on disk bool existsOnDisk = false; - // Check generated textures dir QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QString genPath = QDir(dataPath).filePath("generated_textures/" + texName); if (QFileInfo::exists(genPath)) existsOnDisk = true; - // Check media textures dirs QStringList searchDirs = { "media/materials/textures/" + texName, "../media/materials/textures/" + texName, @@ -2803,21 +2813,30 @@ void MaterialEditorQML::onLLMGenerationCompleted(const QString &generatedText) if (QFileInfo::exists(p)) { existsOnDisk = true; break; } } if (existsOnDisk) continue; - // This texture doesn't exist — generate it with SD using the texture name as prompt - // Convert underscores/hyphens to spaces for a better prompt QString prompt = texName; - prompt.replace(QRegularExpression(R"(\.\w+$)"), ""); // remove extension + prompt.replace(QRegularExpression(R"(\.\w+$)"), ""); QString cleanPrompt = prompt; cleanPrompt.replace('_', ' ').replace('-', ' '); if (!cleanPrompt.isEmpty()) { qDebug() << "MaterialEditorQML: Auto-triggering SD for missing texture:" << texName << "prompt:" << cleanPrompt; + // Defer material apply until SD completes + m_pendingMaterialScript = cleanedText; + m_sdPendingForMaterial = true; + emit sdPendingForMaterialChanged(); sdManager->generateTexture(cleanPrompt, 0, 0, texName); - break; // Generate one texture at a time + sdTriggered = true; + break; } } } #endif // LCOV_EXCL_STOP + + if (!sdTriggered) { + // No SD needed — emit immediately so QML can apply + emit aiGenerationCompleted(cleanedText); + } + // If SD was triggered, aiGenerationCompleted will be emitted from onSDGenerationCompleted } void MaterialEditorQML::onLLMGenerationError(const QString &error) @@ -2890,64 +2909,6 @@ void MaterialEditorQML::generateTextureFromPrompt(const QString &prompt, int wid #endif } -void MaterialEditorQML::editTextureFromPrompt(const QString &prompt, float strength) -{ - if (prompt.isEmpty()) { - emit sdGenerationError("Please enter an edit prompt"); - return; - } - -#ifdef ENABLE_STABLE_DIFFUSION - SDManager *sdManager = SDManager::instance(); - if (!sdManager->isModelLoaded()) { - emit sdGenerationError("No SD model loaded. Please load a model from AI Settings."); - return; - } - - // Find the current texture's file path - QString inputPath; - - // First try getTexturePreviewPath - QString texPreviewPath = getTexturePreviewPath(); - if (!texPreviewPath.isEmpty()) { - inputPath = QUrl(texPreviewPath).toLocalFile(); - } - - // If not found via preview path, check generated textures dir directly - if (inputPath.isEmpty() && !m_textureName.isEmpty() && m_textureName != "*Select a texture*") { - QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - QString genPath = QDir(dataPath).filePath("generated_textures/" + m_textureName); - if (QFileInfo::exists(genPath)) { - inputPath = genPath; - } - } - - if (inputPath.isEmpty() || !QFileInfo::exists(inputPath)) { - // Texture only exists in GPU memory — fall back to txt2img - QString outputName = m_textureName; - if (outputName.isEmpty() || outputName == "*Select a texture*") { - outputName = "edited_texture.png"; - } - sdManager->generateTexture(prompt, 0, 0, outputName); - return; - } - - m_sdGenerationProgress = 0.0f; - emit sdGenerationProgressChanged(); - - // Use current texture name as output, or generate a new name - QString outputName = m_textureName; - if (outputName.isEmpty() || outputName == "*Select a texture*") { - outputName = "edited_texture.png"; - } - - sdManager->editTexture(prompt, inputPath, strength, outputName); -#else - Q_UNUSED(strength); - emit sdGenerationError("Stable Diffusion support is not enabled. Rebuild with ENABLE_STABLE_DIFFUSION=ON"); -#endif -} - void MaterialEditorQML::stopTextureGeneration() { #ifdef ENABLE_STABLE_DIFFUSION @@ -2984,43 +2945,43 @@ void MaterialEditorQML::onSDGenerationCompleted(const QString &outputPath) QString dirPath = fileInfo.absolutePath(); QString fileName = fileInfo.fileName(); + // Helper lambda to emit deferred material completion if SD was triggered by LLM + auto emitDeferredCompletion = [this]() { + if (m_sdPendingForMaterial) { + m_sdPendingForMaterial = false; + emit sdPendingForMaterialChanged(); + emit aiGenerationCompleted(m_pendingMaterialScript); + m_pendingMaterialScript.clear(); + } + }; + if (!isOgreAvailable()) { - // Update texture name for preview even without Ogre m_textureName = fileName; emit textureNameChanged(); emit sdTextureGenerated(outputPath); + emitDeferredCompletion(); return; } - // Register the directory and apply texture — all in a single try block - // to catch any Ogre segfault-like exceptions try { auto *root = Ogre::Root::getSingletonPtr(); if (!root) { m_textureName = fileName; emit textureNameChanged(); emit sdTextureGenerated(outputPath); + emitDeferredCompletion(); return; } - auto &rgm = Ogre::ResourceGroupManager::getSingleton(); - - // Add resource location (Ogre handles duplicates) + // Register resource location try { + auto &rgm = Ogre::ResourceGroupManager::getSingleton(); rgm.addResourceLocation(dirPath.toStdString(), "FileSystem", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); rgm.initialiseResourceGroup(Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); } catch (...) {} - // Remove any previously loaded texture with the same name so Ogre reloads from new file - try { - auto existingTex = Ogre::TextureManager::getSingleton().getByName(fileName.toStdString()); - if (existingTex) { - Ogre::TextureManager::getSingleton().remove(existingTex); - } - } catch (...) {} - - // Apply texture to current material + // Apply texture to current material pass Ogre::Pass *pass = getCurrentPass(); if (pass) { Ogre::TextureUnitState *texUnit = getCurrentTextureUnit(); @@ -3035,14 +2996,13 @@ void MaterialEditorQML::onSDGenerationCompleted(const QString &outputPath) texUnit->setTextureName(fileName.toStdString()); } } - } catch (...) { - // If Ogre calls fail, still update the UI - } + } catch (...) {} m_textureName = fileName; emit textureNameChanged(); updateMaterialText(); emit sdTextureGenerated(outputPath); + emitDeferredCompletion(); } void MaterialEditorQML::onSDGenerationError(const QString &error) @@ -3051,6 +3011,30 @@ void MaterialEditorQML::onSDGenerationError(const QString &error) emit sdGenerationProgressChanged(); emit sdIsGeneratingChanged(); emit sdGenerationError(error); + + // If SD failed but was triggered by LLM, still emit the material completion + // so the material gets applied (just without the texture) + if (m_sdPendingForMaterial) { + m_sdPendingForMaterial = false; + emit sdPendingForMaterialChanged(); + emit aiGenerationCompleted(m_pendingMaterialScript); + m_pendingMaterialScript.clear(); + } +} + +void MaterialEditorQML::onSDGenerationStopped() +{ + m_sdGenerationProgress = 0.0f; + emit sdGenerationProgressChanged(); + emit sdIsGeneratingChanged(); + + // If SD was stopped but was triggered by LLM, still apply the material + if (m_sdPendingForMaterial) { + m_sdPendingForMaterial = false; + emit sdPendingForMaterialChanged(); + emit aiGenerationCompleted(m_pendingMaterialScript); + m_pendingMaterialScript.clear(); + } } void MaterialEditorQML::onSDModelLoadedChanged() diff --git a/src/MaterialEditorQML.h b/src/MaterialEditorQML.h index db6cd31..3aaf673 100644 --- a/src/MaterialEditorQML.h +++ b/src/MaterialEditorQML.h @@ -133,6 +133,7 @@ class MaterialEditorQML : public QObject Q_PROPERTY(bool sdModelLoaded READ sdModelLoaded NOTIFY sdModelLoadedChanged) Q_PROPERTY(bool sdIsGenerating READ sdIsGenerating NOTIFY sdIsGeneratingChanged) Q_PROPERTY(float sdGenerationProgress READ sdGenerationProgress NOTIFY sdGenerationProgressChanged) + Q_PROPERTY(bool sdPendingForMaterial READ sdPendingForMaterial NOTIFY sdPendingForMaterialChanged) public: explicit MaterialEditorQML(QObject *parent = nullptr); @@ -243,6 +244,7 @@ class MaterialEditorQML : public QObject bool sdModelLoaded() const; bool sdIsGenerating() const; float sdGenerationProgress() const { return m_sdGenerationProgress; } + bool sdPendingForMaterial() const { return m_sdPendingForMaterial; } // Static factory for QML singleton static MaterialEditorQML* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); @@ -388,7 +390,6 @@ public slots: // AI Texture Generation Q_INVOKABLE void generateTextureFromPrompt(const QString &prompt, int width = 0, int height = 0); - Q_INVOKABLE void editTextureFromPrompt(const QString &prompt, float strength = 0.5f); Q_INVOKABLE void stopTextureGeneration(); // Undo/Redo functionality @@ -500,6 +501,7 @@ public slots: void sdGenerationProgressChanged(); void sdTextureGenerated(const QString &filePath); void sdGenerationError(const QString &error); + void sdPendingForMaterialChanged(); private: void updateTechniqueList(); @@ -610,6 +612,8 @@ public slots: // Stable Diffusion float m_sdGenerationProgress = 0.0f; + bool m_sdPendingForMaterial = false; // True when SD is generating a texture triggered by LLM + QString m_pendingMaterialScript; // Deferred material script waiting for SD // Undo/Redo stacks QStringList m_undoStack; @@ -630,6 +634,7 @@ private slots: void onSDGenerationProgress(); void onSDGenerationCompleted(const QString &outputPath); void onSDGenerationError(const QString &error); + void onSDGenerationStopped(); void onSDModelLoadedChanged(); #endif diff --git a/src/SDManager.cpp b/src/SDManager.cpp index 2cb9593..319f7fb 100644 --- a/src/SDManager.cpp +++ b/src/SDManager.cpp @@ -252,7 +252,7 @@ QString SDManager::enhanceTexturePrompt(const QString &prompt) } // Add 3D-specific keywords - enhanced += ", flat surface, top-down view, even lighting, PBR material"; + enhanced += ", flat surface, top-down view, even lighting, normal map compatible"; return enhanced; } @@ -408,51 +408,6 @@ void SDManager::generateTexture(const QString &prompt, int width, int height, co // LCOV_EXCL_STOP } -void SDManager::editTexture(const QString &prompt, const QString &inputImagePath, float strength, const QString &outputFileName) -{ - if (!isModelLoaded()) { - emit generationError("No SD model loaded. Please load a model first."); - return; - } - - // LCOV_EXCL_START — requires a loaded SD model - QString outputPath; - if (!outputFileName.isEmpty()) { - QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - QDir outputDir(QDir(dataPath).filePath("generated_textures")); - if (!outputDir.exists()) { - outputDir.mkpath("."); - } - QString fileName = outputFileName.trimmed(); - fileName = QFileInfo(fileName).fileName(); // Strip path separators to prevent path traversal - if (!fileName.endsWith(".png", Qt::CaseInsensitive) && - !fileName.endsWith(".jpg", Qt::CaseInsensitive)) { - fileName.replace(QRegularExpression(R"(\.\w+$)"), ""); - fileName += ".png"; - } - outputPath = outputDir.filePath(fileName); - } else { - outputPath = generateOutputPath(); - } - - float clampedStrength = qBound(0.0f, strength, 1.0f); - - // Enhance prompt for texture editing - QString enhancedPrompt = enhanceTexturePrompt(prompt); - - SDSettings genSettings = m_settings; - if (genSettings.negativePrompt.isEmpty() || - genSettings.negativePrompt == "blurry, low quality, distorted, simple, cartoon") { - genSettings.negativePrompt = getTextureNegativePrompt(); - } - - QMetaObject::invokeMethod(m_worker, [this, enhancedPrompt, inputImagePath, outputPath, clampedStrength, genSettings]() { - m_worker->setSettings(genSettings); - m_worker->generateFromImage(enhancedPrompt, inputImagePath, outputPath, clampedStrength); - }, Qt::QueuedConnection); - // LCOV_EXCL_STOP -} - void SDManager::stopGeneration() { if (m_worker) { diff --git a/src/SDManager.h b/src/SDManager.h index 4863d8f..5dc65f2 100644 --- a/src/SDManager.h +++ b/src/SDManager.h @@ -104,7 +104,7 @@ public slots: Q_INVOKABLE void scanForModels(); Q_INVOKABLE void generateTexture(const QString &prompt, int width = 0, int height = 0, const QString &outputFileName = QString()); - Q_INVOKABLE void editTexture(const QString &prompt, const QString &inputImagePath, float strength = 0.5f, const QString &outputFileName = QString()); + // img2img disabled — crashes on macOS Metal. Edits use txt2img with combined prompt. Q_INVOKABLE void stopGeneration(); Q_INVOKABLE void tryAutoLoadModel(); diff --git a/src/SDWorker.cpp b/src/SDWorker.cpp index 8c733c8..f98e8d0 100644 --- a/src/SDWorker.cpp +++ b/src/SDWorker.cpp @@ -157,6 +157,31 @@ void SDWorker::requestStop() // LCOV_EXCL_STOP +#ifdef ENABLE_STABLE_DIFFUSION +void SDWorker::recreateContext() +{ + if (m_ctx) { + free_sd_ctx(m_ctx); + m_ctx = nullptr; + } + if (m_modelPath.isEmpty()) return; + + sd_ctx_params_t params; + sd_ctx_params_init(¶ms); + QByteArray pathUtf8 = m_modelPath.toUtf8(); + params.model_path = pathUtf8.constData(); + params.n_threads = m_settings.threads > 0 ? m_settings.threads : QThread::idealThreadCount(); + if (params.n_threads > 16) params.n_threads = 16; + params.vae_decode_only = true; + + m_ctx = new_sd_ctx(¶ms); + if (!m_ctx) { + m_isModelLoaded.store(false); + emit modelLoadError("Failed to recreate SD context"); + } +} +#endif + void SDWorker::generateTexture(const QString &prompt, const QString &outputPath) { #ifdef ENABLE_STABLE_DIFFUSION @@ -177,9 +202,13 @@ void SDWorker::generateTexture(const QString &prompt, const QString &outputPath) QMutexLocker locker(&m_mutex); + // sd.cpp crashes on second generate_image() call with the same context. + // Recreate the context before each generation to ensure clean state. + recreateContext(); + if (!m_ctx) { m_isGenerating.store(false); - emit generationError("SD model was unloaded"); + emit generationError("SD context creation failed"); return; } @@ -271,129 +300,6 @@ void SDWorker::generateTexture(const QString &prompt, const QString &outputPath) #endif } -void SDWorker::generateFromImage(const QString &prompt, const QString &inputImagePath, const QString &outputPath, float strength) -{ -#ifdef ENABLE_STABLE_DIFFUSION - if (!isModelLoaded()) { - emit generationError("No SD model loaded"); - return; - } - - if (m_isGenerating.load()) { - emit generationError("Generation already in progress"); - return; - } - - // LCOV_EXCL_START — requires a loaded SD model and input image - // Load input image - QImage inputImg(inputImagePath); - if (inputImg.isNull()) { - emit generationError(QString("Failed to load input image: %1").arg(inputImagePath)); - return; - } - - // Convert to RGB888 for sd.cpp - QImage rgbImg = inputImg.convertToFormat(QImage::Format_RGB888); - - m_stopRequested.store(false); - m_isGenerating.store(true); - - QMutexLocker locker(&m_mutex); - - if (!m_ctx) { - m_isGenerating.store(false); - emit generationError("SD model was unloaded"); - return; - } - - emit generationStarted(); - - qDebug() << "SDWorker: img2img with prompt:" << prompt << "strength:" << strength; - qDebug() << "SDWorker: Input:" << inputImagePath << rgbImg.width() << "x" << rgbImg.height(); - - sd_set_progress_callback(progressCallback, this); - - sd_image_t *result = nullptr; - try { - sd_img_gen_params_t img_params; - sd_img_gen_params_init(&img_params); - - QByteArray promptUtf8 = prompt.toUtf8(); - QByteArray negPromptUtf8 = m_settings.negativePrompt.toUtf8(); - - img_params.prompt = promptUtf8.constData(); - img_params.negative_prompt = negPromptUtf8.isEmpty() ? "" : negPromptUtf8.constData(); - img_params.width = rgbImg.width(); - img_params.height = rgbImg.height(); - img_params.sample_params.sample_steps = m_settings.steps; - img_params.sample_params.guidance.txt_cfg = m_settings.cfgScale; - img_params.seed = m_settings.seed; - img_params.sample_params.sample_method = static_cast(m_settings.sampleMethod); - - // Set init image for img2img - img_params.init_image.width = rgbImg.width(); - img_params.init_image.height = rgbImg.height(); - img_params.init_image.channel = 3; - img_params.init_image.data = rgbImg.bits(); - img_params.strength = strength; - - result = generate_image(m_ctx, &img_params); - } catch (const std::exception &e) { - sd_set_progress_callback(nullptr, nullptr); - m_isGenerating.store(false); - emit generationError(QString("Exception during img2img: %1").arg(e.what())); - return; - } catch (...) { - sd_set_progress_callback(nullptr, nullptr); - m_isGenerating.store(false); - emit generationError("Unknown exception during img2img generation"); - return; - } - - sd_set_progress_callback(nullptr, nullptr); - - if (m_stopRequested.load()) { - if (result) free(result); - m_isGenerating.store(false); - emit generationStopped(); - return; - } - - if (!result || !result->data) { - if (result) free(result); - m_isGenerating.store(false); - emit generationError("Failed to generate image - no output produced"); - return; - } - - QImage::Format format = (result->channel == 4) ? QImage::Format_RGBA8888 : QImage::Format_RGB888; - int bytesPerLine = static_cast(result->width * result->channel); - size_t dataSize = static_cast(bytesPerLine) * result->height; - QByteArray pixelCopy(reinterpret_cast(result->data), dataSize); - QImage imgCopy(reinterpret_cast(pixelCopy.constData()), - result->width, result->height, bytesPerLine, format); - imgCopy = imgCopy.copy(); - - free(result); - - bool saved = imgCopy.save(outputPath, "PNG"); - m_isGenerating.store(false); - - if (saved) { - qDebug() << "SDWorker: img2img texture saved to" << outputPath; - emit generationCompleted(outputPath); - } else { - emit generationError(QString("Failed to save img2img texture to: %1").arg(outputPath)); - } - // LCOV_EXCL_STOP -#else - Q_UNUSED(prompt); - Q_UNUSED(inputImagePath); - Q_UNUSED(outputPath); - Q_UNUSED(strength); - emit generationError("Stable Diffusion support is not enabled"); -#endif -} #ifdef ENABLE_STABLE_DIFFUSION void SDWorker::progressCallback(int step, int steps, float time, void *data) diff --git a/src/SDWorker.h b/src/SDWorker.h index 215b1f0..8d9325e 100644 --- a/src/SDWorker.h +++ b/src/SDWorker.h @@ -44,7 +44,6 @@ class SDWorker : public QObject public slots: void generateTexture(const QString &prompt, const QString &outputPath); - void generateFromImage(const QString &prompt, const QString &inputImagePath, const QString &outputPath, float strength); signals: void modelLoaded(const QString &modelPath); @@ -70,6 +69,7 @@ public slots: #ifdef ENABLE_STABLE_DIFFUSION sd_ctx_t *m_ctx = nullptr; + void recreateContext(); static void progressCallback(int step, int steps, float time, void *data); #endif };