feat: implement component children API with Creo Feature ID support

- Add ComponentChildrenManager class for retrieving first-level child components
- Support path-based component lookup with intelligent top-level assembly handling
- Return Creo Feature IDs instead of filenames for precise component identification
- Add comprehensive path resolution logic for nested assemblies
- Implement new API endpoint /api/creo/component/children

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-21 12:42:57 +08:00
parent 6aaead4c6e
commit 8fd63500f2
5 changed files with 732 additions and 0 deletions

View File

@ -0,0 +1,502 @@
#include "pch.h"
#include "ComponentChildrenManager.h"
#include <sstream>
#include <algorithm>
#include <iomanip>
// Main interface method
ComponentChildrenManager::ComponentChildrenResult ComponentChildrenManager::GetComponentFirstLevelChildren(const ComponentChildrenRequest& request) {
ComponentChildrenResult result;
if (request.component_path.empty()) {
result.error_message = "Component path is required";
return result;
}
SessionInfo sessionInfo = GetSessionInfo();
if (!sessionInfo.is_valid) {
result.error_message = "Creo session not available";
return result;
}
try {
pfcModel_ptr current_model = sessionInfo.session->GetCurrentModel();
if (!current_model) {
result.error_message = "No current model loaded";
return result;
}
if (current_model->GetType() != pfcMDL_ASSEMBLY) {
result.error_message = "Current model is not an assembly";
return result;
}
wfcWAssembly_ptr currentAssembly = wfcWAssembly::cast(current_model);
if (!currentAssembly) {
result.error_message = "Failed to cast current model to assembly";
return result;
}
// Check if component_path refers to the current top-level assembly
std::string currentModelName = "";
try {
xstring filename_xstr = current_model->GetFileName();
currentModelName = XStringToString(filename_xstr);
} catch (...) {
// If can't get filename, try origin
try {
xstring origin = current_model->GetOrigin();
std::string origin_str = XStringToString(origin);
size_t pos = origin_str.find_last_of("/\\");
if (pos != std::string::npos) {
currentModelName = origin_str.substr(pos + 1);
} else {
currentModelName = origin_str;
}
} catch (...) {
currentModelName = "";
}
}
// Parse the component path
std::string actualPath = request.component_path;
ParsedPath targetPath;
if (actualPath.empty()) {
// Empty path is valid (represents top-level query)
targetPath.is_valid = true;
} else {
targetPath = ParseComponentPath(actualPath);
if (!targetPath.is_valid) {
result.error_message = "Invalid component path format";
return result;
}
// If path starts with current model name, skip the first segment
if (!targetPath.path_segments.empty() &&
!currentModelName.empty() &&
CaseInsensitiveCompare(targetPath.path_segments[0], currentModelName)) {
// Remove first segment as it refers to current assembly itself
targetPath.path_segments.erase(targetPath.path_segments.begin());
if (!targetPath.path_segments.empty()) {
targetPath.target_component = targetPath.path_segments.back();
} else {
// Only top-level name provided, treat as top-level query
actualPath = "";
}
}
}
// Handle top-level assembly case (empty path after conversion)
ComponentMatch match;
bool found = false;
if (actualPath.empty() || targetPath.path_segments.empty()) {
// Direct query of top-level assembly, no need to search
match.found = true;
match.owner_assembly = currentAssembly;
found = true;
} else {
// Search for the component in the assembly
found = FindComponentByPath(currentAssembly, targetPath, match);
if (!found || !match.found) {
result.error_message = "Component not found at path: " + request.component_path;
return result;
}
}
// Validate component ID if provided
if (request.component_id != -1) {
int actualFeatureId = -1;
try {
if (match.feature) {
actualFeatureId = match.feature->GetId();
}
} catch (...) {
actualFeatureId = -1;
}
if (actualFeatureId != request.component_id) {
result.error_message = "Component Feature ID mismatch. Expected: " + std::to_string(request.component_id) +
", Found: " + std::to_string(actualFeatureId);
return result;
}
}
// Set parent component info
result.parent_component_path = request.component_path;
pfcModel_ptr componentModel = nullptr;
wfcWAssembly_ptr componentAssembly = nullptr;
if (actualPath.empty()) {
// Top-level assembly case
result.parent_component_id = -1; // No Feature ID for top-level
result.parent_component_filename = currentModelName;
componentModel = current_model;
componentAssembly = currentAssembly;
} else {
// Sub-component case
try {
if (match.feature) {
result.parent_component_id = match.feature->GetId();
}
auto modelDescr = match.component_feature->GetModelDescr();
if (modelDescr) {
xstring filename_xstr = modelDescr->GetFileName();
result.parent_component_filename = XStringToString(filename_xstr);
}
} catch (...) {
result.parent_component_id = -1;
result.parent_component_filename = "unknown";
}
// Load the component model to get its children
componentModel = LoadComponentModel(match.component_feature);
if (!componentModel) {
result.error_message = "Failed to load component model";
return result;
}
// Check if the component is an assembly (has children)
if (componentModel->GetType() != pfcMDL_ASSEMBLY) {
// Part has no children, return empty list
result.success = true;
result.message = "Component is a part (no children)";
result.children_count = 0;
return result;
}
// Cast to assembly and get first level children
componentAssembly = wfcWAssembly::cast(componentModel);
}
if (!componentAssembly) {
result.error_message = "Failed to cast component to assembly";
return result;
}
// Get all component features at the first level
pfcFeatures_ptr features = componentAssembly->ListFeaturesByType(xfalse, pfcFEATTYPE_COMPONENT);
if (!features) {
result.success = true;
result.message = "Assembly has no components";
result.children_count = 0;
return result;
}
int features_count = features->getarraysize();
if (features_count <= 0) {
result.success = true;
result.message = "Assembly has no components";
result.children_count = 0;
return result;
}
// Collect all first-level children
for (int i = 0; i < features_count; i++) {
try {
pfcFeature_ptr feature = features->get(i);
if (!feature) continue;
pfcComponentFeat_ptr compFeat = pfcComponentFeat::cast(feature);
if (!compFeat) continue;
ChildComponentInfo childInfo = CreateChildComponentFromFeature(compFeat, 1, request.component_path);
result.children.push_back(childInfo);
} catch (...) {
continue;
}
}
result.children_count = result.children.size();
result.success = true;
result.message = "Successfully retrieved " + std::to_string(result.children_count) + " child components";
} catch (const std::exception& e) {
result.error_message = "Exception: " + std::string(e.what());
} catch (...) {
result.error_message = "Unknown exception occurred";
}
return result;
}
// Helper method implementations
ComponentChildrenManager::SessionInfo ComponentChildrenManager::GetSessionInfo() {
SessionInfo info;
try {
info.session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible);
info.is_valid = (info.session != nullptr);
} catch (...) {
info.is_valid = false;
}
return info;
}
ComponentChildrenManager::ParsedPath ComponentChildrenManager::ParseComponentPath(const std::string& full_path) {
ParsedPath result;
if (full_path.empty()) {
return result;
}
// Split path by '/' or '\'
std::string path = full_path;
std::replace(path.begin(), path.end(), '\\', '/');
std::istringstream iss(path);
std::string segment;
while (std::getline(iss, segment, '/')) {
if (!segment.empty()) {
result.path_segments.push_back(segment);
}
}
if (!result.path_segments.empty()) {
result.target_component = result.path_segments.back();
result.is_valid = true;
}
return result;
}
bool ComponentChildrenManager::FindComponentByPath(wfcWAssembly_ptr assembly,
const ParsedPath& target_path,
ComponentMatch& match) {
if (!assembly || !target_path.is_valid) return false;
try {
return RecursiveSearchComponent(assembly, target_path, "", 0, match);
} catch (...) {
return false;
}
}
bool ComponentChildrenManager::RecursiveSearchComponent(wfcWAssembly_ptr currentAssembly,
const ParsedPath& target_path,
const std::string& pathSoFar,
int currentLevel,
ComponentMatch& match) {
if (!currentAssembly || currentLevel >= (int)target_path.path_segments.size()) {
return false;
}
try {
std::string currentTarget = target_path.path_segments[currentLevel];
pfcFeatures_ptr features = currentAssembly->ListFeaturesByType(xfalse, pfcFEATTYPE_COMPONENT);
if (!features) return false;
int features_count = features->getarraysize();
for (int i = 0; i < features_count; i++) {
try {
pfcFeature_ptr feature = features->get(i);
if (!feature) continue;
pfcComponentFeat_ptr compFeat = pfcComponentFeat::cast(feature);
if (!compFeat) continue;
auto modelDescr = compFeat->GetModelDescr();
if (!modelDescr) continue;
xstring filename_xstr = modelDescr->GetFileName();
std::string componentName = XStringToString(filename_xstr);
std::string newPath = pathSoFar.empty() ? componentName : pathSoFar + "/" + componentName;
if (CaseInsensitiveCompare(componentName, currentTarget)) {
// Found the target at this level
if (currentLevel == (int)target_path.path_segments.size() - 1) {
// This is the final target
match.feature = feature;
match.component_feature = compFeat;
match.owner_assembly = currentAssembly;
match.actual_path = newPath;
match.found = true;
return true;
} else {
// Need to go deeper
pfcModel_ptr childModel = LoadComponentModel(compFeat);
if (childModel && childModel->GetType() == pfcMDL_ASSEMBLY) {
wfcWAssembly_ptr childAssembly = wfcWAssembly::cast(childModel);
if (childAssembly) {
if (RecursiveSearchComponent(childAssembly, target_path, newPath, currentLevel + 1, match)) {
return true;
}
}
}
}
}
} catch (...) {
continue;
}
}
} catch (...) {
return false;
}
return false;
}
ComponentChildrenManager::ChildComponentInfo ComponentChildrenManager::CreateChildComponentFromFeature(pfcComponentFeat_ptr compFeat,
int level,
const std::string& parentPath) {
ChildComponentInfo component;
component.level = level;
component.children_count = 0;
component.is_visible = true;
component.file_size = "0.0MB";
component.id = -1; // Default Feature ID
try {
// Get Feature ID first
pfcFeature_ptr feature = pfcFeature::cast(compFeat);
if (feature) {
component.id = feature->GetId();
}
auto modelDescr = compFeat->GetModelDescr();
if (modelDescr) {
xstring filename_xstr = modelDescr->GetFileName();
component.filename = XStringToString(filename_xstr);
component.name = component.filename;
size_t ext_pos = component.name.find_last_of(".");
if (ext_pos != std::string::npos) {
component.name = component.name.substr(0, ext_pos);
}
if (!component.name.empty()) {
component.name[0] = std::toupper(component.name[0]);
}
// Build paths
if (parentPath.empty()) {
component.path = component.filename;
component.full_path = component.filename;
} else {
component.path = parentPath + "/" + component.filename;
component.full_path = component.path;
}
// Determine component type
std::string ext = "";
if (ext_pos != std::string::npos) {
ext = component.filename.substr(ext_pos + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
}
if (ext == "asm") {
component.type = "assembly";
component.model_type = "MDL_ASSEMBLY";
} else {
component.type = "part";
component.model_type = "MDL_PART";
}
// Try to load model and get additional info
pfcModel_ptr childModel = LoadComponentModel(compFeat);
if (childModel) {
component.file_size = GetModelFileSize(childModel);
if (childModel->GetType() == pfcMDL_ASSEMBLY) {
wfcWAssembly_ptr childAssembly = wfcWAssembly::cast(childModel);
if (childAssembly) {
try {
pfcFeatures_ptr childFeatures = childAssembly->ListFeaturesByType(xfalse, pfcFEATTYPE_COMPONENT);
if (childFeatures) {
component.children_count = childFeatures->getarraysize();
}
} catch (...) {
component.children_count = 0;
}
}
}
}
}
} catch (...) {
component.id = -1;
component.filename = "unknown_component";
component.name = "Unknown";
component.type = "unknown";
component.model_type = "UNKNOWN";
}
return component;
}
pfcModel_ptr ComponentChildrenManager::LoadComponentModel(pfcComponentFeat_ptr compFeat) {
if (!compFeat) return nullptr;
try {
auto modelDescr = compFeat->GetModelDescr();
if (!modelDescr) return nullptr;
SessionInfo sessionInfo = GetSessionInfo();
if (!sessionInfo.is_valid) return nullptr;
pfcModel_ptr model = sessionInfo.session->GetModelFromDescr(modelDescr);
if (!model) {
model = sessionInfo.session->RetrieveModel(modelDescr);
}
return model;
} catch (...) {
return nullptr;
}
}
std::string ComponentChildrenManager::GetModelFileSize(pfcModel_ptr model) {
try {
return "N/A";
} catch (...) {
return "0.0MB";
}
}
std::string ComponentChildrenManager::XStringToString(const xstring& xstr) {
try {
const wchar_t* wstr = xstr;
if (!wstr) return "";
int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
if (bufferSize <= 0) return "";
std::string result(bufferSize - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &result[0], bufferSize, nullptr, nullptr);
return result;
} catch (...) {
return "";
}
}
xstring ComponentChildrenManager::StringToXString(const std::string& str) {
try {
int bufferSize = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0);
if (bufferSize <= 0) return xstring();
std::vector<wchar_t> wstr(bufferSize);
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], bufferSize);
return xstring(&wstr[0]);
} catch (...) {
return xstring();
}
}
bool ComponentChildrenManager::CaseInsensitiveCompare(const std::string& str1, const std::string& str2) {
if (str1.length() != str2.length()) return false;
return std::equal(str1.begin(), str1.end(), str2.begin(),
[](char a, char b) {
return std::tolower(a) == std::tolower(b);
});
}

118
ComponentChildrenManager.h Normal file
View File

@ -0,0 +1,118 @@
#pragma once
#include <string>
#include <vector>
#include <map>
// OTK headers
#include <pfcGlobal.h>
#include <pfcSession.h>
#include <wfcSession.h>
#include <wfcGlobal.h>
#include <pfcModel.h>
#include <pfcExceptions.h>
#include <pfcFeature.h>
#include <pfcComponentFeat.h>
#include <pfcFeature_s.h>
#include <pfcSolid.h>
#include <wfcSolid.h>
#include <wfcAssembly.h>
#include <stdcols.h>
class ComponentChildrenManager {
public:
// Request data structure
struct ComponentChildrenRequest {
std::string component_path; // Required: component path like "Assembly.asm/SubAsm.asm/Part.prt"
int component_id = -1; // Optional: Creo Feature ID for additional validation (-1 means not specified)
};
// Child component info structure
struct ChildComponentInfo {
int id; // Creo Feature ID
std::string filename; // Component filename (e.g., "part.prt")
std::string name;
std::string type; // "part", "assembly"
int level;
std::string path;
std::string full_path;
std::string file_size;
bool is_visible;
std::string model_type; // "MDL_PART", "MDL_ASSEMBLY"
int children_count;
};
// Response data structure
struct ComponentChildrenResult {
bool success = false;
std::string message;
int parent_component_id = -1; // Creo Feature ID
std::string parent_component_filename; // Component filename
std::string parent_component_path;
int children_count = 0;
std::vector<ChildComponentInfo> children;
std::string error_message;
};
private:
// Session info structure
struct SessionInfo {
pfcSession_ptr session;
bool is_valid = false;
};
// Path parsing structure
struct ParsedPath {
std::vector<std::string> path_segments;
std::string target_component;
bool is_valid = false;
};
// Component match result
struct ComponentMatch {
pfcFeature_ptr feature;
pfcComponentFeat_ptr component_feature;
wfcWAssembly_ptr owner_assembly;
std::string actual_path;
bool found = false;
};
private:
// Helper methods
SessionInfo GetSessionInfo();
ParsedPath ParseComponentPath(const std::string& full_path);
bool FindComponentByPath(wfcWAssembly_ptr assembly,
const ParsedPath& target_path,
ComponentMatch& match);
bool RecursiveSearchComponent(wfcWAssembly_ptr currentAssembly,
const ParsedPath& target_path,
const std::string& pathSoFar,
int currentLevel,
ComponentMatch& match);
ChildComponentInfo CreateChildComponentFromFeature(pfcComponentFeat_ptr compFeat,
int level,
const std::string& parentPath);
pfcModel_ptr LoadComponentModel(pfcComponentFeat_ptr compFeat);
std::string GetModelFileSize(pfcModel_ptr model);
// String utility methods
std::string XStringToString(const xstring& xstr);
xstring StringToXString(const std::string& str);
bool CaseInsensitiveCompare(const std::string& str1, const std::string& str2);
public:
// Main interface method
ComponentChildrenResult GetComponentFirstLevelChildren(const ComponentChildrenRequest& request);
// Singleton pattern
static ComponentChildrenManager& Instance() {
static ComponentChildrenManager instance;
return instance;
}
private:
// Private constructor for singleton
ComponentChildrenManager() = default;
ComponentChildrenManager(const ComponentChildrenManager&) = delete;
ComponentChildrenManager& operator=(const ComponentChildrenManager&) = delete;
};

View File

@ -11,6 +11,7 @@
#include "GeometryAnalyzer.h"
#include "ModelSearchHandler.h"
#include "HierarchyStatisticsAnalyzer.h"
#include "ComponentChildrenManager.h"
#include <wfcSession.h>
#include <wfcGlobal.h>
#include <string>
@ -1648,6 +1649,108 @@ HttpResponse HierarchyStatisticsHandler(const HttpRequest& request) {
return response;
}
// Component Children Handler
HttpResponse ComponentChildrenHandler(const HttpRequest& request) {
HttpResponse response;
response.headers["Content-Type"] = "application/json";
response.headers["Access-Control-Allow-Origin"] = "*";
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS";
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization";
if (request.method != "POST") {
response.status_code = 405;
response.body = "{\"success\": false, \"error\": \"Method not allowed\"}";
return response;
}
try {
// Parse JSON request
ComponentChildrenManager::ComponentChildrenRequest children_request;
children_request.component_path = ExtractJsonValue(request.body, "component_path");
// Parse component_id as integer (Creo Feature ID)
std::string component_id_str = ExtractJsonValue(request.body, "component_id");
if (!component_id_str.empty()) {
try {
children_request.component_id = std::stoi(component_id_str);
} catch (...) {
response.status_code = 400;
response.body = "{\"success\": false, \"error\": \"Invalid component_id: must be a number\"}";
return response;
}
}
if (children_request.component_path.empty()) {
response.status_code = 400;
response.body = "{\"success\": false, \"error\": \"Missing required parameter: component_path\"}";
return response;
}
// Call ComponentChildrenManager
ComponentChildrenManager::ComponentChildrenResult result =
ComponentChildrenManager::Instance().GetComponentFirstLevelChildren(children_request);
if (result.success) {
// Build success response
std::ostringstream json;
json << "{"
<< "\"success\": true,"
<< "\"message\": \"" << EscapeJsonString(result.message) << "\","
<< "\"data\": {"
<< "\"parent_component_id\": " << result.parent_component_id << ","
<< "\"parent_component_filename\": \"" << EscapeJsonString(result.parent_component_filename) << "\","
<< "\"parent_component_path\": \"" << EscapeJsonString(result.parent_component_path) << "\","
<< "\"children_count\": " << result.children_count << ","
<< "\"children\": [";
// Build children array
for (size_t i = 0; i < result.children.size(); ++i) {
if (i > 0) json << ",";
const auto& child = result.children[i];
json << "{"
<< "\"id\": " << child.id << ","
<< "\"filename\": \"" << EscapeJsonString(child.filename) << "\","
<< "\"name\": \"" << EscapeJsonString(child.name) << "\","
<< "\"type\": \"" << child.type << "\","
<< "\"level\": " << child.level << ","
<< "\"path\": \"" << EscapeJsonString(child.path) << "\","
<< "\"full_path\": \"" << EscapeJsonString(child.full_path) << "\","
<< "\"file_size\": \"" << child.file_size << "\","
<< "\"is_visible\": " << (child.is_visible ? "true" : "false") << ","
<< "\"model_type\": \"" << child.model_type << "\","
<< "\"children_count\": " << child.children_count
<< "}";
}
json << "]"
<< "}"
<< "}";
response.body = json.str();
} else {
// Build error response
std::ostringstream json;
json << "{"
<< "\"success\": false,"
<< "\"data\": null,"
<< "\"error\": \"" << EscapeJsonString(result.error_message) << "\""
<< "}";
response.body = json.str();
response.status_code = 500;
}
} catch (const std::exception& e) {
response.status_code = 500;
response.body = "{\"success\": false, \"error\": \"Exception: " + std::string(e.what()) + "\"}";
} catch (...) {
response.status_code = 500;
response.body = "{\"success\": false, \"error\": \"Unknown error during component children retrieval\"}";
}
return response;
}
extern "C" int user_initialize(
int argc,
@ -1684,6 +1787,7 @@ extern "C" int user_initialize(
g_http_server->SetRouteHandler("/api/creo/component/delete-by-path", PathDeleteHandler);
g_http_server->SetRouteHandler("/api/search/models", ModelSearchHandler::HandleModelSearchRequest);
g_http_server->SetRouteHandler("/api/analysis/hierarchy-statistics", HierarchyStatisticsHandler);
g_http_server->SetRouteHandler("/api/creo/component/children", ComponentChildrenHandler);
if (g_http_server->Start()) {

View File

@ -207,6 +207,7 @@ ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AuthManager.cpp" />
<ClCompile Include="ComponentChildrenManager.cpp" />
<ClCompile Include="CreoManager.cpp" />
<ClCompile Include="GeometryAnalyzer.cpp" />
<ClCompile Include="HierarchyStatisticsAnalyzer.cpp" />
@ -236,6 +237,7 @@ ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</ItemGroup>
<ItemGroup>
<ClInclude Include="AuthManager.h" />
<ClInclude Include="ComponentChildrenManager.h" />
<ClInclude Include="Config.h" />
<ClInclude Include="CreoManager.h" />
<ClInclude Include="framework.h" />

View File

@ -90,6 +90,9 @@
<ClCompile Include="HierarchyStatisticsAnalyzer.cpp">
<Filter>源文件\src\creo</Filter>
</ClCompile>
<ClCompile Include="ComponentChildrenManager.cpp">
<Filter>源文件\src\creo</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="MFCCreoDll.def">
@ -172,6 +175,9 @@
<ClInclude Include="HierarchyStatisticsAnalyzer.h">
<Filter>源文件\src\creo</Filter>
</ClInclude>
<ClInclude Include="ComponentChildrenManager.h">
<Filter>源文件\src\creo</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="MFCCreoDll.rc">