//########################################################################## //# # //# CLOUDCOMPARE # //# # //# This program is free software; you can redistribute it and/or modify # //# it under the terms of the GNU General Public License as published by # //# the Free Software Foundation; version 2 or later of the License. # //# # //# This program is distributed in the hope that it will be useful, # //# but WITHOUT ANY WARRANTY; without even the implied warranty of # //# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # //# GNU General Public License for more details. # //# # //# COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI) # //# # //########################################################################## #include "mainwindow.h" //CCCoreLib Includes #include #include #include #include #include #include #include #include #include #include //for tests #include #include //qCC_db #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //qCC_io #include #include #include #include //QCC_glWindow #include #include //local includes #include "ccConsole.h" #include "ccEntityAction.h" #include "ccHistogramWindow.h" #include "ccInnerRect2DFinder.h" //common #include //common dialogs #include #include #include #include //dialogs #include "ccAboutDialog.h" #include "ccAdjustZoomDlg.h" #include "ccAlignDlg.h" #include "ccApplication.h" #include "ccApplyTransformationDlg.h" #include "ccAskThreeDoubleValuesDlg.h" #include "ccBoundingBoxEditorDlg.h" #include "ccCamSensorProjectionDlg.h" #include "ccClippingBoxTool.h" #include "ccColorFromScalarDlg.h" #include "ccColorScaleEditorDlg.h" #include "ccComparisonDlg.h" #include "ccPrimitiveDistanceDlg.h" #include "ccFilterByValueDlg.h" #include "ccGBLSensorProjectionDlg.h" #include "ccGeomFeaturesDlg.h" #include "ccGraphicalSegmentationTool.h" #include "ccGraphicalTransformationTool.h" #include "ccItemSelectionDlg.h" #include "ccLabelingDlg.h" #include "ccMatchScalesDlg.h" #include "ccNoiseFilterDlg.h" #include "ccOrderChoiceDlg.h" #include "ccPlaneEditDlg.h" #include "ccPointListPickingDlg.h" #include "ccPointPairRegistrationDlg.h" #include "ccPointPropertiesDlg.h" #include "ccPrimitiveFactoryDlg.h" #include "ccPtsSamplingDlg.h" #include "ccRasterizeTool.h" #include "ccRegistrationDlg.h" #include "ccRenderToFileDlg.h" #include "ccScaleDlg.h" #include "ccSectionExtractionTool.h" #include "ccSensorComputeDistancesDlg.h" #include "ccSensorComputeScatteringAnglesDlg.h" #include "ccSORFilterDlg.h" #include "ccSubsamplingDlg.h" #include "ccTracePolylineTool.h" #include "ccTranslationManager.h" #include "ccUnrollDlg.h" #include "ccVolumeCalcTool.h" #include "ccWaveformDialog.h" #include "ccEntitySelectionDlg.h" #include "ccSmoothPolylineDlg.h" //other #include "ccCropTool.h" #include "ccPersistentSettings.h" #include "ccRecentFiles.h" #include "ccRegistrationTools.h" #include "ccUtils.h" #include "db_tree/ccDBRoot.h" #include "pluginManager/ccPluginUIManager.h" #include "ccGlFilter.h" //3D mouse handler #ifdef CC_3DXWARE_SUPPORT #include "cc3DMouseManager.h" #endif //Gamepads #ifdef CC_GAMEPAD_SUPPORT #include "ccGamepadManager.h" #endif //Qt #include #include //Qt UI files #include #include #include //System #include #include //global static pointer (as there should only be one instance of MainWindow!) static MainWindow* s_instance = nullptr; //default file filter separator static const QString s_fileFilterSeparator(";;"); enum PickingOperation { NO_PICKING_OPERATION, PICKING_ROTATION_CENTER, PICKING_LEVEL_POINTS, }; static ccGLWindowInterface* s_pickingWindow = nullptr; static PickingOperation s_currentPickingOperation = NO_PICKING_OPERATION; static std::vector s_levelLabels; static ccPointCloud* s_levelMarkersCloud = nullptr; static ccHObject* s_levelEntity = nullptr; static QFileDialog::Options CCFileDialogOptions() { //dialog options QFileDialog::Options dialogOptions = QFileDialog::Options(); dialogOptions |= QFileDialog::DontResolveSymlinks; if (!ccOptions::Instance().useNativeDialogs) { dialogOptions |= QFileDialog::DontUseNativeDialog; } return dialogOptions; } MainWindow::MainWindow() : m_UI( new Ui::MainWindow ) , m_ccRoot(nullptr) , m_uiFrozen(false) , m_recentFiles(new ccRecentFiles(this)) , m_3DMouseManager(nullptr) , m_gamepadManager(nullptr) , m_viewModePopupButton(nullptr) , m_pivotVisibilityPopupButton(nullptr) , m_firstShow(true) , m_pickingHub(nullptr) , m_cpeDlg(nullptr) , m_gsTool(nullptr) , m_tplTool(nullptr) , m_seTool(nullptr) , m_transTool(nullptr) , m_clipTool(nullptr) , m_compDlg(nullptr) , m_ppDlg(nullptr) , m_plpDlg(nullptr) , m_pprDlg(nullptr) , m_pfDlg(nullptr) { m_UI->setupUi( this ); setWindowTitle(QStringLiteral("CloudCompare v") + ccApp->versionLongStr(false)); m_pluginUIManager = new ccPluginUIManager( this, this ); ccTranslationManager::Get().populateMenu( m_UI->menuLanguage, ccApp->translationPath() ); #ifdef Q_OS_MAC m_UI->actionAbout->setMenuRole( QAction::AboutRole ); m_UI->actionAboutPlugins->setMenuRole( QAction::ApplicationSpecificRole ); m_UI->actionFullScreen->setText( tr( "Enter Full Screen" ) ); m_UI->actionFullScreen->setShortcut( QKeySequence( Qt::CTRL + Qt::META + Qt::Key_F ) ); #endif // Set up dynamic menus m_UI->menuFile->insertMenu(m_UI->actionSave, m_recentFiles->menu()); //Console ccConsole::Init(m_UI->consoleWidget, this, this); m_UI->actionEnableQtWarnings->setChecked(ccConsole::QtMessagesEnabled()); //advanced widgets not handled by QDesigner { //view mode pop-up menu { m_viewModePopupButton = new QToolButton(); QMenu* menu = new QMenu(m_viewModePopupButton); menu->addAction(m_UI->actionSetOrthoView); menu->addAction(m_UI->actionSetCenteredPerspectiveView); menu->addAction(m_UI->actionSetViewerPerspectiveView); m_viewModePopupButton->setMenu(menu); m_viewModePopupButton->setPopupMode(QToolButton::InstantPopup); m_viewModePopupButton->setToolTip(tr("Set current view mode")); m_viewModePopupButton->setStatusTip(m_viewModePopupButton->toolTip()); m_UI->toolBarView->insertWidget(m_UI->actionZoomAndCenter, m_viewModePopupButton); m_viewModePopupButton->setEnabled(false); } //pivot center pop-up menu { m_pivotVisibilityPopupButton = new QToolButton(); QMenu* menu = new QMenu(m_pivotVisibilityPopupButton); menu->addAction(m_UI->actionSetPivotAlwaysOn); menu->addAction(m_UI->actionSetPivotRotationOnly); menu->addAction(m_UI->actionSetPivotOff); m_pivotVisibilityPopupButton->setMenu(menu); m_pivotVisibilityPopupButton->setPopupMode(QToolButton::InstantPopup); m_pivotVisibilityPopupButton->setToolTip(tr("Set pivot visibility")); m_pivotVisibilityPopupButton->setStatusTip(m_pivotVisibilityPopupButton->toolTip()); m_UI->toolBarView->insertWidget(m_UI->actionZoomAndCenter,m_pivotVisibilityPopupButton); m_pivotVisibilityPopupButton->setEnabled(false); } } //tabifyDockWidget(DockableDBTree,DockableProperties); //db-tree { m_ccRoot = new ccDBRoot(m_UI->dbTreeView, m_UI->propertiesTreeView, this); connect(m_ccRoot, &ccDBRoot::selectionChanged, this, &MainWindow::updateUIWithSelection, Qt::QueuedConnection); connect(m_ccRoot, &ccDBRoot::dbIsEmpty, this, [=]() { updateUIWithSelection(); updateMenus(); }, Qt::QueuedConnection); //we don't call updateUI because there's no need to update the properties dialog connect(m_ccRoot, &ccDBRoot::dbIsNotEmptyAnymore, this, [=]() { updateUIWithSelection(); updateMenus(); }, Qt::QueuedConnection); //we don't call updateUI because there's no need to update the properties dialog } //MDI Area { m_mdiArea = new QMdiArea(this); setCentralWidget(m_mdiArea); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::updateMenus); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::on3DViewActivated); m_mdiArea->installEventFilter(this); } //picking hub { m_pickingHub = new ccPickingHub(this, this); connect(m_mdiArea, &QMdiArea::subWindowActivated, m_pickingHub, &ccPickingHub::onActiveWindowChanged); } // restore the state of the 'auto-restore' menu entry // (do that before connecting the actions) { QSettings settings; bool doNotAutoRestoreGeometry = settings.value(ccPS::DoNotRestoreWindowGeometry(), !m_UI->actionRestoreWindowOnStartup->isChecked()).toBool(); m_UI->actionRestoreWindowOnStartup->setChecked(!doNotAutoRestoreGeometry); } connectActions(); new3DView(); setupInputDevices(); freezeUI(false); updateUI(); QMainWindow::statusBar()->showMessage(tr("Ready")); #ifdef CC_CORE_LIB_USES_TBB ccConsole::Print( QStringLiteral( "[TBB] Using Intel's Threading Building Blocks %1" ) .arg( QString( TBB_VERSION ) ) ); #endif ccConsole::Print(tr("CloudCompare started!")); } MainWindow::~MainWindow() { destroyInputDevices(); cancelPreviousPickingOperation(false); //just in case assert(m_ccRoot && m_mdiArea); m_ccRoot->disconnect(); m_mdiArea->disconnect(); //we don't want any other dialog/function to use the following structures ccDBRoot* ccRoot = m_ccRoot; m_ccRoot = nullptr; //remove all entities from 3D views before quitting to avoid any side-effect //(this won't be done automatically since we've just reset m_ccRoot) ccRoot->getRootEntity()->setDisplay_recursive(nullptr); for (int i = 0; i < getGLWindowCount(); ++i) { getGLWindow(i)->setSceneDB(nullptr); } m_cpeDlg = nullptr; m_gsTool = nullptr; m_seTool = nullptr; m_transTool = nullptr; m_clipTool = nullptr; m_compDlg = nullptr; m_ppDlg = nullptr; m_plpDlg = nullptr; m_pprDlg = nullptr; m_pfDlg = nullptr; //release all 'overlay' dialogs while (!m_mdiDialogs.empty()) { ccMDIDialogs mdiDialog = m_mdiDialogs.back(); m_mdiDialogs.pop_back(); mdiDialog.dialog->disconnect(); mdiDialog.dialog->stop(false); mdiDialog.dialog->setParent(nullptr); delete mdiDialog.dialog; } //m_mdiDialogs.clear(); m_mdiArea->closeAllSubWindows(); if (ccRoot) { delete ccRoot; ccRoot = nullptr; } delete m_UI; m_UI = nullptr; ccConsole::ReleaseInstance(false); //if we flush the console, it will try to display the console window while we are destroying everything! } void MainWindow::initPlugins( ) { m_pluginUIManager->init(); // Set up dynamic tool bars addToolBar( Qt::RightToolBarArea, m_pluginUIManager->glFiltersToolbar() ); addToolBar( Qt::RightToolBarArea, m_pluginUIManager->mainPluginToolbar() ); for ( QToolBar *toolbar : m_pluginUIManager->additionalPluginToolbars() ) { addToolBar( Qt::TopToolBarArea, toolbar ); } // Set up dynamic menus m_UI->menubar->insertMenu( m_UI->menu3DViews->menuAction(), m_pluginUIManager->pluginMenu() ); m_UI->menuDisplay->insertMenu( m_UI->menuActiveScalarField->menuAction(), m_pluginUIManager->shaderAndFilterMenu() ); m_UI->menuToolbars->addAction( m_pluginUIManager->actionShowMainPluginToolbar() ); m_UI->menuToolbars->addAction( m_pluginUIManager->actionShowGLFilterToolbar() ); } void MainWindow::doEnableQtWarnings(bool state) { ccConsole::EnableQtMessages(state); } void MainWindow::increasePointSize() { //active window? ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setPointSize(win->getViewportParameters().defaultPointSize + 1); win->redraw(); } } void MainWindow::decreasePointSize() { //active window? ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setPointSize(win->getViewportParameters().defaultPointSize - 1); win->redraw(); } } void MainWindow::setupInputDevices() { #ifdef CC_3DXWARE_SUPPORT m_3DMouseManager = new cc3DMouseManager( this, this ); m_UI->menuFile->insertMenu(m_UI->actionCloseAll, m_3DMouseManager->menu()); #endif #ifdef CC_GAMEPAD_SUPPORT m_gamepadManager = new ccGamepadManager( this, this ); m_UI->menuFile->insertMenu(m_UI->actionCloseAll, m_gamepadManager->menu()); #endif #if defined(CC_3DXWARE_SUPPORT) || defined(CC_GAMEPAD_SUPPORT) m_UI->menuFile->insertSeparator(m_UI->actionCloseAll); #endif } void MainWindow::destroyInputDevices() { #ifdef CC_GAMEPAD_SUPPORT delete m_gamepadManager; m_gamepadManager = nullptr; #endif #ifdef CC_3DXWARE_SUPPORT delete m_3DMouseManager; m_3DMouseManager = nullptr; #endif } void MainWindow::connectActions() { assert(m_ccRoot); assert(m_mdiArea); //Keyboard shortcuts //'A': toggles selected items activation connect(m_UI->actionToggleActivation, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::ACTIVE ); }); //'V': toggles selected items visibility connect(m_UI->actionToggleVisibility, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::VISIBLE ); }); //'N': toggles selected items normals visibility connect(m_UI->actionToggleNormals, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::NORMALS ); }); //'C': toggles selected items colors visibility connect(m_UI->actionToggleColors, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::COLOR ); }); //'S': toggles selected items SF visibility connect(m_UI->actionToggleSF, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::SCALAR_FIELD ); }); //'D': toggles selected items '3D name' visibility connect(m_UI->actionToggleShowName, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::NAME ); }); //'M': toggles selected items materials/textures visibility connect(m_UI->actionToggleMaterials, &QAction::triggered, this, [=]() { toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY::MATERIAL ); }); //TODO... but not ready yet ;) m_UI->actionLoadShader->setVisible(false); m_UI->actionDeleteShader->setVisible(false); m_UI->actionKMeans->setVisible(false); m_UI->actionFrontPropagation->setVisible(false); /*** MAIN MENU ***/ //"File" menu connect(m_UI->actionOpen, &QAction::triggered, this, &MainWindow::doActionLoadFile); connect(m_UI->actionSave, &QAction::triggered, this, &MainWindow::doActionSaveFile); connect(m_UI->actionSaveProject, &QAction::triggered, this, &MainWindow::doActionSaveProject); connect(m_UI->actionGlobalShiftSettings, &QAction::triggered, this, &MainWindow::doActionGlobalShiftSeetings); connect(m_UI->actionPrimitiveFactory, &QAction::triggered, this, &MainWindow::doShowPrimitiveFactory); connect(m_UI->actionCloseAll, &QAction::triggered, this, &MainWindow::closeAll); connect(m_UI->actionQuit, &QAction::triggered, this, &QWidget::close); //"Edit > Colors" menu connect(m_UI->actionSetUniqueColor, &QAction::triggered, this, &MainWindow::doActionSetUniqueColor); connect(m_UI->actionSetColorGradient, &QAction::triggered, this, &MainWindow::doActionSetColorGradient); connect(m_UI->actionChangeColorLevels, &QAction::triggered, this, &MainWindow::doActionChangeColorLevels); connect(m_UI->actionColorize, &QAction::triggered, this, &MainWindow::doActionColorize); connect(m_UI->actionRGBToGreyScale, &QAction::triggered, this, &MainWindow::doActionRGBToGreyScale); connect(m_UI->actionInterpolateColors, &QAction::triggered, this, &MainWindow::doActionInterpolateColors); connect(m_UI->actionEnhanceRGBWithIntensities, &QAction::triggered, this, &MainWindow::doActionEnhanceRGBWithIntensities); connect(m_UI->actionColorFromScalarField, &QAction::triggered, this, &MainWindow::doActionColorFromScalars); connect(m_UI->actionClearColor, &QAction::triggered, this, [=]() { clearSelectedEntitiesProperty( ccEntityAction::CLEAR_PROPERTY::COLORS ); }); connect(m_UI->actionRGBGaussianFilter, &QAction::triggered, this, &MainWindow::doActionRGBGaussianFilter); connect(m_UI->actionRGBBilateralFilter, &QAction::triggered, this, &MainWindow::doActionRGBBilateralFilter); connect(m_UI->actionRGBMeanFilter, &QAction::triggered, this, &MainWindow::doActionRGBMeanFilter); connect(m_UI->actionRGBMedianFilter, &QAction::triggered, this, &MainWindow::doActionRGBMedianFilter); //"Edit > Normals" menu connect(m_UI->actionComputeNormals, &QAction::triggered, this, &MainWindow::doActionComputeNormals); connect(m_UI->actionInvertNormals, &QAction::triggered, this, &MainWindow::doActionInvertNormals); connect(m_UI->actionConvertNormalToHSV, &QAction::triggered, this, &MainWindow::doActionConvertNormalsToHSV); connect(m_UI->actionConvertNormalToDipDir, &QAction::triggered, this, &MainWindow::doActionConvertNormalsToDipDir); connect(m_UI->actionExportNormalToSF, &QAction::triggered, this, &MainWindow::doActionExportNormalToSF); connect(m_UI->actionSetSFsAsNormal, &QAction::triggered, this, &MainWindow::doActionSetSFsAsNormal); connect(m_UI->actionOrientNormalsMST, &QAction::triggered, this, &MainWindow::doActionOrientNormalsMST); connect(m_UI->actionOrientNormalsFM, &QAction::triggered, this, &MainWindow::doActionOrientNormalsFM); connect(m_UI->actionShiftPointsAlongNormals, &QAction::triggered, this, &MainWindow::doActionShiftPointsAlongNormals); connect(m_UI->actionClearNormals, &QAction::triggered, this, [=]() { clearSelectedEntitiesProperty( ccEntityAction::CLEAR_PROPERTY::NORMALS ); }); //"Edit > Octree" menu connect(m_UI->actionComputeOctree, &QAction::triggered, this, &MainWindow::doActionComputeOctree); connect(m_UI->actionResampleWithOctree, &QAction::triggered, this, &MainWindow::doActionResampleWithOctree); //"Edit > Grid" menu connect(m_UI->actionDeleteScanGrid, &QAction::triggered, this, &MainWindow::doActionDeleteScanGrids); //"Edit > Cloud" menu connect(m_UI->actionCreateSinglePointCloud, &QAction::triggered, this, &MainWindow::createSinglePointCloud); connect(m_UI->actionPasteCloudFromClipboard, &QAction::triggered, this, &MainWindow::createPointCloudFromClipboard); //the 'Paste from clipboard' tool depends on the clipboard state { const QClipboard* clipboard = QApplication::clipboard(); assert(clipboard); m_UI->actionPasteCloudFromClipboard->setEnabled(clipboard->mimeData()->hasText()); connect(clipboard, &QClipboard::dataChanged, [&]() { m_UI->actionPasteCloudFromClipboard->setEnabled(clipboard->mimeData()->hasText()); }); } //"Edit > Mesh" menu connect(m_UI->actionComputeMeshAA, &QAction::triggered, this, &MainWindow::doActionComputeMeshAA); connect(m_UI->actionComputeMeshLS, &QAction::triggered, this, &MainWindow::doActionComputeMeshLS); connect(m_UI->actionMeshTwoPolylines, &QAction::triggered, this, &MainWindow::doMeshTwoPolylines); connect(m_UI->actionMeshScanGrids, &QAction::triggered, this, &MainWindow::doActionMeshScanGrids); connect(m_UI->actionConvertTextureToColor, &QAction::triggered, this, &MainWindow::doActionConvertTextureToColor); connect(m_UI->actionSamplePointsOnMesh, &QAction::triggered, this, &MainWindow::doActionSamplePointsOnMesh); connect(m_UI->actionSmoothMeshLaplacian, &QAction::triggered, this, &MainWindow::doActionSmoothMeshLaplacian); connect(m_UI->actionSubdivideMesh, &QAction::triggered, this, &MainWindow::doActionSubdivideMesh); connect(m_UI->actionFlipMeshTriangles, &QAction::triggered, this, &MainWindow::doActionFlipMeshTriangles); connect(m_UI->actionMeasureMeshSurface, &QAction::triggered, this, &MainWindow::doActionMeasureMeshSurface); connect(m_UI->actionMeasureMeshVolume, &QAction::triggered, this, &MainWindow::doActionMeasureMeshVolume); connect(m_UI->actionFlagMeshVertices, &QAction::triggered, this, &MainWindow::doActionFlagMeshVertices); //"Edit > Mesh > Scalar Field" menu connect(m_UI->actionSmoothMeshSF, &QAction::triggered, this, &MainWindow::doActionSmoothMeshSF); connect(m_UI->actionEnhanceMeshSF, &QAction::triggered, this, &MainWindow::doActionEnhanceMeshSF); //"Edit > Polyline" menu connect(m_UI->actionSamplePointsOnPolyline, &QAction::triggered, this, &MainWindow::doActionSamplePointsOnPolyline); connect(m_UI->actionSmoothPolyline, &QAction::triggered, this, &MainWindow::doActionSmoohPolyline); //"Edit > Plane" menu connect(m_UI->actionCreatePlane, &QAction::triggered, this, &MainWindow::doActionCreatePlane); connect(m_UI->actionEditPlane, &QAction::triggered, this, &MainWindow::doActionEditPlane); connect(m_UI->actionFlipPlane, &QAction::triggered, this, &MainWindow::doActionFlipPlane); connect(m_UI->actionComparePlanes, &QAction::triggered, this, &MainWindow::doActionComparePlanes); //"Edit > Sensor > Ground-Based lidar" menu connect(m_UI->actionShowDepthBuffer, &QAction::triggered, this, &MainWindow::doActionShowDepthBuffer); connect(m_UI->actionExportDepthBuffer, &QAction::triggered, this, &MainWindow::doActionExportDepthBuffer); connect(m_UI->actionComputePointsVisibility, &QAction::triggered, this, &MainWindow::doActionComputePointsVisibility); //"Edit > Sensor" menu connect(m_UI->actionCreateGBLSensor, &QAction::triggered, this, &MainWindow::doActionCreateGBLSensor); connect(m_UI->actionCreateCameraSensor, &QAction::triggered, this, &MainWindow::doActionCreateCameraSensor); connect(m_UI->actionModifySensor, &QAction::triggered, this, &MainWindow::doActionModifySensor); connect(m_UI->actionProjectUncertainty, &QAction::triggered, this, &MainWindow::doActionProjectUncertainty); connect(m_UI->actionCheckPointsInsideFrustum, &QAction::triggered, this, &MainWindow::doActionCheckPointsInsideFrustum); connect(m_UI->actionComputeDistancesFromSensor, &QAction::triggered, this, &MainWindow::doActionComputeDistancesFromSensor); connect(m_UI->actionComputeScatteringAngles, &QAction::triggered, this, &MainWindow::doActionComputeScatteringAngles); connect(m_UI->actionViewFromSensor, &QAction::triggered, this, &MainWindow::doActionSetViewFromSensor); //"Edit > Scalar fields" menu connect(m_UI->actionShowHistogram, &QAction::triggered, this, &MainWindow::showSelectedEntitiesHistogram); connect(m_UI->actionComputeStatParams, &QAction::triggered, this, &MainWindow::doActionComputeStatParams); connect(m_UI->actionSFGradient, &QAction::triggered, this, &MainWindow::doActionSFGradient); connect(m_UI->actionGaussianFilter, &QAction::triggered, this, &MainWindow::doActionSFGaussianFilter); connect(m_UI->actionBilateralFilter, &QAction::triggered, this, &MainWindow::doActionSFBilateralFilter); connect(m_UI->actionFilterByValue, &QAction::triggered, this, &MainWindow::doActionFilterByValue); connect(m_UI->actionAddConstantSF, &QAction::triggered, this, &MainWindow::doActionAddConstantSF); connect(m_UI->actionAddClassificationSF, &QAction::triggered, this, &MainWindow::doActionAddClassificationSF); connect(m_UI->actionScalarFieldArithmetic, &QAction::triggered, this, &MainWindow::doActionScalarFieldArithmetic); connect(m_UI->actionScalarFieldFromColor, &QAction::triggered, this, &MainWindow::doActionScalarFieldFromColor); connect(m_UI->actionConvertToRGB, &QAction::triggered, this, &MainWindow::doActionSFConvertToRGB); connect(m_UI->actionConvertToRandomRGB, &QAction::triggered, this, &MainWindow::doActionSFConvertToRandomRGB); connect(m_UI->actionRenameSF, &QAction::triggered, this, &MainWindow::doActionRenameSF); connect(m_UI->actionOpenColorScalesManager, &QAction::triggered, this, &MainWindow::doActionOpenColorScalesManager); connect(m_UI->actionAddIdField, &QAction::triggered, this, &MainWindow::doActionAddIdField); connect(m_UI->actionSplitCloudUsingSF, &QAction::triggered, this, &MainWindow::doActionSplitCloudUsingSF); connect(m_UI->actionSetSFAsCoord, &QAction::triggered, this, &MainWindow::doActionSetSFAsCoord); connect(m_UI->actionInterpolateSFs, &QAction::triggered, this, &MainWindow::doActionInterpolateScalarFields); connect(m_UI->actionDeleteScalarField, &QAction::triggered, this, [=]() { clearSelectedEntitiesProperty( ccEntityAction::CLEAR_PROPERTY::CURRENT_SCALAR_FIELD ); }); connect(m_UI->actionDeleteAllSF, &QAction::triggered, this, [=]() { clearSelectedEntitiesProperty( ccEntityAction::CLEAR_PROPERTY::ALL_SCALAR_FIELDS ); }); //"Edit > Waveform" menu connect(m_UI->actionShowWaveDialog, &QAction::triggered, this, &MainWindow::doActionShowWaveDialog); connect(m_UI->actionCompressFWFData, &QAction::triggered, this, &MainWindow::doActionCompressFWFData); //"Edit" menu connect(m_UI->actionClone, &QAction::triggered, this, &MainWindow::doActionClone); connect(m_UI->actionMerge, &QAction::triggered, this, &MainWindow::doActionMerge); connect(m_UI->actionApplyTransformation, &QAction::triggered, this, &MainWindow::doActionApplyTransformation); connect(m_UI->actionApplyScale, &QAction::triggered, this, &MainWindow::doActionApplyScale); connect(m_UI->actionTranslateRotate, &QAction::triggered, this, &MainWindow::activateTranslateRotateMode); connect(m_UI->actionSegment, &QAction::triggered, this, &MainWindow::activateSegmentationMode); connect(m_UI->actionTracePolyline, &QAction::triggered, this, &MainWindow::activateTracePolylineMode); connect(m_UI->actionCrop, &QAction::triggered, this, &MainWindow::doActionCrop); connect(m_UI->actionEditGlobalShiftAndScale, &QAction::triggered, this, &MainWindow::doActionEditGlobalShiftAndScale); connect(m_UI->actionSubsample, &QAction::triggered, this, &MainWindow::doActionSubsample); connect(m_UI->actionDelete, &QAction::triggered, m_ccRoot, &ccDBRoot::deleteSelectedEntities); //"Tools > Clean" menu connect(m_UI->actionSORFilter, &QAction::triggered, this, &MainWindow::doActionSORFilter); connect(m_UI->actionNoiseFilter, &QAction::triggered, this, &MainWindow::doActionFilterNoise); //"Tools > Projection" menu connect(m_UI->actionUnroll, &QAction::triggered, this, &MainWindow::doActionUnroll); connect(m_UI->actionRasterize, &QAction::triggered, this, &MainWindow::doActionRasterize); connect(m_UI->actionConvertPolylinesToMesh, &QAction::triggered, this, &MainWindow::doConvertPolylinesToMesh); //connect(m_UI->actionCreateSurfaceBetweenTwoPolylines, &QAction::triggered, this, &MainWindow::doMeshTwoPolylines); //DGM: already connected to actionMeshTwoPolylines connect(m_UI->actionExportCoordToSF, &QAction::triggered, this, &MainWindow::doActionExportCoordToSF); //"Tools > Registration" menu connect(m_UI->actionMatchBBCenters, &QAction::triggered, this, &MainWindow::doActionMatchBBCenters); connect(m_UI->actionMatchScales, &QAction::triggered, this, &MainWindow::doActionMatchScales); connect(m_UI->actionRegister, &QAction::triggered, this, &MainWindow::doActionRegister); connect(m_UI->actionPointPairsAlign, &QAction::triggered, this, &MainWindow::activateRegisterPointPairTool); connect(m_UI->actionBBCenterToOrigin, &QAction::triggered, this, &MainWindow::doActionMoveBBCenterToOrigin); connect(m_UI->actionBBMinCornerToOrigin, &QAction::triggered, this, &MainWindow::doActionMoveBBMinCornerToOrigin); connect(m_UI->actionBBMaxCornerToOrigin, &QAction::triggered, this, &MainWindow::doActionMoveBBMaxCornerToOrigin); //"Tools > Distances" menu connect(m_UI->actionCloudCloudDist, &QAction::triggered, this, &MainWindow::doActionCloudCloudDist); connect(m_UI->actionCloudMeshDist, &QAction::triggered, this, &MainWindow::doActionCloudMeshDist); connect(m_UI->actionCloudPrimitiveDist, &QAction::triggered, this, &MainWindow::doActionCloudPrimitiveDist); connect(m_UI->actionCPS, &QAction::triggered, this, &MainWindow::doActionComputeCPS); //"Tools > Volume" menu connect(m_UI->actionCompute2HalfDimVolume, &QAction::triggered, this, &MainWindow::doCompute2HalfDimVolume); //"Tools > Statistics" menu connect(m_UI->actionComputeStatParams2, &QAction::triggered, this, &MainWindow::doActionComputeStatParams); //duplicated action --> we can't use the same otherwise we get an ugly console warning on Linux :( connect(m_UI->actionStatisticalTest, &QAction::triggered, this, &MainWindow::doActionStatisticalTest); //"Tools > Segmentation" menu connect(m_UI->actionLabelConnectedComponents, &QAction::triggered, this, &MainWindow::doActionLabelConnectedComponents); connect(m_UI->actionKMeans, &QAction::triggered, this, &MainWindow::doActionKMeans); connect(m_UI->actionFrontPropagation, &QAction::triggered, this, &MainWindow::doActionFrontPropagation); connect(m_UI->actionCrossSection, &QAction::triggered, this, &MainWindow::activateClippingBoxMode); connect(m_UI->actionExtractSections, &QAction::triggered, this, &MainWindow::activateSectionExtractionMode); //"Tools > Fit" menu connect(m_UI->actionFitPlane, &QAction::triggered, this, &MainWindow::doActionFitPlane); connect(m_UI->actionFitSphere, &QAction::triggered, this, &MainWindow::doActionFitSphere); connect(m_UI->actionFitCircle, &QAction::triggered, this, &MainWindow::doActionFitCircle); connect(m_UI->actionFitFacet, &QAction::triggered, this, &MainWindow::doActionFitFacet); connect(m_UI->actionFitQuadric, &QAction::triggered, this, &MainWindow::doActionFitQuadric); //"Tools > Batch export" menu connect(m_UI->actionExportCloudInfo, &QAction::triggered, this, &MainWindow::doActionExportCloudInfo); connect(m_UI->actionExportPlaneInfo, &QAction::triggered, this, &MainWindow::doActionExportPlaneInfo); //"Tools > Other" menu connect(m_UI->actionComputeGeometricFeature, &QAction::triggered, this, &MainWindow::doComputeGeometricFeature); connect(m_UI->actionRemoveDuplicatePoints, &QAction::triggered, this, &MainWindow::doRemoveDuplicatePoints); //"Tools" connect(m_UI->actionLevel, &QAction::triggered, this, &MainWindow::doLevel); connect(m_UI->actionPointListPicking, &QAction::triggered, this, &MainWindow::activatePointListPickingMode); connect(m_UI->actionPointPicking, &QAction::triggered, this, &MainWindow::activatePointPickingMode); //"Tools > Sand box (research)" menu connect(m_UI->actionComputeKdTree, &QAction::triggered, this, &MainWindow::doActionComputeKdTree); connect(m_UI->actionDistanceMap, &QAction::triggered, this, &MainWindow::doActionComputeDistanceMap); connect(m_UI->actionDistanceToBestFitQuadric3D, &QAction::triggered, this, &MainWindow::doActionComputeDistToBestFitQuadric3D); connect(m_UI->actionComputeBestFitBB, &QAction::triggered, this, &MainWindow::doComputeBestFitBB); connect(m_UI->actionAlign, &QAction::triggered, this, &MainWindow::doAction4pcsRegister); //Aurelien BEY le 13/11/2008 connect(m_UI->actionSNETest, &QAction::triggered, this, &MainWindow::doSphericalNeighbourhoodExtractionTest); connect(m_UI->actionCNETest, &QAction::triggered, this, &MainWindow::doCylindricalNeighbourhoodExtractionTest); connect(m_UI->actionFindBiggestInnerRectangle, &QAction::triggered, this, &MainWindow::doActionFindBiggestInnerRectangle); connect(m_UI->actionCreateCloudFromEntCenters, &QAction::triggered, this, &MainWindow::doActionCreateCloudFromEntCenters); connect(m_UI->actionComputeBestICPRmsMatrix, &QAction::triggered, this, &MainWindow::doActionComputeBestICPRmsMatrix); //"Display" menu connect(m_UI->actionFullScreen, &QAction::toggled, this, &MainWindow::toggleFullScreen); connect(m_UI->actionExclusiveFullScreen, &QAction::toggled, this, &MainWindow::toggleExclusiveFullScreen); connect(m_UI->actionRefresh, &QAction::triggered, this, &MainWindow::refreshAll); connect(m_UI->actionTestFrameRate, &QAction::triggered, this, &MainWindow::testFrameRate); connect(m_UI->actionToggleCenteredPerspective, &QAction::triggered, this, &MainWindow::toggleActiveWindowCenteredPerspective); connect(m_UI->actionToggleViewerBasedPerspective, &QAction::triggered, this, &MainWindow::toggleActiveWindowViewerBasedPerspective); connect(m_UI->actionShowCursor3DCoordinates, &QAction::toggled, this, &MainWindow::toggleActiveWindowShowCursorCoords); connect(m_UI->actionLockRotationAxis, &QAction::triggered, this, &MainWindow::toggleLockRotationAxis); connect(m_UI->actionEnterBubbleViewMode, &QAction::triggered, this, &MainWindow::doActionEnableBubbleViewMode); connect(m_UI->actionEditCamera, &QAction::triggered, this, &MainWindow::doActionEditCamera); connect(m_UI->actionAdjustZoom, &QAction::triggered, this, &MainWindow::doActionAdjustZoom); connect(m_UI->actionSaveViewportAsObject, &QAction::triggered, this, &MainWindow::doActionSaveViewportAsCamera); //"Display > Lights & Materials" menu connect(m_UI->actionDisplaySettings, &QAction::triggered, this, &MainWindow::showDisplaySettings); connect(m_UI->actionToggleSunLight, &QAction::triggered, this, &MainWindow::toggleActiveWindowSunLight); connect(m_UI->actionToggleCustomLight, &QAction::triggered, this, &MainWindow::toggleActiveWindowCustomLight); connect(m_UI->actionRenderToFile, &QAction::triggered, this, &MainWindow::doActionRenderToFile); //"Display > Shaders & filters" menu connect(m_UI->actionLoadShader, &QAction::triggered, this, &MainWindow::doActionLoadShader); connect(m_UI->actionDeleteShader, &QAction::triggered, this, &MainWindow::doActionDeleteShader); //"Display > Active SF" menu connect(m_UI->actionToggleActiveSFColorScale, &QAction::triggered, this, &MainWindow::doActionToggleActiveSFColorScale); connect(m_UI->actionShowActiveSFPrevious, &QAction::triggered, this, &MainWindow::doActionShowActiveSFPrevious); connect(m_UI->actionShowActiveSFNext, &QAction::triggered, this, &MainWindow::doActionShowActiveSFNext); //"Display" menu connect(m_UI->actionResetGUIElementsPos, &QAction::triggered, this, &MainWindow::doActionResetGUIElementsPos); connect(m_UI->actionRestoreWindowOnStartup, &QAction::toggled, this, &MainWindow::doActionToggleRestoreWindowOnStartup); connect(m_UI->actionResetAllVBOs, &QAction::triggered, this, &MainWindow::doActionResetAllVBOs); //"3D Views" menu connect(m_UI->menu3DViews, &QMenu::aboutToShow, this, &MainWindow::update3DViewsMenu); connect(m_UI->actionNew3DView, &QAction::triggered, this, &MainWindow::new3DView); connect(m_UI->actionZoomIn, &QAction::triggered, this, &MainWindow::zoomIn); connect(m_UI->actionZoomOut, &QAction::triggered, this, &MainWindow::zoomOut); connect(m_UI->actionClose3DView, &QAction::triggered, m_mdiArea, &QMdiArea::closeActiveSubWindow); connect(m_UI->actionCloseAll3DViews, &QAction::triggered, m_mdiArea, &QMdiArea::closeAllSubWindows); connect(m_UI->actionTile3DViews, &QAction::triggered, m_mdiArea, &QMdiArea::tileSubWindows); connect(m_UI->actionCascade3DViews, &QAction::triggered, m_mdiArea, &QMdiArea::cascadeSubWindows); connect(m_UI->actionNext3DView, &QAction::triggered, m_mdiArea, &QMdiArea::activateNextSubWindow); connect(m_UI->actionPrevious3DView, &QAction::triggered, m_mdiArea, &QMdiArea::activatePreviousSubWindow); //"About" menu entry connect(m_UI->actionHelp, &QAction::triggered, this, &MainWindow::doActionShowHelpDialog); connect(m_UI->actionAboutPlugins, &QAction::triggered, m_pluginUIManager, &ccPluginUIManager::showAboutDialog); connect(m_UI->actionEnableQtWarnings, &QAction::toggled, this, &MainWindow::doEnableQtWarnings); connect(m_UI->actionAbout, &QAction::triggered, this, [this] () { ccAboutDialog* aboutDialog = new ccAboutDialog(this); aboutDialog->exec(); }); /*** Toolbars ***/ //View toolbar connect(m_UI->actionGlobalZoom, &QAction::triggered, this, &MainWindow::setGlobalZoom); connect(m_UI->actionPickRotationCenter, &QAction::triggered, this, &MainWindow::doPickRotationCenter); connect(m_UI->actionZoomAndCenter, &QAction::triggered, this, &MainWindow::zoomOnSelectedEntities); connect(m_UI->actionSetPivotAlwaysOn, &QAction::triggered, this, &MainWindow::setPivotAlwaysOn); connect(m_UI->actionSetPivotRotationOnly, &QAction::triggered, this, &MainWindow::setPivotRotationOnly); connect(m_UI->actionSetPivotOff, &QAction::triggered, this, &MainWindow::setPivotOff); connect(m_UI->actionSetOrthoView, &QAction::triggered, this, [this] () { setOrthoView( getActiveGLWindow() ); }); connect(m_UI->actionSetCenteredPerspectiveView, &QAction::triggered, this, [this] () { setCenteredPerspectiveView( getActiveGLWindow() ); }); connect(m_UI->actionSetViewerPerspectiveView, &QAction::triggered, this, [this] () { setViewerPerspectiveView( getActiveGLWindow() ); }); connect(m_UI->actionEnableStereo, &QAction::toggled, this, &MainWindow::toggleActiveWindowStereoVision); connect(m_UI->actionAutoPickRotationCenter, &QAction::toggled, this, &MainWindow::toggleActiveWindowAutoPickRotCenter); connect(m_UI->actionSetViewTop, &QAction::triggered, this, [=]() { setView( CC_TOP_VIEW ); }); connect(m_UI->actionSetViewBottom, &QAction::triggered, this, [=]() { setView( CC_BOTTOM_VIEW ); }); connect(m_UI->actionSetViewFront, &QAction::triggered, this, [=]() { setView( CC_FRONT_VIEW ); }); connect(m_UI->actionSetViewBack, &QAction::triggered, this, [=]() { setView( CC_BACK_VIEW ); }); connect(m_UI->actionSetViewLeft, &QAction::triggered, this, [=]() { setView( CC_LEFT_VIEW ); }); connect(m_UI->actionSetViewRight, &QAction::triggered, this, [=]() { setView( CC_RIGHT_VIEW ); }); connect(m_UI->actionSetViewIso1, &QAction::triggered, this, [=]() { setView( CC_ISO_VIEW_1 ); }); connect(m_UI->actionSetViewIso2, &QAction::triggered, this, [=]() { setView( CC_ISO_VIEW_2 ); }); //hidden connect(m_UI->actionEnableVisualDebugTraces, &QAction::triggered, this, &MainWindow::toggleVisualDebugTraces); } void MainWindow::doActionColorize() { doActionSetColor(true); } void MainWindow::doActionSetUniqueColor() { doActionSetColor(false); } void MainWindow::doActionSetColor(bool colorize) { if ( !ccEntityAction::setColor(m_selectedEntities, colorize, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionRGBToGreyScale() { if ( !ccEntityAction::rgbToGreyScale( m_selectedEntities ) ) return; refreshAll(); } void MainWindow::doActionSetColorGradient() { if ( !ccEntityAction::setColorGradient(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionChangeColorLevels() { ccEntityAction::changeColorLevels(m_selectedEntities, this); } void MainWindow::doActionInterpolateColors() { if ( !ccEntityAction::interpolateColors(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionInterpolateScalarFields() { if (!ccEntityAction::interpolateSFs(m_selectedEntities, this)) return; refreshAll(); updateUI(); } void MainWindow::doActionEnhanceRGBWithIntensities() { if (!ccEntityAction::enhanceRGBWithIntensities(m_selectedEntities, this)) return; refreshAll(); } void MainWindow::doActionColorFromScalars() { for (ccHObject *entity : getSelectedEntities()) { //for "real" point clouds only ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (cloud) { //create color from scalar dialogue ccColorFromScalarDlg* cfsDlg = new ccColorFromScalarDlg(this, cloud); cfsDlg->setAttribute(Qt::WA_DeleteOnClose, true); cfsDlg->show(); } } } void MainWindow::doActionInvertNormals() { if ( !ccEntityAction::invertNormals(m_selectedEntities) ) return; refreshAll(); } void MainWindow::doActionConvertNormalsToDipDir() { if ( !ccEntityAction::convertNormalsTo( m_selectedEntities, ccEntityAction::NORMAL_CONVERSION_DEST::DIP_DIR_SFS) ) { return; } refreshAll(); updateUI(); } void MainWindow::doActionConvertNormalsToHSV() { if ( !ccEntityAction::convertNormalsTo( m_selectedEntities, ccEntityAction::NORMAL_CONVERSION_DEST::HSV_COLORS) ) { return; } refreshAll(); updateUI(); } static double s_kdTreeMaxErrorPerCell = 0.1; void MainWindow::doActionComputeKdTree() { ccGenericPointCloud* cloud = nullptr; if ( haveOneSelection() ) { ccHObject* ent = m_selectedEntities.front(); bool lockedVertices; cloud = ccHObjectCaster::ToGenericPointCloud(ent, &lockedVertices); if (lockedVertices) { ccUtils::DisplayLockedVerticesWarning(ent->getName(), true); return; } } if (!cloud) { ccLog::Error(tr("Selected one and only one point cloud or mesh!")); return; } bool ok; s_kdTreeMaxErrorPerCell = QInputDialog::getDouble(this, tr("Compute Kd-tree"), tr("Max error per leaf cell:"), s_kdTreeMaxErrorPerCell, 1.0e-6, 1.0e6, 6, &ok); if (!ok) return; ccProgressDialog pDlg(true, this); //computation QElapsedTimer eTimer; eTimer.start(); ccKdTree* kdtree = new ccKdTree(cloud); if (kdtree->build(s_kdTreeMaxErrorPerCell, CCCoreLib::DistanceComputationTools::MAX_DIST_95_PERCENT, 4, 1000, &pDlg)) { qint64 elapsedTime_ms = eTimer.elapsed(); ccConsole::Print("[doActionComputeKdTree] Timing: %2.3f s", elapsedTime_ms / 1.0e3); cloud->setEnabled(true); //for mesh vertices! cloud->addChild(kdtree); kdtree->setDisplay(cloud->getDisplay()); kdtree->setVisible(true); kdtree->prepareDisplayForRefresh(); #ifdef QT_DEBUG kdtree->convertCellIndexToSF(); #else kdtree->convertCellIndexToRandomColor(); #endif addToDB(kdtree); refreshAll(); updateUI(); } else { ccLog::Error(tr("An error occurred")); delete kdtree; kdtree = nullptr; } } void MainWindow::doActionComputeOctree() { if ( !ccEntityAction::computeOctree(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionResampleWithOctree() { bool ok; int pointCount = QInputDialog::getInt(this, tr("Resample with octree"), tr("Points (approx.)"), 1000000, 1, INT_MAX, 100000, &ok); if (!ok) return; ccProgressDialog pDlg(false, this); pDlg.setAutoClose(false); assert(pointCount > 0); unsigned aimedPoints = static_cast(pointCount); bool errors = false; for ( ccHObject *entity : getSelectedEntities() ) { ccPointCloud* cloud = nullptr; /*if (ent->isKindOf(CC_TYPES::MESH)) //TODO cloud = ccHObjectCaster::ToGenericMesh(ent)->getAssociatedCloud(); else */ if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { cloud = static_cast(entity); } if (cloud) { ccOctree::Shared octree = cloud->getOctree(); if (!octree) { octree = cloud->computeOctree(&pDlg); if (!octree) { ccConsole::Error(tr("Could not compute octree for cloud '%1'").arg(cloud->getName())); continue; } } cloud->setEnabled(false); QElapsedTimer eTimer; eTimer.start(); CCCoreLib::GenericIndexedCloud* result = CCCoreLib::CloudSamplingTools::resampleCloudWithOctree ( cloud, aimedPoints, CCCoreLib::CloudSamplingTools::CELL_GRAVITY_CENTER, &pDlg, octree.data() ); if (result) { ccConsole::Print("[ResampleWithOctree] Timing: %3.2f s.", eTimer.elapsed() / 1.0e3); ccPointCloud* newCloud = ccPointCloud::From(result, cloud); delete result; result = nullptr; if (newCloud) { addToDB(newCloud); newCloud->setDisplay(cloud->getDisplay()); newCloud->prepareDisplayForRefresh(); } else { errors = true; } } } } if (errors) ccLog::Error(tr("[ResampleWithOctree] Errors occurred during the process, result may be incomplete")); refreshAll(); } void MainWindow::doActionApplyTransformation() { ccApplyTransformationDlg dlg(this); if (!dlg.exec()) return; bool applyToGlobal = false; ccGLMatrixd transMat = dlg.getTransformation(applyToGlobal); applyTransformation(transMat, applyToGlobal); } ccHObject::Container MainWindow::getTopLevelSelectedEntities() const { ccHObject::Container topLevelSelectedEntities; for (size_t i = 0; i < m_selectedEntities.size(); ++i) { ccHObject* entity = m_selectedEntities[i]; bool hasParentsInselection = false; for (size_t j = 0; j < m_selectedEntities.size(); ++j) { if (i == j) continue; ccHObject* otherEntity = m_selectedEntities[j]; if (otherEntity->isAncestorOf(entity)) { hasParentsInselection = true; break; } } if (!hasParentsInselection) { topLevelSelectedEntities.push_back(entity); } } return topLevelSelectedEntities; } void MainWindow::applyTransformation(const ccGLMatrixd& mat, bool applyToGlobal) { //if the transformation is partly converted to global shift/scale bool autoApplyPreviousGlobalShiftAndScale = false; double previousScale = 1.0; CCVector3d previousShift(0, 0, 0); //we don't want any entity that would be the children of other selected entities ccHObject::Container selectedEntities = getTopLevelSelectedEntities(); for (ccHObject* entity : selectedEntities) //warning, getSelectedEntites may change during this loop! { ccGLMatrixd transMat = mat; if (applyToGlobal && nullptr != dynamic_cast(entity)) { // the user wants to apply the transformation to the global coordinates ccShiftedObject* shiftedEntity = static_cast(entity); CCVector3d globalShift = shiftedEntity->getGlobalShift(); double globalScale = shiftedEntity->getGlobalScale(); // we compute the impact to the local coordinate system without changing the // current Global Shift & Scale parameters (for now) // Here is the formula, assuming: // - the Global Shift is Ts // - the Global scale is Sc // - the transformation is (R, T) // - the Global point coordinates Pg are derived from the local ones Pl with: Pg = Pl/Sc - Ts // Therefore, Pg' = R * Pg + T // i.e. Pg' = R.(Pl/Sc - Ts) + T // i.e. Pg' = (R.Pl)/Sc - R.Ts + T // i.e. Pg' = (R.Pl)/Sc - R.Ts + T + Ts - Ts // i.e. Pg' = (R.Pl + Sc.[Ts - R.Ts + T])/Sc - Ts // i.e. Pl' = Sc.[Ts -R.Ts + T] // i.e. the translation of the 'local' coordinate system is: Sc.[T + Ts - R.Ts] CCVector3d rotatedGlobalShift = globalShift; mat.applyRotation(rotatedGlobalShift); CCVector3d localTranslation = globalScale * (globalShift - rotatedGlobalShift + mat.getTranslationAsVec3D()); // we switch to a local transformation matrix transMat.setTranslation(localTranslation); } //we don't test primitives (it's always ok while the 'vertices lock' test would fail) if (!entity->isKindOf(CC_TYPES::PRIMITIVE)) { //specific test for locked vertices bool lockedVertices; ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity, &lockedVertices); if (cloud) { if (lockedVertices) { ccUtils::DisplayLockedVerticesWarning(entity->getName(), haveOneSelection()); continue; } //test if the translated cloud coordinates were already "too large" //(in which case we won't bother the user about the fact that the transformed cloud coordinates will be too large...) ccBBox localBBox = entity->getOwnBB(); CCVector3d Pl = localBBox.minCorner(); double Dl = localBBox.getDiagNormd(); //if the cloud is alright if ( !ccGlobalShiftManager::NeedShift(Pl) && !ccGlobalShiftManager::NeedRescale(Dl) ) { //test if the translated cloud (local) coordinates are too large ccBBox transformedLocalBox = entity->getOwnBB() * transMat; CCVector3d transformedPl = transformedLocalBox.minCorner(); double transformedDl = transformedLocalBox.getDiagNormd(); bool needShift = ccGlobalShiftManager::NeedShift(transformedPl) || ccGlobalShiftManager::NeedRescale(transformedDl); if (needShift) { //existing shift information CCVector3d globalShift = cloud->getGlobalShift(); double globalScale = cloud->getGlobalScale(); //we compute the global coordinates and scale of the reference point (= the min corner of the bounding-box) CCVector3d Pg = cloud->toGlobal3d(transformedPl); double Dg = transformedDl / globalScale; //let's try to find better Global Shift and Scale values CCVector3d newShift(0.0, 0.0, 0.0); double newScale = 1.0; bool updateShiftAndscale = false; //should we try to use the previous Global Shift and Scale values? if (autoApplyPreviousGlobalShiftAndScale) { if ( !ccGlobalShiftManager::NeedShift(Pg + previousShift) && !ccGlobalShiftManager::NeedRescale(Dg * previousScale)) { newScale = previousScale; newShift = previousShift; needShift = false; updateShiftAndscale = true; } } //if we still need to define new Global Shift and Scale if (needShift) { //ask the user the right values! ccShiftAndScaleCloudDlg sasDlg(transformedPl, transformedDl, Pg, Dg, this); sasDlg.showApplyAllButton(selectedEntities.size() > 1); sasDlg.showTitle(true); sasDlg.setKeepGlobalPos(true); sasDlg.showKeepGlobalPosCheckbox(false); //we don't want the user to mess with this! sasDlg.showPreserveShiftOnSave(true); //add "original" entry int index = sasDlg.addShiftInfo(ccGlobalShiftManager::ShiftInfo(tr("Original"), globalShift, globalScale)); //add "previous" entry (if any) if (autoApplyPreviousGlobalShiftAndScale) { index = sasDlg.addShiftInfo(ccGlobalShiftManager::ShiftInfo(tr("Previous"), previousShift, previousScale)); } //add "last" entries (if any) int matchingIndex = -1; const auto& previousEntries = ccGlobalShiftManager::GetLast(); for (const ccGlobalShiftManager::ShiftInfo& shiftInfo : previousEntries) { index = sasDlg.addShiftInfo(shiftInfo); if (matchingIndex < 0) { if ( !ccGlobalShiftManager::NeedShift(Pg + shiftInfo.shift) && !ccGlobalShiftManager::NeedRescale(Dg * shiftInfo.scale) ) { matchingIndex = index; } } } //if no good solution was found... if (matchingIndex < 0) { //add a "suggested" entry CCVector3d suggestedShift = ccGlobalShiftManager::BestShift(Pg); double suggestedScale = ccGlobalShiftManager::BestScale(Dg); matchingIndex = sasDlg.addShiftInfo(ccGlobalShiftManager::ShiftInfo(tr("Suggested"), suggestedShift, suggestedScale)); } sasDlg.setCurrentProfile(matchingIndex); if (sasDlg.exec()) { newScale = sasDlg.getScale(); newShift = sasDlg.getShift(); needShift = false; updateShiftAndscale = true; //store the shift for next time! ccGlobalShiftManager::StoreShift(newShift, newScale); if (sasDlg.applyAll()) { autoApplyPreviousGlobalShiftAndScale = true; previousScale = newScale; previousShift = newShift; } } else if (sasDlg.cancelled()) { ccLog::Warning(tr("[ApplyTransformation] Process cancelled by user")); return; } else { // the user did not want to change the shift & scale } } if (updateShiftAndscale) { assert(!needShift); //get the relative modification to existing global shift/scale info assert(globalScale != 0); double scaleChange = newScale / globalScale; CCVector3d shiftChange = newShift - globalShift; if (scaleChange != 1.0 || shiftChange.norm2() != 0) { //apply translation as global shift cloud->setGlobalShift(newShift); cloud->setGlobalScale(newScale); ccLog::Warning(tr("[ApplyTransformation] Cloud '%1' global shift/scale information has been updated: shift = (%2,%3,%4) / scale = %5").arg(cloud->getName()).arg(newShift.x).arg(newShift.y).arg(newShift.z).arg(newScale)); transMat.scaleRotation(scaleChange); transMat.setTranslation(transMat.getTranslationAsVec3D() + newScale * shiftChange); } } } } } } //we temporarily detach the entity, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccHObject::applyRigidTransformation ccHObjectContext objContext = removeObjectTemporarilyFromDBTree(entity); entity->setGLTransformation(ccGLMatrix(transMat.data())); //DGM FIXME: we only test the entity own bounding box (and we update its shift & scale info) but we apply the transformation to all its children?! entity->applyGLTransformation_recursive(); entity->prepareDisplayForRefresh_recursive(); putObjectBackIntoDBTree(entity, objContext); if (applyToGlobal) { ccLog::Print(tr("[ApplyTransformation] Transformation matrix applied to the local coordinates of %1:").arg(entity->getName())); ccLog::Print(transMat.toString(12, ' ')); //full precision } } if (!applyToGlobal) { ccLog::Print(tr("[ApplyTransformation] Applied transformation matrix:")); } else { ccLog::Print(tr("[ApplyTransformation] Global transformation matrix:")); } ccLog::Print(mat.toString(12, ' ')); //full precision ccLog::Print(tr("Hint: you can copy a transformation matrix (CTRL+C) and apply it - or its inverse - to another entity with the 'Edit > Apply transformation' tool")); //reselect previously selected entities! if (m_ccRoot) m_ccRoot->selectEntities(selectedEntities); zoomOnSelectedEntities(); refreshAll(); } void MainWindow::doActionApplyScale() { ccScaleDlg dlg(this); if (!dlg.exec()) return; dlg.saveState(); //save values for next time CCVector3d scales = dlg.getScales(); bool keepInPlace = dlg.keepInPlace(); bool rescaleGlobalShift = dlg.rescaleGlobalShift(); //we must backup 'm_selectedEntities' as removeObjectTemporarilyFromDBTree can modify it! ccHObject::Container selectedEntities = m_selectedEntities; //first check that all coordinates are kept 'small' std::vector< std::pair > candidates; { bool testBigCoordinates = true; for (ccHObject *entity : selectedEntities) //warning, getSelectedEntites may change during this loop! { bool lockedVertices; //try to get the underlying cloud (or the vertices set for a mesh) ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity, &lockedVertices); //otherwise we can look if the selected entity is a polyline if (!cloud && entity->isA(CC_TYPES::POLY_LINE)) { cloud = dynamic_cast(static_cast(entity)->getAssociatedCloud()); if (!cloud || cloud->isAncestorOf(entity)) { lockedVertices = true; } } if (!cloud || !cloud->isKindOf(CC_TYPES::POINT_CLOUD)) { ccLog::Warning(tr("[Apply scale] Entity '%1' can't be scaled this way").arg(entity->getName())); continue; } if (lockedVertices) { ccUtils::DisplayLockedVerticesWarning(entity->getName(), haveOneSelection()); continue; } CCVector3 C(0, 0, 0); if (keepInPlace) { C = cloud->getOwnBB().getCenter(); } //we must check that the resulting cloud coordinates are not too big if (testBigCoordinates) { ccBBox bbox = cloud->getOwnBB(); CCVector3 bbMin = bbox.minCorner(); CCVector3 bbMax = bbox.maxCorner(); double maxx = std::max(std::abs(bbMin.x), std::abs(bbMax.x)); double maxy = std::max(std::abs(bbMin.y), std::abs(bbMax.y)); double maxz = std::max(std::abs(bbMin.z), std::abs(bbMax.z)); const double maxCoord = ccGlobalShiftManager::MaxCoordinateAbsValue(); bool oldCoordsWereTooBig = ( maxx > maxCoord || maxy > maxCoord || maxz > maxCoord ); if (!oldCoordsWereTooBig) { maxx = std::max(std::abs((bbMin.x - C.x) * scales.x + C.x), std::abs((bbMax.x - C.x) * scales.x + C.x)); maxy = std::max(std::abs((bbMin.y - C.y) * scales.y + C.y), std::abs((bbMax.y - C.y) * scales.y + C.y)); maxz = std::max(std::abs((bbMin.z - C.z) * scales.z + C.z), std::abs((bbMax.z - C.z) * scales.z + C.z)); bool newCoordsAreTooBig = ( maxx > maxCoord || maxy > maxCoord || maxz > maxCoord ); if (newCoordsAreTooBig) { if (QMessageBox::question( this, tr("Big coordinates"), tr("Resutling coordinates will be too big (original precision may be lost!). Proceed anyway?"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { //ok, we won't test anymore and proceed testBigCoordinates = false; } else { //we stop the process return; } } } } assert(cloud); candidates.emplace_back(entity, cloud); } } if (candidates.empty()) { ccConsole::Warning(tr("[Apply scale] No eligible entities (point clouds or meshes) were selected!")); return; } //now do the real scaling work { for ( auto& candidate : candidates ) { ccHObject* ent = candidate.first; ccGenericPointCloud* cloud = candidate.second; CCVector3 C(0, 0, 0); if (keepInPlace) { C = cloud->getOwnBB().getCenter(); } //we temporarily detach the entity, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccPointCloud::scale ccHObjectContext objContext = removeObjectTemporarilyFromDBTree(cloud); cloud->scale( static_cast(scales.x), static_cast(scales.y), static_cast(scales.z), C ); putObjectBackIntoDBTree(cloud, objContext); cloud->prepareDisplayForRefresh_recursive(); //don't forget the 'global shift'! //DGM: but not the global scale! if (rescaleGlobalShift) { const CCVector3d& shift = cloud->getGlobalShift(); cloud->setGlobalShift( CCVector3d( shift.x*scales.x, shift.y*scales.y, shift.z*scales.z) ); } //specific case for polyline vertices if (cloud->getParent() && cloud->getParent()->isA(CC_TYPES::POLY_LINE)) { ccPolyline* poly = static_cast(cloud->getParent()); if (poly->getAssociatedCloud() == cloud) { poly->invalidateBoundingBox(); } } ent->prepareDisplayForRefresh_recursive(); } } //reselect previously selected entities! if (m_ccRoot) m_ccRoot->selectEntities(selectedEntities); if (!keepInPlace) zoomOnSelectedEntities(); refreshAll(); updateUI(); } void MainWindow::doActionEditGlobalShiftAndScale() { //get the global shift/scale info and bounding box of all selected clouds std::vector< std::pair > shiftedEntities; CCVector3d Pl(0, 0, 0); double Dl = 1.0; CCVector3d Pg(0, 0, 0); double Dg = 1.0; //shift and scale (if unique) CCVector3d shift(0, 0, 0); double scale = 1.0; { bool uniqueShift = true; bool uniqueScale = true; ccBBox localBB; //sadly we don't have a double-typed bounding box class yet ;) CCVector3d globalBBmin(0, 0, 0); CCVector3d globalBBmax(0, 0, 0); for ( ccHObject *entity : getSelectedEntities() ) { bool lockedVertices; ccShiftedObject* shifted = ccHObjectCaster::ToShifted(entity, &lockedVertices); if (!shifted) { continue; } //for (unlocked) entities only if (lockedVertices) { //get the vertices ccGenericPointCloud* vertices = nullptr; //if it's a mesh if (entity->isKindOf(CC_TYPES::MESH)) { vertices = static_cast(entity)->getAssociatedCloud(); } else if (entity->isKindOf(CC_TYPES::POLY_LINE)) { vertices = dynamic_cast(static_cast(entity)->getAssociatedCloud()); } if (!vertices || !entity->isAncestorOf(vertices)) { ccUtils::DisplayLockedVerticesWarning(entity->getName(), haveOneSelection()); continue; } entity = vertices; } CCVector3 Al = entity->getOwnBB().minCorner(); CCVector3 Bl = entity->getOwnBB().maxCorner(); CCVector3d Ag = shifted->toGlobal3d(Al); CCVector3d Bg = shifted->toGlobal3d(Bl); //update local BB localBB.add(Al); localBB.add(Bl); //update global BB if (shiftedEntities.empty()) { globalBBmin = Ag; globalBBmax = Bg; shift = shifted->getGlobalShift(); uniqueScale = shifted->getGlobalScale(); } else { globalBBmin = CCVector3d( std::min(globalBBmin.x, Ag.x), std::min(globalBBmin.y, Ag.y), std::min(globalBBmin.z, Ag.z) ); globalBBmax = CCVector3d( std::max(globalBBmax.x, Bg.x), std::max(globalBBmax.y, Bg.y), std::max(globalBBmax.z, Bg.z) ); if (uniqueShift) { uniqueShift = CCCoreLib::LessThanEpsilon((shifted->getGlobalShift() - shift).norm()); } if (uniqueScale) { uniqueScale = CCCoreLib::LessThanEpsilon(std::abs(shifted->getGlobalScale() - scale)); } } shiftedEntities.emplace_back(shifted, entity); } Pg = globalBBmin; Dg = (globalBBmax - globalBBmin).norm(); Pl = localBB.minCorner(); Dl = (localBB.maxCorner() - localBB.minCorner()).normd(); if (!uniqueShift) shift = Pl - Pg; if (!uniqueScale) scale = Dg / Dl; } if (shiftedEntities.empty()) { return; } ccShiftAndScaleCloudDlg sasDlg(Pl, Dl, Pg, Dg, this); sasDlg.showApplyAllButton(shiftedEntities.size() > 1); sasDlg.showApplyButton(shiftedEntities.size() == 1); sasDlg.showNoButton(false); sasDlg.setShiftFieldsPrecision(6); //add "original" entry int index = sasDlg.addShiftInfo(ccGlobalShiftManager::ShiftInfo(tr("Original"), shift, scale)); sasDlg.setCurrentProfile(index); //add "last" entries (if any) sasDlg.addShiftInfo(ccGlobalShiftManager::GetLast()); if (!sasDlg.exec()) return; shift = sasDlg.getShift(); scale = sasDlg.getScale(); bool preserveGlobalPos = sasDlg.keepGlobalPos(); ccLog::Print(tr("[Global Shift/Scale] New shift: (%1, %2, %3)").arg(shift.x).arg(shift.y).arg(shift.z)); ccLog::Print(tr("[Global Shift/Scale] New scale: %1").arg(scale)); //apply new shift { for ( auto &entity : shiftedEntities ) { ccShiftedObject* shifted = entity.first; ccHObject* ent = entity.second; if (preserveGlobalPos) { //to preserve the global position of the cloud, we may have to translate and/or rescale the cloud CCVector3d Ql = ent->getOwnBB().minCorner(); CCVector3d Qg = shifted->toGlobal3d(Ql); CCVector3d Ql2 = Qg * scale + shift; CCVector3d T = Ql2 - Ql; assert(shifted->getGlobalScale() > 0); double scaleCoef = scale / shifted->getGlobalScale(); if ( CCCoreLib::GreaterThanEpsilon( T.norm() ) || CCCoreLib::GreaterThanEpsilon( std::abs(scaleCoef - 1.0) ) ) { ccGLMatrix transMat; transMat.toIdentity(); transMat.scaleRotation(static_cast(scaleCoef)); transMat.setTranslation(T); //DGM FIXME: we only test the entity own bounding box (and we update its shift & scale info) but we apply the transformation to all its children?! ent->applyGLTransformation_recursive(&transMat); ent->prepareDisplayForRefresh_recursive(); ccLog::Warning(tr("[Global Shift/Scale] To preserve its original position, the entity '%1' has been translated of (%2 ; %3 ; %4) and rescaled of a factor %5") .arg(ent->getName()) .arg(T.x) .arg(T.y) .arg(T.z) .arg(scaleCoef)); } } shifted->setGlobalShift(shift); shifted->setGlobalScale(scale); } } refreshAll(); updateUI(); } void MainWindow::doComputeBestFitBB() { if (QMessageBox::warning( this, tr("This method is for test purpose only"), tr("Cloud(s) are going to be rotated while still displayed in their previous position! Proceed?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) != QMessageBox::Yes) { return; } //backup selected entities as removeObjectTemporarilyFromDBTree can modify them ccHObject::Container selectedEntities = getSelectedEntities(); for (ccHObject *entity : selectedEntities) //warning, getSelectedEntites may change during this loop! { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity); if (cloud && cloud->isA(CC_TYPES::POINT_CLOUD)) // TODO { CCCoreLib::Neighbourhood Yk(cloud); CCCoreLib::SquareMatrixd covMat = Yk.computeCovarianceMatrix(); if (covMat.isValid()) { CCCoreLib::SquareMatrixd eigVectors; std::vector eigValues; if (CCCoreLib::Jacobi::ComputeEigenValuesAndVectors(covMat, eigVectors, eigValues, true)) { CCCoreLib::Jacobi::SortEigenValuesAndVectors(eigVectors, eigValues); ccGLMatrix trans; GLfloat* rotMat = trans.data(); for (unsigned j = 0; j < 3; ++j) { double u[3]; CCCoreLib::Jacobi::GetEigenVector(eigVectors, j, u); CCVector3 v(static_cast(u[0]), static_cast(u[1]), static_cast(u[2])); v.normalize(); rotMat[j*4] = static_cast(v.x); rotMat[j*4+1] = static_cast(v.y); rotMat[j*4+2] = static_cast(v.z); } const CCVector3* G = Yk.getGravityCenter(); assert(G); trans.shiftRotationCenter(*G); cloud->setGLTransformation(trans); trans.invert(); //we temporarily detach the entity, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccPointCloud::applyRigidTransformation ccHObjectContext objContext = removeObjectTemporarilyFromDBTree(cloud); static_cast(cloud)->applyRigidTransformation(trans); putObjectBackIntoDBTree(cloud,objContext); entity->prepareDisplayForRefresh_recursive(); } } } } refreshAll(); } void MainWindow::doActionFlagMeshVertices() { bool errors = false; bool success = false; for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::MESH)) { ccGenericMesh* mesh = ccHObjectCaster::ToGenericMesh(entity); ccPointCloud* vertices = ccHObjectCaster::ToPointCloud(mesh ? mesh->getAssociatedCloud() : nullptr); if (mesh && vertices) { //prepare a new scalar field int sfIdx = vertices->getScalarFieldIndexByName(CC_DEFAULT_MESH_VERT_FLAGS_SF_NAME); if (sfIdx < 0) { sfIdx = vertices->addScalarField(CC_DEFAULT_MESH_VERT_FLAGS_SF_NAME); if (sfIdx < 0) { ccConsole::Warning(tr("Not enough memory to flag the vertices of mesh '%1'!").arg(mesh->getName())); errors = true; continue; } } CCCoreLib::ScalarField* flags = vertices->getScalarField(sfIdx); CCCoreLib::MeshSamplingTools::EdgeConnectivityStats stats; if (CCCoreLib::MeshSamplingTools::flagMeshVerticesByType(mesh,flags,&stats)) { vertices->setCurrentDisplayedScalarField(sfIdx); ccScalarField* sf = vertices->getCurrentDisplayedScalarField(); if (sf) { sf->setColorScale(ccColorScalesManager::GetDefaultScale(ccColorScalesManager::VERTEX_QUALITY)); //sf->setColorRampSteps(3); //ugly :( } vertices->showSF(true); mesh->showSF(true); mesh->prepareDisplayForRefresh_recursive(); success = true; //display stats in the Console as well ccConsole::Print(tr("[Mesh Quality] Mesh '%1' edges: %2 total (normal: %3 / on hole borders: %4 / non-manifold: %5)").arg(entity->getName()).arg(stats.edgesCount).arg(stats.edgesSharedByTwo).arg(stats.edgesNotShared).arg(stats.edgesSharedByMore)); } else { vertices->deleteScalarField(sfIdx); sfIdx = -1; ccConsole::Warning(tr("Not enough memory to flag the vertices of mesh '%1'!").arg(mesh->getName())); errors = true; } } else { assert(false); } } } refreshAll(); updateUI(); if (success) { //display reminder forceConsoleDisplay(); ccConsole::Print(tr("[Mesh Quality] SF flags: %1 (NORMAL) / %2 (BORDER) / (%3) NON-MANIFOLD").arg(CCCoreLib::MeshSamplingTools::VERTEX_NORMAL).arg(CCCoreLib::MeshSamplingTools::VERTEX_BORDER).arg(CCCoreLib::MeshSamplingTools::VERTEX_NON_MANIFOLD)); } if (errors) { ccConsole::Error(tr("Error(s) occurred! Check the console...")); } } void MainWindow::doActionMeasureMeshVolume() { for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::MESH)) { ccMesh* mesh = ccHObjectCaster::ToMesh(entity); if (mesh) { //we compute the mesh volume double V = CCCoreLib::MeshSamplingTools::computeMeshVolume(mesh); //we force the console to display itself forceConsoleDisplay(); ccConsole::Print(tr("[Mesh Volume] Mesh '%1': V=%2 (cube units)").arg(entity->getName()).arg(V)); //check that the mesh is closed CCCoreLib::MeshSamplingTools::EdgeConnectivityStats stats; if (CCCoreLib::MeshSamplingTools::computeMeshEdgesConnectivity(mesh, stats)) { if (stats.edgesNotShared != 0) { ccConsole::Warning(tr("[Mesh Volume] The above volume might be invalid (mesh has holes)")); } else if (stats.edgesSharedByMore != 0) { ccConsole::Warning(tr("[Mesh Volume] The above volume might be invalid (mesh has non-manifold edges)")); } } else { ccConsole::Warning(tr("[Mesh Volume] The above volume might be invalid (not enough memory to check if the mesh is closed)")); } } else { assert(false); } } } } void MainWindow::doActionMeasureMeshSurface() { for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::MESH)) { ccGenericMesh* mesh = ccHObjectCaster::ToGenericMesh(entity); if (mesh) { double S = CCCoreLib::MeshSamplingTools::computeMeshArea(mesh); //we force the console to display itself forceConsoleDisplay(); ccConsole::Print(tr("[Mesh Surface] Mesh '%1': S=%2 (square units)").arg(entity->getName()).arg(S)); if (mesh->size()) { ccConsole::Print(tr("[Mesh Surface] Average triangle surface: %1 (square units)").arg(S / double(mesh->size()))); } } else { assert(false); } } } } void MainWindow::doActionComputeDistancesFromSensor() { //we support more than just one sensor in selection if (!haveSelection()) { ccConsole::Error(tr("Select at least one sensor")); return; } //start dialog ccSensorComputeDistancesDlg cdDlg(this); if (!cdDlg.exec()) return; for ( ccHObject *entity : getSelectedEntities() ) { ccSensor* sensor = ccHObjectCaster::ToSensor( entity ); assert(sensor); if (!sensor) continue; //skip this entity //get associated cloud ccHObject* defaultCloud = sensor->getParent() && sensor->getParent()->isA(CC_TYPES::POINT_CLOUD) ? sensor->getParent() : nullptr; ccPointCloud* cloud = askUserToSelectACloud(defaultCloud, tr("Select a cloud on which to project the uncertainty:")); if (!cloud) { return; } //sensor center CCVector3 sensorCenter; if (!sensor->getActiveAbsoluteCenter(sensorCenter)) return; //squared required? bool squared = cdDlg.computeSquaredDistances(); //set up a new scalar field const char* defaultRangesSFname = squared ? CC_DEFAULT_SQUARED_RANGES_SF_NAME : CC_DEFAULT_RANGES_SF_NAME; int sfIdx = cloud->getScalarFieldIndexByName(defaultRangesSFname); if (sfIdx < 0) { sfIdx = cloud->addScalarField(defaultRangesSFname); if (sfIdx < 0) { ccConsole::Error(tr("Not enough memory!")); return; } } CCCoreLib::ScalarField* distances = cloud->getScalarField(sfIdx); for (unsigned i = 0; i < cloud->size(); ++i) { const CCVector3* P = cloud->getPoint(i); ScalarType s = static_cast(squared ? (*P-sensorCenter).norm2() : (*P-sensorCenter).norm()); distances->setValue(i, s); } distances->computeMinAndMax(); cloud->setCurrentDisplayedScalarField(sfIdx); cloud->showSF(true); cloud->prepareDisplayForRefresh_recursive(); } refreshAll(); updateUI(); } void MainWindow::doActionComputeScatteringAngles() { //there should be only one sensor in current selection! if (!haveOneSelection() || !m_selectedEntities.front()->isKindOf(CC_TYPES::GBL_SENSOR)) { ccConsole::Error(tr("Select one and only one GBL sensor!")); return; } ccSensor* sensor = ccHObjectCaster::ToSensor(m_selectedEntities.front()); assert(sensor); //sensor center CCVector3 sensorCenter; if (!sensor->getActiveAbsoluteCenter(sensorCenter)) return; //get associated cloud ccHObject* defaultCloud = sensor->getParent() && sensor->getParent()->isA(CC_TYPES::POINT_CLOUD) ? sensor->getParent() : nullptr; ccPointCloud* cloud = askUserToSelectACloud(defaultCloud, tr("Select a cloud on which to project the uncertainty:")); if (!cloud) { return; } if (!cloud->hasNormals()) { ccConsole::Error(tr("The cloud must have normals!")); return; } ccSensorComputeScatteringAnglesDlg cdDlg(this); if (!cdDlg.exec()) return; bool toDegreeFlag = cdDlg.anglesInDegrees(); //prepare a new scalar field const char* defaultScatAnglesSFname = toDegreeFlag ? CC_DEFAULT_DEG_SCATTERING_ANGLES_SF_NAME : CC_DEFAULT_RAD_SCATTERING_ANGLES_SF_NAME; int sfIdx = cloud->getScalarFieldIndexByName(defaultScatAnglesSFname); if (sfIdx < 0) { sfIdx = cloud->addScalarField(defaultScatAnglesSFname); if (sfIdx < 0) { ccConsole::Error(tr("Not enough memory!")); return; } } CCCoreLib::ScalarField* angles = cloud->getScalarField(sfIdx); //perform computations for (unsigned i = 0; i < cloud->size(); ++i) { //the point position const CCVector3* P = cloud->getPoint(i); //build the ray CCVector3 ray = *P - sensorCenter; ray.normalize(); //get the current normal CCVector3 normal(cloud->getPointNormal(i)); //normal.normalize(); //should already be the case! //compute the angle PointCoordinateType cosTheta = ray.dot(normal); ScalarType theta = std::acos(std::min(std::abs(cosTheta), 1.0f)); if (toDegreeFlag) { theta = CCCoreLib::RadiansToDegrees( theta ); } angles->setValue(i,theta); } angles->computeMinAndMax(); cloud->setCurrentDisplayedScalarField(sfIdx); cloud->showSF(true); cloud->prepareDisplayForRefresh_recursive(); refreshAll(); updateUI(); } void MainWindow::doActionSetViewFromSensor() { //there should be only one sensor in current selection! if (!haveOneSelection() || !m_selectedEntities.front()->isKindOf(CC_TYPES::SENSOR)) { ccConsole::Error(tr("Select one and only one sensor!")); return; } ccSensor* sensor = ccHObjectCaster::ToSensor(m_selectedEntities.front()); assert(sensor); //try to find the associated window ccGenericGLDisplay* win = sensor->getDisplay(); if (!win) { //get associated cloud ccPointCloud * cloud = ccHObjectCaster::ToPointCloud(sensor->getParent()); if (cloud) win = cloud->getDisplay(); } if (sensor->applyViewport(win)) { ccConsole::Print(tr("[DoActionSetViewFromSensor] Viewport applied")); } } void MainWindow::doActionCreateGBLSensor() { ccGBLSensorProjectionDlg spDlg(this); spDlg.initWithPrevious(); if (!spDlg.exec()) return; spDlg.saveForNextTime(); //We create the corresponding sensor for each input cloud (in a perfect world, there should be only one ;) for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity); //we create a new sensor ccGBLSensor* sensor = new ccGBLSensor(); //we init its parameters with the dialog spDlg.updateGBLSensor(sensor); //we compute projection if (sensor->computeAutoParameters(cloud)) { cloud->addChild(sensor); //we try to guess the sensor relative size (dirty) ccBBox bb = cloud->getOwnBB(); double diag = bb.getDiagNorm(); if (diag < 1.0) sensor->setGraphicScale(static_cast(1.0e-3)); else if (diag > 10000.0) sensor->setGraphicScale(static_cast(1.0e3)); //we display depth buffer int errorCode; if (sensor->computeDepthBuffer(cloud,errorCode)) { ccRenderingTools::ShowDepthBuffer(sensor,this); } else { ccConsole::Error(ccGBLSensor::GetErrorString(errorCode)); } ////DGM: test //{ // //add positions // const unsigned count = 1000; // const PointCoordinateType R = 100; // const PointCoordinateType dh = 100; // for (unsigned i=0; i<1000; ++i) // { // float angle = (float)i/(float)count * 6 * M_PI; // float X = R * cos(angle); // float Y = R * sin(angle); // float Z = (float)i/(float)count * dh; // ccIndexedTransformation trans; // trans.initFromParameters(-angle,CCVector3(0,0,1),CCVector3(X,Y,Z)); // sensor->addPosition(trans,i); // } //} //set position //ccIndexedTransformation trans; //sensor->addPosition(trans,0); ccGLWindowInterface* win = static_cast(cloud->getDisplay()); if (win) { sensor->setDisplay_recursive(win); sensor->setVisible(true); ccBBox box = cloud->getOwnBB(); win->updateConstellationCenterAndZoom(&box); } addToDB(sensor); } else { ccLog::Error(tr("Failed to create sensor")); delete sensor; sensor = nullptr; } } } updateUI(); } void MainWindow::doActionCreateCameraSensor() { //we create the camera sensor ccCameraSensor* sensor = new ccCameraSensor(); ccHObject* ent = nullptr; if (haveSelection()) { assert(haveOneSelection()); ent = m_selectedEntities.front(); } //we try to guess the sensor relative size (dirty) if (ent && ent->isKindOf(CC_TYPES::POINT_CLOUD)) { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(ent); ccBBox bb = cloud->getOwnBB(); double diag = bb.getDiagNorm(); if (diag < 1.0) sensor->setGraphicScale(static_cast(1.0e-3)); else if (diag > 10000.0) sensor->setGraphicScale(static_cast(1.0e3)); //set position ccIndexedTransformation trans; sensor->addPosition(trans, 0); } ccCamSensorProjectionDlg spDlg(this); //spDlg.initWithCamSensor(sensor); //DGM: we'd better leave the default parameters of the dialog! if (!spDlg.exec()) { delete sensor; return; } spDlg.updateCamSensor(sensor); ccGLWindowInterface* win = nullptr; if (ent) { ent->addChild(sensor); win = static_cast(ent->getDisplay()); } else { win = getActiveGLWindow(); } if (win) { sensor->setDisplay(win); sensor->setVisible(true); if (ent) { ccBBox box = ent->getOwnBB(); win->updateConstellationCenterAndZoom(&box); } } addToDB(sensor); updateUI(); } void MainWindow::doActionModifySensor() { //there should be only one point cloud with sensor in current selection! if (!haveOneSelection() || !m_selectedEntities.front()->isKindOf(CC_TYPES::SENSOR)) { ccConsole::Error(tr("Select one and only one sensor!")); return; } ccSensor* sensor = static_cast(m_selectedEntities.front()); //Ground based laser sensors if (sensor->isA(CC_TYPES::GBL_SENSOR)) { ccGBLSensor* gbl = static_cast(sensor); ccGBLSensorProjectionDlg spDlg(this); spDlg.initWithGBLSensor(gbl); if (!spDlg.exec()) return; //we update its parameters spDlg.updateGBLSensor(gbl); //we re-project the associated cloud (if any) if (gbl->getParent() && gbl->getParent()->isKindOf(CC_TYPES::POINT_CLOUD)) { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(gbl->getParent()); int errorCode; if (gbl->computeDepthBuffer(cloud,errorCode)) { //we display depth buffer ccRenderingTools::ShowDepthBuffer(gbl,this); } else { ccConsole::Error(ccGBLSensor::GetErrorString(errorCode)); } } else { //ccConsole::Warning(tr("Internal error: sensor ('%1') parent is not a point cloud!").arg(sensor->getName())); } } //Camera sensors else if (sensor->isA(CC_TYPES::CAMERA_SENSOR)) { ccCameraSensor* cam = static_cast(sensor); ccCamSensorProjectionDlg spDlg(this); spDlg.initWithCamSensor(cam); if (!spDlg.exec()) return; //we update its parameters spDlg.updateCamSensor(cam); } else { ccConsole::Error(tr("Can't modify this kind of sensor!")); return; } if (sensor->isVisible() && sensor->isEnabled()) { sensor->prepareDisplayForRefresh(); refreshAll(); } updateUI(); } void MainWindow::doActionProjectUncertainty() { //there should only be one sensor in the current selection! if (!haveOneSelection() || !m_selectedEntities.front()->isKindOf(CC_TYPES::CAMERA_SENSOR)) { ccConsole::Error(tr("Select one and only one camera (projective) sensor!")); return; } ccCameraSensor* sensor = ccHObjectCaster::ToCameraSensor(m_selectedEntities.front()); if (!sensor) { assert(false); return; } const ccCameraSensor::LensDistortionParameters::Shared& distParams = sensor->getDistortionParameters(); if (!distParams || distParams->getModel() != ccCameraSensor::BROWN_DISTORTION) { ccLog::Error(tr("Sensor has no associated uncertainty model! (Brown, etc.)")); return; } //we need a cloud to project the uncertainty on! ccHObject* defaultCloud = sensor->getParent() && sensor->getParent()->isA(CC_TYPES::POINT_CLOUD) ? sensor->getParent() : nullptr; ccPointCloud* pointCloud = askUserToSelectACloud(defaultCloud, tr("Select a cloud on which to project the uncertainty:")); if (!pointCloud) { return; } CCCoreLib::ReferenceCloud points(pointCloud); if (!points.reserve(pointCloud->size())) { ccConsole::Error(tr("Not enough memory!")); return; } points.addPointIndex(0,pointCloud->size()); // compute uncertainty std::vector< Vector3Tpl > accuracy; if (!sensor->computeUncertainty(&points, accuracy/*, false*/)) { ccConsole::Error(tr("Not enough memory!")); return; } ///////////// // SIGMA D // ///////////// const char dimChar[3] = {'x','y','z'}; for (unsigned d = 0; d < 3; ++d) { // add scalar field QString sfName = tr("[%1] Uncertainty (%2)").arg(sensor->getName()).arg(dimChar[d]); int sfIdx = pointCloud->getScalarFieldIndexByName(sfName.toStdString()); if (sfIdx < 0) sfIdx = pointCloud->addScalarField(sfName.toStdString()); if (sfIdx < 0) { ccLog::Error(tr("An error occurred! (see console)")); return; } // fill scalar field CCCoreLib::ScalarField* sf = pointCloud->getScalarField(sfIdx); assert(sf); if (sf) { unsigned count = static_cast(accuracy.size()); assert(count == pointCloud->size()); for (unsigned i = 0; i < count; i++) sf->setValue(i, accuracy[i].u[d]); sf->computeMinAndMax(); } } ///////////////// // SIGMA TOTAL // ///////////////// // add scalar field { QString sfName = tr("[%1] Uncertainty (3D)").arg(sensor->getName()); int sfIdx = pointCloud->getScalarFieldIndexByName(sfName.toStdString()); if (sfIdx < 0) sfIdx = pointCloud->addScalarField(sfName.toStdString()); if (sfIdx < 0) { ccLog::Error(tr("An error occurred! (see console)")); return; } // fill scalar field CCCoreLib::ScalarField* sf = pointCloud->getScalarField(sfIdx); assert(sf); if (sf) { unsigned count = static_cast(accuracy.size()); assert(count == pointCloud->size()); for (unsigned i = 0; i < count; i++) sf->setValue(i, accuracy[i].norm()); sf->computeMinAndMax(); } pointCloud->showSF(true); pointCloud->setCurrentDisplayedScalarField(sfIdx); pointCloud->prepareDisplayForRefresh(); } refreshAll(); } void MainWindow::doActionCheckPointsInsideFrustum() { //there should be only one camera sensor in the current selection! if (!haveOneSelection() || !m_selectedEntities.front()->isKindOf(CC_TYPES::CAMERA_SENSOR)) { ccConsole::Error(tr("Select one and only one camera sensor!")); return; } ccCameraSensor* sensor = ccHObjectCaster::ToCameraSensor(m_selectedEntities.front()); if (!sensor) return; //we need a cloud to filter! ccHObject* defaultCloud = sensor->getParent() && sensor->getParent()->isA(CC_TYPES::POINT_CLOUD) ? sensor->getParent() : nullptr; ccPointCloud* pointCloud = askUserToSelectACloud(defaultCloud, tr("Select a cloud to filter:")); if (!pointCloud) { return; } //comupte/get the point cloud's octree ccOctree::Shared octree = pointCloud->getOctree(); if (!octree) { octree = pointCloud->computeOctree(); if (!octree) { ccConsole::Error(tr("Failed to compute the octree!")); return; } } assert(octree); // filter octree then project the points std::vector inCameraFrustum; if (!octree->intersectWithFrustum(sensor, inCameraFrustum)) { ccConsole::Error(tr("Failed to intersect sensor frustum with octree!")); } else { // scalar field const char sfName[] = "Frustum visibility"; int sfIdx = pointCloud->getScalarFieldIndexByName(sfName); if (inCameraFrustum.empty()) { ccConsole::Error(tr("No point fell inside the frustum!")); if (sfIdx >= 0) pointCloud->deleteScalarField(sfIdx); } else { if (sfIdx < 0) sfIdx = pointCloud->addScalarField(sfName); if (sfIdx < 0) { ccLog::Error(tr("Failed to allocate memory for output scalar field!")); return; } CCCoreLib::ScalarField* sf = pointCloud->getScalarField(sfIdx); assert(sf); if (sf) { sf->fill(0); const ScalarType c_insideValue = static_cast(1); for ( unsigned index : inCameraFrustum ) { sf->setValue(index, c_insideValue); } sf->computeMinAndMax(); pointCloud->setCurrentDisplayedScalarField(sfIdx); pointCloud->showSF(true); pointCloud->redrawDisplay(); } } } updateUI(); } void MainWindow::doActionShowDepthBuffer() { if (!haveSelection()) return; for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::GBL_SENSOR)) { ccGBLSensor* sensor = static_cast(m_selectedEntities.front()); if (sensor->getDepthBuffer().zBuff.empty()) { //look for depending cloud ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity->getParent()); if (cloud) { //force depth buffer computation int errorCode; if (!sensor->computeDepthBuffer(cloud, errorCode)) { ccConsole::Error(ccGBLSensor::GetErrorString(errorCode)); } } else { ccConsole::Error(tr("Internal error: sensor ('%1') parent is not a point cloud!").arg(sensor->getName())); return; } } ccRenderingTools::ShowDepthBuffer(sensor, this); } } } void MainWindow::doActionExportDepthBuffer() { if (!haveSelection()) return; //persistent settings QSettings settings; settings.beginGroup(ccPS::SaveFile()); QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); QString filename = QFileDialog::getSaveFileName(this, tr("Select output file"), currentPath, DepthMapFileFilter::GetFileFilter(), nullptr, CCFileDialogOptions() ); if (filename.isEmpty()) { //process cancelled by user return; } //save last saving location settings.setValue(ccPS::CurrentPath(),QFileInfo(filename).absolutePath()); settings.endGroup(); ccHObject* toSave = nullptr; bool multEntities = false; if (haveOneSelection()) { toSave = m_selectedEntities.front(); } else { toSave = new ccHObject("Temp Group"); for ( ccHObject *entity : getSelectedEntities() ) { toSave->addChild(entity,ccHObject::DP_NONE); } multEntities = true; } DepthMapFileFilter::SaveParameters parameters; { parameters.alwaysDisplaySaveDialog = true; } CC_FILE_ERROR result = DepthMapFileFilter().saveToFile(toSave, filename, parameters); if (result != CC_FERR_NO_ERROR) { FileIOFilter::DisplayErrorMessage(result, tr("saving"), filename); } else { ccLog::Print(tr("[I/O] File '%1' saved successfully").arg(filename)); } if (multEntities) { delete toSave; toSave = nullptr; } } void MainWindow::doActionComputePointsVisibility() { //there should be only one camera sensor in the current selection! if (!haveOneSelection() || !m_selectedEntities.front()->isKindOf(CC_TYPES::GBL_SENSOR)) { ccConsole::Error(tr("Select one and only one GBL/TLS sensor!")); return; } ccGBLSensor* sensor = ccHObjectCaster::ToGBLSensor(m_selectedEntities.front()); if (!sensor) return; //we need a cloud to filter! ccHObject* defaultCloud = sensor->getParent() && sensor->getParent()->isA(CC_TYPES::POINT_CLOUD) ? sensor->getParent() : nullptr; ccPointCloud* pointCloud = askUserToSelectACloud(defaultCloud, tr("Select a cloud to filter:")); if (!pointCloud) { return; } if (sensor->getDepthBuffer().zBuff.empty()) { if (defaultCloud) { //the sensor has no depth buffer, we'll ask the user if he wants to compute it first if (QMessageBox::warning( this, tr("Depth buffer"), tr("Sensor has no depth buffer: do you want to compute it now?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes ) == QMessageBox::No) { //we can stop then... return; } int errorCode; if (sensor->computeDepthBuffer(static_cast(defaultCloud), errorCode)) { ccRenderingTools::ShowDepthBuffer(sensor, this); } else { ccConsole::Error(ccGBLSensor::GetErrorString(errorCode)); return; } } else { ccConsole::Error(tr("Sensor has no depth buffer (and no associated cloud?)")); return; } } // scalar field const char sfName[] = "Sensor visibility"; int sfIdx = pointCloud->getScalarFieldIndexByName(sfName); if (sfIdx < 0) sfIdx = pointCloud->addScalarField(sfName); if (sfIdx < 0) { ccLog::Error(tr("Failed to allocate memory for output scalar field!")); return; } CCCoreLib::ScalarField* sf = pointCloud->getScalarField(sfIdx); assert(sf); if (sf) { sf->fill(0); //progress bar ccProgressDialog pdlg(true); CCCoreLib::NormalizedProgress nprogress(&pdlg,pointCloud->size()); pdlg.setMethodTitle(tr("Compute visibility")); pdlg.setInfo(tr("Points: %L1").arg( pointCloud->size() )); pdlg.start(); QApplication::processEvents(); for (unsigned i = 0; i < pointCloud->size(); i++) { const CCVector3* P = pointCloud->getPoint(i); unsigned char visibility = sensor->checkVisibility(*P); ScalarType visValue = static_cast(visibility); sf->setValue(i, visValue); if (!nprogress.oneStep()) { //cancelled by user pointCloud->deleteScalarField(sfIdx); sf = nullptr; break; } } if (sf) { sf->computeMinAndMax(); pointCloud->setCurrentDisplayedScalarField(sfIdx); pointCloud->showSF(true); ccConsole::Print(tr("Visibility computed for cloud '%1'").arg(pointCloud->getName())); ccConsole::Print(tr("\tVisible = %1").arg(CCCoreLib::POINT_VISIBLE)); ccConsole::Print(tr("\tHidden = %1").arg(CCCoreLib::POINT_HIDDEN)); ccConsole::Print(tr("\tOut of range = %1").arg(CCCoreLib::POINT_OUT_OF_RANGE)); ccConsole::Print(tr("\tOut of fov = %1").arg(CCCoreLib::POINT_OUT_OF_FOV)); } pointCloud->redrawDisplay(); } updateUI(); } void MainWindow::doActionConvertTextureToColor() { if ( !ccEntityAction::convertTextureToColor(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionSamplePointsOnMesh() { static unsigned s_ptsSamplingCount = 1000000; static double s_ptsSamplingDensity = 10.0; static bool s_ptsSampleNormals = true; static bool s_useDensity = false; ccPtsSamplingDlg dlg(this); //restore last parameters dlg.setPointsNumber(s_ptsSamplingCount); dlg.setDensityValue(s_ptsSamplingDensity); dlg.setGenerateNormals(s_ptsSampleNormals); dlg.setUseDensity(s_useDensity); if (!dlg.exec()) return; ccProgressDialog pDlg(false, this); pDlg.setAutoClose(false); bool withNormals = dlg.generateNormals(); bool withRGB = dlg.interpolateRGB(); bool withTexture = dlg.interpolateTexture(); s_useDensity = dlg.useDensity(); s_ptsSamplingCount = dlg.getPointsNumber(); s_ptsSamplingDensity = dlg.getDensityValue(); s_ptsSampleNormals = withNormals; bool errors = false; for ( ccHObject *entity : getSelectedEntities() ) { if (!entity->isKindOf(CC_TYPES::MESH)) continue; ccGenericMesh* mesh = ccHObjectCaster::ToGenericMesh(entity); assert(mesh); ccPointCloud* cloud = mesh->samplePoints( s_useDensity, s_useDensity ? s_ptsSamplingDensity : s_ptsSamplingCount, withNormals, withRGB, withTexture, &pDlg ); if (cloud) { addToDB(cloud); } else { errors = true; } } if (errors) ccLog::Error(tr("[doActionSamplePointsOnMesh] Errors occurred during the process! Result may be incomplete!")); refreshAll(); } void MainWindow::doActionSamplePointsOnPolyline() { static unsigned s_ptsSamplingCount = 1000; static double s_ptsSamplingDensity = 10.0; static bool s_useDensity = false; ccPtsSamplingDlg dlg(this); //restore last parameters dlg.setPointsNumber(s_ptsSamplingCount); dlg.setDensityValue(s_ptsSamplingDensity); dlg.setUseDensity(s_useDensity); dlg.optionsFrame->setVisible(false); if (!dlg.exec()) return; s_ptsSamplingCount = dlg.getPointsNumber(); s_ptsSamplingDensity = dlg.getDensityValue(); s_useDensity = dlg.useDensity(); bool errors = false; for (ccHObject *entity : getSelectedEntities()) { if (!entity->isKindOf(CC_TYPES::POLY_LINE)) continue; ccPolyline* poly = ccHObjectCaster::ToPolyline(entity); assert(poly); ccPointCloud* cloud = poly->samplePoints( s_useDensity, s_useDensity ? s_ptsSamplingDensity : s_ptsSamplingCount, true); if (cloud) { addToDB(cloud); } else { errors = true; } } if (errors) { ccLog::Error(tr("[DoActionSamplePointsOnPolyline] Errors occurred during the process! Result may be incomplete!")); } refreshAll(); } void MainWindow::doActionSmoohPolyline() { static int s_iterationCount = 5; static double s_ratio = 0.25; ccSmoothPolylineDialog dlg(this); //restore last parameters dlg.setIerationCount(s_iterationCount); dlg.setRatio(s_ratio); if (!dlg.exec()) return; s_iterationCount = dlg.getIerationCount(); s_ratio = dlg.getRatio(); bool errors = false; ccHObject::Container selectedEntities = getSelectedEntities(); m_ccRoot->unselectAllEntities(); for (ccHObject *entity : selectedEntities) { if (!entity->isKindOf(CC_TYPES::POLY_LINE)) continue; ccPolyline* poly = ccHObjectCaster::ToPolyline(entity); assert(poly); ccPolyline* smoothPoly = poly->smoothChaikin(s_ratio, static_cast(s_iterationCount)); if (smoothPoly) { if (poly->getParent()) { poly->getParent()->addChild(smoothPoly); } poly->setEnabled(false); addToDB(smoothPoly); m_ccRoot->selectEntity(smoothPoly, true); } else { errors = true; } } if (errors) { ccLog::Error(tr("[DoActionSmoohPolyline] Errors occurred during the process! Result may be incomplete!")); } refreshAll(); } void MainWindow::doRemoveDuplicatePoints() { if (!haveSelection()) return; bool first = true; //persistent setting(s) QSettings settings; settings.beginGroup(ccPS::DuplicatePointsGroup()); double minDistanceBetweenPoints = settings.value(ccPS::DuplicatePointsMinDist(),1.0e-12).toDouble(); bool ok; minDistanceBetweenPoints = QInputDialog::getDouble(this, tr("Remove duplicate points"), tr("Min distance between points:"), minDistanceBetweenPoints, 0, 1.0e8, 12, &ok); if (!ok) return; //save parameter settings.setValue(ccPS::DuplicatePointsMinDist(), minDistanceBetweenPoints); ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); ccHObject::Container selectedEntities = getSelectedEntities(); //we have to use a local copy: 'unselectAllEntities' and 'selectEntity' will change the set of currently selected entities! for (ccHObject* entity : selectedEntities) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (cloud) { ccPointCloud* filteredCloud = cloud->removeDuplicatePoints(minDistanceBetweenPoints, &pDlg); if (!filteredCloud) { ccConsole::Error(tr("Process failed (see Console)")); break; } if (filteredCloud != cloud) // otherwise the cloud has no duplicate point { filteredCloud->prepareDisplayForRefresh(); addToDB(filteredCloud); if (first) { m_ccRoot->unselectAllEntities(); first = false; } cloud->setEnabled(false); m_ccRoot->selectEntity(filteredCloud, true); } } } if (!first) { ccConsole::Warning(tr("Previously selected entities (sources) have been hidden!")); } refreshAll(); } void MainWindow::doActionFilterByValue() { typedef std::pair EntityAndVerticesType; std::vector toFilter; for ( ccHObject *entity : getSelectedEntities() ) { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity); if (cloud && cloud->isA(CC_TYPES::POINT_CLOUD)) { ccPointCloud* pc = static_cast(cloud); //la methode est activee sur le champ scalaire affiche CCCoreLib::ScalarField* sf = pc->getCurrentDisplayedScalarField(); if (sf) { toFilter.emplace_back(entity,pc); } else { ccConsole::Warning(tr("Entity [%1] has no active scalar field!").arg(entity->getName())); } } } if (toFilter.empty()) return; double minVald = 0.0; double maxVald = 1.0; //compute min and max "displayed" scalar values of currently selected //entities (the ones with an active scalar field only!) { for (size_t i = 0; i < toFilter.size(); ++i) { ccScalarField* sf = toFilter[i].second->getCurrentDisplayedScalarField(); assert(sf); if (i == 0) { minVald = static_cast(sf->displayRange().start()); maxVald = static_cast(sf->displayRange().stop()); } else { if (minVald > static_cast(sf->displayRange().start())) minVald = static_cast(sf->displayRange().start()); if (maxVald < static_cast(sf->displayRange().stop())) maxVald = static_cast(sf->displayRange().stop()); } } } ccFilterByValueDlg dlg(minVald, maxVald, -1.0e9, 1.0e9, this); if (!dlg.exec()) return; ccFilterByValueDlg::Mode mode = dlg.mode(); assert(mode != ccFilterByValueDlg::CANCEL); ScalarType minVal = static_cast(dlg.minDoubleSpinBox->value()); ScalarType maxVal = static_cast(dlg.maxDoubleSpinBox->value()); ccHObject::Container results; { for ( auto &item : toFilter ) { ccHObject* ent = item.first; ccPointCloud* pc = item.second; //we set as output (OUT) the currently displayed scalar field int outSfIdx = pc->getCurrentDisplayedScalarFieldIndex(); assert(outSfIdx >= 0); pc->setCurrentOutScalarField(outSfIdx); ccHObject* resultInside = nullptr; ccHObject* resultOutside = nullptr; if (ent->isKindOf(CC_TYPES::MESH)) { pc->hidePointsByScalarValue(minVal, maxVal); if (ent->isA(CC_TYPES::MESH)/*|| ent->isKindOf(CC_TYPES::PRIMITIVE)*/) //TODO resultInside = ccHObjectCaster::ToMesh(ent)->createNewMeshFromSelection(false, nullptr, true); else if (ent->isA(CC_TYPES::SUB_MESH)) resultInside = ccHObjectCaster::ToSubMesh(ent)->createNewSubMeshFromSelection(false); if (resultInside == ent) { //specific case: all triangles were selected, nothing to do ccLog::Warning(QString("Mesh %1 is fully inside the specified range").arg(ent->getName())); resultInside = nullptr; } else if (mode == ccFilterByValueDlg::SPLIT) { pc->invertVisibilityArray(); if (ent->isA(CC_TYPES::MESH)/*|| ent->isKindOf(CC_TYPES::PRIMITIVE)*/) //TODO resultOutside = ccHObjectCaster::ToMesh(ent)->createNewMeshFromSelection(false, nullptr, true); else if (ent->isA(CC_TYPES::SUB_MESH)) resultOutside = ccHObjectCaster::ToSubMesh(ent)->createNewSubMeshFromSelection(false); if (resultOutside == ent) { //specific case: all triangles were selected, nothing to do ccLog::Warning(QString("Mesh %1 is fully outside the specified range").arg(ent->getName())); ent->setEnabled(false); ent->prepareDisplayForRefresh(); delete resultInside; // we don't need it resultInside = nullptr; resultOutside = nullptr; } } pc->unallocateVisibilityArray(); } else if (ent->isKindOf(CC_TYPES::POINT_CLOUD)) { //shortcut, as we know here that the point cloud is a "ccPointCloud" resultInside = pc->filterPointsByScalarValue(minVal, maxVal, false); if (resultInside == ent) { //specific case: all points were selected, nothing to do ccLog::Warning(QString("Cloud %1 is fully inside the specified range").arg(ent->getName())); resultInside = nullptr; } else if (mode == ccFilterByValueDlg::SPLIT) { resultOutside = pc->filterPointsByScalarValue(minVal, maxVal, true); if (resultOutside == ent) { //specific case: all points were selected, nothing to do ccLog::Warning(QString("Cloud %1 is fully outside the specified range").arg(ent->getName())); ent->setEnabled(false); ent->prepareDisplayForRefresh(); delete resultInside; resultInside = nullptr; resultOutside = nullptr; } } } if (resultInside) { ent->setEnabled(false); resultInside->setDisplay(ent->getDisplay()); resultInside->prepareDisplayForRefresh(); addToDB(resultInside); results.push_back(resultInside); } if (resultOutside) { ent->setEnabled(false); resultOutside->setDisplay(ent->getDisplay()); resultOutside->prepareDisplayForRefresh(); resultOutside->setName(resultOutside->getName() + ".outside"); addToDB(resultOutside); results.push_back(resultOutside); } } } if (!results.empty()) { ccConsole::Warning(tr("Previously selected entities (sources) have been hidden!")); if (m_ccRoot) { m_ccRoot->selectEntities(results); } } refreshAll(); } void MainWindow::doActionSFConvertToRandomRGB() { if ( !ccEntityAction::sfConvertToRandomRGB(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionSFConvertToRGB() { if ( !ccEntityAction::sfConvertToRGB(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionToggleActiveSFColorScale() { doApplyActiveSFAction(0); } void MainWindow::doActionShowActiveSFPrevious() { doApplyActiveSFAction(1); } void MainWindow::doActionShowActiveSFNext() { doApplyActiveSFAction(2); } void MainWindow::doApplyActiveSFAction(int action) { if (!haveOneSelection()) { if (haveSelection()) { ccConsole::Error(tr("Select only one cloud or one mesh!")); } return; } ccHObject* ent = m_selectedEntities.front(); bool lockedVertices; ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(ent,&lockedVertices); //for "real" point clouds only if (!cloud) return; if (lockedVertices && !ent->isAncestorOf(cloud)) { //see ccPropertiesTreeDelegate::fillWithMesh ccUtils::DisplayLockedVerticesWarning(ent->getName(),true); return; } assert(cloud); int sfIdx = cloud->getCurrentDisplayedScalarFieldIndex(); switch (action) { case 0: //Toggle SF color scale if (sfIdx >= 0) { cloud->showSFColorsScale(!cloud->sfColorScaleShown()); cloud->prepareDisplayForRefresh(); } else ccConsole::Warning(tr("No active scalar field on entity '%1'").arg(ent->getName())); break; case 1: //Activate previous SF if (sfIdx >= 0) { cloud->setCurrentDisplayedScalarField(sfIdx-1); cloud->prepareDisplayForRefresh(); } break; case 2: //Activate next SF if (sfIdx+1 < static_cast(cloud->getNumberOfScalarFields())) { cloud->setCurrentDisplayedScalarField(sfIdx+1); cloud->prepareDisplayForRefresh(); } break; } refreshAll(); updateUI(); } void MainWindow::doActionRenameSF() { if ( !ccEntityAction::sfRename(m_selectedEntities, this) ) return; updateUI(); } void MainWindow::doActionOpenColorScalesManager() { ccColorScaleEditorDialog cseDlg(ccColorScalesManager::GetUniqueInstance(), this, ccColorScale::Shared(nullptr), this); if (cseDlg.exec()) { //save current scale manager state to persistent settings ccColorScalesManager::GetUniqueInstance()->toPersistentSettings(); } updateUI(); } void MainWindow::doActionAddIdField() { if (!ccEntityAction::sfAddIdField(m_selectedEntities)) { return; } refreshAll(); updateUI(); } void MainWindow::doActionSplitCloudUsingSF() { if (!ccEntityAction::sfSplitCloud(m_selectedEntities, this)) return; refreshAll(); updateUI(); } void MainWindow::doActionRGBGaussianFilter() { ccPointCloud::RgbFilterOptions(filterParams); filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::GAUSSIAN; if (!ccEntityAction::rgbGaussianFilter(m_selectedEntities, filterParams, this)) return; refreshAll(); updateUI(); } void MainWindow::doActionRGBBilateralFilter() { ccPointCloud::RgbFilterOptions(filterParams); filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::BILATERAL; if (!ccEntityAction::rgbGaussianFilter(m_selectedEntities, filterParams, this)) return; refreshAll(); updateUI(); } void MainWindow::doActionRGBMeanFilter() { ccPointCloud::RgbFilterOptions(filterParams); filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::MEAN; if (!ccEntityAction::rgbGaussianFilter(m_selectedEntities, filterParams, this)) return; refreshAll(); updateUI(); } void MainWindow::doActionRGBMedianFilter() { ccPointCloud::RgbFilterOptions(filterParams); filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::MEDIAN; if (!ccEntityAction::rgbGaussianFilter(m_selectedEntities, filterParams, this)) return; refreshAll(); updateUI(); } void MainWindow::doActionSFGaussianFilter() { ccPointCloud::RgbFilterOptions(filterParams); filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::GAUSSIAN; if ( !ccEntityAction::sfGaussianFilter(m_selectedEntities, filterParams, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionSFBilateralFilter() { ccPointCloud::RgbFilterOptions(filterParams); filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::BILATERAL; if ( !ccEntityAction::sfGaussianFilter(m_selectedEntities, filterParams, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionSmoothMeshSF() { if ( !ccEntityAction::processMeshSF(m_selectedEntities, ccMesh::SMOOTH_MESH_SF, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionEnhanceMeshSF() { if ( !ccEntityAction::processMeshSF(m_selectedEntities, ccMesh::ENHANCE_MESH_SF, this) ) return; refreshAll(); updateUI(); } static double s_subdivideMaxArea = 1.0; void MainWindow::doActionSubdivideMesh() { bool ok; s_subdivideMaxArea = QInputDialog::getDouble(this, tr("Subdivide mesh"), tr("Max area per triangle:"), s_subdivideMaxArea, 1e-6, 1e6, 8, &ok); if (!ok) return; //ccProgressDialog pDlg(true, this); //pDlg.setAutoClose(false); bool warningIssued = false; for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::MESH)) { //single mesh? if (entity->isA(CC_TYPES::MESH)) { ccMesh* mesh = static_cast(entity); ccMesh* subdividedMesh = nullptr; try { subdividedMesh = mesh->subdivide(static_cast(s_subdivideMaxArea)); } catch(...) { ccLog::Error(tr("[Subdivide] An error occurred while trying to subdivide mesh '%1' (not enough memory?)").arg(mesh->getName())); } if (subdividedMesh) { subdividedMesh->setName(QString("%1.subdivided(S<%2)").arg(mesh->getName()).arg(s_subdivideMaxArea)); subdividedMesh->setDisplay(mesh->getDisplay()); mesh->redrawDisplay(); mesh->setEnabled(false); addToDB(subdividedMesh); } else { ccConsole::Warning(tr("[Subdivide] Failed to subdivide mesh '%1' (not enough memory?)").arg(mesh->getName())); } } else if (!warningIssued) { ccLog::Warning(tr("[Subdivide] Works only on real meshes!")); warningIssued = true; } } } refreshAll(); updateUI(); } void MainWindow::doActionFlipMeshTriangles() { bool warningIssued = false; for (ccHObject *entity : getSelectedEntities()) { if (entity->isKindOf(CC_TYPES::MESH)) { //single mesh? if (entity->isA(CC_TYPES::MESH)) { ccMesh* mesh = static_cast(entity); mesh->flipTriangles(); mesh->prepareDisplayForRefresh(); } else if (!warningIssued) { ccLog::Warning(tr("[Flip triangles] Works only on real meshes!")); warningIssued = true; } } } refreshAll(); } void MainWindow::doActionSmoothMeshLaplacian() { static unsigned s_laplacianSmooth_nbIter = 20; static double s_laplacianSmooth_factor = 0.2; bool ok; s_laplacianSmooth_nbIter = QInputDialog::getInt(this, tr("Smooth mesh"), tr("Iterations:"), s_laplacianSmooth_nbIter, 1, 1000, 1, &ok); if (!ok) return; s_laplacianSmooth_factor = QInputDialog::getDouble(this, tr("Smooth mesh"), tr("Smoothing factor:"), s_laplacianSmooth_factor, 0, 100, 3, &ok); if (!ok) return; ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isA(CC_TYPES::MESH) || entity->isA(CC_TYPES::PRIMITIVE)) //FIXME: can we really do this with primitives? { ccMesh* mesh = ccHObjectCaster::ToMesh(entity); if (mesh->laplacianSmooth( s_laplacianSmooth_nbIter, static_cast(s_laplacianSmooth_factor), &pDlg) ) { mesh->prepareDisplayForRefresh_recursive(); } else { ccConsole::Warning(tr("Failed to apply Laplacian smoothing to mesh '%1'").arg(mesh->getName())); } } } refreshAll(); updateUI(); } // helper for doActionMerge void AddToRemoveList(ccHObject* toRemove, ccHObject::Container& toBeRemovedList) { // is a parent or sibling already in the "toBeRemoved" list? size_t count = toBeRemovedList.size(); for (size_t j = 0; j < count;) { if (toBeRemovedList[j]->isAncestorOf(toRemove)) { // nothing to do, we already have an ancestor return; } else if (toRemove->isAncestorOf(toBeRemovedList[j])) { // we don't need to keep the children toBeRemovedList[j] = toBeRemovedList.back(); toBeRemovedList.pop_back(); count--; } else { // forward ++j; } } toBeRemovedList.push_back(toRemove); } void MainWindow::doActionMerge() { //let's look for clouds or meshes (warning: we don't mix them) std::vector clouds; std::vector meshes; try { for (ccHObject* entity : getSelectedEntities()) { if (!entity) { continue; } if (entity->isA(CC_TYPES::POINT_CLOUD)) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); clouds.push_back(cloud); // check whether this cloud is an ancestor of the first cloud in the selection if (clouds.size() > 1) { if (clouds.back()->isAncestorOf(clouds.front())) { // this way we are sure that the first cloud is not below any other cloud std::swap(clouds.front(), clouds.back()); } } } else if (entity->isKindOf(CC_TYPES::MESH)) { ccMesh* mesh = ccHObjectCaster::ToMesh(entity); //this is a purely theoretical test for now! if (mesh && mesh->getAssociatedCloud() && mesh->getAssociatedCloud()->isA(CC_TYPES::POINT_CLOUD)) { meshes.push_back(mesh); } else { ccConsole::Warning(tr("Only meshes with standard vertices are handled for now! Can't merge entity '%1'...").arg(entity->getName())); } } else { ccConsole::Warning(tr("Entity '%1' is neither a cloud nor a mesh, can't merge it!").arg(entity->getName())); } } } catch (const std::bad_alloc&) { ccLog::Error(tr("Not enough memory!")); return; } if (clouds.empty() && meshes.empty()) { ccLog::Error(tr("Select only clouds or meshes!")); return; } if (!clouds.empty() && !meshes.empty()) { ccLog::Error(tr("Can't mix point clouds and meshes!")); } //merge clouds? if (!clouds.empty()) { //we deselect all selected entities (as most of them are going to disappear) if (m_ccRoot) { m_ccRoot->unselectAllEntities(); assert(!haveSelection()); //m_selectedEntities.clear(); } //we will remove the useless clouds/meshes later ccHObject::Container toBeRemoved; ccPointCloud* firstCloud = nullptr; ccHObjectContext firstCloudContext; //whether to generate the 'original cloud index' scalar field or not CCCoreLib::ScalarField* ocIndexSF = nullptr; size_t cloudIndex = 0; //compute total size of the final cloud unsigned totalSize = 0; for (size_t i = 0; i < clouds.size(); ++i) { totalSize += clouds[i]->size(); } for (size_t i = 0; i < clouds.size(); ++i) { ccPointCloud* pc = clouds[i]; if (!firstCloud) { //we don't delete the first cloud (we'll merge the other one 'inside' it firstCloud = pc; //we still have to temporarily detach the first cloud, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccPointCloud::operator += firstCloudContext = removeObjectTemporarilyFromDBTree(firstCloud); //reserve the final required number of points if (!firstCloud->reserve(totalSize)) { ccConsole::Error(tr("Not enough memory!")); break; } if (QMessageBox::question(this, tr("Original cloud index"), tr("Do you want to generate a scalar field with the original cloud index?")) == QMessageBox::Yes) { int sfIdx = pc->getScalarFieldIndexByName(CC_ORIGINAL_CLOUD_INDEX_SF_NAME); if (sfIdx < 0) { sfIdx = pc->addScalarField(CC_ORIGINAL_CLOUD_INDEX_SF_NAME); } if (sfIdx < 0) { ccConsole::Error(tr("Couldn't allocate a new scalar field for storing the original cloud index! Try to free some memory ...")); return; } else { ocIndexSF = pc->getScalarField(sfIdx); if (ocIndexSF) { ocIndexSF->fill(0); firstCloud->setCurrentDisplayedScalarField(sfIdx); } } } } else { unsigned countBefore = firstCloud->size(); unsigned countAdded = pc->size(); firstCloud->append(pc, countBefore, false, false); //append without recalculating SF min/max //success? if (firstCloud->size() == countBefore + countAdded) { firstCloud->prepareDisplayForRefresh_recursive(); ccHObject* toRemove = nullptr; //if the entity to remove is inside a group with a unique child, we can remove the group as well ccHObject* parent = pc->getParent(); if (parent && parent->isA(CC_TYPES::HIERARCHY_OBJECT) && parent->getChildrenNumber() == 1 && parent != firstCloudContext.parent) toRemove = parent; else toRemove = pc; AddToRemoveList(toRemove, toBeRemoved); if (ocIndexSF) { ScalarType index = static_cast(++cloudIndex); for (unsigned i = 0; i < countAdded; ++i) { ocIndexSF->setValue(countBefore + i, index); } } } else { ccConsole::Error(tr("Fusion failed! (not enough memory?)")); break; } pc = nullptr; } } //compute min and max once after all appends are done if (firstCloud) { for (unsigned i = 0; i < firstCloud->getNumberOfScalarFields(); i++) { firstCloud->getScalarField(i)->computeMinAndMax(); } if (ocIndexSF) { firstCloud->showSF(true); } } //something to remove? for (ccHObject* toRemove : toBeRemoved) { if (firstCloud->isAncestorOf(toRemove)) { // we cannot call 'removeElement' on a child of the first cloud, as it's temporarily detached from the DB tree! if (toRemove->getParent()) toRemove->getParent()->removeChild(toRemove); else delete toRemove; } else { m_ccRoot->removeElement(toRemove); } } toBeRemoved.clear(); //eventually we can put back the first cloud in DB if (firstCloud) { putObjectBackIntoDBTree(firstCloud, firstCloudContext); if (m_ccRoot) m_ccRoot->selectEntity(firstCloud); } } //merge meshes? else if (!meshes.empty()) { bool createSubMeshes = true; //createSubMeshes = (QMessageBox::question(this, tr("Create sub-meshes"), tr("Do you want to create sub-mesh entities corresponding to each source mesh? (requires more memory)"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes); //meshes are merged ccPointCloud* baseVertices = new ccPointCloud("vertices"); ccMesh* baseMesh = new ccMesh(baseVertices); baseMesh->setName("Merged mesh"); baseMesh->addChild(baseVertices); baseVertices->setEnabled(false); for (ccMesh *mesh : meshes) { //if (mesh->isA(CC_TYPES::PRIMITIVE)) //{ // mesh = mesh->ccMesh::cloneMesh(); //we want a clone of the mesh part, not the primitive! //} if (!baseMesh->merge(mesh, createSubMeshes)) { ccConsole::Error(tr("Fusion failed! (not enough memory?)")); break; } } baseMesh->setDisplay_recursive(meshes.front()->getDisplay()); baseMesh->setVisible(true); addToDB(baseMesh); if (m_ccRoot) m_ccRoot->selectEntity(baseMesh); } refreshAll(); updateUI(); } void MainWindow::zoomOn(ccHObject* object) { ccGLWindowInterface* win = static_cast(object->getDisplay()); if (win) { ccBBox box = object->getDisplayBB_recursive(false,win); win->updateConstellationCenterAndZoom(&box); } } void MainWindow::doActionRegister() { if ( m_selectedEntities.size() != 2 || (!m_selectedEntities.front()->isKindOf(CC_TYPES::POINT_CLOUD) && !m_selectedEntities.front()->isKindOf(CC_TYPES::MESH)) || (!m_selectedEntities.back()->isKindOf(CC_TYPES::POINT_CLOUD) && !m_selectedEntities.back()->isKindOf(CC_TYPES::MESH)) ) { ccConsole::Error(tr("Select 2 point clouds or meshes!")); return; } ccHObject* data = static_cast(m_selectedEntities.front()); ccHObject* model = static_cast(m_selectedEntities.back()); if (data->isKindOf(CC_TYPES::MESH) && model->isKindOf(CC_TYPES::POINT_CLOUD)) { //by default, prefer the mesh as the reference std::swap(data, model); } ccRegistrationDlg rDlg(data, model, this); if (!rDlg.exec()) return; //model and data order may have changed! model = rDlg.getModelEntity(); data = rDlg.getDataEntity(); double minRMSDecrease = rDlg.getMinRMSDecrease(); if (std::isnan(minRMSDecrease)) { ccLog::Error(tr("Invalid minimum RMS decrease value")); return; } if (minRMSDecrease < ccRegistrationDlg::GetAbsoluteMinRMSDecrease()) { minRMSDecrease = ccRegistrationDlg::GetAbsoluteMinRMSDecrease(); ccLog::Error(tr("Minimum RMS decrease value is too small.\n%1 will be used instead (numerical accuracy limit).").arg(minRMSDecrease, 0, 'E', 1)); rDlg.setMinRMSDecrease(minRMSDecrease); } CCCoreLib::ICPRegistrationTools::Parameters parameters; { parameters.convType = rDlg.getConvergenceMethod(); parameters.minRMSDecrease = minRMSDecrease; parameters.nbMaxIterations = rDlg.getMaxIterationCount(); parameters.adjustScale = rDlg.adjustScale(); parameters.filterOutFarthestPoints = rDlg.removeFarthestPoints(); parameters.samplingLimit = rDlg.randomSamplingLimit(); parameters.finalOverlapRatio = rDlg.getFinalOverlap() / 100.0; parameters.transformationFilters = rDlg.getTransformationFilters(); parameters.maxThreadCount = rDlg.getMaxThreadCount(); parameters.useC2MSignedDistances = rDlg.useC2MSignedDistances(parameters.robustC2MSignedDistances); parameters.normalsMatching = rDlg.normalsMatchingOption(); } bool useDataSFAsWeights = rDlg.useDataSFAsWeights(); bool useModelSFAsWeights = rDlg.useModelSFAsWeights(); //semi-persistent storage (for next call) rDlg.saveParameters(); ccGLMatrix transMat; double finalError = 0.0; double finalScale = 1.0; unsigned finalPointCount = 0; if (ccRegistrationTools::ICP( data, model, transMat, finalScale, finalError, finalPointCount, parameters, useDataSFAsWeights, useModelSFAsWeights, this)) { QString rmsString = tr("Final RMS*: %1 (computed on %2 points)").arg(finalError).arg(finalPointCount); QString rmsDisclaimerString = tr("(* RMS is potentially weighted, depending on the selected options)"); ccLog::Print(QString("[Register] ") + rmsString); ccLog::Print(QString("[Register] ") + rmsDisclaimerString); QStringList summary; summary << rmsString; summary << rmsDisclaimerString; summary << "----------------"; //transformation matrix { summary << "Transformation matrix"; summary << transMat.toString(3, '\t'); //low precision, just for display summary << "----------------"; ccLog::Print(tr("[Register] Applied transformation matrix:")); ccLog::Print(transMat.toString(12, ' ')); //full precision ccLog::Print(tr("Hint: copy it (CTRL+C) and apply it - or its inverse - on any entity with the 'Edit > Apply transformation' tool")); } if (parameters.adjustScale) { QString scaleString = tr("Scale: %1 (already integrated in above matrix!)").arg(finalScale); ccLog::Warning(QString("[Register] ") + scaleString); summary << scaleString; } else { ccLog::Print(tr("[Register] Scale: fixed (1.0)")); summary << tr("Scale: fixed (1.0)"); } //overlap summary << "----------------"; QString overlapString = tr("Theoretical overlap: %1%").arg(static_cast(parameters.finalOverlapRatio * 100)); ccLog::Print(QString("[Register] ") + overlapString); summary << overlapString; summary << "----------------"; summary << tr("This report has been output to Console (F8)"); //cloud to move ccGenericPointCloud* pc = nullptr; if (data->isKindOf(CC_TYPES::POINT_CLOUD)) { pc = ccHObjectCaster::ToGenericPointCloud(data); } else if (data->isKindOf(CC_TYPES::MESH)) { ccGenericMesh* mesh = ccHObjectCaster::ToGenericMesh(data); pc = mesh->getAssociatedCloud(); //warning: point cloud is locked! if (pc->isLocked()) { pc = nullptr; //we ask the user about cloning the 'data' mesh QMessageBox::StandardButton result = QMessageBox::question( this, tr("Registration"), tr("Data mesh vertices are locked (they may be shared with other meshes): Do you wish to clone this mesh to apply transformation?"), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); //continue process? if (result == QMessageBox::Ok) { ccGenericMesh* newMesh = nullptr; if (mesh->isA(CC_TYPES::MESH)) newMesh = static_cast(mesh)->cloneMesh(); else { //FIXME TODO ccLog::Error(tr("Doesn't work on sub-meshes yet!")); } if (newMesh) { newMesh->setDisplay(data->getDisplay()); addToDB(newMesh); data = newMesh; pc = newMesh->getAssociatedCloud(); } else { ccLog::Error(tr("Failed to clone 'data' mesh! (not enough memory?)")); } } } } //if we managed to get a point cloud to move! if (pc) { //we temporarily detach cloud, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccPointCloud::applyRigidTransformation ccHObjectContext objContext = removeObjectTemporarilyFromDBTree(pc); // check if the 'model' entity is a child of the 'data' one (would be strange, but can still happen!) bool modelIsChildOfData = data->isAncestorOf(model); if (modelIsChildOfData) { if (pc->getChildrenNumber() != 0) { ccLog::Warning(tr("[ICP] The reference entity is a child of the aligned one! CC will move only the aligned entity, and not its children")); } pc->applyRigidTransformation(transMat); } else { // we can safely apply the transformation to all the children pc->applyGLTransformation_recursive(&transMat); } putObjectBackIntoDBTree(pc, objContext); //don't forget to update mesh bounding box also! if (data->isKindOf(CC_TYPES::MESH)) { ccHObjectCaster::ToGenericMesh(data)->refreshBB(); } //don't forget global shift ccGenericPointCloud* refPC = ccHObjectCaster::ToGenericPointCloud(model); if (refPC) { if (refPC->isShifted()) { const CCVector3d& Pshift = refPC->getGlobalShift(); double scale = refPC->getGlobalScale(); pc->setGlobalShift(Pshift); pc->setGlobalScale(scale); ccLog::Warning(tr("[ICP] Aligned entity global shift has been updated to match the reference: (%1,%2,%3) [x%4]").arg(Pshift.x).arg(Pshift.y).arg(Pshift.z).arg(scale)); ccGLMatrixd transMatD(transMat.data()); transMatD.scale(1.0 / scale); transMatD.setTranslation(transMatD.getTranslationAsVec3D() - Pshift); ccLog::Print("[ICP] Transformation to global coordinates:"); ccLog::Print(transMatD.toString(12, ' ')); //full precision } else if (pc->isShifted()) //we'll ask the user first before dropping the shift information on the aligned cloud { if (QMessageBox::question(this, tr("Drop shift information?"), tr("Aligned entity is shifted but reference cloud is not: drop global shift information?"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { pc->setGlobalShift(0, 0, 0); pc->setGlobalScale(1.0); ccLog::Warning(tr("[ICP] Aligned entity global shift has been reset to match the reference!")); } } } data->prepareDisplayForRefresh_recursive(); data->setName(data->getName() + QString(".registered")); zoomOn(data); } //pop-up summary QMessageBox::information(this, tr("Registration info"), summary.join("\n")); forceConsoleDisplay(); } refreshAll(); updateUI(); } //Aurelien BEY le 13/11/2008 : ajout de la fonction permettant de traiter la fonctionnalite de recalage grossier void MainWindow::doAction4pcsRegister() { if (QMessageBox::warning( this, tr("Work in progress"), tr("This method is still under development: are you sure you want to use it? (a crash may likely happen)"), QMessageBox::Yes,QMessageBox::No) == QMessageBox::No ) return; if (m_selectedEntities.size() != 2) { ccConsole::Error(tr("Select 2 point clouds!")); return; } if (!m_selectedEntities.front()->isKindOf(CC_TYPES::POINT_CLOUD) || !m_selectedEntities.back()->isKindOf(CC_TYPES::POINT_CLOUD)) { ccConsole::Error(tr("Select 2 point clouds!")); return; } ccGenericPointCloud* data = ccHObjectCaster::ToGenericPointCloud(m_selectedEntities.back()); ccGenericPointCloud* model = ccHObjectCaster::ToGenericPointCloud(m_selectedEntities.front()); ccAlignDlg aDlg(data, model); if (!aDlg.exec()) { return; } // model = aDlg.getModelObject(); data = aDlg.getDataObject(); //Take the correct number of points among the clouds CCCoreLib::ReferenceCloud* subModel = aDlg.getSampledModel(); CCCoreLib::ReferenceCloud* subData = aDlg.getSampledData(); if (!subModel || !subData) { delete subModel; delete subData; assert(false); return; } unsigned nbMaxCandidates = aDlg.isNumberOfCandidatesLimited() ? aDlg.getMaxNumberOfCandidates() : 0; ccProgressDialog pDlg(true, this); CCCoreLib::PointProjectionTools::Transformation transform; if (CCCoreLib::FPCSRegistrationTools::RegisterClouds( subModel, subData, transform, static_cast(aDlg.getDelta()), static_cast(aDlg.getDelta()/2), static_cast(aDlg.getOverlap()), aDlg.getNbTries(), 5000, &pDlg, nbMaxCandidates)) { //output resulting transformation matrix { ccGLMatrix transMat = FromCCLibMatrix(transform.R, transform.T); forceConsoleDisplay(); ccConsole::Print(tr("[Align] Resulting matrix:")); ccConsole::Print(transMat.toString(12, ' ')); //full precision ccConsole::Print(tr("Hint: copy it (CTRL+C) and apply it - or its inverse - on any entity with the 'Edit > Apply transformation' tool")); } ccPointCloud* newDataCloud = data->isA(CC_TYPES::POINT_CLOUD) ? static_cast(data)->cloneThis() : ccPointCloud::From(data, data); if (data->getParent()) { data->getParent()->addChild(newDataCloud); } newDataCloud->setName(data->getName() + QString(".registered")); transform.apply(*newDataCloud); newDataCloud->invalidateBoundingBox(); //invalidate bb newDataCloud->setDisplay(data->getDisplay()); newDataCloud->prepareDisplayForRefresh(); zoomOn(newDataCloud); addToDB(newDataCloud); data->setEnabled(false); data->prepareDisplayForRefresh_recursive(); } else { ccConsole::Warning(tr("[Align] Registration failed!")); } delete subModel; subModel = nullptr; delete subData; subData = nullptr; refreshAll(); updateUI(); } void MainWindow::doActionSubsample() { //find candidates std::vector clouds; unsigned maxPointCount = 0; double maxCloudRadius = 0; ScalarType sfMin = CCCoreLib::NAN_VALUE; ScalarType sfMax = CCCoreLib::NAN_VALUE; { for ( ccHObject* entity : getSelectedEntities() ) { if (entity->isA(CC_TYPES::POINT_CLOUD)) { ccPointCloud* cloud = static_cast(entity); clouds.push_back(cloud); maxPointCount = std::max(maxPointCount, cloud->size()); maxCloudRadius = std::max(maxCloudRadius, cloud->getOwnBB().getDiagNorm()); //we also look for the min and max sf values ccScalarField* sf = cloud->getCurrentDisplayedScalarField(); if (sf) { if (!ccScalarField::ValidValue(sfMin) || sfMin > sf->getMin()) sfMin = sf->getMin(); if (!ccScalarField::ValidValue(sfMax) || sfMax < sf->getMax()) sfMax = sf->getMax(); } } } } if (clouds.empty()) { ccConsole::Error(tr("Select at least one point cloud!")); return; } //Display dialog ccSubsamplingDlg sDlg(maxPointCount, maxCloudRadius, this); sDlg.loadFromPersistentSettings(); bool hasValidSF = ccScalarField::ValidValue(sfMin) && ccScalarField::ValidValue(sfMax); if (hasValidSF) { sDlg.enableSFModulation(sfMin, sfMax); } if (!sDlg.exec()) { return; } sDlg.saveToPersistentSettings(); //process clouds ccHObject::Container resultingClouds; { ccProgressDialog pDlg(false, this); pDlg.setAutoClose(false); pDlg.setMethodTitle(tr("Subsampling")); bool errors = false; QElapsedTimer eTimer; eTimer.start(); for (size_t i = 0; i < clouds.size(); ++i) { ccPointCloud* cloud = clouds[i]; CCCoreLib::ReferenceCloud* sampledCloud = sDlg.getSampledCloud(cloud,&pDlg); if (!sampledCloud) { ccConsole::Warning(tr("[Subsampling] Failed to subsample cloud '%1'!").arg(cloud->getName())); errors = true; continue; } int warnings = 0; ccPointCloud* newPointCloud = cloud->partialClone(sampledCloud, &warnings); delete sampledCloud; sampledCloud = nullptr; if (newPointCloud) { newPointCloud->setName(cloud->getName() + QString(".subsampled")); newPointCloud->copyGlobalShiftAndScale(*cloud); newPointCloud->setDisplay(cloud->getDisplay()); newPointCloud->prepareDisplayForRefresh(); if (cloud->getParent()) cloud->getParent()->addChild(newPointCloud); cloud->setEnabled(false); addToDB(newPointCloud); newPointCloud->prepareDisplayForRefresh(); resultingClouds.push_back(newPointCloud); if (warnings) { ccLog::Warning(tr("[Subsampling] Not enough memory: colors, normals or scalar fields may be missing!")); errors = true; } } else { ccLog::Error(tr("Not enough memory!")); break; } } ccLog::Print("[Subsampling] Timing: %3.3f s.",eTimer.elapsed()/1000.0); if (errors) { ccLog::Error(tr("Errors occurred (see console)")); } } if (m_ccRoot) m_ccRoot->selectEntities(resultingClouds); refreshAll(); updateUI(); } void MainWindow::doActionStatisticalTest() { if ( !ccEntityAction::statisticalTest(m_selectedEntities, this ) ) return; refreshAll(); updateUI(); } void MainWindow::doActionComputeStatParams() { ccEntityAction::computeStatParams(m_selectedEntities, this ); } struct ComponentIndexAndSize { unsigned index; unsigned size; ComponentIndexAndSize(unsigned i, unsigned s) : index(i), size(s) {} static bool DescendingCompOperator(const ComponentIndexAndSize& a, const ComponentIndexAndSize& b) { return a.size > b.size; } }; void MainWindow::createComponentsClouds(ccGenericPointCloud* cloud, CCCoreLib::ReferenceCloudContainer& components, unsigned minPointsPerComponent, bool randomColors, bool selectComponents, bool sortBysize/*=true*/) { if (!cloud || components.empty()) return; std::vector sortedIndexes; std::vector* _sortedIndexes = nullptr; if (sortBysize) { try { sortedIndexes.reserve(components.size()); } catch (const std::bad_alloc&) { ccLog::Warning(tr("[CreateComponentsClouds] Not enough memory to sort components by size!")); sortBysize = false; } if (sortBysize) //still ok? { unsigned compCount = static_cast(components.size()); for (unsigned i = 0; i < compCount; ++i) { sortedIndexes.emplace_back(i, components[i]->size()); } ParallelSort(sortedIndexes.begin(), sortedIndexes.end(), ComponentIndexAndSize::DescendingCompOperator); _sortedIndexes = &sortedIndexes; } } //we create "real" point clouds for all input components { ccPointCloud* pc = cloud->isA(CC_TYPES::POINT_CLOUD) ? static_cast(cloud) : nullptr; //we create a new group to store all CCs ccHObject* ccGroup = new ccHObject(cloud->getName() + QString(" [CCs]")); //for each component for (size_t i = 0; i < components.size(); ++i) { CCCoreLib::ReferenceCloud* compIndexes = _sortedIndexes ? components[_sortedIndexes->at(i).index] : components[i]; //if it has enough points if (compIndexes->size() >= minPointsPerComponent) { //we create a new entity ccPointCloud* compCloud = (pc ? pc->partialClone(compIndexes) : ccPointCloud::From(compIndexes)); if (compCloud) { //shall we colorize it with random color? if (randomColors) { ccColor::Rgb col = ccColor::Generator::Random(); compCloud->setColor(col); compCloud->showColors(true); compCloud->showSF(false); } //'shift on load' information if (pc) { compCloud->copyGlobalShiftAndScale(*pc); } compCloud->setVisible(true); compCloud->setName(QString("CC#%1").arg(ccGroup->getChildrenNumber())); //we add new CC to group ccGroup->addChild(compCloud); if (selectComponents && m_ccRoot) m_ccRoot->selectEntity(compCloud, true); } else { ccConsole::Warning(tr("[CreateComponentsClouds] Failed to create component #%1! (not enough memory)").arg(ccGroup->getChildrenNumber() + 1)); } } delete compIndexes; compIndexes = nullptr; } components.clear(); if (ccGroup->getChildrenNumber() == 0) { ccConsole::Error(tr("No component was created! Check the minimum size...")); delete ccGroup; ccGroup = nullptr; } else { ccGroup->setDisplay(cloud->getDisplay()); addToDB(ccGroup); ccConsole::Print(tr("[CreateComponentsClouds] %1 component(s) were created from cloud '%2'").arg(ccGroup->getChildrenNumber()).arg(cloud->getName())); } cloud->prepareDisplayForRefresh(); //auto-hide original cloud if (ccGroup) { cloud->setEnabled(false); ccConsole::Warning(tr("[CreateComponentsClouds] Original cloud has been automatically hidden")); } } } void MainWindow::doActionLabelConnectedComponents() { //keep only the point clouds! std::vector clouds; { for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) clouds.push_back(ccHObjectCaster::ToGenericPointCloud(entity)); } } size_t count = clouds.size(); if (count == 0) return; static int s_octreeLevel = 8; static unsigned s_minComponentSize = 10; static bool s_randomColors = false; ccLabelingDlg dlg(this); if (count == 1) dlg.octreeLevelSpinBox->setCloud(clouds.front()); dlg.setOctreeLevel(s_octreeLevel); dlg.setMinPointsNb(s_minComponentSize); dlg.setRandomColors(s_randomColors); if (!dlg.exec()) return; s_octreeLevel = dlg.getOctreeLevel(); s_minComponentSize = static_cast(std::max(0, dlg.getMinPointsNb())); s_randomColors = dlg.randomColors(); ccProgressDialog pDlg(false, this); pDlg.setAutoClose(false); //we unselect all entities as we are going to automatically select the created components //(otherwise the user won't perceive the change!) if (m_ccRoot) { m_ccRoot->unselectAllEntities(); } for ( ccGenericPointCloud *cloud : clouds ) { if (cloud && cloud->isA(CC_TYPES::POINT_CLOUD)) { ccPointCloud* pc = static_cast(cloud); ccOctree::Shared theOctree = cloud->getOctree(); if (!theOctree) { ccProgressDialog pOctreeDlg(true, this); theOctree = cloud->computeOctree(&pOctreeDlg); if (!theOctree) { ccConsole::Error(tr("Couldn't compute octree for cloud '%1'!").arg(cloud->getName())); break; } } //we create/activate CCs label scalar field int sfIdx = pc->getScalarFieldIndexByName(CC_CONNECTED_COMPONENTS_DEFAULT_LABEL_NAME); if (sfIdx < 0) { sfIdx = pc->addScalarField(CC_CONNECTED_COMPONENTS_DEFAULT_LABEL_NAME); } if (sfIdx < 0) { ccConsole::Error(tr("Couldn't allocate a new scalar field for computing CC labels! Try to free some memory ...")); break; } pc->setCurrentScalarField(sfIdx); //we try to label all CCs CCCoreLib::ReferenceCloudContainer components; int componentCount = CCCoreLib::AutoSegmentationTools::labelConnectedComponents(cloud, static_cast(s_octreeLevel), false, &pDlg, theOctree.data()); if (componentCount >= 0) { //if successful, we extract each CC (stored in "components") //safety test int realComponentCount = 0; { for (size_t i = 0; i < components.size(); ++i) { if (components[i]->size() >= s_minComponentSize) { ++realComponentCount; } } } if (realComponentCount > 500) { //too many components if (QMessageBox::warning(this, tr("Many components"), tr("Do you really expect up to %1 components?\n(this may take a lot of time to process and display)").arg(realComponentCount), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { //cancel pc->deleteScalarField(sfIdx); if (pc->getNumberOfScalarFields() != 0) { pc->setCurrentDisplayedScalarField(static_cast(pc->getNumberOfScalarFields()) - 1); } else { pc->showSF(false); } pc->prepareDisplayForRefresh(); continue; } } pc->getCurrentInScalarField()->computeMinAndMax(); if (!CCCoreLib::AutoSegmentationTools::extractConnectedComponents(cloud, components)) { ccConsole::Warning(tr("[DoActionLabelConnectedComponents] Something went wrong while extracting CCs from cloud %1...").arg(cloud->getName())); } } else { ccConsole::Warning(tr("[DoActionLabelConnectedComponents] Something went wrong while extracting CCs from cloud %1...").arg(cloud->getName())); } //we delete the CCs label scalar field (we don't need it anymore) pc->deleteScalarField(sfIdx); sfIdx = -1; //we create "real" point clouds for all CCs if (!components.empty()) { createComponentsClouds(cloud, components, s_minComponentSize, s_randomColors, true); } } } refreshAll(); updateUI(); } void MainWindow::doActionSetSFAsCoord() { if (!ccEntityAction::sfSetAsCoord(m_selectedEntities, this)) { return; } zoomOnSelectedEntities(); updateUI(); } void MainWindow::doActionExportCoordToSF() { if (!ccEntityAction::exportCoordToSF(m_selectedEntities, this)) { return; } refreshAll(); updateUI(); } void MainWindow::doActionExportNormalToSF() { if (!ccEntityAction::exportNormalToSF(m_selectedEntities, this)) { return; } refreshAll(); updateUI(); } void MainWindow::doActionSetSFsAsNormal() { if (!haveOneSelection()) { if (haveSelection()) ccConsole::Error(tr("Select only one cloud or one mesh!")); return; } ccHObject* ent = m_selectedEntities.front(); if (!ccEntityAction::setSFsAsNormal(ent, this)) { return; } refreshAll(); updateUI(); } void MainWindow::doMeshTwoPolylines() { if (m_selectedEntities.size() != 2) return; ccPolyline* p1 = ccHObjectCaster::ToPolyline(m_selectedEntities.front()); ccPolyline* p2 = ccHObjectCaster::ToPolyline(m_selectedEntities.back()); if (!p1 || !p2) { ccConsole::Error(tr("Select 2 and only 2 polylines")); return; } //Ask the user how the 2D projection should be computed bool useViewingDir = false; CCVector3 viewingDir(0, 0, 0); if (p1->getDisplay()) { useViewingDir = (QMessageBox::question(this, tr("Projection method"), tr("Use best fit plane (yes) or the current viewing direction (no)"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No); if (useViewingDir) { viewingDir = -p1->getDisplay()->getViewportParameters().getViewDir().toPC(); } } ccMesh* mesh = ccMesh::TriangulateTwoPolylines(p1, p2, useViewingDir ? &viewingDir : nullptr); if (mesh) { addToDB(mesh); if (mesh->computePerVertexNormals()) { mesh->showNormals(true); } else { ccLog::Warning(tr("[Mesh two polylines] Failed to compute normals!")); } if (mesh->getDisplay()) { mesh->getDisplay()->redraw(); } } else { ccLog::Error(tr("Failed to create mesh (see Console)")); forceConsoleDisplay(); } } void MainWindow::doConvertPolylinesToMesh() { if (!haveSelection()) return; std::vector polylines; try { if (haveOneSelection() && m_selectedEntities.back()->isA(CC_TYPES::HIERARCHY_OBJECT)) { ccHObject* obj = m_selectedEntities.back(); for (unsigned i = 0; i < obj->getChildrenNumber(); ++i) { if (obj->getChild(i)->isA(CC_TYPES::POLY_LINE)) polylines.push_back(static_cast(obj->getChild(i))); } } else { for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isA(CC_TYPES::POLY_LINE)) { polylines.push_back(static_cast(entity)); } } } } catch (const std::bad_alloc&) { ccConsole::Error(tr("Not enough memory!")); return; } if (polylines.empty()) { ccConsole::Error(tr("Select a group of polylines or multiple polylines (contour plot)!")); return; } ccPickOneElementDlg poeDlg(tr("Projection dimension"), tr("Contour plot to mesh"), this); poeDlg.addElement("X"); poeDlg.addElement("Y"); poeDlg.addElement("Z"); poeDlg.setDefaultIndex(2); if (!poeDlg.exec()) return; int dim = poeDlg.getSelectedIndex(); assert(dim >= 0 && dim < 3); const unsigned char Z = static_cast(dim); const unsigned char X = (Z == 2 ? 0 : Z + 1); const unsigned char Y = (X == 2 ? 0 : X + 1); //number of segments unsigned segmentCount = 0; unsigned vertexCount = 0; { for ( ccPolyline *poly : polylines ) { if (poly) { //count the total number of vertices and segments vertexCount += poly->size(); segmentCount += poly->segmentCount(); } } } if (segmentCount < 2) { //not enough points/segments ccLog::Error(tr("Not enough segments!")); return; } //we assume we link with CGAL now (if not the call to Delaunay2dMesh::buildMesh will fail anyway) std::vector points2D; std::vector segments2D; try { points2D.reserve(vertexCount); segments2D.reserve(segmentCount * 2); } catch (const std::bad_alloc&) { //not enough memory ccLog::Error(tr("Not enough memory!")); return; } //fill arrays { for ( ccPolyline *poly : polylines ) { if (poly == nullptr) continue; unsigned vertCount = poly->size(); int vertIndex0 = static_cast(points2D.size()); bool closed = poly->isClosed(); for (unsigned v = 0; v < vertCount; ++v) { const CCVector3* P = poly->getPoint(v); int vertIndex = static_cast(points2D.size()); points2D.push_back(CCVector2(P->u[X], P->u[Y])); if (v + 1 < vertCount) { segments2D.push_back(vertIndex); segments2D.push_back(vertIndex + 1); } else if (closed) { segments2D.push_back(vertIndex); segments2D.push_back(vertIndex0); } } } assert(points2D.size() == vertexCount); assert(segments2D.size() == segmentCount * 2); } CCCoreLib::Delaunay2dMesh* delaunayMesh = new CCCoreLib::Delaunay2dMesh; std::string errorStr; if (!delaunayMesh->buildMesh(points2D, segments2D, errorStr)) { ccLog::Error( tr("Third party library error: %1").arg( QString::fromStdString( errorStr ) ) ); delete delaunayMesh; return; } ccPointCloud* vertices = new ccPointCloud("vertices"); if (!vertices->reserve(vertexCount)) { //not enough memory ccLog::Error(tr("Not enough memory!")); delete vertices; delete delaunayMesh; return; } //fill vertices cloud { for ( ccPolyline *poly : polylines ) { unsigned vertCount = poly->size(); for (unsigned v = 0; v < vertCount; ++v) { const CCVector3* P = poly->getPoint(v); vertices->addPoint(*P); } } delaunayMesh->linkMeshWith(vertices, false); } #ifdef QT_DEBUG //Test delaunay output { unsigned vertCount = vertices->size(); for (unsigned i = 0; i < delaunayMesh->size(); ++i) { const CCCoreLib::VerticesIndexes* tsi = delaunayMesh->getTriangleVertIndexes(i); assert(tsi->i1 < vertCount && tsi->i2 < vertCount && tsi->i3 < vertCount); } } #endif ccMesh* mesh = new ccMesh(delaunayMesh, vertices); if (mesh->size() != delaunayMesh->size()) { //not enough memory (error will be issued later) delete mesh; mesh = nullptr; } //don't need this anymore delete delaunayMesh; delaunayMesh = nullptr; if (mesh) { mesh->addChild(vertices); mesh->setVisible(true); vertices->setEnabled(false); addToDB(mesh); if (mesh->computePerVertexNormals()) { mesh->showNormals(true); } else { ccLog::Warning(tr("[Contour plot to mesh] Failed to compute normals!")); } if (mesh->getDisplay()) { mesh->getDisplay()->redraw(); } //global shift & scale (we copy it from the first polyline by default) mesh->copyGlobalShiftAndScale(*polylines.front()); } else { ccLog::Error(tr("Not enough memory!")); delete vertices; vertices = nullptr; } } void MainWindow::doCompute2HalfDimVolume() { if (m_selectedEntities.empty() || m_selectedEntities.size() > 2) { ccConsole::Error(tr("Select one or two point clouds!")); return; } ccGenericPointCloud* cloud1 = nullptr; { ccHObject* ent = m_selectedEntities.front(); if (!ent->isKindOf(CC_TYPES::POINT_CLOUD) ) { ccConsole::Error(tr("Select point clouds only!")); return; } else { cloud1 = ccHObjectCaster::ToGenericPointCloud(ent); } } ccGenericPointCloud* cloud2 = nullptr; if (m_selectedEntities.size() > 1) { ccHObject* ent = m_selectedEntities[1]; if (!ent->isKindOf(CC_TYPES::POINT_CLOUD) ) { ccConsole::Error(tr("Select point clouds only!")); return; } else { cloud2 = ccHObjectCaster::ToGenericPointCloud(ent); } } ccVolumeCalcTool calcVolumeTool(cloud1, cloud2, this); calcVolumeTool.exec(); } void MainWindow::doActionRasterize() { if (!haveOneSelection()) { ccConsole::Error(tr("Select only one point cloud!")); return; } ccHObject* ent = m_selectedEntities.front(); if (!ent->isKindOf(CC_TYPES::POINT_CLOUD) ) { ccConsole::Error(tr("Select a point cloud!")); return; } ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(ent); ccRasterizeTool rasterizeTool(cloud, this); rasterizeTool.exec(); } void MainWindow::doActionDeleteScanGrids() { //look for clouds with scan grids for ( ccHObject *entity : getSelectedEntities() ) { if (!entity || !entity->isA(CC_TYPES::POINT_CLOUD)) { continue; } ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); assert(cloud); if(cloud->gridCount() > 0) { cloud->removeGrids(); } } refreshAll(); updateUI(); } void MainWindow::doActionMeshScanGrids() { //ask the user for the min angle (inside triangles) static double s_meshMinTriangleAngle_deg = 1.0; { bool ok = true; double minAngle_deg = QInputDialog::getDouble(this, tr("Triangulate"), tr("Min triangle angle (in degrees)"), s_meshMinTriangleAngle_deg, 0, 90.0, 3, &ok); if (!ok) return; s_meshMinTriangleAngle_deg = minAngle_deg; } //look for clouds with scan grids for ( ccHObject *entity : getSelectedEntities() ) { if (!entity || !entity->isA(CC_TYPES::POINT_CLOUD)) { continue; } ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); assert(cloud); for (size_t i = 0; i < cloud->gridCount(); ++i) { ccPointCloud::Grid::Shared grid = cloud->grid(i); if (!grid) { assert(false); continue; } ccMesh* gridMesh = cloud->triangulateGrid(*grid, s_meshMinTriangleAngle_deg); if (gridMesh) { cloud->addChild(gridMesh); cloud->setVisible(false); //hide the cloud gridMesh->setDisplay(cloud->getDisplay()); addToDB(gridMesh, false, true, false, false); gridMesh->prepareDisplayForRefresh(); } } } refreshAll(); updateUI(); } void MainWindow::doActionComputeMeshAA() { doActionComputeMesh(CCCoreLib::DELAUNAY_2D_AXIS_ALIGNED); } void MainWindow::doActionComputeMeshLS() { doActionComputeMesh(CCCoreLib::DELAUNAY_2D_BEST_LS_PLANE); } void MainWindow::doActionComputeMesh(CCCoreLib::TRIANGULATION_TYPES type) { //ask the user for the max edge length static double s_meshMaxEdgeLength = 0.0; { bool ok = true; double maxEdgeLength = QInputDialog::getDouble(this, tr("Triangulate"), tr("Max edge length (0 = no limit)"), s_meshMaxEdgeLength, 0, 1.0e9, 8, &ok); if (!ok) return; s_meshMaxEdgeLength = maxEdgeLength; } //select candidates ccHObject::Container clouds; bool hadNormals = false; { for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { clouds.push_back(entity); if (entity->isA(CC_TYPES::POINT_CLOUD)) { hadNormals |= static_cast(entity)->hasNormals(); } } } } //if the cloud(s) already had normals, ask the use if wants to update them or keep them as is (can look strange!) bool updateNormals = false; if (hadNormals) { updateNormals = (QMessageBox::question( this, tr("Keep old normals?"), tr("Cloud(s) already have normals. Do you want to update them (yes) or keep the old ones (no)?"), QMessageBox::Yes, QMessageBox::No ) == QMessageBox::Yes); } ccProgressDialog pDlg(false, this); pDlg.setAutoClose(false); pDlg.setWindowTitle(tr("Triangulation")); pDlg.setInfo(tr("Triangulation in progress...")); pDlg.setRange(0, 0); pDlg.show(); QApplication::processEvents(); bool errors = false; for (size_t i = 0; i < clouds.size(); ++i) { ccHObject* ent = clouds[i]; assert(ent->isKindOf(CC_TYPES::POINT_CLOUD)); //compute mesh ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(ent); ccMesh* mesh = ccMesh::Triangulate( cloud, type, updateNormals, static_cast(s_meshMaxEdgeLength), 2 //XY plane by default ); if (mesh) { cloud->setVisible(false); //can't disable the cloud as the resulting mesh will be its child! cloud->addChild(mesh); cloud->prepareDisplayForRefresh_recursive(); addToDB(mesh); if (i == 0) { m_ccRoot->selectEntity(mesh); //auto-select first element } } else { errors = true; } } if (errors) { ccConsole::Error(tr("Error(s) occurred! See the Console messages")); } refreshAll(); updateUI(); } void MainWindow::doActionFitQuadric() { bool errors = false; //for all selected entities for ( ccHObject *entity : getSelectedEntities() ) { //look for clouds if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity); double rms = 0.0; ccQuadric* quadric = ccQuadric::Fit(cloud, &rms); if (quadric) { cloud->addChild(quadric); quadric->setName(QString("Quadric (%1)").arg(cloud->getName())); quadric->setDisplay(cloud->getDisplay()); quadric->prepareDisplayForRefresh(); quadric->copyGlobalShiftAndScale(*cloud); addToDB(quadric); ccConsole::Print(tr("[DoActionFitQuadric] Quadric local coordinate system:")); ccConsole::Print(quadric->getTransformation().toString(12,' ')); //full precision ccConsole::Print(tr("[DoActionFitQuadric] Quadric equation (in local coordinate system): ") + quadric->getEquationString()); ccConsole::Print(QString("[DoActionFitQuadric] RMS: %1").arg(rms)); #if 0 //test: project the input cloud on the quadric if (cloud->isA(CC_TYPES::POINT_CLOUD)) { ccPointCloud* newCloud = static_cast(cloud)->cloneThis(); if (newCloud) { const PointCoordinateType* eq = quadric->getEquationCoefs(); const Tuple3ub& dims = quadric->getEquationDims(); const unsigned char dX = dims.x; const unsigned char dY = dims.y; const unsigned char dZ = dims.z; const ccGLMatrix& trans = quadric->getTransformation(); ccGLMatrix invTrans = trans.inverse(); for (unsigned i=0; isize(); ++i) { CCVector3* P = const_cast(newCloud->getPoint(i)); CCVector3 Q = invTrans * (*P); Q.u[dZ] = eq[0] + eq[1]*Q.u[dX] + eq[2]*Q.u[dY] + eq[3]*Q.u[dX]*Q.u[dX] + eq[4]*Q.u[dX]*Q.u[dY] + eq[5]*Q.u[dY]*Q.u[dY]; *P = trans * Q; } newCloud->invalidateBoundingBox(); newCloud->setName(newCloud->getName() + ".projection_on_quadric"); addToDB(newCloud); } } #endif } else { ccConsole::Warning(tr("Failed to compute quadric on cloud '%1'").arg(cloud->getName())); errors = true; } } } if (errors) { ccConsole::Error(tr("Error(s) occurred: see console")); } refreshAll(); } void MainWindow::doActionComputeDistanceMap() { static unsigned steps = 128; static double margin = 0.0; static bool filterRange = false; static double range[2] = { 0.0, 1.0 }; //show dialog { QDialog dialog(this); Ui_DistanceMapDialog ui; ui.setupUi(&dialog); ui.stepsSpinBox->setValue(static_cast(steps)); ui.marginDoubleSpinBox->setValue(margin); ui.rangeCheckBox->setChecked(filterRange); ui.minDistDoubleSpinBox->setValue(range[0]); ui.maxDistDoubleSpinBox->setValue(range[1]); if (!dialog.exec()) { return; } steps = static_cast(ui.stepsSpinBox->value()); margin = ui.marginDoubleSpinBox->value(); filterRange = ui.rangeCheckBox->isChecked(); range[0] = ui.minDistDoubleSpinBox->value(); range[1] = ui.maxDistDoubleSpinBox->value(); } ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); for ( ccHObject *entity : getSelectedEntities() ) { if (!entity->isKindOf(CC_TYPES::MESH) && !entity->isKindOf(CC_TYPES::POINT_CLOUD)) { //non handled entity type continue; } //CCCoreLib::ChamferDistanceTransform cdt; CCCoreLib::SaitoSquaredDistanceTransform cdt; if (!cdt.initGrid(Tuple3ui(steps, steps, steps))) { //not enough memory ccLog::Error(tr("Not enough memory!")); return; } ccBBox box = entity->getOwnBB(); PointCoordinateType largestDim = box.getMaxBoxDim() + static_cast(margin); CCVector3 minCorner = box.getCenter() - CCVector3(1, 1, 1) * (largestDim / 2); PointCoordinateType cellDim = largestDim / steps + std::numeric_limits::epsilon(); //to avoid rounding issues when projecting triangles or points inside the grid bool result = false; if (entity->isKindOf(CC_TYPES::MESH)) { ccMesh* mesh = static_cast(entity); result = cdt.initDT(mesh, cellDim, minCorner, &pDlg); } else { ccGenericPointCloud* cloud = static_cast(entity); result = cdt.initDT(cloud, cellDim, minCorner, &pDlg); } if (!result) { ccLog::Error(tr("Not enough memory!")); return; } //cdt.propagateDistance(CHAMFER_345, &pDlg); cdt.propagateDistance(&pDlg); //convert the grid to a cloud ccPointCloud* gridCloud = new ccPointCloud(entity->getName() + QString(".distance_grid(%1)").arg(steps)); { unsigned pointCount = steps*steps*steps; if (!gridCloud->reserve(pointCount)) { ccLog::Error(tr("Not enough memory!")); delete gridCloud; return; } ccScalarField* sf = new ccScalarField("DT values"); if (!sf->reserveSafe(pointCount)) { ccLog::Error(tr("Not enough memory!")); delete gridCloud; sf->release(); return; } for (unsigned i = 0; i < steps; ++i) { for (unsigned j = 0; j < steps; ++j) { for (unsigned k = 0; k < steps; ++k) { ScalarType d = std::sqrt(static_cast(cdt.getValue(i, j, k))) * cellDim; if (!filterRange || (d >= range[0] && d <= range[1])) { gridCloud->addPoint(minCorner + CCVector3(i + 0.5, j + 0.5, k + 0.5) * cellDim); sf->addElement(d); } } } } sf->computeMinAndMax(); int sfIdx = gridCloud->addScalarField(sf); if (gridCloud->size() == 0) { ccLog::Warning(tr("[DistanceMap] Cloud '%1': no point falls inside the specified range").arg(entity->getName())); delete gridCloud; gridCloud = nullptr; } else { gridCloud->setCurrentDisplayedScalarField(sfIdx); gridCloud->showSF(true); gridCloud->setDisplay(entity->getDisplay()); gridCloud->shrinkToFit(); entity->prepareDisplayForRefresh(); addToDB(gridCloud); } } } refreshAll(); } void MainWindow::doActionComputeDistToBestFitQuadric3D() { bool ok = true; int steps = QInputDialog::getInt(this, tr("Distance to best fit quadric (3D)"), tr("Steps (per dim.)"), 50, 10, 10000, 10, &ok); if (!ok) return; for ( ccHObject *entity : getSelectedEntities() ) { if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(entity); CCCoreLib::Neighbourhood Yk(cloud); double Q[10]; if (Yk.compute3DQuadric(Q)) { const double& a = Q[0]; const double& b = Q[1]; const double& c = Q[2]; const double& e = Q[3]; const double& f = Q[4]; const double& g = Q[5]; const double& l = Q[6]; const double& m = Q[7]; const double& n = Q[8]; const double& d = Q[9]; //gravity center const CCVector3* G = Yk.getGravityCenter(); if (!G) { ccConsole::Warning(tr("Failed to get the center of gravity of cloud '%1'!").arg(cloud->getName())); continue; } const ccBBox bbox = cloud->getOwnBB(); PointCoordinateType maxDim = bbox.getMaxBoxDim(); CCVector3 C = bbox.getCenter(); //Sample points on a cube and compute for each of them the distance to the Quadric ccPointCloud* newCloud = new ccPointCloud(); if (!newCloud->reserve(steps*steps*steps)) { ccConsole::Error(tr("Not enough memory!")); } const char defaultSFName[] = "Dist. to 3D quadric"; int sfIdx = newCloud->getScalarFieldIndexByName(defaultSFName); if (sfIdx < 0) sfIdx = newCloud->addScalarField(defaultSFName); if (sfIdx < 0) { ccConsole::Error(tr("Couldn't allocate a new scalar field for computing distances! Try to free some memory ...")); delete newCloud; continue; } ccScalarField* sf = static_cast(newCloud->getScalarField(sfIdx)); assert(sf); for (int x = 0; x < steps; ++x) { CCVector3 P; P.x = C.x + maxDim * (static_cast(x) / static_cast(steps - 1) - CCCoreLib::PC_ONE / 2); for (int y = 0; y < steps; ++y) { P.y = C.y + maxDim * (static_cast(y) / static_cast(steps - 1) - CCCoreLib::PC_ONE / 2); for (int z = 0; z < steps; ++z) { P.z = C.z + maxDim * (static_cast(z) / static_cast(steps - 1) - CCCoreLib::PC_ONE / 2); newCloud->addPoint(P); //compute distance to quadric CCVector3 Pc = P-*G; ScalarType dist = static_cast( a*Pc.x*Pc.x + b*Pc.y*Pc.y + c*Pc.z*Pc.z + e*Pc.x*Pc.y + f*Pc.y*Pc.z + g*Pc.x*Pc.z + l*Pc.x + m*Pc.y + n*Pc.z + d); sf->addElement(dist); } } } if (sf) { sf->computeMinAndMax(); newCloud->setCurrentDisplayedScalarField(sfIdx); newCloud->showSF(true); } newCloud->setName(tr("Distance map to 3D quadric")); newCloud->setDisplay(cloud->getDisplay()); newCloud->prepareDisplayForRefresh(); addToDB(newCloud); } else { ccConsole::Warning(tr("Failed to compute 3D quadric on cloud '%1'").arg(cloud->getName())); } } } refreshAll(); } void MainWindow::doActionComputeCPS() { if (m_selectedEntities.size() != 2) { ccConsole::Error(tr("Select 2 point clouds!")); return; } if (!m_selectedEntities.front()->isKindOf(CC_TYPES::POINT_CLOUD) || !m_selectedEntities.back()->isKindOf(CC_TYPES::POINT_CLOUD)) { ccConsole::Error(tr("Select 2 point clouds!")); return; } ccOrderChoiceDlg dlg( m_selectedEntities.front(), tr("Compared"), m_selectedEntities.back(), tr("Reference"), this ); if (!dlg.exec()) return; ccGenericPointCloud* compCloud = ccHObjectCaster::ToGenericPointCloud(dlg.getFirstEntity()); ccGenericPointCloud* srcCloud = ccHObjectCaster::ToGenericPointCloud(dlg.getSecondEntity()); if (!compCloud->isA(CC_TYPES::POINT_CLOUD)) //TODO { ccConsole::Error(tr("Compared cloud must be a real point cloud!")); return; } ccPointCloud* cmpPC = static_cast(compCloud); static const char DEFAULT_CPS_TEMP_SF_NAME[] = "CPS temporary"; int sfIdx = cmpPC->getScalarFieldIndexByName(DEFAULT_CPS_TEMP_SF_NAME); if (sfIdx < 0) sfIdx = cmpPC->addScalarField(DEFAULT_CPS_TEMP_SF_NAME); if (sfIdx < 0) { ccConsole::Error(tr("Couldn't allocate a new scalar field for computing distances! Try to free some memory ...")); return; } cmpPC->setCurrentScalarField(sfIdx); if (!cmpPC->enableScalarField()) { ccConsole::Error(tr("Not enough memory!")); return; } //cmpPC->forEach(CCCoreLib::ScalarFieldTools::SetScalarValueToNaN); //now done by default by computeCloud2CloudDistances CCCoreLib::ReferenceCloud CPSet(srcCloud); ccProgressDialog pDlg(true, this); CCCoreLib::DistanceComputationTools::Cloud2CloudDistancesComputationParams params; params.CPSet = &CPSet; int result = CCCoreLib::DistanceComputationTools::computeCloud2CloudDistances(compCloud, srcCloud, params, &pDlg); cmpPC->deleteScalarField(sfIdx); if (result >= 0) { ccPointCloud* newCloud = nullptr; //if the source cloud is a "true" cloud, the extracted CPS //will also get its attributes newCloud = srcCloud->isA(CC_TYPES::POINT_CLOUD) ? static_cast(srcCloud)->partialClone(&CPSet) : ccPointCloud::From(&CPSet, srcCloud); newCloud->setName(QString("[%1]->CPSet(%2)").arg(srcCloud->getName(), compCloud->getName())); newCloud->setDisplay(compCloud->getDisplay()); newCloud->prepareDisplayForRefresh(); addToDB(newCloud); //we hide the source cloud (for a clearer display) srcCloud->setEnabled(false); srcCloud->prepareDisplayForRefresh(); } refreshAll(); } void MainWindow::doActionComputeNormals() { if ( !ccEntityAction::computeNormals(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionOrientNormalsMST() { if ( !ccEntityAction::orientNormalsMST(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionOrientNormalsFM() { if ( !ccEntityAction::orientNormalsFM(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionShiftPointsAlongNormals() { std::vector candidates; for (ccHObject* entity : m_selectedEntities) { if (entity && entity->isA(CC_TYPES::POINT_CLOUD)) { ccPointCloud* pc = static_cast(entity); if (pc->hasNormals()) { candidates.push_back(pc); } } } if (candidates.empty()) { ccConsole::Error(QObject::tr("Select at least one point cloud with normals")); return; } // ask the user to define the shift value static double s_shift = 0.0; bool ok = false; double shift = QInputDialog::getDouble(this, tr("Shift along normals"), tr("Shift quantity"), s_shift, -1.0e6, 1.0e6, 6, &ok); if (!ok) { return; } // remember for next time s_shift = shift; for (ccPointCloud* pc : candidates) { pc->shiftPointsAlongNormals(static_cast(shift)); pc->prepareDisplayForRefresh(); } refreshAll(); } void MainWindow::doActionFindBiggestInnerRectangle() { if (!haveSelection()) return; ccHObject* entity = haveOneSelection() ? m_selectedEntities.front() : nullptr; if (!entity || !entity->isKindOf(CC_TYPES::POINT_CLOUD)) { ccConsole::Error(tr("Select one point cloud!")); return; } bool ok; static int s_innerRectDim = 2; int dim = QInputDialog::getInt(this, tr("Dimension"), tr("Orthogonal dim (X=0 / Y=1 / Z=2)"), s_innerRectDim, 0, 2, 1, &ok); if (!ok) return; s_innerRectDim = dim; ccGenericPointCloud* cloud = static_cast(entity); ccBox* box = ccInnerRect2DFinder().process(cloud,static_cast(dim)); if (box) { cloud->addChild(box); box->setVisible(true); box->setDisplay(cloud->getDisplay()); box->setDisplay(cloud->getDisplay()); addToDB(box); } updateUI(); } void MainWindow::doActionFastRegistration(FastRegistrationMode mode) { //we need at least 1 entity if (m_selectedEntities.empty()) return; //we must backup 'm_selectedEntities' as removeObjectTemporarilyFromDBTree can modify it! ccHObject::Container selectedEntities = m_selectedEntities; for (ccHObject *entity : selectedEntities) { ccBBox box = entity->getBB_recursive(); CCVector3 T; //translation switch (mode) { case MoveBBCenterToOrigin: T = -box.getCenter(); break; case MoveBBMinCornerToOrigin: T = -box.minCorner(); break; case MoveBBMaxCornerToOrigin: T = -box.maxCorner(); break; default: assert(false); return; } //transformation (used only for translation) ccGLMatrix glTrans; glTrans.setTranslation(T); forceConsoleDisplay(); ccConsole::Print(tr("[Synchronize] Transformation matrix (%1):").arg(entity->getName())); ccConsole::Print(glTrans.toString(12, ' ')); //full precision ccConsole::Print(tr("Hint: copy it (CTRL+C) and apply it - or its inverse - on any entity with the 'Edit > Apply transformation' tool")); //we temporarily detach the entity, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccHObject::applyGLTransformation ccHObjectContext objContext = removeObjectTemporarilyFromDBTree(entity); entity->applyGLTransformation_recursive(&glTrans); putObjectBackIntoDBTree(entity, objContext); entity->prepareDisplayForRefresh_recursive(); } //reselect previously selected entities! if (m_ccRoot) m_ccRoot->selectEntities(selectedEntities); zoomOnSelectedEntities(); updateUI(); } void MainWindow::doActionMatchBBCenters() { //we need at least 2 entities if (m_selectedEntities.size() < 2) return; //we must backup 'm_selectedEntities' as removeObjectTemporarilyFromDBTree can modify it! ccHObject::Container selectedEntities = m_selectedEntities; //by default, we take the first entity as reference //TODO: maybe the user would like to select the reference himself ;) ccHObject* refEnt = selectedEntities[0]; CCVector3 refCenter = refEnt->getBB_recursive().getCenter(); for (ccHObject *entity : selectedEntities) //warning, getSelectedEntites may change during this loop! { CCVector3 center = entity->getBB_recursive().getCenter(); CCVector3 T = refCenter-center; //transformation (used only for translation) ccGLMatrix glTrans; glTrans += T; forceConsoleDisplay(); ccConsole::Print(tr("[Synchronize] Transformation matrix (%1 --> %2):").arg(entity->getName(),selectedEntities[0]->getName())); ccConsole::Print(glTrans.toString(12,' ')); //full precision ccConsole::Print(tr("Hint: copy it (CTRL+C) and apply it - or its inverse - on any entity with the 'Edit > Apply transformation' tool")); //we temporarily detach the entity, as it may undergo //'severe' modifications (octree deletion, etc.) --> see ccHObject::applyGLTransformation ccHObjectContext objContext = removeObjectTemporarilyFromDBTree(entity); entity->applyGLTransformation_recursive(&glTrans); putObjectBackIntoDBTree(entity, objContext); entity->prepareDisplayForRefresh_recursive(); } //reselect previously selected entities! if (m_ccRoot) m_ccRoot->selectEntities(selectedEntities); zoomOnSelectedEntities(); updateUI(); } //semi-persistent parameters static ccLibAlgorithms::ScaleMatchingAlgorithm s_msAlgorithm = ccLibAlgorithms::PCA_MAX_DIM; static double s_msRmsDiff = 1.0e-5; static int s_msFinalOverlap = 100; void MainWindow::doActionMatchScales() { //we need at least 2 entities if (m_selectedEntities.size() < 2) return; //we must select the point clouds and meshes ccHObject::Container selectedEntities; try { for ( ccHObject *entity : getSelectedEntities() ) { if ( entity->isKindOf(CC_TYPES::POINT_CLOUD) || entity->isKindOf(CC_TYPES::MESH)) { selectedEntities.push_back(entity); } } } catch (const std::bad_alloc&) { ccConsole::Error(tr("Not enough memory!")); return; } ccMatchScalesDlg msDlg(selectedEntities, 0, this); msDlg.setSelectedAlgorithm(s_msAlgorithm); msDlg.rmsDifferenceLineEdit->setText(QString::number(s_msRmsDiff, 'e', 1)); msDlg.overlapSpinBox->setValue(s_msFinalOverlap); if (!msDlg.exec()) return; //save semi-persistent parameters s_msAlgorithm = msDlg.getSelectedAlgorithm(); if (s_msAlgorithm == ccLibAlgorithms::ICP_SCALE) { s_msRmsDiff = msDlg.rmsDifferenceLineEdit->text().toDouble(); s_msFinalOverlap = msDlg.overlapSpinBox->value(); } ccLibAlgorithms::ApplyScaleMatchingAlgorithm( s_msAlgorithm, selectedEntities, s_msRmsDiff, s_msFinalOverlap, msDlg.getSelectedIndex(), this); //reselect previously selected entities! if (m_ccRoot) m_ccRoot->selectEntities(selectedEntities); refreshAll(); updateUI(); } void MainWindow::doActionSORFilter() { ccSORFilterDlg sorDlg(this); //set semi-persistent/dynamic parameters static int s_sorFilterKnn = 6; static double s_sorFilterNSigma = 1.0; sorDlg.setKNN(s_sorFilterKnn); sorDlg.setNSigma(s_sorFilterNSigma); if (!sorDlg.exec()) { return; } //update semi-persistent/dynamic parameters s_sorFilterKnn = sorDlg.KNN(); s_sorFilterNSigma = sorDlg.nSigma(); ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); bool firstCloud = true; ccHObject::Container selectedEntities = getSelectedEntities(); //we have to use a local copy: 'selectEntity' will change the set of currently selected entities! for (ccHObject* entity : selectedEntities) { //specific test for locked vertices bool lockedVertices; ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity, &lockedVertices); if (!cloud) { continue; } if (lockedVertices) { ccUtils::DisplayLockedVerticesWarning(entity->getName(), haveOneSelection()); continue; } //computation CCCoreLib::ReferenceCloud* selection = CCCoreLib::CloudSamplingTools::sorFilter(cloud, s_sorFilterKnn, s_sorFilterNSigma, cloud->getOctree().data(), &pDlg); if (selection) { if (selection->size() == cloud->size()) { ccLog::Warning(tr("[DoActionSORFilter] No points were removed from cloud '%1'").arg(cloud->getName())); } else { ccPointCloud* cleanCloud = cloud->partialClone(selection); if (cleanCloud) { cleanCloud->setName(cloud->getName() + QString(".clean")); cleanCloud->setDisplay(cloud->getDisplay()); if (cloud->getParent()) cloud->getParent()->addChild(cleanCloud); addToDB(cleanCloud); cloud->setEnabled(false); if (firstCloud) { ccConsole::Warning(tr("Previously selected entities (sources) have been hidden!")); firstCloud = false; m_ccRoot->selectEntity(cleanCloud, true); } } else { ccConsole::Warning(tr("[DoActionSORFilter] Not enough memory to create a clean version of cloud '%1'!").arg(cloud->getName())); } } delete selection; selection = nullptr; } else { //no points fall inside selection! ccConsole::Warning(tr("[DoActionSORFilter] Failed to apply the noise filter to cloud '%1'! (not enough memory?)").arg(cloud->getName())); } } refreshAll(); updateUI(); } void MainWindow::doActionFilterNoise() { double kernelRadius = ccLibAlgorithms::GetDefaultCloudKernelSize(m_selectedEntities); ccNoiseFilterDlg noiseDlg(this); //set semi-persistent/dynamic parameters static bool s_noiseFilterUseKnn = false; static int s_noiseFilterKnn = 6; static bool s_noiseFilterUseAbsError = false; static double s_noiseFilterAbsError = 1.0; static double s_noiseFilterNSigma = 1.0; static bool s_noiseFilterRemoveIsolatedPoints = false; noiseDlg.radiusDoubleSpinBox->setValue(kernelRadius); noiseDlg.knnSpinBox->setValue(s_noiseFilterKnn); noiseDlg.nSigmaDoubleSpinBox->setValue(s_noiseFilterNSigma); noiseDlg.absErrorDoubleSpinBox->setValue(s_noiseFilterAbsError); noiseDlg.removeIsolatedPointsCheckBox->setChecked(s_noiseFilterRemoveIsolatedPoints); if (s_noiseFilterUseAbsError) noiseDlg.absErrorRadioButton->setChecked(true); else noiseDlg.relativeRadioButton->setChecked(true); if (s_noiseFilterUseKnn) noiseDlg.knnRadioButton->setChecked(true); else noiseDlg.radiusRadioButton->setChecked(true); if (!noiseDlg.exec()) return; //update semi-persistent/dynamic parameters kernelRadius = noiseDlg.radiusDoubleSpinBox->value(); s_noiseFilterUseKnn = noiseDlg.knnRadioButton->isChecked(); s_noiseFilterKnn = noiseDlg.knnSpinBox->value(); s_noiseFilterUseAbsError = noiseDlg.absErrorRadioButton->isChecked(); s_noiseFilterNSigma = noiseDlg.nSigmaDoubleSpinBox->value(); s_noiseFilterAbsError = noiseDlg.absErrorDoubleSpinBox->value(); s_noiseFilterRemoveIsolatedPoints = noiseDlg.removeIsolatedPointsCheckBox->isChecked(); ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); bool firstCloud = true; ccHObject::Container selectedEntities = getSelectedEntities(); //we have to use a local copy: and 'selectEntity' will change the set of currently selected entities! for ( ccHObject *entity : selectedEntities ) { //specific test for locked vertices bool lockedVertices; ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity, &lockedVertices); if (!cloud) { continue; } if (lockedVertices) { ccUtils::DisplayLockedVerticesWarning(entity->getName(), haveOneSelection()); continue; } //computation CCCoreLib::ReferenceCloud* selection = CCCoreLib::CloudSamplingTools::noiseFilter( cloud, static_cast(kernelRadius), s_noiseFilterNSigma, s_noiseFilterRemoveIsolatedPoints, s_noiseFilterUseKnn, s_noiseFilterKnn, s_noiseFilterUseAbsError, s_noiseFilterAbsError, cloud->getOctree().data(), &pDlg); if (selection) { if (selection->size() == cloud->size()) { ccLog::Warning(tr("[DoActionFilterNoise] No points were removed from cloud '%1'").arg(cloud->getName())); } else { ccPointCloud* cleanCloud = cloud->partialClone(selection); if (cleanCloud) { cleanCloud->setName(cloud->getName() + QString(".clean")); cleanCloud->setDisplay(cloud->getDisplay()); if (cloud->getParent()) cloud->getParent()->addChild(cleanCloud); addToDB(cleanCloud); cloud->setEnabled(false); if (firstCloud) { ccConsole::Warning(tr("Previously selected entities (sources) have been hidden!")); firstCloud = false; m_ccRoot->selectEntity(cleanCloud, true); } } else { ccConsole::Warning(tr("[DoActionFilterNoise] Not enough memory to create a clean version of cloud '%1'!").arg(cloud->getName())); } } delete selection; selection = nullptr; } else { //no points fall inside selection! ccConsole::Warning(tr("[DoActionFilterNoise] Failed to apply the noise filter to cloud '%1'! (not enough memory?)").arg(cloud->getName())); } } refreshAll(); updateUI(); } void MainWindow::doActionUnroll() { //there should be only one point cloud or one mesh! if (!haveOneSelection()) { ccConsole::Error(tr("Select one and only one entity!")); return; } //if selected entity is a mesh, the method will be applied to its vertices bool lockedVertices; ccGenericPointCloud* cloud = ccHObjectCaster::ToGenericPointCloud(m_selectedEntities.front(), &lockedVertices); if (lockedVertices) { ccUtils::DisplayLockedVerticesWarning(m_selectedEntities.front()->getName(), true); return; } //for "real" point clouds only if (!cloud || !cloud->isA(CC_TYPES::POINT_CLOUD)) { ccConsole::Error(tr("Method can't be applied on locked vertices or virtual point clouds!")); return; } ccPointCloud* pc = static_cast(cloud); ccUnrollDlg unrollDlg(m_ccRoot ? m_ccRoot->getRootEntity() : nullptr, this); unrollDlg.fromPersistentSettings(); if (!unrollDlg.exec()) return; unrollDlg.toPersistentSettings(); ccPointCloud::UnrollMode mode = unrollDlg.getType(); PointCoordinateType radius = static_cast(unrollDlg.getRadius()); CCVector3d axisDir = unrollDlg.getAxis(); bool exportDeviationSF = unrollDlg.exportDeviationSF(); bool arbitraryOutputCS = unrollDlg.useArbitraryOutputCS(); CCVector3 center = unrollDlg.getAxisPosition(); //let's rock unroll ;) ccProgressDialog pDlg(true, this); double startAngle_deg = 0.0; double stopAngle_deg = 360.0; unrollDlg.getAngleRange(startAngle_deg, stopAngle_deg); if (startAngle_deg >= stopAngle_deg) { QMessageBox::critical(this, tr("Error"), tr("Invalid angular range")); return; } ccPointCloud* output = nullptr; switch (mode) { case ccPointCloud::CYLINDER: { ccPointCloud::UnrollCylinderParams params; params.radius = radius; params.axisDir = CCVector3::fromArray(axisDir.u); if (unrollDlg.isAxisPositionAuto()) { center = pc->getOwnBB().getCenter(); } params.center = center; output = pc->unroll(mode, ¶ms, exportDeviationSF, startAngle_deg, stopAngle_deg, arbitraryOutputCS, &pDlg); } break; case ccPointCloud::CONE_CONICAL: case ccPointCloud::CONE_CYLINDRICAL_FIXED_RADIUS: case ccPointCloud::CONE_CYLINDRICAL_ADAPTIVE_RADIUS: { ccPointCloud::UnrollConeParams params; params.radius = (mode == ccPointCloud::CONE_CYLINDRICAL_FIXED_RADIUS ? radius : 0); params.apex = center; params.coneAngle_deg = unrollDlg.getConeHalfAngle(); params.axisDir = CCVector3::fromArray(axisDir.u); params.spanRatio = unrollDlg.getConicalProjSpanRatio(); output = pc->unroll(mode, ¶ms, exportDeviationSF, startAngle_deg, stopAngle_deg, arbitraryOutputCS, &pDlg); } break; default: assert(false); break; } if (output) { if (m_selectedEntities.front()->isA(CC_TYPES::MESH)) { ccMesh* mesh = ccHObjectCaster::ToMesh(m_selectedEntities.front()); mesh->setEnabled(false); ccConsole::Warning(tr("[Unroll] Original mesh has been automatically hidden")); ccMesh* outputMesh = mesh->cloneMesh(output); outputMesh->addChild(output); addToDB(outputMesh, true, true, false, true); outputMesh->setEnabled(true); outputMesh->setVisible(true); } else { pc->setEnabled(false); ccConsole::Warning(tr("[Unroll] Original cloud has been automatically hidden")); if (pc->getParent()) { pc->getParent()->addChild(output); } addToDB(output, true, true, false, true); } updateUI(); } } ccGLWindowInterface* MainWindow::getActiveGLWindow() { if (!m_mdiArea) { return nullptr; } QMdiSubWindow *activeSubWindow = m_mdiArea->activeSubWindow(); if (activeSubWindow) { return ccGLWindowInterface::FromWidget(activeSubWindow->widget()); } else { QList subWindowList = m_mdiArea->subWindowList(); if (!subWindowList.isEmpty()) { return ccGLWindowInterface::FromWidget(subWindowList[0]->widget()); } } return nullptr; } QMdiSubWindow* MainWindow::getMDISubWindow(ccGLWindowInterface* win) { QList subWindowList = m_mdiArea->subWindowList(); for (int i = 0; i < subWindowList.size(); ++i) { if (ccGLWindowInterface::FromWidget(subWindowList[i]->widget()) == win) return subWindowList[i]; } //not found! return nullptr; } ccGLWindowInterface* MainWindow::getGLWindow(int index) const { QList subWindowList = m_mdiArea->subWindowList(); if (index >= 0 && index < subWindowList.size()) { ccGLWindowInterface* win = ccGLWindowInterface::FromWidget(subWindowList[index]->widget()); assert(win); return win; } else { assert(false); return nullptr; } } int MainWindow::getGLWindowCount() const { return m_mdiArea ? m_mdiArea->subWindowList().size() : 0; } void MainWindow::zoomIn() { ccGLWindowInterface* win = MainWindow::getActiveGLWindow(); if (win) { //we simulate a real wheel event win->onWheelEvent(15.0f); } } void MainWindow::zoomOut() { ccGLWindowInterface* win = MainWindow::getActiveGLWindow(); if (win) { //we simulate a real wheel event win->onWheelEvent(-15.0f); } } ccGLWindowInterface* MainWindow::new3DViewInternal( bool allowEntitySelection ) { assert(m_ccRoot && m_mdiArea); QWidget* viewWidget = nullptr; ccGLWindowInterface* view3D = nullptr; createGLWindow(view3D, viewWidget); if (!viewWidget || !view3D) { ccLog::Error(tr("Failed to create the 3D view")); assert(false); return nullptr; } //restore options { QSettings settings; bool autoPickRotationCenter = settings.value(ccPS::AutoPickRotationCenter(), true).toBool(); view3D->setAutoPickPivotAtCenter(autoPickRotationCenter); } viewWidget->setMinimumSize(400, 300); m_mdiArea->addSubWindow(viewWidget); if ( allowEntitySelection ) { connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::entitySelectionChanged, this, [=] (ccHObject *entity) { m_ccRoot->selectEntity( entity ); }); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::entitiesSelectionChanged, this, [=] (std::unordered_set entities){ m_ccRoot->selectEntities( entities ); }); } //'echo' mode connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::mouseWheelRotated, this, &MainWindow::echoMouseWheelRotate); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::viewMatRotated, this, &MainWindow::echoBaseViewMatRotation); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::cameraPosChanged, this, &MainWindow::echoCameraPosChanged); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::pivotPointChanged, this, &MainWindow::echoPivotPointChanged); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::aboutToClose, this, &MainWindow::prepareWindowDeletion); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::filesDropped, this, &MainWindow::addToDBAuto, Qt::QueuedConnection); //DGM: we don't want to block the 'dropEvent' method of ccGLWindow instances! connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::newLabel, this, &MainWindow::handleNewLabel); connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::exclusiveFullScreenToggled, this, &MainWindow::onExclusiveFullScreenToggled); if (m_pickingHub) { //we must notify the picking hub as well if the window is destroyed connect(view3D->signalEmitter(), &ccGLWindowSignalEmitter::aboutToClose, m_pickingHub, &ccPickingHub::onActiveWindowDeleted); } view3D->setSceneDB(m_ccRoot->getRootEntity()); viewWidget->setAttribute(Qt::WA_DeleteOnClose); m_ccRoot->updatePropertiesView(); QMainWindow::statusBar()->showMessage(tr("New 3D View"), 2000); viewWidget->showMaximized(); viewWidget->update(); return view3D; } void MainWindow::prepareWindowDeletion(ccGLWindowInterface* glWindow) { if (!m_ccRoot) return; if (glWindow) { m_ccRoot->hidePropertiesView(); m_ccRoot->getRootEntity()->removeFromDisplay_recursive(glWindow); m_ccRoot->updatePropertiesView(); } else { assert(false); } } static bool s_autoSaveGuiElementPos = true; void MainWindow::doActionResetGUIElementsPos() { //show the user it will be maximized showMaximized(); QSettings settings; settings.remove(ccPS::MainWinGeom()); settings.remove(ccPS::MainWinState()); QMessageBox::information( this, tr("Restart"), tr("To finish the process, you'll have to close and restart CloudCompare") ); //to avoid saving them right away! s_autoSaveGuiElementPos = false; } void MainWindow::doActionToggleRestoreWindowOnStartup(bool state) { QSettings settings; settings.setValue(ccPS::DoNotRestoreWindowGeometry(), !state); } void MainWindow::doActionResetAllVBOs() { ccHObject::Container clouds; m_ccRoot->getRootEntity()->filterChildren(clouds, true, CC_TYPES::POINT_CLOUD, true); size_t releasedSize = 0; for (ccHObject* entity : clouds) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (cloud) { releasedSize += cloud->vboSize(); cloud->releaseVBOs(); } } if (releasedSize != 0) { ccLog::Print(tr("All VBOs have been released (%1 Mb)").arg(releasedSize / static_cast(1 << 20), 0, 'f', 2)); if (ccGui::Parameters().useVBOs) { ccLog::Warning(tr("You might want to disable the 'use VBOs' option in the Display Settings to keep the GPU memory empty")); } } else { ccLog::Print(tr("No VBO allocated")); } } void MainWindow::restoreGUIElementsPos() { QSettings settings; QVariant previousState = settings.value(ccPS::MainWinState()); if (previousState.isValid()) { restoreState(previousState.toByteArray()); } QVariant previousGeometry; if (!settings.value(ccPS::DoNotRestoreWindowGeometry(), false).toBool()) { previousGeometry = settings.value(ccPS::MainWinGeom()); } if (previousGeometry.isValid()) { restoreGeometry(previousGeometry.toByteArray()); } else { showMaximized(); } if (isFullScreen()) { m_UI->actionFullScreen->blockSignals(true); m_UI->actionFullScreen->setChecked(true); m_UI->actionFullScreen->blockSignals(false); } } void MainWindow::showEvent(QShowEvent* event) { if (m_firstShow) { restoreGUIElementsPos(); m_firstShow = false; } QMainWindow::showEvent(event); } void MainWindow::closeEvent(QCloseEvent* event) { const ccOptions &opts = ccOptions::Instance(); // If we don't have anything displayed, then just close... if (!opts.confirmQuit || (m_ccRoot && (m_ccRoot->getRootEntity()->getChildrenNumber() == 0))) { event->accept(); } else // ...otherwise confirm { QMessageBox message_box( QMessageBox::Question, tr("Quit"), tr("Are you sure you want to quit?"), QMessageBox::NoButton, this); message_box.addButton(QMessageBox::Button::Yes); message_box.addButton(tr("Yes, don't ask again"), QMessageBox::ButtonRole::YesRole); message_box.addButton(QMessageBox::Button::No); message_box.exec(); switch (message_box.buttons().indexOf(message_box.clickedButton())) { case 1: // Yes, don't ask again { ccOptions optsCopied = opts; optsCopied.confirmQuit = false; optsCopied.toPersistentSettings(); Q_FALLTHROUGH(); // Fallthrough to the yes case, to accept the event } case 0: // Yes event->accept(); break; case 2: // No default: event->ignore(); } } if (s_autoSaveGuiElementPos) { saveGUIElementsPos(); } } void MainWindow::saveGUIElementsPos() { //save the state as settings QSettings settings; settings.setValue(ccPS::MainWinGeom(), saveGeometry()); settings.setValue(ccPS::MainWinState(), saveState()); } void MainWindow::moveEvent(QMoveEvent* event) { QMainWindow::moveEvent(event); updateOverlayDialogsPlacement(); } void MainWindow::resizeEvent(QResizeEvent* event) { QMainWindow::resizeEvent(event); updateOverlayDialogsPlacement(); } void MainWindow::registerOverlayDialog(ccOverlayDialog* dlg, Qt::Corner pos) { //check for existence for (ccMDIDialogs& mdi : m_mdiDialogs) { if (mdi.dialog == dlg) { //we only update its position in this case mdi.position = pos; repositionOverlayDialog(mdi); return; } } //otherwise we add it to DB m_mdiDialogs.push_back(ccMDIDialogs(dlg, pos)); //automatically update the dialog placement when its shown connect(dlg, &ccOverlayDialog::shown, this, [=]() { //check for existence for (ccMDIDialogs& mdi : m_mdiDialogs) { if (mdi.dialog == dlg) { repositionOverlayDialog(mdi); break; } } }); repositionOverlayDialog(m_mdiDialogs.back()); } void MainWindow::unregisterOverlayDialog(ccOverlayDialog* dialog) { for (std::vector::iterator it = m_mdiDialogs.begin(); it != m_mdiDialogs.end(); ++it) { if (it->dialog == dialog) { m_mdiDialogs.erase(it); break; } } } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::Resize: case QEvent::Move: updateOverlayDialogsPlacement(); break; default: //nothing to do break; } // standard event processing return QObject::eventFilter(obj, event); } void MainWindow::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Escape: { if ( s_pickingWindow != nullptr ) { cancelPreviousPickingOperation( true ); } break; } default: QMainWindow::keyPressEvent(event); } } void MainWindow::updateOverlayDialogsPlacement() { for (ccMDIDialogs& mdiDlg : m_mdiDialogs) { repositionOverlayDialog(mdiDlg); } } void MainWindow::repositionOverlayDialog(ccMDIDialogs& mdiDlg) { if (!mdiDlg.dialog || !mdiDlg.dialog->isVisible() || !m_mdiArea) return; int dx = 0; int dy = 0; static const int margin = 5; switch (mdiDlg.position) { case Qt::TopLeftCorner: dx = margin; dy = margin; break; case Qt::TopRightCorner: dx = std::max(margin, m_mdiArea->width() - mdiDlg.dialog->width() - margin); dy = margin; break; case Qt::BottomLeftCorner: dx = margin; dy = std::max(margin, m_mdiArea->height() - mdiDlg.dialog->height() - margin); break; case Qt::BottomRightCorner: dx = std::max(margin, m_mdiArea->width() - mdiDlg.dialog->width() - margin); dy = std::max(margin, m_mdiArea->height() - mdiDlg.dialog->height() - margin); break; } //show(); mdiDlg.dialog->move(m_mdiArea->mapToGlobal(QPoint(dx, dy))); mdiDlg.dialog->raise(); } void MainWindow::toggleVisualDebugTraces() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->toggleDebugTrace(); win->redraw(false, false); } } void MainWindow::toggleFullScreen(bool state) { if (state) showFullScreen(); else showNormal(); #ifdef Q_OS_MAC if ( state ) { m_UI->actionFullScreen->setText( tr( "Exit Full Screen" ) ); } else { m_UI->actionFullScreen->setText( tr( "Enter Full Screen" ) ); } #endif } void MainWindow::toggleExclusiveFullScreen(bool state) { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->toggleExclusiveFullScreen(state); } } void MainWindow::doActionShowHelpDialog() { QMessageBox messageBox; messageBox.setTextFormat(Qt::RichText); messageBox.setWindowTitle("Documentation"); messageBox.setText("Please look at the wiki"); messageBox.setStandardButtons(QMessageBox::Ok); messageBox.exec(); } void MainWindow::freezeUI(bool state) { //freeze standard plugins m_UI->toolBarMainTools->setDisabled(state); m_UI->toolBarSFTools->setDisabled(state); m_pluginUIManager->mainPluginToolbar()->setDisabled(state); //freeze plugin toolbars for ( QToolBar *toolbar : m_pluginUIManager->additionalPluginToolbars() ) { toolbar->setDisabled(state); } m_UI->DockableDBTree->setDisabled(state); m_UI->menubar->setDisabled(state); if (state) { m_UI->menuEdit->setDisabled(true); m_UI->menuTools->setDisabled(true); } else { updateMenus(); } m_uiFrozen = state; } void MainWindow::activateRegisterPointPairTool() { if (!haveSelection()) { ccConsole::Error(tr("Select at least one entity (point cloud or mesh)!")); return; } ccHObject::Container alignedEntities; ccHObject::Container refEntities; try { ccHObject::Container entities; entities.reserve(m_selectedEntities.size()); for (ccHObject* entity : m_selectedEntities) { //for now, we only handle clouds or meshes if (entity->isKindOf(CC_TYPES::POINT_CLOUD) || entity->isKindOf(CC_TYPES::MESH)) { entities.push_back(entity); } } if (entities.empty()) { ccConsole::Error("Select at least one entity (point cloud or mesh)!"); return; } else if (entities.size() == 1) { alignedEntities = entities; } else { std::vector indexes; if (!ccEntitySelectionDialog::SelectEntities(entities, indexes, this, tr("Select to-be-aligned entities"))) { //process cancelled by the user return; } //add the selected indexes as 'aligned' entities alignedEntities.reserve(indexes.size()); for (size_t i = 0; i < indexes.size(); ++i) { alignedEntities.push_back(entities[indexes[i]]); } //add the others as 'reference' entities assert(indexes.size() <= entities.size()); refEntities.reserve(entities.size() - indexes.size()); for (size_t i = 0; i < entities.size(); ++i) { if (std::find(indexes.begin(), indexes.end(), i) == indexes.end()) { refEntities.push_back(entities[i]); } } } } catch (const std::bad_alloc&) { ccLog::Error(tr("Not enough memory")); return; } if (alignedEntities.empty()) { ccLog::Error(tr("No to-be-aligned entity selected")); return; } //deselect all entities if (m_ccRoot) { m_ccRoot->unselectAllEntities(); } ccGLWindowInterface* win = new3DView(); if (!win) { ccLog::Error(tr("[PointPairRegistration] Failed to create dedicated 3D view!")); return; } //we disable all windows disableAllBut(win); if (!m_pprDlg) { m_pprDlg = new ccPointPairRegistrationDlg(m_pickingHub, this, this); connect(m_pprDlg, &ccOverlayDialog::processFinished, this, &MainWindow::deactivateRegisterPointPairTool); registerOverlayDialog(m_pprDlg, Qt::TopRightCorner); } if (!m_pprDlg->init(win, alignedEntities, &refEntities)) { deactivateRegisterPointPairTool(false); } freezeUI(true); if (!m_pprDlg->start()) deactivateRegisterPointPairTool(false); else updateOverlayDialogsPlacement(); } void MainWindow::deactivateRegisterPointPairTool(bool state) { if (m_pprDlg) m_pprDlg->clear(); //we enable all GL windows enableAll(); QList subWindowList = m_mdiArea->subWindowList(); if (!subWindowList.isEmpty()) subWindowList.first()->showMaximized(); freezeUI(false); updateUI(); setGlobalZoom(); } void MainWindow::activateSectionExtractionMode() { if (!haveSelection()) return; if (!m_seTool) { m_seTool = new ccSectionExtractionTool(this); connect(m_seTool, &ccOverlayDialog::processFinished, this, &MainWindow::deactivateSectionExtractionMode); registerOverlayDialog(m_seTool, Qt::TopRightCorner); } //add clouds ccGLWindowInterface* firstDisplay = nullptr; { unsigned validCount = 0; for (ccHObject *entity : getSelectedEntities()) { if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { if (m_seTool->addCloud(static_cast(entity))) { if (!firstDisplay && entity->getDisplay()) { firstDisplay = static_cast(entity->getDisplay()); } ++validCount; } } } if (validCount == 0) { ccConsole::Error(tr("No cloud in selection!")); return; } } //deselect all entities if (m_ccRoot) { m_ccRoot->unselectAllEntities(); } ccGLWindowInterface* win = new3DViewInternal(false); if (!win) { ccLog::Error(tr("[SectionExtraction] Failed to create dedicated 3D view!")); return; } if (firstDisplay && firstDisplay->getGlFilter()) { win->setGlFilter(firstDisplay->getGlFilter()->clone()); } m_seTool->linkWith(win); freezeUI(true); m_UI->toolBarView->setDisabled(true); //we disable all other windows disableAllBut(win); if (!m_seTool->start()) deactivateSectionExtractionMode(false); else updateOverlayDialogsPlacement(); } void MainWindow::deactivateSectionExtractionMode(bool state) { if (m_seTool) m_seTool->removeAllEntities(); //we enable all GL windows enableAll(); QList subWindowList = m_mdiArea->subWindowList(); if (!subWindowList.isEmpty()) subWindowList[0]->showMaximized(); freezeUI(false); m_UI->toolBarView->setDisabled(false); updateUI(); ccGLWindowInterface* win = getActiveGLWindow(); if (win) win->redraw(); } void MainWindow::activateSegmentationMode() { ccGLWindowInterface* win = getActiveGLWindow(); if (!win) return; if (!haveSelection()) return; if (!m_gsTool) { m_gsTool = new ccGraphicalSegmentationTool(this); connect(m_gsTool, &ccOverlayDialog::processFinished, this, &MainWindow::deactivateSegmentationMode); registerOverlayDialog(m_gsTool, Qt::TopRightCorner); } m_gsTool->linkWith(win); for ( ccHObject *entity : getSelectedEntities() ) { m_gsTool->addEntity(entity); } if (m_gsTool->getNumberOfValidEntities() == 0) { ccConsole::Error(tr("No segmentable entity in active window!")); return; } freezeUI(true); m_UI->toolBarView->setDisabled(false); //we disable all other windows disableAllBut(win); if (!m_gsTool->start()) deactivateSegmentationMode(false); else updateOverlayDialogsPlacement(); } void MainWindow::deactivateSegmentationMode(bool state) { if (!m_gsTool) { assert(false); return; } bool deleteHiddenParts = false; //shall we apply segmentation? if (state) { ccHObject::Container result; m_gsTool->applySegmentation(this, result); if (m_ccRoot) { m_ccRoot->selectEntities(result); } } else { m_gsTool->removeAllEntities(); } //we enable all GL windows enableAll(); freezeUI(false); updateUI(); ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->redraw(); } } void MainWindow::activateTracePolylineMode() { ccGLWindowInterface* win = getActiveGLWindow(); if (!win) { return; } if (!m_tplTool) { m_tplTool = new ccTracePolylineTool(m_pickingHub, this); connect(m_tplTool, &ccOverlayDialog::processFinished, this, &MainWindow::deactivateTracePolylineMode); registerOverlayDialog(m_tplTool, Qt::TopRightCorner); } m_tplTool->linkWith(win); freezeUI(true); m_UI->toolBarView->setDisabled(false); //we disable all other windows disableAllBut(win); if (!m_tplTool->start()) deactivateTracePolylineMode(false); else updateOverlayDialogsPlacement(); } void MainWindow::deactivateTracePolylineMode(bool) { //we enable all GL windows enableAll(); freezeUI(false); updateUI(); ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->redraw(); } } void MainWindow::activatePointListPickingMode() { ccGLWindowInterface* win = getActiveGLWindow(); if (!win) return; //there should be only one point cloud in current selection! if (!haveOneSelection()) { ccConsole::Error(tr("Select one and only one entity!")); return; } ccHObject* entity = m_selectedEntities.front(); if (!entity->isKindOf(CC_TYPES::POINT_CLOUD) && !entity->isKindOf(CC_TYPES::MESH)) { ccConsole::Error(tr("Select a cloud or a mesh")); return; } if (!entity->isVisible() || !entity->isEnabled()) { ccConsole::Error(tr("Entity must be visible!")); return; } if (!m_plpDlg) { m_plpDlg = new ccPointListPickingDlg(m_pickingHub, this); connect(m_plpDlg, &ccOverlayDialog::processFinished, this, &MainWindow::deactivatePointListPickingMode); registerOverlayDialog(m_plpDlg, Qt::TopRightCorner); } //DGM: we must update marker size spin box value (as it may have changed by the user with the "display dialog") m_plpDlg->markerSizeSpinBox->setValue(win->getDisplayParameters().labelMarkerSize); m_plpDlg->linkWith(win); m_plpDlg->linkWithEntity(entity); freezeUI(true); //we disable all other windows disableAllBut(win); if (!m_plpDlg->start()) deactivatePointListPickingMode(false); else updateOverlayDialogsPlacement(); } void MainWindow::deactivatePointListPickingMode(bool state) { if (m_plpDlg) { m_plpDlg->linkWithEntity(nullptr); } //we enable all GL windows enableAll(); freezeUI(false); updateUI(); } void MainWindow::activatePointPickingMode() { ccGLWindowInterface* win = getActiveGLWindow(); if (!win) { return; } if (m_ccRoot) { m_ccRoot->unselectAllEntities(); //we don't want any entity selected (especially existing labels!) } if (!m_ppDlg) { m_ppDlg = new ccPointPropertiesDlg(m_pickingHub, this); connect(m_ppDlg, &ccOverlayDialog::processFinished, this, &MainWindow::deactivatePointPickingMode); connect(m_ppDlg, &ccPointPropertiesDlg::newLabel, this, &MainWindow::handleNewLabel); registerOverlayDialog(m_ppDlg, Qt::TopRightCorner); } m_ppDlg->linkWith(win); freezeUI(true); //we disable all other windows disableAllBut(win); if (!m_ppDlg->start()) deactivatePointPickingMode(false); else updateOverlayDialogsPlacement(); } void MainWindow::deactivatePointPickingMode(bool state) { //if (m_ppDlg) // m_ppDlg->linkWith(nullptr); //we enable all GL windows enableAll(); freezeUI(false); updateUI(); redrawAll(); } void MainWindow::activateClippingBoxMode() { if ( !haveSelection() ) { return; } ccGLWindowInterface* win = getActiveGLWindow(); if (!win) { return; } if (!m_clipTool) { m_clipTool = new ccClippingBoxTool(this); connect(m_clipTool, &ccOverlayDialog::processFinished, this, &MainWindow::deactivateClippingBoxMode); } m_clipTool->linkWith(win); ccHObject::Container selectedEntities = getSelectedEntities(); //we have to use a local copy: 'unselectEntity' will change the set of currently selected entities! for (ccHObject *entity : selectedEntities) { if (m_clipTool->addAssociatedEntity(entity)) { //automatically deselect the entity (to avoid seeing its bounding box ;) m_ccRoot->unselectEntity(entity); } } if (m_clipTool->getNumberOfAssociatedEntity() == 0) { m_clipTool->close(); return; } if (m_clipTool->start()) { registerOverlayDialog(m_clipTool, Qt::TopRightCorner); freezeUI(true); updateOverlayDialogsPlacement(); //deactivate all other GL windows disableAllBut(win); } else { ccConsole::Error(tr("Unexpected error!")); //indeed... } } void MainWindow::deactivateClippingBoxMode(bool state) { //we reactivate all GL windows enableAll(); freezeUI(false); updateUI(); } void MainWindow::activateTranslateRotateMode() { if (!haveSelection()) return; ccGLWindowInterface* win = getActiveGLWindow(); if (!win) return; if (!m_transTool) m_transTool = new ccGraphicalTransformationTool(this); assert(m_transTool->getNumberOfValidEntities() == 0); m_transTool->linkWith(win); bool rejectedEntities = false; for ( ccHObject *entity : getSelectedEntities() ) { if (!m_transTool->addEntity(entity)) rejectedEntities = true; } if (m_transTool->getNumberOfValidEntities() == 0) { ccConsole::Error(tr("No entity eligible for manual transformation! (see console)")); return; } else if (rejectedEntities) { ccConsole::Error(tr("Some entities were ignored! (see console)")); } //try to activate "moving mode" in current GL window if (m_transTool->start()) { connect(m_transTool, &ccOverlayDialog::processFinished, this, &MainWindow::deactivateTranslateRotateMode); registerOverlayDialog(m_transTool, Qt::TopRightCorner); freezeUI(true); updateOverlayDialogsPlacement(); //deactivate all other GL windows disableAllBut(win); } else { ccConsole::Error(tr("Unexpected error!")); //indeed... } } void MainWindow::deactivateTranslateRotateMode(bool state) { if (m_transTool) { //reselect previously selected entities! if (state && m_ccRoot) { const ccHObject& transformedSet = m_transTool->getValidEntities(); try { ccHObject::Container transformedEntities; transformedEntities.resize(transformedSet.getChildrenNumber()); for (unsigned i = 0; i < transformedSet.getChildrenNumber(); ++i) { transformedEntities[i] = transformedSet.getChild(i); } m_ccRoot->selectEntities(transformedEntities); } catch (const std::bad_alloc&) { //not enough memory (nothing to do) } } //m_transTool->close(); } //we reactivate all GL windows enableAll(); freezeUI(false); updateUI(); } void MainWindow::testFrameRate() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) win->startFrameRateTest(); } void MainWindow::showDisplaySettings() { ccDisplaySettingsDlg displayOptionsDlg(this); connect(&displayOptionsDlg, &ccDisplaySettingsDlg::aspectHasChanged, this, [=] () { redrawAll(); }); displayOptionsDlg.exec(); disconnect(&displayOptionsDlg); } void MainWindow::doActionRenderToFile() { ccGLWindowInterface* win = getActiveGLWindow(); if (!win) return; ccRenderToFileDlg rtfDlg(win->glWidth(), win->glHeight(), this); if (rtfDlg.exec()) { QApplication::processEvents(); win->renderToFile(rtfDlg.getFilename(), rtfDlg.getZoom(), rtfDlg.dontScalePoints(), rtfDlg.renderOverlayItems()); } } void MainWindow::doActionEditCamera() { //current active MDI area QMdiSubWindow* qWin = m_mdiArea->activeSubWindow(); if (!qWin) return; if (!m_cpeDlg) { m_cpeDlg = new ccCameraParamEditDlg(qWin, m_pickingHub); //m_cpeDlg->makeFrameless(); //does not work on linux connect(m_mdiArea, &QMdiArea::subWindowActivated, m_cpeDlg, qOverload(&ccCameraParamEditDlg::linkWith)); registerOverlayDialog(m_cpeDlg, Qt::BottomLeftCorner); } m_cpeDlg->linkWith(qWin); m_cpeDlg->start(); updateOverlayDialogsPlacement(); } void MainWindow::doActionAdjustZoom() { //current active MDI area ccGLWindowInterface* win = getActiveGLWindow(); if (!win) return; const ccViewportParameters& params = win->getViewportParameters(); if (params.perspectiveView) { ccConsole::Error(tr("Orthographic mode only!")); return; } ccAdjustZoomDlg azDlg(win, this); if (!azDlg.exec()) return; //apply new focal double focalDist = azDlg.getFocalDistance(); win->setFocalDistance(focalDist); win->redraw(); } static unsigned s_viewportIndex = 0; void MainWindow::doActionSaveViewportAsCamera() { ccGLWindowInterface* win = getActiveGLWindow(); if (!win) return; cc2DViewportObject* viewportObject = new cc2DViewportObject(QString("Viewport #%1").arg(++s_viewportIndex)); viewportObject->setParameters(win->getViewportParameters()); viewportObject->setDisplay(win); // Save the custom light position as well { bool customLightEnabled = win->customLightEnabled(); CCVector3f customLightPos = win->getCustomLightPosition(); viewportObject->setMetaData("CustomLightEnabled", customLightEnabled); viewportObject->setMetaData("CustomLightPosX", customLightPos.x); viewportObject->setMetaData("CustomLightPosY", customLightPos.y); viewportObject->setMetaData("CustomLightPosZ", customLightPos.z); } addToDB(viewportObject); } void MainWindow::zoomOnSelectedEntities() { ccGLWindowInterface* win = nullptr; ccHObject tempGroup("TempGroup"); size_t selNum = m_selectedEntities.size(); for (size_t i = 0; i < selNum; ++i) { ccHObject *entity = m_selectedEntities[i]; if (i == 0 || !win) { //take the first valid window as reference win = static_cast(entity->getDisplay()); } if (win) { if (entity->getDisplay() == win) { tempGroup.addChild(entity,ccHObject::DP_NONE); } else if (entity->getDisplay() != nullptr) { ccLog::Error(tr("All selected entities must be displayed in the same 3D view!")); return; } } } if (tempGroup.getChildrenNumber() != 0) { ccBBox box = tempGroup.getDisplayBB_recursive(false, win); if (!box.isValid()) { ccLog::Warning(tr("Selected entities have no valid bounding-box!")); } else { if ( win != nullptr ) { win->updateConstellationCenterAndZoom(&box); } } } refreshAll(); } void MainWindow::setGlobalZoom() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) win->zoomGlobal(); } void MainWindow::setPivotAlwaysOn() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setPivotVisibility(ccGLWindowInterface::PIVOT_ALWAYS_SHOW); win->redraw(); //update pop-up menu 'top' icon if (m_pivotVisibilityPopupButton) m_pivotVisibilityPopupButton->setIcon(m_UI->actionSetPivotAlwaysOn->icon()); } } void MainWindow::setPivotRotationOnly() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setPivotVisibility(ccGLWindowInterface::PIVOT_SHOW_ON_MOVE); win->redraw(); //update pop-up menu 'top' icon if (m_pivotVisibilityPopupButton) m_pivotVisibilityPopupButton->setIcon(m_UI->actionSetPivotRotationOnly->icon()); } } void MainWindow::setPivotOff() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setPivotVisibility(ccGLWindowInterface::PIVOT_HIDE); win->redraw(); //update pop-up menu 'top' icon if (m_pivotVisibilityPopupButton) m_pivotVisibilityPopupButton->setIcon(m_UI->actionSetPivotOff->icon()); } } void MainWindow::setOrthoView(ccGLWindowInterface* win) { if (win) { if (!checkStereoMode(win)) { return; } win->setPerspectiveState(false, true); win->redraw(); //update pop-up menu 'top' icon if (m_viewModePopupButton) m_viewModePopupButton->setIcon(m_UI->actionSetOrthoView->icon()); if (m_pivotVisibilityPopupButton) m_pivotVisibilityPopupButton->setEnabled(true); } } void MainWindow::setCenteredPerspectiveView(ccGLWindowInterface* win, bool autoRedraw/*=true*/) { if (win) { win->setPerspectiveState(true, true); if (autoRedraw) win->redraw(); //update pop-up menu 'top' icon if (m_viewModePopupButton) m_viewModePopupButton->setIcon(m_UI->actionSetCenteredPerspectiveView->icon()); if (m_pivotVisibilityPopupButton) m_pivotVisibilityPopupButton->setEnabled(true); } } void MainWindow::setViewerPerspectiveView(ccGLWindowInterface* win) { if (win) { win->setPerspectiveState(true,false); win->redraw(); //update pop-up menu 'top' icon if (m_viewModePopupButton) m_viewModePopupButton->setIcon(m_UI->actionSetViewerPerspectiveView->icon()); if (m_pivotVisibilityPopupButton) m_pivotVisibilityPopupButton->setEnabled(false); } } void MainWindow::enablePickingOperation(ccGLWindowInterface* win, QString message) { if (!win) { assert(false); return; } assert(m_pickingHub); if (!m_pickingHub->addListener(this)) { ccLog::Error(tr("Can't start the picking mechanism (another tool is already using it)")); return; } //specific case: we prevent the 'point-pair based alignment' tool to process the picked point! //if (m_pprDlg) // m_pprDlg->pause(true); s_pickingWindow = win; win->displayNewMessage(message, ccGLWindowInterface::LOWER_LEFT_MESSAGE, true, 24 * 3600); win->redraw(true, false); freezeUI(true); } void MainWindow::cancelPreviousPickingOperation(bool aborted) { if (!s_pickingWindow) return; switch(s_currentPickingOperation) { case PICKING_ROTATION_CENTER: //nothing to do break; case PICKING_LEVEL_POINTS: if (s_levelMarkersCloud) { s_pickingWindow->removeFromOwnDB(s_levelMarkersCloud); delete s_levelMarkersCloud; s_levelMarkersCloud = nullptr; } break; default: assert(false); break; } if (aborted) { s_pickingWindow->displayNewMessage(QString(), ccGLWindowInterface::LOWER_LEFT_MESSAGE); //clear previous messages s_pickingWindow->displayNewMessage(tr("Picking operation aborted"), ccGLWindowInterface::LOWER_LEFT_MESSAGE); } s_pickingWindow->redraw(false); //specific case: we allow the 'point-pair based alignment' tool to process the picked point! if (m_pprDlg) m_pprDlg->pause(false); freezeUI(false); m_pickingHub->removeListener(this); s_pickingWindow = nullptr; s_currentPickingOperation = NO_PICKING_OPERATION; } void MainWindow::onItemPicked(const PickedItem& pi) { if (!s_pickingWindow || !m_pickingHub) { return; } if (!pi.entity) { return; } if (m_pickingHub->activeWindow() != s_pickingWindow) { ccLog::Warning(tr("The point was picked in the wrong window")); return; } CCVector3 pickedPoint = pi.P3D; switch(s_currentPickingOperation) { case PICKING_LEVEL_POINTS: { //we only accept points picked on the right entity! //if (obj != s_levelEntity) //{ // ccLog::Warning(tr("[Level] Only points picked on '%1' are considered!").arg(s_levelEntity->getName())); // return; //} if (!s_levelMarkersCloud) { assert(false); cancelPreviousPickingOperation(true); } for (unsigned i = 0; i < s_levelMarkersCloud->size(); ++i) { const CCVector3* P = s_levelMarkersCloud->getPoint(i); if ((pickedPoint - *P).norm() < 1.0e-6) { ccLog::Warning(tr("[Level] Point is too close from the others!")); return; } } //add the corresponding marker s_levelMarkersCloud->addPoint(pickedPoint); unsigned markerCount = s_levelMarkersCloud->size(); cc2DLabel* label = new cc2DLabel(); label->addPickedPoint(s_levelMarkersCloud, markerCount - 1); label->setName(QString("P#%1").arg(markerCount)); label->setDisplayedIn2D(false); label->setDisplay(s_pickingWindow); label->setVisible(true); s_levelMarkersCloud->addChild(label); s_pickingWindow->redraw(); if (markerCount == 3) { //we have enough points! const CCVector3* A = s_levelMarkersCloud->getPoint(0); const CCVector3* B = s_levelMarkersCloud->getPoint(1); const CCVector3* C = s_levelMarkersCloud->getPoint(2); CCVector3 X = *B - *A; CCVector3 Y = *C - *A; CCVector3 Z = X.cross(Y); //we choose 'Z' so that it points 'upward' relatively to the camera (assuming the user will be looking from the top) CCVector3d viewDir = s_pickingWindow->getViewportParameters().getViewDir(); if (Z.toDouble().dot(viewDir) > 0) { Z = -Z; } Y = Z.cross(X); X.normalize(); Y.normalize(); Z.normalize(); ccGLMatrixd trans; { double* mat = trans.data(); mat[0] = X.x; mat[4] = X.y; mat[8] = X.z; mat[12] = 0.0; mat[1] = Y.x; mat[5] = Y.y; mat[9] = Y.z; mat[13] = 0.0; mat[2] = Z.x; mat[6] = Z.y; mat[10] = Z.z; mat[14] = 0.0; mat[3] = 0.0; mat[7] = 0.0; mat[11] = 0.0; mat[15] = 1.0; } CCVector3d T = -A->toDouble(); trans.apply(T); T += *A; trans.setTranslation(T); assert(haveOneSelection() && m_selectedEntities.front() == s_levelEntity); applyTransformation(trans, false); //clear message s_pickingWindow->displayNewMessage(QString(), ccGLWindowInterface::LOWER_LEFT_MESSAGE, false); //clear previous message s_pickingWindow->setView(CC_TOP_VIEW); } else { //we need more points! return; } } //we use the next 'case' entry (PICKING_ROTATION_CENTER) to redefine the rotation center as well! assert(s_levelMarkersCloud && s_levelMarkersCloud->size() != 0); pickedPoint = *s_levelMarkersCloud->getPoint(0); //break; case PICKING_ROTATION_CENTER: { CCVector3d newPivot = pickedPoint; //specific case: transformation tool is enabled if (m_transTool && m_transTool->started()) { m_transTool->setRotationCenter(newPivot); const unsigned& precision = s_pickingWindow->getDisplayParameters().displayedNumPrecision; s_pickingWindow->displayNewMessage(QString(), ccGLWindowInterface::LOWER_LEFT_MESSAGE, false); //clear previous message s_pickingWindow->displayNewMessage(QString("Point (%1 ; %2 ; %3) set as rotation center for interactive transformation") .arg(pickedPoint.x, 0, 'f', precision) .arg(pickedPoint.y, 0, 'f', precision) .arg(pickedPoint.z, 0, 'f', precision), ccGLWindowInterface::LOWER_LEFT_MESSAGE, true); } else { const ccViewportParameters& params = s_pickingWindow->getViewportParameters(); if (!params.perspectiveView || params.objectCenteredView) { //apply current GL transformation (if any) pi.entity->getGLTransformation().apply(newPivot); s_pickingWindow->setPivotPoint(newPivot, true, true); } } //s_pickingWindow->redraw(); //already called by 'cancelPreviousPickingOperation' (see below) } break; default: assert(false); break; } cancelPreviousPickingOperation(false); } void MainWindow::doLevel() { //picking operation already in progress if (s_pickingWindow) { if (s_currentPickingOperation == PICKING_LEVEL_POINTS) { cancelPreviousPickingOperation(true); } else { ccConsole::Error(tr("Stop the other picking operation first!")); } return; } ccGLWindowInterface* win = getActiveGLWindow(); if (!win) { ccConsole::Error(tr("No active 3D view!")); return; } if (!haveOneSelection()) { ccConsole::Error(tr("Select an entity!")); return; } //create markers cloud assert(!s_levelMarkersCloud); { s_levelMarkersCloud = new ccPointCloud("Level points"); if (!s_levelMarkersCloud->reserve(3)) { ccConsole::Error(tr("Not enough memory!")); return; } win->addToOwnDB(s_levelMarkersCloud); } s_levelEntity = m_selectedEntities.front(); s_levelLabels.clear(); s_currentPickingOperation = PICKING_LEVEL_POINTS; enablePickingOperation(win, tr("Pick three points on the floor plane (click the Level button or press Escape to cancel)")); } void MainWindow::doPickRotationCenter() { //picking operation already in progress if (s_pickingWindow) { if (s_currentPickingOperation == PICKING_ROTATION_CENTER) { cancelPreviousPickingOperation(true); } else { ccConsole::Error(tr("Stop the other picking operation first!")); } return; } ccGLWindowInterface* win = getActiveGLWindow(); if (!win) { ccConsole::Error(tr("No active 3D view!")); return; } bool objectCentered = true; bool perspectiveEnabled = win->getPerspectiveState(objectCentered); if (perspectiveEnabled && !objectCentered) { ccLog::Error(tr("Perspective mode is viewer-centered: can't use a point as rotation center!")); return; } s_currentPickingOperation = PICKING_ROTATION_CENTER; enablePickingOperation(win, tr("Pick a point to be used as rotation center (click on icon again to cancel)")); } ccPointCloud* MainWindow::askUserToSelectACloud(ccHObject* defaultCloudEntity/*=nullptr*/, QString inviteMessage/*=QString()*/) { ccHObject::Container clouds; m_ccRoot->getRootEntity()->filterChildren(clouds, true, CC_TYPES::POINT_CLOUD, true); if (clouds.empty()) { ccConsole::Error(tr("No cloud in database!")); return nullptr; } //default selected index int selectedIndex = 0; if (defaultCloudEntity) { for (size_t i = 1; i < clouds.size(); ++i) { if (clouds[i] == defaultCloudEntity) { selectedIndex = static_cast(i); break; } } } //ask the user to choose a cloud { selectedIndex = ccItemSelectionDlg::SelectEntity(clouds, selectedIndex, this, inviteMessage); if (selectedIndex < 0) return nullptr; } assert(selectedIndex >= 0 && static_cast(selectedIndex) < clouds.size()); return ccHObjectCaster::ToPointCloud(clouds[selectedIndex]); } void MainWindow::toggleSelectedEntitiesProperty( ccEntityAction::TOGGLE_PROPERTY property ) { if ( !ccEntityAction::toggleProperty( m_selectedEntities, property ) ) { return; } refreshAll(); updateUI(); } void MainWindow::clearSelectedEntitiesProperty( ccEntityAction::CLEAR_PROPERTY property ) { if ( !ccEntityAction::clearProperty( m_selectedEntities, property, this ) ) { return; } refreshAll(); updateUI(); } void MainWindow::setView( CC_VIEW_ORIENTATION view ) { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setView(view); } } void MainWindow::spawnHistogramDialog(const std::vector& histoValues, double minVal, double maxVal, QString title, QString xAxisLabel) { ccHistogramWindowDlg* hDlg = new ccHistogramWindowDlg(this); hDlg->setAttribute(Qt::WA_DeleteOnClose, true); hDlg->setWindowTitle(tr("Histogram")); ccHistogramWindow* histogram = hDlg->window(); { histogram->setTitle(title); histogram->fromBinArray(histoValues, minVal, maxVal); histogram->setAxisLabels(xAxisLabel, tr("Count")); histogram->refresh(); } hDlg->show(); } void MainWindow::showSelectedEntitiesHistogram() { for ( ccHObject *entity : getSelectedEntities() ) { //for "real" point clouds only ccPointCloud* cloud = ccHObjectCaster::ToPointCloud( entity ); if (cloud) { //we display the histogram of the current scalar field ccScalarField* sf = static_cast(cloud->getCurrentDisplayedScalarField()); if (sf) { ccHistogramWindowDlg* hDlg = new ccHistogramWindowDlg(this); hDlg->setAttribute(Qt::WA_DeleteOnClose, true); hDlg->setWindowTitle(tr("Histogram [%1]").arg(cloud->getName())); ccHistogramWindow* histogram = hDlg->window(); { unsigned numberOfPoints = cloud->size(); unsigned numberOfClasses = static_cast(sqrt(static_cast(numberOfPoints))); //we take the 'nearest' multiple of 4 numberOfClasses &= (~3); numberOfClasses = std::max(4, numberOfClasses); numberOfClasses = std::min(256, numberOfClasses); histogram->setTitle(tr("%1 (%2 values) ").arg(QString::fromStdString(sf->getName())).arg(numberOfPoints)); bool showNaNValuesInGrey = sf->areNaNValuesShownInGrey(); histogram->fromSF(sf, numberOfClasses, true, showNaNValuesInGrey); histogram->setAxisLabels(QString::fromStdString(sf->getName()), tr("Count")); histogram->refresh(); } hDlg->show(); } } } } void MainWindow::doActionCrop() { //find candidates std::vector candidates; ccBBox baseBB; { const ccHObject::Container& selectedEntities = getSelectedEntities(); for ( ccHObject *entity : selectedEntities ) { if ( entity->isA(CC_TYPES::POINT_CLOUD) || entity->isKindOf(CC_TYPES::MESH) ) { candidates.push_back(entity); baseBB += entity->getOwnBB(); } } } if (candidates.empty()) { ccConsole::Warning(tr("[Crop] No eligible candidate found!")); return; } ccBoundingBoxEditorDlg bbeDlg(false, false, this); bbeDlg.setBaseBBox(baseBB, false); bbeDlg.showInclusionWarning(false); bbeDlg.setWindowTitle("Crop"); if (!bbeDlg.exec()) { //process cancelled by user return; } //deselect all entities if (m_ccRoot) { m_ccRoot->unselectAllEntities(); } //cropping box ccBBox box = bbeDlg.getBox(); //process cloud/meshes bool errors = false; bool successes = false; { for ( ccHObject *entity : candidates ) { ccHObject* croppedEnt = ccCropTool::Crop(entity, box, true); if (croppedEnt) { croppedEnt->setName(entity->getName() + QString(".cropped")); croppedEnt->setDisplay(entity->getDisplay()); croppedEnt->prepareDisplayForRefresh(); if (entity->getParent()) entity->getParent()->addChild(croppedEnt); entity->setEnabled(false); addToDB(croppedEnt); //select output entity m_ccRoot->selectEntity(croppedEnt, true); successes = true; } else { errors = true; } } } if (successes) ccLog::Warning(tr("[Crop] Selected entities have been hidden")); if (errors) ccLog::Error(tr("Error(s) occurred! See the Console")); refreshAll(); updateUI(); } void MainWindow::doActionClone() { ccHObject* lastClone = nullptr; for ( ccHObject *entity : getSelectedEntities() ) { ccHObject* clone = nullptr; if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { clone = ccHObjectCaster::ToGenericPointCloud(entity)->clone(); if (!clone) { ccConsole::Error(tr("An error occurred while cloning cloud %1").arg(entity->getName())); } } else if (entity->isKindOf(CC_TYPES::PRIMITIVE)) { clone = static_cast(entity)->clone(); if (!clone) { ccConsole::Error(tr("An error occurred while cloning primitive %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::MESH)) { clone = ccHObjectCaster::ToMesh(entity)->cloneMesh(); if (!clone) { ccConsole::Error(tr("An error occurred while cloning mesh %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::POLY_LINE)) { clone = ccHObjectCaster::ToPolyline(entity)->clone(); if (!clone) { ccConsole::Error(tr("An error occurred while cloning polyline %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::FACET)) { clone = ccHObjectCaster::ToFacet(entity); if (!clone) { ccConsole::Error(tr("An error occurred while cloning facet %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::CAMERA_SENSOR)) { ccCameraSensor* camera = ccHObjectCaster::ToCameraSensor(entity); if (camera) { clone = new ccCameraSensor(*camera); if (camera->getParent()) { camera->getParent()->addChild(clone); } } if (!clone) { ccConsole::Error(tr("An error occurred while cloning camera sensor %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::GBL_SENSOR)) { ccGBLSensor* sensor = ccHObjectCaster::ToGBLSensor(entity); if (sensor) { clone = new ccGBLSensor(*sensor); if (sensor->getParent()) { sensor->getParent()->addChild(clone); } } if (!clone) { ccConsole::Error(tr("An error occurred while cloning GBL sensor %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::IMAGE)) { ccImage* image = ccHObjectCaster::ToImage(entity); if (image) { clone = new ccImage(*image); if (image->getParent()) { image->getParent()->addChild(clone); } } if (!clone) { ccConsole::Error(tr("An error occurred while cloning image %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::LABEL_2D)) { cc2DLabel* label = ccHObjectCaster::To2DLabel(entity); if (label) { clone = new cc2DLabel(*label, true); if (label->getParent()) { label->getParent()->addChild(clone); } } if (!clone) { ccConsole::Error(tr("An error occurred while cloning label %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::VIEWPORT_2D_OBJECT)) { cc2DViewportObject* viewport = ccHObjectCaster::To2DViewportObject(entity); if (viewport) { clone = new cc2DViewportObject(*viewport); if (viewport->getParent()) { viewport->getParent()->addChild(clone); } } if (!clone) { ccConsole::Error(tr("An error occurred while cloning viewport %1").arg(entity->getName())); } } else if (entity->isA(CC_TYPES::VIEWPORT_2D_LABEL)) { cc2DViewportLabel* viewportLabel = ccHObjectCaster::To2DViewportLabel(entity); if (viewportLabel) { clone = new cc2DViewportLabel(*viewportLabel); if (viewportLabel->getParent()) { viewportLabel->getParent()->addChild(clone); } } if (!clone) { ccConsole::Error(tr("An error occurred while cloning viewport %1").arg(entity->getName())); } } else { ccLog::Warning(tr("Entity '%1' can't be cloned (type not supported yet!)").arg(entity->getName())); } if (clone) { //copy GL transformation history clone->setGLTransformationHistory(entity->getGLTransformationHistory()); //copy display clone->setDisplay(entity->getDisplay()); addToDB(clone); lastClone = clone; } } if (lastClone && m_ccRoot) { m_ccRoot->selectEntity(lastClone); } updateUI(); } void MainWindow::doActionAddConstantSF() { if (!haveOneSelection()) { if (haveSelection()) ccConsole::Error(tr("Select only one cloud or one mesh!")); return; } ccHObject* ent = m_selectedEntities.front(); bool lockedVertices; ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(ent, &lockedVertices); //for "real" point clouds only if (!cloud) { return; } if (lockedVertices && !ent->isAncestorOf(cloud)) { ccUtils::DisplayLockedVerticesWarning(ent->getName(),true); return; } QString defaultName = "Constant"; unsigned trys = 1; while (cloud->getScalarFieldIndexByName(defaultName.toStdString()) >= 0 || trys > 99) { defaultName = tr("Constant #%1").arg(++trys); } //ask for a name bool ok; QString sfName = QInputDialog::getText(this, tr("New SF name"), tr("SF name (must be unique)"), QLineEdit::Normal, defaultName, &ok); if (!ok) return; if (ccEntityAction::sfAddConstant(cloud, sfName, false, this)) { updateUI(); cloud->redrawDisplay(); } } void MainWindow::doActionAddClassificationSF() { if (!haveOneSelection()) { if (haveSelection()) ccConsole::Error(tr("Select only one cloud or one mesh!")); return; } ccHObject* ent = m_selectedEntities.front(); bool lockedVertices; ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(ent, &lockedVertices); //for "real" point clouds only if (!cloud) { return; } if (lockedVertices && !ent->isAncestorOf(cloud)) { ccUtils::DisplayLockedVerticesWarning(ent->getName(), true); return; } if (ccEntityAction::sfAddConstant(cloud, "Classification", true, this)) { updateUI(); cloud->redrawDisplay(); } } void MainWindow::doActionScalarFieldFromColor() { if ( !ccEntityAction::sfFromColor(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionScalarFieldArithmetic() { if ( !ccEntityAction::sfArithmetic(m_selectedEntities, this) ) return; refreshAll(); updateUI(); } void MainWindow::doActionFitSphere() { double outliersRatio = 0.5; double confidence = 0.99; ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); for ( ccHObject *entity : getSelectedEntities() ) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (!cloud) continue; CCVector3 center; PointCoordinateType radius = 0; double rms = std::numeric_limits::quiet_NaN(); if (CCCoreLib::GeometricalAnalysisTools::DetectSphereRobust(cloud, outliersRatio, center, radius, rms, &pDlg, confidence) != CCCoreLib::GeometricalAnalysisTools::NoError) { ccLog::Warning(tr("[Fit sphere] Failed to fit a sphere on cloud '%1'").arg(cloud->getName())); continue; } ccLog::Print(tr("[Fit sphere] Cloud '%1': center (%2,%3,%4) - radius = %5 [RMS = %6]") .arg(cloud->getName()) .arg(center.x) .arg(center.y) .arg(center.z) .arg(radius) .arg(rms)); ccGLMatrix trans; trans.setTranslation(center); ccSphere* sphere = new ccSphere(radius, &trans, tr("Sphere r=%1").arg(radius)); cloud->addChild(sphere); //sphere->setDisplay(cloud->getDisplay()); sphere->prepareDisplayForRefresh(); sphere->copyGlobalShiftAndScale(*cloud); sphere->setMetaData("RMS", rms); addToDB(sphere, false, false, false); } refreshAll(); } void MainWindow::doActionFitCircle() { ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); for (ccHObject* entity : getSelectedEntities()) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (!cloud) continue; CCVector3 center; CCVector3 normal; PointCoordinateType radius = 0; double rms = std::numeric_limits::quiet_NaN(); if (CCCoreLib::GeometricalAnalysisTools::DetectCircle( cloud, center, normal, radius, rms, &pDlg) != CCCoreLib::GeometricalAnalysisTools::NoError) { ccLog::Warning(tr("[Fit circle] Failed to fit a circle on cloud '%1'").arg(cloud->getName())); continue; } ccLog::Print(tr("[Fit circle] Cloud '%1': center (%2,%3,%4) - radius = %5 [RMS = %6]") .arg(cloud->getName()) .arg(center.x) .arg(center.y) .arg(center.z) .arg(radius) .arg(rms)); ccLog::Print(tr("[Fit circle] Normal (%1,%2,%3)") .arg(normal.x) .arg(normal.y) .arg(normal.z)); // create the circle representation as a polyline ccPolyline* circle = ccPolyline::Circle(CCVector3(0, 0, 0), radius, 128); if (circle) { circle->setName(QObject::tr("Circle r=%1").arg(radius)); cloud->addChild(circle); circle->prepareDisplayForRefresh(); circle->copyGlobalShiftAndScale(*cloud); circle->setMetaData("RMS", rms); ccGLMatrix trans = ccGLMatrix::FromToRotation(CCVector3(0, 0, 1), normal); trans.setTranslation(center); circle->applyGLTransformation_recursive(&trans); addToDB(circle, false, false, false); } } refreshAll(); } void MainWindow::doActionFitPlane() { doComputePlaneOrientation(false); } void MainWindow::doActionFitFacet() { doComputePlaneOrientation(true); } void MainWindow::doComputePlaneOrientation(bool fitFacet) { if (!haveSelection()) return; double maxEdgeLength = 0.0; if (fitFacet) { bool ok = true; static double s_polygonMaxEdgeLength = 0.0; maxEdgeLength = QInputDialog::getDouble(this, tr("Fit facet"), tr("Max edge length (0 = no limit)"), s_polygonMaxEdgeLength, 0, 1.0e9, 8, &ok); if (!ok) return; s_polygonMaxEdgeLength = maxEdgeLength; } ccHObject::Container selectedEntities = getSelectedEntities(); //warning, getSelectedEntites may change during this loop! bool firstEntity = true; for (ccHObject *entity : selectedEntities) { ccShiftedObject* shifted = nullptr; CCCoreLib::GenericIndexedCloudPersist* cloud = nullptr; if (entity->isKindOf(CC_TYPES::POLY_LINE)) { ccPolyline* poly = ccHObjectCaster::ToPolyline(entity); cloud = static_cast(poly); shifted = poly; } else { ccGenericPointCloud* gencloud = ccHObjectCaster::ToGenericPointCloud(entity); if (gencloud) { cloud = static_cast(gencloud); shifted = gencloud; } } if (cloud) { double rms = 0.0; CCVector3 C; CCVector3 N; ccHObject* plane = nullptr; if (fitFacet) { ccFacet* facet = ccFacet::Create(cloud, static_cast(maxEdgeLength)); if (facet) { plane = static_cast(facet); N = facet->getNormal(); C = facet->getCenter(); rms = facet->getRMS(); //manually copy shift & scale info! if (shifted) { ccPolyline* contour = facet->getContour(); if (contour) { contour->copyGlobalShiftAndScale(*shifted); } ccMesh* polygon = facet->getPolygon(); if (polygon) { polygon->copyGlobalShiftAndScale(*shifted); } ccPointCloud* points = facet->getOriginPoints(); if (points) { points->copyGlobalShiftAndScale(*shifted); } } } } else { ccPlane* pPlane = ccPlane::Fit(cloud, &rms); if (pPlane) { plane = static_cast(pPlane); N = pPlane->getNormal(); C = *CCCoreLib::Neighbourhood(cloud).getGravityCenter(); pPlane->enableStippling(true); if (shifted) { pPlane->copyGlobalShiftAndScale(*shifted); } } } //as all information appears in Console... forceConsoleDisplay(); if (plane) { ccConsole::Print(tr("[Orientation] Entity '%1'").arg(entity->getName())); ccConsole::Print(tr("\t- plane fitting RMS: %1").arg(rms)); //We always consider the normal with a positive 'Z' by default! if (N.z < 0.0) N *= -1.0; ccConsole::Print(tr("\t- normal: (%1, %2, %3)").arg(N.x).arg(N.y).arg(N.z)); //we compute strike & dip by the way PointCoordinateType dip = 0.0f; PointCoordinateType dipDir = 0.0f; ccNormalVectors::ConvertNormalToDipAndDipDir(N, dip, dipDir); QString dipAndDipDirStr = ccNormalVectors::ConvertDipAndDipDirToString(dip, dipDir); ccConsole::Print(QString("\t- %1").arg(dipAndDipDirStr)); //hack: output the transformation matrix that would make this normal points towards +Z ccGLMatrix makeZPosMatrix = ccGLMatrix::FromToRotation(N, CCVector3(0, 0, CCCoreLib::PC_ONE)); CCVector3 Gt = C; makeZPosMatrix.applyRotation(Gt); makeZPosMatrix.setTranslation(C-Gt); ccConsole::Print(tr("[Orientation] A matrix that would make this plane horizontal (normal towards Z+) is:")); ccConsole::Print(makeZPosMatrix.toString(12,' ')); //full precision ccConsole::Print(tr("[Orientation] You can copy this matrix values (CTRL+C) and paste them in the 'Apply transformation tool' dialog")); plane->setName(dipAndDipDirStr); plane->applyGLTransformation_recursive(); //not yet in DB plane->setVisible(true); plane->setSelectionBehavior(ccHObject::SELECTION_FIT_BBOX); entity->addChild(plane); plane->setDisplay(entity->getDisplay()); plane->prepareDisplayForRefresh_recursive(); addToDB(plane); if (firstEntity) { m_ccRoot->unselectAllEntities(); m_ccRoot->selectEntity(plane); } } else { ccConsole::Warning(tr("Failed to fit a plane/facet on entity '%1'").arg(entity->getName())); } } } refreshAll(); updateUI(); } void MainWindow::doShowPrimitiveFactory() { if (!m_pfDlg) m_pfDlg = new ccPrimitiveFactoryDlg(this); m_pfDlg->setModal(false); m_pfDlg->setWindowModality(Qt::NonModal); m_pfDlg->show(); } void MainWindow::doComputeGeometricFeature() { static ccLibAlgorithms::GeomCharacteristicSet s_selectedCharacteristics; static CCVector3 s_upDir(0, 0, 1); static bool s_upDirDefined = false; ccGeomFeaturesDlg gfDlg(this); double radius = ccLibAlgorithms::GetDefaultCloudKernelSize(m_selectedEntities); gfDlg.setRadius(radius); // restore semi-persistent parameters gfDlg.setSelectedFeatures(s_selectedCharacteristics); if (s_upDirDefined) { gfDlg.setUpDirection(s_upDir); } if (!gfDlg.exec()) return; radius = gfDlg.getRadius(); if (!gfDlg.getSelectedFeatures(s_selectedCharacteristics)) { ccLog::Error(tr("Not enough memory!")); return; } CCVector3* upDir = gfDlg.getUpDirection(); // remember semi-persistent parameters s_upDirDefined = (upDir != nullptr); if (s_upDirDefined) { s_upDir = *upDir; } ccLibAlgorithms::ComputeGeomCharacteristics(s_selectedCharacteristics, static_cast(radius), m_selectedEntities, upDir, this); refreshAll(); updateUI(); } void MainWindow::doActionSFGradient() { if (!ccLibAlgorithms::ApplyCCLibAlgorithm(ccLibAlgorithms::CCLIB_ALGO_SF_GRADIENT, m_selectedEntities, this)) return; refreshAll(); updateUI(); } void MainWindow::doSphericalNeighbourhoodExtractionTest() { size_t selNum = m_selectedEntities.size(); if (selNum < 1) return; //spherical neighborhood extraction radius double sphereRadius = ccLibAlgorithms::GetDefaultCloudKernelSize(m_selectedEntities); if (sphereRadius < 0) { ccConsole::Error(tr("Invalid kernel size!")); return; } bool ok; double val = QInputDialog::getDouble(this, tr("SNE test"), tr("Radius:"), sphereRadius, DBL_MIN, 1.0e9, 8, &ok); if (!ok) return; sphereRadius = val; QString sfName = tr("Spherical extraction test (%1)").arg(sphereRadius); ccProgressDialog pDlg(true, this); pDlg.setAutoClose(false); for (size_t i = 0; i < selNum; ++i) { //we only process clouds if (!m_selectedEntities[i]->isA(CC_TYPES::POINT_CLOUD)) { continue; } ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(m_selectedEntities[i]); int sfIdx = cloud->getScalarFieldIndexByName(sfName.toStdString()); if (sfIdx < 0) sfIdx = cloud->addScalarField(sfName.toStdString()); if (sfIdx < 0) { ccConsole::Error(tr("Failed to create scalar field on cloud '%1' (not enough memory?)").arg(cloud->getName())); return; } ccOctree::Shared octree = cloud->getOctree(); if (!octree) { pDlg.reset(); pDlg.show(); octree = cloud->computeOctree(&pDlg); if (!octree) { ccConsole::Error(tr("Couldn't compute octree for cloud '%1'!").arg(cloud->getName())); return; } } CCCoreLib::ScalarField* sf = cloud->getScalarField(sfIdx); sf->fill(CCCoreLib::NAN_VALUE); cloud->setCurrentScalarField(sfIdx); QElapsedTimer eTimer; eTimer.start(); size_t extractedPoints = 0; unsigned char level = octree->findBestLevelForAGivenNeighbourhoodSizeExtraction(static_cast(sphereRadius)); std::random_device rd; // non-deterministic generator std::mt19937 gen(rd()); // to seed mersenne twister. std::uniform_int_distribution dist(0, cloud->size() - 1); const unsigned samples = 1000; for (unsigned j = 0; j < samples; ++j) { unsigned randIndex = dist(gen); CCCoreLib::DgmOctree::NeighboursSet neighbours; octree->getPointsInSphericalNeighbourhood( *cloud->getPoint(randIndex), static_cast(sphereRadius), neighbours, level ); size_t neihgboursCount = neighbours.size(); extractedPoints += neihgboursCount; for (size_t k = 0; k < neihgboursCount; ++k) { cloud->setPointScalarValue(neighbours[k].pointIndex, static_cast(sqrt(neighbours[k].squareDistd))); } } ccConsole::Print(tr("[SNE_TEST] Mean extraction time = %1 ms (radius = %2, mean (neighbours) = %3)").arg(eTimer.elapsed()).arg(sphereRadius).arg(extractedPoints / static_cast(samples), 0, 'f', 1)); sf->computeMinAndMax(); cloud->setCurrentDisplayedScalarField(sfIdx); cloud->showSF(true); cloud->prepareDisplayForRefresh(); } refreshAll(); updateUI(); } void MainWindow::doCylindricalNeighbourhoodExtractionTest() { bool ok; double radius = QInputDialog::getDouble(this, tr("CNE Test"), tr("radius"), 0.02, 1.0e-6, 1.0e6, 6, &ok); if (!ok) return; double height = QInputDialog::getDouble(this, tr("CNE Test"), tr("height"), 0.05, 1.0e-6, 1.0e6, 6, &ok); if (!ok) return; ccPointCloud* cloud = new ccPointCloud(tr("cube")); const unsigned ptsCount = 1000000; if (!cloud->reserve(ptsCount)) { ccConsole::Error(tr("Not enough memory!")); delete cloud; return; } //fill a unit cube with random points { std::random_device rd; // non-deterministic generator std::mt19937 gen(rd()); // to seed mersenne twister. std::uniform_real_distribution dist(0, 1); for (unsigned i = 0; i < ptsCount; ++i) { CCVector3 P(dist(gen), dist(gen), dist(gen)); cloud->addPoint(P); } } //get/Add scalar field static const char DEFAULT_CNE_TEST_TEMP_SF_NAME[] = "CNE test"; int sfIdx = cloud->getScalarFieldIndexByName(DEFAULT_CNE_TEST_TEMP_SF_NAME); if (sfIdx < 0) sfIdx = cloud->addScalarField(DEFAULT_CNE_TEST_TEMP_SF_NAME); if (sfIdx < 0) { ccConsole::Error(tr("Not enough memory!")); delete cloud; return; } cloud->setCurrentScalarField(sfIdx); //reset scalar field cloud->getScalarField(sfIdx)->fill(CCCoreLib::NAN_VALUE); ccProgressDialog pDlg(true, this); ccOctree::Shared octree = cloud->computeOctree(&pDlg); if (octree) { QElapsedTimer subTimer; subTimer.start(); unsigned long long extractedPoints = 0; unsigned char level = octree->findBestLevelForAGivenNeighbourhoodSizeExtraction(static_cast(2.5*radius)); //2.5 = empirical const unsigned samples = 1000; std::random_device rd; // non-deterministic generator std::mt19937 gen(rd()); // to seed mersenne twister. std::uniform_real_distribution distAngle(0, static_cast(2 * M_PI)); std::uniform_int_distribution distIndex(0, ptsCount - 1); for (unsigned j = 0; j < samples; ++j) { //generate random normal vector CCVector3 dir(0, 0, 1); { ccGLMatrix rot; rot.initFromParameters(distAngle(gen), distAngle(gen), distAngle(gen), CCVector3(0, 0, 0)); rot.applyRotation(dir); } unsigned randIndex = distIndex(gen); CCCoreLib::DgmOctree::CylindricalNeighbourhood cn; cn.center = *cloud->getPoint(randIndex); cn.dir = dir; cn.level = level; cn.radius = static_cast(radius); cn.maxHalfLength = static_cast(height / 2); octree->getPointsInCylindricalNeighbourhood(cn); //octree->getPointsInSphericalNeighbourhood(*cloud->getPoint(randIndex),radius,neighbours,level); size_t neihgboursCount = cn.neighbours.size(); extractedPoints += static_cast(neihgboursCount); for (size_t k = 0; k < neihgboursCount; ++k) { cloud->setPointScalarValue(cn.neighbours[k].pointIndex, static_cast(sqrt(cn.neighbours[k].squareDistd))); } } ccConsole::Print(tr("[CNE_TEST] Mean extraction time = %1 ms (radius = %2, height = %3, mean (neighbours) = %4))").arg(subTimer.elapsed()).arg(radius).arg(height).arg(static_cast(extractedPoints) / samples, 0, 'f', 1)); } else { ccConsole::Error(tr("Failed to compute octree!")); } ccScalarField* sf = static_cast(cloud->getScalarField(sfIdx)); sf->computeMinAndMax(); sf->showNaNValuesInGrey(false); cloud->setCurrentDisplayedScalarField(sfIdx); cloud->showSF(true); addToDB(cloud); refreshAll(); updateUI(); } void MainWindow::doActionCreateCloudFromEntCenters() { size_t selNum = getSelectedEntities().size(); ccPointCloud* centers = new ccPointCloud(tr("centers")); if (!centers->reserve(static_cast(selNum))) { ccLog::Error(tr("Not enough memory!")); delete centers; centers = nullptr; return; } //look for clouds { for ( ccHObject *entity : getSelectedEntities() ) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (cloud == nullptr) { continue; } centers->addPoint(cloud->getOwnBB().getCenter()); //we display the cloud in the same window as the first (selected) cloud we encounter if (!centers->getDisplay()) { centers->setDisplay(cloud->getDisplay()); } } } if (centers->size() == 0) { ccLog::Error(tr("No cloud in selection?!")); delete centers; centers = nullptr; } else { centers->resize(centers->size()); centers->setPointSize(10); centers->setVisible(true); addToDB(centers); } } void MainWindow::doActionComputeBestICPRmsMatrix() { //look for clouds std::vector clouds; try { for ( ccHObject *entity : getSelectedEntities() ) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (cloud) { clouds.push_back(cloud); } } } catch (const std::bad_alloc&) { ccLog::Error(tr("Not enough memory!")); return; } size_t cloudCount = clouds.size(); if (cloudCount < 2) { ccLog::Error(tr("Need at least two clouds!")); return; } //init matrices std::vector rmsMatrix; std::vector matrices; std::vector< std::pair > matrixAngles; try { rmsMatrix.resize(cloudCount*cloudCount, 0); //init all possible transformations static const double angularStep_deg = 45.0; unsigned phiSteps = static_cast(360.0 / angularStep_deg); assert(CCCoreLib::LessThanEpsilon(std::abs(360.0 - phiSteps * angularStep_deg))); unsigned thetaSteps = static_cast(180.0 / angularStep_deg); assert(CCCoreLib::LessThanEpsilon(std::abs(180.0 - thetaSteps * angularStep_deg))); unsigned rotCount = phiSteps * (thetaSteps - 1) + 2; matrices.reserve(rotCount); matrixAngles.reserve(rotCount); for (unsigned j = 0; j <= thetaSteps; ++j) { //we want to cover the full [0-180] interval! ([-90;90] in fact) double theta_deg = j * angularStep_deg - 90.0; for (unsigned i = 0; i < phiSteps; ++i) { double phi_deg = i * angularStep_deg; ccGLMatrix trans; trans.initFromParameters( static_cast( CCCoreLib::DegreesToRadians( phi_deg ) ), static_cast( CCCoreLib::DegreesToRadians( theta_deg ) ), 0, CCVector3(0,0,0) ); matrices.push_back(trans); matrixAngles.push_back( std::pair(phi_deg,theta_deg) ); //for poles, no need to rotate! if (j == 0 || j == thetaSteps) break; } } } catch (const std::bad_alloc&) { ccLog::Error(tr("Not enough memory!")); return; } //let's start! { ccProgressDialog pDlg(true, this); pDlg.setMethodTitle(tr("Testing all possible positions")); pDlg.setInfo(tr("%1 clouds and %2 positions").arg(cloudCount).arg(matrices.size())); CCCoreLib::NormalizedProgress nProgress(&pDlg, static_cast(((cloudCount*(cloudCount - 1)) / 2)*matrices.size())); pDlg.start(); QApplication::processEvents(); //#define TEST_GENERATION #ifdef TEST_GENERATION ccPointCloud* testSphere = new ccPointCloud(); testSphere->reserve(matrices.size()); #endif for (size_t i = 0; i < cloudCount - 1; ++i) { ccPointCloud* A = clouds[i]; A->computeOctree(); for (size_t j = i + 1; j < cloudCount; ++j) { ccGLMatrix transBToZero; transBToZero.toIdentity(); transBToZero.setTranslation(-clouds[j]->getOwnBB().getCenter()); ccGLMatrix transFromZeroToA; transFromZeroToA.toIdentity(); transFromZeroToA.setTranslation(A->getOwnBB().getCenter()); #ifndef TEST_GENERATION double minRMS = -1.0; int bestMatrixIndex = -1; ccPointCloud* bestB = nullptr; #endif for (size_t k = 0; k < matrices.size(); ++k) { ccPointCloud* B = clouds[j]->cloneThis(); if (!B) { ccLog::Error(tr("Not enough memory!")); return; } ccGLMatrix BtoA = transFromZeroToA * matrices[k] * transBToZero; B->applyRigidTransformation(BtoA); #ifndef TEST_GENERATION double finalRMS = 0.0; unsigned finalPointCount = 0; CCCoreLib::ICPRegistrationTools::RESULT_TYPE result; CCCoreLib::ICPRegistrationTools::ScaledTransformation registerTrans; CCCoreLib::ICPRegistrationTools::Parameters params; { params.convType = CCCoreLib::ICPRegistrationTools::MAX_ERROR_CONVERGENCE; params.minRMSDecrease = 1.0e-6; } result = CCCoreLib::ICPRegistrationTools::Register(A, nullptr, B, params, registerTrans, finalRMS, finalPointCount); if (result >= CCCoreLib::ICPRegistrationTools::ICP_ERROR) { delete B; if (bestB) delete bestB; ccLog::Error(tr("An error occurred while performing ICP!")); return; } if (minRMS < 0 || finalRMS < minRMS) { minRMS = finalRMS; bestMatrixIndex = static_cast(k); std::swap(bestB, B); } if (B) { delete B; B = nullptr; } #else addToDB(B); //Test sphere CCVector3 Y(0,1,0); matrices[k].apply(Y); testSphere->addPoint(Y); #endif if (!nProgress.oneStep()) { //process cancelled by user return; } } #ifndef TEST_GENERATION if (bestMatrixIndex >= 0) { assert(bestB); ccHObject* group = new ccHObject(tr("Best case #%1 / #%2 - RMS = %3").arg(i+1).arg(j+1).arg(minRMS)); group->addChild(bestB); group->setDisplay_recursive(A->getDisplay()); addToDB(group); ccLog::Print(tr("[DoActionComputeBestICPRmsMatrix] Comparison #%1 / #%2: min RMS = %3 (phi = %4 / theta = %5 deg.)").arg(i+1).arg(j+1).arg(minRMS).arg(matrixAngles[bestMatrixIndex].first).arg(matrixAngles[bestMatrixIndex].second)); } else { assert(!bestB); ccLog::Warning(tr("[DoActionComputeBestICPRmsMatrix] Comparison #%1 / #%2: INVALID").arg(i+1).arg(j+1)); } rmsMatrix[i*cloudCount + j] = minRMS; #else addToDB(testSphere); i = cloudCount; break; #endif } } } //export result as a CSV file #ifdef TEST_GENERATION if (false) #endif { //persistent settings QSettings settings; settings.beginGroup(ccPS::SaveFile()); QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); QString outputFilename = QFileDialog::getSaveFileName( this, tr("Select output file"), currentPath, "*.csv", nullptr, CCFileDialogOptions()); if (outputFilename.isEmpty()) return; QFile fp(outputFilename); if (fp.open(QFile::Text | QFile::WriteOnly)) { QTextStream stream(&fp); //header { stream << "RMS"; for ( ccPointCloud *cloud : clouds ) { stream << ';'; stream << cloud->getName(); } stream << endl; } //rows for (size_t j = 0; j < cloudCount; ++j) { stream << clouds[j]->getName(); stream << ';'; for (size_t i = 0; i < cloudCount; ++i) { stream << rmsMatrix[j*cloudCount+i]; stream << ';'; } stream << endl; } ccLog::Print(tr("[DoActionComputeBestICPRmsMatrix] Job done")); } else { ccLog::Error(tr("Failed to save output file?!")); } } } void MainWindow::doActionExportPlaneInfo() { ccHObject::Container planes; const ccHObject::Container& selectedEntities = getSelectedEntities(); if (selectedEntities.size() == 1 && selectedEntities.front()->isA(CC_TYPES::HIERARCHY_OBJECT)) { //a group selectedEntities.front()->filterChildren(planes, true, CC_TYPES::PLANE, false); } else { for (ccHObject* ent : selectedEntities) { if (ent->isKindOf(CC_TYPES::PLANE)) { //a single plane planes.push_back(static_cast(ent)); } } } if (planes.size() == 0) { ccLog::Error(tr("No plane in selection")); return; } //persistent settings QSettings settings; settings.beginGroup(ccPS::SaveFile()); QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); QString outputFilename = QFileDialog::getSaveFileName( this, tr("Select output file"), currentPath, "*.csv", nullptr, CCFileDialogOptions()); if (outputFilename.isEmpty()) { //process cancelled by the user return; } QFile csvFile(outputFilename); if (!csvFile.open(QFile::WriteOnly | QFile::Text)) { ccConsole::Error(tr("Failed to open file for writing! (check file permissions)")); return; } //save last saving location settings.setValue(ccPS::CurrentPath(), QFileInfo(outputFilename).absolutePath()); settings.endGroup(); //write CSV header QTextStream csvStream(&csvFile); csvStream << "Name;"; csvStream << "Width;"; csvStream << "Height;"; csvStream << "Cx;"; csvStream << "Cy;"; csvStream << "Cz;"; csvStream << "Nx;"; csvStream << "Ny;"; csvStream << "Nz;"; csvStream << "Dip;"; csvStream << "Dip dir;"; csvStream << endl; QChar separator(';'); //write one line per plane for (ccHObject* ent : planes) { ccPlane* plane = static_cast(ent); CCVector3 C = plane->getOwnBB().getCenter(); CCVector3 N = plane->getNormal(); PointCoordinateType dip_deg = 0; PointCoordinateType dipDir_deg = 0; ccNormalVectors::ConvertNormalToDipAndDipDir(N, dip_deg, dipDir_deg); csvStream << plane->getName() << separator; //Name csvStream << plane->getXWidth() << separator; //Width csvStream << plane->getYWidth() << separator; //Height csvStream << C.x << separator; //Cx csvStream << C.y << separator; //Cy csvStream << C.z << separator; //Cz csvStream << N.x << separator; //Nx csvStream << N.y << separator; //Ny csvStream << N.z << separator; //Nz csvStream << dip_deg << separator; //Dip csvStream << dipDir_deg << separator; //Dip direction csvStream << endl; } ccConsole::Print(tr("[I/O] File '%1' successfully saved (%2 plane(s))").arg(outputFilename).arg(planes.size())); csvFile.close(); } void MainWindow::doActionExportCloudInfo() { //look for clouds ccHObject::Container clouds; const ccHObject::Container& selectedEntities = getSelectedEntities(); if (selectedEntities.size() == 1 && selectedEntities.front()->isA(CC_TYPES::HIERARCHY_OBJECT)) { //a group selectedEntities.front()->filterChildren(clouds, true, CC_TYPES::POINT_CLOUD, true); } else { for (ccHObject* entity : selectedEntities) { ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity); if (cloud) { clouds.push_back(cloud); } } } if (clouds.empty()) { ccConsole::Error(tr("Select at least one point cloud!")); return; } //persistent settings QSettings settings; settings.beginGroup(ccPS::SaveFile()); QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); QString outputFilename = QFileDialog::getSaveFileName( this, tr("Select output file"), currentPath, "*.csv", nullptr, CCFileDialogOptions()); if (outputFilename.isEmpty()) { //process cancelled by the user return; } QFile csvFile(outputFilename); if (!csvFile.open(QFile::WriteOnly | QFile::Text)) { ccConsole::Error(tr("Failed to open file for writing! (check file permissions)")); return; } //save last saving location settings.setValue(ccPS::CurrentPath(), QFileInfo(outputFilename).absolutePath()); settings.endGroup(); //determine the maximum number of SFs unsigned maxSFCount = 0; for (ccHObject* entity : clouds) { maxSFCount = std::max(maxSFCount, static_cast(entity)->getNumberOfScalarFields()); } //write CSV header QTextStream csvStream(&csvFile); csvStream << "Name;"; csvStream << "Points;"; csvStream << "meanX;"; csvStream << "meanY;"; csvStream << "meanZ;"; { for (unsigned i = 0; i < maxSFCount; ++i) { QString sfIndex = QString("SF#%1").arg(i + 1); csvStream << sfIndex << " name;"; csvStream << sfIndex << " valid values;"; csvStream << sfIndex << " mean;"; csvStream << sfIndex << " std.dev.;"; csvStream << sfIndex << " sum;"; } } csvStream << endl; //write one line per cloud { for (ccHObject* entity : clouds) { ccPointCloud* cloud = static_cast(entity); CCVector3 G = *CCCoreLib::Neighbourhood(cloud).getGravityCenter(); csvStream << cloud->getName() << ';' /*"Name;"*/; csvStream << cloud->size() << ';' /*"Points;"*/; csvStream << G.x << ';' /*"meanX;"*/; csvStream << G.y << ';' /*"meanY;"*/; csvStream << G.z << ';' /*"meanZ;"*/; for (unsigned j = 0; j < cloud->getNumberOfScalarFields(); ++j) { CCCoreLib::ScalarField* sf = cloud->getScalarField(j); csvStream << QString::fromStdString(sf->getName()) << ';' /*"SF name;"*/; unsigned validCount = 0; double sfSum = 0.0; double sfSum2 = 0.0; for (unsigned k = 0; k < sf->currentSize(); ++k) { const ScalarType& val = sf->getValue(k); if (CCCoreLib::ScalarField::ValidValue(val)) { ++validCount; sfSum += val; sfSum2 += static_cast(val)*val; } } csvStream << validCount << ';' /*"SF valid values;"*/; if (validCount) { double mean = sfSum / validCount; csvStream << mean << ';' /*"SF mean;"*/; csvStream << sqrt(std::abs(sfSum2 / validCount - mean * mean)) << ';' /*"SF std.dev.;"*/; } else { csvStream << "N/A;" /*"SF mean;"*/; csvStream << "N/A;" /*"SF std.dev.;"*/; } csvStream << sfSum << ';' /*"SF sum;"*/; } csvStream << endl; } } ccConsole::Print(tr("[I/O] File '%1' successfully saved (%2 cloud(s))").arg(outputFilename).arg(clouds.size())); csvFile.close(); } void MainWindow::doActionCloudCloudDist() { if (getSelectedEntities().size() != 2) { ccConsole::Error(tr("Select 2 point clouds!")); return; } if (!m_selectedEntities.front()->isKindOf(CC_TYPES::POINT_CLOUD) || !m_selectedEntities.back()->isKindOf(CC_TYPES::POINT_CLOUD)) { ccConsole::Error(tr("Select 2 point clouds!")); return; } ccOrderChoiceDlg dlg( m_selectedEntities.front(), tr("Compared"), m_selectedEntities.back(), tr("Reference"), this ); if (!dlg.exec()) return; ccGenericPointCloud* compCloud = ccHObjectCaster::ToGenericPointCloud(dlg.getFirstEntity()); ccGenericPointCloud* refCloud = ccHObjectCaster::ToGenericPointCloud(dlg.getSecondEntity()); //assert(!m_compDlg); if (m_compDlg) delete m_compDlg; m_compDlg = new ccComparisonDlg(compCloud, refCloud, ccComparisonDlg::CLOUDCLOUD_DIST, this); if (!m_compDlg->initDialog()) { ccConsole::Error(tr("Failed to initialize comparison dialog")); delete m_compDlg; m_compDlg = nullptr; return; } connect(m_compDlg, &QDialog::finished, this, &MainWindow::deactivateComparisonMode); m_compDlg->show(); //cDlg.setModal(false); //cDlg.exec(); freezeUI(true); } void MainWindow::doActionCloudMeshDist() { if (getSelectedEntities().size() != 2) { ccConsole::Error(tr("Select 2 entities!")); return; } bool isMesh[2]{ false,false }; unsigned meshNum = 0; unsigned cloudNum = 0; for (unsigned i = 0; i < 2; ++i) { if (m_selectedEntities[i]->isKindOf(CC_TYPES::MESH)) { ++meshNum; isMesh[i] = true; } else if (m_selectedEntities[i]->isKindOf(CC_TYPES::POINT_CLOUD)) { ++cloudNum; } } if (meshNum == 0) { ccConsole::Error(tr("Select at least one mesh!")); return; } else if (meshNum + cloudNum < 2) { ccConsole::Error(tr("Select one mesh and one cloud or two meshes!")); return; } ccHObject* compEnt = nullptr; ccGenericMesh* refMesh = nullptr; if (meshNum == 1) { compEnt = m_selectedEntities[isMesh[0] ? 1 : 0]; refMesh = ccHObjectCaster::ToGenericMesh(m_selectedEntities[isMesh[0] ? 0 : 1]); if (refMesh->isKindOf(CC_TYPES::PRIMITIVE)) { static bool DontShowPrimitiveDistWarning = false; if (!DontShowPrimitiveDistWarning) { QMessageBox::StandardButton answer = QMessageBox::warning( this, tr("Distance to primitive"), tr("Computing distances to a primitive is faster and more accurate with the 'Tools > Distances > Cloud / Primitive Dist.' tool.\nDo you want to use this other tool instead?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::NoToAll, QMessageBox::Yes ); if (answer == QMessageBox::Yes) { return doActionCloudPrimitiveDist(); } else if (answer == QMessageBox::NoToAll) { DontShowPrimitiveDistWarning = true; } } } } else { ccOrderChoiceDlg dlg( m_selectedEntities.front(), tr("Compared"), m_selectedEntities.back(), tr("Reference"), this ); if (!dlg.exec()) return; compEnt = dlg.getFirstEntity(); refMesh = ccHObjectCaster::ToGenericMesh(dlg.getSecondEntity()); } //assert(!m_compDlg); if (m_compDlg) delete m_compDlg; m_compDlg = new ccComparisonDlg(compEnt, refMesh, ccComparisonDlg::CLOUDMESH_DIST, this); if (!m_compDlg->initDialog()) { ccConsole::Error(tr("Failed to initialize comparison dialog")); delete m_compDlg; m_compDlg = nullptr; return; } connect(m_compDlg, &QDialog::finished, this, &MainWindow::deactivateComparisonMode); m_compDlg->show(); freezeUI(true); } void MainWindow::doActionCloudPrimitiveDist() { ccHObject::Container clouds; ccHObject* refEntity = nullptr; for (ccHObject* entity : getSelectedEntities()) { if (entity->isKindOf(CC_TYPES::PRIMITIVE) || entity->isA(CC_TYPES::POLY_LINE)) { if (entity->isA(CC_TYPES::PLANE) || entity->isA(CC_TYPES::SPHERE) || entity->isA(CC_TYPES::CYLINDER) || entity->isA(CC_TYPES::CONE) || entity->isA(CC_TYPES::BOX) || entity->isA(CC_TYPES::POLY_LINE)) { if (!refEntity) { // first primitive encountered refEntity = entity; } else { ccConsole::Error(tr("Select only one primitive (Plane/Box/Sphere/Cylinder/Cone) or polyline")); return; } } } else if (entity->isKindOf(CC_TYPES::POINT_CLOUD)) { clouds.push_back(entity); } } if (!refEntity) { ccConsole::Error(tr("Select one prmitive (Plane/Box/Sphere/Cylinder/Cone) or a polyline")); return; } if (clouds.empty()) { ccConsole::Error(tr("Select at least one cloud")); return; } ccPrimitiveDistanceDlg pDD{ this }; static bool s_treatPlanesAsBounded = false; static bool s_signedDist = true; static bool s_flipNormals = false; if (refEntity->isA(CC_TYPES::PLANE)) { pDD.treatPlanesAsBoundedCheckBox->setEnabled(true); pDD.treatPlanesAsBoundedCheckBox->setChecked(s_treatPlanesAsBounded); } else { pDD.treatPlanesAsBoundedCheckBox->setEnabled(false); } pDD.flipNormalsCheckBox->setChecked(s_flipNormals); pDD.signedDistCheckBox->setChecked(s_signedDist); if (!refEntity->isA(CC_TYPES::POLY_LINE)) { if (!pDD.exec()) { return; } } s_signedDist = pDD.signedDistances(); s_flipNormals = pDD.flipNormals(); s_treatPlanesAsBounded = pDD.treatPlanesAsBounded(); size_t errorCount = 0; for (auto &cloud : clouds) { ccPointCloud* compEnt = ccHObjectCaster::ToPointCloud(cloud); int sfIdx = compEnt->getScalarFieldIndexByName(CC_TEMP_DISTANCES_DEFAULT_SF_NAME); if (sfIdx < 0) { //we need to create a new scalar field sfIdx = compEnt->addScalarField(CC_TEMP_DISTANCES_DEFAULT_SF_NAME); if (sfIdx < 0) { ccLog::Warning(tr("[Compute Primitive Distances] [Cloud: %1] Couldn't allocate a new scalar field for computing distances! Try to free some memory ...").arg(compEnt->getName())); ++errorCount; continue; } } compEnt->setCurrentScalarField(sfIdx); if (!compEnt->enableScalarField()) { ccLog::Warning(tr("[Compute Primitive Distances] [Cloud: %1] Not enough memory").arg(compEnt->getName())); ++errorCount; continue; } compEnt->forEach(CCCoreLib::ScalarFieldTools::SetScalarValueToNaN); int returnCode = 0; QString errString = tr("[Compute Primitive Distances] Cloud to %1 distance computation failed (error code = %2)"); switch (refEntity->getClassID()) { case CC_TYPES::SPHERE: { if (!(returnCode = CCCoreLib::DistanceComputationTools::computeCloud2SphereEquation(compEnt, refEntity->getOwnBB().getCenter(), static_cast(refEntity)->getRadius(), s_signedDist))) ccConsole::Error(errString.arg(tr("Sphere")).arg(returnCode)); break; } case CC_TYPES::PLANE: { ccPlane* plane = static_cast(refEntity); if (s_treatPlanesAsBounded) { CCCoreLib::SquareMatrix rotationTransform(plane->getTransformation().data(), true); if (!(returnCode = CCCoreLib::DistanceComputationTools::computeCloud2RectangleEquation(compEnt, plane->getXWidth(), plane->getYWidth(), rotationTransform, plane->getCenter(), s_signedDist))) { ccConsole::Warning(errString.arg(tr("Bounded Plane")).arg(returnCode)); ++errorCount; } } else { if (!(returnCode = CCCoreLib::DistanceComputationTools::computeCloud2PlaneEquation(compEnt, static_cast(refEntity)->getEquation(), s_signedDist))) { ccConsole::Warning(errString.arg(tr("Infinite Plane")).arg(returnCode)); ++errorCount; } } break; } case CC_TYPES::CYLINDER: { if (!(returnCode = CCCoreLib::DistanceComputationTools::computeCloud2CylinderEquation(compEnt, static_cast(refEntity)->getBottomCenter(), static_cast(refEntity)->getTopCenter(), static_cast(refEntity)->getBottomRadius(), s_signedDist))) { ccConsole::Warning(errString.arg(tr("Cylinder")).arg(returnCode)); ++errorCount; } break; } case CC_TYPES::CONE: { if (!(returnCode = CCCoreLib::DistanceComputationTools::computeCloud2ConeEquation(compEnt, static_cast(refEntity)->getLargeCenter(), static_cast(refEntity)->getSmallCenter(), static_cast(refEntity)->getLargeRadius(), static_cast(refEntity)->getSmallRadius(), s_signedDist))) { ccConsole::Warning(errString.arg(tr("Cone")).arg(returnCode)); ++errorCount; } break; } case CC_TYPES::BOX: { const ccGLMatrix& glTransform = refEntity->getGLTransformationHistory(); CCCoreLib::SquareMatrix rotationTransform(glTransform.data(), true); CCVector3 boxCenter = glTransform.getColumnAsVec3D(3); if (!(returnCode = CCCoreLib::DistanceComputationTools::computeCloud2BoxEquation(compEnt, static_cast(refEntity)->getDimensions(), rotationTransform, boxCenter, s_signedDist))) { ccConsole::Warning(errString.arg(tr("Box")).arg(returnCode)); ++errorCount; } break; } case CC_TYPES::POLY_LINE: { ccPolyline* line = static_cast(refEntity); returnCode = CCCoreLib::DistanceComputationTools::computeCloud2PolylineEquation(compEnt, line); if (!returnCode) { ccConsole::Warning(errString.arg(tr("Polyline")).arg(returnCode)); ++errorCount; } break; } default: { ccConsole::Error(tr("Unsupported primitive type")); //Shouldn't ever reach here... return; } } QString sfName(CC_CLOUD2PRIMITIVE_DISTANCES_DEFAULT_SF_NAME); if (!refEntity->isKindOf(CC_TYPES::POLY_LINE)) { if (s_signedDist) { sfName = CC_CLOUD2PRIMITIVE_SIGNED_DISTANCES_DEFAULT_SF_NAME; } if (s_flipNormals) { compEnt->forEach(CCCoreLib::ScalarFieldTools::SetScalarValueInverted); sfName += "[-]"; } } int _sfIdx = compEnt->getScalarFieldIndexByName(sfName.toStdString()); if (_sfIdx >= 0) { compEnt->deleteScalarField(_sfIdx); //we update sfIdx because indexes are all messed up after deletion sfIdx = compEnt->getScalarFieldIndexByName(CC_TEMP_DISTANCES_DEFAULT_SF_NAME); } compEnt->renameScalarField(sfIdx, sfName.toStdString()); ccScalarField* sf = static_cast(compEnt->getScalarField(sfIdx)); if (sf) { ScalarType mean; ScalarType variance; sf->computeMinAndMax(); sf->computeMeanAndVariance(mean, &variance); ccLog::Print(tr("[Compute Primitive Distances] [Primitive: %1] [Cloud: %2] [%3] Mean distance = %4 / std deviation = %5") .arg(refEntity->getName()) .arg(compEnt->getName()) .arg(sfName) .arg(mean) .arg(sqrt(variance))); } compEnt->setCurrentDisplayedScalarField(sfIdx); compEnt->showSF(sfIdx >= 0); compEnt->prepareDisplayForRefresh_recursive(); } if (errorCount != 0) { ccLog::Error(tr("%1 error(s) occurred: refer to the Console (F8)").arg(errorCount)); } MainWindow::UpdateUI(); MainWindow::RefreshAllGLWindow(false); } void MainWindow::deactivateComparisonMode(int result) { //DGM: a bug apperead with recent changes (from CC or QT?) //which prevent us from deleting the dialog right away... //(it seems that QT has not yet finished the dialog closing //when the 'finished' signal is sent). //if(m_compDlg) // delete m_compDlg; //m_compDlg = 0; //if the comparison is a success, we select only the compared entity if (m_compDlg && result == QDialog::Accepted && m_ccRoot) { ccHObject* compEntity = m_compDlg->getComparedEntity(); if (compEntity) { m_ccRoot->selectEntity(compEntity); } } freezeUI(false); updateUI(); } void MainWindow::toggleActiveWindowSunLight() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->toggleSunLight(); win->redraw(false); } } void MainWindow::toggleActiveWindowCustomLight() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->toggleCustomLight(); win->redraw(false); } } void MainWindow::toggleActiveWindowAutoPickRotCenter(bool state) { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setAutoPickPivotAtCenter(state); //save the option { QSettings settings; settings.setValue(ccPS::AutoPickRotationCenter(), state); } } } void MainWindow::toggleActiveWindowShowCursorCoords(bool state) { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->showCursorCoordinates(state); } } void MainWindow::toggleActiveWindowStereoVision(bool state) { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { bool isActive = win->stereoModeIsEnabled(); if (isActive == state) { //nothing to do return; } if (isActive) { win->disableStereoMode(); if ( win->getStereoParams().glassType == ccGLWindowInterface::StereoParams::NVIDIA_VISION || win->getStereoParams().glassType == ccGLWindowInterface::StereoParams::GENERIC_STEREO_DISPLAY) { //disable (exclusive) full screen m_UI->actionExclusiveFullScreen->setChecked(false); } } else { //display a parameters dialog ccStereoModeDlg smDlg(this); smDlg.setParameters(win->getStereoParams()); if (!smDlg.exec()) { //cancelled by the user m_UI->actionEnableStereo->blockSignals(true); m_UI->actionEnableStereo->setChecked(false); m_UI->actionEnableStereo->blockSignals(false); return; } ccGLWindowInterface::StereoParams params = smDlg.getParameters(); ccLog::WarningDebug("Stereo strength: " + QString::number(params.stereoStrength)); if (!ccGLWindowInterface::StereoSupported() && !params.isAnaglyph()) { ccLog::Error(tr("It seems your graphic card doesn't support Quad Buffered Stereo rendering")); //activation of the stereo mode failed: cancel selection m_UI->actionEnableStereo->blockSignals(true); m_UI->actionEnableStereo->setChecked(false); m_UI->actionEnableStereo->blockSignals(false); return; } //force perspective state! if (!win->getViewportParameters().perspectiveView) { setCenteredPerspectiveView(win, false); } if ( params.glassType == ccGLWindowInterface::StereoParams::NVIDIA_VISION || params.glassType == ccGLWindowInterface::StereoParams::GENERIC_STEREO_DISPLAY) { //force (exclusive) full screen m_UI->actionExclusiveFullScreen->setChecked(true); } if (smDlg.updateFOV()) { //set the right FOV double fov_deg = 2 * CCCoreLib::RadiansToDegrees( std::atan(params.screenWidth_mm / (2.0 * params.screenDistance_mm)) ); ccLog::Print(tr("[Stereo] F.O.V. forced to %1 deg.").arg(fov_deg)); win->setFov(fov_deg); } if (!win->enableStereoMode(params)) { if ( params.glassType == ccGLWindowInterface::StereoParams::NVIDIA_VISION || params.glassType == ccGLWindowInterface::StereoParams::GENERIC_STEREO_DISPLAY) { //disable (exclusive) full screen m_UI->actionExclusiveFullScreen->setChecked(false); } //activation of the stereo mode failed: cancel selection m_UI->actionEnableStereo->blockSignals(true); m_UI->actionEnableStereo->setChecked(false); m_UI->actionEnableStereo->blockSignals(false); } } win->redraw(); } } bool MainWindow::checkStereoMode(ccGLWindowInterface* win) { assert(win); if (win && win->getViewportParameters().perspectiveView && win->stereoModeIsEnabled()) { ccGLWindowInterface::StereoParams params = win->getStereoParams(); bool wasExclusiveFullScreen = win->exclusiveFullScreen(); if (wasExclusiveFullScreen) { win->toggleExclusiveFullScreen(false); } win->disableStereoMode(); if (QMessageBox::question( this, tr("Stereo mode"), tr("Stereo-mode only works in perspective mode. Do you want to disable it?"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No ) { if (wasExclusiveFullScreen) { win->toggleExclusiveFullScreen(true); win->enableStereoMode(params); } return false; } else { if (win == getActiveGLWindow()) { m_UI->actionEnableStereo->setChecked(false); } else { assert(false); m_UI->actionEnableStereo->blockSignals(true); m_UI->actionEnableStereo->setChecked(false); m_UI->actionEnableStereo->blockSignals(false); } } } return true; } void MainWindow::toggleActiveWindowCenteredPerspective() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { const ccViewportParameters& params = win->getViewportParameters(); if (params.perspectiveView && params.objectCenteredView && !checkStereoMode(win)) //we need to check this only if we are already in object-centered perspective mode { return; } win->togglePerspective(true); win->redraw(false); updateViewModePopUpMenu(win); updatePivotVisibilityPopUpMenu(win); } } void MainWindow::toggleActiveWindowViewerBasedPerspective() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { const ccViewportParameters& params = win->getViewportParameters(); if (params.perspectiveView && !params.objectCenteredView && !checkStereoMode(win)) //we need to check this only if we are already in viewer-based perspective mode { return; } win->togglePerspective(false); win->redraw(false); updateViewModePopUpMenu(win); updatePivotVisibilityPopUpMenu(win); } } void MainWindow::createSinglePointCloud() { // ask the user to input the point coordinates static CCVector3d s_lastPoint(0, 0, 0); static size_t s_lastPointIndex = 0; ccAskThreeDoubleValuesDlg axisDlg("x", "y", "z", -1.0e12, 1.0e12, s_lastPoint.x, s_lastPoint.y, s_lastPoint.z, 4, tr("Point coordinates"), this); if (axisDlg.buttonBox->button(QDialogButtonBox::Ok)) axisDlg.buttonBox->button(QDialogButtonBox::Ok)->setFocus(); if (!axisDlg.exec()) return; s_lastPoint.x = axisDlg.doubleSpinBox1->value(); s_lastPoint.y = axisDlg.doubleSpinBox2->value(); s_lastPoint.z = axisDlg.doubleSpinBox3->value(); // create the cloud ccPointCloud* cloud = new ccPointCloud(); if (!cloud->reserve(1)) { delete cloud; ccLog::Error(tr("Not enough memory!")); return; } cloud->setName(tr("Point #%1").arg(++s_lastPointIndex)); cloud->addPoint(s_lastPoint.toPC()); cloud->setPointSize(5); // add it to the DB tree addToDB(cloud, true, true, true, true); // select it m_ccRoot->unselectAllEntities(); setSelectedInDB(cloud, true); } void MainWindow::createPointCloudFromClipboard() { const QClipboard* clipboard = QApplication::clipboard(); assert(clipboard); const QMimeData* mimeData = clipboard->mimeData(); if (!mimeData) { ccLog::Warning(tr("Clipboard is empty")); return; } if (!mimeData->hasText()) { ccLog::Error("ASCII/text data expected"); return; } // try to convert the data to a point cloud FileIOFilter::LoadParameters parameters; { parameters.alwaysDisplayLoadDialog = true; parameters.shiftHandlingMode = ccGlobalShiftManager::DIALOG_IF_NECESSARY; parameters.parentWidget = this; } ccHObject container; QByteArray data = mimeData->data("text/plain"); CC_FILE_ERROR result = AsciiFilter().loadAsciiData(data, tr("Clipboard"), container, parameters); if (result != CC_FERR_NO_ERROR) { FileIOFilter::DisplayErrorMessage(result, tr("loading"), tr("from the clipboard")); return; } // we only expect clouds ccHObject::Container clouds; if (container.filterChildren(clouds, true, CC_TYPES::POINT_CLOUD) == 0) { assert(false); ccLog::Error(tr("No cloud loaded")); return; } // detach the clouds from the loading container for (ccHObject* cloud : clouds) { if (cloud) { container.removeDependencyWith(cloud); } } container.removeAllChildren(); // retrieve or create the group to store the 'clipboard' clouds ccHObject* clipboardGroup = nullptr; { static unsigned s_clipboardGroupID = 0; if (s_clipboardGroupID != 0) { clipboardGroup = dbRootObject()->find(s_clipboardGroupID); if (nullptr == clipboardGroup) { // can't find the previous group s_clipboardGroupID = 0; } } if (s_clipboardGroupID == 0) { clipboardGroup = new ccHObject(tr("Clipboard")); s_clipboardGroupID = clipboardGroup->getUniqueID(); addToDB(clipboardGroup, false, false, false, false); } } assert(clipboardGroup); bool normalsDisplayedByDefault = ccOptions::Instance().normalsDisplayedByDefault; for (ccHObject* cloud : clouds) { if (cloud) { clipboardGroup->addChild(cloud); cloud->setName(tr("Cloud #%1").arg(clipboardGroup->getChildrenNumber())); if (!normalsDisplayedByDefault) { // disable the normals on all loaded clouds! static_cast(cloud)->showNormals(false); } } } // eventually, we can add the clouds to the DB tree for (size_t i = 0; i < clouds.size(); ++i) { ccHObject* cloud = clouds[i]; if (cloud) { bool lastCloud = (i + 1 == clouds.size()); addToDB(cloud, lastCloud, lastCloud, true, lastCloud); } } QMainWindow::statusBar()->showMessage(tr("%1 cloud(s) loaded from the clipboard").arg(clouds.size()), 2000); } void MainWindow::toggleLockRotationAxis() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { bool wasLocked = win->isRotationAxisLocked(); bool isLocked = !wasLocked; static CCVector3d s_lastAxis(0.0, 0.0, 1.0); if (isLocked) { ccAskThreeDoubleValuesDlg axisDlg("x", "y", "z", -1.0e12, 1.0e12, s_lastAxis.x, s_lastAxis.y, s_lastAxis.z, 4, tr("Lock rotation axis"), this); if (axisDlg.buttonBox->button(QDialogButtonBox::Ok)) axisDlg.buttonBox->button(QDialogButtonBox::Ok)->setFocus(); if (!axisDlg.exec()) return; s_lastAxis.x = axisDlg.doubleSpinBox1->value(); s_lastAxis.y = axisDlg.doubleSpinBox2->value(); s_lastAxis.z = axisDlg.doubleSpinBox3->value(); } win->lockRotationAxis(isLocked, s_lastAxis); m_UI->actionLockRotationAxis->blockSignals(true); m_UI->actionLockRotationAxis->setChecked(isLocked); m_UI->actionLockRotationAxis->blockSignals(false); if (isLocked) { win->displayNewMessage(tr("[ROTATION LOCKED]"), ccGLWindowInterface::UPPER_CENTER_MESSAGE, false, 24 * 3600, ccGLWindowInterface::ROTAION_LOCK_MESSAGE); } else { win->displayNewMessage(QString(), ccGLWindowInterface::UPPER_CENTER_MESSAGE, false, 0, ccGLWindowInterface::ROTAION_LOCK_MESSAGE); } win->redraw(true, false); } } void MainWindow::doActionEnableBubbleViewMode() { //special case: the selected entity is a TLS sensor or a cloud with a TLS sensor if (m_ccRoot) { ccHObject::Container selectedEntities; m_ccRoot->getSelectedEntities(selectedEntities); if (selectedEntities.size() == 1) { ccHObject* ent = selectedEntities.front(); ccGBLSensor* sensor = nullptr; if (ent->isA(CC_TYPES::GBL_SENSOR)) { sensor = static_cast(ent); } else if (ent->isA(CC_TYPES::POINT_CLOUD)) { ccHObject::Container sensors; ent->filterChildren(sensors, false, CC_TYPES::GBL_SENSOR, true); if (sensors.size() >= 1) { sensor = static_cast(sensors.front()); } } if (sensor) { sensor->applyViewport(); return; } } } //otherwise we simply enable the bubble view mode in the active 3D view ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setBubbleViewMode(true); win->redraw(false); } } void MainWindow::doActionDeleteShader() { ccGLWindowInterface* win = getActiveGLWindow(); if (win) { win->setShader(nullptr); } } void MainWindow::removeFromDB(ccHObject* obj, bool autoDelete/*=true*/) { if (!obj) return; //remove dependency to avoid deleting the object when removing it from DB tree if (!autoDelete && obj->getParent()) obj->getParent()->removeDependencyWith(obj); if (m_ccRoot) m_ccRoot->removeElement(obj); } void MainWindow::setSelectedInDB(ccHObject* obj, bool selected) { if (obj && m_ccRoot) { if (selected) m_ccRoot->selectEntity(obj); else m_ccRoot->unselectEntity(obj); } } void MainWindow::addToDB( ccHObject* obj, bool updateZoom/*=true*/, bool autoExpandDBTree/*=true*/, bool checkDimensions/*=true*/, bool autoRedraw/*=true*/) { //let's check that the new entity is not too big nor too far from scene center! if (checkDimensions) { //get entity bounding box ccBBox bBox = obj->getBB_recursive(); CCVector3 center = bBox.getCenter(); PointCoordinateType diag = bBox.getDiagNorm(); CCVector3d P = center; CCVector3d Pshift(0, 0, 0); double scale = 1.0; bool preserveCoordinateShift = true; //here we must test that coordinates are not too big whatever the case because OpenGL //really doesn't like big ones (even if we work with GLdoubles :( ). if (ccGlobalShiftManager::Handle( P, diag, ccGlobalShiftManager::DIALOG_IF_NECESSARY, false, Pshift, &preserveCoordinateShift, &scale) ) { bool needRescale = (scale != 1.0); bool needShift = (Pshift.norm2() > 0); if (needRescale || needShift) { ccGLMatrix mat; mat.toIdentity(); mat.data()[0] = mat.data()[5] = mat.data()[10] = static_cast(scale); mat.setTranslation(Pshift); obj->applyGLTransformation_recursive(&mat); ccConsole::Warning(tr("Entity '%1' has been translated: (%2,%3,%4) and rescaled of a factor %5 [original position will be restored when saving]").arg(obj->getName()).arg(Pshift.x,0,'f',2).arg(Pshift.y,0,'f',2).arg(Pshift.z,0,'f',2).arg(scale,0,'f',6)); } //update 'global shift' and 'global scale' for ALL clouds recursively if (preserveCoordinateShift) { //FIXME: why don't we do that all the time by the way?! ccHObject::Container children; children.push_back(obj); while (!children.empty()) { ccHObject* child = children.back(); children.pop_back(); if (child->isKindOf(CC_TYPES::POINT_CLOUD)) { ccGenericPointCloud* pc = ccHObjectCaster::ToGenericPointCloud(child); pc->setGlobalShift(pc->getGlobalShift() + Pshift); pc->setGlobalScale(pc->getGlobalScale() * scale); } for (unsigned i = 0; i < child->getChildrenNumber(); ++i) { children.push_back(child->getChild(i)); } } } } } //add object to DB root if (m_ccRoot) { //force a 'global zoom' if the DB was emtpy! if (!m_ccRoot->getRootEntity() || m_ccRoot->getRootEntity()->getChildrenNumber() == 0) { updateZoom = true; } m_ccRoot->addElement(obj, autoExpandDBTree); } else { ccLog::Warning(tr("[MainWindow::addToDB] Internal error: no associated DB?!")); assert(false); } //we can now set destination display (if none already) if (!obj->getDisplay()) { ccGLWindowInterface* activeWin = getActiveGLWindow(); if (!activeWin) { //no active GL window?! return; } obj->setDisplay_recursive(activeWin); } //eventually we update the corresponding display assert(obj->getDisplay()); if (updateZoom) { static_cast(obj->getDisplay())->zoomGlobal(); //automatically calls ccGLWindowInterface::redraw } else if (autoRedraw) { obj->redrawDisplay(); } } void MainWindow::onExclusiveFullScreenToggled(bool state) { //we simply update the fullscreen action method icon (whatever the window) ccGLWindowInterface* win = getActiveGLWindow(); if (win == nullptr) return; m_UI->actionExclusiveFullScreen->blockSignals(true); m_UI->actionExclusiveFullScreen->setChecked(win->exclusiveFullScreen()); m_UI->actionExclusiveFullScreen->blockSignals(false); if ( !state && win->stereoModeIsEnabled() && ( win->getStereoParams().glassType == ccGLWindowInterface::StereoParams::NVIDIA_VISION || win->getStereoParams().glassType == ccGLWindowInterface::StereoParams::GENERIC_STEREO_DISPLAY )) { //auto disable stereo mode as NVidia Vision only works in full screen mode! m_UI->actionEnableStereo->setChecked(false); } } ccHObject* MainWindow::loadFile(QString filename, bool silent) { FileIOFilter::LoadParameters parameters; { parameters.alwaysDisplayLoadDialog = silent ? false : true; parameters.shiftHandlingMode = ccGlobalShiftManager::NO_DIALOG_AUTO_SHIFT; parameters.parentWidget = silent ? nullptr : this; } CC_FILE_ERROR result = CC_FERR_NO_ERROR; ccHObject* newGroup = FileIOFilter::LoadFromFile(filename, parameters, result); return newGroup; } void MainWindow::addToDBAuto(const QStringList& filenames) { ccGLWindowInterface* win = ccGLWindowInterface::FromEmitter(sender()); if (win) { addToDB(filenames, QString(), win); } else { assert(false); } } void MainWindow::addToDB( const QStringList& filenames, QString fileFilter/*=QString()*/, ccGLWindowInterface* destWin/*=nullptr*/) { //to use the same 'global shift' for multiple files CCVector3d loadCoordinatesShift(0, 0, 0); bool loadCoordinatesTransEnabled = false; bool loadCoordinatesTransForced = false; FileIOFilter::LoadParameters parameters; { parameters.alwaysDisplayLoadDialog = true; parameters.shiftHandlingMode = ccGlobalShiftManager::DIALOG_IF_NECESSARY; parameters._coordinatesShift = &loadCoordinatesShift; parameters._coordinatesShiftEnabled = &loadCoordinatesTransEnabled; parameters._coordinatesShiftForced = &loadCoordinatesTransForced; parameters.parentWidget = this; } bool normalsDisplayedByDefault = ccOptions::Instance().normalsDisplayedByDefault; FileIOFilter::ResetSesionCounter(); for ( const QString &filename : filenames ) { CC_FILE_ERROR result = CC_FERR_NO_ERROR; ccHObject* newGroup = FileIOFilter::LoadFromFile(filename, parameters, result, fileFilter); if (newGroup) { if (!normalsDisplayedByDefault) { //disable the normals on all loaded clouds! ccHObject::Container clouds; newGroup->filterChildren(clouds, true, CC_TYPES::POINT_CLOUD); for (ccHObject* cloud : clouds) { if (cloud) { static_cast(cloud)->showNormals(false); } } } if (destWin) { newGroup->setDisplay_recursive(destWin); } addToDB(newGroup, true, true, false); m_recentFiles->addFilePath( filename ); } if (result == CC_FERR_CANCELED_BY_USER) { //stop importing the file if the user has cancelled the current process! break; } } QMainWindow::statusBar()->showMessage(tr("%1 file(s) loaded").arg(filenames.size()),2000); } void MainWindow::handleNewLabel(ccHObject* entity) { if (entity) { addToDB(entity, false, true, false, false); } else { assert(false); } } void MainWindow::forceConsoleDisplay() { //if the console is hidden, we autoamtically display it! if (m_UI->DockableConsole && m_UI->DockableConsole->isHidden()) { m_UI->DockableConsole->show(); QApplication::processEvents(); } } ccColorScalesManager* MainWindow::getColorScalesManager() { return ccColorScalesManager::GetUniqueInstance(); } void MainWindow::closeAll() { if (!m_ccRoot) { return; } QMessageBox message_box( QMessageBox::Question, tr("Close all"), tr("Are you sure you want to remove all loaded entities?"), QMessageBox::Yes | QMessageBox::No, this ); if (message_box.exec() == QMessageBox::No) { return; } m_ccRoot->unloadAll(); redrawAll(false); } void MainWindow::doActionLoadFile() { //persistent settings QSettings settings; settings.beginGroup(ccPS::LoadFile()); QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); QString currentOpenDlgFilter = settings.value(ccPS::SelectedInputFilter(), BinFilter::GetFileFilter()).toString(); // Add all available file I/O filters (with import capabilities) const QStringList filterStrings = FileIOFilter::ImportFilterList(); const QString &allFilter = filterStrings.at( 0 ); if ( !filterStrings.contains( currentOpenDlgFilter ) ) { currentOpenDlgFilter = allFilter; } //file choosing dialog QStringList selectedFiles = QFileDialog::getOpenFileNames( this, tr("Open file(s)"), currentPath, filterStrings.join(s_fileFilterSeparator), ¤tOpenDlgFilter, CCFileDialogOptions()); if (selectedFiles.isEmpty()) return; //save last loading parameters currentPath = QFileInfo(selectedFiles[0]).absolutePath(); settings.setValue(ccPS::CurrentPath(),currentPath); settings.setValue(ccPS::SelectedInputFilter(),currentOpenDlgFilter); settings.endGroup(); if (currentOpenDlgFilter == allFilter) { currentOpenDlgFilter.clear(); //this way FileIOFilter will try to guess the file type automatically! } //load files addToDB(selectedFiles, currentOpenDlgFilter); } //Helper: check for a filename validity static bool IsValidFileName(QString filename) { #ifdef CC_WINDOWS QString sPattern("^(?!^(PRN|AUX|CLOCK\\$|NUL|CON|COM\\d|LPT\\d|\\..*)(\\..+)?$)[^\\x00-\\x1f\\\\?*:\\"";|/]+$"); #else QString sPattern("^(([a-zA-Z]:|\\\\)\\\\)?(((\\.)|(\\.\\.)|([^\\\\/:\\*\\?""\\|<>\\. ](([^\\\\/:\\*\\?""\\|<>\\. ])|([^\\\\/:\\*\\?""\\|<>]*[^\\\\/:\\*\\?""\\|<>\\. ]))?))\\\\)*[^\\\\/:\\*\\?""\\|<>\\. ](([^\\\\/:\\*\\?""\\|<>\\. ])|([^\\\\/:\\*\\?""\\|<>]*[^\\\\/:\\*\\?""\\|<>\\. ]))?$"); #endif return QRegExp(sPattern).exactMatch(filename); } void MainWindow::doActionSaveFile() { if (!haveSelection()) return; ccHObject clouds("clouds"); ccHObject meshes("meshes"); ccHObject images("images"); ccHObject polylines("polylines"); ccHObject other("other"); ccHObject otherSerializable("serializable"); ccHObject::Container entitiesToDispatch; entitiesToDispatch.insert(entitiesToDispatch.begin(), m_selectedEntities.begin(), m_selectedEntities.end()); ccHObject entitiesToSave; while (!entitiesToDispatch.empty()) { ccHObject* child = entitiesToDispatch.back(); entitiesToDispatch.pop_back(); if (child->isA(CC_TYPES::HIERARCHY_OBJECT)) { for (unsigned j = 0; j < child->getChildrenNumber(); ++j) entitiesToDispatch.push_back(child->getChild(j)); } else { //we put the entity in the container corresponding to its type ccHObject* dest = nullptr; if (child->isA(CC_TYPES::POINT_CLOUD)) dest = &clouds; else if (child->isKindOf(CC_TYPES::MESH)) dest = &meshes; else if (child->isKindOf(CC_TYPES::IMAGE)) dest = &images; else if (child->isKindOf(CC_TYPES::POLY_LINE)) dest = &polylines; else if (child->isSerializable()) dest = &otherSerializable; else dest = &other; assert(dest); //we don't want double insertions if the user has highlighted both the father and the child if (!dest->find(child->getUniqueID())) { dest->addChild(child, ccHObject::DP_NONE); entitiesToSave.addChild(child, ccHObject::DP_NONE); } } } bool hasCloud = (clouds.getChildrenNumber() != 0); bool hasMesh = (meshes.getChildrenNumber() != 0); bool hasImages = (images.getChildrenNumber() != 0); bool hasPolylines = (polylines.getChildrenNumber() != 0); bool hasSerializable = (otherSerializable.getChildrenNumber() != 0); bool hasOther = (other.getChildrenNumber() != 0); int stdSaveTypes = static_cast(hasCloud) + static_cast(hasMesh) + static_cast(hasImages) + static_cast(hasPolylines) + static_cast(hasSerializable); if (stdSaveTypes == 0) { ccConsole::Error(tr("Can't save selected entity(ies) this way!")); return; } //we set up the right file filters, depending on the selected //entities type (cloud, mesh, etc.). QStringList fileFilters; { for ( const FileIOFilter::Shared& filter : FileIOFilter::GetFilters() ) { bool atLeastOneExclusive = false; //can this filter export one or several clouds? bool canExportClouds = true; if (hasCloud) { bool isExclusive = true; bool multiple = false; canExportClouds = ( filter->canSave(CC_TYPES::POINT_CLOUD, multiple, isExclusive) && (multiple || clouds.getChildrenNumber() == 1) ); atLeastOneExclusive |= isExclusive; } //can this filter export one or several meshes? bool canExportMeshes = true; if (hasMesh) { bool isExclusive = true; bool multiple = false; canExportMeshes = ( filter->canSave(CC_TYPES::MESH, multiple, isExclusive) && (multiple || meshes.getChildrenNumber() == 1) ); atLeastOneExclusive |= isExclusive; } //can this filter export one or several polylines? bool canExportPolylines = true; if (hasPolylines) { bool isExclusive = true; bool multiple = false; canExportPolylines = ( filter->canSave(CC_TYPES::POLY_LINE, multiple, isExclusive) && (multiple || polylines.getChildrenNumber() == 1) ); atLeastOneExclusive |= isExclusive; } //can this filter export one or several images? bool canExportImages = true; if (hasImages) { bool isExclusive = true; bool multiple = false; canExportImages = ( filter->canSave(CC_TYPES::IMAGE, multiple, isExclusive) && (multiple || images.getChildrenNumber() == 1) ); atLeastOneExclusive |= isExclusive; } //can this filter export one or several other serializable entities? bool canExportSerializables = true; if (hasSerializable) { //check if all entities have the same type { CC_CLASS_ENUM firstClassID = otherSerializable.getChild(0)->getUniqueID(); for (unsigned j = 1; j < otherSerializable.getChildrenNumber(); ++j) { if (otherSerializable.getChild(j)->getUniqueID() != firstClassID) { //we add a virtual second 'stdSaveType' so as to properly handle exlusivity ++stdSaveTypes; break; } } } for (unsigned j = 0; j < otherSerializable.getChildrenNumber(); ++j) { ccHObject* child = otherSerializable.getChild(j); bool isExclusive = true; bool multiple = false; canExportSerializables &= ( filter->canSave(child->getClassID(), multiple, isExclusive) && (multiple || otherSerializable.getChildrenNumber() == 1) ); atLeastOneExclusive |= isExclusive; } } bool useThisFilter = canExportClouds && canExportMeshes && canExportImages && canExportPolylines && canExportSerializables && (!atLeastOneExclusive || stdSaveTypes == 1); if (useThisFilter) { QStringList ff = filter->getFileFilters(false); for (int j = 0; j < ff.size(); ++j) fileFilters.append(ff[j]); } } } //persistent settings QSettings settings; settings.beginGroup(ccPS::SaveFile()); //default filter QString selectedFilter = fileFilters.first(); if (hasCloud) selectedFilter = settings.value(ccPS::SelectedOutputFilterCloud(), selectedFilter).toString(); else if (hasMesh) selectedFilter = settings.value(ccPS::SelectedOutputFilterMesh(), selectedFilter).toString(); else if (hasImages) selectedFilter = settings.value(ccPS::SelectedOutputFilterImage(), selectedFilter).toString(); else if (hasPolylines) selectedFilter = settings.value(ccPS::SelectedOutputFilterPoly(), selectedFilter).toString(); //default output path (+ filename) QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); QString fullPathName = currentPath; if (haveOneSelection()) { //hierarchy objects have generally as name: 'filename.ext (fullpath)' //so we must only take the first part! (otherwise this type of name //with a path inside disturbs QFileDialog a lot ;)) QString defaultFileName(m_selectedEntities.front()->getName()); if (m_selectedEntities.front()->isA(CC_TYPES::HIERARCHY_OBJECT)) { QStringList parts = defaultFileName.split(' ', QString::SkipEmptyParts); if (!parts.empty()) { defaultFileName = parts[0]; } } //we remove the extension defaultFileName = QFileInfo(defaultFileName).completeBaseName(); if (!IsValidFileName(defaultFileName)) { ccLog::Warning(tr("[I/O] First entity's name would make an invalid filename! Can't use it...")); defaultFileName = "project"; } fullPathName += QString("/") + defaultFileName; } //ask the user for the output filename QString selectedFilename = QFileDialog::getSaveFileName(this, tr("Save file"), fullPathName, fileFilters.join(s_fileFilterSeparator), &selectedFilter, CCFileDialogOptions()); if (selectedFilename.isEmpty()) { //process cancelled by the user return; } //ignored items if (hasOther) { ccConsole::Warning(tr("[I/O] The following selected entities won't be saved:")); for (unsigned i = 0; i < other.getChildrenNumber(); ++i) { ccConsole::Warning(QString("\t- %1s").arg(other.getChild(i)->getName())); } } CC_FILE_ERROR result = CC_FERR_NO_ERROR; FileIOFilter::SaveParameters parameters; { parameters.alwaysDisplaySaveDialog = true; parameters.parentWidget = this; } //specific case: BIN format if (selectedFilter == BinFilter::GetFileFilter()) { if ( haveOneSelection() ) { result = FileIOFilter::SaveToFile(m_selectedEntities.front(), selectedFilename, parameters, selectedFilter); } else { //we'll regroup all selected entities in a temporary group ccHObject tempContainer; ConvertToGroup(m_selectedEntities, tempContainer, ccHObject::DP_NONE); if (tempContainer.getChildrenNumber()) { result = FileIOFilter::SaveToFile(&tempContainer, selectedFilename, parameters, selectedFilter); } else { ccLog::Warning(tr("[I/O] None of the selected entities can be saved this way...")); result = CC_FERR_NO_SAVE; } } } else if (entitiesToSave.getChildrenNumber() != 0) { //ignored items //if (hasSerializable) //{ // if (!hasOther) // ccConsole::Warning(tr("[I/O] The following selected entites won't be saved:")); //display this warning only if not already done // for (unsigned i = 0; i < otherSerializable.getChildrenNumber(); ++i) // ccConsole::Warning(tr("\t- %1").arg(otherSerializable.getChild(i)->getName())); //} result = FileIOFilter::SaveToFile( entitiesToSave.getChildrenNumber() > 1 ? &entitiesToSave : entitiesToSave.getChild(0), selectedFilename, parameters, selectedFilter); if (result == CC_FERR_NO_ERROR && m_ccRoot) { m_ccRoot->unselectAllEntities(); } } if (result == CC_FERR_NO_ERROR && selectedFilter == BinFilter::GetFileFilter()) { //only for BIN files: display the compatible CC version short fileVersion = BinFilter::GetLastSavedFileVersion(); if (0 != fileVersion) { QString minCCVersion = ccApplication::GetMinCCVersionForFileVersion(fileVersion); ccLog::Print(QString("This file can be loaded by CloudCompare version %1 and later").arg(minCCVersion)); } } //update default filters if (hasCloud) settings.setValue(ccPS::SelectedOutputFilterCloud(),selectedFilter); if (hasMesh) settings.setValue(ccPS::SelectedOutputFilterMesh(), selectedFilter); if (hasImages) settings.setValue(ccPS::SelectedOutputFilterImage(),selectedFilter); if (hasPolylines) settings.setValue(ccPS::SelectedOutputFilterPoly(), selectedFilter); //we update current file path currentPath = QFileInfo(selectedFilename).absolutePath(); settings.setValue(ccPS::CurrentPath(),currentPath); settings.endGroup(); } void MainWindow::doActionSaveProject() { if (!m_ccRoot || !m_ccRoot->getRootEntity()) { assert(false); return; } ccHObject* rootEntity = m_ccRoot->getRootEntity(); if (rootEntity->getChildrenNumber() == 0) { return; } //default output path (+ filename) QSettings settings; settings.beginGroup(ccPS::SaveFile()); QString currentPath = settings.value(ccPS::CurrentPath(), ccFileUtils::defaultDocPath()).toString(); ccLog::PrintDebug(currentPath); QString fullPathName = currentPath; static QString s_previousProjectName{ "project" }; QString defaultFileName = s_previousProjectName; if (rootEntity->getChildrenNumber() == 1) { // If there's only on top entity, we can try to use its name as the project name. ccHObject* topEntity = rootEntity->getChild(0); defaultFileName = topEntity->getName(); if (topEntity->isA(CC_TYPES::HIERARCHY_OBJECT)) { // Hierarchy objects have generally as name: 'filename.ext (fullpath)' // so we must only take the first part! (otherwise this type of name // with a path inside disturbs the QFileDialog a lot ;)) QStringList parts = defaultFileName.split(' ', QString::SkipEmptyParts); if (!parts.empty()) { defaultFileName = parts[0]; } } //we remove the extension defaultFileName = QFileInfo(defaultFileName).completeBaseName(); if (!IsValidFileName(defaultFileName)) { ccLog::Warning(tr("[I/O] Top entity's name would make an invalid filename! Can't use it...")); defaultFileName = "project"; } } fullPathName += QString("/") + defaultFileName; QString binFilter = BinFilter::GetFileFilter(); //ask the user for the output filename QString selectedFilename = QFileDialog::getSaveFileName(this, tr("Save file"), fullPathName, binFilter, &binFilter, CCFileDialogOptions()); if (selectedFilename.isEmpty()) { //process cancelled by the user return; } FileIOFilter::SaveParameters parameters; { parameters.alwaysDisplaySaveDialog = true; parameters.parentWidget = this; } CC_FILE_ERROR result = FileIOFilter::SaveToFile(rootEntity->getChildrenNumber() == 1 ? rootEntity->getChild(0) : rootEntity, selectedFilename, parameters, binFilter); if (result == CC_FERR_NO_ERROR) { //only for BIN files: display the compatible CC version short fileVersion = BinFilter::GetLastSavedFileVersion(); if (0 != fileVersion) { QString minCCVersion = ccApplication::GetMinCCVersionForFileVersion(fileVersion); ccLog::Print(QString("This file can be loaded by CloudCompare version %1 and later").arg(minCCVersion)); } } //we update the current 'save' path QFileInfo fi(selectedFilename); s_previousProjectName = fi.fileName(); currentPath = fi.absolutePath(); settings.setValue(ccPS::CurrentPath(), currentPath); settings.endGroup(); } void MainWindow::on3DViewActivated(QMdiSubWindow* mdiWin) { ccGLWindowInterface* win = mdiWin ? ccGLWindowInterface::FromWidget(mdiWin->widget()) : nullptr; if (win) { updateViewModePopUpMenu(win); updatePivotVisibilityPopUpMenu(win); m_UI->actionLockRotationAxis->blockSignals(true); m_UI->actionLockRotationAxis->setChecked(win->isRotationAxisLocked()); m_UI->actionLockRotationAxis->blockSignals(false); m_UI->actionEnableStereo->blockSignals(true); m_UI->actionEnableStereo->setChecked(win->stereoModeIsEnabled()); m_UI->actionEnableStereo->blockSignals(false); m_UI->actionExclusiveFullScreen->blockSignals(true); m_UI->actionExclusiveFullScreen->setChecked(win->exclusiveFullScreen()); m_UI->actionExclusiveFullScreen->blockSignals(false); m_UI->actionShowCursor3DCoordinates->blockSignals(true); m_UI->actionShowCursor3DCoordinates->setChecked(win->cursorCoordinatesShown()); m_UI->actionShowCursor3DCoordinates->blockSignals(false); m_UI->actionAutoPickRotationCenter->blockSignals(true); m_UI->actionAutoPickRotationCenter->setChecked(win->autoPickPivotAtCenter()); m_UI->actionAutoPickRotationCenter->blockSignals(false); } m_UI->actionLockRotationAxis->setEnabled(win != nullptr); m_UI->actionEnableStereo->setEnabled(win != nullptr); m_UI->actionExclusiveFullScreen->setEnabled(win != nullptr); } void MainWindow::updateViewModePopUpMenu(ccGLWindowInterface* win) { if (!m_viewModePopupButton) return; //update the view mode pop-up 'top' icon if (win) { bool objectCentered = true; bool perspectiveEnabled = win->getPerspectiveState(objectCentered); QAction* currentModeAction = nullptr; if (!perspectiveEnabled) { currentModeAction = m_UI->actionSetOrthoView; } else if (objectCentered) { currentModeAction = m_UI->actionSetCenteredPerspectiveView; } else { currentModeAction = m_UI->actionSetViewerPerspectiveView; } assert(currentModeAction); m_viewModePopupButton->setIcon(currentModeAction->icon()); m_viewModePopupButton->setEnabled(true); } else { m_viewModePopupButton->setIcon(QIcon()); m_viewModePopupButton->setEnabled(false); } } void MainWindow::updatePivotVisibilityPopUpMenu(ccGLWindowInterface* win) { if (!m_pivotVisibilityPopupButton) return; //update the pivot visibility pop-up 'top' icon if (win) { QAction* visibilityAction = nullptr; switch(win->getPivotVisibility()) { case ccGLWindowInterface::PIVOT_HIDE: visibilityAction = m_UI->actionSetPivotOff; break; case ccGLWindowInterface::PIVOT_SHOW_ON_MOVE: visibilityAction = m_UI->actionSetPivotRotationOnly; break; case ccGLWindowInterface::PIVOT_ALWAYS_SHOW: visibilityAction = m_UI->actionSetPivotAlwaysOn; break; default: assert(false); } if (visibilityAction) m_pivotVisibilityPopupButton->setIcon(visibilityAction->icon()); //pivot is not available in viewer-based perspective! bool objectCentered = true; win->getPerspectiveState(objectCentered); m_pivotVisibilityPopupButton->setEnabled(objectCentered); } else { m_pivotVisibilityPopupButton->setIcon(QIcon()); m_pivotVisibilityPopupButton->setEnabled(false); } } void MainWindow::updateMenus() { ccGLWindowInterface* active3DView = getActiveGLWindow(); bool hasMdiChild = (active3DView != nullptr); int mdiChildCount = getGLWindowCount(); bool hasLoadedEntities = (m_ccRoot && m_ccRoot->getRootEntity() && m_ccRoot->getRootEntity()->getChildrenNumber() != 0); bool hasSelectedEntities = (m_ccRoot && m_ccRoot->countSelectedEntities() > 0); //General Menu m_UI->menuEdit->setEnabled(true/*hasSelectedEntities*/); m_UI->menuTools->setEnabled(true/*hasSelectedEntities*/); //3D Views Menu m_UI->actionClose3DView ->setEnabled(hasMdiChild); m_UI->actionCloseAll3DViews->setEnabled(mdiChildCount != 0); m_UI->actionTile3DViews ->setEnabled(mdiChildCount > 1); m_UI->actionCascade3DViews ->setEnabled(mdiChildCount > 1); m_UI->actionNext3DView ->setEnabled(mdiChildCount > 1); m_UI->actionPrevious3DView ->setEnabled(mdiChildCount > 1); //Shaders & Filters display Menu bool shadersEnabled = (active3DView ? active3DView->areShadersEnabled() : false); m_UI->actionLoadShader->setEnabled(shadersEnabled); m_UI->actionDeleteShader->setEnabled(shadersEnabled); //View Menu m_UI->toolBarView->setEnabled(hasMdiChild); //oher actions m_UI->actionSegment->setEnabled(hasMdiChild && hasSelectedEntities); m_UI->actionTranslateRotate->setEnabled(hasMdiChild && hasSelectedEntities); m_UI->actionPointPicking->setEnabled(hasMdiChild && hasLoadedEntities); m_UI->actionTestFrameRate->setEnabled(hasMdiChild); m_UI->actionRenderToFile->setEnabled(hasMdiChild); m_UI->actionToggleSunLight->setEnabled(hasMdiChild); m_UI->actionToggleCustomLight->setEnabled(hasMdiChild); m_UI->actionToggleCenteredPerspective->setEnabled(hasMdiChild); m_UI->actionToggleViewerBasedPerspective->setEnabled(hasMdiChild); //plugins m_pluginUIManager->updateMenus(); } void MainWindow::update3DViewsMenu() { m_UI->menu3DViews->clear(); m_UI->menu3DViews->addAction(m_UI->actionNew3DView); m_UI->menu3DViews->addSeparator(); m_UI->menu3DViews->addAction(m_UI->actionZoomIn); m_UI->menu3DViews->addAction(m_UI->actionZoomOut); m_UI->menu3DViews->addSeparator(); m_UI->menu3DViews->addAction(m_UI->actionClose3DView); m_UI->menu3DViews->addAction(m_UI->actionCloseAll3DViews); m_UI->menu3DViews->addSeparator(); m_UI->menu3DViews->addAction(m_UI->actionTile3DViews); m_UI->menu3DViews->addAction(m_UI->actionCascade3DViews); m_UI->menu3DViews->addSeparator(); m_UI->menu3DViews->addAction(m_UI->actionNext3DView); m_UI->menu3DViews->addAction(m_UI->actionPrevious3DView); QList windows = m_mdiArea->subWindowList(); if (!windows.isEmpty()) { //Dynamic Separator QAction* separator = new QAction(this); separator->setSeparator(true); m_UI->menu3DViews->addAction(separator); int i = 0; for ( QMdiSubWindow* window : windows ) { ccGLWindowInterface *child = ccGLWindowInterface::FromWidget(window->widget()); QString text = QString("&%1 %2").arg(++i).arg(child->getWindowTitle()); QAction *action = m_UI->menu3DViews->addAction(text); action->setCheckable(true); action->setChecked(child == getActiveGLWindow()); connect(action, &QAction::triggered, this, [=] () { setActiveSubWindow( window ); } ); } } } void MainWindow::setActiveSubWindow(QWidget *window) { if (!window || !m_mdiArea) return; m_mdiArea->setActiveSubWindow(qobject_cast(window)); } void MainWindow::redrawAll(bool only2D/*=false*/) { for (QMdiSubWindow* window : m_mdiArea->subWindowList()) { ccGLWindowInterface::FromWidget(window->widget())->redraw(only2D); } } void MainWindow::refreshAll(bool only2D/*=false*/) { for (QMdiSubWindow* window : m_mdiArea->subWindowList()) { ccGLWindowInterface::FromWidget(window->widget())->refresh(only2D); } } void MainWindow::updateUI() { updateUIWithSelection(); updateMenus(); updatePropertiesView(); } void MainWindow::updatePropertiesView() { if (m_ccRoot) { m_ccRoot->updatePropertiesView(); } } void MainWindow::updateUIWithSelection() { dbTreeSelectionInfo selInfo; m_selectedEntities.clear(); if (m_ccRoot) { m_ccRoot->getSelectedEntities(m_selectedEntities, CC_TYPES::OBJECT, &selInfo); } enableUIItems(selInfo); } void MainWindow::enableAll() { for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { window->setEnabled( true ); } } void MainWindow::disableAll() { for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { window->setEnabled( false ); } } void MainWindow::disableAllBut(ccGLWindowInterface* win) { //we disable all other windows for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { if (ccGLWindowInterface::FromWidget(window->widget()) != win) { window->setEnabled(false); } } } void MainWindow::enableUIItems(dbTreeSelectionInfo& selInfo) { bool dbIsEmpty = (!m_ccRoot || !m_ccRoot->getRootEntity() || m_ccRoot->getRootEntity()->getChildrenNumber() == 0); bool atLeastOneEntity = (selInfo.selCount > 0); bool atLeastOneCloud = (selInfo.cloudCount > 0); bool atLeastOneMesh = (selInfo.meshCount > 0); bool atLeastOnePrimitive = (selInfo.primitiveCount > 0); //bool atLeastOneOctree = (selInfo.octreeCount > 0); bool atLeastOneNormal = (selInfo.normalsCount > 0); bool atLeastOneColor = (selInfo.colorCount > 0); bool atLeastOneSF = (selInfo.sfCount > 0); bool atLeastOneGrid = (selInfo.gridCound > 0); //bool atLeastOneSensor = (selInfo.sensorCount > 0); bool atLeastOneGBLSensor = (selInfo.gblSensorCount > 0); bool atLeastOneCameraSensor = (selInfo.cameraSensorCount > 0); bool atLeastOnePolyline = (selInfo.polylineCount > 0); bool activeWindow = (getActiveGLWindow() != nullptr); //menuEdit->setEnabled(atLeastOneEntity); //menuTools->setEnabled(atLeastOneEntity); m_UI->actionTracePolyline->setEnabled(!dbIsEmpty); m_UI->actionZoomAndCenter->setEnabled(atLeastOneEntity && activeWindow); m_UI->actionSave->setEnabled(atLeastOneEntity); m_UI->actionSaveProject->setEnabled(!dbIsEmpty); m_UI->actionClone->setEnabled(atLeastOneEntity); m_UI->actionDelete->setEnabled(atLeastOneEntity); m_UI->actionExportCoordToSF->setEnabled(atLeastOneEntity); m_UI->actionExportNormalToSF->setEnabled(atLeastOneNormal); m_UI->actionSegment->setEnabled(atLeastOneEntity && activeWindow); m_UI->actionTranslateRotate->setEnabled(atLeastOneEntity && activeWindow); m_UI->actionShowDepthBuffer->setEnabled(atLeastOneGBLSensor); m_UI->actionExportDepthBuffer->setEnabled(atLeastOneGBLSensor); m_UI->actionComputePointsVisibility->setEnabled(atLeastOneGBLSensor); m_UI->actionResampleWithOctree->setEnabled(atLeastOneCloud); m_UI->actionApplyScale->setEnabled(atLeastOneCloud || atLeastOneMesh || atLeastOnePolyline); m_UI->actionApplyTransformation->setEnabled(atLeastOneEntity); m_UI->actionComputeOctree->setEnabled(atLeastOneCloud || atLeastOneMesh); m_UI->actionComputeNormals->setEnabled(atLeastOneCloud || atLeastOneMesh); m_UI->actionChangeColorLevels->setEnabled(atLeastOneCloud || atLeastOneMesh); m_UI->actionEditGlobalShiftAndScale->setEnabled(atLeastOneCloud || atLeastOneMesh || atLeastOnePolyline); m_UI->actionCrop->setEnabled(atLeastOneCloud || atLeastOneMesh); m_UI->actionSetUniqueColor->setEnabled(atLeastOneEntity/*atLeastOneCloud || atLeastOneMesh*/); //DGM: we can set color to a group now! m_UI->actionSetColorGradient->setEnabled(atLeastOneCloud || atLeastOneMesh); m_UI->actionColorize->setEnabled(atLeastOneEntity/*atLeastOneCloud || atLeastOneMesh*/); //DGM: we can set color to a group now! m_UI->actionDeleteScanGrid->setEnabled(atLeastOneGrid); m_UI->actionScalarFieldFromColor->setEnabled(atLeastOneEntity && atLeastOneColor); m_UI->actionComputeMeshAA->setEnabled(atLeastOneCloud); m_UI->actionComputeMeshLS->setEnabled(atLeastOneCloud); m_UI->actionMeshScanGrids->setEnabled(atLeastOneGrid); //actionComputeQuadric3D->setEnabled(atLeastOneCloud); m_UI->actionComputeBestFitBB->setEnabled(atLeastOneEntity); m_UI->actionComputeGeometricFeature->setEnabled(atLeastOneCloud); m_UI->actionRemoveDuplicatePoints->setEnabled(atLeastOneCloud); m_UI->actionFitPlane->setEnabled(atLeastOneEntity); m_UI->actionFitPlaneProxy->setEnabled(atLeastOneEntity); m_UI->actionFitSphere->setEnabled(atLeastOneCloud); m_UI->actionFitCircle->setEnabled(atLeastOneCloud); m_UI->actionLevel->setEnabled(atLeastOneEntity); m_UI->actionFitFacet->setEnabled(atLeastOneEntity); m_UI->actionFitQuadric->setEnabled(atLeastOneCloud); m_UI->actionSubsample->setEnabled(atLeastOneCloud); m_UI->actionSNETest->setEnabled(atLeastOneCloud); m_UI->actionExportCloudInfo->setEnabled(atLeastOneEntity); m_UI->actionExportPlaneInfo->setEnabled(atLeastOneEntity); m_UI->actionFilterByValue->setEnabled(atLeastOneSF); m_UI->actionConvertToRGB->setEnabled(atLeastOneSF); m_UI->actionConvertToRandomRGB->setEnabled(atLeastOneSF); m_UI->actionRenameSF->setEnabled(atLeastOneSF); m_UI->actionAddIdField->setEnabled(atLeastOneCloud); m_UI->actionSplitCloudUsingSF->setEnabled(atLeastOneSF); m_UI->actionComputeStatParams->setEnabled(atLeastOneSF); m_UI->actionComputeStatParams2->setEnabled(atLeastOneSF); m_UI->actionShowHistogram->setEnabled(atLeastOneSF); m_UI->actionGaussianFilter->setEnabled(atLeastOneSF); m_UI->actionBilateralFilter->setEnabled(atLeastOneSF); m_UI->actionDeleteScalarField->setEnabled(atLeastOneSF); m_UI->actionDeleteAllSF->setEnabled(atLeastOneSF); m_UI->actionMultiplySF->setEnabled(/*TODO: atLeastOneSF*/false); m_UI->actionSFGradient->setEnabled(atLeastOneSF); m_UI->actionSetSFAsCoord->setEnabled(atLeastOneSF && atLeastOneCloud); m_UI->actionInterpolateSFs->setEnabled(atLeastOneCloud || atLeastOneMesh); m_UI->actionSamplePointsOnMesh->setEnabled(atLeastOneMesh); m_UI->actionMeasureMeshSurface->setEnabled(atLeastOneMesh); m_UI->actionMeasureMeshVolume->setEnabled(atLeastOneMesh); m_UI->actionFlagMeshVertices->setEnabled(atLeastOneMesh); m_UI->actionSmoothMeshLaplacian->setEnabled(atLeastOneMesh); m_UI->actionConvertTextureToColor->setEnabled(atLeastOneMesh); m_UI->actionSubdivideMesh->setEnabled(atLeastOneMesh); m_UI->actionFlipMeshTriangles->setEnabled(atLeastOneMesh); m_UI->actionDistanceToBestFitQuadric3D->setEnabled(atLeastOneCloud); m_UI->actionDistanceMap->setEnabled(atLeastOneMesh || atLeastOneCloud); m_UI->menuMeshScalarField->setEnabled(atLeastOneSF && atLeastOneMesh); //actionSmoothMeshSF->setEnabled(atLeastOneSF && atLeastOneMesh); //actionEnhanceMeshSF->setEnabled(atLeastOneSF && atLeastOneMesh); m_UI->actionOrientNormalsMST->setEnabled(atLeastOneCloud && atLeastOneNormal); m_UI->actionOrientNormalsFM->setEnabled(atLeastOneCloud && atLeastOneNormal); m_UI->actionShiftPointsAlongNormals->setEnabled(atLeastOneCloud && atLeastOneNormal); m_UI->actionClearNormals->setEnabled(atLeastOneNormal); m_UI->actionInvertNormals->setEnabled(atLeastOneNormal); m_UI->actionConvertNormalToHSV->setEnabled(atLeastOneNormal); m_UI->actionConvertNormalToDipDir->setEnabled(atLeastOneNormal); m_UI->actionClearColor->setEnabled(atLeastOneColor); m_UI->actionRGBToGreyScale->setEnabled(atLeastOneColor); m_UI->actionEnhanceRGBWithIntensities->setEnabled(atLeastOneColor); m_UI->actionRGBGaussianFilter->setEnabled(atLeastOneColor); m_UI->actionRGBBilateralFilter->setEnabled(atLeastOneColor); m_UI->actionRGBMeanFilter->setEnabled(atLeastOneColor); m_UI->actionRGBMedianFilter->setEnabled(atLeastOneColor); m_UI->actionColorFromScalarField->setEnabled(atLeastOneSF); // == 1 bool exactlyOneEntity = (selInfo.selCount == 1); bool exactlyOneGroup = (selInfo.groupCount == 1); bool exactlyOneCloud = (selInfo.cloudCount == 1); bool exactlyOneMesh = (selInfo.meshCount == 1); bool exactlyOneSF = (selInfo.sfCount == 1); bool exactlyOneSensor = (selInfo.sensorCount == 1); bool exactlyOneCameraSensor = (selInfo.cameraSensorCount == 1); m_UI->actionConvertPolylinesToMesh->setEnabled(atLeastOnePolyline || exactlyOneGroup); m_UI->actionSamplePointsOnPolyline->setEnabled(atLeastOnePolyline); m_UI->actionSmoothPolyline->setEnabled(atLeastOnePolyline); m_UI->actionMeshTwoPolylines->setEnabled(selInfo.selCount == 2 && selInfo.polylineCount == 2); m_UI->actionCreateSurfaceBetweenTwoPolylines->setEnabled(m_UI->actionMeshTwoPolylines->isEnabled()); //clone of actionMeshTwoPolylines m_UI->actionModifySensor->setEnabled(exactlyOneSensor); m_UI->actionComputeDistancesFromSensor->setEnabled(atLeastOneCameraSensor || atLeastOneGBLSensor); m_UI->actionComputeScatteringAngles->setEnabled(exactlyOneSensor); m_UI->actionViewFromSensor->setEnabled(exactlyOneSensor); m_UI->actionCreateGBLSensor->setEnabled(atLeastOneCloud); m_UI->actionCreateCameraSensor->setEnabled(selInfo.selCount <= 1); //free now m_UI->actionProjectUncertainty->setEnabled(exactlyOneCameraSensor); m_UI->actionCheckPointsInsideFrustum->setEnabled(exactlyOneCameraSensor); m_UI->actionLabelConnectedComponents->setEnabled(atLeastOneCloud); m_UI->actionSORFilter->setEnabled(atLeastOneCloud); m_UI->actionNoiseFilter->setEnabled(atLeastOneCloud); m_UI->actionUnroll->setEnabled(exactlyOneEntity); m_UI->actionStatisticalTest->setEnabled(exactlyOneEntity && exactlyOneSF); m_UI->actionAddConstantSF->setEnabled(exactlyOneCloud || exactlyOneMesh); m_UI->actionAddClassificationSF->setEnabled(exactlyOneCloud || exactlyOneMesh); m_UI->actionEditGlobalScale->setEnabled(exactlyOneCloud || exactlyOneMesh); m_UI->actionComputeKdTree->setEnabled(exactlyOneCloud || exactlyOneMesh); m_UI->actionSetSFsAsNormal->setEnabled(exactlyOneCloud || exactlyOneMesh); m_UI->actionShowWaveDialog->setEnabled(exactlyOneCloud); m_UI->actionCompressFWFData->setEnabled(atLeastOneCloud); m_UI->actionKMeans->setEnabled(/*TODO: exactlyOneEntity && exactlyOneSF*/false); m_UI->actionFrontPropagation->setEnabled(/*TODO: exactlyOneEntity && exactlyOneSF*/false); //actionCreatePlane->setEnabled(true); m_UI->actionEditPlane->setEnabled(selInfo.planeCount == 1); m_UI->actionFlipPlane->setEnabled(selInfo.planeCount != 0); m_UI->actionComparePlanes->setEnabled(selInfo.planeCount == 2); m_UI->actionFindBiggestInnerRectangle->setEnabled(exactlyOneCloud); m_UI->menuActiveScalarField->setEnabled((exactlyOneCloud || exactlyOneMesh) && selInfo.sfCount > 0); m_UI->actionCrossSection->setEnabled(atLeastOneCloud || atLeastOneMesh || (selInfo.groupCount != 0)); m_UI->actionExtractSections->setEnabled(atLeastOneCloud); m_UI->actionRasterize->setEnabled(exactlyOneCloud); m_UI->actionCompute2HalfDimVolume->setEnabled(selInfo.cloudCount == selInfo.selCount && selInfo.cloudCount >= 1 && selInfo.cloudCount <= 2); //one or two clouds! m_UI->actionPointListPicking->setEnabled(exactlyOneCloud || exactlyOneMesh); // == 2 bool exactlyTwoEntities = (selInfo.selCount == 2); bool exactlyTwoClouds = (selInfo.cloudCount == 2); //bool exactlyTwoSF = (selInfo.sfCount == 2); m_UI->actionRegister->setEnabled(exactlyTwoEntities); m_UI->actionInterpolateColors->setEnabled(exactlyTwoEntities && atLeastOneColor); m_UI->actionPointPairsAlign->setEnabled(atLeastOneEntity); m_UI->actionBBCenterToOrigin->setEnabled(atLeastOneEntity); m_UI->actionBBMinCornerToOrigin->setEnabled(atLeastOneEntity); m_UI->actionBBMaxCornerToOrigin->setEnabled(atLeastOneEntity); m_UI->actionAlign->setEnabled(exactlyTwoEntities); //Aurelien BEY le 13/11/2008 m_UI->actionCloudCloudDist->setEnabled(exactlyTwoClouds); m_UI->actionCloudMeshDist->setEnabled(exactlyTwoEntities && atLeastOneMesh); m_UI->actionCloudPrimitiveDist->setEnabled(atLeastOneCloud && (atLeastOnePrimitive || atLeastOnePolyline)); m_UI->actionCPS->setEnabled(exactlyTwoClouds); m_UI->actionScalarFieldArithmetic->setEnabled(exactlyOneEntity && atLeastOneSF); //>1 bool atLeastTwoEntities = (selInfo.selCount > 1); m_UI->actionMerge->setEnabled(atLeastTwoEntities); m_UI->actionMatchBBCenters->setEnabled(atLeastTwoEntities); m_UI->actionMatchScales->setEnabled(atLeastTwoEntities); //standard plugins m_pluginUIManager->handleSelectionChanged(); } void MainWindow::echoMouseWheelRotate(float wheelDelta_deg) { if (!m_UI->actionEnableCameraLink->isChecked()) return; ccGLWindowInterface* sendingWindow = ccGLWindowInterface::FromEmitter(sender()); if (!sendingWindow) return; for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { ccGLWindowInterface* child = ccGLWindowInterface::FromWidget(window->widget()); if (child != sendingWindow) { child->signalEmitter()->blockSignals(true); child->onWheelEvent(wheelDelta_deg); child->signalEmitter()->blockSignals(false); child->redraw(); } } } void MainWindow::echoBaseViewMatRotation(const ccGLMatrixd& rotMat) { if (!m_UI->actionEnableCameraLink->isChecked()) return; ccGLWindowInterface* sendingWindow = ccGLWindowInterface::FromEmitter(sender()); if (!sendingWindow) return; for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { ccGLWindowInterface* child = ccGLWindowInterface::FromWidget(window->widget()); if (child != sendingWindow) { child->signalEmitter()->blockSignals(true); child->rotateBaseViewMat(rotMat); child->signalEmitter()->blockSignals(false); child->redraw(); } } } void MainWindow::echoCameraPosChanged(const CCVector3d& P) { if (!m_UI->actionEnableCameraLink->isChecked()) return; ccGLWindowInterface* sendingWindow = ccGLWindowInterface::FromEmitter(sender()); if (!sendingWindow) return; for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { ccGLWindowInterface* child = ccGLWindowInterface::FromWidget(window->widget()); if (child != sendingWindow) { child->signalEmitter()->blockSignals(true); child->setCameraPos(P); child->signalEmitter()->blockSignals(false); child->redraw(); } } } void MainWindow::echoPivotPointChanged(const CCVector3d& P) { if (!m_UI->actionEnableCameraLink->isChecked()) return; ccGLWindowInterface* sendingWindow = ccGLWindowInterface::FromEmitter(sender()); if (!sendingWindow) return; for ( QMdiSubWindow* window : m_mdiArea->subWindowList() ) { ccGLWindowInterface* child = ccGLWindowInterface::FromWidget(window->widget()); if (child != sendingWindow) { child->signalEmitter()->blockSignals(true); child->setPivotPoint(P); child->signalEmitter()->blockSignals(false); child->redraw(); } } } void MainWindow::dispToConsole(QString message, ConsoleMessageLevel level/*=STD_CONSOLE_MESSAGE*/) { switch(level) { case STD_CONSOLE_MESSAGE: ccConsole::Print(message); break; case WRN_CONSOLE_MESSAGE: ccConsole::Warning(message); break; case ERR_CONSOLE_MESSAGE: ccConsole::Error(message); break; } } void MainWindow::doActionLoadShader() //TODO { ccConsole::Error(tr("Not yet implemented! Sorry ...")); } void MainWindow::doActionKMeans()//TODO { ccConsole::Error(tr("Not yet implemented! Sorry ...")); } void MainWindow::doActionFrontPropagation() //TODO { ccConsole::Error(tr("Not yet implemented! Sorry ...")); } /************** STATIC METHODS ******************/ MainWindow* MainWindow::TheInstance() { if (!s_instance) s_instance = new MainWindow(); return s_instance; } void MainWindow::DestroyInstance() { delete s_instance; s_instance=nullptr; } void MainWindow::GetGLWindows(std::vector& glWindows) { const QList windows = TheInstance()->m_mdiArea->subWindowList(); if ( windows.empty() ) return; glWindows.clear(); glWindows.reserve( windows.size() ); for ( QMdiSubWindow* window : windows ) { glWindows.push_back(ccGLWindowInterface::FromWidget(window->widget())); } } ccGLWindowInterface* MainWindow::GetActiveGLWindow() { return TheInstance()->getActiveGLWindow(); } ccGLWindowInterface* MainWindow::GetGLWindow(const QString& title) { const QList windows = TheInstance()->m_mdiArea->subWindowList(); if ( windows.empty() ) return nullptr; for ( QMdiSubWindow* window : windows ) { ccGLWindowInterface* win = ccGLWindowInterface::FromWidget(window->widget()); if (win->getWindowTitle() == title) return win; } return nullptr; } void MainWindow::RefreshAllGLWindow(bool only2D/*=false*/) { TheInstance()->refreshAll(only2D); } void MainWindow::UpdateUI() { TheInstance()->updateUI(); } ccDBRoot* MainWindow::db() { return m_ccRoot; } void MainWindow::addEditPlaneAction( QMenu &menu ) const { menu.addAction( m_UI->actionEditPlane ); } ccHObject* MainWindow::dbRootObject() { return (m_ccRoot ? m_ccRoot->getRootEntity() : nullptr); } ccUniqueIDGenerator::Shared MainWindow::getUniqueIDGenerator() { return ccObject::GetUniqueIDGenerator(); } void MainWindow::createGLWindow(ccGLWindowInterface*& window, QWidget*& widget) const { bool stereoMode = ccGLWindowInterface::TestStereoSupport(); ccGLWindowInterface::Create(window, widget, stereoMode); assert(window && widget); } void MainWindow::destroyGLWindow(ccGLWindowInterface* view3D) const { if (view3D) { view3D->asQObject()->setParent(nullptr); delete view3D; } } ccMainAppInterface::ccHObjectContext MainWindow::removeObjectTemporarilyFromDBTree(ccHObject* obj) { ccHObjectContext context; assert(obj); if (!m_ccRoot || !obj) return context; //mandatory (to call putObjectBackIntoDBTree) context.parent = obj->getParent(); //remove the object's dependency to its father (in case it undergoes 'severe' modifications) if (context.parent) { context.parentFlags = context.parent->getDependencyFlagsWith(obj); context.childFlags = obj->getDependencyFlagsWith(context.parent); context.parent->removeDependencyWith(obj); obj->removeDependencyWith(context.parent); } m_ccRoot->removeElement(obj); return context; } void MainWindow::putObjectBackIntoDBTree(ccHObject* obj, const ccHObjectContext& context) { assert(obj); if (!obj || !m_ccRoot) return; if (context.parent) { context.parent->addChild(obj,context.parentFlags); obj->addDependency(context.parent,context.childFlags); } //DGM: we must call 'notifyGeometryUpdate' as any call to this method //while the object was temporarily 'cut' from the DB tree were //ineffective! obj->notifyGeometryUpdate(); m_ccRoot->addElement(obj,false); } void MainWindow::doActionGlobalShiftSeetings() { QDialog dialog(this); Ui_GlobalShiftSettingsDialog ui; ui.setupUi(&dialog); ui.maxAbsCoordSpinBox->setValue(static_cast(log10(ccGlobalShiftManager::MaxCoordinateAbsValue()))); ui.maxAbsDiagSpinBox->setValue(static_cast(log10(ccGlobalShiftManager::MaxBoundgBoxDiagonal()))); if (!dialog.exec()) { return; } double maxAbsCoord = pow(10.0, static_cast(ui.maxAbsCoordSpinBox->value())); double maxAbsDiag = pow(10.0, static_cast(ui.maxAbsDiagSpinBox->value())); ccGlobalShiftManager::SetMaxCoordinateAbsValue(maxAbsCoord); ccGlobalShiftManager::SetMaxBoundgBoxDiagonal(maxAbsDiag); ccLog::Print(tr("[Global Shift] Max abs. coord = %1 / max abs. diag = %2") .arg(ccGlobalShiftManager::MaxCoordinateAbsValue(), 0, 'e', 0) .arg(ccGlobalShiftManager::MaxBoundgBoxDiagonal(), 0, 'e', 0)); //save to persistent settings { QSettings settings; settings.beginGroup(ccPS::GlobalShift()); settings.setValue(ccPS::MaxAbsCoord(), maxAbsCoord); settings.setValue(ccPS::MaxAbsDiag(), maxAbsDiag); settings.endGroup(); } } void MainWindow::doActionCompressFWFData() { for ( ccHObject *entity : getSelectedEntities() ) { if (!entity || !entity->isKindOf(CC_TYPES::POINT_CLOUD)) { continue; } ccPointCloud* cloud = static_cast(entity); cloud->compressFWFData(); } } void MainWindow::doActionShowWaveDialog() { if (!haveSelection()) return; ccHObject* entity = haveOneSelection() ? m_selectedEntities.front() : nullptr; if (!entity || !entity->isKindOf(CC_TYPES::POINT_CLOUD)) { ccConsole::Error(tr("Select one point cloud!")); return; } ccPointCloud* cloud = static_cast(entity); if (!cloud->hasFWF()) { ccConsole::Error(tr("Cloud has no associated waveform information")); return; } ccWaveDialog* wDlg = new ccWaveDialog(cloud, m_pickingHub, this); wDlg->setAttribute(Qt::WA_DeleteOnClose); wDlg->setModal(false); wDlg->show(); } void MainWindow::doActionCreatePlane() { ccPlaneEditDlg* peDlg = new ccPlaneEditDlg(m_pickingHub, this); peDlg->show(); } void MainWindow::doActionEditPlane() { if (!haveSelection()) { assert(false); return; } ccPlane* plane = ccHObjectCaster::ToPlane(m_selectedEntities.front()); if (!plane) { assert(false); return; } ccPlaneEditDlg* peDlg = new ccPlaneEditDlg(m_pickingHub, this); peDlg->initWithPlane(plane); peDlg->show(); } void MainWindow::doActionFlipPlane() { if (!haveSelection()) { assert(false); return; } for (ccHObject* entity : m_selectedEntities) { ccPlane* plane = ccHObjectCaster::ToPlane(entity); if (plane) { plane->flip(); plane->prepareDisplayForRefresh(); } } refreshAll(); updatePropertiesView(); } void MainWindow::doActionComparePlanes() { if (m_selectedEntities.size() != 2) { ccConsole::Error(tr("Select 2 planes!")); return; } if (!m_selectedEntities.front()->isKindOf(CC_TYPES::PLANE) || !m_selectedEntities.back()->isKindOf(CC_TYPES::PLANE)) { ccConsole::Error(tr("Select 2 planes!")); return; } ccPlane* p1 = ccHObjectCaster::ToPlane(m_selectedEntities.front()); ccPlane* p2 = ccHObjectCaster::ToPlane(m_selectedEntities.back()); QStringList info; info << tr("Plane 1: %1").arg(p1->getName()); ccLog::Print(tr("[Compare] ") + info.last()); info << tr("Plane 2: %1").arg(p2->getName()); ccLog::Print(tr("[Compare] ") + info.last()); CCVector3 N1; CCVector3 N2; PointCoordinateType d1; PointCoordinateType d2; p1->getEquation(N1, d1); p2->getEquation(N2, d2); double angle_rad = N1.angle_rad(N2); info << tr("Angle P1/P2: %1 deg.").arg( CCCoreLib::RadiansToDegrees( angle_rad ) ); ccLog::Print(tr("[Compare] ") + info.last()); PointCoordinateType planeEq1[4] = { N1.x, N1.y, N1.z, d1 }; PointCoordinateType planeEq2[4] = { N2.x, N2.y, N2.z, d2 }; CCVector3 C1 = p1->getCenter(); ScalarType distCenter1ToPlane2 = CCCoreLib::DistanceComputationTools::computePoint2PlaneDistance(&C1, planeEq2); info << tr("Distance Center(P1)/P2: %1").arg(distCenter1ToPlane2); ccLog::Print(tr("[Compare] ") + info.last()); CCVector3 C2 = p2->getCenter(); ScalarType distCenter2ToPlane1 = CCCoreLib::DistanceComputationTools::computePoint2PlaneDistance(&C2, planeEq1); info << tr("Distance Center(P2)/P1: %1").arg(distCenter2ToPlane1); ccLog::Print(tr("[Compare] ") + info.last()); //pop-up summary QMessageBox::information(this, tr("Plane comparison"), info.join("\n")); forceConsoleDisplay(); }