diff --git a/src/FileCache.cpp b/src/FileCache.cpp index ab6c3387..b0d236ac 100644 --- a/src/FileCache.cpp +++ b/src/FileCache.cpp @@ -5,6 +5,8 @@ #include "editors/source/SourceEditor.h" #include "editors/texture/TextureEditor.h" #include "session/SessionModel.h" +#include +#include #include #include @@ -467,4 +469,51 @@ void FileCache::handleReloadingFailed(const QString &fileName) mUpdateFileSystemWatchesTimer.start(); } +bool FileCache::getSequenceTexture(const QString &baseFileName, + const QString &pattern, + int frameNumber, + bool flipVertically, + TextureData *texture) const +{ + if (!texture) + return false; + + const QString actualFileName = buildSequenceFileName(baseFileName, pattern, frameNumber); + return getTexture(actualFileName, flipVertically, texture); +} + +QString FileCache::buildSequenceFileName(const QString &baseFileName, + const QString &pattern, + int frameNumber) const +{ + if (baseFileName.isEmpty() || pattern.isEmpty()) + return baseFileName; + + QFileInfo fileInfo(baseFileName); + QString dir = fileInfo.absolutePath(); + QString originalExtension = fileInfo.suffix(); + + // Process the pattern to replace %0Xd with zero-padded frame number + QString processedPattern = pattern; + QRegularExpression regex(R"(%0?(\d+)d)"); + QRegularExpressionMatch match = regex.match(pattern); + + if (match.hasMatch()) { + int padding = match.captured(1).toInt(); + QString frameStr = QString("%1").arg(frameNumber, padding, 10, QChar('0')); + processedPattern.replace(regex, frameStr); + } else { + // Fallback: replace any %d with frame number + processedPattern.replace("%d", QString::number(frameNumber)); + } + + // If pattern already has an extension, use it as-is + // Otherwise, append the original file's extension + if (processedPattern.contains('.')) { + return QString("%1/%2").arg(dir, processedPattern); + } else { + return QString("%1/%2.%3").arg(dir, processedPattern, originalExtension); + } +} + #include "FileCache.moc" diff --git a/src/FileCache.h b/src/FileCache.h index 8a757dfb..6e4a531b 100644 --- a/src/FileCache.h +++ b/src/FileCache.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -19,10 +20,19 @@ class FileCache final : public QObject bool getSource(const QString &fileName, QString *source) const; bool getTexture(const QString &fileName, bool flipVertically, TextureData *texture) const; + bool getSequenceTexture(const QString &baseFileName, + const QString &pattern, + int frameNumber, + bool flipVertically, + TextureData *texture) const; bool getBinary(const QString &fileName, QByteArray *binary) const; bool updateTexture(const QString &fileName, bool flippedVertically, TextureData texture); + QString buildSequenceFileName(const QString &baseFileName, + const QString &pattern, + int frameNumber) const; + // only call from main thread void unloadAll(); void invalidateFile(const QString &fileName); diff --git a/src/SynchronizeLogic.cpp b/src/SynchronizeLogic.cpp index f1fde8c5..24be0324 100644 --- a/src/SynchronizeLogic.cpp +++ b/src/SynchronizeLogic.cpp @@ -145,6 +145,11 @@ void SynchronizeLogic::manualEvaluation() evaluate(EvaluationType::Manual); } +void SynchronizeLogic::automaticEvaluation() +{ + evaluate(EvaluationType::Automatic); +} + void SynchronizeLogic::setEvaluationMode(EvaluationMode mode) { if (mEvaluationMode == mode) @@ -304,7 +309,16 @@ void SynchronizeLogic::handleEditorFileRenamed(const QString &prevFileName, void SynchronizeLogic::handleFileItemFileChanged(const FileItem &item) { // update item name - auto name = FileDialog::getFileTitle(item.fileName); + QString displayFileName = item.fileName; + + // For textures, use baseName for display (baseName = fileName for single textures, baseName for sequences) + if (auto texture = castItem(&item)) { + if (!texture->baseName.isEmpty()) { + displayFileName = texture->baseName; + } + } + + auto name = FileDialog::getFileTitle(displayFileName); if (name.isEmpty()) { // only reset to type name when it currently has a filename if (FileDialog::getFileExtension(item.name).isEmpty()) @@ -386,10 +400,71 @@ void SynchronizeLogic::handleEvaluateTimout() void SynchronizeLogic::evaluate(EvaluationType evaluationType) { + // Reset sequence frames when evaluation is reset (F5) + if (evaluationType == EvaluationType::Reset) { + mModel.forEachItem([&](const Texture &texture) { + if (texture.isSequence) { + const_cast(texture).currentFrame = 0; + } + }); + } + + // Advance sequence frames for manual evaluation (F6) + if (evaluationType == EvaluationType::Manual) { + mModel.forEachItem([&](const Texture &texture) { + if (texture.isSequence) { + auto &mutableTexture = const_cast(texture); + + // Advance zero-based frame index + mutableTexture.currentFrame++; + + // Calculate max zero-based index (frameEnd - frameStart) + int maxFrameIndex = texture.frameEnd - texture.frameStart; + + if (mutableTexture.currentFrame > maxFrameIndex) { + if (texture.loopSequence) { + mutableTexture.currentFrame = 0; // Reset to zero index + } else { + mutableTexture.currentFrame = maxFrameIndex; // Clamp to last zero-based index + } + } + } + }); + } + + // Advance sequence frames for steady evaluation (F8 auto-advancing) + if (evaluationType == EvaluationType::Steady) { + mModel.forEachItem([&](const Texture &texture) { + if (texture.isSequence) { + auto &mutableTexture = const_cast(texture); + + // Advance zero-based frame index + mutableTexture.currentFrame++; + + // Calculate max zero-based index (frameEnd - frameStart) + int maxFrameIndex = texture.frameEnd - texture.frameStart; + + if (mutableTexture.currentFrame > maxFrameIndex) { + if (texture.loopSequence) { + mutableTexture.currentFrame = 0; // Reset to zero index + } else { + mutableTexture.currentFrame = maxFrameIndex; // Clamp to last zero-based index + } + } + } + }); + } + + Singletons::fileCache().updateFromEditors(); const auto itemsChanged = std::exchange(mRenderSessionInvalidated, false); initializeRenderSession(); mRenderSession->update(itemsChanged, evaluationType); + + // No frame state restoration needed + + // Notify texture editors about evaluation updates + Q_EMIT evaluationUpdated(); } void SynchronizeLogic::updateEditors() diff --git a/src/SynchronizeLogic.h b/src/SynchronizeLogic.h index a6eef186..f1963327 100644 --- a/src/SynchronizeLogic.h +++ b/src/SynchronizeLogic.h @@ -27,6 +27,7 @@ class SynchronizeLogic final : public QObject EvaluationMode evaluationMode() const { return mEvaluationMode; } void resetEvaluation(); void manualEvaluation(); + void automaticEvaluation(); bool resetRenderSessionInvalidationState(); void updateEditor(ItemId itemId, bool activated); @@ -50,6 +51,7 @@ class SynchronizeLogic final : public QObject Q_SIGNALS: void outputChanged(QVariant output); + void evaluationUpdated(); private: void invalidateRenderSession(); diff --git a/src/TextureData.cpp b/src/TextureData.cpp index 808819b5..723cbf99 100644 --- a/src/TextureData.cpp +++ b/src/TextureData.cpp @@ -956,7 +956,16 @@ bool TextureData::saveOpenImageIO(const QString &fileName, return false; const auto channelCount = getTextureComponentCount(format()); - const auto spec = ImageSpec(width(), height(), channelCount, typeDesc); + auto spec = ImageSpec(width(), height(), channelCount, typeDesc); + + // TODO: Expose in Preferences UI + spec.attribute("Compression", "jpeg:98"); + spec.attribute("pnm:binary", 1); + spec.attribute("pnm:pfmflip", 0); + spec.attribute("oiio:UnassociatedAlpha", 1); + spec.attribute("jpeg:subsampling", "4:4:4"); + spec.attribute("png:compressionLevel", 4); + if (!output->open(fileName.toStdWString(), spec)) return false; if (!output->write_image(typeDesc, getData(0, 0, 0))) diff --git a/src/editors/EditorManager.cpp b/src/editors/EditorManager.cpp index d4282ab8..e3a49f61 100644 --- a/src/editors/EditorManager.cpp +++ b/src/editors/EditorManager.cpp @@ -3,6 +3,7 @@ #include "FileDialog.h" #include "Singletons.h" #include "SynchronizeLogic.h" +#include "../session/SessionModel.h" #include "binary/BinaryEditor.h" #include "binary/BinaryEditorToolBar.h" #include "qml/QmlView.h" @@ -406,9 +407,14 @@ BinaryEditor *EditorManager::getBinaryEditor(const QString &fileName) TextureEditor *EditorManager::getTextureEditor(const QString &fileName) { - for (TextureEditor *editor : std::as_const(mTextureEditors)) - if (editor->fileName() == fileName) + // Get the editor key for this fileName + QString editorKey = getTextureEditorKey(fileName); + + for (TextureEditor *editor : std::as_const(mTextureEditors)) { + QString existingKey = getTextureEditorKey(editor->fileName()); + if (existingKey == editorKey) return editor; + } return nullptr; } @@ -726,11 +732,44 @@ void EditorManager::handleEditorFilenameChanged(QDockWidget *dock) void EditorManager::setDockWindowTitle(QDockWidget *dock, const QString &fileName) { - dock->setWindowTitle(FileDialog::getWindowTitle(fileName)); + QString displayFileName = fileName; + + // For textures, show baseName for display (baseName = fileName for single textures, baseName for sequences) + auto &model = Singletons::sessionModel(); + model.forEachItem([&](const Item &item) { + if (auto texture = castItem(&item)) { + if (texture->fileName == fileName && !texture->baseName.isEmpty()) { + displayFileName = texture->baseName; + return false; // stop iteration + } + } + return true; // continue iteration + }); + + dock->setWindowTitle(FileDialog::getWindowTitle(displayFileName)); if (!FileDialog::isEmptyOrUntitled(fileName)) dock->setStatusTip(fileName); } +QString EditorManager::getTextureEditorKey(const QString &fileName) const +{ + // Always use baseName as editor key (baseName = fileName for single textures, baseName for sequences) + auto &model = Singletons::sessionModel(); + QString editorKey = fileName; // fallback if texture not found + + model.forEachItem([&](const Item &item) { + if (auto texture = castItem(&item)) { + if (texture->fileName == fileName && !texture->baseName.isEmpty()) { + editorKey = texture->baseName; + return false; // stop iteration + } + } + return true; // continue iteration + }); + + return editorKey; +} + bool EditorManager::saveDock(QDockWidget *dock) { auto currentDock = mCurrentDock; diff --git a/src/editors/EditorManager.h b/src/editors/EditorManager.h index 3302ad34..84e1f15f 100644 --- a/src/editors/EditorManager.h +++ b/src/editors/EditorManager.h @@ -112,6 +112,7 @@ class EditorManager final : public DockWindow void clearNavigationStack(); void addNavigationPosition(const QString &position, bool update); bool restoreNavigationPosition(int index); + QString getTextureEditorKey(const QString &fileName) const; QList mSourceEditors; QList mBinaryEditors; diff --git a/src/editors/texture/TextureEditor.cpp b/src/editors/texture/TextureEditor.cpp index 1fe02cde..991d1edd 100644 --- a/src/editors/texture/TextureEditor.cpp +++ b/src/editors/texture/TextureEditor.cpp @@ -13,9 +13,13 @@ #include "getEventPosition.h" #include "render/opengl/GLContext.h" #include "session/Item.h" +#include "session/SessionModel.h" #include #include #include +#include +#include +#include #include #include #include @@ -39,13 +43,14 @@ bool createFromRaw(const QByteArray &binary, const TextureEditor::RawFormat &r, return true; } + TextureEditor::TextureEditor(QString fileName, TextureEditorToolBar *editorToolBar, TextureInfoBar *textureInfoBar, QWidget *parent) : QAbstractScrollArea(parent) , mEditorToolBar(*editorToolBar) , mTextureInfoBar(*textureInfoBar) - , mFileName(fileName) + , mFileName(getTextureEditorKey(fileName)) { mGLWidget = new GLWidget(this); setViewport(mGLWidget); @@ -58,6 +63,28 @@ TextureEditor::TextureEditor(QString fileName, setAcceptDrops(false); setMouseTracking(true); setFrameStyle(QFrame::NoFrame); + + // Connect to evaluation system for sequence texture updates + connectToEvaluationSystem(); +} + +QString TextureEditor::getTextureEditorKey(const QString &fileName) const +{ + // Use same logic as EditorManager::getTextureEditorKey() + auto &model = Singletons::sessionModel(); + QString editorKey = fileName; // fallback if texture not found + + model.forEachItem([&](const Item &item) { + if (auto texture = castItem(&item)) { + if (texture->fileName == fileName && !texture->baseName.isEmpty()) { + editorKey = texture->baseName; + return false; // stop iteration + } + } + return true; // continue iteration + }); + + return editorKey; } TextureEditor::~TextureEditor() @@ -90,13 +117,14 @@ QList TextureEditor::connectEditActions( { actions.copy->setEnabled(true); actions.findReplace->setEnabled(true); - actions.windowFileName->setText(fileName()); + actions.windowFileName->setText(getDisplayFileName()); actions.windowFileName->setEnabled(isModified()); auto c = QList(); c += connect(actions.copy, &QAction::triggered, this, &TextureEditor::copy); - c += connect(this, &TextureEditor::fileNameChanged, actions.windowFileName, - &QAction::setText); + c += connect(this, &TextureEditor::fileNameChanged, [this, actions](const QString &) { + actions.windowFileName->setText(getDisplayFileName()); + }); c += connect(this, &TextureEditor::modificationChanged, actions.windowFileName, &QAction::setEnabled); @@ -200,10 +228,15 @@ bool TextureEditor::load() { auto texture = TextureData(); auto isRaw = false; - if (!Singletons::fileCache().getTexture(mFileName, false, &texture)) { + + // For sequences: use baseName file for initial load, later updates use current frame + QString fileToLoad = mFileName; + qDebug() << "TextureEditor::load() loading file:" << fileToLoad; + + if (!Singletons::fileCache().getTexture(fileToLoad, false, &texture)) { auto binary = QByteArray(); - if (!Singletons::fileCache().getBinary(mFileName, &binary)) - if (!FileDialog::isEmptyOrUntitled(mFileName)) + if (!Singletons::fileCache().getBinary(fileToLoad, &binary)) + if (!FileDialog::isEmptyOrUntitled(fileToLoad)) return false; if (!createFromRaw(binary, mRawFormat, &texture)) return false; @@ -246,7 +279,9 @@ int TextureEditor::tabifyGroup() const bool TextureEditor::save() { - if (!mTexture.save(fileName(), !mTextureItem->flipVertically())) + // fileName now always contains the actual file path + qDebug() << "Saving texture to:" << mFileName; + if (!mTexture.save(mFileName, !mTextureItem->flipVertically())) return false; setModified(false); @@ -587,3 +622,137 @@ void TextureEditor::paintGL() const auto y = scrollY / height; mTextureItem->paintGL(QTransform(sx, 0, 0, 0, sy, 0, x, y, 1)); } + +void TextureEditor::connectToEvaluationSystem() +{ + // Connect to the synchronize logic to get notified of evaluations + auto &synchronizeLogic = Singletons::synchronizeLogic(); + connect(&synchronizeLogic, &SynchronizeLogic::evaluationUpdated, + this, &TextureEditor::handleEvaluationUpdate); +} + +void TextureEditor::handleEvaluationUpdate() +{ + // Check if this texture editor needs updates + auto &model = Singletons::sessionModel(); + bool shouldAutoSave = false; + bool sequenceProcessed = false; + + model.forEachItem([&](const Item &item) { + if (item.type == Item::Type::Texture) { + const auto &textureItem = static_cast(item); + + // Check if this editor displays this texture + // Use same logic as EditorManager::getTextureEditorKey() + QString textureEditorKey = textureItem.isSequence && !textureItem.baseName.isEmpty() + ? textureItem.baseName + : textureItem.fileName; + + qDebug() << "TextureEditor: Checking texture - editorKey=" << textureEditorKey + << "mFileName=" << mFileName + << "currentFrame=" << textureItem.fileName + << "isSequence=" << textureItem.isSequence; + + if (textureEditorKey == mFileName) { + qDebug() << "TextureEditor: Found matching texture!"; + // For sequences: always reload current frame + if (textureItem.isSequence) { + qDebug() << "TextureEditor: Loading frame" << textureItem.fileName; + sequenceProcessed = true; + // Load the current frame file directly + auto texture = TextureData(); + if (Singletons::fileCache().getTexture(textureItem.fileName, false, &texture)) { + replace(texture, false); + } + } + + // Check for auto-save textures + if (textureItem.autoSave) { + shouldAutoSave = true; + } + } + } + }); + + // For single textures: reload from fileName + // For sequences: frame already loaded above via replace() + if (!sequenceProcessed) { + qDebug() << "TextureEditor: Single texture - calling load()"; + if (load()) { + viewport()->update(); + } + } else { + qDebug() << "TextureEditor: Sequence processed - skipping load()"; + viewport()->update(); + } + + if (shouldAutoSave) { + autoSave(); + } +} + +void TextureEditor::autoSave() +{ + // Generate auto-save filename based on current texture + auto &model = Singletons::sessionModel(); + QString autoSaveFileName; + + model.forEachItem([&](const Item &item) { + if (item.type == Item::Type::Texture) { + const auto &textureItem = static_cast(item); + if (textureItem.fileName == mFileName && textureItem.autoSave) { + // Use baseName for sequences, or fileName for single textures + QString sourceFileName = textureItem.isSequence && !textureItem.baseName.isEmpty() + ? textureItem.baseName + : textureItem.fileName; + + // Generate timestamp filename + QFileInfo fileInfo(sourceFileName); + QString baseName = fileInfo.completeBaseName(); + QString extension = fileInfo.suffix(); + QString dirPath = fileInfo.absolutePath(); + + // Generate timestamp: YYYYMMDDSS:sss (SS=seconds, sss=milliseconds) + QDateTime now = QDateTime::currentDateTime(); + QString timestamp = now.toString("yyyyMMddhhmmss"); + int milliseconds = now.time().msec(); + QString timestampWithMs = QString("%1%2").arg(timestamp).arg(milliseconds, 3, 10, QChar('0')); + + // Create filename: FileName-YYYYMMDDSSsss.ext + QString autoSaveFileNameOnly = QString("%1-%2.%3") + .arg(baseName) + .arg(timestampWithMs) + .arg(extension); + + // Return full path + autoSaveFileName = QDir(dirPath).filePath(autoSaveFileNameOnly); + } + } + }); + + if (!autoSaveFileName.isEmpty()) { + // Save texture to auto-save filename + qDebug() << "Auto-saving texture to:" << autoSaveFileName; + mTexture.save(autoSaveFileName, !mTextureItem->flipVertically()); + } +} + +QString TextureEditor::getDisplayFileName() const +{ + // For textures, return baseName for display (baseName = fileName for single textures, baseName for sequences) + auto &model = Singletons::sessionModel(); + QString displayFileName = mFileName; + + model.forEachItem([&](const Item &item) { + if (item.type == Item::Type::Texture) { + const auto &textureItem = static_cast(item); + if (textureItem.fileName == mFileName && !textureItem.baseName.isEmpty()) { + displayFileName = textureItem.baseName; + return false; // stop iteration + } + } + return true; // continue iteration + }); + + return displayFileName; +} diff --git a/src/editors/texture/TextureEditor.h b/src/editors/texture/TextureEditor.h index cf2d7abe..fb6f3c10 100644 --- a/src/editors/texture/TextureEditor.h +++ b/src/editors/texture/TextureEditor.h @@ -46,6 +46,9 @@ class TextureEditor final : public QAbstractScrollArea, public IEditor void updatePreviewTexture(ShareSyncPtr shareSync, ShareHandle handle, int samples); const TextureData &texture() const { return mTexture; } + void connectToEvaluationSystem(); + void handleEvaluationUpdate(); + void autoSave(); Q_SIGNALS: void modificationChanged(bool modified); @@ -74,6 +77,8 @@ class TextureEditor final : public QAbstractScrollArea, public IEditor double getZoomScale() const; void setModified(bool modified); void updateEditorToolBar(); + QString getTextureEditorKey(const QString &fileName) const; + QString getDisplayFileName() const; void updateScrollBars(); int margin() const; void zoomToFit(); @@ -84,7 +89,7 @@ class TextureEditor final : public QAbstractScrollArea, public IEditor GLWidget *mGLWidget{}; TextureEditorToolBar &mEditorToolBar; TextureInfoBar &mTextureInfoBar; - QString mFileName; + QString mFileName; // Editor key: baseName for sequences, fileName for singles RawFormat mRawFormat{}; bool mIsRaw{}; bool mModified{}; diff --git a/src/icons/light/icons/media-back_playback-start.svg b/src/icons/light/icons/media-back_playback-start.svg new file mode 100644 index 00000000..7879c743 --- /dev/null +++ b/src/icons/light/icons/media-back_playback-start.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/icons/light/icons/media-skip-backward.svg b/src/icons/light/icons/media-skip-backward.svg new file mode 100644 index 00000000..1e0583f5 --- /dev/null +++ b/src/icons/light/icons/media-skip-backward.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/icons/light/icons/media-step-backward.svg b/src/icons/light/icons/media-step-backward.svg new file mode 100644 index 00000000..cf969952 --- /dev/null +++ b/src/icons/light/icons/media-step-backward.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/icons/light/icons/media-step-forward.svg b/src/icons/light/icons/media-step-forward.svg new file mode 100644 index 00000000..2ca91723 --- /dev/null +++ b/src/icons/light/icons/media-step-forward.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/render/RenderSessionBase.cpp b/src/render/RenderSessionBase.cpp index c4df92bc..192ea20f 100644 --- a/src/render/RenderSessionBase.cpp +++ b/src/render/RenderSessionBase.cpp @@ -52,9 +52,21 @@ void RenderSessionBase::prepare(bool itemsChanged, } else { mEvaluationType = EvaluationType::Reset; } - if (mItemsChanged || mEvaluationType == EvaluationType::Reset) { + if (mItemsChanged || mEvaluationType == EvaluationType::Reset || + mEvaluationType == EvaluationType::Manual || mEvaluationType == EvaluationType::Steady) { mUsedItems.clear(); mSessionModelCopy = Singletons::sessionModel(); + + // Debug: Check if sequence textures have updated currentFrame values + mSessionModelCopy.forEachItem([&](const Texture &texture) { + if (texture.isSequence) { + int baseFrame = texture.frameStart + texture.currentFrame; + int actualFrame = texture.getActualFrameNumber(); + qDebug() << QString("Update sequence texture %1 Base=%2 Offset=%3 Final=%4") + .arg(texture.fileName).arg(baseFrame).arg(texture.frameOffset).arg(actualFrame); + qDebug() << QString(" baseName: %1").arg(texture.baseName); + } + }); } } diff --git a/src/render/TextureBase.cpp b/src/render/TextureBase.cpp index 84337d06..a2a30079 100644 --- a/src/render/TextureBase.cpp +++ b/src/render/TextureBase.cpp @@ -6,6 +6,7 @@ #include "RenderSessionBase.h" #include + void transformClearColor(std::array &color, TextureSampleType sampleType) { @@ -49,6 +50,14 @@ TextureBase::TextureBase(const Texture &texture, , mFormat(texture.format) , mSamples(texture.samples) , mKind(getKind(texture)) + , mIsSequence(texture.isSequence) + , mSequencePattern(texture.sequencePattern) + , mFrameStart(texture.frameStart) + , mFrameEnd(texture.frameEnd) + , mFrameOffset(texture.frameOffset) + , mLoopSequence(texture.loopSequence) + , mCurrentFrame(texture.currentFrame) + , mFrameLoaded(texture.currentFrame > 0) { renderSession.evaluateTextureProperties(texture, &mWidth, &mHeight, &mDepth, &mLayers); @@ -67,6 +76,13 @@ TextureBase::TextureBase(const Texture &texture, mSamples = 1; mUsedItems += texture.id; + + // Debug: Show sequence mode initialization + if (mIsSequence) { + mMessages += MessageList::insert(mItemId, MessageType::ScriptMessage, + QString("Sequence initialized: pattern='%1', frames %2-%3, loop=%4") + .arg(mSequencePattern).arg(mFrameStart).arg(mFrameEnd).arg(mLoopSequence ? "on" : "off")); + } } TextureBase::TextureBase(const Buffer &buffer, Texture::Format format, @@ -88,7 +104,7 @@ bool TextureBase::operator==(const TextureBase &rhs) const { const auto properties = [](const TextureBase &a) { return std::tie(a.mMessages, a.mFileName, a.mFlipVertically, a.mTarget, - a.mFormat, a.mWidth, a.mHeight, a.mDepth, a.mLayers, a.mSamples); + a.mFormat, a.mWidth, a.mHeight, a.mDepth, a.mLayers, a.mSamples, a.mCurrentFrame); }; return properties(*this) == properties(rhs); } @@ -110,8 +126,14 @@ bool TextureBase::swap(TextureBase &other) void TextureBase::reload(bool forWriting) { + // Update sequence frame before loading + updateSequenceFrame(); + auto fileData = TextureData{}; - if (Singletons::fileCache().getTexture(mFileName, mFlipVertically, + QString actualFileName = resolveCurrentFileName(); + + + if (Singletons::fileCache().getTexture(actualFileName, mFlipVertically, &fileData)) { // check if cache still matches the file before conversion if (!mFileData.isSharedWith(fileData)) { @@ -125,12 +147,12 @@ void TextureBase::reload(bool forWriting) } } else if (!forWriting) { mMessages += MessageList::insert(mItemId, - MessageType::ConvertingFileFailed, mFileName); + MessageType::ConvertingFileFailed, actualFileName); } } - } else if (!FileDialog::isEmptyOrUntitled(mFileName)) { + } else if (!FileDialog::isEmptyOrUntitled(actualFileName)) { mMessages += MessageList::insert(mItemId, - MessageType::LoadingFileFailed, mFileName); + MessageType::LoadingFileFailed, actualFileName); } if (mData.isNull()) { @@ -145,3 +167,22 @@ void TextureBase::reload(bool forWriting) mSystemCopyModified = true; } } + +QString TextureBase::resolveCurrentFileName() const +{ + if (!mIsSequence) + return mFileName; + + // Use the same calculation as the Texture struct + int actualFrameNumber = mFrameStart + mCurrentFrame + mFrameOffset; + return Singletons::fileCache().buildSequenceFileName( + mFileName, mSequencePattern, actualFrameNumber); +} + +void TextureBase::updateSequenceFrame() +{ + // TextureBase should NOT advance frames - it should use the currentFrame + // from the session model which is already managed by SynchronizeLogic + // This method is kept for compatibility but doesn't modify mCurrentFrame +} + diff --git a/src/render/TextureBase.h b/src/render/TextureBase.h index 920ee39c..9b485080 100644 --- a/src/render/TextureBase.h +++ b/src/render/TextureBase.h @@ -33,6 +33,8 @@ class TextureBase protected: bool swap(TextureBase &other); void reload(bool forWriting); + QString resolveCurrentFileName() const; + void updateSequenceFrame(); ItemId mItemId{}; MessagePtrSet mMessages; @@ -52,6 +54,16 @@ class TextureBase bool mSystemCopyModified{}; bool mDeviceCopyModified{}; bool mMipmapsInvalidated{}; + + // Sequence support + bool mIsSequence{}; + QString mSequencePattern; + int mFrameStart{}; + int mFrameEnd{}; + int mFrameOffset{}; + bool mLoopSequence{}; + mutable int mCurrentFrame{}; + mutable bool mFrameLoaded{}; }; void transformClearColor(std::array &color, diff --git a/src/render/opengl/GLRenderSession.cpp b/src/render/opengl/GLRenderSession.cpp index 77c3910a..926ff550 100644 --- a/src/render/opengl/GLRenderSession.cpp +++ b/src/render/opengl/GLRenderSession.cpp @@ -367,7 +367,8 @@ void GLRenderSession::buildCommandQueue() void GLRenderSession::render() { - if (mItemsChanged || mEvaluationType == EvaluationType::Reset) { + if (mItemsChanged || mEvaluationType == EvaluationType::Reset || + mEvaluationType == EvaluationType::Manual || mEvaluationType == EvaluationType::Steady) { createCommandQueue(); buildCommandQueue(); } diff --git a/src/render/vulkan/VKRenderSession.cpp b/src/render/vulkan/VKRenderSession.cpp index 06f58b72..de6e25d8 100644 --- a/src/render/vulkan/VKRenderSession.cpp +++ b/src/render/vulkan/VKRenderSession.cpp @@ -413,7 +413,8 @@ void VKRenderSession::render() if (!mShareSync) mShareSync = std::make_shared(renderer().device()); - if (mItemsChanged || mEvaluationType == EvaluationType::Reset) { + if (mItemsChanged || mEvaluationType == EvaluationType::Reset || + mEvaluationType == EvaluationType::Manual || mEvaluationType == EvaluationType::Steady) { createCommandQueue(); buildCommandQueue(); } diff --git a/src/session/Item.h b/src/session/Item.h index 53d6f155..d435a65b 100644 --- a/src/session/Item.h +++ b/src/session/Item.h @@ -93,6 +93,24 @@ struct Texture : FileItem QString layers{ "1" }; int samples{ 1 }; bool flipVertically{}; + bool autoSave{}; + QString baseName; // For sequences: stores original base path + + // Image sequence support + bool isSequence{}; + QString sequencePattern{ "%06d" }; + int frameStart{ 1 }; + int frameEnd{ 100 }; + int frameOffset{ 0 }; + bool loopSequence{ true }; + + // Runtime state (not serialized) - zero-based index + int currentFrame{ 0 }; + + // Utility function to calculate actual frame number with offset + int getActualFrameNumber() const { + return frameStart + currentFrame + frameOffset; + } }; struct Program : Item diff --git a/src/session/PropertiesEditor.cpp b/src/session/PropertiesEditor.cpp index 27298bca..99dda0fa 100644 --- a/src/session/PropertiesEditor.cpp +++ b/src/session/PropertiesEditor.cpp @@ -597,6 +597,8 @@ void PropertiesEditor::saveCurrentItemFileAs(FileDialog::Options options) const auto prevFileName = currentItemFileName(); auto fileName = prevFileName; + // fileName already contains the correct path for both single and sequence textures + switchToCurrentFileItemDirectory(); while (Singletons::fileDialog().exec(options, fileName)) { fileName = Singletons::fileDialog().fileName(); diff --git a/src/session/SessionModelCore.cpp b/src/session/SessionModelCore.cpp index 5072a3cb..faf7e147 100644 --- a/src/session/SessionModelCore.cpp +++ b/src/session/SessionModelCore.cpp @@ -677,8 +677,19 @@ void SessionModelCore::undoableFileNameAssignment(const QModelIndex &index, } Q_ASSERT(isNativeCanonicalFilePath(fileName)); - if (item.fileName != fileName) + if (item.fileName != fileName) { undoableAssignment(index, &item.fileName, fileName); + + // For textures: always update baseName for single textures + if (auto texture = castItem(&item)) { + if (!texture->isSequence) { + // Single texture: always set baseName = fileName (even if baseName already exists) + auto &textureRef = static_cast(item); + undoableAssignment(index, &textureRef.baseName, fileName); + } + // Note: For sequences, baseName is managed explicitly in TextureProperties UI + } + } } bool SessionModelCore::isDynamicGroup(const Item &item) const diff --git a/src/session/SessionModelCore.h b/src/session/SessionModelCore.h index 1b3d54fd..de19a518 100644 --- a/src/session/SessionModelCore.h +++ b/src/session/SessionModelCore.h @@ -39,6 +39,15 @@ class SessionModelCore : public QAbstractItemModel TextureLayers, TextureSamples, TextureFlipVertically, + TextureAutoSave, + TextureBaseName, + TextureIsSequence, + TextureSequencePattern, + TextureFrameStart, + TextureFrameEnd, + TextureFrameOffset, + TextureLoopSequence, + TextureCurrentFrame, ScriptExecuteOn, ShaderType, ShaderLanguage, diff --git a/src/session/SessionModelPriv.h b/src/session/SessionModelPriv.h index 84260399..80015ce6 100644 --- a/src/session/SessionModelPriv.h +++ b/src/session/SessionModelPriv.h @@ -33,6 +33,15 @@ ADD(TextureLayers, Texture, layers) \ ADD(TextureSamples, Texture, samples) \ ADD(TextureFlipVertically, Texture, flipVertically) \ + ADD(TextureAutoSave, Texture, autoSave) \ + ADD(TextureBaseName, Texture, baseName) \ + ADD(TextureIsSequence, Texture, isSequence) \ + ADD(TextureSequencePattern, Texture, sequencePattern) \ + ADD(TextureFrameStart, Texture, frameStart) \ + ADD(TextureFrameEnd, Texture, frameEnd) \ + ADD(TextureFrameOffset, Texture, frameOffset) \ + ADD(TextureLoopSequence, Texture, loopSequence) \ + ADD(TextureCurrentFrame, Texture, currentFrame) \ ADD(ScriptExecuteOn, Script, executeOn) \ ADD(ShaderType, Shader, shaderType) \ ADD(ShaderLanguage, Shader, language) \ diff --git a/src/session/TextureProperties.cpp b/src/session/TextureProperties.cpp index 9261fc82..b36a6126 100644 --- a/src/session/TextureProperties.cpp +++ b/src/session/TextureProperties.cpp @@ -2,13 +2,82 @@ #include "FileCache.h" #include "PropertiesEditor.h" #include "Singletons.h" +#include "SynchronizeLogic.h" #include "editors/EditorManager.h" #include "session/SessionModel.h" #include "ui_TextureProperties.h" #include #include +#include +#include +#include +#include +#include namespace { + enum class TextureUsageType { + Unassigned, // No file path, not assigned to target + LoadTexture, // Has file path but not assigned to target + TargetTexture // Assigned to any target attachment + }; + + + TextureUsageType getTextureUsageType(ItemId textureId, const SessionModel &model) { + bool hasFilePath = false; + bool isAssignedToTarget = false; + + // Check if texture has a file path + if (auto texture = model.findItem(textureId)) { + hasFilePath = !texture->fileName.isEmpty(); + } + + // Check if texture is assigned to any target attachment + model.forEachItem([&](const Attachment &attachment) { + if (attachment.textureId == textureId) { + isAssignedToTarget = true; + } + }); + + if (isAssignedToTarget) { + return TextureUsageType::TargetTexture; + } else if (hasFilePath) { + return TextureUsageType::LoadTexture; + } else { + return TextureUsageType::Unassigned; + } + } + + QString getTextureUsageString(TextureUsageType usageType) { + switch (usageType) { + case TextureUsageType::TargetTexture: return "Target texture"; + case TextureUsageType::LoadTexture: return "Load texture"; + case TextureUsageType::Unassigned: return "Unassigned texture"; + } + return "Unknown"; + } + + QString generateAutoSaveFileName(const QString &originalFilePath) { + QFileInfo fileInfo(originalFilePath); + QString baseName = fileInfo.completeBaseName(); + QString extension = fileInfo.suffix(); + QString dirPath = fileInfo.absolutePath(); + + // Generate timestamp: YYYYMMDDSS:sss (SS=seconds, sss=milliseconds) + QDateTime now = QDateTime::currentDateTime(); + QString timestamp = now.toString("yyyyMMddhhmmss"); + int milliseconds = now.time().msec(); + QString timestampWithMs = QString("%1%2").arg(timestamp).arg(milliseconds, 3, 10, QChar('0')); + + // Create filename: FileName-YYYYMMDDSSsss.ext + QString autoSaveFileName = QString("%1-%2.%3") + .arg(baseName) + .arg(timestampWithMs) + .arg(extension); + + // Return full path + return QDir(dirPath).filePath(autoSaveFileName); + } + enum FormatType { R, RG, @@ -270,14 +339,60 @@ TextureProperties::TextureProperties(PropertiesEditor *propertiesEditor) mPropertiesEditor.saveCurrentItemFileAs(FileDialog::TextureExtensions); }); connect(mUi->fileBrowse, &QToolButton::clicked, [this]() { - mPropertiesEditor.openCurrentItemFile(FileDialog::TextureExtensions); + auto index = mPropertiesEditor.currentModelIndex(); + bool wasSequence = false; + if (auto texture = mPropertiesEditor.model().item(index)) { + wasSequence = texture->isSequence; + qDebug() << "FileBrowse: wasSequence=" << wasSequence << "baseName=" << texture->baseName; + } + + if (mPropertiesEditor.openCurrentItemFile(FileDialog::TextureExtensions)) { + qDebug() << "FileBrowse: File opened successfully"; + // If this was a sequence, we need to update baseName to the new file path + if (wasSequence) { + qDebug() << "FileBrowse: Handling sequence update"; + // Refresh the index to get updated data + index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + // Update baseName to the newly loaded file path + qDebug() << "Updating baseName from" << texture->baseName << "to" << texture->fileName; + mPropertiesEditor.model().setData( + mPropertiesEditor.model().getIndex(index, SessionModel::TextureBaseName), + texture->fileName); + // Now update the fileName to the correct sequence frame + updateFileNameForSequence(); + // Force render system to refresh with updated fileName + Singletons::synchronizeLogic().automaticEvaluation(); + } + } else { + qDebug() << "FileBrowse: Not a sequence, skipping baseName update"; + } + } else { + qDebug() << "FileBrowse: File open failed or cancelled"; + } updateWidgets(); applyFileFormat(); }); connect(mUi->file, qOverload(&ReferenceComboBox::activated), this, &TextureProperties::applyFileFormat); connect(mUi->file, &ReferenceComboBox::textRequired, - [](auto data) { return FileDialog::getFileTitle(data.toString()); }); + [this](auto data) { + QString displayFileName = data.toString(); + + // Show the logical filename for user display + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + if (texture->fileName == displayFileName) { + if (texture->isSequence && !texture->baseName.isEmpty()) { + // Sequence mode: show baseName (e.g., "sequence.jpg") + displayFileName = texture->baseName; + } + // Single frame mode: use fileName as-is + } + } + + return FileDialog::getFileTitle(displayFileName); + }); connect(mUi->file, &ReferenceComboBox::listRequired, [this]() { return mPropertiesEditor.getFileNames(Item::Type::Texture, true); }); @@ -291,6 +406,131 @@ TextureProperties::TextureProperties(PropertiesEditor *propertiesEditor) connect(mUi->formatData, &DataComboBox::currentDataChanged, this, &TextureProperties::updateFormat); + // Sequence controls + connect(mUi->isSequence, &QCheckBox::toggled, this, [this](bool enabled) { + handleSequenceModeChanged(enabled); + updateSequenceVisibility(); + }); + connect(mUi->frameStart, qOverload(&QSpinBox::valueChanged), this, + [this](int value) { + qDebug() << "TextureProperties: frameStart valueChanged to" << value; + if (value > mUi->frameEnd->value()) { + qDebug() << "TextureProperties: Adjusting frameEnd from" << mUi->frameEnd->value() << "to" << value; + mUi->frameEnd->setValue(value); + } + updateCurrentFrameLabel(); + }); + connect(mUi->frameEnd, qOverload(&QSpinBox::valueChanged), this, + [this](int value) { + qDebug() << "TextureProperties: frameEnd valueChanged to" << value; + if (value < mUi->frameStart->value()) { + qDebug() << "TextureProperties: Adjusting frameStart from" << mUi->frameStart->value() << "to" << value; + mUi->frameStart->setValue(value); + } + updateCurrentFrameLabel(); + }); + connect(mUi->frameOffset, qOverload(&QSpinBox::valueChanged), this, [this](int value) { + qDebug() << "TextureProperties: frameOffset valueChanged to" << value; + updateCurrentFrameLabel(); + mUi->file->update(); + }); + + // Initially hide sequence controls + updateSequenceVisibility(); + + // Connect to session model for current frame updates + connect(&Singletons::sessionModel(), &SessionModel::dataChanged, + this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { + updateCurrentFrameLabel(); + + // Check if sequence-related fields changed + for (int column = topLeft.column(); column <= bottomRight.column(); ++column) { + if (column == SessionModel::TextureFrameStart || + column == SessionModel::TextureFrameEnd || + column == SessionModel::TextureFrameOffset) { + qDebug() << "TextureProperties: Sequence field changed, column=" << column + << "triggering updateFileNameForSequence"; + updateFileNameForSequence(); + // Trigger evaluation to update TextureEditor (without advancing currentFrame) + QTimer::singleShot(0, []() { + qDebug() << "TextureProperties: Triggering automaticEvaluation after sequence change"; + Singletons::synchronizeLogic().automaticEvaluation(); + }); + break; + } + } + }); + + // Connect to evaluation updates for immediate frame counter updates + connect(&Singletons::synchronizeLogic(), &SynchronizeLogic::evaluationUpdated, + this, [this]() { + updateCurrentFrameLabel(); + updateFileNameForSequence(); + }); + + // Connect usage label updates + connect(&Singletons::sessionModel(), &SessionModel::dataChanged, + this, &TextureProperties::updateUsageLabel); + connect(mUi->file, &ReferenceComboBox::currentDataChanged, + this, &TextureProperties::updateUsageLabel); + + // Connect auto save state updates + connect(&Singletons::sessionModel(), &SessionModel::dataChanged, + this, &TextureProperties::updateAutoSaveState); + connect(mUi->file, &ReferenceComboBox::currentDataChanged, + this, &TextureProperties::updateAutoSaveState); + + // Connect sequence state updates + connect(&Singletons::sessionModel(), &SessionModel::dataChanged, + this, &TextureProperties::updateSequenceState); + connect(mUi->file, &ReferenceComboBox::currentDataChanged, + this, &TextureProperties::updateSequenceState); + + // Connect auto save warning (only for user-initiated changes) + connect(mUi->autoSave, &QCheckBox::clicked, this, [this](bool enabled) { + if (enabled) { + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + QString fileName = texture->fileName; + QFileInfo fileInfo(fileName); + QString baseName = fileInfo.completeBaseName(); + QString extension = fileInfo.suffix(); + QString dirPath = fileInfo.absolutePath(); + + // Check if the directory exists + QDir dir(dirPath); + if (!dir.exists()) { + QMessageBox msgBox(this); + msgBox.setWindowTitle("Warning!"); + msgBox.setText("Directory does not exist"); + msgBox.setInformativeText(QString("The directory '%1' does not exist.\n\nAuto-save cannot be enabled.") + .arg(dirPath)); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); + + // Uncheck the checkbox since the directory doesn't exist + mUi->autoSave->setChecked(false); + return; + } + + // Create example filename with literal timestamp string + QString exampleFileName = QString("%1-YYYYMMDDSSsss.%2") + .arg(baseName) + .arg(extension); + + QMessageBox msgBox(this); + msgBox.setWindowTitle("Warning!"); + msgBox.setText("File will be auto-saved on every evaluation"); + msgBox.setInformativeText(QString("File will be saved as\n%1\n\nAuto-save disabled after restart") + .arg(exampleFileName)); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); + } + } + }); + fillComboBox(mUi->target, { { "1D Texture", QOpenGLTexture::Target1D }, @@ -320,6 +560,7 @@ TextureProperties::TextureProperties(PropertiesEditor *propertiesEditor) }); updateWidgets(); + updateUsageLabel(); } TextureProperties::~TextureProperties() @@ -360,6 +601,13 @@ void TextureProperties::addMappings(QDataWidgetMapper &mapper) mapper.addMapping(mUi->layers, SessionModel::TextureLayers); mapper.addMapping(mUi->samples, SessionModel::TextureSamples); mapper.addMapping(mUi->flipVertically, SessionModel::TextureFlipVertically); + mapper.addMapping(mUi->autoSave, SessionModel::TextureAutoSave); + mapper.addMapping(mUi->isSequence, SessionModel::TextureIsSequence); + mapper.addMapping(mUi->sequencePattern, SessionModel::TextureSequencePattern); + mapper.addMapping(mUi->frameStart, SessionModel::TextureFrameStart); + mapper.addMapping(mUi->frameEnd, SessionModel::TextureFrameEnd); + mapper.addMapping(mUi->frameOffset, SessionModel::TextureFrameOffset); + mapper.addMapping(mUi->loopSequence, SessionModel::TextureLoopSequence); } void TextureProperties::setFormat(QVariant value) @@ -393,6 +641,11 @@ void TextureProperties::updateWidgets() mUi->flipVertically, !FileDialog::isEmptyOrUntitled(fileName) && (kind.dimensions == 2 || kind.cubeMap)); + + updateCurrentFrameLabel(); + updateUsageLabel(); + updateAutoSaveState(); + updateSequenceState(); } void TextureProperties::updateFormatDataWidget(QVariant formatType) @@ -498,3 +751,138 @@ void TextureProperties::applyFileFormat() mUi->layers->setText(QString::number(texture.layers())); } } + +void TextureProperties::updateSequenceVisibility() +{ + bool isSequenceEnabled = mUi->isSequence->isChecked(); + + // Show/hide sequence controls + mUi->labelPattern->setVisible(isSequenceEnabled); + mUi->sequencePattern->setVisible(isSequenceEnabled); + mUi->labelFrameRange->setVisible(isSequenceEnabled); + mUi->widgetFrameRange->setVisible(isSequenceEnabled); + mUi->labelOffset->setVisible(isSequenceEnabled); + mUi->frameOffset->setVisible(isSequenceEnabled); + mUi->labelLoop->setVisible(isSequenceEnabled); + mUi->loopSequence->setVisible(isSequenceEnabled); + + // Update the current frame label when visibility changes + updateCurrentFrameLabel(); +} + +void TextureProperties::updateCurrentFrameLabel() +{ + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + if (texture->isSequence) { + // Show the actual frame number that will be loaded + int actualFrameNumber = texture->getActualFrameNumber(); + mUi->currentFrameLabel->setText(QString::number(actualFrameNumber)); + } else { + mUi->currentFrameLabel->setText("1"); + } + } +} + +void TextureProperties::updateUsageLabel() +{ + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + auto usageType = getTextureUsageType(texture->id, mPropertiesEditor.model()); + QString usageText = getTextureUsageString(usageType); + + mUi->usageLabel->setText(usageText); + } +} + +void TextureProperties::updateAutoSaveState() +{ + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + auto usageType = getTextureUsageType(texture->id, mPropertiesEditor.model()); + bool hasFilePath = !texture->fileName.isEmpty(); + bool isTargetTexture = (usageType == TextureUsageType::TargetTexture); + + // Enable Auto Save only for Target textures with file path + bool shouldEnable = isTargetTexture && hasFilePath; + mUi->autoSave->setEnabled(shouldEnable); + + // If not enabled, uncheck it + if (!shouldEnable && mUi->autoSave->isChecked()) { + mUi->autoSave->setChecked(false); + } + } +} + +void TextureProperties::updateSequenceState() +{ + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + auto usageType = getTextureUsageType(texture->id, mPropertiesEditor.model()); + bool hasFilePath = !texture->fileName.isEmpty(); + bool isLoadTexture = (usageType == TextureUsageType::LoadTexture); + + // Enable Sequence only for Load textures with file path + bool shouldEnable = isLoadTexture && hasFilePath; + mUi->isSequence->setEnabled(shouldEnable); + + // If not enabled, uncheck it + if (!shouldEnable && mUi->isSequence->isChecked()) { + mUi->isSequence->setChecked(false); + } + } +} + +void TextureProperties::handleSequenceModeChanged(bool enabled) +{ + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + if (enabled) { + // Switching TO sequence mode + if (texture->baseName.isEmpty()) { + // Copy fileName to baseName + mPropertiesEditor.model().setData( + mPropertiesEditor.model().getIndex(index, SessionModel::TextureBaseName), + texture->fileName); + } + // Update fileName to actual frame file + updateFileNameForSequence(); + } else { + // Switching FROM sequence mode + if (!texture->baseName.isEmpty()) { + // Copy baseName back to fileName + mPropertiesEditor.model().setData( + mPropertiesEditor.model().getIndex(index, SessionModel::FileName), + texture->baseName); + // Clear baseName + mPropertiesEditor.model().setData( + mPropertiesEditor.model().getIndex(index, SessionModel::TextureBaseName), + QString()); + } + } + } +} + +void TextureProperties::updateFileNameForSequence() +{ + auto index = mPropertiesEditor.currentModelIndex(); + if (auto texture = mPropertiesEditor.model().item(index)) { + if (texture->isSequence && !texture->baseName.isEmpty()) { + // Calculate actual frame filename from baseName + QString actualFileName = Singletons::fileCache().buildSequenceFileName( + texture->baseName, + texture->sequencePattern, + texture->getActualFrameNumber()); + + qDebug() << "updateFileNameForSequence: baseName=" << texture->baseName + << "pattern=" << texture->sequencePattern + << "frame=" << texture->getActualFrameNumber() + << "result=" << actualFileName; + + // Update fileName to the actual frame file + mPropertiesEditor.model().setData( + mPropertiesEditor.model().getIndex(index, SessionModel::FileName), + actualFileName); + } + } +} diff --git a/src/session/TextureProperties.h b/src/session/TextureProperties.h index 5686a8ce..67ef8812 100644 --- a/src/session/TextureProperties.h +++ b/src/session/TextureProperties.h @@ -33,6 +33,13 @@ class TextureProperties final : public QWidget void updateFormatDataWidget(QVariant formatType); void updateFormat(QVariant formatData); void applyFileFormat(); + void updateSequenceVisibility(); + void updateCurrentFrameLabel(); + void updateUsageLabel(); + void updateAutoSaveState(); + void updateSequenceState(); + void handleSequenceModeChanged(bool enabled); + void updateFileNameForSequence(); PropertiesEditor &mPropertiesEditor; Ui::TextureProperties *mUi; diff --git a/src/session/TextureProperties.ui b/src/session/TextureProperties.ui index 3c439673..8b3450fa 100644 --- a/src/session/TextureProperties.ui +++ b/src/session/TextureProperties.ui @@ -22,14 +22,30 @@ 2 - + + + + Unassigned texture + + + Qt::AlignmentFlag::AlignCenter + + + + 75 + true + + + + + File - + @@ -85,7 +101,212 @@ - + + + + Auto Save + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 24 + + + + On every evaluation + + + false + + + false + + + + + + + Sequence + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 24 + + + + Enable + + + + + + + Pattern + + + + + + + %06d + + + + + + + Frames + + + + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 999999 + + + 1 + + + + + + + to + + + Qt::AlignmentFlag::AlignCenter + + + + + + + 1 + + + 999999 + + + 100 + + + + + + + Ofs: + + + Qt::AlignmentFlag::AlignCenter + + + + + + + -999999 + + + 999999 + + + 0 + + + + + + + Frame: + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + 30 + 24 + + + + 1 + + + Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + Loop + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 24 + + + + Circle loop + + + true + + + + @@ -98,70 +319,70 @@ - + - + Format - + - + - + Width - + - + Height - + - + Depth - + - + Layers - + - + Samples - + @@ -200,7 +421,7 @@ - + Flip Vert. @@ -213,7 +434,7 @@ - + diff --git a/src/windows/MessageWindow.cpp b/src/windows/MessageWindow.cpp index f3ca32aa..05e6a8e4 100644 --- a/src/windows/MessageWindow.cpp +++ b/src/windows/MessageWindow.cpp @@ -197,7 +197,21 @@ QString MessageWindow::getLocationText(const Message &message) const locationText += Singletons::sessionModel().getFullItemName(message.itemId); } else if (!message.fileName.isEmpty()) { - locationText = FileDialog::getFileTitle(message.fileName); + QString displayFileName = message.fileName; + + // For textures, use baseName for display (baseName = fileName for single textures, baseName for sequences) + auto &model = Singletons::sessionModel(); + model.forEachItem([&](const Item &item) { + if (auto texture = castItem(&item)) { + if (texture->fileName == message.fileName && !texture->baseName.isEmpty()) { + displayFileName = texture->baseName; + return false; // stop iteration + } + } + return true; // continue iteration + }); + + locationText = FileDialog::getFileTitle(displayFileName); if (message.line > 0) locationText += ":" + QString::number(message.line); }