From 81af7c350f77b95c8bcc37f6f63f100d96f43ab9 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Wed, 16 Dec 2020 21:48:11 +0000 Subject: [PATCH 01/10] disable --output-dir arg which is not in use --- src/python/pppPipelineController.py | 43 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/python/pppPipelineController.py b/src/python/pppPipelineController.py index f371150a..2f20fc80 100644 --- a/src/python/pppPipelineController.py +++ b/src/python/pppPipelineController.py @@ -56,15 +56,15 @@ def ppp_add_argument(parser): ) parser.add_argument( - "-o", "--output-file", help="output file name (without folder path)", + "-o", "--output-file", help="output file name relative to output-dir or absolute path", dest = "outputFile" ) - parser.add_argument( - "--output-dir", - help="output folder path, by default a subfolder in the working dir", - dest = "outputDir" - ) + # parser.add_argument( + # "--output-dir", + # help="output folder path, by default a subfolder (input file stem) in the working-dir", + # dest = "outputDir" + # ) parser.add_argument( "--config", @@ -112,7 +112,7 @@ def ppp_parse_input(args): return "input_data" elif os.path.exists(args.input): - return args.input + return os.path.abspath(args.input) # must be abspath, as current dir may change else: raise IOError("input file does not exist: ", args.input) else: @@ -161,18 +161,28 @@ def generate_config_file_header(args): } ) +def ppp_get_case_name(inputFile): + inputDir, inputFilename = os.path.split(inputFile) + case_name = inputFilename[: inputFilename.rfind(".")] + return case_name + +def ppp_get_output_dir(args): + # if args.outputDir: + # outputDir = os.path.abspath(args.outputDir) + # else: + + inputFile = ppp_parse_input(args) + case_name = ppp_get_case_name(inputFile) + # this naming must be consistent with DataStorage::generateStoragePath(input_name) + outputDir = (args.workingDir + os.path.sep + case_name + "_processed") # can be detected from output_filename full path + return outputDir def ppp_post_process(args): - # + # currently, only make symbolic link to input file into the output dir + inputFile = ppp_parse_input(args) inputDir, inputFilename = os.path.split(inputFile) - case_name = inputFilename[: inputFilename.rfind(".")] - - outputDir = ( # this naming must be consistent with DataStorage::generateStoragePath(input_name) - args.workingDir + os.path.sep + case_name + "_processed" - ) # can be detected from output_filename full path - if args.outputDir: - outputDir = args.outputDir + outputDir = ppp_get_output_dir(args) linkToInputFile = outputDir + os.path.sep + inputFilename if os.path.exists(outputDir): @@ -264,8 +274,7 @@ def config(self): # after inputFile is given, all other default filenames are generated inputFile = ppp_parse_input(args) - inputDir, inputFilename = os.path.split(inputFile) - case_name = inputFilename[: inputFilename.rfind(".")] + case_name = ppp_get_case_name(inputFile) ######################## module specific ########################## outputFile = case_name + "_processed.txt" # saved to case output folder if args.outputFile: From 9f71fba2264b778a9ffff0ae6d9fc83ff6124152 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Wed, 16 Dec 2020 21:51:12 +0000 Subject: [PATCH 02/10] add shape scale feature for GeometryWriter --- src/Geom/GeometryWriter.h | 49 ++++++++++++++++++++++++++++++++++----- src/Geom/OccUtils.cpp | 33 +++++++++++++++----------- src/Geom/OccUtils.h | 5 ++++ 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/Geom/GeometryWriter.h b/src/Geom/GeometryWriter.h index 37770d95..eba254d0 100644 --- a/src/Geom/GeometryWriter.h +++ b/src/Geom/GeometryWriter.h @@ -105,8 +105,26 @@ namespace Geom LOG_F(INFO, "%s", sout.str().c_str()); } + // TODO: query inputData() metadata for inputUnit + /// return 1 which means no scaling is needed + /// not quite useful for + double calcOutputScale(const std::string& outputUnit) + { + const std::string inputUnit = "MM"; + if (outputUnit == inputUnit) + return 1; + else if ("MM" == inputUnit && outputUnit == "M") + return 0.001; + else + { + LOG_F(ERROR, "scaling for input length unit %s and output unit %s is not supported", inputUnit.c_str(), + outputUnit.c_str()); + return 1; + } + } + /// can export floating shapes which can not be compoSolid - void exportCompound(const std::string& file_name) + void exportCompound(const std::string& file_name, double scale = 1.0) { summary(); @@ -122,6 +140,10 @@ namespace Geom LOG_F(INFO, "result is not merged (duplicated face removed) for result brep file"); finalShape = OccUtils::createCompound(*mySolids, myShapeErrors); } + + if (scale != 1) // double type has integer value can be compared with integer by == + finalShape = OccUtils::scaleShape(finalShape, scale); + /// NOTE: exportMetaData() is done in the second GeometryPropertyBuilder processor BRepTools::Write(finalShape, file_name.c_str()); // progress reporter can be the last arg } @@ -131,15 +153,19 @@ namespace Geom * */ bool exportGeometry(const std::string file_name) { + std::string defaultLengthUnit = "MM"; + auto outputUnit = parameterValue("outputUnit", defaultLengthUnit); + double scale = calcOutputScale(outputUnit); + if (Utilities::hasFileExt(file_name, "brp") || Utilities::hasFileExt(file_name, "brep")) { - exportCompound(file_name); + exportCompound(file_name, scale); return true; } else if (Utilities::hasFileExt(file_name, "stp") || Utilities::hasFileExt(file_name, "step")) { // LOG_F(INFO, "export Dataset pointed by member hDoc"); - Handle(TDocStd_Document) aDoc = createDocument(); + Handle(TDocStd_Document) aDoc = createDocument(scale); /// the user can work with an already prepared WorkSession or create a new one Standard_Boolean scratch = Standard_False; @@ -150,15 +176,23 @@ namespace Geom // recommended value, others shape mode are available // Interface_Static::SetCVal("write.step.schema", "AP214IS"); Interface_Static::SetIVal("write.step.assembly", 1); // global variable + // Interface_Static::SetIVal ("write.step.nonmanifold", 1); // "write.precision.val" = 0.0001 is the default value + if (outputUnit != defaultLengthUnit) + { + Interface_Static::SetCVal("xstep.cascade.unit", outputUnit.c_str()); + Interface_Static::SetCVal("write.step.unit", outputUnit.c_str()); + // all vertex coordinate will not be scaled during writing out, change unit, + // but it causes scaling at reading back + } + STEPCAFControl_Writer writer(WS, scratch); // this writer contains a STEPControl_Writer class, not by inheritance // writer.SetColorMode(mode); if (!writer.Transfer(aDoc, mode)) { LOG_F(ERROR, "The Dataset cannot be translated or gives no result"); - // abandon .. } IFSelect_ReturnStatus stat = writer.Write(file_name.c_str()); @@ -173,7 +207,7 @@ namespace Geom return false; } - Handle(TDocStd_Document) createDocument() + Handle(TDocStd_Document) createDocument(double scale = 1) { Handle(XCAFApp_Application) hApp = XCAFApp_Application::GetApplication(); @@ -194,7 +228,10 @@ namespace Geom for (auto& item : *mySolids) { TDF_Label partLabel = shapeTool->NewShape(); - shapeTool->SetShape(partLabel, item.second); + if (scale != 1) + shapeTool->SetShape(partLabel, OccUtils::scaleShape(item.second, scale)); + else + shapeTool->SetShape(partLabel, item.second); colorTool->SetColor(partLabel, (*myColorMap)[item.first], XCAFDoc_ColorGen); TDataStd_Name::Set(partLabel, TCollection_ExtendedString((*myNameMap)[item.first].c_str(), true)); /// Material not yet supported diff --git a/src/Geom/OccUtils.cpp b/src/Geom/OccUtils.cpp index 5ca053cc..26ed7de6 100644 --- a/src/Geom/OccUtils.cpp +++ b/src/Geom/OccUtils.cpp @@ -821,30 +821,28 @@ namespace Geom Standard_Boolean saveMesh(TopoDS_Shape& aShape, const std::string file_name) { -#if 0 // OCC_VERSION_HEX >= 0x070400 - // not find this `RWStl::WriteFile` if (file_name.find("stl") != file_name.npos) { - Handle(Poly_Triangulation) aTriangulation = RWStl::WriteFile(file_name.c_str(), aTriangulation); - return true; - } - // in lower version, each triangle is read as TopoDS_Face which is not efficient -#else - if (file_name.find("stl") != file_name.npos) - { - std::string fileMode = "ascii"; // only ASCII mode in occt 7.3 +#if OCC_VERSION_HEX >= 0x070300 + // RWStl::WriteFile(file_name.c_str(), aTriangulation); + // low level API used by StlAPI_Writer + // in lower version, each triangle is read as TopoDS_Face which is not efficient auto m = meshShape(aShape); - auto stl_exporter = StlAPI_Writer(); - /* + auto stl_exporter = StlAPI_Writer(); // high level API + // only ASCII mode in occt 7.3 + /* by default ascii write mode, occ 7.4 support binary STL file write if(fileMode == "ascii") stl_exporter.SetASCIIMode(true); else // binary, just set the ASCII flag to False stl_exporter.SetASCIIMode(false); */ - stl_exporter.Write(aShape, file_name.c_str()); + stl_exporter.Write(aShape, file_name.c_str()); // shape must has mesh for each face return true; - } +#else + return false; #endif + } + else { LOG_F(ERROR, "output mesh file suffix is not supported: %s", file_name.c_str()); @@ -852,5 +850,12 @@ namespace Geom } } + TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const gp_Pnt origin) + { + gp_Trsf ts; + ts.SetScale(origin, scale); + return BRepBuilderAPI_Transform(from, ts, true); + } + } // namespace OccUtils } // namespace Geom diff --git a/src/Geom/OccUtils.h b/src/Geom/OccUtils.h index b47d0ffd..52db31f3 100644 --- a/src/Geom/OccUtils.h +++ b/src/Geom/OccUtils.h @@ -111,10 +111,15 @@ namespace Geom GeomExport TopoDS_Shape fuseShape(const VectorType v, bool occInternalParallel = true); GeomExport TopoDS_Shape cutShape(const TopoDS_Shape& from, const TopoDS_Shape& substractor); + /// surface mesh GeomExport std::shared_ptr meshShape(const TopoDS_Shape& aShape, double resolution = 0.01); + /// save to STL mesh format GeomExport Standard_Boolean saveMesh(TopoDS_Shape& aShape, const std::string file_name); + /// scale up or down shape + GeomExport TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const gp_Pnt origin = gp_Pnt()); + } // namespace OccUtils } // namespace Geom From 57f726ff801bc5865e87c06f131c0c4cb9fb306e Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Wed, 16 Dec 2020 21:53:39 +0000 Subject: [PATCH 03/10] minor code tweaks (mainly in source doc) --- src/Geom/GeometryPropertyBuilder.h | 4 ++-- src/Geom/GeometryTypes.h | 10 +++++++++- src/Geom/ProcessorSample.h | 4 +++- src/PPP/CoreTests/CMakeLists.txt | 19 ++++++------------- src/PPP/TypeDefs.h | 6 ++++++ src/python/pppPipelineController.py | 2 +- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/Geom/GeometryPropertyBuilder.h b/src/Geom/GeometryPropertyBuilder.h index 317aa9a2..4217e5f0 100644 --- a/src/Geom/GeometryPropertyBuilder.h +++ b/src/Geom/GeometryPropertyBuilder.h @@ -76,8 +76,8 @@ namespace Geom { // max and min volume check! double max_volume = 0; - double min_volume = 1e100; - double max_volume_threshold = 1e16; // todo: get from config + double min_volume = 1e100; // unit is mm^3 + double max_volume_threshold = 1e16; // todo: get from config parameter for (size_t i = 0; i < myInputData->itemCount(); i++) { const auto& p = myGeometryProperties[i]; diff --git a/src/Geom/GeometryTypes.h b/src/Geom/GeometryTypes.h index f947a1f5..0ad78e9e 100644 --- a/src/Geom/GeometryTypes.h +++ b/src/Geom/GeometryTypes.h @@ -152,7 +152,7 @@ namespace Geom return ShapeErrorType::NoError; } - class CollisionInfo + struct CollisionInfo { public: ItemIndexType first; @@ -161,6 +161,14 @@ namespace Geom CollisionType type; CollisionInfo() = default; + // C++20 prevents conversion form to this type + CollisionInfo(ItemIndexType _first, ItemIndexType _second, double _value, CollisionType _type) + : first(_first) + , second(_second) + , value(_value) + , type(_type) + { + } }; inline void to_json(json& j, const CollisionInfo& p) { diff --git a/src/Geom/ProcessorSample.h b/src/Geom/ProcessorSample.h index 7977916a..1d895be7 100644 --- a/src/Geom/ProcessorSample.h +++ b/src/Geom/ProcessorSample.h @@ -32,7 +32,9 @@ namespace Geom ~ProcessorSample() = default; /** - * \brief preparing work in serial mode + * \brief preparing work in serial mode, + * memory allocation must been done in serial mode, there may be lock in allocator + * memory allocation is processItem() is forbidden */ virtual void prepareInput() override final { diff --git a/src/PPP/CoreTests/CMakeLists.txt b/src/PPP/CoreTests/CMakeLists.txt index 20fe03af..5fffdc84 100644 --- a/src/PPP/CoreTests/CMakeLists.txt +++ b/src/PPP/CoreTests/CMakeLists.txt @@ -1,17 +1,15 @@ # https://github.com/catchorg/Catch2/blob/master/docs/cmake-integration.md # catch2 must be a subfolder, so `add_subdirectory` is done in toplevel cmakelists.txt -#add_subdirectory(./Catch2) -# std can and should be applied to target only -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +# if C++17 is not available, then conditionally turn off one test +#set(CMAKE_CXX_STANDARD 17) +#set(CMAKE_CXX_STANDARD_REQUIRED ON) ######################################### find_package(Threads) -# -pthread option is not portable -#SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread") # not portable -#target_link_libraries(MyApp ${CMAKE_THREAD_LIBS_INIT}) # portable +# NOTE: -pthread option is not portable +# target_link_libraries(MyApp ${CMAKE_THREAD_LIBS_INIT}) # portable #file(GLOB TEST_SOURCES "*est*.cpp") # not recommended way to find source set(APP_TEST_SOURCES @@ -23,7 +21,6 @@ add_executable(app_tests ${APP_TEST_SOURCES}) set_target_properties(app_tests PROPERTIES OUTPUT_NAME "pppAppTests") target_link_libraries(app_tests ${CMAKE_THREAD_LIBS_INIT}) # For pthreads, portable -#add_dependencies(app_tests MyBase) target_link_libraries(app_tests MyBase) target_link_libraries(app_tests MyApp) target_link_libraries(app_tests Catch2::Catch2) @@ -33,7 +30,7 @@ target_link_libraries(app_tests Catch2::Catch2) set(PARALLEL_TEST_SOURCES "ParallelAccessorTest.cpp" ) -# Now simply link against gtest or gtest_main as needed. Eg + add_executable(parallel_tests ${PARALLEL_TEST_SOURCES}) set_target_properties(parallel_tests PROPERTIES OUTPUT_NAME "pppParallelTests") @@ -46,10 +43,6 @@ if(MSVC) target_compile_options(parallel_tests PRIVATE /wd4251 /wd4275 ) endif() -# if in catch2 test mode in the future, uncomment this line below -#target_link_libraries(parallel_tests Catch2::Catch2) -########################################################## - ######################### test registration ##################### # using contrib/Catch.cmake, ParseAndAddCatchTests.cmake # current not working diff --git a/src/PPP/TypeDefs.h b/src/PPP/TypeDefs.h index 85627741..528cbe5a 100644 --- a/src/PPP/TypeDefs.h +++ b/src/PPP/TypeDefs.h @@ -65,6 +65,12 @@ namespace PPP * */ template using VectorType = std::vector; + /** + * Sparse vector, based on shared_ptr, initialize value and set size before usage + * e.g. `mySpVec>` + * */ + template using SparseVector = std::vector>; + /** * MapType choice for parallel preprocessor * std::set or std::map, based on binary tree, has time complexity O(logN) for find and insert diff --git a/src/python/pppPipelineController.py b/src/python/pppPipelineController.py index 2f20fc80..9e930d0c 100644 --- a/src/python/pppPipelineController.py +++ b/src/python/pppPipelineController.py @@ -56,7 +56,7 @@ def ppp_add_argument(parser): ) parser.add_argument( - "-o", "--output-file", help="output file name relative to output-dir or absolute path", + "-o", "--output-file", help="output file name relative to working-dir or absolute path", dest = "outputFile" ) From e633f95ee5bf4625b37b06e7faf29e0435ac88b5 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Thu, 17 Dec 2020 09:34:01 +0000 Subject: [PATCH 04/10] add "--output-unit" to geomPipeline.py CLI (tested) --- scripts/run_all_tests.sh | 5 +++++ src/python/geomPipeline.py | 33 ++++++++++++++++++++++++++++++++- wiki/GetStarted.md | 2 -- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/scripts/run_all_tests.sh b/scripts/run_all_tests.sh index 38e0000e..8f911636 100644 --- a/scripts/run_all_tests.sh +++ b/scripts/run_all_tests.sh @@ -47,6 +47,11 @@ geomPipeline.py ../data/test_geometry/test_geometry.stp -o test_geometry_result. geomPipeline.py check ../data/test_geometry/test_geometry_manifest.json --verbosity WARNING if [ ! $? -eq 0 ] ; then echo "Error during geometry test" ; exit 1 ; fi +# test scaling, output to a diff output folder +geomPipeline.py imprint ../data/test_geometry/test_geometry.stp -o /tmp/test_geometry_scaled.brep --output-unit M +geomPipeline.py imprint ../data/test_geometry/test_geometry.stp --working-dir /tmp/ -o test_geometry_scaled.stp --output-unit M +if [ ! $? -eq 0 ] ; then echo "Error during geometry output unit (scaling) test " ; exit 1 ; fi + # test single thread mode, only for non-coupled operations geomPipeline.py check ../data/test_geometry/test_geometry.stp --thread-count 1 --verbosity WARNING if [ ! $? -eq 0 ] ; then echo "Error during geometry test in single thread mode" ; exit 1 ; fi diff --git a/src/python/geomPipeline.py b/src/python/geomPipeline.py index d50a0af8..f4abba40 100644 --- a/src/python/geomPipeline.py +++ b/src/python/geomPipeline.py @@ -89,6 +89,22 @@ def geom_add_argument(parser): action="store_true", help="ignore failed (in BOP check, collision detect, etc) solids", ) + + # it is not arbitrary scaling, but can change output units (m, cm, mm) of STEP file format + parser.add_argument( + "-ou", "--output-unit", + dest="output_unit", + type=str, + help="output geometry unit, by default, `MM` for CAD, change unit will cause scaling when read back", + ) + + # it is not arbitrary scaling, but can change output units (m, cm, mm) of STEP file format + parser.add_argument( + "--input-unit", + dest="input_unit", + type=str, + help="input geometry unit, by default, only needed for brep file without meta data for length ", + ) return parser @@ -144,6 +160,16 @@ def geom_add_argument(parser): if args.ignore_failed != None and args.ignore_failed: ignoreFailed = True +outputUnit = "MM" # argument can be capital or uncapital letter +if args.output_unit: + if args.output_unit.upper() in ("MM", "CM", "M", "INCH"): # STEP supported units + outputUnit = args.output_unit.upper() + +# in CAD, the default lengths unit is MM (millimeter) +inputUnit = "MM" +if args.input_unit: + if args.input_unit.upper() in ("MM", "CM", "M", "INCH"): # STEP supported units + inputUnit = args.input_unit.upper() if debugging > 0: print("action on the geometry is ", args.action) @@ -183,7 +209,12 @@ def geom_add_argument(parser): "value": mergeResultShapes, "doc": "control whether imprinted solids will be merged (remove duplicate faces) before write-out", }, - "doc": "if no directory part in `outputFile`, saved into the case `outputDir`", + "outputUnit": { + "type": "string", + "value": outputUnit, + "doc": "control output geometry length unit, for CAD, default mm", + }, + "doc": "if no directory part in `outputFile`, saved into the case `outputDir` folder", } ] diff --git a/wiki/GetStarted.md b/wiki/GetStarted.md index cf8ff1b0..4b690b44 100644 --- a/wiki/GetStarted.md +++ b/wiki/GetStarted.md @@ -20,8 +20,6 @@ Optional arguments for all pipelines: output file name (without folder path) --working-dir WORKING_DIR working folder path, by default the current working folder - --output-dir OUTPUT_DIR - output folder path, by default, a subfolder in workingDir --config only generate config.json without run the pipeline -nt THREAD_COUNT, --thread-count THREAD_COUNT From 6bb104b0849a10ec3684594bbbd5014b95939c24 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Thu, 17 Dec 2020 10:39:21 +0000 Subject: [PATCH 05/10] make all path is absolute in config.json --- src/python/geomPipeline.py | 5 +++-- src/python/pppPipelineController.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/python/geomPipeline.py b/src/python/geomPipeline.py index f4abba40..fbf2db41 100644 --- a/src/python/geomPipeline.py +++ b/src/python/geomPipeline.py @@ -130,6 +130,7 @@ def geom_add_argument(parser): if args.outputFile: outputFile = args.outputFile print("args.outputFile = ", args.outputFile) +outputFile = os.path.abspath(outputFile) # output metadata filename outputMetadataFile = outputFile[: outputFile.rfind(".")] + "_metadata.json" @@ -143,7 +144,7 @@ def geom_add_argument(parser): else: raise IOError("input metadata file does not exist: ", args.metadata) else: - if inputFile.find(".brep") > 0 or inputFile.find(".brp"): + if inputFile.endswith(".brep") or inputFile.endswith(".brp"): inputMetadataFile = inputFile[: inputFile.rfind(".")] + "_metadata.json" if os.path.exists(inputMetadataFile): hasInputMetadataFile = True @@ -195,7 +196,7 @@ def geom_add_argument(parser): { "className": "Geom::GeometryReader", "dataFileName": inputFile, - "metadataFileName": None if not hasInputMetadataFile else inputMetadataFile, + "metadataFileName": None if not hasInputMetadataFile else os.path.abspath(inputMetadataFile), "doc": "only step, iges, FCStd, brep+json metadata are supported", } ] diff --git a/src/python/pppPipelineController.py b/src/python/pppPipelineController.py index 9e930d0c..bfa88e6e 100644 --- a/src/python/pppPipelineController.py +++ b/src/python/pppPipelineController.py @@ -108,15 +108,16 @@ def ppp_parse_input(args): # download to current folder import urllib.request - urllib.request.urlretrieve(args.input, "input_data") - return "input_data" + urllib.request.urlretrieve(args.input, "downloaded_input_data") + args.input = os.path.abspath("downloaded_input_data") elif os.path.exists(args.input): - return os.path.abspath(args.input) # must be abspath, as current dir may change + args.input = os.path.abspath(args.input) else: raise IOError("input file does not exist: ", args.input) else: raise Exception("input file must be given as an argument") + return args.input # must be abspath, as current dir may change def ppp_check_argument(args): @@ -127,9 +128,8 @@ def ppp_check_argument(args): else: args.thread_count = cpu_count() # can set args.value just like a dict if not args.workingDir: - args.workingDir = "./" # debug and config file will be put here, + args.workingDir = os.path.abspath("./") # debug and config file will be put here, # before copy to outputDir at the end of processing in C++ Context::finalize() - # print(args.thread_count) #################################################################### From bdaf0ae20f4b921e3d5fce3fec5707e45fac1497 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Thu, 17 Dec 2020 15:57:57 +0000 Subject: [PATCH 06/10] add InscribedShapeBuilder class (part 1) --- src/Geom/CMakeLists.txt | 1 + src/Geom/Geom.cpp | 2 + src/Geom/GeometryTypes.h | 4 + src/Geom/InscribedShapeBuilder.cpp | 107 ++++++++++++++++++++++ src/Geom/InscribedShapeBuilder.h | 142 +++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 src/Geom/InscribedShapeBuilder.cpp create mode 100644 src/Geom/InscribedShapeBuilder.h diff --git a/src/Geom/CMakeLists.txt b/src/Geom/CMakeLists.txt index f827b19c..2dc7bf0c 100644 --- a/src/Geom/CMakeLists.txt +++ b/src/Geom/CMakeLists.txt @@ -9,6 +9,7 @@ include_directories("third-party/SGeom/src/GEOMAlgo") set(MyGeom_SOURCES "OccUtils.cpp" #"GeometryFixer.cpp" + "InscribedShapeBuilder.cpp" "CollisionDetector.cpp" "Geom.cpp" "OpenCascadeAll.cpp" diff --git a/src/Geom/Geom.cpp b/src/Geom/Geom.cpp index 9966a8b3..7118e080 100644 --- a/src/Geom/Geom.cpp +++ b/src/Geom/Geom.cpp @@ -15,6 +15,7 @@ #include "GeometryPropertyBuilder.h" #include "GeometrySearchBuilder.h" #include "GeometryShapeChecker.h" +#include "InscribedShapeBuilder.h" #include "GeometryReader.h" #include "GeometryWriter.h" @@ -37,6 +38,7 @@ TYPESYSTEM_SOURCE(Geom::GeometryShapeChecker, Geom::GeometryProcessor); TYPESYSTEM_SOURCE(Geom::GeometryPropertyBuilder, Geom::GeometryProcessor); TYPESYSTEM_SOURCE(Geom::GeometrySearchBuilder, Geom::GeometryProcessor); TYPESYSTEM_SOURCE(Geom::BoundBoxBuilder, Geom::GeometryProcessor); +TYPESYSTEM_SOURCE(Geom::InscribedShapeBuilder, Geom::GeometryProcessor); TYPESYSTEM_SOURCE(Geom::CollisionDetector, Geom::GeometryProcessor); TYPESYSTEM_SOURCE(Geom::GeometryImprinter, Geom::CollisionDetector); diff --git a/src/Geom/GeometryTypes.h b/src/Geom/GeometryTypes.h index 0ad78e9e..18f19894 100644 --- a/src/Geom/GeometryTypes.h +++ b/src/Geom/GeometryTypes.h @@ -53,6 +53,10 @@ namespace Geom typedef MapType ItemContainerType; typedef std::shared_ptr ItemContainerPType; + /// conventional C enum starting from zero, can be used as array index + typedef GeomAbs_SurfaceType SurfaceType; + const size_t SurfacTypeCount = 11; /// total count of SurfaceType enum elements + /** * from OCCT to FreeCAD style better enum name * integer value: ShapeType == TopAbs_ShapeEnum; but NOT compatible diff --git a/src/Geom/InscribedShapeBuilder.cpp b/src/Geom/InscribedShapeBuilder.cpp new file mode 100644 index 00000000..d9ae08e7 --- /dev/null +++ b/src/Geom/InscribedShapeBuilder.cpp @@ -0,0 +1,107 @@ +#include "InscribedShapeBuilder.h" +#include "OccUtils.h" + +namespace Geom +{ + + bool isSharedFace(const TopoDS_Face& F, TopTools_MapOfShape& aFwdFMap, TopTools_MapOfShape& aRvsFMap) + { + TopAbs_Orientation anOri = F.Orientation(); + Standard_Boolean isFwd = anOri == TopAbs_FORWARD; + Standard_Boolean isRvs = Standard_False; + if (!isFwd) + { + isRvs = anOri == TopAbs_REVERSED; + } + if ((isFwd && !aFwdFMap.Add(F)) || (isRvs && !aRvsFMap.Add(F))) + { + return true; + } + return false; + } + + /// + std::vector InscribedShapeBuilder::calcAreaPerSurfaceType(const ItemType& s) + { + auto v = std::vector(SurfacTypeCount, 0.0); + // for each surface, calc area, added to v[stype], no interior, skipShared + TopTools_MapOfShape aFwdFMap; + TopTools_MapOfShape aRvsFMap; + TopLoc_Location aLocDummy; + + for (TopExp_Explorer ex(s, TopAbs_FACE); ex.More(); ex.Next()) + { + const TopoDS_Face& F = TopoDS::Face(ex.Current()); + + if (isSharedFace(F, aFwdFMap, aRvsFMap)) + continue; + + const Handle(Geom_Surface)& S = BRep_Tool::Surface(F, aLocDummy); + if (S.IsNull()) + continue; // is that common? + + GeomAdaptor_Surface AS(S); + GeomAbs_SurfaceType sType = AS.GetType(); + if (sType == GeomAbs_OffsetSurface) + { + // TODO: is that possible to get elementary surface type of OffsetSurface + v[int(sType)] += OccUtils::area(F); + } + else + { + v[int(sType)] += OccUtils::area(F); + } + } + return v; + } + + + + /// + InscribedShapeType calcInscribedSphere(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + { + InscribedShapeType ret; + + return ret; + } + + InscribedShapeType calcInscribedOBB(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + { + InscribedShapeType ret; + + return ret; + } + + InscribedShapeType calcInscribedCylinder(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + { + InscribedShapeType ret; + + return ret; + } + + /// return inscribed shape type can be judged by vector size + /// todo: find a way to decided, which is the best/max inscribed shape. + /// coil is fine + /// half cylinder, half sphere? max (bbox and inscribed_volume) + /// there maybe a way to check hollow shape, or outer shape? + InscribedShapeType InscribedShapeBuilder::calcInscribedShape(const ItemType& s, const GeometryProperty& gp, + const Bnd_OBB& obb) + { + InscribedShapeType ret; + auto a = estimateShapeType(s, gp, obb); + if (a == GeomAbs_Plane) + { + return calcInscribedOBB(s, gp, obb); + } + else if (a == GeomAbs_Sphere) + { + return calcInscribedSphere(s, gp, obb); + } + else + { + return calcInscribedCylinder(s, gp, obb); + } + return ret; + } + +} // namespace Geom \ No newline at end of file diff --git a/src/Geom/InscribedShapeBuilder.h b/src/Geom/InscribedShapeBuilder.h new file mode 100644 index 00000000..fffbcb87 --- /dev/null +++ b/src/Geom/InscribedShapeBuilder.h @@ -0,0 +1,142 @@ +#pragma once + +#include "GeometryProcessor.h" + +namespace Geom +{ + using namespace PPP; + + /// \ingroup Geom + + /// Inscribed Shape (Sphere, Box, etc) is distinguished by `std::vector` length + /// Sphere: Center(x, y, z), r + /// Box: like a OBB bound box, 6 elements: xmin, ymin, zmin, xmax, ymax, zmax + /// Cylinder: 7 elements: start, end, r + /// consider Geom3D base class? gp_Sphere + /// CONSIDER: std::variant + using InscribedShapeType = std::vector; + + + /** + * \brief calculate the Inscribed shape for a given shape if it is shell type + * + * result will be saved to a sparse vector of InscribedShapeType + * which is a std::vector, could be Sphere, Cylinder, Box + */ + class InscribedShapeBuilder : public GeometryProcessor + { + TYPESYSTEM_HEADER(); + + private: + /// consider return more than one Inscribed shape? + SparseVector myInscribedShapes; + + std::shared_ptr> myGeometryProperties; + std::shared_ptr> myShapeOrientedBoundBoxes; + + public: + InscribedShapeBuilder() + { + myCharacteristics["indexPattern"] = IndexPattern::Linear; + // myCharacteristics["indexDimension"] = 1; + myCharacteristics["requiredProperties"] = {"myGeometryProperties"}; + myCharacteristics["producedProperties"] = {"myInscribedShapes"}; + } + ~InscribedShapeBuilder() = default; + + /** + * \brief preparing work in serial mode + */ + virtual void prepareInput() override final + { + GeometryProcessor::prepareInput(); + + myGeometryProperties = myInputData->get>("myGeometryProperties"); + if (myInputData->contains("myShapeOrientedBoundBoxes")) + myShapeOrientedBoundBoxes = myInputData->get>("myShapeOrientedBoundBoxes"); + + /// prepare private properties like `std::vector.resize(myInputData->itemCount());` + /// therefore accessing item will not cause memory reallocation and items copying + myInscribedShapes = SparseVector>(myInputData->itemCount(), nullptr); + } + + /** + * \brief preparing work in serial mode, write report, move data into `myOutputData` + */ + virtual void prepareOutput() override final + { + myOutputData->emplace("myInscribedShapes", std::move(myInscribedShapes)); + } + + /** + * \brief process single data item in parallel without affecting other data items + * @param index: index to get/set by item(index)/setItem(index, newDataItem) + */ + virtual void processItem(const ItemIndexType index) override final + { + const auto gp = (*myGeometryProperties)[index]; + const auto obb = (*myShapeOrientedBoundBoxes)[index]; + if (isShell(gp, obb)) + { + auto ret = calcInscribedShape(item(index), gp, obb); + myInscribedShapes[index] = std::make_shared(std::move(ret)); + } + } + + /// implement this virtual function, if you need to process item(i) + // which is related to /coupled with /depending on/modifying item(j) + // virtual void processItemPair(const ItemIndexType i, const ItemIndexType j) override final + + /// private, protected functions + private: + /// detect if the shape is roughly a shell or hollow shape + /// condition 1: volume and area ratio, the max is a sphere + /// condition 2: volume / OBB volume, to exclude one planar sheet + /// resolved shape? + bool isShell(const GeometryProperty& gp, const Bnd_OBB& obb) + { + const double sphere_ratio = 1.0 / 3.0 / std::sqrt(M_PI * 4); + const double th = sphere_ratio * 0.2; + bool b1 = ((gp.volume / gp.area) / std::sqrt(gp.area) < th); + bool b2 = gp.volume * 5 < volume(obb); + if (b1 && b2) + return true; + return false; + } + + std::vector calcAreaPerSurfaceType(const ItemType& s); + InscribedShapeType calcInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb); + + /// shape is roughly a sphere, cylinder, box, + SurfaceType estimateShapeType(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + { + auto v = calcAreaPerSurfaceType(s); + // calc the max, get the index + auto i = std::distance(v.cbegin(), std::max_element(v.cbegin(), v.cend())); + + return SurfaceType(i); + } + + /// detect if the shape is roughly a planar sheet + /// obb vol slightly bigger than the shape volume, aspect ratios + bool isPlanarSheet(const GeometryProperty& gp, const Bnd_OBB& obb) const + { + auto ar = aspectRatios(obb); + const double ratio = 0.2; + return ar[0] < ratio and ar[1] < ratio; + } + + inline double volume(const Bnd_OBB& obb) const + { + return obb.XHSize() * obb.YHSize() * obb.ZHSize() * 8; + } + + inline std::vector aspectRatios(const Bnd_OBB& obb) const + { + std::vector myvector = {obb.XHSize(), obb.YHSize(), obb.ZHSize()}; + std::sort(myvector.begin(), myvector.end()); + auto max = myvector[2]; + return {myvector[0] / max, myvector[1] / max}; + } + }; +} // namespace Geom From 047480a006871b7727de1953a8c457d2a59d1c30 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Mon, 21 Dec 2020 16:00:33 +0000 Subject: [PATCH 07/10] add InscribedShapeBuilder class (part 2) --- src/Geom/Geom.cpp | 1 + src/Geom/GeometryTypes.h | 12 +++- src/Geom/InscribedShapeBuilder.cpp | 107 +++++++++++++++++++++++++--- src/Geom/InscribedShapeBuilder.h | 81 ++++++++++++++++++--- src/Geom/OccUtils.cpp | 23 +++++- src/Geom/OccUtils.h | 7 +- src/python/geomPipeline.py | 19 +++++ src/python/pppPipelineController.py | 26 +++++-- 8 files changed, 245 insertions(+), 31 deletions(-) diff --git a/src/Geom/Geom.cpp b/src/Geom/Geom.cpp index 7118e080..f45b546d 100644 --- a/src/Geom/Geom.cpp +++ b/src/Geom/Geom.cpp @@ -62,6 +62,7 @@ namespace Geom GeometryPropertyBuilder::init(); GeometrySearchBuilder::init(); BoundBoxBuilder::init(); + InscribedShapeBuilder::init(); GeometryShapeChecker::init(); CollisionDetector::init(); GeometryImprinter::init(); diff --git a/src/Geom/GeometryTypes.h b/src/Geom/GeometryTypes.h index 18f19894..e96b49ec 100644 --- a/src/Geom/GeometryTypes.h +++ b/src/Geom/GeometryTypes.h @@ -22,6 +22,15 @@ inline void from_json(const nlohmann::json& j, Bnd_Box& b) b.Update(v[0], v[1], v[2], v[3], v[4], v[5]); } +inline void to_json(nlohmann::json& j, const Bnd_Sphere& b) +{ + gp_XYZ c = b.Center(); + j = nlohmann::json{c.X(), c.Y(), c.Z(), b.Radius()}; +} + +/// OpenCASCADE has T::DumpJson(Standard_OStream & theOStream, Standard_Integer theDepth = -1 ) +/// T::InitFromJson(const Standard_SStream & theSStream, Standard_Integer & theStreamPos) + /// RGBA float array, conmponent value range [0, 1.0] inline void to_json(nlohmann::json& j, const Quantity_Color& p) { @@ -48,11 +57,12 @@ namespace Geom typedef Standard_Integer ItemHashType; static const ItemHashType ItemHashMax = INT_MAX; - typedef std::uint64_t UniqueIdType; // also define in PPP/UniqueId.h + typedef std::uint64_t UniqueIdType; /// defined in PPP/UniqueId.h /// map has order (non contiguous in memory), can increase capacity typedef MapType ItemContainerType; typedef std::shared_ptr ItemContainerPType; + typedef gp_Pnt PointType; /// conventional C enum starting from zero, can be used as array index typedef GeomAbs_SurfaceType SurfaceType; const size_t SurfacTypeCount = 11; /// total count of SurfaceType enum elements diff --git a/src/Geom/InscribedShapeBuilder.cpp b/src/Geom/InscribedShapeBuilder.cpp index d9ae08e7..c1e3bb60 100644 --- a/src/Geom/InscribedShapeBuilder.cpp +++ b/src/Geom/InscribedShapeBuilder.cpp @@ -1,9 +1,12 @@ #include "InscribedShapeBuilder.h" #include "OccUtils.h" +#include "BRepBuilderAPI_MakeVertex.hxx" + + namespace Geom { - + /// this function is not thread-safe bool isSharedFace(const TopoDS_Face& F, TopTools_MapOfShape& aFwdFMap, TopTools_MapOfShape& aRvsFMap) { TopAbs_Orientation anOri = F.Orientation(); @@ -20,6 +23,87 @@ namespace Geom return false; } + + /// generate coordinate index, then save all, otherwise, local not global max + /// const std::vector voids, or 3D array + PointType searchOBBGrid(const ItemType& shape, const Bnd_OBB& obb) + { + gp_Trsf trsf; + auto ax3 = gp_Ax3(obb.Center(), obb.ZDirection(), obb.XDirection()); + trsf.SetTransformation(ax3); + + const int dim = 3; + const int gz[dim] = {4, 4, 4}; // should consider aspect ratio + const Standard_Real s[dim] = {obb.XHSize() / gz[0], obb.XHSize() / gz[1], obb.XHSize() / gz[2]}; + auto& c = obb.Center(); + int i[dim] = {-gz[0], -gz[1], -gz[2]}; // shift to cell center by i+0.5 + for (; i[2] < gz[2]; i[2]++) + { + for (; i[1] < gz[1]; i[1]++) + { + for (; i[0] < gz[0]; i[0]++) + { + PointType p = {s[0] * (i[0] + 0.5), s[1] * (i[1] + 0.5), s[20] * (i[2] + 0.5)}; + p.Transform(trsf); + if (OccUtils::distance(shape, p) > Precision::Confusion()) + { + // size_t vi = i + /// assigned to result value voids[] + return p; + } + } + } + } + throw ProcessorError("searched all grid, can not find a void point"); + } + + PointType calcInitPoint(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + { + PointType cm = PointType(gp.centerOfMass[0], gp.centerOfMass[1], gp.centerOfMass[2]); + if (OccUtils::distance(s, cm) > Precision::Confusion()) + return cm; + PointType cb{obb.Center()}; + if (OccUtils::distance(s, cb) > Precision::Confusion()) + return cb; + PointType c{(cm.X() + cb.X()) * 0.5, (cm.Y() + cb.Y()) * 0.5, (cm.Z() + cb.Z()) * 0.5}; + if (OccUtils::distance(s, c) > Precision::Confusion()) + return c; + /// still can not find a void space inside the shape, then generete grid of OBB, then + throw ProcessorError("not implement for grid void space search"); + } + + + /* + template + ShapeT initInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb); + template <> + Bnd_Sphere initInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + { + + return Bnd_Sphere(gp_XYZ(c.X(), c.Y(), c.Z()), Precision::Confusion(), 10, 10); + } + + template <> Bnd_OBB initInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& + obb) + { + PointType c{obb.Center()}; + // if () detect if the point is out of this shape, + // c = PointType(gp.centerOfMass[0], gp.centerOfMass[1],gp.centerOfMass[2]); + + return Bnd_OBB(); + } + + + template ShapeT _calcInscribedShape(const ItemType& s, const ShapeT& initShape) + { + ShapeT maxShape = initShape; + + // center must not moved out Bnd_Bnd, radius + + return maxShape; + } + */ + /// std::vector InscribedShapeBuilder::calcAreaPerSurfaceType(const ItemType& s) { @@ -56,20 +140,23 @@ namespace Geom } - - /// + /// how to grow the shape? DOF InscribedShapeType calcInscribedSphere(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) { - InscribedShapeType ret; - - return ret; + auto c = calcInitPoint(s, gp, obb); + Standard_Real initLength = 1; // Precision::Confusion() + Bnd_Sphere ss(gp_XYZ(c.X(), c.Y(), c.Z()), initLength, 10, 10); // what does the U, V mean here? + return ss; } InscribedShapeType calcInscribedOBB(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) { - InscribedShapeType ret; - - return ret; + Bnd_OBB b(obb); + Standard_Real initLength = 1; // Precision::Confusion() + b.SetXComponent(obb.XDirection(), initLength); + b.SetYComponent(obb.YDirection(), initLength); + b.SetZComponent(obb.ZDirection(), initLength); + return b; } InscribedShapeType calcInscribedCylinder(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) @@ -79,6 +166,8 @@ namespace Geom return ret; } + // + /// return inscribed shape type can be judged by vector size /// todo: find a way to decided, which is the best/max inscribed shape. /// coil is fine diff --git a/src/Geom/InscribedShapeBuilder.h b/src/Geom/InscribedShapeBuilder.h index fffbcb87..6d66ce7b 100644 --- a/src/Geom/InscribedShapeBuilder.h +++ b/src/Geom/InscribedShapeBuilder.h @@ -1,6 +1,9 @@ #pragma once #include "GeometryProcessor.h" +#include "OccUtils.h" + +#include namespace Geom { @@ -10,11 +13,13 @@ namespace Geom /// Inscribed Shape (Sphere, Box, etc) is distinguished by `std::vector` length /// Sphere: Center(x, y, z), r - /// Box: like a OBB bound box, 6 elements: xmin, ymin, zmin, xmax, ymax, zmax - /// Cylinder: 7 elements: start, end, r + /// Box: like 6 elements: xmin, ymin, zmin, xmax, ymax, zmax + /// a OBB bound box, + /// Cylinder: 7 elements: start, end, r, gp_Cylinder /// consider Geom3D base class? gp_Sphere - /// CONSIDER: std::variant - using InscribedShapeType = std::vector; + /// CONSIDER: std::variant, then use std::visit + /// they + using InscribedShapeType = std::variant; /** @@ -57,7 +62,7 @@ namespace Geom /// prepare private properties like `std::vector.resize(myInputData->itemCount());` /// therefore accessing item will not cause memory reallocation and items copying - myInscribedShapes = SparseVector>(myInputData->itemCount(), nullptr); + myInscribedShapes = SparseVector(myInputData->itemCount(), nullptr); } /** @@ -65,6 +70,12 @@ namespace Geom */ virtual void prepareOutput() override final { + if (myConfig.contains("output")) + { + auto file_name = dataStoragePath(parameter("output")); + writeResult(file_name); + LOG_F(INFO, "output inscribed shapes into file: %s", file_name.c_str()); + } myOutputData->emplace("myInscribedShapes", std::move(myInscribedShapes)); } @@ -76,7 +87,7 @@ namespace Geom { const auto gp = (*myGeometryProperties)[index]; const auto obb = (*myShapeOrientedBoundBoxes)[index]; - if (isShell(gp, obb)) + if (true) // hasLargeVoid(gp, obb) { auto ret = calcInscribedShape(item(index), gp, obb); myInscribedShapes[index] = std::make_shared(std::move(ret)); @@ -90,15 +101,16 @@ namespace Geom /// private, protected functions private: /// detect if the shape is roughly a shell or hollow shape - /// condition 1: volume and area ratio, the max is a sphere + /// condition 1: volume and area ratio, the max is a sphere, tge smaller is sheet /// condition 2: volume / OBB volume, to exclude one planar sheet /// resolved shape? - bool isShell(const GeometryProperty& gp, const Bnd_OBB& obb) + bool hasLargeVoid(const GeometryProperty& gp, const Bnd_OBB& obb) { const double sphere_ratio = 1.0 / 3.0 / std::sqrt(M_PI * 4); const double th = sphere_ratio * 0.2; bool b1 = ((gp.volume / gp.area) / std::sqrt(gp.area) < th); - bool b2 = gp.volume * 5 < volume(obb); + const double volume_ratio = 3; + bool b2 = gp.volume * volume_ratio < volume(obb); if (b1 && b2) return true; return false; @@ -108,7 +120,7 @@ namespace Geom InscribedShapeType calcInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb); /// shape is roughly a sphere, cylinder, box, - SurfaceType estimateShapeType(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) + SurfaceType estimateShapeType(const ItemType& s, const GeometryProperty&, const Bnd_OBB&) { auto v = calcAreaPerSurfaceType(s); // calc the max, get the index @@ -119,7 +131,7 @@ namespace Geom /// detect if the shape is roughly a planar sheet /// obb vol slightly bigger than the shape volume, aspect ratios - bool isPlanarSheet(const GeometryProperty& gp, const Bnd_OBB& obb) const + bool isPlanarSheet(const Bnd_OBB& obb) const { auto ar = aspectRatios(obb); const double ratio = 0.2; @@ -131,6 +143,7 @@ namespace Geom return obb.XHSize() * obb.YHSize() * obb.ZHSize() * 8; } + /// consider: split out into BndUtils.h inline std::vector aspectRatios(const Bnd_OBB& obb) const { std::vector myvector = {obb.XHSize(), obb.YHSize(), obb.ZHSize()}; @@ -138,5 +151,51 @@ namespace Geom auto max = myvector[2]; return {myvector[0] / max, myvector[1] / max}; } + + ItemType inscribedShapeToShape(const InscribedShapeType& shape) + { + std::cout << "variant index = " << shape.index() << std::endl; + // if (std::holds_alternative(shape)) + // return toShape(std::get(shape)); + // else if (std::holds_alternative(shape)) + // return toShape(std::get(shape)); + if (std::holds_alternative(shape)) + return toShape(std::get(shape)); + else if (std::holds_alternative(shape)) + return toShape(std::get(shape)); + else + throw ProcessorError("BoundShape type not supported in std::variant"); + } + // template ItemType toShape(const ShapeT& s); + ItemType toShape(const Bnd_Sphere& s) + { + return BRepPrimAPI_MakeSphere(s.Center(), s.Radius()); + } + + ItemType toShape(const Bnd_OBB& obb) + { + auto ax2 = gp_Ax2(obb.Center(), obb.ZDirection(), obb.YDirection()); // not sure + ax2.Translate(gp_Vec(obb.XHSize() * -1, obb.YHSize() * -1, obb.ZHSize() * -1)); + // todo: translate the box + return BRepPrimAPI_MakeBox(ax2, obb.XHSize() * 2, obb.YHSize() * 2, obb.ZHSize() * 2).Shape(); + } + + void writeResult(const std::string filename) + { + std::vector shapes; + // auto converter = [&, this](const auto& s) -> ItemType { return toShape(s); } + // std::visit(converter, *p) + for (const auto& p : myInscribedShapes) + { + if (p) + shapes.push_back(inscribedShapeToShape(*p)); + } + + /// may save in dataStorage folder + if (shapes.size() > 0) + OccUtils::saveShape(OccUtils::createCompound(shapes), filename); + else + LOG_F(INFO, "result inscribed shapes are empty, skip writting"); + } }; } // namespace Geom diff --git a/src/Geom/OccUtils.cpp b/src/Geom/OccUtils.cpp index 26ed7de6..b2b32af3 100644 --- a/src/Geom/OccUtils.cpp +++ b/src/Geom/OccUtils.cpp @@ -346,6 +346,22 @@ namespace Geom return ss; } + TopoDS_Compound createCompound(const SparseVector shapes) + { + TopoDS_Builder cBuilder; + TopoDS_Compound merged; + cBuilder.MakeCompound(merged); + size_t i = 0; + for (const auto& p : shapes) + { + if (p) + { + cBuilder.Add(merged, *p); + } + i++; + } + return merged; + } TopoDS_Compound createCompound(const ItemContainerType theSolids, std::shared_ptr> suppressed) @@ -605,6 +621,11 @@ namespace Geom v_props.Mass(), {v_props.CentreOfMass().X(), v_props.CentreOfMass().Y(), v_props.CentreOfMass().Z()}); return gid; } + Standard_Real distance(const TopoDS_Shape& s, const PointType& p) + { + auto v = BRepBuilderAPI_MakeVertex(p).Vertex(); + return distance(s, v); + } Standard_Real distance(const TopoDS_Shape& s1, const TopoDS_Shape& s2) { @@ -850,7 +871,7 @@ namespace Geom } } - TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const gp_Pnt origin) + TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const PointType& origin) { gp_Trsf ts; ts.SetScale(origin, scale); diff --git a/src/Geom/OccUtils.h b/src/Geom/OccUtils.h index 52db31f3..0b178b44 100644 --- a/src/Geom/OccUtils.h +++ b/src/Geom/OccUtils.h @@ -57,7 +57,7 @@ namespace Geom GeomExport Standard_Boolean isCoincidentDomain(const TopoDS_Shape& shape, const TopoDS_Shape& shape2); /// two-step unifying: first unify edges, seconly unify faces, - /// deprecated: use glueFaces() instead + /// DEPRECATED: use glueFaces() instead GeomExport TopoDS_Shape unifyFaces(const TopoDS_Shape& _shape); /// the input shapes to be glued should be a compound @@ -66,6 +66,7 @@ namespace Geom GeomExport TopoDS_Shape glueFaces(const TopoDS_Shape& _shape, const Standard_Real tolerance = 0.0); + GeomExport TopoDS_Compound createCompound(const SparseVector shapes); GeomExport TopoDS_Compound createCompound(const ItemContainerType theSolids, std::shared_ptr> suppressed = nullptr); @@ -93,6 +94,8 @@ namespace Geom GeomExport Standard_Real volume(const TopoDS_Shape& s); GeomExport Standard_Real tolerance(const TopoDS_Shape& s); GeomExport Standard_Real distance(const TopoDS_Shape& s1, const TopoDS_Shape& s2); + GeomExport Standard_Real distance(const TopoDS_Shape& s, const PointType& p); + GeomExport UniqueIdType uniqueId(const GeometryProperty& p); GeomExport UniqueIdType uniqueId(const TopoDS_Shape& s); @@ -118,7 +121,7 @@ namespace Geom GeomExport Standard_Boolean saveMesh(TopoDS_Shape& aShape, const std::string file_name); /// scale up or down shape - GeomExport TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const gp_Pnt origin = gp_Pnt()); + GeomExport TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const PointType& origin = gp_Pnt()); } // namespace OccUtils diff --git a/src/python/geomPipeline.py b/src/python/geomPipeline.py index fbf2db41..2d279ecc 100644 --- a/src/python/geomPipeline.py +++ b/src/python/geomPipeline.py @@ -83,6 +83,13 @@ def geom_add_argument(parser): help="do not merge the imprinted shapes, for two-step workflow", ) + # bool argument for imprint + parser.add_argument( + "--use-inscribed-shape", + action="store_true", + help="", + ) + # bool argument for imprint parser.add_argument( "--ignore-failed", @@ -254,6 +261,16 @@ def geom_add_argument(parser): "doc": "calc boundbox for collision detection, decompose", } +InscribedShapeBuilder = { + "className": "Geom::InscribedShapeBuilder", + "doc": "calc max void or hollow space of a shape's oriented boundbox can hold", + "output": { + "type": "filename", + "value": "inscribed_shapes.brep", + "doc": "optional: result of inscribed shape calculation", + }, +} + # shared by all actions basic_processors = [GeometryShapeChecker, GeometryPropertyBuilder, BoundBoxBuilder] @@ -400,6 +417,8 @@ def geom_add_argument(parser): processors.append(post_GeometryPropertyBuilder) config_file_content["writers"] = writers elif args.action == Action.imprint: + if args.use_inscribed_shape: + processors.append(InscribedShapeBuilder) processors.append(GeometryImprinter) processors += post_processors config_file_content["writers"] = writers diff --git a/src/python/pppPipelineController.py b/src/python/pppPipelineController.py index bfa88e6e..2c03cf7d 100644 --- a/src/python/pppPipelineController.py +++ b/src/python/pppPipelineController.py @@ -15,6 +15,7 @@ import sys import os.path +import shutil import json import copy from collections import OrderedDict @@ -33,6 +34,8 @@ ################################################################### USAGE = "input data file is the only compulsory argument, get help by -h argument" +generated_config_file_name = "config.json" +default_log_filename = "debug_info.log" debugging = 0 # this verbosity only control this python script, # args.verbosity argument control the console verbosity @@ -155,7 +158,7 @@ def generate_config_file_header(args): }, "logger": { "verbosity": args.verbosity, # print to console: DEBUG, PROGRESS, INFO, WARNING - "logFileName": "debug_info.log", # only INFO level or above will print to console + "logFileName": default_log_filename, # only INFO level or above will print to console "logLevel": "DEBUG", # DEBUG, PROGRESS, INFO, WARNING }, } @@ -174,18 +177,28 @@ def ppp_get_output_dir(args): inputFile = ppp_parse_input(args) case_name = ppp_get_case_name(inputFile) # this naming must be consistent with DataStorage::generateStoragePath(input_name) - outputDir = (args.workingDir + os.path.sep + case_name + "_processed") # can be detected from output_filename full path - return outputDir + caseDir = (args.workingDir + os.path.sep + case_name + "_processed") # can be detected from output_filename full path + return caseDir def ppp_post_process(args): # currently, only make symbolic link to input file into the output dir inputFile = ppp_parse_input(args) inputDir, inputFilename = os.path.split(inputFile) - outputDir = ppp_get_output_dir(args) - linkToInputFile = outputDir + os.path.sep + inputFilename + caseDir = ppp_get_output_dir(args) + linkToInputFile = caseDir + os.path.sep + inputFilename + + if os.path.exists(generated_config_file_name): + shutil.copyfile(generated_config_file_name, caseDir + os.path.sep + generated_config_file_name) + else: + print("config file does not exists in the current dir", os.curdir) + + if os.path.exists(default_log_filename): + shutil.copyfile(default_log_filename, caseDir + os.path.sep + default_log_filename) + else: + print("log file does not exists in the current dir", os.curdir) - if os.path.exists(outputDir): + if os.path.exists(caseDir): try: # failed on windows os.symlink(inputFile, linkToInputFile) except: @@ -196,7 +209,6 @@ def generate_config_file(config_file_content, args): # parse args first, the write config file config_file_given = False - generated_config_file_name = "config.json" # input json file is config, but not geomtry input manifest file if args.input.find(".json") > 0: with open(args.input, "r") as f: From fbd7bb9117c0f998768167e6f05a279a68c17fa2 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Mon, 21 Dec 2020 16:01:29 +0000 Subject: [PATCH 08/10] refactor GeomTestBase and derived unit test classes --- src/python/GeomTestBase.py | 60 +++++++++++++++++++++--------------- src/python/test_collision.py | 9 ------ src/python/test_imprint.py | 8 ----- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/python/GeomTestBase.py b/src/python/GeomTestBase.py index 149ce0cc..12be7ffb 100644 --- a/src/python/GeomTestBase.py +++ b/src/python/GeomTestBase.py @@ -12,7 +12,6 @@ import unittest ###################################################### -#from geomPipeline import ppp_start_pipeline from pppStartPipeline import ppp_start_pipeline from detectFreeCAD import append_freecad_mod_path @@ -27,25 +26,18 @@ import FreeCAD as App import Part -############################################################ - -default_geometry_filename = ( - gettempdir() - + os.path.sep - + "test_geometry.stp" # tempfile.tempdir is None on Ubuntu? -) # todo: fix the tmp folder for windows ######################### utilities ############################ -def generate_config(inf, outf, action="imprint", default_config_file_name="config.json"): +def generate_config(inf, outf, action="imprint", args="", default_config_file_name="config.json"): # generate a configuration without running it this_geomPipeline = os.path.dirname(os.path.abspath(__file__)) + os.path.sep + "geomPipeline.py" #print("geomPipeline.py {} {} --config".format(action, inf)) if not os.path.exists(this_geomPipeline): this_geomPipeline = "geomPipeline.py" - os.system("{} {} {} --config".format(this_geomPipeline, action, inf)) + os.system("{} {} {} {} --config".format(this_geomPipeline, action, inf, args)) jf = open(default_config_file_name, "r", encoding="utf-8") config_file_content = json.load(jf) @@ -63,8 +55,9 @@ def generate_config(inf, outf, action="imprint", default_config_file_name="confi class GeomTestBase(unittest.TestCase): """ - derived class need to implement 2 methods: - build_geometry(freecadDoc) and validate_geometry(partObject) + For imprint tests, derived classes only need to implement 2 methods: + `build_geometry(freecadDoc)` and `validate_geometry(partObject)` + For other tests, `setup` may need to be rewritten to generate config.json """ # there is error to user constructor for unit test, not new style object @@ -75,7 +68,6 @@ class GeomTestBase(unittest.TestCase): # pass def build_geometry(self, doc): - self.skip_test_base = True pass def validate_geometry(self, partObject): @@ -84,6 +76,9 @@ def validate_geometry(self, partObject): def generate_test_filename(self): return gettempdir() + os.path.sep + self.__class__.__name__ + ".stp" + def get_processed_folder(self): + return gettempdir() + os.path.sep + self.__class__.__name__ + "_processed" + def build(self): outfile = self.generate_test_filename() document_name = self.__class__.__name__ @@ -100,22 +95,34 @@ def build(self): App.closeDocument(document_name) return outfile - def _setUp(self): - self.geometry_file = self.build() - self.output_geometry_file = self.geometry_file.replace( - ".stp", "_processed.brep" - ) - config = generate_config(self.geometry_file, self.output_geometry_file) + def setup_config(self, action ="imprint", args = "", input_file=None, result_file=None): + # setup up input and output files and generate pipeline config.json + if input_file: + self.geometry_file = input_file + else: + self.geometry_file = self.build() + input_stem = ".".join(self.geometry_file.split(".")[:-1]) + self.output_geometry_file = input_stem + "_processed.brep" + + self.config_file = generate_config(self.geometry_file, self.output_geometry_file, action, args=args) - ppp_start_pipeline(config) + if result_file: + self.result_file = result_file + + def setup(self): + self.setup_config() def validate(self): - self._setUp() # this method should be override if result is not single DocumentObject + + ppp_start_pipeline(self.config_file) assert os.path.exists(self.geometry_file) default_document_name = self.__class__.__name__ + "_processed" App.newDocument(default_document_name) - Part.insert(self.output_geometry_file, default_document_name) + if hasattr(self, "result_file"): + Part.insert(self.result_file, default_document_name) + else: + Part.insert(self.output_geometry_file, default_document_name) doc = App.getDocument(default_document_name) obj = doc.Objects[0] # get the first and only obj for brep if hasattr(obj, "Shape"): @@ -124,6 +131,9 @@ def validate(self): # tear down App.closeDocument(default_document_name) # will this be done if failed? - # def test(self): - # if hasattr(self, "skip_test_base") and (not self.skip_test_base): - # self.validate() + def test(self): + # skip the test of this BaseClass + if self.__class__.__name__ != "GeomTestBase": + self.build() + self.setup() + self.validate() \ No newline at end of file diff --git a/src/python/test_collision.py b/src/python/test_collision.py index cd4c04d4..8ce51ef8 100644 --- a/src/python/test_collision.py +++ b/src/python/test_collision.py @@ -68,9 +68,6 @@ def validate_geometry(self, shape): assert shape.ShapeType == "CompSolid" or shape.ShapeType == "Compound" # assert len(shape.Faces) == 20 # 4 cubes shares 4 faces, total 20 faces - def test(self): - self.validate() - class WeakInterferenceTest(GeomTestBase): # def __init__(self, test_name="weak_interference"): @@ -97,9 +94,6 @@ def validate_geometry(self, shape): assert len(shape.Faces) == 30 # 4 cubes shares 4 faces, total 20 faces + 10 # todo: face count needs to be further investigated - def test(self): - self.validate() - class StrongInterferenceTest(GeomTestBase): def build_geometry(self, doc): @@ -123,9 +117,6 @@ def validate_geometry(self, shape, supressing=True): assert len(shape.Solids) == 5 assert len(shape.Faces) == 26 # output geometry == input - def test(self): - self.validate() - if __name__ == "__main__": unittest.main(exit=False) diff --git a/src/python/test_imprint.py b/src/python/test_imprint.py index 4f0da351..128cae56 100644 --- a/src/python/test_imprint.py +++ b/src/python/test_imprint.py @@ -75,9 +75,6 @@ def validate_geometry(self, shape): assert len(shape.Faces) == nonshared_faces # 2 cubes shares 1 face, total 11 faces - def test(self): - self.validate() - class AlignedBoxesTest(GeomTestBase): def build_geometry(self, doc): @@ -93,9 +90,6 @@ def validate_geometry(self, shape): N = len(shape.Solids) assert len(shape.Faces) == N * 6 - (N - 1) - def test(self): - self.validate() - class StackedCurvedTest(GeomTestBase): def build_geometry(self, doc): @@ -146,8 +140,6 @@ def validate_geometry(self, shape): # 4 boxes has faces = (N-1) * 6 - 4, cylinder has 4 or 5 on side, plus 2 assert len(shape.Faces) == (N - 1) * 6 - 4 + 6 - def test(self): - self.validate() # =============================================================== From ee35349de610995e203054efb1be520a23753b68 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Mon, 21 Dec 2020 16:02:51 +0000 Subject: [PATCH 09/10] add a python test for InscribedShapeBuilder --- scripts/run_all_tests.sh | 5 ++ src/PPP/DataStorage.h | 6 +- src/python/test_inscribedShape.py | 124 ++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/python/test_inscribedShape.py diff --git a/scripts/run_all_tests.sh b/scripts/run_all_tests.sh index 8f911636..c76a5d9b 100644 --- a/scripts/run_all_tests.sh +++ b/scripts/run_all_tests.sh @@ -57,6 +57,7 @@ geomPipeline.py check ../data/test_geometry/test_geometry.stp --thread-count 1 - if [ ! $? -eq 0 ] ; then echo "Error during geometry test in single thread mode" ; exit 1 ; fi # test_*.py has not been installed by package, so must be copied into this place +# todo: pytest to auto discover tests cp ../../src/python/*.py ./ if [ $? -eq 0 ] then @@ -67,6 +68,10 @@ then if [ $? -eq 0 ]; then test_collision.py fi + + if [ $? -eq 0 ]; then + test_inscribedShape.py + fi echo "test completed" else echo "geometry pipeline test failed" diff --git a/src/PPP/DataStorage.h b/src/PPP/DataStorage.h index 67ffaf85..d60abec9 100644 --- a/src/PPP/DataStorage.h +++ b/src/PPP/DataStorage.h @@ -50,9 +50,9 @@ namespace PPP LOG_F(INFO, "all result data will be saved to folder: %s", myStoragePath.c_str()); } - std::string generateStoragePath(std::string input_name) + std::string generateStoragePath(std::string case_name) { - return input_name + "_processed"; + return case_name + "_processed"; } virtual std::string storagePath() @@ -71,7 +71,7 @@ namespace PPP fs::path _path{filename}; if (_path.is_absolute()) { - LOG_F(WARNING, "path `%s` is absolute, return it as it is", filename.c_str()); + // LOG_F(WARNING, "path `%s` is absolute, return it as it is", filename.c_str()); return filename; } auto target = fs::path(myStoragePath) / filename; diff --git a/src/python/test_inscribedShape.py b/src/python/test_inscribedShape.py new file mode 100644 index 00000000..d01d52c0 --- /dev/null +++ b/src/python/test_inscribedShape.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# copyright Qingfeng Xia + + +from __future__ import print_function, division +import sys +import os.path +import os + + +"""feature test done by FreeCAD script, non-MPI console mode only, on smaller geometry +this script may only works on POSIX system, +USAGE: python3 this_script.py +""" + +import unittest +from GeomTestBase import GeomTestBase, App, Part + +length = 10 +sheet_ratio = 0.1 + +def makeInscribedBox(doc, hollow=True): + + smallBox = doc.addObject("Part::Box", "Box") + smallBox.Length = length*(1.0-2*sheet_ratio) + smallBox.Width = length*(1.0-2*sheet_ratio) + if hollow: + smallBox.Placement = App.Placement( + App.Vector(length*sheet_ratio, length*sheet_ratio), + App.Rotation(App.Vector(0, 0, 1), 0), + ) + doc.recompute() + return smallBox + +def makeInscribedCylinder(doc, hollow=True): + + s = doc.addObject("Part::Cylinder", "Cylinder") + if hollow: + s.Radius = length*(0.5 - sheet_ratio) + s.Height = length + + s.Placement = App.Placement( + App.Vector(length/2, length/2), + App.Rotation(App.Vector(0, 0, 1), 0), + ) + else: + s.Radius = length*(1-2*sheet_ratio) + s.Height = length + + s.Placement = App.Placement( + App.Vector(length*sheet_ratio, length*sheet_ratio), + App.Rotation(App.Vector(0, 0, 1), 0), + ) + doc.recompute() + return s + +def makeInscribedSphere(doc, hollow=True): + sheet_ratio = 0.05 + s = doc.addObject("Part::Sphere","Sphere") + if hollow: + s.Radius = length*(0.5 - sheet_ratio) + + s.Placement = App.Placement( + App.Vector(length/2, length/2, length*sheet_ratio*3), + App.Rotation(App.Vector(0, 0, 1), 0), + ) + else: + s.Radius = length*(1-2*sheet_ratio) + + s.Placement = App.Placement( + App.Vector(0, 0, 0), + App.Rotation(App.Vector(0, 0, 1), 0), + ) + doc.recompute() + return s + +############################################################################# +def makeHollowBoxes(doc): + print("a geometry of box without internal cut out") + distance = length + N = 6 + + tools = [] + makers = [makeInscribedBox, makeInscribedCylinder, makeInscribedSphere] + for f in makers: + tools.append(f(doc, True)) + tools.append(f(doc, False)) + + cuts = [] + for i in range(N): + cut= doc.addObject("Part::Cut","Cut") + cut.Base = doc.addObject("Part::Box", "Box") + cut.Tool = tools[i] + cut.Placement = App.Placement( + App.Vector(i * distance, 0, 0), + App.Rotation(App.Vector(0, 0, 1), 0) # todo: rotate the result shape&inscribed shape + ) + doc.recompute() + cuts.append(cut) + + return cuts + + +############################################################## +class InscribedBoxesTest(GeomTestBase): + def build_geometry(self, doc): + return makeHollowBoxes(doc) + + def validate_geometry(self, shape): + + assert len(shape.Solids) >= 1 + assert shape.ShapeType == "CompSolid" or shape.ShapeType == "Compound" + + def setup(self): + args = "--use-inscribed-shape" + result_file = self.get_processed_folder() + os.path.sep + "inscribed_shapes.brep" + self.setup_config(action ="imprint", args = args, result_file=result_file) + + +# =============================================================== +if __name__ == "__main__": # no need this if used with pytest-3 + unittest.main(exit=False) + From c9d2bd8f107ada5cd2dd1f5dadb316957b463ef1 Mon Sep 17 00:00:00 2001 From: qingfengxia Date: Mon, 4 Jan 2021 13:57:57 +0000 Subject: [PATCH 10/10] add a naive inscribed_sphere calc solution --- src/Geom/InscribedShapeBuilder.cpp | 119 ++++++++++++++++++++++++++--- src/Geom/InscribedShapeBuilder.h | 46 +++++++++-- src/Geom/OccUtils.cpp | 68 +++++++++++++++++ src/Geom/OccUtils.h | 3 + 4 files changed, 216 insertions(+), 20 deletions(-) diff --git a/src/Geom/InscribedShapeBuilder.cpp b/src/Geom/InscribedShapeBuilder.cpp index c1e3bb60..2f2d771c 100644 --- a/src/Geom/InscribedShapeBuilder.cpp +++ b/src/Geom/InscribedShapeBuilder.cpp @@ -2,6 +2,7 @@ #include "OccUtils.h" #include "BRepBuilderAPI_MakeVertex.hxx" +#include namespace Geom @@ -23,6 +24,30 @@ namespace Geom return false; } + /// work only for composolid or solid, after imprinting, excluding all shared faces + std::vector getFreeFaces(const ItemType& s) + { + TopTools_MapOfShape aFwdFMap; + TopTools_MapOfShape aRvsFMap; + std::vector faces; // item must be the pointer to TopoDS_Shape or derived + + TopExp_Explorer ex(s, TopAbs_FACE); + // faces.reserve(ex.Depth()); + TopLoc_Location aLocDummy; + for (; ex.More(); ex.Next()) + { + const TopoDS_Face& F = TopoDS::Face(ex.Current()); + // TopAbs_INTERNAL can be a face embedded in a solid + if (F.Orientation() == TopAbs_INTERNAL or F.Orientation() == TopAbs_EXTERNAL) + continue; + const Handle(Geom_Surface)& S = BRep_Tool::Surface(F, aLocDummy); + if (S.IsNull()) + continue; // is that common? + if (not isSharedFace(F, aFwdFMap, aRvsFMap)) + faces.push_back(F); + } + return faces; + } /// generate coordinate index, then save all, otherwise, local not global max /// const std::vector voids, or 3D array @@ -68,8 +93,9 @@ namespace Geom PointType c{(cm.X() + cb.X()) * 0.5, (cm.Y() + cb.Y()) * 0.5, (cm.Z() + cb.Z()) * 0.5}; if (OccUtils::distance(s, c) > Precision::Confusion()) return c; - /// still can not find a void space inside the shape, then generete grid of OBB, then - throw ProcessorError("not implement for grid void space search"); + /// still can not find a void space inside the shape, then generete grid of OBB and search + /// throw ProcessorError("not implement for grid void space search"); + return c; } @@ -104,11 +130,13 @@ namespace Geom } */ - /// + /// may ignore if surface is not elementary types std::vector InscribedShapeBuilder::calcAreaPerSurfaceType(const ItemType& s) { auto v = std::vector(SurfacTypeCount, 0.0); + const bool elementarySurfaceOnly = true; // for each surface, calc area, added to v[stype], no interior, skipShared +#if 1 TopTools_MapOfShape aFwdFMap; TopTools_MapOfShape aRvsFMap; TopLoc_Location aLocDummy; @@ -119,7 +147,12 @@ namespace Geom if (isSharedFace(F, aFwdFMap, aRvsFMap)) continue; - +#else + TopLoc_Location aLocDummy; + auto freeFaces = getFreeFaces(s); + for (const auto& F : freeFaces) + { +#endif const Handle(Geom_Surface)& S = BRep_Tool::Surface(F, aLocDummy); if (S.IsNull()) continue; // is that common? @@ -128,31 +161,91 @@ namespace Geom GeomAbs_SurfaceType sType = AS.GetType(); if (sType == GeomAbs_OffsetSurface) { - // TODO: is that possible to get elementary surface type of OffsetSurface - v[int(sType)] += OccUtils::area(F); + const auto bs = AS.BasisSurface(); + sType = bs->GetType(); } + + if (sType < GeomAbs_BezierSurface || sType == GeomAbs_SurfaceOfRevolution) + v[int(sType)] += OccUtils::area(F); else { - v[int(sType)] += OccUtils::area(F); + if (!elementarySurfaceOnly) + v[int(sType)] += OccUtils::area(F); } } return v; } + /// distance, contact point and normal + struct ContactInfo + { + double distance; + PointType point; + gp_Dir normal; + Standard_Integer hash; + }; + + // return zero distance if there is contact and interference + std::vector calcContact(const ItemType& s, const PointType& p) + { + TopLoc_Location aLocDummy; + auto freeFaces = getFreeFaces(s); + std::vector infos; + for (const auto& F : freeFaces) + { + auto V = BRepBuilderAPI_MakeVertex(p).Vertex(); + auto dss = BRepExtrema_DistShapeShape(); // GeomAPI_ExtremaSurfaceSurface + dss.LoadS1(F); + dss.LoadS2(V); + dss.Perform(); + auto contact = dss.PointOnShape1(1); + auto distance = dss.Value(); + Standard_Integer hash = HashCode(F, INT_MAX); +#if 0 + double u, v; + dss.ParOnFaceS1(1, u, v); // Exception here, why? + + const Handle(Geom_Surface)& S = BRep_Tool::Surface(F, aLocDummy); + if (S.IsNull()) + continue; // this has been filtered out by getFreeFaces() + GeomAdaptor_Surface AS(S); + + const double precision = 1e-3; + GeomLProp_SLProps props(AS.Surface(), u, v, 1 /* max 1 derivation */, precision); + auto normal = props.Normal(); +#else + gp_Dir normal; +#endif + infos.emplace_back(ContactInfo{distance, contact, normal, hash}); + } + return infos; + } - /// how to grow the shape? DOF + /// TODO: not completed, how to grow the shape? InscribedShapeType calcInscribedSphere(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) { auto c = calcInitPoint(s, gp, obb); - Standard_Real initLength = 1; // Precision::Confusion() + auto infos = calcContact(s, c); + std::vector dist; + for (const auto& info : infos) + { + dist.push_back(info.distance); + } + auto min = *std::min_element(dist.begin(), dist.end()); + Standard_Real initLength = Precision::Confusion(); + if (min > 0) + initLength = min; + // todo: based on infos, it may be possible to quickly get local max inscribed sphere Bnd_Sphere ss(gp_XYZ(c.X(), c.Y(), c.Z()), initLength, 10, 10); // what does the U, V mean here? return ss; } + /// TODO: not completed, maybe based on inscribed_sphere InscribedShapeType calcInscribedOBB(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) { Bnd_OBB b(obb); Standard_Real initLength = 1; // Precision::Confusion() + b.SetXComponent(obb.XDirection(), initLength); b.SetYComponent(obb.YDirection(), initLength); b.SetZComponent(obb.ZDirection(), initLength); @@ -176,21 +269,23 @@ namespace Geom InscribedShapeType InscribedShapeBuilder::calcInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb) { - InscribedShapeType ret; auto a = estimateShapeType(s, gp, obb); + auto ins = calcInscribedSphere(s, gp, obb); + return ins; + /* if (a == GeomAbs_Plane) { return calcInscribedOBB(s, gp, obb); } else if (a == GeomAbs_Sphere) { - return calcInscribedSphere(s, gp, obb); + return ins; } else { return calcInscribedCylinder(s, gp, obb); } - return ret; + */ } } // namespace Geom \ No newline at end of file diff --git a/src/Geom/InscribedShapeBuilder.h b/src/Geom/InscribedShapeBuilder.h index 6d66ce7b..d9b8b09a 100644 --- a/src/Geom/InscribedShapeBuilder.h +++ b/src/Geom/InscribedShapeBuilder.h @@ -27,6 +27,17 @@ namespace Geom * * result will be saved to a sparse vector of InscribedShapeType * which is a std::vector, could be Sphere, Cylinder, Box + * + * Current implementation: non optimal inscribed sphere can be generated as a demo + * + * Note: + * inscribed shape calc is much harder than bounding shape + * since no matter where you start you will get the final/minimium bounding shape. + * Meanwhile a inscribed shape highly depends on initial point to search; + * it may ended with a local convergent solution but it may be not the global optimal solution. + * + * Stop for the time being, to see if any published algorithm can be found + * */ class InscribedShapeBuilder : public GeometryProcessor { @@ -89,8 +100,15 @@ namespace Geom const auto obb = (*myShapeOrientedBoundBoxes)[index]; if (true) // hasLargeVoid(gp, obb) { - auto ret = calcInscribedShape(item(index), gp, obb); - myInscribedShapes[index] = std::make_shared(std::move(ret)); + try + { + auto ret = calcInscribedShape(item(index), gp, obb); + myInscribedShapes[index] = std::make_shared(std::move(ret)); + } + catch (...) + { + LOG_F(WARNING, "failed to calculate the inscribed shape for item %lu, just skip", index); + } } } @@ -107,9 +125,9 @@ namespace Geom bool hasLargeVoid(const GeometryProperty& gp, const Bnd_OBB& obb) { const double sphere_ratio = 1.0 / 3.0 / std::sqrt(M_PI * 4); - const double th = sphere_ratio * 0.2; + const double th = sphere_ratio * 0.2; // todo: make a parameter bool b1 = ((gp.volume / gp.area) / std::sqrt(gp.area) < th); - const double volume_ratio = 3; + const double volume_ratio = 3; // todo: make a parameter bool b2 = gp.volume * volume_ratio < volume(obb); if (b1 && b2) return true; @@ -120,13 +138,25 @@ namespace Geom InscribedShapeType calcInscribedShape(const ItemType& s, const GeometryProperty& gp, const Bnd_OBB& obb); /// shape is roughly a sphere, cylinder, box, + /// for OffSet and Revolution surface, it is likely + /// call only after `hasLargeVoid()` + /// use Aspect ratio is decide Cylinder or Sphere SurfaceType estimateShapeType(const ItemType& s, const GeometryProperty&, const Bnd_OBB&) { auto v = calcAreaPerSurfaceType(s); // calc the max, get the index auto i = std::distance(v.cbegin(), std::max_element(v.cbegin(), v.cend())); - - return SurfaceType(i); + // if a shape has SweptSurface, Cylinder can be the best inscribed shape + auto t = SurfaceType(i); + if (t == GeomAbs_Plane || t == GeomAbs_Sphere) + return t; + else if (t < GeomAbs_BezierSurface) + return GeomAbs_Sphere; // may also Cylinder + else + { + // return GeomAbs_Sphere; + throw ProcessorError("Should not get here, as non-elementary surface area is not calculated"); + } } /// detect if the shape is roughly a planar sheet @@ -154,7 +184,7 @@ namespace Geom ItemType inscribedShapeToShape(const InscribedShapeType& shape) { - std::cout << "variant index = " << shape.index() << std::endl; + // std::cout << "variant index = " << shape.index() << std::endl; // fine // if (std::holds_alternative(shape)) // return toShape(std::get(shape)); // else if (std::holds_alternative(shape)) @@ -164,7 +194,7 @@ namespace Geom else if (std::holds_alternative(shape)) return toShape(std::get(shape)); else - throw ProcessorError("BoundShape type not supported in std::variant"); + throw ProcessorError("Inscribed shape type not supported in std::variant"); } // template ItemType toShape(const ShapeT& s); ItemType toShape(const Bnd_Sphere& s) diff --git a/src/Geom/OccUtils.cpp b/src/Geom/OccUtils.cpp index b2b32af3..05c14e2e 100644 --- a/src/Geom/OccUtils.cpp +++ b/src/Geom/OccUtils.cpp @@ -153,6 +153,7 @@ namespace Geom } /// only works on compound? return face count + /// opencascade has a GUI inspect tool, ShapeView to explore a brep shape std::map exploreShape(const TopoDS_Shape& aShape) { using namespace std; @@ -878,5 +879,72 @@ namespace Geom return BRepBuilderAPI_Transform(from, ts, true); } + //======================================================================= + // Function : IsLinear + // purpose : Returns TRUE if theC is line-like. + // Source: not exposed function in OpenCASCADE BRepBndLib_1.cxx + //======================================================================= + Standard_Boolean IsLinear(const Adaptor3d_Curve& theC) + { + const GeomAbs_CurveType aCT = theC.GetType(); + if (aCT == GeomAbs_OffsetCurve) + { + return IsLinear(GeomAdaptor_Curve(theC.OffsetCurve()->BasisCurve())); + } + + if ((aCT == GeomAbs_BSplineCurve) || (aCT == GeomAbs_BezierCurve)) + { + // Indeed, curves with C0-continuity and degree==1, may be + // represented with set of points. It will be possible made + // in the future. + + return ((theC.Degree() == 1) && (theC.Continuity() != GeomAbs_C0)); + } + + if (aCT == GeomAbs_Line) + { + return Standard_True; + } + + return Standard_False; + } + + //======================================================================= + // Function : IsPlanar + // purpose : Returns TRUE if theS is plane-like. + // Source: not exposed function in OpenCASCADE BRepBndLib_1.cxx + //======================================================================= + Standard_Boolean IsPlanar(const Adaptor3d_Surface& theS) + { + const GeomAbs_SurfaceType aST = theS.GetType(); + if (aST == GeomAbs_OffsetSurface) + { + return IsPlanar(theS.BasisSurface()->Surface()); + } + + if (aST == GeomAbs_SurfaceOfExtrusion) + { + return IsLinear(theS.BasisCurve()->Curve()); + } + + if ((aST == GeomAbs_BSplineSurface) || (aST == GeomAbs_BezierSurface)) + { + if ((theS.UDegree() != 1) || (theS.VDegree() != 1)) + return Standard_False; + + // Indeed, surfaces with C0-continuity and degree==1, may be + // represented with set of points. It will be possible made + // in the future. + + return ((theS.UContinuity() != GeomAbs_C0) && (theS.VContinuity() != GeomAbs_C0)); + } + + if (aST == GeomAbs_Plane) + { + return Standard_True; + } + + return Standard_False; + } } // namespace OccUtils } // namespace Geom diff --git a/src/Geom/OccUtils.h b/src/Geom/OccUtils.h index 0b178b44..d41d01c5 100644 --- a/src/Geom/OccUtils.h +++ b/src/Geom/OccUtils.h @@ -123,6 +123,9 @@ namespace Geom /// scale up or down shape GeomExport TopoDS_Shape scaleShape(const TopoDS_Shape& from, double scale, const PointType& origin = gp_Pnt()); + GeomExport bool IsLinear(const Adaptor3d_Curve& theC); + GeomExport bool IsPlanar(const Adaptor3d_Surface& theS); + } // namespace OccUtils } // namespace Geom