Compare commits

...

203 Commits

Author SHA1 Message Date
Oğuzhan Koral 6053d3eac1 Fix (Attributes): Rescue from problematic dictionaries 2023-01-20 11:50:18 +03:00
oguzhankoral 529830f36b Rescue from problematic dictionaries 2023-01-20 11:47:34 +03:00
Oğuzhan Koral 505cf6265c Feat (Scene): Send and receive scenes 2023-01-19 20:09:19 +03:00
oguzhankoral 8cd9673eec Send scenes to speckle 2023-01-19 20:05:44 +03:00
oguzhankoral ac9cb28558 Receive named views as scene 2023-01-19 16:38:32 +03:00
Oğuzhan Koral 6eefe0698c Fix (Branch): Activate created branch 2023-01-19 12:17:58 +03:00
oguzhankoral d0113532b6 Activate created branch 2023-01-19 11:09:23 +03:00
Oğuzhan Koral 5a1d2ad5f4 Feat (Branch): Create branch button
Thanks Fabians for helps
2023-01-18 17:02:20 +03:00
oguzhankoral dfe02f4c74 Rename CreateBranchDialog 2023-01-18 16:58:05 +03:00
oguzhankoral 5349d556b9 Group dialogs to folder 2023-01-18 16:21:43 +03:00
oguzhankoral 7f7d8a501b Add notes about refresh 2023-01-18 15:58:43 +03:00
oguzhankoral cbee0e465b Remove unused combineFacesByMaterialHandler method 2023-01-18 15:44:20 +03:00
oguzhankoral faf00f0b0d Refetch stream on StreamCard whenever branch created 2023-01-18 15:42:33 +03:00
oguzhankoral fa97da5781 Track create stream and branch via mixpanel 2023-01-18 15:03:14 +03:00
oguzhankoral e7bab546db Remove loggings 2023-01-18 14:16:52 +03:00
oguzhankoral e2f4a30b5b Fix updating problem on created branch
- Thanks to Fabians
2023-01-18 11:53:50 +03:00
oguzhankoral ccff1df041 Add tooltip to create branch method 2023-01-18 11:53:11 +03:00
oguzhankoral 2037cfc25a Import and use CreateBranchDialog per stream card 2023-01-17 19:24:53 +03:00
oguzhankoral 268a091d8a Add dialog for branch creation 2023-01-17 19:23:44 +03:00
Oğuzhan Koral 6b52dfab3e Chore (Theming): Store light mode setting separately 2023-01-17 16:16:39 +03:00
oguzhankoral 839999851f Correct preference hash for theming 2023-01-17 16:12:21 +03:00
oguzhankoral a87470b7b5 Split Sketchup settings 2023-01-17 16:11:56 +03:00
Oğuzhan Koral e76aeb80fd Fix (attributes): Use from_face method to face consider attributes 2023-01-11 12:05:47 +03:00
oguzhankoral 28292e59e2 Use from_face method to consider attributes 2023-01-11 11:59:50 +03:00
Oğuzhan Koral 25dda481b2 Comment out vertex count log 2023-01-10 16:47:42 +03:00
Oğuzhan Koral bbda233fd8 Comment out vertex count log 2023-01-10 16:46:56 +03:00
Oğuzhan Koral 349218f0b5 Feat (Mesh): mesh improvements
Mesh grouping methods are improved with options:

Shared vertices (It is not supported by viewer currently, but when available it is ready to approach)
Separated vertices
2023-01-10 14:36:11 +03:00
oguzhankoral f18d00a69d Remove disable rubocop issues 2023-01-10 14:30:28 +03:00
oguzhankoral 25ea6504de Note about when viewer supports shared vertices 2023-01-10 11:33:56 +03:00
oguzhankoral 43081c70e2 Send vertices separately 2023-01-10 10:51:03 +03:00
oguzhankoral 0fde1c2026 Optimize meshes with dynamic vertex adding 2023-01-10 09:14:11 +03:00
Oğuzhan Koral b35383571e Merge pull request #126 from specklesystems/gergo/updateCiContext
use innosetup context in the windows build
2023-01-09 14:57:18 +03:00
Oğuzhan Koral 45a84847a2 Fix (block): Base point for block definition 2023-01-08 17:41:56 +03:00
oguzhankoral 70d92f26d6 Note for reason to having block definition base points 2023-01-08 17:40:57 +03:00
oguzhankoral 737ed86e69 Comparison method for point object 2023-01-08 15:15:17 +03:00
oguzhankoral 3865057b7a Fallback geometry for block definition 2023-01-08 14:50:47 +03:00
oguzhankoral 42a84dcd86 Receive blocks from rhino 2023-01-07 00:35:02 +03:00
Gergő Jedlicska e2d819c59d Merge branch 'main' of github.com:specklesystems/speckle-sketchup into gergo/updateCiContext 2023-01-06 14:30:16 +01:00
oguzhankoral bfee6a88dc Add base point for block definition 2023-01-06 16:28:32 +03:00
Oğuzhan Koral 68f3be17df Fix (UI): closing UI cause state loss 2023-01-06 16:25:52 +03:00
oguzhankoral 929c97ff5e Bring to front dialog if it is minimized when user reclicked UI button 2023-01-06 16:20:53 +03:00
oguzhankoral 4b66a2e4d0 Reset dialog if it's closed 2023-01-06 16:20:32 +03:00
Gergő Jedlicska 46e740154e use innosetup context in the windows build 2023-01-06 14:03:50 +01:00
Oğuzhan Koral 05e89f49da Feat (attributes): Send/receive entity attributes 2023-01-06 04:39:47 +03:00
oguzhankoral 358e9071e3 Disable nested groups for now 2023-01-06 04:36:43 +03:00
oguzhankoral e37b6a1cc0 Fix receive groups as group
Previously groups were receiving as component
2023-01-06 03:45:21 +03:00
oguzhankoral 266721973b Check block instance and definition's sketchup_attributes 2023-01-06 03:17:45 +03:00
oguzhankoral 7c27ac85cb Check having sketchup_attributes already in line and mesh before apply dicts 2023-01-06 02:36:06 +03:00
oguzhankoral 4b79732e38 Comment out UI dev mode 2023-01-06 01:51:45 +03:00
oguzhankoral 2ceeea5298 Send attributes according to model preference 2023-01-06 01:41:41 +03:00
oguzhankoral 3ec659a59b Write definition dictionaries on receive 2023-01-06 01:22:58 +03:00
oguzhankoral 4309056851 Return if dictionaries nil 2023-01-06 01:22:58 +03:00
oguzhankoral b768f20f7a Include entity attributes on send/receive 2023-01-06 01:22:58 +03:00
oguzhankoral b3a71bcf53 Stage sqlite3 2 2023-01-06 01:22:58 +03:00
oguzhankoral 50c199bc03 Stage sqlite3 2023-01-06 01:22:58 +03:00
Oğuzhan Koral d6302ac128 Feat (settings): Settings dialog implemented 2023-01-06 01:22:36 +03:00
oguzhankoral 6a5d9e1394 Fix rubocop issues 2023-01-05 22:56:53 +03:00
oguzhankoral ac5fc3e6ea Improve positioning for switches 2023-01-05 22:50:18 +03:00
oguzhankoral aa6cbceeb9 Consider strategies on send/receive 2023-01-05 22:29:56 +03:00
oguzhankoral 46a7395382 Sync speckle user_state with UI 2023-01-05 22:29:56 +03:00
oguzhankoral a782811dad Assign preferences to data of App 2023-01-05 22:29:56 +03:00
oguzhankoral d22039bc96 Get rid of storing theming on localStorage
It stores now on database and sync with it
2023-01-05 22:29:56 +03:00
oguzhankoral 15539c258e Update config.db when theme has changed 2023-01-05 22:29:56 +03:00
oguzhankoral f9ca4acf16 Add unit tests for sqlite3 2023-01-05 22:29:56 +03:00
oguzhankoral 66d2a9b7fe Update sqlite3 submodule reference hash 2023-01-05 22:29:56 +03:00
oguzhankoral 6dff8c3221 Create test.db file for unit tests 2023-01-05 22:29:56 +03:00
oguzhankoral f13c65e083 Update sqlite3_27.so with read/write database 2023-01-05 22:29:56 +03:00
oguzhankoral 56a7d5cb86 Move theme to settings 2023-01-05 22:29:56 +03:00
oguzhankoral c63c0675d5 Init settings dialog 2023-01-05 22:29:56 +03:00
oguzhankoral 22bc4b8c9e Log upload time 2023-01-05 22:29:56 +03:00
Gergő Jedlicska ead17b8906 Merge pull request #122 from specklesystems/gergo/updateCiContext
add CI context reference to deploy job
2023-01-05 18:21:52 +01:00
Gergő Jedlicska 1a211daac2 make sure dir 2023-01-05 17:58:52 +01:00
Gergő Jedlicska c7e502da4e remove gh bot context 2023-01-05 17:56:53 +01:00
Gergő Jedlicska 70df5e6cec how touching 2023-01-05 17:55:56 +01:00
Gergő Jedlicska 793f287c35 make sure to create the known hosts 2023-01-05 17:53:46 +01:00
Gergő Jedlicska 8e8b1c60b8 use ssh cloning 2023-01-05 17:51:58 +01:00
Gergő Jedlicska ba2cd51852 rename gh token env var 2023-01-05 17:05:53 +01:00
Gergő Jedlicska 3986a4ef60 add gh devbot context 2023-01-05 17:03:38 +01:00
Gergő Jedlicska c99d89fb11 add CI context reference to deploy job 2023-01-05 16:59:23 +01:00
Oğuzhan Koral 0a9c33de91 Fix (Material): Send back material as fallback for front 2023-01-03 22:01:13 +03:00
oguzhankoral 88c940fc53 Send back material as fallback for front 2023-01-03 21:59:58 +03:00
Oğuzhan Koral 88c861cbde Fix (group): Add missing subgroup conversions 2023-01-03 21:19:44 +03:00
oguzhankoral cd071ca144 Fix missing group conversions 2023-01-03 21:17:37 +03:00
Oğuzhan Koral 9970b8ec36 Feat (Component): Store always face camera option for definitions 2023-01-02 22:24:58 +03:00
oguzhankoral ffc564becd Store always face camera option for defitions 2023-01-02 22:22:01 +03:00
Oğuzhan Koral 6289fd5941 Fix (blocks): Remove bbox and base_point from block objects 2023-01-02 18:05:12 +03:00
oguzhankoral 7f44fe76c7 Remove bbox and base_point from block objects 2023-01-02 18:03:50 +03:00
Oğuzhan Koral ea86dc6785 Fix (edge): Check all definition entities has any orphan edge 2023-01-02 15:56:33 +03:00
oguzhankoral 754f9e1ed1 Check all definition entities has any orphan edge 2023-01-02 15:51:54 +03:00
Oğuzhan Koral 9a1a02e664 Fix (accounts): Disable streams when there are no account and show message 2022-12-21 11:31:18 +03:00
oguzhankoral 1e92195355 Disable streams when there are no account and show message 2022-12-21 11:24:11 +03:00
Oğuzhan Koral bb63c1c990 Chore (Dependency) Update speckle object loader to 2.11.4
Update speckle object loader to 2.11.4
2022-12-21 00:45:56 +03:00
oguzhankoral 5af494de76 Update speckle object loader to 2.11.4
Receiving on big models there was null reference error on loader, which is fixed by Dim and Gergö
2022-12-20 18:54:48 +03:00
Oğuzhan Koral 5bcac73a45 Fix: Check object is hash before checking have speckle_object 2022-12-15 09:16:43 +03:00
oguzhankoral 7a709e48d7 Check object is hash before checking have speckle_object 2022-12-15 09:06:56 +03:00
Oğuzhan Koral d2d9fe010e Comment out console logging
Comment out console logging
2022-12-14 16:11:02 +03:00
oguzhankoral 4b6b129934 Comment out console logging 2022-12-14 16:10:03 +03:00
Oğuzhan Koral 7645146d77 Feat (DisplayValue): Support display value as base object
Support display value as base object
2022-12-14 16:07:09 +03:00
oguzhankoral 9b8cca38ae Support display value as base object 2022-12-14 16:04:23 +03:00
Oğuzhan Koral e872fda3f2 Feat (Group): Send meshes separately under the group
Feat (Group): Send meshes separately under the group
2022-12-13 14:01:49 +03:00
oguzhankoral 6ccb03c557 Improve clean up with scoped faces 2022-12-13 11:55:22 +03:00
oguzhankoral b8237b1be7 Send meshes separately under the group 2022-12-13 10:32:59 +03:00
Oğuzhan Koral 68e19cf8e4 Fix (type): Do not convert if entity type does not match with supported types
Fix (type): Do not convert if entity type does not match with supported types
2022-12-12 17:52:18 +03:00
oguzhankoral 91fba729b8 Return nothing if type does not match 2022-12-12 17:50:43 +03:00
Oğuzhan Koral 0af0e09b6e Fix (typo): Typo on keyword parameter values
Fix (typo): Typo on keyword parameter values
2022-12-12 17:38:04 +03:00
oguzhankoral 1276d56627 Fix typo 2022-12-12 17:37:00 +03:00
Oğuzhan Koral bd2773f9c1 Move base objects and their traversing process to Ruby
Move base objects and their traversing process to Ruby
2022-12-12 16:50:29 +03:00
oguzhankoral b8b1b8ef36 Fix rubocop issues and add documentation 2022-12-12 14:34:11 +03:00
oguzhankoral 0f98d72e30 Remove transport tests 2022-12-12 14:08:17 +03:00
oguzhankoral 44ec7734d4 Mark BaseObjectSerializer as deprecated in js 2022-12-12 13:52:57 +03:00
oguzhankoral c5adb063ca Add traversing benchmark for js 2022-12-12 13:18:42 +03:00
oguzhankoral 83ef8f9789 Benchmark for traversing 2022-12-12 13:03:55 +03:00
oguzhankoral b8a803a641 Replace ruby batches with javascript batches 2022-12-12 12:59:19 +03:00
oguzhankoral 31c0effebe Add inline documentation for serialization.js 2022-12-12 12:59:01 +03:00
oguzhankoral e90c4b3eb5 Traverse and serialize base object with send info 2022-12-12 12:57:07 +03:00
oguzhankoral ae6efcc27d Add sketchup unit value to sketchup state 2022-12-12 12:51:45 +03:00
oguzhankoral 7922ac127b Format callback message according to being string or not 2022-12-12 12:50:53 +03:00
oguzhankoral 559f0443af Inherit from Base for all Speckle objects 2022-12-12 12:43:25 +03:00
oguzhankoral 4e3faf1573 Port base object serializer to ruby from js 2022-12-12 12:42:30 +03:00
oguzhankoral e3039bd2d8 Add many to one relation immutable class 2022-12-08 17:49:24 +03:00
oguzhankoral e668cba839 Add immutable_ruby exterior library 2022-12-08 12:42:46 +03:00
oguzhankoral fcc47c0b7f Add dictionary handlers 2022-12-07 15:30:09 +03:00
oguzhankoral 2f189bc2ff Inherit from Base object for geometric speckle objects 2022-12-07 11:34:13 +03:00
oguzhankoral ee9d134f0e Line inherits from Base 2022-12-07 11:34:13 +03:00
oguzhankoral c1fad7cc02 Add test send batch for single line 2022-12-07 11:34:09 +03:00
Oğuzhan Koral 3a48b4ee4f Feat (Stream): Create stream by UI
Feat (Stream): Create stream by UI
2022-12-07 10:26:02 +03:00
oguzhankoral ec626f9741 Refresh streams after creation 2022-12-07 09:27:03 +03:00
oguzhankoral 90906cdc0e Disable Add a Stream by ID or URL dialog 2022-12-07 09:13:18 +03:00
oguzhankoral a694527645 Create stream on active account 2022-12-06 19:58:15 +03:00
oguzhankoral bd68b20d31 Add initial create stream component 2022-12-06 12:05:05 +03:00
Oğuzhan Koral 080d976400 Fix (Groups): Enable nested group send/receive
Fix (Groups): Enable nested group send/receive
2022-12-05 15:56:26 +03:00
oguzhankoral e200a8525b Enable nested group send/receive 2022-12-05 15:52:57 +03:00
Oğuzhan Koral 5bdf7abc2b Fix (Block): Fix missing references and typos
Fix (Block): Fix missing references and typos
2022-12-02 18:46:25 +03:00
oguzhankoral 9faa1d2e1e Fix typos 2022-12-02 18:41:20 +03:00
Oğuzhan Koral f586353cdb Feat (Layers): Support layers
Feat (Layers): Layers are available by Sketchup connector
2022-12-01 19:53:28 +03:00
oguzhankoral 2c9f2ee5cf Fix rubocop issues 2022-12-01 19:49:09 +03:00
oguzhankoral d8bd45bf54 Document layer implementations 2022-12-01 19:04:40 +03:00
oguzhankoral 9341e0b500 Remove commit layer folder 2022-12-01 18:08:09 +03:00
oguzhankoral eebf303f3e Introduce dynamic base object 2022-12-01 14:52:40 +03:00
oguzhankoral df487d5488 Pass layer information to native converter to place them into correct layers 2022-12-01 14:52:11 +03:00
oguzhankoral 7b66da5823 Pass stream and branch information to ruby
- This is needed when we create layers
2022-12-01 14:51:22 +03:00
oguzhankoral 3a929c2a03 Change target sketchup version with 2021 for rubocop 2022-12-01 14:50:04 +03:00
oguzhankoral a8ac3c4771 Send objects with layers to server 2022-11-30 23:58:23 +03:00
oguzhankoral 8d223786b9 Pass base object from ruby instead javascript 2022-11-30 23:57:55 +03:00
oguzhankoral 068c6a6ea0 Disable stripping @ for detaching 2022-11-30 23:57:34 +03:00
oguzhankoral 0db677121a Pass base object to BaseObjectSerializer prepared by ruby 2022-11-30 23:57:09 +03:00
Oğuzhan Koral 7591170bca Fix (Mesh): Do not merge SKP meshes
Fix (Mesh): Do not merge SKP meshes
2022-11-28 11:00:59 +03:00
oguzhankoral 7272541826 Check upcoming mesh comes from SKP or not 2022-11-28 10:53:32 +03:00
oguzhankoral 2460912532 Feat(Attributes): Store face soften property
This information will help us to create same geometry that sent.
2022-11-28 08:50:24 +03:00
oguzhankoral c1e4c22e3f Fix typo 2022-11-28 08:47:22 +03:00
oguzhankoral 17bd17fa3c Get/set correct face smooth properties from/to mesh 2022-11-25 21:40:39 +03:00
oguzhankoral 05da3ccb66 Add sketchup_attributes to speckle objects 2022-11-25 21:33:04 +03:00
oguzhankoral 578044884c Convert cleanup with static methods 2022-11-25 21:32:31 +03:00
oguzhankoral 226148d04f Chore(Readme): Update README with Repo Structure and contribution guidelines
Chore(Readme): Update README with Repo Structure and contribution guidelines
2022-11-25 11:54:39 +03:00
oguzhankoral c44c7516ff Update README with Repo Structure and contribution guidelines 2022-11-25 11:50:44 +03:00
oguzhankoral c3c0749222 Chore (sqlite3): Add sqlite3 project as submodule
Chore (sqlite3): Add sqlite3 project as submodule
2022-11-24 12:06:54 +03:00
oguzhankoral b7e63b0e54 Add sketchup-sqlite3 repo as submodule 2022-11-24 11:31:55 +03:00
oguzhankoral 60cd2cb0f9 Remove ported sqlite3 compiler from sketchup repo 2022-11-24 11:29:46 +03:00
oguzhankoral e6dd630caf Feat (sqlite3): compile sqlite3 from c as sketchup compatible
Feat (sqlite3): compile sqlite3 from c as sketchup compatible
2022-11-24 03:14:34 +03:00
oguzhankoral cad14b318a Use native sqlite3 module for queries 2022-11-24 03:04:34 +03:00
oguzhankoral 4df1cc17bf Add switch for operating system 2022-11-24 03:04:09 +03:00
oguzhankoral 884bb331b3 Add compiled sqlite3_27.so file for windows 2022-11-24 03:03:29 +03:00
oguzhankoral ff4a83af47 Add ruby version number constant 2022-11-24 03:02:58 +03:00
oguzhankoral f6f323b307 Remove externally compiled files, was workaround 2022-11-24 03:02:24 +03:00
oguzhankoral 330280d611 Implement conversion methods from C/C++ to Ruby for our connector 2022-11-24 03:01:30 +03:00
oguzhankoral 809432cbbd Feat (Converters): refactor and document converters
ToSpeckle and ToNative modules are refactored with classes that parallels to objects in the Speckle.

 Length
 Point
 Vector
 Plane
 BoundingBox
 Plane
 Mesh
 Transform
 RenderMaterial
 BlockDefinition
 BlockInstance
 Interval
2022-11-23 16:37:12 +03:00
oguzhankoral d64fee1d15 Refactor ToNative methods 2022-11-23 16:31:19 +03:00
oguzhankoral 3d01e15710 Pass data to reload_accounts action 2022-11-23 13:10:47 +03:00
oguzhankoral c2d7a5aca8 Extract block definition and block instance to related objects 2022-11-22 12:28:08 +03:00
oguzhankoral 2e8a040210 Implement speckle Mesh and RenderMaterial objects 2022-11-22 10:55:16 +03:00
oguzhankoral 50886147ae Use typescript objects as parent for speckle objects 2022-11-22 10:13:28 +03:00
oguzhankoral c0f2885de6 Introduce Speckle Geometry objects for conversion 2022-11-22 01:58:23 +03:00
oguzhankoral e80574ecd7 Add accounts to speckle state 2022-11-21 18:08:36 +03:00
oguzhankoral 7cdcf9e86f Get rid of from Sketchup.active_model and duplications 2022-11-21 17:52:05 +03:00
oguzhankoral 304720fc8e Add sketchup state to application state
- This will help to reach sketchup model instead of calling Sketchup.active_model everytime
2022-11-21 16:19:49 +03:00
oguzhankoral 88adfb1446 Fix(circle-ci): Rename html with vue_ui for built files
Fix(circle-ci): Rename html with vue_ui for built files
2022-11-20 15:02:05 +03:00
oguzhankoral f8719d0912 Rename html with vue_ui for built files 2022-11-20 15:01:23 +03:00
oguzhankoral c6d36fccd9 Fix: Save/Load streams locally
Fix: Save/Load streams locally
2022-11-17 16:24:00 +03:00
oguzhankoral 13d061f7e9 Remove unnessary calls 2022-11-17 16:19:58 +03:00
oguzhankoral ba11c5beb2 Fix: Disable one click send
Fix: Disable one click send
2022-11-17 09:32:45 +03:00
oguzhankoral c1800d9a02 Disable one click send 2022-11-17 09:30:13 +03:00
oguzhankoral 868859ac83 Feat(tools): add compiler project for sqlite3 and sqlite3 ruby files
* Ignore also old (html) ui built folder

* Ignore sqlite3 ext library ide settings

* Initialize sqlite3 project for .so and .bundle files

* Enable _MSC_VER check on 2.7

* Initialize ruby cpp transition file

* Communicate first time with sqlite3 c base code in Sketchup

* Update compiler project with sqlite3 cpp files

* Add sqlite3 ruby source files to ext libraries

* Update gitignore with Release and Debug folders of sqlite3 compiler

* Exclude rubocops for ext libraries

* Exclude rubocops for _sqlite3 compiler

* Fix rubocop issues
2022-11-16 14:18:28 +03:00
oguzhankoral d91114b340 Refactor: Implement all commands to new design
* Add command for receive_objects

* Fix path on sqlite3 gem

* Add commands

- init_local_accounts
- queue_send
- send_from_queue
- save_stream
- remove_stream
- reload_accounts
- load_saved_streams

* Disable other toolbar commands unless speckle initialized

* Introduce one click send
2022-11-11 21:54:23 +03:00
oguzhankoral 0dc4d46fa3 Refactor: Implement new plugin design
* Move icons to img folder

* Rename built folder name from html to vue_ui

* Rename main to bootstrap

* Remove unnecessary module for SpeckleSystems

* Replace dialog with App-View-Command design

* Fix rubocop issues

- disable to_native and to_speckle for now, they will be handled in different PR
2022-11-08 21:18:16 +03:00
oguzhankoral 0442aaaa7f CI: Add rubocop and rubycritic to CI and Rakefile to test locally
Oguzhan/setup code quality and ci
2022-11-07 10:33:59 +03:00
oguzhankoral 2c8b120de1 CI: Add rubocop and rubycritic to CI and Rakefile to test locally
Configure Gemfile.lock

Update README details about load/reload file(s)

Update version for only 2.7

Add linux and unknown platforms to Gemfile.lock

Add default task

Remove unnecssary dependencies from Rakefile

Assign default name to first task

Remove superseded cops

Update ruby version in workflow

Remove solorgraph and update it's dependencies

Update rubyversion

Exclude vendor bundle from rubocop

Autocorrected rubocop fixes

Extend SU related rubocop settings

Implement ruby critic for rake

Add documentation for code quality
2022-11-07 10:22:58 +03:00
oguzhankoral fc50ef68b4 Merge pull request #64 from specklesystems/oguzhan/setup-environment
Feat(tools): add tools to increase efficiency on dev mode
2022-11-01 21:03:42 +03:00
oguzhankoral 92613a37cd Feat(tools): add tools to increase efficiency on dev mode
Setup _tools for developer environment

Remove unnecassary lines on loader
2022-11-01 20:55:18 +03:00
Matteo Cominetti 77843fe697 ci: removes old deploy step 2022-10-28 09:22:39 +01:00
izzy lyseggen 9aea5ddc97 fix(metrics): remove suuid as it is no longer used (#60) 2022-09-07 10:08:29 +01:00
izzy lyseggen 4d1333c302 ci: add deploy for manager 2 (#59) 2022-08-25 16:20:28 +01:00
oguzhankoral 9ca55f6f0e Merge coplanar faces (#58)
* Update gitignore for RubyMine IDE

* Update gemfiles

- Include skippy, pry, rubycritic

* Add speckle_connector loader

This helps to navigate files that read by SU according to packaged version or development version

* Update gemfile and .lock files

* Merge coplanar faces by erasing edges between

* Cite about the idea behind merging coplanar faces

* Document remove_edge_have_coplanar_faces with control steps

* Improve loader file with the idea of release version

- This is another issue that need to be navigated

* Remove speckle_connector_loader
2022-08-18 14:42:16 +01:00
izzy lyseggen 4698799b43 ci: fix patch version script 2022-06-21 13:10:31 +01:00
izzy lyseggen 5d4e6ac89a ci: use semver tag for patching 2022-06-21 13:02:23 +01:00
izzy lyseggen b1c09c62d9 feat(converter): mesh enhancements (#50)
* feat(converter): smooth & preserve raw mesh

also only create components for `displayValue` geometry

wip - to try another edge strategy as this has caused quite a perf hit

* fix(converter): don't preserve edges for now

it is way too slow in the current implementation.
i think we can make it work in sketchup 2022 with the entities building,
but as of rn it seems like a no go for 2021
2022-06-21 12:58:59 +01:00
izzy lyseggen 2b7b74dbdd chore(ui): test removing core j (#48) 2022-06-20 16:28:07 +01:00
izzy lyseggen 42f3ae8490 ci: update tag parsing and ui build (#47)
* ci: update tag parsing and ui build

* chore(deps): stackoverflow and i are besties

testing out this workaround - will do a beta release

* ci: deploy filter tags

* ci: fix release label tag
2022-06-10 14:51:16 +01:00
izzy lyseggen 479cd9584a fix(ci): build on node 16 (#46)
* chore(ui): revert package lock

* chore(ui): upgrade object loader again (???)

* ci: omg i'm so dumb (node 16)
2022-06-10 09:40:30 +01:00
izzy lyseggen ec250fe6a5 fix(ui): revert package upgrades (regeneratorRuntime err) (#45)
* chore: del some old files

* chore(ui): revert to old packages

* chore(ui): updgrade speckle object loader
2022-06-10 07:24:52 +01:00
izzy lyseggen 7d73ebf7d0 feat(converter): edges and preserved faces on to_speckle (#44)
* chore: update packages

* chore: add debug 2022 vscode task

* feat(converter): ngons and edges on `to_speckle`

* feat(converter): big improvements 🥳

* feat(convert): add triangulation for faces with holes

you'll lose the true face with hole on receive, but this is the best
intermediary solution that won't break other connectors
2022-06-09 17:05:09 +01:00
izzy lyseggen a965065e62 style: formattinggggg (#39) 2022-03-29 15:49:22 +01:00
izzy lyseggen c9c9ecf5c6 chore(metrics): update oneclick reporting (#38) 2022-03-22 11:32:35 +00:00
izzy lyseggen 9b5c043029 feat(ui): saved streams & 1-click send (#37)
* feat(ui): saved streams and wip 1 click

* style(connector): remove some puts statements

* feat(accounts): default acct helper

* feat(ui): more 1 click send

need a way to wait for connector launch...

* fix(ui): waiting quick send!

* feat(ui): view in web from toast notif

note to self: the one in frontend is kinda ugly 😓 
went with outine btn for now bc it was better than that big ol dark gray btn

* fix: turn off dev

i always fkn do this i needa just use an env file gdi

* feat(ui): notify ui of oneclick send

* feat(metrics): remove old and use the new!
2022-03-08 10:15:21 +00:00
175 changed files with 34409 additions and 6830 deletions
+48 -33
View File
@@ -9,22 +9,19 @@ orbs:
jobs:
build-ui:
docker:
- image: 'circleci/node:14'
- image: "circleci/node:16"
steps:
- checkout
- run:
command: 'rm package-lock.json'
working_directory: 'ui'
command: "npm install"
working_directory: "ui"
- run:
command: 'npm install'
working_directory: 'ui'
- run:
command: 'npm run build'
working_directory: 'ui'
command: "npm run build"
working_directory: "ui"
- persist_to_workspace:
root: ./
paths:
- speckle_connector/html
- speckle_connector/vue_ui
build-connector: # Reusable job for basic connectors
executor:
@@ -43,15 +40,14 @@ jobs:
shell: powershell.exe
command:
| # If no tag, use 0.0.0.1 and don't make any YML (for testing only!)
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.1" } else { $env:CIRCLE_TAG }
$semver = $tag.replace("-beta","")
$version = "$($semver).$($env:CIRCLE_BUILD_NUM)"
$channel = "latest"
if($tag -like "*-beta") { $channel = "beta" }
# only create the yml if we have a tag
New-Item -Force "speckle-sharp-ci-tools/Installers/sketchup/$channel.yml" -ItemType File -Value "version: $version"
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.0" } else { $env:CIRCLE_TAG }
$semver = if($tag.Contains('/')) {$tag.Split("/")[1] } else { $tag }
$ver = if($semver.Contains('-')) {$semver.Split("-")[0] } else { $semver }
$channel = if($semver.Contains('-')) {$semver.Split("-")[1] } else { "latest" }
$version = "$($ver).$($env:CIRCLE_BUILD_NUM)"
New-Item -Force "speckle-sharp-ci-tools/Installers/sketchup/$channel.yml" -ItemType File -Value "version: $semver"
echo $version
python patch_version.py $version
python patch_version.py $semver
speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\sketchup.iss
- persist_to_workspace:
root: ./
@@ -62,9 +58,17 @@ jobs:
docker:
- image: cimg/base:2021.01
steps:
- run:
- add_ssh_keys:
fingerprints:
- "03:2e:ee:4f:14:67:2b:88:32:e8:cc:f0:cb:df:92:29"
- run:
name: I know Github as a host
command: |
mkdir ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
name: Clone
command: git clone https://$GITHUB_TOKEN@github.com/specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
command: git clone git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
- persist_to_workspace:
root: ./
paths:
@@ -73,23 +77,29 @@ jobs:
root: ./
paths:
- speckle-sharp-ci-tools
deploy: # Uploads all installers found to S3
deploy-manager2:
docker:
- image: cimg/base:2021.01
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
slug:
type: string
os:
type: string
extension:
type: string
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: List contents
command: ls -R speckle-sharp-ci-tools/Installers
- aws-s3/copy:
arguments: "--recursive --endpoint=https://$SPACES_REGION.digitaloceanspaces.com --acl public-read"
aws-access-key-id: SPACES_KEY
aws-region: SPACES_REGION
aws-secret-access-key: SPACES_SECRET
from: '"speckle-sharp-ci-tools/Installers/"'
to: s3://speckle-releases/installers/
name: Install Manager Feed CLI
command: dotnet tool install --global Speckle.Manager.Feed
- run:
name: Upload new version
command: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/\/[a-zA-Z-]*//')
/root/.dotnet/tools/Speckle.Manager.Feed deploy -s << parameters.slug >> -v ${SEMVER} -u https://releases.speckle.dev/installers/<< parameters.slug >>/<< parameters.slug >>-${SEMVER}.<< parameters.extension >> -o << parameters.os >> -f speckle-sharp-ci-tools/Installers/<< parameters.slug >>/<< parameters.slug >>-${SEMVER}.<< parameters.extension >>
workflows:
build-and-deploy:
@@ -105,6 +115,7 @@ workflows:
only: /.*/
- build-connector:
context: innosetup
slug: sketchup
requires:
- get-ci-tools
@@ -113,13 +124,17 @@ workflows:
tags:
only: /.*/
- deploy:
- deploy-manager2:
context: do-spaces-speckle-releases
slug: sketchup
os: Win
extension: exe
requires:
- get-ci-tools
- build-ui
- build-connector
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
+38
View File
@@ -0,0 +1,38 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
name: Ruby
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.7']
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
# change this to (see https://github.com/ruby/setup-ruby#versioning):
# uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@0a29871fe2b0200a17a4497bae54fe5df0d973aa # v1.115.3
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Run tests
run: bundle exec rake
+19 -1
View File
@@ -10,8 +10,23 @@
settings.json
# vue app build dist folder
speckle_connector/vue_ui
speckle_connector/html
# speckle-sharp-ci-tools
/speckle-sharp-ci-tools
# _sqlite3
/_sqlite3/.vs
/_sqlite3/Release (2.7)
/_sqlite3/Release (2.5)
/_sqlite3/Release (2.2)
/_sqlite3/Release (2.0)
/_sqlite3/Debug (2.7)
/_sqlite3/Debug (2.5)
/_sqlite3/Debug (2.2)
/_sqlite3/Debug (2.0)
*.gem
*.rbc
/.config
@@ -24,6 +39,9 @@ speckle_connector/html
/test/version_tmp/
/tmp/
# IDE
.idea
# Used by dotenv library to load environment variables.
.env
+3
View File
@@ -0,0 +1,3 @@
[submodule "_sqlite3"]
path = _sqlite3
url = git@github.com:specklesystems/sketchup-sqlite3.git
+108 -25
View File
@@ -1,49 +1,132 @@
require:
- rubocop-sketchup
- rubocop-minitest
- rubocop-rake
AllCops:
TargetRubyVersion: 2.7
EnabledByDefault: true
AutoCorrect: true
TargetRubyVersion: 2.5
DisabledByDefault: false
NewCops: enable
DisplayCopNames: true
ExtraDetails: true
SuggestExtensions: false
Exclude:
- '_tools/jf_RubyPanel.rb'
- '_tools/jf_RubyPanel/**/*.rb'
- '_tools/su_attributes.rb'
- '_tools/su_attributes/**/*.rb'
- '_tools/su_attributes/**/*.rb'
- '_sqlite3/**/*.rb'
- 'ui/**/*'
- 'speckle_connector/src/ext/**/*.rb'
- 'vendor/bundle/**/*'
- 'tests/**/*.rb'
SketchUp:
SourcePath: .
TargetSketchUpVersion: 2021
Exclude: # Exclude common folders.
- 'tests/**/*'
- 'benchmarks/**/*'
- '_tools/**/*'
- 'Rakefile'
Style/StringLiterals:
Layout:
Enabled: true
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
Layout/IndentationStyle:
EnforcedStyle: spaces
IndentationWidth: 2
# If DisabledByDefault is set to true then we need to enable the SketchUp
# related departments:
SketchupDeprecations:
Enabled: true
EnforcedStyle: double_quotes
Layout/LineLength:
Max: 120
SketchupPerformance:
Enabled: true
Lint/ConstantResolution:
SketchupRequirements:
Enabled: true
SketchupSuggestions:
Enabled: true
SketchupBugs:
Enabled: true
SketchupRequirements/FileStructure:
Enabled: false
Style/Copyright:
Enabled: false
Style/DocumentationMethod:
SketchupSuggestions/ModelEntities:
Enabled: false
Metrics/AbcSize:
Enabled: false
Max: 30
Metrics/BlockLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
# Exclude spec tests
Exclude:
- "**/*_spec.rb"
Metrics/MethodLength:
Max: 20
Metrics/ClassLength:
Max: 200
Layout/EndOfLine:
Enabled: false
EnforcedStyle: lf
Minitest/MultipleAssertions:
Max: 5
Naming/MethodParameterName:
AllowedNames: [x, y, z, id]
Naming/VariableNumber:
EnforcedStyle: snake_case
# SketchUp 2017 uses Ruby 2.2 where safe navigation is not available
Style/SafeNavigation:
Enabled: false
Metrics/ParameterLists:
Style/AndOr:
Enabled: false
Metrics/CyclomaticComplexity:
Style/Documentation:
Exclude:
- "*tests/**/*_spec.rb"
- "*tests/**/*_test.rb"
Style/Not:
Enabled: false
Metrics/PerceivedComplexity:
Style/NumericLiterals:
Enabled: false
Style/NumericPredicate:
EnforcedStyle: comparison
Style/Proc:
Enabled: false
Style/RedundantReturn:
Enabled: false
# SketchUp 2017 uses Ruby 2.2 where safe navigation is not available
Style/SlicingWithRange:
Enabled: false
# SketchUp 2017 uses Ruby 2.2 where transform_values is not available
Style/HashTransformValues:
Enabled: false
# SketchUp 2017 uses Ruby 2.2 where transform_keys is not available
Style/HashTransformKeys:
Enabled: false
# SketchUp 2017 uses Ruby 2.2 where block needs to be wrapped in begin/end if ensure can be used
Style/RedundantBegin:
Enabled: false
+10 -1
View File
@@ -12,6 +12,15 @@
"command": "&'C:/Program Files/SketchUp/SketchUp 2021/SketchUp.exe' -rdebug 'ide port=7000'",
},
"problemMatcher": []
}
},
{
"label": "Debug SketchUp 2022",
"type": "shell",
"command": "open -a '/Applications/SketchUp 2022/SketchUp.app' --args -rdebug 'ide port=7000'",
"windows": {
"command": "&'C:/Program Files/SketchUp/SketchUp 2022/SketchUp.exe' -rdebug 'ide port=7000'",
},
"problemMatcher": []
},
]
}
+24 -11
View File
@@ -1,16 +1,29 @@
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rake", "~> 13.0"
gem "rubocop", "~> 1.7"
source 'https://rubygems.org'
group :development do
gem "minitest"
gem "sketchup-api-stubs"
gem "solargraph"
# mini tests for ruby classes
gem 'minitest'
# Git hooks manager
gem 'overcommit', require: false
# Pry is a runtime developer console and IRB alternative with powerful introspection capabilities.
# Pry aims to be more than an IRB replacement. It is an attempt to bring REPL driven programming to the Ruby language.
gem 'pry'
# Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.
gem 'rake'
# RuboCop is a Ruby static code analyzer (a.k.a. linter) and code formatter.
gem 'rubocop'
# A RuboCop extension focused on enforcing Minitest best practices and coding conventions.
gem 'rubocop-minitest'
# A RuboCop plugin for Rake.
gem 'rubocop-rake'
# Code analysis for SketchUp extensions using the SketchUp Ruby API.
gem 'rubocop-sketchup'
# wraps around static analysis gems such as Reek, Flay and Flog to provide a quality report of your Ruby code.
gem 'rubycritic', '~> 4.3', '>= 4.3.3', require: false
# Auto completions for SketchUp API.
gem 'sketchup-api-stubs'
# Aid with common SketchUp extension tasks.
gem 'skippy', '~> 0.4.1.a'
end
gem "sqlite3", "~> 1.4"
+112 -52
View File
@@ -1,74 +1,134 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
backport (1.2.0)
benchmark (0.1.1)
diff-lcs (1.4.4)
e2mmap (0.1.0)
jaro_winkler (1.5.4)
kramdown (2.3.1)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
minitest (5.14.4)
nokogiri (1.12.5-x64-mingw32)
racc (~> 1.4)
nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4)
parallel (1.20.1)
parser (3.0.2.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
childprocess (4.1.0)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
docile (1.4.0)
equalizer (0.0.11)
erubi (1.11.0)
flay (2.13.0)
erubi (~> 1.10)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flog (4.6.6)
path_expander (~> 1.0)
ruby_parser (~> 3.1, > 3.1.0)
sexp_processor (~> 4.8)
git (1.12.0)
addressable (~> 2.8)
rchardet (~> 1.8)
ice_nine (0.11.2)
iniparse (1.5.0)
kwalify (0.7.2)
launchy (2.5.0)
addressable (~> 2.7)
method_source (1.0.0)
minitest (5.16.3)
naturally (2.2.1)
overcommit (0.59.1)
childprocess (>= 0.6.3, < 5)
iniparse (~> 1.4)
rexml (~> 3.2)
parallel (1.22.1)
parser (3.1.2.1)
ast (~> 2.4.1)
racc (1.6.0)
rainbow (3.0.0)
regexp_parser (2.1.1)
reverse_markdown (2.0.0)
nokogiri
path_expander (1.1.1)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
psych (3.3.4)
public_suffix (5.0.0)
rainbow (3.1.1)
rake (13.0.6)
rchardet (1.8.0)
reek (6.1.1)
kwalify (~> 0.7.0)
parser (~> 3.1.0)
rainbow (>= 2.0, < 4.0)
regexp_parser (2.6.0)
rexml (3.2.5)
rubocop (1.19.1)
rubocop (1.7.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.9.1, < 2.0)
rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.11.0)
parser (>= 3.0.1.1)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (1.4.1)
parser (>= 2.7.1.5)
rubocop-minitest (0.23.0)
rubocop (>= 0.90, < 2.0)
rubocop-rake (0.6.0)
rubocop (~> 1.0)
rubocop-sketchup (1.3.0)
rubocop (>= 0.82, < 2.0)
ruby-progressbar (1.11.0)
sketchup-api-stubs (0.7.7)
solargraph (0.43.0)
backport (~> 1.2)
benchmark
bundler (>= 1.17.2)
diff-lcs (~> 1.4)
e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
parser (~> 3.0)
reverse_markdown (>= 1.0.5, < 3)
rubocop (>= 0.52)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
sqlite3 (1.4.2)
thor (1.1.0)
tilt (2.0.10)
unicode-display_width (2.0.0)
yard (0.9.26)
ruby_parser (3.19.1)
sexp_processor (~> 4.16)
rubycritic (4.7.0)
flay (~> 2.8)
flog (~> 4.4)
launchy (>= 2.0.0)
parser (>= 2.6.0)
rainbow (~> 3.0)
reek (~> 6.0, < 7.0)
ruby_parser (~> 3.8)
simplecov (>= 0.17.0)
tty-which (~> 0.4.0)
virtus (~> 1.0)
sexp_processor (4.16.1)
simplecov (0.21.2)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
sketchup-api-stubs (0.7.8)
skippy (0.4.3.a)
git (~> 1.3)
naturally (~> 2.1)
thor (~> 0.19)
thor (0.20.3)
thread_safe (0.3.6)
tty-which (0.4.2)
unicode-display_width (1.8.0)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
PLATFORMS
x64-mingw32
x64-unknown
x86_64-linux
DEPENDENCIES
minitest
rubocop (~> 1.7)
overcommit
pry
rake
rubocop
rubocop-minitest
rubocop-rake
rubocop-sketchup
rubycritic (~> 4.3, >= 4.3.3)
sketchup-api-stubs
solargraph
sqlite3 (~> 1.4)
skippy (~> 0.4.1.a)
BUNDLED WITH
2.2.26
2.3.25
+86 -30
View File
@@ -41,27 +41,42 @@ Give Speckle a try in no time by:
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
# Repo structure
# Repo structure
This is the beginning of the Speckle SketchUp Connector. It is still in very early development and is not ready for general use.
This repo is split into three parts:
This repo is split into two parts: `speckle_connector` which is the Ruby SketchUp plugin and `ui` which is the Vue frontend.
### 1. **Speckle Connector extension**
Includes the `ruby` source files to run extension on SketchUp environment. SketchUp Extensions are composed of
a **.rb** file as entry and **folder** that .rb file refers to. In our case entry file is `speckle_connector.rb`
that responsible to register Speckle Connector extension to SketchUp and also it shows address to where extension
will start to read extension. Source folder is `speckle_connector`.
## Usage
### 2. **User Interface**
> NOTE: this connector is still in early development and isn't ready for general use.
Includes the `Vue` frontend lives in the `ui` folder.
Copy the whole `speckle_connector` folder to you SketchUp Plugins folder. You will likely find this at:
### 3. **SketchUp Sqlite3 extension** [submodule](https://github.com/specklesystems/sketchup-sqlite3)
C:\Users\{YOU}\AppData\Roaming\SketchUp\SketchUp 2021\SketchUp\Plugins
Includes source codes of base `SQLite3` C/C++ library and `ruby` compiler files to be able to run SQLite3
functionality on SketchUp in the same ruby module like `SpeckleConnector::Sqlite3::Database`. By this way
we use extensions as native part of the source `ruby` code.
After building `sqlite3.sln` file, compiled `sqlite3.so` (for Windows) and `sqlite3.bundle` (for OSX) dynamic library files are created
by solution to place them into source code into `speckle_connector/src/ext`. Building this project should be only
happen when SketchUp starts to support newer Ruby versions (currently it is `2.7`).
You'll need to serve the ui before launching the connector:
## Contribution Guide
cd ui
npm install
npm run serve
Before start to contribute, it is better to understand how align with other contributors. It will make easier job
of reviewer when you submit an issue or PR. If it is your first repo to contribute Speckle environment make sure that you read
[Contribution Guideline](https://github.com/specklesystems/speckle-sharp/blob/main/.github/CONTRIBUTING.md).
Additionally as mentioned on [Repo Structure](#3-sketchup-sqlite3-extension-submodulehttpsgithubcomspecklesystemssketchup-sqlite3),
this repo includes a submodule. Contributions on this source files should be done on the [sketchup-sqlite](https://github.com/specklesystems/sketchup-sqlite3)
by creating issues and PRs on it. If it is your first time works with submodules, please read [git docs](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
briefly to get some insight about it.
## Development
@@ -89,43 +104,63 @@ Clone this repo and run:
This will install all the necessary packages for the connector.
Next, install the Sketchup Ruby Debugger. You can find installation instructions [here](https://github.com/SketchUp/sketchup-ruby-debugger/blob/main/README.md). This will involve downloading the `dll` and copying it into the SketchUp installation directory:
Next, install the Sketchup Ruby Debugger. You can find installation instructions
[here](https://github.com/SketchUp/sketchup-ruby-debugger/blob/main/README.md).
This will involve downloading the `dll` and copying it into the SketchUp installation
directory:
C:\Program Files\SketchUp\SketchUp 2021\
C:\Program Files\SketchUp\SketchUp 20XX\
You can now open up the repo in VS Code.
You can now open up the repo in VS Code or you can use JetBrains' tools RubyMine and Webstorm.
Make sure you've installed the Ruby extension for VS Code.
If you will use VS Code, make sure you've installed the Ruby extension for VS Code.
### Loading the Plugin
### Loading the Speckle Connector Plugin
To tell SketchUp to load the plugin from wherever you happen to be developing, you'll need to create a ruby file with the following contents:
1. Find already prepared `speckle_connector_loader.rb` file on the `_tools`
folder.
2. Copy this Ruby file into your SketchUp Plugins directory. You will likely find this at:
`C:\Users\{YOU}\AppData\Roaming\SketchUp\SketchUp 20XX\SketchUp\Plugins`
3. Update below line on the copied file with your local git file.
```ruby
speckle_path = File.join(home_folder, 'Git', 'Speckle', 'speckle-sketchup')
```
By this way SketchUp will directly read your local repository. Do not forget,
this file also loads additional tools on the `_tools` folder.
Those are will be only available on dev mode.
```ruby
$LOAD_PATH << 'C:\YOUR\PATH\TO\THE\sketchup_connector'
require 'speckle_connector.rb'
```
Due to the fact that Ruby is interpreted language, so you can reload your file(s) when
you changed them. There are different kinds of ways to reload them.
Drop this Ruby file into your SketchUp Plugins directory. You will likely find this at:
1. To reload the whole plugin files while SketchUp is running, open up the Ruby console
and run the following:
```ruby
SpeckleConnector.reload
```
2. To reload only specific files, use `jf ruby toolbar` plugin that already available
on SketchUp toolbar.
C:\Users\{YOU}\AppData\Roaming\SketchUp\SketchUp 2021\SketchUp\Plugins
### User Interface
To reload the plugin while SketchUp is running, open up the Ruby console and run the following:
If it is your first time you cloned the project and willing to see Speckle UI, you
should make sure that you compiled the `vue.js` project in the `ui` folder.
SpeckleSystems::SpeckleConnector.reload
To run the `ui`, create a `.env` based on `.env-example` and paste in your Speckle token. Then:
To run the `ui`, create a `.env` based on `.env-example` and paste in your
Speckle token. Then:
cd ui
npm run serve
### Debugging
To run SketchUp in debug mode, you will run the task specified in `tasks.json`. Before you do this, make sure your integrated shell for tasks is using powershell. You can specify this by adding the following option to your workspace's `settings.json`
To run SketchUp in debug mode, you will run the task specified in `tasks.json`.
Before you do this, make sure your integrated shell for tasks is using powershell.
You can specify this by adding the following option to your workspace's `settings.json`
"terminal.integrated.automationShell.windows": "powershell.exe",
To start the task, use the keyboard shortcut `ctrl` + `shift` + `p` to open up the Command Palette. Search for `Tasks: Run Task` and select it:
To start the task, use the keyboard shortcut `ctrl` + `shift` + `p` to open up
the Command Palette. Search for `Tasks: Run Task` and select it:
![command palette](https://user-images.githubusercontent.com/7717434/135051668-35fee34e-5270-4b83-9c7b-dabb872370ee.png)
@@ -133,9 +168,30 @@ Then choose the `Debug Sketchup 2021` task to run it:
![debug sketchup task](https://user-images.githubusercontent.com/7717434/135051777-4c350a62-45fb-400e-9b24-4fbb02331b83.png)
Once Sketchup has launched, start the `Listen for rdebug-ide` debug configuration. Once the debugger has connected, you'll be able to debug the connector normally.
Once Sketchup has launched, start the `Listen for rdebug-ide` debug configuration.
Once the debugger has connected, you'll be able to debug the connector normally.
Make sure you run the `ui` before starting the SketchUp Connector
cd ui
npm run serve
npm run serve
### Code Quality
Tracking your code quality before merging any code to `main` branch might not seem at the
first time crucial, but when repo became huge, you might have many spaghetti code and technical
depth. It is always better to keep your work tough from the beginning. For this reason some
workflows have already setup on CI, those workflows must be passed before considering to
merge.
To track your code quality locally,
1. Make sure that you do not have any RuboCop issue, run below
```ruby
bundle exec rake
```
2. To check overall state of repository by RubyCritic, run below
```ruby
rake rubycritic
```
+52
View File
@@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'rake/testtask'
require 'rubocop/rake_task'
require 'rubycritic/rake_task'
module SpeckleConnector
# Custom utility functions for rake tasks
module RakeUtils
module_function
# Find ruby files that were changed from `main` to the latest revision
def changed_rb_files(previous_revision: 'main', latest_revision: '')
range = latest_revision.empty? ? previous_revision : "#{latest_revision}..#{previous_revision}"
command = "git diff #{range} --name-only"
changed_files = `#{command}`.split("\n")
# filter changed files with ruby files (.rb), Gemfile and Rakefile.
filtered_files = changed_files.grep(/.*\.rb$|Gemfile|Rakefile/)
filtered_files.select { |file| File.exist?(file) }
end
end
end
# Add default rubocop task
RuboCop::RakeTask.new(:default)
# Add task to only verify ruby files that are different than in the `main` branch
desc('Run rubocop on changed files')
RuboCop::RakeTask.new(:rubocop_changed) do |t|
t.patterns = FileList.new(SpeckleConnector::RakeUtils.changed_rb_files)
end
# Glob pattern to match source files. Defaults to FileList['.'].
ruby_critic_paths = FileList[
'speckle_connector/**/*.rb',
'speckle_connector.rb',
'tests/**/*.rb'] -
FileList[
'_tools/**/*.rb',
'speckle_connector/src/ext/**/*.rb',
]
# for local
RubyCritic::RakeTask.new('rubycritic') do |task|
task.paths = ruby_critic_paths
end
# for CI
RubyCritic::RakeTask.new('rubycritic-ci') do |task|
task.options = '--mode-ci --format console --no-browser --branch main'
task.paths = ruby_critic_paths
end
Submodule
+1
Submodule _sqlite3 added at 800dd5e366
+25
View File
@@ -0,0 +1,25 @@
# Tools
This folder stores the external tools and helper scripts to make easier life of the developer,
they are not the part of the main functionality of the Speckle.
Tools and scripts inside the folder will be loaded with `sketchup_connector_loader.rb` file.
In order to load your own `.rb` files please add this file names into list in the loader.
````ruby
...
files = %w[speckle_connector jf_RubyPanel su_attributes <put-your-file-here>]
# This line placed before loading started.
files.each do |ruby_file|
puts "Loading #{ruby_file}"
begin
require ruby_file
rescue LoadError
puts "Could not load #{ruby_file}"
end
end
````
Track load status of your tools and scripts on the ruby console when SketchUp UI initializing.
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
# #-------------------------------------------------------------------------------------------------
# *************************************************************************************************
# RubyPanel Toolbar (C) 2007 jim.foltz@gmail.com
#
# With special thanks to Chris Phillips (Sketchy Physics)
# for the Win32API code examples.
#
# 2011-01-05 <jim.foltz@gmail.com>
# * Changed Toolbar name from "Ruby COnsole" to "Ruby Toolbar" (TT)
# http://forums.sketchucation.com/viewtopic.php?f=323&t=1542&p=298587#p298587
# * Wrapped in addition module RubyToolbar
# * Use $suString.GetSting to get proper "Ruby Console" name string.
# * Better check if TB was previously visible
# * Use UI.start_timer to restore Toolbar
# ICONS: located in the subfolder "rubytoolbar"
# MODIFICATION: by Fredo6 for compliance with SU 2014 (and no dependency on Win32API) - 18 Sep 2013
# *************************************************************************************************
#-------------------------------------------------------------------------------------------------
require 'sketchup'
require 'extensions'
ext = SketchupExtension.new('Ruby Toolbar', 'jf_RubyPanel/rubytoolbar.rb')
ext.creator = 'Jim Foltz <jim.foltz@gmail.com>'
ext.description = 'Toolbar for manipulating the Ruby Console. Compatible with SketchUp 2014'
ext.version = '2014'
Sketchup.register_extension(ext, true)
Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+89
View File
@@ -0,0 +1,89 @@
# frozen_string_literal: true
#-------------------------------------------------------------------------------------------------------------------------------------------------
# RubyPanel Toolbar (C) 2007 jim.foltz@gmail.com
# Permission to use, copy, modify, and distribute this software for # any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies.
# THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
# Description: Manage the loading of Ruby files and display of the Ruby console
# CREDITS: Special thanks to Chris Phillips (Sketchy Physics) for the Win32API code examples
# Revision: 3 Aug 2009, by Fredo6
# ICONS: located in the subfolder "rubytoolbar"
# MODIFICATION: by Fredo6 for compliance with SU 2014 (and no dependency on Win32API) - 18 Sep 2013
#-------------------------------------------------------------------------------------------------------------------------------------------------
require 'English'
require 'sketchup'
module JF_RubyToolbar
# Load the toolbar icons and commands, and do some initialization
def self.load_toolbar
@last_dir = "#{$LOAD_PATH[0]}/"
@last_dir = @last_dir.gsub('/', '\\\\\\\\')
@last_dir = File.join($JF_RUBYTOOLBAR, 'speckle_connector')
curdir = File.dirname __FILE__
# create toolbar
tb = UI::Toolbar.new 'Ruby Toolbar'
# Toggle console
cmd = UI::Command.new('Show/Hide') { SKETCHUP_CONSOLE.visible? ? SKETCHUP_CONSOLE.hide : SKETCHUP_CONSOLE.show }
cmd.large_icon = cmd.small_icon = File.join(curdir, 'rubypanel.png')
cmd.status_bar_text = cmd.tooltip = 'Show/Hide Ruby Console'
tb.add_item cmd
# Clear Console
cmd = UI::Command.new('Clear') { SKETCHUP_CONSOLE.clear }
cmd.status_bar_text = cmd.tooltip = 'Clear Console'
cmd.large_icon = cmd.small_icon = File.join(curdir, 'Delete24.png')
tb.add_item cmd
# Load a Ruby script
cmd = UI::Command.new('LoadScript') { load_script }
cmd.large_icon = cmd.small_icon = File.join(curdir, 'doc_ruby.png')
cmd.tooltip = cmd.status_bar_text = 'Load Script'
tb.add_item cmd
# Reload the last Ruby Script
@cmd_reload = UI::Command.new('Reload') { load_script @last_file }
@cmd_reload.large_icon = @cmd_reload.small_icon = File.join(curdir, 'reload.png')
@cmd_reload.status_bar_text = @cmd_reload.tooltip = 'Reload Script'
tb.add_item @cmd_reload
# Open the SU plugins directory panel
cmd = UI::Command.new('PluginsDir') { UI.openURL @last_dir }
cmd.tooltip = cmd.status_bar_text = 'Browse Plugins Folder'
cmd.large_icon = cmd.small_icon = File.join(curdir, 'open_folder.png')
tb.add_item cmd
# showing the toolbar
tb.get_last_state == -1 ? tb.show : tb.restore
end
# Load a script file - if <file> is nil, open the dialog panel to select the file
def self.load_script(file = nil)
file ||= UI.openpanel 'Load Script', @last_dir, '*.rb*'
return unless file
begin
load file
Sketchup.set_status_text "#{File.basename(file)} loaded (#{Time.now.strftime('%H:%M:%S')})"
@last_file = file
@last_dir = "#{File.dirname(file)}/"
@last_dir = @last_dir.gsub('/', '\\\\\\\\')
@cmd_reload.status_bar_text = @cmd_reload.tooltip = "Reload Script: #{File.basename(file)}"
rescue StandardError
UI.messagebox("Couldn't load #{File.basename(file)}: #{$ERROR_INFO}")
end
end
# Loading the toolbar once
unless file_loaded?('RubyToolbar.rb')
load_toolbar
file_loaded('RubyToolbar.rb')
end
end
+44
View File
@@ -0,0 +1,44 @@
# frozen_string_literal: true
# The purpose of this file is customizing environment of the developer on SketchUp.
# Each developer can customize it's own loader(this file), by this way developer can load their helper tools
# and helper methods ONLY in dev mode.
# Change the base folder and copy this file to Sketchup Plugins directory
# If you need to test in several versions of SketchUp, create symlinks to this file
# ( AppData\Roaming\SketchUp\SketchUp <version>\SketchUp\Plugins )
# Create a link to Plugins folder with this command
# rubocop:disable Layout/LineLength
# New-Item -ItemType SymbolicLink -Path '~\AppData\Roaming\SketchUp\SketchUp 2022\SketchUp\Plugins\speckle_connector_loader.rb' -Target ~\Git\Speckle\speckle-sketchup\_tools\speckle_connector_loader.rb
# rubocop:enable Layout/LineLength
SKETCHUP_CONSOLE.show # if you want to show Ruby console on startup
# base location of your repos - will be merged with specific repos in next step
home_folder = File.expand_path('~')
# If you use some other location for your repository, you can change it here
# but make sure it is not committed as it will change thi setting for all
# users that use the default setup. Eg:
# Add Speckle folder - uncomment the one you need
speckle_path = File.join(home_folder, 'Git', 'Speckle', 'speckle-sketchup')
$LOAD_PATH << speckle_path
$LOAD_PATH << File.join(speckle_path, '_tools')
# Defining this path will help to tool to browse related source file directly when
# developer attempted to reload/load file.
# rubocop:disable Style/GlobalVars
$JF_RUBYTOOLBAR = speckle_path
# rubocop:enable Style/GlobalVars
files = %w[speckle_connector jf_RubyPanel su_attributes]
files.each do |ruby_file|
puts "Loading #{ruby_file}"
begin
require ruby_file
rescue LoadError
puts "Could not load #{ruby_file}"
end
end
+49
View File
@@ -0,0 +1,49 @@
# Copyright 2014-2021, Trimble Inc.
#
# License: The MIT License (MIT)
#
# A SketchUp Ruby Extension that surfaces attributes attached to components.
# More info at https://github.com/SketchUp/sketchup-attribute-helper
require 'sketchup.rb'
require 'extensions.rb'
#-------------------------------------------------------------------------------
module Trimble
module AttributeHelper
### CONSTANTS ### ------------------------------------------------------------
# Plugin information
PLUGIN_ID = 'AttributeHelper'.freeze
PLUGIN_NAME = 'SketchUp Attribute Helper'.freeze
PLUGIN_VERSION = '1.0.3'.freeze
# Resource paths
FILENAMESPACE = File.basename(__FILE__, '.*')
PATH_ROOT = File.dirname(__FILE__).freeze
PATH = File.join(PATH_ROOT, FILENAMESPACE).freeze
### EXTENSION ### ------------------------------------------------------------
unless file_loaded?(__FILE__)
loader = File.join( PATH, 'core.rb' )
ex = SketchupExtension.new(PLUGIN_NAME, loader)
ex.description = 'Visually inspect nested attributes in SketchUp.'
ex.version = PLUGIN_VERSION
ex.copyright = 'Trimble Inc © 2015-2021'
ex.creator = 'SketchUp'
Sketchup.register_extension(ex, true)
end
end # module AttributeHelper
end # module Trimble
#-------------------------------------------------------------------------------
file_loaded(__FILE__)
#-------------------------------------------------------------------------------
+285
View File
@@ -0,0 +1,285 @@
# Copyright 2014-2021, Trimble Inc.
#
# License: The MIT License (MIT)
require "sketchup.rb"
require "stringio"
module Trimble
module AttributeHelper
PLUGIN = self
class << self
attr_reader :app_observer
attr_reader :model_observer
attr_reader :selection_observer
end
def self.visualize_selected
content = self.traverse_selected
html = self.wrap_content(content)
options = {
:dialog_title => "Attribute Visualizer",
:preferences_key => 'AttributeVisualizer',
:scrollable => true,
:resizable => true,
:height => 300,
:width => 400,
:left => 200,
:top => 200
}
@window ||= UI::WebDialog.new(options)
@window.set_html(html)
@window.set_on_close {
@window = nil
self.detach_observers
}
unless @window.visible?
@window.show
self.attach_observers
end
end
def self.attach_observers
@app_observer ||= AppObserver.new
@model_observer ||= ModelObserver.new
@selection_observer ||= SelectionObserver.new
model = Sketchup.active_model
Sketchup.remove_observer(@app_observer)
model.remove_observer(@model_observer)
model.selection.remove_observer(@selection_observer)
Sketchup.add_observer(@app_observer)
model.add_observer(@model_observer)
model.selection.add_observer(@selection_observer)
end
def self.detach_observers
Sketchup.remove_observer(@app_observer)
Sketchup.active_model.remove_observer(@model_observer)
Sketchup.active_model.selection.remove_observer(@selection_observer)
end
def self.traverse_selected
html = StringIO.new
model = Sketchup.active_model
selection = model.selection
if selection.empty?
if model.active_path.nil?
entity = model
else
entity = model.active_path.last
end
else
return "Invalid selection size" unless selection.size == 1
entity = selection[0]
end
html.puts "<h1>#{self.escape_html(entity)}</h1>"
if entity.respond_to?(:name)
html.puts "<h2>#{self.escape_html(entity.name)}</h2>"
end
if entity.attribute_dictionaries
entity.attribute_dictionaries.each { |dictionary|
html.puts self.format_dictionary(dictionary)
}
else
html.puts "No dictionaries"
end
if entity.is_a?(Sketchup::Group)
definition = entity.entities.parent
elsif entity.is_a?(Sketchup::ComponentInstance)
definition = entity.definition
else
definition = nil
end
if definition && definition.attribute_dictionaries
html.puts "<h1>#{self.escape_html(definition)}</h1>"
html.puts "<h2>#{self.escape_html(definition.name)}</h2>"
definition.attribute_dictionaries.each { |dictionary|
html.puts self.format_dictionary(dictionary)
}
end
html.string
end
def self.format_dictionary(dictionary, path = "")
html_name = self.escape_html(dictionary.name)
path = "#{path}:#{html_name}"
html = StringIO.new
html.puts "<table>"
html.puts "<caption title='#{path}'>#{html_name}</caption>"
html.puts "<tbody>"
dictionary.each { |key, value|
html_key = self.escape_html(key)
html_value = self.escape_html(value)
node_path = "#{path}:#{html_key}"
html.puts "<tr title='#{node_path}'><td>#{html_key}</td><td>#{html_value}</td><td class='value_type'>#{value.class}</td></tr>"
}
if dictionary.attribute_dictionaries
dictionary.attribute_dictionaries.each { |sub_dic|
html.puts "<tr><td colspan='3' class='dictionary'>"
html.puts self.format_dictionary(sub_dic, path)
html.puts "</td></tr>"
}
end
html.puts "</tbody>"
html.puts "</table>"
html.string
end
def self.escape_html(data)
data.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
end
def self.wrap_content(content)
html = <<-EOT
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta charset="UTF-8">
<style>
html {
font-family: "Calibri", sans-serif;
font-size: 10pt;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
table {
width: 100%;
/*padding: 0.5em;*/
border: 1px solid #666;
}
caption {
font-weight: bold;
text-align: left;
/*border-bottom: 1px solid silver;*/
padding: 0.2em;
}
td {
background: #f3f3f3;
padding: 0.2em;
}
td.dictionary {
background: none;
padding-left: 1em;
}
tr:hover td {
background: rgba(255,210,180,0.2);
}
.value_type {
text-align: right;
width: 5%;
}
</style>
<head>
<body>
#{content}
</body>
</html>
EOT
end
class SelectionObserver < Sketchup::SelectionObserver
def onSelectionAdded(selection, element)
selection_changed()
end
def onSelectionBulkChange(selection)
selection_changed()
end
def onSelectionCleared(selection)
selection_changed()
end
def onSelectionRemoved(selection, element)
selection_changed()
end
private
def selection_changed
PLUGIN.visualize_selected
end
end # class SelectionObserver
class ModelObserver < Sketchup::ModelObserver
def onActivePathChanged(model)
PLUGIN.visualize_selected
end
def onTransactionCommit(model)
model_changed(model)
end
def onTransactionEmpty(model)
model_changed(model)
end
def onTransactionRedo(model)
model_changed(model)
end
def onTransactionUndo(model)
model_changed(model)
end
private
def model_changed(model)
if @timer.nil?
@timer = UI.start_timer(0.0, false) {
@timer = nil
PLUGIN.visualize_selected
}
end
end
end # class ModelObserver
class AppObserver < Sketchup::AppObserver
def onNewModel(model)
observe_model(model)
end
def onOpenModel(model)
observe_model(model)
end
private
def observe_model(model)
model.add_observer(PLUGIN.model_observer)
model.selection.add_observer(PLUGIN.selection_observer)
PLUGIN.visualize_selected
end
end # class AppObserver
unless file_loaded?(__FILE__)
command = UI::Command.new("Attribute Helper") { self.visualize_selected }
command.status_bar_text = "Inspect and edit the attributes of a selection."
menu_name = Sketchup.version.to_f < 21.1 ? 'Plugins' : 'Developer'
menu = UI.menu(menu_name)
menu.add_item(command)
file_loaded(__FILE__)
end
end # module AttributeHelper
end # module Sketchup
+2
View File
@@ -0,0 +1,2 @@
ID=f4d9d053-4479-4a9a-90da-b79fa16e28c4
VERSION_ID=b787af5e-8e8e-4932-92ef-a3c99681795d
Binary file not shown.
-15
View File
@@ -1,15 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "speckle_connector"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
-8
View File
@@ -1,8 +0,0 @@
# frozen_string_literal: true
require_relative "speckle_connector/version"
module SpeckleConnector
class Error < StandardError; end
# Your code goes here...
end
+3 -2
View File
@@ -25,7 +25,8 @@ def patch_installer(tag):
with open(iss_file, "r") as file:
lines = file.readlines()
lines.insert(11, f'#define AppVersion "{tag}"\n')
lines.insert(11, f'#define AppVersion "{tag.split("-")[0]}"\n')
lines.insert(12, f'#define AppInfoVersion "{tag}"\n')
with open(iss_file, "w") as file:
file.writelines(lines)
@@ -39,7 +40,7 @@ def main():
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
raise ValueError(f"Invalid tag provided: {tag}")
print(f"Patching version: {tag}")
+27 -33
View File
@@ -1,45 +1,39 @@
require "sketchup"
# frozen_string_literal: true
require "extensions"
require 'sketchup'
require 'extensions'
module SpeckleSystems
module SpeckleConnector
# Version - patched by CI
CONNECTOR_VERSION = "0.0.0"
# Speckle connector module to enable multiplayer mode ON!
module SpeckleConnector
# Version - patched by CI
CONNECTOR_VERSION = '0.0.0'
file = __FILE__.dup
# Account for Ruby encoding bug under Windows.
file.force_encoding("UTF-8") if file.respond_to?(:force_encoding)
# Support folder should be named the same as the root .rb file.
folder_name = File.basename(file, ".*")
file = __FILE__.dup
# Path to the root .rb file (this file).
PATH_ROOT = File.dirname(file).freeze
# Account for Ruby encoding bug under Windows.
file.force_encoding('UTF-8') if file.respond_to?(:force_encoding)
# Path to the support folder.
PATH = File.join(PATH_ROOT, folder_name).freeze
# Support folder should be named the same as the root .rb file.
folder_name = File.basename(file, '.*')
# Run from localhost or from build files
DEV_MODE = false
puts("Loading Speckle Connector v#{CONNECTOR_VERSION} from #{DEV_MODE ? 'dev' : 'build'}")
# Path to the root .rb file (this file).
PATH_ROOT = File.dirname(file).freeze
# Path to the support folder.
PATH = File.join(PATH_ROOT, folder_name).freeze
unless file_loaded?(__FILE__)
# Run from localhost or from build files
DEV_MODE = false
puts("Loading Speckle Connector v#{CONNECTOR_VERSION} from #{DEV_MODE ? 'dev' : 'build'}")
ex = SketchupExtension.new("Speckle SketchUp", File.join(PATH, "main"))
unless file_loaded?(__FILE__)
ex = SketchupExtension.new('Speckle SketchUp', File.join(PATH, 'bootstrap'))
ex.description = 'Speckle Connector for SketchUp'
ex.version = CONNECTOR_VERSION
ex.copyright = 'AEC Systems Ltd.'
ex.creator = 'Speckle Systems'
Sketchup.register_extension(ex, true)
ex.description = "Speckle Connector for SketchUp"
ex.version = CONNECTOR_VERSION
ex.copyright = "AEC Systems Ltd."
ex.creator = "Speckle Systems"
Sketchup.register_extension(ex, true)
file_loaded(__FILE__)
end
file_loaded(__FILE__)
end
end
-52
View File
@@ -1,52 +0,0 @@
require "JSON"
begin
require("sqlite3")
rescue LoadError
# ty msp-greg! https://github.com/MSP-Greg/SUMisc/releases/tag/sqlite3-mingw-1
Gem.install(File.join(File.dirname(File.expand_path(__FILE__)), "utils/sqlite3-1.4.2.mspgreg-x64-mingw32.gem"))
require("sqlite3")
end
module SpeckleSystems::SpeckleConnector
module Accounts
def self.load_accounts
dir = _get_speckle_dir
db_path = File.join(dir, "Accounts.db")
unless File.exist?(db_path)
raise(IOError, "No Accounts db found. Please read the guide for different options for adding your account: \nhttps://speckle.guide/user/manager.html#adding-accounts")
end
db = SQLite3::Database.new(db_path)
rows = db.execute("SELECT * FROM objects")
db.close
rows.map { |row| JSON.parse(row[1]) }
end
def self.get_suuid
dir = _get_speckle_dir
suuid_path = File.join(dir, "suuid")
return unless File.exist?(suuid_path)
File.read(suuid_path)
end
def self._get_speckle_dir
speckle_dir =
case Sketchup.platform
# sometimes Dir.home on windows points somewhere else bc I guess it's picking up a higher level user?
when :platform_win then File.join(Dir.pwd[%r{^((?:[^/]*/){3})}], "AppData/Roaming/Speckle")
when :platform_osx then File.join(Dir.home, ".config", "Speckle")
else
nil
end
return speckle_dir if Dir.exist?(speckle_dir)
raise(
IOError,
"No Speckle Directory exists. Please read the guide to get Speckle set up on your machine: \nhttps://speckle.guide/user/manager.html"
)
end
end
end
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'sketchup'
require 'pathname'
require 'speckle_connector/debug'
require_relative 'src/ui/sketchup_ui'
require_relative 'src/ui/ui_controller'
require_relative 'src/commands/menu_command_handler'
require_relative 'src/app/speckle_connector_app'
require_relative 'src/states/user_state'
require_relative 'src/states/initial_state'
require_relative 'src/commands/speckle_menu_commands'
# Speckle Connector on SketchUp to enable Multiplayer mode ON!
module SpeckleConnector
SKETCHUP_VERSION = Sketchup.version.to_i
dir = __dir__.dup
dir.force_encoding('UTF-8') if dir.respond_to?(:force_encoding)
SPECKLE_CONNECTOR_SRC_PATH = Pathname.new(File.expand_path(dir)).cleanpath.to_s
def self.initialize_app
sketchup_ui = Ui::SketchupUi.new
ui_controller = Ui::UiController.new(sketchup_ui)
menu_commands = Commands::MenuCommandHandler.new
user_state = SpeckleConnector::States::UserState.new({})
initial_state = SpeckleConnector::States::InitialState.new(user_state)
app = SpeckleConnector::App::SpeckleConnectorApp.new(menu_commands, initial_state, ui_controller)
# Add menu commands to SketchUp and Speckle application
Commands::SpeckleMenuCommands.add_initial_commands!(app)
app
end
app = initialize_app
SPECKLE_APP = app
end
@@ -1,30 +0,0 @@
require "sketchup"
require "speckle_connector/converter/to_speckle"
require "speckle_connector/converter/to_native"
module SpeckleSystems::SpeckleConnector
SKETCHUP_UNIT_STRINGS = { "m" => "m", "mm" => "mm", "ft" => "feet", "in" => "inch", "yd" => "yard", "cm" => "cm" }.freeze
public_constant :SKETCHUP_UNIT_STRINGS
class ConverterSketchup
include ToNative
include ToSpeckle
attr_accessor :units, :component_defs
def initialize(units = "m")
@units = units
@component_defs = {}
end
def convert_to_speckle(obj)
case obj.typename
when "Edge" then edge_to_speckle(obj)
when "Face" then face_to_speckle(obj)
when "Group" then component_instance_to_speckle(obj, is_group: true)
when "ComponentDefinition" then component_definition_to_speckle(obj)
when "ComponentInstance" then component_instance_to_speckle(obj)
else nil
end
end
end
end
-175
View File
@@ -1,175 +0,0 @@
require "sketchup"
# To Native conversions for the ConverterSketchup
module SpeckleSystems::SpeckleConnector::ToNative
def traverse_commit_object(obj)
if can_convert_to_native(obj)
convert_to_native(obj, Sketchup.active_model.entities)
elsif obj.is_a?(Hash) && obj.key?("speckle_type")
return if is_ignored_speckle_type(obj)
puts(">>> Found #{obj["speckle_type"]}: #{obj["id"]}")
props = obj.keys.filter_map { |key| key unless key.start_with?("_") }
props.each { |prop| traverse_commit_object(obj[prop]) }
elsif obj.is_a?(Hash)
obj.each_value { |value| traverse_commit_object(value) }
elsif obj.is_a?(Array)
obj.each { |value| traverse_commit_object(value) }
else
nil
end
end
def can_convert_to_native(obj)
return false unless obj.is_a?(Hash) && obj.key?("speckle_type")
[
"Objects.Geometry.Line",
"Objects.Geometry.Polyline",
"Objects.Geometry.Mesh",
"Objects.Geometry.Brep",
"Objects.Other.BlockInstance",
"Objects.Other.BlockDefinition",
"Objects.Other.RenderMaterial"
].include?(obj["speckle_type"])
end
def is_ignored_speckle_type(obj)
["Objects.BuiltElements.Revit.Parameter"].include?(obj["speckle_type"])
end
def convert_to_native(obj, entities = SketchUp.active_model.entities)
puts(">>> Converting #{obj["speckle_type"]}: #{obj["id"]}")
case obj["speckle_type"]
when "Objects.Geometry.Line", "Objects.Geometry.Polyline" then edge_to_native(obj, entities)
when "Objects.Other.BlockInstance" then component_instance_to_native(obj, entities)
when "Objects.Other.BlockDefinition" then component_definition_to_native(obj)
when "Objects.Geometry.Mesh" then mesh_to_native(obj, entities)
when "Objects.Geometry.Brep" then mesh_to_native(obj["displayMesh"], entities)
else
nil
end
# rescue StandardError => e
# puts("Failed to convert #{obj["speckle_type"]} (id: #{obj["id"]})")
# puts(e)
# nil
end
def length_to_native(length, units = @units)
length.__send__(SpeckleSystems::SpeckleConnector::SKETCHUP_UNIT_STRINGS[units])
end
def edge_to_native(line, entities)
if line.key?("value")
values = line["value"]
points = values.each_slice(3).to_a.map { |pt| point_to_native(pt[0], pt[1], pt[2], line["units"]) }
points.push(points[0]) if line["closed"]
entities.add_edges(*points)
else
start_pt = point_to_native(line["start"]["x"], line["start"]["y"], line["start"]["z"], line["units"])
end_pt = point_to_native(line["end"]["x"], line["end"]["y"], line["end"]["z"], line["units"])
entities.add_edges(start_pt, end_pt)
end
end
def face_to_native
nil
end
def point_to_native(x, y, z, units)
Geom::Point3d.new(length_to_native(x, units), length_to_native(y, units), length_to_native(z, units))
end
def mesh_to_native(mesh, entities)
native_mesh = Geom::PolygonMesh.new(mesh["vertices"].count / 3)
points = []
mesh["vertices"].each_slice(3) do |pt|
points.push(point_to_native(pt[0], pt[1], pt[2], mesh["units"]))
end
faces = mesh["faces"]
while faces.count.positive?
num_pts = faces.shift
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
num_pts += 3 if num_pts < 3
indices = faces.shift(num_pts)
native_mesh.add_polygon(indices.map { |index| points[index] })
end
entities.add_faces_from_mesh(native_mesh, 4, material_to_native(mesh["renderMaterial"]))
native_mesh
end
def component_definition_to_native(block_def)
definition = Sketchup.active_model.definitions[block_def["name"]]
return definition if definition && (definition.name == block_def["name"] || definition.guid == block_def["applicationId"])
definition&.entities&.clear!
definition ||= Sketchup.active_model.definitions.add(block_def["name"])
block_def["geometry"].each { |obj| convert_to_native(obj, definition.entities) }
puts("definition finished: #{block_def["name"]} (#{block_def["id"]})")
puts(" entity count: #{definition.entities.count}")
definition
end
def component_instance_to_native(block, entities)
# is_group = block.key?("is_sketchup_group") && block["is_sketchup_group"]
# something about this conversion is freaking out if nested block geo is a group
# so this is set to false always until I can figure this out
is_group = false
definition = component_definition_to_native(block["blockDefinition"])
transform = transform_to_native(
block["transform"].is_a?(Hash) ? block["transform"]["value"] : block["transform"],
block["units"]
)
instance =
if is_group
entities.add_group(definition.entities.to_a)
else
entities.add_instance(definition, transform)
end
puts("Failed to create instance for speckle block instance #{block["id"]}") if instance.nil?
instance.transformation = transform if is_group
instance.material = material_to_native(block["renderMaterial"])
instance
end
def transform_to_native(t_arr, units = @units)
Geom::Transformation.new(
[
t_arr[0],
t_arr[4],
t_arr[8],
t_arr[12],
t_arr[1],
t_arr[5],
t_arr[9],
t_arr[13],
t_arr[2],
t_arr[6],
t_arr[10],
t_arr[14],
length_to_native(t_arr[3], units),
length_to_native(t_arr[7], units),
length_to_native(t_arr[11], units),
t_arr[15]
]
)
end
def material_to_native(render_mat)
return if render_mat.nil?
# return material with same name if it exists
name = render_mat["name"] || render_mat["id"]
material = Sketchup.active_model.materials[name]
return material if material
# create a new sketchup material
material = Sketchup.active_model.materials.add(name)
material.alpha = render_mat["opacity"]
argb = render_mat["diffuse"]
material.color = Sketchup::Color.new((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
material
end
end
-245
View File
@@ -1,245 +0,0 @@
require "sketchup"
# To Speckle conversions for the ConverterSketchup
module SpeckleSystems::SpeckleConnector::ToSpeckle
def length_to_speckle(length)
length.__send__("to_#{SpeckleSystems::SpeckleConnector::SKETCHUP_UNIT_STRINGS[@units]}")
end
def edge_to_speckle(edge)
{
speckle_type: "Objects.Geometry.Line",
applicationId: edge.persistent_id.to_s,
units: @units,
start: vertex_to_speckle(edge.start),
end: vertex_to_speckle(edge.end),
domain: speckle_interval(0, Float(edge.length)),
bbox: bounds_to_speckle(edge.bounds)
}
end
def component_definition_to_speckle(definition)
guid = definition.guid
return @component_defs[guid] if @component_defs.key?(guid)
speckle_def = {
speckle_type: "Objects.Other.BlockDefinition",
applicationId: guid,
units: @units,
name: definition.name,
# i think the base point is always the origin?
basePoint: speckle_point,
"@geometry" => if %w[Edge Face].include?(definition.entities[0].typename)
group_mesh_to_speckle(definition)
else
definition.entities.map { |entity| convert_to_speckle(entity) }
end
}
@component_defs[guid] = speckle_def
end
def component_instance_to_speckle(instance, is_group: false)
transform = instance.transformation
{
speckle_type: "Objects.Other.BlockInstance",
applicationId: instance.guid,
is_sketchup_group: is_group,
units: @units,
bbox: bounds_to_speckle(instance.bounds),
name: instance.name,
renderMaterial: instance.material.nil? ? nil : material_to_speckle(instance.material),
transform: transform_to_speckle(transform),
"@blockDefinition" => component_definition_to_speckle(instance.definition)
}
end
def group_mesh_to_speckle(component_def)
mat_groups = {}
nested_blocks = []
component_def.entities.each do |entity|
nested_blocks.push(component_instance_to_speckle(entity)) if entity.typename == "ComponentInstance"
next unless entity.typename == "Face"
face = entity
# convert material
mat_id = face.material.nil? ? "none" : face.material.entityID
mat_groups[mat_id] = initialise_group_mesh(face, component_def.bounds) unless mat_groups.key?(mat_id)
# add points and texture coordinates
mesh = face.mesh(1)
mat_groups[mat_id]["@(31250)vertices"].push(*points_to_array(mesh))
mat_groups[mat_id]["@(31250)textureCoordinates"].push(*uvs_to_array(mesh))
# add faces
mat_groups[mat_id]["@(62500)faces"].push(*faces_to_array(mesh, mat_groups[mat_id][:pt_count]))
mat_groups[mat_id][:pt_count] += mesh.points.count
end
mat_groups.values.map { |group| group.delete(:pt_count) }
mat_groups.values + nested_blocks
end
def transform_to_speckle(transform)
t_arr = transform.to_a
{
speckle_type: "Objects.Other.Transform",
units: @units,
value: [
t_arr[0],
t_arr[4],
t_arr[8],
length_to_speckle(t_arr[12]),
t_arr[1],
t_arr[5],
t_arr[9],
length_to_speckle(t_arr[13]),
t_arr[2],
t_arr[6],
t_arr[10],
length_to_speckle(t_arr[14]),
t_arr[3],
t_arr[7],
t_arr[11],
t_arr[15]
]
}
end
def initialise_group_mesh(face, bounds)
{
speckle_type: "Objects.Geometry.Mesh",
units: @units,
bbox: bounds_to_speckle(bounds),
"@(31250)vertices" => [],
"@(62500)faces" => [],
"@(31250)textureCoordinates" => [],
pt_count: -1,
renderMaterial: face.material.nil? ? nil : material_to_speckle(face.material)
}
end
def faces_to_array(mesh, offset)
faces = []
mesh.polygons.each do |poly|
faces.push(
case poly.count
when 3 then 0 # tris
when 4 then 1 # polys
else
poly.count # ngons
end,
*poly.map { |coord| coord.abs + offset }
)
end
faces
end
def points_to_array(mesh)
pts_array = []
mesh.points.each do |pt|
pts_array.push(
length_to_speckle(pt[0]),
length_to_speckle(pt[1]),
length_to_speckle(pt[2])
)
end
pts_array
end
def uvs_to_array(mesh)
uvs_array = []
mesh.uvs(true).each do |pt|
uvs_array.push(
length_to_speckle(pt[0] / pt[2]),
length_to_speckle(pt[1] / pt[2])
)
end
uvs_array
end
def face_to_speckle(face)
mesh = face.mesh(1)
{
speckle_type: "Objects.Geometry.Mesh",
units: @units,
renderMaterial: face.material.nil? ? nil : material_to_speckle(face.material),
bbox: bounds_to_speckle(face.bounds),
"@(31250)vertices" => points_to_array(mesh),
"@(62500)faces" => faces_to_array(mesh, -1),
"@(31250)textureCoordinates" => uvs_to_array(mesh)
}
end
def vertex_to_speckle(vertex)
point = vertex.position
{
speckle_type: "Objects.Geometry.Point",
units: @units,
x: length_to_speckle(point[0]),
y: length_to_speckle(point[1]),
z: length_to_speckle(point[2])
}
end
def material_to_speckle(material)
rgba = material.color.to_a
{
speckle_type: "Objects.Other.RenderMaterial",
name: material.name,
diffuse: [rgba[3] << 24 | rgba[0] << 16 | rgba[1] << 8 | rgba[2]].pack("l").unpack1("l"),
opacity: material.alpha,
emissive: -16_777_216,
metalness: 0,
roughness: 1
}
end
def bounds_to_speckle(bounds)
min_pt = bounds.min
{
speckle_type: "Objects.Geometry.Box",
units: @units,
area: 0,
volume: 0,
xSize: speckle_interval(min_pt[0], bounds.width),
ySize: speckle_interval(min_pt[1], bounds.height),
zSize: speckle_interval(min_pt[2], bounds.depth),
basePlane: speckle_plane
}
end
def speckle_interval(start_val, end_val)
{
speckle_type: "Objects.Primitive.Interval",
units: @units,
start: start_val.is_a?(Length) ? length_to_speckle(start_val) : start_val,
end: end_val.is_a?(Length) ? length_to_speckle(end_val) : end_val
}
end
def speckle_point(x = 0.0, y = 0.0, z = 0.0, vector: false)
{
speckle_type: vector ? "Objects.Geometry.Vector" : "Objects.Geometry.Point",
units: @units,
x: x.is_a?(Length) ? length_to_speckle(x) : x,
y: y.is_a?(Length) ? length_to_speckle(y) : y,
z: z.is_a?(Length) ? length_to_speckle(z) : z
}
end
def speckle_vector(x = 0.0, y = 0.0, z = 0.0)
speckle_point(x, y, z, vector: true)
end
def speckle_plane(xdir: [1, 0, 0], ydir: [0, 1, 0], normal: [0, 0, 1], origin: [0, 0, 0])
{
speckle_type: "Objects.Geometry.Plane",
units: @units,
xdir: speckle_vector(*xdir),
ydir: speckle_vector(*ydir),
normal: speckle_vector(*normal),
origin: speckle_point(*origin)
}
end
end
+8 -3
View File
@@ -1,17 +1,22 @@
module SpeckleSystems::SpeckleConnector
# frozen_string_literal: true
# Speckle connector module to enable multiplayer mode ON!
module SpeckleConnector
# from thomthom
# https://github.com/thomthom/true-bend/blob/master/src/tt_truebend/debug.rb
# @note Debug method to reload the plugin.
#
# @example
# SpeckleSystems::SpeckleConnector.reload
# SpeckleConnector.reload
#
# @return [Integer] Number of files reloaded.
# rubocop:disable SketchupSuggestions/FileEncoding
def self.reload
load(__FILE__)
pattern = File.join(__dir__, "**/*.rb")
pattern = File.join(__dir__, '**/*.rb')
Dir.glob(pattern).each { |file| load(file) }
.size
end
# rubocop:enable SketchupSuggestions/FileEncoding
end
-92
View File
@@ -1,92 +0,0 @@
require "JSON"
require "json"
require "sketchup"
require "speckle_connector/converter/converter_sketchup"
require "speckle_connector/accounts"
module SpeckleSystems::SpeckleConnector
UNITS = { 0 => "in", 1 => "ft", 2 => "mm", 3 => "cm", 4 => "m", 5 => "yd" }.freeze
public_constant :UNITS
def self.create_dialog
options = {
dialog_title: "SpeckleSketchUp",
preferences_key: "example.htmldialog.materialinspector",
style: UI::HtmlDialog::STYLE_DIALOG,
min_width: 250,
min_height: 50
}
dialog = UI::HtmlDialog.new(options)
dialog.center
dialog
end
def self.show_dialog
if @dialog&.visible?
@dialog.bring_to_front
else
@dialog ||= create_dialog
@dialog.add_action_callback("send_selection") do |_action_context, stream_id|
send_selection(stream_id)
nil
end
@dialog.add_action_callback("receive_objects") do |_action_context, base, stream_id|
receive_objects(base, stream_id)
nil
end
@dialog.add_action_callback("reload_accounts") do |_action_context|
reload_accounts
end
@dialog.add_action_callback("init_local_accounts") do |_action_context|
init_local_accounts
end
if DEV_MODE
puts('Launching Speckle Connector from http://localhost:8081')
@dialog.set_url("http://localhost:8081")
else
html_file = File.join(File.dirname(File.expand_path(__FILE__)), "html", "index.html")
puts("Launching Speckle Connector from #{html_file}")
@dialog.set_file(html_file)
end
@dialog.show
@dialog
end
end
def self.send_selection(stream_id)
model = Sketchup.active_model
converter = ConverterSketchup.new(UNITS[model.options["UnitsOptions"]["LengthUnit"]])
converted = model.selection.map { |entity| converter.convert_to_speckle(entity) }
puts("converted #{converted.count} objects for stream #{stream_id}")
# puts(converted.to_json)
@dialog.execute_script("convertedFromSketchup('#{stream_id}',#{converted.to_json})")
rescue StandardError => e
puts(e)
@dialog.execute_script("sketchupOperationFailed('#{stream_id}')")
end
def self.receive_objects(base, stream_id)
puts("received objects from stream #{stream_id}")
model = Sketchup.active_model
converter = ConverterSketchup.new(UNITS[model.options["UnitsOptions"]["LengthUnit"]])
converter.traverse_commit_object(base)
@dialog.execute_script("finishedReceiveInSketchup('#{stream_id}')")
rescue StandardError => e
puts(e)
@dialog.execute_script("sketchupOperationFailed('#{stream_id}')")
end
def self.init_local_accounts
puts("Initialisation of Speckle accounts requested by plugin")
@dialog.execute_script("loadAccounts(#{Accounts.load_accounts.to_json}, #{Accounts.get_suuid.to_json})")
end
def self.reload_accounts
puts("Reload of Speckle accounts requested by plugin")
@dialog.execute_script("loadAccounts(#{Accounts.load_accounts.to_json})")
end
end

Before

Width:  |  Height:  |  Size: 798 B

After

Width:  |  Height:  |  Size: 798 B

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 665 B

After

Width:  |  Height:  |  Size: 665 B

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 639 B

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

-27
View File
@@ -1,27 +0,0 @@
# frozen_string_literal: true
require "sketchup"
require "speckle_connector/dialog.rb"
require "speckle_connector/debug.rb"
module SpeckleSystems
module SpeckleConnector
unless file_loaded?(__FILE__)
cmd_cube = UI::Command.new("Dialog") { show_dialog }
cmd_cube.tooltip = "Launch Connector"
cmd_cube.status_bar_text = "Opens the Speckle Connector window"
cmd_cube.small_icon = "icons/s2logo.png"
cmd_cube.large_icon = "icons/s2logo.png"
menu = UI.menu("Tools")
menu.add_item(cmd_cube)
toolbar = UI::Toolbar.new("Speckle")
toolbar.add_item(cmd_cube)
toolbar.restore
file_loaded(__FILE__)
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'JSON'
require_relative '../ext/sqlite3'
require_relative '../constants/path_constants'
module SpeckleConnector
# Accounts to communicate with models on user's account.
module Accounts
def self.load_accounts
db_path = SPECKLE_ACCOUNTS_DB_PATH
unless File.exist?(db_path)
raise(
IOError,
"No Accounts db found. Please read the guide for different options for adding your account:\n
https://speckle.guide/user/manager.html#adding-accounts"
)
end
db = Sqlite3::Database.new(db_path)
rows = db.exec('SELECT * FROM objects')
db.close
rows.map { |row| JSON.parse(row[1]) }
end
def self.default_account
accounts = load_accounts
accounts.select { |acc| acc['isDefault'] }[0] || accounts[0]
end
end
end
+15
View File
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module SpeckleConnector
module Actions
# State changer object.
class Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @param parameters [Array] parameters that the action takes
# @return [States::State] the new updated state object
def self.update_state(_state, *_parameters)
raise NotImplementedError, 'Implement in subclass.'
end
end
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
require_relative 'action'
module SpeckleConnector
module Actions
# Clear queue from state.
class ClearQueue < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state)
new_speckle_state = state.speckle_state.with(:@message_queue => {})
state.with(:@speckle_state => new_speckle_state)
end
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../ext/sqlite3'
require_relative '../constants/path_constants'
module SpeckleConnector
module Actions
# Action to collect preferences from database to UI.
class CollectPreferences < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state, _data)
state.with_add_queue('collectPreferences', state.user_state.preferences.to_json, [])
end
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module SpeckleConnector
module Actions
# Action to update connected state of application.
class Connected < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state)
puts 'Speckle connected!'
state.with(:@connected => true)
end
end
end
end
@@ -0,0 +1,66 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
require_relative '../actions/save_stream'
require_relative '../actions/queue_send'
require_relative '../convertors/converter'
module SpeckleConnector
module Actions
# Create stream.
class CreateStream < Action
def initialize(stream_name: nil)
super()
@stream_name = stream_name
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
# rubocop:disable Metrics/MethodLength
def update_state(state)
puts 'send to speckle'
acct = Accounts.default_account
if acct.nil?
puts 'No local account found. Please refer to speckle.guide for more information.'
return state
end
sketchup_model = state.sketchup_state.sketchup_model
path = sketchup_model.path
if @stream_name.nil?
@stream_name = path ? File.basename(path, '.*') : 'Untitled SketchUp Model'
end
query = 'mutation streamCreate($stream: StreamCreateInput!) {streamCreate(stream: $stream)}'
vars = { stream: { name: @stream_name, description: 'Stream created from SketchUp' } }
request = Sketchup::Http::Request.new("#{acct['serverInfo']['url']}/graphql", Sketchup::Http::POST)
request.headers = { 'Authorization' => "Bearer #{acct['token']}", 'Content-Type' => 'application/json' }
request.body = { query: query, variables: vars }.to_json
to_convert = if sketchup_model.selection.count > 0
sketchup_model.selection
else
sketchup_model.entities
end
state = evaluate_request(sketchup_model, request, state, to_convert)
Actions::LoadSavedStreams.update_state(state, {})
end
# rubocop:enable Metrics/MethodLength
private
def evaluate_request(sketchup_model, request, state, to_convert)
converter = Converters::Converter.new(sketchup_model)
request.start do |_req, res|
res_data = JSON.parse(res.body)['data']
raise(StandardError) unless res_data
stream_id = res_data['streamCreate']
state = Actions::SaveStream.new(stream_id).update_state(state)
converted = to_convert.map { |entity| converter.convert_to_speckle(entity) }
state = Actions::QueueSend.new(stream_id, converted).update_state(state)
end
state
end
end
end
end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
require_relative 'load_saved_streams'
module SpeckleConnector
module Actions
# Action to initialize local accounts from database.
class InitLocalAccounts < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state, _data)
puts 'Initialisation of Speckle accounts requested by plugin'
accounts_data = state.speckle_state.accounts
state.with_add_queue('loadAccounts', accounts_data.to_json, [])
end
end
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../states/state'
require_relative '../states/speckle_state'
require_relative '../states/sketchup_state'
require_relative '../accounts/accounts'
require_relative '../preferences/preferences'
module SpeckleConnector
module Actions
# Initialization of the real state of the speckle.
class InitializeSpeckle < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state)
accounts = SpeckleConnector::Accounts.load_accounts
speckle_state = States::SpeckleState.new(accounts, {}, {})
# This should be the only point that `Sketchup_active_model` passed to application state.
sketchup_state = States::SketchupState.new(Sketchup.active_model)
preferences = Preferences.init_preferences(sketchup_state.sketchup_model)
user_state_with_preferences = state.user_state.with_preferences(preferences)
States::State.new(user_state_with_preferences, speckle_state, sketchup_state, false)
end
end
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
require_relative 'action'
module SpeckleConnector
module Actions
# Action to load saved streams.
class LoadSavedStreams < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state, _data)
(saved_streams = state.sketchup_state.sketchup_model.attribute_dictionary('speckle', true)['streams']) or []
state.with_add_queue('setSavedStreams', saved_streams, [])
end
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../ext/sqlite3'
require_relative '../accounts/accounts'
require_relative '../constants/path_constants'
require_relative '../sketchup_model/dictionary/speckle_model_dictionary_handler'
module SpeckleConnector
module Actions
# When preference updated by UI.
class ModelPreferencesUpdated < Action
def initialize(pref, value)
super()
@preference = pref
@value = value
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
model = state.user_state.preferences[:model].dup
model[@preference.to_sym] = @value
new_preferences = state.user_state.preferences.put(:model, model)
SketchupModel::Dictionary::SpeckleModelDictionaryHandler.set_attribute(
state.sketchup_state.sketchup_model,
@preference.to_sym,
@value,
'Speckle'
)
new_user_state = state.user_state.with_preferences(new_preferences)
state.with_user_state(new_user_state)
end
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
require_relative '../actions/create_stream'
require_relative '../actions/queue_send'
require_relative '../convertors/to_speckle'
module SpeckleConnector
module Actions
# Sends to speckle.
class OneClickSend < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state)
puts 'send to speckle'
default_account = Accounts.default_account
if default_account.nil?
puts 'No local account found. Please refer to speckle.guide for more information.'
return state
end
sketchup_model = state.sketchup_state.sketchup_model
to_convert = sketchup_model.selection.count > 0 ? sketchup_model.selection : sketchup_model.entities
first_saved_stream = first_saved_stream(sketchup_model)
action = if first_saved_stream.nil?
Actions::CreateStream.new
else
Actions::QueueSend.new(first_saved_stream, convert_to_speckle(sketchup_model, to_convert))
end
action.update_state(state)
end
def self.first_saved_stream(model)
(saved_streams = model.attribute_dictionary('speckle', true)['streams']) or []
saved_streams.nil? || saved_streams.empty? ? nil : saved_streams[0]
end
def self.convert_to_speckle(sketchup_model, to_convert)
converter = Converters::ToSpeckle.new(sketchup_model)
to_convert.map { |entity| converter.convert(entity) }
end
end
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../states/state'
require_relative '../states/speckle_state'
require_relative '../actions/send_from_queue'
module SpeckleConnector
module Actions
# Send queue from state.
class QueueSend < Action
def initialize(stream_id, converted)
super()
@stream_id = stream_id
@converted = converted
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
to_send = { stream_id: @stream_id, converted: @converted }
new_speckle_state = state.speckle_state.with(:@stream_queue => to_send)
new_state = state.with(:@speckle_state => new_speckle_state)
if new_state.is_connected
action = Actions::SendFromQueue.new(@stream_id)
new_state = action.update_state(new_state)
end
new_state
end
end
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../convertors/units'
require_relative '../convertors/to_native'
module SpeckleConnector
module Actions
# Action to receive objects from Speckle Server.
class ReceiveObjects < Action
def initialize(stream_id, base, stream_name, branch_name, branch_id)
super()
@stream_id = stream_id
@base = base
@stream_name = stream_name
@branch_name = branch_name
@branch_id = branch_id
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
converter = Converters::ToNative.new(state.sketchup_state.sketchup_model)
# Have side effects on the sketchup model. It effects directly on the entities by adding new objects.
start_time = Time.now.to_f
converter.receive_commit_object(@base, state.user_state.preferences[:model])
elapsed_time = (Time.now.to_f - start_time).round(3)
puts "==== Converting to Native executed in #{elapsed_time} sec ===="
state.with_add_queue('finishedReceiveInSketchup', @stream_id, [])
end
end
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
require_relative 'load_saved_streams'
module SpeckleConnector
module Actions
# Action to reload accounts from database.
class ReloadAccounts < Action
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def self.update_state(state, _data)
puts 'Reload of Speckle accounts requested by plugin'
new_speckle_state = state.speckle_state.with_accounts(Accounts.load_accounts)
state = state.with_speckle_state(new_speckle_state)
accounts_data = state.speckle_state.accounts
state.with_add_queue('loadAccounts', accounts_data.to_json, [])
end
end
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
require_relative '../convertors/units'
require_relative '../convertors/converter'
module SpeckleConnector
module Actions
# Action to remove stream.
# Currently it is not a state changer.
class RemoveStream < Action
def initialize(stream_id)
super()
@stream_id = stream_id
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
speckle_dict = state.sketchup_state.sketchup_model.attribute_dictionary('speckle', true)
saved = speckle_dict['streams'] || []
saved -= [@stream_id]
speckle_dict['streams'] = saved
state
end
end
end
end
@@ -0,0 +1,27 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
module SpeckleConnector
module Actions
# Save stream.
# Currently it is not a state changer.
class SaveStream < Action
def initialize(stream_id)
super()
@stream_id = stream_id
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
speckle_dict = state.sketchup_state.sketchup_model.attribute_dictionary('speckle', true)
saved = speckle_dict['streams'] || []
saved = saved.empty? ? [@stream_id] : saved.unshift(@stream_id)
speckle_dict['streams'] = saved
state
end
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../accounts/accounts'
module SpeckleConnector
module Actions
# Send already converted objects from queue if exist on stream.
class SendFromQueue < Action
def initialize(stream_id)
super()
@stream_id = stream_id
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
to_send_stream_id = state.speckle_state.stream_queue[:stream_id]
return state if to_send_stream_id == @stream_id
to_send_converted = state.speckle_state.stream_queue[:converted].to_json
new_state = state.with_add_queue('convertedFromSketchup', to_send_stream_id, [to_send_converted])
new_state = new_state.with_add_queue('oneClickSend', to_send_stream_id, [])
new_state.with_empty_stream_queue
end
end
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../convertors/units'
require_relative '../convertors/to_speckle'
module SpeckleConnector
module Actions
# Send selection to server.
class SendSelection < Action
def initialize(stream_id)
super()
@stream_id = stream_id
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
sketchup_model = state.sketchup_state.sketchup_model
converter = Converters::ToSpeckle.new(sketchup_model)
base = converter.convert_selection_to_base(state.user_state.preferences)
id, total_children_count, batches = converter.send_info(base)
puts("converted #{base.count} objects for stream #{@stream_id}")
state.with_add_queue('convertedFromSketchup', @stream_id, [
{ is_string: false, val: batches },
{ is_string: true, val: id },
{ is_string: false, val: total_children_count }
])
end
end
end
end
@@ -0,0 +1,53 @@
# frozen_string_literal: true
require_relative 'action'
require_relative '../ext/sqlite3'
require_relative '../accounts/accounts'
require_relative '../constants/path_constants'
module SpeckleConnector
module Actions
# When preference updated by UI.
class UserPreferencesUpdated < Action
def initialize(pref_hash, pref, value)
super()
@preference_hash = pref_hash
@preference = pref
@value = value
end
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
# @return [States::State] the new updated state object
def update_state(state)
# Init sqlite database
db = Sqlite3::Database.new(SPECKLE_CONFIG_DB_PATH)
# Select data
data = db.exec("SELECT content FROM 'objects' WHERE hash = '#{@preference_hash}'").first.first
# Parse string to hash
data_hash = JSON.parse(data).to_h
# Get current preference value
old_preference_value = data_hash[@preference]
# Return old state if it is equal to new one
return state if @value == old_preference_value
data_hash[@preference] = @value
# Update entry unless equal old to new
db.exec("UPDATE 'objects' SET content = '#{data_hash.to_json}' WHERE hash = '#{@preference_hash}'")
# Close db when process done
db.close
user = state.user_state.preferences[:user].dup
user[@preference.to_sym] = @value
new_preferences = state.user_state.preferences.put(:user, user)
new_user_state = state.user_state.with_preferences(new_preferences)
state.with_user_state(new_user_state)
end
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
require_relative '../actions/clear_queue'
module SpeckleConnector
module App
# Application for the Speckle Connector.
class SpeckleConnectorApp
# @return [Commands::MenuCommandHandler] the commands registered in the extension menu in Sketchup
attr_reader :menu_commands
# @return [States::State] the current states of the app
attr_reader :state
# @return [Ui::UiController] controller for ui views
attr_reader :ui_controller
def initialize(menu_commands, state, ui_controller)
@menu_commands = menu_commands
@state = state
@ui_controller = ui_controller
end
def speckle_loaded?
state.speckle_state?
end
def update_ui!
ui_controller.update_ui(state)
end
def send_messages!
queue = @state.speckle_state.message_queue
queue.each_value { |value| ui_controller.user_interfaces[Ui::SPECKLE_UI_ID].dialog.execute_script(value) }
update_state!(Actions::ClearQueue)
end
def update_state!(action, *parameters)
old_state = @state
@state = action.update_state(old_state, *parameters)
send_messages! if @state.speckle_state.message_queue.any?
update_ui! unless @state.equal?(old_state)
end
end
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
module SpeckleConnector
module Callbacks
# Helper class to serialize messages to send dialog.
class CallbackMessage
# @param callback_name [String] name of the callback command
# @param stream_id [String] id of the stream
# @param parameters [Array<String>] parameters of the callback method call
def self.serialize(callback_name, stream_id, parameters)
if parameters.any?
serialize_with_parameters(callback_name, stream_id, parameters)
else
serialize_without_parameters(callback_name, stream_id)
end
end
# @param callback_name [String] name of the callback command
# @param stream_id [String] id of the stream
# @param parameters [Array<Object>] parameters of the callback method call
def self.serialize_with_parameters(callback_name, stream_id, parameters)
message = "#{callback_name}('#{stream_id}'"
parameters.each { |par| message += par[:is_string] ? ",'#{par[:val]}'" : ",#{par[:val]}" }
message += ')'
message
end
# @param callback_name [String] name of the callback command
# @param stream_id [String] id of the stream
def self.serialize_without_parameters(callback_name, stream_id)
if %w[setSavedStreams loadAccounts].include?(callback_name)
"#{callback_name}(#{stream_id})"
else
"#{callback_name}('#{stream_id}')"
end
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
require_relative 'command'
module SpeckleConnector
module Commands
# Command to update state of the application.
class ActionCommand < Command
# @param app [App::SpeckleConnectorApp] the app object to run command on
# @param action [#update_state] the action that knows how to change the state of the speckle app
def initialize(app, action)
super(app)
@app = app
@action = action
end
private
def _run(*parameters)
app.update_state!(@action, *parameters)
end
end
end
end
+31
View File
@@ -0,0 +1,31 @@
# frozen_string_literal: true
module SpeckleConnector
module Commands
# Base command schema to wrap common operations for all commands.
class Command
# @return [App::SpeckleConnectorApp] the main app object
attr_reader :app
# @return [Ui::View] view object holds dialog and it's state
attr_reader :view
# @@param app [App::SpeckleConnectorApp] the main app object
def initialize(app)
@app = app
@view = app.ui_controller.user_interfaces[Ui::SPECKLE_UI_ID]
end
def run(*parameters)
# Run here common operations that same for each command.
_run(*parameters)
end
private
def _run(*_parameters)
raise NotImplementedError, 'Implement in subclass'
end
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
require_relative 'command'
module SpeckleConnector
module Commands
# Run this command when the UI is ready to get data
class DialogReady < Command
# Update the selected user interface
def _run(_data)
view.update_view(app.state)
end
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../states/initial_state'
require_relative '../ui/vue_view'
require_relative '../actions/initialize_speckle'
module SpeckleConnector
module Commands
# Command to initialize Speckle UI and register it to ui_controller.
# This is the command where we show UI to user.
class InitializeSpeckle < Command
def dialog_title
"Speckle #{CONNECTOR_VERSION}"
end
private
def _run
app = self.app
unless app.state.instance_of?(States::InitialState)
vue_view = app.ui_controller.user_interfaces[Ui::SPECKLE_UI_ID]
vue_view.show
return
end
initialize_speckle(app)
end
# Do the actual Speckle initialization.
def initialize_speckle(app)
# TODO: Initialize here speckle states and observers.
app.update_state!(Actions::InitializeSpeckle)
dialog_specs = {
dialog_id: Ui::SPECKLE_UI_ID,
htm_file: Ui::VUE_UI_HTML,
dialog_title: dialog_title,
height: 950,
width: 300
}
vue_view = Ui::VueView.new(dialog_specs, app)
app.ui_controller.register_ui(Ui::SPECKLE_UI_ID, vue_view)
vue_view.show
end
end
end
end
@@ -0,0 +1,51 @@
# frozen_string_literal: true
module SpeckleConnector
module Commands
# Helper class to register, handle menu and toolbar commands.
class MenuCommandHandler
# @param command [#run] command that can be run
# @param menu_text [String] name of the command that will be displayed on the menu
# @return [UI::Command] the command that can be added to Sketchup menu or toolbar
def self.sketchup_command(command, menu_text)
UI::Command.new(menu_text) do
command.run
end
end
# Validate if the user has started the Speckle and return a status code that can be used by
# {UI::Command#set_validation_proc} to disable menu entries and toolbar entries before Speckle is loaded.
def self.speckle_started(app)
return MF_ENABLED if app.speckle_loaded?
MF_GRAYED
end
def initialize
@menu_commands = {}
@added_to_menu = []
@added_to_toolbar = []
end
def []=(command_id, command)
@menu_commands[command_id] = command
end
# Add command to menu.
def add_to_menu!(command_id, menu)
return if @added_to_menu.include? command_id
menu.add_item(@menu_commands[command_id])
@added_to_menu << command_id
end
# Add command to toolbar.
def add_to_toolbar!(command_id, toolbar)
return if @added_to_toolbar.include? command_id
toolbar.add_item(@menu_commands[command_id])
@added_to_toolbar << command_id
end
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../accounts/accounts'
require_relative '../actions/model_preference_updated'
module SpeckleConnector
module Commands
# Command to update theme.
class ModelPreferencesUpdated < Command
def _run(data)
preference = data['preference']
new_value = data['value']
app.update_state!(Actions::ModelPreferencesUpdated.new(preference, new_value))
end
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../actions/connected'
require_relative '../actions/send_from_queue'
module SpeckleConnector
module Commands
# Command to notify connected.
class NotifyConnected < Command
def _run(data)
stream_id = data['stream_id']
app.update_state!(Actions::Connected)
app.update_state!(Actions::SendFromQueue.new(stream_id))
end
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../actions/receive_objects'
module SpeckleConnector
module Commands
# Command to receive objects from Speckle Server.
class ReceiveObjects < Command
def _run(data)
stream_id = data['stream_id']
base = data['base']
branch_name = data['branch_name']
branch_id = data['branch_id']
stream_name = data['stream_name']
action = Actions::ReceiveObjects.new(stream_id, base, stream_name, branch_name, branch_id)
app.update_state!(action)
end
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../actions/remove_stream'
require_relative '../actions/load_saved_streams'
module SpeckleConnector
module Commands
# Command to remove stream.
class RemoveStream < Command
def _run(data)
stream_id = data['stream_id']
action = Actions::RemoveStream.new(stream_id)
app.update_state!(action)
app.update_state!(Actions::LoadSavedStreams)
end
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../accounts/accounts'
require_relative '../actions/save_stream'
require_relative '../actions/load_saved_streams'
module SpeckleConnector
module Commands
# Command to saved stream.
class SaveStream < Command
def _run(data)
stream_id = data['stream_id']
app.update_state!(Actions::SaveStream.new(stream_id))
end
end
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../actions/send_selection'
module SpeckleConnector
module Commands
# Command to send selection to Speckle Server.
class SendSelection < Command
def _run(data)
stream_id = data['stream_id']
action = Actions::SendSelection.new(stream_id)
app.update_state!(action)
end
end
end
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
require_relative 'menu_command_handler'
require_relative 'action_command'
require_relative 'initialize_speckle'
require_relative '../actions/one_click_send'
module SpeckleConnector
module Commands
# Speckle menu commands that adds them to Sketchup menu and toolbar.
class SpeckleMenuCommands
CMD_INITIALIZE_SPECKLE = :initialize_speckle
CMD_SEND_TO_SPECKLE = :send_to_speckle
CMD_RECEIVE_FROM_SPECKLE = :receive_from_speckle
# Add initial set of commands to Speckle application object and to Sketchup menu and toolbar
# @param app [App::SpeckleConnectorApp] the application object
def self.add_initial_commands!(app)
commands = app.menu_commands
ui_controller = app.ui_controller
sketchup_ui = ui_controller.sketchup_ui
speckle_menu = sketchup_ui.speckle_menu
speckle_toolbar = sketchup_ui.speckle_toolbar
commands[CMD_INITIALIZE_SPECKLE] = initialize_speckle_command(app)
commands.add_to_menu!(CMD_INITIALIZE_SPECKLE, speckle_menu)
commands.add_to_toolbar!(CMD_INITIALIZE_SPECKLE, speckle_toolbar)
# commands[CMD_SEND_TO_SPECKLE] = send_command(app)
# commands.add_to_menu!(CMD_SEND_TO_SPECKLE, speckle_menu)
# commands.add_to_toolbar!(CMD_SEND_TO_SPECKLE, speckle_toolbar)
end
def self.initialize_speckle_command(app)
cmd = MenuCommandHandler.sketchup_command(
InitializeSpeckle.new(app), 'Initialize Speckle'
)
cmd.tooltip = 'Launch Connector'
cmd.status_bar_text = 'Opens the Speckle Connector window'
cmd.small_icon = '../../img/s2logo.png'
cmd.large_icon = '../../img/s2logo.png'
cmd
end
def self.send_command(app)
cmd = MenuCommandHandler.sketchup_command(
ActionCommand.new(app, Actions::OneClickSend), 'Send to Speckle'
)
cmd.tooltip = 'Send to Speckle'
cmd.status_bar_text = 'Send to Speckle'
cmd.small_icon = '../../img/Sender.png'
cmd.large_icon = '../../img/Sender.png'
cmd.set_validation_proc { MenuCommandHandler.speckle_started(app) }
cmd
end
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../accounts/accounts'
require_relative '../actions/user_preferences_updated'
module SpeckleConnector
module Commands
# Command to update preferences.
class UserPreferencesUpdated < Command
def _run(data)
preference_hash = data['preference_hash']
preference = data['preference']
new_value = data['value']
app.update_state!(Actions::UserPreferencesUpdated.new(preference_hash, preference, new_value))
end
end
end
end
@@ -0,0 +1,8 @@
# frozen_string_literal: true
module SpeckleConnector
SPECKLE_ID = 'speckle_id'
SPECKLE_TYPE = 'speckle_type'
APPLICATION_ID = 'application_id'
TOTAL_CHILDREN_COUNT = 'total_children_count'
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'pathname'
require_relative 'platform_constants'
# Speckle connector module to enable multiplayer mode ON!
module SpeckleConnector
dir = __dir__.dup
dir.force_encoding('UTF-8') if dir.respond_to?(:force_encoding)
SPECKLE_SRC_PATH = Pathname.new(File.expand_path('..', dir)).cleanpath.to_s
SPECKLE_APPDATA_PATH = case OPERATING_SYSTEM
when OS_WIN
path = ENV.fetch('APPDATA')
Pathname.new(File.join(path, 'Speckle')).cleanpath.to_s
when OS_MAC
File.join(Dir.home, 'Library/Application Support/Speckle')
else
raise 'Speckle could not determine your Appdata path'
end
SPECKLE_ACCOUNTS_DB_PATH = File.join(SPECKLE_APPDATA_PATH, 'Accounts.db')
SPECKLE_CONFIG_DB_PATH = File.join(SPECKLE_APPDATA_PATH, 'Config.db')
SPECKLE_TEST_DB_PATH = File.join(SPECKLE_APPDATA_PATH, 'sketchup_test.db')
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module SpeckleConnector
host_os = RbConfig::CONFIG['host_os']
OS_WIN = :windows
OS_MAC = :macos
OPERATING_SYSTEM = case host_os
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
OS_WIN
when /darwin|mac os/
OS_MAC
else
raise "Unsupported OS: #{host_os.inspect}"
end
RUBY_VERSION_NUMBER = RUBY_VERSION.split('.')[0..1].join.to_i
end
# rubocop:enable Style/Documentation
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module SpeckleConnector
VERSION = "0.1.0"
BASE_OBJECT = 'Base'
end
@@ -0,0 +1,268 @@
# frozen_string_literal: true
# rubocop:disable SketchupPerformance/OpenSSL
require 'securerandom'
# rubocop:enable SketchupPerformance/OpenSSL
require 'digest'
require_relative 'converter'
require_relative '../relations/many_to_one_relation'
module SpeckleConnector
module Converters
# Serializer of the base object.
# Responsible to create id (hash) of the objects by holding their lineage and detaching relationships.
class BaseObjectSerializer
# @return [Integer] default chunk size the determine splitting base prop into chucks
attr_reader :default_chunk_size
def initialize(default_chunk_size = 1000)
@default_chunk_size = default_chunk_size
@detach_lineage = []
@lineage = []
@family_tree = {}
@family_tree_relation = Relations::ManyToOneRelation.new
@closure_table = {}
@objects = {}
end
# @param base [Object] top base object to populate all children and their relationship
# @return [String, String] id (hash) and traversed hash
def serialize(base)
id, traversed = traverse_base(base)
@objects[id] = traversed
return id, traversed
end
def total_children_count(id)
@objects[id][:totalChildrenCount]
end
# @param base [Object] base object to populate all children and their relationship
# rubocop:disable Metrics/MethodLength
def traverse_base(base)
# 1. Create random string for lineage tracking.
@lineage.append(SecureRandom.hex)
# 2. Initialize traversed base object that will be filled with traversed values or
# traversed base objects as props.
traversed_base = SpeckleObjects::Base.new(speckle_type: base[:speckle_type], id: '')
traversed_base.delete(:applicationId)
# 3. Iterate all entries (key, value) of the base {Base > Hash} object
traverse_base_props(base, traversed_base)
# this is where all props are done for current `traversed_base`
# 4. Get last item from detach_lineage array
is_detached = @detach_lineage.pop
# 5. Add closures
closure = {}
parent = @lineage.pop
unless @family_tree[parent].nil?
@family_tree[parent].each do |ref, depth|
closure[ref] = depth - @detach_lineage.length
end
end
# 6. Add total children count
traversed_base[:totalChildrenCount] = closure.keys.length
# 7. Finally create id
id = get_id(traversed_base)
# 8. Add id to traversed base
traversed_base[:id] = id
# 9. Update __closure table on the traversed base
unless traversed_base[:totalChildrenCount].nil?
@closure_table[id] = closure
traversed_base[:__closure] = closure unless closure.empty?
end
# 10. Save object string if detached
@objects[id] = traversed_base if is_detached
return id, traversed_base
end
# rubocop:enable Metrics/MethodLength
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/BlockLength
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def traverse_base_props(base, traversed_base)
base.each do |prop, value|
# 3.1. Ignore nil, starts with '_' and 'id'
next if value.nil? || prop[0] == '_' || prop == 'id' || prop == :id
# 3.2. Pass primitives without any operation (string, numeric, boolean)
unless value.is_a?(Hash) || value.is_a?(Array)
traversed_base[prop] = value
next
end
# 3.3. Determine prop is detached or not
is_prop_detach = prop[0] == '@'
# 3.4. Check prop needs to split into chunks
chunked_detach_match = prop.match(/^@\((\d*)\)/)
# 3.5. If split chunk is needed and prop value is array, then run chunking process
if value.is_a?(Array) && chunked_detach_match
# 3.5.1. Determine chunk size, get it from prop if defined. ex: '@(31250)faces' -> 31250 = chunk size
chunk_size = chunked_detach_match[1] == '' ? default_chunk_size : chunked_detach_match[1].to_i
# 3.5.2. Init empty array for chunks
chunks = []
# 3.5.3. Init empty data chunk core object
chunk = {
speckle_type: 'Speckle.Core.Models.DataChunk',
data: []
}
# 3.5.4. Iterate each element on array to fill them into chunks
value.each_with_index do |el, index|
# 3.5.4.1. If current index is the multiplier of the chunk size, then need to append chunk into chunks
# and reinitialize empty chunk for next batch
if (index % chunk_size == 0) && index != 0
chunks.append(chunk)
chunk = {
speckle_type: 'Speckle.Core.Models.DataChunk',
data: []
}
end
# 3.5.4.2. Add element into chunk
chunk[:data].append(el)
end
# 3.5.5. Add trailing batch to the chunks also unless is empty
chunks.append(chunk) unless chunk[:data].empty?
# 3.5.6. Initialize empty chunk reference array
chunk_references = []
chunks.each do |chunk_element|
@detach_lineage.append(is_prop_detach)
id, _traversed = traverse_base(chunk_element)
chunk_references.append(detach_helper(id))
end
# 3.5.7. Add chunk references to the traversed base prop without @(<chunk_size>)
traversed_base[prop.to_s.sub(chunked_detach_match[0], '')] = chunk_references
# 3.5.8. We are done chunking, good to go next
next
end
# 3.6. traverse value according to value is a speckle object or not
if value.is_a?(Hash) && !value[:speckle_type].nil?
child = traverse_value(value, is_prop_detach)
traversed_base[prop] = is_prop_detach ? detach_helper(child[:id]) : child
else
traversed_base[prop] = traverse_value(value, is_prop_detach)
end
end
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/BlockLength
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Style/OptionalBooleanParameter
def traverse_value(value, is_detach = false)
# 1. Return same value if value is primitive type (string, numeric, boolean)
return value unless value.is_a?(Hash) || value.is_a?(Array)
# 2. Arrays
if value.is_a?(Array)
# 2.1. If it is not detached then iterate array by traversing with their value
return value.collect { |el| traverse_value(el) } unless is_detach
# 2.2. If it is detached than collect them into detached_list
detached_list = []
value.each do |el|
if (el.is_a?(Array) || el.is_a?(Hash)) && !el[:speckle_type].nil?
@detach_lineage.append(is_detach)
id, _traversed_base = traverse_base(el)
detached_list.append(detach_helper(id))
else
detached_list.append(traverse_value(el, is_detach))
end
end
return detached_list
end
# 3. Hash
return value if value[:speckle_type].nil?
# 4. Base objects
unless value[:speckle_type].nil?
@detach_lineage.append(is_detach)
_id, traversed_base = traverse_base(value)
return traversed_base
end
# 5. If it is not returned until here then there is unsupported type
raise StandardError "Unsupported type #{value.class} : #{value}"
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Style/OptionalBooleanParameter
def detach_helper(reference_id)
@lineage.each do |parent|
# init parent on the family tree unless exist
@family_tree[parent] = {} if @family_tree[parent].nil?
is_ref_exist = !@family_tree[parent].nil? && !@family_tree[parent][reference_id].nil?
if !is_ref_exist || @family_tree[parent][reference_id] > @detach_lineage.length
@family_tree[parent][reference_id] = @detach_lineage.length
end
end
{
referencedId: reference_id,
speckle_type: 'reference'
}
end
# @param traversed_base [SpeckleConnector::SpeckleObjects::Base] traversed base object.
def get_id(traversed_base)
Digest::MD5.hexdigest(traversed_base.to_json)
end
# rubocop:disable Metrics/MethodLength
def batch_objects(max_batch_size_mb = 1)
max_size = 1000 * 1000 * max_batch_size_mb
batches = []
batch = '['
batch_size = 0
objects = @objects.values
objects.each do |obj|
obj_json = obj.to_json
if batch_size + obj_json.length < max_size
batch += obj_json
batch += ','
batch_size += obj_json.length
else
batch = batch.chop
batches.append("#{batch}]")
batch = "[#{obj_json},"
batch_size = obj_json.length
end
end
batch = batch.chop
batches.append("#{batch}]")
batches
end
# rubocop:enable Metrics/MethodLength
end
end
end
@@ -0,0 +1,138 @@
# frozen_string_literal: true
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
module SpeckleConnector
module Converters
# CleanUp is a plugin developed by [Thomas Thomassen](https://github.com/thomthom).
module CleanUp
# Removes coplanar entities from the given entities.
# @param entities [Sketchup::Entities] entities to remove edges between that make entities coplanar.
# @note Merging coplanar faces idea originated from [CleanUp](https://github.com/thomthom/cleanup) plugin
# which is developed by [Thomas Thomassen](https://github.com/thomthom).
def self.merge_coplanar_faces(entities)
edges = []
faces = entities.collect { |entity| entity if entity.is_a? Sketchup::Face }.compact
faces.each { |face| face.edges.each { |edge| edges << edge } }
edges.uniq!
edges.each { |edge| remove_edge_have_coplanar_faces(edge, faces, false) }
end
# Detect edges to remove by checking following controls respectively;
# - Upcoming Sketchup entity is Sketchup::Edge or not.
# - Whether edge has 2 face or not.
# - Whether faces are duplicated or not.
# - Whether edges safe to merge or not.
# - Whether faces have same material or not.
# - Whether UV texture map is aligned between faces or not.
# - Finally, if faces are coplanar by correcting these checks, then removes edge from Sketchup.active_model.
# @param edge [Sketchup::Edge] edge to check.
# @param faces [Array<Sketchup::Face>] scoped faces to check 'edge.faces' both (first and second)
# belongs to this faces or not. If any of this faces does not involve this scoped faces, then do not delete.
# @param ignore_materials [Boolean] whether ignore materials or not.
# Returns true if the given edge separating two coplanar faces.
# Return false otherwise.
# rubocop:disable Metrics/AbcSize
def self.remove_edge_have_coplanar_faces(edge, faces, ignore_materials)
return false unless edge.valid? && edge.is_a?(Sketchup::Edge)
return false unless edge.faces.size == 2
# Check scoped faces have this edges
if edge.faces.size == 2
is_first = faces.include?(edge.faces[0])
is_second = faces.include?(edge.faces[1])
return false unless is_first && is_second
end
face_1, face_2 = edge.faces
return false if face_duplicate?(face_1, face_2)
# Check for troublesome faces which might lead to missing geometry if merged.
return false unless edge_safe_to_merge?(edge)
# Check materials match.
unless ignore_materials
return false unless (face_1.material == face_2.material) && (face_1.back_material == face_2.back_material)
# Verify UV mapping match.
return false if !face_1.material.nil? && !continuous_uv?(face_1, face_2, edge) && face_1.material.texture.nil?
end
# Check faces are coplanar or not.
return false unless faces_coplanar?(face_1, face_2)
edge.erase!
true
end
# rubocop:enable Metrics/AbcSize
# Determines if two faces are overlapped.
def self.face_duplicate?(face_1, face_2, overlapping: false)
return false if face_1 == face_2
v_1 = face_1.outer_loop.vertices
v_2 = face_2.outer_loop.vertices
return true if (v_1 - v_2).empty? && (v_2 - v_1).empty?
if overlapping && (v_2 - v_1).empty?
edges = (face_2.outer_loop.edges - face_1.outer_loop.edges)
unless edges.empty?
point = edges[0].start.position.offset(edges[0].line[1], 0.01)
return true if face_1.classify_point(point) <= 4
end
end
false
end
# Checks the given edge for potential problems if the connected faces would
# be merged.
def self.edge_safe_to_merge?(edge)
edge.faces.all? { |face| face_safe_to_merge?(face) }
end
# Returns true if the two faces connected by the edge has continuous UV mapping.
# UV's are normalized to 0.0..1.0 before comparison.
def self.continuous_uv?(face_1, face_2, edge)
tw = Sketchup.create_texture_writer
uvh_1 = face_1.get_UVHelper(true, true, tw)
uvh_2 = face_2.get_UVHelper(true, true, tw)
p_1 = edge.start.position
p_2 = edge.end.position
uv_equal?(uvh_1.get_front_UVQ(p_1), uvh_2.get_front_UVQ(p_1)) &&
uv_equal?(uvh_1.get_front_UVQ(p_2), uvh_2.get_front_UVQ(p_2)) &&
uv_equal?(uvh_1.get_back_UVQ(p_1), uvh_2.get_back_UVQ(p_1)) &&
uv_equal?(uvh_1.get_back_UVQ(p_2), uvh_2.get_back_UVQ(p_2))
end
# Normalize UV's to 0.0..1.0 and compare them.
def self.uv_equal?(uvq_1, uvq_2)
uv_1 = uvq_1.to_a.map { |n| n % 1 }
uv_2 = uvq_2.to_a.map { |n| n % 1 }
uv_1 == uv_2
end
# Validates that the given face can be merged with other faces without causing
# problems.
def self.face_safe_to_merge?(face)
stack = face.outer_loop.edges
edge = stack.shift
direction = edge.line[1]
until stack.empty?
edge = stack.shift
return true unless edge.line[1].parallel?(direction)
end
false
end
# Determines if two faces are coplanar.
def self.faces_coplanar?(face_1, face_2)
vertices = face_1.vertices + face_2.vertices
plane = Geom.fit_plane_to_points(vertices)
vertices.all? { |v| v.position.on_plane?(plane) }
end
end
end
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module SpeckleConnector
module Converters
# Helper class to convert geometries between server and Sketchup.
class Converter
# @return [Sketchup::Model] active sketchup model.
attr_reader :sketchup_model
attr_accessor :units, :definitions, :registry, :entity_observer
def initialize(sketchup_model)
@sketchup_model = sketchup_model
su_unit = @sketchup_model.options['UnitsOptions']['LengthUnit']
@units = Converters::SKETCHUP_UNITS[su_unit]
@definitions = {}
# @registry = Sketchup.active_model.attribute_dictionary("speckle_id_registry", true)
# @entity_observer = SpeckleEntityObserver.new
end
end
end
end
@@ -0,0 +1,266 @@
# frozen_string_literal: true
require_relative 'converter'
require_relative '../speckle_objects/other/transform'
require_relative '../speckle_objects/other/render_material'
require_relative '../speckle_objects/other/block_definition'
require_relative '../speckle_objects/other/block_instance'
require_relative '../speckle_objects/geometry/point'
require_relative '../speckle_objects/geometry/line'
require_relative '../speckle_objects/geometry/mesh'
module SpeckleConnector
module Converters
# Converts sketchup entities to speckle objects.
class ToNative < Converter
# Module aliases
GEOMETRY = SpeckleObjects::Geometry
OTHER = SpeckleObjects::Other
# Class aliases
POINT = GEOMETRY::Point
LINE = GEOMETRY::Line
MESH = GEOMETRY::Mesh
BLOCK_DEFINITION = OTHER::BlockDefinition
BLOCK_INSTANCE = OTHER::BlockInstance
BASE_OBJECT_PROPS = %w[applicationId id speckle_type totalChildrenCount].freeze
CONVERTABLE_SPECKLE_TYPES = %w[
Objects.Geometry.Line
Objects.Geometry.Polyline
Objects.Geometry.Mesh
Objects.Geometry.Brep
Objects.Other.BlockInstance
Objects.Other.BlockDefinition
Objects.Other.RenderMaterial
].freeze
def can_convert_to_native(obj)
return false unless obj.is_a?(Hash) && obj.key?('speckle_type')
CONVERTABLE_SPECKLE_TYPES.include?(obj['speckle_type'])
end
def ignored_speckle_type?(obj)
['Objects.BuiltElements.Revit.Parameter'].include?(obj['speckle_type'])
end
# @param obj [Object] speckle commit object.
def receive_commit_object(obj, model_preferences)
# First create layers on the sketchup before starting traversing
filtered_layer_containers = obj.keys.filter_map { |key| key if key.start_with?('@') && key != '@Named Views' }
create_layers(filtered_layer_containers, sketchup_model.layers)
create_views(obj.filter_map { |key, value| value if key == '@Named Views' }, sketchup_model)
# Define default commit layer which is the fallback
default_commit_layer = sketchup_model.layers.layers.find { |layer| layer.display_name == '@Untagged' }
traverse_commit_object(obj, sketchup_model.layers, default_commit_layer, model_preferences)
end
# Create actual Sketchup layers from layer_paths that taken from Speckle base object.
# @param layer_paths [Array<String>] layer paths to decompose it to folders and it's layers.
# @param folder [Sketchup::Layers, Sketchup::LayerFolder] folder to create folders and layers under it.
def create_layers(layer_paths, folder)
# Strip leading '@'
layers_with_folders = layer_paths.map { |layer| layer[1..-1] }
# Split layer_paths according to having parent folder or not.
layers_with_head_folder, headless_layers = layers_with_folders.partition { |layer| layer.include?('::') }
# Create array of array that split with '::'
folder_layer_arrays = layers_with_head_folder.collect { |folder_layer| folder_layer.split('::') }
# Add headless layers into `Sketchup.active_model.layers`
create_headless_layers(headless_layers, folder)
# Create layers that have parent folder(s)- this method is recursive until all tree is created.
create_folder_layers(folder_layer_arrays, folder)
end
# @param views [Array] views.
# @param sketchup_model [Sketchup::Model] active sketchup model.
def create_views(views, sketchup_model)
return if views.empty?
views.first.each do |view|
origin = view['origin']
target = view['target']
origin = SpeckleObjects::Geometry::Point.to_native(origin['x'], origin['y'], origin['z'], origin['units'])
target = SpeckleObjects::Geometry::Point.to_native(target['x'], target['y'], target['z'], target['units'])
# Set camera position before creating scene on it.
my_camera = Sketchup::Camera.new(origin, target, [0, 0, 1], !view['isOrthogonal'], view['lens'])
sketchup_model.active_view.camera = my_camera
sketchup_model.pages.add(view['name'])
end
end
# @param headless_layers [Array<String>] headless layer names.
# @param folder [Sketchup::Layers, Sketchup::LayerFolder] layer folder to create commit layers under it.
def create_headless_layers(headless_layers, folder)
headless_layers.each do |layer_name|
# Add layer first to the layers object of sketchup model.
layer = sketchup_model.layers.add(layer_name)
folder.add_layer(layer) unless folder.layers.any? { |l| l.display_name == layer_name }
end
end
# Create layers with it's parent folders.
# @param folder [Sketchup::LayerFolder] layer folder to create commit layers under it.
def create_folder_layers(folder_layer_arrays, folder)
folder_layer_arrays.each do |folder_layer_array|
create_folder_layer(folder_layer_array, folder)
end
end
# Create layers that have parent folder(s)- this method is recursive (self-caller) until all tree is created.
def create_folder_layer(folder_array, folder)
if folder_array.length > 1
# add folder if it is not exist.
folder.add_folder(folder_array[0]) unless folder.folders.any? { |f| f.display_name == folder_array[0] }
new_folder = folder.folders.find { |f| f.display_name == folder_array[0] }
create_folder_layer(folder_array[1..-1], new_folder)
else
# Add layer first to the layers object of sketchup model.
layer = sketchup_model.layers.add(folder_array[0])
folder.add_layer(layer) unless folder.layers.any? { |l| l.display_name == layer }
end
end
# Traversal method to create Sketchup objects from upcoming base object.
# @param obj [Hash, Array] object might be source base object or it's sub objects, because this method is a
# self-caller method means that call itself according to conditions inside of it.
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def traverse_commit_object(obj, commit_folder, layer, model_preferences)
if can_convert_to_native(obj)
convert_to_native(obj, layer, model_preferences)
elsif obj.is_a?(Hash) && obj.key?('speckle_type')
return if ignored_speckle_type?(obj)
if obj['displayValue'].nil?
# puts(">>> Found #{obj['speckle_type']}: #{obj['id']}. Continuing traversal.")
props = obj.keys.filter_map { |key| key unless key.start_with?('_') }
props.each do |prop|
layer_path = prop if prop.start_with?('@') && obj[prop].is_a?(Array)
layer = find_layer(layer_path, commit_folder, layer)
traverse_commit_object(obj[prop], commit_folder, layer, model_preferences)
end
else
# puts(">>> Found #{obj['speckle_type']}: #{obj['id']} with displayValue.")
convert_to_native(obj, layer, model_preferences)
end
elsif obj.is_a?(Hash)
obj.each_value { |value| traverse_commit_object(value, commit_folder, layer, model_preferences) }
elsif obj.is_a?(Array)
obj.each { |value| traverse_commit_object(value, commit_folder, layer, model_preferences) }
end
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# Find layer of the Speckle object by checking iteratively into folder.
# @param layer_path [String] complete layer_path to retrieve
# @param folder [Sketchup::LayerFolder, Sketchup::Layers] entry folder to search layer
# @param fallback_layer [Sketchup::Layer] fallback layer to assign object later if any error occur.
# @return [Sketchup::Layer] layer according to path
# @example
# "@folder_1::folder_2::layer_1"
# # it will return the layer object which has display name as `layer_1`.
def find_layer(layer_path, folder, fallback_layer)
begin
# Split folders and it's tail layer (last one is layer, others are folders.)
layer_path_array = layer_path[1..-1].split('::')
# Get sub folders as array, might be empty if `layer_path_array` has only 1 entry
sub_folders = layer_path_array.length > 1 ? layer_path_array[0..-2] : []
# Get exact layer name from last entry
layer_name = layer_path_array.last
# Iterate sub folders to find new sub folder to switch it.
# It help to search in the tree by switching the target search folder.
# Finally we can reach the layer name.
sub_folders.each do |sub_folder|
# Try to find sub folder into source folder passes by argument
s_f = folder.folders.find { |f| f.display_name == sub_folder }
# Switch source folder if any exist
folder = s_f unless s_f.nil?
end
# Find finally the layer into related folder
folder.layers.find { |l| l.display_name == layer_name }
rescue StandardError
return fallback_layer
end
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/MethodLength
def convert_to_native(obj, layer, model_preferences, entities = sketchup_model.entities)
convert = method(:convert_to_native)
unless obj['displayValue'].nil?
return display_value_to_native_component(obj, layer, entities, model_preferences, &convert)
end
case obj['speckle_type']
when 'Objects.Geometry.Line', 'Objects.Geometry.Polyline' then LINE.to_native(obj, layer, entities)
when 'Objects.Other.BlockInstance' then BLOCK_INSTANCE.to_native(sketchup_model, obj, layer, entities,
model_preferences, &convert)
when 'Objects.Other.BlockDefinition' then BLOCK_DEFINITION.to_native(sketchup_model, obj, layer,
obj['name'],
obj['always_face_camera'],
model_preferences,
obj['sketchup_attributes'],
obj['applicationId'],
&convert)
when 'Objects.Geometry.Mesh' then MESH.to_native(sketchup_model, obj, layer, entities, model_preferences)
when 'Objects.Geometry.Brep' then MESH.to_native(sketchup_model, obj['displayValue'], layer, entities,
model_preferences)
end
rescue StandardError => e
puts("Failed to convert #{obj['speckle_type']} (id: #{obj['id']})")
puts(e)
nil
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/MethodLength
# Creates a component definition and instance from a speckle object with a display value
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/MethodLength
def display_value_to_native_component(obj, layer, entities, model_preferences, &convert)
obj_id = obj['applicationId'].to_s.empty? ? obj['id'] : obj['applicationId']
block_definition = obj['@blockDefinition'] || obj['blockDefinition']
definition = BLOCK_DEFINITION.to_native(
sketchup_model,
obj['displayValue'],
layer,
"def::#{obj_id}",
if block_definition.nil?
false
else
block_definition['always_face_camera'].nil? ? false : block_definition['always_face_camera']
end,
model_preferences,
if block_definition.nil?
nil
else
block_definition['sketchup_attributes'].nil? ? nil : block_definition['sketchup_attributes']
end,
obj_id,
&convert
)
find_and_erase_existing_instance(definition, obj_id)
t_arr = obj['transform']
transform = t_arr.nil? ? Geom::Transformation.new : OTHER::Transform.to_native(t_arr, units)
instance = entities.add_instance(definition, transform)
instance.name = obj_id
instance
end
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/MethodLength
# Takes a component definition and finds and erases the first instance with the matching name
# (and optionally the applicationId)
def find_and_erase_existing_instance(definition, name, app_id = '')
definition.instances.find { |ins| ins.name == name || ins.guid == app_id }&.erase!
end
end
end
end
@@ -0,0 +1,179 @@
# frozen_string_literal: true
require_relative 'converter'
require_relative 'base_object_serializer'
require_relative '../speckle_objects/base'
require_relative '../speckle_objects/geometry/line'
require_relative '../speckle_objects/geometry/length'
require_relative '../speckle_objects/geometry/mesh'
require_relative '../speckle_objects/other/block_instance'
require_relative '../speckle_objects/other/block_definition'
require_relative '../speckle_objects/built_elements/view3d'
module SpeckleConnector
module Converters
# Converts sketchup entities to speckle objects.
class ToSpeckle < Converter
# @return [Hash{Symbol=>Array}] layers to hold it's objects under the base object.
attr_reader :layers
def initialize(sketchup_model)
super(sketchup_model)
@layers = add_all_layers
end
# Convert selected objects by putting them into related array that grouped by layer.
# @return [Hash{Symbol=>Array}] layers -which only have objects- to hold it's objects under the base object.
def convert_selection_to_base(preferences)
sketchup_model.selection.each do |entity|
converted_object = convert(entity, preferences)
layer_name = entity_layer_path(entity)
layers[layer_name].push(converted_object)
end
# send only layers that have any object
base_object_properties = layers.reject { |_layer_name, objects| objects.empty? }
add_views(base_object_properties) if sketchup_model.pages.any?
SpeckleObjects::Base.with_detached_layers(base_object_properties)
end
# Add views from pages.
# @param base_object_properties [Hash] dynamically attached base object properties.
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def add_views(base_object_properties)
views = []
sketchup_model.pages.each do |page|
cam = page.camera
origin = SpeckleObjects::Geometry::Point.new(
SpeckleObjects::Geometry.length_to_speckle(cam.eye[0], @units),
SpeckleObjects::Geometry.length_to_speckle(cam.eye[1], @units),
SpeckleObjects::Geometry.length_to_speckle(cam.eye[2], @units),
@units
)
target = SpeckleObjects::Geometry::Point.new(
SpeckleObjects::Geometry.length_to_speckle(cam.target[0], @units),
SpeckleObjects::Geometry.length_to_speckle(cam.target[1], @units),
SpeckleObjects::Geometry.length_to_speckle(cam.target[2], @units),
@units
)
direction = SpeckleObjects::Geometry::Vector.new(
SpeckleObjects::Geometry.length_to_speckle(cam.direction[0], @units),
SpeckleObjects::Geometry.length_to_speckle(cam.direction[1], @units),
SpeckleObjects::Geometry.length_to_speckle(cam.direction[2], @units),
@units
)
view = SpeckleObjects::BuiltElements::View3d.new(
page.name,
origin, target, direction, SpeckleObjects::Geometry::Vector.new(0, 0, 1, @units),
cam.perspective?, cam.fov, @units, page.name
)
views.append(view)
end
base_object_properties['@Named Views'] = views
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# Serialized and traversed information to send batches.
# @param base [SpeckleObjects::Base] base object to serialize.
# @return [String, Integer, Array<Object>] base id, total_children_count of base and batches
def send_info(base)
serializer = SpeckleConnector::Converters::BaseObjectSerializer.new
# t = Time.now.to_f
id, _traversed = serializer.serialize(base)
# puts "Generating traversed object elapsed #{Time.now.to_f - t} s"
base_total_children_count = serializer.total_children_count(id)
return id, base_total_children_count, serializer.batch_objects
end
# @param entity [Sketchup::Entity] sketchup entity to convert Speckle.
def convert(entity, preferences)
convert = method(:convert)
if entity.is_a?(Sketchup::Edge)
return SpeckleObjects::Geometry::Line.from_edge(entity, @units, preferences[:model]).to_h
end
if entity.is_a?(Sketchup::Face)
return SpeckleObjects::Geometry::Mesh.from_face(entity, @units, preferences[:model])
end
if entity.is_a?(Sketchup::Group)
return SpeckleObjects::Other::BlockInstance.from_group(entity, @units, @definitions, preferences, &convert)
end
if entity.is_a?(Sketchup::ComponentInstance)
return SpeckleObjects::Other::BlockInstance.from_component_instance(entity, @units, @definitions,
preferences, &convert)
end
if entity.is_a?(Sketchup::ComponentDefinition)
return SpeckleObjects::Other::BlockDefinition.from_definition(entity, @units, @definitions, preferences,
&convert)
end
nil
end
# Create layers -> {Hash{Symbol=>Array}} from sketchup model with empty array as hash entry values.
# This method add first headless layers (not belong to any folder),
# then goes through each folder, their sub-folders and their layers.
# @return [Hash{Symbol=>Array}] layers from sketchup model with empty array as hash entry values.
def add_all_layers
# add headless layers
layer_objects = add_layers(sketchup_model.layers.layers)
# add layers from folders
add_layers_from_folders(sketchup_model.layers.folders, layer_objects)
layer_objects
end
# @param layers [Array<Sketchup::Layer>] layers in sketchup model
# @return [Hash{Symbol=>Array}] layers with empty array value.
def add_layers(layers, layer_objects = {}, parent_name = '')
layers.each do |layer|
layer_name = parent_name.empty? ? "@#{layer.display_name}" : "#{parent_name}::#{layer.display_name}"
layer_objects[layer_name] = []
end
layer_objects
end
# @param folders [Array<Sketchup::LayerFolder>] layer folders in sketchup model.
# @param layer_objects [Hash{Symbol=>Array}] layer objects to fill in.
# @param parent_name [String] parent folder name to structure layer path before send to Speckle.
# ex: "@#{parent_name}::#{layer_name}"
def add_layers_from_folders(folders, layer_objects, parent_name = '')
folders.each do |folder|
folder_name = parent_name.empty? ? "@#{folder.display_name}" : "#{parent_name}::#{folder.display_name}"
add_layers(folder.layers, layer_objects, folder_name)
add_layers_from_folders(folder.folders, layer_objects, folder_name) unless folder.folders.empty?
end
end
# Find layer path of given Sketchup entity.
# @param entity [Sketchup::Entity] entity to find root layer.
# @return [String] layer path of Sketchup entity.
def entity_layer_path(entity)
layer_name = entity.layer.display_name
if entity.layer.folder.nil?
"@#{layer_name}"
else
folders = folder_name(entity.layer.folder)
path = ''
folders.reverse.each do |folder|
path += "#{folder}::"
end
"@#{path}#{layer_name}"
end
end
# Nested method to retrieve sub-folders until nothing found.
# @return [Array<String>] folder names as list from bottom to top. Might need to be reversed if you want to see
# from top to bottom.
def folder_name(folder, folders = [])
if folder.folder.nil?
folders.push(folder.display_name)
else
folder_name(folder.folder, folders.push(folder.display_name))
end
end
end
end
end
@@ -0,0 +1,9 @@
# frozen_string_literal: true
module SpeckleConnector
module Converters
SKETCHUP_UNITS = { 0 => 'in', 1 => 'ft', 2 => 'mm', 3 => 'cm', 4 => 'm', 5 => 'yd' }.freeze
SKETCHUP_UNIT_STRINGS = { 'm' => 'm', 'mm' => 'mm', 'ft' => 'feet', 'in' => 'inch', 'yd' => 'yard',
'cm' => 'cm' }.freeze
end
end
@@ -0,0 +1,23 @@
Licensing
=========
Copyright (c) 2009-2014 Simon Harris
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,35 @@
# frozen_string_literal: true
require_relative 'hash'
require_relative 'set'
require_relative 'vector'
# Add json conversion methods
require_relative 'json'
module SpeckleConnector
module Immutable
class Hash
# Return a new {Set} containing the keys from this `Hash`.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3, "D" => 2].keys
# # => Immutable::Set["D", "C", "B", "A"]
#
# @return [Set]
def keys
Set.alloc(@trie)
end
# Return a new {Vector} populated with the values from this `Hash`.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3, "D" => 2].values
# # => Immutable::Vector[2, 3, 2, 1]
#
# @return [Vector]
def values
Vector.new(each_value.to_a.freeze)
end
end
end
end
@@ -0,0 +1,293 @@
# frozen_string_literal: true
require_relative 'list'
module SpeckleConnector
module Immutable
# A `Deque` (or double-ended queue) is an ordered, sequential collection of
# objects, which allows elements to be retrieved, added and removed at the
# front and end of the sequence in constant time. This makes `Deque` perfect
# for use as an immutable queue or stack.
#
# A `Deque` differs from a {Vector} in that vectors allow indexed access to
# any element in the collection. `Deque`s only allow access to the first and
# last element. But adding and removing from the ends of a `Deque` is faster
# than adding and removing from the ends of a {Vector}.
#
# To create a new `Deque`:
#
# Immutable::Deque.new([:first, :second, :third])
# Immutable::Deque[1, 2, 3, 4, 5]
#
# Or you can start with an empty deque and build it up:
#
# Immutable::Deque.empty.push('b').push('c').unshift('a')
#
# Like all `immutable-ruby` collections, `Deque` is immutable. The four basic
# operations that "modify" deques ({#push}, {#pop}, {#shift}, and
# {#unshift}) all return a new collection and leave the existing one
# unchanged.
#
# @example
# deque = Immutable::Deque.empty # => Immutable::Deque[]
# deque = deque.push('a').push('b').push('c') # => Immutable::Deque['a', 'b', 'c']
# deque.first # => 'a'
# deque.last # => 'c'
# deque = deque.shift # => Immutable::Deque['b', 'c']
#
# @see http://en.wikipedia.org/wiki/Deque "Deque" on Wikipedia
#
class Deque
class << self
# Create a new `Deque` populated with the given items.
# @return [Deque]
def [](*items)
items.empty? ? empty : new(items)
end
# Return an empty `Deque`. If used on a subclass, returns an empty instance
# of that class.
#
# @return [Deque]
def empty
@empty ||= new
end
# "Raw" allocation of a new `Deque`. Used internally to create a new
# instance quickly after consing onto the front/rear lists or taking their
# tails.
#
# @return [Deque]
# @private
def alloc(front, rear)
result = allocate
result.instance_variable_set(:@front, front)
result.instance_variable_set(:@rear, rear)
result.freeze
end
end
def initialize(items=[])
@front = List.from_enum(items)
@rear = EmptyList
freeze
end
# Return `true` if this `Deque` contains no items.
# @return [Boolean]
def empty?
@front.empty? && @rear.empty?
end
# Return the number of items in this `Deque`.
#
# @example
# Immutable::Deque["A", "B", "C"].size # => 3
#
# @return [Integer]
def size
@front.size + @rear.size
end
alias length size
# Return the first item in the `Deque`. If the deque is empty, return `nil`.
#
# @example
# Immutable::Deque["A", "B", "C"].first # => "A"
#
# @return [Object]
def first
return @front.head unless @front.empty?
@rear.last # memoize?
end
# Return the last item in the `Deque`. If the deque is empty, return `nil`.
#
# @example
# Immutable::Deque["A", "B", "C"].last # => "C"
#
# @return [Object]
def last
return @rear.head unless @rear.empty?
@front.last # memoize?
end
# Return a new `Deque` with elements rotated by `n` positions.
# A positive rotation moves elements to the right, negative to the left, and 0 is a no-op.
#
# @example
# Immutable::Deque["A", "B", "C"].rotate(1)
# # => Immutable::Deque["C", "A", "B"]
# Immutable::Deque["A", "B", "C"].rotate(-1)
# # => Immutable::Deque["B", "C", "A"]
#
# @param n [Integer] number of positions to move elements by
# @return [Deque]
def rotate(n)
return self.class.empty if empty?
n %= size
return self if n == 0
a, b = @front, @rear
if b.size >= n
n.times { a = a.cons(b.head); b = b.tail }
else
(size - n).times { b = b.cons(a.head); a = a.tail }
end
self.class.alloc(a, b)
end
# Return a new `Deque` with `item` added at the end.
#
# @example
# Immutable::Deque["A", "B", "C"].push("Z")
# # => Immutable::Deque["A", "B", "C", "Z"]
#
# @param item [Object] The item to add
# @return [Deque]
def push(item)
self.class.alloc(@front, @rear.cons(item))
end
alias enqueue push
# Return a new `Deque` with the last item removed.
#
# @example
# Immutable::Deque["A", "B", "C"].pop
# # => Immutable::Deque["A", "B"]
#
# @return [Deque]
def pop
front, rear = @front, @rear
if rear.empty?
return self.class.empty if front.empty?
front, rear = EmptyList, front.reverse
end
self.class.alloc(front, rear.tail)
end
# Return a new `Deque` with `item` added at the front.
#
# @example
# Immutable::Deque["A", "B", "C"].unshift("Z")
# # => Immutable::Deque["Z", "A", "B", "C"]
#
# @param item [Object] The item to add
# @return [Deque]
def unshift(item)
self.class.alloc(@front.cons(item), @rear)
end
# Return a new `Deque` with the first item removed.
#
# @example
# Immutable::Deque["A", "B", "C"].shift
# # => Immutable::Deque["B", "C"]
#
# @return [Deque]
def shift
front, rear = @front, @rear
if front.empty?
return self.class.empty if rear.empty?
front, rear = rear.reverse, EmptyList
end
self.class.alloc(front.tail, rear)
end
alias dequeue shift
# Return an empty `Deque` instance, of the same class as this one. Useful if you
# have multiple subclasses of `Deque` and want to treat them polymorphically.
#
# @return [Deque]
def clear
self.class.empty
end
# Return a new `Deque` with the same items, but in reverse order.
#
# @return [Deque]
def reverse
self.class.alloc(@rear, @front)
end
# Return true if `other` has the same type and contents as this `Deque`.
#
# @param other [Object] The collection to compare with
# @return [Boolean]
def eql?(other)
return true if other.equal?(self)
instance_of?(other.class) && to_ary.eql?(other.to_ary)
end
alias == eql?
# Return an `Array` with the same elements, in the same order.
# @return [Array]
def to_a
@front.to_a.concat(@rear.to_a.tap(&:reverse!))
end
alias entries to_a
alias to_ary to_a
# Return a {List} with the same elements, in the same order.
# @return [Immutable::List]
def to_list
@front.append(@rear.reverse)
end
# Return the contents of this `Deque` as a programmer-readable `String`. If all the
# items in the deque are serializable as Ruby literal strings, the returned string can
# be passed to `eval` to reconstitute an equivalent `Deque`.
#
# @return [String]
def inspect
result = "#{self.class}["
i = 0
@front.each { |obj| result << ', ' if i > 0; result << obj.inspect; i += 1 }
@rear.to_a.tap(&:reverse!).each { |obj| result << ', ' if i > 0; result << obj.inspect; i += 1 }
result << ']'
end
# Return `self`. Since this is an immutable object duplicates are
# equivalent.
# @return [Deque]
def dup
self
end
alias clone dup
# @private
def pretty_print(pp)
pp.group(1, "#{self.class}[", ']') do
pp.breakable ''
pp.seplist(to_a) { |obj| obj.pretty_print(pp) }
end
end
# @return [::Array]
# @private
def marshal_dump
to_a
end
# @private
def marshal_load(array)
initialize(array)
end
end
# The canonical empty `Deque`. Returned by `Deque[]` when
# invoked with no arguments; also returned by `Deque.empty`. Prefer using this
# one rather than creating many empty deques using `Deque.new`.
#
# @private
EmptyDeque = SpeckleConnector::Immutable::Deque.empty
end
end
@@ -0,0 +1,163 @@
module SpeckleConnector
module Immutable
# Helper module for immutable-ruby's sequential collections
#
# Classes including `Immutable::Enumerable` must implement:
#
# - `#each` (just like `::Enumerable`).
# - `#select`, which takes a block, and returns an instance of the same class
# with only the items for which the block returns a true value
module Enumerable
include ::Enumerable
# Return a new collection with all the elements for which the block returns false.
def reject
return enum_for(:reject) if not block_given?
select { |item| !yield(item) }
end
alias delete_if reject
# Return a new collection with all `nil` elements removed.
def compact
select { |item| !item.nil? }
end
# Search the collection for elements which are `#===` to `item`. Yield them to
# the optional code block if provided, and return them as a new collection.
def grep(pattern, &block)
result = select { |item| pattern === item }
result = result.map(&block) if block_given?
result
end
# Search the collection for elements which are not `#===` to `item`. Yield
# them to the optional code block if provided, and return them as a new
# collection.
def grep_v(pattern, &block)
result = select { |item| !(pattern === item) }
result = result.map(&block) if block_given?
result
end
# Yield all integers from 0 up to, but not including, the number of items in
# this collection. For collections which provide indexed access, these are all
# the valid, non-negative indices into the collection.
def each_index(&block)
return enum_for(:each_index) unless block_given?
0.upto(size-1, &block)
self
end
# Multiply all the items (presumably numeric) in this collection together.
def product
reduce(1, &:*)
end
# Add up all the items (presumably numeric) in this collection.
def sum
reduce(0, &:+)
end
# Return 2 collections, the first containing all the elements for which the block
# evaluates to true, the second containing the rest.
def partition
return enum_for(:partition) if not block_given?
a,b = super
[self.class.new(a), self.class.new(b)].freeze
end
# Groups the collection into sub-collections by the result of yielding them to
# the block. Returns a {Hash} where the keys are return values from the block,
# and the values are sub-collections. All the sub-collections are built up from
# `empty_group`, which should respond to `#add` by returning a new collection
# with an added element.
def group_by_with(empty_group, &block)
block ||= lambda { |item| item }
reduce(SpeckleConnector::Immutable::EmptyHash) do |hash, item|
key = block.call(item)
group = hash.get(key) || empty_group
hash.put(key, group.add(item))
end
end
protected :group_by_with
# Groups the collection into sub-collections by the result of yielding them to
# the block. Returns a {Hash} where the keys are return values from the block,
# and the values are sub-collections (of the same type as this one).
def group_by(&block)
group_by_with(self.class.empty, &block)
end
# Compare with `other`, and return 0, 1, or -1 if it is (respectively) equal to,
# greater than, or less than this collection.
def <=>(other)
return 0 if equal?(other)
enum1, enum2 = to_enum, other.to_enum
loop do
item1 = enum1.next
item2 = enum2.next
comp = (item1 <=> item2)
return comp if comp != 0
end
size1, size2 = size, other.size
return 0 if size1 == size2
size1 > size2 ? 1 : -1
end
# Return true if `other` contains the same elements, in the same order.
# @return [Boolean]
def ==(other)
eql?(other) || (other.respond_to?(:to_ary) && to_ary == other.to_ary)
end
# Convert all the elements into strings and join them together, separated by
# `separator`. By default, the `separator` is `$,`, the global default string
# separator, which is normally `nil`.
def join(separator = $,)
result = ''
if separator
each_with_index { |obj, i| result << separator if i > 0; result << obj.to_s }
else
each { |obj| result << obj.to_s }
end
result
end
# Convert this collection to a {Set}.
def to_set
SpeckleConnector::Immutable::Set.new(self)
end
# Convert this collection to a programmer-readable `String` representation.
def inspect
result = "#{self.class}["
each_with_index { |obj, i| result << ', ' if i > 0; result << obj.inspect }
result << ']'
end
# @private
def pretty_print(pp)
pp.group(1, "#{self.class}[", ']') do
pp.breakable ''
pp.seplist(self) { |obj| obj.pretty_print(pp) }
end
end
alias to_ary to_a
alias index find_index
## Compatibility fixes
if RUBY_ENGINE == 'rbx'
# Rubinius implements Enumerable#sort_by using Enumerable#map
# Because we do our own, custom implementations of #map, that doesn't work well
# @private
def sort_by(&block)
result = to_a
result.frozen? ? result.sort_by(&block) : result.sort_by!(&block)
end
end
end
end
end
@@ -0,0 +1,921 @@
# frozen_string_literal: true
require_relative 'undefined'
require_relative 'enumerable'
require_relative 'trie'
module SpeckleConnector
module Immutable
# An `Immutable::Hash` maps a set of unique keys to corresponding values, much
# like a dictionary maps from words to definitions. Given a key, it can store
# and retrieve an associated value in constant time. If an existing key is
# stored again, the new value will replace the old. It behaves much like
# Ruby's built-in Hash, which we will call RubyHash for clarity. Like
# RubyHash, two keys that are `#eql?` to each other and have the same
# `#hash` are considered identical in an `Immutable::Hash`.
#
# An `Immutable::Hash` can be created in a couple of ways:
#
# Immutable::Hash.new(font_size: 10, font_family: 'Arial')
# Immutable::Hash[first_name: 'John', last_name: 'Smith']
#
# Any `Enumerable` object which yields two-element `[key, value]` arrays
# can be used to initialize an `Immutable::Hash`:
#
# Immutable::Hash.new([[:first_name, 'John'], [:last_name, 'Smith']])
#
# Key/value pairs can be added using {#put}. A new hash is returned and the
# existing one is left unchanged:
#
# hash = Immutable::Hash[a: 100, b: 200]
# hash.put(:c, 500) # => Immutable::Hash[:a => 100, :b => 200, :c => 500]
# hash # => Immutable::Hash[:a => 100, :b => 200]
#
# {#put} can also take a block, which is used to calculate the value to be
# stored.
#
# hash.put(:a) { |current| current + 200 } # => Immutable::Hash[:a => 300, :b => 200]
#
# Since it is immutable, all methods which you might expect to "modify" a
# `Immutable::Hash` actually return a new hash and leave the existing one
# unchanged. This means that the `hash[key] = value` syntax from RubyHash
# *cannot* be used with `Immutable::Hash`.
#
# Nested data structures can easily be updated using {#update_in}:
#
# hash = Immutable::Hash["a" => Immutable::Vector[Immutable::Hash["c" => 42]]]
# hash.update_in("a", 0, "c") { |value| value + 5 }
# # => Immutable::Hash["a" => Immutable::Hash["b" => Immutable::Hash["c" => 47]]]
#
# While an `Immutable::Hash` can iterate over its keys or values, it does not
# guarantee any specific iteration order (unlike RubyHash). Methods like
# {#flatten} do not guarantee the order of returned key/value pairs.
#
# Like RubyHash, an `Immutable::Hash` can have a default block which is used
# when looking up a key that does not exist. Unlike RubyHash, the default
# block will only be passed the missing key, without the hash itself:
#
# hash = Immutable::Hash.new { |missing_key| missing_key * 10 }
# hash[5] # => 50
class Hash
include SpeckleConnector::Immutable::Enumerable
class << self
# Create a new `Hash` populated with the given key/value pairs.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2] # => Immutable::Hash["A" => 1, "B" => 2]
# Immutable::Hash[["A", 1], ["B", 2]] # => Immutable::Hash["A" => 1, "B" => 2]
#
# @param pairs [::Enumerable] initial content of hash. An empty hash is returned if not provided.
# @return [Hash]
def [](pairs = nil)
(pairs.nil? || pairs.empty?) ? empty : new(pairs)
end
# Return an empty `Hash`. If used on a subclass, returns an empty instance
# of that class.
#
# @return [Hash]
def empty
@empty ||= new
end
# "Raw" allocation of a new `Hash`. Used internally to create a new
# instance quickly after obtaining a modified {Trie}.
#
# @return [Hash]
# @private
def alloc(trie = EmptyTrie, block = nil)
obj = allocate
obj.instance_variable_set(:@trie, trie)
obj.instance_variable_set(:@default, block)
obj.freeze
end
end
# @param pairs [::Enumerable] initial content of hash. An empty hash is returned if not provided.
# @yield [key] Optional _default block_ to be stored and used to calculate the default value of a missing key. It will not be yielded during this method. It will not be preserved when marshalling.
# @yieldparam key Key that was not present in the hash.
def initialize(pairs = nil, &block)
@trie = pairs ? Trie[pairs] : EmptyTrie
@default = block
freeze
end
# Return the default block if there is one. Otherwise, return `nil`.
#
# @return [Proc]
def default_proc
@default
end
# Return the number of key/value pairs in this `Hash`.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].size # => 3
#
# @return [Integer]
def size
@trie.size
end
alias length size
# Return `true` if this `Hash` contains no key/value pairs.
#
# @return [Boolean]
def empty?
@trie.empty?
end
# Return `true` if the given key object is present in this `Hash`. More precisely,
# return `true` if a key with the same `#hash` code, and which is also `#eql?`
# to the given key object is present.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].key?("B") # => true
#
# @param key [Object] The key to check for
# @return [Boolean]
def key?(key)
@trie.key?(key)
end
alias has_key? key?
alias include? key?
alias member? key?
# Return `true` if this `Hash` has one or more keys which map to the provided value.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].value?(2) # => true
#
# @param value [Object] The value to check for
# @return [Boolean]
def value?(value)
each { |k,v| return true if value == v }
false
end
alias has_value? value?
# Retrieve the value corresponding to the provided key object. If not found, and
# this `Hash` has a default block, the default block is called to provide the
# value. Otherwise, return `nil`.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h["B"] # => 2
# h.get("B") # => 2
# h.get("Elephant") # => nil
#
# # Immutable Hash with a default proc:
# h = Immutable::Hash.new("A" => 1, "B" => 2, "C" => 3) { |key| key.size }
# h.get("B") # => 2
# h.get("Elephant") # => 8
#
# @param key [Object] The key to look up
# @return [Object]
def get(key)
entry = @trie.get(key)
if entry
entry[1]
elsif @default
@default.call(key)
end
end
alias [] get
# Retrieve the value corresponding to the given key object, or use the provided
# default value or block, or otherwise raise a `KeyError`.
#
# @overload fetch(key)
# Retrieve the value corresponding to the given key, or raise a `KeyError`
# if it is not found.
# @param key [Object] The key to look up
# @overload fetch(key) { |key| ... }
# Retrieve the value corresponding to the given key, or call the optional
# code block (with the missing key) and get its return value.
# @yield [key] The key which was not found
# @yieldreturn [Object] Object to return since the key was not found
# @param key [Object] The key to look up
# @overload fetch(key, default)
# Retrieve the value corresponding to the given key, or else return
# the provided `default` value.
# @param key [Object] The key to look up
# @param default [Object] Object to return if the key is not found
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.fetch("B") # => 2
# h.fetch("Elephant") # => KeyError: key not found: "Elephant"
#
# # with a default value:
# h.fetch("B", 99) # => 2
# h.fetch("Elephant", 99) # => 99
#
# # with a block:
# h.fetch("B") { |key| key.size } # => 2
# h.fetch("Elephant") { |key| key.size } # => 8
#
# @return [Object]
def fetch(key, default = Undefined)
entry = @trie.get(key)
if entry
entry[1]
elsif block_given?
yield(key)
elsif default != Undefined
default
else
raise KeyError, "key not found: #{key.inspect}"
end
end
# Return a new `Hash` with the existing key/value associations, plus an association
# between the provided key and value. If an equivalent key is already present, its
# associated value will be replaced with the provided one.
#
# If the `value` argument is missing, but an optional code block is provided,
# it will be passed the existing value (or `nil` if there is none) and what it
# returns will replace the existing value. This is useful for "transforming"
# the value associated with a certain key.
#
# Avoid mutating objects which are used as keys. `String`s are an exception:
# unfrozen `String`s which are used as keys are internally duplicated and
# frozen. This matches RubyHash's behaviour.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2]
# h.put("C", 3)
# # => Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.put("B") { |value| value * 10 }
# # => Immutable::Hash["A" => 1, "B" => 20]
#
# @param key [Object] The key to store
# @param value [Object] The value to associate it with
# @yield [value] The previously stored value, or `nil` if none.
# @yieldreturn [Object] The new value to store
# @return [Hash]
def put(key, value = yield(get(key)))
new_trie = @trie.put(key, value)
if new_trie.equal?(@trie)
self
else
self.class.alloc(new_trie, @default)
end
end
# @private
# @raise NoMethodError
def []=(*)
raise NoMethodError, "Immutable::Hash doesn't support `[]='; use `put' instead"
end
# Return a new `Hash` with a deeply nested value modified to the result of
# the given code block. When traversing the nested `Hash`es and `Vector`s,
# non-existing keys are created with empty `Hash` values.
#
# The code block receives the existing value of the deeply nested key (or
# `nil` if it doesn't exist). This is useful for "transforming" the value
# associated with a certain key.
#
# Note that the original `Hash` and sub-`Hash`es and sub-`Vector`s are left
# unmodified; new data structure copies are created along the path wherever
# needed.
#
# @example
# hash = Immutable::Hash["a" => Immutable::Hash["b" => Immutable::Hash["c" => 42]]]
# hash.update_in("a", "b", "c") { |value| value + 5 }
# # => Immutable::Hash["a" => Immutable::Hash["b" => Immutable::Hash["c" => 47]]]
#
# @param key_path [::Array<Object>] List of keys which form the path to the key to be modified
# @yield [value] The previously stored value
# @yieldreturn [Object] The new value to store
# @return [Hash]
def update_in(*key_path, &block)
if key_path.empty?
raise ArgumentError, 'must have at least one key in path'
end
key = key_path[0]
if key_path.size == 1
new_value = block.call(get(key))
else
value = fetch(key, EmptyHash)
new_value = value.update_in(*key_path[1..-1], &block)
end
put(key, new_value)
end
# An alias for {#put} to match RubyHash's API. Does not support {#put}'s
# block form.
#
# @see #put
# @param key [Object] The key to store
# @param value [Object] The value to associate it with
# @return [Hash]
def store(key, value)
put(key, value)
end
# Return a new `Hash` with `key` removed. If `key` is not present, return
# `self`.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].delete("B")
# # => Immutable::Hash["A" => 1, "C" => 3]
#
# @param key [Object] The key to remove
# @return [Hash]
def delete(key)
derive_new_hash(@trie.delete(key))
end
# Call the block once for each key/value pair in this `Hash`, passing the key/value
# pair as parameters. No specific iteration order is guaranteed, though the order will
# be stable for any particular `Hash`.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].each { |k, v| puts "k=#{k} v=#{v}" }
#
# k=A v=1
# k=C v=3
# k=B v=2
# # => Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
#
# @yield [key, value] Once for each key/value pair.
# @return [self]
def each(&block)
return to_enum if not block_given?
@trie.each(&block)
self
end
alias each_pair each
# Call the block once for each key/value pair in this `Hash`, passing the key/value
# pair as parameters. Iteration order will be the opposite of {#each}.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].reverse_each { |k, v| puts "k=#{k} v=#{v}" }
#
# k=B v=2
# k=C v=3
# k=A v=1
# # => Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
#
# @yield [key, value] Once for each key/value pair.
# @return [self]
def reverse_each(&block)
return enum_for(:reverse_each) if not block_given?
@trie.reverse_each(&block)
self
end
# Call the block once for each key/value pair in this `Hash`, passing the key as a
# parameter. Ordering guarantees are the same as {#each}.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].each_key { |k| puts "k=#{k}" }
#
# k=A
# k=C
# k=B
# # => Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
#
# @yield [key] Once for each key/value pair.
# @return [self]
def each_key
return enum_for(:each_key) if not block_given?
@trie.each { |k,v| yield k }
self
end
# Call the block once for each key/value pair in this `Hash`, passing the value as a
# parameter. Ordering guarantees are the same as {#each}.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].each_value { |v| puts "v=#{v}" }
#
# v=1
# v=3
# v=2
# # => Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
#
# @yield [value] Once for each key/value pair.
# @return [self]
def each_value
return enum_for(:each_value) if not block_given?
@trie.each { |k,v| yield v }
self
end
# Call the block once for each key/value pair in this `Hash`, passing the key/value
# pair as parameters. The block should return a `[key, value]` array each time.
# All the returned `[key, value]` arrays will be gathered into a new `Hash`.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.map { |k, v| ["new-#{k}", v * v] }
# # => Hash["new-C" => 9, "new-B" => 4, "new-A" => 1]
#
# @yield [key, value] Once for each key/value pair.
# @return [Hash]
def map
return enum_for(:map) unless block_given?
return self if empty?
self.class.new(super, &@default)
end
# Return a new `Hash` with all the key/value pairs for which the block returns true.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.select { |k, v| v >= 2 }
# # => Immutable::Hash["B" => 2, "C" => 3]
#
# @yield [key, value] Once for each key/value pair.
# @yieldreturn Truthy if this pair should be present in the new `Hash`.
# @return [Hash]
def select(&block)
return enum_for(:select) unless block_given?
derive_new_hash(@trie.select(&block))
end
alias find_all select
alias keep_if select
# Yield `[key, value]` pairs until one is found for which the block returns true.
# Return that `[key, value]` pair. If the block never returns true, return `nil`.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.find { |k, v| v.even? }
# # => ["B", 2]
#
# @return [Array]
# @yield [key, value] At most once for each key/value pair, until the block returns `true`.
# @yieldreturn Truthy to halt iteration and return the yielded key/value pair.
def find
return enum_for(:find) unless block_given?
each { |entry| return entry if yield entry }
nil
end
alias detect find
# Return a new `Hash` containing all the key/value pairs from this `Hash` and
# `other`. If no block is provided, the value for entries with colliding keys
# will be that from `other`. Otherwise, the value for each duplicate key is
# determined by calling the block.
#
# `other` can be an `Immutable::Hash`, a built-in Ruby `Hash`, or any `Enumerable`
# object which yields `[key, value]` pairs.
#
# @example
# h1 = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h2 = Immutable::Hash["C" => 70, "D" => 80]
# h1.merge(h2)
# # => Immutable::Hash["C" => 70, "A" => 1, "D" => 80, "B" => 2]
# h1.merge(h2) { |key, v1, v2| v1 + v2 }
# # => Immutable::Hash["C" => 73, "A" => 1, "D" => 80, "B" => 2]
#
# @param other [::Enumerable] The collection to merge with
# @yieldparam key [Object] The key which was present in both collections
# @yieldparam my_value [Object] The associated value from this `Hash`
# @yieldparam other_value [Object] The associated value from the other collection
# @yieldreturn [Object] The value to associate this key with in the new `Hash`
# @return [Hash]
def merge(other)
trie = if block_given?
other.reduce(@trie) do |trie, (key, value)|
if (entry = trie.get(key))
trie.put(key, yield(key, entry[1], value))
else
trie.put(key, value)
end
end
else
@trie.bulk_put(other)
end
derive_new_hash(trie)
end
# Retrieve the value corresponding to the given key object, or use the provided
# default value or block, or otherwise raise a `KeyError`.
#
# @overload fetch(key)
# Retrieve the value corresponding to the given key, or raise a `KeyError`
# if it is not found.
# @param key [Object] The key to look up
# @overload fetch(key) { |key| ... }
# Return a sorted {Vector} which contains all the `[key, value]` pairs in
# this `Hash` as two-element `Array`s.
#
# @overload sort
# Uses `#<=>` to determine sorted order.
# @overload sort { |(k1, v1), (k2, v2)| ... }
# Uses the block as a comparator to determine sorted order.
#
# @example
# h = Immutable::Hash["Dog" => 1, "Elephant" => 2, "Lion" => 3]
# h.sort { |(k1, v1), (k2, v2)| k1.size <=> k2.size }
# # => Immutable::Vector[["Dog", 1], ["Lion", 3], ["Elephant", 2]]
# @yield [(k1, v1), (k2, v2)] Any number of times with different pairs of key/value associations.
# @yieldreturn [Integer] Negative if the first pair should be sorted
# lower, positive if the latter pair, or 0 if equal.
#
# @see ::Enumerable#sort
#
# @return [Vector]
def sort
Vector.new(super)
end
# Return a {Vector} which contains all the `[key, value]` pairs in this `Hash`
# as two-element Arrays. The order which the pairs will appear in is determined by
# passing each pair to the code block to obtain a sort key object, and comparing
# the sort keys using `#<=>`.
#
# @see ::Enumerable#sort_by
#
# @example
# h = Immutable::Hash["Dog" => 1, "Elephant" => 2, "Lion" => 3]
# h.sort_by { |key, value| key.size }
# # => Immutable::Vector[["Dog", 1], ["Lion", 3], ["Elephant", 2]]
#
# @yield [key, value] Once for each key/value pair.
# @yieldreturn a sort key object for the yielded pair.
# @return [Vector]
def sort_by
Vector.new(super)
end
# Return a new `Hash` with the associations for all of the given `keys` removed.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.except("A", "C") # => Immutable::Hash["B" => 2]
#
# @param keys [Array] The keys to remove
# @return [Hash]
def except(*keys)
keys.reduce(self) { |hash, key| hash.delete(key) }
end
# Return a new `Hash` with only the associations for the `wanted` keys retained.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.slice("B", "C") # => Immutable::Hash["B" => 2, "C" => 3]
#
# @param wanted [::Enumerable] The keys to retain
# @return [Hash]
def slice(*wanted)
trie = Trie.new(0)
wanted.each { |key| trie.put!(key, get(key)) if key?(key) }
self.class.alloc(trie, @default)
end
# Return a {Vector} of the values which correspond to the `wanted` keys.
# If any of the `wanted` keys are not present in this `Hash`, `nil` will be
# placed instead, or the result of the default proc (if one is defined),
# similar to the behavior of {#get}.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.values_at("B", "A", "D") # => Immutable::Vector[2, 1, nil]
#
# @param wanted [Array] The keys to retrieve
# @return [Vector]
def values_at(*wanted)
Vector.new(wanted.map { |key| get(key) }.freeze)
end
# Return a {Vector} of the values which correspond to the `wanted` keys.
# If any of the `wanted` keys are not present in this `Hash`, raise `KeyError`
# exception.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.fetch_values("C", "A") # => Immutable::Vector[3, 1]
# h.fetch_values("C", "Z") # => KeyError: key not found: "Z"
#
# @param wanted [Array] The keys to retrieve
# @return [Vector]
def fetch_values(*wanted)
array = wanted.map { |key| fetch(key) }
Vector.new(array.freeze)
end
# Return the value of successively indexing into a nested collection.
# If any of the keys is not present, return `nil`.
#
# @example
# h = Immutable::Hash[a: 9, b: Immutable::Hash[c: 'a', d: 4], e: nil]
# h.dig(:b, :c) # => "a"
# h.dig(:b, :f) # => nil
#
# @return [Object]
def dig(key, *rest)
value = self[key]
if rest.empty? || value.nil?
value
else
value.dig(*rest)
end
end
# Return a new `Hash` created by using keys as values and values as keys.
# If there are multiple values which are equivalent (as determined by `#hash` and
# `#eql?`), only one out of each group of equivalent values will be
# retained. Which one specifically is undefined.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3, "D" => 2].invert
# # => Immutable::Hash[1 => "A", 3 => "C", 2 => "B"]
#
# @return [Hash]
def invert
pairs = []
each { |k,v| pairs << [v, k] }
self.class.new(pairs, &@default)
end
# Return a new {Vector} which is a one-dimensional flattening of this `Hash`.
# If `level` is 1, all the `[key, value]` pairs in the hash will be concatenated
# into one {Vector}. If `level` is greater than 1, keys or values which are
# themselves `Array`s or {Vector}s will be recursively flattened into the output
# {Vector}. The depth to which that flattening will be recursively applied is
# determined by `level`.
#
# As a special case, if `level` is 0, each `[key, value]` pair will be a
# separate element in the returned {Vector}.
#
# @example
# h = Immutable::Hash["A" => 1, "B" => [2, 3, 4]]
# h.flatten
# # => Immutable::Vector["A", 1, "B", [2, 3, 4]]
# h.flatten(2)
# # => Immutable::Vector["A", 1, "B", 2, 3, 4]
#
# @param level [Integer] The number of times to recursively flatten the `[key, value]` pairs in this `Hash`.
# @return [Vector]
def flatten(level = 1)
return Vector.new(self) if level == 0
array = []
each { |k,v| array << k; array << v }
array.flatten!(level-1) if level > 1
Vector.new(array.freeze)
end
# Searches through the `Hash`, comparing `obj` with each key (using `#==`).
# When a matching key is found, return the `[key, value]` pair as an array.
# Return `nil` if no match is found.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].assoc("B") # => ["B", 2]
#
# @param obj [Object] The key to search for (using #==)
# @return [Array]
def assoc(obj)
each { |entry| return entry if obj == entry[0] }
nil
end
# Searches through the `Hash`, comparing `obj` with each value (using `#==`).
# When a matching value is found, return the `[key, value]` pair as an array.
# Return `nil` if no match is found.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].rassoc(2) # => ["B", 2]
#
# @param obj [Object] The value to search for (using #==)
# @return [Array]
def rassoc(obj)
each { |entry| return entry if obj == entry[1] }
nil
end
# Searches through the `Hash`, comparing `value` with each value (using `#==`).
# When a matching value is found, return its associated key object.
# Return `nil` if no match is found.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].key(2) # => "B"
#
# @param value [Object] The value to search for (using #==)
# @return [Object]
def key(value)
each { |entry| return entry[0] if value == entry[1] }
nil
end
# Return a randomly chosen `[key, value]` pair from this `Hash`. If the hash is empty,
# return `nil`.
#
# @example
# Immutable::Hash["A" => 1, "B" => 2, "C" => 3].sample
# # => ["C", 3]
#
# @return [Array]
def sample
@trie.at(rand(size))
end
# Return an empty `Hash` instance, of the same class as this one. Useful if you
# have multiple subclasses of `Hash` and want to treat them polymorphically.
# Maintains the default block, if there is one.
#
# @return [Hash]
def clear
if @default
self.class.alloc(EmptyTrie, @default)
else
self.class.empty
end
end
# Return true if `other` has the same type and contents as this `Hash`.
#
# @param other [Object] The collection to compare with
# @return [Boolean]
def eql?(other)
return true if other.equal?(self)
instance_of?(other.class) && @trie.eql?(other.instance_variable_get(:@trie))
end
# Return true if `other` has the same contents as this `Hash`. Will convert
# `other` to a Ruby `Hash` using `#to_hash` if necessary.
#
# @param other [Object] The object to compare with
# @return [Boolean]
def ==(other)
eql?(other) || (other.respond_to?(:to_hash) && to_hash == other.to_hash)
end
# Return true if this `Hash` is a proper superset of `other`, which means
# all `other`'s keys are contained in this `Hash` with identical
# values, and the two hashes are not identical.
#
# @param other [Immutable::Hash] The object to compare with
# @return [Boolean]
def >(other)
self != other && self >= other
end
# Return true if this `Hash` is a superset of `other`, which means all
# `other`'s keys are contained in this `Hash` with identical values.
#
# @param other [Immutable::Hash] The object to compare with
# @return [Boolean]
def >=(other)
other.each do |key, value|
if self[key] != value
return false
end
end
true
end
# Return true if this `Hash` is a proper subset of `other`, which means all
# its keys are contained in `other` with the identical values, and the two
# hashes are not identical.
#
# @param other [Immutable::Hash] The object to compare with
# @return [Boolean]
def <(other)
other > self
end
# Return true if this `Hash` is a subset of `other`, which means all its
# keys are contained in `other` with the identical values, and the two
# hashes are not identical.
#
# @param other [Immutable::Hash] The object to compare with
# @return [Boolean]
def <=(other)
other >= self
end
# See `Object#hash`.
# @return [Integer]
def hash
keys.to_a.sort.reduce(0) do |hash, key|
(hash << 32) - hash + key.hash + get(key).hash
end
end
# Return the contents of this `Hash` as a programmer-readable `String`. If all the
# keys and values are serializable as Ruby literal strings, the returned string can
# be passed to `eval` to reconstitute an equivalent `Hash`. The default
# block (if there is one) will be lost when doing this, however.
#
# @return [String]
def inspect
result = "#{self.class}["
i = 0
each do |key, val|
result += ', ' if i > 0
result += "#{key.inspect} => #{val.inspect}"
i += 1
end
"#{result}]"
end
# Return `self`. Since this is an immutable object duplicates are
# equivalent.
# @return [Hash]
def dup
self
end
alias clone dup
# Allows this `Hash` to be printed at the `pry` console, or using `pp` (from the
# Ruby standard library), in a way which takes the amount of horizontal space on
# the screen into account, and which indents nested structures to make them easier
# to read.
#
# @private
def pretty_print(pp)
pp.group(1, "#{self.class}[", ']') do
pp.breakable ''
pp.seplist(self, nil) do |key, val|
pp.group do
key.pretty_print(pp)
pp.text ' => '
pp.group(1) do
pp.breakable ''
val.pretty_print(pp)
end
end
end
end
end
# Convert this `Immutable::Hash` to an instance of Ruby's built-in `Hash`.
#
# @return [::Hash]
def to_hash
output = {}
each do |key, value|
output[key] = value
end
output
end
alias to_h to_hash
# Return a `Proc` which accepts a key as an argument and returns the value.
# The `Proc` behaves like {#get} (when the key is missing, it returns nil or
# the result of the default proc).
#
# @example
# h = Immutable::Hash["A" => 1, "B" => 2, "C" => 3]
# h.to_proc.call("B")
# # => 2
# ["A", "C", "X"].map(&h) # The & is short for .to_proc in Ruby
# # => [1, 3, nil]
#
# @return [Proc]
def to_proc
lambda { |key| get(key) }
end
# @return [::Hash]
# @private
def marshal_dump
to_hash
end
# @private
def marshal_load(dictionary)
@trie = Trie[dictionary]
end
private
# Return a new `Hash` which is derived from this one, using a modified {Trie}.
# The new `Hash` will retain the existing default block, if there is one.
#
def derive_new_hash(trie)
if trie.equal?(@trie)
self
elsif trie.empty?
if @default
self.class.alloc(EmptyTrie, @default)
else
self.class.empty
end
else
self.class.alloc(trie, @default)
end
end
end
# The canonical empty `Hash`. Returned by `Hash[]` when
# invoked with no arguments; also returned by `Hash.empty`. Prefer using this
# one rather than creating many empty hashes using `Hash.new`.
#
# @private
EmptyHash = SpeckleConnector::Immutable::Hash.empty
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
# Define the method to export immutable structures to JSON. Default exporter would only export the name of the class as
# a string. In order to export it properly, we need to add `to_json` methods to the classes we want to serialize as JSON.
module SpeckleConnector
module Immutable
class Vector
# Convert the object to JSON
# @return [String] json encoded string
# @param args [Array] the arguments that will be passed to JSON.to_json method
def to_json(*args)
to_a.to_json(*args)
end
end
class Hash
# Convert the object to JSON
# @return [String] json encoded string
# @param args [Array] the arguments that will be passed to JSON.to_json method
def to_json(*args)
to_h.to_json(*args)
end
end
end
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,589 @@
require_relative 'undefined'
require_relative 'enumerable'
require_relative 'trie'
require_relative 'sorted_set'
require 'set'
module SpeckleConnector
module Immutable
# `Immutable::Set` is a collection of unordered values with no duplicates. Testing whether
# an object is present in the `Set` can be done in constant time. `Set` is also `Enumerable`, so you can
# iterate over the members of the set with {#each}, transform them with {#map}, filter
# them with {#select}, and so on. Some of the `Enumerable` methods are overridden to
# return `immutable-ruby` collections.
#
# Like the `Set` class in Ruby's standard library, which we will call RubySet,
# `Immutable::Set` defines equivalency of objects using `#hash` and `#eql?`. No two
# objects with the same `#hash` code, and which are also `#eql?`, can coexist in the
# same `Set`. If one is already in the `Set`, attempts to add another one will have
# no effect.
#
# `Set`s have no natural ordering and cannot be compared using `#<=>`. However, they
# define {#<}, {#>}, {#<=}, and {#>=} as shorthand for {#proper_subset?},
# {#proper_superset?}, {#subset?}, and {#superset?} respectively.
#
# The basic set-theoretic operations {#union}, {#intersection}, {#difference}, and
# {#exclusion} work with any `Enumerable` object.
#
# A `Set` can be created in either of the following ways:
#
# Immutable::Set.new([1, 2, 3]) # any Enumerable can be used to initialize
# Immutable::Set['A', 'B', 'C', 'D']
#
# The latter 2 forms of initialization can be used with your own, custom subclasses
# of `Immutable::Set`.
#
# Unlike RubySet, all methods which you might expect to "modify" an `Immutable::Set`
# actually return a new set and leave the existing one unchanged.
#
# @example
# set1 = Immutable::Set[1, 2] # => Immutable::Set[1, 2]
# set2 = Immutable::Set[1, 2] # => Immutable::Set[1, 2]
# set1 == set2 # => true
# set3 = set1.add("foo") # => Immutable::Set[1, 2, "foo"]
# set3 - set2 # => Immutable::Set["foo"]
# set3.subset?(set1) # => false
# set1.subset?(set3) # => true
#
class Set
include SpeckleConnector::Immutable::Enumerable
class << self
# Create a new `Set` populated with the given items.
# @return [Set]
def [](*items)
items.empty? ? empty : new(items)
end
# Return an empty `Set`. If used on a subclass, returns an empty instance
# of that class.
#
# @return [Set]
def empty
@empty ||= new
end
# "Raw" allocation of a new `Set`. Used internally to create a new
# instance quickly after obtaining a modified {Trie}.
#
# @return [Set]
# @private
def alloc(trie = EmptyTrie)
allocate.tap { |s| s.instance_variable_set(:@trie, trie) }.freeze
end
end
def initialize(items=[])
@trie = Trie.new(0)
items.each { |item| @trie.put!(item, nil) }
freeze
end
# Return `true` if this `Set` contains no items.
# @return [Boolean]
def empty?
@trie.empty?
end
# Return the number of items in this `Set`.
# @return [Integer]
def size
@trie.size
end
alias length size
# Return a new `Set` with `item` added. If `item` is already in the set,
# return `self`.
#
# @example
# Immutable::Set[1, 2, 3].add(4) # => Immutable::Set[1, 2, 4, 3]
# Immutable::Set[1, 2, 3].add(2) # => Immutable::Set[1, 2, 3]
#
# @param item [Object] The object to add
# @return [Set]
def add(item)
include?(item) ? self : self.class.alloc(@trie.put(item, nil))
end
alias << add
# If `item` is not a member of this `Set`, return a new `Set` with `item` added.
# Otherwise, return `false`.
#
# @example
# Immutable::Set[1, 2, 3].add?(4) # => Immutable::Set[1, 2, 4, 3]
# Immutable::Set[1, 2, 3].add?(2) # => false
#
# @param item [Object] The object to add
# @return [Set, false]
def add?(item)
!include?(item) && add(item)
end
# Return a new `Set` with `item` removed. If `item` is not a member of the set,
# return `self`.
#
# @example
# Immutable::Set[1, 2, 3].delete(1) # => Immutable::Set[2, 3]
# Immutable::Set[1, 2, 3].delete(99) # => Immutable::Set[1, 2, 3]
#
# @param item [Object] The object to remove
# @return [Set]
def delete(item)
trie = @trie.delete(item)
new_trie(trie)
end
# If `item` is a member of this `Set`, return a new `Set` with `item` removed.
# Otherwise, return `false`.
#
# @example
# Immutable::Set[1, 2, 3].delete?(1) # => Immutable::Set[2, 3]
# Immutable::Set[1, 2, 3].delete?(99) # => false
#
# @param item [Object] The object to remove
# @return [Set, false]
def delete?(item)
include?(item) && delete(item)
end
# Call the block once for each item in this `Set`. No specific iteration order
# is guaranteed, but the order will be stable for any particular `Set`. If
# no block is given, an `Enumerator` is returned instead.
#
# @example
# Immutable::Set["Dog", "Elephant", "Lion"].each { |e| puts e }
# Elephant
# Dog
# Lion
# # => Immutable::Set["Dog", "Elephant", "Lion"]
#
# @yield [item] Once for each item.
# @return [self, Enumerator]
def each
return to_enum if not block_given?
@trie.each { |key, _| yield(key) }
self
end
# Call the block once for each item in this `Set`. Iteration order will be
# the opposite of {#each}. If no block is given, an `Enumerator` is
# returned instead.
#
# @example
# Immutable::Set["Dog", "Elephant", "Lion"].reverse_each { |e| puts e }
# Lion
# Dog
# Elephant
# # => Immutable::Set["Dog", "Elephant", "Lion"]
#
# @yield [item] Once for each item.
# @return [self]
def reverse_each
return enum_for(:reverse_each) if not block_given?
@trie.reverse_each { |key, _| yield(key) }
self
end
# Return a new `Set` with all the items for which the block returns true.
#
# @example
# Immutable::Set["Elephant", "Dog", "Lion"].select { |e| e.size >= 4 }
# # => Immutable::Set["Elephant", "Lion"]
# @yield [item] Once for each item.
# @return [Set]
def select
return enum_for(:select) unless block_given?
trie = @trie.select { |key, _| yield(key) }
new_trie(trie)
end
alias find_all select
alias keep_if select
# Call the block once for each item in this `Set`. All the values returned
# from the block will be gathered into a new `Set`. If no block is given,
# an `Enumerator` is returned instead.
#
# @example
# Immutable::Set["Cat", "Elephant", "Dog", "Lion"].map { |e| e.size }
# # => Immutable::Set[8, 4, 3]
#
# @yield [item] Once for each item.
# @return [Set]
def map
return enum_for(:map) if not block_given?
return self if empty?
self.class.new(super)
end
alias collect map
# Return `true` if the given item is present in this `Set`. More precisely,
# return `true` if an object with the same `#hash` code, and which is also `#eql?`
# to the given object is present.
#
# @example
# Immutable::Set["A", "B", "C"].include?("B") # => true
# Immutable::Set["A", "B", "C"].include?("Z") # => false
#
# @param object [Object] The object to check for
# @return [Boolean]
def include?(object)
@trie.key?(object)
end
alias member? include?
# Return a member of this `Set`. The member chosen will be the first one which
# would be yielded by {#each}. If the set is empty, return `nil`.
#
# @example
# Immutable::Set["A", "B", "C"].first # => "C"
#
# @return [Object]
def first
(entry = @trie.at(0)) && entry[0]
end
# Return a {SortedSet} which contains the same items as this `Set`, ordered by
# the given comparator block.
#
# @example
# Immutable::Set["Elephant", "Dog", "Lion"].sort
# # => Immutable::SortedSet["Dog", "Elephant", "Lion"]
# Immutable::Set["Elephant", "Dog", "Lion"].sort { |a,b| a.size <=> b.size }
# # => Immutable::SortedSet["Dog", "Lion", "Elephant"]
#
# @yield [a, b] Any number of times with different pairs of elements.
# @yieldreturn [Integer] Negative if the first element should be sorted
# lower, positive if the latter element, or 0 if
# equal.
# @return [SortedSet]
def sort(&comparator)
SortedSet.new(to_a, &comparator)
end
# Return a {SortedSet} which contains the same items as this `Set`, ordered
# by mapping each item through the provided block to obtain sort keys, and
# then sorting the keys.
#
# @example
# Immutable::Set["Elephant", "Dog", "Lion"].sort_by { |e| e.size }
# # => Immutable::SortedSet["Dog", "Lion", "Elephant"]
#
# @yield [item] Once for each item to create the set, and then potentially
# again depending on what operations are performed on the
# returned {SortedSet}. As such, it is recommended that the
# block be a pure function.
# @yieldreturn [Object] sort key for the item
# @return [SortedSet]
def sort_by(&mapper)
SortedSet.new(to_a, &mapper)
end
# Return a new `Set` which contains all the members of both this `Set` and `other`.
# `other` can be any `Enumerable` object.
#
# @example
# Immutable::Set[1, 2] | Immutable::Set[2, 3] # => Immutable::Set[1, 2, 3]
#
# @param other [Enumerable] The collection to merge with
# @return [Set]
def union(other)
if other.is_a?(SpeckleConnector::Immutable::Set)
if other.size > size
small_set_pairs = @trie
large_set_trie = other.instance_variable_get(:@trie)
else
small_set_pairs = other.instance_variable_get(:@trie)
large_set_trie = @trie
end
else
if other.respond_to?(:lazy)
small_set_pairs = other.lazy.map { |e| [e, nil] }
else
small_set_pairs = other.map { |e| [e, nil] }
end
large_set_trie = @trie
end
trie = large_set_trie.bulk_put(small_set_pairs)
new_trie(trie)
end
alias | union
alias + union
alias merge union
# Return a new `Set` which contains all the items which are members of both
# this `Set` and `other`. `other` can be any `Enumerable` object.
#
# @example
# Immutable::Set[1, 2] & Immutable::Set[2, 3] # => Immutable::Set[2]
#
# @param other [Enumerable] The collection to intersect with
# @return [Set]
def intersection(other)
if other.size < @trie.size
if other.is_a?(SpeckleConnector::Immutable::Set)
trie = other.instance_variable_get(:@trie).select { |key, _| include?(key) }
else
trie = Trie.new(0)
other.each { |obj| trie.put!(obj, nil) if include?(obj) }
end
else
trie = @trie.select { |key, _| other.include?(key) }
end
new_trie(trie)
end
alias & intersection
# Return a new `Set` with all the items in `other` removed. `other` can be
# any `Enumerable` object.
#
# @example
# Immutable::Set[1, 2] - Immutable::Set[2, 3] # => Immutable::Set[1]
#
# @param other [Enumerable] The collection to subtract from this set
# @return [Set]
def difference(other)
trie = if (@trie.size <= other.size) && (other.is_a?(SpeckleConnector::Immutable::Set) || (defined?(::Set) && other.is_a?(::Set)))
@trie.select { |key, _| !other.include?(key) }
else
@trie.bulk_delete(other)
end
new_trie(trie)
end
alias subtract difference
alias - difference
# Return a new `Set` which contains all the items which are members of this
# `Set` or of `other`, but not both. `other` can be any `Enumerable` object.
#
# @example
# Immutable::Set[1, 2] ^ Immutable::Set[2, 3] # => Immutable::Set[1, 3]
#
# @param other [Enumerable] The collection to take the exclusive disjunction of
# @return [Set]
def exclusion(other)
((self | other) - (self & other))
end
alias ^ exclusion
# Return `true` if all items in this `Set` are also in `other`.
#
# @example
# Immutable::Set[2, 3].subset?(Immutable::Set[1, 2, 3]) # => true
#
# @param other [Set]
# @return [Boolean]
def subset?(other)
return false if other.size < size
# This method has the potential to be very slow if 'other' is a large Array, so to avoid that,
# we convert those Arrays to Sets before checking presence of items
# Time to convert Array -> Set is linear in array.size
# Time to check for presence of all items in an Array is proportional to set.size * array.size
# Note that both sides of that equation have array.size -- hence those terms cancel out,
# and the break-even point is solely dependent on the size of this collection
# After doing some benchmarking to estimate the constants, it appears break-even is at ~190 items
# We also check other.size, to avoid the more expensive #is_a? checks in cases where it doesn't matter
#
if other.size >= 150 && @trie.size >= 190 && !(other.is_a?(SpeckleConnector::Immutable::Set) || other.is_a?(::Set))
other = ::Set.new(other)
end
all? { |item| other.include?(item) }
end
alias <= subset?
# Return `true` if all items in `other` are also in this `Set`.
#
# @example
# Immutable::Set[1, 2, 3].superset?(Immutable::Set[2, 3]) # => true
#
# @param other [Set]
# @return [Boolean]
def superset?(other)
other.subset?(self)
end
alias >= superset?
# Returns `true` if `other` contains all the items in this `Set`, plus at least
# one item which is not in this set.
#
# @example
# Immutable::Set[2, 3].proper_subset?(Immutable::Set[1, 2, 3]) # => true
# Immutable::Set[1, 2, 3].proper_subset?(Immutable::Set[1, 2, 3]) # => false
#
# @param other [Set]
# @return [Boolean]
def proper_subset?(other)
return false if other.size <= size
# See comments above
if other.size >= 150 && @trie.size >= 190 && !(other.is_a?(SpeckleConnector::Immutable::Set) || other.is_a?(::Set))
other = ::Set.new(other)
end
all? { |item| other.include?(item) }
end
alias < proper_subset?
# Returns `true` if this `Set` contains all the items in `other`, plus at least
# one item which is not in `other`.
#
# @example
# Immutable::Set[1, 2, 3].proper_superset?(Immutable::Set[2, 3]) # => true
# Immutable::Set[1, 2, 3].proper_superset?(Immutable::Set[1, 2, 3]) # => false
#
# @param other [Set]
# @return [Boolean]
def proper_superset?(other)
other.proper_subset?(self)
end
alias > proper_superset?
# Return `true` if this `Set` and `other` do not share any items.
#
# @example
# Immutable::Set[1, 2].disjoint?(Immutable::Set[8, 9]) # => true
#
# @param other [Set]
# @return [Boolean]
def disjoint?(other)
if other.size <= size
other.each { |item| return false if include?(item) }
else
# See comment on #subset?
if other.size >= 150 && @trie.size >= 190 && !(other.is_a?(SpeckleConnector::Immutable::Set) || other.is_a?(::Set))
other = ::Set.new(other)
end
each { |item| return false if other.include?(item) }
end
true
end
# Return `true` if this `Set` and `other` have at least one item in common.
#
# @example
# Immutable::Set[1, 2].intersect?(Immutable::Set[2, 3]) # => true
#
# @param other [Set]
# @return [Boolean]
def intersect?(other)
!disjoint?(other)
end
# Recursively insert the contents of any nested `Set`s into this `Set`, and
# remove them.
#
# @example
# Immutable::Set[Immutable::Set[1, 2], Immutable::Set[3, 4]].flatten
# # => Immutable::Set[1, 2, 3, 4]
#
# @return [Set]
def flatten
reduce(self.class.empty) do |set, item|
next set.union(item.flatten) if item.is_a?(Set)
set.add(item)
end
end
alias group group_by
alias classify group_by
# Return a randomly chosen item from this `Set`. If the set is empty, return `nil`.
#
# @example
# Immutable::Set[1, 2, 3, 4, 5].sample # => 3
#
# @return [Object]
def sample
empty? ? nil : @trie.at(rand(size))[0]
end
# Return an empty `Set` instance, of the same class as this one. Useful if you
# have multiple subclasses of `Set` and want to treat them polymorphically.
#
# @return [Set]
def clear
self.class.empty
end
# Return true if `other` has the same type and contents as this `Set`.
#
# @param other [Object] The object to compare with
# @return [Boolean]
def eql?(other)
return true if other.equal?(self)
return false if not instance_of?(other.class)
other_trie = other.instance_variable_get(:@trie)
return false if @trie.size != other_trie.size
@trie.each do |key, _|
return false if !other_trie.key?(key)
end
true
end
alias == eql?
# See `Object#hash`.
# @return [Integer]
def hash
reduce(0) { |hash, item| (hash << 5) - hash + item.hash }
end
# Return `self`. Since this is an immutable object duplicates are
# equivalent.
# @return [Set]
def dup
self
end
alias clone dup
def <=>(*_args)
raise NotImplementedError, 'Sets are not ordered, so Enumerable#<=> will give a meaningless result'
end
def each_index(*_args)
raise NotImplementedError, "members cannot be accessed by 'index', so #each_index is not meaningful"
end
# Return `self`.
#
# @return [self]
def to_set
self
end
# @private
def marshal_dump
output = {}
each do |key|
output[key] = nil
end
output
end
# @private
def marshal_load(dictionary)
@trie = dictionary.reduce(EmptyTrie) do |trie, key_value|
trie.put(key_value.first, nil)
end
end
private
def new_trie(trie)
if trie.empty?
self.class.empty
elsif trie.equal?(@trie)
self
else
self.class.alloc(trie)
end
end
end
# The canonical empty `Set`. Returned by `Set[]` when
# invoked with no arguments; also returned by `Set.empty`. Prefer using this
# one rather than creating many empty sets using `Set.new`.
#
# @private
EmptySet = SpeckleConnector::Immutable::Set.empty
end
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,332 @@
module SpeckleConnector
module Immutable
# @private
class Trie
def self.[](pairs)
result = new(0)
pairs.each { |key, val| result.put!(key, val) }
result
end
# Returns the number of key-value pairs in the trie.
attr_reader :size
def initialize(bitshift, size = 0, entries = [], children = [])
@bitshift = bitshift
@entries = entries
@children = children
@size = size
end
# Returns <tt>true</tt> if the trie contains no key-value pairs.
def empty?
@size == 0
end
# Returns <tt>true</tt> if the given key is present in the trie.
def key?(key)
!!get(key)
end
# Calls <tt>block</tt> once for each entry in the trie, passing the key-value pair as parameters.
def each(&block)
@entries.each { |entry| yield entry if entry }
@children.each do |child|
child.each(&block) if child
end
nil
end
def reverse_each(&block)
@children.reverse_each do |child|
child.reverse_each(&block) if child
end
@entries.reverse_each { |entry| yield(entry) if entry }
nil
end
def reduce(memo)
each { |entry| memo = yield(memo, entry) }
memo
end
def select
keys_to_delete = []
each { |entry| keys_to_delete << entry[0] unless yield(entry) }
bulk_delete(keys_to_delete)
end
# @return [Trie] A copy of `self` with the given value associated with the
# key (or `self` if no modification was needed because an identical
# key-value pair was already stored
def put(key, value)
index = index_for(key)
entry = @entries[index]
if !entry
entries = @entries.dup
key = key.dup.freeze if key.is_a?(String) && !key.frozen?
entries[index] = [key, value].freeze
Trie.new(@bitshift, @size + 1, entries, @children)
elsif entry[0].eql?(key)
if entry[1].equal?(value)
self
else
entries = @entries.dup
key = key.dup.freeze if key.is_a?(String) && !key.frozen?
entries[index] = [key, value].freeze
Trie.new(@bitshift, @size, entries, @children)
end
else
child = @children[index]
if child
new_child = child.put(key, value)
if new_child.equal?(child)
self
else
children = @children.dup
children[index] = new_child
new_self_size = @size + (new_child.size - child.size)
Trie.new(@bitshift, new_self_size, @entries, children)
end
else
children = @children.dup
children[index] = Trie.new(@bitshift + 5).put!(key, value)
Trie.new(@bitshift, @size + 1, @entries, children)
end
end
end
# Put multiple elements into a Trie. This is more efficient than several
# calls to `#put`.
#
# @param key_value_pairs Enumerable of pairs (`[key, value]`)
# @return [Trie] A copy of `self` after associated the given keys and
# values (or `self` if no modifications where needed).
def bulk_put(key_value_pairs)
new_entries = nil
new_children = nil
new_size = @size
key_value_pairs.each do |key, value|
index = index_for(key)
entry = (new_entries || @entries)[index]
if !entry
new_entries ||= @entries.dup
key = key.dup.freeze if key.is_a?(String) && !key.frozen?
new_entries[index] = [key, value].freeze
new_size += 1
elsif entry[0].eql?(key)
if !entry[1].equal?(value)
new_entries ||= @entries.dup
key = key.dup.freeze if key.is_a?(String) && !key.frozen?
new_entries[index] = [key, value].freeze
end
else
child = (new_children || @children)[index]
if child
new_child = child.put(key, value)
if !new_child.equal?(child)
new_children ||= @children.dup
new_children[index] = new_child
new_size += new_child.size - child.size
end
else
new_children ||= @children.dup
new_children[index] = Trie.new(@bitshift + 5).put!(key, value)
new_size += 1
end
end
end
if new_entries || new_children
Trie.new(@bitshift, new_size, new_entries || @entries, new_children || @children)
else
self
end
end
# Returns <tt>self</tt> after overwriting the element associated with the specified key.
def put!(key, value)
index = index_for(key)
entry = @entries[index]
if !entry
@size += 1
key = key.dup.freeze if key.is_a?(String) && !key.frozen?
@entries[index] = [key, value].freeze
elsif entry[0].eql?(key)
key = key.dup.freeze if key.is_a?(String) && !key.frozen?
@entries[index] = [key, value].freeze
else
child = @children[index]
if child
old_child_size = child.size
@children[index] = child.put!(key, value)
@size += child.size - old_child_size
else
@children[index] = Trie.new(@bitshift + 5).put!(key, value)
@size += 1
end
end
self
end
# Retrieves the entry corresponding to the given key. If not found, returns <tt>nil</tt>.
def get(key)
index = index_for(key)
entry = @entries[index]
if entry && entry[0].eql?(key)
entry
else
child = @children[index]
child.get(key) if child
end
end
# Returns a copy of <tt>self</tt> with the given key (and associated value) deleted. If not found, returns <tt>self</tt>.
def delete(key)
find_and_delete(key) || Trie.new(@bitshift)
end
# Delete multiple elements from a Trie. This is more efficient than
# several calls to `#delete`.
#
# @param keys [Enumerable] The keys to delete
# @return [Trie]
def bulk_delete(keys)
new_entries = nil
new_children = nil
new_size = @size
keys.each do |key|
index = index_for(key)
entry = (new_entries || @entries)[index]
if !entry
next
elsif entry[0].eql?(key)
new_entries ||= @entries.dup
child = (new_children || @children)[index]
if child
# Bring up the first entry from the child into entries
new_children ||= @children.dup
new_children[index] = child.delete_at do |entry|
new_entries[index] = entry
end
else
new_entries[index] = nil
end
new_size -= 1
else
child = (new_children || @children)[index]
if child
copy = child.find_and_delete(key)
unless copy.equal?(child)
new_children ||= @children.dup
new_children[index] = copy
new_size -= (child.size - copy_size(copy))
end
end
end
end
if new_entries || new_children
Trie.new(@bitshift, new_size, new_entries || @entries, new_children || @children)
else
self
end
end
def include?(key, value)
entry = get(key)
entry && value.eql?(entry[1])
end
def at(index)
@entries.each do |entry|
if entry
return entry if index == 0
index -= 1
end
end
@children.each do |child|
if child
if child.size >= index+1
return child.at(index)
else
index -= child.size
end
end
end
nil
end
# Returns <tt>true</tt> if . <tt>eql?</tt> is synonymous with <tt>==</tt>
def eql?(other)
return true if equal?(other)
return false unless instance_of?(other.class) && size == other.size
each do |entry|
return false unless other.include?(entry[0], entry[1])
end
true
end
alias == eql?
protected
# Returns a replacement instance after removing the specified key.
# If not found, returns <tt>self</tt>.
# If empty, returns <tt>nil</tt>.
def find_and_delete(key)
index = index_for(key)
entry = @entries[index]
if entry && entry[0].eql?(key)
return delete_at(index)
else
child = @children[index]
if child
copy = child.find_and_delete(key)
unless copy.equal?(child)
children = @children.dup
children[index] = copy
new_size = @size - (child.size - copy_size(copy))
return Trie.new(@bitshift, new_size, @entries, children)
end
end
end
self
end
# Returns a replacement instance after removing the specified entry. If empty, returns <tt>nil</tt>
def delete_at(index = @entries.index { |e| e })
yield(@entries[index]) if block_given?
if size > 1
entries = @entries.dup
child = @children[index]
if child
children = @children.dup
children[index] = child.delete_at do |entry|
entries[index] = entry
end
else
entries[index] = nil
end
Trie.new(@bitshift, @size - 1, entries, children || @children)
end
end
private
def index_for(key)
(key.hash.abs >> @bitshift) & 31
end
def copy_size(copy)
copy ? copy.size : 0
end
end
# @private
EmptyTrie = Trie.new(0).freeze
end
end

Some files were not shown because too many files have changed in this diff Show More