Files
JuliaFEM.jl/ext/JuliaFEMGmshExt.jl
T
Jukka Aho 6657ec8419 feat(ext): add Gmsh and WriteVTK package extensions
Gmsh and WriteVTK remain optional weakdeps; these extensions implement the
typed mesh import and VTU export entry points used by examples and tests.

- Import linear Tris, Quads, Tets, Hexes from GMSH MSH with physical groups
- Export Mesh to VTU with optional point and cell named tuples
2026-05-11 02:57:38 +03:00

163 lines
5.5 KiB
Julia

# SPDX-FileCopyrightText: 2015-2026 Jukka Aho
# SPDX-License-Identifier: MIT
module JuliaFEMGmshExt
using JuliaFEM
using Gmsh
using Tensors: Vec
const _GMSH_LINEAR_TYPE = Dict{Int32,DataType}(
Int32(2) => Tri3,
Int32(3) => Quad4,
Int32(4) => Tet4,
Int32(5) => Hex8,
)
function _physical_symbol(name::AbstractString, dim::Integer, tag::Integer)::Symbol
s = strip(String(name))
if isempty(s)
return Symbol("physical_", dim, "_", tag)
end
symstr = replace(s, r"[^0-9a-zA-Z_]+" => "_")
if isempty(symstr)
return Symbol("physical_", dim, "_", tag)
end
return Symbol(symstr)
end
function _mesh_dim!(gmsh, dim::Union{Nothing,Int})::Int32
if dim === nothing
et3, _, _ = gmsh.model.mesh.getElements(3, -1)
if !isempty(et3)
return Int32(3)
end
et2, _, _ = gmsh.model.mesh.getElements(2, -1)
if !isempty(et2)
return Int32(2)
end
throw(ArgumentError("Gmsh model has no 2D or 3D mesh elements"))
end
dim == 2 || dim == 3 || throw(ArgumentError("dim must be 2 or 3, got $dim"))
return Int32(dim)
end
function _topology_from_elem_types(elem_types::Vector{Int32})::DataType
isempty(elem_types) && throw(ArgumentError("Gmsh returned no element types for the requested dimension"))
Ts = DataType[]
for gtyp in elem_types
T = get(_GMSH_LINEAR_TYPE, gtyp, nothing)
T === nothing && throw(
ArgumentError(
"Unsupported Gmsh element type $gtyp (only linear types " *
"$(join(sort!(collect(keys(_GMSH_LINEAR_TYPE))), ", ")) are supported)",
),
)
push!(Ts, T)
end
all(==(Ts[1]), Ts) ||
throw(ArgumentError("Mixed Gmsh element types in one dimension are not supported: $Ts"))
return Ts[1]
end
function _import_mesh(gmsh; dim::Union{Nothing,Int}, quiet::Bool)
mesh_dim = _mesh_dim!(gmsh, dim)
elem_types, elem_tags, elem_node_tags = gmsh.model.mesh.getElements(mesh_dim, -1)
Ttop = _topology_from_elem_types(elem_types)
node_tags, coord, _ = gmsh.model.mesh.getNodes()
length(node_tags) * 3 == length(coord) ||
throw(ArgumentError("Unexpected Gmsh node coordinate layout"))
n_nodes = length(node_tags)
tag_to_idx = Dict{UInt64,UInt32}()
nodes = Vector{Vec{3,Float64}}(undef, n_nodes)
@inbounds for i in 1:n_nodes
tag_to_idx[node_tags[i]] = UInt32(i)
base = 3 * (i - 1)
nodes[i] = Vec(coord[base + 1], coord[base + 2], coord[base + 3])
end
N = nnodes(Ttop())
connectivity = NTuple{N,UInt32}[]
elem_tag_to_julia = Dict{UInt64,UInt32}()
@inbounds for it in eachindex(elem_types)
tags_i = elem_tags[it]
nt_i = elem_node_tags[it]
n_elem = length(tags_i)
expected_len = n_elem * N
length(nt_i) == expected_len ||
throw(ArgumentError("Gmsh node tag list length mismatch for element type $(elem_types[it])"))
for e in 1:n_elem
julia_e = UInt32(length(connectivity) + 1)
elem_tag_to_julia[tags_i[e]] = julia_e
offs = N * (e - 1)
conn = ntuple(N) do k
tag = nt_i[offs + k]
idx = get(tag_to_idx, tag, nothing)
idx === nothing && throw(ArgumentError("Unknown node tag $tag in element connectivity"))
idx
end
push!(connectivity, conn)
end
end
element_sets = Dict{Symbol,Set{UInt32}}(:all => Set(UInt32(1):UInt32(length(connectivity))))
node_sets = Dict{Symbol,Set{UInt32}}(:all => Set(UInt32(1):UInt32(n_nodes)))
for (pdim, ptag) in gmsh.model.getPhysicalGroups()
name = gmsh.model.getPhysicalName(Int(pdim), Int(ptag))
sym = _physical_symbol(name, pdim, ptag)
if pdim == mesh_dim
acc = get!(Set{UInt32}, element_sets, sym)
for ent in gmsh.model.getEntitiesForPhysicalGroup(Int(pdim), Int(ptag))
_, e_tags_b, _ = gmsh.model.mesh.getElements(Int(pdim), Int(ent))
for it in eachindex(e_tags_b)
for etag in e_tags_b[it]
ji = get(elem_tag_to_julia, etag, nothing)
ji === nothing || push!(acc, ji)
end
end
end
elseif pdim == mesh_dim - 1
acc = get!(Set{UInt32}, node_sets, sym)
for ent in gmsh.model.getEntitiesForPhysicalGroup(Int(pdim), Int(ptag))
ntags_b, _, _ = gmsh.model.mesh.getNodes(Int(pdim), Int(ent))
for t in ntags_b
ni = get(tag_to_idx, t, nothing)
ni === nothing || push!(acc, ni)
end
end
end
end
for d in (element_sets, node_sets)
for k in collect(keys(d))
k === :all && continue
isempty(d[k]) && delete!(d, k)
end
end
return Mesh{N,Ttop}(nodes, connectivity, element_sets, node_sets)
end
function JuliaFEM.read_gmsh_msh(path::AbstractString; dim=nothing, quiet::Bool=true)
argv = quiet ? String["-v", "0"] : String[]
started = Gmsh.initialize(argv; finalize_atexit=false)
try
Gmsh.gmsh.open(String(path))
return _import_mesh(Gmsh.gmsh; dim, quiet)
finally
started && Gmsh.finalize()
end
end
function JuliaFEM.mesh_from_current_gmsh_model(; dim=nothing, quiet::Bool=true)
Bool(Gmsh.gmsh.isInitialized()) ||
throw(ArgumentError("Gmsh is not initialized; call Gmsh.initialize first"))
_ = quiet
return _import_mesh(Gmsh.gmsh; dim, quiet)
end
end # module