///|
/// Source indices and local facet indices are zero-based; tags are unchanged.
/// explicit_element=0 means a new facet, not a source element with tag zero.
pub(all) struct BoundarySource {
element : Int
owner : Int
owner_index : Int
local_facet : Int
explicit_element : Int
} derive(ToJson)
///|
pub extend BoundarySource with ToJson::{to_json}
///|
pub(all) struct BoundaryNode {
tag : Int
source_index : Int
} derive(ToJson)
///|
pub extend BoundaryNode with ToJson::{to_json}
///|
pub(all) struct BoundaryField {
source_index : Int
input_entries : Int
retained_entries : Int
} derive(ToJson)
///|
pub extend BoundaryField with ToJson::{to_json}
///|
pub(all) struct BoundaryReport {
source_dimension : Int
boundary_dimension : Int
selected_elements : Int
nodes : Array[BoundaryNode]
facets : Array[BoundarySource]
fields : Array[BoundaryField]
losses : Array[String]
} derive(ToJson)
///|
pub extend BoundaryReport with ToJson::{to_json}
///|
/// Owned result. Inspect the loss report before requesting the derived mesh.
pub struct Boundary {
priv output : Mesh
priv summary : BoundaryReport
}
///|
pub fn Boundary::report(self : Boundary) -> BoundaryReport {
{
..self.summary,
nodes: self.summary.nodes.copy(),
facets: self.summary.facets.copy(),
fields: self.summary.fields.copy(),
losses: self.summary.losses.copy(),
}
}
///|
/// A derived, unclassified 2.2 model: matched facet labels and sparse fields
/// survive. Entity graphs/parameters and excluded data are never silently lost.
/// Later encode/VTK/OBJ calls apply their own independent loss gates too.
pub fn Boundary::mesh(
self : Boundary,
allow_loss? : Bool = false,
) -> Mesh raise {
if !allow_loss && !self.summary.losses.is_empty() {
bad(
"boundary extraction requires allow_loss: " +
self.summary.losses.join(", "),
)
}
self.output
}
///|
fn facet_key(nodes : Array[Int]) -> String {
let sorted = nodes.copy()
sorted.sort()
id_key(sorted)
}
///|
/// Same polygon up to rotation/reversal, not merely the same node set.
fn same_cycle(a : Array[Int], b : Array[Int]) -> Bool {
if a.length() != b.length() {
return false
}
for offset in 0.. Boundary raise {
if !self.unknown.is_empty() {
bad("boundary extraction requires explicit removal of opaque sections")
}
if max_facets < 0 || max_facets > 1000000 {
bad("max_facets must be 0..1000000")
}
let mut dim = dimension
if dim == -1 {
for e in self.es {
let d = element_shape(e.kind).0
if d > dim {
dim = d
}
}
}
if dim < 1 || dim > 3 {
bad("boundary source dimension must be 1, 2 or 3")
}
let seen : Map[String, Bool] = Map([])
let explicit : Map[String, Array[Int]] = Map([])
let mut selected = 0
for i, e in self.es {
let d = element_shape(e.kind).0
if d != dim && d != dim - 1 {
continue
}
ignore(local_facets(e.kind)) // Reject curvature, never strip midside nodes.
let sorted = e.nodes.copy()
sorted.sort()
for j in 1.. ids.push(i)
None => explicit[key] = [i]
}
}
}
if selected == 0 {
bad("no elements at boundary source dimension")
}
let topology = self.topology(dimension=dim)
if !topology.nonmanifold.is_empty() {
bad("nonmanifold incidence in boundary extraction")
}
if topology.boundary.length() > max_facets {
bad("boundary facet budget exceeded; no partial mesh returned")
}
let es : Array[Element] = []
let sources : Array[BoundarySource] = []
let used : Map[Int, Bool] = Map([])
let retained : Map[Int, Bool] = Map([])
let mut next = 1
for f in topology.boundary {
let owner_index = self.ei[f.owners[0]]
let owner = self.es[owner_index]
let mut local_facet = -1
for i, corners in local_facets(owner.kind) {
if corners.map(j => owner.nodes[j]) == f.nodes {
local_facet = i
break
}
}
let matches = explicit.get(facet_key(f.nodes)).unwrap_or([])
if matches.length() > 1 {
bad("ambiguous explicit boundary facet")
}
let mut explicit_element = 0
let e = if matches.length() == 1 {
let e = self.es[matches[0]]
if !same_cycle(e.nodes, f.nodes) {
bad("explicit facet has incompatible polygon order")
}
explicit_element = e.tag
retained[e.tag] = true
e
} else {
// At most 1M occupied source tags + 1M output tags: no signed-32 overflow,
// including inputs that already contain the maximum tag 2147483647.
while self.ei.contains(next) {
next = next + 1
}
let tag = next
next = next + 1
Element::{
tag,
kind: match f.nodes.length() {
1 => 15
2 => 1
3 => 2
_ => 3
},
nodes: f.nodes.copy(),
entity: 0,
physical: [],
extra: [],
}
}
es.push(e)
for tag in e.nodes {
used[tag] = true
}
sources.push({
element: e.tag,
owner: owner.tag,
owner_index,
local_facet,
explicit_element,
})
}
let node_map : Array[BoundaryNode] = []
let ns : Array[Node] = []
let mut classified = false
for i, n in self.ns {
if used.contains(n.tag) {
node_map.push({ tag: n.tag, source_index: i, })
classified = classified || n.dim != -1 || !n.parameters.is_empty()
ns.push({ ..n, dim: -1, entity: 0, parameters: [], })
}
}
let fields : Array[BoundaryField] = []
let fs : Array[Field] = []
let losses : Array[String] = []
if !self.ents.is_empty() || classified {
losses.push("entity_graph_and_node_classification")
}
if self.es.any(e => {
!retained.contains(e.tag) &&
(e.entity != 0 || !e.physical.is_empty() || !e.extra.is_empty())
}) {
losses.push("excluded_element_labels_not_inherited")
}
for i, f in self.fs {
let entries = f.entries.filter(d => {
if f.location == "NodeData" {
used.contains(d.tag)
} else {
retained.contains(d.tag)
}
})
fields.push({
source_index: i,
input_entries: f.entries.length(),
retained_entries: entries.length(),
})
if entries.length() != f.entries.length() {
losses.push("field_entries:\{i}")
}
let integers = f.integers.copy()
integers[2] = entries.length()
fs.push({ ..f, entries, integers, })
}
let physical : Map[Int, Bool] = Map([])
for e in es {
for tag in e.physical {
physical[tag] = true
}
}
let names = self.names.filter(n => {
n.dim == dim - 1 && physical.contains(n.tag)
})
if names.length() != self.names.length() {
losses.push("unused_physical_names")
}
{
output: mesh(ns, es, names~, fields=fs),
summary: {
source_dimension: dim,
boundary_dimension: dim - 1,
selected_elements: selected,
nodes: node_map,
facets: sources,
fields,
losses,
},
}
}