Compare commits

..

1 Commits

Author SHA1 Message Date
izzy lyseggen 635c6e6a5b chore(ui): test removing core j 2022-06-10 14:58:10 +01:00
195 changed files with 1156 additions and 16266 deletions
+26 -67
View File
@@ -21,7 +21,7 @@ jobs:
- persist_to_workspace:
root: ./
paths:
- speckle_connector/vue_ui
- speckle_connector/html
build-connector: # Reusable job for basic connectors
executor:
@@ -36,41 +36,19 @@ jobs:
- attach_workspace:
at: ./
- run:
name: Create Innosetup signing cert
name: Patch
shell: powershell.exe
command: |
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
- run:
name: Set Environment Variable
shell: powershell.exe
command: |
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "2.0.999" } else { $env:CIRCLE_TAG }
$semver = if($tag.Contains('/')) {$tag.Split("/")[0] } else { $tag }
command:
| # If no tag, use 0.0.0.1 and don't make any YML (for testing only!)
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.0" } else { $env:CIRCLE_TAG }
$semver = if($tag.Contains('/')) {$tag.Split("/")[1] } else { $tag }
$ver = if($semver.Contains('-')) {$semver.Split("-")[0] } else { $semver }
$version = "$($ver).$($env:WORKFLOW_NUM)"
python patch_version.py $semver
environment:
WORKFLOW_NUM: << pipeline.number >>
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\sketchup.iss /Sbyparam=$p
shell: cmd.exe #does not work in powershell
#- run:
# name: Patch
# shell: powershell.exe
# command:
# | # If no tag, use 0.0.0.1 and don't make any YML (for testing only!)
# $tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.0" } else { $env:CIRCLE_TAG }
# $semver = if($tag.Contains('/')) {$tag.Split("/")[1] } else { $tag }
# $ver = if($semver.Contains('-')) {$semver.Split("-")[0] } else { $semver }
# $channel = if($semver.Contains('-')) {$semver.Split("-")[1] } else { "latest" }
# $version = "$($ver).$($env:CIRCLE_BUILD_NUM)"
# New-Item -Force "speckle-sharp-ci-tools/Installers/sketchup/$channel.yml" -ItemType File -Value "version: $semver"
# echo $version
# python patch_version.py $semver
# speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\sketchup.iss
$channel = if($semver.Contains('-')) {$semver.Split("-")[1] } else { "latest" }
$version = "$($ver).$($env:CIRCLE_BUILD_NUM)"
New-Item -Force "speckle-sharp-ci-tools/Installers/sketchup/$channel.yml" -ItemType File -Value "version: $semver"
echo $version
python patch_version.py $version
speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\sketchup.iss
- persist_to_workspace:
root: ./
paths:
@@ -80,17 +58,9 @@ 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 git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
command: git clone https://$GITHUB_TOKEN@github.com/specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
- persist_to_workspace:
root: ./
paths:
@@ -99,29 +69,23 @@ jobs:
root: ./
paths:
- speckle-sharp-ci-tools
deploy-manager2:
deploy: # Uploads all installers found to S3
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
slug:
type: string
os:
type: string
extension:
type: string
- image: cimg/base:2021.01
steps:
- checkout
- attach_workspace:
at: ./
- run:
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 >>
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/
workflows:
build-and-deploy:
@@ -144,13 +108,8 @@ workflows:
filters:
tags:
only: /.*/
context: innosetup
- deploy-manager2:
context: do-spaces-speckle-releases
slug: sketchup
os: Win
extension: exe
- deploy:
requires:
- get-ci-tools
- build-ui
-38
View File
@@ -1,38 +0,0 @@
# 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
+1 -19
View File
@@ -10,23 +10,8 @@
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
@@ -39,9 +24,6 @@ speckle_connector/html
/test/version_tmp/
/tmp/
# IDE
.idea
# Used by dotenv library to load environment variables.
.env
-3
View File
@@ -1,3 +0,0 @@
[submodule "_sqlite3"]
path = _sqlite3
url = git@github.com:specklesystems/sketchup-sqlite3.git
+25 -108
View File
@@ -1,132 +1,49 @@
require:
- rubocop-sketchup
- rubocop-minitest
- rubocop-rake
AllCops:
TargetRubyVersion: 2.5
DisabledByDefault: false
NewCops: enable
DisplayCopNames: true
ExtraDetails: true
TargetRubyVersion: 2.7
EnabledByDefault: true
AutoCorrect: 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'
Layout:
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
Layout/IndentationStyle:
EnforcedStyle: spaces
IndentationWidth: 2
# If DisabledByDefault is set to true then we need to enable the SketchUp
# related departments:
SketchupDeprecations:
Style/StringLiteralsInInterpolation:
Enabled: true
EnforcedStyle: double_quotes
SketchupPerformance:
Enabled: true
Layout/LineLength:
Max: 120
SketchupRequirements:
Enabled: true
SketchupSuggestions:
Enabled: true
SketchupBugs:
Enabled: true
SketchupRequirements/FileStructure:
Lint/ConstantResolution:
Enabled: false
SketchupSuggestions/ModelEntities:
Style/Copyright:
Enabled: false
Style/DocumentationMethod:
Enabled: false
Metrics/AbcSize:
Max: 30
Enabled: false
Metrics/BlockLength:
# Exclude spec tests
Exclude:
- "**/*_spec.rb"
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
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
Style/AndOr:
Metrics/ParameterLists:
Enabled: false
Style/Documentation:
Exclude:
- "*tests/**/*_spec.rb"
- "*tests/**/*_test.rb"
Style/Not:
Metrics/CyclomaticComplexity:
Enabled: false
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:
Metrics/PerceivedComplexity:
Enabled: false
+11 -24
View File
@@ -1,29 +1,16 @@
# frozen_string_literal: true
source 'https://rubygems.org'
source "https://rubygems.org"
# gem "rake", "~> 13.0"
gem "rubocop", "~> 1.7"
group :development do
# 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'
gem "minitest"
gem "sketchup-api-stubs"
gem "solargraph"
end
gem "sqlite3", "~> 1.4"
+52 -112
View File
@@ -1,134 +1,74 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
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)
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)
ast (~> 2.4.1)
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)
racc (1.6.0)
rainbow (3.0.0)
regexp_parser (2.1.1)
reverse_markdown (2.0.0)
nokogiri
rexml (3.2.5)
rubocop (1.7.0)
rubocop (1.19.1)
parallel (~> 1.10)
parser (>= 2.7.1.5)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.2.0, < 2.0)
rubocop-ast (>= 1.9.1, < 2.0)
ruby-progressbar (~> 1.7)
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)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.11.0)
parser (>= 3.0.1.1)
ruby-progressbar (1.11.0)
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)
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)
PLATFORMS
x64-mingw32
x64-unknown
x86_64-linux
DEPENDENCIES
minitest
overcommit
pry
rake
rubocop
rubocop-minitest
rubocop-rake
rubocop-sketchup
rubycritic (~> 4.3, >= 4.3.3)
rubocop (~> 1.7)
sketchup-api-stubs
skippy (~> 0.4.1.a)
solargraph
sqlite3 (~> 1.4)
BUNDLED WITH
2.3.25
2.2.26
+30 -86
View File
@@ -41,42 +41,27 @@ Give Speckle a try in no time by:
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
# Repo structure
# Repo structure
This is the beginning of the Speckle SketchUp Connector. It is still in very early development and is not ready for general use.
This repo is split into three parts:
### 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`.
This repo is split into two parts: `speckle_connector` which is the Ruby SketchUp plugin and `ui` which is the Vue frontend.
### 2. **User Interface**
## Usage
Includes the `Vue` frontend lives in the `ui` folder.
> NOTE: this connector is still in early development and isn't ready for general use.
### 3. **SketchUp Sqlite3 extension** [submodule](https://github.com/specklesystems/sketchup-sqlite3)
Copy the whole `speckle_connector` folder to you SketchUp Plugins folder. You will likely find this at:
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.
C:\Users\{YOU}\AppData\Roaming\SketchUp\SketchUp 2021\SketchUp\Plugins
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`).
## Contribution Guide
You'll need to serve the ui before launching the connector:
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).
cd ui
npm install
npm run serve
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
@@ -104,63 +89,43 @@ 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 20XX\
C:\Program Files\SketchUp\SketchUp 2021\
You can now open up the repo in VS Code or you can use JetBrains' tools RubyMine and Webstorm.
You can now open up the repo in VS Code.
If you will use VS Code, make sure you've installed the Ruby extension for VS Code.
Make sure you've installed the Ruby extension for VS Code.
### Loading the Speckle Connector Plugin
### Loading the Plugin
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.
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:
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.
```ruby
$LOAD_PATH << 'C:\YOUR\PATH\TO\THE\sketchup_connector'
require 'speckle_connector.rb'
```
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.
Drop this Ruby file into your SketchUp Plugins directory. You will likely find this at:
### User Interface
C:\Users\{YOU}\AppData\Roaming\SketchUp\SketchUp 2021\SketchUp\Plugins
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.
To reload the plugin while SketchUp is running, open up the Ruby console and run the following:
To run the `ui`, create a `.env` based on `.env-example` and paste in your
Speckle token. Then:
SpeckleSystems::SpeckleConnector.reload
To run the `ui`, create a `.env` based on `.env-example` and paste in your Speckle token. Then:
cd ui
npm run serve
### Debugging
To run SketchUp in debug mode, you will run the task specified in `tasks.json`.
Before you do this, make sure your integrated shell for tasks is using powershell.
You can specify this by adding the following option to your workspace's `settings.json`
To run SketchUp in debug mode, you will run the task specified in `tasks.json`. Before you do this, make sure your integrated shell for tasks is using powershell. You can specify this by adding the following option to your workspace's `settings.json`
"terminal.integrated.automationShell.windows": "powershell.exe",
To start the task, use the keyboard shortcut `ctrl` + `shift` + `p` to open up
the Command Palette. Search for `Tasks: Run Task` and select it:
To start the task, use the keyboard shortcut `ctrl` + `shift` + `p` to open up the Command Palette. Search for `Tasks: Run Task` and select it:
![command palette](https://user-images.githubusercontent.com/7717434/135051668-35fee34e-5270-4b83-9c7b-dabb872370ee.png)
@@ -168,30 +133,9 @@ Then choose the `Debug Sketchup 2021` task to run it:
![debug sketchup task](https://user-images.githubusercontent.com/7717434/135051777-4c350a62-45fb-400e-9b24-4fbb02331b83.png)
Once Sketchup has launched, start the `Listen for rdebug-ide` debug configuration.
Once the debugger has connected, you'll be able to debug the connector normally.
Once Sketchup has launched, start the `Listen for rdebug-ide` debug configuration. Once the debugger has connected, you'll be able to debug the connector normally.
Make sure you run the `ui` before starting the SketchUp Connector
cd ui
npm run serve
### 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
```
npm run serve
-52
View File
@@ -1,52 +0,0 @@
# frozen_string_literal: true
require 'rake/testtask'
require 'rubocop/rake_task'
require 'rubycritic/rake_task'
module SpeckleConnector
# Custom utility functions for rake tasks
module RakeUtils
module_function
# Find ruby files that were changed from `main` to the latest revision
def changed_rb_files(previous_revision: 'main', latest_revision: '')
range = latest_revision.empty? ? previous_revision : "#{latest_revision}..#{previous_revision}"
command = "git diff #{range} --name-only"
changed_files = `#{command}`.split("\n")
# filter changed files with ruby files (.rb), Gemfile and Rakefile.
filtered_files = changed_files.grep(/.*\.rb$|Gemfile|Rakefile/)
filtered_files.select { |file| File.exist?(file) }
end
end
end
# Add default rubocop task
RuboCop::RakeTask.new(:default)
# Add task to only verify ruby files that are different than in the `main` branch
desc('Run rubocop on changed files')
RuboCop::RakeTask.new(:rubocop_changed) do |t|
t.patterns = FileList.new(SpeckleConnector::RakeUtils.changed_rb_files)
end
# Glob pattern to match source files. Defaults to FileList['.'].
ruby_critic_paths = FileList[
'speckle_connector/**/*.rb',
'speckle_connector.rb',
'tests/**/*.rb'] -
FileList[
'_tools/**/*.rb',
'speckle_connector/src/ext/**/*.rb',
]
# for local
RubyCritic::RakeTask.new('rubycritic') do |task|
task.paths = ruby_critic_paths
end
# for CI
RubyCritic::RakeTask.new('rubycritic-ci') do |task|
task.options = '--mode-ci --format console --no-browser --branch main'
task.paths = ruby_critic_paths
end
Submodule _sqlite3 deleted from 800dd5e366
-25
View File
@@ -1,25 +0,0 @@
# Tools
This folder stores the external tools and helper scripts to make easier life of the developer,
they are not the part of the main functionality of the Speckle.
Tools and scripts inside the folder will be loaded with `sketchup_connector_loader.rb` file.
In order to load your own `.rb` files please add this file names into list in the loader.
````ruby
...
files = %w[speckle_connector jf_RubyPanel su_attributes <put-your-file-here>]
# This line placed before loading started.
files.each do |ruby_file|
puts "Loading #{ruby_file}"
begin
require ruby_file
rescue LoadError
puts "Could not load #{ruby_file}"
end
end
````
Track load status of your tools and scripts on the ruby console when SketchUp UI initializing.
-29
View File
@@ -1,29 +0,0 @@
# frozen_string_literal: true
# #-------------------------------------------------------------------------------------------------
# *************************************************************************************************
# RubyPanel Toolbar (C) 2007 jim.foltz@gmail.com
#
# With special thanks to Chris Phillips (Sketchy Physics)
# for the Win32API code examples.
#
# 2011-01-05 <jim.foltz@gmail.com>
# * Changed Toolbar name from "Ruby COnsole" to "Ruby Toolbar" (TT)
# http://forums.sketchucation.com/viewtopic.php?f=323&t=1542&p=298587#p298587
# * Wrapped in addition module RubyToolbar
# * Use $suString.GetSting to get proper "Ruby Console" name string.
# * Better check if TB was previously visible
# * Use UI.start_timer to restore Toolbar
# ICONS: located in the subfolder "rubytoolbar"
# MODIFICATION: by Fredo6 for compliance with SU 2014 (and no dependency on Win32API) - 18 Sep 2013
# *************************************************************************************************
#-------------------------------------------------------------------------------------------------
require 'sketchup'
require 'extensions'
ext = SketchupExtension.new('Ruby Toolbar', 'jf_RubyPanel/rubytoolbar.rb')
ext.creator = 'Jim Foltz <jim.foltz@gmail.com>'
ext.description = 'Toolbar for manipulating the Ruby Console. Compatible with SketchUp 2014'
ext.version = '2014'
Sketchup.register_extension(ext, true)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

-89
View File
@@ -1,89 +0,0 @@
# frozen_string_literal: true
#-------------------------------------------------------------------------------------------------------------------------------------------------
# RubyPanel Toolbar (C) 2007 jim.foltz@gmail.com
# Permission to use, copy, modify, and distribute this software for # any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies.
# THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
# Description: Manage the loading of Ruby files and display of the Ruby console
# CREDITS: Special thanks to Chris Phillips (Sketchy Physics) for the Win32API code examples
# Revision: 3 Aug 2009, by Fredo6
# ICONS: located in the subfolder "rubytoolbar"
# MODIFICATION: by Fredo6 for compliance with SU 2014 (and no dependency on Win32API) - 18 Sep 2013
#-------------------------------------------------------------------------------------------------------------------------------------------------
require 'English'
require 'sketchup'
module JF_RubyToolbar
# Load the toolbar icons and commands, and do some initialization
def self.load_toolbar
@last_dir = "#{$LOAD_PATH[0]}/"
@last_dir = @last_dir.gsub('/', '\\\\\\\\')
@last_dir = File.join($JF_RUBYTOOLBAR, 'speckle_connector')
curdir = File.dirname __FILE__
# create toolbar
tb = UI::Toolbar.new 'Ruby Toolbar'
# Toggle console
cmd = UI::Command.new('Show/Hide') { SKETCHUP_CONSOLE.visible? ? SKETCHUP_CONSOLE.hide : SKETCHUP_CONSOLE.show }
cmd.large_icon = cmd.small_icon = File.join(curdir, 'rubypanel.png')
cmd.status_bar_text = cmd.tooltip = 'Show/Hide Ruby Console'
tb.add_item cmd
# Clear Console
cmd = UI::Command.new('Clear') { SKETCHUP_CONSOLE.clear }
cmd.status_bar_text = cmd.tooltip = 'Clear Console'
cmd.large_icon = cmd.small_icon = File.join(curdir, 'Delete24.png')
tb.add_item cmd
# Load a Ruby script
cmd = UI::Command.new('LoadScript') { load_script }
cmd.large_icon = cmd.small_icon = File.join(curdir, 'doc_ruby.png')
cmd.tooltip = cmd.status_bar_text = 'Load Script'
tb.add_item cmd
# Reload the last Ruby Script
@cmd_reload = UI::Command.new('Reload') { load_script @last_file }
@cmd_reload.large_icon = @cmd_reload.small_icon = File.join(curdir, 'reload.png')
@cmd_reload.status_bar_text = @cmd_reload.tooltip = 'Reload Script'
tb.add_item @cmd_reload
# Open the SU plugins directory panel
cmd = UI::Command.new('PluginsDir') { UI.openURL @last_dir }
cmd.tooltip = cmd.status_bar_text = 'Browse Plugins Folder'
cmd.large_icon = cmd.small_icon = File.join(curdir, 'open_folder.png')
tb.add_item cmd
# showing the toolbar
tb.get_last_state == -1 ? tb.show : tb.restore
end
# Load a script file - if <file> is nil, open the dialog panel to select the file
def self.load_script(file = nil)
file ||= UI.openpanel 'Load Script', @last_dir, '*.rb*'
return unless file
begin
load file
Sketchup.set_status_text "#{File.basename(file)} loaded (#{Time.now.strftime('%H:%M:%S')})"
@last_file = file
@last_dir = "#{File.dirname(file)}/"
@last_dir = @last_dir.gsub('/', '\\\\\\\\')
@cmd_reload.status_bar_text = @cmd_reload.tooltip = "Reload Script: #{File.basename(file)}"
rescue StandardError
UI.messagebox("Couldn't load #{File.basename(file)}: #{$ERROR_INFO}")
end
end
# Loading the toolbar once
unless file_loaded?('RubyToolbar.rb')
load_toolbar
file_loaded('RubyToolbar.rb')
end
end
-44
View File
@@ -1,44 +0,0 @@
# frozen_string_literal: true
# The purpose of this file is customizing environment of the developer on SketchUp.
# Each developer can customize it's own loader(this file), by this way developer can load their helper tools
# and helper methods ONLY in dev mode.
# Change the base folder and copy this file to Sketchup Plugins directory
# If you need to test in several versions of SketchUp, create symlinks to this file
# ( AppData\Roaming\SketchUp\SketchUp <version>\SketchUp\Plugins )
# Create a link to Plugins folder with this command
# rubocop:disable Layout/LineLength
# New-Item -ItemType SymbolicLink -Path '~\AppData\Roaming\SketchUp\SketchUp 2022\SketchUp\Plugins\speckle_connector_loader.rb' -Target ~\Git\Speckle\speckle-sketchup\_tools\speckle_connector_loader.rb
# rubocop:enable Layout/LineLength
SKETCHUP_CONSOLE.show # if you want to show Ruby console on startup
# base location of your repos - will be merged with specific repos in next step
home_folder = File.expand_path('~')
# If you use some other location for your repository, you can change it here
# but make sure it is not committed as it will change thi setting for all
# users that use the default setup. Eg:
# Add Speckle folder - uncomment the one you need
speckle_path = File.join(home_folder, 'Git', 'Speckle', 'speckle-sketchup')
$LOAD_PATH << speckle_path
$LOAD_PATH << File.join(speckle_path, '_tools')
# Defining this path will help to tool to browse related source file directly when
# developer attempted to reload/load file.
# rubocop:disable Style/GlobalVars
$JF_RUBYTOOLBAR = speckle_path
# rubocop:enable Style/GlobalVars
files = %w[speckle_connector jf_RubyPanel su_attributes]
files.each do |ruby_file|
puts "Loading #{ruby_file}"
begin
require ruby_file
rescue LoadError
puts "Could not load #{ruby_file}"
end
end
-49
View File
@@ -1,49 +0,0 @@
# Copyright 2014-2021, Trimble Inc.
#
# License: The MIT License (MIT)
#
# A SketchUp Ruby Extension that surfaces attributes attached to components.
# More info at https://github.com/SketchUp/sketchup-attribute-helper
require 'sketchup.rb'
require 'extensions.rb'
#-------------------------------------------------------------------------------
module Trimble
module AttributeHelper
### CONSTANTS ### ------------------------------------------------------------
# Plugin information
PLUGIN_ID = 'AttributeHelper'.freeze
PLUGIN_NAME = 'SketchUp Attribute Helper'.freeze
PLUGIN_VERSION = '1.0.3'.freeze
# Resource paths
FILENAMESPACE = File.basename(__FILE__, '.*')
PATH_ROOT = File.dirname(__FILE__).freeze
PATH = File.join(PATH_ROOT, FILENAMESPACE).freeze
### EXTENSION ### ------------------------------------------------------------
unless file_loaded?(__FILE__)
loader = File.join( PATH, 'core.rb' )
ex = SketchupExtension.new(PLUGIN_NAME, loader)
ex.description = 'Visually inspect nested attributes in SketchUp.'
ex.version = PLUGIN_VERSION
ex.copyright = 'Trimble Inc © 2015-2021'
ex.creator = 'SketchUp'
Sketchup.register_extension(ex, true)
end
end # module AttributeHelper
end # module Trimble
#-------------------------------------------------------------------------------
file_loaded(__FILE__)
#-------------------------------------------------------------------------------
-285
View File
@@ -1,285 +0,0 @@
# Copyright 2014-2021, Trimble Inc.
#
# License: The MIT License (MIT)
require "sketchup.rb"
require "stringio"
module Trimble
module AttributeHelper
PLUGIN = self
class << self
attr_reader :app_observer
attr_reader :model_observer
attr_reader :selection_observer
end
def self.visualize_selected
content = self.traverse_selected
html = self.wrap_content(content)
options = {
:dialog_title => "Attribute Visualizer",
:preferences_key => 'AttributeVisualizer',
:scrollable => true,
:resizable => true,
:height => 300,
:width => 400,
:left => 200,
:top => 200
}
@window ||= UI::WebDialog.new(options)
@window.set_html(html)
@window.set_on_close {
@window = nil
self.detach_observers
}
unless @window.visible?
@window.show
self.attach_observers
end
end
def self.attach_observers
@app_observer ||= AppObserver.new
@model_observer ||= ModelObserver.new
@selection_observer ||= SelectionObserver.new
model = Sketchup.active_model
Sketchup.remove_observer(@app_observer)
model.remove_observer(@model_observer)
model.selection.remove_observer(@selection_observer)
Sketchup.add_observer(@app_observer)
model.add_observer(@model_observer)
model.selection.add_observer(@selection_observer)
end
def self.detach_observers
Sketchup.remove_observer(@app_observer)
Sketchup.active_model.remove_observer(@model_observer)
Sketchup.active_model.selection.remove_observer(@selection_observer)
end
def self.traverse_selected
html = StringIO.new
model = Sketchup.active_model
selection = model.selection
if selection.empty?
if model.active_path.nil?
entity = model
else
entity = model.active_path.last
end
else
return "Invalid selection size" unless selection.size == 1
entity = selection[0]
end
html.puts "<h1>#{self.escape_html(entity)}</h1>"
if entity.respond_to?(:name)
html.puts "<h2>#{self.escape_html(entity.name)}</h2>"
end
if entity.attribute_dictionaries
entity.attribute_dictionaries.each { |dictionary|
html.puts self.format_dictionary(dictionary)
}
else
html.puts "No dictionaries"
end
if entity.is_a?(Sketchup::Group)
definition = entity.entities.parent
elsif entity.is_a?(Sketchup::ComponentInstance)
definition = entity.definition
else
definition = nil
end
if definition && definition.attribute_dictionaries
html.puts "<h1>#{self.escape_html(definition)}</h1>"
html.puts "<h2>#{self.escape_html(definition.name)}</h2>"
definition.attribute_dictionaries.each { |dictionary|
html.puts self.format_dictionary(dictionary)
}
end
html.string
end
def self.format_dictionary(dictionary, path = "")
html_name = self.escape_html(dictionary.name)
path = "#{path}:#{html_name}"
html = StringIO.new
html.puts "<table>"
html.puts "<caption title='#{path}'>#{html_name}</caption>"
html.puts "<tbody>"
dictionary.each { |key, value|
html_key = self.escape_html(key)
html_value = self.escape_html(value)
node_path = "#{path}:#{html_key}"
html.puts "<tr title='#{node_path}'><td>#{html_key}</td><td>#{html_value}</td><td class='value_type'>#{value.class}</td></tr>"
}
if dictionary.attribute_dictionaries
dictionary.attribute_dictionaries.each { |sub_dic|
html.puts "<tr><td colspan='3' class='dictionary'>"
html.puts self.format_dictionary(sub_dic, path)
html.puts "</td></tr>"
}
end
html.puts "</tbody>"
html.puts "</table>"
html.string
end
def self.escape_html(data)
data.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
end
def self.wrap_content(content)
html = <<-EOT
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta charset="UTF-8">
<style>
html {
font-family: "Calibri", sans-serif;
font-size: 10pt;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
table {
width: 100%;
/*padding: 0.5em;*/
border: 1px solid #666;
}
caption {
font-weight: bold;
text-align: left;
/*border-bottom: 1px solid silver;*/
padding: 0.2em;
}
td {
background: #f3f3f3;
padding: 0.2em;
}
td.dictionary {
background: none;
padding-left: 1em;
}
tr:hover td {
background: rgba(255,210,180,0.2);
}
.value_type {
text-align: right;
width: 5%;
}
</style>
<head>
<body>
#{content}
</body>
</html>
EOT
end
class SelectionObserver < Sketchup::SelectionObserver
def onSelectionAdded(selection, element)
selection_changed()
end
def onSelectionBulkChange(selection)
selection_changed()
end
def onSelectionCleared(selection)
selection_changed()
end
def onSelectionRemoved(selection, element)
selection_changed()
end
private
def selection_changed
PLUGIN.visualize_selected
end
end # class SelectionObserver
class ModelObserver < Sketchup::ModelObserver
def onActivePathChanged(model)
PLUGIN.visualize_selected
end
def onTransactionCommit(model)
model_changed(model)
end
def onTransactionEmpty(model)
model_changed(model)
end
def onTransactionRedo(model)
model_changed(model)
end
def onTransactionUndo(model)
model_changed(model)
end
private
def model_changed(model)
if @timer.nil?
@timer = UI.start_timer(0.0, false) {
@timer = nil
PLUGIN.visualize_selected
}
end
end
end # class ModelObserver
class AppObserver < Sketchup::AppObserver
def onNewModel(model)
observe_model(model)
end
def onOpenModel(model)
observe_model(model)
end
private
def observe_model(model)
model.add_observer(PLUGIN.model_observer)
model.selection.add_observer(PLUGIN.selection_observer)
PLUGIN.visualize_selected
end
end # class AppObserver
unless file_loaded?(__FILE__)
command = UI::Command.new("Attribute Helper") { self.visualize_selected }
command.status_bar_text = "Inspect and edit the attributes of a selection."
menu_name = Sketchup.version.to_f < 21.1 ? 'Plugins' : 'Developer'
menu = UI.menu(menu_name)
menu.add_item(command)
file_loaded(__FILE__)
end
end # module AttributeHelper
end # module Sketchup
-2
View File
@@ -1,2 +0,0 @@
ID=f4d9d053-4479-4a9a-90da-b79fa16e28c4
VERSION_ID=b787af5e-8e8e-4932-92ef-a3c99681795d
Binary file not shown.
+2 -3
View File
@@ -25,8 +25,7 @@ def patch_installer(tag):
with open(iss_file, "r") as file:
lines = file.readlines()
lines.insert(11, f'#define AppVersion "{tag.split("-")[0]}"\n')
lines.insert(12, f'#define AppInfoVersion "{tag}"\n')
lines.insert(11, f'#define AppVersion "{tag}"\n')
with open(iss_file, "w") as file:
file.writelines(lines)
@@ -40,7 +39,7 @@ def main():
return
tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
print(f"Patching version: {tag}")
+33 -27
View File
@@ -1,39 +1,45 @@
# frozen_string_literal: true
require "sketchup"
require 'sketchup'
require 'extensions'
require "extensions"
# Speckle connector module to enable multiplayer mode ON!
module SpeckleConnector
# Version - patched by CI
CONNECTOR_VERSION = '0.0.0'
module SpeckleSystems
module SpeckleConnector
# Version - patched by CI
CONNECTOR_VERSION = "0.0.0"
file = __FILE__.dup
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, ".*")
# Account for Ruby encoding bug under Windows.
file.force_encoding('UTF-8') if file.respond_to?(:force_encoding)
# Path to the root .rb file (this file).
PATH_ROOT = File.dirname(file).freeze
# Support folder should be named the same as the root .rb file.
folder_name = File.basename(file, '.*')
# Path to the support folder.
PATH = File.join(PATH_ROOT, folder_name).freeze
# Path to the root .rb file (this file).
PATH_ROOT = File.dirname(file).freeze
# 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 support folder.
PATH = File.join(PATH_ROOT, folder_name).freeze
# Run from localhost or from build files
DEV_MODE = false
puts("Loading Speckle Connector v#{CONNECTOR_VERSION} from #{DEV_MODE ? 'dev' : 'build'}")
unless file_loaded?(__FILE__)
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 = SketchupExtension.new("Speckle SketchUp", File.join(PATH, "main"))
file_loaded(__FILE__)
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
end
end
+57
View File
@@ -0,0 +1,57 @@
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
-36
View File
@@ -1,36 +0,0 @@
# 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
@@ -0,0 +1,32 @@
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
+21
View File
@@ -0,0 +1,21 @@
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
+243
View File
@@ -0,0 +1,243 @@
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
+270
View File
@@ -0,0 +1,270 @@
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
+3 -8
View File
@@ -1,22 +1,17 @@
# frozen_string_literal: true
# Speckle connector module to enable multiplayer mode ON!
module SpeckleConnector
module SpeckleSystems::SpeckleConnector
# from thomthom
# https://github.com/thomthom/true-bend/blob/master/src/tt_truebend/debug.rb
# @note Debug method to reload the plugin.
#
# @example
# SpeckleConnector.reload
# SpeckleSystems::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
+204
View File
@@ -0,0 +1,204 @@
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

+37
View File
@@ -0,0 +1,37 @@
# 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
@@ -1,31 +0,0 @@
# frozen_string_literal: true
require 'JSON'
require_relative '../ext/sqlite3'
require_relative '../constants/path_constants'
module SpeckleConnector
# Accounts to communicate with models on user's account.
module Accounts
def self.load_accounts
db_path = SPECKLE_ACCOUNTS_DB_PATH
unless File.exist?(db_path)
raise(
IOError,
"No Accounts db found. Please read the guide for different options for adding your account:\n
https://speckle.guide/user/manager.html#adding-accounts"
)
end
db = Sqlite3::Database.new(db_path)
rows = db.exec('SELECT * FROM objects')
db.close
rows.map { |row| JSON.parse(row[1]) }
end
def self.default_account
accounts = load_accounts
accounts.select { |acc| acc['isDefault'] }[0] || accounts[0]
end
end
end
-15
View File
@@ -1,15 +0,0 @@
# 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
@@ -1,33 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
require_relative 'deactivate_diffing'
module SpeckleConnector
module Actions
# Deactivate diffing for stream.
class ActivateDiffing < 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)
state = DeactivateDiffing.update_state(state, {})
puts "Diffing activated for #{@stream_id}"
speckle_entities = state.speckle_state.speckle_entities
invalid_speckle_entities = speckle_entities.select do |_id, entity|
entity.invalid_stream_ids.include?(@stream_id) && entity.sketchup_entity.is_a?(Sketchup::Face)
end
invalid_speckle_entities.each do |id, entity|
new_entity = entity.activate_diffing(@stream_id, state.sketchup_state.materials.by_id(MAT_EDIT))
speckle_entities = speckle_entities.put(id, new_entity)
end
new_speckle_state = state.speckle_state.with_speckle_entities(speckle_entities)
state.with_speckle_state(new_speckle_state)
end
end
end
end
@@ -1,52 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
module SpeckleConnector
module Actions
# Adds material to speckle state and Sketchup.
class AddMaterial < Action
def self.update_state(state, material_name:, color:, material_id:, alpha: nil)
materials = state.sketchup_state.materials
existing_material = materials.by_id(material_id)
return state if existing_material&.valid?
new_material = create_or_get_material(state.sketchup_state.sketchup_model,
material_name,
color,
material_id,
alpha: alpha)
new_materials = materials.add_material(material_id, new_material)
new_sketchup_state = state.sketchup_state.with(:@materials => new_materials)
state.with(:@sketchup_state => new_sketchup_state)
end
def self.create_or_get_material(model, material_name, color, material_id, alpha: nil)
materials = model.materials
existing_material = materials.find { |mat| mat.name == material_name }
return existing_material if existing_material&.valid?
existing_material = materials.add material_name
existing_material.set_attribute(MAT_DICTIONARY, MAT_ID, material_id.to_s)
set_hex_color(existing_material, color)
existing_material.alpha = alpha if alpha
existing_material
end
def self.set_hex_color(skp_material, hex_value)
hex_value = hex_value.to_s
col_blue, col_green, col_red = parse_hex_color(hex_value)
skp_material.color = col_red, col_green, col_blue
end
def self.parse_hex_color(hex_value)
split_values = hex_value.match(/^#([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})$/) ||
hex_value.match(/^#([a-fA-F\d])([a-fA-F\d])([a-fA-F\d])$/)
col_red = split_values[1].hex
col_green = split_values[2].hex
col_blue = split_values[3].hex
return col_blue, col_green, col_red
end
end
end
end
@@ -1,17 +0,0 @@
# 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
@@ -1,18 +0,0 @@
# 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
@@ -1,20 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
module SpeckleConnector
module Actions
# Action to collect versions from sketchup and connector to track user's version by mixpanel.
class CollectVersions < 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)
versions = {
sketchup: Sketchup.version.to_i,
speckle: SpeckleConnector::CONNECTOR_VERSION
}
state.with_add_queue('collectVersions', versions.to_json, [])
end
end
end
end
@@ -1,15 +0,0 @@
# 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
@@ -1,66 +0,0 @@
# 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
@@ -1,26 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
module SpeckleConnector
module Actions
# Deactivate diffing.
class DeactivateDiffing < 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 'Diffing deactivated!'
speckle_entities = state.speckle_state.speckle_entities
diffing_activated_speckle_entities = speckle_entities.reject do |_id, entity|
entity.active_diffing_stream_id.nil?
end
diffing_activated_speckle_entities.each do |id, entity|
new_entity = entity.deactivate_diffing
speckle_entities = speckle_entities.put(id, new_entity)
end
new_speckle_state = state.speckle_state.with_speckle_entities(speckle_entities)
state.with_speckle_state(new_speckle_state)
end
end
end
end
@@ -1,56 +0,0 @@
# frozen_string_literal: true
require_relative 'event_action'
require_relative '../load_sketchup_model'
require_relative '../collect_preferences'
module SpeckleConnector
module Actions
module Events
# Handle events that are triggered by the {AppObserver}.
class AppEventAction < EventAction
# Handle loading new or existing model
class OnNewOrChangedModel
# Handle events when the new or existing model is loaded in Sketchup
# @param state [States::State] the current state of speckle application
# @param event_data [Array<(Sketchup::Model)>] the event data for the given event. It consists of
# a double array with a single element that is the {Sketchup::Model} object of the loaded model.
def self.update_state(state, event_data)
return state unless event_data&.any?
model = event_data.flatten.first
# LoadSketchupModel action should be responsible to update all model specific data for state and then
# should notify the UI to update it's components.
new_state = Actions::LoadSketchupModel.update_state(state, model)
# Action to let UI to render itself with new preferences state
# TODO: Later UI should be updated if any stream is invalid after
# we collected speckle_entities appropriately
CollectPreferences.update_state(new_state, {})
end
end
# Run actions that are needed before the sketchup quits
class OnQuit
# Handle when Sketchup application closes
# @param state [States::State] the current state of speckle application
# @param _event_data [Array] the event data
# @return [States::State] the transformed state object
def self.update_state(state, _event_data)
state
end
end
# Handlers that are used to handle specific events
ACTIONS = {
onNewModel: OnNewOrChangedModel,
onOpenModel: OnNewOrChangedModel,
onQuit: OnQuit
}.freeze
def self.actions
ACTIONS
end
end
end
end
end
@@ -1,84 +0,0 @@
# frozen_string_literal: true
require_relative 'event_action'
require_relative '../../sketchup_model/utils/face_utils'
require_relative '../../constants/dict_constants'
module SpeckleConnector
module Actions
module Events
# Event actions related to entities.
class EntitiesEventAction < EventAction
# Event action when element added.
class OnElementAdded
# @param state [States::State] the current state of the SpeckleConnector Application
def self.update_state(state, event_data)
modified_entities = event_data.to_a.collect { |e| e[1] }
# do not copy speckle base object specific attributes, because they are entity specific
modified_entities.each { |entity| entity.delete_attribute(SPECKLE_BASE_OBJECT) }
state
end
end
# Event action when element modified.
class OnElementModified
# @param state [States::State] the current state of the SpeckleConnector Application
def self.update_state(state, event_data)
speckle_state = state.speckle_state
modified_entity = event_data[0][1]
if modified_entity.is_a?(Sketchup::Face)
path = state.sketchup_state.sketchup_model.active_path
modified_faces = SketchupModel::Utils::FaceUtils.near_faces(modified_entity.edges)
path_objects = path.nil? ? [] : path + path.collect(&:definition)
parent_ids = path_objects.collect(&:persistent_id)
ids_to_invalidate = modified_faces.collect(&:persistent_id) + parent_ids
entities_to_invalidate = speckle_entities_to_invalidate(speckle_state, ids_to_invalidate)
new_speckle_state = invalidate_speckle_entities(speckle_state, entities_to_invalidate)
# This is the place we can send information to UI for diffing check
diffing = state.user_state.preferences[:user][:diffing]
new_speckle_state = new_speckle_state.with_invalid_streams_queue if diffing
return state.with_speckle_state(new_speckle_state)
end
state
end
# @param speckle_state [States::SpeckleState] the current state of the Speckle
def self.speckle_entities_to_invalidate(speckle_state, ids)
speckle_state.speckle_entities.to_h.select { |id, _| ids.include?(id) }
end
# @param speckle_state [States::SpeckleState] the current state of the Speckle
def self.invalidate_speckle_entities(speckle_state, entities_to_invalidate)
speckle_entities = speckle_state.speckle_entities
entities_to_invalidate.each do |id, speckle_entity|
edited_speckle_entity = speckle_entity.with_invalid
speckle_entities = speckle_entities.put(id, edited_speckle_entity)
end
speckle_state.with_speckle_entities(speckle_entities)
end
end
# Event action when element removed.
class OnElementRemoved
# @param state [States::State] the current state of the SpeckleConnector Application
def self.update_state(state, _event_data)
# TODO: Do state updates when element removed
state
end
end
# Handlers that are used to handle specific events
ACTIONS = {
onElementRemoved: OnElementRemoved,
onElementAdded: OnElementAdded,
onElementModified: OnElementModified
}.freeze
def self.actions
ACTIONS
end
end
end
end
end
@@ -1,34 +0,0 @@
# frozen_string_literal: true
module SpeckleConnector
module Actions
# This module contains actions that are performed to handle events triggered by observers in Sketchup.
module Events
# Base action for Handling events
class EventAction
def self.actions
raise NoMethodError, 'Implement in a subclass'
end
# Handle the events that were collected by the observer. In case of the selection observer,
# we only need to handle the events once if any of the events actually happened.
# @param state [States::State] the current state of the SpeckleConnector Application
# @param events [Hash{Symbol=>Array}] the event data grouped by the event name
# @return [States::State] the transformed state
def self.update_state(state, events)
# Don't do anything if there are no events for this action
return state unless events
actions = self.actions
actions.each do |event_name, action|
next unless events.key?(event_name)
event_data = events[event_name]
state = action.update_state(state, event_data)
end
state
end
end
end
end
end
@@ -1,41 +0,0 @@
# frozen_string_literal: true
require_relative 'event_action'
require_relative '../load_sketchup_model'
module SpeckleConnector
module Actions
module Events
# Handle events that are triggered by the {ModelObserver}.
class ModelEventAction < EventAction
# Handle loading new or existing model
class OnActivePathChanged
# Handle events when the new or existing model is loaded in Sketchup
# @param state [States::State] the current state of speckle application
# @param event_data [Array<(Sketchup::Model)>] the event data for the given event. It consists of
# a double array with a single element that is the {Sketchup::Model} object of the loaded model.
def self.update_state(state, _event_data)
sketchup_state = state.sketchup_state
active_path = sketchup_state.sketchup_model.active_path
observers = state.speckle_state.observers
update_object_observers(active_path, observers)
return state
end
def self.update_object_observers(path, observers)
path[-1].definition.entities.add_observer(observers[ENTITIES_OBSERVER]) unless path.nil?
end
end
# Handlers that are used to handle specific events
ACTIONS = {
onActivePathChanged: OnActivePathChanged
}.freeze
def self.actions
ACTIONS
end
end
end
end
end
@@ -1,20 +0,0 @@
# 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
@@ -1,30 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
require_relative 'add_material'
require_relative '../constants/mat_constants'
module SpeckleConnector
module Actions
# Action to initialize materials
class InitializeMaterials < 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_state = recreate_material(state, DEFAULT_NAMES[MAT_ADD], DEFAULT_COLORS[MAT_ADD], MAT_ADD)
new_state = recreate_material(new_state, DEFAULT_NAMES[MAT_EDIT], DEFAULT_COLORS[MAT_EDIT], MAT_EDIT)
recreate_material(new_state, DEFAULT_NAMES[MAT_REMOVE], DEFAULT_COLORS[MAT_REMOVE], MAT_REMOVE)
end
def self.recreate_material(state, name, color, id, alpha: nil)
Actions::AddMaterial.update_state(
state,
material_name: name,
color: color,
material_id: id,
alpha: alpha
)
end
end
end
end
@@ -1,34 +0,0 @@
# 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'
require_relative '../constants/observer_constants'
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, observers)
attach_app_observer!(observers[APP_OBSERVER])
accounts = SpeckleConnector::Accounts.load_accounts
speckle_state = States::SpeckleState.new(accounts, observers, {}, {})
# This should be the only point that `Sketchup_active_model` passed to application state.
sketchup_state = States::SketchupState.new(Sketchup.active_model)
preferences = Preferences.read_preferences(sketchup_state.sketchup_model)
user_state_with_preferences = state.user_state.with_preferences(preferences)
state = States::State.new(user_state_with_preferences, speckle_state, sketchup_state, false)
Actions::LoadSketchupModel.update_state(state, sketchup_state.sketchup_model)
end
def self.attach_app_observer!(observer)
Sketchup.add_observer(observer)
end
end
end
end
@@ -1,18 +0,0 @@
# 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)['saved_streams']) or []
state.with_add_queue('setSavedStreams', saved_streams, [])
end
end
end
end
@@ -1,59 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
require_relative 'initialize_materials'
require_relative '../sketchup_model/reader/speckle_entities_reader'
require_relative '../preferences/preferences'
require_relative '../states/state'
require_relative '../states/sketchup_state'
require_relative '../constants/observer_constants'
module SpeckleConnector
module Actions
# Switch sketchup model wit a new one
class LoadSketchupModel < Action
# Replace current model state with the state of a new model. This action is triggered when user opens new or
# existing Sketchup model.
# @param state [States::State] the current state of Speckle
# @param additional_parameters [Array] parameters that the action takes
# @return [States::State] the new updated state object
def self.update_state(state, sketchup_model)
# Init sketchup state again with new model
new_sketchup_state = States::SketchupState.new(sketchup_model)
sketchup_model.rendering_options['DisplaySectionPlanes'] = true
new_state = state.with(:@sketchup_state => new_sketchup_state)
# Init materials again
new_state = InitializeMaterials.update_state(new_state)
# Read speckle entities
new_speckle_entities = SketchupModel::Reader::SpeckleEntitiesReader.read(sketchup_model.entities)
new_speckle_state = new_state.speckle_state.with_speckle_entities(Immutable::Hash.new(new_speckle_entities))
new_state = new_state.with_speckle_state(new_speckle_state)
# Read preferences from database and model.
preferences = Preferences.read_preferences(new_state.sketchup_state.sketchup_model)
new_user_state = new_state.user_state.with_preferences(preferences)
new_state = new_state.with(:@user_state => new_user_state)
attach_observers(sketchup_model, new_state.speckle_state.observers)
new_state
end
# Attach observers to the sketchup model
# @param sketchup_model [Sketchup::Model] the model to attach observers to
# @param observers [Hash{Class=>}] the observer objects indexed by their class that will be attached
def self.attach_observers(sketchup_model, observers)
# selection = sketchup_model.selection
# selection.add_observer(observers[SELECTION_OBSERVER_NAME])
# layers = sketchup_model.layers
# layers.add_observer(observers[LAYERS_OBSERVER_NAME])
entities = sketchup_model.entities
entities.add_observer(observers[ENTITIES_OBSERVER])
sketchup_model.add_observer(observers[MODEL_OBSERVER])
# materials = sketchup_model.materials
# materials.add_observer(observers[MATERIALS_OBSERVER_NAME])
# pages = sketchup_model.pages
# pages.add_observer(observers[PAGES_OBSERVER_NAME])
end
end
end
end
@@ -1,36 +0,0 @@
# 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
@@ -1,34 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
require_relative 'events/app_event_action'
require_relative 'events/entities_event_action'
require_relative 'events/model_event_action'
require_relative '../constants/observer_constants'
module SpeckleConnector
module Actions
# Handle events that were collected by observers
class OnEventsAction < Action
RUN_ORDER = {
APP_OBSERVER => Events::AppEventAction,
ENTITIES_OBSERVER => Events::EntitiesEventAction,
MODEL_OBSERVER => Events::ModelEventAction
# MATERIALS_OBSERVER => Events::MaterialsEventAction,
# LAYERS_OBSERVER => Events::LayerEventAction,
# PAGES_OBSERVER => Events::PagesEventAction,
# SELECTION_OBSERVER => Events::SelectionEventAction
}.freeze
def self.update_state(state, events)
RUN_ORDER.each do |observer_name, action|
next unless events.key?(observer_name)
parameters = events[observer_name]
state = action.update_state(state, parameters)
end
state
end
end
end
end
@@ -1,45 +0,0 @@
# 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
@@ -1,32 +0,0 @@
# 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
@@ -1,37 +0,0 @@
# 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
# rubocop:disable Metrics/ParameterLists
def initialize(stream_id, base, stream_name, branch_name, branch_id, source_app)
super()
@stream_id = stream_id
@base = base
@stream_name = stream_name
@branch_name = branch_name
@branch_id = branch_id
@source_app = source_app
end
# rubocop:enable Metrics/ParameterLists
# @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, @stream_id, @stream_name, @branch_name, @source_app)
# Have side effects on the sketchup model. It effects directly on the entities by adding new objects.
start_time = Time.now.to_f
state = converter.receive_commit_object(@base)
elapsed_time = (Time.now.to_f - start_time).round(3)
puts "==== Converting to Native executed in #{elapsed_time} sec ===="
puts "==== Source application is #{@source_app}. ===="
state.with_add_queue('finishedReceiveInSketchup', @stream_id, [])
end
end
end
end
@@ -1,22 +0,0 @@
# 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
@@ -1,29 +0,0 @@
# 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['saved_streams'] || []
saved -= [@stream_id]
speckle_dict['saved_streams'] = saved
state
end
end
end
end
@@ -1,27 +0,0 @@
# 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['saved_streams'] || []
saved = saved.empty? ? [@stream_id] : saved.unshift(@stream_id)
speckle_dict['saved_streams'] = saved
state
end
end
end
end
@@ -1,27 +0,0 @@
# 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_stream_id.nil?
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.with_empty_stream_queue
end
end
end
end
@@ -1,40 +0,0 @@
# frozen_string_literal: true
require_relative 'action'
require_relative 'deactivate_diffing'
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)
state = DeactivateDiffing.update_state(state, {})
converter = Converters::ToSpeckle.new(state, @stream_id)
new_speckle_state, base = converter.convert_selection_to_base(state.user_state.preferences)
id, total_children_count, batches, new_speckle_state = converter.serialize(base, new_speckle_state,
state.user_state.preferences)
puts("converted #{base.count} objects for stream #{@stream_id}")
# This is the place we can send information to UI for diffing check
diffing = state.user_state.preferences[:user][:diffing]
new_speckle_state = new_speckle_state.with_invalid_streams_queue if diffing
new_state = state.with_speckle_state(new_speckle_state)
new_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
@@ -1,66 +0,0 @@
# 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
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
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)
# This is the place we can send information to UI for diffing check. It is a technical depth!
if @preference == 'diffing'
new_speckle_state = if @value
state.speckle_state.with_invalid_streams_queue
else
state.speckle_state.with_empty_invalid_streams_queue
end
state = state.with_speckle_state(new_speckle_state)
end
state.with_user_state(new_user_state)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end
end
end
@@ -1,53 +0,0 @@
# 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
# @return [Observers::Handler] the observers indexed by their classes to handle
attr_reader :observer_handler
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 add_observer_handler!(observer_handler)
@observer_handler = observer_handler
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
@@ -1,39 +0,0 @@
# 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
@@ -1,24 +0,0 @@
# 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
@@ -1,17 +0,0 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../actions/activate_diffing'
module SpeckleConnector
module Commands
# Command to activate diffing for stream.
class ActivateDiffing < Command
def _run(data)
stream_id = data['stream_id']
action = Actions::ActivateDiffing.new(stream_id)
app.update_state!(action)
end
end
end
end
-42
View File
@@ -1,42 +0,0 @@
# 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.
with_observers_disabled do
_run(*parameters)
end
end
private
def with_observers_disabled(&block)
observer_handler = @app.observer_handler
if observer_handler
observer_handler.with_observers_disabled(&block)
else
block.call
end
end
def _run(*_parameters)
raise NotImplementedError, 'Implement in subclass'
end
end
end
end
@@ -1,15 +0,0 @@
# 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
@@ -1,51 +0,0 @@
# frozen_string_literal: true
require_relative 'command'
require_relative '../states/initial_state'
require_relative '../ui/vue_view'
require_relative '../actions/initialize_speckle'
require_relative '../observers/factory'
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.
observer_handler = Observers::Factory.create_handler(app)
app.add_observer_handler!(observer_handler)
observers = Observers::Factory.create_observers(observer_handler)
app.update_state!(Actions::InitializeSpeckle, observers)
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
@@ -1,51 +0,0 @@
# 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
@@ -1,18 +0,0 @@
# 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
@@ -1,18 +0,0 @@
# 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
@@ -1,22 +0,0 @@
# 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']
source_app = data['source_app']
action = Actions::ReceiveObjects.new(stream_id, base, stream_name, branch_name, branch_id, source_app)
app.update_state!(action)
end
end
end
end
@@ -1,19 +0,0 @@
# 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
@@ -1,18 +0,0 @@
# 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
@@ -1,17 +0,0 @@
# 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
@@ -1,58 +0,0 @@
# 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
@@ -1,19 +0,0 @@
# 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
@@ -1,13 +0,0 @@
# frozen_string_literal: true
module SpeckleConnector
SPECKLE_BASE_OBJECT = 'Speckle_Base_Object'
SPECKLE_ID = 'speckle_id'
SPECKLE_TYPE = 'speckle_type'
APPLICATION_ID = 'application_id'
TOTAL_CHILDREN_COUNT = 'total_children_count'
CHILDREN = 'children'
PARENT = 'parent'
VALID_STREAM_IDS = 'valid_stream_ids'
INVALID_STREAM_IDS = 'invalid_stream_ids'
end
@@ -1,22 +0,0 @@
# frozen_string_literal: true
module SpeckleConnector
MAT_DICTIONARY = 'Speckle_Connector_Materials'
MAT_ID = 'Speckle_Connector_Material_Id'
MAT_ADD = :speckle_connector_add_material
MAT_EDIT = :speckle_connector_edit_material
MAT_REMOVE = :speckle_connector_remove_material
DEFAULT_COLORS = {
MAT_ADD => '#66FF66',
MAT_EDIT => '#FFFF9F',
MAT_REMOVE => '#FF6666'
}.freeze
DEFAULT_NAMES = {
MAT_ADD => 'Speckle_Material_Add',
MAT_EDIT => 'Speckle_Material_Edit',
MAT_REMOVE => 'Speckle_Material_Remove'
}.freeze
end
@@ -1,7 +0,0 @@
# frozen_string_literal: true
module SpeckleConnector
APP_OBSERVER = 'SpeckleConnector::Observers::AppObserver'
ENTITIES_OBSERVER = 'SpeckleConnector::Observers::EntitiesObserver'
MODEL_OBSERVER = 'SpeckleConnector::Observers::ModelObserver'
end
@@ -1,24 +0,0 @@
# 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)
HOME_PATH = (ENV['HOME']).to_s
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
@@ -1,18 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module SpeckleConnector
host_os = RbConfig::CONFIG['host_os']
OS_WIN = :windows
OS_MAC = :macos
OPERATING_SYSTEM = case host_os
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
OS_WIN
when /darwin|mac os/
OS_MAC
else
raise "Unsupported OS: #{host_os.inspect}"
end
RUBY_VERSION_NUMBER = RUBY_VERSION.split('.')[0..1].join.to_i
end
# rubocop:enable Style/Documentation
@@ -1,25 +0,0 @@
# frozen_string_literal: true
require_relative '../speckle_objects/geometry/length'
module SpeckleConnector
COMBINE_FACES_BY_MATERIAL = :combine_faces_by_material
INCLUDE_ENTITY_ATTRIBUTES = :include_entity_attributes
INCLUDE_FACE_ENTITY_ATTRIBUTES = :include_face_entity_attributes
INCLUDE_EDGE_ENTITY_ATTRIBUTES = :include_edge_entity_attributes
INCLUDE_GROUP_ENTITY_ATTRIBUTES = :include_group_entity_attributes
INCLUDE_COMPONENT_ENTITY_ATTRIBUTES = :include_component_entity_attributes
MERGE_COPLANAR_FACES = :merge_coplanar_faces
LEVEL_SHIFT_VALUE = SpeckleObjects::Geometry.length_to_native(1.5, 'm')
DEFAULT_MODEL_PREFERENCES = {
COMBINE_FACES_BY_MATERIAL => true,
INCLUDE_ENTITY_ATTRIBUTES => true,
INCLUDE_FACE_ENTITY_ATTRIBUTES => true,
INCLUDE_EDGE_ENTITY_ATTRIBUTES => true,
INCLUDE_GROUP_ENTITY_ATTRIBUTES => true,
INCLUDE_COMPONENT_ENTITY_ATTRIBUTES => true,
MERGE_COPLANAR_FACES => true
}.freeze
end
@@ -1,19 +0,0 @@
# frozen_string_literal: true
module SpeckleConnector
BASE_OBJECT = 'Base'
OBJECTS_BUILTELEMENTS_VIEW3D = 'Objects.BuiltElements.View:Objects.BuiltElements.View3D'
OBJECTS_GEOMETRY_LINE = 'Objects.Geometry.Line'
OBJECTS_GEOMETRY_POLYLINE = 'Objects.Geometry.Polyline'
OBJECTS_GEOMETRY_MESH = 'Objects.Geometry.Mesh'
OBJECTS_GEOMETRY_BREP = 'Objects.Geometry.Brep'
OBJECTS_OTHER_BLOCKINSTANCE = 'Objects.Other.BlockInstance'
OBJECTS_OTHER_BLOCKINSTANCE_FULL = 'Objects.Other.Instance:Objects.Other.BlockInstance'
OBJECTS_OTHER_INSTANCE = 'Objects.Other.Instance:Objects.Other.Instance'
OBJECTS_OTHER_REVIT_REVITINSTANCE = 'Objects.Other.Revit.RevitInstance'
OBJECTS_OTHER_BLOCKDEFINITION = 'Objects.Other.BlockDefinition'
OBJECTS_OTHER_RENDERMATERIAL = 'Objects.Other.RenderMaterial'
end
@@ -1,364 +0,0 @@
# frozen_string_literal: true
# rubocop:disable SketchupPerformance/OpenSSL
require 'securerandom'
# rubocop:enable SketchupPerformance/OpenSSL
require 'digest'
require_relative 'converter'
require_relative '../speckle_entities/speckle_entity'
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
# @return [String] stream id to send conversion
attr_reader :stream_id
attr_accessor :speckle_state
# @param stream_id [String] stream id to send conversion
def initialize(speckle_state, stream_id, preferences, default_chunk_size = 1000)
@speckle_state = speckle_state
@stream_id = stream_id
@preferences = preferences
@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
id
end
def total_children_count(id)
@objects[id][:totalChildrenCount]
end
# @param base_and_entities [Object] base object to populate all children and their relationship
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def traverse_base(base_and_entities)
base, entities = base_and_entities
# 1. Create random string for lineage tracking.
@lineage.append(SecureRandom.hex)
# 2. Get last item from detach_lineage array
is_detached = @detach_lineage.pop
# unless entities.nil?
# is_sent_before = entities.all? do |entity|
# check_base_available_on_state(entity, speckle_state)
# end
# if is_sent_before
# speckle_entity = speckle_state.speckle_entities[entities.first.persistent_id]
# ref_object = detach_helper(speckle_entity.id)
# parent = @lineage[-1]
# unless @family_tree[parent].nil?
# @family_tree[parent] = @family_tree[parent].merge(speckle_entity.speckle_object[:__closure])
# end
# @objects[speckle_entity.id] = ref_object if is_detached
# return speckle_entity.id, ref_object
# end
# end
# 3. 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: '')
# 3.1 Remove applicationId if it is nil
traversed_base.delete(:applicationId)
# 4. Iterate all entries (key, value) of the base {Base > Hash} object
# speckle_state = traverse_base_props(base, traversed_base)
traverse_base_props(base, traversed_base)
# this is where all props are done for current `traversed_base`
# 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
if @preferences[:user][:register_speckle_entity] && !entities.nil?
entities.uniq.each do |entity|
speckle_entity = create_or_update_speckle_entity(entity, id, traversed_base)
@speckle_state = speckle_state.with_speckle_entity(speckle_entity)
end
end
return id, traversed_base
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize
# 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 dynamically detached or not
is_detach_prop = prop[0] == '@'
is_dynamically_detached = prop[0] == '@' && prop.length > 2 && prop[1] == '@'
prop = prop[2..-1] if is_dynamically_detached
# 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) && !base_and_entities?(value) && 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_detach_prop)
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 property
next
end
child = traverse_value(value, is_detach_prop)
is_base = (value.is_a?(Hash) && !value[:speckle_type].nil?) ||
(base_and_entities?(value) && value[0].is_a?(Hash) && !value[0][:speckle_type].nil?)
# 3.6. traverse value according to value is a speckle object or not
traversed_base[prop] = if is_base
is_detach_prop ? detach_helper(child[:id]) : child
else
child
end
end
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/BlockLength
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# Whether value has a pattern [<converted>, [<entity>, <entity>, ... <entity>]] or not.
def base_and_entities?(value)
is_array = value.is_a?(Array)
return false unless is_array
return false unless is_array && value.length == 2
value[1].all? { |v| v.is_a?(Sketchup::Entity) }
end
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Style/OptionalBooleanParameter
# rubocop:disable Metrics/AbcSize
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. For pure arrays (Without referencing any Sketchup Entity)
if value.is_a?(Array) && !base_and_entities?(value)
# 2.1. If it is not detached then iterate array by traversing with their value
unless is_detach
values = value.collect do |el|
el_value = traverse_value(el)
el_value
end
return values
end
# 2.2. If it is detached than collect them into detached_list
detached_list = []
value.each do |el|
if (el.is_a?(Hash) && !el[:speckle_type].nil?) || base_and_entities?(el)
@detach_lineage.append(is_detach)
id, _traversed_base = traverse_base(el)
detached_list.append(detach_helper(id))
else
el_value = traverse_value(el, is_detach)
detached_list.append(el_value)
end
end
return detached_list
end
# 3. Hash
return value if value.is_a?(Hash) && value[:speckle_type].nil?
# 4. Base objects
if (value.is_a?(Hash) && !value[:speckle_type].nil?) || base_and_entities?(value)
@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
# rubocop:enable Metrics/AbcSize
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
# @param entity [Sketchup::Entity] source entity object
# @param speckle_state [States::SpeckleState] the current speckle state of the {States::State}
def check_base_available_on_state(entity, speckle_state)
is_exist = speckle_state.speckle_entities.keys.include?(entity.persistent_id)
return is_exist unless is_exist
speckle_state.speckle_entities[entity.persistent_id].valid_stream_ids.include?(stream_id)
end
# Creates or updates speckle entity.
# If speckle entity exist in state, creates new one by updating old one.
# Else creates new one
# @return [SpeckleEntity] speckle entity that collects both speckle and sketchup information.
def create_or_update_speckle_entity(entity, id, traversed_base)
if speckle_state.speckle_entities.keys.include?(entity.persistent_id)
speckle_state.speckle_entities[entity.persistent_id].with_valid_stream_id(stream_id)
else
children = traversed_base[:__closure].nil? ? {} : traversed_base[:__closure]
speckle_entity = SpeckleEntities::SpeckleEntity.new(entity, id, entity.persistent_id,
traversed_base[:speckle_type],
children.keys, [stream_id])
speckle_entity.write_initial_base_data
speckle_entity
end
end
end
end
end
@@ -1,143 +0,0 @@
# 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) }
merged_faces(faces)
end
def self.merged_faces(faces)
faces.reject(&:deleted?)
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
@@ -1,36 +0,0 @@
# frozen_string_literal: true
module SpeckleConnector
module Converters
# Helper class to convert geometries between server and Sketchup.
class Converter
# @return [States::State] the current state of the {SpeckleConnector::App}
attr_reader :state
# @return [States::SpeckleState] the current speckle state of the {States::State}
attr_reader :speckle_state
# @return [Sketchup::Model] active sketchup model.
attr_reader :sketchup_model
# @return [String] stream id that conversion happening with it
attr_reader :stream_id
# @return [String] speckle units
attr_reader :units
attr_accessor :definitions
# @param state [States::State] the current state of the {SpeckleConnector::App}
def initialize(state, stream_id)
@state = state
@speckle_state = state.speckle_state
@sketchup_model = state.sketchup_state.sketchup_model
@stream_id = stream_id
su_unit = state.sketchup_state.length_units
@units = Converters::SKETCHUP_UNITS[su_unit]
@definitions = {}
end
end
end
end
@@ -1,385 +0,0 @@
# frozen_string_literal: true
require_relative 'converter'
require_relative '../constants/type_constants'
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/other/display_value'
require_relative '../speckle_objects/other/revit/revit_instance'
require_relative '../speckle_objects/geometry/point'
require_relative '../speckle_objects/geometry/line'
require_relative '../speckle_objects/geometry/mesh'
require_relative '../speckle_objects/built_elements/view3d'
module SpeckleConnector
module Converters
# Converts sketchup entities to speckle objects.
# rubocop:disable Metrics/ClassLength
class ToNative < Converter
# @return [States::SpeckleState] the current speckle state of the {States::State}
attr_accessor :speckle_state
# @return [String] source application of received object that will be converted to native
attr_reader :source_app
def initialize(state, stream_id, stream_name, branch_name, source_app)
super(state, stream_id)
@stream_name = stream_name
@branch_name = branch_name
@source_app = source_app.downcase
end
# 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
REVIT_INSTANCE = OTHER::Revit::RevitInstance
RENDER_MATERIAL = OTHER::RenderMaterial
DISPLAY_VALUE = OTHER::DisplayValue
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.Revit.RevitInstance
Objects.Other.BlockDefinition
Objects.Other.RenderMaterial
Objects.Other.Instance:Objects.Other.BlockInstance
].freeze
def from_revit
@from_revit ||= source_app.include?('revit')
end
def from_sketchup
@from_sketchup ||= source_app.include?('sketchup')
end
# ReceiveObjects action call this method by giving everything that comes from server.
# Upcoming object is a referencedObject of selected commit to receive.
# UI is responsible currently to fetch objects from ObjectLoader module by calling getAndConstruct method.
# @param obj [Object] speckle commit object.
def receive_commit_object(obj)
# First create layers on the sketchup before starting traversing
# @Named Views are exception here. It does not mean a layer. But it is anti-pattern for now.
filtered_layer_containers = obj.keys.filter_map { |key| key if key.start_with?('@') && key != '@Named Views' }
create_layers(filtered_layer_containers, sketchup_model.layers) unless from_revit
# Convert views to sketchup scenes
SpeckleObjects::BuiltElements::View3d.to_native(obj, sketchup_model)
# Get default commit layer from sketchup model which will be used as fallback
default_commit_layer = sketchup_model.layers.layers.find { |layer| layer.display_name == '@Untagged' }
@entities_to_fill = entities_to_fill(obj)
traverse_commit_object(obj, sketchup_model.layers, default_commit_layer, @entities_to_fill)
create_levels_from_section_planes
check_hiding_layers_needed
@state
end
def levels_layer
@levels_layer ||= sketchup_model.layers.add('Levels')
end
# Create levels from section planes that already created for this commit object.
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def create_levels_from_section_planes
return unless from_revit
section_planes = @entities_to_fill.grep(Sketchup::SectionPlane)
bbox = @entities_to_fill.parent.bounds
c_1 = bbox.corner(0)
c_2 = bbox.corner(1)
c_3 = bbox.corner(3)
c_4 = bbox.corner(2)
section_planes.each do |section_plane|
level_name = "#{@definition_name}-#{section_plane.name}"
definition = sketchup_model.definitions.add(level_name)
@entities_to_fill.add_instance(definition, Geom::Transformation.new)
elevation = section_plane.bounds.center.z
c1_e = Geom::Point3d.new(c_1.x, c_1.y, elevation - LEVEL_SHIFT_VALUE)
c2_e = Geom::Point3d.new(c_2.x, c_2.y, elevation - LEVEL_SHIFT_VALUE)
c3_e = Geom::Point3d.new(c_3.x, c_3.y, elevation - LEVEL_SHIFT_VALUE)
c4_e = Geom::Point3d.new(c_4.x, c_4.y, elevation - LEVEL_SHIFT_VALUE)
cline_1 = definition.entities.add_cline(c1_e, c2_e)
cline_2 = definition.entities.add_cline(c2_e, c3_e)
cline_3 = definition.entities.add_cline(c3_e, c4_e)
cline_4 = definition.entities.add_cline(c4_e, c1_e)
text = definition.entities.add_text(" #{section_plane.name}", c1_e)
[cline_1, cline_2, cline_3, cline_4, text, definition].each { |o| o.layer = levels_layer }
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
def entities_to_fill(_obj)
return sketchup_model.entities if from_sketchup
@definition_name = "#{@branch_name}-#{@stream_name}"
definition = sketchup_model.definitions.find { |d| d.name == @definition_name }
if definition.nil?
definition = sketchup_model.definitions.add(@definition_name)
sketchup_model.entities.add_instance(definition, Geom::Transformation.new)
end
definition.entities
end
LAYERS_WILL_BE_HIDDEN = [
'Rooms',
'Mass',
'Mass Floor',
'Grid',
'Shaft Openings'
].freeze
def check_hiding_layers_needed
return unless from_revit
sketchup_model.layers.each do |layer|
if LAYERS_WILL_BE_HIDDEN.any? { |layer_name| layer.display_name.include?(layer_name) }
layer.visible = false
sketchup_model.pages.each { |page| page.update(PAGE_USE_LAYER_VISIBILITY) }
end
end
end
# Conditions for converting speckle object to native sketchup entity:
# 1- `obj` is a hash
# 2- `obj` has a property as 'speckle_type'
# 3- `obj` is a convertable 'speckle_type' which sketchup supports
# @param obj [Object] candidate object to convert from speckle to sketchup.
# @return [Boolean] whether object is convertable or not.
def convertible_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
# 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, entities)
if convertible_to_native?(obj)
@state = convert_to_native(@state, obj, layer, entities)
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, entities)
end
else
# puts(">>> Found #{obj['speckle_type']}: #{obj['id']} with displayValue.")
@state = convert_to_native(@state, obj, layer, entities)
end
elsif obj.is_a?(Hash)
obj.each_value { |value| traverse_commit_object(value, commit_folder, layer, entities) }
elsif obj.is_a?(Array)
obj.each { |value| traverse_commit_object(value, commit_folder, layer, entities) }
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
def speckle_object_to_native(obj)
return DISPLAY_VALUE.method(:to_native) unless obj['displayValue'].nil?
SPECKLE_OBJECT_TO_NATIVE[obj['speckle_type']]
end
SPECKLE_OBJECT_TO_NATIVE = {
OBJECTS_GEOMETRY_LINE => LINE.method(:to_native),
OBJECTS_GEOMETRY_POLYLINE => LINE.method(:to_native),
OBJECTS_GEOMETRY_MESH => MESH.method(:to_native),
OBJECTS_GEOMETRY_BREP => MESH.method(:to_native),
OBJECTS_OTHER_BLOCKDEFINITION => BLOCK_DEFINITION.method(:to_native),
OBJECTS_OTHER_BLOCKINSTANCE => BLOCK_INSTANCE.method(:to_native),
OBJECTS_OTHER_BLOCKINSTANCE_FULL => BLOCK_INSTANCE.method(:to_native),
OBJECTS_OTHER_REVIT_REVITINSTANCE => REVIT_INSTANCE.method(:to_native),
OBJECTS_OTHER_RENDERMATERIAL => RENDER_MATERIAL.method(:to_native)
}.freeze
# @param state [States::State] state of the speckle application
def convert_to_native(state, obj, layer, entities = sketchup_model.entities)
# store this method as parameter to re-call it inner callstack
convert_to_native = method(:convert_to_native)
# Get 'to_native' method to convert upcoming speckle object to native sketchup entity
to_native_method = speckle_object_to_native(obj)
# Call 'to_native' method by passing this method itself to handle nested 'to_native' conversions.
# It returns updated state and converted entities.
state, converted_entities = to_native_method.call(state, obj, layer, entities, &convert_to_native)
if from_revit
# Create levels as section planes if they exists
create_levels(state, obj)
# Create layers from category of object and place object in it
create_layers_from_categories(state, obj, converted_entities)
end
# Create speckle entities from sketchup entities to achieve continuous traversal.
convert_to_speckle_entities(state, obj, converted_entities)
rescue StandardError => e
puts("Failed to convert #{obj['speckle_type']} (id: #{obj['id']})")
puts(e)
return state
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def create_layers_from_categories(state, speckle_object, entities)
return state if speckle_object['category'].nil?
layer = sketchup_model.layers.find { |l| l.display_name == speckle_object['category'] }
unless layer.nil?
entities.each { |entity| entity.layer = layer } if layer
return state
end
layer = sketchup_model.layers.add(speckle_object['category'])
unless layer.nil?
entities.each { |entity| entity.layer = layer } if layer
state
end
state
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# @param state [States::State] state of the speckle application
def create_levels(state, speckle_object)
return state if speckle_object['level'].nil?
return state unless speckle_object['level']['speckle_type'].include?('Objects.BuiltElements.Level')
level_name = speckle_object['level']['name'] || speckle_object['level']['id']
is_exist = @entities_to_fill.grep(Sketchup::SectionPlane).any? { |sp| sp.name == level_name }
return state if is_exist
elevation = SpeckleObjects::Geometry.length_to_native(speckle_object['level']['elevation'],
speckle_object['level']['units'])
section_plane = @entities_to_fill.add_section_plane([0, 0, elevation + LEVEL_SHIFT_VALUE], [0, 0, -1])
section_plane.name = level_name
state
end
# @param state [States::State] state of the application
def convert_to_speckle_entities(state, speckle_object, entities)
speckle_id = speckle_object['id']
application_id = speckle_object['applicationId']
speckle_type = speckle_object['speckle_type']
children = speckle_object['__closure'].nil? ? [] : speckle_object['__closure']
speckle_state = state.speckle_state
entities.each do |entity|
next if entity.is_a?(Sketchup::Material)
next if (entity.is_a?(Sketchup::Face) || entity.is_a?(Sketchup::Edge)) &&
!state.user_state.user_preferences[:register_speckle_entity]
ent = SpeckleEntities::SpeckleEntity.new(entity, speckle_id, application_id, speckle_type, children,
[stream_id])
ent.write_initial_base_data
speckle_state = speckle_state.with_speckle_entity(ent)
end
state.with_speckle_state(speckle_state)
end
end
# rubocop:enable Metrics/ClassLength
end
end

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