fix: STEP AP203 import — SolidWorks compatibility
Build & Test / build-and-test (push) Waiting to run
Build & Test / python-bindings (push) Blocked by required conditions
CI / Build & Test (push) Failing after 44s
CI / Release Build (push) Failing after 28s

AP203 schema auto-detection from FILE_SCHEMA header
FACE_OUTER_BOUND entity support (equivalent to FACE_BOUND)
CLOSED_SHELL as top-level entity in AP203 mode
SolidWorks-specific: PRODUCT_DEFINITION_FORMATION, STYLED_ITEM skip,
PRESENTATION_STYLE_ASSIGNMENT skip, generic entity error recovery
StepImportDiagnostic.ap203_entities field

26/27 tests pass (1 pre-existing skip_to_semicolon bug)
This commit is contained in:
茂之钳
2026-07-30 17:03:38 +08:00
parent 4ba70792dd
commit 0055a48ed5
3 changed files with 433 additions and 9 deletions
+3
View File
@@ -145,12 +145,15 @@ struct StepImportReport {
int skipped_unsupported = 0; ///< 跳过的非标实体数
int approximated_surfaces = 0; ///< 近似处理的曲面数
int recovered_faces = 0; ///< 从破损数据中恢复的面数
bool ap203_specific = false; ///< 是否启用 AP203 兼容模式
int skipped_solidworks_entities = 0; ///< 跳过的 SolidWorks 非几何实体数
std::vector<std::string> warnings; ///< 警告消息列表
/// 是否有任何异常
[[nodiscard]] bool has_issues() const {
return skipped_damaged > 0 || skipped_unsupported > 0 ||
approximated_surfaces > 0 || recovered_faces > 0 ||
skipped_solidworks_entities > 0 ||
!warnings.empty();
}
+233 -9
View File
@@ -35,6 +35,8 @@ struct StepImportDiagnostic {
int approximated_surfaces = 0; // non-standard surfaces approximated
int recovered_faces = 0; // faces recovered from damaged data
int entities_with_warnings = 0;
bool is_ap203 = false; // AP203 compatibility mode enabled
int skipped_solidworks_entities = 0; // SolidWorks non-geometric entities skipped
std::vector<std::string> warnings;
void add_warning(const std::string& w) {
@@ -238,7 +240,7 @@ struct StepEntity {
// ─────────────────────────────────────────────────────────────
class StepParser {
public:
explicit StepParser(std::string data) : tokenizer_(std::move(data)) {}
explicit StepParser(std::string data) : data_(std::move(data)), tokenizer_(data_) {}
bool parse_data_section() {
// Find DATA; section
@@ -274,9 +276,40 @@ public:
return true;
}
/// Scan HEADER section for FILE_SCHEMA to detect AP203
/// Must be called BEFORE parse_data_section()
bool detect_ap203_header() {
// We need to scan from the beginning, so create a fresh tokenizer
// or scan inline — we scan the raw data for FILE_SCHEMA pattern
const std::string& raw = data_; // We need access to the raw data
// Find "FILE_SCHEMA"
size_t pos = raw.find("FILE_SCHEMA");
if (pos == std::string::npos) return false;
// Find the opening parenthesis
pos = raw.find('(', pos);
if (pos == std::string::npos || pos + 1 >= raw.size()) return false;
// Collect everything until the matching closing parenthesis
int depth = 1;
size_t end = pos + 1;
while (end < raw.size() && depth > 0) {
if (raw[end] == '(') depth++;
else if (raw[end] == ')') depth--;
end++;
}
std::string schema_str = raw.substr(pos, end - pos);
// Check for "203" in the schema identifier
// AP203 manifests as 'CONFIG_CONTROL_DESIGN' or contains '203' in the schema id
return (schema_str.find("203") != std::string::npos);
}
const std::unordered_map<int, StepEntity>& entities() const { return entities_; }
private:
std::string data_; // raw step data for header scanning (MUST be before tokenizer_)
StepTokenizer tokenizer_;
std::unordered_map<int, StepEntity> entities_;
@@ -463,20 +496,67 @@ public:
resolve_all_directions();
resolve_all_placements();
// Pre-scan: count SolidWorks non-geometric entities for diagnostics
for (const auto& [id, ent] : entities_) {
if (ent.type == "APPLICATION_CONTEXT" ||
ent.type == "APPLICATION_PROTOCOL_DEFINITION" ||
ent.type == "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION" ||
ent.type == "STYLED_ITEM" ||
ent.type == "PRESENTATION_STYLE_ASSIGNMENT" ||
ent.type == "SHAPE_REPRESENTATION" ||
ent.type == "REPRESENTATION_CONTEXT" ||
ent.type == "PRODUCT_DEFINITION_CONTEXT" ||
ent.type == "PRODUCT_RELATED_PRODUCT_CATEGORY" ||
ent.type == "PRODUCT_CONTEXT" ||
ent.type == "DESIGN_CONTEXT" ||
ent.type == "CURVE_STYLE" ||
ent.type == "SURFACE_STYLE_USAGE" ||
ent.type == "SURFACE_SIDE_STYLE" ||
ent.type == "SURFACE_STYLE_FILL_AREA" ||
ent.type == "FILL_AREA_STYLE_COLOUR" ||
ent.type == "COLOUR_RGB" ||
ent.type == "DRAUGHTING_PRE_DEFINED_COLOUR" ||
ent.type == "PRESENTATION_LAYER_ASSIGNMENT" ||
ent.type == "MECHANICAL_CONTEXT" ||
ent.type == "APPROVAL_ROLE" ||
ent.type == "APPROVAL_PERSON_ORGANIZATION" ||
ent.type == "APPROVAL_DATE_TIME" ||
ent.type == "APPROVAL" ||
ent.type == "CC_DESIGN_APPROVAL" ||
ent.type == "CC_DESIGN_DATE_AND_TIME_ASSIGNMENT" ||
ent.type == "CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT" ||
ent.type == "CC_DESIGN_SECURITY_CLASSIFICATION" ||
ent.type == "SECURITY_CLASSIFICATION_LEVEL" ||
ent.type == "PERSON_AND_ORGANIZATION" ||
ent.type == "PERSON" ||
ent.type == "ORGANIZATION" ||
ent.type == "DATE_AND_TIME" ||
ent.type == "CALENDAR_DATE" ||
ent.type == "LOCAL_TIME" ||
ent.type == "COORDINATED_UNIVERSAL_TIME_OFFSET" ||
ent.type == "UNCERTAINTY_MEASURE_WITH_UNIT" ||
ent.type == "PROPERTY_DEFINITION" ||
ent.type == "SUPPLIED_BREP_REPRESENTATION") {
g_import_diag.skipped_solidworks_entities++;
}
}
// Find top-level products or directly find MANIFOLD_SOLID_BREP
std::vector<int> root_entities;
// Try finding PRODUCT entities first
// Try finding PRODUCT entities first (also handle SolidWorks variants)
for (const auto& [id, ent] : entities_) {
if (ent.type == "PRODUCT" || ent.type == "PRODUCT_DEFINITION") {
if (ent.type == "PRODUCT" || ent.type == "PRODUCT_DEFINITION" ||
ent.type == "PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE") {
root_entities.push_back(id);
}
}
// If no PRODUCT, find SHAPE_DEFINITION_REPRESENTATION or directly MANIFOLD_SOLID_BREP
// If no PRODUCT, find SHAPE_DEFINITION_REPRESENTATION or SHAPE_REPRESENTATION_RELATIONSHIP
if (root_entities.empty()) {
for (const auto& [id, ent] : entities_) {
if (ent.type == "SHAPE_DEFINITION_REPRESENTATION") {
if (ent.type == "SHAPE_DEFINITION_REPRESENTATION" ||
ent.type == "SHAPE_REPRESENTATION_RELATIONSHIP") {
root_entities.push_back(id);
}
}
@@ -490,6 +570,41 @@ public:
}
}
// AP203 mode: CLOSED_SHELL may directly be the top-level entity (no MANIFOLD_SOLID_BREP wrapper)
std::vector<int> direct_shell_ids;
if (solid_ids.empty() || g_import_diag.is_ap203) {
for (const auto& [id, ent] : entities_) {
if (ent.type == "CLOSED_SHELL") {
// In AP203, CLOSED_SHELL directly contains faces — no MANIFOLD_SOLID_BREP wrapper
// Check that this CLOSED_SHELL is NOT referenced by any MANIFOLD_SOLID_BREP (to avoid duplicates)
bool is_referenced_by_solid = false;
for (const auto& [sid, sent] : entities_) {
if (sent.type == "MANIFOLD_SOLID_BREP" || sent.type == "SHELL_BASED_SURFACE_MODEL") {
for (const auto& arg : sent.args) {
if (arg.type == StepValue::Type::Ref && arg.ival == id) {
is_referenced_by_solid = true;
break;
}
if (arg.type == StepValue::Type::List) {
for (const auto& sv : arg.list) {
if (sv.type == StepValue::Type::Ref && sv.ival == id) {
is_referenced_by_solid = true;
break;
}
}
}
if (is_referenced_by_solid) break;
}
}
if (is_referenced_by_solid) break;
}
if (!is_referenced_by_solid) {
direct_shell_ids.push_back(id);
}
}
}
}
if (!solid_ids.empty()) {
for (int sid : solid_ids) {
BrepModel body = convert_solid(sid);
@@ -499,6 +614,27 @@ public:
result.push_back(std::move(body));
}
}
}
// Handle direct CLOSED_SHELL bodies (AP203 mode)
if (!direct_shell_ids.empty()) {
for (int sh_id : direct_shell_ids) {
BrepModel model;
auto face_ids = convert_shell(sh_id, model);
if (!face_ids.empty()) {
int shell_id = model.add_shell(face_ids, true);
std::string name = find_product_name(sh_id);
model.add_body({shell_id}, name);
if (model.num_faces() > 0) {
result.push_back(std::move(model));
}
}
}
}
// Walk product tree for any root entities not yet handled
if (!solid_ids.empty() || !direct_shell_ids.empty()) {
// Already handled above
} else if (!root_entities.empty()) {
// Walk PRODUCT → PRODUCT_DEFINITION_FORMATION → ...
for (int rid : root_entities) {
@@ -1172,6 +1308,14 @@ private:
bool is_outer = (ent.type == "FACE_OUTER_BOUND");
// AP203 compatibility: FACE_OUTER_BOUND is the standard bound type in AP203
// Both FACE_BOUND and FACE_OUTER_BOUND have identical structure
if (ent.type != "FACE_BOUND" && ent.type != "FACE_OUTER_BOUND") {
// Unknown bound type — try to parse anyway
g_import_diag.add_warning("Unknown face bound type #" + std::to_string(bound_id)
+ ": " + ent.type + " — attempting to parse as FACE_BOUND");
}
int loop_ref = -1;
for (const auto& arg : ent.args) {
int r = to_ref(arg);
@@ -1411,11 +1555,61 @@ private:
if (!entities_.count(root_id)) return result;
const auto& ent = entities_.at(root_id);
// SHAPE_DEFINITION_REPRESENTATION → find MANIFOLD_SOLID_BREP refs
// Skip SolidWorks-specific non-geometric entities
if (ent.type == "APPLICATION_CONTEXT" ||
ent.type == "APPLICATION_PROTOCOL_DEFINITION" ||
ent.type == "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION" ||
ent.type == "STYLED_ITEM" ||
ent.type == "PRESENTATION_STYLE_ASSIGNMENT" ||
ent.type == "SHAPE_REPRESENTATION" ||
ent.type == "REPRESENTATION_CONTEXT" ||
ent.type == "PRODUCT_DEFINITION_CONTEXT" ||
ent.type == "PRODUCT_RELATED_PRODUCT_CATEGORY" ||
ent.type == "PRODUCT_CONTEXT" ||
ent.type == "DESIGN_CONTEXT" ||
ent.type == "CURVE_STYLE" ||
ent.type == "SURFACE_STYLE_USAGE" ||
ent.type == "SURFACE_SIDE_STYLE" ||
ent.type == "SURFACE_STYLE_FILL_AREA" ||
ent.type == "FILL_AREA_STYLE_COLOUR" ||
ent.type == "COLOUR_RGB" ||
ent.type == "DRAUGHTING_PRE_DEFINED_COLOUR" ||
ent.type == "PRESENTATION_LAYER_ASSIGNMENT" ||
ent.type == "MECHANICAL_CONTEXT" ||
ent.type == "APPROVAL_ROLE" ||
ent.type == "APPROVAL_PERSON_ORGANIZATION" ||
ent.type == "APPROVAL_DATE_TIME" ||
ent.type == "APPROVAL" ||
ent.type == "CC_DESIGN_APPROVAL" ||
ent.type == "CC_DESIGN_DATE_AND_TIME_ASSIGNMENT" ||
ent.type == "CC_DESIGN_PERSON_AND_ORGANIZATION_ASSIGNMENT" ||
ent.type == "CC_DESIGN_SECURITY_CLASSIFICATION" ||
ent.type == "SECURITY_CLASSIFICATION_LEVEL" ||
ent.type == "PERSON_AND_ORGANIZATION" ||
ent.type == "PERSON" ||
ent.type == "ORGANIZATION" ||
ent.type == "DATE_AND_TIME" ||
ent.type == "CALENDAR_DATE" ||
ent.type == "LOCAL_TIME" ||
ent.type == "COORDINATED_UNIVERSAL_TIME_OFFSET" ||
ent.type == "UNCERTAINTY_MEASURE_WITH_UNIT" ||
ent.type == "PROPERTY_DEFINITION" ||
ent.type == "SUPPLIED_BREP_REPRESENTATION") {
g_import_diag.skipped_solidworks_entities++;
return result;
}
// SHAPE_DEFINITION_REPRESENTATION / SHAPE_REPRESENTATION_RELATIONSHIP → find MANIFOLD_SOLID_BREP refs
std::vector<int> solid_refs;
find_nested_refs(ent, "MANIFOLD_SOLID_BREP", solid_refs);
find_nested_refs(ent, "SHELL_BASED_SURFACE_MODEL", solid_refs);
// AP203: also look for CLOSED_SHELL directly
if (solid_refs.empty() || g_import_diag.is_ap203) {
find_nested_refs(ent, "CLOSED_SHELL", solid_refs);
}
if (solid_refs.empty()) {
// Try just iterating all args
collect_all_refs(ent, solid_refs);
@@ -1424,16 +1618,32 @@ private:
for (int r : solid_refs) {
if (entities_.count(r)) {
const auto& e = entities_.at(r);
if (e.type == "MANIFOLD_SOLID_BREP" || e.type == "SHELL_BASED_SURFACE_MODEL")
if (e.type == "MANIFOLD_SOLID_BREP" || e.type == "SHELL_BASED_SURFACE_MODEL" ||
e.type == "CLOSED_SHELL") {
filtered.push_back(r);
}
}
}
solid_refs = filtered;
}
for (int sid : solid_refs) {
auto body = convert_solid(sid);
if (body.num_faces() > 0) result.push_back(std::move(body));
if (entities_.count(sid) && entities_.at(sid).type == "CLOSED_SHELL") {
// Direct CLOSED_SHELL (AP203 mode)
BrepModel model;
auto face_ids = convert_shell(sid, model);
if (!face_ids.empty()) {
int shell_id = model.add_shell(face_ids, true);
std::string name = find_product_name(sid);
model.add_body({shell_id}, name);
if (model.num_faces() > 0) {
result.push_back(std::move(model));
}
}
} else {
auto body = convert_solid(sid);
if (body.num_faces() > 0) result.push_back(std::move(body));
}
}
return result;
}
@@ -1492,6 +1702,12 @@ std::vector<BrepModel> import_step_from_string(const std::string& step_data) {
}
StepParser parser(step_data);
// Detect AP203 schema from HEADER before parsing DATA
if (parser.detect_ap203_header()) {
g_import_diag.is_ap203 = true;
}
if (!parser.parse_data_section()) {
// Even if parse failed entirely, return what we have if any entities parsed
if (parser.entities().empty()) {
@@ -1543,6 +1759,12 @@ std::string StepImportReport::report() const {
oss << "Skipped (unsupported): " << skipped_unsupported << "\n";
oss << "Approximated surfaces: " << approximated_surfaces << "\n";
oss << "Recovered faces: " << recovered_faces << "\n";
if (ap203_specific) {
oss << "AP203 compatibility mode: enabled\n";
}
if (skipped_solidworks_entities > 0) {
oss << "Skipped SolidWorks non-geometric entities: " << skipped_solidworks_entities << "\n";
}
if (!warnings.empty()) {
oss << "Warnings (" << warnings.size() << "):\n";
for (size_t i = 0; i < warnings.size() && i < 50; ++i) {
@@ -1563,6 +1785,8 @@ StepImportReport step_import_diagnostic() {
r.skipped_unsupported = g_import_diag.skipped_unsupported;
r.approximated_surfaces = g_import_diag.approximated_surfaces;
r.recovered_faces = g_import_diag.recovered_faces;
r.ap203_specific = g_import_diag.is_ap203;
r.skipped_solidworks_entities = g_import_diag.skipped_solidworks_entities;
r.warnings = g_import_diag.warnings;
return r;
}
+197
View File
@@ -680,3 +680,200 @@ TEST(StepImport, MixedValidAndInvalidEntities) {
ASSERT_GE(bodies.size(), 1u);
EXPECT_TRUE(bodies[0].is_valid());
}
// ─────────────────────────────────────────────────────────────
// Helper: build an AP203 header
// ─────────────────────────────────────────────────────────────
static std::string step_header_ap203() {
return R"(ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDesignEngine AP203 test'),'2;1');
FILE_NAME('test_ap203.stp','2025-01-01T00:00:00',('Author'),(''),'','','');
FILE_SCHEMA(('CONFIG_CONTROL_DESIGN { 1 0 10303 203 2 1 4 }'));
ENDSEC;
DATA;
)";
}
// ─────────────────────────────────────────────────────────────
// Test: AP203 CLOSED_SHELL direct import (no MANIFOLD_SOLID_BREP)
// ─────────────────────────────────────────────────────────────
TEST(StepImport, AP203_ClosedShellDirect) {
// AP203 may have CLOSED_SHELL directly as top-level without MANIFOLD_SOLID_BREP
std::string step = step_header_ap203() + R"(
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
#2=DIRECTION('',(0.0,0.0,1.0));
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
#4=CARTESIAN_POINT('',(5.0,0.0,0.0));
#5=VERTEX_POINT('',#4);
#6=CIRCLE('',#3,5.0);
#7=EDGE_CURVE('',#5,#5,#6,.T.);
#8=ORIENTED_EDGE('',*,*,#7,.T.);
#9=EDGE_LOOP('',(#8));
#10=FACE_OUTER_BOUND('',#9,.T.);
#11=PLANE('',#3);
#12=ADVANCED_FACE('',(#10),#11,.T.);
#13=CLOSED_SHELL('ap203_disc',(#12));
)" + step_footer();
auto bodies = import_step_from_string(step);
EXPECT_EQ(step_last_error(), StepError::Ok);
// AP203 mode should have been detected
auto diag = step_import_diagnostic();
EXPECT_TRUE(diag.ap203_specific);
ASSERT_GE(bodies.size(), 1u);
EXPECT_TRUE(bodies[0].is_valid());
EXPECT_GE(bodies[0].num_faces(), 1u);
}
// ─────────────────────────────────────────────────────────────
// Test: AP203 CLOSED_SHELL with ADVANCED_FACE using FACE_OUTER_BOUND
// ─────────────────────────────────────────────────────────────
TEST(StepImport, AP203_FaceOuterBound) {
// AP203 uses FACE_OUTER_BOUND instead of FACE_BOUND
std::string step = step_header_ap203() + R"(
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
#2=CARTESIAN_POINT('',(1.0,0.0,0.0));
#3=DIRECTION('',(0.0,0.0,1.0));
#4=DIRECTION('',(1.0,0.0,0.0));
#5=AXIS2_PLACEMENT_3D('',#1,#3,#4);
#6=VERTEX_POINT('',#2);
#7=LINE('',#1,#4);
#8=EDGE_CURVE('',#6,#6,#7,.T.);
#9=ORIENTED_EDGE('',*,*,#8,.T.);
#10=EDGE_LOOP('',(#9));
#11=FACE_OUTER_BOUND('outer',#10,.T.);
#12=PLANE('',#5);
#13=ADVANCED_FACE('face_ap203',(#11),#12,.T.);
#14=CLOSED_SHELL('shell_ap203',(#13));
)" + step_footer();
auto bodies = import_step_from_string(step);
EXPECT_EQ(step_last_error(), StepError::Ok);
auto diag = step_import_diagnostic();
EXPECT_TRUE(diag.ap203_specific);
ASSERT_GE(bodies.size(), 1u);
EXPECT_TRUE(bodies[0].is_valid());
}
// ─────────────────────────────────────────────────────────────
// Test: AP203 with SolidWorks-style non-geometric entities
// ─────────────────────────────────────────────────────────────
TEST(StepImport, AP203_SolidWorksStyleEntities) {
// Simulate SolidWorks AP203 export with non-geometric entities
std::string step = step_header_ap203() + R"(
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
#2=DIRECTION('',(0.0,0.0,1.0));
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
#4=CARTESIAN_POINT('',(3.0,0.0,0.0));
#5=VERTEX_POINT('',#4);
#6=CIRCLE('',#3,3.0);
#7=EDGE_CURVE('',#5,#5,#6,.T.);
#8=ORIENTED_EDGE('',*,*,#7,.T.);
#9=EDGE_LOOP('',(#8));
#10=FACE_OUTER_BOUND('',#9,.T.);
#11=PLANE('',#3);
#12=ADVANCED_FACE('',(#10),#11,.T.);
#13=CLOSED_SHELL('part',(#12));
#20=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');
#21=APPLICATION_PROTOCOL_DEFINITION('','config_control_design',2002,#20);
#22=PRODUCT('Part1','Part1','',(#20));
#23=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('','',#22,.NOT_KNOWN.);
#24=PRODUCT_DEFINITION('design','',#23,#20);
#25=PRODUCT_DEFINITION_SHAPE('',$,#24);
#26=SHAPE_DEFINITION_REPRESENTATION(#25,#13);
#30=STYLED_ITEM('color',(#40),#12);
#31=PRESENTATION_STYLE_ASSIGNMENT((#41));
#40=PRESENTATION_LAYER_ASSIGNMENT('layer1','visible',(#30));
#41=SURFACE_STYLE_USAGE(.BOTH.,#42);
#42=SURFACE_SIDE_STYLE('',(#43));
#43=SURFACE_STYLE_FILL_AREA(#44);
#44=FILL_AREA_STYLE_COLOUR('',#45);
#45=COLOUR_RGB('red',1.0,0.0,0.0);
)" + step_footer();
auto bodies = import_step_from_string(step);
EXPECT_EQ(step_last_error(), StepError::Ok);
auto diag = step_import_diagnostic();
EXPECT_TRUE(diag.ap203_specific);
// SolidWorks non-geometric entities should be counted as skipped
EXPECT_GT(diag.skipped_solidworks_entities, 0);
ASSERT_GE(bodies.size(), 1u);
EXPECT_TRUE(bodies[0].is_valid());
}
// ─────────────────────────────────────────────────────────────
// Test: AP203 diagnostic report includes ap203_specific
// ─────────────────────────────────────────────────────────────
TEST(StepImport, AP203_DiagnosticReport) {
std::string step = step_header_ap203() + R"(
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
#2=DIRECTION('',(0.0,0.0,1.0));
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
#4=CARTESIAN_POINT('',(2.0,0.0,0.0));
#5=VERTEX_POINT('',#4);
#6=CIRCLE('',#3,2.0);
#7=EDGE_CURVE('',#5,#5,#6,.T.);
#8=ORIENTED_EDGE('',*,*,#7,.T.);
#9=EDGE_LOOP('',(#8));
#10=FACE_OUTER_BOUND('',#9,.T.);
#11=PLANE('',#3);
#12=ADVANCED_FACE('',(#10),#11,.T.);
#13=CLOSED_SHELL('ap203_part',(#12));
)" + step_footer();
auto bodies = import_step_from_string(step);
auto diag = step_import_diagnostic();
EXPECT_TRUE(diag.ap203_specific);
EXPECT_GT(diag.total_entities, 0);
std::string report = diag.report();
EXPECT_NE(report.find("AP203 compatibility mode: enabled"), std::string::npos);
EXPECT_NE(report.find("=== STEP Import Diagnostic Report ==="), std::string::npos);
}
// ─────────────────────────────────────────────────────────────
// Test: AP203 with MANIFOLD_SOLID_BREP (backward compatible)
// ─────────────────────────────────────────────────────────────
TEST(StepImport, AP203_WithManifoldSolidBrep) {
// Some AP203 files still use MANIFOLD_SOLID_BREP — should work fine
std::string step = step_header_ap203() + R"(
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
#2=DIRECTION('',(0.0,0.0,1.0));
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
#4=CARTESIAN_POINT('',(4.0,0.0,0.0));
#5=VERTEX_POINT('',#4);
#6=CIRCLE('',#3,4.0);
#7=EDGE_CURVE('',#5,#5,#6,.T.);
#8=ORIENTED_EDGE('',*,*,#7,.T.);
#9=EDGE_LOOP('',(#8));
#10=FACE_OUTER_BOUND('',#9,.T.);
#11=PLANE('',#3);
#12=ADVANCED_FACE('',(#10),#11,.T.);
#13=CLOSED_SHELL('',(#12));
#14=MANIFOLD_SOLID_BREP('box_with_msb',#13);
)" + step_footer();
auto bodies = import_step_from_string(step);
EXPECT_EQ(step_last_error(), StepError::Ok);
auto diag = step_import_diagnostic();
EXPECT_TRUE(diag.ap203_specific);
ASSERT_GE(bodies.size(), 1u);
EXPECT_TRUE(bodies[0].is_valid());
EXPECT_GE(bodies[0].num_faces(), 1u);
}