#include "fesa/elements/element_factory.h" #include #include #include #include #include #include #include #include "fesa/elements/euler_beam_3d.h" #include "fesa/elements/hex8_element.h" #include "fesa/elements/mitc4_shell.h" #include "fesa/materials/material.h" #include "fesa/model/domain.h" #include "fesa/properties/element_property.h" namespace fesa { namespace { /// @brief Creates one deterministic factory failure for a semantic element. Result> FactoryFailure( const ElementDefinition& definition, const Domain& domain, std::string code, std::string message) { return Result>::Failure(Status::Failure( FailureCategory::kModel, {{Severity::kError, std::move(code), {domain.SourcePath(), 0U}, "*ELEMENT", definition.SourceId().source_label_text, std::move(message)}})); } /// @brief Returns the shared stable six-component node ordering. std::vector FullNodeComponents() { return {DofComponent::kUx, DofComponent::kUy, DofComponent::kUz, DofComponent::kUrx, DofComponent::kUry, DofComponent::kUrz}; } /// @brief Finds one element's unified stable Domain collection position. std::optional FindElementIndex(const ElementDefinition& definition, const Domain& domain) { for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { if (&domain.Elements()[index] == &definition) { return static_cast(index); } } return std::nullopt; } /// @brief Finds a Domain-owned B33 definition without performing a cast. std::optional FindBeamIndex(const ElementDefinition& definition, const Domain& domain) { for (std::size_t index = 0U; index < domain.BeamElements().Size(); ++index) { const ElementDefinition* candidate = &domain.BeamElements()[index]; if (candidate == &definition) { return static_cast(index); } } return std::nullopt; } /// @brief Finds a Domain-owned MITC4 definition without performing a cast. std::optional FindShellIndex(const ElementDefinition& definition, const Domain& domain) { for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) { const ElementDefinition* candidate = &domain.ShellElements()[index]; if (candidate == &definition) { return static_cast(index); } } return std::nullopt; } /// @brief Verifies every topology entry before indexing Domain nodes. bool HasValidNodes(const ElementDefinition& definition, const Domain& domain, const std::size_t expected_count) { if (definition.NodeIndices().size() != expected_count) { return false; } for (const EntityIndex node : definition.NodeIndices()) { if (node >= domain.Nodes().size()) { return false; } } return true; } } // namespace Result> ElementFactory::CreateBeam( const ElementDefinition& definition, const Domain& domain) const { const auto element_index = FindElementIndex(definition, domain); const auto beam_index = FindBeamIndex(definition, domain); if (!element_index.has_value() || !beam_index.has_value()) { return FactoryFailure( definition, domain, "invalid-element-definition", "The B33 definition must be owned by the supplied Domain."); } // The concrete access is valid only after the kind and Domain-owned // collection identity checks performed by Create and FindBeamIndex. const auto& beam_definition = static_cast(definition); if (!HasValidNodes(definition, domain, 2U)) { return FactoryFailure(definition, domain, "invalid-element-topology", "B33 requires two valid Domain node indices."); } if (definition.PropertyIndex() >= domain.Properties().Size() || definition.MaterialIndex() >= domain.Materials().Size()) { return FactoryFailure( definition, domain, "invalid-element-assignment", "B33 property and material indices must resolve in the Domain."); } const ElementProperty& property = domain.Properties()[definition.PropertyIndex()]; const Material& material = domain.Materials()[definition.MaterialIndex()]; if (property.Kind() != ElementPropertyKind::kGeneralBeamSection || material.Kind() != MaterialKind::kIsotropicLinearElastic) { return FactoryFailure( definition, domain, "incompatible-element-assignment", "B33 requires a general beam section and isotropic elastic material."); } // These casts are centralized after explicit discriminator validation. const auto& section = static_cast(property); const auto& elastic = static_cast(material); auto candidate = EulerBeam3D::Create( domain.Nodes()[definition.NodeIndices()[0U]], domain.Nodes()[definition.NodeIndices()[1U]], section, elastic); if (!candidate.HasValue()) { return Result>::Failure(candidate.GetStatus()); } ElementDofLayout layout{definition.SourceId(), definition.NodeIndices(), FullNodeComponents()}; std::vector node_source_ids; node_source_ids.reserve(2U); for (const EntityIndex node : definition.NodeIndices()) { node_source_ids.push_back(domain.Nodes()[node].source_id); } auto runtime = std::make_unique(std::move(candidate.Value())); runtime->BindRuntime(std::move(layout), *element_index, std::move(node_source_ids), beam_definition.location); std::unique_ptr result = std::move(runtime); return Result>::Success(std::move(result)); } Result> ElementFactory::CreateShell( const ElementDefinition& definition, const Domain& domain) const { const auto element_index = FindElementIndex(definition, domain); const auto shell_index = FindShellIndex(definition, domain); if (!element_index.has_value() || !shell_index.has_value()) { return FactoryFailure( definition, domain, "invalid-element-definition", "The MITC4 definition must be owned by the supplied Domain."); } // The concrete access is valid only after the kind and Domain-owned // collection identity checks performed by Create and FindShellIndex. const auto& shell_definition = static_cast(definition); if (!HasValidNodes(definition, domain, 4U)) { return FactoryFailure(definition, domain, "invalid-element-topology", "MITC4 requires four valid Domain node indices."); } if (definition.PropertyIndex() >= domain.Properties().Size() || definition.MaterialIndex() >= domain.Materials().Size()) { return FactoryFailure( definition, domain, "invalid-element-assignment", "MITC4 property and material indices must resolve in the Domain."); } const ElementProperty& property = domain.Properties()[definition.PropertyIndex()]; const Material& material = domain.Materials()[definition.MaterialIndex()]; if (property.Kind() != ElementPropertyKind::kShellSection || material.Kind() != MaterialKind::kIsotropicLinearElastic) { return FactoryFailure( definition, domain, "incompatible-element-assignment", "MITC4 requires a shell section and isotropic elastic material."); } const auto& section = static_cast(property); if (section.MaterialIndex() != definition.MaterialIndex()) { return FactoryFailure( definition, domain, "incompatible-element-assignment", "MITC4 definition and shell section must reference one material."); } // These casts are centralized after explicit discriminator validation. const auto& elastic = static_cast(material); std::vector frame_by_node(domain.Nodes().size(), nullptr); for (const auto& frame : domain.ShellNodeInitialFrames()) { if (frame.node_index >= frame_by_node.size() || frame_by_node[frame.node_index] != nullptr) { return FactoryFailure( definition, domain, "invalid-shell-frame", "Shell initial frames must map uniquely to valid Domain nodes."); } frame_by_node[frame.node_index] = &frame; } std::array nodes{}; std::array, 4> directors{}; for (std::size_t position = 0U; position < nodes.size(); ++position) { const EntityIndex node = definition.NodeIndices()[position]; const ShellNodeInitialFrame* frame = frame_by_node[node]; if (frame == nullptr) { return FactoryFailure( definition, domain, "missing-shell-frame", "MITC4 requires one initial frame for every element node."); } nodes[position] = &domain.Nodes()[node]; directors[position] = frame->director; } auto candidate = Mitc4Shell::Create(nodes, directors, section, elastic); if (!candidate.HasValue()) { return Result>::Failure(candidate.GetStatus()); } ElementDofLayout layout{definition.SourceId(), definition.NodeIndices(), FullNodeComponents()}; auto runtime = std::make_unique(std::move(candidate.Value())); runtime->BindRuntime(std::move(layout), *element_index, shell_definition.location); std::unique_ptr result = std::move(runtime); return Result>::Success(std::move(result)); } Result> ElementFactory::CreateSolid( const ElementDefinition& definition, const Domain& domain) const { const auto element_index = FindElementIndex(definition, domain); const Hex8Definition* solid_definition = nullptr; for (std::size_t index = 0U; index < domain.SolidElements().Size(); ++index) { const auto& solid = domain.SolidElements()[index]; if (&solid == &definition) { solid_definition = &solid; break; } } if (!element_index.has_value() || solid_definition == nullptr) { return FactoryFailure( definition, domain, "invalid-element-definition", "The C3D8 definition must be owned by the supplied Domain."); } if (!HasValidNodes(definition, domain, 8U)) { return FactoryFailure(definition, domain, "invalid-element-topology", "C3D8 requires eight valid Domain node indices."); } if (definition.PropertyIndex() >= domain.Properties().Size() || definition.MaterialIndex() >= domain.Materials().Size()) { return FactoryFailure( definition, domain, "invalid-element-assignment", "C3D8 property and material indices must resolve in the Domain."); } const auto& property = domain.Properties()[definition.PropertyIndex()]; const auto& material = domain.Materials()[definition.MaterialIndex()]; if (property.Kind() != ElementPropertyKind::kSolidSection || material.Kind() != MaterialKind::kIsotropicLinearElastic) { return FactoryFailure( definition, domain, "incompatible-element-assignment", "C3D8 requires a Solid section and isotropic elastic material."); } const auto& section = static_cast(property); if (section.MaterialIndex() != definition.MaterialIndex()) { return FactoryFailure( definition, domain, "incompatible-element-assignment", "C3D8 section and definition must select the same material."); } Hex8Element::Coordinates coordinates{}; for (std::size_t node = 0U; node < coordinates.size(); ++node) { coordinates[node] = domain.Nodes()[definition.NodeIndices()[node]].coordinates; } std::optional density; for (const auto& record : domain.Densities()) { if (record.material_index == definition.MaterialIndex()) { density = record.rho; break; } } ElementDofLayout layout{definition.SourceId(), definition.NodeIndices(), {DofComponent::kUx, DofComponent::kUy, DofComponent::kUz}}; auto candidate = Hex8Element::Create( *element_index, std::move(layout), coordinates, static_cast(material), density, solid_definition->location); if (!candidate.HasValue()) { return Result>::Failure(candidate.GetStatus()); } std::unique_ptr result = std::make_unique(std::move(candidate.Value())); return Result>::Success(std::move(result)); } Result> ElementFactory::Create( const ElementDefinition& definition, const Domain& domain) const { switch (definition.Kind()) { case ElementDefinitionKind::kEulerBeam3D: return CreateBeam(definition, domain); case ElementDefinitionKind::kMitc4Shell: return CreateShell(definition, domain); case ElementDefinitionKind::kHex8: return CreateSolid(definition, domain); } return FactoryFailure(definition, domain, "unsupported-element-definition", "The element definition kind is not supported."); } } // namespace fesa