ccCommandCrossSection.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #include "ccCommandCrossSection.h"
  2. #include "ccCommandLineCommands.h"
  3. #include <ccHObjectCaster.h>
  4. #include <ccMesh.h>
  5. #include "ccCropTool.h"
  6. #include <QDir>
  7. #include <QXmlStreamReader> // to read the 'Cross Section' tool XML parameters file
  8. constexpr char COMMAND_CROSS_SECTION[] = "CROSS_SECTION";
  9. CommandCrossSection::CommandCrossSection()
  10. : ccCommandLineInterface::Command("Cross section", COMMAND_CROSS_SECTION)
  11. {}
  12. bool CommandCrossSection::process(ccCommandLineInterface &cmd)
  13. {
  14. cmd.print("[CROSS SECTION]");
  15. static QString s_xmlCloudCompare = "CloudCompare";
  16. static QString s_xmlBoxThickness = "BoxThickness";
  17. static QString s_xmlBoxCenter = "BoxCenter";
  18. static QString s_xmlRepeatDim = "RepeatDim";
  19. static QString s_xmlRepeatGap = "RepeatGap";
  20. static QString s_xmlFilePath = "FilePath";
  21. static QString s_outputXmlFilePath = "OutputFilePath";
  22. //expected argument: XML file
  23. if (cmd.arguments().empty())
  24. return cmd.error(QString("Missing parameter: XML parameters file after \"-%1\"").arg(COMMAND_CROSS_SECTION));
  25. QString xmlFilename = cmd.arguments().takeFirst();
  26. //read the XML file
  27. CCVector3 boxCenter(0, 0, 0);
  28. CCVector3 boxThickness(0, 0, 0);
  29. bool repeatDim[3] = { false, false, false };
  30. double repeatGap = 0.0;
  31. bool inside = true;
  32. bool autoCenter = true;
  33. QString inputFilePath;
  34. QString outputFilePath;
  35. {
  36. QFile file(xmlFilename);
  37. if (!file.open(QFile::ReadOnly | QFile::Text))
  38. {
  39. return cmd.error(QString("Couldn't open XML file '%1'").arg(xmlFilename));
  40. }
  41. //read file content
  42. QXmlStreamReader stream(&file);
  43. //expected: CloudCompare
  44. if (!stream.readNextStartElement()
  45. || stream.name() != s_xmlCloudCompare)
  46. {
  47. return cmd.error(QString("Invalid XML file (should start by '<%1>')").arg(s_xmlCloudCompare));
  48. }
  49. unsigned mandatoryCount = 0;
  50. while (stream.readNextStartElement()) //loop over the elements
  51. {
  52. if (stream.name() == s_xmlBoxThickness)
  53. {
  54. QXmlStreamAttributes attributes = stream.attributes();
  55. if (!readVector(attributes, boxThickness, s_xmlBoxThickness, cmd))
  56. return false;
  57. stream.skipCurrentElement();
  58. ++mandatoryCount;
  59. }
  60. else if (stream.name() == s_xmlBoxCenter)
  61. {
  62. QXmlStreamAttributes attributes = stream.attributes();
  63. if (!readVector(attributes, boxCenter, s_xmlBoxCenter, cmd))
  64. return false;
  65. stream.skipCurrentElement();
  66. autoCenter = false;
  67. }
  68. else if (stream.name() == s_xmlRepeatDim)
  69. {
  70. QString itemValue = stream.readElementText();
  71. bool ok = false;
  72. int dim = itemValue.toInt(&ok);
  73. if (!ok || dim < 0 || dim > 2)
  74. {
  75. return cmd.error(QString("Invalid XML file (invalid value for '<%1>')").arg(s_xmlRepeatDim));
  76. }
  77. repeatDim[dim] = true;
  78. }
  79. else if (stream.name() == s_xmlRepeatGap)
  80. {
  81. QString itemValue = stream.readElementText();
  82. bool ok = false;
  83. repeatGap = itemValue.toDouble(&ok);
  84. if (!ok)
  85. {
  86. return cmd.error(QString("Invalid XML file (invalid value for '<%1>')").arg(s_xmlRepeatGap));
  87. }
  88. }
  89. else if (stream.name() == s_xmlFilePath)
  90. {
  91. inputFilePath = stream.readElementText();
  92. if (!QDir(inputFilePath).exists())
  93. {
  94. return cmd.error(QString("Invalid file path (directory pointed by '<%1>' doesn't exist)").arg(s_xmlFilePath));
  95. }
  96. //++mandatoryCount;
  97. }
  98. else if (stream.name() == s_outputXmlFilePath)
  99. {
  100. outputFilePath = stream.readElementText();
  101. if (!QDir(outputFilePath).exists())
  102. {
  103. return cmd.error(QString("Invalid output file path (directory pointed by '<%1>' doesn't exist)").arg(s_outputXmlFilePath));
  104. }
  105. //++mandatoryCount;
  106. }
  107. else
  108. {
  109. cmd.warning(QString("Unknown element: %1").arg(stream.name().toString()));
  110. stream.skipCurrentElement();
  111. }
  112. }
  113. if (mandatoryCount < 1 || (!repeatDim[0] && !repeatDim[1] && !repeatDim[2]))
  114. {
  115. return cmd.error(QString("Some mandatory elements are missing in the XML file (see documentation)"));
  116. }
  117. }
  118. //safety checks
  119. if ( CCCoreLib::LessThanEpsilon( boxThickness.x )
  120. || CCCoreLib::LessThanEpsilon( boxThickness.y )
  121. || CCCoreLib::LessThanEpsilon( boxThickness.z )
  122. )
  123. {
  124. return cmd.error(QString("Invalid box thickness"));
  125. }
  126. CCVector3 repeatStep = boxThickness + CCVector3(repeatGap, repeatGap, repeatGap);
  127. if ( (repeatDim[0] && CCCoreLib::LessThanEpsilon( repeatStep.x ) )
  128. || (repeatDim[1] && CCCoreLib::LessThanEpsilon( repeatStep.y ) )
  129. || (repeatDim[2] && CCCoreLib::LessThanEpsilon( repeatStep.z ) )
  130. )
  131. {
  132. return cmd.error(QString("Repeat gap can't be equal or smaller than 'minus' box width"));
  133. }
  134. if (outputFilePath.isEmpty())
  135. {
  136. outputFilePath = inputFilePath;
  137. }
  138. int iterationCount = 1;
  139. //shall we load the entities?
  140. QStringList files;
  141. QDir dir;
  142. bool fromFiles = false;
  143. if (!inputFilePath.isEmpty())
  144. {
  145. //look for all files in the input directory
  146. dir = QDir(inputFilePath);
  147. assert(dir.exists());
  148. files = dir.entryList(QDir::Files);
  149. iterationCount = files.size();
  150. fromFiles = true;
  151. //remove any cloud or mesh in memory!
  152. cmd.removeClouds();
  153. cmd.removeMeshes();
  154. }
  155. for (int f = 0; f < iterationCount; ++f)
  156. {
  157. //shall we load files?
  158. QString filename;
  159. if (fromFiles)
  160. {
  161. assert(f < files.size());
  162. filename = dir.absoluteFilePath(files[f]);
  163. QFileInfo fileinfo(filename);
  164. if (!fileinfo.isFile() || fileinfo.suffix().toUpper() == "XML")
  165. {
  166. continue;
  167. }
  168. //let's try to load the file
  169. cmd.print(QString("Processing file: '%1'").arg(files[f]));
  170. bool result = false;
  171. {
  172. //hack: replace the current argument list by a fake 'load file' sequence
  173. QStringList realArguments = cmd.arguments();
  174. QStringList loadArguments;
  175. loadArguments << filename;
  176. cmd.arguments() = loadArguments;
  177. result = CommandLoad().process(cmd);
  178. //end of hack: restore the current argument list
  179. cmd.arguments() = realArguments;
  180. }
  181. if (!result)
  182. {
  183. cmd.warning("\tFailed to load file!");
  184. continue;
  185. }
  186. }
  187. else
  188. {
  189. assert(iterationCount == 1);
  190. }
  191. //repeat crop process on each file (or do it only once on the currently loaded entities)
  192. {
  193. ccHObject::Container entities;
  194. try
  195. {
  196. for (size_t i = 0; i < cmd.clouds().size(); ++i)
  197. entities.push_back(cmd.clouds()[i].pc);
  198. for (size_t j = 0; j < cmd.meshes().size(); ++j)
  199. entities.push_back(cmd.meshes()[j].mesh);
  200. }
  201. catch (const std::bad_alloc&)
  202. {
  203. return cmd.error("Not enough memory!");
  204. }
  205. for (size_t i = 0; i < entities.size(); ++i)
  206. {
  207. //check entity bounding-box
  208. ccHObject* ent = entities[i];
  209. ccBBox bbox = ent->getOwnBB();
  210. if (!bbox.isValid())
  211. {
  212. cmd.warning(QString("Entity '%1' has an invalid bounding-box!").arg(ent->getName()));
  213. continue;
  214. }
  215. //browse to/create a subdirectory with the (base) filename as name
  216. QString basename;
  217. if (fromFiles)
  218. {
  219. basename = QFileInfo(filename).baseName();
  220. }
  221. else
  222. {
  223. basename = i < cmd.clouds().size() ? cmd.clouds()[i].basename : cmd.meshes()[i - cmd.clouds().size()].basename;
  224. }
  225. if (entities.size() > 1)
  226. basename += QString("_%1").arg(i + 1);
  227. QDir outputDir(outputFilePath);
  228. if (outputFilePath.isEmpty())
  229. {
  230. if (fromFiles)
  231. {
  232. assert(false);
  233. outputDir = QDir::current();
  234. }
  235. else
  236. {
  237. outputDir = QDir(i < cmd.clouds().size() ? cmd.clouds()[i].path : cmd.meshes()[i - cmd.clouds().size()].path);
  238. }
  239. }
  240. assert(outputDir.exists());
  241. if (outputDir.cd(basename))
  242. {
  243. //if the directory already exists...
  244. cmd.warning(QString("Subdirectory '%1' already exists").arg(basename));
  245. }
  246. else if (outputDir.mkdir(basename))
  247. {
  248. outputDir.cd(basename);
  249. }
  250. else
  251. {
  252. cmd.warning(QString("Failed to create subdirectory '%1' (check access rights and base name validity!)").arg(basename));
  253. continue;
  254. }
  255. int toto = ceil(-0.4);
  256. int toto2 = ceil(-0.6);
  257. //place the initial box at the beginning of the entity bounding box
  258. CCVector3 C0 = autoCenter ? bbox.getCenter() : boxCenter;
  259. unsigned steps[3] = { 1, 1, 1 };
  260. for (unsigned d = 0; d < 3; ++d)
  261. {
  262. if (repeatDim[d])
  263. {
  264. PointCoordinateType boxHalfWidth = boxThickness.u[d] / 2;
  265. PointCoordinateType distToMinBorder = C0.u[d] - boxHalfWidth - bbox.minCorner().u[d];
  266. int stepsToMinBorder = static_cast<int>(ceil(distToMinBorder / repeatStep.u[d]));
  267. C0.u[d] -= stepsToMinBorder * repeatStep.u[d];
  268. PointCoordinateType distToMaxBorder = bbox.maxCorner().u[d] - C0.u[d] - boxHalfWidth;
  269. int stepsToMaxBoder = static_cast<int>(ceil(distToMaxBorder / repeatStep.u[d]) + 1);
  270. assert(stepsToMaxBoder >= 0);
  271. steps[d] = std::max<unsigned>(stepsToMaxBoder, 1);
  272. }
  273. }
  274. cmd.print(QString("Will extract up to (%1 x %2 x %3) = %4 sections").arg(steps[0]).arg(steps[1]).arg(steps[2]).arg(steps[0] * steps[1] * steps[2]));
  275. //now extract the slices
  276. for (unsigned dx = 0; dx < steps[0]; ++dx)
  277. {
  278. for (unsigned dy = 0; dy < steps[1]; ++dy)
  279. {
  280. for (unsigned dz = 0; dz < steps[2]; ++dz)
  281. {
  282. CCVector3 C = C0 + CCVector3(dx*repeatStep.x, dy*repeatStep.y, dz*repeatStep.z);
  283. ccBBox cropBox(C - boxThickness / 2, C + boxThickness / 2, true);
  284. cmd.print(QString("Box (%1;%2;%3) --> (%4;%5;%6)")
  285. .arg(cropBox.minCorner().x).arg(cropBox.minCorner().y).arg(cropBox.minCorner().z)
  286. .arg(cropBox.maxCorner().x).arg(cropBox.maxCorner().y).arg(cropBox.maxCorner().z)
  287. );
  288. ccHObject* croppedEnt = ccCropTool::Crop(ent, cropBox, inside);
  289. if (croppedEnt)
  290. {
  291. QString outputBasename = basename + QString("_%1_%2_%3").arg(C.x).arg(C.y).arg(C.z);
  292. QString errorStr;
  293. //original entity is a cloud?
  294. if (i < cmd.clouds().size())
  295. {
  296. CLCloudDesc desc(static_cast<ccPointCloud*>(croppedEnt),
  297. outputBasename,
  298. outputDir.absolutePath(),
  299. entities.size() > 1 ? static_cast<int>(i) : -1);
  300. errorStr = cmd.exportEntity(desc);
  301. }
  302. else //otherwise it's a mesh
  303. {
  304. CLMeshDesc desc(static_cast<ccMesh*>(croppedEnt),
  305. outputBasename,
  306. outputDir.absolutePath(),
  307. entities.size() > 1 ? static_cast<int>(i) : -1);
  308. errorStr = cmd.exportEntity(desc);
  309. }
  310. delete croppedEnt;
  311. croppedEnt = nullptr;
  312. if (!errorStr.isEmpty())
  313. return cmd.error(errorStr);
  314. }
  315. }
  316. }
  317. }
  318. }
  319. if (fromFiles)
  320. {
  321. //unload entities
  322. cmd.removeClouds();
  323. cmd.removeMeshes();
  324. }
  325. }
  326. }
  327. return true;
  328. }
  329. bool CommandCrossSection::readVector(const QXmlStreamAttributes &attributes, CCVector3 &P, QString element, const ccCommandLineInterface &cmd)
  330. {
  331. if (attributes.size() < 3)
  332. {
  333. return cmd.error(QString("Invalid XML file (3 attributes expected for element '<%1>')").arg(element));
  334. }
  335. int count = 0;
  336. for (int i = 0; i < attributes.size(); ++i)
  337. {
  338. QString name = attributes[i].name().toString().toUpper();
  339. QString value = attributes[i].value().toString();
  340. bool ok = false;
  341. if (name == "X")
  342. {
  343. P.x = value.toDouble(&ok);
  344. ++count;
  345. }
  346. else if (name == "Y")
  347. {
  348. P.y = value.toDouble(&ok);
  349. ++count;
  350. }
  351. else if (name == "Z")
  352. {
  353. P.z = value.toDouble(&ok);
  354. ++count;
  355. }
  356. else
  357. {
  358. ok = true;
  359. }
  360. if (!ok)
  361. {
  362. return cmd.error(QString("Invalid XML file (numerical attribute expected for attribute '%1' of element '<%2>')").arg(name, element));
  363. }
  364. }
  365. if (count < 3)
  366. {
  367. return cmd.error(QString("Invalid XML file (attributes 'X','Y' and 'Z' are mandatory for element '<%1>')").arg(element));
  368. }
  369. return true;
  370. }