From baae6979603ac6e85b257f2dd3b207936e0bf50b Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Sat, 24 Oct 2026 12:39:25 -0700 Subject: [PATCH] Fix in-place submodel editing context and navigation In-place editing could use the tab model's step and focus, lose its active state after switching tabs, or resolve a nested instance through the wrong parent. Entering before the final animation key also moved geometry when drilling into a child. Use the edited model for step controls, step edits, visibility, and focus coordinates. Restore that editing target when returning to its tab, include animation keys and finite hide steps in the last step, and retain the selected instance path and transforms during nested editing. Show the editing path only during in-place edits, with links to ancestors and a close control to return to the tab model. Stack Find/Replace beneath it and preserve full names in accessible text and tooltips. Add translated source entries without changing existing translations. Add a regression test for final-key tracking through time and key edits. Validated with the macOS Qt 6 application build, all 8 Qt Test cases, and 51 focused checks against the compiled application for steps, tab switching, focus status, breadcrumbs, and Find/Replace layout. Visually checked the viewport overlay and its placement during nested editing. --- common/camera.cpp | 5 + common/camera.h | 1 + common/lc_findreplacewidget.cpp | 1 - common/lc_mainwindow.cpp | 33 +++--- common/lc_model.cpp | 12 +- common/lc_objectproperty.h | 5 + common/lc_view.cpp | 78 ++++++++++--- common/lc_view.h | 3 + common/lc_viewwidget.cpp | 194 +++++++++++++++++++++++++++++++ common/lc_viewwidget.h | 14 +++ common/light.cpp | 9 ++ common/light.h | 1 + common/object.h | 1 + common/piece.cpp | 10 ++ common/piece.h | 1 + common/project.cpp | 9 +- resources/leocad_cs.ts | 23 ++++ resources/leocad_de.ts | 23 ++++ resources/leocad_es.ts | 23 ++++ resources/leocad_fr.ts | 23 ++++ resources/leocad_pt.ts | 23 ++++ resources/leocad_ru.ts | 23 ++++ resources/leocad_uk.ts | 23 ++++ resources/leocad_zh_CN.ts | 23 ++++ tests/README.md | 3 +- tests/lc_objectproperty_test.cpp | 29 +++++ tests/lc_tests.h | 1 + tests/tests.pro | 2 + 28 files changed, 558 insertions(+), 38 deletions(-) create mode 100644 tests/lc_objectproperty_test.cpp diff --git a/common/camera.cpp b/common/camera.cpp index 1b6b0dd8..bbac53b3 100644 --- a/common/camera.cpp +++ b/common/camera.cpp @@ -792,6 +792,11 @@ bool lcCamera::SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool return false; } +lcStep lcCamera::GetLastStep() const +{ + return std::max({ mPosition.GetLastKeyFrame(), mTargetPosition.GetLastKeyFrame(), mUpVector.GetLastKeyFrame() }); +} + bool lcCamera::HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const { switch (PropertyId) diff --git a/common/camera.h b/common/camera.h index e8af95d6..ff8c1ac9 100644 --- a/common/camera.h +++ b/common/camera.h @@ -189,6 +189,7 @@ public: QVariant GetPropertyValue(lcObjectPropertyId PropertyId) const override; bool SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool AddKey, QVariant Value) override; bool HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const override; + lcStep GetLastStep() const override; bool SetKeyFrame(lcObjectPropertyId PropertyId, lcStep Time, bool KeyFrame) override; void RemoveKeyFrames() override; lcCameraHistoryState GetHistoryState() const; diff --git a/common/lc_findreplacewidget.cpp b/common/lc_findreplacewidget.cpp index 47fdb255..f0eb663c 100644 --- a/common/lc_findreplacewidget.cpp +++ b/common/lc_findreplacewidget.cpp @@ -126,7 +126,6 @@ lcFindReplaceWidget::lcFindReplaceWidget(QWidget* Parent, lcModel* Model, bool R mFindPartComboBox->setFocus(); adjustSize(); - move(1, 1); show(); } diff --git a/common/lc_mainwindow.cpp b/common/lc_mainwindow.cpp index bea8b6a4..bb841934 100644 --- a/common/lc_mainwindow.cpp +++ b/common/lc_mainwindow.cpp @@ -2386,8 +2386,11 @@ void lcMainWindow::UpdateSelectedObjects(bool SelectionChanged) } mStatusBarLabel->setText(Message); - lcVector3 Position; - lcGetActiveModel()->GetFocusPosition(Position); + + lcVector3 Position(0.0f, 0.0f, 0.0f); + + if (ActiveModel) + ActiveModel->GetFocusPosition(Position); QString Label("X: %1 Y: %2 Z: %3"); Label = Label.arg(QLocale::system().toString(Position[0], 'f', 2), QLocale::system().toString(Position[1], 'f', 2), QLocale::system().toString(Position[2], 'f', 2)); @@ -2410,9 +2413,13 @@ void lcMainWindow::UpdatePaste(bool Enabled) void lcMainWindow::UpdateCurrentStep() { - lcModel* Model = lcGetActiveModel(); - lcStep CurrentStep = Model->GetCurrentStep(); - lcStep LastStep = Model->GetLastStep(); + lcModel* ActiveModel = GetActiveModel(); + + if (!ActiveModel) + return; + + lcStep CurrentStep = ActiveModel->GetCurrentStep(); + lcStep LastStep = ActiveModel->GetLastStep(); mActions[LC_VIEW_TIME_FIRST]->setEnabled(CurrentStep != 1); mActions[LC_VIEW_TIME_PREVIOUS]->setEnabled(CurrentStep > 1); @@ -3387,14 +3394,7 @@ void lcMainWindow::HandleCommand(lcCommandId CommandId) case LC_PIECE_EDIT_END_SUBMODEL: if (ActiveView) - { ActiveView->SetTopSubmodelActive(); - if (ActiveModel) - { - std::vector UpdatedModels; - ActiveModel->UpdatePieceData(UpdatedModels); - } - } break; case LC_PIECE_EDIT_SELECTED_SUBMODEL: @@ -3532,15 +3532,18 @@ void lcMainWindow::HandleCommand(lcCommandId CommandId) break; case LC_VIEW_TIME_INSERT_BEFORE: - lcGetActiveModel()->InsertStepAction(lcGetActiveModel()->GetCurrentStep()); + if (ActiveModel) + ActiveModel->InsertStepAction(ActiveModel->GetCurrentStep()); break; case LC_VIEW_TIME_INSERT_AFTER: - lcGetActiveModel()->InsertStepAction(lcGetActiveModel()->GetCurrentStep() + 1); + if (ActiveModel) + ActiveModel->InsertStepAction(ActiveModel->GetCurrentStep() + 1); break; case LC_VIEW_TIME_DELETE: - lcGetActiveModel()->RemoveStepAction(lcGetActiveModel()->GetCurrentStep()); + if (ActiveModel) + ActiveModel->RemoveStepAction(ActiveModel->GetCurrentStep()); break; case LC_VIEW_VIEWPOINT_FRONT: diff --git a/common/lc_model.cpp b/common/lc_model.cpp index 4ec7e1ce..91a34e1a 100644 --- a/common/lc_model.cpp +++ b/common/lc_model.cpp @@ -1662,7 +1662,9 @@ void lcModel::AddSubModelRenderMeshes(lcScene* Scene, const lcMatrix44& WorldMat { for (const std::unique_ptr& Piece : mPieces) { - if (Piece->IsVisibleInSubModel()) + const bool Visible = ParentActive ? (Piece->IsSelected() || Piece->IsVisible(mCurrentStep)) : Piece->IsVisibleInSubModel(); + + if (Visible) { if (Piece->IsFocused()) UpdateTrainTrackConnections(Piece.get(), false); @@ -2649,7 +2651,13 @@ lcStep lcModel::GetLastStep() const lcStep Step = 1; for (const std::unique_ptr& Piece : mPieces) - Step = lcMax(Step, Piece->GetStepShow()); + Step = lcMax(Step, Piece->GetLastStep()); + + for (const std::unique_ptr& Camera : mCameras) + Step = lcMax(Step, Camera->GetLastStep()); + + for (const std::unique_ptr& Light : mLights) + Step = lcMax(Step, Light->GetLastStep()); return Step; } diff --git a/common/lc_objectproperty.h b/common/lc_objectproperty.h index e9546924..7d87a0b4 100644 --- a/common/lc_objectproperty.h +++ b/common/lc_objectproperty.h @@ -99,6 +99,11 @@ public: mKeys.clear(); } + lcStep GetLastKeyFrame() const + { + return mKeys.empty() ? 0 : mKeys.back().Step; + } + void Update(lcStep Step); bool ChangeKey(const T& Value, lcStep Step, bool AddKey); void InsertTime(lcStep Start, lcStep Time); diff --git a/common/lc_view.cpp b/common/lc_view.cpp index 8e855950..ecbe0037 100644 --- a/common/lc_view.cpp +++ b/common/lc_view.cpp @@ -105,7 +105,10 @@ void lcView::MakeCurrent() void lcView::Redraw() { if (mWidget) + { + mWidget->UpdateModelEditingPath(); mWidget->update(); + } } void lcView::SetOffscreenContext() @@ -145,44 +148,82 @@ lcModel* lcView::GetActiveModel() const return !mActiveSubmodelInstance ? mModel : mActiveSubmodelInstance->mPieceData->GetModel(); } -void lcView::SetTopSubmodelActive() +std::vector lcView::GetModelEditingPath() const { + std::vector Models = { mModel }; + + for (const std::pair& Entry : mSubmodelEditingPath) + Models.push_back(Entry.first->mPieceData->GetModel()); + + return Models; +} + +void lcView::SetSubmodelActive(size_t Depth) +{ + if (Depth > mSubmodelEditingPath.size()) + return; + + StopTracking(true); + lcModel* ActiveModel = GetActiveModel(); if (mActiveSubmodelInstance) { ActiveModel->SetActive(false); - mActiveSubmodelInstance = nullptr; + + std::vector UpdatedModels; + ActiveModel->UpdatePieceData(UpdatedModels); } + mSubmodelEditingPath.resize(Depth); + mActiveSubmodelInstance = nullptr; + + if (!mSubmodelEditingPath.empty()) + { + mActiveSubmodelInstance = mSubmodelEditingPath.back().first; + mActiveSubmodelTransform = mSubmodelEditingPath.back().second; + } + + GetActiveModel()->SetActive(true); GetActiveModel()->UpdateInterface(); } +void lcView::SetTopSubmodelActive() +{ + SetSubmodelActive(0); +} + void lcView::SetSelectedSubmodelActive() { lcModel* ActiveModel = GetActiveModel(); lcObject* Object = ActiveModel->GetFocusObject(); + if (!Object || !Object->IsPiece()) + return; + + lcPiece* Piece = (lcPiece*)Object; + + if (!Piece->mPieceData->IsModel()) + return; + + // Preserve the selected instance path and capture its transform before SetActive(false) changes the step. + lcMatrix44 SubmodelTransform = Piece->mModelWorld; + + if (mActiveSubmodelInstance) + SubmodelTransform = lcMul(SubmodelTransform, mActiveSubmodelTransform); + if (mActiveSubmodelInstance) - { ActiveModel->SetActive(false); - mActiveSubmodelInstance = nullptr; - } - if (Object && Object->IsPiece()) - { - lcPiece* Piece = (lcPiece*)Object; + mSubmodelEditingPath.emplace_back(Piece, SubmodelTransform); + mActiveSubmodelTransform = SubmodelTransform; + mActiveSubmodelInstance = Piece; - if (Piece->mPieceData->IsModel()) - { - mActiveSubmodelTransform = lcMatrix44Identity(); - mModel->GetPieceWorldMatrix(Piece, mActiveSubmodelTransform); - mActiveSubmodelInstance = Piece; - ActiveModel = mActiveSubmodelInstance->mPieceData->GetModel(); - ActiveModel->SetActive(true); - RemoveCamera(); - } - } + ActiveModel = mActiveSubmodelInstance->mPieceData->GetModel(); + ActiveModel->SetTemporaryStep(ActiveModel->GetLastStep()); + ActiveModel->SetActive(true); + + RemoveCamera(); GetActiveModel()->UpdateInterface(); } @@ -422,6 +463,7 @@ void lcView::ShowFindReplaceWidget(bool Replace) delete mFindWidget; mFindReplaceParams = lcFindReplaceParams(); mFindWidget = new lcFindReplaceWidget(mWidget, GetActiveModel(), Replace); + mWidget->SetFindReplaceWidget(mFindWidget); } lcVector3 lcView::GetMoveDirection(const lcVector3& Direction) const diff --git a/common/lc_view.h b/common/lc_view.h index c8afed19..3f1e1ca4 100644 --- a/common/lc_view.h +++ b/common/lc_view.h @@ -235,6 +235,8 @@ public: void SetMouseModifiers(Qt::KeyboardModifiers MouseModifiers); lcModel* GetActiveModel() const; + std::vector GetModelEditingPath() const; + void SetSubmodelActive(size_t Depth); void SetTopSubmodelActive(); void SetSelectedSubmodelActive(); @@ -393,6 +395,7 @@ protected: lcModel* mModel = nullptr; lcPiece* mActiveSubmodelInstance = nullptr; lcMatrix44 mActiveSubmodelTransform; + std::vector> mSubmodelEditingPath; lcCamera* mCamera = nullptr; diff --git a/common/lc_viewwidget.cpp b/common/lc_viewwidget.cpp index 58b952c7..cbbf835b 100644 --- a/common/lc_viewwidget.cpp +++ b/common/lc_viewwidget.cpp @@ -11,6 +11,8 @@ #include "lc_mesh.h" #include "lc_profile.h" #include "lc_previewwidget.h" +#include "lc_qutils.h" +#include "lc_viewsphere.h" lcViewWidget::lcViewWidget(QWidget* Parent, lcView* View) : QOpenGLWidget(Parent), mView(View) @@ -23,6 +25,62 @@ lcViewWidget::lcViewWidget(QWidget* Parent, lcView* View) { setFocusPolicy(Qt::StrongFocus); setAcceptDrops(true); + + mOverlay = new QWidget(this); + mOverlay->setObjectName(QStringLiteral("ViewOverlay")); + mOverlayLayout = new QVBoxLayout(mOverlay); + mOverlayLayout->setContentsMargins(0, 0, 0, 0); + mOverlayLayout->setSpacing(2); + mOverlayLayout->setSizeConstraint(QLayout::SetNoConstraint); + mOverlay->installEventFilter(this); + + mModelEditingBar = new QWidget(mOverlay); + mModelEditingBar->setObjectName(QStringLiteral("ModelEditingBar")); + mModelEditingBar->setAutoFillBackground(true); + QPalette Palette = palette(); + Palette.setColor(QPalette::Window, QApplication::palette().color(QPalette::Button)); + mModelEditingBar->setPalette(Palette); + mModelEditingBar->setWhatsThis(tr("

In-place Editing

The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.

")); + + QHBoxLayout* EditingLayout = new QHBoxLayout(mModelEditingBar); + EditingLayout->setContentsMargins(5, 3, 5, 3); + + mModelEditingLabel = new QLabel(mModelEditingBar); + mModelEditingLabel->setObjectName(QStringLiteral("ModelEditingPath")); + mModelEditingLabel->setTextFormat(Qt::RichText); + mModelEditingLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); + mModelEditingLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + mModelEditingLabel->setMinimumWidth(0); + EditingLayout->addWidget(mModelEditingLabel, 1); + + mEndEditingButton = new QToolButton(mModelEditingBar); + mEndEditingButton->setIcon(QIcon(":/stylesheet/close.svg")); + mEndEditingButton->setAccessibleName(tr("End Editing")); + mEndEditingButton->setToolTip(tr("Return to editing the tab's model.")); + mEndEditingButton->setAutoRaise(true); + EditingLayout->addWidget(mEndEditingButton); + mOverlayLayout->addWidget(mModelEditingBar); + + connect(mModelEditingLabel, &QLabel::linkActivated, this, [this](const QString& Link) + { + bool Valid; + const uint Depth = Link.toUInt(&Valid); + + if (!Valid || Depth + 1 >= static_cast(mModelEditingNames.size())) + return; + + setFocus(Qt::OtherFocusReason); + mView->SetSubmodelActive(Depth); + }); + + connect(mEndEditingButton, &QToolButton::clicked, this, [this]() + { + setFocus(Qt::OtherFocusReason); + mView->SetTopSubmodelActive(); + }); + + lcSetupWidgetHelp(mModelEditingBar, nullptr); + UpdateModelEditingPath(); } } @@ -68,6 +126,142 @@ void lcViewWidget::SetView(lcView* View) } mView = std::unique_ptr(View); + UpdateModelEditingPath(); +} + +void lcViewWidget::UpdateModelEditingPath() +{ + if (!mOverlay || !mView) + return; + + QStringList Names; + + for (const lcModel* Model : mView->GetModelEditingPath()) + Names.append(Model->GetProperties().mFileName); + + if (Names != mModelEditingNames) + { + mModelEditingNames = Names; + const QString Path = Names.join(QStringLiteral(" \u203a ")); + mModelEditingLabel->setToolTip(Path); + mModelEditingLabel->setAccessibleName(tr("Editing model: %1").arg(Path)); + mModelEditingBar->setVisible(Names.size() > 1); + } + + UpdateOverlayGeometry(); +} + +void lcViewWidget::SetFindReplaceWidget(QWidget* Widget) +{ + if (!mOverlay) + return; + + mFindReplaceWidget = Widget; + mOverlayLayout->addWidget(Widget); + UpdateOverlayGeometry(); +} + +void lcViewWidget::UpdateOverlayGeometry() +{ + if (!mOverlay) + return; + + const bool Editing = mModelEditingNames.size() > 1; + + mOverlay->setVisible(Editing || mFindReplaceWidget); + + if (!Editing && !mFindReplaceWidget) + return; + + int Left = 1; + int Right = width() - 1; + const lcPreferences& Preferences = lcGetPreferences(); + + if (Preferences.mViewSphereEnabled && Preferences.mViewSphereSize > 0) + { + if (Preferences.mViewSphereLocation == lcViewSphereLocation::TopLeft) + Left += Preferences.mViewSphereSize + 2; + else if (Preferences.mViewSphereLocation == lcViewSphereLocation::TopRight) + Right -= Preferences.mViewSphereSize + 2; + } + + int PreferredWidth = 0; + + if (Editing) + { + QFont Font = mModelEditingLabel->font(); + Font.setBold(true); + const QFontMetrics Metrics(Font); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) + PreferredWidth = Metrics.horizontalAdvance(mModelEditingNames.join(QStringLiteral(" \u203a "))); +#else + PreferredWidth = Metrics.width(mModelEditingNames.join(QStringLiteral(" \u203a "))); +#endif + PreferredWidth += mEndEditingButton->sizeHint().width() + mModelEditingBar->layout()->spacing() + 10; + } + + if (mFindReplaceWidget) + PreferredWidth = lcMax(PreferredWidth, mFindReplaceWidget->sizeHint().width()); + + const int OverlayWidth = lcClamp(PreferredWidth, 1, lcMax(1, Right - Left)); + + mOverlay->setGeometry(Left, 1, OverlayWidth, mOverlayLayout->sizeHint().height()); + mOverlayLayout->activate(); + mModelEditingBar->layout()->activate(); + + UpdateModelEditingLabel(); +} + +void lcViewWidget::UpdateModelEditingLabel() +{ + if (mModelEditingNames.size() < 2) + return; + + const QString Separator = QStringLiteral(" \u203a "); + QFont Font = mModelEditingLabel->font(); + Font.setBold(true); + const QFontMetrics Metrics(Font); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) + const int SeparatorWidth = Metrics.horizontalAdvance(Separator); + const int PathWidth = Metrics.horizontalAdvance(mModelEditingNames.join(Separator)); +#else + const int SeparatorWidth = Metrics.width(Separator); + const int PathWidth = Metrics.width(mModelEditingNames.join(Separator)); +#endif + const int NameCount = static_cast(mModelEditingNames.size()); + const int NameWidth = lcMax(0, (mModelEditingLabel->width() - SeparatorWidth * (NameCount - 1)) / NameCount); + QStringList Segments; + + for (int Index = 0; Index < mModelEditingNames.size(); Index++) + { + QString Name = mModelEditingNames[Index]; + + if (PathWidth > mModelEditingLabel->width()) + Name = Metrics.elidedText(Name, Qt::ElideMiddle, NameWidth); + + Name = Name.toHtmlEscaped(); + + if (Index + 1 == mModelEditingNames.size()) + Segments.append(QStringLiteral("%1").arg(Name)); + else + Segments.append(QStringLiteral("%2").arg(Index).arg(Name)); + } + + mModelEditingLabel->setText(Segments.join(Separator)); +} + +bool lcViewWidget::eventFilter(QObject* Object, QEvent* Event) +{ + if (Object == mOverlay && Event->type() == QEvent::LayoutRequest) + UpdateOverlayGeometry(); + + return QOpenGLWidget::eventFilter(Object, Event); +} + +void lcViewWidget::resizeEvent(QResizeEvent* Event) +{ + QOpenGLWidget::resizeEvent(Event); + UpdateOverlayGeometry(); } void lcViewWidget::UpdateMousePosition() diff --git a/common/lc_viewwidget.h b/common/lc_viewwidget.h index 56f7eb2d..51e681bc 100644 --- a/common/lc_viewwidget.h +++ b/common/lc_viewwidget.h @@ -12,6 +12,8 @@ public: void SetView(lcView* View); void UpdateMousePosition(); + void UpdateModelEditingPath(); + void SetFindReplaceWidget(QWidget* Widget); QSize sizeHint() const override; @@ -29,6 +31,8 @@ protected: void resizeGL(int Width, int Height) override; void paintGL() override; bool event(QEvent* Event) override; + bool eventFilter(QObject* Object, QEvent* Event) override; + void resizeEvent(QResizeEvent* Event) override; void focusInEvent(QFocusEvent* FocusEvent) override; void focusOutEvent(QFocusEvent* FocusEvent) override; void keyPressEvent(QKeyEvent* KeyEvent) override; @@ -44,7 +48,17 @@ protected: void dragMoveEvent(QDragMoveEvent* DragMoveEvent) override; void dropEvent(QDropEvent* DropEvent) override; + void UpdateOverlayGeometry(); + void UpdateModelEditingLabel(); + std::unique_ptr mView; + QWidget* mOverlay = nullptr; + QVBoxLayout* mOverlayLayout = nullptr; + QWidget* mModelEditingBar = nullptr; + QLabel* mModelEditingLabel = nullptr; + QToolButton* mEndEditingButton = nullptr; + QPointer mFindReplaceWidget; + QStringList mModelEditingNames; QSize mPreferredSize; int mWheelAccumulator = 0; }; diff --git a/common/light.cpp b/common/light.cpp index a7787bba..3704c551 100644 --- a/common/light.cpp +++ b/common/light.cpp @@ -1309,6 +1309,15 @@ bool lcLight::SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool return false; } +lcStep lcLight::GetLastStep() const +{ + return std::max({ mPosition.GetLastKeyFrame(), mRotation.GetLastKeyFrame(), mColor.GetLastKeyFrame(), + mBlenderPower.GetLastKeyFrame(), mBlenderRadius.GetLastKeyFrame(), mBlenderAngle.GetLastKeyFrame(), + mPOVRayPower.GetLastKeyFrame(), mPOVRayFadeDistance.GetLastKeyFrame(), mPOVRayFadePower.GetLastKeyFrame(), + mSpotConeAngle.GetLastKeyFrame(), mSpotPenumbraAngle.GetLastKeyFrame(), mPOVRaySpotTightness.GetLastKeyFrame(), + mAreaSizeX.GetLastKeyFrame(), mAreaSizeY.GetLastKeyFrame(), mPOVRayAreaGridX.GetLastKeyFrame(), mPOVRayAreaGridY.GetLastKeyFrame() }); +} + bool lcLight::HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const { switch (PropertyId) diff --git a/common/light.h b/common/light.h index 73c4add4..2c9f0eca 100644 --- a/common/light.h +++ b/common/light.h @@ -195,6 +195,7 @@ public: QVariant GetPropertyValue(lcObjectPropertyId PropertyId) const override; bool SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool AddKey, QVariant Value) override; bool HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const override; + lcStep GetLastStep() const override; bool SetKeyFrame(lcObjectPropertyId PropertyId, lcStep Time, bool KeyFrame) override; void RemoveKeyFrames() override; lcLightHistoryState GetHistoryState() const; diff --git a/common/object.h b/common/object.h index 9a8c3e84..8d458ad0 100644 --- a/common/object.h +++ b/common/object.h @@ -144,6 +144,7 @@ public: virtual QVariant GetPropertyValue(lcObjectPropertyId PropertyId) const = 0; virtual bool SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool AddKey, QVariant Value) = 0; virtual bool HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const = 0; + virtual lcStep GetLastStep() const = 0; virtual bool SetKeyFrame(lcObjectPropertyId PropertyId, lcStep Time, bool KeyFrame) = 0; virtual void RemoveKeyFrames() = 0; virtual QString GetName() const = 0; diff --git a/common/piece.cpp b/common/piece.cpp index 18a3279f..32b18abd 100644 --- a/common/piece.cpp +++ b/common/piece.cpp @@ -864,6 +864,16 @@ bool lcPiece::SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool return false; } +lcStep lcPiece::GetLastStep() const +{ + lcStep Step = std::max({ mStepShow, mPosition.GetLastKeyFrame(), mRotation.GetLastKeyFrame() }); + + if (mStepHide != LC_STEP_MAX) + Step = lcMax(Step, mStepHide); + + return Step; +} + bool lcPiece::HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const { switch (PropertyId) diff --git a/common/piece.h b/common/piece.h index ee499993..8a9c01d7 100644 --- a/common/piece.h +++ b/common/piece.h @@ -93,6 +93,7 @@ public: QVariant GetPropertyValue(lcObjectPropertyId PropertyId) const override; bool SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool AddKey, QVariant Value) override; bool HasKeyFrame(lcObjectPropertyId PropertyId, lcStep Time) const override; + lcStep GetLastStep() const override; bool SetKeyFrame(lcObjectPropertyId PropertyId, lcStep Time, bool KeyFrame) override; void RemoveKeyFrames() override; lcPieceHistoryState GetHistoryState() const; diff --git a/common/project.cpp b/common/project.cpp index 02d31d89..91f551d0 100644 --- a/common/project.cpp +++ b/common/project.cpp @@ -233,7 +233,14 @@ void Project::SetActiveModel(lcModel* ActiveModel, bool UpdateInterface) if (!mIsPreview && gMainWindow && UpdateInterface) { gMainWindow->SetCurrentModelTab(mActiveModel, true); - mActiveModel->UpdateInterface(); + + // The tab may still be editing a nested submodel at its current step. + lcModel* EditingModel = gMainWindow->GetActiveModel(); + if (EditingModel) + { + EditingModel->SetActive(true); + EditingModel->UpdateInterface(); + } } } diff --git a/resources/leocad_cs.ts b/resources/leocad_cs.ts index 4fba0f46..8331ec2e 100644 --- a/resources/leocad_cs.ts +++ b/resources/leocad_cs.ts @@ -16114,4 +16114,27 @@ Nepodporované grafické karty používají odstín obličeje a barevné okraje. %2 + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_de.ts b/resources/leocad_de.ts index 3be84af1..cdb3033d 100644 --- a/resources/leocad_de.ts +++ b/resources/leocad_de.ts @@ -16325,4 +16325,27 @@ Nicht unterstützte Grafikkarten verwenden Gesichtstönung und farbige Kanten. + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_es.ts b/resources/leocad_es.ts index 342ae3bc..886129aa 100644 --- a/resources/leocad_es.ts +++ b/resources/leocad_es.ts @@ -15969,4 +15969,27 @@ Las tarjetas de video no compatibles usan tinte frontal y bordes coloreados. + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_fr.ts b/resources/leocad_fr.ts index 4dce0604..27fa2d2e 100644 --- a/resources/leocad_fr.ts +++ b/resources/leocad_fr.ts @@ -16319,4 +16319,27 @@ Les cartes vidéo non prises en charge utilisent une teinte de visage et des bor %2 + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_pt.ts b/resources/leocad_pt.ts index 5c0f709c..8d4f248c 100644 --- a/resources/leocad_pt.ts +++ b/resources/leocad_pt.ts @@ -16327,4 +16327,27 @@ As placas de vídeo não suportadas utilizam a tonalidade da face e bordas color %2 + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_ru.ts b/resources/leocad_ru.ts index 4ed2cbfd..b9ace636 100644 --- a/resources/leocad_ru.ts +++ b/resources/leocad_ru.ts @@ -15845,4 +15845,27 @@ Unsupported video cards use face tint and colored edges. %2 + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_uk.ts b/resources/leocad_uk.ts index 0d89d542..c7659161 100644 --- a/resources/leocad_uk.ts +++ b/resources/leocad_uk.ts @@ -16451,4 +16451,27 @@ Unsupported video cards use face tint and colored edges. %2 + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/resources/leocad_zh_CN.ts b/resources/leocad_zh_CN.ts index d4cf504d..59cd4727 100644 --- a/resources/leocad_zh_CN.ts +++ b/resources/leocad_zh_CN.ts @@ -16073,4 +16073,27 @@ Unsupported video cards use face tint and colored edges. %2 + + lcViewWidget + + + <p><b>In-place Editing</b></p><p>The path shows the model being edited inside this view. The last name is the current editing target. Choose an earlier name to edit that ancestor, or the close button to return to the tab's model.</p> + + + + + End Editing + + + + + Return to editing the tab's model. + + + + + Editing model: %1 + + + diff --git a/tests/README.md b/tests/README.md index 02c124b2..59676c3b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,8 @@ # LeoCAD tests All tests run in one Qt Test executable, `leocad_tests`. The current tests cover -object history and property-key comparisons, including 8,000 deterministic +object history, property-key comparisons, and the final animation key after +step insertion, removal, and key deletion, including 8,000 deterministic randomized undo/redo and merge transitions. The executable creates a `QCoreApplication`; it does not open windows or require diff --git a/tests/lc_objectproperty_test.cpp b/tests/lc_objectproperty_test.cpp new file mode 100644 index 00000000..aa531093 --- /dev/null +++ b/tests/lc_objectproperty_test.cpp @@ -0,0 +1,29 @@ +#include "lc_global.h" +#include "lc_tests.h" +#include "lc_objectproperty.h" + +void lcTests::LastKeyFrameTracksTimeEdits() +{ + lcObjectProperty Position(0.0f); + QCOMPARE(Position.GetLastKeyFrame(), lcStep(0)); + + // Animation can extend past the step that introduces the piece. + QVERIFY(Position.ChangeKey(0.0f, 1, true)); + QVERIFY(Position.ChangeKey(200.0f, 2, true)); + QCOMPARE(Position.GetLastKeyFrame(), lcStep(2)); + Position.Update(Position.GetLastKeyFrame()); + QCOMPARE(static_cast(Position), 200.0f); + + Position.InsertTime(2, 3); + QCOMPARE(Position.GetLastKeyFrame(), lcStep(5)); + Position.RemoveTime(2, 2); + QCOMPARE(Position.GetLastKeyFrame(), lcStep(3)); + + QVERIFY(Position.SetKeyFrame(3, false)); + QCOMPARE(Position.GetLastKeyFrame(), lcStep(1)); + Position.Update(Position.GetLastKeyFrame()); + QCOMPARE(static_cast(Position), 0.0f); + + Position.RemoveAllKeys(); + QCOMPARE(Position.GetLastKeyFrame(), lcStep(0)); +} diff --git a/tests/lc_tests.h b/tests/lc_tests.h index b5cd9f9c..ef066053 100644 --- a/tests/lc_tests.h +++ b/tests/lc_tests.h @@ -8,6 +8,7 @@ class lcTests : public QObject private slots: void PropertyKeysAffectEquality(); + void LastKeyFrameTracksTimeEdits(); void HistoryStoresOnlyChangedObjects(); void HistoryCancelsPendingEdit(); void HistoryReplaysAndMergesEdits(); diff --git a/tests/tests.pro b/tests/tests.pro index 793899eb..eb984051 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -15,5 +15,7 @@ INCLUDEPATH += ../common HEADERS += lc_tests.h SOURCES += \ + ../common/lc_objectproperty.cpp \ lc_objecthistory_test.cpp \ + lc_objectproperty_test.cpp \ main.cpp