View Issue Details

IDProjectCategoryView StatusLast Update
0003815OpenFOAMFeaturepublic2022-03-09 11:14
Reporterkasperbilde Assigned Tohenry  
PrioritynormalSeveritymajorReproducibilityalways
Status resolvedResolutionfixed 
PlatformGNU/LinuxOSUbuntuOS Version20.04
Product Versiondev 
Fixed in Versiondev 
Summary0003815: fluent3DMeshToFoam not able to read mesh from R2021
DescriptionHi,

I've previously used fluent3DMeshToFoam to convert Fluent meshes to Foam and I recently upgraded to Fluent R2021 R2. I created a mesh and got the following error due to square brackets in the .msh ASCII file.

/*---------------------------------------------------------------------------*\
  ========= |
  \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
   \\ / O peration | Website: https://openfoam.org
    \\ / A nd | Version: dev
     \\/ M anipulation |
\*---------------------------------------------------------------------------*/
Build : dev-69858a80ec41
Exec : fluent3DMeshToFoam fluent3DmeshToFoam_bug.msh
Date : Mar 08 2022
Time : 14:31:14
Host : "DKAALT0912"
PID : 7777
I/O : uncollated
Case : /mnt/c/Users/dkaakbea/OpenFOAM/flocculatorDesign/basecase
nProcs : 1
sigFpe : Enabling floating point exception trapping (FOAM_SIGFPE).
fileModificationChecking : Monitoring run-time modified files using timeStampMaster (fileModificationSkew 10)
allowSystemOperations : Allowing user-supplied system call operations

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
Create time

Reading header: "ANSYS(R) TGLib(TM) 3D, revision 18.1.0"
Reading header: "ANSYS(R) TGLib(TM) 3D, revision 18.1.0"
Dimension of grid: 3
Number of points: 8263
Number of faces: 12816
Number of cells: 2617
PointGroup: 64 start: 1274 end: 8262. Reading points...done.
PointGroup: 136 start: 0 end: 1273. Reading points...done.
FaceGroup: 62 start: 763 end: 12815. Reading mixed faces...done.
FaceGroup: 13 start: 0 end: 179. Reading mixed faces...done.
FaceGroup: 14 start: 180 end: 359. Reading mixed faces...done.
FaceGroup: 12 start: 360 end: 762. Reading mixed faces...done.
CellGroup: 60 start: 0 end: 2616 type: 1
Zone: 60 name: solid type: fluid. Reading zone data...done.
Zone: 12 name: walls type: wall. Reading zone data...done.
Zone: 14 name: outlet type: pressure-outlet. Reading zone data...done.
Zone: 13 name: inlet type: velocity-inlet. Reading zone data...done.
Zone: 62 name: interior--solid type: interior. Reading zone data...done.


--> FOAM FATAL ERROR:
Do not understand characters: [
    on line 21154

    From function virtual int yyFlexLexer::yylex()
    in file fluent3DMeshToFoam.L at line 749.

FOAM exiting

Not sure whether this was introduced in an earlier version of Fluent, but I can confirm it was working well with Fluent 2019 R3.

When removing the square brackets from the ASCII file, fluent3DMeshToFoam functions again.

sed -e 's/\|\[0-9\]//g' -i fluent3DmeshToFoam_bug.msh
fluent3DMeshToFoam -scale 0.001 fluent3DmeshToFoam_bug.msh
Steps To ReproduceReproduce error:
1. Insert fluent3DmeshToFoam_bug.msh in a case.
2. fluent3DMeshToFoam fluent3DmeshToFoam_bug.msh

Fix by removing square brackets from ASCII .msh file.
1. sed -e 's/\|\[0-9\]//g' -i fluent3DmeshToFoam_bug.msh
2. fluent3DMeshToFoam fluent3DmeshToFoam_bug.msh
TagsNo tags attached.

Activities

kasperbilde

2022-03-08 13:34

reporter  

henry

2022-03-08 13:49

manager   ~0012520

Last edited: 2022-03-08 13:59

We have no access to Fluent and so no way to maintain the converter. Can you supply a patch which ensures the converter works with the current AND previous Fluent releases?

I tested the .msh file you attached but it converted fine and does not contain any square brackets.

kasperbilde

2022-03-08 14:24

reporter   ~0012521

I'll look into it. It might take a while, as I haven't got so much time at the moment. I don't know if you can put it "on hold" or something. If anyone else encounters this from Google search, they can simply remove the brackets from the .msh-file until a patch is made.

henry

2022-03-08 14:38

manager   ~0012522

I couldn't find any square brackets in the .msh file you sent and it converts fine.

henry

2022-03-08 14:47

manager   ~0012523

Here is the log I get when I convert the file attached here:
Create time

Reading header: "ANSYS(R) TGLib(TM) 3D, revision 18.1.0"
Reading header: "ANSYS(R) TGLib(TM) 3D, revision 18.1.0"
Dimension of grid: 3
Number of points: 8263
Number of faces: 12816
Number of cells: 2617
PointGroup: 64 start: 1274 end: 8262. Reading points...done.
PointGroup: 136 start: 0 end: 1273. Reading points...done.
FaceGroup: 62 start: 763 end: 12815. Reading mixed faces...done.
FaceGroup: 13 start: 0 end: 179. Reading mixed faces...done.
FaceGroup: 14 start: 180 end: 359. Reading mixed faces...done.
FaceGroup: 12 start: 360 end: 762. Reading mixed faces...done.
CellGroup: 60 start: 0 end: 2616 type: 1
Zone: 60 name: solid type: fluid. Reading zone data...done.
Zone: 12 name: walls type: wall. Reading zone data...done.
Zone: 14 name: outlet type: pressure-outlet. Reading zone data...done.
Zone: 13 name: inlet type: velocity-inlet. Reading zone data...done.
Zone: 62 name: interior--solid type: interior. Reading zone data...done.
--> FOAM Warning : Found unknown block of type: "73"
    on line 22257

FINISHED LEXING

Creating patch 0 for zone: 13 name: inlet type: velocity-inlet
Creating patch 1 for zone: 14 name: outlet type: pressure-outlet
Creating patch 2 for zone: 12 name: walls type: wall
Creating cellZone 0 name: solid type: fluid
Creating faceZone 0 name: interior--solid type: interior
faceZone from Fluent indices: 763 to: 12815 type: interior
patch 0 from Fluent indices: 0 to: 179 type: velocity-inlet
patch 1 from Fluent indices: 180 to: 359 type: pressure-outlet
patch 2 from Fluent indices: 360 to: 762 type: wall

Writing mesh to "constant/region0"

kasperbilde

2022-03-08 18:32

reporter   ~0012524

I accidentally attached the file where the brackets had already been removed. This is the right one.

henry

2022-03-08 21:32

manager   ~0012525

I have played around with the text parsing and the attached version parses your case but I don't have many .msh files to test it on to make sure it is backward compatible. Could you test it before I commit it to OpenFOAM-dev?
fluent3DMeshToFoam.L (43,055 bytes)   
/*--------------------------------*- C++ -*----------------------------------*\
  =========                 |
  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
   \\    /   O peration     | Website:  https://openfoam.org
    \\  /    A nd           | Copyright (C) 2011-2021 OpenFOAM Foundation
     \\/     M anipulation  |
-------------------------------------------------------------------------------
License
    This file is part of OpenFOAM.

    OpenFOAM 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.

    OpenFOAM 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 OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.

\*---------------------------------------------------------------------------*/

%{
#undef yyFlexLexer

 /* ------------------------------------------------------------------------ *\
   ------ local definitions
 \* ------------------------------------------------------------------------ */

#include "cyclicPolyPatch.H"
#include "argList.H"
#include "Time.H"
#include "polyMesh.H"
#include "polyTopoChange.H"
#include "polyMeshZipUpCells.H"
#include "wallPolyPatch.H"
#include "symmetryPolyPatch.H"
#include "mergedCyclicPolyPatch.H"
#include "Swap.H"
#include "IFstream.H"
#include "readHexLabel.H"
#include "polyMeshUnMergeCyclics.H"

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

using namespace Foam;

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

// Line number
label lineNo = 1;

// Scale factor used to scale points (optional command line argument)
scalar scaleFactor = 1.0;

label dimensionOfGrid = 0;
label nPoints = 0;
label nFaces = 0;
label nCells = 0;

bool hangingNodes = false;

pointField points(0);
faceList faces(0);
labelList owner(0);
labelList neighbour(0);

// Group type and name
Map<word> groupType(100);
Map<word> groupName(100);

// Point groups
DynamicList<label> pointGroupZoneID;
DynamicList<label> pointGroupStartIndex;
DynamicList<label> pointGroupEndIndex;

// Face groups
DynamicList<label> faceGroupZoneID;
DynamicList<label> faceGroupStartIndex;
DynamicList<label> faceGroupEndIndex;

// Cell groups
DynamicList<label> cellGroupZoneID;
DynamicList<label> cellGroupStartIndex;
DynamicList<label> cellGroupEndIndex;
DynamicList<label> cellGroupType;

// Special parsing of (incorrect) Cubit files
bool cubitFile = false;


void uniquify(word& name, HashSet<word>& patchNames)
{
    if (!patchNames.found(name))
    {
        patchNames.insert(name);
    }
    else
    {
        Info<< "    name " << name << " already used";

        label i = 1;
        word baseName = name;

        do
        {
            name = baseName + "_" + Foam::name(i++);
        } while (patchNames.found(name));

        Info<< ", changing to " << name << endl;
    }
}


// Dummy yywrap to keep yylex happy at compile time.
// It is called by yylex but is not used as the mechanism to change file.
// See <<EOF>>
#if YY_FLEX_MINOR_VERSION < 6 && YY_FLEX_SUBMINOR_VERSION < 34
extern "C" int yywrap()
#else
int yyFlexLexer::yywrap()
#endif
{
    return 1;
}

%}

one_space                  [ \t\f]
space                      {one_space}*
some_space                 {one_space}+

alpha                      [_[:alpha:]]
digit                      [[:digit:]]
decDigit                   [[:digit:]]
octalDigit                 [0-7]
hexDigit                   [[:xdigit:]]

lbrac                      "("
rbrac                      ")"
quote                      \"
dash                       "-"
dotColonDash               [.:-]
commaPipe                  [,\|]
squareBracket              [\[\]]

schemeSpecialInitial       [!$%&*/\\:;<=>?~_^#.@']
schemeSpecialSubsequent    [.+-]
schemeSymbol               (({some_space}|{alpha}|{quote}|{schemeSpecialInitial})({alpha}|{quote}|{digit}|{schemeSpecialInitial}|{schemeSpecialSubsequent})*)


identifier                 {alpha}({alpha}|{digit})*
integer                    {decDigit}+
label                      [1-9]{decDigit}*
hexLabel                   {hexDigit}+
zeroLabel                  {digit}*
signedInteger              [-+]?{integer}
word                       ({alpha}|{digit}|{dotColonDash})*
wordBraces                 ({word}|{lbrac}|{rbrac})*
wordSquareBrackets         ({word}|{squareBracket})*
wordBracesExtras           ({word}|{lbrac}|{rbrac}|{commaPipe})*

exponent_part              [eE][-+]?{digit}+
fractional_constant        [-+]?(({digit}*"."{digit}+)|({digit}+".")|({digit}))

double                     ((({fractional_constant}{exponent_part}?)|({digit}+{exponent_part}))|0)

x                          {double}
y                          {double}
z                          {double}
scalar                     {double}
labelListElement           {space}{zeroLabel}
hexLabelListElement        {space}{hexLabel}
scalarListElement          {space}{double}
schemeSymbolListElement    {space}{schemeSymbol}
labelList                  ({labelListElement}+{space})
hexLabelList               ({hexLabelListElement}+{space})
scalarList                 ({scalarListElement}+{space})
schemeSymbolList           ({schemeSymbolListElement}+{space})

starStar                   ("**")
text                       ({space}({wordSquareBrackets}*{space})*)
textBraces                 ({space}({wordBraces}*{space})*)
textExtras                 ({space}({word}*{commaPipe}{space})*)
textBracesExtras           ({space}({wordBracesExtras}*{space})*)
anythingInBlock            ([^)]*)

dateDDMMYYYY               ({digit}{digit}"/"{digit}{digit}"/"{digit}{digit}{digit}{digit})
dateDDMonYYYY              ((({digit}{digit}{space})|({digit}{space})){alpha}*{space}{digit}{digit}{digit}{digit})
time                       ({digit}{digit}":"{digit}{digit}":"{digit}{digit})

versionNumber              ({digit}|".")*

header                     {space}"(1"{space}
dimension                  {space}"(2"{space}
points                     {space}"(10"{space}
faces                      {space}"(13"{space}
cells                      {space}"(12"{space}
zoneVariant1               {space}"(39"{space}
zoneVariant2               {space}"(45"{space}
faceTree                   {space}"(59"{space}

comment                    "0"{space}
unknownPeriodicFace        "17"{space}
periodicFace               "18"{space}
cellTree                   "58"{space}
faceParents                "61"{space}
ignoreBlocks               ("4"|"37"|"38"|"40"|"41"|"60"|"64"){space}

redundantBlock             {space}({comment}|{unknownPeriodicFace}|{periodicFace}|{cellTree}|{faceParents}|{ignoreBlocks}){space}

endOfSection               {space}")"{space}



 /* ------------------------------------------------------------------------ *\
                      -----  Exclusive start states -----
 \* ------------------------------------------------------------------------ */

%option stack

%x readHeader
%x readDimension
%x readPoint
%x readPointHeader
%x readNumberOfPoints
%x readPointGroupData
%x readPointData
%x readScalarList
%x fluentFace
%x readFaceHeader
%x readNumberOfFaces
%x readFaceGroupData
%x readFaceData
%x readFacesMixed
%x readFacesUniform
%x cell
%x readCellHeader
%x readNumberOfCells
%x readCellGroupData
%x readCellData
%x readCellsUniform
%x zone
%x readZoneHeader
%x readZoneGroupData
%x readZoneData
%x readZoneBlock

%x ignoreBlock
%x ignoreEmbeddedBlock
%%

%{
    // End of read character pointer returned by strtol and strtod
    char* endPtr;

    // Point data
    label pointGroupNumberOfComponents = 3;
    label pointi = 0; // index used for reading points
    label cmpt = 0;   // component index used for reading points

    // Face data
    label faceGroupElementType = -1;
    label facei = 0;  // index used for reading faces
%}


 /* ------------------------------------------------------------------------ *\
                            ------ Start Lexing ------
 \* ------------------------------------------------------------------------ */

 /*                      ------ Reading control header ------                */

{header} {
        BEGIN(readHeader);
    }

<readHeader>{quote}{textBracesExtras}{quote} {
        Info<< "Reading header: " << YYText() << endl;
    }

{dimension} {
        BEGIN(readDimension);
    }

<readDimension>{space}{label}{space} {
        dimensionOfGrid = atoi(YYText());
        Info<< "Dimension of grid: " << dimensionOfGrid << endl;
    }


{points} {
        BEGIN(readPointHeader);
    }

<readPointHeader>{space}{lbrac}{space}"0"{space}"1"{space} {
        BEGIN(readNumberOfPoints);
    }

<readNumberOfPoints>{hexLabel}{space}{labelList} {
        nPoints = strtol(YYText(), &endPtr, 16);
        Info<< "Number of points: " << nPoints << endl;
        points.setSize(nPoints);

        // Ignore rest of stream
    }

<readPointHeader>{space}{lbrac} {
        BEGIN(readPointGroupData);
    }

<readPointGroupData>{space}{hexLabel}{space}{hexLabel}{space}{hexLabel}{labelList} {
        // Read point zone-ID, start and end-label
        // the indices will be used for checking later.
        pointGroupZoneID.append(strtol(YYText(), &endPtr, 16));

        // In FOAM, indices start from zero - adjust
        pointGroupStartIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        pointGroupEndIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        // point group type skipped
        (void)strtol(endPtr, &endPtr, 16);

        pointi = pointGroupStartIndex.last();

        // reset number of components to default
        pointGroupNumberOfComponents = 3;

        // read number of components in the vector
        if (endPtr < &(YYText()[YYLeng()-1]))
        {
            pointGroupNumberOfComponents = strtol(endPtr, &endPtr, 16);
        }

        Info<< "PointGroup: "
            << pointGroupZoneID.last()
            << " start: "
            << pointGroupStartIndex.last()
            << " end: "
            << pointGroupEndIndex.last() << flush;
    }

<readNumberOfPoints,readPointGroupData>{endOfSection} {
        BEGIN(readPointData);
    }

<readPointData>{space}{lbrac}{space} {
        Info<< ".  Reading points..." << flush;
        cmpt = 0;
        yy_push_state(readScalarList);
    }

<readScalarList>{signedInteger}{space} {
        points[pointi][cmpt++] = scaleFactor*atol(YYText());

        if (cmpt == pointGroupNumberOfComponents)
        {
            if (pointGroupNumberOfComponents == 2)
            {
                points[pointi].z() = 0.0;
            }

            cmpt = 0;
            pointi++;
        }
    }

<readScalarList>{scalar}{space} {
        points[pointi][cmpt++] = scaleFactor*atof(YYText());

        if (cmpt == pointGroupNumberOfComponents)
        {
            if (pointGroupNumberOfComponents == 2)
            {
                points[pointi].z() = 0.0;
            }

            cmpt = 0;
            pointi++;
        }
    }

<readScalarList>{endOfSection} {
        Info<< "done." << endl;

        // check read of points
        if (pointi != pointGroupEndIndex.last()+1)
        {
            Warning
                << "Problem with reading points: " << nl
                << "    start index: "
                << pointGroupStartIndex.last()
                << " end index: "
                << pointGroupEndIndex.last()
                << " last points read: " << pointi << nl
                << "    on line " << lineNo << endl;
        }

        yy_pop_state();
    }

{faces} {
        BEGIN(readFaceHeader);
    }

<readFaceHeader>{space}{lbrac}{space}"0"{space}"1"{space} {
        BEGIN(readNumberOfFaces);
    }

<readNumberOfFaces>{space}{hexLabel}{space}{labelListElement}+ {
        nFaces = strtol(YYText(), &endPtr, 16);

        Info<< "Number of faces: " << nFaces << endl;

        faces.setSize(nFaces);
        owner.setSize(nFaces);
        neighbour.setSize(nFaces);

        // Type and element type not read
    }

<readFaceHeader>{space}{lbrac} {
        BEGIN(readFaceGroupData);
    }

<readFaceGroupData>{space}{hexLabel}{space}{hexLabel}{space}{hexLabel}{hexLabelListElement}+ {
        // read fluentFace zone-ID, start and end-label
        faceGroupZoneID.append(strtol(YYText(), &endPtr, 16));

        // In FOAM, indices start from zero - adjust
        faceGroupStartIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        faceGroupEndIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        // face group type
        (void)strtol(endPtr, &endPtr, 16);

        faceGroupElementType = strtol(endPtr, &endPtr, 16);

        facei = faceGroupStartIndex.last();

        Info<< "FaceGroup: "
            << faceGroupZoneID.last()
            << " start: "
            << faceGroupStartIndex.last()
            << " end: "
            << faceGroupEndIndex.last() << flush;
    }

<readNumberOfFaces,readFaceGroupData>{space}{endOfSection} {
        BEGIN(readFaceData);
    }

<readFaceData>{space}{lbrac} {
        if (faceGroupElementType == 0 || faceGroupElementType > 4)
        {
            Info<< ".  Reading mixed faces..." << flush;
            yy_push_state(readFacesMixed);
        }
        else
        {
            Info<< ".  Reading uniform faces..." << flush;
            yy_push_state(readFacesUniform);
        }
    }

<readFacesMixed>{space}{hexLabelList} {
        face& curFaceLabels = faces[facei];

        // set size of label list
        curFaceLabels.setSize(strtol(YYText(), &endPtr, 16));

        forAll(curFaceLabels, i)
        {
            curFaceLabels[i] = strtol(endPtr, &endPtr, 16) - 1;
        }

        // read neighbour and owner. Neighbour comes first
        neighbour[facei] = strtol(endPtr, &endPtr, 16) - 1;
        owner[facei] = strtol(endPtr, &endPtr, 16) - 1;
        facei++;
    }

<readFacesUniform>{space}{hexLabelList} {
        face& curFaceLabels = faces[facei];

        // Set size of label list.
        curFaceLabels.setSize(faceGroupElementType);

        curFaceLabels[0] = strtol(YYText(), &endPtr, 16) - 1;

        for (int i=1; i<faceGroupElementType; i++)
        {
            curFaceLabels[i] = strtol(endPtr, &endPtr, 16) - 1;
        }

        // read neighbour and owner. Neighbour comes first
        neighbour[facei] = strtol(endPtr, &endPtr, 16) - 1;
        owner[facei] = strtol(endPtr, &endPtr, 16) - 1;
        facei++;
    }

<readFacesMixed,readFacesUniform>{space}{endOfSection} {
        Info<< "done." << endl;

        // check read of fluentFaces
        if (facei != faceGroupEndIndex.last()+1)
        {
            Warning
                << "Problem with reading fluentFaces: " << nl
                << "    start index: "
                << faceGroupStartIndex.last()
                << " end index: "
                << faceGroupEndIndex.last()
                << " last fluentFaces read: " << facei << nl
                << "    on line " << lineNo << endl;
        }

        yy_pop_state();
    }


{cells} {
        BEGIN(readCellHeader);
    }

<readCellHeader>{space}{lbrac}{space}"0"{space}"1"{space} {
        BEGIN(readNumberOfCells);
    }

<readNumberOfCells>{space}{hexLabel}{space}{labelListElement}+ {
        nCells = strtol(YYText(), &endPtr, 16);
        Info<< "Number of cells: " << nCells << endl;
    }

<readCellHeader>{space}{lbrac} {
        BEGIN(readCellGroupData);
    }

<readCellGroupData>{space}{hexLabel}{space}{hexLabel}{space}{hexLabel}{space}{hexLabel} {
        // Warning. This entry must be above the next one because of the lexing
        // rules. It is introduced to deal with the problem of reading
        // non-standard cell definition from Tgrid, which misses the type label.

        Warning
            << "Tgrid syntax problem: " << YYText() << nl
            << "    on line " << lineNo << endl;

        // read cell zone-ID, start and end-label
        cellGroupZoneID.append(strtol(YYText(), &endPtr, 16));

        // the indices will be used for checking later.
        cellGroupStartIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        cellGroupEndIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        cellGroupType.append(strtol(endPtr, &endPtr, 16));

        Info<< "CellGroup: "
            << cellGroupZoneID.last()
            << " start: "
            << cellGroupStartIndex.last()
            << " end: "
            << cellGroupEndIndex.last()
            << " type: "
            << cellGroupType.last()
            << endl;
    }

<readCellGroupData>{space}{hexLabel}{space}{hexLabel}{space}{hexLabel}{space}{hexLabel}{space}{hexLabel} {
        // Warning. See above

        // read cell zone-ID, start and end-label
        cellGroupZoneID.append(strtol(YYText(), &endPtr, 16));

        // the indices will be used for checking later.
        cellGroupStartIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        cellGroupEndIndex.append(strtol(endPtr, &endPtr, 16) - 1);

        cellGroupType.append(strtol(endPtr, &endPtr, 16));

        // Note. Potentially skip cell set if type is zero.
        (void)strtol(endPtr, &endPtr, 16);

        Info<< "CellGroup: "
            << cellGroupZoneID.last()
            << " start: "
            << cellGroupStartIndex.last()
            << " end: "
            << cellGroupEndIndex.last()
            << " type: "
            << cellGroupType.last()
            << endl;
    }

<readNumberOfCells,readCellGroupData>{endOfSection} {
        BEGIN(readCellData);
    }

<readCellData>{space}{lbrac} {
        // Quickly scan to the end of the cell data block and discard
        int c;
        while ((c = yyinput()) != 0 && c != ')')
        {}
    }

{faceTree} {
        // There are hanging nodes in the mesh so make sure it gets zipped-up
        hangingNodes = true;
        yy_push_state(ignoreBlock);
    }

{zoneVariant1} {
        BEGIN(readZoneHeader);
    }

{zoneVariant2} {
        BEGIN(readZoneHeader);
    }

<readZoneHeader>{space}{lbrac} {
        BEGIN(readZoneGroupData);
    }

<readZoneGroupData>{space}{hexLabel}{space}{word}{space}{word}{space}{label}? {
        IStringStream zoneDataStream(YYText());

        // cell zone-ID not in hexadecimal!!! Inconsistency
        label zoneID = -1;

        if (cubitFile)
        {
            zoneID = readHexLabel(zoneDataStream);
        }
        else
        {
            zoneID = readLabel(zoneDataStream);
        }

        groupType.insert(zoneID, word(zoneDataStream));
        groupName.insert(zoneID, word(zoneDataStream));

        Info<< "Zone: " << zoneID
            << " name: " << groupName[zoneID]
            << " type: " << groupType[zoneID] << flush;
    }

<readZoneGroupData>{endOfSection} {
        BEGIN(readZoneData);
    }

<readZoneData>{space}{lbrac} {
        Info<< ".  Reading zone data..." << flush;
        yy_push_state(readZoneBlock);
    }

<readZoneBlock>{space}{schemeSymbolList} {
    }

<readZoneBlock>{lbrac} {
        // Warning
        //    << "Found unknown block in zone: " << YYText() << nl
        //    << "    on line " << lineNo << endl;
        yy_push_state(ignoreBlock);
    }

<readZoneBlock>{endOfSection} {
        Info<< "done." << endl;
        yy_pop_state();
    }



 /*             ------ Reading end of section and others ------              */

<readHeader,readDimension,readPointData,readFaceData,readCellData,readZoneData>{space}{endOfSection} {
        BEGIN(INITIAL);
    }

 /*    ------ Reading unknown type or non-standard comment ------            */

{lbrac}{label} {
        Warning
            << "Found unknown block of type: "
            << Foam::string(YYText())(1, YYLeng()-1) << nl
            << "    on line " << lineNo << endl;

        yy_push_state(ignoreBlock);
    }

{lbrac}{redundantBlock} {
        yy_push_state(ignoreBlock);
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{quote}{text}{quote} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{schemeSymbol} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{lbrac} {
        yy_push_state(ignoreEmbeddedBlock);

    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{endOfSection} {
        yy_pop_state();
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{labelList} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{hexLabelList} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{scalarList} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{schemeSymbolList} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{text} {
    }

<ignoreBlock,ignoreEmbeddedBlock>{space}{textExtras} {
    }

 /* ------              Count newlines.                              ------  */

<*>\n {
        lineNo++;
    }


 /* ------              Ignore remaining space.                      ------  */

<*>{some_space}|\r {
    }


 /* ------              Any other characters are errors.              ------ */

<*>. {
        // This is a catch all.
        FatalErrorInFunction
            << "Do not understand characters: " << YYText() << nl
            << "    on line " << lineNo
            << exit(FatalError);
    }


 /*  ------ On EOF return to previous file, if none exists terminate. ------ */

<<EOF>> {
            yyterminate();
    }
%%

int main(int argc, char *argv[])
{
    argList::noParallel();
    argList::validArgs.append("Fluent mesh file");
    argList::addOption
    (
        "scale",
        "factor",
        "geometry scaling factor - default is 1"
    );
    argList::addOption
    (
        "includedAngle",
        "angle",
        "feature angle with which to split cyclics"
    );
    argList::addOption
    (
        "ignoreCellGroups",
        "names",
        "specify cell groups to ignore"
    );
    argList::addOption
    (
        "ignoreFaceGroups",
        "names",
        "specify face groups to ignore"
    );

    argList::addBoolOption
    (
        "cubit",
        "special parsing of (incorrect) cubit files"
    );

    argList args(argc, argv);

    if (!args.check())
    {
        FatalError.exit();
    }

    args.optionReadIfPresent("scale", scaleFactor);

    wordHashSet ignoreCellGroups;
    wordHashSet ignoreFaceGroups;

    args.optionReadIfPresent("ignoreCellGroups", ignoreCellGroups);
    args.optionReadIfPresent("ignoreFaceGroups", ignoreFaceGroups);

    cubitFile = args.options().found("cubit");

    if (cubitFile)
    {
        Info<< nl
            << "Assuming Cubit generated file"
            << " (incorrect face orientation; hexadecimal zoneIDs)."
            << nl << endl;
    }


    #include "createTime.H"

    const fileName fluentFile = args[1];
    IFstream fluentStream(fluentFile);

    if (!fluentStream)
    {
        FatalErrorInFunction
            << ": file " << fluentFile << " not found"
            << exit(FatalError);
    }

    yyFlexLexer lexer(&fluentStream.stdStream());

    while (lexer.yylex() != 0)
    {}

    Info<< "\nFINISHED LEXING\n\n";

    if (dimensionOfGrid != 3)
    {
        FatalErrorInFunction
            << "Mesh is not 3D, dimension of grid: " << dimensionOfGrid
            << exit(FatalError);
    }

    pointGroupZoneID.shrink();
    pointGroupStartIndex.shrink();
    pointGroupEndIndex.shrink();

    faceGroupZoneID.shrink();
    faceGroupStartIndex.shrink();
    faceGroupEndIndex.shrink();

    cellGroupZoneID.shrink();
    cellGroupStartIndex.shrink();
    cellGroupEndIndex.shrink();
    cellGroupType.shrink();


    // Pre-filtering: flip "owner" boundary or wrong oriented internal
    // faces and move to neighbour

    boolList fm(faces.size(), false);
    forAll(faces, facei)
    {
        if
        (
            owner[facei] == -1
         || (neighbour[facei] != -1 && owner[facei] > neighbour[facei])
        )
        {
            fm[facei] = true;
            if (!cubitFile)
            {
                faces[facei].flip();
            }
            Swap(owner[facei], neighbour[facei]);
        }
    }


    // Foam type for Fluent type
    // ~~~~~~~~~~~~~~~~~~~~~~~~~

    HashTable<word> fluentToFoamType;

    fluentToFoamType.insert("pressure", polyPatch::typeName);
    fluentToFoamType.insert("pressure-inlet", polyPatch::typeName);
    fluentToFoamType.insert("inlet-vent", polyPatch::typeName);
    fluentToFoamType.insert("intake-fan", polyPatch::typeName);
    fluentToFoamType.insert("pressure-outlet", polyPatch::typeName);
    fluentToFoamType.insert("exhaust-fan", polyPatch::typeName);
    fluentToFoamType.insert("outlet-vent", polyPatch::typeName);
    fluentToFoamType.insert("pressure-far-field", polyPatch::typeName);
    fluentToFoamType.insert("velocity-inlet", polyPatch::typeName);
    fluentToFoamType.insert("mass-flow-inlet", polyPatch::typeName);
    fluentToFoamType.insert("outflow", polyPatch::typeName);

    fluentToFoamType.insert("wall" , wallPolyPatch::typeName);

    fluentToFoamType.insert("symmetry",  symmetryPolyPatch::typeName);
    fluentToFoamType.insert("axis",  symmetryPolyPatch::typeName);

    fluentToFoamType.insert("interior", polyPatch::typeName);
    fluentToFoamType.insert("interface", polyPatch::typeName);
    fluentToFoamType.insert("internal", polyPatch::typeName);
    fluentToFoamType.insert("solid", polyPatch::typeName);

    fluentToFoamType.insert("fan", mergedCyclicPolyPatch::typeName);
    fluentToFoamType.insert("radiator", mergedCyclicPolyPatch::typeName);
    fluentToFoamType.insert("porous-jump", mergedCyclicPolyPatch::typeName);

    fluentToFoamType.insert("periodic", cyclicPolyPatch::typeName);
    fluentToFoamType.insert("shadow", cyclicPolyPatch::typeName);


    // Foam patch type for Fluent zone type
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    HashSet<word> fluentGroupToFoamPatch;
    fluentGroupToFoamPatch.insert("wall");
    fluentGroupToFoamPatch.insert("fan");
    fluentGroupToFoamPatch.insert("radiator");
    fluentGroupToFoamPatch.insert("porous-jump");


    // Create initial empty polyMesh
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    polyMesh mesh
    (
        IOobject
        (
            polyMesh::defaultRegion,
            runTime.constant(),
            runTime
        ),
        pointField(),
        faceList(),
        labelList(),
        labelList()
    );


    // Check the cell groups for zones ignoring those in ignoreCellGroups
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    label nCellZones = 0;
    labelList cellZoneIDs(cellGroupZoneID.size());

    forAll(cellGroupZoneID, cgi)
    {
        if (!ignoreCellGroups.found(groupName[cellGroupZoneID[cgi] ]))
        {
            cellZoneIDs[nCellZones++] = cgi;
        }
    }

    cellZoneIDs.setSize(nCellZones);


    // Check the face groups for boundary patches, baffles and faceZones
    // ignoring the interior zones in ignoreCellGroups
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    DynamicList<label> patchIDs(faceGroupZoneID.size());
    DynamicList<label> faceZoneIDs(faceGroupZoneID.size());

    forAll(faceGroupZoneID, fgi)
    {
        label zoneID = faceGroupZoneID[fgi];
        label start = faceGroupStartIndex[fgi];

        if (groupType.found(zoneID))
        {
            const word& type = groupType[zoneID];

            // Check the first element of neighbour for boundary group
            if (neighbour[start] == -1 || fluentGroupToFoamPatch.found(type))
            {
                patchIDs.append(fgi);
            }
            else
            {
                if (!ignoreFaceGroups.found(groupName[faceGroupZoneID[fgi] ]))
                {
                    faceZoneIDs.append(fgi);
                }
            }
        }
        else if (hangingNodes)
        {
            label end = faceGroupEndIndex[fgi];

            Info<< "Unknown FaceGroup " << zoneID
                << " assumed to be parent faces of refinement "
                   "patterns and ignored."
                << endl;

            // Set the owner of these faces to -1 so that they do not get
            // added to the mesh
            for (label facei = start; facei <= end; facei++)
            {
                owner[facei] = -1;
            }
        }
        else
        {
            if (neighbour[start] == -1)
            {
                // Boundary face in unknown group. Create a patch for it.
                groupType.insert(zoneID, "unknown");
                groupName.insert(zoneID, "FaceGroup" + Foam::name(zoneID));
                patchIDs.append(fgi);
                Info<< "Created patch " << fgi << " for unknown FaceGroup "
                    << zoneID << '.' << endl;
            }
            else
            {
                WarningInFunction
                    << "Unknown FaceGroup " << zoneID << " not in a zone"
                    << endl;
            }
        }
    }

    patchIDs.shrink();
    faceZoneIDs.shrink();


    // Pair up cyclics
    // ~~~~~~~~~~~~~~~

    labelList nbrPatchis(patchIDs.size(), -1);
    forAll(patchIDs, patchi)
    {
        const label zonei = faceGroupZoneID[patchIDs[patchi]];
        const word& name = groupName[zonei];
        const word& type = groupType[zonei];

        HashTable<word>::const_iterator iter = fluentToFoamType.find(type);

        if (iter != fluentToFoamType.end())
        {
            if
            (
                iter() == cyclicPolyPatch::typeName
             && nbrPatchis[patchi] == -1
            )
            {
                // This is one half of a pair of patches defining a cyclic
                // interface. Find the neighbouring patch.

                forAll(patchIDs, nbrPatchi)
                {
                    const label nbrZonei = faceGroupZoneID[patchIDs[nbrPatchi]];
                    const word& nbrName = groupName[nbrZonei];
                    const word& nbrType = groupType[nbrZonei];

                    HashTable<word>::const_iterator nbrIter =
                    fluentToFoamType.find(nbrType);

                    if (nbrIter != fluentToFoamType.end())
                    {
                        if
                        (
                            nbrIter() == cyclicPolyPatch::typeName
                         && nbrPatchis[nbrPatchi] == -1
                        )
                        {
                            // The neighbour must have a different type
                            // (periodic =/= shadow) and its name must share a
                            // prefix with the patch.

                            if
                            (
                                nbrType != type
                             && nbrName(min(name.size(), nbrName.size()))
                             == name(min(name.size(), nbrName.size()))
                            )
                            {
                                nbrPatchis[nbrPatchi] = patchi;
                                nbrPatchis[patchi] = nbrPatchi;
                                break;
                            }
                        }
                    }
                }

                if (nbrPatchis[patchi] == -1)
                {
                    FatalErrorInFunction
                        << "Could not find neighbour patch for " << type
                        << " patch " << name << exit(FatalError);
                }
            }
        }
    }


    // Add empty patches
    // ~~~~~~~~~~~~~~~~~

    List<polyPatch*> newPatches(patchIDs.size());
    HashSet<word> patchNames;

    forAll(patchIDs, patchi)
    {
        const label zoneID = faceGroupZoneID[patchIDs[patchi]];
        word name = groupName[zoneID];
        const word& type = groupType[zoneID];

        Info<< "Creating patch " << patchi
            << " for zone: " << zoneID
            << " name: " << name
            << " type: " << type
            << endl;

        uniquify(name, patchNames);

        HashTable<word>::const_iterator iter = fluentToFoamType.find(type);

        if (iter != fluentToFoamType.end())
        {
            if (iter() == cyclicPolyPatch::typeName)
            {
                const label nbrPatchi = nbrPatchis[patchi];
                const label nbrZoneID = faceGroupZoneID[patchIDs[nbrPatchi]];
                const word nbrName = groupName[nbrZoneID];

                newPatches[patchi] = new cyclicPolyPatch
                (
                    name,
                    0,
                    0,
                    patchi,
                    mesh.boundaryMesh(),
                    cyclicPolyPatch::typeName,
                    nbrName
                );
            }
            else
            {
                newPatches[patchi] = polyPatch::New
                (
                    iter(),
                    name,
                    0,
                    0,
                    patchi,
                    mesh.boundaryMesh()
                ).ptr();
            }
        }
        else
        {
            newPatches[patchi] = new polyPatch
            (
                name,
                0,
                0,
                patchi,
                mesh.boundaryMesh(),
                polyPatch::typeName
            );
        }
    }
    mesh.addPatches(newPatches);


    // Add empty zones
    // ~~~~~~~~~~~~~~~

    // Cell zones
    mesh.cellZones().setSize(cellZoneIDs.size());
    HashSet<word> cellZoneNames;

    forAll(cellZoneIDs, cellZonei)
    {
        label zoneID = cellGroupZoneID[cellZoneIDs[cellZonei] ];
        word name = groupName[zoneID];
        const word& type = groupType[zoneID];

        Info<< "Creating cellZone " << cellZonei
            << " name: " << name
            << " type: " << type
            << endl;

        uniquify(name, cellZoneNames);

        mesh.cellZones().set
        (
            cellZonei,
            new cellZone
            (
                name,
                labelList(0),
                cellZonei,
                mesh.cellZones()
            )
        );
    }

    // Face zones
    mesh.faceZones().setSize(faceZoneIDs.size());
    HashSet<word> faceZoneNames;

    forAll(faceZoneIDs, faceZonei)
    {
        label zoneID = faceGroupZoneID[faceZoneIDs[faceZonei] ];
        word name = groupName[zoneID];
        const word& type = groupType[zoneID];

        Info<< "Creating faceZone " << faceZonei
            << " name: " << name
            << " type: " << type
            << endl;

        uniquify(name, faceZoneNames);

        mesh.faceZones().set
        (
            faceZonei,
            new faceZone
            (
                name,
                labelList(0),
                boolList(0),
                faceZonei,
                mesh.faceZones()
            )
        );
    }


    // Modify mesh for points/cells/faces
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // Mesh-change container
    polyTopoChange meshMod(mesh, false);

    // Add all points
    forAll(points, pointi)
    {
        meshMod.addPoint(points[pointi], pointi, -1, true);
    }
    points.setSize(0);

    // Add all cells
    for (label celli = 0; celli < nCells; celli++)
    {
        meshMod.addCell
        (
            -1,         // masterPointID
            -1,         // masterEdgeID
            -1,         // masterFaceID
            celli,      // masterCellID
            -1          // zoneID
        );
    }

    // Modify cells to be in zones as required
    forAll(cellZoneIDs, cellZonei)
    {
        label cgi = cellZoneIDs[cellZonei];

        for
        (
            label celli = cellGroupStartIndex[cgi];
            celli <= cellGroupEndIndex[cgi];
            celli++
        )
        {
            meshMod.modifyCell(celli, cellZonei);
        }
    }


    bool doneWarning = false;

    // Add faceZone faces
    forAll(faceZoneIDs, faceZonei)
    {
        label fgi = faceZoneIDs[faceZonei];
        label start = faceGroupStartIndex[fgi];
        label end = faceGroupEndIndex[fgi];
        label zoneID = faceGroupZoneID[fgi];

        Info<< "faceZone from Fluent indices: " << start
            << " to: " << end
            << " type: " << groupType[zoneID]
            << endl;

        for (label facei = start; facei <= end; facei++)
        {
            if (owner[facei] >= nCells || neighbour[facei] >= nCells)
            {
                if (!doneWarning)
                {
                    WarningInFunction
                        << "Ignoring internal face " << facei
                        << " on FaceZone " << zoneID
                        << " since owner " << owner[facei] << " or neighbour "
                        << neighbour[facei] << " outside range of cells 0.."
                        << nCells-1 << endl
                        << "    Suppressing future warnings." << endl;
                    doneWarning = true;
                }
            }
            else
            {
                meshMod.addFace
                (
                    faces[facei],
                    owner[facei],
                    neighbour[facei],
                    -1,                 // masterPointID
                    -1,                 // masterEdgeID
                    facei,              // masterFace
                    false,              // flipFaceFlux
                    -1,                 // patchID
                    faceZonei,          // zoneID
                    fm[facei]           // zoneFlip
                );
            }

            // Mark face as being done
            owner[facei] = -1;
        }
    }

    // Add patch faces
    forAll(patchIDs, patchi)
    {
        label fgi = patchIDs[patchi];
        label start = faceGroupStartIndex[fgi];
        label end = faceGroupEndIndex[fgi];
        label zoneID = faceGroupZoneID[fgi];

        Info<< "patch " << patchi << " from Fluent indices: " << start
            << " to: " << end
            << " type: " << groupType[zoneID]
            << endl;

        for (label facei = start; facei <= end; facei++)
        {
            if (owner[facei] >= nCells || neighbour[facei] >= nCells)
            {
                if (!doneWarning)
                {
                    WarningInFunction
                        << "Ignoring patch face " << facei
                        << " on FaceZone " << zoneID
                        << " since owner " << owner[facei] << " or neighbour "
                        << neighbour[facei] << " outside range of cells 0.."
                        << nCells-1 << endl
                        << "    Suppressing future warnings." << endl;
                    doneWarning = true;
                }
            }
            else
            {
                meshMod.addFace
                (
                    faces[facei],
                    owner[facei],
                    -1,
                    -1,                 // masterPointID
                    -1,                 // masterEdgeID
                    facei,              // masterFace
                    false,              // flipFaceFlux
                    patchi,             // patchID
                    -1,                 // zoneID
                    false               // zoneFlip
                );

                // For baffles create the opposite face
                if (neighbour[start] != -1)
                {
                    meshMod.addFace
                    (
                        faces[facei].reverseFace(),
                        neighbour[facei],
                        -1,
                        -1,                 // masterPointID
                        -1,                 // masterEdgeID
                        facei,              // masterFace
                        false,              // flipFaceFlux
                        patchi,             // patchID
                        -1,                 // zoneID
                        false               // zoneFlip
                    );
                }
            }

            // Mark face as being done
            owner[facei] = -1;
        }
    }

    // Add remaining internal faces
    forAll(owner, facei)
    {
        if (owner[facei] != -1)
        {
            // Check the face being added as an internal face actually is one
            if (neighbour[facei] == -1)
            {
                FatalErrorInFunction
                    << "Attempt of add internal face " << facei
                    << " which is a boundary face"
                    << exit(FatalError);
            }

            if (owner[facei] >= nCells || neighbour[facei] >= nCells)
            {
                if (!doneWarning)
                {
                    WarningInFunction
                        << "Ignoring internal face " << facei
                        << " since owner " << owner[facei] << " or neighbour "
                        << neighbour[facei] << " outside range of cells 0.."
                        << nCells-1 << endl
                        << "    Suppressing future warnings." << endl;
                    doneWarning = true;
                }
            }
            else
            {
                meshMod.addFace
                (
                    faces[facei],
                    owner[facei],
                    neighbour[facei],
                    -1,                 // masterPointID
                    -1,                 // masterEdgeID
                    facei,              // masterFace
                    false,              // flipFaceFlux
                    -1,                 // patchID
                    -1,                 // zoneID
                    false               // zoneFlip
                );
            }
        }
    }

    // Reclaim storage
    faces.setSize(0);
    owner.setSize(0);
    neighbour.setSize(0);


    // Modify mesh
    // ~~~~~~~~~~~

    autoPtr<mapPolyMesh> map = meshMod.changeMesh(mesh, false);

    // Zip-up the mesh if it contained hanging nodes
    if (hangingNodes)
    {
        Info<< "Zipping mesh to remove hanging nodes" << endl;
        polyMeshZipUpCells(mesh);
    }

    // Un-merge any merged cyclics
    if (args.optionFound("includedAngle"))
    {
        polyMeshUnMergeCyclics(mesh, args.optionRead<scalar>("includedAngle"));
    }
    else
    {
        polyMeshUnMergeCyclics(mesh);
    }

    mesh.setInstance(runTime.constant());

    // Set the precision of the points data to 10
    IOstream::defaultPrecision(max(10u, IOstream::defaultPrecision()));

    Info<< nl << "Writing mesh to " << mesh.relativeObjectPath() << endl;
    mesh.write();


    Info<< "\nEnd\n" << endl;
    return 0;
}


 /* ------------------------------------------------------------------------ *\
    ------ End of fluentMeshToFoam.L
 \* ------------------------------------------------------------------------ */
fluent3DMeshToFoam.L (43,055 bytes)   

kasperbilde

2022-03-09 09:45

reporter   ~0012526

Ah, that was quick! I have tested the updated mesh converter on .msh files generated by Fluent 2019 R3, Fluent 2020 R1 and Fluent 2021 R2. I, unfortunately, don't have licenses for previous versions. The updated converter works for all meshes tested.

henry

2022-03-09 11:14

manager   ~0012527

Resolved by commit 318f78b660ba2aa8dc5f4ed1e7643997f47546e6

Issue History

Date Modified Username Field Change
2022-03-08 13:34 kasperbilde New Issue
2022-03-08 13:34 kasperbilde File Added: fluent3DmeshToFoam_bug.msh
2022-03-08 13:49 henry Note Added: 0012520
2022-03-08 13:59 henry Note Edited: 0012520
2022-03-08 14:24 kasperbilde Note Added: 0012521
2022-03-08 14:38 henry Note Added: 0012522
2022-03-08 14:47 henry Note Added: 0012523
2022-03-08 18:32 kasperbilde File Added: fluent3DmeshToFoam_bug-2.msh
2022-03-08 18:32 kasperbilde Note Added: 0012524
2022-03-08 21:32 henry File Added: fluent3DMeshToFoam.L
2022-03-08 21:32 henry Note Added: 0012525
2022-03-09 09:45 kasperbilde Note Added: 0012526
2022-03-09 11:14 henry Category Bug => Feature
2022-03-09 11:14 henry Assigned To => henry
2022-03-09 11:14 henry Status new => resolved
2022-03-09 11:14 henry Resolution open => fixed
2022-03-09 11:14 henry Fixed in Version => dev
2022-03-09 11:14 henry Note Added: 0012527