Merge branch 'FreeCAD:main' into flickering-cursor

This commit is contained in:
Captain
2025-03-15 03:28:34 +05:30
committed by GitHub
28 changed files with 1411 additions and 36 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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}

View File

@@ -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");

View File

@@ -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
View 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

View File

@@ -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;

View File

@@ -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()

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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)

View 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()))

View File

@@ -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

View 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()

View File

@@ -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

View File

@@ -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):

View 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>&amp;Measure</source>
<translation>&amp;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>

View File

@@ -1,3 +1,6 @@
if(BUILD_TRACY_FRAME_PROFILER)
add_definitions(-DBUILD_TRACY_FRAME_PROFILER)
endif()
add_subdirectory(App)
if(BUILD_GUI)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)

View File

@@ -228,7 +228,7 @@ public:
private:
std::vector<double> weights;
double numpoints;
std::size_t numpoints;
};
// Weighted Linear Combination

View File

@@ -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/",