Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0113532b6 | |||
| 5a1d2ad5f4 | |||
| dfe02f4c74 | |||
| 5349d556b9 | |||
| 7f7d8a501b | |||
| cbee0e465b | |||
| faf00f0b0d | |||
| fa97da5781 | |||
| e7bab546db | |||
| e2f4a30b5b | |||
| ccff1df041 | |||
| 2037cfc25a | |||
| 268a091d8a | |||
| 6b52dfab3e | |||
| 839999851f | |||
| a87470b7b5 | |||
| e76aeb80fd | |||
| 28292e59e2 | |||
| 25dda481b2 | |||
| bbda233fd8 | |||
| 349218f0b5 | |||
| f18d00a69d | |||
| 25ea6504de | |||
| 43081c70e2 | |||
| 0fde1c2026 | |||
| b35383571e | |||
| 45a84847a2 | |||
| 70d92f26d6 | |||
| 737ed86e69 | |||
| 3865057b7a | |||
| 42a84dcd86 | |||
| e2d819c59d | |||
| bfee6a88dc | |||
| 68f3be17df | |||
| 929c97ff5e | |||
| 4b66a2e4d0 | |||
| 46e740154e | |||
| 05e89f49da | |||
| 358e9071e3 | |||
| e37b6a1cc0 | |||
| 266721973b | |||
| 7c27ac85cb | |||
| 4b79732e38 | |||
| 2ceeea5298 | |||
| 3ec659a59b | |||
| 4309056851 | |||
| b768f20f7a | |||
| b3a71bcf53 | |||
| 50c199bc03 | |||
| d6302ac128 | |||
| 6a5d9e1394 | |||
| ac5fc3e6ea | |||
| aa6cbceeb9 | |||
| 46a7395382 | |||
| a782811dad | |||
| d22039bc96 | |||
| 15539c258e | |||
| f9ca4acf16 | |||
| 66d2a9b7fe | |||
| 6dff8c3221 | |||
| f13c65e083 | |||
| 56a7d5cb86 | |||
| c63c0675d5 | |||
| 22bc4b8c9e | |||
| ead17b8906 | |||
| 1a211daac2 | |||
| c7e502da4e | |||
| 70df5e6cec | |||
| 793f287c35 | |||
| 8e8b1c60b8 | |||
| ba2cd51852 | |||
| 3986a4ef60 | |||
| c99d89fb11 | |||
| 0a9c33de91 | |||
| 88c940fc53 | |||
| 88c861cbde | |||
| cd071ca144 | |||
| 9970b8ec36 | |||
| ffc564becd | |||
| 6289fd5941 | |||
| 7f44fe76c7 | |||
| ea86dc6785 | |||
| 754f9e1ed1 | |||
| 9a1a02e664 | |||
| 1e92195355 | |||
| bb63c1c990 | |||
| 5af494de76 | |||
| 5bcac73a45 | |||
| 7a709e48d7 | |||
| d2d9fe010e | |||
| 4b6b129934 | |||
| 7645146d77 | |||
| 9b8cca38ae | |||
| e872fda3f2 | |||
| 6ccb03c557 | |||
| b8237b1be7 | |||
| 68e19cf8e4 | |||
| 91fba729b8 | |||
| 0af0e09b6e | |||
| 1276d56627 | |||
| bd2773f9c1 | |||
| b8b1b8ef36 | |||
| 0f98d72e30 | |||
| 44ec7734d4 | |||
| c5adb063ca | |||
| 83ef8f9789 | |||
| b8a803a641 | |||
| 31c0effebe | |||
| e90c4b3eb5 | |||
| ae6efcc27d | |||
| 7922ac127b | |||
| 559f0443af | |||
| 4e3faf1573 | |||
| e3039bd2d8 | |||
| e668cba839 | |||
| fcc47c0b7f | |||
| 2f189bc2ff | |||
| ee9d134f0e | |||
| c1fad7cc02 | |||
| 3a48b4ee4f | |||
| ec626f9741 | |||
| 90906cdc0e | |||
| a694527645 | |||
| bd68b20d31 | |||
| 080d976400 | |||
| e200a8525b | |||
| 5bdf7abc2b | |||
| 9faa1d2e1e | |||
| f586353cdb | |||
| 2c9f2ee5cf | |||
| d8bd45bf54 | |||
| 9341e0b500 | |||
| eebf303f3e | |||
| df487d5488 | |||
| 7b66da5823 | |||
| 3a929c2a03 | |||
| a8ac3c4771 | |||
| 8d223786b9 | |||
| 068c6a6ea0 | |||
| 0db677121a | |||
| 7591170bca | |||
| 7272541826 | |||
| 2460912532 | |||
| c1e4c22e3f | |||
| 17bd17fa3c | |||
| 05da3ccb66 | |||
| 578044884c | |||
| 226148d04f | |||
| c44c7516ff | |||
| c3c0749222 | |||
| b7e63b0e54 | |||
| 60cd2cb0f9 | |||
| e6dd630caf | |||
| cad14b318a | |||
| 4df1cc17bf | |||
| 884bb331b3 | |||
| ff4a83af47 | |||
| f6f323b307 | |||
| 330280d611 | |||
| 809432cbbd | |||
| d64fee1d15 | |||
| 3d01e15710 | |||
| c2d7a5aca8 | |||
| 2e8a040210 | |||
| 50886147ae | |||
| c0f2885de6 | |||
| e80574ecd7 | |||
| 7cdcf9e86f | |||
| 304720fc8e | |||
| 88adfb1446 | |||
| f8719d0912 | |||
| c6d36fccd9 | |||
| 13d061f7e9 | |||
| ba11c5beb2 | |||
| c1800d9a02 | |||
| 868859ac83 | |||
| d91114b340 | |||
| 0dc4d46fa3 | |||
| 0442aaaa7f | |||
| 2c8b120de1 | |||
| fc50ef68b4 | |||
| 92613a37cd | |||
| 77843fe697 | |||
| 9aea5ddc97 | |||
| 4d1333c302 | |||
| 9ca55f6f0e | |||
| 4698799b43 | |||
| 5d4e6ac89a | |||
| b1c09c62d9 | |||
| 2b7b74dbdd |
@@ -21,7 +21,7 @@ jobs:
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle_connector/html
|
||||
- speckle_connector/vue_ui
|
||||
|
||||
build-connector: # Reusable job for basic connectors
|
||||
executor:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
$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: ./
|
||||
@@ -58,9 +58,17 @@ jobs:
|
||||
docker:
|
||||
- image: cimg/base:2021.01
|
||||
steps:
|
||||
- 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:
|
||||
@@ -69,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:
|
||||
@@ -101,6 +115,7 @@ workflows:
|
||||
only: /.*/
|
||||
|
||||
- build-connector:
|
||||
context: innosetup
|
||||
slug: sketchup
|
||||
requires:
|
||||
- get-ci-tools
|
||||
@@ -109,7 +124,11 @@ workflows:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- deploy:
|
||||
- deploy-manager2:
|
||||
context: do-spaces-speckle-releases
|
||||
slug: sketchup
|
||||
os: Win
|
||||
extension: exe
|
||||
requires:
|
||||
- get-ci-tools
|
||||
- build-ui
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "_sqlite3"]
|
||||
path = _sqlite3
|
||||
url = git@github.com:specklesystems/sketchup-sqlite3.git
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,27 +41,42 @@ Give Speckle a try in no time by:
|
||||
- [](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:
|
||||
|
||||

|
||||
|
||||
@@ -133,9 +168,30 @@ Then choose the `Debug Sketchup 2021` task to run it:
|
||||
|
||||

|
||||
|
||||
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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 811 B |
|
After Width: | Height: | Size: 1006 B |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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__)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
@@ -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("&", "&").gsub("<", "<").gsub(">", ">")
|
||||
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
|
||||
@@ -0,0 +1,2 @@
|
||||
ID=f4d9d053-4479-4a9a-90da-b79fa16e28c4
|
||||
VERSION_ID=b787af5e-8e8e-4932-92ef-a3c99681795d
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,57 +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.default_account
|
||||
accts = load_accounts
|
||||
accts.select { |acc| acc["isDefault"] }[0] || accts[0]
|
||||
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
|
||||
@@ -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,32 +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, :registry, :entity_observer
|
||||
|
||||
def initialize(units = "m")
|
||||
@units = units
|
||||
@component_defs = {}
|
||||
# @registry = Sketchup.active_model.attribute_dictionary("speckle_id_registry", true)
|
||||
# @entity_observer = SpeckleEntityObserver.new
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
module SpeckleSystems::SpeckleConnector
|
||||
class SpeckleEntityObserver < Sketchup::EntityObserver
|
||||
attr_accessor :registry
|
||||
|
||||
def initialize
|
||||
super()
|
||||
@registry = Sketchup.active_model.attribute_dictionary("speckle_id_registry", true)
|
||||
end
|
||||
|
||||
def onEraseEntity(entity)
|
||||
app_id = entity.get_attribute("speckle", "applicationId")
|
||||
return if app_id.nil?
|
||||
|
||||
p(app_id)
|
||||
|
||||
@registry.delete_key(app_id)
|
||||
|
||||
p(@registry)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,243 +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 if entities == Sketchup.active_model.entities
|
||||
edge_to_native_component(obj, entities)
|
||||
else
|
||||
edge_to_native(obj, entities)
|
||||
end
|
||||
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 if entities == Sketchup.active_model.entities
|
||||
mesh_to_native_component(obj, entities)
|
||||
else
|
||||
mesh_to_native(obj, entities)
|
||||
end
|
||||
when "Objects.Geometry.Brep" then if entities == Sketchup.active_model.entities
|
||||
mesh_to_native_component(obj["displayMesh"], entities)
|
||||
else
|
||||
mesh_to_native(obj["displayMesh"], entities)
|
||||
end
|
||||
when obj.key?["displayValue"]
|
||||
parent_id = obj["applicationId"] || obj["id"]
|
||||
obj["displayValue"].each do |o|
|
||||
o["applicationId"] = "#{parent_id}::#{o["id"]}" if o["applicationId"].nil?
|
||||
convert_to_native(o, entities)
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
rescue StandardError => e
|
||||
puts("Failed to convert #{obj["speckle_type"]} (id: #{obj["id"]})")
|
||||
puts(e)
|
||||
nil
|
||||
end
|
||||
|
||||
# def register_receive(entity, id)
|
||||
# # entity.add_observer(@entity_observer)
|
||||
# # entity.set_attribute("speckle", "applicationId", id)
|
||||
# @registry[id] = entity.persistent_id
|
||||
# end
|
||||
|
||||
# def get_received_entity(app_id)
|
||||
# return if @registry[app_id].nil?
|
||||
# end
|
||||
|
||||
# def received?(id)
|
||||
# !@registry[id].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 edge_to_native_component(line, entities)
|
||||
line_id = line["applicationId"] || line["id"]
|
||||
definition = component_definition_to_native([line], "def::#{line_id}")
|
||||
find_and_erase_existing_instance(definition, line_id)
|
||||
instance = entities.add_instance(definition, Geom::Transformation.new)
|
||||
instance.name = line_id
|
||||
instance
|
||||
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
|
||||
|
||||
# converts a mesh to a native mesh and adds the faces to the given entities collection
|
||||
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
|
||||
|
||||
# creates a component definition and instance from a mesh
|
||||
def mesh_to_native_component(mesh, entities)
|
||||
mesh_id = mesh["applicationId"] || mesh["id"]
|
||||
definition = component_definition_to_native([mesh], "def::#{mesh_id}")
|
||||
find_and_erase_existing_instance(definition, mesh_id)
|
||||
instance = entities.add_instance(definition, Geom::Transformation.new)
|
||||
instance.name = mesh_id
|
||||
instance.material = material_to_native(mesh["renderMaterial"])
|
||||
instance
|
||||
end
|
||||
|
||||
# finds or creates a component definition from the geometry and the given name
|
||||
def component_definition_to_native(geometry, name, application_id = "")
|
||||
definition = Sketchup.active_model.definitions[name]
|
||||
return definition if definition && (definition.name == name || definition.guid == application_id)
|
||||
|
||||
definition&.entities&.clear!
|
||||
definition ||= Sketchup.active_model.definitions.add(name)
|
||||
geometry.each { |obj| convert_to_native(obj, definition.entities) }
|
||||
puts("definition finished: #{name} (#{application_id})")
|
||||
puts(" entity count: #{definition.entities.count}")
|
||||
definition
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
# creates a component instance from a block
|
||||
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"]["geometry"],
|
||||
block["blockDefinition"]["name"],
|
||||
block["blockDefinition"]["applicationId"]
|
||||
)
|
||||
name = block["name"].nil? || block["name"].empty? ? block["id"] : block["name"]
|
||||
find_and_erase_existing_instance(definition, name, block["applicationId"])
|
||||
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.name = name
|
||||
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
|
||||
@@ -1,270 +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
|
||||
|
||||
# convert an edge to a speckle line
|
||||
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
|
||||
|
||||
# covnert a component definition to a speckle block definition
|
||||
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
|
||||
|
||||
# convert a component instane to a speckle block instance
|
||||
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 == "" ? nil : 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 = []
|
||||
lines = []
|
||||
|
||||
component_def.entities.each do |entity|
|
||||
nested_blocks.push(component_instance_to_speckle(entity)) if entity.typename == "ComponentInstance"
|
||||
next unless %w[Edge Face].include?(entity.typename)
|
||||
|
||||
if entity.typename == "Edge"
|
||||
lines.push(edge_to_speckle(entity))
|
||||
else
|
||||
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)
|
||||
|
||||
if face.loops.size > 1
|
||||
mesh = face.mesh
|
||||
mat_groups[mat_id]["@(31250)vertices"].push(*mesh_points_to_array(mesh))
|
||||
mat_groups[mat_id]["@(62500)faces"].push(*mesh_faces_to_array(mesh, mat_groups[mat_id][:pt_count] - 1))
|
||||
else
|
||||
mat_groups[mat_id]["@(31250)vertices"].push(*face_vertices_to_array(face))
|
||||
mat_groups[mat_id]["@(62500)faces"].push(*face_indices_to_array(face, mat_groups[mat_id][:pt_count]))
|
||||
end
|
||||
mat_groups[mat_id][:pt_count] += face.vertices.count
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
mat_groups.values.map { |group| group.delete(:pt_count) }
|
||||
mat_groups.values + lines + 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: 0,
|
||||
renderMaterial: face.material.nil? ? nil : material_to_speckle(face.material)
|
||||
}
|
||||
end
|
||||
|
||||
# get an array of face indices from a sketchup polygon mesh
|
||||
def mesh_faces_to_array(mesh, offset)
|
||||
faces = []
|
||||
puts(faces)
|
||||
mesh.polygons.each do |poly|
|
||||
faces.push(
|
||||
poly.count, *poly.map { |index| index.abs + offset }
|
||||
)
|
||||
end
|
||||
faces
|
||||
end
|
||||
|
||||
# get a flat array of vertices from a sketchup polygon mesh
|
||||
def mesh_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
|
||||
|
||||
# get a flat array of face indices from a sketchup face
|
||||
def face_indices_to_array(face, offset)
|
||||
face_array = [face.vertices.count]
|
||||
face_array.push(*face.vertices.count.times.map { |index| index + offset })
|
||||
face_array
|
||||
end
|
||||
|
||||
# get a flat array of vertices from a list of sketchup vertices
|
||||
def face_vertices_to_array(face)
|
||||
pts_array = []
|
||||
face.vertices.each do |v|
|
||||
pt = v.position
|
||||
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.loops.count > 1 ? face.mesh : nil
|
||||
{
|
||||
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" => mesh.nil? ? face_vertices_to_array(face) : mesh_points_to_array(mesh),
|
||||
"@(62500)faces" => mesh.nil? ? face_indices_to_array(face, 0) : mesh_faces_to_array(mesh, -1)
|
||||
}
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -1,204 +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
|
||||
@to_send = {}
|
||||
@connected = false
|
||||
|
||||
def self.queue_send(stream_id, converted)
|
||||
@to_send = { stream_id: stream_id, converted: converted }
|
||||
send_from_queue(stream_id) if @connected
|
||||
end
|
||||
|
||||
def self.send_from_queue(stream_id)
|
||||
return unless @to_send[:stream_id] == stream_id
|
||||
|
||||
@dialog.execute_script("convertedFromSketchup('#{@to_send[:stream_id]}',#{@to_send[:converted].to_json})")
|
||||
@dialog.execute_script("oneClickSend('#{@to_send[:stream_id]}')")
|
||||
@to_send = {}
|
||||
end
|
||||
|
||||
def self.init_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.create_dialog(show: true)
|
||||
if @dialog&.visible?
|
||||
@dialog.bring_to_front
|
||||
else
|
||||
@dialog ||= init_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
|
||||
@dialog.add_action_callback("load_saved_streams") do |_action_context|
|
||||
load_saved_streams
|
||||
end
|
||||
@dialog.add_action_callback("save_stream") do |_action_context, stream_id|
|
||||
save_stream(stream_id)
|
||||
end
|
||||
@dialog.add_action_callback("remove_stream") do |_action_context, stream_id|
|
||||
remove_stream(stream_id)
|
||||
end
|
||||
@dialog.add_action_callback("notify_connected") do |_action_context, stream_id|
|
||||
notify_connected(stream_id)
|
||||
end
|
||||
|
||||
# set connected to false when dialog is closed
|
||||
@dialog.set_can_close do
|
||||
@connected = false
|
||||
!@connected
|
||||
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 if show
|
||||
end
|
||||
@dialog
|
||||
end
|
||||
|
||||
def self.notify_connected(stream_id)
|
||||
@connected = true
|
||||
send_from_queue(stream_id)
|
||||
end
|
||||
|
||||
def self.convert_to_speckle(to_convert)
|
||||
converter = ConverterSketchup.new(UNITS[Sketchup.active_model.options["UnitsOptions"]["LengthUnit"]])
|
||||
to_convert.map { |entity| converter.convert_to_speckle(entity) }
|
||||
end
|
||||
|
||||
def self.send_selection(stream_id)
|
||||
converted = convert_to_speckle(Sketchup.active_model.selection)
|
||||
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.one_click_send
|
||||
acct = Accounts.default_account
|
||||
return puts("No local account found. Please refer to speckle.guide for more information.") if acct.nil?
|
||||
|
||||
create_dialog
|
||||
to_convert = Sketchup.active_model.selection.count > 0 ? Sketchup.active_model.selection : Sketchup.active_model.entities
|
||||
if first_saved_stream.nil?
|
||||
create_stream(to_convert)
|
||||
else
|
||||
queue_send(first_saved_stream, convert_to_speckle(to_convert))
|
||||
end
|
||||
rescue StandardError => e
|
||||
puts(e)
|
||||
@dialog.execute_script("sketchupOperationFailed('#{@to_send[:stream_id]}')")
|
||||
end
|
||||
|
||||
def self.first_saved_stream
|
||||
saved_streams = Sketchup.active_model.attribute_dictionary("speckle", true)["streams"] or []
|
||||
saved_streams.nil? || saved_streams.empty? ? nil : saved_streams[0]
|
||||
end
|
||||
|
||||
def self.load_saved_streams
|
||||
saved_streams = Sketchup.active_model.attribute_dictionary("speckle", true)["streams"] or []
|
||||
@dialog.execute_script("setSavedStreams(#{saved_streams})")
|
||||
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})")
|
||||
load_saved_streams
|
||||
end
|
||||
|
||||
def self.save_stream(stream_id)
|
||||
speckle_dict = Sketchup.active_model.attribute_dictionary("speckle", true)
|
||||
saved = speckle_dict["streams"] || []
|
||||
saved = saved.empty? ? [stream_id] : saved.unshift(stream_id)
|
||||
speckle_dict["streams"] = saved
|
||||
|
||||
load_saved_streams
|
||||
end
|
||||
|
||||
def self.remove_stream(stream_id)
|
||||
speckle_dict = Sketchup.active_model.attribute_dictionary("speckle", true)
|
||||
saved = speckle_dict["streams"] || []
|
||||
saved -= [stream_id]
|
||||
speckle_dict["streams"] = saved
|
||||
|
||||
load_saved_streams
|
||||
end
|
||||
|
||||
def self.create_stream(to_convert)
|
||||
acct = Accounts.default_account
|
||||
return if acct.nil?
|
||||
|
||||
path = Sketchup.active_model.path
|
||||
stream_name = path ? File.basename(path, ".*") : "Untitled SketchUp Model"
|
||||
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
|
||||
|
||||
request.start do |_req, res|
|
||||
res_data = JSON.parse(res.body)["data"]
|
||||
raise(StandardError) unless res_data
|
||||
|
||||
stream_id = res_data["streamCreate"]
|
||||
save_stream(stream_id)
|
||||
queue_send(stream_id, convert_to_speckle(to_convert))
|
||||
# send_selection(stream_id)
|
||||
end
|
||||
load_saved_streams
|
||||
rescue StandardError => e
|
||||
puts(e)
|
||||
puts("Could not create a new stream")
|
||||
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 |
@@ -1,37 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "sketchup"
|
||||
require "speckle_connector/dialog"
|
||||
require "speckle_connector/debug"
|
||||
|
||||
module SpeckleSystems
|
||||
module SpeckleConnector
|
||||
unless file_loaded?(__FILE__)
|
||||
cmd_cube = UI::Command.new("Dialog") { create_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)
|
||||
|
||||
cmd_send = UI::Command.new("Send") { one_click_send }
|
||||
cmd_send.tooltip = "Send to Speckle"
|
||||
cmd_send.status_bar_text = "Send to Speckle"
|
||||
cmd_send.small_icon = "icons/Sender.png"
|
||||
cmd_send.large_icon = "icons/Sender.png"
|
||||
|
||||
menu = UI.menu("Tools")
|
||||
menu.add_item(cmd_send)
|
||||
|
||||
toolbar = UI::Toolbar.new("Speckle")
|
||||
toolbar.add_item(cmd_cube)
|
||||
toolbar.add_item(cmd_send)
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SpeckleConnector
|
||||
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,247 @@
|
||||
# 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
|
||||
create_layers(obj.keys.filter_map { |key| key if key.start_with?('@') }, sketchup_model.layers)
|
||||
# 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 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,138 @@
|
||||
# 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/mesh'
|
||||
require_relative '../speckle_objects/other/block_instance'
|
||||
require_relative '../speckle_objects/other/block_definition'
|
||||
|
||||
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? }
|
||||
SpeckleObjects::Base.with_detached_layers(base_object_properties)
|
||||
end
|
||||
|
||||
# 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
module SpeckleConnector
|
||||
module Immutable
|
||||
# @private
|
||||
module Undefined
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../constants/platform_constants'
|
||||
|
||||
module SpeckleConnector
|
||||
extension = if OPERATING_SYSTEM == OS_WIN
|
||||
'so'
|
||||
else
|
||||
'bundle'
|
||||
end
|
||||
sqlite3_file = "sqlite3_#{RUBY_VERSION_NUMBER}.#{extension}"
|
||||
require_relative(File.join('sqlite3', sqlite3_file))
|
||||
end
|
||||