You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

553 lines
17 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU General Public License
  8. * as published by the Free Software Foundation; either version 2
  9. * of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program; if not, you may find one here:
  18. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  19. * or you may search the http://www.gnu.org website for the version 2 license,
  20. * or you may write to the Free Software Foundation, Inc.,
  21. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  22. */
  23. #include "tool/construction_manager.h"
  24. #include <chrono>
  25. #include <condition_variable>
  26. #include <thread>
  27. #include <advanced_config.h>
  28. #include <hash.h>
  29. /**
  30. * A helper class to manage the activation of a "proposal" after a timeout.
  31. *
  32. * When a proposal is made, a timer starts. If no new proposal is made and the proposal
  33. * is not cancelled before the timer expires, the proposal is "accepted" via a callback.
  34. *
  35. * Propos
  36. *
  37. * @tparam T The type of the proposal, which will be passed to the callback (by value)
  38. */
  39. template <typename T>
  40. class ACTIVATION_HELPER
  41. {
  42. public:
  43. using ACTIVATION_CALLBACK = std::function<void( T&& )>;
  44. ACTIVATION_HELPER( std::chrono::milliseconds aTimeout, ACTIVATION_CALLBACK aCallback ) :
  45. m_timeout( aTimeout ), m_callback( std::move( aCallback ) ), m_stop( false ),
  46. m_thread( &ACTIVATION_HELPER::ProposalCheckFunction, this )
  47. {
  48. }
  49. ~ACTIVATION_HELPER()
  50. {
  51. // Stop the delay thread and wait for it
  52. {
  53. std::lock_guard<std::mutex> lock( m_mutex );
  54. m_stop = true;
  55. m_cv.notify_all();
  56. }
  57. if( m_thread.joinable() )
  58. {
  59. m_thread.join();
  60. }
  61. }
  62. void ProposeActivation( T&& aProposal, std::size_t aProposalTag )
  63. {
  64. std::lock_guard<std::mutex> lock( m_mutex );
  65. if( m_lastAcceptedProposalTag.has_value() && aProposalTag == *m_lastAcceptedProposalTag )
  66. {
  67. // This proposal was accepted last time
  68. // (could be made optional if we want to allow re-accepting the same proposal)
  69. return;
  70. }
  71. if( m_pendingProposalTag.has_value() && aProposalTag == *m_pendingProposalTag )
  72. {
  73. // This proposal is already pending
  74. return;
  75. }
  76. m_pendingProposalTag = aProposalTag;
  77. m_lastProposal = std::move( aProposal );
  78. m_proposalDeadline = std::chrono::steady_clock::now() + m_timeout;
  79. m_cv.notify_all();
  80. }
  81. void CancelProposal()
  82. {
  83. std::lock_guard<std::mutex> lock( m_mutex );
  84. m_pendingProposalTag.reset();
  85. m_cv.notify_all();
  86. }
  87. void ProposalCheckFunction()
  88. {
  89. while( !m_stop )
  90. {
  91. std::unique_lock<std::mutex> lock( m_mutex );
  92. if( !m_stop && !m_pendingProposalTag.has_value() )
  93. {
  94. // No active proposal - wait for one (unlocks while waiting)
  95. m_cv.wait( lock );
  96. }
  97. if( !m_stop && m_pendingProposalTag.has_value() )
  98. {
  99. // Active proposal - wait for timeout
  100. auto now = std::chrono::steady_clock::now();
  101. if( m_cv.wait_for( lock, m_proposalDeadline - now ) == std::cv_status::timeout )
  102. {
  103. // See if the timeout was extended for a new proposal
  104. now = std::chrono::steady_clock::now();
  105. if( now < m_proposalDeadline )
  106. {
  107. // Extended - wait for the new deadline
  108. continue;
  109. }
  110. // See if there is still a proposal to accept
  111. // (could have been cancelled in the meantime)
  112. if( m_pendingProposalTag )
  113. {
  114. m_lastAcceptedProposalTag = m_pendingProposalTag;
  115. m_pendingProposalTag.reset();
  116. T proposalToAccept = std::move( m_lastProposal );
  117. lock.unlock();
  118. // Call the callback (outside the lock)
  119. m_callback( std::move( proposalToAccept ) );
  120. }
  121. }
  122. }
  123. }
  124. }
  125. private:
  126. mutable std::mutex m_mutex;
  127. // Activation timeout in milliseconds
  128. std::chrono::milliseconds m_timeout;
  129. ///< Callback to call when the proposal is accepted
  130. ACTIVATION_CALLBACK m_callback;
  131. std::condition_variable m_cv;
  132. std::atomic<bool> m_stop;
  133. std::thread m_thread;
  134. std::chrono::time_point<std::chrono::steady_clock> m_proposalDeadline;
  135. ///< The last proposal tag that was made
  136. std::optional<std::size_t> m_pendingProposalTag;
  137. ///< The last proposal that was accepted
  138. std::optional<std::size_t> m_lastAcceptedProposalTag;
  139. // The most recently-proposed item
  140. T m_lastProposal;
  141. };
  142. struct CONSTRUCTION_MANAGER::PENDING_BATCH
  143. {
  144. CONSTRUCTION_ITEM_BATCH Batch;
  145. bool IsPersistent;
  146. };
  147. CONSTRUCTION_MANAGER::CONSTRUCTION_MANAGER( CONSTRUCTION_VIEW_HANDLER& aHelper ) :
  148. m_viewHandler( aHelper )
  149. {
  150. const std::chrono::milliseconds acceptanceTimeout(
  151. ADVANCED_CFG::GetCfg().m_ExtensionSnapTimeoutMs );
  152. m_activationHelper = std::make_unique<ACTIVATION_HELPER<std::unique_ptr<PENDING_BATCH>>>(
  153. acceptanceTimeout,
  154. [this]( std::unique_ptr<PENDING_BATCH>&& aAccepted )
  155. {
  156. acceptConstructionItems( std::move( aAccepted ) );
  157. } );
  158. }
  159. CONSTRUCTION_MANAGER::~CONSTRUCTION_MANAGER()
  160. {
  161. }
  162. /**
  163. * Construct a hash based on the sources of the items in the batch.
  164. */
  165. static std::size_t
  166. HashConstructionBatchSources( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH& aBatch,
  167. bool aIsPersistent )
  168. {
  169. std::size_t hash = hash_val( aIsPersistent );
  170. for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& item : aBatch )
  171. {
  172. hash_combine( hash, item.Source, item.Item );
  173. }
  174. return hash;
  175. }
  176. void CONSTRUCTION_MANAGER::ProposeConstructionItems(
  177. std::unique_ptr<CONSTRUCTION_ITEM_BATCH> aBatch, bool aIsPersistent )
  178. {
  179. if( aBatch->empty() )
  180. {
  181. // There's no point in proposing an empty batch
  182. // It would just clear existing construction items for nothing new
  183. return;
  184. }
  185. const std::size_t hash = HashConstructionBatchSources( *aBatch, aIsPersistent );
  186. m_activationHelper->ProposeActivation(
  187. std::make_unique<PENDING_BATCH>( std::move( *aBatch ), aIsPersistent ), hash );
  188. }
  189. void CONSTRUCTION_MANAGER::CancelProposal()
  190. {
  191. m_activationHelper->CancelProposal();
  192. }
  193. void CONSTRUCTION_MANAGER::acceptConstructionItems( std::unique_ptr<PENDING_BATCH> aAcceptedBatch )
  194. {
  195. const auto getInvolved = [&]( const CONSTRUCTION_ITEM_BATCH& aBatchToAdd )
  196. {
  197. for( const CONSTRUCTION_ITEM& item : aBatchToAdd )
  198. {
  199. // Only show the item if it's not already involved
  200. // (avoid double-drawing the same item)
  201. if( m_involvedItems.count( item.Item ) == 0 )
  202. {
  203. m_involvedItems.insert( item.Item );
  204. }
  205. }
  206. };
  207. // Copies for use outside the lock
  208. std::vector<CONSTRUCTION_ITEM_BATCH> persistentBatches, temporaryBatches;
  209. {
  210. std::lock_guard<std::mutex> lock( m_batchesMutex );
  211. if( aAcceptedBatch->IsPersistent )
  212. {
  213. // We only keep one previous persistent batch for the moment
  214. m_persistentConstructionBatch = std::move( aAcceptedBatch->Batch );
  215. }
  216. else
  217. {
  218. bool anyNewItems = false;
  219. for( CONSTRUCTION_ITEM& item : aAcceptedBatch->Batch )
  220. {
  221. if( m_involvedItems.count( item.Item ) == 0 )
  222. {
  223. anyNewItems = true;
  224. break;
  225. }
  226. }
  227. // If there are no new items involved, don't bother adding the batch
  228. if( !anyNewItems )
  229. {
  230. return;
  231. }
  232. // We only keep up to one previous temporary batch and the current one
  233. // we could make this a setting if we want to keep more, but it gets cluttered
  234. const int maxTempItems = 2;
  235. while( m_temporaryConstructionBatches.size() >= maxTempItems )
  236. {
  237. m_temporaryConstructionBatches.pop_front();
  238. }
  239. m_temporaryConstructionBatches.emplace_back( std::move( aAcceptedBatch->Batch ) );
  240. }
  241. m_involvedItems.clear();
  242. // Copy the batches for use outside the lock
  243. if( m_persistentConstructionBatch )
  244. {
  245. getInvolved( *m_persistentConstructionBatch );
  246. persistentBatches.push_back( *m_persistentConstructionBatch );
  247. }
  248. for( const CONSTRUCTION_ITEM_BATCH& batch : m_temporaryConstructionBatches )
  249. {
  250. getInvolved( batch );
  251. temporaryBatches.push_back( batch );
  252. }
  253. }
  254. KIGFX::CONSTRUCTION_GEOM& geom = m_viewHandler.GetViewItem();
  255. geom.ClearDrawables();
  256. const auto addDrawables =
  257. [&]( const std::vector<CONSTRUCTION_ITEM_BATCH>& aBatches, bool aIsPersistent )
  258. {
  259. for( const CONSTRUCTION_ITEM_BATCH& batch : aBatches )
  260. {
  261. for( const CONSTRUCTION_ITEM& item : batch )
  262. {
  263. for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& drawable : item.Constructions )
  264. {
  265. geom.AddDrawable( drawable, aIsPersistent );
  266. }
  267. }
  268. }
  269. };
  270. addDrawables( persistentBatches, true );
  271. addDrawables( temporaryBatches, false );
  272. m_viewHandler.updateView();
  273. }
  274. bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const
  275. {
  276. for( EDA_ITEM* item : aItems )
  277. {
  278. // Null items (i.e. construction items) are always considered involved
  279. if( item && m_involvedItems.count( item ) == 0 )
  280. {
  281. return false;
  282. }
  283. }
  284. return true;
  285. }
  286. void CONSTRUCTION_MANAGER::GetConstructionItems(
  287. std::vector<CONSTRUCTION_ITEM_BATCH>& aToExtend ) const
  288. {
  289. std::lock_guard<std::mutex> lock( m_batchesMutex );
  290. if( m_persistentConstructionBatch )
  291. {
  292. aToExtend.push_back( *m_persistentConstructionBatch );
  293. }
  294. for( const CONSTRUCTION_ITEM_BATCH& batch : m_temporaryConstructionBatches )
  295. {
  296. aToExtend.push_back( batch );
  297. }
  298. }
  299. bool CONSTRUCTION_MANAGER::HasActiveConstruction() const
  300. {
  301. std::lock_guard<std::mutex> lock( m_batchesMutex );
  302. return m_persistentConstructionBatch.has_value() || !m_temporaryConstructionBatches.empty();
  303. }
  304. SNAP_LINE_MANAGER::SNAP_LINE_MANAGER( CONSTRUCTION_VIEW_HANDLER& aViewHandler ) :
  305. m_viewHandler( aViewHandler )
  306. {
  307. }
  308. void SNAP_LINE_MANAGER::SetSnapLineOrigin( const VECTOR2I& aOrigin )
  309. {
  310. // Setting the origin clears the snap line as the end point is no longer valid
  311. ClearSnapLine();
  312. m_snapLineOrigin = aOrigin;
  313. }
  314. void SNAP_LINE_MANAGER::SetSnapLineEnd( const OPT_VECTOR2I& aSnapEnd )
  315. {
  316. if( m_snapLineOrigin && aSnapEnd != m_snapLineEnd )
  317. {
  318. m_snapLineEnd = aSnapEnd;
  319. if( m_snapLineEnd )
  320. m_viewHandler.GetViewItem().SetSnapLine( SEG{ *m_snapLineOrigin, *m_snapLineEnd } );
  321. else
  322. m_viewHandler.GetViewItem().ClearSnapLine();
  323. m_viewHandler.updateView();
  324. }
  325. }
  326. void SNAP_LINE_MANAGER::ClearSnapLine()
  327. {
  328. m_snapLineOrigin.reset();
  329. m_snapLineEnd.reset();
  330. m_viewHandler.GetViewItem().ClearSnapLine();
  331. m_viewHandler.updateView();
  332. }
  333. void SNAP_LINE_MANAGER::SetSnappedAnchor( const VECTOR2I& aAnchorPos )
  334. {
  335. if( m_snapLineOrigin.has_value() )
  336. {
  337. if( aAnchorPos.x == m_snapLineOrigin->x || aAnchorPos.y == m_snapLineOrigin->y )
  338. {
  339. SetSnapLineEnd( aAnchorPos );
  340. }
  341. else
  342. {
  343. // Snapped to something that is not the snap line origin, so
  344. // this anchor is now the new snap line origin
  345. SetSnapLineOrigin( aAnchorPos );
  346. }
  347. }
  348. else
  349. {
  350. // If there's no snap line, start one
  351. SetSnapLineOrigin( aAnchorPos );
  352. }
  353. }
  354. /**
  355. * Check if the cursor has moved far enough away from the snap line origin to escape snapping
  356. * in the X direction.
  357. *
  358. * This is defined as within aEscapeRange of the snap line origin, and within aLongRangeEscapeAngle
  359. * of the vertical line passing through the snap line origin.
  360. */
  361. static bool pointHasEscapedSnapLineX( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
  362. int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
  363. {
  364. if( std::abs( aCursor.x - aSnapLineOrigin.x ) < aEscapeRange )
  365. {
  366. return false;
  367. }
  368. EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin ) + EDA_ANGLE( 90, DEGREES_T );
  369. return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
  370. }
  371. /**
  372. * As above, but for the Y direction.
  373. */
  374. static bool pointHasEscapedSnapLineY( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
  375. int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
  376. {
  377. if( std::abs( aCursor.y - aSnapLineOrigin.y ) < aEscapeRange )
  378. {
  379. return false;
  380. }
  381. EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin );
  382. return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
  383. }
  384. OPT_VECTOR2I SNAP_LINE_MANAGER::GetNearestSnapLinePoint( const VECTOR2I& aCursor,
  385. const VECTOR2I& aNearestGrid,
  386. std::optional<int> aDistToNearest,
  387. int aSnapRange ) const
  388. {
  389. // return std::nullopt;
  390. if( m_snapLineOrigin )
  391. {
  392. bool snapLine = false;
  393. VECTOR2I bestSnapPoint = aNearestGrid;
  394. // If there's no snap anchor, or it's too far away, prefer the grid
  395. const bool gridBetterThanNearest = !aDistToNearest || *aDistToNearest > aSnapRange;
  396. // The escape range is how far you go before the snap line is de-activated.
  397. // Make this a bit more forgiving than the snap range, as you can easily cancel
  398. // deliberately with a mouse move.
  399. // These are both a bit arbitrary, and can be adjusted as preferred
  400. const int escapeRange = 2 * aSnapRange;
  401. const EDA_ANGLE longRangeEscapeAngle( 4, DEGREES_T );
  402. const bool escapedX = pointHasEscapedSnapLineX( aCursor, *m_snapLineOrigin, escapeRange,
  403. longRangeEscapeAngle );
  404. const bool escapedY = pointHasEscapedSnapLineY( aCursor, *m_snapLineOrigin, escapeRange,
  405. longRangeEscapeAngle );
  406. /// Allows de-snapping from the line if you are closer to another snap point
  407. /// Or if you have moved far enough away from the line
  408. if( !escapedX && gridBetterThanNearest )
  409. {
  410. bestSnapPoint.x = m_snapLineOrigin->x;
  411. snapLine = true;
  412. }
  413. if( !escapedY && gridBetterThanNearest )
  414. {
  415. bestSnapPoint.y = m_snapLineOrigin->y;
  416. snapLine = true;
  417. }
  418. if( snapLine )
  419. {
  420. return bestSnapPoint;
  421. }
  422. }
  423. return std::nullopt;
  424. }
  425. SNAP_MANAGER::SNAP_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper ) :
  426. CONSTRUCTION_VIEW_HANDLER( aHelper ), m_snapLineManager( *this ),
  427. m_constructionManager( *this )
  428. {
  429. }
  430. void SNAP_MANAGER::updateView()
  431. {
  432. if( m_updateCallback )
  433. {
  434. bool showAnything = m_constructionManager.HasActiveConstruction()
  435. || m_snapLineManager.HasCompleteSnapLine();
  436. m_updateCallback( showAnything );
  437. }
  438. }
  439. std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
  440. SNAP_MANAGER::GetConstructionItems() const
  441. {
  442. std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH> batches;
  443. m_constructionManager.GetConstructionItems( batches );
  444. if( const OPT_VECTOR2I& snapLineOrigin = m_snapLineManager.GetSnapLineOrigin();
  445. snapLineOrigin.has_value() )
  446. {
  447. CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH batch;
  448. CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& snapPointItem =
  449. batch.emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
  450. CONSTRUCTION_MANAGER::SOURCE::FROM_SNAP_LINE,
  451. nullptr,
  452. {},
  453. } );
  454. // One horizontal and one vertical infinite line from the snap point
  455. snapPointItem.Constructions.push_back(
  456. LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 100000, 0 ) } );
  457. snapPointItem.Constructions.push_back(
  458. LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 0, 100000 ) } );
  459. batches.push_back( std::move( batch ) );
  460. }
  461. return batches;
  462. }