Loading files
0e185510cThis file is pinned to the selected release. Download the original to open it in your design tool.
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "occmodel.h"
#include "../exceptions.h"
#include "../fileio/filepath.h"
#include "../fileio/fileutils.h"
#include "../utils/toolbox.h"
#include "../utils/transform.h"
#include "librepcb_build_env.h"
// clang-format off
#if USE_OPENCASCADE
#include <APIHeaderSection_MakeHeader.hxx>
#include <BRep_Tool.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepBuilderAPI.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepPrimAPI_MakePrism.hxx>
#include <BRepMesh_IncrementalMesh.hxx>
#include <gp_Circ.hxx>
#include <gp_Pnt.hxx>
#include <Message.hxx>
#include <Message_Gravity.hxx>
#include <Message_Messenger.hxx>
#include <Message_Printer.hxx>
#include <Message_SequenceOfPrinters.hxx>
#include <NCollection_List.hxx>
#include <Poly_Triangulation.hxx>
#include <Quantity_Color.hxx>
#include <Standard_Version.hxx>
#if OCC_VERSION_HEX >= 0x070500
#include <Quantity_ColorRGBA.hxx>
#endif
#include <StdFail_NotDone.hxx>
#include <STEPCAFControl_Reader.hxx>
#include <STEPCAFControl_Writer.hxx>
#include <STEPControl_Reader.hxx>
#include <TColgp_Array1OfPnt.hxx>
#include <TCollection_HAsciiString.hxx>
#include <TDataStd_Name.hxx>
#include <TDF_ChildIterator.hxx>
#include <TDF_Label.hxx>
#include <TDF_LabelSequence.hxx>
#include <TDocStd_Document.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS.hxx>
#include <XCAFApp_Application.hxx>
#include <XCAFDoc_ColorTool.hxx>
#include <XCAFDoc_DocumentTool.hxx>
#include <XCAFDoc_ShapeTool.hxx>
#include <memory>
#endif
// clang-format on
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
bool OccModel::sOutputVerbosityConfigured = false;
/*******************************************************************************
* Data
******************************************************************************/
struct OccModel::Data {
#if USE_OPENCASCADE
Handle(TDocStd_Document) doc;
TDF_Label assemblyLabel;
#else
int dummy;
#endif
};
/*******************************************************************************
* Helper Functions
******************************************************************************/
#if USE_OPENCASCADE
static bool tryGetColor(Handle(XCAFDoc_ColorTool) colorTool,
const TopoDS_Shape& shape, Quantity_Color& color) {
return colorTool->GetColor(shape, XCAFDoc_ColorSurf, color) ||
colorTool->GetColor(shape, XCAFDoc_ColorGen, color) ||
colorTool->GetColor(shape, XCAFDoc_ColorCurv, color);
}
static bool tryGetColor(Handle(XCAFDoc_ColorTool) colorTool,
const TDF_Label& label, Quantity_Color& color) {
return colorTool->GetColor(label, XCAFDoc_ColorSurf, color) ||
colorTool->GetColor(label, XCAFDoc_ColorGen, color) ||
colorTool->GetColor(label, XCAFDoc_ColorCurv, color);
}
static bool tryGetColor(Handle(XCAFDoc_ColorTool) colorTool,
const TopoDS_Shape& shape, OccModel::Color& color) {
#if OCC_VERSION_HEX >= 0x070500
Quantity_ColorRGBA rgba;
if (colorTool->GetColor(shape, XCAFDoc_ColorSurf, rgba) ||
colorTool->GetColor(shape, XCAFDoc_ColorGen, rgba) ||
colorTool->GetColor(shape, XCAFDoc_ColorCurv, rgba)) {
const Quantity_Color rgb = rgba.GetRGB();
color = std::make_tuple(rgb.Red(), rgb.Green(), rgb.Blue(), rgba.Alpha());
return true;
}
#endif
Quantity_Color rgb;
if (tryGetColor(colorTool, shape, rgb)) {
color = std::make_tuple(rgb.Red(), rgb.Green(), rgb.Blue(), 1.0);
return true;
}
return false;
}
static bool tryGetColor(Handle(XCAFDoc_ColorTool) colorTool,
const TDF_Label& label, OccModel::Color& color) {
#if OCC_VERSION_HEX >= 0x070500
Quantity_ColorRGBA rgba;
if (colorTool->GetColor(label, XCAFDoc_ColorSurf, rgba) ||
colorTool->GetColor(label, XCAFDoc_ColorGen, rgba) ||
colorTool->GetColor(label, XCAFDoc_ColorCurv, rgba)) {
const Quantity_Color rgb = rgba.GetRGB();
color = std::make_tuple(rgb.Red(), rgb.Green(), rgb.Blue(), rgba.Alpha());
return true;
}
#endif
Quantity_Color rgb;
if (tryGetColor(colorTool, label, rgb)) {
color = std::make_tuple(rgb.Red(), rgb.Green(), rgb.Blue(), 1.0);
return true;
}
return false;
}
static bool tesselateFace(Handle(XCAFDoc_ColorTool) colorTool,
const TopoDS_Face& face, const gp_Trsf& transform,
OccModel::Color color,
QMap<OccModel::Color, QVector<QVector3D>>& result) {
if (face.IsNull()) return false;
const Standard_Real deflectionAngle = 20. * 3.141 / 180.;
const Standard_Real deflection = 0.01;
TopLoc_Location loc;
Handle(Poly_Triangulation) triangulation =
BRep_Tool::Triangulation(face, loc);
if (triangulation.IsNull() ||
(triangulation->Deflection() > (deflection + Precision::Confusion()))) {
BRepMesh_IncrementalMesh(face, deflection, Standard_False, deflectionAngle);
triangulation = BRep_Tool::Triangulation(face, loc);
}
if (triangulation.IsNull()) return false;
tryGetColor(colorTool, face, color);
QVector<QVector3D>& triangles = result[color];
Standard_Integer n1, n2, n3;
for (Standard_Integer i = 1; i < (triangulation->NbTriangles() + 1); ++i) {
triangulation->Triangles().Value(i).Get(n1, n2, n3);
#if OCC_VERSION_HEX >= 0x070600
gp_XYZ p1 = triangulation->Node(n1).XYZ();
gp_XYZ p2 = triangulation->Node(n2).XYZ();
gp_XYZ p3 = triangulation->Node(n3).XYZ();
#else
gp_XYZ p1 = triangulation->Nodes().Value(n1).XYZ();
gp_XYZ p2 = triangulation->Nodes().Value(n2).XYZ();
gp_XYZ p3 = triangulation->Nodes().Value(n3).XYZ();
#endif
transform.Transforms(p1);
transform.Transforms(p2);
transform.Transforms(p3);
triangles.append(QVector3D(p1.X(), p1.Y(), p1.Z()));
triangles.append(QVector3D(p2.X(), p2.Y(), p2.Z()));
triangles.append(QVector3D(p3.X(), p3.Y(), p3.Z()));
}
return true;
}
static void tesselateShell(Handle(XCAFDoc_ColorTool) colorTool,
const TopoDS_Shape& shell, const gp_Trsf& transform,
OccModel::Color color,
QMap<OccModel::Color, QVector<QVector3D>>& result) {
tryGetColor(colorTool, shell, color);
TopoDS_Iterator it;
for (it.Initialize(shell); it.More(); it.Next()) {
const TopoDS_Face& face = TopoDS::Face(it.Value());
tesselateFace(colorTool, face, transform, color, result);
}
}
static void tesselateSolid(Handle(XCAFDoc_ColorTool) colorTool,
const TopoDS_Shape& solid, const gp_Trsf& transform,
OccModel::Color color,
QMap<OccModel::Color, QVector<QVector3D>>& result) {
tryGetColor(colorTool, solid, color);
TopoDS_Iterator it;
for (it.Initialize(solid); it.More(); it.Next()) {
const TopoDS_Shape& subShape = it.Value();
if (subShape.ShapeType() == TopAbs_SHELL) {
tesselateShell(colorTool, TopoDS::Shell(subShape), transform, color,
result);
}
}
}
static void tesselateLabel(Handle(XCAFDoc_ShapeTool) shapeTool,
Handle(XCAFDoc_ColorTool) colorTool,
gp_Trsf transform, OccModel::Color color,
TDF_Label label,
QMap<OccModel::Color, QVector<QVector3D>>& result) {
if (!colorTool->IsVisible(label)) {
return;
}
tryGetColor(colorTool, label, color);
TopoDS_Shape shape;
if (!shapeTool->GetShape(label, shape)) {
return;
}
if (!shape.Location().IsIdentity()) {
transform *= shape.Location().Transformation();
}
if (shapeTool->GetReferredShape(label, label)) {
if (!shapeTool->GetShape(label, shape)) {
return;
}
}
switch (shape.ShapeType()) {
case TopAbs_COMPOUND: {
if (!shapeTool->IsAssembly(label)) {
TopExp_Explorer ex;
tryGetColor(colorTool, shape, color);
for (ex.Init(shape, TopAbs_SOLID); ex.More(); ex.Next()) {
tesselateSolid(colorTool, ex.Current(), transform, color, result);
}
for (ex.Init(shape, TopAbs_SHELL, TopAbs_SOLID); ex.More(); ex.Next()) {
tesselateShell(colorTool, ex.Current(), transform, color, result);
}
for (ex.Init(shape, TopAbs_FACE, TopAbs_SHELL); ex.More(); ex.Next()) {
const TopoDS_Face& face = TopoDS::Face(ex.Current());
tesselateFace(colorTool, face, transform, color, result);
}
}
break;
}
case TopAbs_SOLID: {
tesselateSolid(colorTool, shape, transform, color, result);
break;
}
case TopAbs_SHELL: {
tesselateShell(colorTool, shape, transform, color, result);
break;
}
case TopAbs_FACE: {
tesselateFace(colorTool, TopoDS::Face(shape), transform, color, result);
break;
}
default:
break;
}
if ((!shapeTool->IsSimpleShape(label)) && label.HasChild()) {
tryGetColor(colorTool, shape, color);
TDF_ChildIterator it;
for (it.Initialize(label); it.More(); it.Next()) {
tesselateLabel(shapeTool, colorTool, transform, color, it.Value(),
result);
}
}
}
static gp_Pnt convertPoint(const Point& xy, const Length& z) {
return gp_Pnt(xy.getX().toMm(), xy.getY().toMm(), z.toMm());
}
static TopoDS_Face pathToFace(const Path& path, const Length& z) {
BRepBuilderAPI_MakeWire wire;
for (int i = 1; i < path.getVertices().count(); ++i) {
const Vertex& v0 = path.getVertices().at(i - 1);
const Vertex& v1 = path.getVertices().at(i);
if (auto center =
Toolbox::arcCenter(v0.getPos(), v1.getPos(), v0.getAngle())) {
// Arc segment.
const UnsignedLength radiusAbs = (v0.getPos() - (*center)).getLength();
gp_Circ arc(
gp_Ax2(gp_Pnt(center->getX().toMm(), center->getY().toMm(), z.toMm()),
gp_Dir(0.0, 0.0, (v0.getAngle() < 0) ? -1.0 : 1.0)),
radiusAbs->toMm());
try {
wire.Add(BRepBuilderAPI_MakeEdge(arc, convertPoint(v0.getPos(), z),
convertPoint(v1.getPos(), z)));
} catch (const StdFail_NotDone&) {
// OpenCascade did not accept the arc due to precision errors. As
// a workaround, we approximate it with straight line segments.
qWarning() << "Failed to export arc to STEP, approximating with "
"straight lines instead.";
const Path p = Path::flatArc(v0.getPos(), v1.getPos(), v0.getAngle(),
PositiveLength(10000));
for (int k = 1; k < p.getVertices().count(); ++k) {
wire.Add(BRepBuilderAPI_MakeEdge(
convertPoint(p.getVertices().at(k - 1).getPos(), z),
convertPoint(p.getVertices().at(k).getPos(), z)));
}
}
} else {
// Straight segment.
wire.Add(BRepBuilderAPI_MakeEdge(convertPoint(v0.getPos(), z),
convertPoint(v1.getPos(), z)));
}
}
return BRepBuilderAPI_MakeFace(wire);
}
#endif
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
OccModel::OccModel(std::unique_ptr<Data> data) : mImpl(std::move(data)) {
Q_ASSERT(mImpl);
}
OccModel::~OccModel() noexcept {
}
/*******************************************************************************
* General Methods
******************************************************************************/
void OccModel::addToAssembly(const OccModel& model, const Point3D& pos,
const Angle3D& rot, const Transform& transform,
const QString& name) {
#if USE_OPENCASCADE
try {
Handle(XCAFDoc_ShapeTool) assemblyShapeTool =
XCAFDoc_DocumentTool::ShapeTool(mImpl->doc->Main());
Handle(XCAFDoc_ShapeTool) modelShapeTool =
XCAFDoc_DocumentTool::ShapeTool(model.mImpl->doc->Main());
Handle(XCAFDoc_ColorTool) assemblyColorTool =
XCAFDoc_DocumentTool::ColorTool(model.mImpl->doc->Main());
Handle(XCAFDoc_ColorTool) modelColorTool =
XCAFDoc_DocumentTool::ColorTool(mImpl->doc->Main());
TopExp_Explorer assemblyExplorer;
TopExp_Explorer modelExplorer;
TDF_Label newLabel = assemblyShapeTool->NewShape();
TCollection_ExtendedString newName(cleanString(name).toStdString().c_str());
TDataStd_Name::Set(newLabel, newName);
TDF_LabelSequence modelShapes;
modelShapeTool->GetFreeShapes(modelShapes);
for (int i = 1; i <= modelShapes.Length(); ++i) {
TopoDS_Shape shape = modelShapeTool->GetShape(modelShapes.Value(i));
if (shape.IsNull()) continue;
TDF_Label shapeLabel = assemblyShapeTool->AddShape(shape, Standard_False);
const QString shapeName = QString("%1:%2").arg(cleanString(name)).arg(i);
TDataStd_Name::Set(shapeLabel, shapeName.toStdString().c_str());
// ATTENTION: Until LibrePCB 1.1.0 we passed shape.Location() instead of
// TopLoc_Location(), but this caused wrong placement in rare cases.
// Although TopLoc_Location() sounds wrong(?), it fixes the issue without
// observing any negative consequences so far. If any issues are observed
// in future, we might have to reconsider this change.
// See details in https://github.com/LibrePCB/LibrePCB/issues/1387.
TDF_Label cmpLabel = assemblyShapeTool->AddComponent(newLabel, shapeLabel,
TopLoc_Location());
// Copy face colors.
modelExplorer.Init(shape, TopAbs_FACE);
assemblyExplorer.Init(assemblyShapeTool->GetShape(cmpLabel), TopAbs_FACE);
while (modelExplorer.More() && assemblyExplorer.More()) {
Quantity_Color color;
TDF_Label label;
if (modelShapeTool->FindShape(modelExplorer.Current(), label)) {
if (tryGetColor(assemblyColorTool, label, color)) {
modelColorTool->SetColor(assemblyExplorer.Current(), color,
XCAFDoc_ColorSurf);
}
} else if (tryGetColor(assemblyColorTool, modelExplorer.Current(),
color)) {
modelColorTool->SetColor(assemblyExplorer.Current(), color,
XCAFDoc_ColorSurf);
}
modelExplorer.Next();
assemblyExplorer.Next();
}
// Copy solid colors.
modelExplorer.Init(shape, TopAbs_SOLID, TopAbs_FACE);
assemblyExplorer.Init(assemblyShapeTool->GetShape(cmpLabel), TopAbs_SOLID,
TopAbs_FACE);
while (modelExplorer.More() && assemblyExplorer.More()) {
Quantity_Color color;
TDF_Label label;
if (modelShapeTool->FindShape(modelExplorer.Current(), label)) {
if (tryGetColor(assemblyColorTool, label, color)) {
modelColorTool->SetColor(assemblyExplorer.Current(), color,
XCAFDoc_ColorSurf);
}
} else if (tryGetColor(assemblyColorTool, modelExplorer.Current(),
color)) {
modelColorTool->SetColor(assemblyExplorer.Current(), color,
XCAFDoc_ColorSurf);
}
modelExplorer.Next();
assemblyExplorer.Next();
}
}
gp_Trsf t, tTmp;
t.SetTranslation(gp_Vec(transform.getPosition().getX().toMm(),
transform.getPosition().getY().toMm(), 0));
tTmp.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)),
transform.getRotation().toRad());
t *= tTmp;
if (transform.getMirrored()) {
tTmp.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)),
Angle::deg180().toRad());
t *= tTmp;
}
tTmp.SetTranslation(gp_Vec(std::get<0>(pos).toMm(), std::get<1>(pos).toMm(),
std::get<2>(pos).toMm()));
t *= tTmp;
tTmp.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)),
std::get<2>(rot).toRad());
t *= tTmp;
tTmp.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)),
std::get<1>(rot).toRad());
t *= tTmp;
tTmp.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)),
std::get<0>(rot).toRad());
t *= tTmp;
assemblyShapeTool->AddComponent(mImpl->assemblyLabel, newLabel,
TopLoc_Location(t));
#if OCC_VERSION_HEX >= 0x070200
assemblyShapeTool->UpdateAssemblies();
#endif
} catch (const Standard_Failure& e) {
qCritical() << "OpenCascade error:" << e.GetMessageString();
throw RuntimeError(
__FILE__, __LINE__,
QString("STEP assembly failed: %1").arg(e.GetMessageString()));
}
#else
Q_UNUSED(model);
Q_UNUSED(pos);
Q_UNUSED(rot);
Q_UNUSED(transform);
Q_UNUSED(name);
#endif
}
void OccModel::saveAsStep(const QString& name, const FilePath& fp) const {
#if USE_OPENCASCADE
try {
STEPCAFControl_Writer writer;
writer.SetColorMode(Standard_True);
writer.SetNameMode(Standard_True);
APIHeaderSection_MakeHeader hdr(writer.ChangeWriter().Model());
hdr.SetName(
new TCollection_HAsciiString(cleanString(name).toStdString().c_str()));
hdr.SetAuthorValue(1, new TCollection_HAsciiString(""));
hdr.SetOrganizationValue(1, new TCollection_HAsciiString(""));
hdr.SetOriginatingSystem(new TCollection_HAsciiString("LibrePCB"));
hdr.SetDescriptionValue(1, new TCollection_HAsciiString("PCB Assembly"));
FileUtils::makePath(fp.getParentDir());
if (writer.Perform(mImpl->doc, qPrintable(fp.toStr())) != Standard_True) {
throw RuntimeError(__FILE__, __LINE__, tr("Failed to write STEP file."));
}
} catch (const Standard_Failure& e) {
qCritical() << "OpenCascade error:" << e.GetMessageString();
throw RuntimeError(
__FILE__, __LINE__,
QString("STEP export failed: %1").arg(e.GetMessageString()));
}
#else
Q_UNUSED(name);
Q_UNUSED(fp);
throwNotAvailable();
#endif
}
QMap<OccModel::Color, QVector<QVector3D>> OccModel::tesselate() const {
QMap<OccModel::Color, QVector<QVector3D>> result;
#if USE_OPENCASCADE
try {
Handle(XCAFDoc_ShapeTool) shapeTool =
XCAFDoc_DocumentTool::ShapeTool(mImpl->doc->Main());
Handle(XCAFDoc_ColorTool) colorTool =
XCAFDoc_DocumentTool::ColorTool(mImpl->doc->Main());
TDF_LabelSequence labels;
shapeTool->GetFreeShapes(labels);
for (Standard_Integer i = 1; i <= labels.Length(); ++i) {
tesselateLabel(shapeTool, colorTool, gp_Trsf(),
std::make_tuple(0.5, 0.5, 0.5, 1.0), labels.Value(i),
result);
}
} catch (const Standard_Failure& e) {
qCritical() << "OpenCascade error:" << e.GetMessageString();
throw RuntimeError(
__FILE__, __LINE__,
QString("STEP tesselation failed: %1").arg(e.GetMessageString()));
}
#endif
return result;
}
/*******************************************************************************
* Static Methods
******************************************************************************/
bool OccModel::isAvailable() noexcept {
return (USE_OPENCASCADE != 0);
}
QString OccModel::getOccVersionString() noexcept {
QString s = OCC_EDITION_NAME;
#if USE_OPENCASCADE
s += " ";
s += OCC_VERSION_COMPLETE;
#endif
return s;
}
void OccModel::setVerboseOutput(bool verbose) noexcept {
#if USE_OPENCASCADE
const Message_SequenceOfPrinters& printers =
Message::DefaultMessenger()->Printers();
for (int i = 1; i <= printers.Length(); ++i) {
printers.Value(i)->SetTraceLevel(verbose ? Message_Trace : Message_Alarm);
}
#else
Q_UNUSED(verbose);
#endif
sOutputVerbosityConfigured = true;
}
std::unique_ptr<OccModel> OccModel::createAssembly(const QString& name) {
std::unique_ptr<OccModel> result;
#if USE_OPENCASCADE
try {
initOpenCascade();
Handle(XCAFApp_Application) app = XCAFApp_Application::GetApplication();
Handle(TDocStd_Document) doc;
app->NewDocument("MDTV-XCAF", doc);
Handle(XCAFDoc_ShapeTool) shapeTool =
XCAFDoc_DocumentTool::ShapeTool(doc->Main());
TDF_Label label = shapeTool->NewShape();
TCollection_ExtendedString shapeName(
cleanString(name).toStdString().c_str());
TDataStd_Name::Set(label, shapeName);
result.reset(new OccModel(std::make_unique<Data>(Data{doc, label})));
} catch (const Standard_Failure& e) {
qCritical() << "OpenCascade error:" << e.GetMessageString();
throw RuntimeError(
__FILE__, __LINE__,
QString("Failed to create 3D assembly: %1").arg(e.GetMessageString()));
}
#else
Q_UNUSED(name);
throwNotAvailable();
#endif
return result;
}
std::unique_ptr<OccModel> OccModel::createBoard(const librepcb::Path& outline,
const QVector<Path>& holes,
const PositiveLength& thickness,
const QColor& color) {
std::unique_ptr<OccModel> result;
#if USE_OPENCASCADE
try {
initOpenCascade();
Handle(XCAFApp_Application) app = XCAFApp_Application::GetApplication();
Handle(TDocStd_Document) doc;
app->NewDocument("MDTV-XCAF", doc);
Handle(XCAFDoc_ShapeTool) shapeTool =
XCAFDoc_DocumentTool::ShapeTool(doc->Main());
TopoDS_Shape face;
if (holes.isEmpty()) {
// Cutting fails if there are no holes.
face = pathToFace(outline.cleaned(), Length(0));
} else {
NCollection_List<TopoDS_Shape> boardFaces;
boardFaces.Append(pathToFace(outline.cleaned(), Length(0)));
NCollection_List<TopoDS_Shape> holeFaces;
foreach (const Path& hole, holes) {
holeFaces.Append(pathToFace({hole.cleaned()}, Length(0)));
}
BRepAlgoAPI_Cut cutter;
cutter.SetArguments(boardFaces);
cutter.SetTools(holeFaces);
cutter.SetRunParallel(Standard_True);
cutter.Build();
face = cutter.Shape();
}
if (face.IsNull()) {
// Handle error to avoid segfault in code below.
throw LogicError(__FILE__, __LINE__, "OCC failed to build board shape.");
}
TopoDS_Shape shape =
BRepPrimAPI_MakePrism(face, gp_Vec(0, 0, thickness->toMm()));
TDF_Label label = shapeTool->AddShape(shape, false);
if (!label.IsNull()) {
Handle(XCAFDoc_ColorTool) colorTool =
XCAFDoc_DocumentTool::ColorTool(doc->Main());
#if OCC_VERSION_HEX >= 0x070500
Quantity_ColorRGBA shapeColor(
Quantity_Color(color.redF(), color.greenF(), color.blueF(),
Quantity_TOC_RGB),
color.alphaF());
#else
Quantity_Color shapeColor(color.redF(), color.greenF(), color.blueF(),
Quantity_TOC_RGB);
#endif
colorTool->SetColor(label, shapeColor, XCAFDoc_ColorSurf);
TopExp_Explorer explorer;
explorer.Init(shape, TopAbs_SOLID);
while (explorer.More()) {
colorTool->SetColor(explorer.Current(), shapeColor, XCAFDoc_ColorSurf);
explorer.Next();
}
} else {
qWarning() << "Failed to apply color to PCB 3D model.";
}
#if OCC_VERSION_HEX >= 0x070200
shapeTool->UpdateAssemblies();
#endif
result.reset(new OccModel(std::make_unique<Data>(Data{doc, TDF_Label()})));
} catch (const Standard_Failure& e) {
qCritical() << "OpenCascade error:" << e.GetMessageString();
throw RuntimeError(__FILE__, __LINE__,
QString("Failed to create board 3D model: %1")
.arg(e.GetMessageString()));
}
#else
Q_UNUSED(outline);
Q_UNUSED(holes);
Q_UNUSED(thickness);
Q_UNUSED(color);
throwNotAvailable();
#endif
return result;
}
std::unique_ptr<OccModel> OccModel::loadStep(const QByteArray content) {
std::unique_ptr<OccModel> result;
#if USE_OPENCASCADE
try {
initOpenCascade();
Handle(XCAFApp_Application) app = XCAFApp_Application::GetApplication();
Handle(TDocStd_Document) doc;
app->NewDocument("MDTV-XCAF", doc);
STEPCAFControl_Reader stepReader;
stepReader.SetColorMode(Standard_True);
stepReader.SetNameMode(Standard_False);
stepReader.SetLayerMode(Standard_False);
STEPControl_Reader& reader = stepReader.ChangeReader();
#if OCC_VERSION_HEX >= 0x070500
std::istringstream is(content.data());
const IFSelect_ReturnStatus ret = reader.ReadStream("stream.step", is);
#else
FilePath tmp = FilePath::getRandomTempPath();
FileUtils::writeFile(tmp, content); // can throw
const IFSelect_ReturnStatus ret = reader.ReadFile(qPrintable(tmp.toStr()));
FileUtils::removeFile(tmp); // can throw
#endif
if (ret != IFSelect_RetDone) {
throw RuntimeError(__FILE__, __LINE__, tr("Failed to read STEP file!"));
}
if (!stepReader.Transfer(doc)) {
doc->Close();
throw RuntimeError(__FILE__, __LINE__, "Failed to transfer STEP model.");
}
result.reset(new OccModel(std::make_unique<Data>(Data{doc, TDF_Label()})));
} catch (const Standard_Failure& e) {
qCritical() << "OpenCascade error:" << e.GetMessageString();
throw RuntimeError(
__FILE__, __LINE__,
QString("Failed to read STEP file: %1").arg(e.GetMessageString()));
}
#else
Q_UNUSED(content);
throwNotAvailable();
#endif
return result;
}
QByteArray OccModel::minifyStep(const QByteArray& content) {
QElapsedTimer timer;
timer.start();
// Split linhes and clean whitespaces.
QStringList lines;
foreach (QString line, content.split('\n')) {
const QString trimmed = line.trimmed();
if (!trimmed.startsWith('*')) {
line = trimmed;
}
line.replace("\r", "");
while (line.endsWith(" ;")) {
line.chop(2);
line.append(';');
}
if (!line.isEmpty()) {
lines.append(line);
}
}
// Split header, data and footer.
const int dataStart = lines.indexOf("DATA;");
if (dataStart < 0) {
throw RuntimeError(__FILE__, __LINE__, "STEP data section not found.");
}
const int dataEnd = lines.indexOf("ENDSEC;", dataStart + 1);
if (dataEnd < 0) {
throw RuntimeError(__FILE__, __LINE__, "STEP data section end not found.");
}
const QStringList headerLines = lines.mid(0, dataStart + 1);
const QStringList footerLines = lines.mid(dataEnd);
// Unwrap multi-line data items and replace "-0." by "0." to allow
// eliminating more duplicates.
QRegularExpression re("\\-0\\.([^0-9])");
const QStringList dataLines =
lines.mid(dataStart + 1, dataEnd - dataStart - 1)
.join("")
.replace(re, "0.\\1")
.split(';', Qt::SkipEmptyParts);
// Parse data into key-value structure.
// Note: The last item of the QList<int> is not part of the data, but only
// used internally to make particular entries unique.
typedef std::pair<QStringList, QList<int>> Value;
QMap<int, Value> data;
re = QRegularExpression("#([0-9]+)");
foreach (const QString& line, dataLines) {
const int equalPos = line.indexOf("=");
bool ok = false;
const int id = line.mid(1, equalPos - 1).trimmed().toInt(&ok);
if (!ok) {
throw RuntimeError(__FILE__, __LINE__,
"Failed to parse data section of STEP file.");
}
const QString valueStr = line.mid(equalPos + 1).trimmed();
Value value;
int consumed = 0;
auto it = re.globalMatch(valueStr);
while (it.hasNext()) {
auto match = it.next();
value.first.append(
valueStr.mid(consumed, match.capturedStart(1) - consumed));
value.second.append(match.captured(1).toInt());
consumed = match.capturedEnd(1);
}
value.first.append(valueStr.mid(consumed));
// Important: It seems some entries must not be merged even if they are
// identical. When merged, the STEP model won't be rendered anymore
// and FreeCAD displays a wrong shape object tree. We add a unique
// number to these entries to ensure they are left untouched.
// See also https://github.com/LibrePCB/LibrePCB/issues/1286.
if (valueStr.contains("PRODUCT_DEFINITION") ||
valueStr.contains("REPRESENTATION")) {
value.second.append(data.count() + 1);
} else {
value.second.append(0);
}
data.insert(id, value);
}
// Eliminate duplicate data items.
QHash<Value, int> uniqueData;
QHash<int, int> idMap;
while (true) {
uniqueData.clear();
idMap.clear();
for (auto it = data.begin(); it != data.end(); it++) {
auto newIt = uniqueData.find(it.value());
if (newIt == uniqueData.end()) {
newIt = uniqueData.insert(it.value(), uniqueData.count() + 1);
}
idMap.insert(it.key(), newIt.value());
}
if (uniqueData.count() == data.count()) {
break;
}
data.clear();
for (auto it = uniqueData.begin(); it != uniqueData.end(); it++) {
data.insert(it.value(), it.key());
}
for (auto& value : data) {
for (auto& id : value.second) {
id = idMap.value(id, id);
}
}
}
// Build new STEP file.
QByteArray output;
output += headerLines.join('\n').toUtf8() + '\n';
for (auto it = data.begin(); it != data.end(); it++) {
output += QString("#%1=").arg(it.key()).toUtf8();
for (int i = 0; i < it.value().first.count(); ++i) {
output += it.value().first.at(i).toUtf8();
if (i < (it.value().second.count() - 1)) { // Ignore unique ID.
output += QString::number(it.value().second.at(i)).toUtf8();
}
}
output += ";\n";
}
output += footerLines.join('\n').toUtf8() + '\n';
qDebug() << "Minified STEP file from" << (content.size() / 1024.0) << "kB to"
<< (output.size() / 1024.0) << "kB in" << timer.elapsed() << "ms.";
return output;
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void OccModel::initOpenCascade() {
#if USE_OPENCASCADE
auto initOnce = []() {
// Make console output less verbose.
if (!sOutputVerbosityConfigured) {
setVerboseOutput(false);
}
// Apply global settings.
XCAFDoc_ShapeTool::SetAutoNaming(false);
// Initially we used 1e-6 (same as Horizon EDA) but it caused STEP export
// errors in rare cases, due to arc vertices not within the precision.
// Thus now allowing a larger tolerance.
BRepBuilderAPI::Precision(5.0e-6);
return true;
};
static const bool done = initOnce();
Q_UNUSED(done);
#endif
}
QString OccModel::cleanString(const QString& str) {
const QString validChars("-a-zA-Z0-9_+/!?<>(){}.|&@# :");
return str.normalized(QString::NormalizationForm_KD)
.remove(QRegularExpression(QString("[^%1]").arg(validChars)));
}
void OccModel::throwNotAvailable() {
throw LogicError(__FILE__, __LINE__,
"Attempted to work with STEP file, but LibrePCB was "
"compiled without OpenCascade.");
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace librepcb