- Ifc: refactor code, move opening generation and boolean clipping code to separate units.
parent
2359a83132
commit
f7680f7f28
|
@ -381,6 +381,8 @@ SET(IFC_SRCS
|
|||
IFCMaterial.cpp
|
||||
IFCProfile.cpp
|
||||
IFCCurve.cpp
|
||||
IFCBoolean.cpp
|
||||
IFCOpenings.cpp
|
||||
STEPFile.h
|
||||
STEPFileReader.h
|
||||
STEPFileReader.cpp
|
||||
|
|
1939
code/IFCGeometry.cpp
1939
code/IFCGeometry.cpp
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
124
code/IFCUtil.h
124
code/IFCUtil.h
|
@ -61,7 +61,9 @@ namespace IFC {
|
|||
typedef aiColor4t<IfcFloat> IfcColor4;
|
||||
|
||||
|
||||
// helper for std::for_each to delete all heap-allocated items in a container
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Helper for std::for_each to delete all heap-allocated items in a container
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
template<typename T>
|
||||
struct delete_fun
|
||||
{
|
||||
|
@ -70,10 +72,43 @@ struct delete_fun
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Helper used during mesh construction. Aids at creating aiMesh'es out of relatively few polygons.
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
struct TempMesh
|
||||
{
|
||||
std::vector<IfcVector3> verts;
|
||||
std::vector<unsigned int> vertcnt;
|
||||
|
||||
// utilities
|
||||
aiMesh* ToMesh();
|
||||
void Clear();
|
||||
void Transform(const IfcMatrix4& mat);
|
||||
IfcVector3 Center() const;
|
||||
void Append(const TempMesh& other);
|
||||
|
||||
bool IsEmpty() const {
|
||||
return verts.empty() && vertcnt.empty();
|
||||
}
|
||||
|
||||
void RemoveAdjacentDuplicates();
|
||||
void RemoveDegenerates();
|
||||
|
||||
void FixupFaceOrientation();
|
||||
IfcVector3 ComputeLastPolygonNormal(bool normalize = true) const;
|
||||
void ComputePolygonNormals(std::vector<IfcVector3>& normals,
|
||||
bool normalize = true,
|
||||
size_t ofs = 0) const;
|
||||
|
||||
void Swap(TempMesh& other);
|
||||
};
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Temporary representation of an opening in a wall or a floor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
struct TempMesh;
|
||||
struct TempOpening
|
||||
{
|
||||
const IFC::IfcSolidModel* solid;
|
||||
|
@ -110,6 +145,21 @@ struct TempOpening
|
|||
|
||||
// ------------------------------------------------------------------------------
|
||||
void Transform(const IfcMatrix4& mat); // defined later since TempMesh is not complete yet
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helper to sort openings by distance from a given base point
|
||||
struct DistanceSorter {
|
||||
|
||||
DistanceSorter(const IfcVector3& base) : base(base) {}
|
||||
|
||||
bool operator () (const TempOpening& a, const TempOpening& b) const {
|
||||
return (a.profileMesh->Center()-base).SquareLength() < (b.profileMesh->Center()-base).SquareLength();
|
||||
}
|
||||
|
||||
IfcVector3 base;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
@ -160,6 +210,7 @@ struct ConversionData
|
|||
std::vector<TempOpening>* collect_openings;
|
||||
};
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Binary predicate to compare vectors with a given, quadratic epsilon.
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
@ -175,40 +226,21 @@ struct FuzzyVectorCompare {
|
|||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Helper used during mesh construction. Aids at creating aiMesh'es out of relatively few polygons.
|
||||
// Ordering predicate to totally order R^2 vectors first by x and then by y
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
struct TempMesh
|
||||
{
|
||||
std::vector<IfcVector3> verts;
|
||||
std::vector<unsigned int> vertcnt;
|
||||
struct XYSorter {
|
||||
|
||||
// utilities
|
||||
aiMesh* ToMesh();
|
||||
void Clear();
|
||||
void Transform(const IfcMatrix4& mat);
|
||||
IfcVector3 Center() const;
|
||||
void Append(const TempMesh& other);
|
||||
|
||||
bool IsEmpty() const {
|
||||
return verts.empty() && vertcnt.empty();
|
||||
// sort first by X coordinates, then by Y coordinates
|
||||
bool operator () (const IfcVector2&a, const IfcVector2& b) const {
|
||||
if (a.x == b.x) {
|
||||
return a.y < b.y;
|
||||
}
|
||||
return a.x < b.x;
|
||||
}
|
||||
|
||||
void RemoveAdjacentDuplicates();
|
||||
void RemoveDegenerates();
|
||||
|
||||
void FixupFaceOrientation();
|
||||
IfcVector3 ComputeLastPolygonNormal(bool normalize = true) const;
|
||||
void ComputePolygonNormals(std::vector<IfcVector3>& normals,
|
||||
bool normalize = true,
|
||||
size_t ofs = 0) const;
|
||||
|
||||
void Swap(TempMesh& other);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// conversion routines for common IFC entities, implemented in IFCUtil.cpp
|
||||
void ConvertColor(aiColor4D& out, const IfcColourRgb& in);
|
||||
void ConvertColor(aiColor4D& out, const IfcColourOrFactor& in,ConversionData& conv,const aiColor4D* base);
|
||||
|
@ -232,9 +264,40 @@ bool ProcessProfile(const IfcProfileDef& prof, TempMesh& meshout, ConversionData
|
|||
unsigned int ProcessMaterials(const IFC::IfcRepresentationItem& item, ConversionData& conv);
|
||||
|
||||
// IFCGeometry.cpp
|
||||
IfcMatrix3 DerivePlaneCoordinateSpace(const TempMesh& curmesh, bool& ok, IfcVector3& norOut);
|
||||
bool ProcessRepresentationItem(const IfcRepresentationItem& item, std::vector<unsigned int>& mesh_indices, ConversionData& conv);
|
||||
void AssignAddedMeshes(std::vector<unsigned int>& mesh_indices,aiNode* nd,ConversionData& /*conv*/);
|
||||
|
||||
void ProcessSweptAreaSolid(const IfcSweptAreaSolid& swept, TempMesh& meshout,
|
||||
ConversionData& conv);
|
||||
|
||||
void ProcessExtrudedAreaSolid(const IfcExtrudedAreaSolid& solid, TempMesh& result,
|
||||
ConversionData& conv, bool collect_openings);
|
||||
|
||||
// IFCBoolean.cpp
|
||||
|
||||
void ProcessBoolean(const IfcBooleanResult& boolean, TempMesh& result, ConversionData& conv);
|
||||
void ProcessBooleanHalfSpaceDifference(const IfcHalfSpaceSolid* hs, TempMesh& result,
|
||||
const TempMesh& first_operand,
|
||||
ConversionData& conv);
|
||||
|
||||
void ProcessPolygonalBoundedBooleanHalfSpaceDifference(const IfcPolygonalBoundedHalfSpace* hs, TempMesh& result,
|
||||
const TempMesh& first_operand,
|
||||
ConversionData& conv);
|
||||
void ProcessBooleanExtrudedAreaSolidDifference(const IfcExtrudedAreaSolid* as, TempMesh& result,
|
||||
const TempMesh& first_operand,
|
||||
ConversionData& conv);
|
||||
|
||||
|
||||
// IFCOpenings.cpp
|
||||
|
||||
bool GenerateOpenings(std::vector<TempOpening>& openings,
|
||||
const std::vector<IfcVector3>& nors,
|
||||
TempMesh& curmesh,
|
||||
bool check_intersection,
|
||||
bool generate_connection_geometry,
|
||||
const IfcVector3& wall_extrusion_axis = IfcVector3(0,1,0));
|
||||
|
||||
|
||||
// IFCCurve.cpp
|
||||
|
||||
|
@ -338,7 +401,8 @@ public:
|
|||
using Curve::SampleDiscrete;
|
||||
};
|
||||
|
||||
|
||||
// IfcProfile.cpp
|
||||
bool ProcessCurve(const IfcCurve& curve, TempMesh& meshout, ConversionData& conv);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
Open Asset Import Library (assimp)
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Copyright (c) 2006-2010, assimp team
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use of this software in source and binary forms,
|
||||
with or without modification, are permitted provided that the
|
||||
following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above
|
||||
copyright notice, this list of conditions and the
|
||||
following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the
|
||||
following disclaimer in the documentation and/or other
|
||||
materials provided with the distribution.
|
||||
|
||||
* Neither the name of the assimp team, nor the names of its
|
||||
contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior
|
||||
written permission of the assimp team.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/** @file IFCBoolean.cpp
|
||||
* @brief Implements a subset of Ifc boolean operations
|
||||
*/
|
||||
|
||||
#include "AssimpPCH.h"
|
||||
|
||||
#ifndef ASSIMP_BUILD_NO_IFC_IMPORTER
|
||||
#include "IFCUtil.h"
|
||||
#include "PolyTools.h"
|
||||
#include "ProcessHelper.h"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
namespace Assimp {
|
||||
namespace IFC {
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
enum Intersect {
|
||||
Intersect_No,
|
||||
Intersect_LiesOnPlane,
|
||||
Intersect_Yes
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
Intersect IntersectSegmentPlane(const IfcVector3& p,const IfcVector3& n, const IfcVector3& e0,
|
||||
const IfcVector3& e1,
|
||||
IfcVector3& out)
|
||||
{
|
||||
const IfcVector3 pdelta = e0 - p, seg = e1-e0;
|
||||
const IfcFloat dotOne = n*seg, dotTwo = -(n*pdelta);
|
||||
|
||||
if (fabs(dotOne) < 1e-6) {
|
||||
return fabs(dotTwo) < 1e-6f ? Intersect_LiesOnPlane : Intersect_No;
|
||||
}
|
||||
|
||||
const IfcFloat t = dotTwo/dotOne;
|
||||
// t must be in [0..1] if the intersection point is within the given segment
|
||||
if (t > 1.f || t < 0.f) {
|
||||
return Intersect_No;
|
||||
}
|
||||
out = e0+t*seg;
|
||||
return Intersect_Yes;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
void ProcessBooleanHalfSpaceDifference(const IfcHalfSpaceSolid* hs, TempMesh& result,
|
||||
const TempMesh& first_operand,
|
||||
ConversionData& conv)
|
||||
{
|
||||
ai_assert(hs != NULL);
|
||||
|
||||
const IfcPlane* const plane = hs->BaseSurface->ToPtr<IfcPlane>();
|
||||
if(!plane) {
|
||||
IFCImporter::LogError("expected IfcPlane as base surface for the IfcHalfSpaceSolid");
|
||||
return;
|
||||
}
|
||||
|
||||
// extract plane base position vector and normal vector
|
||||
IfcVector3 p,n(0.f,0.f,1.f);
|
||||
if (plane->Position->Axis) {
|
||||
ConvertDirection(n,plane->Position->Axis.Get());
|
||||
}
|
||||
ConvertCartesianPoint(p,plane->Position->Location);
|
||||
|
||||
if(!IsTrue(hs->AgreementFlag)) {
|
||||
n *= -1.f;
|
||||
}
|
||||
|
||||
// clip the current contents of `meshout` against the plane we obtained from the second operand
|
||||
const std::vector<IfcVector3>& in = first_operand.verts;
|
||||
std::vector<IfcVector3>& outvert = result.verts;
|
||||
|
||||
std::vector<unsigned int>::const_iterator begin = first_operand.vertcnt.begin(),
|
||||
end = first_operand.vertcnt.end(), iit;
|
||||
|
||||
outvert.reserve(in.size());
|
||||
result.vertcnt.reserve(first_operand.vertcnt.size());
|
||||
|
||||
unsigned int vidx = 0;
|
||||
for(iit = begin; iit != end; vidx += *iit++) {
|
||||
|
||||
unsigned int newcount = 0;
|
||||
for(unsigned int i = 0; i < *iit; ++i) {
|
||||
const IfcVector3& e0 = in[vidx+i], e1 = in[vidx+(i+1)%*iit];
|
||||
|
||||
// does the next segment intersect the plane?
|
||||
IfcVector3 isectpos;
|
||||
const Intersect isect = IntersectSegmentPlane(p,n,e0,e1,isectpos);
|
||||
if (isect == Intersect_No || isect == Intersect_LiesOnPlane) {
|
||||
if ( (e0-p).Normalize()*n > 0 ) {
|
||||
outvert.push_back(e0);
|
||||
++newcount;
|
||||
}
|
||||
}
|
||||
else if (isect == Intersect_Yes) {
|
||||
if ( (e0-p).Normalize()*n > 0 ) {
|
||||
// e0 is on the right side, so keep it
|
||||
outvert.push_back(e0);
|
||||
outvert.push_back(isectpos);
|
||||
newcount += 2;
|
||||
}
|
||||
else {
|
||||
// e0 is on the wrong side, so drop it and keep e1 instead
|
||||
outvert.push_back(isectpos);
|
||||
++newcount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!newcount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IfcVector3 vmin,vmax;
|
||||
ArrayBounds(&*(outvert.end()-newcount),newcount,vmin,vmax);
|
||||
|
||||
// filter our IfcFloat points - those may happen if a point lies
|
||||
// directly on the intersection line. However, due to IfcFloat
|
||||
// precision a bitwise comparison is not feasible to detect
|
||||
// this case.
|
||||
const IfcFloat epsilon = (vmax-vmin).SquareLength() / 1e6f;
|
||||
FuzzyVectorCompare fz(epsilon);
|
||||
|
||||
std::vector<IfcVector3>::iterator e = std::unique( outvert.end()-newcount, outvert.end(), fz );
|
||||
|
||||
if (e != outvert.end()) {
|
||||
newcount -= static_cast<unsigned int>(std::distance(e,outvert.end()));
|
||||
outvert.erase(e,outvert.end());
|
||||
}
|
||||
if (fz(*( outvert.end()-newcount),outvert.back())) {
|
||||
outvert.pop_back();
|
||||
--newcount;
|
||||
}
|
||||
if(newcount > 2) {
|
||||
result.vertcnt.push_back(newcount);
|
||||
}
|
||||
else while(newcount-->0) {
|
||||
result.verts.pop_back();
|
||||
}
|
||||
|
||||
}
|
||||
IFCImporter::LogDebug("generating CSG geometry by plane clipping (IfcBooleanClippingResult)");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
void ProcessPolygonalBoundedBooleanHalfSpaceDifference(const IfcPolygonalBoundedHalfSpace* hs, TempMesh& result,
|
||||
const TempMesh& first_operand,
|
||||
ConversionData& conv)
|
||||
{
|
||||
ai_assert(hs != NULL);
|
||||
|
||||
return; // niy
|
||||
|
||||
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
void ProcessBooleanExtrudedAreaSolidDifference(const IfcExtrudedAreaSolid* as, TempMesh& result,
|
||||
const TempMesh& first_operand,
|
||||
ConversionData& conv)
|
||||
{
|
||||
ai_assert(as != NULL);
|
||||
|
||||
// This case is handled by reduction to an instance of the quadrify() algorithm.
|
||||
// Obviously, this won't work for arbitrarily complex cases. In fact, the first
|
||||
// operand should be near-planar. Luckily, this is usually the case in Ifc
|
||||
// buildings.
|
||||
|
||||
boost::shared_ptr<TempMesh> meshtmp(new TempMesh());
|
||||
ProcessExtrudedAreaSolid(*as,*meshtmp,conv,false);
|
||||
|
||||
std::vector<TempOpening> openings(1, TempOpening(as,IfcVector3(0,0,0),meshtmp,boost::shared_ptr<TempMesh>(NULL)));
|
||||
|
||||
result = first_operand;
|
||||
|
||||
TempMesh temp;
|
||||
|
||||
std::vector<IfcVector3>::const_iterator vit = first_operand.verts.begin();
|
||||
BOOST_FOREACH(unsigned int pcount, first_operand.vertcnt) {
|
||||
temp.Clear();
|
||||
|
||||
temp.verts.insert(temp.verts.end(), vit, vit + pcount);
|
||||
temp.vertcnt.push_back(pcount);
|
||||
|
||||
// The algorithms used to generate mesh geometry sometimes
|
||||
// spit out lines or other degenerates which must be
|
||||
// filtered to avoid running into assertions later on.
|
||||
|
||||
// ComputePolygonNormal returns the Newell normal, so the
|
||||
// length of the normal is the area of the polygon.
|
||||
const IfcVector3& normal = temp.ComputeLastPolygonNormal(false);
|
||||
if (normal.SquareLength() < static_cast<IfcFloat>(1e-5)) {
|
||||
IFCImporter::LogWarn("skipping degenerate polygon (ProcessBooleanExtrudedAreaSolidDifference)");
|
||||
continue;
|
||||
}
|
||||
|
||||
GenerateOpenings(openings, std::vector<IfcVector3>(1,IfcVector3(1,0,0)), temp, false, true);
|
||||
result.Append(temp);
|
||||
|
||||
vit += pcount;
|
||||
}
|
||||
|
||||
IFCImporter::LogDebug("generating CSG geometry by geometric difference to a solid (IfcExtrudedAreaSolid)");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
void ProcessBoolean(const IfcBooleanResult& boolean, TempMesh& result, ConversionData& conv)
|
||||
{
|
||||
// supported CSG operations:
|
||||
// DIFFERENCE
|
||||
if(const IfcBooleanResult* const clip = boolean.ToPtr<IfcBooleanResult>()) {
|
||||
if(clip->Operator != "DIFFERENCE") {
|
||||
IFCImporter::LogWarn("encountered unsupported boolean operator: " + (std::string)clip->Operator);
|
||||
return;
|
||||
}
|
||||
|
||||
// supported cases (1st operand):
|
||||
// IfcBooleanResult -- call ProcessBoolean recursively
|
||||
// IfcSweptAreaSolid -- obtain polygonal geometry first
|
||||
|
||||
// supported cases (2nd operand):
|
||||
// IfcHalfSpaceSolid -- easy, clip against plane
|
||||
// IfcExtrudedAreaSolid -- reduce to an instance of the quadrify() algorithm
|
||||
|
||||
|
||||
const IfcHalfSpaceSolid* const hs = clip->SecondOperand->ResolveSelectPtr<IfcHalfSpaceSolid>(conv.db);
|
||||
const IfcExtrudedAreaSolid* const as = clip->SecondOperand->ResolveSelectPtr<IfcExtrudedAreaSolid>(conv.db);
|
||||
if(!hs && !as) {
|
||||
IFCImporter::LogError("expected IfcHalfSpaceSolid or IfcExtrudedAreaSolid as second clipping operand");
|
||||
return;
|
||||
}
|
||||
|
||||
TempMesh first_operand;
|
||||
if(const IfcBooleanResult* const op0 = clip->FirstOperand->ResolveSelectPtr<IfcBooleanResult>(conv.db)) {
|
||||
ProcessBoolean(*op0,first_operand,conv);
|
||||
}
|
||||
else if (const IfcSweptAreaSolid* const swept = clip->FirstOperand->ResolveSelectPtr<IfcSweptAreaSolid>(conv.db)) {
|
||||
ProcessSweptAreaSolid(*swept,first_operand,conv);
|
||||
}
|
||||
else {
|
||||
IFCImporter::LogError("expected IfcSweptAreaSolid or IfcBooleanResult as first clipping operand");
|
||||
return;
|
||||
}
|
||||
|
||||
if(hs) {
|
||||
const IfcPolygonalBoundedHalfSpace* const hs_bounded = clip->SecondOperand->ResolveSelectPtr<IfcPolygonalBoundedHalfSpace>(conv.db);
|
||||
if (hs_bounded) {
|
||||
ProcessPolygonalBoundedBooleanHalfSpaceDifference(hs_bounded, result, first_operand, conv);
|
||||
}
|
||||
else {
|
||||
ProcessBooleanHalfSpaceDifference(hs, result, first_operand, conv);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ProcessBooleanExtrudedAreaSolidDifference(as, result, first_operand, conv);
|
||||
}
|
||||
}
|
||||
else {
|
||||
IFCImporter::LogWarn("skipping unknown IfcBooleanResult entity, type is " + boolean.GetClassName());
|
||||
}
|
||||
}
|
||||
|
||||
} // ! IFC
|
||||
} // ! Assimp
|
||||
|
||||
#endif
|
||||
|
|
@ -1967,6 +1967,10 @@
|
|||
<Filter
|
||||
Name="ifc"
|
||||
>
|
||||
<File
|
||||
RelativePath="..\..\code\IfcBoolean.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\code\IFCCurve.cpp"
|
||||
>
|
||||
|
@ -1987,6 +1991,10 @@
|
|||
RelativePath="..\..\code\IFCMaterial.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\code\IFCOpenings.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\code\IFCProfile.cpp"
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue