Expand far clipping distance when fitting large models

Large models can extend beyond the default far plane after fitting,
leaving the view blank or clipped. Pass an explicit adjustment flag
through view, model, and camera fitting, and reuse the fitted bounds
to expand clipping with a margin, rounded up to multiples of 10,000.

Adjust restored ordinary cameras after applying their saved state.
Preserve named cameras and explicit command-line clipping planes,
and avoid recalculating clipping during redraws or navigation.

Route output fitting through the view and move the bounds policy
definition out of the precompiled header.
This commit is contained in:
Leonardo Zide committed 2026-10-09 15:00:18 -07:00
1 parent ce26574116
commit 6438bf2682
18 files changed
+123 -58

No files matched your search

+22 -1
View File
@@ -1079,7 +1079,25 @@ void lcCamera::RemoveTime(lcStep Start, lcStep Time)
mUpVector.RemoveTime(Start, Time);
}
void lcCamera::ZoomExtents(float AspectRatio, const lcVector3& Center, const std::vector<lcVector3>& Points, lcStep Step, bool AddKey)
void lcCamera::ExpandFarPlane(const std::vector<lcVector3>& Points)
{
if (!IsSimple())
return;
float FarDistance = 0.0f;
for (const lcVector3& Point : Points)
FarDistance = lcMax(FarDistance, -lcMul31(Point, mWorldView).z);
// Orthographic views use four times the camera's far plane.
if (GetProjection() == lcCameraProjection::Orthographic)
FarDistance /= 4.0f;
if (FarDistance > DefaultFarPlane && FarDistance > m_zFar)
m_zFar = ceilf(FarDistance * 1.1f / 10000.0f) * 10000.0f;
}
void lcCamera::ZoomExtents(float AspectRatio, const lcVector3& Center, const std::vector<lcVector3>& Points, lcStep Step, bool AddKey, bool AdjustFarPlane)
{
lcVector3 Position, TargetPosition;
@@ -1137,6 +1155,9 @@ void lcCamera::ZoomExtents(float AspectRatio, const lcVector3& Center, const std
mTargetPosition.ChangeKey(TargetPosition, Step, AddKey);
UpdatePosition(Step);
if (AdjustFarPlane)
ExpandFarPlane(Points);
}
void lcCamera::ZoomRegion(float AspectRatio, const lcVector3& Position, const lcVector3& TargetPosition, const lcVector3* Corners, lcStep Step, bool AddKey)
+4 -2
View File
@@ -204,7 +204,8 @@ public:
void CopyPosition(const lcCamera* Camera);
void CopySettings(const lcCamera* Camera);
void ZoomExtents(float AspectRatio, const lcVector3& Center, const std::vector<lcVector3>& Points, lcStep Step, bool AddKey);
void ZoomExtents(float AspectRatio, const lcVector3& Center, const std::vector<lcVector3>& Points, lcStep Step, bool AddKey, bool AdjustFarPlane);
void ExpandFarPlane(const std::vector<lcVector3>& Points);
void ZoomRegion(float AspectRatio, const lcVector3& Position, const lcVector3& TargetPosition, const lcVector3* Corners, lcStep Step, bool AddKey);
void Zoom(float Distance, lcStep Step, bool AddKey);
void Pan(const lcVector3& Distance, lcStep Step, bool AddKey);
@@ -222,7 +223,8 @@ public:
float m_fovy = 30.0f;
float m_zNear = 25.0f;
float m_zFar = 50000;
static constexpr float DefaultFarPlane = 50000.0f;
float m_zFar = DefaultFarPlane;
lcMatrix44 mWorldView;
lcObjectProperty<lcVector3> mPosition = lcObjectProperty<lcVector3>(lcVector3(0.0f, 0.0f, 0.0f));
+1 -1
View File
@@ -1148,7 +1148,7 @@ lcStartupMode lcApplication::Initialize(const QList<QPair<QString, bool>>& Libra
{
const lcStep FitStep = Options.ImageStart == Options.ImageEnd ? Options.ImageStart : ActiveModel->GetLastStep();
ActiveModel->ZoomExtentsAtStep(ActiveView->GetCamera(), (float)ActiveView->GetWidth() / (float)ActiveView->GetHeight(), FitStep, lcGeometryBoundsMode::AvailableGeometryOnly);
ActiveView->ZoomExtentsAtStep(FitStep, lcGeometryBoundsMode::AvailableOnly, !Options.SetZPlanes);
}
auto ProgressCallback = [&StdOut](const QString& FileName)
+1 -6
View File
@@ -63,12 +63,6 @@ class QPrinter;
typedef quint32 lcStep;
#define LC_STEP_MAX 0xffffffff
enum class lcGeometryBoundsMode
{
IncludeFallbackGeometry,
AvailableGeometryOnly
};
// Version number.
#define LC_VERSION_MAJOR 26
#define LC_VERSION_MINOR 9
@@ -78,6 +72,7 @@ enum class lcGeometryBoundsMode
// Forward declarations.
class Project;
class lcModel;
enum class lcGeometryBoundsMode;
class lcObject;
class lcPiece;
class lcCamera;
+6 -5
View File
@@ -1737,7 +1737,7 @@ void lcMainWindow::RestoreTabLayout(const QByteArray& TabLayout)
if (Model)
{
SetCurrentModelTab(Model);
SetCurrentModelTab(Model, false);
TabWidget = (lcModelTabWidget*)mModelTabWidget->widget(mModelTabWidget->count() - 1);
}
@@ -1792,6 +1792,7 @@ void lcMainWindow::RestoreTabLayout(const QByteArray& TabLayout)
Camera->SetUpVector(UpVector, 1, false);
}
Camera->UpdatePosition(1);
Camera->ExpandFarPlane(CurrentView->GetModel()->GetPiecesBoundingBoxPoints(lcGeometryBoundsMode::IncludeFallback));
}
}
else
@@ -1857,7 +1858,7 @@ void lcMainWindow::CloseCurrentModelTab()
NewProject();
}
void lcMainWindow::SetCurrentModelTab(lcModel* Model)
void lcMainWindow::SetCurrentModelTab(lcModel* Model, bool AdjustFarPlane)
{
for (int TabIdx = 0; TabIdx < mModelTabWidget->count(); TabIdx++)
{
@@ -1884,7 +1885,7 @@ void lcMainWindow::SetCurrentModelTab(lcModel* Model)
ViewWidget->show();
ViewWidget->setFocus();
NewView->ZoomExtents();
NewView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, AdjustFarPlane);
}
void lcMainWindow::AddView(lcView* View)
@@ -2152,7 +2153,7 @@ void lcMainWindow::ResetViews()
ViewWidget->show();
ViewWidget->setFocus();
NewView->ZoomExtents();
NewView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, true);
}
void lcMainWindow::ToggleDockWidget(QWidget* DockWidget)
@@ -3401,7 +3402,7 @@ void lcMainWindow::HandleCommand(lcCommandId CommandId)
case LC_VIEW_ZOOM_EXTENTS:
if (ActiveView)
ActiveView->ZoomExtents();
ActiveView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
break;
case LC_VIEW_LOOK_AT:
+1 -1
View File
@@ -199,7 +199,7 @@ public:
void RestoreTabLayout(const QByteArray& TabLayout);
void RemoveAllModelTabs();
void CloseCurrentModelTab();
void SetCurrentModelTab(lcModel* Model);
void SetCurrentModelTab(lcModel* Model, bool AdjustFarPlane);
void AddView(lcView* View);
void RemoveView(lcView* View);
+2 -1
View File
@@ -7,6 +7,7 @@
#include "pieceinf.h"
#include "lc_library.h"
#include "lc_view.h"
#include "lc_model.h"
#include "camera.h"
#include "lc_doublespinbox.h"
#include "lc_qutils.h"
@@ -133,7 +134,7 @@ lcMinifigDialog::lcMinifigDialog(QWidget* Parent)
mMinifigWizard->Calculate();
mView->GetCamera()->SetViewpoint(lcVector3(0.0f, -270.0f, 90.0f));
mView->ZoomExtents();
mView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setFocus();
+25 -22
View File
@@ -1731,14 +1731,20 @@ lcResult<QImage> lcModel::RenderStepImageWithReadyAssets(bool Zoom, int Width, i
View.SetOffscreenContext();
View.MakeCurrent();
View.SetSize(Width, Height);
if (!OutputCamera && !ActiveView)
ZoomExtentsAtStep(View.GetCamera(), (float)Width / (float)Height, GetLastStep(), lcGeometryBoundsMode::AvailableGeometryOnly);
{
SetTemporaryStep(GetLastStep());
View.ZoomExtents(lcGeometryBoundsMode::AvailableOnly, true);
}
SetTemporaryStep(Step);
if (Zoom && ActiveView && !OutputCamera)
ZoomExtents(View.GetCamera(), (float)Width / (float)Height, lcMatrix44Identity(), lcGeometryBoundsMode::AvailableGeometryOnly);
{
View.ZoomExtents(lcGeometryBoundsMode::AvailableOnly, true);
}
if (!View.BeginRenderToImage(Width, Height))
{
@@ -2057,14 +2063,23 @@ lcResult<void> lcModel::SaveStepImages(const QString& BaseName, bool AddStepSuff
{
WaitForAssets();
lcCamera OutputCamera(true);
std::unique_ptr<lcView> OutputView;
lcCamera* Camera = nullptr;
if (!gMainWindow || !gMainWindow->GetActiveView())
{
OutputCamera.SetViewpoint(lcViewpoint::Home);
ZoomExtentsAtStep(&OutputCamera, (float)Width / (float)Height, GetLastStep(), lcGeometryBoundsMode::AvailableGeometryOnly);
Camera = &OutputCamera;
const lcStep CurrentStep = mCurrentStep;
OutputView = std::make_unique<lcView>(lcViewType::View, this);
OutputView->SetSize(Width, Height);
SetTemporaryStep(GetLastStep());
OutputView->ZoomExtents(lcGeometryBoundsMode::AvailableOnly, true);
SetTemporaryStep(CurrentStep);
if (!mActive)
CalculateStep(LC_STEP_MAX);
Camera = OutputView->GetCamera();
}
for (lcStep Step = Start; Step <= End; Step++)
@@ -2184,7 +2199,7 @@ void lcModel::SubModelAddBoundingBoxPoints(const lcMatrix44& WorldMatrix, std::v
{
const lcMesh* Mesh = mPieceInfo ? mPieceInfo->GetDisplayMesh() : nullptr;
if (Mode == lcGeometryBoundsMode::AvailableGeometryOnly && mPieceInfo && mPieceInfo->mState != lcPieceInfoState::Loaded)
if (Mode == lcGeometryBoundsMode::AvailableOnly && mPieceInfo && mPieceInfo->mState != lcPieceInfoState::Loaded)
Mesh = nullptr;
if (Mesh)
@@ -4864,7 +4879,7 @@ std::vector<lcVector3> lcModel::GetPiecesBoundingBoxPoints(lcGeometryBoundsMode
std::vector<lcVector3> Points;
const lcMesh* Mesh = mPieceInfo ? mPieceInfo->GetDisplayMesh() : nullptr;
if (Mode == lcGeometryBoundsMode::AvailableGeometryOnly && mPieceInfo && mPieceInfo->mState != lcPieceInfoState::Loaded)
if (Mode == lcGeometryBoundsMode::AvailableOnly && mPieceInfo && mPieceInfo->mState != lcPieceInfoState::Loaded)
Mesh = nullptr;
if (Mesh)
@@ -6038,7 +6053,7 @@ void lcModel::MoveCamera(lcCamera* Camera, const lcVector3& Direction)
}
}
void lcModel::ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& WorldMatrix, lcGeometryBoundsMode Mode)
void lcModel::ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& WorldMatrix, lcGeometryBoundsMode Mode, bool AdjustFarPlane)
{
std::vector<lcVector3> Points = GetPiecesBoundingBoxPoints(Mode);
@@ -6063,7 +6078,7 @@ void lcModel::ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& Worl
BeginEditHistory();
}
Camera->ZoomExtents(Aspect, Center, Points, mCurrentStep, gMainWindow ? gMainWindow->GetAddKeys() : false);
Camera->ZoomExtents(Aspect, Center, Points, mCurrentStep, gMainWindow ? gMainWindow->GetAddKeys() : false, AdjustFarPlane);
if (!mIsPreview && gMainWindow)
gMainWindow->UpdateSelectedObjects(false);
@@ -6077,18 +6092,6 @@ void lcModel::ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& Worl
}
}
void lcModel::ZoomExtentsAtStep(lcCamera* Camera, float Aspect, lcStep Step, lcGeometryBoundsMode Mode)
{
const lcStep CurrentStep = mCurrentStep;
SetTemporaryStep(Step);
ZoomExtents(Camera, Aspect, lcMatrix44Identity(), Mode);
SetTemporaryStep(CurrentStep);
if (!mActive)
CalculateStep(LC_STEP_MAX);
}
void lcModel::Zoom(lcCamera* Camera, float Amount)
{
if (!Camera->IsSimple())
+7 -2
View File
@@ -5,6 +5,12 @@
#include "lc_objectproperty.h"
#include "lc_result.h"
enum class lcGeometryBoundsMode
{
IncludeFallback,
AvailableOnly
};
enum class lcCameraProjection;
struct lcSetInventoryItem;
struct lcModelHistoryEditState;
@@ -424,8 +430,7 @@ public:
void ZoomRegionToolClicked(lcView* View, float AspectRatio, const lcVector3& Position, const lcVector3& TargetPosition, const lcVector3* Corners);
void LookAt(lcCamera* Camera);
void MoveCamera(lcCamera* Camera, const lcVector3& Direction);
void ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& WorldMatrix, lcGeometryBoundsMode Mode);
void ZoomExtentsAtStep(lcCamera* Camera, float Aspect, lcStep Step, lcGeometryBoundsMode Mode);
void ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& WorldMatrix, lcGeometryBoundsMode Mode, bool AdjustFarPlane);
void Zoom(lcCamera* Camera, float Amount);
void MoveSelectedObjects(const lcVector3& Distance, lcModelTransformFlags Flags, lcModelHistoryEditMerge ModelHistoryEditMerge)
+2 -2
View File
@@ -195,7 +195,7 @@ void lcPreviewDockWidget::RefreshPendingAssets()
}
if (mAssetRefreshPending && mRefitAfterAssetLoad)
mPreview->ZoomExtents();
mPreview->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
mAssetRefreshPending = false;
mRefitAfterAssetLoad = true;
@@ -321,7 +321,7 @@ bool lcPreview::SetCurrentPiece(PieceInfo* Info, int ColorCode)
mModel->UpdatePieceInfo(UpdatedModels);
Library->ReleasePieceInfo(Info);
ZoomExtents();
ZoomExtents(lcGeometryBoundsMode::IncludeFallback, true);
return true;
}
+1 -1
View File
@@ -377,7 +377,7 @@ void lcThumbnailManager::DrawThumbnail(lcPartThumbnailId ThumbnailId, lcPartThum
mView->GetCamera()->SetViewpoint(Position, Center, lcVector3(0, 0, 1));
mView->GetCamera()->m_fovy = 20.0f;
mView->ZoomExtents();
mView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
mView->OnDraw();
+24 -7
View File
@@ -1832,7 +1832,7 @@ void lcView::SetViewpoint(lcViewpoint Viewpoint)
}
mCamera->SetViewpoint(Viewpoint);
ZoomExtents();
ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
Redraw();
}
@@ -1849,7 +1849,7 @@ void lcView::SetViewpoint(const lcVector3& Position)
}
mCamera->SetViewpoint(Position);
ZoomExtents();
ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
Redraw();
}
@@ -1882,7 +1882,7 @@ void lcView::SetCameraAngles(float Latitude, float Longitude)
}
mCamera->SetAngles(Latitude, Longitude, 1.0f);
ZoomExtents();
ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
Redraw();
}
@@ -1892,7 +1892,7 @@ void lcView::SetDefaultCamera()
mCamera = new lcCamera(true);
mCamera->SetViewpoint(lcViewpoint::Home);
ZoomExtents();
ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
Redraw();
}
@@ -1978,7 +1978,7 @@ void lcView::Zoom(float Amount)
ActiveModel->Zoom(mCamera, Amount);
}
void lcView::ZoomExtents()
void lcView::ZoomExtents(lcGeometryBoundsMode Mode, bool AdjustFarPlane)
{
lcModel* ActiveModel = GetActiveModel();
@@ -1986,10 +1986,27 @@ void lcView::ZoomExtents()
{
const lcMatrix44 WorldMatrix = mActiveSubmodelInstance ? mActiveSubmodelTransform : lcMatrix44Identity();
ActiveModel->ZoomExtents(mCamera, (float)mWidth / (float)mHeight, WorldMatrix, lcGeometryBoundsMode::IncludeFallbackGeometry);
ActiveModel->ZoomExtents(mCamera, (float)mWidth / (float)mHeight, WorldMatrix, Mode, AdjustFarPlane);
}
}
void lcView::ZoomExtentsAtStep(lcStep Step, lcGeometryBoundsMode Mode, bool AdjustFarPlane)
{
lcModel* ActiveModel = GetActiveModel();
if (!ActiveModel)
return;
const lcStep CurrentStep = ActiveModel->GetCurrentStep();
ActiveModel->SetTemporaryStep(Step);
ZoomExtents(Mode, AdjustFarPlane);
ActiveModel->SetTemporaryStep(CurrentStep);
if (!ActiveModel->IsActive())
ActiveModel->CalculateStep(LC_STEP_MAX);
}
lcCursor lcView::GetCursor() const
{
const lcTool CurrentTool = GetCurrentTool();
@@ -2840,7 +2857,7 @@ void lcView::OnLeftButtonDoubleClick()
{
if (mViewType != lcViewType::View)
{
ZoomExtents();
ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
return;
}
+2 -1
View File
@@ -275,7 +275,8 @@ public:
void LookAt();
void MoveCamera(const lcVector3& Direction);
void Zoom(float Amount);
void ZoomExtents();
void ZoomExtents(lcGeometryBoundsMode Mode, bool AdjustFarPlane);
void ZoomExtentsAtStep(lcStep Step, lcGeometryBoundsMode Mode, bool AdjustFarPlane);
void RemoveCamera();
void ShowContextMenu() const;
+1 -1
View File
@@ -100,7 +100,7 @@ bool lcViewWidget::event(QEvent* Event)
break;
case Qt::SmartZoomNativeGesture:
mView->ZoomExtents();
mView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
break;
default:
+1 -1
View File
@@ -1145,7 +1145,7 @@ void lcPiece::SubModelCompareBoundingBox(const lcMatrix44& WorldMatrix, lcVector
void lcPiece::SubModelAddBoundingBoxPoints(const lcMatrix44& WorldMatrix, std::vector<lcVector3>& Points, lcGeometryBoundsMode Mode) const
{
if (Mode == lcGeometryBoundsMode::AvailableGeometryOnly && mPieceInfo->GetSynthInfo() && !HasGeneratedMesh())
if (Mode == lcGeometryBoundsMode::AvailableOnly && mPieceInfo->GetSynthInfo() && !HasGeneratedMesh())
return;
lcMesh* Mesh = GetDisplayMesh();
+1 -1
View File
@@ -422,7 +422,7 @@ void PieceInfo::CompareBoundingBox(const lcMatrix44& WorldMatrix, lcVector3& Min
void PieceInfo::AddSubModelBoundingBoxPoints(const lcMatrix44& WorldMatrix, std::vector<lcVector3>& Points, lcGeometryBoundsMode Mode) const
{
if (Mode == lcGeometryBoundsMode::AvailableGeometryOnly)
if (Mode == lcGeometryBoundsMode::AvailableOnly)
{
if (IsModel())
{
+1 -1
View File
@@ -233,7 +233,7 @@ void Project::SetActiveModel(lcModel* ActiveModel, bool UpdateInterface)
if (!mIsPreview && gMainWindow && UpdateInterface)
{
gMainWindow->SetCurrentModelTab(mActiveModel);
gMainWindow->SetCurrentModelTab(mActiveModel, true);
mActiveModel->UpdateInterface();
}
}
+21 -2
View File
@@ -27,6 +27,25 @@ the visible step. The spinner hides immediately when work settles; there is no
until the project's failures disappear or the project changes.
Views redraw and update bounds as results arrive, but keep the user's camera.
Interactive and output camera fits go through `lcView::ZoomExtents()`, with
fallback geometry included for interactive views and only available geometry
included for outputs. The view delegates fitting and saved camera undo history
to `lcModel::ZoomExtents()`. `lcView::ZoomExtentsAtStep()` restores the model's
current step after fitting.
The adjustment flag passes through view, model, and camera. The camera adjusts
clipping after updating its fitted position, using the fit's transformed bounding
points without collecting bounds again.
The view fit entry points take an explicit `AdjustFarPlane` flag. Initial view
and preview fits and automatic image output fits enable it, using the bounds
available at that time; ordinary fits disable it. The distance expands only when
those bounds extend beyond both the default 50,000 and the current distance. Later asset
updates, redraws, navigation, and fits do not adjust clipping again.
Increases include a 10% margin and round up to multiples of 10,000; they never
shrink the distance or change saved model cameras. Explicit command-line clipping
planes remain authoritative for image output.
Restored tab layouts defer clipping adjustment until the ordinary camera's saved
position and settings have been applied, so a saved distance cannot undo the
load-time expansion. Named model cameras retain their saved clipping settings.
During synth edits, the previous generated mesh stays visible until a replacement
settles. Images and geometry exports wait for their requirements and omit failed
geometry instead of saving loading cubes. See [Caller policies](#caller-policies)
@@ -355,8 +374,8 @@ including current synth generations, and repeats picking before placement. If
loading fails, both dragging and insertion use the available or fallback
geometry rather than rejecting placement.
Automatic output fitting uses `lcGeometryBoundsMode::AvailableGeometryOnly`;
interactive fitting uses `IncludeFallbackGeometry`. A whole-model wait can
Automatic output fitting uses `lcGeometryBoundsMode::AvailableOnly`;
interactive fitting uses `IncludeFallback`. A whole-model wait can
conservatively include later steps even when output requests an earlier step.
Scene flags preventing fallback cubes do not themselves initiate a wait or
guarantee success. See [lc_view.cpp](../common/lc_view.cpp) and