* ENH: Show Recent File Image Keep Scale Change-Id: Ib8a6cf916eaee8e353bf858bc4f2ea503705809e * FIX: wipetower position problem jira: STUDIO-4914 Change-Id: I7b05d3c53931ed8ce3d4603ff21ee6ef675611e5 * FIX: dailytips adapts screen scale jira: STUDIO-5019 STUDIO-5026 STUDIO-5027 STUDIO-5028 STUDIO-5025 Change-Id: I63d3af1870218ba8e0f048a6ef03fb29fabe27cb * FIX: generate process preset based on template Jira: XXXX Change-Id: I50adf0790dc239307d236a4cebece860ef6beb16 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: object list plate name edit Change-Id: I61d3dcd7d9598d759a3a0b44cc77d2af2adca25a Jira: STUDIO-4937 * ENH:no longer checking nozzle type jira:[for nozzle type check] Change-Id: I0e88445a264f21b0c11519e9a22a165d05611d14 * ENH: improve first layer tree support First layer support can't be top interface, and min brim width of auto mode should be larger than 0. Jira: STUDIO-5010 Change-Id: I02f8b017b535f8a47965387e8679f692b1966e04 (cherry picked from commit 3e7d54abe352e8ab5f9d6492b5a86a96f9067f94) * ENH: version: bumped to 1.8 JIRA: no jira Change-Id: I50903098b59f1dd9a6b6cf7656cec7d388f3ff17 * ENH:try again after subscription failure jira:[Try again after subscription failure] Change-Id: Ibfb1e8e26eb166d786a372632a86ef98030db034 * ENH:display msg dialog once jira:[for http error msg] Change-Id: I12e9c155fdb567cac99c35b6feeef650269ba75d * ENH:remove config.json file Change-Id: Idfcf3a63fefe968e88153c26fb691fd05cd83dc4 * ENH:add protection in threads jira:[for random crash] Change-Id: I6286012dd77abccba461f7cd72a6fc531a84c95f * FIX: add protection for get_model_task_thread thread Jira: XXXX Change-Id: I3cbc17d181a0e13c658f31eaeb6a4df878e6df41 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: delete all compatible presets when delete third printer Jira: XXXX Change-Id: I1a294402627e7ab7a8c6701f20679b3d04aff059 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ci: update build version to 01.08.00.51 Change-Id: I20a01adacbdb5fe69c106b9efd029f7308136e10 * ENH: default open support_interface_not_for_body jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I48e084deb18633f9ec47a8ec4ec643163bf66318 * ENH:modified text with too low version jira:[for low version] Change-Id: I862a0defda976a35f326a8805e002330f2ed7fdf * NEW:update printer config file version Change-Id: I9a46b29b03beb67a3da0b8f76d8b5c4b3c482928 * FIX:The plane should rotate around the world coordinate system Jira: STUDIO-5054 Change-Id: I16e484b38d79cabd9473acf1abf3c5c6b0adc4c6 * ENH:translate for limit file size and so on Jira: STUDIO-5007 Change-Id: I2c279eb690841aa51cd8128f8028266cbc17e977 * ENH:use on_render_rotate_gizmos() replace GLGizmoRotate3D::on_render() Jira: STUDIO-4227 Change-Id: If9b9ea5596e59472d5fa87ac56aeb7f6ecc65643 * FIX: some mistakes in filament profiles jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ibe7f3650f2d9cf47561dd5f2ec591a5f6c553503 * FIX: fix shard_ptr is null Change-Id: I0187cf64ffbb08a2265a11900b5c865e9ac9678f * FIX:N1 printer image in dark mode JIRA:STUDIO-4057 Change-Id: I22c001d96839daf213d5096f6ff6e3d6398fa8c4 * FIX: create printer issue Jira: 5034 5059 5053 5034 create printer but filament is repeat 5039 create successful dialog remove to center 5053 create existing printer copywriting adjustments and preset updates Delete printer secondary confirmation dialog Change-Id: Ifb3822d1e168459d2af11e02b31ecaf3719d338a Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH:just don't check the nozzle diameter jira:[for nozzle check] Change-Id: I678e7d62832eaa14b9be47d6dce70f29ebd601f6 * NEW:p1 and x1 series added motor noise calibration JIRA: 5085 Change-Id: Id73cc2d34b6130f215d81ffcdc39ba6b241445bf * ci: update build version to 01.08.00.52 Change-Id: I93d800b413f2751d132fac53fbd9b191603d0352 * FIX: ObjectSetting changed when search plate JIRA: STUDIO-5095 Signed-off-by: Kunlong Ma <kunlong.ma@bambulab.com> Change-Id: I39b1ad997d51ac4224ff5ad2b3555f56da4bd911 * FIX: invalid support params in 3rd party profiles Many params are not right.Just use default jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I5c4a1e8b046940e174f5681a79031b5f20fcafc5 * ENH: update A1 mini start gcode Change x-axis freq sweep amp 5->10 jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I2e731cc6392c0204d5e4467bf4b933ab233bc157 * FIX: [STUDIO-4946] use utf8 path to create sub process Change-Id: I5873c114e8cd36978a7d50bf13c3aa7bf8b740ca Jira: STUDIO-4946 * FIX: fix a plate state not correct issue JIRA: no-jira the object and instance states lost after undo Change-Id: I527df9a7d426d994501a4ed5bbb198c7bbac810b * FIX: some translation Jira: 5096 5089 5036 5004 Change-Id: I4f1bd6e352b11451f5caf02cbc4eeb31dfa03eee Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: [STUDIO-4935] plate name edit in object list Change-Id: I271fa217281d0c7ceca61166497136628a66681e Jira: STUDIO-4935 * FIX: take custom root as AMS sync candicate Change-Id: I9c71babcd74238d1533b15d77a978b19997c70c0 Jira: none * FIX: modify some default support params in code 1. Modify default values of some supports params, so 3rd party profiles are easier to setup. 3. Fix a bug that organic support may cause crash. Jira: none Change-Id: Icae348d8fe5985f4287404e96089198a499283f2 (cherry picked from commit 8889cfc703b72e142f288e525b89c87619f2213c) * FIX: do not generate sheath for normal support Jira: none Change-Id: I8f3f3e39171055f8d18c06ceee8e245594273238 (cherry picked from commit 93bc7ecf4346f179f502bebc3cf47b0030b56e2c) * FIX: push_notification on GUI thread Change-Id: Iaec347f5684fe0f65d6418759518189b67033c42 Jira: STUDIO-5106 * ENH: CLI: add some params to support more functions 1. uptodate_filaments to support update the original filaments to newest config 2. allow_rotations/avoid_extrusion_cali_region for auto-arrange 3. skip_modified_gcodes to support skip modified gcodes JIRA: STUDIO-5112 Change-Id: I95c09af1b5462cce3bf27aea32228d6d1d1d201d * FIX: missed manually entered values for secondary processing Jira: STUDIO-4964 Change-Id: I5cf0da1ae77cccd34de05b4a0318a751ac9f6753 * FIX: Z hop is still enabled when upper boundary is zero. Jira: STUDIO-4893 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: I5f46a02e1fbb15ff43e253e3a184aa6cc38e7598 * ENH: update default filaments for Bambu printers jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ic6380f39e546854ad0b7dc36929a8605c9ab3acc * ENH: dailytips modification 1. modify closing behavior 2. dailytips can adjust self size according to the canvas size. And also adjust GodeViewer legend window size 3. fix a button text encoding bug 4. support vertical/horizontal layout(horizontal layout currently not used) jira: new Change-Id: I8e0b6e85c455d0608d7388fb441829c1991ad01f * FIX: [4857 5097] export list and del preset two confirm issue Jira: 4857 5097 Change-Id: If7cc4967a663f575527a227e9c4ac31e0491930c * FIX: UUID conflict issue when referencing volume Jira: XXXX 3mf file standard Change-Id: I953a87294684ea85d03a95e7d2843c096904aeae Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: [4483 5003 5109] create printer and edit filament issue Jira: 4483 5003 5109 4483 dialog blink 5003 preset list too long 5109 encode Change-Id: I190e12272ca09f36b841f2f85a6cf60f2c2614bd Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: cloud use presets limit notify Change-Id: I6cc7b4e560cb83db0fc30921633b10531957128e Jira: STUDIO-5091, STUDIO-5104 * FIX: do user preset sync later on startup Change-Id: I0653a0438477b1c803ce1cddc66ef47f95616dae Jira: STUDIO-5106 * FIX: linux: pressing enter in height range will crash jira: STUDIO-4391 Change-Id: I6bf990951d1456f5b2605b8d62a05bceb3cc4c10 * FIX: failed to limit the max width of DropDown Jira: STUDIO-4503 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: Id9352d16f4bc016daade72a9c8d3d90164a1cb3d * FIX: not jump to preview after first wizard Change-Id: I8c94d66a91aa15a7874441a300b40438638bd33b Jira: STUDIO-5018 * ENH: CLI: clear custom gcodes when skip_modified_gcodes JIRA: STUDIO-5112 Change-Id: I2e7346d2ac57519029a4e80e5492c34d3d91ed77 * FIX: [4492 4851 4883 5121] create printer issue Jira: 4492 4851 4883 5121 Change-Id: If252b5f30be0403f79410aa8c00fc14b066d5bbd Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH: add 'edit preset' and 'delete preset' btn for each preset Jira: 5200 5113 Change-Id: I208ad63eb4b895306fa76db424da2e1df10a582e Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: add skip label before tool change Jira: 5074 github: 2776 Signed-off-by: qing.zhang <qing.zhang@bambulab.com> Change-Id: Icaafd3b45da1e78c1a82e7d17d7505d9439b9100 * FIX:Network test dark mode adaptation JIRA:STUDIO-2468 Change-Id: I20cb7f1fd8eca3ce852acb563c1cc87978e216dc * FIX:n1 external feed prompt pop-up without retry button JIRA: STUDIO-4696 Change-Id: I31069c72e29d3398469d71cdbc2a344a5430fc2c * FIX: not show device page when switch printer preset Change-Id: I00d8524625a4682b6a39876ddb66bf8bd928dbef Jira: none * ENH: Check the nozzle diameter when sending calibration Jira: 4977 Change-Id: Iabbba44583bbd9fbaaa889ca546ee0ccbb2aa77f * FIX: Generate UUID from objectID and volumeIndex Jira: XXXX Change-Id: I65147ef9b695f8af8de260d722e604b0e0bab563 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: disable filament_typep Jira: XXXX Change-Id: Ib605b33e4474525fbe49e70596fc09aa356f478a Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ci: update build version to 01.08.00.53 Change-Id: I1d574fa2cf2a4d0eb63a38eb8ced7587d06a4272 * ENH: refine display logic of param 1. Refine the display logic of "support_interface_not_for_body".Only toggle if support_filament is default and support_interface_filament is specified jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ia2af030c4531ad6b04a198bfe8a1677b3d20a800 * FIX: user preset sync token Change-Id: Id2aa865b778ee9ac4cfddb68ceef0374507b519b Jira: none * FIX: Bitmap cache not take effect Change-Id: I972098fdbda0b4542c6c759a8f5e1f0f2a30852b Jira: STUDIO-4991 * NEW: Open HotModel Link With GetParam-From bambustudio JIRA: NO JIRA Change-Id: I4ac49bac5ee0c50988c76a38b00b7ba7dc3201f5 * NEW:AmsMaterialsSetting Support for user-preset JIRA: STUDIO-5135 Change-Id: If848047cd5dbd059d440de30989c505c361305a7 * FIX: upload custom root preset fail Change-Id: I621c8d542dd604b07cc5df63d97d7a31558d3aba Jira: none * FIX: show custom filament in AMS filament list Change-Id: I79b9f8f2f08db8c52bbed76f1ea133baff383c00 Jira: none * FIX: dailytips window and gcodeviwer legend window size issue reset to original logic of dailytips and legend window size jira: new Change-Id: Iacb016bb222ba3f87317cfbe1f2b003802d773a5 * ENH: add text translation jira: new Change-Id: I780cfb8a0a64d806b5e0a414b6598e3b7bdf52dc * FIX: Delete and search object outside the plate JIRA: 1. STUDIO-5132 Deleting object outside the plate will crash 2. STUDIO-5146 The search function cannot search for object outside the plate Signed-off-by: Kunlong Ma <kunlong.ma@bambulab.com> Change-Id: I84cb3fe990a9c2a182e7434c262466a70545280e * FIX: [5149 5142 5141 5140 5136] create printer and filament issue Jira: 5149 5142 5141 5140 5136 5149 process preset name can not show all 5142 improt configs combobox not update 5141 disable modify filament_vendor 5140 disable input Bambu and Generic vendor 5136 preset list window adjust Change-Id: I111a23996146cc16cc7f533c8616d50223d34c40 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ci: update build version to 01.08.00.54 Change-Id: Ifd69c01a82f627a9c6cf4fe0d48a759563ee90e7 * FIX: print model from sdcard with p1p Change-Id: If85383ba762022ead3dd754ae02a08817b891114 Jira: none * FIX: dailytips text translation jira: STUDIO-2556 Change-Id: If44e503615b09ee1692f42ba1f998918ec5bd691 * FIX: clone shortcut key conflict with quit in macos jira: STUDIO-5166 Change-Id: I548f275bb68d3b0e6bb3cfad6fe93df09d507da3 * FIX:User preset material settings dependent on firmware JIRA: 5167 Change-Id: I82cf26848594b01155883ad0aa2e9ee77d371fb2 * ENH:update the description of nozzle detection Change-Id: Id27b25c69dc11fcf66fc82053af705906ae8c370 * FIX: [5159 5165 5171 5172] create printer and filament issue Jira: 5159 5165 5171 5172 5159 create printer dialog no refresh 5165 create printer 2 step dialog no refersh 5171 change font 5172 edit filament dialog darkUI issue input special character is prohibited '/' in preset name translate to '-' update printer combobox Change-Id: I5fa27836dab7f604f1a065c65efa099c7a2f0f96 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ci: update build version to 01.08.00.55 Change-Id: If1865d561cf274719204662314de163497759e89 * FIX:fix GLmodel deconstruction causing section not to be rendered Jira: STUDIO-5156 Change-Id: Ibb2f459920989ee54f7b827352dc8893424b4de6 * FIX: missing unlock cause device or resource busy Change-Id: I87563312ea9c6ce4e4e471da7ce7a02b53b64762 * FIX: some translation Change-Id: I9758cbc758030b5a3945697a50ca4898af9fcb1b * ci: update build version to 01.08.00.56 Change-Id: Id5ee53dd2ebb0b37b6927dc58b3cca94a1f66a83 * ENH: remove PLA GLOW in A1 mini jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Id99c1bbd4248e28df9150a85eecec831f6f32856 * ci: update build version to 01.08.00.57 Change-Id: Ib4dfa60f097128b76b95bb14ca04978619021b56 * Allow line width of nozzle diameter * 2.5 As it were, 1 mm would be disallowed but 0.99 would be allowed for 0.4 nozzle. 1 mm is the sane maximum and 0.99 is unnecessary tedious to write. * Russian translation update Russian translation Bambu Studio_v1.8.0 Beta * FIX: scale problem in needs_retraction jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Idfbe954b22fa6aa5769c55e46874fa6a80ecbf45 (cherry picked from commit 4e853f50b748e3af11e2d64862b6ee557fda361b) * ENH: CLI: support load_assemble_list JIRA: STUDIO-4848 Change-Id: Ife11533740988331ea71eac86c370e625970cb8b * FIX: align to Y not working This is a bug introduced in 7fbb650 when solving jira STUDIO-4695. Now we use a more decent way to solve it. Change-Id: I92deffcb9fe53e8a24c93fe973446ae37df07375 (cherry picked from commit bd98430dbd15eb6c9bb4b447990e0dcf8a50eef0) * ENH: Add buried points for cut and meshboolean JIRA: NONE Signed-off-by: Kunlong Ma <kunlong.ma@bambulab.com> Change-Id: I67ce498d0c335dd7a8582f56b880c2c8314f8541 * FIX: 5092 edit filament add scrolled window Jira: 5092 Change-Id: I53ae996b04e4e2f1b1ddce6a858d505001b11615 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: can not select user preset when create filament Jira: XXXX github: 1936 and fix add preset for printer dialog can not show selected printer Change-Id: Id4308c6bdca17d52d4aa321db359941aa87e0e45 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH: template filament don't be show in filament list and sort Jira: 5160 5179 Change-Id: I56a7e1897e1ef3c061dc66d318896413ca25b76b Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: [5174] export configs dialog issue filament name too long to can not show all Jira: 5174 Change-Id: I92018c9d7f86009b78b533592d899b4b5d78c3c8 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH: add filament Bambu TPU 95A HF 1.As title jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I752ec43da6297a6c172679997ce68f2318a7b8fb * ENH: modify some params with filaments 1.Modify the PEI bed temperature of PLA Basic, Matte, and Tough to 65 in A1 mini. Set the bed temperature for the first layer of Bambu PETG-CF to 65 and 70 for the other layers jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ia902bbb7f824082d5346709d781cac64296f47a8 * ENH: add more status during printing JIRA: STUDIO-5195 Change-Id: I85b3107839c6e2fdecbc10d90a876463e284468c Signed-off-by: Stone Li <stone.li@bambulab.com> * FIX:cut imgui has overlapping rendering on Apple Jira: STUDIO-5150 Change-Id: I7969e19dc189cd617026a183067dad628208955c * FIX:not TakeSnapshot for m_start_dragging_m Jira: STUDIO-5176 Change-Id: Ia03e3e2c2664dbdcffa19ec8d0fa97dfd95e6d35 * FIX: rendered color changes Jira: STUDIO-4956 during the drag processin connectors editing state Change-Id: I3027176ea9f93a9ba9d6a2052f41aaa4adef79f1 * FIX: merge the patch from Prusa Thanks for PrusaSlicer and YuSanka Jira:STUDIO-5175 commit 510d59687b3b19c4a0f4e6540620d0694dd1b7ac Author: YuSanka <yusanka@gmail.com> Date: Thu Oct 5 14:13:14 2023 +0200 Follow-up 1b451cdf: Fixed #11415 - Connectors disappear when slicing => only when using multiple cut planes AND excluding parts Change-Id: I9ccd5b85f482d723d21fccf5e104c9e0a9cc4849 * FIX:Press ESC directly to exit after entering the profile rendering rendering is not normal,Code from PrusaSlicer,thanks for PrusaSlicer and enricoturri1966 commit a078627552f54497ed0518dc7bc349d243576d19 Author: enricoturri1966 <enricoturri@seznam.cz> Date: Mon Jan 30 14:00:02 2023 +0100 Follow-up of 1218103fd620b319c56fd08116f81b581c537188 - Fixed gizmo missbehavior when closing a gizmo by resetting the selection clicking on the scene Jira: STUDIO-5164 Change-Id: I261da9dba2a5ac37f3e263c175fbccd80d8045bd * FIX: correct the strings and move create printer dialog center Jira: 5221 5183 Change-Id: Ida4eba63f0e962ffcc8000fcc04cf20849577217 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH: CLI: skip layer height limit validate when slicing for existing models JIRA: no jira Change-Id: I1444a28b500ca7d08ed2606eecfa5cfaf261105e * ENH:update the translation of auto refill jira:[for translation] Change-Id: Iaa7b4f3d7cd88c8b4f69a3db721ebd8ca8986eea * FIX: icon issue for copying Jira: STUDIO-4168 Icon issue when filling bed with copies Change-Id: I61a03ecae02b75602c236ed2810e9c9cfe5a19f9 (cherry picked from commit b5079f8a2e79f19f65803f23ef4fd83aff17c84a) * ENH: update some filament params 1. Modify texture bed temp to 65 2. Modify max-v-speed for ABS 3. Modify some params in Generic PA 4. Modify PLA,PVA params jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I42584a6015b8526f6bbb93024316968198bd76ce * FIX: 3770 printable checkbox incorrect display in darkUI Jira: 3770 Change-Id: I97f67d7a0ffc41f6ee625abeecc52ee4e73cf318 * FIX:Display garbled code in AmsMaterialsSetting pop-up Change-Id: I50531e939afa7715ae376bac47172ccf7b248114 * ENH:Modifying the Line Color of Transparent Materials JIRA: STUDIO-4311,5088,4983 Change-Id: I9e63413dc9cd7d523f0f7f1a2e32c4537a84467a * FIX: crash when async delete printer file Change-Id: I92c5e812d04da263338fb0eea2fd7583cf9ecde0 Jira: STUDIO-5222 * FIX: 3733 backup time not effective Jira: 3733 Change-Id: I50c2ce156fcbd0a17aa8a6777bce04aa6093c830 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: enable edit and delete preset btn and fix issue Jira: XXXX Change-Id: I724d7236b28fcc4746698f094531948a8fbb5d93 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX:send print job,file name displays error JIRA:3137 Change-Id: I1c113025d274a13fba1b845a58aada14058fadd4 * FIX: skip hold user preset from sync Change-Id: I2252246e19bd80903ad82170782ea49535d30d05 Jira: STUDIO-5185 * FIX: 5115 translations Jira: 5115 Change-Id: I21b03bdd4d28c0bb097226143177e763cf8c777f Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: add link for ironing parameter Change-Id: I451f5549db3ac2205aa5703a2e5edc831e946af8 * FIX: scale problem in lift type decide 1. Scale the travel threshhold jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ib594d640fe63b0919bc9318af88577513f7dbf30 * ENH: add small perimeter speed and threshold The original param is added by Prusa. Thanks orca for adding threshold. 1. Re add small perimeter speed and threhold. github: #2221 Change-Id: I35b269b26f085d80f0edca28650bb21fc04898d7 * FIX: modify the picture of pa manual cali Jira: STUDIO-5102 Change-Id: Id87898959ad4461b7bd2505b159271f2aa589c36 * FIX: Filament preset is the same with the first one Jira: STUDIO-4519 Filament preset is the same wit the first one, it should align with the last one. Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: I304d0ff0fbc1c8948d410ea552e4d42b6a4e8fd9 * FIX: scoreDailog dark mode issue Jira: 4570 Change-Id: I8be97b306a1494f73e3bba678ecc864e7ff88ca3 * FIX: CLI: fix the slicing issue while only one object with multicolor using seq-print JIRA: no-jira Change-Id: Iea2d23ff8e484bf2fd58aa2f596a8e4e4292fe39 * ENH: open support wall count for normal support 1. open support wall count for normal support Enabling this option makes normal support stronger and gives better overhang quality, but also more difficult to removal. Jira: STUDIO-5192 2. fix a bug where tree support (hybrid style) may get overlapped extrusions near the walls. 3. fix a bug where raft layers can't be 1 in tree support Jira: STUDIO-5261 Change-Id: Iadc0c67a9b50b5b221c8e83d5aa22ed282018cf8 (cherry picked from commit c0bb0084e386cb70ed6e16edf93190e4b38f5b90) * FIX: compiling error on linux jira: none Change-Id: I1a4563503b5ddf74a1979cc0cee7a15b8aced904 (cherry picked from commit de52c6ca62c9f3a6314ddf5a856c1d8534329886) * ENH: add translation for small perimeter jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I95013649e4e0c07c0f04b89a91488814c8d228cc * FIX: clone shortcut key issue on macos jira: STUDIO-5166 Change-Id: I1967da1d443ed43bd750dad8e11560688d7bd533 * FIX: custom gcode window cannot paste/ navigate jira: STUDIO-5208、STUDIO-5070 Change-Id: I4ecb9d06cf5db0ae53a4678181aae9298bac106b * ENH: modify dailytips collapse & expand interaction jira: STUDIO-5209、STUDIO-5210 Change-Id: Ifb0b998e5004d4b49390ba5a250eaf4743bf3471 * ENH:Add shortcut keys and lists for objects search JIRA: STUDIO-5157 STUDIO-5158 STUDIO-5240 Signed-off-by: Kunlong Ma <kunlong.ma@bambulab.com> Change-Id: Ic7cfaaa9b4bb8a85208bafab7fe3bafdb78f0045 * FIX:Re-calculate button with White Box displayed in dark mode JIRA: STUDIO-5098 Signed-off-by: Kunlong Ma <kunlong.ma@bambulab.com> Change-Id: I07cc6c72d5dbd03b72573cd27dd5938bb0e6a29a * NEW: display plate index when printing a task JIRA: STUDIO-2689 display on the thumbnail of the current task Change-Id: I5f1f46c56e9d1e9120a66d491551908dfad099d6 Signed-off-by: Stone Li <stone.li@bambulab.com> * ENH:fixed incorrect path prefix jira:[for file path prefix] Change-Id: Ie9e3999f02b78272e528ceceb479e746d46a7e6c * FIX: thumbnail is not clear in dark mode JIRA: STUDIO-5087 Change-Id: Ie86493ed71b5554095927f061509a1f551758b61 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> * FIX: translation Jira: XXXX Change-Id: Id03f0d704aa852632a907ea628d1277400112062 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH: first nozzle change to 0.4 and nozzle change to mm Jira: XXXX Change-Id: I14defd36442dbd7008e46782b08415b6244224f1 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * ENH:editing_window_width's value is small on the laptop Jira: STUDIO-5238 STUDIO-5265 apply_selected_connectors should check_and_update_connectors_state Change-Id: I8c2c1c920cc4d197d1908815a3e62f4962335451 * FIX: fix new_bed_shape's calculation process Jira: STUDIO-5122 Change-Id: I5f3e6a301a297123af28692c90bef6759f425b06 * ENH:update some translations jira:[STUDIO-5262] Change-Id: Idb1d3586888043ac325f272bc7a2b788adb3e9e5 * FIX: edit text command resets object settings Jira: STUDIO-4655 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: Iac25c4e40f1d0d32e6d1f40e62226cc22bc96042 * ci: update build version to 01.08.00.58 Change-Id: Iacfec02aa50c617e4c9fe566319b07b30d47dce1 * FIX: remove GetUserAgent Change-Id: I92886e1f0dcb091109231a10da8c19d51178e13b Jira: STUDIO-5205 * FIX: nozzle_diameter_map data structure adjustment Change-Id: Ifb724afc0fcf974e6d331e73ecac723107a102cf * ENH:add _A and _B for perform_with_groove Jira: STUDIO-5267 Change-Id: Iee3310dfa1cd8e6680310f0af0eff5c817490813 * ENH:is_equal for min_z and max_z Jira: STUDIO-5267 Change-Id: I9493883d8be9d44e26ddc9afe62b7e9eb09c5052 * ci: update build version to 01.08.00.59 Change-Id: Ie8ed29ccf5d6c94594eb2ab8b717416fbeace3bd * FIX:Image display unclear in light mode JIRA:5161 Change-Id: I134cc64a2af0dfff60c47d0ff09d78d9c0f86b3f * FIX:fix bugs of non manifold edge Jira: STUDIO-5267 Change-Id: I8ac9a2cf96da0bc07ee00b309e65611b92fd174d * ENH:nozzle type detection jira:[STUDIO-5246] Change-Id: Ic41a2161a0e41d23f56af93ad8ec34cc83ada0e3 * ENH: upadte P1S start gcode 1.turn on MC board fan by default on P1S jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I5b2f7868e350942fb8b7baf7d429e22a0987184a (cherry picked from commit e866a575b6b7d9552f7412f84272f4b48dfc3961) * ENH: improve support style's tooltip jira: none Change-Id: I8ee858d7052f04ce7ea6b226a500c7d1bf8a482f (cherry picked from commit 665f31c4fcde22bd894cbb4a5fb160635947f2a4) * ENH: set layer range error to warning 1. If layer range exceeds maximum/minimum layer range in printer settings,pop up a window to warn jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I0304ee790e557ecf967f355c171993d1f51b5057 * ENH: CLI: remove the warning of layer height JIRA: no jira Change-Id: Idaceee4f52611479fc3f4238d016d891b4dc8cd1 * FIX: the word search is not translated Jira: STUDIO-5224 The world search in the device panel is not translated. Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: Ia3d651c2159a3aad94e10cd0a6da98848f53ee2a (cherry picked from commit 4a46a0a4750d82d49c9523f4b668a1a00c41ed83) * FIX: Bitmap will flash when sending printing task Jira: STUDIO-5278 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: Ib0c8710b8d5d6b98fad043c950f054aa35bea965 * ENH:display the euler angle of rotation plane Jira: STUDIO-5268 Change-Id: I6b7b431931d60f1a9a832908400417781798c472 * ci: update build version to 01.08.00.60 Change-Id: I1c15b5c6437554c43327cd6b537f7a5860dba5a0 * ENH:cancel EnterReturnsTrue for imgui in cut Jira: STUDIO-5269 Change-Id: I2832e1dccaf9755448debe7b2bd56426f90dfe0d * ci: update build version to 01.08.00.61 Change-Id: Ib03e664a20990322c788686550c491d0139d8237 * FIX: some translation problems jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: If9f2af53b4f0bfa9469e84bcba68cc182df4a473 * add: Ukrainian lang support for 1.8 * fix linux * fix some string and colors * fix linux build error 2 * fix .gitignore * FIX: calibration selected preset is null in some case jira: STUDIO-5258 Change-Id: Iee63593c5f833c5a43e3b1d1c9ddb82f8c69569a * FIX: create filament issue Jira: 5296 5297 5295 5302 5311 5276 5296 create filament: list has same printer 5297 create filament: filament combobox has blank options 5298 edit filament: delete last preset prompt users 5302 create filament: filament combox has cili preset 5311 create filament: printer name too long to can not show all 5276 edit filament: PLA Aero filament type filter issue add prusa vendor Revised copy Change-Id: I5dcc615ce0951b1a9953fa12283f6090f5069045 * FIX: some translation Change-Id: Icb8614a0af18f96d15f3b97c17e0f6f708296847 * FIX:cancel is_equal for slicing function Jira: STUDIO-5267 Change-Id: I66d759aa2c968f8a28a6a5d8378929754f2db689 * FIX:UI stuck due to pop-up window with wrong chamber temperature JIRA: 5304 Change-Id: I1a49a7219b7a6f5700243704c348724e7930ce1a * FIX: allow input '+' and hide edit preset btn Change-Id: I97aec7c3ac4cc8b9d6c535f0126aaa1926553d86 * ENH: handle printer direct close and not retry Change-Id: I5dd55f8085cf6383a8420ab41e614ea6ae210c78 Jira: STUDIO-5305 * ci: update build version to 01.08.00.62 Change-Id: I09716bf79354b503197c751272fd3171e0abc8fd * add: new translation to ukr for AirFlow and Prusa * add: Texture Plate name fix * add new feature to localization .de, fix .it (#2876) * FIX:add slice_facet_for_cut_mesh api for cut tool and modify section_vertices_map 's traverse Jira: STUDIO-5267 Change-Id: Ifc4b183a4e4c4fdb4f47742f14f70a1ed93fa056 Change-Id: I52bfaef8926ef967b78a6cb712a1731a1b528a24 * FIX: Make the front smaller for Czech in device panel Jira: STUDIO-5151 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: I315174b55f923da069854fb4fed8cf3937b82074 * FIX: there is no object can be jumped to in notification jira: new Change-Id: Ib81bf49236952ede24a2de126051572d63916e01 * FIX: add height range, modifier in Preview pane will crash jira: STUDIO-5340 1. fix crash at add height range, modifiers in Preview from objectList 2. fix an assert hit when slicing 3. fix an assert hit when enter AssembleView 4. forbidden popup menu by right-click objectList in Preview Change-Id: I444bc76b1a4307999b387e4f60386b2d272bd308 * FIX: Black spot in the sending printing task page Jira: STUDIO-5307 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: I3bd97c063ec5f0faf7d12047da39f60ce55cae4b * FIX: reset_cut_by_contours should update_buffer_data Jira: STUDIO-5376 Change-Id: I5aacb1f7b65822031d7138abd61a45b09c743531 * ENH:editing_window_width's value is small on the laptop Jira: STUDIO-5238 STUDIO-5265 Change-Id: Ia958772bcb081817da621115f99328bb62770cd5 * ENH: bumped version to 1.8.1 Change-Id: I9d25403daa5b7b8ca415c0b364670da9e0f932b0 * FIX: create filament dialog: create btn can not show all Jira: 5310 5331 Change-Id: I185272c90d9ff1c3d6b47abbefbf488d0d965cca * FIX:update custom_texture when new_shape=false Jira: STUDIO-5287 Change-Id: I3add95f9f9345c14a48cc7467513d1b3ce95f4c9 * ENH:editing_window_width's value is small on the laptop Jira: STUDIO-5238 Change-Id: I9044129f4e0c8ca7469db05b3e547fca4754342a * FIX:add slash_to_back_slash for saving file path Jira: STUDIO-5287 Change-Id: I9f3c176cd0831c793958f08601c63efac98176a4 * FIX: a button color didn't response to dark mode change jira: STUDIO-5315 Change-Id: I95489f01ccd1f77b9e95b0d0f69e5398d2e88487 * FIX: height range layers displayed in wrong position jira: STUDIO-5341 Change-Id: I83918b4624f367efa54321f1898e1176cdb04ea9 * FIX: auto arranging issues with locked plates 1. global auto arranging may put items overlap with wipe tower if some plates are locked jira: STUDIO-5329 2. items outside bed may overlap with plate boundary if it's rotated jira: STUDIO-5329 3. plate-wise auto arranging uses wrong min_obj_distance if the plate is by-layer printing but global setting is by-object printing jira: STUDIO-5330 Change-Id: I5dba2f1317e183c9aeec1cb2bd227fbddf4316e6 (cherry picked from commit db1eac41efff5f1e8d5ac0af74c6fc7ab59fc253) * FIX: a mistake in upward machine jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ibdb26c3d904634f322aef0dc0c7b8867d9fb5854 * FIX:a blank pop-up appears JIRA:XXXX Change-Id: Ice92b55204e4897fec024a6d99412eb810bddd4a * FIX:fixed failure in updating nozzle type jira:[STUDIO-5248] Change-Id: Iad37b5d5b76d37cb1723ef21d7c39b1e3fcaf8d7 * FIX:fixed issue with AI monitoring settings jira:[STUDIO-5082] Change-Id: I967fe3c1e9da61a55bcbfaa2a8e067dd5af18f72 * FIX:fixed issue with lan mode jira:[STUDIO-5189] Change-Id: I1d0a05f19dcea154cf3ef2b61ed0546d3581905e * FIX:update text for loading or unloading filaments jira:[STUDIO-5231] Change-Id: Ic7729c3ec012485b3d87e3d01f11e87502c67895 * FIX: Revert "ENH: do not leave a gap for top... Revert "ENH: do not leave a gap for top interface if the top z distance is 0" This reverts commit 79ea32c7cbbdb7e689637980af7c36caf42284c9. Revert reason: the supports are impossible to remove in some cases. jira: STUDIO-5385 Change-Id: I376a6f4dfd78da6dfea68b9ac3d552cddd0b4272 (cherry picked from commit 34e38b705fde7f5d7f9a3a89c96a3627ce0c998e) * ENH: improve normal support's quality 1. Add a base_interface_layer when using Supp.W 2. Fix a bug where base_angle and interface_angle are wong jira: STUDIO-5386 Change-Id: I52ab32c63b3cd1e6e2ba6463b01ae26699cf13d3 (cherry picked from commit 92ddd4a10b793572a1fa009da5b9e44fcdf81de2) * NEW:tracking stl model files jira:[STUDIO-5372] Change-Id: Idb1275b07441f0cd06c24588d5f7c20f81f1556c * FIX: edit filament dialog: preset name too long to del btn nan't show Jira: 5336 5174 and verify string normalization Change-Id: I380c3bed2bf43d01094b68979a8b67f4187c0b93 * FIX: some translation Jira: 5232 5300 5334 Change-Id: Ie474ca823011e81aab82a9809af3d6e42980496b * FIX: some translation Change-Id: Iaabe6087bed3b7d47d911cf4fb51c770804e72fb * ENH: change default tree_support_wall_count to 0 Now normal support also uses this option, so we can't default it to 1, otherwise normal supports will be too hard to remove. jira: none Change-Id: Ic5700af5c17e3a7b265c8915f28b0db35c6e06e6 (cherry picked from commit 6b84a9826da108b76569e686bd9def0b23ae29fd) * FIX:The name of the material in the error prompt is empty JIRA:STUDIO-4907 Change-Id: I3cf44f099256a51f21a44a89c89c000f734d1f36 * ci: update build version to 01.08.01.51 Change-Id: Ib20f5a12b65472102befec0a2adf82744fc29c46 * FIX: imgui textinput cannot paste on macos jira: STUDIO-5070、STUDIO-5365 Change-Id: Iea8f41e12744ecda0fbb95c1a8f2e014a7cdc384 * FIX: not cache printer file list on error Change-Id: I99843aedbf14d3d1d553ccac9b0bd26403274a82 Jira: none * FIX: thread of close BBLUserPresetExceedLimit notify Change-Id: I9698134ba1cc91fc83eac441f900d68c4c4b556a * ENH: Resolve non manifold edges by fixing model interfaces Jira: STUDIO-5124 Change-Id: I7ea86be44acb80b6c4762a76208b4a031acd0b27 * FIX:nozzle type sync jira:[STUDIO-5248] Change-Id: I63d48628832473d8d371ed643dc8528b00382531 * FIX: array bound happen in TriangleSelector::deserialize Jira: STUDIO-5170 Change-Id: I92b72a887845b462fad208f13607293b44d3d333 * FIX:cropping rendering without considering assembly views Jira: STUDIO-5260 Change-Id: Ia56cf80b305ae05f25f06ec277f85b3c5430a6df * FIX: PA for custom filament not available in BL Studio github: 2971 Change-Id: I6ccd36a183e7367d69557300f7242f5403f4bb33 * FIX: Bitmap is way too small on Mac Jira: STUDIO-5393 Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Change-Id: I6b550669fa8cd5fc9bfa6ed96d64d19a949f01b2 * FIX: move shutdown wait to OnExit Change-Id: I70d9a2bb686525ae5273aa9d63e25691da4ab65c Jira: STUDIO-2884 * FIX: calibration manage result dialog issue on macos jira: STUDIO-4949 STUDIO-5378 Change-Id: I00abefd45a0d274a4b68bb1ab18debe8f91d169e * FIX: adjust bed shape dialog button UI style fix that button text is hard to see in dark mode jira: STUDIO-5247 Change-Id: I2cf5b3cdd2eff9b821bdf5525bec4f329fc58dd1 * FIX: 5331 rescale btn Jira: STUDIO-5331 Change-Id: If153424b8480e64b166018e3cd98c17db557d0a8 Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> * FIX: support do not generate jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Ide9709d95203185538e280517f7aa6136beeda44 * FIX: remove not match printer config ota cache Change-Id: Ib73fc2ea31fa2186061cfcb5a170bc59b9db84ca Jira: none * FIX:cancel the variable of "checkbox_size" as a fixed value Jira: STUDIO-5150 Change-Id: I30d876d141b8b35ab4a3fee4889993d87b7c1741 * ENH:add reset_cut_by_contours in on_load function Jira:STUDIO-5269 m_connector_size_tolerance default value is 0.1f Change-Id: I6c67fff3cb0c1190e9141ed6f68fbfa848679f35 * ENH:cancel EnterReturnsTrue for imgui in cut Jira: STUDIO-5269 Change-Id: Ifc4b183a4e4c4fdb4f47742f14f70a1ed93fa056 Signed-off-by: zhou.xu <zhou.xu@bambulab.com> * FIX: dailytips should not change content frequently when slicing all jira: STUDIO-5234 Change-Id: Icb7e9c28404d9db8ebed58d937e13f89c5403b5c * FIX: objectList clone shortcut key issue jira: new Change-Id: Ia75bf58a7d53f962e1af6c2fd97497270b7eea84 * ENH:handling cases without msgs jira:[STUDIO-5401 STUDIO-5399] Change-Id: Iae651d5a19a45b0138a6aa621326a8b4a9649824 * ENH: optimize param description jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: Id0ca9224227a716b21fc0b8430722264dc319344 * ENH: add translation jira:[NEW] Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Change-Id: I3b1f04fee3cd6322793794ad8b8707859f6c7d26 * FIX: close edit preset paramsDialog, mac unresponsive Jira: 5298 Change-Id: I021e00567354cfb1f2f5f1f2bf6ba1fc35b164c5 * ENH:disable AI monitoring on the p1p series Change-Id: I514bb1fb1ced6c03dd619230a9adac3be63f2de2 * ci: update build version to 01.08.01.52 Change-Id: I9f5e30d3fc4b7ef9321c522d3c18fce98f03742f * FIX: close edit preset paramsDialog, mac unresponsive Change-Id: Ic816754a20b7f6a5cdb46475750eb301fec3ad3a * FIX: organic support not work with raft only There is no raft generated when only raft enabled but no support needed. jira: none Change-Id: Ic0c9269e2f98038d85c9bc54e4a85f892dc5d764 * FIX: CLI: add object config when assemble JIRA: no jira Change-Id: I945f820fb58f2f643170b4b0b66742f6bbbdfd29 * FIX: delete preset prompt Jira: XXXX Change-Id: I6511c806c56393d4f6bd72d1c506da59675d49ff * FIX:Reorganize the assignment of variables of "m_editing_window_width" Jira: STUDIO-5238 Change-Id: If369916f3f5c21510f5f297bfd05c1230bdda7a4 * ENH: CLI: re-compute flush_volumes_matrix when it is missed Change-Id: Ie8f53c6bef003b1434de02ea14de5787b376484f * FIX: some translation for delete filament Change-Id: Ib46a8eba33f2e21016476aaab4a57a740e86b1b8 * FIX: scrolled window / del preset / edit filament issue Jira: 5092 GitHub: 1936 edit filament: just one preset, the scroll bar obscures the preset name edit filament: delete selected preset, click no, but preset be deleted from UI edit filament: serial sometimes displays incorrectly Change-Id: Ibc91609e252179de0c05ca065099756da6631165 * ci: update build version to 01.08.01.53 Change-Id: I5563a2c0812ab9a0d7727df27e17e681066ffa08 --------- Signed-off-by: maosheng.wei <maosheng.wei@bambulab.com> Signed-off-by: xun.zhang <xun.zhang@bambulab.com> Signed-off-by: Kunlong Ma <kunlong.ma@bambulab.com> Signed-off-by: wenjie.guo <wenjie.guo@bambulab.com> Signed-off-by: qing.zhang <qing.zhang@bambulab.com> Signed-off-by: Stone Li <stone.li@bambulab.com> Signed-off-by: zhou.xu <zhou.xu@bambulab.com> Co-authored-by: zorro.zhang <zorro.zhang@bambulab.com> Co-authored-by: liz.li <liz.li@bambulab.com> Co-authored-by: maosheng.wei <maosheng.wei@bambulab.com> Co-authored-by: chunmao.guo <chunmao.guo@bambulab.com> Co-authored-by: tao wang <tao.wang@bambulab.com> Co-authored-by: Arthur <arthur.tang@bambulab.com> Co-authored-by: lane.wei <lane.wei@bambulab.com> Co-authored-by: gerrit <gerrit@bambulab.com> Co-authored-by: xun.zhang <xun.zhang@bambulab.com> Co-authored-by: zhou.xu <zhou.xu@bambulab.com> Co-authored-by: hu.wang <hu.wang@bambulab.com> Co-authored-by: Kunlong Ma <kunlong.ma@bambulab.com> Co-authored-by: wenjie.guo <wenjie.guo@bambulab.com> Co-authored-by: qing.zhang <qing.zhang@bambulab.com> Co-authored-by: zhimin.zeng <zhimin.zeng@bambulab.com> Co-authored-by: the Raz <rasmus@abc.se> Co-authored-by: Andy <andylg@yandex.ru> Co-authored-by: Stone Li <stone.li@bambulab.com> Co-authored-by: enricoturri1966 <enricoturri@seznam.cz> Co-authored-by: Dmytro Chystiakov <dlchistyakov@gmail.com> Co-authored-by: Heiko Liebscher <hliebscher@idn.de>
4799 lines
277 KiB
C++
4799 lines
277 KiB
C++
// Tree supports by Thomas Rahm, losely based on Tree Supports by CuraEngine.
|
||
// Original source of Thomas Rahm's tree supports:
|
||
// https://github.com/ThomasRahm/CuraEngine
|
||
//
|
||
// Original CuraEngine copyright:
|
||
// Copyright (c) 2021 Ultimaker B.V.
|
||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||
|
||
#include "TreeSupport3D.hpp"
|
||
#include "AABBTreeIndirect.hpp"
|
||
#include "AABBTreeLines.hpp"
|
||
#include "BuildVolume.hpp"
|
||
#include "ClipperUtils.hpp"
|
||
#include "EdgeGrid.hpp"
|
||
#include "Fill/Fill.hpp"
|
||
#include "Layer.hpp"
|
||
#include "Print.hpp"
|
||
#include "MultiPoint.hpp"
|
||
#include "Polygon.hpp"
|
||
#include "Polyline.hpp"
|
||
#include "MutablePolygon.hpp"
|
||
#include "SupportMaterial.hpp"
|
||
#include "TriangleMeshSlicer.hpp"
|
||
#include "TreeSupport.hpp"
|
||
#include "I18N.hpp"
|
||
|
||
#include <cassert>
|
||
#include <chrono>
|
||
#include <fstream>
|
||
#include <optional>
|
||
#include <stdio.h>
|
||
#include <string>
|
||
#include <string_view>
|
||
|
||
#include <boost/log/trivial.hpp>
|
||
|
||
#define TBB_PREVIEW_GLOBAL_CONTROL 1
|
||
#include <tbb/global_control.h>
|
||
#include <tbb/parallel_for.h>
|
||
#include <tbb/spin_mutex.h>
|
||
|
||
#if defined(TREE_SUPPORT_SHOW_ERRORS) && defined(_WIN32)
|
||
#define TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
#endif
|
||
|
||
#define TREE_SUPPORT_ORGANIC_NUDGE_NEW 1
|
||
|
||
#ifndef TREE_SUPPORT_ORGANIC_NUDGE_NEW
|
||
// Old version using OpenVDB, works but it is extremely slow for complex meshes.
|
||
#include "OpenVDBUtilsLegacy.hpp"
|
||
#include <openvdb/tools/VolumeToSpheres.h>
|
||
#endif // TREE_SUPPORT_ORGANIC_NUDGE_NEW
|
||
|
||
#ifndef _L
|
||
#define _L(s) Slic3r::I18N::translate(s)
|
||
#endif
|
||
|
||
//#define TREESUPPORT_DEBUG_SVG
|
||
|
||
namespace Slic3r
|
||
{
|
||
|
||
namespace TreeSupport3D
|
||
{
|
||
|
||
enum class LineStatus
|
||
{
|
||
INVALID,
|
||
TO_MODEL,
|
||
TO_MODEL_GRACIOUS,
|
||
TO_MODEL_GRACIOUS_SAFE,
|
||
TO_BP,
|
||
TO_BP_SAFE
|
||
};
|
||
|
||
using LineInformation = std::vector<std::pair<Point, LineStatus>>;
|
||
using LineInformations = std::vector<LineInformation>;
|
||
using namespace std::literals;
|
||
|
||
static inline void validate_range(const Point &pt)
|
||
{
|
||
static constexpr const int32_t hi = 65536 * 16384;
|
||
if (pt.x() > hi || pt.y() > hi || -pt.x() > hi || -pt.y() > hi)
|
||
throw ClipperLib::clipperException("Coordinate outside allowed range");
|
||
}
|
||
|
||
static inline void validate_range(const Points &points)
|
||
{
|
||
for (const Point &p : points)
|
||
validate_range(p);
|
||
}
|
||
|
||
static inline void validate_range(const MultiPoint &mp)
|
||
{
|
||
validate_range(mp.points);
|
||
}
|
||
|
||
static inline void validate_range(const Polygons &polygons)
|
||
{
|
||
for (const Polygon &p : polygons)
|
||
validate_range(p);
|
||
}
|
||
|
||
static inline void validate_range(const Polylines &polylines)
|
||
{
|
||
for (const Polyline &p : polylines)
|
||
validate_range(p);
|
||
}
|
||
|
||
static inline void validate_range(const LineInformation &lines)
|
||
{
|
||
for (const auto& p : lines)
|
||
validate_range(p.first);
|
||
}
|
||
|
||
static inline void validate_range(const LineInformations &lines)
|
||
{
|
||
for (const LineInformation &l : lines)
|
||
validate_range(l);
|
||
}
|
||
|
||
static inline void check_self_intersections(const Polygons &polygons, const std::string_view message)
|
||
{
|
||
#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
if (!intersecting_edges(polygons).empty())
|
||
::MessageBoxA(nullptr, (std::string("TreeSupport infill self intersections: ") + std::string(message)).c_str(), "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING);
|
||
#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
}
|
||
static inline void check_self_intersections(const ExPolygon &expoly, const std::string_view message)
|
||
{
|
||
#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
check_self_intersections(to_polygons(expoly), message);
|
||
#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
}
|
||
|
||
static constexpr const auto tiny_area_threshold = sqr(scaled<double>(0.001));
|
||
|
||
static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_meshes(const Print &print, const std::vector<size_t> &print_object_ids)
|
||
{
|
||
std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> grouped_meshes;
|
||
|
||
//FIXME this is ugly, it does not belong here.
|
||
for (size_t object_id : print_object_ids) {
|
||
const PrintObject &print_object = *print.get_object(object_id);
|
||
const PrintObjectConfig &object_config = print_object.config();
|
||
if (object_config.support_top_z_distance < EPSILON)
|
||
// || min_feature_size < scaled<coord_t>(0.1) that is the minimum line width
|
||
TreeSupportSettings::soluble = true;
|
||
}
|
||
|
||
size_t largest_printed_mesh_idx = 0;
|
||
|
||
// Group all meshes that can be processed together. NOTE this is different from mesh-groups! Only one setting object is needed per group,
|
||
// as different settings in the same group may only occur in the tip, which uses the original settings objects from the meshes.
|
||
for (size_t object_id : print_object_ids) {
|
||
const PrintObject &print_object = *print.get_object(object_id);
|
||
#ifndef NDEBUG
|
||
const PrintObjectConfig &object_config = print_object.config();
|
||
#endif // NDEBUG
|
||
// Support must be enabled and set to Tree style.
|
||
//assert(object_config.support_material);
|
||
//assert(object_config.support_material_style == smsTree || object_config.support_material_style == smsOrganic);
|
||
|
||
bool found_existing_group = false;
|
||
TreeSupportSettings next_settings{ TreeSupportMeshGroupSettings{ print_object }, print_object.slicing_parameters() };
|
||
//FIXME for now only a single object per group is enabled.
|
||
#if 0
|
||
for (size_t idx = 0; idx < grouped_meshes.size(); ++ idx)
|
||
if (next_settings == grouped_meshes[idx].first) {
|
||
found_existing_group = true;
|
||
grouped_meshes[idx].second.emplace_back(object_id);
|
||
// handle some settings that are only used for performance reasons. This ensures that a horrible set setting intended to improve performance can not reduce it drastically.
|
||
grouped_meshes[idx].first.performance_interface_skip_layers = std::min(grouped_meshes[idx].first.performance_interface_skip_layers, next_settings.performance_interface_skip_layers);
|
||
}
|
||
#endif
|
||
if (! found_existing_group)
|
||
grouped_meshes.emplace_back(next_settings, std::vector<size_t>{ object_id });
|
||
|
||
// no need to do this per mesh group as adaptive layers and raft setting are not setable per mesh.
|
||
if (print.get_object(largest_printed_mesh_idx)->layers().back()->print_z < print_object.layers().back()->print_z)
|
||
largest_printed_mesh_idx = object_id;
|
||
}
|
||
|
||
#if 0
|
||
{
|
||
std::vector<coord_t> known_z(storage.meshes[largest_printed_mesh_idx].layers.size());
|
||
for (size_t z = 0; z < storage.meshes[largest_printed_mesh_idx].layers.size(); z++)
|
||
known_z[z] = storage.meshes[largest_printed_mesh_idx].layers[z].printZ;
|
||
for (size_t idx = 0; idx < grouped_meshes.size(); ++ idx)
|
||
grouped_meshes[idx].first.setActualZ(known_z);
|
||
}
|
||
#endif
|
||
|
||
return grouped_meshes;
|
||
}
|
||
|
||
#if 0
|
||
// todo remove as only for debugging relevant
|
||
[[nodiscard]] static std::string getPolygonAsString(const Polygons& poly)
|
||
{
|
||
std::string ret;
|
||
for (auto path : poly)
|
||
for (Point p : path) {
|
||
if (ret != "")
|
||
ret += ",";
|
||
ret += "(" + std::to_string(p.x()) + "," + std::to_string(p.y()) + ")";
|
||
}
|
||
return ret;
|
||
}
|
||
#endif
|
||
|
||
[[nodiscard]] static const std::vector<Polygons> generate_overhangs(const TreeSupportSettings &settings, const PrintObject &print_object, std::function<void()> throw_on_cancel)
|
||
{
|
||
const size_t num_raft_layers = settings.raft_layers.size();
|
||
const size_t num_object_layers = print_object.layer_count();
|
||
const size_t num_layers = num_object_layers + num_raft_layers;
|
||
std::vector<Polygons> out(num_layers, Polygons{});
|
||
|
||
const PrintConfig &print_config = print_object.print()->config();
|
||
const PrintObjectConfig &config = print_object.config();
|
||
const bool support_auto = is_auto(config.support_type.value);
|
||
const int support_enforce_layers = config.enforce_support_layers.value;
|
||
std::vector<Polygons> enforcers_layers{ print_object.slice_support_enforcers() };
|
||
std::vector<Polygons> blockers_layers{ print_object.slice_support_blockers() };
|
||
print_object.project_and_append_custom_facets(false, EnforcerBlockerType::ENFORCER, enforcers_layers);
|
||
print_object.project_and_append_custom_facets(false, EnforcerBlockerType::BLOCKER, blockers_layers);
|
||
const int support_threshold = config.support_threshold_angle.value;
|
||
const bool support_threshold_auto = support_threshold == 0;
|
||
// +1 makes the threshold inclusive
|
||
double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.);
|
||
//FIXME this is a fudge constant!
|
||
double support_tree_tip_diameter = 0.8;
|
||
auto enforcer_overhang_offset = scaled<double>(support_tree_tip_diameter);
|
||
|
||
size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size()));
|
||
tbb::parallel_for(tbb::blocked_range<LayerIndex>(1, num_overhang_layers),
|
||
[&print_object, &config, &print_config, &enforcers_layers, &blockers_layers,
|
||
support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, &throw_on_cancel, &out]
|
||
(const tbb::blocked_range<LayerIndex> &range) {
|
||
for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) {
|
||
const Layer ¤t_layer = *print_object.get_layer(layer_id);
|
||
const Layer &lower_layer = *print_object.get_layer(layer_id - 1);
|
||
// Full overhangs with zero lower_layer_offset and no blockers applied.
|
||
Polygons raw_overhangs;
|
||
bool raw_overhangs_calculated = false;
|
||
// Final overhangs.
|
||
Polygons overhangs;
|
||
// For how many layers full overhangs shall be supported.
|
||
const bool enforced_layer = layer_id < support_enforce_layers;
|
||
if (support_auto || enforced_layer) {
|
||
float lower_layer_offset;
|
||
if (enforced_layer)
|
||
lower_layer_offset = 0;
|
||
else if (support_threshold_auto) {
|
||
float external_perimeter_width = 0;
|
||
for (const LayerRegion *layerm : lower_layer.regions())
|
||
external_perimeter_width += layerm->flow(frExternalPerimeter).scaled_width();
|
||
external_perimeter_width /= lower_layer.region_count();
|
||
lower_layer_offset = float(0.5 * external_perimeter_width);
|
||
} else
|
||
lower_layer_offset = scaled<float>(lower_layer.height / tan_threshold);
|
||
overhangs = lower_layer_offset == 0 ?
|
||
diff(current_layer.lslices, lower_layer.lslices) :
|
||
diff(current_layer.lslices, offset(lower_layer.lslices, lower_layer_offset));
|
||
if (lower_layer_offset == 0) {
|
||
raw_overhangs = overhangs;
|
||
raw_overhangs_calculated = true;
|
||
}
|
||
if (! (enforced_layer || blockers_layers.empty() || blockers_layers[layer_id].empty()))
|
||
overhangs = diff(overhangs, blockers_layers[layer_id], ApplySafetyOffset::Yes);
|
||
//if (config.bridge_no_support) {
|
||
// for (const LayerRegion *layerm : current_layer.regions())
|
||
// remove_bridges_from_contacts(print_config, lower_layer, *layerm,
|
||
// float(layerm->flow(frExternalPerimeter).scaled_width()), overhangs);
|
||
//}
|
||
}
|
||
//check_self_intersections(overhangs, "generate_overhangs1");
|
||
if (! enforcers_layers.empty() && ! enforcers_layers[layer_id].empty()) {
|
||
// Has some support enforcers at this layer, apply them to the overhangs, don't apply the support threshold angle.
|
||
//enforcers_layers[layer_id] = union_(enforcers_layers[layer_id]);
|
||
//check_self_intersections(enforcers_layers[layer_id], "generate_overhangs - enforcers");
|
||
//check_self_intersections(to_polygons(lower_layer.lslices), "generate_overhangs - lowerlayers");
|
||
if (Polygons enforced_overhangs = intersection(raw_overhangs_calculated ? raw_overhangs : diff(current_layer.lslices, lower_layer.lslices), enforcers_layers[layer_id] /*, ApplySafetyOffset::Yes */);
|
||
! enforced_overhangs.empty()) {
|
||
//FIXME this is a hack to make enforcers work on steep overhangs.
|
||
//check_self_intersections(enforced_overhangs, "generate_overhangs - enforced overhangs1");
|
||
//Polygons enforced_overhangs_prev = enforced_overhangs;
|
||
//check_self_intersections(to_polygons(union_ex(enforced_overhangs)), "generate_overhangs - enforced overhangs11");
|
||
//check_self_intersections(offset(union_ex(enforced_overhangs),
|
||
//FIXME enforcer_overhang_offset is a fudge constant!
|
||
enforced_overhangs = diff(offset(union_ex(enforced_overhangs), enforcer_overhang_offset),
|
||
lower_layer.lslices);
|
||
#ifdef TREESUPPORT_DEBUG_SVG
|
||
// if (! intersecting_edges(enforced_overhangs).empty())
|
||
{
|
||
static int irun = 0;
|
||
SVG::export_expolygons(debug_out_path("treesupport-self-intersections-%d.svg", ++irun),
|
||
{ { { current_layer.lslices }, { "current_layer.lslices", "yellow", 0.5f } },
|
||
{ { lower_layer.lslices }, { "lower_layer.lslices", "gray", 0.5f } },
|
||
{ { union_ex(enforced_overhangs) }, { "enforced_overhangs", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
}
|
||
#endif // TREESUPPORT_DEBUG_SVG
|
||
//check_self_intersections(enforced_overhangs, "generate_overhangs - enforced overhangs2");
|
||
overhangs = overhangs.empty() ? std::move(enforced_overhangs) : union_(overhangs, enforced_overhangs);
|
||
//check_self_intersections(overhangs, "generate_overhangs - enforcers");
|
||
}
|
||
}
|
||
out[layer_id + num_raft_layers] = std::move(overhangs);
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
/*!
|
||
* \brief Precalculates all avoidances, that could be required.
|
||
*/
|
||
[[nodiscard]] static LayerIndex precalculate(const Print &print, const std::vector<Polygons> &overhangs, const TreeSupportSettings &config, const std::vector<size_t> &object_ids, TreeModelVolumes &volumes, std::function<void()> throw_on_cancel)
|
||
{
|
||
// calculate top most layer that is relevant for support
|
||
LayerIndex max_layer = 0;
|
||
for (size_t object_id : object_ids) {
|
||
const PrintObject &print_object = *print.get_object(object_id);
|
||
const int num_raft_layers = int(config.raft_layers.size());
|
||
const int num_layers = int(print_object.layer_count()) + num_raft_layers;
|
||
int max_support_layer_id = 0;
|
||
for (int layer_id = std::max<int>(num_raft_layers, 1); layer_id < num_layers; ++ layer_id)
|
||
if (! overhangs[layer_id].empty())
|
||
max_support_layer_id = layer_id;
|
||
max_layer = std::max(max_support_layer_id - int(config.z_distance_top_layers), 0);
|
||
}
|
||
if (max_layer > 0)
|
||
// The actual precalculation happens in TreeModelVolumes.
|
||
volumes.precalculate(*print.get_object(object_ids.front()), max_layer, throw_on_cancel);
|
||
return max_layer;
|
||
}
|
||
|
||
/*!
|
||
* \brief Converts a Polygons object representing a line into the internal format.
|
||
*
|
||
* \param polylines[in] The Polyline that will be converted.
|
||
* \param layer_idx[in] The current layer.
|
||
* \return All lines of the \p polylines object, with information for each point regarding in which avoidance it is currently valid in.
|
||
*/
|
||
// Called by generate_initial_areas()
|
||
[[nodiscard]] static LineInformations convert_lines_to_internal(
|
||
const TreeModelVolumes &volumes, const TreeSupportSettings &config,
|
||
const Polylines &polylines, LayerIndex layer_idx)
|
||
{
|
||
const bool min_xy_dist = config.xy_distance > config.xy_min_distance;
|
||
|
||
LineInformations result;
|
||
// Also checks if the position is valid, if it is NOT, it deletes that point
|
||
for (const Polyline &line : polylines) {
|
||
LineInformation res_line;
|
||
for (Point p : line) {
|
||
if (! contains(volumes.getAvoidance(config.getRadius(0), layer_idx, TreeModelVolumes::AvoidanceType::FastSafe, false, min_xy_dist), p))
|
||
res_line.emplace_back(p, LineStatus::TO_BP_SAFE);
|
||
else if (! contains(volumes.getAvoidance(config.getRadius(0), layer_idx, TreeModelVolumes::AvoidanceType::Fast, false, min_xy_dist), p))
|
||
res_line.emplace_back(p, LineStatus::TO_BP);
|
||
else if (config.support_rests_on_model && ! contains(volumes.getAvoidance(config.getRadius(0), layer_idx, TreeModelVolumes::AvoidanceType::FastSafe, true, min_xy_dist), p))
|
||
res_line.emplace_back(p, LineStatus::TO_MODEL_GRACIOUS_SAFE);
|
||
else if (config.support_rests_on_model && ! contains(volumes.getAvoidance(config.getRadius(0), layer_idx, TreeModelVolumes::AvoidanceType::Fast, true, min_xy_dist), p))
|
||
res_line.emplace_back(p, LineStatus::TO_MODEL_GRACIOUS);
|
||
else if (config.support_rests_on_model && ! contains(volumes.getCollision(config.getRadius(0), layer_idx, min_xy_dist), p))
|
||
res_line.emplace_back(p, LineStatus::TO_MODEL);
|
||
else if (!res_line.empty()) {
|
||
result.emplace_back(res_line);
|
||
res_line.clear();
|
||
}
|
||
}
|
||
if (!res_line.empty()) {
|
||
result.emplace_back(res_line);
|
||
res_line.clear();
|
||
}
|
||
}
|
||
|
||
validate_range(result);
|
||
return result;
|
||
}
|
||
|
||
#if 0
|
||
/*!
|
||
* \brief Converts lines in internal format into a Polygons object representing these lines.
|
||
*
|
||
* \param lines[in] The lines that will be converted.
|
||
* \return All lines of the \p lines object as a Polygons object.
|
||
*/
|
||
[[nodiscard]] static Polylines convert_internal_to_lines(LineInformations lines)
|
||
{
|
||
Polylines result;
|
||
for (LineInformation line : lines) {
|
||
Polyline path;
|
||
for (auto point_data : line)
|
||
path.points.emplace_back(point_data.first);
|
||
result.emplace_back(std::move(path));
|
||
}
|
||
validate_range(result);
|
||
return result;
|
||
}
|
||
#endif
|
||
|
||
/*!
|
||
* \brief Evaluates if a point has to be added now. Required for a split_lines call in generate_initial_areas().
|
||
*
|
||
* \param current_layer[in] The layer on which the point lies, point and its status.
|
||
* \return whether the point is valid.
|
||
*/
|
||
[[nodiscard]] static bool evaluate_point_for_next_layer_function(
|
||
const TreeModelVolumes &volumes, const TreeSupportSettings &config,
|
||
size_t current_layer, const std::pair<Point, LineStatus> &p)
|
||
{
|
||
using AvoidanceType = TreeModelVolumes::AvoidanceType;
|
||
const bool min_xy_dist = config.xy_distance > config.xy_min_distance;
|
||
if (! contains(volumes.getAvoidance(config.getRadius(0), current_layer - 1, p.second == LineStatus::TO_BP_SAFE ? AvoidanceType::FastSafe : AvoidanceType::Fast, false, min_xy_dist), p.first))
|
||
return true;
|
||
if (config.support_rests_on_model && (p.second != LineStatus::TO_BP && p.second != LineStatus::TO_BP_SAFE))
|
||
return ! contains(
|
||
p.second == LineStatus::TO_MODEL_GRACIOUS || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE ?
|
||
volumes.getAvoidance(config.getRadius(0), current_layer - 1, p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE ? AvoidanceType::FastSafe : AvoidanceType::Fast, true, min_xy_dist) :
|
||
volumes.getCollision(config.getRadius(0), current_layer - 1, min_xy_dist),
|
||
p.first);
|
||
return false;
|
||
}
|
||
|
||
/*!
|
||
* \brief Evaluates which points of some lines are not valid one layer below and which are. Assumes all points are valid on the current layer. Validity is evaluated using supplied lambda.
|
||
*
|
||
* \param lines[in] The lines that have to be evaluated.
|
||
* \param evaluatePoint[in] The function used to evaluate the points.
|
||
* \return A pair with which points are still valid in the first slot and which are not in the second slot.
|
||
*/
|
||
template<typename EvaluatePointFn>
|
||
[[nodiscard]] static std::pair<LineInformations, LineInformations> split_lines(const LineInformations &lines, EvaluatePointFn evaluatePoint)
|
||
{
|
||
// assumes all Points on the current line are valid
|
||
|
||
LineInformations keep;
|
||
LineInformations set_free;
|
||
for (const std::vector<std::pair<Point, LineStatus>> &line : lines) {
|
||
bool current_keep = true;
|
||
LineInformation resulting_line;
|
||
for (const std::pair<Point, LineStatus> &me : line) {
|
||
if (evaluatePoint(me) != current_keep) {
|
||
if (! resulting_line.empty())
|
||
(current_keep ? &keep : &set_free)->emplace_back(std::move(resulting_line));
|
||
current_keep = !current_keep;
|
||
}
|
||
resulting_line.emplace_back(me);
|
||
}
|
||
if (! resulting_line.empty())
|
||
(current_keep ? &keep : &set_free)->emplace_back(std::move(resulting_line));
|
||
}
|
||
validate_range(keep);
|
||
validate_range(set_free);
|
||
return std::pair<std::vector<std::vector<std::pair<Point, LineStatus>>>, std::vector<std::vector<std::pair<Point, LineStatus>>>>(keep, set_free);
|
||
}
|
||
|
||
// Ported from CURA's PolygonUtils::getNextPointWithDistance()
|
||
// Sample a next point at distance "dist" from start_pt on polyline segment (start_idx, start_idx + 1).
|
||
// Returns sample point and start index of its segment on polyline if such sample exists.
|
||
static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_distance(const Points &polyline, const Point &start_pt, size_t start_idx, double dist)
|
||
{
|
||
const double dist2 = sqr(dist);
|
||
const auto dist2i = int64_t(dist2);
|
||
static constexpr const auto eps = scaled<double>(0.01);
|
||
|
||
for (size_t i = start_idx + 1; i < polyline.size(); ++ i) {
|
||
const Point p1 = polyline[i];
|
||
if ((p1 - start_pt).cast<int64_t>().squaredNorm() >= dist2i) {
|
||
// The end point is outside the circle with center "start_pt" and radius "dist".
|
||
const Point p0 = polyline[i - 1];
|
||
Vec2d v = (p1 - p0).cast<double>();
|
||
double l2v = v.squaredNorm();
|
||
if (l2v < sqr(eps)) {
|
||
// Very short segment.
|
||
Point c = (p0 + p1) / 2;
|
||
if (std::abs((start_pt - c).cast<double>().norm() - dist) < eps)
|
||
return std::pair<Point, size_t>{ c, i - 1 };
|
||
else
|
||
continue;
|
||
}
|
||
Vec2d p0f = (start_pt - p0).cast<double>();
|
||
// Foot point of start_pt into v.
|
||
Vec2d foot_pt = v * (p0f.dot(v) / l2v);
|
||
// Vector from foot point of "start_pt" to "start_pt".
|
||
Vec2d xf = p0f - foot_pt;
|
||
// Squared distance of "start_pt" from the ray (p0, p1).
|
||
double l2_from_line = xf.squaredNorm();
|
||
// Squared distance of an intersection point of a circle with center at the foot point.
|
||
if (double l2_intersection = dist2 - l2_from_line;
|
||
l2_intersection > - SCALED_EPSILON) {
|
||
// The ray (p0, p1) touches or intersects a circle centered at "start_pt" with radius "dist".
|
||
// Distance of the circle intersection point from the foot point.
|
||
l2_intersection = std::max(l2_intersection, 0.);
|
||
if ((v - foot_pt).cast<double>().squaredNorm() >= l2_intersection) {
|
||
// Intersection of the circle with the segment (p0, p1) is on the right side (close to p1) from the foot point.
|
||
Point p = p0 + (foot_pt + v * sqrt(l2_intersection / l2v)).cast<coord_t>();
|
||
validate_range(p);
|
||
return std::pair<Point, size_t>{ p, i - 1 };
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
/*!
|
||
* \brief Eensures that every line segment is about distance in length. The resulting lines may differ from the original but all points are on the original
|
||
*
|
||
* \param input[in] The lines on which evenly spaced points should be placed.
|
||
* \param distance[in] The distance the points should be from each other.
|
||
* \param min_points[in] The amount of points that have to be placed. If not enough can be placed the distance will be reduced to place this many points.
|
||
* \return A Polygons object containing the evenly spaced points. Does not represent an area, more a collection of points on lines.
|
||
*/
|
||
[[nodiscard]] static Polylines ensure_maximum_distance_polyline(const Polylines &input, double distance, size_t min_points)
|
||
{
|
||
Polylines result;
|
||
for (Polyline part : input) {
|
||
if (part.empty())
|
||
continue;
|
||
|
||
double len = length(part.points);
|
||
Polyline line;
|
||
double current_distance = std::max(distance, scaled<double>(0.1));
|
||
if (len < 2 * distance && min_points <= 1)
|
||
{
|
||
// Insert the opposite point of the first one.
|
||
//FIXME pretty expensive
|
||
Polyline pl(part);
|
||
pl.clip_end(len / 2);
|
||
line.points.emplace_back(pl.points.back());
|
||
}
|
||
else
|
||
{
|
||
size_t optimal_end_index = part.size() - 1;
|
||
|
||
if (part.front() == part.back()) {
|
||
size_t optimal_start_index = 0;
|
||
// If the polyline was a polygon, there is a high chance it was an overhang. Overhangs that are <60<36> tend to be very thin areas, so lets get the beginning and end of them and ensure that they are supported.
|
||
// The first point of the line will always be supported, so rotate the order of points in this polyline that one of the two corresponding points that are furthest from each other is in the beginning.
|
||
// The other will be manually added (optimal_end_index)
|
||
coord_t max_dist2_between_vertecies = 0;
|
||
for (size_t idx = 0; idx < part.size() - 1; ++ idx) {
|
||
for (size_t inner_idx = 0; inner_idx < part.size() - 1; inner_idx++) {
|
||
if ((part[idx] - part[inner_idx]).cast<double>().squaredNorm() > max_dist2_between_vertecies) {
|
||
optimal_start_index = idx;
|
||
optimal_end_index = inner_idx;
|
||
max_dist2_between_vertecies = (part[idx] - part[inner_idx]).cast<double>().squaredNorm();
|
||
}
|
||
}
|
||
}
|
||
std::rotate(part.begin(), part.begin() + optimal_start_index, part.end() - 1);
|
||
part[part.size() - 1] = part[0]; // restore that property that this polyline ends where it started.
|
||
optimal_end_index = (part.size() + optimal_end_index - optimal_start_index - 1) % (part.size() - 1);
|
||
}
|
||
|
||
while (line.size() < min_points && current_distance >= scaled<double>(0.1))
|
||
{
|
||
line.clear();
|
||
Point current_point = part[0];
|
||
line.points.emplace_back(part[0]);
|
||
if (min_points > 1 || (part[0] - part[optimal_end_index]).cast<double>().norm() > current_distance)
|
||
line.points.emplace_back(part[optimal_end_index]);
|
||
size_t current_index = 0;
|
||
std::optional<std::pair<Point, size_t>> next_point;
|
||
double next_distance = current_distance;
|
||
// Get points so that at least min_points are added and they each are current_distance away from each other. If that is impossible, decrease current_distance a bit.
|
||
// The input are lines, that means that the line from the last to the first vertex does not have to exist, so exclude all points that are on this line!
|
||
while ((next_point = polyline_sample_next_point_at_distance(part.points, current_point, current_index, next_distance))) {
|
||
// Not every point that is distance away, is valid, as it may be much closer to another point. This is especially the case when the overhang is very thin.
|
||
// So this ensures that the points are actually a certain distance from each other.
|
||
// This assurance is only made on a per polygon basis, as different but close polygon may not be able to use support below the other polygon.
|
||
double min_distance_to_existing_point = std::numeric_limits<double>::max();
|
||
for (Point p : line)
|
||
min_distance_to_existing_point = std::min(min_distance_to_existing_point, (p - next_point->first).cast<double>().norm());
|
||
if (min_distance_to_existing_point >= current_distance) {
|
||
// viable point was found. Add to possible result.
|
||
line.points.emplace_back(next_point->first);
|
||
current_point = next_point->first;
|
||
current_index = next_point->second;
|
||
next_distance = current_distance;
|
||
} else {
|
||
if (current_point == next_point->first) {
|
||
// In case a fixpoint is encountered, better aggressively overcompensate so the code does not become stuck here...
|
||
BOOST_LOG_TRIVIAL(warning) << "Tree Support: Encountered a fixpoint in polyline_sample_next_point_at_distance. This is expected to happen if the distance (currently " << next_distance <<
|
||
") is smaller than 100";
|
||
tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing."sv, true);
|
||
if (next_distance > 2 * current_distance)
|
||
// This case should never happen, but better safe than sorry.
|
||
break;
|
||
next_distance += current_distance;
|
||
continue;
|
||
}
|
||
// if the point was too close, the next possible viable point is at least distance-min_distance_to_existing_point away from the one that was just checked.
|
||
next_distance = std::max(current_distance - min_distance_to_existing_point, scaled<double>(0.1));
|
||
current_point = next_point->first;
|
||
current_index = next_point->second;
|
||
}
|
||
}
|
||
current_distance *= 0.9;
|
||
}
|
||
}
|
||
result.emplace_back(std::move(line));
|
||
}
|
||
validate_range(result);
|
||
return result;
|
||
}
|
||
|
||
/*!
|
||
* \brief Returns Polylines representing the (infill) lines that will result in slicing the given area
|
||
*
|
||
* \param area[in] The area that has to be filled with infill.
|
||
* \param roof[in] Whether the roofing or regular support settings should be used.
|
||
* \param layer_idx[in] The current layer index.
|
||
* \param support_infill_distance[in] The distance that should be between the infill lines.
|
||
*
|
||
* \return A Polygons object that represents the resulting infill lines.
|
||
*/
|
||
[[nodiscard]] static Polylines generate_support_infill_lines(
|
||
const Polygons &polygon,
|
||
const SupportParameters &support_params,
|
||
bool roof, LayerIndex layer_idx, coord_t support_infill_distance)
|
||
{
|
||
#if 0
|
||
Polygons gaps;
|
||
// as we effectivly use lines to place our supportPoints we may use the Infill class for it, while not made for it it works perfect
|
||
|
||
const EFillMethod pattern = roof ? config.roof_pattern : config.support_pattern;
|
||
|
||
// const bool zig_zaggify_infill = roof ? pattern == EFillMethod::ZIG_ZAG : config.zig_zaggify_support;
|
||
const bool connect_polygons = false;
|
||
constexpr coord_t support_roof_overlap = 0;
|
||
constexpr size_t infill_multiplier = 1;
|
||
constexpr coord_t outline_offset = 0;
|
||
const int support_shift = roof ? 0 : support_infill_distance / 2;
|
||
const size_t wall_line_count = include_walls && !roof ? config.support_wall_count : 0;
|
||
const Point infill_origin;
|
||
constexpr Polygons* perimeter_gaps = nullptr;
|
||
constexpr bool use_endpieces = true;
|
||
const bool connected_zigzags = roof ? false : config.connect_zigzags;
|
||
const size_t zag_skip_count = roof ? 0 : config.zag_skip_count;
|
||
constexpr coord_t pocket_size = 0;
|
||
std::vector<AngleRadians> angles = roof ? config.support_roof_angles : config.support_infill_angles;
|
||
std::vector<VariableWidthLines> toolpaths;
|
||
|
||
const coord_t z = config.getActualZ(layer_idx);
|
||
int divisor = static_cast<int>(angles.size());
|
||
int index = ((layer_idx % divisor) + divisor) % divisor;
|
||
const AngleRadians fill_angle = angles[index];
|
||
Infill roof_computation(pattern, true /* zig_zaggify_infill */, connect_polygons, polygon,
|
||
roof ? config.support_roof_line_width : config.support_line_width, support_infill_distance, support_roof_overlap, infill_multiplier,
|
||
fill_angle, z, support_shift, config.resolution, wall_line_count, infill_origin,
|
||
perimeter_gaps, connected_zigzags, use_endpieces, false /* skip_some_zags */, zag_skip_count, pocket_size);
|
||
Polygons polygons;
|
||
Polygons lines;
|
||
roof_computation.generate(toolpaths, polygons, lines, config.settings);
|
||
append(lines, to_polylines(polygons));
|
||
return lines;
|
||
#else
|
||
const Flow &flow = roof ? support_params.support_material_interface_flow : support_params.support_material_flow;
|
||
std::unique_ptr<Fill> filler = std::unique_ptr<Fill>(Fill::new_from_type(roof ? support_params.interface_fill_pattern : support_params.base_fill_pattern));
|
||
FillParams fill_params;
|
||
|
||
filler->layer_id = layer_idx;
|
||
filler->spacing = flow.spacing();
|
||
filler->angle = roof ?
|
||
//fixme support_layer.interface_id() instead of layer_idx
|
||
(support_params.interface_angle + (layer_idx & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)) :
|
||
support_params.base_angle;
|
||
|
||
fill_params.density = float(roof ? support_params.interface_density : scaled<float>(filler->spacing) / (scaled<float>(filler->spacing) + float(support_infill_distance)));
|
||
fill_params.dont_adjust = true;
|
||
|
||
Polylines out;
|
||
for (ExPolygon &expoly : union_ex(polygon)) {
|
||
// The surface type does not matter.
|
||
assert(area(expoly) > 0.);
|
||
#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
if (area(expoly) <= 0.)
|
||
::MessageBoxA(nullptr, "TreeSupport infill negative area", "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING);
|
||
#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
assert(intersecting_edges(to_polygons(expoly)).empty());
|
||
check_self_intersections(expoly, "generate_support_infill_lines");
|
||
Surface surface(stInternal, std::move(expoly));
|
||
try {
|
||
Polylines pl = filler->fill_surface(&surface, fill_params);
|
||
assert(pl.empty() || get_extents(surface.expolygon).inflated(SCALED_EPSILON).contains(get_extents(pl)));
|
||
#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
if (! pl.empty() && ! get_extents(surface.expolygon).inflated(SCALED_EPSILON).contains(get_extents(pl)))
|
||
::MessageBoxA(nullptr, "TreeSupport infill failure", "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING);
|
||
#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32
|
||
append(out, std::move(pl));
|
||
} catch (InfillFailedException &) {
|
||
}
|
||
}
|
||
validate_range(out);
|
||
return out;
|
||
#endif
|
||
}
|
||
|
||
/*!
|
||
* \brief Unions two Polygons. Ensures that if the input is non empty that the output also will be non empty.
|
||
* \param first[in] The first Polygon.
|
||
* \param second[in] The second Polygon.
|
||
* \return The union of both Polygons
|
||
*/
|
||
[[nodiscard]] static Polygons safe_union(const Polygons first, const Polygons second = {})
|
||
{
|
||
// unionPolygons can slowly remove Polygons under certain circumstances, because of rounding issues (Polygons that have a thin area).
|
||
// This does not cause a problem when actually using it on large areas, but as influence areas (representing centerpoints) can be very thin, this does occur so this ugly workaround is needed
|
||
// Here is an example of a Polygons object that will loose vertices when unioning, and will be gone after a few times unionPolygons was called:
|
||
/*
|
||
Polygons example;
|
||
Polygon exampleInner;
|
||
exampleInner.add(Point(120410,83599));//A
|
||
exampleInner.add(Point(120384,83643));//B
|
||
exampleInner.add(Point(120399,83618));//C
|
||
exampleInner.add(Point(120414,83591));//D
|
||
exampleInner.add(Point(120423,83570));//E
|
||
exampleInner.add(Point(120419,83580));//F
|
||
example.add(exampleInner);
|
||
for(int i=0;i<10;i++){
|
||
log("Iteration %d Example area: %f\n",i,area(example));
|
||
example=example.unionPolygons();
|
||
}
|
||
*/
|
||
|
||
Polygons result;
|
||
if (! first.empty() || ! second.empty()) {
|
||
result = union_(first, second);
|
||
if (result.empty()) {
|
||
BOOST_LOG_TRIVIAL(debug) << "Caught an area destroying union, enlarging areas a bit.";
|
||
// just take the few lines we have, and offset them a tiny bit. Needs to be offsetPolylines, as offset may aleady have problems with the area.
|
||
result = union_(offset(to_polylines(first), scaled<float>(0.002), jtMiter, 1.2), offset(to_polylines(second), scaled<float>(0.002), jtMiter, 1.2));
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/*!
|
||
* \brief Offsets (increases the area of) a polygons object in multiple steps to ensure that it does not lag through over a given obstacle.
|
||
* \param me[in] Polygons object that has to be offset.
|
||
* \param distance[in] The distance by which me should be offset. Expects values >=0.
|
||
* \param collision[in] The area representing obstacles.
|
||
* \param last_step_offset_without_check[in] The most it is allowed to offset in one step.
|
||
* \param min_amount_offset[in] How many steps have to be done at least. As this uses round offset this increases the amount of vertices, which may be required if Polygons get very small. Required as arcTolerance is not exposed in offset, which should result with a similar result.
|
||
* \return The resulting Polygons object.
|
||
*/
|
||
[[nodiscard]] static Polygons safe_offset_inc(const Polygons& me, coord_t distance, const Polygons& collision, coord_t safe_step_size, coord_t last_step_offset_without_check, size_t min_amount_offset)
|
||
{
|
||
bool do_final_difference = last_step_offset_without_check == 0;
|
||
Polygons ret = safe_union(me); // ensure sane input
|
||
|
||
// Trim the collision polygons with the region of interest for diff() efficiency.
|
||
Polygons collision_trimmed_buffer;
|
||
auto collision_trimmed = [&collision_trimmed_buffer, &collision, &ret, distance]() -> const Polygons& {
|
||
if (collision_trimmed_buffer.empty() && ! collision.empty())
|
||
collision_trimmed_buffer = ClipperUtils::clip_clipper_polygons_with_subject_bbox(collision, get_extents(ret).inflated(std::max(0, distance) + SCALED_EPSILON));
|
||
return collision_trimmed_buffer;
|
||
};
|
||
|
||
if (distance == 0)
|
||
return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret);
|
||
if (safe_step_size < 0 || last_step_offset_without_check < 0) {
|
||
BOOST_LOG_TRIVIAL(error) << "Offset increase got invalid parameter!";
|
||
tree_supports_show_error("Negative offset distance... How did you manage this ?"sv, true);
|
||
return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret);
|
||
}
|
||
|
||
coord_t step_size = safe_step_size;
|
||
int steps = distance > last_step_offset_without_check ? (distance - last_step_offset_without_check) / step_size : 0;
|
||
if (distance - steps * step_size > last_step_offset_without_check) {
|
||
if ((steps + 1) * step_size <= distance)
|
||
// This will be the case when last_step_offset_without_check >= safe_step_size
|
||
++ steps;
|
||
else
|
||
do_final_difference = true;
|
||
}
|
||
if (steps + (distance < last_step_offset_without_check || (distance % step_size) != 0) < int(min_amount_offset) && min_amount_offset > 1) {
|
||
// yes one can add a bool as the standard specifies that a result from compare operators has to be 0 or 1
|
||
// reduce the stepsize to ensure it is offset the required amount of times
|
||
step_size = distance / min_amount_offset;
|
||
if (step_size >= safe_step_size) {
|
||
// effectivly reduce last_step_offset_without_check
|
||
step_size = safe_step_size;
|
||
steps = min_amount_offset;
|
||
} else
|
||
steps = distance / step_size;
|
||
}
|
||
// offset in steps
|
||
for (int i = 0; i < steps; ++ i) {
|
||
ret = diff(offset(ret, step_size, ClipperLib::jtRound, scaled<float>(0.01)), collision_trimmed());
|
||
// ensure that if many offsets are done the performance does not suffer extremely by the new vertices of jtRound.
|
||
if (i % 10 == 7)
|
||
ret = polygons_simplify(ret, scaled<double>(0.015));
|
||
}
|
||
// offset the remainder
|
||
float last_offset = distance - steps * step_size;
|
||
if (last_offset > SCALED_EPSILON)
|
||
ret = offset(ret, distance - steps * step_size, ClipperLib::jtRound, scaled<float>(0.01));
|
||
ret = polygons_simplify(ret, scaled<double>(0.015));
|
||
|
||
if (do_final_difference)
|
||
ret = diff(ret, collision_trimmed());
|
||
return union_(ret);
|
||
}
|
||
|
||
class RichInterfacePlacer : public InterfacePlacer {
|
||
public:
|
||
RichInterfacePlacer(
|
||
const InterfacePlacer &interface_placer,
|
||
const TreeModelVolumes &volumes,
|
||
bool force_tip_to_roof,
|
||
size_t num_support_layers,
|
||
std::vector<SupportElements> &move_bounds)
|
||
:
|
||
InterfacePlacer(interface_placer),
|
||
volumes(volumes), force_tip_to_roof(force_tip_to_roof), move_bounds(move_bounds)
|
||
{
|
||
m_already_inserted.assign(num_support_layers, {});
|
||
this->min_xy_dist = this->config.xy_distance > this->config.xy_min_distance;
|
||
}
|
||
const TreeModelVolumes &volumes;
|
||
// Radius of the tree tip is large enough to be covered by an interface.
|
||
const bool force_tip_to_roof;
|
||
bool min_xy_dist;
|
||
|
||
public:
|
||
// called by sample_overhang_area()
|
||
void add_points_along_lines(
|
||
// Insert points (tree tips or top contact interfaces) along these lines.
|
||
LineInformations lines,
|
||
// Start at this layer.
|
||
LayerIndex insert_layer_idx,
|
||
// Insert this number of interface layers.
|
||
size_t roof_tip_layers,
|
||
// True if an interface is already generated above these lines.
|
||
size_t supports_roof_layers,
|
||
// The element tries to not move until this dtt is reached.
|
||
size_t dont_move_until)
|
||
{
|
||
validate_range(lines);
|
||
// Add tip area as roof (happens when minimum roof area > minimum tip area) if possible
|
||
size_t dtt_roof_tip;
|
||
for (dtt_roof_tip = 0; dtt_roof_tip < roof_tip_layers && insert_layer_idx - dtt_roof_tip >= 1; ++ dtt_roof_tip) {
|
||
size_t this_layer_idx = insert_layer_idx - dtt_roof_tip;
|
||
auto evaluateRoofWillGenerate = [&](const std::pair<Point, LineStatus> &p) {
|
||
//FIXME Vojtech: The circle is just shifted, it has a known size, the infill should fit all the time!
|
||
#if 0
|
||
Polygon roof_circle;
|
||
for (Point corner : base_circle)
|
||
roof_circle.points.emplace_back(p.first + corner * config.min_radius);
|
||
return !generate_support_infill_lines({ roof_circle }, config, true, insert_layer_idx - dtt_roof_tip, config.support_roof_line_distance).empty();
|
||
#else
|
||
return true;
|
||
#endif
|
||
};
|
||
|
||
{
|
||
std::pair<LineInformations, LineInformations> split =
|
||
// keep all lines that are still valid on the next layer
|
||
split_lines(lines, [this, this_layer_idx](const std::pair<Point, LineStatus> &p)
|
||
{ return evaluate_point_for_next_layer_function(volumes, config, this_layer_idx, p); });
|
||
LineInformations points = std::move(split.second);
|
||
// Not all roofs are guaranteed to actually generate lines, so filter these out and add them as points.
|
||
split = split_lines(split.first, evaluateRoofWillGenerate);
|
||
lines = std::move(split.first);
|
||
append(points, split.second);
|
||
// add all points that would not be valid
|
||
for (const LineInformation &line : points)
|
||
for (const std::pair<Point, LineStatus> &point_data : line)
|
||
add_point_as_influence_area(point_data, this_layer_idx,
|
||
// don't move until
|
||
roof_tip_layers - dtt_roof_tip,
|
||
// supports roof
|
||
dtt_roof_tip + supports_roof_layers > 0,
|
||
// disable ovalization
|
||
false);
|
||
}
|
||
|
||
// add all tips as roof to the roof storage
|
||
Polygons new_roofs;
|
||
for (const LineInformation &line : lines)
|
||
//FIXME sweep the tip radius along the line?
|
||
for (const std::pair<Point, LineStatus> &p : line) {
|
||
Polygon roof_circle{ m_base_circle };
|
||
roof_circle.scale(config.min_radius / m_base_radius);
|
||
roof_circle.translate(p.first);
|
||
new_roofs.emplace_back(std::move(roof_circle));
|
||
}
|
||
this->add_roof(std::move(new_roofs), this_layer_idx, dtt_roof_tip + supports_roof_layers);
|
||
}
|
||
|
||
for (const LineInformation &line : lines) {
|
||
// If a line consists of enough tips, the assumption is that it is not a single tip, but part of a simulated support pattern.
|
||
// Ovalisation should be disabled for these to improve the quality of the lines when tip_diameter=line_width
|
||
bool disable_ovalistation = config.min_radius < 3 * config.support_line_width && roof_tip_layers == 0 && dtt_roof_tip == 0 && line.size() > 5;
|
||
for (const std::pair<Point, LineStatus> &point_data : line)
|
||
add_point_as_influence_area(point_data, insert_layer_idx - dtt_roof_tip,
|
||
// don't move until
|
||
dont_move_until > dtt_roof_tip ? dont_move_until - dtt_roof_tip : 0,
|
||
// supports roof
|
||
dtt_roof_tip + supports_roof_layers > 0,
|
||
disable_ovalistation);
|
||
}
|
||
}
|
||
|
||
private:
|
||
// called by this->add_points_along_lines()
|
||
void add_point_as_influence_area(std::pair<Point, LineStatus> p, LayerIndex insert_layer, size_t dont_move_until, bool roof, bool skip_ovalisation)
|
||
{
|
||
bool to_bp = p.second == LineStatus::TO_BP || p.second == LineStatus::TO_BP_SAFE;
|
||
bool gracious = to_bp || p.second == LineStatus::TO_MODEL_GRACIOUS || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE;
|
||
bool safe_radius = p.second == LineStatus::TO_BP_SAFE || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE;
|
||
if (! config.support_rests_on_model && ! to_bp) {
|
||
BOOST_LOG_TRIVIAL(warning) << "Tried to add an invalid support point";
|
||
tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly."sv, true);
|
||
return;
|
||
}
|
||
Polygons circle{ m_base_circle };
|
||
circle.front().translate(p.first);
|
||
{
|
||
Point hash_pos = p.first / ((config.min_radius + 1) / 10);
|
||
std::lock_guard<std::mutex> critical_section_movebounds(m_mutex_movebounds);
|
||
if (!m_already_inserted[insert_layer].count(hash_pos)) {
|
||
// normalize the point a bit to also catch points which are so close that inserting it would achieve nothing
|
||
m_already_inserted[insert_layer].emplace(hash_pos);
|
||
static constexpr const size_t dtt = 0;
|
||
SupportElementState state;
|
||
state.target_height = insert_layer;
|
||
state.target_position = p.first;
|
||
state.next_position = p.first;
|
||
state.layer_idx = insert_layer;
|
||
state.effective_radius_height = dtt;
|
||
state.to_buildplate = to_bp;
|
||
state.distance_to_top = dtt;
|
||
state.result_on_layer = p.first;
|
||
assert(state.result_on_layer_is_set());
|
||
state.increased_to_model_radius = 0;
|
||
state.to_model_gracious = gracious;
|
||
state.elephant_foot_increases = 0;
|
||
state.use_min_xy_dist = min_xy_dist;
|
||
state.supports_roof = roof;
|
||
state.dont_move_until = dont_move_until;
|
||
state.can_use_safe_radius = safe_radius;
|
||
state.missing_roof_layers = force_tip_to_roof ? dont_move_until : 0;
|
||
state.skip_ovalisation = skip_ovalisation;
|
||
move_bounds[insert_layer].emplace_back(state, std::move(circle));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Outputs
|
||
std::vector<SupportElements> &move_bounds;
|
||
|
||
// Temps
|
||
static constexpr const auto m_base_radius = scaled<int>(0.01);
|
||
const Polygon m_base_circle { make_circle(m_base_radius, SUPPORT_TREE_CIRCLE_RESOLUTION) };
|
||
|
||
// Mutexes, guards
|
||
std::mutex m_mutex_movebounds;
|
||
std::vector<std::unordered_set<Point, PointHash>> m_already_inserted;
|
||
};
|
||
|
||
|
||
int generate_raft_contact(
|
||
const PrintObject &print_object,
|
||
const TreeSupportSettings &config,
|
||
InterfacePlacer &interface_placer)
|
||
{
|
||
int raft_contact_layer_idx = -1;
|
||
if (print_object.has_raft() && print_object.layer_count() > 0) {
|
||
// Produce raft contact layer outside of the tree support loop, so that no trees will be generated for the raft contact layer.
|
||
// Raft layers supporting raft contact interface will be produced by the classic raft generator.
|
||
// Find the raft contact layer.
|
||
raft_contact_layer_idx = int(config.raft_layers.size()) - 1;
|
||
while (raft_contact_layer_idx > 0 && config.raft_layers[raft_contact_layer_idx] > print_object.slicing_parameters().raft_contact_top_z + EPSILON)
|
||
-- raft_contact_layer_idx;
|
||
// Create the raft contact layer.
|
||
const ExPolygons &lslices = print_object.get_layer(0)->lslices;
|
||
double expansion = print_object.config().raft_expansion.value;
|
||
interface_placer.add_roof_unguarded(expansion > 0 ? offset(lslices, scaled<float>(expansion)) : to_polygons(lslices), raft_contact_layer_idx, 0);
|
||
}
|
||
return raft_contact_layer_idx;
|
||
}
|
||
|
||
void finalize_raft_contact(
|
||
const PrintObject &print_object,
|
||
const int raft_contact_layer_idx,
|
||
SupportGeneratorLayersPtr &top_contacts,
|
||
std::vector<SupportElements> &move_bounds)
|
||
{
|
||
if (raft_contact_layer_idx >= 0) {
|
||
const size_t first_tree_layer = print_object.slicing_parameters().raft_layers() - 1;
|
||
// Remove tree tips that start below the raft contact,
|
||
// remove interface layers below the raft contact.
|
||
for (size_t i = 0; i < first_tree_layer; ++i) {
|
||
top_contacts[i] = nullptr;
|
||
move_bounds[i].clear();
|
||
}
|
||
if (raft_contact_layer_idx >= 0 && print_object.config().raft_expansion.value > 0) {
|
||
// If any tips at first_tree_layer now are completely inside the expanded raft layer, remove them as well before they are propagated to the ground.
|
||
Polygons &raft_polygons = top_contacts[raft_contact_layer_idx]->polygons;
|
||
EdgeGrid::Grid grid(get_extents(raft_polygons).inflated(SCALED_EPSILON));
|
||
grid.create(raft_polygons, Polylines{}, coord_t(scale_(10.)));
|
||
SupportElements &first_layer_move_bounds = move_bounds[first_tree_layer];
|
||
double threshold = scaled<double>(print_object.config().raft_expansion.value) * 2.;
|
||
first_layer_move_bounds.erase(std::remove_if(first_layer_move_bounds.begin(), first_layer_move_bounds.end(),
|
||
[&grid, threshold](const SupportElement &el) {
|
||
coordf_t dist;
|
||
if (grid.signed_distance_edges(el.state.result_on_layer, threshold, dist)) {
|
||
assert(std::abs(dist) < threshold + SCALED_EPSILON);
|
||
// Support point is inside the expanded raft, remove it.
|
||
return dist < - 0.;
|
||
}
|
||
return false;
|
||
}), first_layer_move_bounds.end());
|
||
#if 0
|
||
// Remove the remaining tips from the raft: Closing operation on tip circles.
|
||
if (! first_layer_move_bounds.empty()) {
|
||
const double eps = 0.1;
|
||
// All tips supporting this layer are expected to have the same radius.
|
||
double radius = support_element_radius(config, first_layer_move_bounds.front());
|
||
// Connect the tips with the following closing radius.
|
||
double closing_distance = radius;
|
||
Polygon circle = make_circle(radius + closing_distance, eps);
|
||
Polygons circles;
|
||
circles.reserve(first_layer_move_bounds.size());
|
||
for (const SupportElement &el : first_layer_move_bounds) {
|
||
circles.emplace_back(circle);
|
||
circles.back().translate(el.state.result_on_layer);
|
||
}
|
||
raft_polygons = diff(raft_polygons, offset(union_(circles), - closing_distance));
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
|
||
// Called by generate_initial_areas(), used in parallel by multiple layers.
|
||
// Produce
|
||
// 1) Maximum num_support_roof_layers roof (top interface & contact) layers.
|
||
// 2) Tree tips supporting either the roof layers or the object itself.
|
||
// num_support_roof_layers should always be respected:
|
||
// If num_support_roof_layers contact layers could not be produced, then the tree tip
|
||
// is augmented with SupportElementState::missing_roof_layers
|
||
// and the top "missing_roof_layers" of such particular tree tips are supposed to be coverted to
|
||
// roofs aka interface layers by the tool path generator.
|
||
void sample_overhang_area(
|
||
// Area to support
|
||
Polygons&& overhang_area,
|
||
// If true, then the overhang_area is likely large and wide, thus it is worth to try
|
||
// to cover it with continuous interfaces supported by zig-zag patterned tree tips.
|
||
const bool large_horizontal_roof,
|
||
// Index of the top suport layer generated by this function.
|
||
const size_t layer_idx,
|
||
// Maximum number of roof (contact, interface) layers between the overhang and tree tips to be generated.
|
||
const size_t num_support_roof_layers,
|
||
//
|
||
const coord_t connect_length,
|
||
// Configuration classes
|
||
const TreeSupportMeshGroupSettings& mesh_group_settings,
|
||
// Configuration & Output
|
||
RichInterfacePlacer& interface_placer)
|
||
{
|
||
// Assumption is that roof will support roof further up to avoid a lot of unnecessary branches. Each layer down it is checked whether the roof area
|
||
// is still large enough to be a roof and aborted as soon as it is not. This part was already reworked a few times, and there could be an argument
|
||
// made to change it again if there are actual issues encountered regarding supporting roofs.
|
||
// Main problem is that some patterns change each layer, so just calculating points and checking if they are still valid an layer below is not useful,
|
||
// as the pattern may be different one layer below. Same with calculating which points are now no longer being generated as result from
|
||
// a decreasing roof, as there is no guarantee that a line will be above these points. Implementing a separate roof support behavior
|
||
// for each pattern harms maintainability as it very well could be >100 LOC
|
||
auto generate_roof_lines = [&interface_placer, &mesh_group_settings](const Polygons& area, LayerIndex layer_idx) -> Polylines {
|
||
return generate_support_infill_lines(area, interface_placer.support_parameters, true, layer_idx, mesh_group_settings.support_roof_line_distance);
|
||
};
|
||
|
||
LineInformations overhang_lines;
|
||
// Track how many top contact / interface layers were already generated.
|
||
size_t dtt_roof = 0;
|
||
size_t layer_generation_dtt = 0;
|
||
|
||
if (large_horizontal_roof) {
|
||
assert(num_support_roof_layers > 0);
|
||
// Sometimes roofs could be empty as the pattern does not generate lines if the area is narrow enough (i am looking at you, concentric infill).
|
||
// To catch these cases the added roofs are saved to be evaluated later.
|
||
std::vector<Polygons> added_roofs(num_support_roof_layers);
|
||
Polygons last_overhang = overhang_area;
|
||
for (dtt_roof = 0; dtt_roof < num_support_roof_layers && layer_idx - dtt_roof >= 1; ++dtt_roof) {
|
||
// here the roof is handled. If roof can not be added the branches will try to not move instead
|
||
Polygons forbidden_next;
|
||
{
|
||
const bool min_xy_dist = interface_placer.config.xy_distance > interface_placer.config.xy_min_distance;
|
||
const Polygons& forbidden_next_raw = interface_placer.config.support_rests_on_model ?
|
||
interface_placer.volumes.getCollision(interface_placer.config.getRadius(0), layer_idx - (dtt_roof + 1), min_xy_dist) :
|
||
interface_placer.volumes.getAvoidance(interface_placer.config.getRadius(0), layer_idx - (dtt_roof + 1), TreeModelVolumes::AvoidanceType::Fast, false, min_xy_dist);
|
||
// prevent rounding errors down the line
|
||
//FIXME maybe use SafetyOffset::Yes at the following diff() instead?
|
||
forbidden_next = offset(union_ex(forbidden_next_raw), scaled<float>(0.005), jtMiter, 1.2);
|
||
}
|
||
Polygons overhang_area_next = diff(overhang_area, forbidden_next);
|
||
if (area(overhang_area_next) < mesh_group_settings.minimum_roof_area) {
|
||
// Next layer down the roof area would be to small so we have to insert our roof support here.
|
||
if (dtt_roof > 0) {
|
||
size_t dtt_before = dtt_roof - 1;
|
||
// Produce support head points supporting an interface layer: First produce the interface lines, then sample them.
|
||
overhang_lines = split_lines(
|
||
convert_lines_to_internal(interface_placer.volumes, interface_placer.config,
|
||
ensure_maximum_distance_polyline(generate_roof_lines(last_overhang, layer_idx - dtt_before), connect_length, 1), layer_idx - dtt_before),
|
||
[&interface_placer, layer_idx, dtt_before](const std::pair<Point, LineStatus>& p)
|
||
{ return evaluate_point_for_next_layer_function(interface_placer.volumes, interface_placer.config, layer_idx - dtt_before, p); })
|
||
.first;
|
||
}
|
||
break;
|
||
}
|
||
added_roofs[dtt_roof] = overhang_area;
|
||
last_overhang = std::move(overhang_area);
|
||
overhang_area = std::move(overhang_area_next);
|
||
}
|
||
|
||
layer_generation_dtt = std::max(dtt_roof, size_t(1)) - 1; // 1 inside max and -1 outside to avoid underflow. layer_generation_dtt=dtt_roof-1 if dtt_roof!=0;
|
||
// if the roof should be valid, check that the area does generate lines. This is NOT guaranteed.
|
||
if (overhang_lines.empty() && dtt_roof != 0 && generate_roof_lines(overhang_area, layer_idx - layer_generation_dtt).empty())
|
||
for (size_t idx = 0; idx < dtt_roof; idx++) {
|
||
// check for every roof area that it has resulting lines. Remember idx 1 means the 2. layer of roof => higher idx == lower layer
|
||
if (generate_roof_lines(added_roofs[idx], layer_idx - idx).empty()) {
|
||
dtt_roof = idx;
|
||
layer_generation_dtt = std::max(dtt_roof, size_t(1)) - 1;
|
||
break;
|
||
}
|
||
}
|
||
added_roofs.erase(added_roofs.begin() + dtt_roof, added_roofs.end());
|
||
interface_placer.add_roofs(std::move(added_roofs), layer_idx);
|
||
}
|
||
|
||
if (overhang_lines.empty()) {
|
||
// support_line_width to form a line here as otherwise most will be unsupported. Technically this violates branch distance, but not only is this the only reasonable choice,
|
||
// but it ensures consistant behaviour as some infill patterns generate each line segment as its own polyline part causing a similar line forming behaviour.
|
||
// This is not doen when a roof is above as the roof will support the model and the trees only need to support the roof
|
||
bool supports_roof = dtt_roof > 0;
|
||
bool continuous_tips = !supports_roof && large_horizontal_roof;
|
||
Polylines polylines = ensure_maximum_distance_polyline(
|
||
generate_support_infill_lines(overhang_area, interface_placer.support_parameters, supports_roof, layer_idx - layer_generation_dtt,
|
||
supports_roof ? mesh_group_settings.support_roof_line_distance : mesh_group_settings.support_tree_branch_distance),
|
||
continuous_tips ? interface_placer.config.min_radius / 2 : connect_length, 1);
|
||
size_t point_count = 0;
|
||
for (const Polyline& poly : polylines)
|
||
point_count += poly.size();
|
||
const size_t min_support_points = std::max(coord_t(1), std::min(coord_t(3), coord_t(total_length(overhang_area) / connect_length)));
|
||
if (point_count <= min_support_points) {
|
||
// add the outer wall (of the overhang) to ensure it is correct supported instead. Try placing the support points in a way that they fully support the outer wall, instead of just the with half of the the support line width.
|
||
// I assume that even small overhangs are over one line width wide, so lets try to place the support points in a way that the full support area generated from them
|
||
// will support the overhang (if this is not done it may only be half). This WILL NOT be the case when supporting an angle of about < 60 degrees so there is a fallback,
|
||
// as some support is better than none.
|
||
Polygons reduced_overhang_area = offset(union_ex(overhang_area), -interface_placer.config.support_line_width / 2.2, jtMiter, 1.2);
|
||
polylines = ensure_maximum_distance_polyline(
|
||
to_polylines(
|
||
!reduced_overhang_area.empty() &&
|
||
area(offset(diff_ex(overhang_area, reduced_overhang_area), std::max(interface_placer.config.support_line_width, connect_length), jtMiter, 1.2)) < sqr(scaled<double>(0.001)) ?
|
||
reduced_overhang_area :
|
||
overhang_area),
|
||
connect_length, min_support_points);
|
||
}
|
||
overhang_lines = convert_lines_to_internal(interface_placer.volumes, interface_placer.config, polylines, layer_idx - dtt_roof);
|
||
}
|
||
|
||
assert(dtt_roof <= layer_idx);
|
||
if (dtt_roof >= layer_idx && large_horizontal_roof)
|
||
// Reached buildplate when generating contact, interface and base interface layers.
|
||
interface_placer.add_roof_build_plate(std::move(overhang_area), dtt_roof);
|
||
else {
|
||
// normal trees have to be generated
|
||
const bool roof_enabled = num_support_roof_layers > 0;
|
||
interface_placer.add_points_along_lines(
|
||
// Sample along these lines
|
||
overhang_lines,
|
||
// First layer index to insert the tree tips or interfaces.
|
||
layer_idx - dtt_roof,
|
||
// Remaining roof tip layers.
|
||
interface_placer.force_tip_to_roof ? num_support_roof_layers - dtt_roof : 0,
|
||
// Supports roof already? How many roof layers were already produced above these tips?
|
||
dtt_roof,
|
||
// Don't move until the following distance to top is reached.
|
||
roof_enabled ? num_support_roof_layers - dtt_roof : 0);
|
||
}
|
||
}
|
||
|
||
inline SupportGeneratorLayer& layer_allocate(
|
||
SupportGeneratorLayerStorage& layer_storage,
|
||
SupporLayerType layer_type,
|
||
const SlicingParameters &slicing_params,
|
||
size_t layer_idx)
|
||
{
|
||
auto& layer = layer_storage.allocate(layer_type);
|
||
return layer_initialize(layer, layer_type, slicing_params, layer_idx);
|
||
}
|
||
|
||
/*!
|
||
* \brief Creates the initial influence areas (that can later be propagated down) by placing them below the overhang.
|
||
*
|
||
* Generates Points where the Model should be supported and creates the areas where these points have to be placed.
|
||
*
|
||
* \param mesh[in] The mesh that is currently processed.
|
||
* \param move_bounds[out] Storage for the influence areas.
|
||
* \param storage[in] Background storage, required for adding roofs.
|
||
*/
|
||
void generate_initial_areas(
|
||
const PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const std::vector<Polygons> &overhangs,
|
||
std::vector<SupportElements> &move_bounds,
|
||
InterfacePlacer &interface_placer,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
using AvoidanceType = TreeModelVolumes::AvoidanceType;
|
||
TreeSupportMeshGroupSettings mesh_group_settings(print_object);
|
||
|
||
const size_t z_distance_delta = config.z_distance_top_layers + 1;
|
||
|
||
const bool min_xy_dist = config.xy_distance > config.xy_min_distance;
|
||
|
||
#if 0
|
||
if (mesh.overhang_areas.size() <= z_distance_delta)
|
||
return;
|
||
#endif
|
||
|
||
const coord_t connect_length = (config.support_line_width * 100. / mesh_group_settings.support_tree_top_rate) + std::max(2. * config.min_radius - 1.0 * config.support_line_width, 0.0);
|
||
// As r*r=x*x+y*y (circle equation): If a circle with center at (0,0) the top most point is at (0,r) as in y=r.
|
||
// This calculates how far one has to move on the x-axis so that y=r-support_line_width/2.
|
||
// In other words how far does one need to move on the x-axis to be support_line_width/2 away from the circle line.
|
||
// As a circle is round this length is identical for every axis as long as the 90 degrees angle between both remains.
|
||
const coord_t circle_length_to_half_linewidth_change = config.min_radius < config.support_line_width ?
|
||
config.min_radius / 2 :
|
||
scale_(sqrt(sqr(unscale<double>(config.min_radius)) - sqr(unscale<double>(config.min_radius - config.support_line_width / 2))));
|
||
// Extra support offset to compensate for larger tip radiis. Also outset a bit more when z overwrites xy, because supporting something with a part of a support line is better than not supporting it at all.
|
||
//FIXME Vojtech: This is not sufficient for support enforcers to work.
|
||
//FIXME There is no account for the support overhang angle.
|
||
//FIXME There is no account for the width of the collision regions.
|
||
const coord_t extra_outset = std::max(coord_t(0), config.min_radius - config.support_line_width / 2) + (min_xy_dist ? config.support_line_width / 2 : 0)
|
||
//FIXME this is a heuristic value for support enforcers to work.
|
||
// + 10 * mesh_config.support_line_width;
|
||
;
|
||
const size_t num_support_roof_layers = mesh_group_settings.support_roof_layers;
|
||
const bool roof_enabled = num_support_roof_layers > 0;
|
||
const bool force_tip_to_roof = roof_enabled && (interface_placer.support_parameters.soluble_interface || sqr<double>(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area);
|
||
// cap for how much layer below the overhang a new support point may be added, as other than with regular support every new inserted point
|
||
// may cause extra material and time cost. Could also be an user setting or differently calculated. Idea is that if an overhang
|
||
// does not turn valid in double the amount of layers a slope of support angle would take to travel xy_distance, nothing reasonable will come from it.
|
||
// The 2*z_distance_delta is only a catch for when the support angle is very high.
|
||
// Used only if not min_xy_dist.
|
||
coord_t max_overhang_insert_lag = 0;
|
||
if (config.z_distance_top_layers > 0) {
|
||
max_overhang_insert_lag = 2 * config.z_distance_top_layers;
|
||
|
||
//FIXME
|
||
if (mesh_group_settings.support_angle > EPSILON && mesh_group_settings.support_angle < 0.5 * M_PI - EPSILON) {
|
||
//FIXME mesh_group_settings.support_angle does not apply to enforcers and also it does not apply to automatic support angle (by half the external perimeter width).
|
||
// take the least restrictive avoidance possible
|
||
const auto max_overhang_speed = coord_t(tan(mesh_group_settings.support_angle) * config.layer_height);
|
||
max_overhang_insert_lag = std::max(max_overhang_insert_lag, round_up_divide(config.xy_distance, max_overhang_speed / 2));
|
||
}
|
||
}
|
||
|
||
size_t num_support_layers;
|
||
int raft_contact_layer_idx;
|
||
// Layers with their overhang regions.
|
||
std::vector<std::pair<size_t, const Polygons*>> raw_overhangs;
|
||
|
||
{
|
||
const size_t num_raft_layers = config.raft_layers.size();
|
||
const size_t first_support_layer = std::max(int(num_raft_layers) - int(z_distance_delta), 1);
|
||
num_support_layers = size_t(std::max(0, int(print_object.layer_count()) + int(num_raft_layers) - int(z_distance_delta)));
|
||
raft_contact_layer_idx = generate_raft_contact(print_object, config, interface_placer);
|
||
// Enumerate layers for which the support tips may be generated from overhangs above.
|
||
raw_overhangs.reserve(num_support_layers - first_support_layer);
|
||
for (size_t layer_idx = first_support_layer; layer_idx < num_support_layers; ++ layer_idx)
|
||
if (const size_t overhang_idx = layer_idx + z_distance_delta; ! overhangs[overhang_idx].empty())
|
||
raw_overhangs.push_back({ layer_idx, &overhangs[overhang_idx] });
|
||
}
|
||
|
||
RichInterfacePlacer rich_interface_placer{ interface_placer, volumes, force_tip_to_roof, num_support_layers, move_bounds };
|
||
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, raw_overhangs.size()),
|
||
[&volumes, &config, &raw_overhangs, &mesh_group_settings,
|
||
min_xy_dist, roof_enabled, num_support_roof_layers, extra_outset, circle_length_to_half_linewidth_change, connect_length,
|
||
&rich_interface_placer, &throw_on_cancel](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t raw_overhang_idx = range.begin(); raw_overhang_idx < range.end(); ++ raw_overhang_idx) {
|
||
size_t layer_idx = raw_overhangs[raw_overhang_idx].first;
|
||
const Polygons &overhang_raw = *raw_overhangs[raw_overhang_idx].second;
|
||
|
||
// take the least restrictive avoidance possible
|
||
Polygons relevant_forbidden;
|
||
{
|
||
const Polygons &relevant_forbidden_raw = config.support_rests_on_model ?
|
||
volumes.getCollision(config.getRadius(0), layer_idx, min_xy_dist) :
|
||
volumes.getAvoidance(config.getRadius(0), layer_idx, AvoidanceType::Fast, false, min_xy_dist);
|
||
// prevent rounding errors down the line, points placed directly on the line of the forbidden area may not be added otherwise.
|
||
relevant_forbidden = offset(union_ex(relevant_forbidden_raw), scaled<float>(0.005), jtMiter, 1.2);
|
||
}
|
||
|
||
// every overhang has saved if a roof should be generated for it. This can NOT be done in the for loop as an area may NOT have a roof
|
||
// even if it is larger than the minimum_roof_area when it is only larger because of the support horizontal expansion and
|
||
// it would not have a roof if the overhang is offset by support roof horizontal expansion instead. (At least this is the current behavior of the regular support)
|
||
Polygons overhang_regular;
|
||
{
|
||
// When support_offset = 0 safe_offset_inc will only be the difference between overhang_raw and relevant_forbidden, that has to be calculated anyway.
|
||
overhang_regular = safe_offset_inc(overhang_raw, mesh_group_settings.support_offset, relevant_forbidden, config.min_radius * 1.75 + config.xy_min_distance, 0, 1);
|
||
//check_self_intersections(overhang_regular, "overhang_regular1");
|
||
|
||
// offset ensures that areas that could be supported by a part of a support line, are not considered unsupported overhang
|
||
Polygons remaining_overhang = intersection(
|
||
diff(mesh_group_settings.support_offset == 0 ?
|
||
overhang_raw :
|
||
offset(union_ex(overhang_raw), mesh_group_settings.support_offset, jtMiter, 1.2),
|
||
offset(union_ex(overhang_regular), config.support_line_width * 0.5, jtMiter, 1.2)),
|
||
relevant_forbidden);
|
||
|
||
// Offset the area to compensate for large tip radiis. Offset happens in multiple steps to ensure the tip is as close to the original overhang as possible.
|
||
//+config.support_line_width / 80 to avoid calculating very small (useless) offsets because of rounding errors.
|
||
//FIXME likely a better approach would be to find correspondences between the full overhang and the trimmed overhang
|
||
// and if there is no correspondence, project the missing points to the clipping curve.
|
||
for (coord_t extra_total_offset_acc = 0; ! remaining_overhang.empty() && extra_total_offset_acc + config.support_line_width / 8 < extra_outset; ) {
|
||
const coord_t offset_current_step = std::min(
|
||
extra_total_offset_acc + 2 * config.support_line_width > config.min_radius ?
|
||
config.support_line_width / 8 :
|
||
circle_length_to_half_linewidth_change,
|
||
extra_outset - extra_total_offset_acc);
|
||
extra_total_offset_acc += offset_current_step;
|
||
const Polygons &raw_collision = volumes.getCollision(0, layer_idx, true);
|
||
const coord_t offset_step = config.xy_min_distance + config.support_line_width;
|
||
// Reducing the remaining overhang by the areas already supported.
|
||
//FIXME 1.5 * extra_total_offset_acc seems to be too much, it may remove some remaining overhang without being supported at all.
|
||
remaining_overhang = diff(remaining_overhang, safe_offset_inc(overhang_regular, 1.5 * extra_total_offset_acc, raw_collision, offset_step, 0, 1));
|
||
// Extending the overhangs by the inflated remaining overhangs.
|
||
overhang_regular = union_(overhang_regular, diff(safe_offset_inc(remaining_overhang, extra_total_offset_acc, raw_collision, offset_step, 0, 1), relevant_forbidden));
|
||
//check_self_intersections(overhang_regular, "overhang_regular2");
|
||
}
|
||
#if 0
|
||
// If the xy distance overrides the z distance, some support needs to be inserted further down.
|
||
//=> Analyze which support points do not fit on this layer and check if they will fit a few layers down (while adding them an infinite amount of layers down would technically be closer the the setting description, it would not produce reasonable results. )
|
||
if (! min_xy_dist) {
|
||
LineInformations overhang_lines;
|
||
{
|
||
//Vojtech: Generate support heads at support_tree_branch_distance spacing by producing a zig-zag infill at support_tree_branch_distance spacing,
|
||
// which is then resmapled
|
||
// support_line_width to form a line here as otherwise most will be unsupported. Technically this violates branch distance,
|
||
// mbut not only is this the only reasonable choice, but it ensures consistent behavior as some infill patterns generate
|
||
// each line segment as its own polyline part causing a similar line forming behavior. Also it is assumed that
|
||
// the area that is valid a layer below is to small for support roof.
|
||
Polylines polylines = ensure_maximum_distance_polyline(
|
||
generate_support_infill_lines(remaining_overhang, support_params, false, layer_idx, mesh_group_settings.support_tree_branch_distance),
|
||
config.min_radius, 1);
|
||
if (polylines.size() <= 3)
|
||
// add the outer wall to ensure it is correct supported instead
|
||
polylines = ensure_maximum_distance_polyline(to_polylines(remaining_overhang), connect_length, 3);
|
||
for (const auto &line : polylines) {
|
||
LineInformation res_line;
|
||
for (Point p : line)
|
||
res_line.emplace_back(p, LineStatus::INVALID);
|
||
overhang_lines.emplace_back(res_line);
|
||
}
|
||
validate_range(overhang_lines);
|
||
}
|
||
for (size_t lag_ctr = 1; lag_ctr <= max_overhang_insert_lag && !overhang_lines.empty() && layer_idx - coord_t(lag_ctr) >= 1; lag_ctr++) {
|
||
// get least restricted avoidance for layer_idx-lag_ctr
|
||
const Polygons &relevant_forbidden_below = config.support_rests_on_model ?
|
||
volumes.getCollision(config.getRadius(0), layer_idx - lag_ctr, min_xy_dist) :
|
||
volumes.getAvoidance(config.getRadius(0), layer_idx - lag_ctr, AvoidanceType::Fast, false, min_xy_dist);
|
||
// it is not required to offset the forbidden area here as the points wont change: If points here are not inside the forbidden area neither will they be later when placing these points, as these are the same points.
|
||
auto evaluatePoint = [&](std::pair<Point, LineStatus> p) { return contains(relevant_forbidden_below, p.first); };
|
||
|
||
std::pair<LineInformations, LineInformations> split = split_lines(overhang_lines, evaluatePoint); // keep all lines that are invalid
|
||
overhang_lines = split.first;
|
||
// Set all now valid lines to their correct LineStatus. Easiest way is to just discard Avoidance information for each point and evaluate them again.
|
||
LineInformations fresh_valid_points = convert_lines_to_internal(volumes, config, convert_internal_to_lines(split.second), layer_idx - lag_ctr);
|
||
validate_range(fresh_valid_points);
|
||
|
||
rich_interface_placer.add_points_along_lines(fresh_valid_points, (force_tip_to_roof && lag_ctr <= num_support_roof_layers) ? num_support_roof_layers : 0, layer_idx - lag_ctr, false, roof_enabled ? num_support_roof_layers : 0);
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
if (roof_enabled) {
|
||
static constexpr const coord_t support_roof_offset = 0;
|
||
Polygons overhang_roofs = safe_offset_inc(overhang_raw, support_roof_offset, relevant_forbidden, config.min_radius * 2 + config.xy_min_distance, 0, 1);
|
||
if (mesh_group_settings.minimum_support_area > 0)
|
||
remove_small(overhang_roofs, mesh_group_settings.minimum_roof_area);
|
||
overhang_regular = diff(overhang_regular, overhang_roofs, ApplySafetyOffset::Yes);
|
||
//check_self_intersections(overhang_regular, "overhang_regular3");
|
||
for (ExPolygon &roof_part : union_ex(overhang_roofs)) {
|
||
sample_overhang_area(to_polygons(std::move(roof_part)), true, layer_idx, num_support_roof_layers, connect_length,
|
||
mesh_group_settings, rich_interface_placer);
|
||
throw_on_cancel();
|
||
}
|
||
}
|
||
// Either the roof is not enabled, then these are all the overhangs to be supported,
|
||
// or roof is enabled and these are the thin overhangs at object slopes (not horizontal overhangs).
|
||
if (mesh_group_settings.minimum_support_area > 0)
|
||
remove_small(overhang_regular, mesh_group_settings.minimum_support_area);
|
||
|
||
for (ExPolygon &support_part : union_ex(overhang_regular)) {
|
||
sample_overhang_area(to_polygons(std::move(support_part)),
|
||
false, layer_idx, num_support_roof_layers, connect_length,
|
||
mesh_group_settings, rich_interface_placer);
|
||
throw_on_cancel();
|
||
}
|
||
}
|
||
});
|
||
|
||
finalize_raft_contact(print_object, raft_contact_layer_idx, interface_placer.top_contacts_mutable(), move_bounds);
|
||
}
|
||
|
||
static unsigned int move_inside(const Polygons &polygons, Point &from, int distance = 0, int64_t maxDist2 = std::numeric_limits<int64_t>::max())
|
||
{
|
||
Point ret = from;
|
||
double bestDist2 = std::numeric_limits<double>::max();
|
||
auto bestPoly = static_cast<unsigned int>(-1);
|
||
bool is_already_on_correct_side_of_boundary = false; // whether [from] is already on the right side of the boundary
|
||
for (unsigned int poly_idx = 0; poly_idx < polygons.size(); ++ poly_idx) {
|
||
const Polygon &poly = polygons[poly_idx];
|
||
if (poly.size() < 2)
|
||
continue;
|
||
Point p0 = poly[poly.size() - 2];
|
||
Point p1 = poly.back();
|
||
// because we compare with vSize2 here (no division by zero), we also need to compare by vSize2 inside the loop
|
||
// to avoid integer rounding edge cases
|
||
bool projected_p_beyond_prev_segment = (p1 - p0).cast<int64_t>().dot((from - p0).cast<int64_t>()) >= (p1 - p0).cast<int64_t>().squaredNorm();
|
||
for (const Point& p2 : poly) {
|
||
// X = A + Normal(B-A) * (((B-A) dot (P-A)) / VSize(B-A));
|
||
// = A + (B-A) * ((B-A) dot (P-A)) / VSize2(B-A);
|
||
// X = P projected on AB
|
||
const Point& a = p1;
|
||
const Point& b = p2;
|
||
const Point& p = from;
|
||
auto ab = (b - a).cast<int64_t>();
|
||
auto ap = (p - a).cast<int64_t>();
|
||
int64_t ab_length2 = ab.squaredNorm();
|
||
if (ab_length2 <= 0) { //A = B, i.e. the input polygon had two adjacent points on top of each other.
|
||
p1 = p2; //Skip only one of the points.
|
||
continue;
|
||
}
|
||
int64_t dot_prod = ab.dot(ap);
|
||
if (dot_prod <= 0) { // x is projected to before ab
|
||
if (projected_p_beyond_prev_segment) {
|
||
// case which looks like: > .
|
||
projected_p_beyond_prev_segment = false;
|
||
Point& x = p1;
|
||
|
||
auto dist2 = (x - p).cast<int64_t>().squaredNorm();
|
||
if (dist2 < bestDist2) {
|
||
bestDist2 = dist2;
|
||
bestPoly = poly_idx;
|
||
if (distance == 0)
|
||
ret = x;
|
||
else {
|
||
Vec2d abd = ab.cast<double>();
|
||
Vec2d p1p2 = (p1 - p0).cast<double>();
|
||
double lab = abd.norm();
|
||
double lp1p2 = p1p2.norm();
|
||
// inward direction irrespective of sign of [distance]
|
||
auto inward_dir = perp(abd * (scaled<double>(10.0) / lab) + p1p2 * (scaled<double>(10.0) / lp1p2));
|
||
// MM2INT(10.0) to retain precision for the eventual normalization
|
||
ret = x + (inward_dir * (distance / inward_dir.norm())).cast<coord_t>();
|
||
is_already_on_correct_side_of_boundary = inward_dir.dot((p - x).cast<double>()) * distance >= 0;
|
||
}
|
||
}
|
||
} else {
|
||
projected_p_beyond_prev_segment = false;
|
||
p0 = p1;
|
||
p1 = p2;
|
||
continue;
|
||
}
|
||
} else if (dot_prod >= ab_length2) {
|
||
// x is projected to beyond ab
|
||
projected_p_beyond_prev_segment = true;
|
||
p0 = p1;
|
||
p1 = p2;
|
||
continue;
|
||
} else {
|
||
// x is projected to a point properly on the line segment (not onto a vertex). The case which looks like | .
|
||
projected_p_beyond_prev_segment = false;
|
||
Point x = a + (ab.cast<double>() * (double(dot_prod) / double(ab_length2))).cast<coord_t>();
|
||
auto dist2 = (p - x).cast<int64_t>().squaredNorm();
|
||
if (dist2 < bestDist2) {
|
||
bestDist2 = dist2;
|
||
bestPoly = poly_idx;
|
||
if (distance == 0)
|
||
ret = x;
|
||
else {
|
||
Vec2d abd = ab.cast<double>();
|
||
Vec2d inward_dir = perp(abd * (distance / abd.norm())); // inward or outward depending on the sign of [distance]
|
||
ret = x + inward_dir.cast<coord_t>();
|
||
is_already_on_correct_side_of_boundary = inward_dir.dot((p - x).cast<double>()) >= 0;
|
||
}
|
||
}
|
||
}
|
||
p0 = p1;
|
||
p1 = p2;
|
||
}
|
||
}
|
||
// when the best point is already inside and we're moving inside, or when the best point is already outside and we're moving outside
|
||
if (is_already_on_correct_side_of_boundary) {
|
||
if (bestDist2 < distance * distance)
|
||
from = ret;
|
||
else {
|
||
// from = from; // original point stays unaltered. It is already inside by enough distance
|
||
}
|
||
return bestPoly;
|
||
} else if (bestDist2 < maxDist2) {
|
||
from = ret;
|
||
return bestPoly;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static Point move_inside_if_outside(const Polygons &polygons, Point from, int distance = 0, int64_t maxDist2 = std::numeric_limits<int64_t>::max())
|
||
{
|
||
if (! contains(polygons, from))
|
||
move_inside(polygons, from);
|
||
return from;
|
||
}
|
||
|
||
/*!
|
||
* \brief Checks if an influence area contains a valid subsection and returns the corresponding metadata and the new Influence area.
|
||
*
|
||
* Calculates an influence areas of the layer below, based on the influence area of one element on the current layer.
|
||
* Increases every influence area by maximum_move_distance_slow. If this is not enough, as in we would change our gracious or to_buildplate status the influence areas are instead increased by maximum_move_distance_slow.
|
||
* Also ensures that increasing the radius of a branch, does not cause it to change its status (like to_buildplate ). If this were the case, the radius is not increased instead.
|
||
*
|
||
* Warning: The used format inside this is different as the SupportElement does not have a valid area member. Instead this area is saved as value of the dictionary. This was done to avoid not needed heap allocations.
|
||
*
|
||
* \param settings[in] Which settings have to be used to check validity.
|
||
* \param layer_idx[in] Number of the current layer.
|
||
* \param parent[in] The metadata of the parents influence area.
|
||
* \param relevant_offset[in] The maximal possible influence area. No guarantee regarding validity with current layer collision required, as it is ensured in-function!
|
||
* \param to_bp_data[out] The part of the Influence area that can reach the buildplate.
|
||
* \param to_model_data[out] The part of the Influence area that do not have to reach the buildplate. This has overlap with new_layer_data.
|
||
* \param increased[out] Area than can reach all further up support points. No assurance is made that the buildplate or the model can be reached in accordance to the user-supplied settings.
|
||
* \param overspeed[in] How much should the already offset area be offset again. Usually this is 0.
|
||
* \param mergelayer[in] Will the merge method be called on this layer. This information is required as some calculation can be avoided if they are not required for merging.
|
||
* \return A valid support element for the next layer regarding the calculated influence areas. Empty if no influence are can be created using the supplied influence area and settings.
|
||
*/
|
||
[[nodiscard]] static std::optional<SupportElementState> increase_single_area(
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const AreaIncreaseSettings &settings,
|
||
const LayerIndex layer_idx,
|
||
const SupportElement &parent,
|
||
const Polygons &relevant_offset,
|
||
Polygons &to_bp_data,
|
||
Polygons &to_model_data,
|
||
Polygons &increased,
|
||
const coord_t overspeed,
|
||
const bool mergelayer)
|
||
{
|
||
SupportElementState current_elem{ SupportElementState::propagate_down(parent.state) };
|
||
Polygons check_layer_data;
|
||
if (settings.increase_radius)
|
||
current_elem.effective_radius_height += 1;
|
||
coord_t radius = support_element_collision_radius(config, current_elem);
|
||
|
||
if (settings.move) {
|
||
increased = relevant_offset;
|
||
if (overspeed > 0) {
|
||
const coord_t safe_movement_distance =
|
||
(current_elem.use_min_xy_dist ? config.xy_min_distance : config.xy_distance) +
|
||
(std::min(config.z_distance_top_layers, config.z_distance_bottom_layers) > 0 ? config.min_feature_size : 0);
|
||
// The difference to ensure that the result not only conforms to wall_restriction, but collision/avoidance is done later.
|
||
// The higher last_safe_step_movement_distance comes exactly from the fact that the collision will be subtracted later.
|
||
increased = safe_offset_inc(increased, overspeed, volumes.getWallRestriction(support_element_collision_radius(config, parent.state), layer_idx, parent.state.use_min_xy_dist),
|
||
safe_movement_distance, safe_movement_distance + radius, 1);
|
||
}
|
||
if (settings.no_error && settings.move)
|
||
// as ClipperLib::jtRound has to be used for offsets this simplify is VERY important for performance.
|
||
polygons_simplify(increased, scaled<float>(0.025));
|
||
} else
|
||
// if no movement is done the areas keep parent area as no move == offset(0)
|
||
increased = parent.influence_area;
|
||
|
||
if (mergelayer || current_elem.to_buildplate) {
|
||
to_bp_data = safe_union(diff_clipped(increased, volumes.getAvoidance(radius, layer_idx - 1, settings.type, false, settings.use_min_distance)));
|
||
if (! current_elem.to_buildplate && area(to_bp_data) > tiny_area_threshold) {
|
||
// mostly happening in the tip, but with merges one should check every time, just to be sure.
|
||
current_elem.to_buildplate = true; // sometimes nodes that can reach the buildplate are marked as cant reach, tainting subtrees. This corrects it.
|
||
BOOST_LOG_TRIVIAL(debug) << "Corrected taint leading to a wrong to model value on layer " << layer_idx - 1 << " targeting " <<
|
||
current_elem.target_height << " with radius " << radius;
|
||
}
|
||
}
|
||
if (config.support_rests_on_model) {
|
||
if (mergelayer || current_elem.to_model_gracious)
|
||
to_model_data = safe_union(diff_clipped(increased, volumes.getAvoidance(radius, layer_idx - 1, settings.type, true, settings.use_min_distance)));
|
||
|
||
if (!current_elem.to_model_gracious) {
|
||
if (mergelayer && area(to_model_data) >= tiny_area_threshold) {
|
||
current_elem.to_model_gracious = true;
|
||
BOOST_LOG_TRIVIAL(debug) << "Corrected taint leading to a wrong non gracious value on layer " << layer_idx - 1 << " targeting " <<
|
||
current_elem.target_height << " with radius " << radius;
|
||
} else
|
||
to_model_data = safe_union(diff_clipped(increased, volumes.getCollision(radius, layer_idx - 1, settings.use_min_distance)));
|
||
}
|
||
}
|
||
|
||
check_layer_data = current_elem.to_buildplate ? to_bp_data : to_model_data;
|
||
|
||
if (settings.increase_radius && area(check_layer_data) > tiny_area_threshold) {
|
||
auto validWithRadius = [&](coord_t next_radius) {
|
||
if (volumes.ceilRadius(next_radius, settings.use_min_distance) <= volumes.ceilRadius(radius, settings.use_min_distance))
|
||
return true;
|
||
|
||
Polygons to_bp_data_2;
|
||
if (current_elem.to_buildplate)
|
||
// regular union as output will not be used later => this area should always be a subset of the safe_union one (i think)
|
||
to_bp_data_2 = diff_clipped(increased, volumes.getAvoidance(next_radius, layer_idx - 1, settings.type, false, settings.use_min_distance));
|
||
Polygons to_model_data_2;
|
||
if (config.support_rests_on_model && !current_elem.to_buildplate)
|
||
to_model_data_2 = diff_clipped(increased,
|
||
current_elem.to_model_gracious ?
|
||
volumes.getAvoidance(next_radius, layer_idx - 1, settings.type, true, settings.use_min_distance) :
|
||
volumes.getCollision(next_radius, layer_idx - 1, settings.use_min_distance));
|
||
Polygons check_layer_data_2 = current_elem.to_buildplate ? to_bp_data_2 : to_model_data_2;
|
||
return area(check_layer_data_2) > tiny_area_threshold;
|
||
};
|
||
coord_t ceil_radius_before = volumes.ceilRadius(radius, settings.use_min_distance);
|
||
|
||
if (support_element_collision_radius(config, current_elem) < config.increase_radius_until_radius && support_element_collision_radius(config, current_elem) < support_element_radius(config, current_elem)) {
|
||
coord_t target_radius = std::min(support_element_radius(config, current_elem), config.increase_radius_until_radius);
|
||
coord_t current_ceil_radius = volumes.getRadiusNextCeil(radius, settings.use_min_distance);
|
||
|
||
while (current_ceil_radius < target_radius && validWithRadius(volumes.getRadiusNextCeil(current_ceil_radius + 1, settings.use_min_distance)))
|
||
current_ceil_radius = volumes.getRadiusNextCeil(current_ceil_radius + 1, settings.use_min_distance);
|
||
size_t resulting_eff_dtt = current_elem.effective_radius_height;
|
||
while (resulting_eff_dtt + 1 < current_elem.distance_to_top &&
|
||
config.getRadius(resulting_eff_dtt + 1, current_elem.elephant_foot_increases) <= current_ceil_radius &&
|
||
config.getRadius(resulting_eff_dtt + 1, current_elem.elephant_foot_increases) <= support_element_radius(config, current_elem))
|
||
++ resulting_eff_dtt;
|
||
current_elem.effective_radius_height = resulting_eff_dtt;
|
||
}
|
||
radius = support_element_collision_radius(config, current_elem);
|
||
|
||
const coord_t foot_radius_increase = std::max(config.bp_radius_increase_per_layer - config.branch_radius_increase_per_layer, 0.0);
|
||
// Is nearly all of the time 1, but sometimes an increase of 1 could cause the radius to become bigger than recommendedMinRadius,
|
||
// which could cause the radius to become bigger than precalculated.
|
||
double planned_foot_increase = std::min(1.0, double(config.recommendedMinRadius(layer_idx - 1) - support_element_radius(config, current_elem)) / foot_radius_increase);
|
||
//FIXME
|
||
bool increase_bp_foot = planned_foot_increase > 0 && current_elem.to_buildplate;
|
||
// bool increase_bp_foot = false;
|
||
|
||
if (increase_bp_foot && support_element_radius(config, current_elem) >= config.branch_radius && support_element_radius(config, current_elem) >= config.increase_radius_until_radius)
|
||
if (validWithRadius(config.getRadius(current_elem.effective_radius_height, current_elem.elephant_foot_increases + planned_foot_increase))) {
|
||
current_elem.elephant_foot_increases += planned_foot_increase;
|
||
radius = support_element_collision_radius(config, current_elem);
|
||
}
|
||
|
||
if (ceil_radius_before != volumes.ceilRadius(radius, settings.use_min_distance)) {
|
||
if (current_elem.to_buildplate)
|
||
to_bp_data = safe_union(diff_clipped(increased, volumes.getAvoidance(radius, layer_idx - 1, settings.type, false, settings.use_min_distance)));
|
||
if (config.support_rests_on_model && (!current_elem.to_buildplate || mergelayer))
|
||
to_model_data = safe_union(diff_clipped(increased,
|
||
current_elem.to_model_gracious ?
|
||
volumes.getAvoidance(radius, layer_idx - 1, settings.type, true, settings.use_min_distance) :
|
||
volumes.getCollision(radius, layer_idx - 1, settings.use_min_distance)
|
||
));
|
||
check_layer_data = current_elem.to_buildplate ? to_bp_data : to_model_data;
|
||
if (area(check_layer_data) < tiny_area_threshold) {
|
||
BOOST_LOG_TRIVIAL(error) << "Lost area by doing catch up from " << ceil_radius_before << " to radius " <<
|
||
volumes.ceilRadius(support_element_collision_radius(config, current_elem), settings.use_min_distance);
|
||
tree_supports_show_error("Area lost catching up radius. May not cause visible malformation."sv, true);
|
||
}
|
||
}
|
||
}
|
||
|
||
return area(check_layer_data) > tiny_area_threshold ? std::optional<SupportElementState>(current_elem) : std::optional<SupportElementState>();
|
||
}
|
||
|
||
struct SupportElementInfluenceAreas {
|
||
// All influence areas: both to build plate and model.
|
||
Polygons influence_areas;
|
||
// Influence areas just to build plate.
|
||
Polygons to_bp_areas;
|
||
// Influence areas just to model.
|
||
Polygons to_model_areas;
|
||
|
||
void clear() {
|
||
this->influence_areas.clear();
|
||
this->to_bp_areas.clear();
|
||
this->to_model_areas.clear();
|
||
}
|
||
};
|
||
|
||
struct SupportElementMerging {
|
||
SupportElementState state;
|
||
/*!
|
||
* \brief All elements in the layer above the current one that are supported by this element
|
||
*/
|
||
SupportElement::ParentIndices parents;
|
||
|
||
SupportElementInfluenceAreas areas;
|
||
// Bounding box of all influence areas.
|
||
Eigen::AlignedBox<coord_t, 2> bbox_data;
|
||
|
||
const Eigen::AlignedBox<coord_t, 2>& bbox() const { return bbox_data;}
|
||
const Point centroid() const { return (bbox_data.min() + bbox_data.max()) / 2; }
|
||
void set_bbox(const BoundingBox& abbox)
|
||
{ Point eps { coord_t(SCALED_EPSILON), coord_t(SCALED_EPSILON) }; bbox_data = { abbox.min - eps, abbox.max + eps }; }
|
||
|
||
// Called by the AABBTree builder to get an index into the vector of source elements.
|
||
// Not needed, thus zero is returned.
|
||
static size_t idx() { return 0; }
|
||
};
|
||
|
||
/*!
|
||
* \brief Increases influence areas as far as required.
|
||
*
|
||
* Calculates influence areas of the layer below, based on the influence areas of the current layer.
|
||
* Increases every influence area by maximum_move_distance_slow. If this is not enough, as in it would change the gracious or to_buildplate status, the influence areas are instead increased by maximum_move_distance.
|
||
* Also ensures that increasing the radius of a branch, does not cause it to change its status (like to_buildplate ). If this were the case, the radius is not increased instead.
|
||
*
|
||
* Warning: The used format inside this is different as the SupportElement does not have a valid area member. Instead this area is saved as value of the dictionary. This was done to avoid not needed heap allocations.
|
||
*
|
||
* \param to_bp_areas[out] Influence areas that can reach the buildplate
|
||
* \param to_model_areas[out] Influence areas that do not have to reach the buildplate. This has overlap with new_layer_data, as areas that can reach the buildplate are also considered valid areas to the model.
|
||
* This redundancy is required if a to_buildplate influence area is allowed to merge with a to model influence area.
|
||
* \param influence_areas[out] Area than can reach all further up support points. No assurance is made that the buildplate or the model can be reached in accordance to the user-supplied settings.
|
||
* \param bypass_merge_areas[out] Influence areas ready to be added to the layer below that do not need merging.
|
||
* \param last_layer[in] Influence areas of the current layer.
|
||
* \param layer_idx[in] Number of the current layer.
|
||
* \param mergelayer[in] Will the merge method be called on this layer. This information is required as some calculation can be avoided if they are not required for merging.
|
||
*/
|
||
static void increase_areas_one_layer(
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
// New areas at the layer below layer_idx
|
||
std::vector<SupportElementMerging> &merging_areas,
|
||
// Layer above merging_areas.
|
||
const LayerIndex layer_idx,
|
||
// Layer elements above merging_areas.
|
||
SupportElements &layer_elements,
|
||
// If false, the merging_areas will not be merged for performance reasons.
|
||
const bool mergelayer,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
using AvoidanceType = TreeModelVolumes::AvoidanceType;
|
||
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, merging_areas.size(), 1),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t merging_area_idx = range.begin(); merging_area_idx < range.end(); ++ merging_area_idx) {
|
||
SupportElementMerging &merging_area = merging_areas[merging_area_idx];
|
||
assert(merging_area.parents.size() == 1);
|
||
SupportElement &parent = layer_elements[merging_area.parents.front()];
|
||
SupportElementState elem = SupportElementState::propagate_down(parent.state);
|
||
const Polygons &wall_restriction =
|
||
// Abstract representation of the model outline. If an influence area would move through it, it could teleport through a wall.
|
||
volumes.getWallRestriction(support_element_collision_radius(config, parent.state), layer_idx, parent.state.use_min_xy_dist);
|
||
|
||
#ifdef TREESUPPORT_DEBUG_SVG
|
||
SVG::export_expolygons(debug_out_path("treesupport-increase_areas_one_layer-%d-%ld.svg", layer_idx, int(merging_area_idx)),
|
||
{ { { union_ex(wall_restriction) }, { "wall_restricrictions", "gray", 0.5f } },
|
||
{ { union_ex(parent.influence_area) }, { "parent", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
#endif // TREESUPPORT_DEBUG_SVG
|
||
|
||
Polygons to_bp_data, to_model_data;
|
||
coord_t radius = support_element_collision_radius(config, elem);
|
||
|
||
// When the radius increases, the outer "support wall" of the branch will have been moved farther away from the center (as this is the definition of radius).
|
||
// As it is not specified that the support_tree_angle has to be one of the center of the branch, it is here seen as the smaller angle of the outer wall of the branch, to the outer wall of the same branch one layer above.
|
||
// As the branch may have become larger the distance between these 2 walls is smaller than the distance of the center points.
|
||
// These extra distance is added to the movement distance possible for this layer.
|
||
|
||
coord_t extra_speed = 5; // The extra speed is added to both movement distances. Also move 5 microns faster than allowed to avoid rounding errors, this may cause issues at VERY VERY small layer heights.
|
||
coord_t extra_slow_speed = 0; // Only added to the slow movement distance.
|
||
const coord_t ceiled_parent_radius = volumes.ceilRadius(support_element_collision_radius(config, parent.state), parent.state.use_min_xy_dist);
|
||
coord_t projected_radius_increased = config.getRadius(parent.state.effective_radius_height + 1, parent.state.elephant_foot_increases);
|
||
coord_t projected_radius_delta = projected_radius_increased - support_element_collision_radius(config, parent.state);
|
||
|
||
// When z distance is more than one layer up and down the Collision used to calculate the wall restriction will always include the wall (and not just the xy_min_distance) of the layer above and below like this (d = blocked area because of z distance):
|
||
/*
|
||
* layer z+1:dddddiiiiiioooo
|
||
* layer z+0:xxxxxdddddddddd
|
||
* layer z-1:dddddxxxxxxxxxx
|
||
* For more detailed visualisation see calculateWallRestrictions
|
||
*/
|
||
const coord_t safe_movement_distance =
|
||
(elem.use_min_xy_dist ? config.xy_min_distance : config.xy_distance) +
|
||
(std::min(config.z_distance_top_layers, config.z_distance_bottom_layers) > 0 ? config.min_feature_size : 0);
|
||
if (ceiled_parent_radius == volumes.ceilRadius(projected_radius_increased, parent.state.use_min_xy_dist) ||
|
||
projected_radius_increased < config.increase_radius_until_radius)
|
||
// If it is guaranteed possible to increase the radius, the maximum movement speed can be increased, as it is assumed that the maximum movement speed is the one of the slower moving wall
|
||
extra_speed += projected_radius_delta;
|
||
else
|
||
// if a guaranteed radius increase is not possible, only increase the slow speed
|
||
// Ensure that the slow movement distance can not become larger than the fast one.
|
||
extra_slow_speed += std::min(projected_radius_delta, (config.maximum_move_distance + extra_speed) - (config.maximum_move_distance_slow + extra_slow_speed));
|
||
|
||
if (config.layer_start_bp_radius > layer_idx &&
|
||
config.recommendedMinRadius(layer_idx - 1) < config.getRadius(elem.effective_radius_height + 1, elem.elephant_foot_increases)) {
|
||
// can guarantee elephant foot radius increase
|
||
if (ceiled_parent_radius == volumes.ceilRadius(config.getRadius(parent.state.effective_radius_height + 1, parent.state.elephant_foot_increases + 1), parent.state.use_min_xy_dist))
|
||
extra_speed += config.bp_radius_increase_per_layer;
|
||
else
|
||
extra_slow_speed += std::min(coord_t(config.bp_radius_increase_per_layer),
|
||
config.maximum_move_distance - (config.maximum_move_distance_slow + extra_slow_speed));
|
||
}
|
||
|
||
const coord_t fast_speed = config.maximum_move_distance + extra_speed;
|
||
const coord_t slow_speed = config.maximum_move_distance_slow + extra_speed + extra_slow_speed;
|
||
|
||
Polygons offset_slow, offset_fast;
|
||
|
||
bool add = false;
|
||
bool bypass_merge = false;
|
||
constexpr bool increase_radius = true, no_error = true, use_min_radius = true, move = true; // aliases for better readability
|
||
|
||
// Determine in which order configurations are checked if they result in a valid influence area. Check will stop if a valid area is found
|
||
std::vector<AreaIncreaseSettings> order;
|
||
auto insertSetting = [&](AreaIncreaseSettings settings, bool back) {
|
||
if (std::find(order.begin(), order.end(), settings) == order.end()) {
|
||
if (back)
|
||
order.emplace_back(settings);
|
||
else
|
||
order.insert(order.begin(), settings);
|
||
}
|
||
};
|
||
|
||
const bool parent_moved_slow = elem.last_area_increase.increase_speed < config.maximum_move_distance;
|
||
const bool avoidance_speed_mismatch = parent_moved_slow && elem.last_area_increase.type != AvoidanceType::Slow;
|
||
if (elem.last_area_increase.move && elem.last_area_increase.no_error && elem.can_use_safe_radius && !mergelayer &&
|
||
!avoidance_speed_mismatch && (elem.distance_to_top >= config.tip_layers || parent_moved_slow)) {
|
||
// assume that the avoidance type that was best for the parent is best for me. Makes this function about 7% faster.
|
||
insertSetting({ elem.last_area_increase.type, elem.last_area_increase.increase_speed < config.maximum_move_distance ? slow_speed : fast_speed,
|
||
increase_radius, elem.last_area_increase.no_error, !use_min_radius, elem.last_area_increase.move }, true);
|
||
insertSetting({ elem.last_area_increase.type, elem.last_area_increase.increase_speed < config.maximum_move_distance ? slow_speed : fast_speed,
|
||
!increase_radius, elem.last_area_increase.no_error, !use_min_radius, elem.last_area_increase.move }, true);
|
||
}
|
||
// branch may still go though a hole, so a check has to be done whether the hole was already passed, and the regular avoidance can be used.
|
||
if (!elem.can_use_safe_radius) {
|
||
// if the radius until which it is always increased can not be guaranteed, move fast. This is to avoid holes smaller than the real branch radius.
|
||
// This does not guarantee the avoidance of such holes, but ensures they are avoided if possible.
|
||
// order.emplace_back(AvoidanceType::Slow,!increase_radius,no_error,!use_min_radius,move);
|
||
insertSetting({ AvoidanceType::Slow, slow_speed, increase_radius, no_error, !use_min_radius, !move }, true); // did we go through the hole
|
||
// in many cases the definition of hole is overly restrictive, so to avoid unnecessary fast movement in the tip, it is ignored there for a bit.
|
||
// This CAN cause a branch to go though a hole it otherwise may have avoided.
|
||
if (elem.distance_to_top < round_up_divide(config.tip_layers, size_t(2)))
|
||
insertSetting({ AvoidanceType::Fast, slow_speed, increase_radius, no_error, !use_min_radius, !move }, true);
|
||
insertSetting({ AvoidanceType::FastSafe, fast_speed, increase_radius, no_error, !use_min_radius, !move }, true); // did we manage to avoid the hole
|
||
insertSetting({ AvoidanceType::FastSafe, fast_speed, !increase_radius, no_error, !use_min_radius, move }, true);
|
||
insertSetting({ AvoidanceType::Fast, fast_speed, !increase_radius, no_error, !use_min_radius, move }, true);
|
||
} else {
|
||
insertSetting({ AvoidanceType::Slow, slow_speed, increase_radius, no_error, !use_min_radius, move }, true);
|
||
// while moving fast to be able to increase the radius (b) may seems preferable (over a) this can cause the a sudden skip in movement,
|
||
// which looks similar to a layer shift and can reduce stability.
|
||
// as such idx have chosen to only use the user setting for radius increases as a friendly recommendation.
|
||
insertSetting({ AvoidanceType::Slow, slow_speed, !increase_radius, no_error, !use_min_radius, move }, true); // a
|
||
if (elem.distance_to_top < config.tip_layers)
|
||
insertSetting({ AvoidanceType::FastSafe, slow_speed, increase_radius, no_error, !use_min_radius, move }, true);
|
||
insertSetting({ AvoidanceType::FastSafe, fast_speed, increase_radius, no_error, !use_min_radius, move }, true); // b
|
||
insertSetting({ AvoidanceType::FastSafe, fast_speed, !increase_radius, no_error, !use_min_radius, move }, true);
|
||
}
|
||
|
||
if (elem.use_min_xy_dist) {
|
||
std::vector<AreaIncreaseSettings> new_order;
|
||
// if the branch currently has to use min_xy_dist check if the configuration would also be valid
|
||
// with the regular xy_distance before checking with use_min_radius (Only happens when Support Distance priority is z overrides xy )
|
||
for (AreaIncreaseSettings settings : order) {
|
||
new_order.emplace_back(settings);
|
||
new_order.push_back({ settings.type, settings.increase_speed, settings.increase_radius, settings.no_error, use_min_radius, settings.move });
|
||
}
|
||
order = new_order;
|
||
}
|
||
if (elem.to_buildplate || (elem.to_model_gracious && intersection(parent.influence_area, volumes.getPlaceableAreas(radius, layer_idx, throw_on_cancel)).empty())) {
|
||
// error case
|
||
// it is normal that we wont be able to find a new area at some point in time if we wont be able to reach layer 0 aka have to connect with the model
|
||
insertSetting({ AvoidanceType::Fast, fast_speed, !increase_radius, !no_error, elem.use_min_xy_dist, move }, true);
|
||
}
|
||
if (elem.distance_to_top < elem.dont_move_until && elem.can_use_safe_radius) // only do not move when holes would be avoided in every case.
|
||
// Only do not move when already in a no hole avoidance with the regular xy distance.
|
||
insertSetting({ AvoidanceType::Slow, 0, increase_radius, no_error, !use_min_radius, !move }, false);
|
||
|
||
Polygons inc_wo_collision;
|
||
// Check whether it is faster to calculate the area increased with the fast speed independently from the slow area, or time could be saved by reusing the slow area to calculate the fast one.
|
||
// Calculated by comparing the steps saved when calcualting idependently with the saved steps when not.
|
||
bool offset_independant_faster = radius / safe_movement_distance - int(config.maximum_move_distance + extra_speed < radius + safe_movement_distance) >
|
||
round_up_divide((extra_speed + extra_slow_speed + config.maximum_move_distance_slow), safe_movement_distance);
|
||
for (const AreaIncreaseSettings &settings : order) {
|
||
if (settings.move) {
|
||
if (offset_slow.empty() && (settings.increase_speed == slow_speed || ! offset_independant_faster)) {
|
||
// offsetting in 2 steps makes our offsetted area rounder preventing (rounding) errors created by to pointy areas. At this point one can see that the Polygons class
|
||
// was never made for precision in the single digit micron range.
|
||
offset_slow = safe_offset_inc(parent.influence_area, extra_speed + extra_slow_speed + config.maximum_move_distance_slow,
|
||
wall_restriction, safe_movement_distance, offset_independant_faster ? safe_movement_distance + radius : 0, 2);
|
||
#ifdef TREESUPPORT_DEBUG_SVG
|
||
SVG::export_expolygons(debug_out_path("treesupport-increase_areas_one_layer-slow-%d-%ld.svg", layer_idx, int(merging_area_idx)),
|
||
{ { { union_ex(wall_restriction) }, { "wall_restricrictions", "gray", 0.5f } },
|
||
{ { union_ex(offset_slow) }, { "offset_slow", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
#endif // TREESUPPORT_DEBUG_SVG
|
||
}
|
||
if (offset_fast.empty() && settings.increase_speed != slow_speed) {
|
||
if (offset_independant_faster)
|
||
offset_fast = safe_offset_inc(parent.influence_area, extra_speed + config.maximum_move_distance,
|
||
wall_restriction, safe_movement_distance, offset_independant_faster ? safe_movement_distance + radius : 0, 1);
|
||
else {
|
||
const coord_t delta_slow_fast = config.maximum_move_distance - (config.maximum_move_distance_slow + extra_slow_speed);
|
||
offset_fast = safe_offset_inc(offset_slow, delta_slow_fast, wall_restriction, safe_movement_distance, safe_movement_distance + radius, offset_independant_faster ? 2 : 1);
|
||
}
|
||
#ifdef TREESUPPORT_DEBUG_SVG
|
||
SVG::export_expolygons(debug_out_path("treesupport-increase_areas_one_layer-fast-%d-%ld.svg", layer_idx, int(merging_area_idx)),
|
||
{ { { union_ex(wall_restriction) }, { "wall_restricrictions", "gray", 0.5f } },
|
||
{ { union_ex(offset_fast) }, { "offset_fast", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
#endif // TREESUPPORT_DEBUG_SVG
|
||
}
|
||
}
|
||
std::optional<SupportElementState> result;
|
||
inc_wo_collision.clear();
|
||
if (!settings.no_error) {
|
||
// ERROR CASE
|
||
// if the area becomes for whatever reason something that clipper sees as a line, offset would stop working, so ensure that even if if wrongly would be a line, it still actually has an area that can be increased
|
||
Polygons lines_offset = offset(to_polylines(parent.influence_area), scaled<float>(0.005), jtMiter, 1.2);
|
||
Polygons base_error_area = union_(parent.influence_area, lines_offset);
|
||
result = increase_single_area(volumes, config, settings, layer_idx, parent,
|
||
base_error_area, to_bp_data, to_model_data, inc_wo_collision, (config.maximum_move_distance + extra_speed) * 1.5, mergelayer);
|
||
#ifdef TREE_SUPPORT_SHOW_ERRORS
|
||
BOOST_LOG_TRIVIAL(error)
|
||
#else // TREE_SUPPORT_SHOW_ERRORS
|
||
BOOST_LOG_TRIVIAL(warning)
|
||
#endif // TREE_SUPPORT_SHOW_ERRORS
|
||
<< "Influence area could not be increased! Data about the Influence area: "
|
||
"Radius: " << radius << " at layer: " << layer_idx - 1 << " NextTarget: " << elem.layer_idx << " Distance to top: " << elem.distance_to_top <<
|
||
" Elephant foot increases " << elem.elephant_foot_increases << " use_min_xy_dist " << elem.use_min_xy_dist << " to buildplate " << elem.to_buildplate <<
|
||
" gracious " << elem.to_model_gracious << " safe " << elem.can_use_safe_radius << " until move " << elem.dont_move_until << " \n "
|
||
"Parent " << &parent << ": Radius: " << support_element_collision_radius(config, parent.state) << " at layer: " << layer_idx << " NextTarget: " << parent.state.layer_idx <<
|
||
" Distance to top: " << parent.state.distance_to_top << " Elephant foot increases " << parent.state.elephant_foot_increases << " use_min_xy_dist " << parent.state.use_min_xy_dist <<
|
||
" to buildplate " << parent.state.to_buildplate << " gracious " << parent.state.to_model_gracious << " safe " << parent.state.can_use_safe_radius << " until move " << parent.state.dont_move_until;
|
||
tree_supports_show_error("Potentially lost branch!"sv, true);
|
||
} else
|
||
result = increase_single_area(volumes, config, settings, layer_idx, parent,
|
||
settings.increase_speed == slow_speed ? offset_slow : offset_fast, to_bp_data, to_model_data, inc_wo_collision, 0, mergelayer);
|
||
|
||
if (result) {
|
||
elem = *result;
|
||
radius = support_element_collision_radius(config, elem);
|
||
elem.last_area_increase = settings;
|
||
add = true;
|
||
// do not merge if the branch should not move or the priority has to be to get farther away from the model.
|
||
bypass_merge = !settings.move || (settings.use_min_distance && elem.distance_to_top < config.tip_layers);
|
||
if (settings.move)
|
||
elem.dont_move_until = 0;
|
||
else
|
||
elem.result_on_layer = parent.state.result_on_layer;
|
||
|
||
elem.can_use_safe_radius = settings.type != AvoidanceType::Fast;
|
||
|
||
if (!settings.use_min_distance)
|
||
elem.use_min_xy_dist = false;
|
||
if (!settings.no_error)
|
||
#ifdef TREE_SUPPORT_SHOW_ERRORS
|
||
BOOST_LOG_TRIVIAL(error)
|
||
#else // TREE_SUPPORT_SHOW_ERRORS
|
||
BOOST_LOG_TRIVIAL(info)
|
||
#endif // TREE_SUPPORT_SHOW_ERRORS
|
||
<< "Trying to keep area by moving faster than intended: Success";
|
||
break;
|
||
} else if (!settings.no_error)
|
||
BOOST_LOG_TRIVIAL(error) << "Trying to keep area by moving faster than intended: FAILURE! WRONG BRANCHES LIKLY!";
|
||
}
|
||
|
||
if (add) {
|
||
// Union seems useless, but some rounding errors somewhere can cause to_bp_data to be slightly bigger than it should be.
|
||
assert(! inc_wo_collision.empty() || ! to_bp_data.empty() || ! to_model_data.empty());
|
||
Polygons max_influence_area = safe_union(
|
||
diff_clipped(inc_wo_collision, volumes.getCollision(radius, layer_idx - 1, elem.use_min_xy_dist)),
|
||
safe_union(to_bp_data, to_model_data));
|
||
merging_area.state = elem;
|
||
assert(!max_influence_area.empty());
|
||
merging_area.set_bbox(get_extents(max_influence_area));
|
||
merging_area.areas.influence_areas = std::move(max_influence_area);
|
||
if (! bypass_merge) {
|
||
if (elem.to_buildplate)
|
||
merging_area.areas.to_bp_areas = std::move(to_bp_data);
|
||
if (config.support_rests_on_model)
|
||
merging_area.areas.to_model_areas = std::move(to_model_data);
|
||
}
|
||
} else {
|
||
// If the bottom most point of a branch is set, later functions will assume that the position is valid, and ignore it.
|
||
// But as branches connecting with the model that are to small have to be culled, the bottom most point has to be not set.
|
||
// A point can be set on the top most tip layer (maybe more if it should not move for a few layers).
|
||
parent.state.result_on_layer_reset();
|
||
parent.state.to_model_gracious = false;
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
}, tbb::simple_partitioner());
|
||
}
|
||
|
||
[[nodiscard]] static SupportElementState merge_support_element_states(
|
||
const SupportElementState &first, const SupportElementState &second, const Point &next_position, const coord_t layer_idx,
|
||
const TreeSupportSettings &config)
|
||
{
|
||
SupportElementState out;
|
||
out.next_position = next_position;
|
||
out.layer_idx = layer_idx;
|
||
out.use_min_xy_dist = first.use_min_xy_dist || second.use_min_xy_dist;
|
||
out.supports_roof = first.supports_roof || second.supports_roof;
|
||
out.dont_move_until = std::max(first.dont_move_until, second.dont_move_until);
|
||
out.can_use_safe_radius = first.can_use_safe_radius || second.can_use_safe_radius;
|
||
out.missing_roof_layers = std::min(first.missing_roof_layers, second.missing_roof_layers);
|
||
out.skip_ovalisation = false;
|
||
if (first.target_height > second.target_height) {
|
||
out.target_height = first.target_height;
|
||
out.target_position = first.target_position;
|
||
} else {
|
||
out.target_height = second.target_height;
|
||
out.target_position = second.target_position;
|
||
}
|
||
out.effective_radius_height = std::max(first.effective_radius_height, second.effective_radius_height);
|
||
out.distance_to_top = std::max(first.distance_to_top, second.distance_to_top);
|
||
|
||
out.to_buildplate = first.to_buildplate && second.to_buildplate;
|
||
out.to_model_gracious = first.to_model_gracious && second.to_model_gracious; // valid as we do not merge non-gracious with gracious
|
||
|
||
out.elephant_foot_increases = 0;
|
||
if (config.bp_radius_increase_per_layer > 0) {
|
||
coord_t foot_increase_radius = std::abs(std::max(support_element_collision_radius(config, second), support_element_collision_radius(config, first)) - support_element_collision_radius(config, out));
|
||
// elephant_foot_increases has to be recalculated, as when a smaller tree with a larger elephant_foot_increases merge with a larger branch
|
||
// the elephant_foot_increases may have to be lower as otherwise the radius suddenly increases. This results often in a non integer value.
|
||
out.elephant_foot_increases = foot_increase_radius / (config.bp_radius_increase_per_layer - config.branch_radius_increase_per_layer);
|
||
}
|
||
|
||
// set last settings to the best out of both parents. If this is wrong, it will only cause a small performance penalty instead of weird behavior.
|
||
out.last_area_increase = {
|
||
std::min(first.last_area_increase.type, second.last_area_increase.type),
|
||
std::min(first.last_area_increase.increase_speed, second.last_area_increase.increase_speed),
|
||
first.last_area_increase.increase_radius || second.last_area_increase.increase_radius,
|
||
first.last_area_increase.no_error || second.last_area_increase.no_error,
|
||
first.last_area_increase.use_min_distance && second.last_area_increase.use_min_distance,
|
||
first.last_area_increase.move || second.last_area_increase.move };
|
||
|
||
return out;
|
||
}
|
||
|
||
static bool merge_influence_areas_two_elements(
|
||
const TreeModelVolumes &volumes, const TreeSupportSettings &config, const LayerIndex layer_idx,
|
||
SupportElementMerging &dst, SupportElementMerging &src)
|
||
{
|
||
// Don't merge gracious with a non gracious area as bad placement could negatively impact reliability of the whole subtree.
|
||
const bool merging_gracious_and_non_gracious = dst.state.to_model_gracious != src.state.to_model_gracious;
|
||
// Could cause some issues with the increase of one area, as it is assumed that if the smaller is increased
|
||
// by the delta to the larger it is engulfed by it already. But because a different collision
|
||
// may be removed from the in draw_area() generated circles, this assumption could be wrong.
|
||
const bool merging_min_and_regular_xy = dst.state.use_min_xy_dist != src.state.use_min_xy_dist;
|
||
|
||
if (merging_gracious_and_non_gracious || merging_min_and_regular_xy)
|
||
return false;
|
||
|
||
const bool dst_radius_bigger = support_element_collision_radius(config, dst.state) > support_element_collision_radius(config, src.state);
|
||
const SupportElementMerging &smaller_rad = dst_radius_bigger ? src : dst;
|
||
const SupportElementMerging &bigger_rad = dst_radius_bigger ? dst : src;
|
||
const coord_t real_radius_delta = std::abs(support_element_radius(config, bigger_rad.state) - support_element_radius(config, smaller_rad.state));
|
||
{
|
||
// Testing intersection of bounding boxes.
|
||
// Expand the smaller radius branch bounding box to match the lambda intersect_small_with_bigger() below.
|
||
// Because the lambda intersect_small_with_bigger() applies a rounded offset, a snug offset of the bounding box
|
||
// is sufficient. On the other side, if a mitered offset was used by the lambda,
|
||
// the bounding box expansion would have to account for the mitered extension of the sharp corners.
|
||
Eigen::AlignedBox<coord_t, 2> smaller_bbox = smaller_rad.bbox();
|
||
smaller_bbox.min() -= Point{ real_radius_delta, real_radius_delta };
|
||
smaller_bbox.max() += Point{ real_radius_delta, real_radius_delta };
|
||
if (! smaller_bbox.intersects(bigger_rad.bbox()))
|
||
return false;
|
||
}
|
||
|
||
// Accumulator of a radius increase of a "to model" branch by merging in a "to build plate" branch.
|
||
coord_t increased_to_model_radius = 0;
|
||
const bool merging_to_bp = dst.state.to_buildplate && src.state.to_buildplate;
|
||
if (! merging_to_bp) {
|
||
// Get the real radius increase as the user does not care for the collision model.
|
||
if (dst.state.to_buildplate != src.state.to_buildplate) {
|
||
// Merging a "to build plate" branch with a "to model" branch.
|
||
// Don't allow merging a thick "to build plate" branch into a thinner "to model" branch.
|
||
const coord_t rdst = support_element_radius(config, dst.state);
|
||
const coord_t rsrc = support_element_radius(config, src.state);
|
||
if (dst.state.to_buildplate) {
|
||
if (rsrc < rdst)
|
||
increased_to_model_radius = src.state.increased_to_model_radius + rdst - rsrc;
|
||
} else {
|
||
if (rsrc > rdst)
|
||
increased_to_model_radius = dst.state.increased_to_model_radius + rsrc - rdst;
|
||
}
|
||
if (increased_to_model_radius > config.max_to_model_radius_increase)
|
||
return false;
|
||
}
|
||
// if a merge could place a stable branch on unstable ground, would be increasing the radius further
|
||
// than allowed to when merging to model and to_bp trees or would merge to model before it is known
|
||
// they will even been drawn the merge is skipped
|
||
if (! dst.state.supports_roof && ! src.state.supports_roof &&
|
||
std::max(src.state.distance_to_top, dst.state.distance_to_top) < config.min_dtt_to_model)
|
||
return false;
|
||
}
|
||
|
||
// Area of the bigger radius is used to ensure correct placement regarding the relevant avoidance,
|
||
// so if that would change an invalid area may be created.
|
||
if (! bigger_rad.state.can_use_safe_radius && smaller_rad.state.can_use_safe_radius)
|
||
return false;
|
||
|
||
// the bigger radius is used to verify that the area is still valid after the increase with the delta.
|
||
// If there were a point where the big influence area could be valid with can_use_safe_radius
|
||
// the element would already be can_use_safe_radius.
|
||
// the smaller radius, which gets increased by delta may reach into the area where use_min_xy_dist is no longer required.
|
||
const bool use_min_radius = bigger_rad.state.use_min_xy_dist && smaller_rad.state.use_min_xy_dist;
|
||
|
||
// The idea is that the influence area with the smaller collision radius is increased by the radius difference.
|
||
// If this area has any intersections with the influence area of the larger collision radius, a branch (of the larger collision radius) placed in this intersection, has already engulfed the branch of the smaller collision radius.
|
||
// Because of this a merge may happen even if the influence areas (that represent possible center points of branches) do not intersect yet.
|
||
// Remember that collision radius <= real radius as otherwise this assumption would be false.
|
||
const coord_t smaller_collision_radius = support_element_collision_radius(config, smaller_rad.state);
|
||
const Polygons &collision = volumes.getCollision(smaller_collision_radius, layer_idx - 1, use_min_radius);
|
||
auto intersect_small_with_bigger = [real_radius_delta, smaller_collision_radius, &collision, &config](const Polygons &small, const Polygons &bigger) {
|
||
return intersection(
|
||
safe_offset_inc(
|
||
small, real_radius_delta, collision,
|
||
// -3 avoids possible rounding errors
|
||
2 * (config.xy_distance + smaller_collision_radius - 3), 0, 0),
|
||
bigger);
|
||
};
|
||
Polygons intersect = intersect_small_with_bigger(
|
||
merging_to_bp ? smaller_rad.areas.to_bp_areas : smaller_rad.areas.to_model_areas,
|
||
merging_to_bp ? bigger_rad.areas.to_bp_areas : bigger_rad.areas.to_model_areas);
|
||
|
||
// dont use empty as a line is not empty, but for this use-case it very well may be (and would be one layer down as union does not keep lines)
|
||
// check if the overlap is large enough (Small ares tend to attract rounding errors in clipper).
|
||
if (area(intersect) <= tiny_area_threshold)
|
||
return false;
|
||
|
||
// While 0.025 was guessed as enough, i did not have reason to change it.
|
||
if (area(offset(intersect, scaled<float>(-0.025), jtMiter, 1.2)) <= tiny_area_threshold)
|
||
return false;
|
||
|
||
// Do the actual merge now that the branches are confirmed to be able to intersect.
|
||
// calculate which point is closest to the point of the last merge (or tip center if no merge above it has happened)
|
||
// used at the end to estimate where to best place the branch on the bottom most layer
|
||
// could be replaced with a random point inside the new area
|
||
Point new_pos = move_inside_if_outside(intersect, dst.state.next_position);
|
||
|
||
SupportElementState new_state = merge_support_element_states(dst.state, src.state, new_pos, layer_idx - 1, config);
|
||
new_state.increased_to_model_radius = increased_to_model_radius == 0 ?
|
||
// increased_to_model_radius was not set yet. Propagate maximum.
|
||
std::max(dst.state.increased_to_model_radius, src.state.increased_to_model_radius) :
|
||
increased_to_model_radius;
|
||
|
||
// Rather unioning with "intersect" due to some rounding errors.
|
||
Polygons influence_areas = safe_union(
|
||
intersect_small_with_bigger(smaller_rad.areas.influence_areas, bigger_rad.areas.influence_areas),
|
||
intersect);
|
||
|
||
Polygons to_model_areas;
|
||
if (merging_to_bp && config.support_rests_on_model)
|
||
to_model_areas = new_state.to_model_gracious ?
|
||
// Rather unioning with "intersect" due to some rounding errors.
|
||
safe_union(
|
||
intersect_small_with_bigger(smaller_rad.areas.to_model_areas, bigger_rad.areas.to_model_areas),
|
||
intersect) :
|
||
influence_areas;
|
||
|
||
dst.parents.insert(dst.parents.end(), src.parents.begin(), src.parents.end());
|
||
dst.state = new_state;
|
||
dst.areas.influence_areas = std::move(influence_areas);
|
||
dst.areas.to_bp_areas.clear();
|
||
dst.areas.to_model_areas.clear();
|
||
if (merging_to_bp) {
|
||
dst.areas.to_bp_areas = std::move(intersect);
|
||
if (config.support_rests_on_model)
|
||
dst.areas.to_model_areas = std::move(to_model_areas);
|
||
} else
|
||
dst.areas.to_model_areas = std::move(intersect);
|
||
// Update the bounding box.
|
||
BoundingBox bbox(get_extents(dst.areas.influence_areas));
|
||
bbox.merge(get_extents(dst.areas.to_bp_areas));
|
||
bbox.merge(get_extents(dst.areas.to_model_areas));
|
||
dst.set_bbox(bbox);
|
||
// Clear the source data.
|
||
src.areas.clear();
|
||
src.parents.clear();
|
||
return true;
|
||
}
|
||
|
||
/*!
|
||
* \brief Merges Influence Areas if possible.
|
||
*
|
||
* Branches which do overlap have to be merged. This helper merges all elements in input with the elements into reduced_new_layer.
|
||
* Elements in input_aabb are merged together if possible, while elements reduced_new_layer_aabb are not checked against each other.
|
||
*
|
||
* \param reduced_aabb[in,out] The already processed elements.
|
||
* \param input_aabb[in] Not yet processed elements
|
||
* \param to_bp_areas[in] The Elements of the current Layer that will reach the buildplate. Value is the influence area where the center of a circle of support may be placed.
|
||
* \param to_model_areas[in] The Elements of the current Layer that do not have to reach the buildplate. Also contains main as every element that can reach the buildplate is not forced to.
|
||
* Value is the influence area where the center of a circle of support may be placed.
|
||
* \param influence_areas[in] The influence areas without avoidance removed.
|
||
* \param insert_bp_areas[out] Elements to be inserted into the main dictionary after the Helper terminates.
|
||
* \param insert_model_areas[out] Elements to be inserted into the secondary dictionary after the Helper terminates.
|
||
* \param insert_influence[out] Elements to be inserted into the dictionary containing the largest possibly valid influence area (ignoring if the area may not be there because of avoidance)
|
||
* \param erase[out] Elements that should be deleted from the above dictionaries.
|
||
* \param layer_idx[in] The Index of the current Layer.
|
||
*/
|
||
|
||
static SupportElementMerging* merge_influence_areas_leaves(
|
||
const TreeModelVolumes &volumes, const TreeSupportSettings &config, const LayerIndex layer_idx,
|
||
SupportElementMerging * const dst_begin, SupportElementMerging *dst_end)
|
||
{
|
||
// Merging at the lowest level of the AABB tree. Checking one against each other, O(n^2).
|
||
assert(dst_begin < dst_end);
|
||
for (SupportElementMerging *i = dst_begin; i + 1 < dst_end;) {
|
||
for (SupportElementMerging *j = i + 1; j != dst_end;)
|
||
if (merge_influence_areas_two_elements(volumes, config, layer_idx, *i, *j)) {
|
||
// i was merged with j, j is empty.
|
||
if (j != -- dst_end)
|
||
*j = std::move(*dst_end);
|
||
goto merged;
|
||
} else
|
||
++ j;
|
||
// not merged
|
||
++ i;
|
||
merged:
|
||
;
|
||
}
|
||
return dst_end;
|
||
}
|
||
|
||
static SupportElementMerging* merge_influence_areas_two_sets(
|
||
const TreeModelVolumes &volumes, const TreeSupportSettings &config, const LayerIndex layer_idx,
|
||
SupportElementMerging * const dst_begin, SupportElementMerging * dst_end,
|
||
SupportElementMerging * src_begin, SupportElementMerging * const src_end)
|
||
{
|
||
// Merging src into dst.
|
||
// Areas of src should not overlap with areas of another elements of src.
|
||
// Areas of dst should not overlap with areas of another elements of dst.
|
||
// The memory from dst_begin to src_end is reserved for the merging operation,
|
||
// src follows dst.
|
||
assert(src_begin < src_end);
|
||
assert(dst_begin < dst_end);
|
||
assert(dst_end <= src_begin);
|
||
for (SupportElementMerging *src = src_begin; src != src_end; ++ src) {
|
||
SupportElementMerging *dst = dst_begin;
|
||
SupportElementMerging *merged = nullptr;
|
||
for (; dst != dst_end; ++ dst)
|
||
if (merge_influence_areas_two_elements(volumes, config, layer_idx, *dst, *src)) {
|
||
merged = dst ++;
|
||
if (src != src_begin)
|
||
// Compactify src.
|
||
*src = std::move(*src_begin);
|
||
++ src_begin;
|
||
break;
|
||
}
|
||
for (; dst != dst_end;)
|
||
if (merge_influence_areas_two_elements(volumes, config, layer_idx, *merged, *dst)) {
|
||
// Compactify dst.
|
||
if (dst != -- dst_end)
|
||
*dst = std::move(*dst_end);
|
||
} else
|
||
++ dst;
|
||
}
|
||
// Compactify src elements that were not merged with dst to the end of dst.
|
||
assert(dst_end <= src_begin);
|
||
if (dst_end == src_begin)
|
||
dst_end = src_end;
|
||
else
|
||
while (src_begin != src_end)
|
||
*dst_end ++ = std::move(*src_begin ++);
|
||
|
||
return dst_end;
|
||
}
|
||
|
||
/*!
|
||
* \brief Merges Influence Areas at one layer if possible.
|
||
*
|
||
* Branches which do overlap have to be merged. This manages the helper and uses a divide and conquer approach to parallelize this problem. This parallelization can at most accelerate the merging by a factor of 2.
|
||
*
|
||
* \param to_bp_areas[in] The Elements of the current Layer that will reach the buildplate.
|
||
* Value is the influence area where the center of a circle of support may be placed.
|
||
* \param to_model_areas[in] The Elements of the current Layer that do not have to reach the buildplate. Also contains main as every element that can reach the buildplate is not forced to.
|
||
* Value is the influence area where the center of a circle of support may be placed.
|
||
* \param influence_areas[in] The Elements of the current Layer without avoidances removed. This is the largest possible influence area for this layer.
|
||
* Value is the influence area where the center of a circle of support may be placed.
|
||
* \param layer_idx[in] The current layer.
|
||
*/
|
||
static void merge_influence_areas(
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const LayerIndex layer_idx,
|
||
std::vector<SupportElementMerging> &influence_areas,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
const size_t input_size = influence_areas.size();
|
||
if (input_size == 0)
|
||
return;
|
||
|
||
// Merging by divide & conquer.
|
||
// The majority of time is consumed by Clipper polygon operations, intersection is accelerated by bounding boxes.
|
||
// Sorting input into an AABB tree helps to perform most of the intersections at first iterations,
|
||
// thus reducing computation when merging larger subtrees.
|
||
// The actual merge logic is found in merge_influence_areas_two_sets.
|
||
|
||
// Build an AABB tree over the influence areas.
|
||
//FIXME A full tree does not need to be built, the lowest level branches will be always bucketed.
|
||
// However the additional time consumed is negligible.
|
||
AABBTreeIndirect::Tree<2, coord_t> tree;
|
||
// Sort influence_areas in place.
|
||
tree.build_modify_input(influence_areas);
|
||
|
||
throw_on_cancel();
|
||
|
||
// Prepare the initial buckets as ranges of influence areas. The initial buckets contain power of 2 influence areas to follow
|
||
// the branching of the AABB tree.
|
||
// Vectors of ranges of influence areas, following the branching of the AABB tree:
|
||
std::vector<std::pair<SupportElementMerging*, SupportElementMerging*>> buckets;
|
||
// Initial number of buckets for 1st round of merging.
|
||
size_t num_buckets_initial;
|
||
{
|
||
// How many buckets per first merge iteration?
|
||
const size_t num_threads = tbb::this_task_arena::max_concurrency();
|
||
// 4 buckets per thread if possible,
|
||
const size_t num_buckets_min = (input_size + 2) / 4;
|
||
// 2 buckets per thread otherwise.
|
||
const size_t num_buckets_max = input_size / 2;
|
||
num_buckets_initial = num_buckets_min >= num_threads ? num_buckets_min : num_buckets_max;
|
||
const size_t bucket_size = num_buckets_min >= num_threads ? 4 : 2;
|
||
// Fill in the buckets.
|
||
SupportElementMerging *it = influence_areas.data();
|
||
// Reserve one more bucket to keep a single influence area which will not be merged in the first iteration.
|
||
buckets.reserve(num_buckets_initial + 1);
|
||
for (size_t i = 0; i < num_buckets_initial; ++ i, it += bucket_size)
|
||
buckets.emplace_back(std::make_pair(it, it + bucket_size));
|
||
SupportElementMerging *it_end = influence_areas.data() + influence_areas.size();
|
||
if (buckets.back().second >= it_end) {
|
||
// Last bucket is less than size 4, but bigger than size 1.
|
||
buckets.back().second = std::min(buckets.back().second, it_end);
|
||
} else {
|
||
// Last bucket is size 1, it will not be merged in the first iteration.
|
||
assert(it + 1 == it_end);
|
||
buckets.emplace_back(std::make_pair(it, it_end));
|
||
}
|
||
}
|
||
|
||
// 1st merge iteration, merge one with each other.
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_buckets_initial),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t idx = range.begin(); idx < range.end(); ++ idx) {
|
||
// Merge bucket_count adjacent to each other, merging uneven bucket numbers into even buckets
|
||
buckets[idx].second = merge_influence_areas_leaves(volumes, config, layer_idx, buckets[idx].first, buckets[idx].second);
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
|
||
// Further merge iterations, merging one AABB subtree with another one, hopefully minimizing intersections between the elements
|
||
// of each of the subtree.
|
||
while (buckets.size() > 1) {
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, buckets.size() / 2),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t idx = range.begin(); idx < range.end(); ++ idx) {
|
||
const size_t bucket_pair_idx = idx * 2;
|
||
// Merge bucket_count adjacent to each other, merging uneven bucket numbers into even buckets
|
||
buckets[bucket_pair_idx].second = merge_influence_areas_two_sets(volumes, config, layer_idx,
|
||
buckets[bucket_pair_idx].first, buckets[bucket_pair_idx].second,
|
||
buckets[bucket_pair_idx + 1].first, buckets[bucket_pair_idx + 1].second);
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
// Remove odd buckets, which were merged into even buckets.
|
||
size_t new_size = (buckets.size() + 1) / 2;
|
||
for (size_t i = 1; i < new_size; ++ i)
|
||
buckets[i] = std::move(buckets[i * 2]);
|
||
buckets.erase(buckets.begin() + new_size, buckets.end());
|
||
}
|
||
}
|
||
|
||
/*!
|
||
* \brief Propagates influence downwards, and merges overlapping ones.
|
||
*
|
||
* \param move_bounds[in,out] All currently existing influence areas
|
||
*/
|
||
void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupportSettings &config, std::vector<SupportElements> &move_bounds, std::function<void()> throw_on_cancel)
|
||
{
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
const double data_size_inverse = 1 / double(move_bounds.size());
|
||
double progress_total = TREE_PROGRESS_PRECALC_AVO + TREE_PROGRESS_PRECALC_COLL + TREE_PROGRESS_GENERATE_NODES;
|
||
#endif // SLIC3R_TREESUPPORTS_PROGRESS
|
||
|
||
auto dur_inc = std::chrono::duration_values<std::chrono::nanoseconds>::zero();
|
||
auto dur_total = std::chrono::duration_values<std::chrono::nanoseconds>::zero();
|
||
|
||
LayerIndex last_merge_layer_idx = move_bounds.size();
|
||
bool new_element = false;
|
||
|
||
// Ensures at least one merge operation per 3mm height, 50 layers, 1 mm movement of slow speed or 5mm movement of fast speed (whatever is lowest). Values were guessed.
|
||
size_t max_merge_every_x_layers = std::min(std::min(5000 / (std::max(config.maximum_move_distance, coord_t(100))), 1000 / std::max(config.maximum_move_distance_slow, coord_t(20))), 3000 / config.layer_height);
|
||
size_t merge_every_x_layers = 1;
|
||
// Calculate the influence areas for each layer below (Top down)
|
||
// This is done by first increasing the influence area by the allowed movement distance, and merging them with other influence areas if possible
|
||
for (int layer_idx = int(move_bounds.size()) - 1; layer_idx > 0; -- layer_idx)
|
||
if (SupportElements &prev_layer = move_bounds[layer_idx]; ! prev_layer.empty()) {
|
||
// merging is expensive and only parallelized to a max speedup of 2. As such it may be useful in some cases to only merge every few layers to improve performance.
|
||
bool had_new_element = new_element;
|
||
const bool merge_this_layer = had_new_element || size_t(last_merge_layer_idx - layer_idx) >= merge_every_x_layers;
|
||
if (had_new_element)
|
||
merge_every_x_layers = 1;
|
||
const auto ta = std::chrono::high_resolution_clock::now();
|
||
|
||
// ### Increase the influence areas by the allowed movement distance
|
||
std::vector<SupportElementMerging> influence_areas;
|
||
influence_areas.reserve(prev_layer.size());
|
||
for (int32_t element_idx = 0; element_idx < int32_t(prev_layer.size()); ++ element_idx) {
|
||
SupportElement &el = prev_layer[element_idx];
|
||
assert(!el.influence_area.empty());
|
||
SupportElement::ParentIndices parents;
|
||
parents.emplace_back(element_idx);
|
||
influence_areas.push_back({ el.state, parents });
|
||
}
|
||
increase_areas_one_layer(volumes, config, influence_areas, layer_idx, prev_layer, merge_this_layer, throw_on_cancel);
|
||
|
||
// Place already fully constructed elements to the output, remove them from influence_areas.
|
||
SupportElements &this_layer = move_bounds[layer_idx - 1];
|
||
influence_areas.erase(std::remove_if(influence_areas.begin(), influence_areas.end(),
|
||
[&this_layer, layer_idx](SupportElementMerging &elem) {
|
||
if (elem.areas.influence_areas.empty())
|
||
// This area was removed completely due to collisions.
|
||
return true;
|
||
if (elem.areas.to_bp_areas.empty() && elem.areas.to_model_areas.empty()) {
|
||
if (area(elem.areas.influence_areas) < tiny_area_threshold) {
|
||
BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area bypass on layer " << layer_idx - 1;
|
||
tree_supports_show_error("Insert error of area after bypassing merge.\n"sv, true);
|
||
}
|
||
// Move the area to output.
|
||
this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(elem.areas.influence_areas));
|
||
return true;
|
||
}
|
||
// Keep the area.
|
||
return false;
|
||
}),
|
||
influence_areas.end());
|
||
|
||
dur_inc += std::chrono::high_resolution_clock::now() - ta;
|
||
new_element = ! move_bounds[layer_idx - 1].empty();
|
||
if (merge_this_layer) {
|
||
bool reduced_by_merging = false;
|
||
if (size_t count_before_merge = influence_areas.size(); count_before_merge > 1) {
|
||
// ### Calculate which influence areas overlap, and merge them into a new influence area (simplified: an intersection of influence areas that have such an intersection)
|
||
merge_influence_areas(volumes, config, layer_idx, influence_areas, throw_on_cancel);
|
||
reduced_by_merging = count_before_merge > influence_areas.size();
|
||
}
|
||
last_merge_layer_idx = layer_idx;
|
||
if (! reduced_by_merging && ! had_new_element)
|
||
merge_every_x_layers = std::min(max_merge_every_x_layers, merge_every_x_layers + 1);
|
||
}
|
||
|
||
dur_total += std::chrono::high_resolution_clock::now() - ta;
|
||
|
||
// Save calculated elements to output, and allocate Polygons on heap, as they will not be changed again.
|
||
for (SupportElementMerging &elem : influence_areas)
|
||
if (! elem.areas.influence_areas.empty()) {
|
||
Polygons new_area = safe_union(elem.areas.influence_areas);
|
||
if (area(new_area) < tiny_area_threshold) {
|
||
BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area on layer " << layer_idx - 1 << ". Origin of " << elem.parents.size() << " areas. Was to bp " << elem.state.to_buildplate;
|
||
tree_supports_show_error("Insert error of area after merge.\n"sv, true);
|
||
}
|
||
this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(new_area));
|
||
}
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
progress_total += data_size_inverse * TREE_PROGRESS_AREA_CALC;
|
||
Progress::messageProgress(Progress::Stage::SUPPORT, progress_total * m_progress_multiplier + m_progress_offset, TREE_PROGRESS_TOTAL);
|
||
#endif
|
||
throw_on_cancel();
|
||
}
|
||
|
||
BOOST_LOG_TRIVIAL(info) << "Time spent with creating influence areas' subtasks: Increasing areas " << dur_inc.count() / 1000000 <<
|
||
" ms merging areas: " << (dur_total - dur_inc).count() / 1000000 << " ms";
|
||
}
|
||
|
||
/*!
|
||
* \brief Sets the result_on_layer for all parents based on the SupportElement supplied.
|
||
*
|
||
* \param elem[in] The SupportElements, which parent's position should be determined.
|
||
*/
|
||
static void set_points_on_areas(const SupportElement &elem, SupportElements *layer_above)
|
||
{
|
||
assert(!elem.state.deleted);
|
||
assert(layer_above != nullptr || elem.parents.empty());
|
||
|
||
// Based on the branch center point of the current layer, the point on the next (further up) layer is calculated.
|
||
if (! elem.state.result_on_layer_is_set()) {
|
||
BOOST_LOG_TRIVIAL(error) << "Uninitialized support element";
|
||
tree_supports_show_error("Uninitialized support element. A branch may be missing.\n"sv, true);
|
||
return;
|
||
}
|
||
|
||
if (layer_above)
|
||
for (int32_t next_elem_idx : elem.parents) {
|
||
assert(next_elem_idx >= 0);
|
||
SupportElement &next_elem = (*layer_above)[next_elem_idx];
|
||
assert(! next_elem.state.deleted);
|
||
// if the value was set somewhere else it it kept. This happens when a branch tries not to move after being unable to create a roof.
|
||
if (! next_elem.state.result_on_layer_is_set()) {
|
||
// Move inside has edgecases (see tests) so DONT use Polygons.inside to confirm correct move, Error with distance 0 is <= 1
|
||
// it is not required to check if how far this move moved a point as is can be larger than maximum_movement_distance.
|
||
// While this seems like a problem it may for example occur after merges.
|
||
next_elem.state.result_on_layer = move_inside_if_outside(next_elem.influence_area, elem.state.result_on_layer);
|
||
// do not call recursive because then amount of layers would be restricted by the stack size
|
||
}
|
||
// Mark the parent element as accessed from a valid child element.
|
||
next_elem.state.marked = true;
|
||
}
|
||
}
|
||
|
||
static void set_to_model_contact_simple(SupportElement &elem)
|
||
{
|
||
const Point best = move_inside_if_outside(elem.influence_area, elem.state.next_position);
|
||
elem.state.result_on_layer = best;
|
||
BOOST_LOG_TRIVIAL(debug) << "Added NON gracious Support On Model Point (" << best.x() << "," << best.y() << "). The current layer is " << elem.state.layer_idx;
|
||
}
|
||
|
||
/*!
|
||
* \brief Get the best point to connect to the model and set the result_on_layer of the relevant SupportElement accordingly.
|
||
*
|
||
* \param move_bounds[in,out] All currently existing influence areas
|
||
* \param first_elem[in,out] SupportElement that did not have its result_on_layer set meaning that it does not have a child element.
|
||
* \param layer_idx[in] The current layer.
|
||
*/
|
||
static void set_to_model_contact_to_model_gracious(
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
std::vector<SupportElements> &move_bounds,
|
||
SupportElement &first_elem,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
SupportElement *last_successfull_layer = nullptr;
|
||
|
||
// check for every layer upwards, up to the point where this influence area was created (either by initial insert or merge) if the branch could be placed on it, and highest up layer index.
|
||
{
|
||
SupportElement *elem = &first_elem;
|
||
for (LayerIndex layer_check = elem->state.layer_idx;
|
||
! intersection(elem->influence_area, volumes.getPlaceableAreas(support_element_collision_radius(config, elem->state), layer_check, throw_on_cancel)).empty();
|
||
elem = &move_bounds[++ layer_check][elem->parents.front()]) {
|
||
assert(elem->state.layer_idx == layer_check);
|
||
assert(! elem->state.deleted);
|
||
assert(elem->state.to_model_gracious);
|
||
last_successfull_layer = elem;
|
||
if (elem->parents.size() != 1)
|
||
// Reached merge point.
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Could not find valid placement, even though it should exist => error handling
|
||
if (last_successfull_layer == nullptr) {
|
||
BOOST_LOG_TRIVIAL(warning) << "No valid placement found for to model gracious element on layer " << first_elem.state.layer_idx;
|
||
tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches."sv, true);
|
||
first_elem.state.to_model_gracious = false;
|
||
set_to_model_contact_simple(first_elem);
|
||
} else {
|
||
// Found a gracious area above first_elem. Remove all below last_successfull_layer.
|
||
{
|
||
LayerIndex parent_layer_idx = first_elem.state.layer_idx;
|
||
for (SupportElement *elem = &first_elem; elem != last_successfull_layer; elem = &move_bounds[++ parent_layer_idx][elem->parents.front()]) {
|
||
assert(! elem->state.deleted);
|
||
elem->state.deleted = true;
|
||
}
|
||
}
|
||
// Guess a point inside the influence area, in which the branch will be placed in.
|
||
const Point best = move_inside_if_outside(last_successfull_layer->influence_area, last_successfull_layer->state.next_position);
|
||
last_successfull_layer->state.result_on_layer = best;
|
||
BOOST_LOG_TRIVIAL(debug) << "Added gracious Support On Model Point (" << best.x() << "," << best.y() << "). The current layer is " << last_successfull_layer;
|
||
}
|
||
}
|
||
|
||
// Remove elements marked as "deleted", update indices to parents.
|
||
static void remove_deleted_elements(std::vector<SupportElements> &move_bounds)
|
||
{
|
||
std::vector<int32_t> map_parents;
|
||
std::vector<int32_t> map_current;
|
||
for (LayerIndex layer_idx = LayerIndex(move_bounds.size()) - 1; layer_idx >= 0; -- layer_idx) {
|
||
SupportElements &layer = move_bounds[layer_idx];
|
||
map_current.clear();
|
||
for (int32_t i = 0; i < int32_t(layer.size());) {
|
||
SupportElement &element = layer[i];
|
||
if (element.state.deleted) {
|
||
if (map_current.empty()) {
|
||
// Initialize with identity map.
|
||
map_current.assign(layer.size(), 0);
|
||
std::iota(map_current.begin(), map_current.end(), 0);
|
||
}
|
||
// Delete all "deleted" elements from the end of the layer vector.
|
||
while (i < int32_t(layer.size()) && layer.back().state.deleted) {
|
||
layer.pop_back();
|
||
// Mark as deleted in the map.
|
||
map_current[layer.size()] = -1;
|
||
}
|
||
assert(i == layer.size() || i + 1 < layer.size());
|
||
if (i + 1 < int32_t(layer.size())) {
|
||
element = std::move(layer.back());
|
||
layer.pop_back();
|
||
// Mark the current element as deleted.
|
||
map_current[i] = -1;
|
||
// Mark the moved element as moved to index i.
|
||
map_current[layer.size()] = i;
|
||
}
|
||
} else {
|
||
// Current element is not deleted. Update its parent indices.
|
||
if (! map_parents.empty())
|
||
for (int32_t &parent_idx : element.parents)
|
||
parent_idx = map_parents[parent_idx];
|
||
++ i;
|
||
}
|
||
}
|
||
std::swap(map_current, map_parents);
|
||
}
|
||
}
|
||
|
||
/*!
|
||
* \brief Set the result_on_layer point for all influence areas
|
||
*
|
||
* \param move_bounds[in,out] All currently existing influence areas
|
||
*/
|
||
void create_nodes_from_area(
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
std::vector<SupportElements> &move_bounds,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
// Initialize points on layer 0, with a "random" point in the influence area.
|
||
// Point is chosen based on an inaccurate estimate where the branches will split into two, but every point inside the influence area would produce a valid result.
|
||
{
|
||
SupportElements *layer_above = move_bounds.size() > 1 ? &move_bounds[1] : nullptr;
|
||
if (layer_above) {
|
||
for (SupportElement &elem : *layer_above)
|
||
elem.state.marked = false;
|
||
}
|
||
for (SupportElement &init : move_bounds.front()) {
|
||
init.state.result_on_layer = move_inside_if_outside(init.influence_area, init.state.next_position);
|
||
// Also set the parent nodes, as these will be required for the first iteration of the loop below and mark the parent nodes.
|
||
set_points_on_areas(init, layer_above);
|
||
}
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
for (LayerIndex layer_idx = 1; layer_idx < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
auto &layer = move_bounds[layer_idx];
|
||
auto *layer_above = layer_idx + 1 < LayerIndex(move_bounds.size()) ? &move_bounds[layer_idx + 1] : nullptr;
|
||
if (layer_above)
|
||
for (SupportElement &elem : *layer_above)
|
||
elem.state.marked = false;
|
||
for (SupportElement &elem : layer) {
|
||
assert(! elem.state.deleted);
|
||
assert(elem.state.layer_idx == layer_idx);
|
||
// check if the resulting center point is not yet set
|
||
if (! elem.state.result_on_layer_is_set()) {
|
||
if (elem.state.to_buildplate || (elem.state.distance_to_top < config.min_dtt_to_model && ! elem.state.supports_roof)) {
|
||
if (elem.state.to_buildplate) {
|
||
BOOST_LOG_TRIVIAL(error) << "Uninitialized Influence area targeting " << elem.state.target_position.x() << "," << elem.state.target_position.y() << ") "
|
||
"at target_height: " << elem.state.target_height << " layer: " << layer_idx;
|
||
tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially."sv, true);
|
||
}
|
||
// we dont need to remove yet the parents as they will have a lower dtt and also no result_on_layer set
|
||
elem.state.deleted = true;
|
||
} else {
|
||
// set the point where the branch will be placed on the model
|
||
if (elem.state.to_model_gracious)
|
||
set_to_model_contact_to_model_gracious(volumes, config, move_bounds, elem, throw_on_cancel);
|
||
else
|
||
set_to_model_contact_simple(elem);
|
||
}
|
||
}
|
||
if (! elem.state.deleted && ! elem.state.marked && elem.state.target_height == layer_idx)
|
||
// Just a tip surface with no supporting element.
|
||
elem.state.deleted = true;
|
||
if (elem.state.deleted) {
|
||
for (int32_t parent_idx : elem.parents)
|
||
// When the roof was not able to generate downwards enough, the top elements may have not moved, and have result_on_layer already set.
|
||
// As this branch needs to be removed => all parents result_on_layer have to be invalidated.
|
||
(*layer_above)[parent_idx].state.result_on_layer_reset();
|
||
}
|
||
if (! elem.state.deleted) {
|
||
// Element is valid now setting points in the layer above and mark the parent nodes.
|
||
set_points_on_areas(elem, layer_above);
|
||
}
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
|
||
#ifndef NDEBUG
|
||
// Verify the tree connectivity including the branch slopes.
|
||
for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
auto &layer = move_bounds[layer_idx];
|
||
auto &above = move_bounds[layer_idx + 1];
|
||
for (SupportElement &elem : layer)
|
||
if (! elem.state.deleted) {
|
||
for (int32_t iparent : elem.parents) {
|
||
SupportElement &parent = above[iparent];
|
||
assert(! parent.state.deleted);
|
||
assert(elem.state.result_on_layer_is_set() == parent.state.result_on_layer_is_set());
|
||
if (elem.state.result_on_layer_is_set()) {
|
||
double radius_increase = support_element_radius(config, elem) - support_element_radius(config, parent);
|
||
assert(radius_increase >= 0);
|
||
double shift = (elem.state.result_on_layer - parent.state.result_on_layer).cast<double>().norm();
|
||
//FIXME this assert fails a lot. Is it correct?
|
||
//assert(shift < radius_increase + 2. * config.maximum_move_distance_slow);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#endif // NDEBUG
|
||
|
||
remove_deleted_elements(move_bounds);
|
||
|
||
#ifndef NDEBUG
|
||
// Verify the tree connectivity including the branch slopes.
|
||
for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
auto &layer = move_bounds[layer_idx];
|
||
auto &above = move_bounds[layer_idx + 1];
|
||
for (SupportElement &elem : layer) {
|
||
assert(! elem.state.deleted);
|
||
for (int32_t iparent : elem.parents) {
|
||
SupportElement &parent = above[iparent];
|
||
assert(! parent.state.deleted);
|
||
assert(elem.state.result_on_layer_is_set() == parent.state.result_on_layer_is_set());
|
||
if (elem.state.result_on_layer_is_set()) {
|
||
double radius_increase = support_element_radius(config, elem) - support_element_radius(config, parent);
|
||
assert(radius_increase >= 0);
|
||
double shift = (elem.state.result_on_layer - parent.state.result_on_layer).cast<double>().norm();
|
||
//FIXME this assert fails a lot. Is it correct?
|
||
//assert(shift < radius_increase + 2. * config.maximum_move_distance_slow);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#endif // NDEBUG
|
||
}
|
||
|
||
// For producing circular / elliptical areas from SupportElements (one DrawArea per one SupportElement)
|
||
// and for smoothing those areas along the tree branches.
|
||
struct DrawArea
|
||
{
|
||
// Element to be processed.
|
||
SupportElement *element;
|
||
// Element below, if there is such an element. nullptr if element is a root of a tree.
|
||
SupportElement *child_element;
|
||
// Polygons to be extruded for this element.
|
||
Polygons polygons;
|
||
};
|
||
|
||
/*!
|
||
* \brief Draws circles around result_on_layer points of the influence areas
|
||
*
|
||
* \param linear_data[in] All currently existing influence areas with the layer they are on
|
||
* \param layer_tree_polygons[out] Resulting branch areas with the layerindex they appear on. layer_tree_polygons.size() has to be at least linear_data.size() as each Influence area in linear_data will save have at least one (that's why it's a vector<vector>) corresponding branch area in layer_tree_polygons.
|
||
* \param inverse_tree_order[in] A mapping that returns the child of every influence area.
|
||
*/
|
||
static void generate_branch_areas(
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const std::vector<SupportElements> &move_bounds,
|
||
std::vector<DrawArea> &linear_data,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
double progress_total = TREE_PROGRESS_PRECALC_AVO + TREE_PROGRESS_PRECALC_COLL + TREE_PROGRESS_GENERATE_NODES + TREE_PROGRESS_AREA_CALC;
|
||
constexpr int progress_report_steps = 10;
|
||
const size_t progress_inserts_check_interval = linear_data.size() / progress_report_steps;
|
||
std::mutex critical_sections;
|
||
#endif // SLIC3R_TREESUPPORTS_PROGRESS
|
||
|
||
// Pre-generate a circle with correct diameter so that we don't have to recompute those (co)sines every time.
|
||
const Polygon branch_circle = make_circle(config.branch_radius, SUPPORT_TREE_CIRCLE_RESOLUTION);
|
||
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, linear_data.size()),
|
||
[&volumes, &config, &move_bounds, &linear_data, &branch_circle, &throw_on_cancel](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t idx = range.begin(); idx < range.end(); ++ idx) {
|
||
DrawArea &draw_area = linear_data[idx];
|
||
const LayerIndex layer_idx = draw_area.element->state.layer_idx;
|
||
const coord_t radius = support_element_radius(config, *draw_area.element);
|
||
bool parent_uses_min = false;
|
||
|
||
// Calculate multiple ovalized circles, to connect with every parent and child. Also generate regular circle for the current layer. Merge all these into one area.
|
||
std::vector<std::pair<Point, coord_t>> movement_directions{ std::pair<Point, coord_t>(Point(0, 0), radius) };
|
||
if (! draw_area.element->state.skip_ovalisation) {
|
||
if (draw_area.child_element != nullptr) {
|
||
const Point movement = draw_area.child_element->state.result_on_layer - draw_area.element->state.result_on_layer;
|
||
movement_directions.emplace_back(movement, radius);
|
||
}
|
||
const SupportElements *layer_above = layer_idx + 1 < LayerIndex(move_bounds.size()) ? &move_bounds[layer_idx + 1] : nullptr;
|
||
for (int32_t parent_idx : draw_area.element->parents) {
|
||
const SupportElement &parent = (*layer_above)[parent_idx];
|
||
const Point movement = parent.state.result_on_layer - draw_area.element->state.result_on_layer;
|
||
//FIXME why max(..., config.support_line_width)?
|
||
movement_directions.emplace_back(movement, std::max(support_element_radius(config, parent), config.support_line_width));
|
||
parent_uses_min |= parent.state.use_min_xy_dist;
|
||
}
|
||
}
|
||
|
||
const Polygons &collision = volumes.getCollision(0, layer_idx, parent_uses_min || draw_area.element->state.use_min_xy_dist);
|
||
auto generateArea = [&collision, &draw_area, &branch_circle, branch_radius = config.branch_radius, support_line_width = config.support_line_width, &movement_directions]
|
||
(coord_t aoffset, double &max_speed) {
|
||
Polygons poly;
|
||
max_speed = 0;
|
||
for (std::pair<Point, coord_t> movement : movement_directions) {
|
||
max_speed = std::max(max_speed, movement.first.cast<double>().norm());
|
||
|
||
// Visualization: https://jsfiddle.net/0zvcq39L/2/
|
||
// Ovalizes the circle to an ellipse, that contains both old center and new target position.
|
||
double used_scale = (movement.second + aoffset) / (1.0 * branch_radius);
|
||
Point center_position = draw_area.element->state.result_on_layer + movement.first / 2;
|
||
const double moveX = movement.first.x() / (used_scale * branch_radius);
|
||
const double moveY = movement.first.y() / (used_scale * branch_radius);
|
||
const double vsize_inv = 0.5 / (0.01 + std::sqrt(moveX * moveX + moveY * moveY));
|
||
|
||
double matrix[] = {
|
||
used_scale * (1 + moveX * moveX * vsize_inv),
|
||
used_scale * (0 + moveX * moveY * vsize_inv),
|
||
used_scale * (0 + moveX * moveY * vsize_inv),
|
||
used_scale * (1 + moveY * moveY * vsize_inv),
|
||
};
|
||
Polygon circle;
|
||
for (Point vertex : branch_circle)
|
||
circle.points.emplace_back(center_position + Point(matrix[0] * vertex.x() + matrix[1] * vertex.y(), matrix[2] * vertex.x() + matrix[3] * vertex.y()));
|
||
poly.emplace_back(std::move(circle));
|
||
}
|
||
|
||
// There seem to be some rounding errors, causing a branch to be a tiny bit further away from the model that it has to be.
|
||
// This can cause the tip to be slightly further away front the overhang (x/y wise) than optimal. This fixes it, and for every other part, 0.05mm will not be noticed.
|
||
poly = diff_clipped(offset(union_(poly), std::min(coord_t(50), support_line_width / 4), jtMiter, 1.2), collision);
|
||
return poly;
|
||
};
|
||
|
||
// Ensure branch area will not overlap with model/collision. This can happen because of e.g. ovalization or increase_until_radius.
|
||
double max_speed;
|
||
Polygons polygons = generateArea(0, max_speed);
|
||
const bool fast_relative_movement = max_speed > radius * 0.75;
|
||
|
||
if (fast_relative_movement || support_element_radius(config, *draw_area.element) - support_element_collision_radius(config, draw_area.element->state) > config.support_line_width) {
|
||
// Simulate the path the nozzle will take on the outermost wall.
|
||
// If multiple parts exist, the outer line will not go all around the support part potentially causing support material to be printed mid air.
|
||
ExPolygons nozzle_path = offset_ex(polygons, - config.support_line_width / 2);
|
||
if (nozzle_path.size() > 1) {
|
||
// Just try to make the area a tiny bit larger.
|
||
polygons = generateArea(config.support_line_width / 2, max_speed);
|
||
nozzle_path = offset_ex(polygons, -config.support_line_width / 2);
|
||
// If larger area did not fix the problem, all parts off the nozzle path that do not contain the center point are removed, hoping for the best.
|
||
if (nozzle_path.size() > 1) {
|
||
ExPolygons polygons_with_correct_center;
|
||
for (ExPolygon &part : nozzle_path) {
|
||
bool drop = false;
|
||
if (! part.contains(draw_area.element->state.result_on_layer)) {
|
||
// try a fuzzy inside as sometimes the point should be on the border, but is not because of rounding errors...
|
||
Point pt = draw_area.element->state.result_on_layer;
|
||
move_inside(to_polygons(part), pt, 0);
|
||
drop = (draw_area.element->state.result_on_layer - pt).cast<double>().norm() >= scaled<double>(0.025);
|
||
}
|
||
if (! drop)
|
||
polygons_with_correct_center.emplace_back(std::move(part));
|
||
}
|
||
// Increase the area again, to ensure the nozzle path when calculated later is very similar to the one assumed above.
|
||
assert(contains(polygons, draw_area.element->state.result_on_layer));
|
||
polygons = diff_clipped(offset(polygons_with_correct_center, config.support_line_width / 2, jtMiter, 1.2),
|
||
//FIXME Vojtech: Clipping may split the region into multiple pieces again, reversing the fixing effort.
|
||
collision);
|
||
}
|
||
}
|
||
}
|
||
|
||
draw_area.polygons = std::move(polygons);
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
if (idx % progress_inserts_check_interval == 0) {
|
||
std::lock_guard<std::mutex> critical_section_progress(critical_sections);
|
||
progress_total += TREE_PROGRESS_GENERATE_BRANCH_AREAS / progress_report_steps;
|
||
Progress::messageProgress(Progress::Stage::SUPPORT, progress_total * m_progress_multiplier + m_progress_offset, TREE_PROGRESS_TOTAL);
|
||
}
|
||
#endif
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
}
|
||
|
||
/*!
|
||
* \brief Applies some smoothing to the outer wall, intended to smooth out sudden jumps as they can happen when a branch moves though a hole.
|
||
*
|
||
* \param layer_tree_polygons[in,out] Resulting branch areas with the layerindex they appear on.
|
||
*/
|
||
static void smooth_branch_areas(
|
||
const TreeSupportSettings &config,
|
||
std::vector<SupportElements> &move_bounds,
|
||
std::vector<DrawArea> &linear_data,
|
||
const std::vector<size_t> &linear_data_layers,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
double progress_total = TREE_PROGRESS_PRECALC_AVO + TREE_PROGRESS_PRECALC_COLL + TREE_PROGRESS_GENERATE_NODES + TREE_PROGRESS_AREA_CALC + TREE_PROGRESS_GENERATE_BRANCH_AREAS;
|
||
#endif // SLIC3R_TREESUPPORTS_PROGRESS
|
||
|
||
const coord_t max_radius_change_per_layer = 1 + config.support_line_width / 2; // this is the upper limit a radius may change per layer. +1 to avoid rounding errors
|
||
|
||
// smooth upwards
|
||
for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(move_bounds.size()) - 1; ++ layer_idx) {
|
||
const size_t processing_base = linear_data_layers[layer_idx];
|
||
const size_t processing_base_above = linear_data_layers[layer_idx + 1];
|
||
const SupportElements &layer_above = move_bounds[layer_idx + 1];
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, processing_base_above - processing_base),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t processing_idx = range.begin(); processing_idx < range.end(); ++ processing_idx) {
|
||
DrawArea &draw_area = linear_data[processing_base + processing_idx];
|
||
assert(draw_area.element->state.layer_idx == layer_idx);
|
||
double max_outer_wall_distance = 0;
|
||
bool do_something = false;
|
||
for (int32_t parent_idx : draw_area.element->parents) {
|
||
const SupportElement &parent = layer_above[parent_idx];
|
||
assert(parent.state.layer_idx == layer_idx + 1);
|
||
if (support_element_radius(config, parent) != support_element_collision_radius(config, parent)) {
|
||
do_something = true;
|
||
max_outer_wall_distance = std::max(max_outer_wall_distance,
|
||
(draw_area.element->state.result_on_layer - parent.state.result_on_layer).cast<double>().norm() - (support_element_radius(config, *draw_area.element) - support_element_radius(config, parent)));
|
||
}
|
||
}
|
||
max_outer_wall_distance += max_radius_change_per_layer; // As this change is a bit larger than what usually appears, lost radius can be slowly reclaimed over the layers.
|
||
if (do_something) {
|
||
assert(contains(draw_area.polygons, draw_area.element->state.result_on_layer));
|
||
Polygons max_allowed_area = offset(draw_area.polygons, float(max_outer_wall_distance), jtMiter, 1.2);
|
||
for (int32_t parent_idx : draw_area.element->parents) {
|
||
const SupportElement &parent = layer_above[parent_idx];
|
||
#ifndef NDEBUG
|
||
assert(parent.state.layer_idx == layer_idx + 1);
|
||
assert(contains(linear_data[processing_base_above + parent_idx].polygons, parent.state.result_on_layer));
|
||
double radius_increase = support_element_radius(config, *draw_area.element) - support_element_radius(config, parent);
|
||
assert(radius_increase >= 0);
|
||
double shift = (draw_area.element->state.result_on_layer - parent.state.result_on_layer).cast<double>().norm();
|
||
assert(shift < radius_increase + 2. * config.maximum_move_distance_slow);
|
||
#endif // NDEBUG
|
||
if (support_element_radius(config, parent) != support_element_collision_radius(config, parent)) {
|
||
// No other element on this layer than the current one may be connected to &parent,
|
||
// thus it is safe to update parent's DrawArea directly.
|
||
Polygons &dst = linear_data[processing_base_above + parent_idx].polygons;
|
||
// Polygons orig = dst;
|
||
if (! dst.empty()) {
|
||
dst = intersection(dst, max_allowed_area);
|
||
#if 0
|
||
if (dst.empty()) {
|
||
static int irun = 0;
|
||
SVG::export_expolygons(debug_out_path("treesupport-extrude_areas-smooth-error-%d.svg", irun ++),
|
||
{ { { union_ex(max_allowed_area) }, { "max_allowed_area", "yellow", 0.5f } },
|
||
{ { union_ex(orig) }, { "orig", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
::MessageBoxA(nullptr, "TreeSupport smoothing bug", "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING);
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
}
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
progress_total += TREE_PROGRESS_SMOOTH_BRANCH_AREAS / 2;
|
||
Progress::messageProgress(Progress::Stage::SUPPORT, progress_total * m_progress_multiplier + m_progress_offset, TREE_PROGRESS_TOTAL); // It is just assumed that both smoothing loops together are one third of the time spent in this function. This was guessed. As the whole function is only 10%, and the smoothing is hard to predict a progress report in the loop may be not useful.
|
||
#endif
|
||
|
||
// smooth downwards
|
||
for (auto& element : move_bounds.back())
|
||
element.state.marked = false;
|
||
for (int layer_idx = int(move_bounds.size()) - 2; layer_idx >= 0; -- layer_idx) {
|
||
const size_t processing_base = linear_data_layers[layer_idx];
|
||
const size_t processing_base_above = linear_data_layers[layer_idx + 1];
|
||
const SupportElements &layer_above = move_bounds[layer_idx + 1];
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, processing_base_above - processing_base),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t processing_idx = range.begin(); processing_idx < range.end(); ++ processing_idx) {
|
||
DrawArea &draw_area = linear_data[processing_base + processing_idx];
|
||
bool do_something = false;
|
||
Polygons max_allowed_area;
|
||
for (int32_t parent_idx : draw_area.element->parents) {
|
||
const SupportElement &parent = layer_above[parent_idx];
|
||
coord_t max_outer_line_increase = max_radius_change_per_layer;
|
||
Polygons result = offset(linear_data[processing_base_above + parent_idx].polygons, max_outer_line_increase, jtMiter, 1.2);
|
||
Point direction = draw_area.element->state.result_on_layer - parent.state.result_on_layer;
|
||
// move the polygons object
|
||
for (auto &outer : result)
|
||
for (Point& p : outer)
|
||
p += direction;
|
||
append(max_allowed_area, std::move(result));
|
||
do_something = do_something || parent.state.marked || support_element_collision_radius(config, parent) != support_element_radius(config, parent);
|
||
}
|
||
if (do_something) {
|
||
// Trim the current drawing areas with max_allowed_area.
|
||
Polygons result = intersection(max_allowed_area, draw_area.polygons);
|
||
if (area(result) < area(draw_area.polygons)) {
|
||
// Mark parent as modified to propagate down.
|
||
draw_area.element->state.marked = true;
|
||
draw_area.polygons = std::move(result);
|
||
}
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
}
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
progress_total += TREE_PROGRESS_SMOOTH_BRANCH_AREAS / 2;
|
||
Progress::messageProgress(Progress::Stage::SUPPORT, progress_total * m_progress_multiplier + m_progress_offset, TREE_PROGRESS_TOTAL);
|
||
#endif
|
||
}
|
||
|
||
/*!
|
||
* \brief Drop down areas that do rest non-gracefully on the model to ensure the branch actually rests on something.
|
||
*
|
||
* \param layer_tree_polygons[in] Resulting branch areas with the layerindex they appear on.
|
||
* \param linear_data[in] All currently existing influence areas with the layer they are on
|
||
* \param dropped_down_areas[out] Areas that have to be added to support all non-graceful areas.
|
||
* \param inverse_tree_order[in] A mapping that returns the child of every influence area.
|
||
*/
|
||
static void drop_non_gracious_areas(
|
||
const TreeModelVolumes &volumes,
|
||
const std::vector<DrawArea> &linear_data,
|
||
std::vector<Polygons> &support_layer_storage,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
std::vector<std::vector<std::pair<LayerIndex, Polygons>>> dropped_down_areas(linear_data.size());
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, linear_data.size()),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t idx = range.begin(); idx < range.end(); ++ idx) {
|
||
// If a element has no child, it connects to whatever is below as no support further down for it will exist.
|
||
if (const DrawArea &draw_element = linear_data[idx]; ! draw_element.element->state.to_model_gracious && draw_element.child_element == nullptr) {
|
||
Polygons rest_support;
|
||
const LayerIndex layer_idx_first = draw_element.element->state.layer_idx - 1;
|
||
for (LayerIndex layer_idx = layer_idx_first; area(rest_support) > tiny_area_threshold && layer_idx >= 0; -- layer_idx) {
|
||
rest_support = diff_clipped(layer_idx == layer_idx_first ? draw_element.polygons : rest_support, volumes.getCollision(0, layer_idx, false));
|
||
dropped_down_areas[idx].emplace_back(layer_idx, rest_support);
|
||
}
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
|
||
for (coord_t i = 0; i < static_cast<coord_t>(dropped_down_areas.size()); i++)
|
||
for (std::pair<LayerIndex, Polygons> &pair : dropped_down_areas[i])
|
||
append(support_layer_storage[pair.first], std::move(pair.second));
|
||
}
|
||
|
||
/*!
|
||
* \brief Generates Support Floor, ensures Support Roof can not cut of branches, and saves the branches as support to storage
|
||
*
|
||
* \param support_layer_storage[in] Areas where support should be generated.
|
||
* \param support_roof_storage[in] Areas where support was replaced with roof.
|
||
* \param storage[in,out] The storage where the support should be stored.
|
||
*/
|
||
static void finalize_interface_and_support_areas(
|
||
const PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const std::vector<Polygons> &overhangs,
|
||
std::vector<Polygons> &support_layer_storage,
|
||
std::vector<Polygons> &support_roof_storage,
|
||
|
||
SupportGeneratorLayersPtr &bottom_contacts,
|
||
SupportGeneratorLayersPtr &top_contacts,
|
||
SupportGeneratorLayersPtr &intermediate_layers,
|
||
SupportGeneratorLayerStorage &layer_storage,
|
||
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
assert(std::all_of(bottom_contacts.begin(), bottom_contacts.end(), [](auto *p) { return p == nullptr; }));
|
||
assert(std::all_of(intermediate_layers.begin(), intermediate_layers.end(), [](auto* p) { return p == nullptr; }));
|
||
InterfacePreference interface_pref = config.interface_preference; // InterfacePreference::SupportLinesOverwriteInterface;
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
double progress_total = TREE_PROGRESS_PRECALC_AVO + TREE_PROGRESS_PRECALC_COLL + TREE_PROGRESS_GENERATE_NODES + TREE_PROGRESS_AREA_CALC + TREE_PROGRESS_GENERATE_BRANCH_AREAS + TREE_PROGRESS_SMOOTH_BRANCH_AREAS;
|
||
#endif // SLIC3R_TREESUPPORTS_PROGRESS
|
||
|
||
// Iterate over the generated circles in parallel and clean them up. Also add support floor.
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, support_layer_storage.size()),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||
// Subtract support lines of the branches from the roof
|
||
SupportGeneratorLayer* support_roof = layer_idx < top_contacts.size() ? top_contacts[layer_idx] : nullptr;
|
||
Polygons support_roof_polygons;
|
||
if (Polygons &src = support_roof_storage[layer_idx]; ! src.empty()) {
|
||
if (support_roof != nullptr && ! support_roof->polygons.empty()) {
|
||
support_roof_polygons = union_(src, support_roof->polygons);
|
||
support_roof->polygons.clear();
|
||
} else
|
||
support_roof_polygons = std::move(src);
|
||
} else if (support_roof != nullptr) {
|
||
support_roof_polygons = std::move(support_roof->polygons);
|
||
support_roof->polygons.clear();
|
||
}
|
||
|
||
//assert(intermediate_layers[layer_idx] == nullptr);
|
||
Polygons base_layer_polygons = std::move(support_layer_storage[layer_idx]);
|
||
|
||
if (! base_layer_polygons.empty()) {
|
||
// Most of the time in this function is this union call. Can take 300+ ms when a lot of areas are to be unioned.
|
||
base_layer_polygons = smooth_outward(union_(base_layer_polygons), config.support_line_width); //FIXME was .smooth(50);
|
||
//smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) :
|
||
// simplify a bit, to ensure the output does not contain outrageous amounts of vertices. Should not be necessary, just a precaution.
|
||
base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled<double>(0.03), double(config.resolution)));
|
||
}
|
||
if (! support_roof_polygons.empty() && ! base_layer_polygons.empty()) {
|
||
// if (area(intersection(base_layer_polygons, support_roof_polygons)) > tiny_area_threshold)
|
||
{
|
||
switch (interface_pref) {
|
||
case InterfacePreference::InterfaceAreaOverwritesSupport:
|
||
base_layer_polygons = diff(base_layer_polygons, support_roof_polygons);
|
||
break;
|
||
case InterfacePreference::SupportAreaOverwritesInterface:
|
||
support_roof_polygons = diff(support_roof_polygons, base_layer_polygons);
|
||
break;
|
||
//FIXME
|
||
#if 1
|
||
case InterfacePreference::InterfaceLinesOverwriteSupport:
|
||
case InterfacePreference::SupportLinesOverwriteInterface:
|
||
assert(false);
|
||
[[fallthrough]];
|
||
#else
|
||
case InterfacePreference::InterfaceLinesOverwriteSupport:
|
||
{
|
||
// Hatch the support roof interfaces, offset them by their line width and subtract them from support base.
|
||
Polygons interface_lines = offset(to_polylines(
|
||
generate_support_infill_lines(support_roof->polygons, true, layer_idx, config.support_roof_line_distance)),
|
||
config.support_roof_line_width / 2);
|
||
base_layer_polygons = diff(base_layer_polygons, interface_lines);
|
||
break;
|
||
}
|
||
case InterfacePreference::SupportLinesOverwriteInterface:
|
||
{
|
||
// Hatch the support roof interfaces, offset them by their line width and subtract them from support base.
|
||
Polygons tree_lines = union_(offset(to_polylines(
|
||
generate_support_infill_lines(base_layer_polygons, false, layer_idx, config.support_line_distance, true)),
|
||
config.support_line_width / 2));
|
||
// do not draw roof where the tree is. I prefer it this way as otherwise the roof may cut of a branch from its support below.
|
||
support_roof->polygons = diff(support_roof->polygons, tree_lines);
|
||
break;
|
||
}
|
||
#endif
|
||
case InterfacePreference::Nothing:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Subtract support floors from the support area and add them to the support floor instead.
|
||
if (config.support_bottom_layers > 0 && ! base_layer_polygons.empty()) {
|
||
SupportGeneratorLayer*& support_bottom = bottom_contacts[layer_idx];
|
||
Polygons layer_outset = diff_clipped(
|
||
config.support_bottom_offset > 0 ? offset(base_layer_polygons, config.support_bottom_offset, jtMiter, 1.2) : base_layer_polygons,
|
||
volumes.getCollision(0, layer_idx, false));
|
||
Polygons floor_layer;
|
||
size_t layers_below = 0;
|
||
while (layers_below <= config.support_bottom_layers) {
|
||
// one sample at 0 layers below, another at config.support_bottom_layers. In-between samples at config.performance_interface_skip_layers distance from each other.
|
||
const size_t sample_layer = static_cast<size_t>(std::max(0, (static_cast<int>(layer_idx) - static_cast<int>(layers_below)) - static_cast<int>(config.z_distance_bottom_layers)));
|
||
//FIXME subtract the wipe tower
|
||
append(floor_layer, intersection(layer_outset, overhangs[sample_layer]));
|
||
if (layers_below < config.support_bottom_layers)
|
||
layers_below = std::min(layers_below + 1, config.support_bottom_layers);
|
||
else
|
||
break;
|
||
}
|
||
if (! floor_layer.empty()) {
|
||
if (support_bottom == nullptr)
|
||
support_bottom = &layer_allocate(layer_storage, SupporLayerType::sltBottomContact, print_object.slicing_parameters(), config, layer_idx);
|
||
support_bottom->polygons = union_(floor_layer, support_bottom->polygons);
|
||
base_layer_polygons = diff_clipped(base_layer_polygons, offset(support_bottom->polygons, scaled<float>(0.01), jtMiter, 1.2)); // Subtract the support floor from the normal support.
|
||
}
|
||
}
|
||
|
||
if (! support_roof_polygons.empty()) {
|
||
if (support_roof == nullptr)
|
||
support_roof = top_contacts[layer_idx] = &layer_allocate(layer_storage, SupporLayerType::sltTopContact, print_object.slicing_parameters(), config, layer_idx);
|
||
support_roof->polygons = union_(support_roof_polygons);
|
||
}
|
||
if (! base_layer_polygons.empty()) {
|
||
SupportGeneratorLayer *base_layer = intermediate_layers[layer_idx] = &layer_allocate(layer_storage, SupporLayerType::sltBase, print_object.slicing_parameters(), config, layer_idx);
|
||
base_layer->polygons = union_(base_layer_polygons);
|
||
}
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
{
|
||
std::lock_guard<std::mutex> critical_section_progress(critical_sections);
|
||
progress_total += TREE_PROGRESS_FINALIZE_BRANCH_AREAS / support_layer_storage.size();
|
||
Progress::messageProgress(Progress::Stage::SUPPORT, progress_total * m_progress_multiplier + m_progress_offset, TREE_PROGRESS_TOTAL);
|
||
}
|
||
#endif
|
||
#if 0
|
||
{
|
||
std::lock_guard<std::mutex> lock(critical_sections);
|
||
if (!storage.support.supportLayers[layer_idx].support_infill_parts.empty() || !storage.support.supportLayers[layer_idx].support_roof.empty())
|
||
storage.support.layer_nr_max_filled_layer = std::max(storage.support.layer_nr_max_filled_layer, static_cast<int>(layer_idx));
|
||
}
|
||
#endif
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
}
|
||
|
||
/*!
|
||
* \brief Draws circles around result_on_layer points of the influence areas and applies some post processing.
|
||
*
|
||
* \param move_bounds[in] All currently existing influence areas
|
||
* \param storage[in,out] The storage where the support should be stored.
|
||
*/
|
||
static void draw_areas(
|
||
PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const std::vector<Polygons> &overhangs,
|
||
std::vector<SupportElements> &move_bounds,
|
||
|
||
SupportGeneratorLayersPtr &bottom_contacts,
|
||
SupportGeneratorLayersPtr &top_contacts,
|
||
SupportGeneratorLayersPtr &intermediate_layers,
|
||
SupportGeneratorLayerStorage &layer_storage,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
std::vector<Polygons> support_layer_storage(move_bounds.size());
|
||
std::vector<Polygons> support_roof_storage(move_bounds.size());
|
||
// All SupportElements are put into a layer independent storage to improve parallelization.
|
||
std::vector<DrawArea> linear_data;
|
||
std::vector<size_t> linear_data_layers;
|
||
{
|
||
std::vector<std::pair<SupportElement*, SupportElement*>> map_downwards_old;
|
||
std::vector<std::pair<SupportElement*, SupportElement*>> map_downwards_new;
|
||
for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
SupportElements *layer_above = layer_idx + 1 < LayerIndex(move_bounds.size()) ? &move_bounds[layer_idx + 1] : nullptr;
|
||
map_downwards_new.clear();
|
||
linear_data_layers.emplace_back(linear_data.size());
|
||
std::sort(map_downwards_old.begin(), map_downwards_old.end(), [](auto &l, auto &r) { return l.first < r.first; });
|
||
for (SupportElement &elem : move_bounds[layer_idx]) {
|
||
SupportElement *child = nullptr;
|
||
if (layer_idx > 0) {
|
||
auto it = std::lower_bound(map_downwards_old.begin(), map_downwards_old.end(), &elem, [](auto &l, const SupportElement *r) { return l.first < r; });
|
||
if (it != map_downwards_old.end() && it->first == &elem) {
|
||
child = it->second;
|
||
// Only one link points to a node above from below.
|
||
assert(! (++ it != map_downwards_old.end() && it->first == &elem));
|
||
}
|
||
assert(child ? child->state.result_on_layer_is_set() : elem.state.target_height > layer_idx);
|
||
}
|
||
for (int32_t parent_idx : elem.parents) {
|
||
SupportElement &parent = (*layer_above)[parent_idx];
|
||
if (parent.state.result_on_layer_is_set())
|
||
map_downwards_new.emplace_back(&parent, &elem);
|
||
}
|
||
linear_data.push_back({ &elem, child });
|
||
}
|
||
std::swap(map_downwards_old, map_downwards_new);
|
||
}
|
||
linear_data_layers.emplace_back(linear_data.size());
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
#ifndef NDEBUG
|
||
for (size_t i = 0; i < move_bounds.size(); ++ i) {
|
||
size_t begin = linear_data_layers[i];
|
||
size_t end = linear_data_layers[i + 1];
|
||
for (size_t j = begin; j < end; ++ j)
|
||
assert(linear_data[j].element == &move_bounds[i][j - begin]);
|
||
}
|
||
#endif // NDEBUG
|
||
|
||
auto t_start = std::chrono::high_resolution_clock::now();
|
||
// Generate the circles that will be the branches.
|
||
generate_branch_areas(volumes, config, move_bounds, linear_data, throw_on_cancel);
|
||
|
||
#if 0
|
||
assert(linear_data_layers.size() == move_bounds.size() + 1);
|
||
for (const auto &draw_area : linear_data)
|
||
assert(contains(draw_area.polygons, draw_area.element->state.result_on_layer));
|
||
for (size_t i = 0; i < move_bounds.size(); ++ i) {
|
||
size_t begin = linear_data_layers[i];
|
||
size_t end = linear_data_layers[i + 1];
|
||
for (size_t j = begin; j < end; ++ j) {
|
||
const auto &draw_area = linear_data[j];
|
||
assert(draw_area.element == &move_bounds[i][j - begin]);
|
||
assert(contains(draw_area.polygons, draw_area.element->state.result_on_layer));
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if 0
|
||
for (size_t area_layer_idx = 0; area_layer_idx + 1 < linear_data_layers.size(); ++ area_layer_idx) {
|
||
size_t begin = linear_data_layers[area_layer_idx];
|
||
size_t end = linear_data_layers[area_layer_idx + 1];
|
||
Polygons polygons;
|
||
for (size_t area_idx = begin; area_idx < end; ++ area_idx) {
|
||
DrawArea &area = linear_data[area_idx];
|
||
append(polygons, area.polygons);
|
||
}
|
||
SVG::export_expolygons(debug_out_path("treesupport-extrude_areas-raw-%d.svg", area_layer_idx),
|
||
{ { { union_ex(polygons) }, { "parent", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
}
|
||
#endif
|
||
|
||
auto t_generate = std::chrono::high_resolution_clock::now();
|
||
// In some edgecases a branch may go though a hole, where the regular radius does not fit. This can result in an apparent jump in branch radius. As such this cases need to be caught and smoothed out.
|
||
smooth_branch_areas(config, move_bounds, linear_data, linear_data_layers, throw_on_cancel);
|
||
|
||
#if 0
|
||
for (size_t area_layer_idx = 0; area_layer_idx + 1 < linear_data_layers.size(); ++area_layer_idx) {
|
||
size_t begin = linear_data_layers[area_layer_idx];
|
||
size_t end = linear_data_layers[area_layer_idx + 1];
|
||
Polygons polygons;
|
||
for (size_t area_idx = begin; area_idx < end; ++area_idx) {
|
||
DrawArea& area = linear_data[area_idx];
|
||
append(polygons, area.polygons);
|
||
}
|
||
SVG::export_expolygons(debug_out_path("treesupport-extrude_areas-smooth-%d.svg", area_layer_idx),
|
||
{ { { union_ex(polygons) }, { "parent", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
}
|
||
#endif
|
||
|
||
auto t_smooth = std::chrono::high_resolution_clock::now();
|
||
// drop down all trees that connect non gracefully with the model
|
||
drop_non_gracious_areas(volumes, linear_data, support_layer_storage, throw_on_cancel);
|
||
auto t_drop = std::chrono::high_resolution_clock::now();
|
||
|
||
// Single threaded combining all support areas to the right layers.
|
||
{
|
||
auto begin = linear_data.begin();
|
||
for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
size_t cnt_roofs = 0;
|
||
size_t cnt_layers = 0;
|
||
auto end = begin;
|
||
for (; end != linear_data.end() && end->element->state.layer_idx == layer_idx; ++ end)
|
||
++ (end->element->state.missing_roof_layers > end->element->state.distance_to_top ? cnt_roofs : cnt_layers);
|
||
auto &this_roofs = support_roof_storage[layer_idx];
|
||
auto &this_layers = support_layer_storage[layer_idx];
|
||
this_roofs.reserve(this_roofs.size() + cnt_roofs);
|
||
this_layers.reserve(this_layers.size() + cnt_layers);
|
||
for (auto it = begin; it != end; ++ it)
|
||
std::move(std::begin(it->polygons), std::end(it->polygons), std::back_inserter(it->element->state.missing_roof_layers > it->element->state.distance_to_top ? this_roofs : this_layers));
|
||
begin = end;
|
||
}
|
||
}
|
||
|
||
finalize_interface_and_support_areas(print_object, volumes, config, overhangs, support_layer_storage, support_roof_storage,
|
||
bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel);
|
||
auto t_end = std::chrono::high_resolution_clock::now();
|
||
|
||
auto dur_gen_tips = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_generate - t_start).count();
|
||
auto dur_smooth = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_smooth - t_generate).count();
|
||
auto dur_drop = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_drop - t_smooth).count();
|
||
auto dur_finalize = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_end - t_drop).count();
|
||
|
||
BOOST_LOG_TRIVIAL(info) <<
|
||
"Time used for drawing subfuctions: generate_branch_areas: " << dur_gen_tips << " ms "
|
||
"smooth_branch_areas: " << dur_smooth << " ms "
|
||
"drop_non_gracious_areas: " << dur_drop << " ms "
|
||
"finalize_interface_and_support_areas " << dur_finalize << " ms";
|
||
}
|
||
|
||
#if 1
|
||
// Test whether two circles, each on its own plane in 3D intersect.
|
||
// Circles are considered intersecting, if the lowest point on one circle is below the other circle's plane.
|
||
// Assumption: The two planes are oriented the same way.
|
||
static bool circles_intersect(
|
||
const Vec3d &p1, const Vec3d &n1, const double r1,
|
||
const Vec3d &p2, const Vec3d &n2, const double r2)
|
||
{
|
||
assert(n1.dot(n2) >= 0);
|
||
|
||
const Vec3d z = n1.cross(n2);
|
||
const Vec3d dir1 = z.cross(n1);
|
||
const Vec3d lowest_point1 = p1 + dir1 * (r1 / dir1.norm());
|
||
assert(n2.dot(p1) >= n2.dot(lowest_point1));
|
||
if (n2.dot(lowest_point1) <= 0)
|
||
return true;
|
||
const Vec3d dir2 = z.cross(n2);
|
||
const Vec3d lowest_point2 = p2 + dir2 * (r2 / dir2.norm());
|
||
assert(n1.dot(p2) >= n1.dot(lowest_point2));
|
||
return n1.dot(lowest_point2) <= 0;
|
||
}
|
||
|
||
template<bool flip_normals>
|
||
void triangulate_fan(indexed_triangle_set &its, int ifan, int ibegin, int iend)
|
||
{
|
||
// at least 3 vertices, increasing order.
|
||
assert(ibegin + 3 <= iend);
|
||
assert(ibegin >= 0 && iend <= its.vertices.size());
|
||
assert(ifan >= 0 && ifan < its.vertices.size());
|
||
int num_faces = iend - ibegin;
|
||
its.indices.reserve(its.indices.size() + num_faces * 3);
|
||
for (int v = ibegin, u = iend - 1; v < iend; u = v ++) {
|
||
if (flip_normals)
|
||
its.indices.push_back({ ifan, u, v });
|
||
else
|
||
its.indices.push_back({ ifan, v, u });
|
||
}
|
||
}
|
||
|
||
static void triangulate_strip(indexed_triangle_set &its, int ibegin1, int iend1, int ibegin2, int iend2)
|
||
{
|
||
// at least 3 vertices, increasing order.
|
||
assert(ibegin1 + 3 <= iend1);
|
||
assert(ibegin1 >= 0 && iend1 <= its.vertices.size());
|
||
assert(ibegin2 + 3 <= iend2);
|
||
assert(ibegin2 >= 0 && iend2 <= its.vertices.size());
|
||
int n1 = iend1 - ibegin1;
|
||
int n2 = iend2 - ibegin2;
|
||
its.indices.reserve(its.indices.size() + (n1 + n2) * 3);
|
||
|
||
// For the first vertex of 1st strip, find the closest vertex on the 2nd strip.
|
||
int istart2 = ibegin2;
|
||
{
|
||
const Vec3f &p1 = its.vertices[ibegin1];
|
||
auto d2min = std::numeric_limits<float>::max();
|
||
for (int i = ibegin2; i < iend2; ++ i) {
|
||
const Vec3f &p2 = its.vertices[i];
|
||
const float d2 = (p2 - p1).squaredNorm();
|
||
if (d2 < d2min) {
|
||
d2min = d2;
|
||
istart2 = i;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Now triangulate the strip zig-zag fashion taking always the shortest connection if possible.
|
||
for (int u = ibegin1, v = istart2; n1 > 0 || n2 > 0;) {
|
||
bool take_first;
|
||
int u2, v2;
|
||
auto update_u2 = [&u2, u, ibegin1, iend1]() {
|
||
u2 = u;
|
||
if (++ u2 == iend1)
|
||
u2 = ibegin1;
|
||
};
|
||
auto update_v2 = [&v2, v, ibegin2, iend2]() {
|
||
v2 = v;
|
||
if (++ v2 == iend2)
|
||
v2 = ibegin2;
|
||
};
|
||
if (n1 == 0) {
|
||
take_first = false;
|
||
update_v2();
|
||
} else if (n2 == 0) {
|
||
take_first = true;
|
||
update_u2();
|
||
} else {
|
||
update_u2();
|
||
update_v2();
|
||
float l1 = (its.vertices[u2] - its.vertices[v]).squaredNorm();
|
||
float l2 = (its.vertices[v2] - its.vertices[u]).squaredNorm();
|
||
take_first = l1 < l2;
|
||
}
|
||
if (take_first) {
|
||
its.indices.push_back({ u, u2, v });
|
||
-- n1;
|
||
u = u2;
|
||
} else {
|
||
its.indices.push_back({ u, v2, v });
|
||
-- n2;
|
||
v = v2;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Discretize 3D circle, append to output vector, return ranges of indices of the points added.
|
||
static std::pair<int, int> discretize_circle(const Vec3f ¢er, const Vec3f &normal, const float radius, const float eps, std::vector<Vec3f> &pts)
|
||
{
|
||
// Calculate discretization step and number of steps.
|
||
float angle_step = 2. * acos(1. - eps / radius);
|
||
auto nsteps = int(ceil(2 * M_PI / angle_step));
|
||
angle_step = 2 * M_PI / nsteps;
|
||
|
||
// Prepare coordinate system for the circle plane.
|
||
Vec3f x = normal.cross(Vec3f(0.f, -1.f, 0.f)).normalized();
|
||
Vec3f y = normal.cross(x).normalized();
|
||
assert(std::abs(x.cross(y).dot(normal) - 1.f) < EPSILON);
|
||
|
||
// Discretize the circle.
|
||
int begin = int(pts.size());
|
||
pts.reserve(pts.size() + nsteps);
|
||
float angle = 0;
|
||
x *= radius;
|
||
y *= radius;
|
||
for (int i = 0; i < nsteps; ++ i) {
|
||
pts.emplace_back(center + x * cos(angle) + y * sin(angle));
|
||
angle += angle_step;
|
||
}
|
||
return { begin, int(pts.size()) };
|
||
}
|
||
|
||
// Discretize polygon, append to output vector, return ranges of indices of the points added.
|
||
static std::pair<int, int> discretize_polygon(const Vec3f& center, const Polygons& polys, std::vector<Vec3f>& pts)
|
||
{
|
||
const Polygon& poly = polys.front();
|
||
size_t nsteps = poly.size();
|
||
// Discretize the circle.
|
||
int begin = int(pts.size());
|
||
pts.reserve(pts.size() + nsteps);
|
||
for (int i = 0; i < nsteps; ++i) {
|
||
Vec3f pt(poly.points[i].x(), poly.points[i].y(), center.z());
|
||
pts.emplace_back(pt);
|
||
}
|
||
return { begin, int(pts.size()) };
|
||
}
|
||
|
||
// Returns Z span of the generated mesh.
|
||
static std::pair<float, float> extrude_branch(
|
||
const std::vector<const SupportElement*>&path,
|
||
const TreeSupportSettings &config,
|
||
const SlicingParameters &slicing_params,
|
||
const std::vector<SupportElements> &move_bounds,
|
||
indexed_triangle_set &result)
|
||
{
|
||
Vec3d p1, p2, p3;
|
||
Vec3d v1, v2;
|
||
Vec3d nprev;
|
||
Vec3d ncurrent;
|
||
assert(path.size() >= 2);
|
||
static constexpr const float eps = 0.015f;
|
||
std::pair<int, int> prev_strip;
|
||
float zmin = 0;
|
||
float zmax = 0;
|
||
|
||
for (size_t ipath = 1; ipath < path.size(); ++ ipath) {
|
||
const SupportElement &prev = *path[ipath - 1];
|
||
const SupportElement ¤t = *path[ipath];
|
||
assert(prev.state.layer_idx + 1 == current.state.layer_idx);
|
||
p1 = to_3d(unscaled<double>(prev .state.result_on_layer), layer_z(slicing_params, config, prev .state.layer_idx));
|
||
p2 = to_3d(unscaled<double>(current.state.result_on_layer), layer_z(slicing_params, config, current.state.layer_idx));
|
||
v1 = (p2 - p1).normalized();
|
||
if (ipath == 1) {
|
||
nprev = v1;
|
||
// Extrude the bottom half sphere.
|
||
float radius = unscaled<float>(support_element_radius(config, prev));
|
||
float angle_step = 2. * acos(1. - eps / radius);
|
||
auto nsteps = int(ceil(M_PI / (2. * angle_step)));
|
||
angle_step = M_PI / (2. * nsteps);
|
||
int ifan = int(result.vertices.size());
|
||
result.vertices.emplace_back((p1 - nprev * radius).cast<float>());
|
||
zmin = result.vertices.back().z();
|
||
float angle = angle_step;
|
||
std::pair<int, int> strip;
|
||
if (current.state.type == TreeSupport::NodeType::ePolygon) {
|
||
strip = discretize_polygon(p1.cast<float>(), current.influence_area, result.vertices);
|
||
prev_strip = strip;
|
||
strip = discretize_polygon(p2.cast<float>(), current.influence_area, result.vertices);
|
||
}
|
||
else {
|
||
for (int i = 1; i < nsteps; ++i, angle += angle_step) {
|
||
strip = discretize_circle((p1 - nprev * radius * cos(angle)).cast<float>(), nprev.cast<float>(), radius * sin(angle), eps, result.vertices);
|
||
if (i == 1)
|
||
triangulate_fan<false>(result, ifan, strip.first, strip.second);
|
||
else
|
||
triangulate_strip(result, prev_strip.first, prev_strip.second, strip.first, strip.second);
|
||
// sprintf(fname, "d:\\temp\\meshes\\tree-partial-%d.obj", ++ irun);
|
||
// its_write_obj(result, fname);
|
||
prev_strip = strip;
|
||
}
|
||
}
|
||
}
|
||
if (ipath + 1 == path.size()) {
|
||
// End of the tube.
|
||
ncurrent = v1;
|
||
// Extrude the top half sphere.
|
||
float radius = unscaled<float>(support_element_radius(config, current));
|
||
float angle_step = 2. * acos(1. - eps / radius);
|
||
auto nsteps = int(ceil(M_PI / (2. * angle_step)));
|
||
angle_step = M_PI / (2. * nsteps);
|
||
auto angle = float(M_PI / 2.);
|
||
std::pair<int, int> strip;
|
||
if (current.state.type == TreeSupport::NodeType::ePolygon) {
|
||
strip = discretize_polygon(p2.cast<float>(), current.influence_area, result.vertices);
|
||
}
|
||
else {
|
||
for (int i = 0; i < nsteps; ++i, angle -= angle_step) {
|
||
strip = discretize_circle((p2 + ncurrent * radius * cos(angle)).cast<float>(), ncurrent.cast<float>(), radius * sin(angle), eps, result.vertices);
|
||
triangulate_strip(result, prev_strip.first, prev_strip.second, strip.first, strip.second);
|
||
// sprintf(fname, "d:\\temp\\meshes\\tree-partial-%d.obj", ++ irun);
|
||
// its_write_obj(result, fname);
|
||
prev_strip = strip;
|
||
}
|
||
int ifan = int(result.vertices.size());
|
||
result.vertices.emplace_back((p2 + ncurrent * radius).cast<float>());
|
||
zmax = result.vertices.back().z();
|
||
triangulate_fan<true>(result, ifan, prev_strip.first, prev_strip.second);
|
||
// sprintf(fname, "d:\\temp\\meshes\\tree-partial-%d.obj", ++ irun);
|
||
// its_write_obj(result, fname);
|
||
}
|
||
} else {
|
||
const SupportElement &next = *path[ipath + 1];
|
||
assert(current.state.layer_idx + 1 == next.state.layer_idx);
|
||
p3 = to_3d(unscaled<double>(next.state.result_on_layer), layer_z(slicing_params, config, next.state.layer_idx));
|
||
v2 = (p3 - p2).normalized();
|
||
ncurrent = (v1 + v2).normalized();
|
||
float radius = unscaled<float>(support_element_radius(config, current));
|
||
std::pair<int, int> strip;
|
||
if (current.state.type == TreeSupport::NodeType::ePolygon) {
|
||
strip = discretize_polygon(p2.cast<float>(), current.influence_area, result.vertices);
|
||
}
|
||
else {
|
||
strip = discretize_circle(p2.cast<float>(), ncurrent.cast<float>(), radius, eps, result.vertices);
|
||
}
|
||
triangulate_strip(result, prev_strip.first, prev_strip.second, strip.first, strip.second);
|
||
prev_strip = strip;
|
||
// sprintf(fname, "d:\\temp\\meshes\\tree-partial-%d.obj", ++irun);
|
||
// its_write_obj(result, fname);
|
||
}
|
||
#if 0
|
||
if (circles_intersect(p1, nprev, support_element_radius(settings, prev), p2, ncurrent, support_element_radius(settings, current))) {
|
||
// Cannot connect previous and current slice using a simple zig-zag triangulation,
|
||
// because the two circles intersect.
|
||
|
||
} else {
|
||
// Continue with chaining.
|
||
|
||
}
|
||
#endif
|
||
}
|
||
|
||
return std::make_pair(zmin, zmax);
|
||
}
|
||
#endif
|
||
|
||
#ifdef TREE_SUPPORT_ORGANIC_NUDGE_NEW
|
||
// New version using per layer AABB trees of lines for nudging spheres away from an object.
|
||
void organic_smooth_branches_avoid_collisions(
|
||
const PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const std::vector<std::pair<SupportElement*, int>> &elements_with_link_down,
|
||
const std::vector<size_t> &linear_data_layers,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
struct LayerCollisionCache {
|
||
coord_t min_element_radius{ std::numeric_limits<coord_t>::max() };
|
||
bool min_element_radius_known() const { return this->min_element_radius != std::numeric_limits<coord_t>::max(); }
|
||
coord_t collision_radius{ 0 };
|
||
std::vector<Linef> lines;
|
||
AABBTreeIndirect::Tree<2, double> aabbtree_lines;
|
||
bool empty() const { return this->lines.empty(); }
|
||
};
|
||
std::vector<LayerCollisionCache> layer_collision_cache;
|
||
layer_collision_cache.reserve(1024);
|
||
const SlicingParameters &slicing_params = print_object.slicing_parameters();
|
||
for (const std::pair<SupportElement*, int>& element : elements_with_link_down) {
|
||
LayerIndex layer_idx = element.first->state.layer_idx;
|
||
if (size_t num_layers = layer_idx + 1; num_layers > layer_collision_cache.size()) {
|
||
if (num_layers > layer_collision_cache.capacity())
|
||
layer_collision_cache.reserve(next_highest_power_of_2(num_layers));
|
||
layer_collision_cache.resize(num_layers, {});
|
||
}
|
||
auto& l = layer_collision_cache[layer_idx];
|
||
l.min_element_radius = std::min(l.min_element_radius, support_element_radius(config, *element.first));
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(layer_collision_cache.size()); ++layer_idx)
|
||
if (LayerCollisionCache& l = layer_collision_cache[layer_idx]; !l.min_element_radius_known())
|
||
l.min_element_radius = 0;
|
||
else {
|
||
//FIXME
|
||
l.min_element_radius = 0;
|
||
std::optional<std::pair<coord_t, std::reference_wrapper<const Polygons>>> res = volumes.get_collision_lower_bound_area(layer_idx, l.min_element_radius);
|
||
assert(res.has_value());
|
||
l.collision_radius = res->first;
|
||
Lines alines = to_lines(res->second.get());
|
||
l.lines.reserve(alines.size());
|
||
for (const Line &line : alines)
|
||
l.lines.push_back({ unscaled<double>(line.a), unscaled<double>(line.b) });
|
||
l.aabbtree_lines = AABBTreeLines::build_aabb_tree_over_indexed_lines(l.lines);
|
||
throw_on_cancel();
|
||
}
|
||
|
||
struct CollisionSphere {
|
||
const SupportElement& element;
|
||
int element_below_id;
|
||
const bool locked;
|
||
float radius;
|
||
// Current position, when nudged away from the collision.
|
||
Vec3f position;
|
||
// Previous position, for Laplacian smoothing.
|
||
Vec3f prev_position;
|
||
//
|
||
Vec3f last_collision;
|
||
double last_collision_depth;
|
||
// Minimum Z for which the sphere collision will be evaluated.
|
||
// Limited by the minimum sloping angle and by the bottom of the tree.
|
||
float min_z{ -std::numeric_limits<float>::max() };
|
||
// Maximum Z for which the sphere collision will be evaluated.
|
||
// Limited by the minimum sloping angle and by the tip of the current branch.
|
||
float max_z{ std::numeric_limits<float>::max() };
|
||
uint32_t layer_begin;
|
||
uint32_t layer_end;
|
||
};
|
||
|
||
std::vector<CollisionSphere> collision_spheres;
|
||
collision_spheres.reserve(elements_with_link_down.size());
|
||
for (const std::pair<SupportElement*, int> &element_with_link : elements_with_link_down) {
|
||
const SupportElement &element = *element_with_link.first;
|
||
const int link_down = element_with_link.second;
|
||
collision_spheres.push_back({
|
||
element,
|
||
link_down,
|
||
// locked
|
||
element.parents.empty() || (link_down == -1 && element.state.layer_idx > 0),
|
||
unscaled<float>(support_element_radius(config, element)),
|
||
// 3D position
|
||
to_3d(unscaled<float>(element.state.result_on_layer), float(layer_z(slicing_params, config, element.state.layer_idx)))
|
||
});
|
||
// Update min_z coordinate to min_z of the tree below.
|
||
CollisionSphere &collision_sphere = collision_spheres.back();
|
||
if (link_down != -1) {
|
||
const size_t offset_below = linear_data_layers[element.state.layer_idx - 1];
|
||
collision_sphere.min_z = collision_spheres[offset_below + link_down].min_z;
|
||
} else
|
||
collision_sphere.min_z = collision_sphere.position.z();
|
||
}
|
||
// Update max_z by propagating max_z from the tips of the branches.
|
||
for (int collision_sphere_id = int(collision_spheres.size()) - 1; collision_sphere_id >= 0; -- collision_sphere_id) {
|
||
CollisionSphere &collision_sphere = collision_spheres[collision_sphere_id];
|
||
if (collision_sphere.element.parents.empty())
|
||
// Tip
|
||
collision_sphere.max_z = collision_sphere.position.z();
|
||
else {
|
||
// Below tip
|
||
const size_t offset_above = linear_data_layers[collision_sphere.element.state.layer_idx + 1];
|
||
for (auto iparent : collision_sphere.element.parents) {
|
||
float parent_z = collision_spheres[offset_above + iparent].max_z;
|
||
// collision_sphere.max_z = collision_sphere.max_z == std::numeric_limits<float>::max() ? parent_z : std::max(collision_sphere.max_z, parent_z);
|
||
collision_sphere.max_z = std::min(collision_sphere.max_z, parent_z);
|
||
}
|
||
}
|
||
}
|
||
// Update min_z / max_z to limit the search Z span of a given sphere for collision detection.
|
||
for (CollisionSphere &collision_sphere : collision_spheres) {
|
||
//FIXME limit the collision span by the tree slope.
|
||
collision_sphere.min_z = std::max(collision_sphere.min_z, collision_sphere.position.z() - collision_sphere.radius);
|
||
collision_sphere.max_z = std::min(collision_sphere.max_z, collision_sphere.position.z() + collision_sphere.radius);
|
||
collision_sphere.layer_begin = std::min(collision_sphere.element.state.layer_idx, layer_idx_ceil(slicing_params, config, collision_sphere.min_z));
|
||
assert(collision_sphere.layer_begin < layer_collision_cache.size());
|
||
collision_sphere.layer_end = std::min(LayerIndex(layer_collision_cache.size()), std::max(collision_sphere.element.state.layer_idx, layer_idx_floor(slicing_params, config, collision_sphere.max_z)) + 1);
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
static constexpr const double collision_extra_gap = 0.1;
|
||
static constexpr const double max_nudge_collision_avoidance = 0.5;
|
||
static constexpr const double max_nudge_smoothing = 0.2;
|
||
static constexpr const size_t num_iter = 100; // 1000;
|
||
for (size_t iter = 0; iter < num_iter; ++ iter) {
|
||
// Back up prev position before Laplacian smoothing.
|
||
for (CollisionSphere &collision_sphere : collision_spheres)
|
||
collision_sphere.prev_position = collision_sphere.position;
|
||
std::atomic<size_t> num_moved{ 0 };
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, collision_spheres.size()),
|
||
[&collision_spheres, &layer_collision_cache, &slicing_params, &config, &linear_data_layers, &num_moved, &throw_on_cancel](const tbb::blocked_range<size_t> range) {
|
||
for (size_t collision_sphere_id = range.begin(); collision_sphere_id < range.end(); ++ collision_sphere_id)
|
||
if (CollisionSphere &collision_sphere = collision_spheres[collision_sphere_id]; ! collision_sphere.locked) {
|
||
// Calculate collision of multiple 2D layers against a collision sphere.
|
||
collision_sphere.last_collision_depth = - std::numeric_limits<double>::max();
|
||
for (uint32_t layer_id = collision_sphere.layer_begin; layer_id != collision_sphere.layer_end; ++ layer_id) {
|
||
if(layer_id>= layer_collision_cache.size())
|
||
continue;
|
||
double dz = (layer_id - collision_sphere.element.state.layer_idx) * slicing_params.layer_height;
|
||
if (double r2 = sqr(collision_sphere.radius) - sqr(dz); r2 > 0) {
|
||
if (const LayerCollisionCache &layer_collision_cache_item = layer_collision_cache[layer_id]; ! layer_collision_cache_item.empty()) {
|
||
size_t hit_idx_out;
|
||
Vec2d hit_point_out;
|
||
if (double dist = sqrt(AABBTreeLines::squared_distance_to_indexed_lines(
|
||
layer_collision_cache_item.lines, layer_collision_cache_item.aabbtree_lines, Vec2d(to_2d(collision_sphere.position).cast<double>()),
|
||
hit_idx_out, hit_point_out, r2)); dist >= 0.) {
|
||
double collision_depth = sqrt(r2) - dist;
|
||
if (collision_depth > collision_sphere.last_collision_depth) {
|
||
collision_sphere.last_collision_depth = collision_depth;
|
||
collision_sphere.last_collision = to_3d(hit_point_out.cast<float>(), float(layer_z(slicing_params, config, layer_id)));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (collision_sphere.last_collision_depth > 0) {
|
||
// Collision detected to be removed.
|
||
// Nudge the circle center away from the collision.
|
||
if (collision_sphere.last_collision_depth > EPSILON)
|
||
// a little bit of hysteresis to detect end of
|
||
++ num_moved;
|
||
// Shift by maximum 2mm.
|
||
double nudge_dist = std::min(std::max(0., collision_sphere.last_collision_depth + collision_extra_gap), max_nudge_collision_avoidance);
|
||
Vec2d nudge_vector = (to_2d(collision_sphere.position) - to_2d(collision_sphere.last_collision)).cast<double>().normalized() * nudge_dist;
|
||
collision_sphere.position.head<2>() += (nudge_vector * nudge_dist).cast<float>();
|
||
}
|
||
// Laplacian smoothing
|
||
Vec2d avg{ 0, 0 };
|
||
const size_t offset_above = linear_data_layers[collision_sphere.element.state.layer_idx + 1];
|
||
double weight = 0.;
|
||
for (auto iparent : collision_sphere.element.parents) {
|
||
double w = collision_sphere.radius;
|
||
avg += w * to_2d(collision_spheres[offset_above + iparent].prev_position.cast<double>());
|
||
weight += w;
|
||
}
|
||
if (collision_sphere.element_below_id != -1) {
|
||
const size_t offset_below = linear_data_layers[collision_sphere.element.state.layer_idx - 1];
|
||
const double w = weight;
|
||
avg += w * to_2d(collision_spheres[offset_below + collision_sphere.element_below_id].prev_position.cast<double>());
|
||
weight += w;
|
||
}
|
||
avg /= weight;
|
||
static constexpr const double smoothing_factor = 0.5;
|
||
Vec2d old_pos = to_2d(collision_sphere.position).cast<double>();
|
||
Vec2d new_pos = (1. - smoothing_factor) * old_pos + smoothing_factor * avg;
|
||
Vec2d shift = new_pos - old_pos;
|
||
double nudge_dist_max = shift.norm();
|
||
// Shift by maximum 1mm, less than the collision avoidance factor.
|
||
double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_smoothing);
|
||
collision_sphere.position.head<2>() += (shift.normalized() * nudge_dist).cast<float>();
|
||
|
||
throw_on_cancel();
|
||
}
|
||
});
|
||
#if 0
|
||
std::vector<double> stat;
|
||
for (CollisionSphere& collision_sphere : collision_spheres)
|
||
if (!collision_sphere.locked)
|
||
stat.emplace_back(collision_sphere.last_collision_depth);
|
||
std::sort(stat.begin(), stat.end());
|
||
printf("iteration: %d, moved: %d, collision depth: min %lf, max %lf, median %lf\n", int(iter), int(num_moved), stat.front(), stat.back(), stat[stat.size() / 2]);
|
||
#endif
|
||
if (num_moved == 0)
|
||
break;
|
||
}
|
||
|
||
for (size_t i = 0; i < collision_spheres.size(); ++ i)
|
||
elements_with_link_down[i].first->state.result_on_layer = scaled<coord_t>(to_2d(collision_spheres[i].position));
|
||
}
|
||
#else // TREE_SUPPORT_ORGANIC_NUDGE_NEW
|
||
// Old version using OpenVDB, works but it is extremely slow for complex meshes.
|
||
static void organic_smooth_branches_avoid_collisions(
|
||
const PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
std::vector<SupportElements> &move_bounds,
|
||
const std::vector<std::pair<SupportElement*, int>> &elements_with_link_down,
|
||
const std::vector<size_t> &linear_data_layers,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
TriangleMesh mesh = print_object.model_object()->raw_mesh();
|
||
mesh.transform(print_object.trafo_centered());
|
||
double scale = 10.;
|
||
openvdb::FloatGrid::Ptr grid = mesh_to_grid(mesh.its, openvdb::math::Transform{}, scale, 0., 0.);
|
||
std::unique_ptr<openvdb::tools::ClosestSurfacePoint<openvdb::FloatGrid>> closest_surface_point = openvdb::tools::ClosestSurfacePoint<openvdb::FloatGrid>::create(*grid);
|
||
std::vector<openvdb::Vec3R> pts, prev, projections;
|
||
std::vector<float> distances;
|
||
for (const std::pair<SupportElement*, int>& element : elements_with_link_down) {
|
||
Vec3d pt = to_3d(unscaled<double>(element.first->state.result_on_layer), layer_z(print_object.slicing_parameters(), config, element.first->state.layer_idx)) * scale;
|
||
pts.push_back({ pt.x(), pt.y(), pt.z() });
|
||
}
|
||
|
||
const double collision_extra_gap = 1. * scale;
|
||
const double max_nudge_collision_avoidance = 2. * scale;
|
||
const double max_nudge_smoothing = 1. * scale;
|
||
|
||
static constexpr const size_t num_iter = 100; // 1000;
|
||
for (size_t iter = 0; iter < num_iter; ++ iter) {
|
||
prev = pts;
|
||
projections = pts;
|
||
distances.assign(pts.size(), std::numeric_limits<float>::max());
|
||
closest_surface_point->searchAndReplace(projections, distances);
|
||
size_t num_moved = 0;
|
||
for (size_t i = 0; i < projections.size(); ++ i) {
|
||
const SupportElement &element = *elements_with_link_down[i].first;
|
||
const int below = elements_with_link_down[i].second;
|
||
const bool locked = (below == -1 && element.state.layer_idx > 0) || element.state.locked();
|
||
if (! locked && pts[i] != projections[i]) {
|
||
// Nudge the circle center away from the collision.
|
||
Vec3d v{ projections[i].x() - pts[i].x(), projections[i].y() - pts[i].y(), projections[i].z() - pts[i].z() };
|
||
double depth = v.norm();
|
||
assert(std::abs(distances[i] - depth) < EPSILON);
|
||
double radius = unscaled<double>(support_element_radius(config, element)) * scale;
|
||
if (depth < radius) {
|
||
// Collision detected to be removed.
|
||
++ num_moved;
|
||
double dxy = sqrt(sqr(radius) - sqr(v.z()));
|
||
double nudge_dist_max = dxy - std::hypot(v.x(), v.y())
|
||
//FIXME 1mm gap
|
||
+ collision_extra_gap;
|
||
// Shift by maximum 2mm.
|
||
double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_collision_avoidance);
|
||
Vec2d nudge_v = to_2d(v).normalized() * (- nudge_dist);
|
||
pts[i].x() += nudge_v.x();
|
||
pts[i].y() += nudge_v.y();
|
||
}
|
||
}
|
||
// Laplacian smoothing
|
||
if (! locked && ! element.parents.empty()) {
|
||
Vec2d avg{ 0, 0 };
|
||
const SupportElements &above = move_bounds[element.state.layer_idx + 1];
|
||
const size_t offset_above = linear_data_layers[element.state.layer_idx + 1];
|
||
double weight = 0.;
|
||
for (auto iparent : element.parents) {
|
||
double w = support_element_radius(config, above[iparent]);
|
||
avg.x() += w * prev[offset_above + iparent].x();
|
||
avg.y() += w * prev[offset_above + iparent].y();
|
||
weight += w;
|
||
}
|
||
size_t cnt = element.parents.size();
|
||
if (below != -1) {
|
||
const size_t offset_below = linear_data_layers[element.state.layer_idx - 1];
|
||
const double w = weight; // config.getRadius(move_bounds[element.state.layer_idx - 1][below].state);
|
||
avg.x() += w * prev[offset_below + below].x();
|
||
avg.y() += w * prev[offset_below + below].y();
|
||
++ cnt;
|
||
weight += w;
|
||
}
|
||
//avg /= double(cnt);
|
||
avg /= weight;
|
||
static constexpr const double smoothing_factor = 0.5;
|
||
Vec2d old_pos{ pts[i].x(), pts[i].y() };
|
||
Vec2d new_pos = (1. - smoothing_factor) * old_pos + smoothing_factor * avg;
|
||
Vec2d shift = new_pos - old_pos;
|
||
double nudge_dist_max = shift.norm();
|
||
// Shift by maximum 1mm, less than the collision avoidance factor.
|
||
double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_smoothing);
|
||
Vec2d nudge_v = shift.normalized() * nudge_dist;
|
||
pts[i].x() += nudge_v.x();
|
||
pts[i].y() += nudge_v.y();
|
||
}
|
||
}
|
||
// printf("iteration: %d, moved: %d\n", int(iter), int(num_moved));
|
||
if (num_moved == 0)
|
||
break;
|
||
}
|
||
|
||
for (size_t i = 0; i < projections.size(); ++ i) {
|
||
elements_with_link_down[i].first->state.result_on_layer.x() = scaled<coord_t>(pts[i].x()) / scale;
|
||
elements_with_link_down[i].first->state.result_on_layer.y() = scaled<coord_t>(pts[i].y()) / scale;
|
||
}
|
||
}
|
||
#endif // TREE_SUPPORT_ORGANIC_NUDGE_NEW
|
||
|
||
// Organic specific: Smooth branches and produce one cummulative mesh to be sliced.
|
||
indexed_triangle_set draw_branches(
|
||
PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
std::vector<SupportElements> &move_bounds,
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
static int irun = 0;
|
||
|
||
// All SupportElements are put into a layer independent storage to improve parallelization.
|
||
std::vector<std::pair<SupportElement*, int>> elements_with_link_down;
|
||
std::vector<size_t> linear_data_layers;
|
||
{
|
||
std::vector<std::pair<SupportElement*, int>> map_downwards_old;
|
||
std::vector<std::pair<SupportElement*, int>> map_downwards_new;
|
||
linear_data_layers.emplace_back(0);
|
||
for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
SupportElements *layer_above = layer_idx + 1 < move_bounds.size() ? &move_bounds[layer_idx + 1] : nullptr;
|
||
map_downwards_new.clear();
|
||
std::sort(map_downwards_old.begin(), map_downwards_old.end(), [](auto& l, auto& r) { return l.first < r.first; });
|
||
SupportElements &layer = move_bounds[layer_idx];
|
||
for (size_t elem_idx = 0; elem_idx < layer.size(); ++ elem_idx) {
|
||
SupportElement &elem = layer[elem_idx];
|
||
int child = -1;
|
||
if (layer_idx > 0) {
|
||
auto it = std::lower_bound(map_downwards_old.begin(), map_downwards_old.end(), &elem, [](auto& l, const SupportElement* r) { return l.first < r; });
|
||
if (it != map_downwards_old.end() && it->first == &elem) {
|
||
child = it->second;
|
||
// Only one link points to a node above from below.
|
||
assert(!(++it != map_downwards_old.end() && it->first == &elem));
|
||
}
|
||
const SupportElement *pchild = child == -1 ? nullptr : &move_bounds[layer_idx - 1][child];
|
||
assert(pchild ? pchild->state.result_on_layer_is_set() : elem.state.target_height > layer_idx);
|
||
}
|
||
for (int32_t parent_idx : elem.parents) {
|
||
SupportElement &parent = (*layer_above)[parent_idx];
|
||
if (parent.state.result_on_layer_is_set())
|
||
map_downwards_new.emplace_back(&parent, elem_idx);
|
||
}
|
||
|
||
elements_with_link_down.push_back({ &elem, int(child) });
|
||
}
|
||
std::swap(map_downwards_old, map_downwards_new);
|
||
linear_data_layers.emplace_back(elements_with_link_down.size());
|
||
}
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
organic_smooth_branches_avoid_collisions(print_object, volumes, config, elements_with_link_down, linear_data_layers, throw_on_cancel);
|
||
|
||
// Unmark all nodes.
|
||
for (SupportElements &elements : move_bounds)
|
||
for (SupportElement &element : elements)
|
||
element.state.marked = false;
|
||
|
||
// Traverse all nodes, generate tubes.
|
||
// Traversal stack with nodes and thier current parent
|
||
const SlicingParameters &slicing_params = print_object.slicing_parameters();
|
||
std::vector<const SupportElement*> path;
|
||
indexed_triangle_set cummulative_mesh;
|
||
indexed_triangle_set partial_mesh;
|
||
indexed_triangle_set temp_mesh;
|
||
for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) {
|
||
SupportElements &layer = move_bounds[layer_idx];
|
||
SupportElements &layer_above = move_bounds[layer_idx + 1];
|
||
|
||
for (SupportElement &start_element : layer)
|
||
if (! start_element.state.marked && ! start_element.parents.empty()) {
|
||
// Collect elements up to a bifurcation above.
|
||
start_element.state.marked = true;
|
||
for (size_t parent_idx = 0; parent_idx < start_element.parents.size(); ++ parent_idx) {
|
||
path.clear();
|
||
path.emplace_back(&start_element);
|
||
// Traverse each branch until it branches again.
|
||
SupportElement &first_parent = layer_above[start_element.parents[parent_idx]];
|
||
assert(path.back()->state.layer_idx + 1 == first_parent.state.layer_idx);
|
||
path.emplace_back(&first_parent);
|
||
if (first_parent.parents.size() < 2)
|
||
first_parent.state.marked = true;
|
||
if (first_parent.parents.size() == 1) {
|
||
for (SupportElement *parent = &first_parent;;) {
|
||
SupportElement &next_parent = move_bounds[parent->state.layer_idx + 1][parent->parents.front()];
|
||
assert(path.back()->state.layer_idx + 1 == next_parent.state.layer_idx);
|
||
path.emplace_back(&next_parent);
|
||
if (next_parent.parents.size() > 1)
|
||
break;
|
||
next_parent.state.marked = true;
|
||
if (next_parent.parents.size() == 0)
|
||
break;
|
||
parent = &next_parent;
|
||
}
|
||
}
|
||
// Triangulate the tube.
|
||
partial_mesh.clear();
|
||
extrude_branch(path, config, slicing_params, move_bounds, partial_mesh);
|
||
#if 1
|
||
char fname[2048];
|
||
sprintf(fname, "SVG\\tree-raw-%d.obj", ++ irun);
|
||
its_write_obj(partial_mesh, fname);
|
||
#if 0
|
||
temp_mesh.clear();
|
||
cut_mesh(partial_mesh, layer_z(slicing_params, path.back()->state.layer_idx) + EPSILON, nullptr, &temp_mesh, false);
|
||
sprintf(fname, "d:\\temp\\meshes\\tree-trimmed1-%d.obj", irun);
|
||
its_write_obj(temp_mesh, fname);
|
||
partial_mesh.clear();
|
||
cut_mesh(temp_mesh, layer_z(slicing_params, path.front()->state.layer_idx) - EPSILON, &partial_mesh, nullptr, false);
|
||
sprintf(fname, "d:\\temp\\meshes\\tree-trimmed2-%d.obj", irun);
|
||
its_write_obj(partial_mesh, fname);
|
||
#endif
|
||
#endif
|
||
its_merge(cummulative_mesh, partial_mesh);
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
}
|
||
return cummulative_mesh;
|
||
}
|
||
|
||
// Organic specific: Slice the cummulative mesh produced by draw_branches().
|
||
void slice_branches(
|
||
PrintObject &print_object,
|
||
const TreeModelVolumes &volumes,
|
||
const TreeSupportSettings &config,
|
||
const std::vector<Polygons> &overhangs,
|
||
std::vector<SupportElements> &move_bounds,
|
||
const indexed_triangle_set &cummulative_mesh,
|
||
|
||
SupportGeneratorLayersPtr &bottom_contacts,
|
||
SupportGeneratorLayersPtr &top_contacts,
|
||
SupportGeneratorLayersPtr &intermediate_layers,
|
||
SupportGeneratorLayerStorage &layer_storage,
|
||
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
const SlicingParameters &slicing_params = print_object.slicing_parameters();
|
||
std::vector<float> slice_z;
|
||
for (size_t layer_idx = 0; layer_idx < move_bounds.size(); ++ layer_idx) {
|
||
double print_z = slicing_params.object_print_z_min + slicing_params.first_object_layer_height + layer_idx * slicing_params.layer_height;
|
||
double layer_height = layer_idx == 0 ? slicing_params.first_object_layer_height : slicing_params.layer_height;
|
||
slice_z.emplace_back(float(print_z - layer_height * 0.5));
|
||
}
|
||
// Remove the trailing slices.
|
||
while (! slice_z.empty())
|
||
if (move_bounds[slice_z.size() - 1].empty())
|
||
slice_z.pop_back();
|
||
else
|
||
break;
|
||
|
||
#if 0
|
||
its_write_obj(cummulative_mesh, "d:\\temp\\meshes\\tree.obj");
|
||
#endif
|
||
|
||
MeshSlicingParamsEx params;
|
||
params.closing_radius = float(print_object.config().slice_closing_radius.value);
|
||
params.mode = MeshSlicingParams::SlicingMode::Positive;
|
||
std::vector<ExPolygons> slices = slice_mesh_ex(cummulative_mesh, slice_z, params, throw_on_cancel);
|
||
for (size_t layer_idx = 0; layer_idx < slice_z.size(); ++ layer_idx)
|
||
if (! slices[layer_idx].empty()) {
|
||
SupportGeneratorLayer *&l = intermediate_layers[layer_idx];
|
||
if (l == nullptr)
|
||
l = &layer_allocate(layer_storage, SupporLayerType::sltBase, slicing_params, layer_idx);
|
||
append(l->polygons, to_polygons(std::move(slices[layer_idx])));
|
||
}
|
||
|
||
// Trim the slices.
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate_layers.size()),
|
||
[&](const tbb::blocked_range<size_t> &range) {
|
||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx)
|
||
if (SupportGeneratorLayer *layer = intermediate_layers[layer_idx]; layer) {
|
||
Polygons &poly = intermediate_layers[layer_idx]->polygons;
|
||
poly = diff_clipped(poly, volumes.getCollision(0, layer_idx, true));
|
||
poly = intersection(poly, volumes.m_bed_area);
|
||
}
|
||
});
|
||
|
||
std::vector<Polygons> support_layer_storage(move_bounds.size());
|
||
std::vector<Polygons> support_roof_storage(move_bounds.size());
|
||
finalize_interface_and_support_areas(print_object, volumes, config, overhangs, support_layer_storage, support_roof_storage,
|
||
bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel);
|
||
}
|
||
|
||
/*!
|
||
* \brief Create the areas that need support.
|
||
*
|
||
* These areas are stored inside the given SliceDataStorage object.
|
||
* \param storage The data storage where the mesh data is gotten from and
|
||
* where the resulting support areas are stored.
|
||
*/
|
||
static void generate_support_areas(Print &print, TreeSupport* tree_support, const BuildVolume &build_volume, const std::vector<size_t> &print_object_ids, std::function<void()> throw_on_cancel)
|
||
{
|
||
// Settings with the indexes of meshes that use these settings.
|
||
std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> grouped_meshes = group_meshes(print, print_object_ids);
|
||
if (grouped_meshes.empty())
|
||
return;
|
||
|
||
size_t counter = 0;
|
||
|
||
// Process every mesh group. These groups can not be processed parallel, as the processing in each group is parallelized, and nested parallelization is disables and slow.
|
||
for (std::pair<TreeSupportSettings, std::vector<size_t>> &processing : grouped_meshes)
|
||
{
|
||
// process each combination of meshes
|
||
// this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed.
|
||
//FIXME this is a copy
|
||
// Contains config settings to avoid loading them in every function. This was done to improve readability of the code.
|
||
TreeSupportSettings &config = processing.first;
|
||
BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes.";
|
||
auto t_start = std::chrono::high_resolution_clock::now();
|
||
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
m_progress_multiplier = 1.0 / double(grouped_meshes.size());
|
||
m_progress_offset = counter == 0 ? 0 : TREE_PROGRESS_TOTAL * (double(counter) * m_progress_multiplier);
|
||
#endif // SLIC3R_TREESUPPORT_PROGRESS
|
||
PrintObject &print_object = *print.get_object(processing.second.front());
|
||
// Generator for model collision, avoidance and internal guide volumes.
|
||
TreeModelVolumes volumes{ print_object, build_volume, config.maximum_move_distance, config.maximum_move_distance_slow, processing.second.front(),
|
||
#ifdef SLIC3R_TREESUPPORTS_PROGRESS
|
||
m_progress_multiplier, m_progress_offset,
|
||
#endif // SLIC3R_TREESUPPORTS_PROGRESS
|
||
/* additional_excluded_areas */{} };
|
||
|
||
//FIXME generating overhangs just for the first mesh of the group.
|
||
assert(processing.second.size() == 1);
|
||
|
||
print.set_status(55, _L("Support: detect overhangs"));
|
||
#if 0
|
||
std::vector<Polygons> overhangs;
|
||
tree_support->detect_overhangs();
|
||
const int num_raft_layers = int(config.raft_layers.size());
|
||
const int num_layers = int(print_object.layer_count()) + num_raft_layers;
|
||
overhangs.resize(num_layers);
|
||
for (size_t i = 0; i < print_object.layer_count(); i++)
|
||
{
|
||
overhangs[i + num_raft_layers] = to_polygons(print_object.get_support_layer(i)->overhang_areas);
|
||
}
|
||
print_object.clear_support_layers();
|
||
#else
|
||
std::vector<Polygons> overhangs = generate_overhangs(config, *print.get_object(processing.second.front()), throw_on_cancel);
|
||
#endif
|
||
// ### Precalculate avoidances, collision etc.
|
||
size_t num_support_layers = precalculate(print, overhangs, processing.first, processing.second, volumes, throw_on_cancel);
|
||
|
||
bool has_support = num_support_layers > 0;
|
||
bool has_raft = config.raft_layers.size() > 0;
|
||
num_support_layers = std::max(num_support_layers, config.raft_layers.size());
|
||
|
||
if (num_support_layers == 0)
|
||
continue;
|
||
|
||
SupportParameters support_params(print_object);
|
||
support_params.with_sheath = true;
|
||
// organic support default pattern is none.
|
||
if (config.support_pattern == smpDefault) {
|
||
config.support_pattern = smpNone;
|
||
support_params.support_density = 0;
|
||
}
|
||
|
||
SupportGeneratorLayerStorage layer_storage;
|
||
SupportGeneratorLayersPtr top_contacts;
|
||
SupportGeneratorLayersPtr bottom_contacts;
|
||
SupportGeneratorLayersPtr interface_layers;
|
||
SupportGeneratorLayersPtr base_interface_layers;
|
||
SupportGeneratorLayersPtr intermediate_layers(num_support_layers, nullptr);
|
||
if (support_params.has_top_contacts || has_raft)
|
||
top_contacts.assign(num_support_layers, nullptr);
|
||
if (support_params.has_bottom_contacts)
|
||
bottom_contacts.assign(num_support_layers, nullptr);
|
||
if (support_params.has_interfaces() || has_raft)
|
||
interface_layers.assign(num_support_layers, nullptr);
|
||
if (support_params.has_base_interfaces() || has_raft)
|
||
base_interface_layers.assign(num_support_layers, nullptr);
|
||
|
||
auto remove_undefined_layers = [&bottom_contacts, &top_contacts, &interface_layers, &base_interface_layers, &intermediate_layers]() {
|
||
auto doit = [](SupportGeneratorLayersPtr& layers) {
|
||
layers.erase(std::remove_if(layers.begin(), layers.end(), [](const SupportGeneratorLayer* ptr) { return ptr == nullptr; }), layers.end());
|
||
};
|
||
doit(bottom_contacts);
|
||
doit(top_contacts);
|
||
doit(interface_layers);
|
||
doit(base_interface_layers);
|
||
doit(intermediate_layers);
|
||
};
|
||
|
||
InterfacePlacer interface_placer{
|
||
print_object.slicing_parameters(), support_params, config,
|
||
// Outputs
|
||
layer_storage, top_contacts, interface_layers, base_interface_layers };
|
||
|
||
|
||
if (has_support) {
|
||
auto t_precalc = std::chrono::high_resolution_clock::now();
|
||
// value is the area where support may be placed. As this is calculated in CreateLayerPathing it is saved and reused in draw_areas
|
||
std::vector<SupportElements> move_bounds(num_support_layers);
|
||
|
||
|
||
for (size_t mesh_idx : processing.second)
|
||
generate_initial_areas(*print.get_object(mesh_idx), volumes, config, overhangs,
|
||
move_bounds, interface_placer, throw_on_cancel);
|
||
auto t_gen = std::chrono::high_resolution_clock::now();
|
||
|
||
// save num of points to log
|
||
for (size_t i = 0; i < move_bounds.size(); i++)
|
||
BOOST_LOG_TRIVIAL(info) << "Number of points in move_bound: " << move_bounds[i].size() << " in layer " << i;
|
||
|
||
#ifdef TREESUPPORT_DEBUG_SVG
|
||
for (size_t layer_idx = 0; layer_idx < move_bounds.size(); ++layer_idx) {
|
||
Polygons polys;
|
||
for (auto& area : move_bounds[layer_idx])
|
||
append(polys, area.influence_area);
|
||
if (auto begin = move_bounds[layer_idx].begin(); begin != move_bounds[layer_idx].end())
|
||
SVG::export_expolygons(debug_out_path("treesupport-initial_areas-%d.svg", layer_idx),
|
||
{ { { union_ex(volumes.getWallRestriction(support_element_collision_radius(config, begin->state), layer_idx, begin->state.use_min_xy_dist)) },
|
||
{ "wall_restricrictions", "gray", 0.5f } },
|
||
{ { union_ex(polys) }, { "parent", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
|
||
}
|
||
#endif // TREESUPPORT_DEBUG_SVG
|
||
|
||
// ### Propagate the influence areas downwards. This is an inherently serial operation.
|
||
print.set_status(60, _L("Support: propagate branches"));
|
||
create_layer_pathing(volumes, config, move_bounds, throw_on_cancel);
|
||
auto t_path = std::chrono::high_resolution_clock::now();
|
||
|
||
// ### Set a point in each influence area
|
||
create_nodes_from_area(volumes, config, move_bounds, throw_on_cancel);
|
||
auto t_place = std::chrono::high_resolution_clock::now();
|
||
|
||
// ### draw these points as circles
|
||
indexed_triangle_set branches = draw_branches(*print.get_object(processing.second.front()), volumes, config, move_bounds, throw_on_cancel);
|
||
// Reduce memory footprint. After this point only slice_branches() will use volumes and from that only collisions with zero radius will be used.
|
||
volumes.clear_all_but_object_collision();
|
||
slice_branches(*print.get_object(processing.second.front()), volumes, config, overhangs, move_bounds, branches,
|
||
bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel);
|
||
|
||
// this new function may cause bad_function_call exception
|
||
//organic_draw_branches(
|
||
// *print.get_object(processing.second.front()), volumes, config, move_bounds,
|
||
// bottom_contacts, top_contacts, interface_placer, intermediate_layers, layer_storage,
|
||
// throw_on_cancel);
|
||
|
||
|
||
remove_undefined_layers();
|
||
|
||
std::tie(interface_layers, base_interface_layers) = generate_interface_layers(print_object.config(), support_params,
|
||
bottom_contacts, top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage);
|
||
|
||
auto t_draw = std::chrono::high_resolution_clock::now();
|
||
auto dur_pre_gen = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_precalc - t_start).count();
|
||
auto dur_gen = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_gen - t_precalc).count();
|
||
auto dur_path = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_path - t_gen).count();
|
||
auto dur_place = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_place - t_path).count();
|
||
auto dur_draw = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_draw - t_place).count();
|
||
auto dur_total = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_draw - t_start).count();
|
||
BOOST_LOG_TRIVIAL(info) <<
|
||
"Total time used creating Tree support for the currently grouped meshes: " << dur_total << " ms. "
|
||
"Different subtasks:\nCalculating Avoidance: " << dur_pre_gen << " ms "
|
||
"Creating inital influence areas: " << dur_gen << " ms "
|
||
"Influence area creation: " << dur_path << "ms "
|
||
"Placement of Points in InfluenceAreas: " << dur_place << "ms "
|
||
"Drawing result as support " << dur_draw << " ms";
|
||
|
||
move_bounds.clear();
|
||
}
|
||
else if (generate_raft_contact(print_object, config, interface_placer) >= 0) {
|
||
remove_undefined_layers();
|
||
}
|
||
else
|
||
// No raft.
|
||
continue;
|
||
|
||
// Produce the support G-code.
|
||
SupportGeneratorLayersPtr raft_layers = generate_raft_base(print_object, support_params, print_object.slicing_parameters(), top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage);
|
||
SupportGeneratorLayersPtr layers_sorted = generate_support_layers(print_object, raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers);
|
||
|
||
// Don't fill in the tree supports, make them hollow with just a single sheath line.
|
||
print.set_status(69, _L("Support: generate toolpath"));
|
||
generate_support_toolpaths(print_object, print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(),
|
||
raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers);
|
||
|
||
#if 0
|
||
//#ifdef SLIC3R_DEBUG
|
||
{
|
||
static int iRun = 0;
|
||
++ iRun;
|
||
size_t layer_id = 0;
|
||
for (int i = 0; i < int(layers_sorted.size());) {
|
||
// Find the last layer with roughly the same print_z, find the minimum layer height of all.
|
||
// Due to the floating point inaccuracies, the print_z may not be the same even if in theory they should.
|
||
int j = i + 1;
|
||
coordf_t zmax = layers_sorted[i]->print_z + EPSILON;
|
||
bool empty = layers_sorted[i]->polygons.empty();
|
||
for (; j < layers_sorted.size() && layers_sorted[j]->print_z <= zmax; ++j)
|
||
if (!layers_sorted[j]->polygons.empty())
|
||
empty = false;
|
||
if (!empty) {
|
||
export_print_z_polygons_to_svg(
|
||
debug_out_path("support-%d-%lf.svg", iRun, layers_sorted[i]->print_z).c_str(),
|
||
layers_sorted.data() + i, j - i);
|
||
export_print_z_polygons_and_extrusions_to_svg(
|
||
debug_out_path("support-w-fills-%d-%lf.svg", iRun, layers_sorted[i]->print_z).c_str(),
|
||
layers_sorted.data() + i, j - i,
|
||
*print_object.support_layers()[layer_id]);
|
||
++layer_id;
|
||
}
|
||
i = j;
|
||
}
|
||
}
|
||
#endif /* SLIC3R_DEBUG */
|
||
|
||
++ counter;
|
||
}
|
||
|
||
// storage.support.generated = true;
|
||
}
|
||
|
||
// Organic specific: Smooth branches and produce one cummulative mesh to be sliced.
|
||
void organic_draw_branches(
|
||
PrintObject& print_object,
|
||
TreeModelVolumes& volumes,
|
||
const TreeSupportSettings& config,
|
||
std::vector<SupportElements>& move_bounds,
|
||
|
||
// I/O:
|
||
SupportGeneratorLayersPtr& bottom_contacts,
|
||
SupportGeneratorLayersPtr& top_contacts,
|
||
InterfacePlacer& interface_placer,
|
||
|
||
// Output:
|
||
SupportGeneratorLayersPtr& intermediate_layers,
|
||
SupportGeneratorLayerStorage& layer_storage,
|
||
|
||
std::function<void()> throw_on_cancel)
|
||
{
|
||
// All SupportElements are put into a layer independent storage to improve parallelization.
|
||
std::vector<std::pair<SupportElement*, int>> elements_with_link_down;
|
||
std::vector<size_t> linear_data_layers;
|
||
{
|
||
std::vector<std::pair<SupportElement*, int>> map_downwards_old;
|
||
std::vector<std::pair<SupportElement*, int>> map_downwards_new;
|
||
linear_data_layers.emplace_back(0);
|
||
for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(move_bounds.size()); ++layer_idx) {
|
||
SupportElements* layer_above = layer_idx + 1 < LayerIndex(move_bounds.size()) ? &move_bounds[layer_idx + 1] : nullptr;
|
||
map_downwards_new.clear();
|
||
std::sort(map_downwards_old.begin(), map_downwards_old.end(), [](auto& l, auto& r) { return l.first < r.first; });
|
||
SupportElements& layer = move_bounds[layer_idx];
|
||
for (size_t elem_idx = 0; elem_idx < layer.size(); ++elem_idx) {
|
||
SupportElement& elem = layer[elem_idx];
|
||
int child = -1;
|
||
if (layer_idx > 0) {
|
||
auto it = std::lower_bound(map_downwards_old.begin(), map_downwards_old.end(), &elem, [](auto& l, const SupportElement* r) { return l.first < r; });
|
||
if (it != map_downwards_old.end() && it->first == &elem) {
|
||
child = it->second;
|
||
// Only one link points to a node above from below.
|
||
assert(!(++it != map_downwards_old.end() && it->first == &elem));
|
||
}
|
||
#ifndef NDEBUG
|
||
{
|
||
const SupportElement* pchild = child == -1 ? nullptr : &move_bounds[layer_idx - 1][child];
|
||
assert(pchild ? pchild->state.result_on_layer_is_set() : elem.state.target_height > layer_idx);
|
||
}
|
||
#endif // NDEBUG
|
||
}
|
||
for (int32_t parent_idx : elem.parents) {
|
||
SupportElement& parent = (*layer_above)[parent_idx];
|
||
if (parent.state.result_on_layer_is_set())
|
||
map_downwards_new.emplace_back(&parent, elem_idx);
|
||
}
|
||
|
||
elements_with_link_down.push_back({ &elem, int(child) });
|
||
}
|
||
std::swap(map_downwards_old, map_downwards_new);
|
||
linear_data_layers.emplace_back(elements_with_link_down.size());
|
||
}
|
||
}
|
||
|
||
throw_on_cancel();
|
||
|
||
organic_smooth_branches_avoid_collisions(print_object, volumes, config, elements_with_link_down, linear_data_layers, throw_on_cancel);
|
||
|
||
// Reduce memory footprint. After this point only finalize_interface_and_support_areas() will use volumes and from that only collisions with zero radius will be used.
|
||
volumes.clear_all_but_object_collision();
|
||
|
||
// Unmark all nodes.
|
||
for (SupportElements& elements : move_bounds)
|
||
for (SupportElement& element : elements)
|
||
element.state.marked = false;
|
||
|
||
// Traverse all nodes, generate tubes.
|
||
// Traversal stack with nodes and their current parent
|
||
|
||
struct Branch {
|
||
std::vector<const SupportElement*> path;
|
||
bool has_root{ false };
|
||
bool has_tip{ false };
|
||
};
|
||
|
||
struct Slice {
|
||
Polygons polygons;
|
||
Polygons bottom_contacts;
|
||
size_t num_branches{ 0 };
|
||
};
|
||
|
||
struct Tree {
|
||
std::vector<Branch> branches;
|
||
|
||
std::vector<Slice> slices;
|
||
LayerIndex first_layer_id{ -1 };
|
||
};
|
||
|
||
std::vector<Tree> trees;
|
||
|
||
struct TreeVisitor {
|
||
static void visit_recursive(std::vector<SupportElements>& move_bounds, SupportElement& start_element, Tree& out) {
|
||
assert(!start_element.state.marked && !start_element.parents.empty());
|
||
// Collect elements up to a bifurcation above.
|
||
start_element.state.marked = true;
|
||
// For each branch bifurcating from this point:
|
||
//SupportElements &layer = move_bounds[start_element.state.layer_idx];
|
||
SupportElements& layer_above = move_bounds[start_element.state.layer_idx + 1];
|
||
bool root = out.branches.empty();
|
||
for (size_t parent_idx = 0; parent_idx < start_element.parents.size(); ++parent_idx) {
|
||
Branch branch;
|
||
branch.path.emplace_back(&start_element);
|
||
// Traverse each branch until it branches again.
|
||
SupportElement& first_parent = layer_above[start_element.parents[parent_idx]];
|
||
assert(!first_parent.state.marked);
|
||
assert(branch.path.back()->state.layer_idx + 1 == first_parent.state.layer_idx);
|
||
branch.path.emplace_back(&first_parent);
|
||
if (first_parent.parents.size() < 2)
|
||
first_parent.state.marked = true;
|
||
SupportElement* next_branch = nullptr;
|
||
if (first_parent.parents.size() == 1) {
|
||
for (SupportElement* parent = &first_parent;;) {
|
||
assert(parent->state.marked);
|
||
SupportElement& next_parent = move_bounds[parent->state.layer_idx + 1][parent->parents.front()];
|
||
assert(!next_parent.state.marked);
|
||
assert(branch.path.back()->state.layer_idx + 1 == next_parent.state.layer_idx);
|
||
branch.path.emplace_back(&next_parent);
|
||
if (next_parent.parents.size() > 1) {
|
||
// Branching point was reached.
|
||
next_branch = &next_parent;
|
||
break;
|
||
}
|
||
next_parent.state.marked = true;
|
||
if (next_parent.parents.size() == 0)
|
||
// Tip is reached.
|
||
break;
|
||
parent = &next_parent;
|
||
}
|
||
}
|
||
else if (first_parent.parents.size() > 1)
|
||
// Branching point was reached.
|
||
next_branch = &first_parent;
|
||
assert(branch.path.size() >= 2);
|
||
assert(next_branch == nullptr || !next_branch->state.marked);
|
||
branch.has_root = root;
|
||
branch.has_tip = !next_branch;
|
||
out.branches.emplace_back(std::move(branch));
|
||
if (next_branch)
|
||
visit_recursive(move_bounds, *next_branch, out);
|
||
}
|
||
}
|
||
};
|
||
|
||
for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++layer_idx) {
|
||
// int ielement;
|
||
for (SupportElement& start_element : move_bounds[layer_idx]) {
|
||
if (!start_element.state.marked && !start_element.parents.empty()) {
|
||
#if 0
|
||
int found = 0;
|
||
if (layer_idx > 0) {
|
||
for (auto& el : move_bounds[layer_idx - 1]) {
|
||
for (auto iparent : el.parents)
|
||
if (iparent == ielement)
|
||
++found;
|
||
}
|
||
if (found != 0)
|
||
printf("Found: %d\n", found);
|
||
}
|
||
#endif
|
||
trees.push_back({});
|
||
TreeVisitor::visit_recursive(move_bounds, start_element, trees.back());
|
||
assert(!trees.back().branches.empty());
|
||
//FIXME debugging
|
||
#if 0
|
||
if (start_element.state.lost) {
|
||
}
|
||
else if (start_element.state.verylost) {
|
||
}
|
||
else
|
||
trees.pop_back();
|
||
#endif
|
||
}
|
||
// ++ ielement;
|
||
}
|
||
}
|
||
|
||
const SlicingParameters& slicing_params = print_object.slicing_parameters();
|
||
MeshSlicingParams mesh_slicing_params;
|
||
mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive;
|
||
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, trees.size(), trees.size()),
|
||
[&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range<size_t>& range) {
|
||
indexed_triangle_set partial_mesh;
|
||
std::vector<float> slice_z;
|
||
std::vector<Polygons> bottom_contacts;
|
||
for (size_t tree_id = range.begin(); tree_id < range.end(); ++tree_id) {
|
||
Tree& tree = trees[tree_id];
|
||
for (const Branch& branch : tree.branches) {
|
||
// Triangulate the tube.
|
||
partial_mesh.clear();
|
||
std::pair<float, float> zspan = extrude_branch(branch.path, config, slicing_params, move_bounds, partial_mesh);
|
||
LayerIndex layer_begin = branch.has_root ?
|
||
branch.path.front()->state.layer_idx :
|
||
std::min(branch.path.front()->state.layer_idx, layer_idx_ceil(slicing_params, config, zspan.first));
|
||
LayerIndex layer_end = (branch.has_tip ?
|
||
branch.path.back()->state.layer_idx :
|
||
std::max(branch.path.back()->state.layer_idx, layer_idx_floor(slicing_params, config, zspan.second))) + 1;
|
||
slice_z.clear();
|
||
for (LayerIndex layer_idx = layer_begin; layer_idx < layer_end; ++layer_idx) {
|
||
const double print_z = layer_z(slicing_params, config, layer_idx);
|
||
const double bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.;
|
||
slice_z.emplace_back(float(0.5 * (bottom_z + print_z)));
|
||
}
|
||
std::vector<Polygons> slices = slice_mesh(partial_mesh, slice_z, mesh_slicing_params, throw_on_cancel);
|
||
bottom_contacts.clear();
|
||
//FIXME parallelize?
|
||
for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i)
|
||
slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); //FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist);
|
||
|
||
size_t num_empty = 0;
|
||
if (slices.front().empty()) {
|
||
// Some of the initial layers are empty.
|
||
num_empty = std::find_if(slices.begin(), slices.end(), [](auto& s) { return !s.empty(); }) - slices.begin();
|
||
}
|
||
else {
|
||
if (branch.has_root) {
|
||
if (branch.path.front()->state.to_model_gracious) {
|
||
if (config.settings.support_floor_layers > 0)
|
||
//FIXME one may just take the whole tree slice as bottom interface.
|
||
bottom_contacts.emplace_back(intersection_clipped(slices.front(), volumes.getPlaceableAreas(0, layer_begin, [] {})));
|
||
}
|
||
else if (layer_begin > 0) {
|
||
// Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something.
|
||
struct BottomExtraSlice {
|
||
Polygons polygons;
|
||
double area;
|
||
};
|
||
std::vector<BottomExtraSlice> bottom_extra_slices;
|
||
Polygons rest_support;
|
||
coord_t bottom_radius = support_element_radius(config, *branch.path.front());
|
||
// Don't propagate further than 1.5 * bottom radius.
|
||
//LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height;
|
||
LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height;
|
||
LayerIndex layer_bottommost = branch.path.front()->state.verylost ?
|
||
// If the tree bottom is hanging in the air, bring it down to some surface.
|
||
0 :
|
||
//FIXME the "verylost" branches should stop when crossing another support.
|
||
std::max(0, layer_begin - layers_propagate_max);
|
||
double support_area_min_radius = M_PI * sqr(double(config.branch_radius));
|
||
double support_area_stop = std::max(0.2 * M_PI * sqr(double(bottom_radius)), 0.5 * support_area_min_radius);
|
||
// Only propagate until the rest area is smaller than this threshold.
|
||
//double support_area_min = 0.1 * support_area_min_radius;
|
||
for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; --layer_idx) {
|
||
rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false));
|
||
double rest_support_area = area(rest_support);
|
||
if (rest_support_area < support_area_stop)
|
||
// Don't propagate a fraction of the tree contact surface.
|
||
break;
|
||
bottom_extra_slices.push_back({ rest_support, rest_support_area });
|
||
}
|
||
// Now remove those bottom slices that are not supported at all.
|
||
#if 0
|
||
while (!bottom_extra_slices.empty()) {
|
||
Polygons this_bottom_contacts = intersection_clipped(
|
||
bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {}));
|
||
if (area(this_bottom_contacts) < support_area_min)
|
||
bottom_extra_slices.pop_back();
|
||
else {
|
||
// At least a fraction of the tree bottom is considered to be supported.
|
||
if (config.settings.support_floor_layers > 0)
|
||
// Turn this fraction of the tree bottom into a contact layer.
|
||
bottom_contacts.emplace_back(std::move(this_bottom_contacts));
|
||
break;
|
||
}
|
||
}
|
||
#endif
|
||
if (config.settings.support_floor_layers > 0)
|
||
for (int i = int(bottom_extra_slices.size()) - 2; i >= 0; --i)
|
||
bottom_contacts.emplace_back(
|
||
intersection_clipped(bottom_extra_slices[i].polygons, volumes.getPlaceableAreas(0, layer_begin - i - 1, [] {})));
|
||
layer_begin -= LayerIndex(bottom_extra_slices.size());
|
||
slices.insert(slices.begin(), bottom_extra_slices.size(), {});
|
||
auto it_dst = slices.begin();
|
||
for (auto it_src = bottom_extra_slices.rbegin(); it_src != bottom_extra_slices.rend(); ++it_src)
|
||
*it_dst++ = std::move(it_src->polygons);
|
||
}
|
||
}
|
||
|
||
#if 0
|
||
//FIXME branch.has_tip seems to not be reliable.
|
||
if (branch.has_tip && interface_placer.support_parameters.has_top_contacts)
|
||
// Add top slices to top contacts / interfaces / base interfaces.
|
||
for (int i = int(branch.path.size()) - 1; i >= 0; --i) {
|
||
const SupportElement& el = *branch.path[i];
|
||
if (el.state.missing_roof_layers == 0)
|
||
break;
|
||
//FIXME Move or not?
|
||
interface_placer.add_roof(std::move(slices[int(slices.size()) - i - 1]), el.state.layer_idx,
|
||
interface_placer.support_parameters.num_top_interface_layers + 1 - el.state.missing_roof_layers);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
layer_begin += LayerIndex(num_empty);
|
||
while (!slices.empty() && slices.back().empty()) {
|
||
slices.pop_back();
|
||
--layer_end;
|
||
}
|
||
if (layer_begin < layer_end) {
|
||
LayerIndex new_begin = tree.first_layer_id == -1 ? layer_begin : std::min(tree.first_layer_id, layer_begin);
|
||
LayerIndex new_end = tree.first_layer_id == -1 ? layer_end : std::max(tree.first_layer_id + LayerIndex(tree.slices.size()), layer_end);
|
||
size_t new_size = size_t(new_end - new_begin);
|
||
if (tree.first_layer_id == -1) {
|
||
}
|
||
else if (tree.slices.capacity() < new_size) {
|
||
std::vector<Slice> new_slices;
|
||
new_slices.reserve(new_size);
|
||
if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0)
|
||
new_slices.insert(new_slices.end(), dif, {});
|
||
append(new_slices, std::move(tree.slices));
|
||
tree.slices.swap(new_slices);
|
||
}
|
||
else if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0)
|
||
tree.slices.insert(tree.slices.begin(), tree.first_layer_id - new_begin, {});
|
||
tree.slices.insert(tree.slices.end(), new_size - tree.slices.size(), {});
|
||
layer_begin -= LayerIndex(num_empty);
|
||
for (LayerIndex i = layer_begin; i != layer_end; ++i) {
|
||
int j = i - layer_begin;
|
||
if (Polygons& src = slices[j]; !src.empty()) {
|
||
Slice& dst = tree.slices[i - new_begin];
|
||
if (++dst.num_branches > 1) {
|
||
append(dst.polygons, std::move(src));
|
||
if (j < int(bottom_contacts.size()))
|
||
append(dst.bottom_contacts, std::move(bottom_contacts[j]));
|
||
}
|
||
else {
|
||
dst.polygons = std::move(std::move(src));
|
||
if (j < int(bottom_contacts.size()))
|
||
dst.bottom_contacts = std::move(bottom_contacts[j]);
|
||
}
|
||
}
|
||
}
|
||
tree.first_layer_id = new_begin;
|
||
}
|
||
}
|
||
}
|
||
}, tbb::simple_partitioner());
|
||
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, trees.size(), 1),
|
||
[&trees, &throw_on_cancel](const tbb::blocked_range<size_t>& range) {
|
||
for (size_t tree_id = range.begin(); tree_id < range.end(); ++tree_id) {
|
||
Tree& tree = trees[tree_id];
|
||
for (Slice& slice : tree.slices)
|
||
if (slice.num_branches > 1) {
|
||
slice.polygons = union_(slice.polygons);
|
||
slice.bottom_contacts = union_(slice.bottom_contacts);
|
||
slice.num_branches = 1;
|
||
}
|
||
throw_on_cancel();
|
||
}
|
||
}, tbb::simple_partitioner());
|
||
|
||
size_t num_layers = 0;
|
||
for (Tree& tree : trees)
|
||
if (tree.first_layer_id >= 0)
|
||
num_layers = std::max(num_layers, size_t(tree.first_layer_id + tree.slices.size()));
|
||
|
||
std::vector<Slice> slices(num_layers, Slice{});
|
||
for (Tree& tree : trees)
|
||
if (tree.first_layer_id >= 0) {
|
||
for (LayerIndex i = tree.first_layer_id; i != tree.first_layer_id + LayerIndex(tree.slices.size()); ++i)
|
||
if (Slice& src = tree.slices[i - tree.first_layer_id]; !src.polygons.empty()) {
|
||
Slice& dst = slices[i];
|
||
if (++dst.num_branches > 1) {
|
||
append(dst.polygons, std::move(src.polygons));
|
||
append(dst.bottom_contacts, std::move(src.bottom_contacts));
|
||
}
|
||
else {
|
||
dst.polygons = std::move(src.polygons);
|
||
dst.bottom_contacts = std::move(src.bottom_contacts);
|
||
}
|
||
}
|
||
}
|
||
|
||
tbb::parallel_for(tbb::blocked_range<size_t>(0, std::min(move_bounds.size(), slices.size()), 1),
|
||
[&print_object, &config, &slices, &bottom_contacts, &top_contacts, &intermediate_layers, &layer_storage, &throw_on_cancel](const tbb::blocked_range<size_t>& range) {
|
||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
|
||
Slice& slice = slices[layer_idx];
|
||
assert(intermediate_layers[layer_idx] == nullptr);
|
||
Polygons base_layer_polygons = slice.num_branches > 1 ? union_(slice.polygons) : std::move(slice.polygons);
|
||
Polygons bottom_contact_polygons = slice.num_branches > 1 ? union_(slice.bottom_contacts) : std::move(slice.bottom_contacts);
|
||
|
||
if (!base_layer_polygons.empty()) {
|
||
// Most of the time in this function is this union call. Can take 300+ ms when a lot of areas are to be unioned.
|
||
base_layer_polygons = smooth_outward(union_(base_layer_polygons), config.support_line_width); //FIXME was .smooth(50);
|
||
//smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) :
|
||
// simplify a bit, to ensure the output does not contain outrageous amounts of vertices. Should not be necessary, just a precaution.
|
||
base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled<double>(0.03), double(config.resolution)));
|
||
}
|
||
|
||
// Subtract top contact layer polygons from support base.
|
||
SupportGeneratorLayer* top_contact_layer = top_contacts.empty() ? nullptr : top_contacts[layer_idx];
|
||
if (top_contact_layer && !top_contact_layer->polygons.empty() && !base_layer_polygons.empty()) {
|
||
base_layer_polygons = diff(base_layer_polygons, top_contact_layer->polygons);
|
||
if (!bottom_contact_polygons.empty())
|
||
//FIXME it may be better to clip bottom contacts with top contacts first after they are propagated to produce interface layers.
|
||
bottom_contact_polygons = diff(bottom_contact_polygons, top_contact_layer->polygons);
|
||
}
|
||
if (!bottom_contact_polygons.empty()) {
|
||
base_layer_polygons = diff(base_layer_polygons, bottom_contact_polygons);
|
||
SupportGeneratorLayer* bottom_contact_layer = bottom_contacts[layer_idx] = &layer_allocate(
|
||
layer_storage, SupporLayerType::sltBottomContact, print_object.slicing_parameters(), config, layer_idx);
|
||
bottom_contact_layer->polygons = std::move(bottom_contact_polygons);
|
||
}
|
||
if (!base_layer_polygons.empty()) {
|
||
SupportGeneratorLayer* base_layer = intermediate_layers[layer_idx] = &layer_allocate(
|
||
layer_storage, SupporLayerType::sltBase, print_object.slicing_parameters(), config, layer_idx);
|
||
base_layer->polygons = union_(base_layer_polygons);
|
||
}
|
||
|
||
throw_on_cancel();
|
||
}
|
||
}, tbb::simple_partitioner());
|
||
}
|
||
|
||
|
||
} // namespace TreeSupport3D
|
||
|
||
void generate_tree_support_3D(PrintObject &print_object, TreeSupport* tree_support, std::function<void()> throw_on_cancel)
|
||
{
|
||
size_t idx = 0;
|
||
for (const PrintObject *po : print_object.print()->objects()) {
|
||
if (po == &print_object)
|
||
break;
|
||
++idx;
|
||
}
|
||
|
||
Points bedpts = tree_support->m_machine_border.contour.points;
|
||
BuildVolume build_volume{ Pointfs{ unscaled(bedpts[0]), unscaled(bedpts[1]),unscaled(bedpts[2]),unscaled(bedpts[3])}, tree_support->m_print_config->printable_height };
|
||
|
||
TreeSupport3D::generate_support_areas(*print_object.print(), tree_support, build_volume, { idx }, throw_on_cancel);
|
||
}
|
||
|
||
} // namespace Slic3r
|