Merge branch 'FreeCAD:main' into flickering-cursor
This commit is contained in:
@@ -17,6 +17,8 @@ macro(InitializeFreeCADBuildOptions)
|
||||
option(FREECAD_PARALLEL_LINK_JOBS "Linkage jobs pool size to fit memory limitations.")
|
||||
option(BUILD_WITH_CONDA "Set ON if you build FreeCAD with conda" OFF)
|
||||
option(BUILD_DYNAMIC_LINK_PYTHON "If OFF extension-modules do not link against python-libraries" ON)
|
||||
option(BUILD_TRACY_FRAME_PROFILER "If ON then enables support for the Tracy frame profiler" OFF)
|
||||
|
||||
option(INSTALL_TO_SITEPACKAGES "If ON the freecad root namespace (python) is installed into python's site-packages" ON)
|
||||
option(INSTALL_PREFER_SYMLINKS "If ON then fc_copy_sources macro will create symlinks instead of copying files" OFF)
|
||||
option(OCCT_CMAKE_FALLBACK "disable usage of occt-config files" OFF)
|
||||
|
||||
11
src/3rdParty/CMakeLists.txt
vendored
11
src/3rdParty/CMakeLists.txt
vendored
@@ -8,8 +8,17 @@ add_subdirectory(libE57Format)
|
||||
|
||||
if (BUILD_ASSEMBLY AND NOT FREECAD_USE_EXTERNAL_ONDSELSOLVER)
|
||||
if( NOT EXISTS "${CMAKE_SOURCE_DIR}/src/3rdParty/OndselSolver/CMakeLists.txt" )
|
||||
message( SEND_ERROR "The OndselSolver git submodule is not available. Please run
|
||||
message(FATAL_ERROR "The OndselSolver git submodule is not available. Please run
|
||||
git submodule update --init" )
|
||||
endif()
|
||||
add_subdirectory(OndselSolver)
|
||||
endif()
|
||||
|
||||
if (BUILD_TRACY_FRAME_PROFILER)
|
||||
if( NOT EXISTS "${CMAKE_SOURCE_DIR}/src/3rdParty/tracy/CMakeLists.txt" )
|
||||
message(FATAL_ERROR "The Tracy git directory is not available. Please clone it manually." )
|
||||
endif()
|
||||
|
||||
set(TRACY_STATIC OFF)
|
||||
add_subdirectory(tracy)
|
||||
endif()
|
||||
|
||||
2
src/3rdParty/GSL
vendored
2
src/3rdParty/GSL
vendored
Submodule src/3rdParty/GSL updated: b39e7e4b09...2828399820
@@ -3,6 +3,10 @@ if(WIN32)
|
||||
add_definitions(-DBOOST_DYN_LINK)
|
||||
endif(WIN32)
|
||||
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
add_definitions(-DBUILD_TRACY_FRAME_PROFILER)
|
||||
endif()
|
||||
|
||||
if(FREECAD_RELEASE_SEH)
|
||||
add_definitions(-DHAVE_SEH)
|
||||
endif(FREECAD_RELEASE_SEH)
|
||||
@@ -71,6 +75,10 @@ set(FreeCADApp_LIBS
|
||||
fmt::fmt
|
||||
)
|
||||
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
list(APPEND FreeCADApp_LIBS TracyClient)
|
||||
endif()
|
||||
|
||||
include_directories(
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
${QtXml_INCLUDE_DIRS}
|
||||
|
||||
@@ -94,6 +94,7 @@ recompute path. Also, it enables more complicated dependencies beyond trees.
|
||||
#include <Base/TimeInfo.h>
|
||||
#include <Base/Reader.h>
|
||||
#include <Base/Writer.h>
|
||||
#include <Base/Profiler.h>
|
||||
#include <Base/Tools.h>
|
||||
#include <Base/Uuid.h>
|
||||
#include <Base/Sequencer.h>
|
||||
@@ -2963,6 +2964,8 @@ int Document::recompute(const std::vector<App::DocumentObject*>& objs,
|
||||
bool* hasError,
|
||||
int options)
|
||||
{
|
||||
ZoneScoped;
|
||||
|
||||
if (d->undoing || d->rollback) {
|
||||
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
||||
FC_WARN("Ignore document recompute on undo/redo");
|
||||
|
||||
@@ -5,6 +5,10 @@ if(WIN32)
|
||||
add_definitions(-DZIPIOS_UTF8)
|
||||
endif(WIN32)
|
||||
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
add_definitions(-DBUILD_TRACY_FRAME_PROFILER)
|
||||
endif()
|
||||
|
||||
include_directories(
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
@@ -65,6 +69,11 @@ endif(MSVC)
|
||||
include_directories(
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
list(APPEND FreeCADBase_LIBS TracyClient)
|
||||
endif()
|
||||
|
||||
list(APPEND FreeCADBase_LIBS ${QtCore_LIBRARIES})
|
||||
|
||||
list(APPEND FreeCADBase_LIBS fmt::fmt)
|
||||
|
||||
134
src/Base/Profiler.h
Normal file
134
src/Base/Profiler.h
Normal file
@@ -0,0 +1,134 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2025 Joao Matos <joao@tritao.eu> *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD 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 *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifdef TRACY_ENABLE
|
||||
#include <tracy/Tracy.hpp>
|
||||
#else
|
||||
#define TracyNoop
|
||||
|
||||
#define ZoneNamed(x, y)
|
||||
#define ZoneNamedN(x, y, z)
|
||||
#define ZoneNamedC(x, y, z)
|
||||
#define ZoneNamedNC(x, y, z, w)
|
||||
|
||||
#define ZoneTransient(x, y)
|
||||
#define ZoneTransientN(x, y, z)
|
||||
|
||||
#define ZoneScoped
|
||||
#define ZoneScopedN(x)
|
||||
#define ZoneScopedC(x)
|
||||
#define ZoneScopedNC(x, y)
|
||||
|
||||
#define ZoneText(x, y)
|
||||
#define ZoneTextV(x, y, z)
|
||||
#define ZoneTextF(x, ...)
|
||||
#define ZoneTextVF(x, y, ...)
|
||||
#define ZoneName(x, y)
|
||||
#define ZoneNameV(x, y, z)
|
||||
#define ZoneNameF(x, ...)
|
||||
#define ZoneNameVF(x, y, ...)
|
||||
#define ZoneColor(x)
|
||||
#define ZoneColorV(x, y)
|
||||
#define ZoneValue(x)
|
||||
#define ZoneValueV(x, y)
|
||||
#define ZoneIsActive false
|
||||
#define ZoneIsActiveV(x) false
|
||||
|
||||
#define FrameMark
|
||||
#define FrameMarkNamed(x)
|
||||
#define FrameMarkStart(x)
|
||||
#define FrameMarkEnd(x)
|
||||
|
||||
#define FrameImage(x, y, z, w, a)
|
||||
|
||||
#define TracyLockable(type, varname) type varname
|
||||
#define TracyLockableN(type, varname, desc) type varname
|
||||
#define TracySharedLockable(type, varname) type varname
|
||||
#define TracySharedLockableN(type, varname, desc) type varname
|
||||
#define LockableBase(type) type
|
||||
#define SharedLockableBase(type) type
|
||||
#define LockMark(x) (void)x
|
||||
#define LockableName(x, y, z)
|
||||
|
||||
#define TracyPlot(x, y)
|
||||
#define TracyPlotConfig(x, y, z, w, a)
|
||||
|
||||
#define TracyMessage(x, y)
|
||||
#define TracyMessageL(x)
|
||||
#define TracyMessageC(x, y, z)
|
||||
#define TracyMessageLC(x, y)
|
||||
#define TracyAppInfo(x, y)
|
||||
|
||||
#define TracyAlloc(x, y)
|
||||
#define TracyFree(x)
|
||||
#define TracyMemoryDiscard(x)
|
||||
#define TracySecureAlloc(x, y)
|
||||
#define TracySecureFree(x)
|
||||
#define TracySecureMemoryDiscard(x)
|
||||
|
||||
#define TracyAllocN(x, y, z)
|
||||
#define TracyFreeN(x, y)
|
||||
#define TracySecureAllocN(x, y, z)
|
||||
#define TracySecureFreeN(x, y)
|
||||
|
||||
#define ZoneNamedS(x, y, z)
|
||||
#define ZoneNamedNS(x, y, z, w)
|
||||
#define ZoneNamedCS(x, y, z, w)
|
||||
#define ZoneNamedNCS(x, y, z, w, a)
|
||||
|
||||
#define ZoneTransientS(x, y, z)
|
||||
#define ZoneTransientNS(x, y, z, w)
|
||||
|
||||
#define ZoneScopedS(x)
|
||||
#define ZoneScopedNS(x, y)
|
||||
#define ZoneScopedCS(x, y)
|
||||
#define ZoneScopedNCS(x, y, z)
|
||||
|
||||
#define TracyAllocS(x, y, z)
|
||||
#define TracyFreeS(x, y)
|
||||
#define TracyMemoryDiscardS(x, y)
|
||||
#define TracySecureAllocS(x, y, z)
|
||||
#define TracySecureFreeS(x, y)
|
||||
#define TracySecureMemoryDiscardS(x, y)
|
||||
|
||||
#define TracyAllocNS(x, y, z, w)
|
||||
#define TracyFreeNS(x, y, z)
|
||||
#define TracySecureAllocNS(x, y, z, w)
|
||||
#define TracySecureFreeNS(x, y, z)
|
||||
|
||||
#define TracyMessageS(x, y, z)
|
||||
#define TracyMessageLS(x, y)
|
||||
#define TracyMessageCS(x, y, z, w)
|
||||
#define TracyMessageLCS(x, y, z)
|
||||
|
||||
#define TracySourceCallbackRegister(x, y)
|
||||
#define TracyParameterRegister(x, y)
|
||||
#define TracyParameterSetup(x, y, z, w)
|
||||
#define TracyIsConnected false
|
||||
#define TracyIsStarted false
|
||||
#define TracySetProgramName(x)
|
||||
|
||||
#define TracyFiberEnter(x)
|
||||
#define TracyFiberEnterHint(x, y)
|
||||
#define TracyFiberLeave
|
||||
#endif
|
||||
@@ -136,6 +136,9 @@
|
||||
#include "WidgetFactory.h"
|
||||
#include "3Dconnexion/navlib/NavlibInterface.h"
|
||||
|
||||
#ifdef BUILD_TRACY_FRAME_PROFILER
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
|
||||
using namespace Gui;
|
||||
using namespace Gui::DockWnd;
|
||||
|
||||
@@ -44,6 +44,10 @@ if (BUILD_ADDONMGR)
|
||||
add_definitions(-DBUILD_ADDONMGR )
|
||||
endif(BUILD_ADDONMGR)
|
||||
|
||||
if (BUILD_TRACY_FRAME_PROFILER)
|
||||
add_definitions(-DBUILD_TRACY_FRAME_PROFILER)
|
||||
endif()
|
||||
|
||||
include_directories(
|
||||
${CMAKE_BINARY_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
@@ -99,6 +103,10 @@ else(MSVC)
|
||||
)
|
||||
endif(MSVC)
|
||||
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
list(APPEND FreeCADGui_LIBS TracyClient)
|
||||
endif()
|
||||
|
||||
if (TARGET Coin::Coin)
|
||||
list(APPEND FreeCADGui_LIBS Coin::Coin)
|
||||
else()
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
#include <Inventor/scxml/ScXML.h>
|
||||
#include <Inventor/scxml/SoScXMLStateMachine.h>
|
||||
|
||||
#include <Base/Profiler.h>
|
||||
|
||||
#include "QuarterWidget.h"
|
||||
#include "InteractionMode.h"
|
||||
#include "QuarterP.h"
|
||||
@@ -839,6 +841,8 @@ void QuarterWidget::resizeEvent(QResizeEvent* event)
|
||||
*/
|
||||
void QuarterWidget::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
ZoneScoped;
|
||||
|
||||
if (updateDevicePixelRatio()) {
|
||||
qreal dev_pix_ratio = devicePixelRatio();
|
||||
int width = static_cast<int>(dev_pix_ratio * this->width());
|
||||
@@ -986,6 +990,7 @@ QuarterWidget::redraw()
|
||||
void
|
||||
QuarterWidget::actualRedraw()
|
||||
{
|
||||
ZoneScoped;
|
||||
PRIVATE(this)->sorendermanager->render(PRIVATE(this)->clearwindow,
|
||||
PRIVATE(this)->clearzbuffer);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
|
||||
#include "SoQTQuarterAdaptor.h"
|
||||
|
||||
#ifdef BUILD_TRACY_FRAME_PROFILER
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
|
||||
// NOLINTBEGIN
|
||||
// clang-format off
|
||||
static unsigned char fps2dfont[][12] = {
|
||||
@@ -766,6 +770,10 @@ void SIM::Coin3D::Quarter::SoQTQuarterAdaptor::paintEvent(QPaintEvent* event)
|
||||
double start = SbTime::getTimeOfDay().getValue();
|
||||
QuarterWidget::paintEvent(event);
|
||||
this->framesPerSecond = addFrametime(start);
|
||||
|
||||
#ifdef BUILD_TRACY_FRAME_PROFILER
|
||||
FrameMark;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SIM::Coin3D::Quarter::SoQTQuarterAdaptor::resetFrameCounter()
|
||||
|
||||
@@ -2885,6 +2885,10 @@ QPushButton[objectName="buttonIFCPropertiesDelete"] {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
QGroupBox[objectName="matOpsGrpBox"] QPushButton {
|
||||
min-width: 235px;
|
||||
}
|
||||
|
||||
/* Below is a fix for indentation in properties, but this is a QT 6 bug only and so is disabled since Windows is as I write this still on QT 5. */
|
||||
/* QTreeView::item:selected:active#groupsTreeView {
|
||||
background-color: @ThemeAccentColor1;
|
||||
|
||||
@@ -2890,6 +2890,10 @@ PushButton[objectName="buttonIFCPropertiesDelete"] {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
QGroupBox[objectName="matOpsGrpBox"] QPushButton {
|
||||
min-width: 235px;
|
||||
}
|
||||
|
||||
/* Below is a fix for indentation in properties, but this is a QT 6 bug only and so is disabled since Windows is as I write this still on QT 5. */
|
||||
/* QTreeView::item:selected:active#groupsTreeView {
|
||||
background-color: @ThemeAccentColor1;
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
# include <Inventor/actions/SoHandleEventAction.h>
|
||||
# include <Inventor/actions/SoRayPickAction.h>
|
||||
# include <Inventor/annex/HardCopy/SoVectorizePSAction.h>
|
||||
# include <Inventor/annex/Profiler/SoProfiler.h>
|
||||
# include <Inventor/annex/Profiler/elements/SoProfilerElement.h>
|
||||
# include <Inventor/details/SoDetail.h>
|
||||
# include <Inventor/elements/SoLightModelElement.h>
|
||||
# include <Inventor/elements/SoOverrideElement.h>
|
||||
@@ -94,6 +96,7 @@
|
||||
#include <Base/Console.h>
|
||||
#include <Base/FileInfo.h>
|
||||
#include <Base/Sequencer.h>
|
||||
#include <Base/Profiler.h>
|
||||
#include <Base/Tools.h>
|
||||
#include <Base/UnitsApi.h>
|
||||
#include <Base/Tools2D.h>
|
||||
@@ -430,6 +433,11 @@ void View3DInventorViewer::init()
|
||||
// setting up the defaults for the spin rotation
|
||||
initialize();
|
||||
|
||||
#ifdef TRACY_ENABLE
|
||||
SoProfiler::init();
|
||||
SoProfiler::enable(TRUE);
|
||||
#endif
|
||||
|
||||
// NOLINTBEGIN
|
||||
auto cam = new SoOrthographicCamera;
|
||||
cam->position = SbVec3f(0, 0, 1);
|
||||
@@ -571,9 +579,14 @@ void View3DInventorViewer::init()
|
||||
// the fix and some details what happens behind the scene have a look at this
|
||||
// https://forum.freecad.org/viewtopic.php?f=10&t=7486&p=74777#p74736
|
||||
uint32_t id = this->getSoRenderManager()->getGLRenderAction()->getCacheContext();
|
||||
this->getSoRenderManager()->setGLRenderAction(new SoBoxSelectionRenderAction);
|
||||
auto boxSelectionAction = new SoBoxSelectionRenderAction;
|
||||
this->getSoRenderManager()->setGLRenderAction(boxSelectionAction);
|
||||
this->getSoRenderManager()->getGLRenderAction()->setCacheContext(id);
|
||||
|
||||
#ifdef TRACY_ENABLE
|
||||
boxSelectionAction->enableElement(SoProfilerElement::getClassTypeId(), SoProfilerElement::getClassStackIndex());
|
||||
#endif
|
||||
|
||||
// set the transparency and antialiasing settings
|
||||
getSoRenderManager()->getGLRenderAction()->setTransparencyType(SoGLRenderAction::SORTED_OBJECT_SORTED_TRIANGLE_BLEND);
|
||||
|
||||
@@ -2415,6 +2428,8 @@ void View3DInventorViewer::renderGLImage()
|
||||
// upon spin.
|
||||
void View3DInventorViewer::renderScene()
|
||||
{
|
||||
ZoneScoped;
|
||||
|
||||
// Must set up the OpenGL viewport manually, as upon resize
|
||||
// operations, Coin won't set it up until the SoGLRenderAction is
|
||||
// applied again. And since we need to do glClear() before applying
|
||||
@@ -2434,15 +2449,19 @@ void View3DInventorViewer::renderScene()
|
||||
glDepthRange(0.1,1.0);
|
||||
#endif
|
||||
|
||||
// Render our scenegraph with the image.
|
||||
SoGLRenderAction* glra = this->getSoRenderManager()->getGLRenderAction();
|
||||
SoState* state = glra->getState();
|
||||
SoDevicePixelRatioElement::set(state, devicePixelRatio());
|
||||
SoGLWidgetElement::set(state, qobject_cast<QOpenGLWidget*>(this->getGLWidget()));
|
||||
SoGLRenderActionElement::set(state, glra);
|
||||
SoGLVBOActivatedElement::set(state, this->vboEnabled);
|
||||
drawSingleBackground(col);
|
||||
glra->apply(this->backgroundroot);
|
||||
|
||||
// Render our scenegraph with the image.
|
||||
{
|
||||
ZoneScopedN("Background");
|
||||
SoDevicePixelRatioElement::set(state, devicePixelRatio());
|
||||
SoGLWidgetElement::set(state, qobject_cast<QOpenGLWidget*>(this->getGLWidget()));
|
||||
SoGLRenderActionElement::set(state, glra);
|
||||
SoGLVBOActivatedElement::set(state, this->vboEnabled);
|
||||
drawSingleBackground(col);
|
||||
glra->apply(this->backgroundroot);
|
||||
}
|
||||
|
||||
if (!this->shading) {
|
||||
state->push();
|
||||
@@ -2480,7 +2499,10 @@ void View3DInventorViewer::renderScene()
|
||||
#endif
|
||||
|
||||
// Render overlay front scenegraph.
|
||||
glra->apply(this->foregroundroot);
|
||||
{
|
||||
ZoneScopedN("Foreground");
|
||||
glra->apply(this->foregroundroot);
|
||||
}
|
||||
|
||||
if (this->axiscrossEnabled) {
|
||||
this->drawAxisCross();
|
||||
@@ -2498,8 +2520,11 @@ void View3DInventorViewer::renderScene()
|
||||
|
||||
printDimension();
|
||||
|
||||
for (auto it : this->graphicsItems) {
|
||||
it->paintGL();
|
||||
{
|
||||
ZoneScopedN("Graphics items");
|
||||
for (auto it : this->graphicsItems) {
|
||||
it->paintGL();
|
||||
}
|
||||
}
|
||||
|
||||
//fps rendering
|
||||
@@ -2661,6 +2686,8 @@ void View3DInventorViewer::selectAll()
|
||||
|
||||
bool View3DInventorViewer::processSoEvent(const SoEvent* ev)
|
||||
{
|
||||
ZoneScoped;
|
||||
|
||||
if (naviCubeEnabled && naviCube->processSoEvent(ev)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -121,13 +121,18 @@ class BIM_Material:
|
||||
buttonClear.clicked.connect(self.onClearSearch)
|
||||
lay.addLayout(searchLayout)
|
||||
|
||||
createButtonsLayoutBox = QtGui.QGroupBox(
|
||||
translate("BIM", " Material operations"), self.dlg
|
||||
)
|
||||
createButtonsLayoutBox.setObjectName("matOpsGrpBox")
|
||||
createButtonsLayout = QtGui.QGridLayout()
|
||||
|
||||
# create
|
||||
createLayout = QtGui.QHBoxLayout()
|
||||
buttonCreate = QtGui.QPushButton(
|
||||
translate("BIM", "Create new material"), self.dlg
|
||||
)
|
||||
buttonCreate.setIcon(QtGui.QIcon(":/icons/Arch_Material.svg"))
|
||||
createLayout.addWidget(buttonCreate)
|
||||
createButtonsLayout.addWidget(buttonCreate, 0, 0)
|
||||
buttonCreate.clicked.connect(self.onCreate)
|
||||
|
||||
# create multi
|
||||
@@ -135,9 +140,8 @@ class BIM_Material:
|
||||
translate("BIM", "Create new multi-material"), self.dlg
|
||||
)
|
||||
buttonMulti.setIcon(QtGui.QIcon(":/icons/Arch_Material_Multi.svg"))
|
||||
createLayout.addWidget(buttonMulti)
|
||||
createButtonsLayout.addWidget(buttonMulti, 0, 1)
|
||||
buttonMulti.clicked.connect(self.onMulti)
|
||||
lay.addLayout(createLayout)
|
||||
|
||||
# merge dupes
|
||||
opsLayout = QtGui.QHBoxLayout()
|
||||
@@ -145,7 +149,7 @@ class BIM_Material:
|
||||
translate("BIM", "Merge duplicates"), self.dlg
|
||||
)
|
||||
buttonMergeDupes.setIcon(QtGui.QIcon(":/icons/view-refresh.svg"))
|
||||
opsLayout.addWidget(buttonMergeDupes)
|
||||
createButtonsLayout.addWidget(buttonMergeDupes, 1, 0)
|
||||
buttonMergeDupes.clicked.connect(self.onMergeDupes)
|
||||
|
||||
# delete unused
|
||||
@@ -153,9 +157,11 @@ class BIM_Material:
|
||||
translate("BIM", "Delete unused"), self.dlg
|
||||
)
|
||||
buttonDeleteUnused.setIcon(QtGui.QIcon(":/icons/delete.svg"))
|
||||
opsLayout.addWidget(buttonDeleteUnused)
|
||||
createButtonsLayout.addWidget(buttonDeleteUnused, 1, 1)
|
||||
buttonDeleteUnused.clicked.connect(self.onDeleteUnused)
|
||||
lay.addLayout(opsLayout)
|
||||
|
||||
createButtonsLayoutBox.setLayout(createButtonsLayout)
|
||||
lay.addWidget(createButtonsLayoutBox)
|
||||
|
||||
# add standard buttons
|
||||
buttonBox = QtGui.QDialogButtonBox(self.dlg)
|
||||
|
||||
307
src/Mod/CAM/CAMTests/TestSnapmakerPost.py
Normal file
307
src/Mod/CAM/CAMTests/TestSnapmakerPost.py
Normal file
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Clair-Loup Sergent <clsergent@free.fr> *
|
||||
# * *
|
||||
# * Licensed under the EUPL-1.2 with the specific provision *
|
||||
# * (EUPL articles 14 & 15) that the applicable law is the French law. *
|
||||
# * and the Jurisdiction Paris. *
|
||||
# * Any redistribution must include the specific provision above. *
|
||||
# * *
|
||||
# * You may obtain a copy of the Licence at: *
|
||||
# * https://joinup.ec.europa.eu/software/page/eupl5 *
|
||||
# * *
|
||||
# * Unless required by applicable law or agreed to in writing, software *
|
||||
# * distributed under the Licence is distributed on an "AS IS" basis, *
|
||||
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or *
|
||||
# * implied. See the Licence for the specific language governing *
|
||||
# * permissions and limitations under the Licence. *
|
||||
# ***************************************************************************
|
||||
import re
|
||||
|
||||
import FreeCAD
|
||||
|
||||
import Path
|
||||
import CAMTests.PathTestUtils as PathTestUtils
|
||||
from Path.Post.Processor import PostProcessorFactory
|
||||
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
|
||||
|
||||
class TestSnapmakerPost(PathTestUtils.PathTestBase):
|
||||
"""Test the Snapmaker postprocessor."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up the test environment"""
|
||||
|
||||
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
|
||||
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
|
||||
cls.job = cls.doc.getObject("Job")
|
||||
cls.post = PostProcessorFactory.get_post_processor(cls.job, "snapmaker")
|
||||
# locate the operation named "Profile"
|
||||
for op in cls.job.Operations.Group:
|
||||
if op.Label == "Profile":
|
||||
# remember the "Profile" operation
|
||||
cls.profile_op = op
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Tear down the test environment"""
|
||||
FreeCAD.closeDocument(cls.doc.Name)
|
||||
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
|
||||
|
||||
def setUp(self):
|
||||
"""Unit test init"""
|
||||
# allow a full length "diff" if an error occurs
|
||||
self.maxDiff = None
|
||||
# reinitialize the postprocessor data structures between tests
|
||||
self.post.initialize()
|
||||
|
||||
def tearDown(self):
|
||||
"""Unit test tear down"""
|
||||
pass
|
||||
|
||||
def get_gcode(self, ops: [str], arguments: str) -> str:
|
||||
"""Get postprocessed gcode from a list of operations and postprocessor arguments"""
|
||||
self.profile_op.Path = Path.Path(ops)
|
||||
self.job.PostProcessorArgs = "--no-show-editor --no-gui --no-thumbnail " + arguments
|
||||
return self.post.export()[0][1]
|
||||
|
||||
def test_general(self):
|
||||
"""Test Output Generation"""
|
||||
|
||||
expected_header = """\
|
||||
;Header Start
|
||||
;header_type: cnc
|
||||
;machine: Snapmaker 2 A350(T)
|
||||
;Post Processor: Snapmaker_post
|
||||
;Cam File: boxtest.fcstd
|
||||
;Output Time: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{0,6}
|
||||
;thumbnail: deactivated."""
|
||||
|
||||
expected_body = """\
|
||||
;Begin preamble
|
||||
G90
|
||||
G17
|
||||
G21
|
||||
;Begin operation: Fixture
|
||||
;Path: Fixture
|
||||
G54
|
||||
;End operation: Fixture
|
||||
;Begin operation: TC: Default Tool
|
||||
;Path: TC: Default Tool
|
||||
;TC: Default Tool
|
||||
;Begin toolchange
|
||||
M5
|
||||
M76
|
||||
M6 T1
|
||||
;End operation: TC: Default Tool
|
||||
;Begin operation: Profile
|
||||
;Path: Profile
|
||||
;End operation: Profile
|
||||
;Begin postamble
|
||||
M400
|
||||
M5
|
||||
"""
|
||||
|
||||
# test header and body with comments
|
||||
gcode = self.get_gcode([], "--machine=A350 --toolhead=50W --spindle-percent")
|
||||
|
||||
g_lines = gcode.splitlines()
|
||||
e_lines = expected_header.splitlines() + expected_body.splitlines()
|
||||
|
||||
self.assertTrue(len(g_lines), len(e_lines))
|
||||
for (nbr, exp), line in zip(enumerate(e_lines), g_lines):
|
||||
if exp.startswith(";Output Time:"):
|
||||
self.assertTrue(re.match(exp, line) is not None)
|
||||
else:
|
||||
self.assertTrue(line, exp)
|
||||
|
||||
# test body without header
|
||||
gcode = self.get_gcode([], "--machine=A350 --toolhead=50W --spindle-percent --no-header")
|
||||
self.assertEqual(gcode, expected_body)
|
||||
|
||||
# test body without comments
|
||||
gcode = self.get_gcode(
|
||||
[], "--machine=A350 --toolhead=50W --spindle-percent --no-header --no-comments"
|
||||
)
|
||||
expected = "".join(
|
||||
[line for line in expected_body.splitlines(keepends=True) if not line.startswith(";")]
|
||||
)
|
||||
self.assertEqual(gcode, expected)
|
||||
|
||||
def test_command(self):
|
||||
"""Test command Generation"""
|
||||
command = Path.Command("G0 X10 Y20 Z30")
|
||||
expected = "G0 X10.000 Y20.000 Z30.000"
|
||||
|
||||
gcode = self.get_gcode(
|
||||
[command], "--machine=A350 --toolhead=50W --spindle-percent --no-header"
|
||||
)
|
||||
result = gcode.splitlines()[18]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_precision(self):
|
||||
"""Test Precision"""
|
||||
# test G0 command with precision 2 digits precision
|
||||
command = Path.Command("G0 X10 Y20 Z30")
|
||||
expected = "G0 X10.00 Y20.00 Z30.00"
|
||||
|
||||
gcode = self.get_gcode(
|
||||
[command], "--machine=A350 --toolhead=50W --spindle-percent --no-header --precision=2"
|
||||
)
|
||||
result = gcode.splitlines()[18]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_lines(self):
|
||||
"""Test Line Numbers"""
|
||||
command = Path.Command("G0 X10 Y20 Z30")
|
||||
expected = "N46 G0 X10.000 Y20.000 Z30.000"
|
||||
|
||||
gcode = self.get_gcode(
|
||||
[command],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --line-numbers --line-number=10 --line-increment=2",
|
||||
)
|
||||
result = gcode.splitlines()[18]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_preamble(self):
|
||||
"""Test Pre-amble"""
|
||||
gcode = self.get_gcode(
|
||||
[],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --preamble='G18 G55' --no-comments",
|
||||
)
|
||||
result = gcode.splitlines()[0]
|
||||
self.assertEqual(result, "G18 G55")
|
||||
|
||||
def test_postamble(self):
|
||||
"""Test Post-amble"""
|
||||
gcode = self.get_gcode(
|
||||
[],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --postamble='G0 Z50\nM2' --no-comments",
|
||||
)
|
||||
result = gcode.splitlines()[-2]
|
||||
self.assertEqual(result, "G0 Z50")
|
||||
self.assertEqual(gcode.splitlines()[-1], "M2")
|
||||
|
||||
def test_inches(self):
|
||||
"""Test inches conversion"""
|
||||
|
||||
command = Path.Command("G0 X10 Y20 Z30")
|
||||
|
||||
# test inches conversion
|
||||
expected = "G0 X0.3937 Y0.7874 Z1.1811"
|
||||
gcode = self.get_gcode(
|
||||
[command], "--machine=A350 --toolhead=50W --spindle-percent --no-header --inches"
|
||||
)
|
||||
self.assertEqual(gcode.splitlines()[3], "G20")
|
||||
result = gcode.splitlines()[18]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
# test inches conversion with 2 digits precision
|
||||
expected = "G0 X0.39 Y0.79 Z1.18"
|
||||
gcode = self.get_gcode(
|
||||
[command],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --inches --precision=2",
|
||||
)
|
||||
result = gcode.splitlines()[18]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_axis_modal(self):
|
||||
"""Test axis modal - Suppress the axis coordinate if the same as previous"""
|
||||
|
||||
c0 = Path.Command("G0 X10 Y20 Z30")
|
||||
c1 = Path.Command("G0 X10 Y30 Z30")
|
||||
expected = "G0 Y30.000"
|
||||
|
||||
gcode = self.get_gcode(
|
||||
[c0, c1], "--machine=A350 --toolhead=50W --spindle-percent --no-header --axis-modal"
|
||||
)
|
||||
result = gcode.splitlines()[19]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_tool_change(self):
|
||||
"""Test tool change"""
|
||||
|
||||
c0 = Path.Command("M6 T2")
|
||||
c1 = Path.Command("M3 S3000")
|
||||
|
||||
gcode = self.get_gcode(
|
||||
[c0, c1], "--machine=A350 --toolhead=50W --spindle-percent --no-header"
|
||||
)
|
||||
self.assertEqual(gcode.splitlines()[19:22], ["M5", "M76", "M6 T2"])
|
||||
self.assertEqual(
|
||||
gcode.splitlines()[22], "M3 P25"
|
||||
) # no TLO on Snapmaker (G43 inserted after tool change)
|
||||
|
||||
def test_spindle(self):
|
||||
"""Test spindle speed conversion from RPM to percents"""
|
||||
|
||||
command = Path.Command("M3 S3600")
|
||||
|
||||
# test 50W toolhead
|
||||
gcode = self.get_gcode(
|
||||
[command], "--machine=A350 --toolhead=50W --spindle-percent --no-header"
|
||||
)
|
||||
self.assertEqual(gcode.splitlines()[18], "M3 P30")
|
||||
|
||||
# test 200W toolhead
|
||||
gcode = self.get_gcode(
|
||||
[command], "--machine=A350 --toolhead=200W --spindle-percent --no-header"
|
||||
)
|
||||
self.assertEqual(gcode.splitlines()[18], "M3 P20")
|
||||
|
||||
# test custom spindle speed extrema
|
||||
gcode = self.get_gcode(
|
||||
[command],
|
||||
"--machine=A350 --toolhead=200W --spindle-percent --no-header --spindle-speeds=3000,4000",
|
||||
)
|
||||
self.assertEqual(gcode.splitlines()[18], "M3 P90")
|
||||
|
||||
def test_comment(self):
|
||||
"""Test comment"""
|
||||
|
||||
command = Path.Command("(comment)")
|
||||
gcode = self.get_gcode(
|
||||
[command], "--machine=A350 --toolhead=50W --spindle-percent --no-header"
|
||||
)
|
||||
result = gcode.splitlines()[18]
|
||||
expected = ";comment"
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_boundaries(self):
|
||||
"""Test boundaries check"""
|
||||
|
||||
# check succeeds
|
||||
command = Path.Command("G0 X100 Y-100.5 Z-1")
|
||||
|
||||
gcode = self.get_gcode(
|
||||
[command],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --boundaries-check",
|
||||
)
|
||||
self.assertTrue(self.post.check_boundaries(gcode.splitlines()))
|
||||
|
||||
# check fails with A350
|
||||
c0 = Path.Command("G01 X100 Y-100.5 Z-1")
|
||||
c1 = Path.Command("G02 Y260")
|
||||
gcode = self.get_gcode(
|
||||
[c0, c1],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --boundaries-check",
|
||||
)
|
||||
self.assertFalse(self.post.check_boundaries(gcode.splitlines()))
|
||||
|
||||
# check succeed with artisan (which base is bigger)
|
||||
gcode = self.get_gcode(
|
||||
[c0, c1],
|
||||
"--machine=artisan --toolhead=50W --spindle-percent --no-header --boundaries-check",
|
||||
)
|
||||
self.assertTrue(self.post.check_boundaries(gcode.splitlines()))
|
||||
|
||||
# check fails with custom boundaries
|
||||
gcode = self.get_gcode(
|
||||
[c0, c1],
|
||||
"--machine=A350 --toolhead=50W --spindle-percent --no-header --boundaries-check --boundaries='50,400,10'",
|
||||
)
|
||||
self.assertFalse(self.post.check_boundaries(gcode.splitlines()))
|
||||
@@ -171,6 +171,7 @@ SET(PathPythonPostScripts_SRCS
|
||||
Path/Post/scripts/rrf_post.py
|
||||
Path/Post/scripts/slic3r_pre.py
|
||||
Path/Post/scripts/smoothie_post.py
|
||||
Path/Post/scripts/snapmaker_post.py
|
||||
Path/Post/scripts/uccnc_post.py
|
||||
Path/Post/scripts/wedm_post.py
|
||||
)
|
||||
@@ -353,6 +354,7 @@ SET(Tests_SRCS
|
||||
CAMTests/TestRefactoredTestPost.py
|
||||
CAMTests/TestRefactoredTestPostGCodes.py
|
||||
CAMTests/TestRefactoredTestPostMCodes.py
|
||||
CAMTests/TestSnapmakerPost.py
|
||||
CAMTests/Tools/Bit/test-path-tool-bit-bit-00.fctb
|
||||
CAMTests/Tools/Library/test-path-tool-bit-library-00.fctl
|
||||
CAMTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd
|
||||
|
||||
642
src/Mod/CAM/Path/Post/scripts/snapmaker_post.py
Normal file
642
src/Mod/CAM/Path/Post/scripts/snapmaker_post.py
Normal file
@@ -0,0 +1,642 @@
|
||||
#!/usr/bin/env python3
|
||||
# A FreeCAD postprocessor targeting Snapmaker machines with CNC capabilities
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2025 Clair-Loup Sergent <clsergent@free.fr> *
|
||||
# * *
|
||||
# * Licensed under the EUPL-1.2 with the specific provision *
|
||||
# * (EUPL articles 14 & 15) that the applicable law is the French law. *
|
||||
# * and the Jurisdiction Paris. *
|
||||
# * Any redistribution must include the specific provision above. *
|
||||
# * *
|
||||
# * You may obtain a copy of the Licence at: *
|
||||
# * https://joinup.ec.europa.eu/software/page/eupl5 *
|
||||
# * *
|
||||
# * Unless required by applicable law or agreed to in writing, software *
|
||||
# * distributed under the Licence is distributed on an "AS IS" basis, *
|
||||
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or *
|
||||
# * implied. See the Licence for the specific language governing *
|
||||
# * permissions and limitations under the Licence. *
|
||||
# ***************************************************************************
|
||||
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import datetime
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import Path.Post.Processor
|
||||
import Path.Post.UtilsArguments
|
||||
import Path.Post.UtilsExport
|
||||
import Path.Post.Utils
|
||||
import Path.Post.UtilsParse
|
||||
import Path.Main.Job
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
if DEBUG := False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
SNAPMAKER_MACHINES = dict(
|
||||
original=dict(name="Snapmaker Original", X=90, Y=90, Z=50),
|
||||
original_z_extension=dict(name="Snapmaker Original with Z extension", X=90, Y=90, Z=146),
|
||||
a150=dict(name="A150", X=160, Y=160, Z=90),
|
||||
**dict.fromkeys(("A250", "A250T"), dict(name="Snapmaker 2 A250(T)", X=230, Y=250, Z=180)),
|
||||
**dict.fromkeys(("A350", "A350T"), dict(name="Snapmaker 2 A350(T)", X=320, Y=350, Z=275)),
|
||||
artisan=dict(name="Snapmaker Artisan", X=400, Y=400, Z=400),
|
||||
)
|
||||
|
||||
SNAPMAKER_TOOLHEADS = {
|
||||
"50W": dict(name="50W CNC module", min=0, max=12000, percent=True),
|
||||
"200W": dict(name="200W CNC module", min=8000, max=18000, percent=False),
|
||||
}
|
||||
|
||||
|
||||
class CoordinatesAction(argparse.Action):
|
||||
"""argparse Action to handle coordinates (x,y,z)"""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
match = re.match(
|
||||
"^\s*(?P<X>-?\d+\.?\d*),?\s*(?P<Y>-?\d+\.?\d*),?\s*(?P<Z>-?\d+\.?\d*)\s*$", values
|
||||
)
|
||||
if match:
|
||||
# setattr(namespace, self.dest, 'G0 X{0} Y{1} Z{2}'.format(*match.groups()))
|
||||
params = {key: float(value) for key, value in match.groupdict().items()}
|
||||
setattr(namespace, self.dest, params)
|
||||
else:
|
||||
raise argparse.ArgumentError(None, message="invalid coordinates provided")
|
||||
|
||||
|
||||
class ExtremaAction(argparse.Action):
|
||||
"""argparse Action to handle integer extrema (min,max)"""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if match := re.match("^ *(\d+),? *(\d+) *$", values):
|
||||
# setattr(namespace, self.dest, 'G0 X{0} Y{1} Z{2}'.format(*match.groups()))
|
||||
params = {
|
||||
key: int(value)
|
||||
for key, value in zip(
|
||||
(
|
||||
"min",
|
||||
"max",
|
||||
),
|
||||
match.groups(),
|
||||
)
|
||||
}
|
||||
setattr(namespace, self.dest, params)
|
||||
else:
|
||||
raise argparse.ArgumentError(None, message="invalid values provided, should be int,int")
|
||||
|
||||
|
||||
class Snapmaker(Path.Post.Processor.PostProcessor):
|
||||
"""FreeCAD postprocessor targeting Snapmaker machines with CNC capabilities"""
|
||||
|
||||
def __init__(self, job) -> None:
|
||||
super().__init__(
|
||||
job=job,
|
||||
tooltip=translate("CAM", "Snapmaker post processor"),
|
||||
tooltipargs=[""],
|
||||
units="Metric",
|
||||
)
|
||||
|
||||
self.initialize()
|
||||
|
||||
def initialize(self):
|
||||
"""initialize values and arguments"""
|
||||
self.values: dict[str, Any] = dict()
|
||||
self.argument_defaults: dict[str, bool] = dict()
|
||||
self.arguments_visible: dict[str, bool] = dict()
|
||||
self.parser = argparse.ArgumentParser()
|
||||
|
||||
self.init_values()
|
||||
self.init_argument_defaults()
|
||||
self.init_arguments_visible()
|
||||
self.parser = self.init_parser(self.values, self.argument_defaults, self.arguments_visible)
|
||||
|
||||
# create another parser with all visible arguments
|
||||
all_arguments_visible = dict()
|
||||
for key in iter(self.arguments_visible):
|
||||
all_arguments_visible[key] = True
|
||||
self.visible_parser = self.init_parser(
|
||||
self.values, self.argument_defaults, all_arguments_visible
|
||||
)
|
||||
|
||||
FreeCAD.Console.PrintLog(f'{self.values["POSTPROCESSOR_FILE_NAME"]}: initialized.\n')
|
||||
|
||||
def init_values(self):
|
||||
"""Initialize values that are used throughout the postprocessor."""
|
||||
Path.Post.UtilsArguments.init_shared_values(self.values)
|
||||
|
||||
# shared values
|
||||
self.values["POSTPROCESSOR_FILE_NAME"] = __name__
|
||||
self.values["COMMENT_SYMBOL"] = ";"
|
||||
self.values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True
|
||||
self.values["END_OF_LINE_CHARACTERS"] = "\n"
|
||||
self.values["FINISH_LABEL"] = "End"
|
||||
self.values["LINE_INCREMENT"] = 1
|
||||
self.values["MACHINE_NAME"] = "Generic Snapmaker"
|
||||
self.values["MODAL"] = False
|
||||
self.values["OUTPUT_PATH_LABELS"] = True
|
||||
self.values["OUTPUT_HEADER"] = (
|
||||
True # remove FreeCAD standard header and use a custom Snapmaker Header
|
||||
)
|
||||
self.values["OUTPUT_TOOL_CHANGE"] = True
|
||||
self.values["PARAMETER_ORDER"] = [
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"I",
|
||||
"J",
|
||||
"F",
|
||||
"S",
|
||||
"T",
|
||||
"Q",
|
||||
"R",
|
||||
"L",
|
||||
"H",
|
||||
"D",
|
||||
"P",
|
||||
"O",
|
||||
]
|
||||
self.values["PREAMBLE"] = f"""G90\nG17"""
|
||||
self.values["PRE_OPERATION"] = """"""
|
||||
self.values["POST_OPERATION"] = """"""
|
||||
self.values["POSTAMBLE"] = """M400\nM5"""
|
||||
self.values["SHOW_MACHINE_UNITS"] = False
|
||||
self.values["SPINDLE_DECIMALS"] = 0
|
||||
self.values["SPINDLE_WAIT"] = 4.0
|
||||
self.values["TOOL_CHANGE"] = "M76" # handle tool change by inserting an HMI pause
|
||||
self.values["TRANSLATE_DRILL_CYCLES"] = True # drill cycle gcode must be translated
|
||||
self.values["USE_TLO"] = False # G43 is not handled.
|
||||
|
||||
# snapmaker values
|
||||
self.values["THUMBNAIL"] = True
|
||||
self.values["BOUNDARIES"] = None
|
||||
self.values["BOUNDARIES_CHECK"] = False
|
||||
self.values["MACHINES"] = SNAPMAKER_MACHINES
|
||||
self.values["TOOLHEADS"] = SNAPMAKER_TOOLHEADS
|
||||
# default toolhead is 50W (the weakest one)
|
||||
self.values["DEFAULT_TOOLHEAD"] = "50W"
|
||||
self.values["TOOLHEAD_NAME"] = SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]]["name"]
|
||||
self.values["SPINDLE_SPEEDS"] = dict(
|
||||
min=SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]]["min"],
|
||||
max=SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]]["max"],
|
||||
)
|
||||
self.values["SPINDLE_PERCENT"] = SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]][
|
||||
"percent"
|
||||
]
|
||||
|
||||
def init_argument_defaults(self) -> None:
|
||||
"""Initialize which arguments (in a pair) are shown as the default argument."""
|
||||
Path.Post.UtilsArguments.init_argument_defaults(self.argument_defaults)
|
||||
|
||||
self.argument_defaults["tlo"] = False
|
||||
self.argument_defaults["translate-drill"] = True
|
||||
|
||||
# snapmaker arguments
|
||||
self.argument_defaults["thumbnail"] = True
|
||||
self.argument_defaults["gui"] = True
|
||||
self.argument_defaults["boundaries-check"] = True
|
||||
self.argument_defaults["spindle-percent"] = True
|
||||
|
||||
def init_arguments_visible(self) -> None:
|
||||
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
|
||||
Path.Post.UtilsArguments.init_arguments_visible(self.arguments_visible)
|
||||
|
||||
self.arguments_visible["axis-modal"] = False
|
||||
self.arguments_visible["header"] = False
|
||||
self.arguments_visible["return-to"] = True
|
||||
self.arguments_visible["tlo"] = False
|
||||
self.arguments_visible["tool_change"] = True
|
||||
self.arguments_visible["translate-drill"] = False
|
||||
self.arguments_visible["wait-for-spindle"] = True
|
||||
|
||||
# snapmaker arguments (for record, always visible)
|
||||
self.arguments_visible["thumbnail"] = True
|
||||
self.arguments_visible["gui"] = True
|
||||
self.arguments_visible["boundaries"] = True
|
||||
self.arguments_visible["boundaries-check"] = True
|
||||
self.arguments_visible["machine"] = True
|
||||
self.arguments_visible["toolhead"] = True
|
||||
self.arguments_visible["line-increment"] = True
|
||||
self.arguments_visible["spindle-speeds"] = True
|
||||
|
||||
def init_parser(self, values, argument_defaults, arguments_visible) -> argparse.ArgumentParser:
|
||||
"""Initialize the postprocessor arguments parser"""
|
||||
parser = Path.Post.UtilsArguments.init_shared_arguments(
|
||||
values, argument_defaults, arguments_visible
|
||||
)
|
||||
|
||||
# snapmaker custom arguments
|
||||
group = parser.add_argument_group("Snapmaker only arguments")
|
||||
# add_flag_type_arguments function is not used as its behavior is inconsistent with argparse
|
||||
# handle thumbnail generation
|
||||
group.add_argument(
|
||||
"--thumbnail",
|
||||
action="store_true",
|
||||
default=argument_defaults["thumbnail"],
|
||||
help="Include a thumbnail (require --gui)",
|
||||
)
|
||||
group.add_argument(
|
||||
"--no-thumbnail", action="store_false", dest="thumbnail", help="Remove thumbnail"
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--gui",
|
||||
action="store_true",
|
||||
default=argument_defaults["gui"],
|
||||
help="allow the postprocessor to execute GUI methods",
|
||||
)
|
||||
group.add_argument(
|
||||
"--no-gui",
|
||||
action="store_false",
|
||||
dest="gui",
|
||||
help="Execute postprocessor without requiring GUI",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--boundaries-check",
|
||||
action="store_true",
|
||||
default=argument_defaults["boundaries-check"],
|
||||
help="check boundaries according to the machine build area",
|
||||
)
|
||||
group.add_argument(
|
||||
"--no-boundaries-check",
|
||||
action="store_false",
|
||||
dest="boundaries_check",
|
||||
help="Disable boundaries check",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--boundaries",
|
||||
action=CoordinatesAction,
|
||||
default=None,
|
||||
help='Custom boundaries (e.g. "100, 200, 300"). Overrides --machine',
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--machine",
|
||||
default=None,
|
||||
choices=self.values["MACHINES"].keys(),
|
||||
help=f"Snapmaker machine",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--toolhead",
|
||||
default=None,
|
||||
choices=self.values["TOOLHEADS"].keys(),
|
||||
help=f"Snapmaker toolhead",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--spindle-speeds",
|
||||
action=ExtremaAction,
|
||||
default=None,
|
||||
help="Set minimum/maximum spindle speeds as --spindle-speeds='min,max'",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--spindle-percent",
|
||||
action="store_true",
|
||||
default=argument_defaults["spindle-percent"],
|
||||
help="use percent as toolhead spindle speed unit",
|
||||
)
|
||||
group.add_argument(
|
||||
"--spindle-rpm",
|
||||
action="store_false",
|
||||
dest="spindle_percent",
|
||||
help="Use RPM as toolhead spindle speed unit",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--line-number",
|
||||
type=int,
|
||||
default=self.values["line_number"],
|
||||
help="Set the line starting value",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--line-increment",
|
||||
type=int,
|
||||
default=self.values["LINE_INCREMENT"],
|
||||
help="Set the line increment value",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def process_arguments(self, filename: str = "-") -> (bool, str | argparse.Namespace):
|
||||
"""Process any arguments to the postprocessor."""
|
||||
(flag, args) = Path.Post.UtilsArguments.process_shared_arguments(
|
||||
self.values, self.parser, self._job.PostProcessorArgs, self.visible_parser, filename
|
||||
)
|
||||
if flag: # process extra arguments only if flag is True
|
||||
self._units = self.values["UNITS"]
|
||||
|
||||
if args.machine:
|
||||
machine = self.values["MACHINES"][args.machine]
|
||||
self.values["MACHINE_NAME"] = machine["name"]
|
||||
self.values["BOUNDARIES"] = {key: machine[key] for key in ("X", "Y", "Z")}
|
||||
|
||||
if args.boundaries: # may override machine boundaries, which is expected
|
||||
self.values["BOUNDARIES"] = args.boundaries
|
||||
|
||||
if args.toolhead:
|
||||
toolhead = self.values["TOOLHEADS"][args.toolhead]
|
||||
self.values["TOOLHEAD_NAME"] = toolhead["name"]
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f'No toolhead selected, using default ({self.values["TOOLHEAD_NAME"]}). '
|
||||
f"Consider adding --toolhead\n"
|
||||
)
|
||||
toolhead = self.values["TOOLHEADS"][self.values["DEFAULT_TOOLHEAD"]]
|
||||
|
||||
self.values["SPINDLE_SPEEDS"] = {key: toolhead[key] for key in ("min", "max")}
|
||||
|
||||
if args.spindle_speeds: # may override toolhead value, which is expected
|
||||
self.values["SPINDLE_SPEEDS"] = args.spindle_speeds
|
||||
|
||||
if args.spindle_percent is not None:
|
||||
if toolhead["percent"] is True:
|
||||
self.values["SPINDLE_PERCENT"] = True
|
||||
if args.spindle_percent is False:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Toolhead does not handle RPM spindle speed, using percents instead.\n"
|
||||
)
|
||||
else:
|
||||
self.values["SPINDLE_PERCENT"] = args.spindle_percent
|
||||
|
||||
self.values["THUMBNAIL"] = args.thumbnail
|
||||
self.values["ALLOW_GUI"] = args.gui
|
||||
self.values["line_number"] = args.line_number
|
||||
self.values["LINE_INCREMENT"] = args.line_increment
|
||||
|
||||
if args.boundaries_check and not self.values["BOUNDARIES"]:
|
||||
FreeCAD.Console.PrintError("Boundary check skipped: no valid boundaries supplied\n")
|
||||
self.values["BOUNDARIES_CHECK"] = False
|
||||
else:
|
||||
self.values["BOUNDARIES_CHECK"] = args.boundaries_check
|
||||
|
||||
return flag, args
|
||||
|
||||
def process_postables(self, filename: str = "-") -> [(str, str)]:
|
||||
"""process job sections to gcode"""
|
||||
sections: [(str, str)] = list()
|
||||
|
||||
postables = self._buildPostList()
|
||||
|
||||
# basic filename handling
|
||||
if len(postables) > 1 and filename != "-":
|
||||
filename = pathlib.Path(filename)
|
||||
filename = str(filename.with_stem(filename.stem + "_{name}"))
|
||||
|
||||
for name, objects in postables:
|
||||
gcode = self.export_common(objects, filename.format(name=name))
|
||||
sections.append((name, gcode))
|
||||
|
||||
return sections
|
||||
|
||||
def get_thumbnail(self) -> str:
|
||||
"""generate a thumbnail of the job from the given objects"""
|
||||
if self.values["THUMBNAIL"] is False:
|
||||
return "thumbnail: deactivated."
|
||||
|
||||
if not (self.values["ALLOW_GUI"] and FreeCAD.GuiUp):
|
||||
FreeCAD.Console.PrintError(
|
||||
"GUI access required: thumbnail generation skipped. Consider adding --gui\n"
|
||||
)
|
||||
return "thumbnail: GUI required."
|
||||
|
||||
# get FreeCAD references
|
||||
import FreeCADGui
|
||||
|
||||
view = FreeCADGui.activeDocument().activeView()
|
||||
selection = FreeCADGui.Selection
|
||||
|
||||
# save current selection
|
||||
selected = [
|
||||
obj.Object for obj in selection.getCompleteSelection() if hasattr(obj, "Object")
|
||||
]
|
||||
selection.clearSelection()
|
||||
|
||||
# clear view
|
||||
FreeCADGui.runCommand("Std_SelectAll", 0)
|
||||
all = []
|
||||
for obj in selection.getCompleteSelection():
|
||||
if hasattr(obj, "Object"):
|
||||
all.append((obj.Object, obj.Object.Visibility))
|
||||
obj.Object.ViewObject.hide()
|
||||
|
||||
# select models to display
|
||||
for model in self._job.Model.Group:
|
||||
model.ViewObject.show()
|
||||
selection.addSelection(model.Document.Name, model.Name)
|
||||
view.fitAll() # center selection
|
||||
view.viewIsometric() # display as isometric
|
||||
selection.clearSelection()
|
||||
|
||||
# generate thumbnail
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = os.path.join(temp, "thumbnail.png")
|
||||
view.saveImage(path, 720, 480, "Transparent")
|
||||
with open(path, "rb") as file:
|
||||
data = file.read()
|
||||
|
||||
# restore view
|
||||
for obj, visibility in all:
|
||||
if visibility:
|
||||
obj.ViewObject.show()
|
||||
|
||||
# restore selection
|
||||
for obj in selected:
|
||||
selection.clearSelection()
|
||||
selection.addSelection(obj.Document.Name, obj.Name)
|
||||
|
||||
return f"thumbnail: data:image/png;base64,{base64.b64encode(data).decode()}"
|
||||
|
||||
def output_header(self, gcode: [[]]):
|
||||
"""custom method derived from Path.Post.UtilsExport.output_header"""
|
||||
cam_file: str
|
||||
comment: str
|
||||
nl: str = "\n"
|
||||
|
||||
if not self.values["OUTPUT_HEADER"]:
|
||||
return
|
||||
|
||||
def add_comment(text):
|
||||
com = Path.Post.UtilsParse.create_comment(self.values, text)
|
||||
gcode.append(
|
||||
f'{Path.Post.UtilsParse.linenumber(self.values)}{com}{self.values["END_OF_LINE_CHARACTERS"]}'
|
||||
)
|
||||
|
||||
add_comment("Header Start")
|
||||
add_comment("header_type: cnc")
|
||||
add_comment(f'machine: {self.values["MACHINE_NAME"]}')
|
||||
comment = Path.Post.UtilsParse.create_comment(
|
||||
self.values, f'Post Processor: {self.values["POSTPROCESSOR_FILE_NAME"]}'
|
||||
)
|
||||
gcode.append(f"{Path.Post.UtilsParse.linenumber(self.values)}{comment}{nl}")
|
||||
if FreeCAD.ActiveDocument:
|
||||
cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName)
|
||||
else:
|
||||
cam_file = "<None>"
|
||||
add_comment(f"Cam File: {cam_file}")
|
||||
add_comment(f"Output Time: {datetime.datetime.now()}")
|
||||
add_comment(self.get_thumbnail())
|
||||
|
||||
def convert_spindle(self, gcode: [str]) -> [str]:
|
||||
"""convert spindle speed values from RPM to percent (%) (M3/M4 commands)"""
|
||||
if self.values["SPINDLE_PERCENT"] is False:
|
||||
return
|
||||
|
||||
# TODO: check if percentage covers range 0-max (most probable) or min-max (200W has a documented min speed)
|
||||
for index, commandline in enumerate(
|
||||
gcode
|
||||
): # .split(self.values["END_OF_LINE_CHARACTERS"]):
|
||||
if match := re.match("(?P<command>M0?[34])\D.*(?P<spindle>S\d+.?\d*)", commandline):
|
||||
percent = (
|
||||
float(match.group("spindle")[1:]) * 100 / self.values["SPINDLE_SPEEDS"]["max"]
|
||||
)
|
||||
gcode[index] = (
|
||||
gcode[index][: match.span("spindle")[0]]
|
||||
+ f'P{percent:.{self.values["SPINDLE_DECIMALS"]}f}'
|
||||
+ gcode[index][match.span("spindle")[1] :]
|
||||
)
|
||||
return gcode
|
||||
|
||||
def check_boundaries(self, gcode: [str]) -> bool:
|
||||
"""Check boundaries and return whether it succeeded"""
|
||||
status = True
|
||||
FreeCAD.Console.PrintLog("Boundaries check\n")
|
||||
|
||||
extrema = dict(X=[0, 0], Y=[0, 0], Z=[0, 0])
|
||||
position = dict(X=0, Y=0, Z=0)
|
||||
relative = False
|
||||
|
||||
for index, commandline in enumerate(gcode):
|
||||
if re.match("G90(?:\D|$)", commandline):
|
||||
relative = False
|
||||
elif re.match("G91(?:\D|$)", commandline):
|
||||
relative = True
|
||||
elif re.match("G0?[12](?:\D|$)", commandline):
|
||||
for axis, value in re.findall(
|
||||
"(?P<axis>[XYZ])(?P<value>-?\d+\.?\d*)(?:\D|$)", commandline
|
||||
):
|
||||
if relative:
|
||||
position[axis] += float(value)
|
||||
else:
|
||||
position[axis] = float(value)
|
||||
extrema[axis][0] = max(extrema[axis][0], position[axis])
|
||||
extrema[axis][1] = min(extrema[axis][1], position[axis])
|
||||
|
||||
for axis in extrema.keys():
|
||||
if abs(extrema[axis][0] - extrema[axis][1]) > self.values["BOUNDARIES"][axis]:
|
||||
# gcode.insert(0, f';WARNING: Boundary check: job exceeds machine limit on {axis} axis{self.values["END_OF_LINE_CHARACTERS"]}')
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Boundary check: job exceeds machine limit on {axis} axis\n"
|
||||
)
|
||||
status = False
|
||||
|
||||
return status
|
||||
|
||||
def export_common(self, objects: list, filename: str | pathlib.Path) -> str:
|
||||
"""custom method derived from Path.Post.UtilsExport.export_common"""
|
||||
final: str
|
||||
gcode: [[]] = []
|
||||
result: bool
|
||||
|
||||
for obj in objects:
|
||||
if not hasattr(obj, "Path"):
|
||||
print(f"The object {obj.Name} is not a path.")
|
||||
print("Please select only path and Compounds.")
|
||||
return ""
|
||||
|
||||
Path.Post.UtilsExport.check_canned_cycles(self.values)
|
||||
self.output_header(gcode)
|
||||
Path.Post.UtilsExport.output_safetyblock(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_tool_list(self.values, gcode, objects)
|
||||
Path.Post.UtilsExport.output_preamble(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_motion_mode(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_units(self.values, gcode)
|
||||
|
||||
for obj in objects:
|
||||
# Skip inactive operations
|
||||
if hasattr(obj, "Active") and not obj.Active:
|
||||
continue
|
||||
if hasattr(obj, "Base") and hasattr(obj.Base, "Active") and not obj.Base.Active:
|
||||
continue
|
||||
coolant_mode = Path.Post.UtilsExport.determine_coolant_mode(obj)
|
||||
Path.Post.UtilsExport.output_start_bcnc(self.values, gcode, obj)
|
||||
Path.Post.UtilsExport.output_preop(self.values, gcode, obj)
|
||||
Path.Post.UtilsExport.output_coolant_on(self.values, gcode, coolant_mode)
|
||||
# output the G-code for the group (compound) or simple path
|
||||
Path.Post.UtilsParse.parse_a_group(self.values, gcode, obj)
|
||||
|
||||
Path.Post.UtilsExport.output_postop(self.values, gcode, obj)
|
||||
Path.Post.UtilsExport.output_coolant_off(self.values, gcode, coolant_mode)
|
||||
|
||||
Path.Post.UtilsExport.output_return_to(self.values, gcode)
|
||||
#
|
||||
# This doesn't make sense to me. It seems that both output_start_bcnc and
|
||||
# output_end_bcnc should be in the for loop or both should be out of the
|
||||
# for loop. However, that is the way that grbl post code was written, so
|
||||
# for now I will leave it that way until someone has time to figure it out.
|
||||
#
|
||||
Path.Post.UtilsExport.output_end_bcnc(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_postamble_header(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_tool_return(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_safetyblock(self.values, gcode)
|
||||
Path.Post.UtilsExport.output_postamble(self.values, gcode)
|
||||
gcode = self.convert_spindle(gcode)
|
||||
|
||||
if self.values["BOUNDARIES_CHECK"]:
|
||||
self.check_boundaries(gcode)
|
||||
|
||||
final = "".join(gcode)
|
||||
|
||||
if FreeCAD.GuiUp and self.values["SHOW_EDITOR"]:
|
||||
# size limit removed as irrelevant on my computer - see if issues occur
|
||||
dia = Path.Post.Utils.GCodeEditorDialog()
|
||||
dia.editor.setText(final)
|
||||
result = dia.exec_()
|
||||
if result:
|
||||
final = dia.editor.toPlainText()
|
||||
|
||||
if not filename == "-":
|
||||
with open(
|
||||
filename, "w", encoding="utf-8", newline=self.values["END_OF_LINE_CHARACTERS"]
|
||||
) as gfile:
|
||||
gfile.write(final)
|
||||
|
||||
return final
|
||||
|
||||
def export(self, filename: str | pathlib.Path = "-"):
|
||||
"""process gcode and export"""
|
||||
(flag, args) = self.process_arguments()
|
||||
if flag:
|
||||
return self.process_postables(filename)
|
||||
else:
|
||||
return [("allitems", args)]
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
tooltip = "Postprocessor of the FreeCAD CAM workbench for the Snapmaker machines"
|
||||
return tooltip
|
||||
|
||||
@property
|
||||
def tooltipArgs(self) -> str:
|
||||
return self.parser.format_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Snapmaker(None).visible_parser.format_help()
|
||||
@@ -80,6 +80,7 @@ from CAMTests.TestRefactoredMach3Mach4Post import TestRefactoredMach3Mach4Post
|
||||
from CAMTests.TestRefactoredTestPost import TestRefactoredTestPost
|
||||
from CAMTests.TestRefactoredTestPostGCodes import TestRefactoredTestPostGCodes
|
||||
from CAMTests.TestRefactoredTestPostMCodes import TestRefactoredTestPostMCodes
|
||||
from CAMTests.TestSnapmakerPost import TestSnapmakerPost
|
||||
|
||||
# dummy usage to get flake8 and lgtm quiet
|
||||
False if TestCAMSanity.__name__ else True
|
||||
@@ -136,3 +137,4 @@ False if TestRefactoredMach3Mach4Post.__name__ else True
|
||||
False if TestRefactoredTestPost.__name__ else True
|
||||
False if TestRefactoredTestPostGCodes.__name__ else True
|
||||
False if TestRefactoredTestPostMCodes.__name__ else True
|
||||
False if TestSnapmakerPost.__name__ else True
|
||||
|
||||
@@ -74,7 +74,6 @@ class Fillet(gui_base_original.Creator):
|
||||
super().Activated(name=name)
|
||||
|
||||
if self.ui:
|
||||
self.rad = params.get_param("FilletRadius")
|
||||
self.chamfer = params.get_param("FilletChamferMode")
|
||||
self.delete = params.get_param("FilletDeleteMode")
|
||||
label = translate("draft", "Fillet radius")
|
||||
@@ -86,7 +85,8 @@ class Fillet(gui_base_original.Creator):
|
||||
self.ui.sourceCmd = self
|
||||
self.ui.labelRadius.setText(label)
|
||||
self.ui.radiusValue.setToolTip(tooltip)
|
||||
self.ui.setRadiusValue(self.rad, "Length")
|
||||
self.ui.radius = params.get_param("FilletRadius")
|
||||
self.ui.setRadiusValue(self.ui.radius, "Length")
|
||||
self.ui.check_delete = self.ui._checkbox("isdelete",
|
||||
self.ui.layout,
|
||||
checked=self.delete)
|
||||
@@ -136,13 +136,10 @@ class Fillet(gui_base_original.Creator):
|
||||
params.set_param("FilletChamferMode", self.chamfer)
|
||||
|
||||
def numericRadius(self, rad):
|
||||
"""Validate the entry radius in the user interface.
|
||||
|
||||
This function is called by the toolbar or taskpanel interface
|
||||
when a valid radius has been entered in the input field.
|
||||
"""This function is called by the taskpanel interface
|
||||
when a radius has been entered in the input field.
|
||||
"""
|
||||
self.rad = rad
|
||||
params.set_param("FilletRadius", self.rad)
|
||||
params.set_param("FilletRadius", rad)
|
||||
self.draw_arc(rad, self.chamfer, self.delete)
|
||||
|
||||
def draw_arc(self, rad, chamfer, delete):
|
||||
|
||||
186
src/Mod/Measure/Gui/Resources/translations/Measure_es-ES.ts
Normal file
186
src/Mod/Measure/Gui/Resources/translations/Measure_es-ES.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="es_ES" sourcelanguage="en_US">
|
||||
<context>
|
||||
<name>Gui::TaskMeasure</name>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="69"/>
|
||||
<source>Measurement</source>
|
||||
<translation>Medición</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="87"/>
|
||||
<source>Show Delta:</source>
|
||||
<translation>Mostrar delta:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="90"/>
|
||||
<source>Auto Save</source>
|
||||
<translation>Guardar automáticamente</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="93"/>
|
||||
<source>Auto saving of the last measurement when starting a new measurement. Use SHIFT to temporarily invert the behaviour.</source>
|
||||
<translation>Guardar automáticamente la última medición al comenzar una nueva medición. Utilice MAYÚS para invertir el comportamiento temporalmente.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="97"/>
|
||||
<source>Additive Selection</source>
|
||||
<translation>Selección aditiva</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="102"/>
|
||||
<source>If checked, new selection will be added to the measurement. If unchecked, CTRL must be pressed to add a selection to the current measurement otherwise a new measurement will be started</source>
|
||||
<translation>Si está marcado, las nuevas selecciones serán agregadas a la medición. De lo contrario, CTRL debe de ser presionado para agregar una selección a la medición actual, de otro modo, una nueva medición será iniciada.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="111"/>
|
||||
<source>Settings</source>
|
||||
<translation>Ajustes</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="152"/>
|
||||
<source>Mode:</source>
|
||||
<translation>Modo:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="154"/>
|
||||
<source>Result:</source>
|
||||
<translation>Resultado:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="183"/>
|
||||
<source>Save</source>
|
||||
<translation>Guardar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="184"/>
|
||||
<source>Save the measurement in the active document.</source>
|
||||
<translation>Guardar la medición en el documento activo.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="190"/>
|
||||
<source>Close</source>
|
||||
<translation>Cerrar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../TaskMeasure.cpp" line="191"/>
|
||||
<source>Close the measurement task.</source>
|
||||
<translation>Cerrar la tarea de medición.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MeasureGui::DlgPrefsMeasureAppearanceImp</name>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="20"/>
|
||||
<source>Appearance</source>
|
||||
<translation>Apariencia</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="142"/>
|
||||
<source>Text color</source>
|
||||
<translation>Color de texto</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="59"/>
|
||||
<source>Text size</source>
|
||||
<translation>Tamaño de texto</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="51"/>
|
||||
<source>Default property values</source>
|
||||
<translation>Valores de propiedad por defecto</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="66"/>
|
||||
<source>Line color</source>
|
||||
<translation>Color de línea</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="76"/>
|
||||
<source> px</source>
|
||||
<translation> px</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../DlgPrefsMeasureAppearanceImp.ui" line="112"/>
|
||||
<source>Background color</source>
|
||||
<translation>Color de fondo</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MeasureGui::QuickMeasure</name>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="204"/>
|
||||
<source>Total area: %1</source>
|
||||
<translation>Área total: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="215"/>
|
||||
<location filename="../../QuickMeasure.cpp" line="229"/>
|
||||
<source>Nominal distance: %1</source>
|
||||
<translation>Distancia nominal: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="218"/>
|
||||
<source>Area: %1</source>
|
||||
<translation>Área: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="222"/>
|
||||
<source>Area: %1, Radius: %2</source>
|
||||
<translation>Área: %1, Radio: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="226"/>
|
||||
<source>Total length: %1</source>
|
||||
<translation>Longitud total: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="232"/>
|
||||
<source>Angle: %1, Total length: %2</source>
|
||||
<translation>Ángulo: %1, Longitud total: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="236"/>
|
||||
<source>Length: %1</source>
|
||||
<translation>Longitud: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="239"/>
|
||||
<source>Radius: %1</source>
|
||||
<translation>Radio: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="242"/>
|
||||
<source>Distance: %1</source>
|
||||
<translation>Distancia: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../QuickMeasure.cpp" line="245"/>
|
||||
<source>Minimum distance: %1</source>
|
||||
<translation>Distancia mínima: %1</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
<message>
|
||||
<location filename="../../AppMeasureGui.cpp" line="113"/>
|
||||
<source>Measure</source>
|
||||
<translation>Medición</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>StdCmdMeasure</name>
|
||||
<message>
|
||||
<location filename="../../Command.cpp" line="48"/>
|
||||
<source>&Measure</source>
|
||||
<translation>&Medición</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../Command.cpp" line="49"/>
|
||||
<location filename="../../Command.cpp" line="51"/>
|
||||
<source>Measure a feature</source>
|
||||
<translation>Medir una característica</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
@@ -1,3 +1,6 @@
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
add_definitions(-DBUILD_TRACY_FRAME_PROFILER)
|
||||
endif()
|
||||
|
||||
add_subdirectory(App)
|
||||
if(BUILD_GUI)
|
||||
|
||||
@@ -24,6 +24,10 @@ set(PartGui_LIBS
|
||||
MatGui
|
||||
)
|
||||
|
||||
if(BUILD_TRACY_FRAME_PROFILER)
|
||||
list(APPEND PartGui_LIBS TracyClient)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
include_directories(
|
||||
${CMAKE_SOURCE_DIR}/src/3rdParty/OpenGL/api
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
# include <Inventor/C/glue/gl.h>
|
||||
#endif
|
||||
|
||||
#include <Base/Profiler.h>
|
||||
|
||||
#include <Gui/SoFCInteractiveElement.h>
|
||||
#include <Gui/Selection/SoFCSelectionAction.h>
|
||||
#include <Gui/Selection/SoFCUnifiedSelection.h>
|
||||
@@ -493,6 +495,8 @@ void SoBrepFaceSet::renderColoredArray(SoMaterialBundle *const materials)
|
||||
|
||||
void SoBrepFaceSet::GLRender(SoGLRenderAction *action)
|
||||
{
|
||||
ZoneScoped;
|
||||
|
||||
//SoBase::staticDataLock();
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
|
||||
@@ -33,9 +33,6 @@ using namespace Sketcher;
|
||||
|
||||
//---------- Geometry Extension
|
||||
|
||||
constexpr std::array<const char*, ExternalGeometryExtension::NumFlags>
|
||||
ExternalGeometryExtension::flag2str;
|
||||
|
||||
TYPESYSTEM_SOURCE(Sketcher::ExternalGeometryExtension, Part::GeometryMigrationPersistenceExtension)
|
||||
|
||||
void ExternalGeometryExtension::copyAttributes(Part::GeometryExtension* cpy) const
|
||||
|
||||
@@ -32,10 +32,6 @@
|
||||
using namespace Sketcher;
|
||||
|
||||
//---------- Geometry Extension
|
||||
constexpr std::array<const char*, InternalType::NumInternalGeometryType>
|
||||
SketchGeometryExtension::internaltype2str;
|
||||
constexpr std::array<const char*, GeometryMode::NumGeometryMode>
|
||||
SketchGeometryExtension::geometrymode2str;
|
||||
|
||||
TYPESYSTEM_SOURCE(Sketcher::SketchGeometryExtension, Part::GeometryMigrationPersistenceExtension)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ public:
|
||||
|
||||
private:
|
||||
std::vector<double> weights;
|
||||
double numpoints;
|
||||
std::size_t numpoints;
|
||||
};
|
||||
|
||||
// Weighted Linear Combination
|
||||
|
||||
@@ -97,6 +97,11 @@ directories = [
|
||||
"workingdir": "./src/Mod/Material/Gui",
|
||||
"tsdir": "Resources/translations",
|
||||
},
|
||||
{
|
||||
"tsname": "Measure",
|
||||
"workingdir": "./src/Mod/Measure/",
|
||||
"tsdir": "Gui/Resources/translations",
|
||||
},
|
||||
{
|
||||
"tsname": "Mesh",
|
||||
"workingdir": "./src/Mod/Mesh/",
|
||||
|
||||
Reference in New Issue
Block a user