Compare commits
232 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d0117f701 | |||
| 08fe6f07cf | |||
| 37be889932 | |||
| ed124dd288 | |||
| 760d8a033d | |||
| 96cd122645 | |||
| 533768eb14 | |||
| c88d3c632d | |||
| 72f5185992 | |||
| b8db07a66b | |||
| 366b961039 | |||
| e244a7e2e5 | |||
| 9b55764b38 | |||
| 4b8012f94b | |||
| 9871000d84 | |||
| 17a056b3b9 | |||
| a8c5bd573f | |||
| 1d2fe047bf | |||
| 3465f86605 | |||
| 540e727b73 | |||
| 8c576f820c | |||
| df3b673064 | |||
| 69bde5539c | |||
| aa12b7b11b | |||
| e0b3b3cca2 | |||
| ea9a5741d7 | |||
| 1fe1c8d5a8 | |||
| ac58560a69 | |||
| d0b234bf3d | |||
| 981fe05c15 | |||
| 918550465b | |||
| 963d4f1738 | |||
| d809732280 | |||
| c4e38a11b7 | |||
| 7abe215bc2 | |||
| 0f269430bc | |||
| b8a298c54b | |||
| e1d33cd250 | |||
| 4563dec9bc | |||
| de284083fd | |||
| 85ee66ca4d | |||
| 5365dc858e | |||
| 69f9544638 | |||
| bf5409cf90 | |||
| bd4f668526 | |||
| d21df0c6fd | |||
| d808fdaa09 | |||
| b32d23b9d6 | |||
| d1b8ba95df | |||
| 5e6825bbdb | |||
| 3e5ba136be | |||
| 294b49a11c | |||
| 9e0cca447f | |||
| bc3f23d8dd | |||
| 4fcf11eaa8 | |||
| 1f45902837 | |||
| bc55543e23 | |||
| c1b8ec7036 | |||
| abe9de1b4a | |||
| 09cc83eb14 | |||
| 8f7b3fce0c | |||
| f121f1adfe | |||
| 522ace443a | |||
| 8f97f59a9a | |||
| 8e9d98cd55 | |||
| 746230caa0 | |||
| 03e80c6361 | |||
| bc547c13db | |||
| 173df6128e | |||
| 666d882ec6 | |||
| 6ea13c4edc | |||
| 39bdff1c02 | |||
| 45fc851722 | |||
| ede81f3a7d | |||
| bae9f30445 | |||
| 527cefa3ef | |||
| 830aee116e | |||
| 243fbea769 | |||
| 9c885258d9 | |||
| 0ad4e15f0a | |||
| a2ba6823a3 | |||
| 3713abd860 | |||
| 5ae78fb192 | |||
| 0b694833fb | |||
| dfd82e6d40 | |||
| 1694786177 | |||
| ec72cc1454 | |||
| 37ba4d9a80 | |||
| 526e497037 | |||
| a81a723ba8 | |||
| 5976898f07 | |||
| 09fec26841 | |||
| 165eb238b5 | |||
| de305a3508 | |||
| d124478a92 | |||
| d9acc5b39d | |||
| 5b91a04e1e | |||
| d145ce52f7 | |||
| ec598bf4a8 | |||
| 568e679e3f | |||
| 85cc49f4d9 | |||
| 65b07d44ab | |||
| f87bc98169 | |||
| 04270a8abc | |||
| 65fa47c9ac | |||
| 9367ea7e17 | |||
| b7ac9d1fee | |||
| f20b26045c | |||
| 4bcbc5131b | |||
| eeea2d7c8d | |||
| 68e9468294 | |||
| 694da73b77 | |||
| 15657bf0ec | |||
| 78c6898ff0 | |||
| 836e97a57c | |||
| 7f708108f2 | |||
| 2d3e282150 | |||
| 75b933ef0a | |||
| c19799f081 | |||
| 848ed922b5 | |||
| 16e96b69a4 | |||
| fbdd7ee7b6 | |||
| d3df5fceec | |||
| 948925d156 | |||
| 7ab6b1b167 | |||
| ca3a109bc8 | |||
| ebe9551e72 | |||
| d2836189c3 | |||
| 3703963ec8 | |||
| 02ca8e3a59 | |||
| 7c73658fa5 | |||
| e03b448618 | |||
| c8d635752b | |||
| df9a5600d9 | |||
| dd730ea517 | |||
| 5bbfe1be59 | |||
| dd04bd8792 | |||
| 20bc1f41f0 | |||
| 9e838734c0 | |||
| 17aeca5e70 | |||
| 98852d8c22 | |||
| 674d9cd37d | |||
| c35f9c22ae | |||
| e5f861c0dc | |||
| 7a31143ef4 | |||
| 9daa0e4059 | |||
| eaedeb4f8b | |||
| bd2f1d2777 | |||
| 52e43bac2d | |||
| daef503970 | |||
| 4d0ba3cd13 | |||
| bd111a2106 | |||
| 22daade5d2 | |||
| a39d72a59d | |||
| 3a2e523589 | |||
| 8bd27c6ad1 | |||
| 4c20cf67c8 | |||
| 337da3d523 | |||
| 3050803e68 | |||
| beb803fc63 | |||
| b056bcae2c | |||
| 6a2c297640 | |||
| 8b3127eda4 | |||
| b63d51ab1d | |||
| 3b2dfd27d6 | |||
| b8753195df | |||
| e76eb113a5 | |||
| d285e10565 | |||
| 66803f9036 | |||
| d7cff48374 | |||
| d66ed0566b | |||
| fde2b3bf0f | |||
| dcc8270cc6 | |||
| 23b6ca7552 | |||
| 437173e6a4 | |||
| 1274facbed | |||
| 180fc2ca59 | |||
| 187cf8f55e | |||
| dd8868b2a2 | |||
| 921e8ea1a8 | |||
| 54e8782e12 | |||
| 9e690dd304 | |||
| 373b42041b | |||
| 30ead47fb9 | |||
| 2dc7206f17 | |||
| ab7594bd23 | |||
| 710a22aea7 | |||
| 2c7c801efc | |||
| 41ebe058e7 | |||
| 288c71b3e0 | |||
| d7a8afcb31 | |||
| a1d120ee22 | |||
| b58d6d0d82 | |||
| 5ab65391ca | |||
| 2b40d58996 | |||
| 96129388ec | |||
| 6f6058a2d1 | |||
| 063ef91613 | |||
| 5233072099 | |||
| cd063b0e8a | |||
| 719d796e20 | |||
| 9767bf5261 | |||
| a9998bf12d | |||
| 2b7dc899e1 | |||
| cbc3f51d4c | |||
| ff1c8c1bfe | |||
| 6d4222bb1a | |||
| 7692a6f258 | |||
| ed8e6b00c6 | |||
| 85db9570dd | |||
| be174b9cce | |||
| f0099090b4 | |||
| b72bdfcbf0 | |||
| e1f5addddc | |||
| 87d3145317 | |||
| 84fc89cbbe | |||
| fbbddd8365 | |||
| dcfb787563 | |||
| 6822b2db9b | |||
| d89b3772de | |||
| e4a4eb0455 | |||
| 759efa7da3 | |||
| bf1c271784 | |||
| 9754893181 | |||
| 5b05182a1c | |||
| 20528f72df | |||
| 7de08d9f24 | |||
| d047f5e6d2 | |||
| 71071f817c | |||
| ad9af9bb3d | |||
| c7c864b8c0 | |||
| 133308141b |
@@ -1,276 +1,17 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
# Using windows for builds
|
||||
win: circleci/windows@2.4.0
|
||||
# Upload artifacts to s3
|
||||
aws-s3: circleci/aws-s3@2.0.0
|
||||
|
||||
# Define the jobs we want to run for this project
|
||||
jobs:
|
||||
build-ui:
|
||||
build:
|
||||
docker:
|
||||
- image: "circleci/node:16"
|
||||
- image: cimg/base:2023.03
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: "npm install"
|
||||
working_directory: "ui"
|
||||
- run:
|
||||
command: "npm run build"
|
||||
working_directory: "ui"
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle_connector/vue_ui
|
||||
|
||||
build-connector: # Reusable job for basic connectors
|
||||
executor:
|
||||
name: win/default # comes with python 3.7.3
|
||||
shell: cmd.exe
|
||||
parameters:
|
||||
slug:
|
||||
type: string
|
||||
default: ""
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Create Innosetup signing cert
|
||||
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 }
|
||||
$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
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers
|
||||
|
||||
build-connector-mac:
|
||||
macos:
|
||||
xcode: 12.5.1
|
||||
parameters:
|
||||
projname:
|
||||
type: string
|
||||
default: ""
|
||||
slug:
|
||||
type: string
|
||||
default: ""
|
||||
installer:
|
||||
type: boolean
|
||||
default: false
|
||||
converter-files:
|
||||
type: string
|
||||
default: ""
|
||||
installername:
|
||||
type: string
|
||||
default: ""
|
||||
build-config:
|
||||
type: string
|
||||
default: Release
|
||||
bundlename:
|
||||
type: string
|
||||
default: ""
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Install dotnet
|
||||
command: |
|
||||
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel Current
|
||||
|
||||
$HOME/.dotnet/dotnet --version
|
||||
$HOME/.dotnet/dotnet --list-runtimes
|
||||
$HOME/.dotnet/dotnet --list-sdks
|
||||
- run:
|
||||
name: Create installer target dir
|
||||
command: |
|
||||
mkdir -p speckle-sharp-ci-tools/Installers/<< parameters.slug >>
|
||||
- run:
|
||||
name: Set Environment Variable
|
||||
command: |
|
||||
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "2.0.999"; fi;)
|
||||
SEMVER=$(echo "$TAG" | sed -e 's/\/[a-zA-Z-]*//')
|
||||
VER=$(echo "$SEMVER" | sed -e 's/-.*//')
|
||||
VERSION=$(echo $VER.$WORKFLOW_NUM)
|
||||
python3 patch_version.py $SEMVER
|
||||
environment:
|
||||
WORKFLOW_NUM: << pipeline.number >>
|
||||
- run:
|
||||
name: Zip Connector files
|
||||
command: |
|
||||
zip -r << parameters.slug >>-mac.zip "./speckle_connector" "./speckle_connector.rb"
|
||||
# Copy installer files
|
||||
- run:
|
||||
name: Copy files to installer
|
||||
command: |
|
||||
mkdir -p speckle-sharp-ci-tools/Mac/<< parameters.installername >>/.installationFiles/
|
||||
cp << parameters.slug >>-mac.zip speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles
|
||||
# Create installer
|
||||
- run:
|
||||
name: Exit if External PR
|
||||
command: if [ "$CIRCLE_PR_REPONAME" ]; then circleci-agent step halt; fi
|
||||
- run:
|
||||
name: Build Mac installer
|
||||
command: ~/.dotnet/dotnet publish speckle-sharp-ci-tools/Mac/<<parameters.installername>>/<<parameters.installername>>.sln -r osx-x64 -c Release
|
||||
- run:
|
||||
name: Zip installer
|
||||
command: |
|
||||
cd speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/osx-x64/publish/
|
||||
zip -r <<parameters.slug>>.zip ./
|
||||
- store_artifacts:
|
||||
path: speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/osx-x64/publish/<<parameters.slug>>.zip
|
||||
- run:
|
||||
name: Copy to installer location
|
||||
command: |
|
||||
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "2.0.999"; fi;)
|
||||
SEMVER=$(echo "$TAG" | sed -e 's/\/[a-zA-Z-]*//')
|
||||
VER=$(echo "$SEMVER" | sed -e 's/-.*//')
|
||||
VERSION=$(echo $VER.$WORKFLOW_NUM)
|
||||
cp speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/osx-x64/publish/<<parameters.slug>>.zip speckle-sharp-ci-tools/Installers/<< parameters.slug >>/<<parameters.slug>>-$SEMVER.zip
|
||||
environment:
|
||||
WORKFLOW_NUM: << pipeline.number >>
|
||||
- when:
|
||||
condition: << pipeline.git.tag >>
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers
|
||||
|
||||
get-ci-tools: # Clones our ci tools and persists them to the workspace
|
||||
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
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools
|
||||
|
||||
deploy-manager2:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/dotnet/sdk:6.0
|
||||
parameters:
|
||||
slug:
|
||||
type: string
|
||||
os:
|
||||
type: string
|
||||
extension:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: 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 >>
|
||||
- run: echo "so long and thanks for all the fish"
|
||||
|
||||
# Orchestrate our job run sequence
|
||||
workflows:
|
||||
build-and-deploy:
|
||||
build_and_test:
|
||||
when:
|
||||
false
|
||||
jobs:
|
||||
- get-ci-tools:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- build-ui:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- build-connector:
|
||||
slug: sketchup
|
||||
requires:
|
||||
- get-ci-tools
|
||||
- build-ui
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
context: innosetup
|
||||
|
||||
- build-connector-mac:
|
||||
slug: sketchup
|
||||
requires:
|
||||
- get-ci-tools
|
||||
- build-ui
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
installername: SpeckleSketchUpInstall
|
||||
|
||||
- deploy-manager2:
|
||||
context: do-spaces-speckle-releases
|
||||
slug: sketchup
|
||||
os: Win
|
||||
extension: exe
|
||||
requires:
|
||||
- build-connector
|
||||
filters:
|
||||
tags:
|
||||
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
|
||||
branches:
|
||||
ignore: /.*/ # For testing only! /ci\/.*/
|
||||
- deploy-manager2:
|
||||
context: do-spaces-speckle-releases
|
||||
slug: sketchup
|
||||
os: OSX
|
||||
extension: zip
|
||||
requires:
|
||||
- build-connector-mac
|
||||
filters:
|
||||
tags:
|
||||
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
|
||||
branches:
|
||||
ignore: /.*/ # For testing only! /ci\/.*/
|
||||
- build
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_call:
|
||||
outputs:
|
||||
semver:
|
||||
description: "The full SemVer 2.0 version of this build, e.g. '3.0.0-alpha.1234' (note: no 'v'-prefix)"
|
||||
value: ${{ jobs.build.outputs.semver }}
|
||||
file_version:
|
||||
description: "The file info version, e.g. '3.0.0.1234'"
|
||||
value: ${{ jobs.build.outputs.file_version }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
semver: ${{ steps.set-version.outputs.semver }}
|
||||
file_version: ${{ steps.set-version.outputs.file-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
TAG="v3.0.99.${{ github.run_number }}"
|
||||
fi
|
||||
SEMVER="${TAG#v}"
|
||||
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
|
||||
|
||||
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
|
||||
echo "file-version=$FILE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo $SEMVER
|
||||
echo $FILE_VERSION
|
||||
|
||||
- name: Set connector version
|
||||
run: |
|
||||
python patch_version.py ${{steps.set-version.outputs.semver}}
|
||||
|
||||
- uses: montudor/action-zip@v1
|
||||
with:
|
||||
args: zip -q -r sketchup.zip vendor speckle_connector_3/ speckle_connector_3.rb
|
||||
|
||||
- name: ⬆️ Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: output-${{steps.set-version.outputs.semver}}
|
||||
path: sketchup.zip
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
compression-level: 0 # no compression
|
||||
@@ -1,78 +0,0 @@
|
||||
name: Update issue Status
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
update_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
echo "$PROJECT_ID"
|
||||
echo "$STATUS_FIELD_ID"
|
||||
|
||||
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo "$DONE_ID"
|
||||
|
||||
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Update Status
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
|
||||
set_status: updateProjectNextItemField(
|
||||
input: {
|
||||
projectId: $project
|
||||
itemId: $id
|
||||
fieldId: $status
|
||||
value: $value
|
||||
}
|
||||
) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||
|
||||
name: Build and deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "installer-test/**"]
|
||||
tags: ["v3.*.*"] # Manual delivery on every 3.x tag
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
|
||||
deploy-installers:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
|
||||
steps:
|
||||
- name: 🔫 Trigger Build Installer(s)
|
||||
uses: the-actions-org/workflow-dispatch@v4.0.0
|
||||
with:
|
||||
workflow: Build Installers
|
||||
repo: specklesystems/connector-installers
|
||||
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
|
||||
inputs: '{
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"semver": "${{ needs.build.outputs.semver }}",
|
||||
"file_version": "${{ needs.build.outputs.file_version }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
|
||||
}'
|
||||
ref: main
|
||||
wait-for-completion: true
|
||||
wait-for-completion-interval: 10s
|
||||
wait-for-completion-timeout: 10m
|
||||
display-workflow-run-url: true
|
||||
display-workflow-run-url-interval: 10s
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: output-*
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Move new issues into Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add Issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
@@ -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
|
||||
@@ -10,8 +10,8 @@
|
||||
settings.json
|
||||
|
||||
# vue app build dist folder
|
||||
speckle_connector/vue_ui
|
||||
speckle_connector/html
|
||||
speckle_connector_3/vue_ui
|
||||
speckle_connector_3/html
|
||||
|
||||
# speckle-sharp-ci-tools
|
||||
/speckle-sharp-ci-tools
|
||||
|
||||
@@ -18,7 +18,7 @@ AllCops:
|
||||
- '_tools/su_attributes/**/*.rb'
|
||||
- '_sqlite3/**/*.rb'
|
||||
- 'ui/**/*'
|
||||
- 'speckle_connector/src/ext/**/*.rb'
|
||||
- 'speckle_connector_3/src/ext/**/*.rb'
|
||||
- 'vendor/bundle/**/*'
|
||||
- 'tests/**/*.rb'
|
||||
SketchUp:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require_paths:
|
||||
- "C:/Program Files/SketchUp/SketchUp 2021/Tools"
|
||||
- speckle_connector
|
||||
- speckle_connector_3
|
||||
|
||||
require:
|
||||
- sketchup-api-stubs
|
||||
|
||||
@@ -24,6 +24,8 @@ group :development do
|
||||
gem 'rubycritic', '~> 4.3', '>= 4.3.3', require: false
|
||||
# Auto completions for SketchUp API.
|
||||
gem 'sketchup-api-stubs'
|
||||
# Runtime dependency of skippy for Ruby 3.2. Have it!
|
||||
gem 'sorted_set', '~> 1.0'
|
||||
# Aid with common SketchUp extension tasks.
|
||||
gem 'skippy', '~> 0.4.1.a'
|
||||
gem 'skippy', '~> 0.5.2.a'
|
||||
end
|
||||
|
||||
@@ -26,7 +26,7 @@ GEM
|
||||
path_expander (~> 1.0)
|
||||
ruby_parser (~> 3.1, > 3.1.0)
|
||||
sexp_processor (~> 4.8)
|
||||
git (1.18.0)
|
||||
git (1.19.1)
|
||||
addressable (~> 2.8)
|
||||
rchardet (~> 1.8)
|
||||
ice_nine (0.11.2)
|
||||
@@ -51,6 +51,7 @@ GEM
|
||||
public_suffix (5.0.1)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rbtree (0.4.6)
|
||||
rchardet (1.8.0)
|
||||
reek (6.1.1)
|
||||
kwalify (~> 0.7.0)
|
||||
@@ -89,6 +90,7 @@ GEM
|
||||
simplecov (>= 0.17.0)
|
||||
tty-which (~> 0.4.0)
|
||||
virtus (~> 1.0)
|
||||
set (1.1.0)
|
||||
sexp_processor (4.16.1)
|
||||
simplecov (0.21.2)
|
||||
docile (~> 1.1)
|
||||
@@ -97,11 +99,15 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
sketchup-api-stubs (0.7.8)
|
||||
skippy (0.4.3.a)
|
||||
skippy (0.5.2.a)
|
||||
git (~> 1.3)
|
||||
naturally (~> 2.1)
|
||||
thor (~> 0.19)
|
||||
thor (0.20.3)
|
||||
sorted_set (~> 1.0)
|
||||
thor (>= 0.19, < 2.0)
|
||||
sorted_set (1.0.3)
|
||||
rbtree
|
||||
set (~> 1.0)
|
||||
thor (1.3.1)
|
||||
thread_safe (0.3.6)
|
||||
tty-which (0.4.2)
|
||||
unicode-display_width (1.8.0)
|
||||
@@ -112,6 +118,7 @@ GEM
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
|
||||
PLATFORMS
|
||||
x64-mingw-ucrt
|
||||
x64-mingw32
|
||||
x64-unknown
|
||||
x86_64-linux
|
||||
@@ -127,7 +134,8 @@ DEPENDENCIES
|
||||
rubocop-sketchup
|
||||
rubycritic (~> 4.3, >= 4.3.3)
|
||||
sketchup-api-stubs
|
||||
skippy (~> 0.4.1.a)
|
||||
skippy (~> 0.5.2.a)
|
||||
sorted_set (~> 1.0)
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.25
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
workflow: GitFlow/v1
|
||||
next-version: 3.0.0
|
||||
mode: ManualDeployment
|
||||
branches:
|
||||
main:
|
||||
label: rc
|
||||
develop:
|
||||
regex: ^dui3/alpha$
|
||||
label: beta
|
||||
unknown:
|
||||
increment: None
|
||||
@@ -49,9 +49,9 @@ 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`
|
||||
a **.rb** file as entry and **folder** that .rb file refers to. In our case entry file is `speckle_connector_3.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`.
|
||||
will start to read extension. Source folder is `speckle_connector_3`.
|
||||
|
||||
### 2. **User Interface**
|
||||
|
||||
@@ -64,7 +64,7 @@ This repo is split into three parts:
|
||||
we use extensions as native part of the source `ruby` code.
|
||||
|
||||
After building `sqlite3.sln` file, compiled `sqlite3.so` (for Windows) and `sqlite3.bundle` (for OSX) dynamic library files are created
|
||||
by solution to place them into source code into `speckle_connector/src/ext`. Building this project should be only
|
||||
by solution to place them into source code into `speckle_connector_3/src/ext`. Building this project should be only
|
||||
happen when SketchUp starts to support newer Ruby versions (currently it is `2.7`).
|
||||
|
||||
## Contribution Guide
|
||||
@@ -115,9 +115,23 @@ You can now open up the repo in VS Code or you can use JetBrains' tools RubyMine
|
||||
|
||||
If you will use VS Code, make sure you've installed the Ruby extension for VS Code.
|
||||
|
||||
#### RubyMine
|
||||
|
||||
To debug:
|
||||
- Add configuration as **'Ruby remote debug'**
|
||||
- Remote host: localhost
|
||||
- Remote port: 7000
|
||||
- Remote root folder: <repo_path>
|
||||
- Local port: 26162
|
||||
- Local root folder: <repo_path>
|
||||
- Run below script
|
||||
|
||||
bundle exec skippy sketchup:debug 2024
|
||||
- When sketchup opened, click Debug button on RubyMine
|
||||
|
||||
### Loading the Speckle Connector Plugin
|
||||
|
||||
1. Find already prepared `speckle_connector_loader.rb` file on the `_tools`
|
||||
1. Find already prepared `speckle_connector_3_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`
|
||||
|
||||
@@ -32,12 +32,12 @@ end
|
||||
|
||||
# Glob pattern to match source files. Defaults to FileList['.'].
|
||||
ruby_critic_paths = FileList[
|
||||
'speckle_connector/**/*.rb',
|
||||
'speckle_connector.rb',
|
||||
'speckle_connector_3/**/*.rb',
|
||||
'speckle_connector_3.rb',
|
||||
'tests/**/*.rb'] -
|
||||
FileList[
|
||||
'_tools/**/*.rb',
|
||||
'speckle_connector/src/ext/**/*.rb',
|
||||
'speckle_connector_3/src/ext/**/*.rb',
|
||||
]
|
||||
|
||||
# for local
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# This is for automated pre-debugger configuration.
|
||||
# We run skippy first, then activate debugger.
|
||||
# The purpose of this file to wait till skp is live
|
||||
|
||||
# To establish a configuration
|
||||
# 1. Create 'Run External Tool' before lunch step
|
||||
# 2. Program -> C:\Ruby32-x64\bin\ruby.exe or whatever
|
||||
# 3. Arguments -> C:\Users\KORAL\Documents\Git\Speckle\speckle-sketchup\_tools\debugger\bundle_exec_2024.rb or whatever
|
||||
# 4. Working directory -> C:\Users\KORAL\Documents\Git\Speckle\speckle-sketchup or whatever
|
||||
|
||||
# Add a delay of 10 seconds, it is arbitrary, do not hesitate to change for what works best for you
|
||||
sleep(10)
|
||||
|
||||
# Execute the original command
|
||||
exec('bundle exec skippy sketchup:debug 2024')
|
||||
@@ -24,7 +24,7 @@ module JF_RubyToolbar
|
||||
def self.load_toolbar
|
||||
@last_dir = "#{$LOAD_PATH[0]}/"
|
||||
@last_dir = @last_dir.gsub('/', '\\\\\\\\')
|
||||
@last_dir = File.join($JF_RUBYTOOLBAR, 'speckle_connector')
|
||||
@last_dir = File.join($JF_RUBYTOOLBAR, 'speckle_connector_3')
|
||||
curdir = File.dirname __FILE__
|
||||
|
||||
# create toolbar
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# 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
|
||||
# New-Item -ItemType SymbolicLink -Path '~\AppData\Roaming\SketchUp\SketchUp 2022\SketchUp\Plugins\speckle_connector_3_loader.rb' -Target ~\Git\Speckle\speckle-sketchup\_tools\speckle_connector_3_loader.rb
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
SKETCHUP_CONSOLE.show # if you want to show Ruby console on startup
|
||||
@@ -32,7 +32,7 @@ $LOAD_PATH << File.join(speckle_path, '_tools')
|
||||
$JF_RUBYTOOLBAR = speckle_path
|
||||
# rubocop:enable Style/GlobalVars
|
||||
|
||||
files = %w[speckle_connector jf_RubyPanel su_attributes]
|
||||
files = %w[speckle_connector_3 jf_RubyPanel su_attributes]
|
||||
|
||||
files.each do |ruby_file|
|
||||
puts "Loading #{ruby_file}"
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
|
||||
def patch_connector(tag):
|
||||
"""Patches the connector version within the connector file"""
|
||||
rb_file = "speckle_connector.rb"
|
||||
rb_file = "speckle_connector_3.rb"
|
||||
|
||||
with open(rb_file, "r") as file:
|
||||
lines = file.readlines()
|
||||
@@ -15,6 +15,12 @@ def patch_connector(tag):
|
||||
print(f"Patched connector version number in {rb_file}")
|
||||
break
|
||||
|
||||
for (index, line) in enumerate(lines):
|
||||
if 'DEV_MODE = ' in line:
|
||||
lines[index] = f' DEV_MODE = false\n'
|
||||
print(f"Patched dev mode to false in {rb_file}")
|
||||
break
|
||||
|
||||
with open(rb_file, "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
@@ -45,7 +51,7 @@ def main():
|
||||
|
||||
print(f"Patching version: {tag}")
|
||||
patch_connector(tag)
|
||||
patch_installer(tag)
|
||||
# patch_installer(tag)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
@@ -1,44 +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
|
||||
# Load accounts from user's app data.
|
||||
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.get_account_by_id(id)
|
||||
accounts = load_accounts
|
||||
accounts.select { |acc| acc['id'] == id }[0]
|
||||
end
|
||||
|
||||
# Default account on the user computer.
|
||||
def self.default_account
|
||||
accounts = load_accounts
|
||||
accounts.select { |acc| acc['isDefault'] }[0] || accounts[0]
|
||||
end
|
||||
|
||||
# Try to get local server account for debug/test purposes.
|
||||
def self.try_get_local_server_account
|
||||
accounts = load_accounts
|
||||
accounts.select { |acc| acc['serverInfo']['url'].include?('localhost') }[0] || nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../cards/send_card'
|
||||
require_relative '../../cards/receive_card'
|
||||
require_relative '../../filters/send/everything_filter'
|
||||
require_relative '../../filters/send/selection_filter'
|
||||
require_relative '../../filters/send_filters'
|
||||
require_relative '../../sketchup_model/dictionary/model_card_dictionary_handler'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Action to add send card.
|
||||
class AddModel < 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, resolve_id, data)
|
||||
if data['typeDiscriminator'] == 'ReceiverModelCard'
|
||||
receive_card = Cards::ReceiveCard.new(data['id'], data['accountId'],
|
||||
data['projectId'], data['projectName'],
|
||||
data['modelId'], data['modelName'],
|
||||
data['referencedObject'])
|
||||
SketchupModel::Dictionary::ModelCardDictionaryHandler
|
||||
.save_card_to_model(receive_card, state.sketchup_state.sketchup_model)
|
||||
new_speckle_state = state.speckle_state.with_receive_card(receive_card)
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
js_script = "baseBinding.receiveResponse('#{resolve_id}')"
|
||||
return state.with_add_queue_js_command('addSendCard', js_script)
|
||||
end
|
||||
|
||||
send_filter = Filters::SendFilters.get_filter_from_ui_data(data['sendFilter'])
|
||||
# Init card and add to the state
|
||||
send_card = Cards::SendCard.new(data['id'], data['accountId'], data['projectId'], data['modelId'],
|
||||
send_filter, {})
|
||||
|
||||
SketchupModel::Dictionary::ModelCardDictionaryHandler
|
||||
.save_card_to_model(send_card, state.sketchup_state.sketchup_model)
|
||||
|
||||
new_speckle_state = state.speckle_state.with_send_card(send_card)
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
# Resolve promise
|
||||
js_script = "baseBinding.receiveResponse('#{resolve_id}')"
|
||||
state.with_add_queue_js_command('addSendCard', js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,30 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../cards/send_card'
|
||||
require_relative '../../filters/send_filters'
|
||||
require_relative '../../sketchup_model/dictionary/model_card_dictionary_handler'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Add model to document state.
|
||||
class AddModelToDocumentState < 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, resolve_id, model)
|
||||
puts model.to_json
|
||||
|
||||
send_filter = Filters::SendFilters.get_filter_from_ui_data(model['sendFilter'])
|
||||
|
||||
send_card = Cards::SendCard.new(model['id'], model['accountId'], model['projectId'], model['modelId'], send_filter, {})
|
||||
SketchupModel::Dictionary::ModelCardDictionaryHandler
|
||||
.save_card_to_model(send_card, state.sketchup_state.sketchup_model)
|
||||
new_speckle_state = state.speckle_state.with_send_card(send_card)
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
|
||||
js_script = "baseBinding.receiveResponse('#{resolve_id}')"
|
||||
state.with_add_queue_js_command('addModelToDocumentState', js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../filters/send_filters'
|
||||
require_relative '../../sketchup_model/dictionary/model_card_dictionary_handler'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Gets document state.
|
||||
class GetDocumentState < 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, resolve_id)
|
||||
send_cards_hash = SketchupModel::Dictionary::ModelCardDictionaryHandler
|
||||
.get_cards_from_dict(state.sketchup_state.sketchup_model)
|
||||
|
||||
send_cards = send_cards_hash.collect do |id, card|
|
||||
filter = Filters::SendFilters.get_filter_from_document(card['sendFilter'])
|
||||
send_card = Cards::SendCard.new(id, card['account_id'], card['project_id'], card['model_id'], filter, {})
|
||||
|
||||
new_speckle_state = state.speckle_state.with_send_card(send_card)
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
{
|
||||
id: send_card.id,
|
||||
accountId: send_card.account_id,
|
||||
projectId: send_card.project_id,
|
||||
modelId: send_card.model_id,
|
||||
sendFilter: send_card.send_filter,
|
||||
typeDiscriminator: send_card.type_discriminator
|
||||
}
|
||||
end
|
||||
|
||||
model_state = { models: send_cards }
|
||||
|
||||
js_script = "baseBinding.receiveResponse('#{resolve_id}', #{model_state.to_json})"
|
||||
state.with_add_queue_js_command('getDocumentState', js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../filters/send_filters'
|
||||
require_relative '../../sketchup_model/dictionary/model_card_dictionary_handler'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Gets model state.
|
||||
class GetModelState < 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, resolve_id)
|
||||
send_cards_hash = SketchupModel::Dictionary::ModelCardDictionaryHandler
|
||||
.get_cards_from_dict(state.sketchup_state.sketchup_model)
|
||||
|
||||
send_cards = send_cards_hash.collect do |id, card|
|
||||
filters = Filters::SendFilters.get_filters_from_model(card['filters'])
|
||||
send_card = Cards::SendCard.new(id, card['account_id'], card['project_id'], card['model_id'], filters)
|
||||
|
||||
new_speckle_state = state.speckle_state.with_send_card(send_card)
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
{
|
||||
accountId: send_card.account_id,
|
||||
projectId: send_card.project_id,
|
||||
modelId: send_card.model_id,
|
||||
filters: send_card.filters
|
||||
}
|
||||
end
|
||||
|
||||
model_state = { sendCards: send_cards }
|
||||
|
||||
js_script = "baseBinding.receiveResponse('#{resolve_id}', #{model_state.to_json})"
|
||||
state.with_add_queue_js_command('getModelState', js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../sketchup_model/query/entity'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Action to add send card.
|
||||
class HighlightModel < 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, resolve_id, model_card_id)
|
||||
# objects_to_highlight = if data['typeDiscriminator'] == 'ReceiverModelCard'
|
||||
# # model_card = state.speckle_state.receive_cards[model_card_id]
|
||||
# # TODO: return received objects
|
||||
# []
|
||||
# else
|
||||
# model_card = state.speckle_state.send_cards[model_card_id]
|
||||
# model_card.send_filter.selected_object_ids
|
||||
# end
|
||||
objects_to_highlight = state.speckle_state.send_cards[model_card_id].send_filter.selected_object_ids
|
||||
|
||||
state.sketchup_state.sketchup_model.selection.clear
|
||||
|
||||
# Flat entities to select entities on card
|
||||
flat_entities = SketchupModel::Query::Entity.flat_entities(state.sketchup_state.sketchup_model.entities)
|
||||
|
||||
flat_entities.each do |entity|
|
||||
next unless objects_to_highlight.include?(entity.persistent_id)
|
||||
|
||||
if entity.is_a?(Sketchup::ComponentDefinition)
|
||||
state.sketchup_state.sketchup_model.selection.add(entity.instances)
|
||||
end
|
||||
state.sketchup_state.sketchup_model.selection.add(entity)
|
||||
end
|
||||
|
||||
state.sketchup_state.sketchup_model.active_view.zoom(state.sketchup_state.sketchup_model.selection)
|
||||
|
||||
# Resolve promise
|
||||
js_script = "baseBinding.receiveResponse('#{resolve_id}')"
|
||||
state.with_add_queue_js_command('highlightModel', js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,89 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'event_action'
|
||||
require_relative '../../actions/send_actions/send_card_expiration_check'
|
||||
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]
|
||||
modified_entities = event_data.collect { |data| data[1] }
|
||||
new_speckle_state = state.speckle_state.with_changed_object_ids(modified_entities.collect(&:persistent_id))
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
state = Actions::SendCardExpirationCheck.update_state(state)
|
||||
# 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,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,26 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../convertors/to_native'
|
||||
require_relative '../../ext/TT_Lib2/progressbar'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Receive from server.
|
||||
class AfterGetObjects < 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, resolve_id, model_card_id, source_application, root_obj)
|
||||
model_card = state.speckle_state.receive_cards[model_card_id]
|
||||
converter = Converters::ToNative.new(state, model_card.model_id, model_card.project_name,
|
||||
model_card.model_name, source_application)
|
||||
# Have side effects on the sketchup model. It effects directly on the entities by adding new objects.
|
||||
state = converter.receive_commit_object(root_obj)
|
||||
|
||||
|
||||
resolve_js_script = "receiveBinding.receiveResponse('#{resolve_id}')"
|
||||
state.with_add_queue_js_command('receive', resolve_js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../ui_data/sketchup/selection_info'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Action to get selection.
|
||||
class GetSelection < 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)
|
||||
selected_object_ids = state.sketchup_state.sketchup_model.selection.collect(&:persistent_id)
|
||||
summary = "Selected #{selected_object_ids.length} objects."
|
||||
selection_info = UiData::Sketchup::SelectionInfo.new(selected_object_ids, summary)
|
||||
# js_script = "selectionBinding.receiveResponse('#{resolve_id}', #{selection_info.to_json})"
|
||||
js_script = "selectionBinding.emit('setSelection', #{selection_info.to_json})"
|
||||
state.with_add_queue_js_command('setSelection', js_script)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../action'
|
||||
require_relative '../../accounts/accounts'
|
||||
require_relative '../../convertors/units'
|
||||
require_relative '../../convertors/to_speckle'
|
||||
require_relative '../../operations/send'
|
||||
require_relative '../../ext/TT_Lib2/progressbar'
|
||||
require_relative '../../ext/worker'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
# Send to server.
|
||||
class Send < 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, resolve_id, model_card_id)
|
||||
model_card = state.speckle_state.send_cards[model_card_id]
|
||||
account = Accounts.get_account_by_id(model_card.account_id)
|
||||
converter = Converters::ToSpeckle.new(state, model_card_id, model_card.send_filter)
|
||||
|
||||
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)
|
||||
|
||||
# update_test(state)
|
||||
|
||||
puts("converted #{base.count} objects for stream #{@stream_id}")
|
||||
|
||||
state = state.with_speckle_state(new_speckle_state)
|
||||
|
||||
resolve_js_script = "sendBinding.receiveResponse('#{resolve_id}')"
|
||||
state = state.with_add_queue_js_command('send', resolve_js_script)
|
||||
args = {
|
||||
modelCardId: model_card_id,
|
||||
projectId: model_card.project_id,
|
||||
modelId: model_card.model_id,
|
||||
token: account['token'],
|
||||
serverUrl: account['serverInfo']['url'],
|
||||
accountId: model_card.account_id,
|
||||
message: model_card.message,
|
||||
sendObject: {
|
||||
id: id,
|
||||
totalChildrenCount: total_children_count,
|
||||
batches: batches
|
||||
}
|
||||
}
|
||||
js_script = "sendBinding.emit('sendViaBrowser', #{args.to_json})"
|
||||
state.with_add_queue_js_command('sendViaBrowser', js_script)
|
||||
end
|
||||
|
||||
def self.update_test(state)
|
||||
dialog = UI::HtmlDialog.new(
|
||||
{
|
||||
:dialog_title => 'Dialog Example',
|
||||
:preferences_key => 'com.sample.plugin',
|
||||
:scrollable => true,
|
||||
:resizable => true,
|
||||
:width => 600,
|
||||
:height => 400,
|
||||
:left => 10,
|
||||
:top => 10,
|
||||
:min_width => 50,
|
||||
:min_height => 50,
|
||||
:max_width =>1000,
|
||||
:max_height => 1000,
|
||||
:style => UI::HtmlDialog::STYLE_DIALOG
|
||||
})
|
||||
html = '<div id="hi"><b>Hello world!</b></div>'
|
||||
dialog.set_html(html)
|
||||
dialog.show
|
||||
|
||||
action = Proc.new do |status|
|
||||
js_command = "document.getElementById('hi').innerHTML = '<b>#{status}</b>'"
|
||||
log_js_command = "console.log('test')"
|
||||
dialog.execute_script(js_command)
|
||||
dialog.execute_script(log_js_command)
|
||||
end
|
||||
|
||||
selected_object_ids = state.sketchup_state.sketchup_model.selection.collect(&:persistent_id)
|
||||
state.worker.add_jobs(1000.times.to_a.map { |i| Job.new(i, &action) })
|
||||
state.worker.do_work(Time.now.to_f, &action)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../speckle_objects/other/color'
|
||||
|
||||
module SpeckleConnector
|
||||
module Cards
|
||||
# Card for sketchup connector to communicate speckle.
|
||||
class Card < Hash
|
||||
# @return [String] id of the card.
|
||||
attr_reader :id
|
||||
|
||||
# @return [String] account id of the card.
|
||||
attr_reader :account_id
|
||||
|
||||
# @return [String] project id of the card.
|
||||
attr_reader :project_id
|
||||
|
||||
# @return [String] model id of the card.
|
||||
attr_reader :model_id
|
||||
|
||||
# @return [Boolean] card is valid or not.
|
||||
attr_reader :valid
|
||||
|
||||
def initialize(card_id, account_id, project_id, model_id)
|
||||
super()
|
||||
@id = card_id
|
||||
@account_id = account_id
|
||||
@project_id = project_id
|
||||
@model_id = model_id
|
||||
@valid = true
|
||||
self[:id] = card_id
|
||||
self[:account_id] = account_id
|
||||
self[:project_id] = project_id
|
||||
self[:model_id] = model_id
|
||||
self[:valid] = @valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'card'
|
||||
|
||||
module SpeckleConnector
|
||||
module Cards
|
||||
# Receive card for sketchup connector to communicate speckle.
|
||||
class ReceiveCard < Card
|
||||
attr_reader :type_discriminator
|
||||
|
||||
# @return [String, NilClass] message to send
|
||||
attr_reader :message
|
||||
|
||||
# @return [String] object id to receive
|
||||
attr_reader :object_id
|
||||
|
||||
# @return [String] name of the project
|
||||
attr_reader :project_name
|
||||
|
||||
# @return [String] name of the model
|
||||
attr_reader :model_name
|
||||
|
||||
def initialize(card_id, account_id, project_id, project_name, model_id, model_name, object_id)
|
||||
super(card_id, account_id, project_id, model_id)
|
||||
@object_id = object_id
|
||||
self[:object_id] = object_id
|
||||
self[:model_name] = model_name
|
||||
self[:project_name] = project_name
|
||||
@model_name = model_name
|
||||
@project_name = project_name
|
||||
@type_discriminator = 'ReceiverModelCard'
|
||||
self[:type_discriminator] = @type_discriminator
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'card'
|
||||
|
||||
module SpeckleConnector
|
||||
module Cards
|
||||
# Send card for sketchup connector to communicate speckle.
|
||||
class SendCard < Card
|
||||
# @return [Filters::Send::EverythingFilter | Filters::Send::SelectionFilter | Filters::Send::LayerFilter] filter of the card.
|
||||
attr_reader :send_filter
|
||||
|
||||
# @return [Object] send settings of the card.
|
||||
attr_reader :send_settings
|
||||
|
||||
attr_reader :type_discriminator
|
||||
|
||||
# @return [String, NilClass] message to send
|
||||
attr_reader :message
|
||||
|
||||
def initialize(card_id, account_id, project_id, model_id, send_filter, send_settings)
|
||||
super(card_id, account_id, project_id, model_id)
|
||||
@send_filter = send_filter
|
||||
@send_settings = send_settings
|
||||
@type_discriminator = 'SenderModelCard'
|
||||
self[:sendFilter] = send_filter
|
||||
self[:sendSettings] = send_settings
|
||||
self[:type_discriminator] = @type_discriminator
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'card'
|
||||
|
||||
module SpeckleConnector
|
||||
module Cards
|
||||
# Send card for sketchup connector to communicate speckle.
|
||||
class SendCardMultipleFilters < Card
|
||||
# @return [Hash{String=>Filter}] filters of the card.
|
||||
attr_reader :filters
|
||||
|
||||
def initialize(card_id, account_id, project_id, model_id, filters)
|
||||
super(card_id, account_id, project_id, model_id)
|
||||
@filters = filters
|
||||
self[:filters] = filters
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SpeckleConnector
|
||||
APP_OBSERVER = 'SpeckleConnector::Observers::AppObserver'
|
||||
ENTITIES_OBSERVER = 'SpeckleConnector::Observers::EntitiesObserver'
|
||||
MODEL_OBSERVER = 'SpeckleConnector::Observers::ModelObserver'
|
||||
SELECTION_OBSERVER = 'SpeckleConnector::Observers::SelectionObserver'
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
module SpeckleConnector
|
||||
module TT
|
||||
module Lib
|
||||
|
||||
### CONSTANTS ### ------------------------------------------------------------
|
||||
|
||||
# Plugin information
|
||||
PLUGIN_ID = 'TT_Lib2'.freeze
|
||||
PLUGIN_NAME = 'TT_Lib²'.freeze
|
||||
PLUGIN_VERSION = '2.13.1'.freeze
|
||||
|
||||
end # module Lib
|
||||
end # module TT
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# CHANGELOG
|
||||
# 2.5.0 - 18.10.2010
|
||||
# * Upgraded JQuery to 1.4.4
|
||||
# * Now bundles JQuery minimized
|
||||
# * Added Win32Utils' Win32::API under TT::Win32::API
|
||||
# * Added module: TT::Bezier
|
||||
# * Added module: TT::Edge
|
||||
# * Added module: TT::Edges
|
||||
# * Added module: TT::Faces
|
||||
# * Added module: TT::Materials
|
||||
# * Added module: TT::Gizmo::Axis
|
||||
# * Added module: TT::Win32
|
||||
# * Added class: TT::Babelfish
|
||||
# * Added class: TT::Dimension
|
||||
# * Added class: TT::GUI::ToolWindow
|
||||
# * Added method: TT::debug
|
||||
# * Added method: TT::Point3d.douglas_peucker
|
||||
# * Added method: TT::Point3d.simplify_curve
|
||||
# * Added method: TT::Geom3d.interpolate_linear
|
||||
# * Added method: TT::Geom3d.average_point
|
||||
# * Added method: TT::GUI::Button.custom_properties
|
||||
# * Added method: TT::GUI::Control.add_event
|
||||
# * Added method: TT::GUI::Control.call_event
|
||||
# * Added method: TT::GUI::Control.positioned?
|
||||
# * Added method: TT::GUI::Control.properties
|
||||
# * Added method: TT::GUI::ContainerElement.add_controls_to_webdialog
|
||||
# * Added method: TT::GUI::ContainerElement.get_control_by_ui_id
|
||||
# * Added methods: TT::GUI::Listbox
|
||||
# * Added method: TT::GUI::Window.add_action_callback
|
||||
# * Added method: TT::GUI::Window.add_control_to_webdialog
|
||||
# * Added method: TT::GUI::Window.set_client_size
|
||||
# * Added method: TT::System.is_windows?
|
||||
# * Changed: TT::Gizmo::Manipulator
|
||||
# * Changed: TT::GUI::Inputbox now inherits from TT::GUI::ToolWindow
|
||||
# * Changed: TT::GUI::Inputbox.add_control accepts a +key+ argument a control id.
|
||||
# * Changed: TT::GUI::Inputbox.prompt returns a Hash instead of Array.
|
||||
# * Changed: TT::UV_Plane
|
||||
# * Fixed: TT::JSON.to_s - Now handles Symbols as values.
|
||||
# * Plus lots lots more - lots track of it all.
|
||||
#
|
||||
# 2.4.0 - 22.09.2010
|
||||
# * Added module: TT::Binary
|
||||
# * Added module: TT::GUI
|
||||
# * Added class: TT::GUI::Window
|
||||
# * Added class: TT::GUI::Inputbox
|
||||
# * Added class: TT::JSON
|
||||
# * Added module: TT::System
|
||||
# * Added module: TT::Locale
|
||||
# * Added module: TT::Cursor
|
||||
#
|
||||
# 2.3.0 - 08.09.2010
|
||||
# * Added method: TT::Geom3d.spiral_sphere
|
||||
# * Fixed: TT::Ray.test
|
||||
#
|
||||
# 2.2.0 - 06.09.2010
|
||||
# * Added module: TT::Bounds
|
||||
#
|
||||
# 2.1.1 - 04.09.2010
|
||||
# * Fixed bug: Ray.test (Workaround for SU8.0 bug)
|
||||
#
|
||||
# 2.1.0 - 03.09.2010
|
||||
# * Added module: TT::Selection
|
||||
# * Added module: TT::UVQ
|
||||
# * Added class: TT::UV_Plane
|
||||
# * Added class: TT::Gizmo::Manipulator
|
||||
# * Added method: TT::Entities.bounds
|
||||
# * Fixed: TT::Ray.test
|
||||
#
|
||||
# 2.0.0 - 01.09.2010
|
||||
# * Initial Release
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -1,12 +0,0 @@
|
||||
= TT_Lib2
|
||||
|
||||
* {SCF Thread}[http://forums.sketchucation.com/viewtopic.php?f=323&t=23307]
|
||||
|
||||
== Description
|
||||
|
||||
Library of common methods and classes for Google SketchUp Ruby plugins.
|
||||
|
||||
== Author
|
||||
|
||||
* Thomas Thomassen
|
||||
* thomas[at]thomthom[dot]net
|
||||
@@ -1,70 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of Arc methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT::Arc
|
||||
|
||||
# Checks if a given +Curve+ is an +ArcCurve+ and not a polygon.
|
||||
#
|
||||
# Check for polygon requires SketchUp 7.1M1 or newer. Older SketchUp
|
||||
# versions will not check for this property.
|
||||
#
|
||||
# @param [Sketchup::Curve] curve
|
||||
# @since 2.0.0
|
||||
def self.is?( curve )
|
||||
return false if curve.respond_to?( :is_polygon? ) && curve.is_polygon?
|
||||
curve.is_a?(Sketchup::ArcCurve)
|
||||
end
|
||||
|
||||
# Checks if a given +Curve+ makes an circle and is not a polygon.
|
||||
#
|
||||
# Check for polygon requires SketchUp 7.1M1 or newer. Older SketchUp
|
||||
# versions will not check for this property.
|
||||
#
|
||||
# SketchUp has a bug where an ArcCurve some times has 720 degrees angle
|
||||
# instead of 360. This method handles this.
|
||||
#
|
||||
# @param [Sketchup::Curve] curve
|
||||
# @since 2.0.0
|
||||
def self.circle?( curve )
|
||||
return false unless self.is?( curve )
|
||||
return ((curve.end_angle - curve.start_angle).radians >= 360) ? true : false
|
||||
end
|
||||
|
||||
# Based on Chris Fullmers "Exploded Arc Centerpoint Finder".
|
||||
# Calculates the centre points of an arc given two edges.
|
||||
#
|
||||
# @param [Sketchup::Edge] e1
|
||||
# @param [Sketchup::Edge] e2
|
||||
#
|
||||
# @return [Geom::Point3d, nil]
|
||||
# @since 2.0.0
|
||||
def self.exploded_center(e1, e2)
|
||||
# Two edges representing an arc must have the same length and can not
|
||||
# be parallel.
|
||||
return nil if e1.length != e2.length
|
||||
v1 = e1.line[1]
|
||||
v2 = e2.line[1]
|
||||
return nil if v1.parallel?( v2 )
|
||||
# Get mid-point of edges from where intersecting lines will origin.
|
||||
m1 = Geom.linear_combination( 0.5, e1.start.position, 0.5, e1.end.position )
|
||||
m2 = Geom.linear_combination( 0.5, e2.start.position, 0.5, e2.end.position )
|
||||
# Get the vectors for the intersecting lines
|
||||
z_axis = v1 * v2
|
||||
line1 = [m1, v1 * z_axis]
|
||||
line2 = [m2, v2 * z_axis]
|
||||
# Return the center
|
||||
Geom.intersect_line_line(line1, line2)
|
||||
end
|
||||
|
||||
end # module TT::Arc
|
||||
end
|
||||
@@ -1,63 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of AttributeDictionary methods.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module SpeckleConnector
|
||||
module TT::Attributes
|
||||
|
||||
# Compare two +AttributeDictionaries+ objects.
|
||||
#
|
||||
# @param [Sketchup::AttributeDictionaries] dictionaries1
|
||||
# @param [Sketchup::AttributeDictionaries] dictionaries2
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.dictionaries_equal?( dictionaries1, dictionaries2 )
|
||||
if dictionaries1.nil? || dictionaries2.nil?
|
||||
if dictionaries1.nil? && dictionaries2.nil?
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
for dictionary in dictionaries1
|
||||
return false unless d = dictionaries2[ dictionary.name ]
|
||||
return false unless self.dictionary_equal?( dictionary, d, false )
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
# Compare two +AttributeDictionary+ objects. By defaults their names must
|
||||
# match, but one can set +compare_name+ to +false+ to only compare their
|
||||
# content.
|
||||
#
|
||||
# @param [Sketchup::AttributeDictionary] dictionary1
|
||||
# @param [Sketchup::AttributeDictionary] dictionary2
|
||||
# @param [Boolean] compare_name
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.dictionary_equal?( dictionary1, dictionary2, compare_name = true )
|
||||
if compare_name
|
||||
return false unless dictionary1.name == dictionary2.name
|
||||
end
|
||||
return false unless dictionary1.length == dictionary2.length
|
||||
return false unless dictionary1.keys == dictionary2.keys
|
||||
for key in dictionary1.keys
|
||||
return false unless dictionary1[key] == dictionary2[key]
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
end # module TT::Attributes
|
||||
end
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Translation dictionary. Pass strings through the +translate+ or +tr+ method
|
||||
# to translate strings to the language given in to the last +load+ call.
|
||||
#
|
||||
# Silently eats errors and pass through the original string if a translation
|
||||
# cannot be made.
|
||||
#
|
||||
# module MyPlugin
|
||||
#
|
||||
# # Assigning the instance to a constant makes it into
|
||||
# # a easy to use shorthand that works in any sub-modules/classes.
|
||||
# S = TT::Babelfish.new( l10n_path )
|
||||
#
|
||||
# def self.init
|
||||
# S.load( 'fr' )
|
||||
# end
|
||||
#
|
||||
# def self.foo
|
||||
# puts S.tr( 'Hello World' )
|
||||
# end
|
||||
#
|
||||
# end # module
|
||||
#
|
||||
#
|
||||
# @since 2.5.0
|
||||
module SpeckleConnector
|
||||
class TT::Babelfish
|
||||
|
||||
# @since 2.5.0
|
||||
attr_reader( :l10n, :default_l10n, :path, :file_ex )
|
||||
|
||||
# @param [String] l10n_path The path where the translation files are located.
|
||||
# @param [String] default_l10n The source translation code.
|
||||
# @param [String] file_ex The file extension of the translation files.
|
||||
#
|
||||
# @since 2.5.0
|
||||
def initialize(l10n_path, default_l10n = 'en', file_ex = 'l10n')
|
||||
@file_ex = file_ex.dup
|
||||
@l10n = default_l10n.dup
|
||||
@default_l10n = default_l10n.dup
|
||||
@dictionary = {}
|
||||
@metadata = {} # (!) Default data
|
||||
|
||||
path = File.expand_path( l10n_path )
|
||||
unless File.exist?( path )
|
||||
raise( ArgumentError, 'Path does not exist.' )
|
||||
end
|
||||
@path = path
|
||||
end
|
||||
|
||||
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def inspect
|
||||
if @metadata
|
||||
name = @metadata['name']
|
||||
size = @dictionary.size
|
||||
"<#{self.class}:#{@l10n} - Speaks #{size} phrases in #{name}>"
|
||||
else
|
||||
"<#{self.class}:#{@l10n}>"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# @return [Hash] A copy of the current translation dictionary.
|
||||
# @since 2.5.0
|
||||
def dictionary
|
||||
@dictionary.dup
|
||||
end
|
||||
|
||||
|
||||
# @return [Hash] A copy of the current translation dictionary.
|
||||
# @since 2.5.0
|
||||
def metadata
|
||||
@metadata.dup
|
||||
end
|
||||
|
||||
|
||||
# Loads a translation dictionary.
|
||||
#
|
||||
# @param [String] l10n must represent the base name of a .l10n file in the
|
||||
# language folder.
|
||||
#
|
||||
# @return [Boolean] +true+ on success, +false+ on failure.
|
||||
# @since 2.5.0
|
||||
def load(l10n)
|
||||
# Special case for default language.
|
||||
if l10n == @default_l10n
|
||||
@l10n = @default_l10n
|
||||
@metadata.clear # (!) Default data
|
||||
@dictionary.clear
|
||||
return true
|
||||
end
|
||||
# Work out the filename and ensure it exists.
|
||||
filename = File.join(@path, "#{l10n}.#{@file_ex}")
|
||||
unless File.exists?(filename)
|
||||
puts "Babelfish - Failed to load l10n: #{l10n} (#{filename}). No such file."
|
||||
return false
|
||||
end
|
||||
# Open the language file and parse the content. Unicode Regex ensures that
|
||||
# UTF-8 files are parsed properly.
|
||||
#
|
||||
# Using a proxy hash prevents a failed appempt of loading a translation
|
||||
# file from ruining the existing translation hash.
|
||||
#
|
||||
# Garbage data - data that doesn't match the expected format is ignored.
|
||||
dictionary = {}
|
||||
metadata = {}
|
||||
File.open(filename, 'r') { |file|
|
||||
metadata = read_metadata(file)
|
||||
unless metadata
|
||||
puts "Babelfish - Failed to load l10n: #{l10n} (#{filename}). Invalid data."
|
||||
return false
|
||||
end
|
||||
file.each { |line|
|
||||
next if line.match(/^\s*#/u) # Ignore comments
|
||||
next if line.match(/^\s*$/u) # Ignore empty lines
|
||||
if match = line.match(/^\s*"(.*)"\s*=\s*"(.*)"\s*$/u)
|
||||
dictionary[ match[1] ] = match[2]
|
||||
end
|
||||
}
|
||||
}
|
||||
@l10n = l10n.dup
|
||||
@metadata = metadata
|
||||
@dictionary = dictionary
|
||||
true
|
||||
end
|
||||
|
||||
|
||||
# Looks up the string and returns a translated string.
|
||||
# If no translated string exists, the original is returned.
|
||||
#
|
||||
# Silently outputs any errors to the console and returns the original
|
||||
# string.
|
||||
#
|
||||
# @param [String] string
|
||||
# @param [Array] args
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def translate(string, *args)
|
||||
translated = @dictionary[string] || string
|
||||
sprintf(translated, *args)
|
||||
rescue => e
|
||||
p e.message
|
||||
puts e.backtrace.join("\n")
|
||||
sprintf(string, *args)
|
||||
end
|
||||
alias :tr :translate
|
||||
|
||||
|
||||
# Used to handle singluar vs plural form.
|
||||
#
|
||||
# When +expression+ is a boolean, +true+ represent singular.
|
||||
#
|
||||
# @param [Numeric, Enumerable, Boolean] expression
|
||||
# @param [String] singular
|
||||
# @param [String] multiple
|
||||
# @param [Array] args
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.12.0
|
||||
def plural(expression, singular, multiple, *args)
|
||||
single = false
|
||||
case expression
|
||||
when Numeric
|
||||
single = expression.to_i == 1
|
||||
when Enumerable
|
||||
[:size, :length, :count].each { |symbol|
|
||||
single = expression.send(symbol) == 1 if expression.respond_to?(symbol)
|
||||
}
|
||||
else
|
||||
single = !!expression
|
||||
end
|
||||
string = single ? singular : multiple
|
||||
translate(string, *args)
|
||||
end
|
||||
alias :pl :plural
|
||||
|
||||
|
||||
# Utility accessor to return a string without arguments.
|
||||
#
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.12.0
|
||||
def [](string)
|
||||
translate(string)
|
||||
end
|
||||
|
||||
|
||||
# Iterates the language folder for .l10n files and extracts the required
|
||||
# display name in the first line of the file.
|
||||
#
|
||||
# Returns a hash of all the languages availible. The key is the translation
|
||||
# code and the value is a hash with meta data.
|
||||
#
|
||||
# The meta data contains one required key: 'name' and optionally 'author'
|
||||
# and/or 'contact'.
|
||||
#
|
||||
# {
|
||||
# 'no' => {
|
||||
# 'name' => '...'
|
||||
# },
|
||||
# 'fr' => {
|
||||
# 'name' => '...',
|
||||
# 'author' => '...',
|
||||
# 'contact' => '...'
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# How to get an array of availible language codes:
|
||||
# codes = babelfish.translations.keys
|
||||
#
|
||||
# Silently eats errors and output any errors or warnings to the console.
|
||||
#
|
||||
# @return [Hash]
|
||||
# @since 2.5.0
|
||||
def translations
|
||||
lang = {}
|
||||
file_filter = File.join(@path, "*.#{@file_ex}")
|
||||
Dir.glob( file_filter ) { |filename|
|
||||
lang_code = File.basename(filename, ".#{@file_ex}")
|
||||
File.open(filename, 'r') { |file|
|
||||
# UTF-8 files might have a BOM mark, account for this.
|
||||
metadata = read_metadata(file)
|
||||
if metadata
|
||||
lang[lang_code] = metadata
|
||||
else
|
||||
puts "WARNING: No @title in #{filename}"
|
||||
end
|
||||
}
|
||||
}
|
||||
lang # (?) This method appears to return false unless this is here...
|
||||
rescue => e
|
||||
p e.message
|
||||
puts e.backtrace.join("\n")
|
||||
ensure
|
||||
lang
|
||||
end
|
||||
|
||||
|
||||
# Try to pick a language based on the Sketchup locale.
|
||||
#
|
||||
# 1. Tries exact matches.
|
||||
# 2. Tries to find by language code, (if SU locale is en-GB) it tries to find
|
||||
# "en.l10n".
|
||||
# 3. Tries to find a similar dialect. (if SU locale is en-GB) it will consider
|
||||
# "en-US.l10n" a match.
|
||||
#
|
||||
# (!)
|
||||
# Norwegian is nn-NO and nb-NO, no
|
||||
# This appear to be different from how English system works.
|
||||
#
|
||||
# http://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo%28VS.80%29.aspx
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def guess_l10n
|
||||
su_locale = Sketchup.get_locale.downcase
|
||||
# Extract list of languages availible
|
||||
file_filter = File.join( @path, "*.#{@file_ex}" )
|
||||
languages = Dir.glob( file_filter ).map { |filename|
|
||||
File.basename(filename, ".#{@file_ex}").downcase
|
||||
}
|
||||
# First search for exact match
|
||||
for lang_code in languages
|
||||
return lang_code if lang_code == su_locale
|
||||
end
|
||||
# Search for partial match - language code
|
||||
su_country = su_locale.split('-').first
|
||||
for lang_code in languages
|
||||
return lang_code if lang_code == su_country
|
||||
end
|
||||
# Search for partial match - language code and dialect
|
||||
for lang_code in languages
|
||||
country = lang_code.split('-').first
|
||||
return lang_code if country == su_country
|
||||
end
|
||||
# Default
|
||||
return @default_l10n
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Debug method that compares the availible translations against a spesified
|
||||
# prototype. Outputs the result to the Console.
|
||||
#
|
||||
# @param [String] prototype_l10n The translation to compare against.
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def check(prototype_l10n)
|
||||
puts "\nChecking language files for missing strings against #{prototype_l10n}..."
|
||||
|
||||
prototype = self.new( @path, @default_l10n )
|
||||
prototype.load( prototype_l10n )
|
||||
|
||||
temp = self.new( @path, @default_l10n )
|
||||
|
||||
keys = prototype.dictionary.keys
|
||||
|
||||
prototype.translations.each { |code, data|
|
||||
next if code == prototype_l10n
|
||||
temp.load(code)
|
||||
puts "\n=== #{data['name']} (#{code}) ==="
|
||||
missing = keys - temp.dictionary.keys
|
||||
puts "> Missing #{missing.length} keys:"
|
||||
missing.each { |str| p str }
|
||||
}
|
||||
|
||||
self.load(org)
|
||||
"\nDone\n\n"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_metadata(file)
|
||||
match = file.readline.match(/^(?:\xEF\xBB\xBF)?@title:\s*"(.+)"/u)
|
||||
return nil if match.nil?
|
||||
# Language data is read into a hash
|
||||
lang_data = {
|
||||
'author' => 'unknown',
|
||||
'contact' => ''
|
||||
}
|
||||
# The Name is required and MUST be the first line in the file.
|
||||
lang_data['name'] = match[1]
|
||||
# The next following lines are optional and can be in any order,
|
||||
# but with comments or skipped lines.
|
||||
2.times {
|
||||
match = file.readline.match(/@(\w+):\s*"(.+)"/u)
|
||||
if match && ['author','contact'].include?(match[1])
|
||||
lang_data[ match[1] ] = match[2]
|
||||
end
|
||||
}
|
||||
lang_data
|
||||
end
|
||||
|
||||
end # class TT::Babelfish
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
# This file exist only as a compatibility with older version of TT_Lib when
|
||||
# the implementation was in pure Ruby. It also ensures the correct version for
|
||||
# the platform is loaded.
|
||||
|
||||
require_relative 'core.rb'
|
||||
require File.join( SpeckleConnector::TT::Lib::PATH_LIBS_CEXT, 'tt_lib2' )
|
||||
@@ -1,38 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# ...
|
||||
#
|
||||
# @since 2.4.0
|
||||
module SpeckleConnector
|
||||
module TT::Binary
|
||||
|
||||
# Base64 encodes binary data.
|
||||
#
|
||||
# @param [Mixed] data
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.4.0
|
||||
def self.encode64(data)
|
||||
return [data].pack('m')
|
||||
end
|
||||
|
||||
|
||||
# Decodes Base64 strings.
|
||||
#
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [Mixed]
|
||||
# @since 2.4.0
|
||||
def self.decode64(string)
|
||||
return string.unpack('m')[0]
|
||||
end
|
||||
|
||||
end # module TT::Binary
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# @example
|
||||
# class Foo
|
||||
# extend TT::BooleanAttributes
|
||||
# battr_accessor :bar
|
||||
# end
|
||||
#
|
||||
# @since 2.7.0
|
||||
module SpeckleConnector
|
||||
module TT::BooleanAttributes
|
||||
|
||||
# @since 2.7.0
|
||||
def battr( symbol, writable = false )
|
||||
self.class_eval {
|
||||
attr( symbol, writable )
|
||||
question = "#{symbol}?".to_sym
|
||||
alias_method( question, symbol )
|
||||
remove_method( symbol )
|
||||
}
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
def battr_accessor( *args )
|
||||
self.class_eval {
|
||||
attr_accessor( *args )
|
||||
for attribute in args
|
||||
question = "#{attribute}?".to_sym
|
||||
alias_method( question, attribute )
|
||||
remove_method( attribute )
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
def battr_reader( *args )
|
||||
self.class_eval {
|
||||
attr_reader( *args )
|
||||
for attribute in args
|
||||
question = "#{attribute}?".to_sym
|
||||
alias_method( question, attribute )
|
||||
remove_method( attribute )
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
end # class TT::BooleanAttributes
|
||||
end
|
||||
@@ -1,112 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of BoundingBox methods.
|
||||
#
|
||||
# @since 2.2.0
|
||||
module SpeckleConnector
|
||||
module TT::Bounds
|
||||
|
||||
# Returns a +Point3d+ from a standard position of the boundingbox.
|
||||
#
|
||||
# @param [Geom::BoundingBox] bounds
|
||||
# @param [Integer] index
|
||||
#
|
||||
# @return [Geom::Point3d]
|
||||
# @since 2.2.0
|
||||
def self.point(bounds, index)
|
||||
case index
|
||||
when 0..7
|
||||
pt = bounds.corner(index)
|
||||
|
||||
when TT::BB_CENTER_FRONT_BOTTOM
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_FRONT_BOTTOM )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_BACK_BOTTOM
|
||||
p1 = bounds.corner( TT::BB_LEFT_BACK_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_BOTTOM )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_FRONT_TOP
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_TOP )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_FRONT_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_BACK_TOP
|
||||
p1 = bounds.corner( TT::BB_LEFT_BACK_TOP )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
|
||||
when TT::BB_LEFT_CENTER_BOTTOM
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_LEFT_BACK_BOTTOM )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_LEFT_CENTER_TOP
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_TOP )
|
||||
p2 = bounds.corner( TT::BB_LEFT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_RIGHT_CENTER_BOTTOM
|
||||
p1 = bounds.corner( TT::BB_RIGHT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_BOTTOM )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_RIGHT_CENTER_TOP
|
||||
p1 = bounds.corner( TT::BB_RIGHT_FRONT_TOP )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
|
||||
when TT::BB_LEFT_FRONT_CENTER
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_LEFT_FRONT_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_RIGHT_FRONT_CENTER
|
||||
p1 = bounds.corner( TT::BB_RIGHT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_FRONT_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_LEFT_BACK_CENTER
|
||||
p1 = bounds.corner( TT::BB_LEFT_BACK_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_LEFT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_RIGHT_BACK_CENTER
|
||||
p1 = bounds.corner( TT::BB_RIGHT_BACK_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
|
||||
when TT::BB_LEFT_CENTER_CENTER
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_LEFT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_RIGHT_CENTER_CENTER
|
||||
p1 = bounds.corner( TT::BB_RIGHT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_FRONT_CENTER
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_FRONT_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_BACK_CENTER
|
||||
p1 = bounds.corner( TT::BB_LEFT_BACK_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_CENTER_TOP
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_TOP )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_TOP )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
when TT::BB_CENTER_CENTER_BOTTOM
|
||||
p1 = bounds.corner( TT::BB_LEFT_FRONT_BOTTOM )
|
||||
p2 = bounds.corner( TT::BB_RIGHT_BACK_BOTTOM )
|
||||
pt = Geom.linear_combination( 0.5, p1, 0.5, p2 )
|
||||
|
||||
when TT::BB_CENTER_CENTER_CENTER
|
||||
pt = bounds.center
|
||||
end
|
||||
|
||||
pt
|
||||
end
|
||||
|
||||
end # module TT::Bounds
|
||||
end
|
||||
@@ -1,241 +0,0 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
module SpeckleConnector
|
||||
module TT
|
||||
|
||||
# Loads the appropriate C Extension loader after ensuring the appropriate
|
||||
# version has been copied from the staging area.
|
||||
#
|
||||
# @since 2.9.0
|
||||
class CExtensionManager
|
||||
|
||||
class IncompatibleVersion < RuntimeError; end
|
||||
|
||||
VERSION_PATTERN = /\d+\.\d+\.\d+$/
|
||||
|
||||
# The `path` argument should point to the path where a 'stage' folder is
|
||||
# located with the following folder structure:
|
||||
#
|
||||
# + `path`
|
||||
# +-+ stage
|
||||
# +-+ 1.8
|
||||
# | +-+ HelloWorld.so
|
||||
# | + HelloWorld.bundle
|
||||
# +-+ 2.0
|
||||
# +-+ HelloWorld.so
|
||||
# + HelloWorld.bundle
|
||||
#
|
||||
# The appropriate file will be copied on demand to a folder structure like:
|
||||
# `path`/<EXTENSION_VERSION>/<RUBY_VERSION>/HelloWorld.so
|
||||
#
|
||||
# When a new version is deployed the files will be copied again from the
|
||||
# staging area to a new folder named with the new extension version.
|
||||
#
|
||||
# The old versions are cleaned up if possible. This attempt is done upon
|
||||
# each time #prepare_path is called.
|
||||
#
|
||||
# This way the C extensions can be updated because they are never loaded
|
||||
# from the staging folder directly.
|
||||
#
|
||||
# @param [String] path The location where the C Extensions are located.
|
||||
# @since 2.9.0
|
||||
def initialize( path, version )
|
||||
# ENV, __FILE__, $LOAD_PATH, $LOADED_FEATURE and more might return an
|
||||
# encoding different from UTF-8. It's often ASCII-US or ASCII-8BIT.
|
||||
# If the developer has derived from these strings the encoding sticks with
|
||||
# it and will often lead to errors further down the road when trying to
|
||||
# load the files. To work around this the path is attempted to be
|
||||
# relabeled as UTF-8 if we can produce a valid UTF-8 string.
|
||||
# I'm forcing an encoding instead of converting because the encoding label
|
||||
# of the strings seem to be consistently mislabeled - the data is in
|
||||
# fact UTF-8.
|
||||
if path.respond_to?(:encoding)
|
||||
test_path = path.dup.force_encoding("UTF-8")
|
||||
path = test_path if test_path.valid_encoding?
|
||||
end
|
||||
|
||||
unless version =~ VERSION_PATTERN
|
||||
raise ArgumentError, 'Version must be in "X.Y.Z" format'
|
||||
end
|
||||
unless File.directory?( path )
|
||||
raise IOError, "Stage path not found: #{path}"
|
||||
end
|
||||
|
||||
@version = version
|
||||
@path = path
|
||||
@stage = File.join( path, 'stage' )
|
||||
@target = File.join( path, version )
|
||||
|
||||
@log = []
|
||||
|
||||
# See method comments for more info.
|
||||
#require_file_utils()
|
||||
end
|
||||
|
||||
# Copies the necessary C Extension libraries to a version dependent folder
|
||||
# from where they can be loaded. This will allow the SketchUp RBZ installer
|
||||
# to update the extension without running into errors when trying to
|
||||
# overwrite files from previous installation.
|
||||
#
|
||||
# @return [String] The path where the extensions are located.
|
||||
# @since 2.9.0
|
||||
def prepare_path
|
||||
log("prepare_path")
|
||||
|
||||
pointer_size = ['a'].pack('P').size * 8 # 32 or 64
|
||||
ruby = RUBY_VERSION.split('.')[0..1].join('.') # Get Major.Minor string.
|
||||
platform = ( TT::System::PLATFORM_IS_OSX ) ? 'osx' : 'win'
|
||||
platform = "#{platform}#{pointer_size}"
|
||||
stage_path = File.join( @stage, ruby, platform )
|
||||
target_path = File.join( @target, ruby, platform )
|
||||
fallback = false
|
||||
|
||||
log("> stage_path: #{stage_path}")
|
||||
log("> target_path: #{target_path}")
|
||||
|
||||
begin
|
||||
# Copy files if target doesn't exist.
|
||||
unless File.directory?(stage_path)
|
||||
raise IncompatibleVersion, "Staging directory not found: #{stage_path}"
|
||||
end
|
||||
unless File.directory?( target_path )
|
||||
log("MKDIR: #{target_path}")
|
||||
require_file_utils() # See method comments for more info.
|
||||
log(FileUtils.mkdir_p( target_path ))
|
||||
end
|
||||
stage_content = Dir.entries( stage_path )
|
||||
target_content = Dir.entries( target_path )
|
||||
log("> stage_content: #{stage_content}")
|
||||
log("> target_content: #{target_content}")
|
||||
unless (stage_content - target_content).empty?
|
||||
log("COPY: #{stage_path} => #{target_path}")
|
||||
require_file_utils() # See method comments for more info.
|
||||
log(FileUtils.copy_entry( stage_path, target_path ))
|
||||
end
|
||||
|
||||
# Clean up old versions.
|
||||
version_pattern = /\d+\.\d+\.\d+$/
|
||||
filter = File.join( @path, '*' )
|
||||
log("> cleanup: #{filter}")
|
||||
Dir.glob( filter ).each { |entry|
|
||||
log(">>> entry: #{entry}")
|
||||
next unless File.directory?( entry )
|
||||
log(">>> @target: #{@target} (#{entry.downcase == @target.downcase})")
|
||||
log(">>> @stage: #{@stage} (#{entry.downcase == @stage.downcase})")
|
||||
log(">>>>> @target vs entry")
|
||||
log(">>>>> #{entry.class.name}: #{entry.bytes}") if entry.respond_to?(:bytes)
|
||||
log(">>>>> #{@target.class.name}: #{@target.bytes}") if @target.respond_to?(:bytes)
|
||||
next if entry.downcase == @stage.downcase || entry.downcase == @target.downcase
|
||||
log(">>> match: #{entry =~ version_pattern}")
|
||||
next unless entry =~ version_pattern
|
||||
begin
|
||||
log("REMOVE: #{entry}")
|
||||
require_file_utils # See method comments for more info.
|
||||
log(FileUtils.rm_r( entry ))
|
||||
rescue
|
||||
log_warn("#{TT::Lib::PLUGIN_NAME} - Unable to clean up: #{entry}")
|
||||
end
|
||||
}
|
||||
rescue Errno::EACCES
|
||||
if fallback
|
||||
UI.messagebox(
|
||||
"Failed to load #{TT::Lib::PLUGIN_NAME}. Missing permissions to " <<
|
||||
"Plugins and temp folder."
|
||||
)
|
||||
raise
|
||||
else
|
||||
# Even though the temp folder contains the username, it appear to be
|
||||
# returned in DOS 8.3 format which Ruby 1.8 can open. Fall back to
|
||||
# using the temp folder for these kind of systems.
|
||||
log_warn("#{TT::Lib::PLUGIN_NAME} - Unable to access: #{target_path}")
|
||||
temp_tt_lib_path = File.join( temp_path, TT::Lib::PLUGIN_ID )
|
||||
target_path = File.join( temp_tt_lib_path, @version, ruby, platform )
|
||||
log_warn("#{TT::Lib::PLUGIN_NAME} - Falling back to: #{target_path}")
|
||||
fallback = true
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
target_path
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.9.0
|
||||
def to_s
|
||||
object_hex_id = "0x%x" % (self.object_id << 1)
|
||||
"<##{self.class}::#{object_hex_id}>"
|
||||
end
|
||||
alias :inspect :to_s
|
||||
|
||||
# @since 2.10.7
|
||||
def print_log
|
||||
puts @log.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @since 2.10.7
|
||||
def log(value)
|
||||
string = value.is_a?(String) ? value : value.inspect
|
||||
@log << string
|
||||
string
|
||||
end
|
||||
|
||||
# @since 2.10.7
|
||||
def log_warn(value)
|
||||
puts log(value)
|
||||
end
|
||||
|
||||
# Return the system temp path from the environment variables.
|
||||
#
|
||||
# @since 2.10.7
|
||||
def temp_path
|
||||
File.expand_path( ENV['TMPDIR'] || ENV['TMP'] || ENV['TEMP'] )
|
||||
end
|
||||
|
||||
# Attempt to load the Standard Library FileUtils module. Fall back to
|
||||
# bundled 1.8 copy.
|
||||
#
|
||||
# @since 2.9.0
|
||||
def require_file_utils
|
||||
if RUBY_VERSION.to_i == 1
|
||||
path = File.dirname( __FILE__ ) # rubocop:disable SketchupSuggestions/FileEncoding
|
||||
require File.join( path, 'thirdparty', 'fileutils.rb' )
|
||||
else
|
||||
begin
|
||||
require 'fileutils'
|
||||
rescue LoadError
|
||||
# A bug in SketchUp 2014 M0 caused the drive letter for the Ruby
|
||||
# Standard Library to be incorrect if SketchUp was started by
|
||||
# clicking a SKP file on a drive (network drive?) different from
|
||||
# where SketchUp was installed.
|
||||
#
|
||||
# This cause the fileutils to fail to load. To work around this the
|
||||
# file is required right before it is needed. That should make the
|
||||
# file needed only the first time after installing a new version.
|
||||
# Makes the code awkward and ugly, but alas. :(
|
||||
std_lib_path = Sketchup.find_support_file( 'Tools/RubyStdLib' ) # rubocop:disable SketchupSuggestions/SketchupFindSupportFile
|
||||
unless $LOAD_PATH.include?( std_lib_path )
|
||||
UI.messagebox(
|
||||
'Due to a bug in SketchUp 2014 M0 the standard library was ' <<
|
||||
'not loaded. Please start SketchUp from a link on the drive ' <<
|
||||
'it was installed to instead of from clicking an SKP on a ' <<
|
||||
'different drive.'
|
||||
) unless @load_error_displayed
|
||||
@load_error_displayed = true
|
||||
end
|
||||
puts $LOAD_PATH.join("\n")
|
||||
raise
|
||||
end
|
||||
end # if RUBY_VERSION
|
||||
end
|
||||
|
||||
end # class
|
||||
|
||||
end # module
|
||||
end
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of Color methods.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module SpeckleConnector
|
||||
module TT::Color
|
||||
|
||||
# Safely clones a Sketchup::Color object. Sketchup::Color.clone appear to
|
||||
# be bugged and prone to crash SU.
|
||||
#
|
||||
# @param [Sketchup::Color] color
|
||||
#
|
||||
# @return [Sketchup::Color]
|
||||
# @since 2.5.0
|
||||
def self.clone(color)
|
||||
Sketchup::Color.new( *color.to_a )
|
||||
end
|
||||
|
||||
end # module TT::Instance
|
||||
end
|
||||
@@ -1,537 +0,0 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
Sketchup::require 'sketchup.rb'
|
||||
# Sketchup::require '../TT_Lib2.rb'
|
||||
require_relative 'system.rb'
|
||||
require_relative 'c_extension_manager.rb'
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Root namespace for Thomas Thomassen (ThomThom, TT)
|
||||
#
|
||||
# Do not modify or extend!
|
||||
#
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT
|
||||
|
||||
### CONSTANTS ### ------------------------------------------------------------
|
||||
|
||||
# BoundingBox Constants
|
||||
# @since 2.0.0
|
||||
|
||||
BB_LEFT_FRONT_BOTTOM = 0
|
||||
BB_RIGHT_FRONT_BOTTOM = 1
|
||||
BB_LEFT_BACK_BOTTOM = 2
|
||||
BB_RIGHT_BACK_BOTTOM = 3
|
||||
BB_LEFT_FRONT_TOP = 4
|
||||
BB_RIGHT_FRONT_TOP = 5
|
||||
BB_LEFT_BACK_TOP = 6
|
||||
BB_RIGHT_BACK_TOP = 7
|
||||
|
||||
BB_CENTER_FRONT_BOTTOM = 8
|
||||
BB_CENTER_BACK_BOTTOM = 9
|
||||
BB_CENTER_FRONT_TOP = 10
|
||||
BB_CENTER_BACK_TOP = 11
|
||||
|
||||
BB_LEFT_CENTER_BOTTOM = 12
|
||||
BB_LEFT_CENTER_TOP = 13
|
||||
BB_RIGHT_CENTER_BOTTOM = 14
|
||||
BB_RIGHT_CENTER_TOP = 15
|
||||
|
||||
BB_LEFT_FRONT_CENTER = 16
|
||||
BB_RIGHT_FRONT_CENTER = 17
|
||||
BB_LEFT_BACK_CENTER = 18
|
||||
BB_RIGHT_BACK_CENTER = 19
|
||||
|
||||
BB_LEFT_CENTER_CENTER = 20
|
||||
BB_RIGHT_CENTER_CENTER = 21
|
||||
BB_CENTER_FRONT_CENTER = 22
|
||||
BB_CENTER_BACK_CENTER = 23
|
||||
BB_CENTER_CENTER_TOP = 24
|
||||
BB_CENTER_CENTER_BOTTOM = 25
|
||||
|
||||
BB_CENTER_CENTER_CENTER = 26
|
||||
BB_CENTER = 26
|
||||
|
||||
# UI.messagebox Constants
|
||||
# @since 2.4.0
|
||||
|
||||
MB_ICONHAND = 0x00000010
|
||||
MB_ICONSTOP = 0x00000010
|
||||
MB_ICONERROR = 0x00000010
|
||||
MB_ICONQUESTION = 0x00000020
|
||||
MB_ICONEXCLAMATION = 0x00000030
|
||||
MB_ICONWARNING = 0x00000030
|
||||
MB_ICONASTERISK = 0x00000040
|
||||
MB_ICONINFORMATION = 0x00000040
|
||||
MB_ICON_NONE = 80
|
||||
|
||||
MB_DEFBUTTON1 = 0x00000000
|
||||
MB_DEFBUTTON2 = 0x00000100
|
||||
MB_DEFBUTTON3 = 0x00000200
|
||||
MB_DEFBUTTON4 = 0x00000300
|
||||
|
||||
# PolygonMesh
|
||||
# @since 2.5.0
|
||||
|
||||
MESH_SHARP = 0
|
||||
MESH_SOFT = 4
|
||||
MESH_SMOOTH = 8
|
||||
MESH_SOFT_SMOOTH = 12
|
||||
|
||||
# view.draw_points
|
||||
# @since 2.5.0
|
||||
|
||||
POINT_OPEN_SQUARE = 1
|
||||
POINT_FILLED_SQUARE = 2
|
||||
POINT_CROSS = 3
|
||||
POINT_X = 4
|
||||
POINT_STAR = 5
|
||||
POINT_OPEN_TRIANGLE = 6
|
||||
POINT_FILLED_TRIANGLE = 7
|
||||
|
||||
# Handle to error message window.
|
||||
# @since 2.7.0
|
||||
@lib2_update = nil
|
||||
def self.lib2_update; @lib2_update; end
|
||||
def self.lib2_update=(window); @lib2_update = window; end
|
||||
|
||||
|
||||
# Defers execution of the given block.
|
||||
#
|
||||
# @param [Numeric] time
|
||||
#
|
||||
# @return [Nil]
|
||||
# @since 2.5.0
|
||||
def self.defer( time = 0 )
|
||||
done = false
|
||||
timer = UI.start_timer( time, false ) {
|
||||
# (i) Unless the timer is stopped before the messagebox it will
|
||||
# continue to trigger until the frist messagebox is closed.
|
||||
unless done
|
||||
done = true
|
||||
yield
|
||||
end
|
||||
}
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
### LIBRARY ### --------------------------------------------------------------
|
||||
|
||||
# TT_Lib related methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module Lib
|
||||
|
||||
# Library version number.
|
||||
# @since 2.0.0
|
||||
VERSION = PLUGIN_VERSION
|
||||
|
||||
# Library preference key.
|
||||
# @since 2.5.0
|
||||
PREF_KEY = 'TT_Lib2'.freeze
|
||||
|
||||
# @since 2.8.0
|
||||
file = File.expand_path( __FILE__ ) # rubocop:disable SketchupSuggestions/FileEncoding
|
||||
file.force_encoding( "UTF-8" ) if file.respond_to?( :force_encoding )
|
||||
PATH = File.dirname( file ).freeze
|
||||
PATH_LIBS = File.join( PATH, 'libraries' ).freeze
|
||||
|
||||
# TT::Lib.cext_manager
|
||||
def self.cext_manager; @cext_manager; end
|
||||
begin
|
||||
@cext_manager = CExtensionManager.new( PATH_LIBS, PLUGIN_VERSION )
|
||||
PATH_LIBS_CEXT = @cext_manager.prepare_path.freeze
|
||||
rescue CExtensionManager::IncompatibleVersion => error
|
||||
unless @compatibility_alert
|
||||
# Avoid this message being called for every extension that rely on TT_Lib.
|
||||
@compatibility_alert = true
|
||||
message = "%{extension_name} version %{version} is not "\
|
||||
"compatible with this version of SketchUp and could not be loaded. "\
|
||||
"Please check for updates to the extension."
|
||||
message %= { extension_name: PLUGIN_NAME, version: PLUGIN_VERSION }
|
||||
TT.defer(1.0) {
|
||||
UI.messagebox(message)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Call this method to check if the installed +TT_Lib+ version is the same
|
||||
# or newer than +version+. If it's not then a messagebox will appear
|
||||
# informing the user that a newer +TT_Lib+ is required.
|
||||
#
|
||||
# @param [String] version a string with the minimun version required in
|
||||
# the format 'x.x.x'.
|
||||
# @param [String] plugin_name a string describing to the user which plugin
|
||||
# require a newer TT_Lib version.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.0.0
|
||||
def self.compatible?(version, plugin_name = 'A plugin installed')
|
||||
major, minor, revision = TT::Lib::VERSION.split('.').map { |s| s.to_i }
|
||||
min_major, min_minor, min_revision = version.split('.').map { |s| s.to_i }
|
||||
return true if major > min_major
|
||||
return true if major == min_major && minor > min_minor
|
||||
return true if major == min_major && minor == min_minor && revision >= min_revision
|
||||
#UI.messagebox("#{plugin_name} requires a newer version, #{version}, of TT_Lib.")
|
||||
if TT.lib2_update.nil?
|
||||
url = 'http://www.thomthom.net/software/sketchup/tt_lib2/errors/outdated'
|
||||
options = {
|
||||
:dialog_title => 'TT_Lib² Outdated',
|
||||
:scrollable => false, :resizable => false, :left => 200, :top => 200
|
||||
}
|
||||
w = UI::WebDialog.new( options )
|
||||
w.set_size( 500, 300 )
|
||||
arguments = "plugin=#{plugin_name}"
|
||||
arguments << "&version=#{TT::Lib::VERSION}"
|
||||
arguments << "&minimum=#{version}"
|
||||
w.set_url( "#{url}?#{arguments}" )
|
||||
w.show
|
||||
TT.lib2_update = w
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
# Compiles a list of the current .rb and .rbs files in the library. This is
|
||||
# used to verify the integirty of the installation upon first run.
|
||||
#
|
||||
# @private
|
||||
# @return [Nil]
|
||||
# @since 2.5.0
|
||||
def self.compile_integrity_list
|
||||
result = UI.messagebox( <<MSG, MB_OKCANCEL )
|
||||
This method is only intended for development purposes. Don't mess about with it!
|
||||
Press Cancel.
|
||||
MSG
|
||||
#'# Silly Sublime Text doesn't handle HereDoc arguments properly.
|
||||
return if result == 2 # CANCEL
|
||||
|
||||
files = Dir.glob( File.join(self.path, '*.{rb,rbs}') ).map! { |file|
|
||||
File.basename( file )
|
||||
}
|
||||
|
||||
File.open( self.integrity_check_file, 'w' ) { |output|
|
||||
output.puts( files )
|
||||
}
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
# @private
|
||||
# @since 2.9.5
|
||||
class IntegrityCheck
|
||||
|
||||
attr_reader :unexpected_files, :missing_files
|
||||
|
||||
def initialize( file_with_list_of_expected_files )
|
||||
integrity_file = file_with_list_of_expected_files
|
||||
@expected_files = IO.readlines( integrity_file ).map! { |file|
|
||||
file.strip
|
||||
}
|
||||
|
||||
@missing_files = @expected_files.select { |file|
|
||||
filename = File.join( PATH, file )
|
||||
!File.exist?( filename )
|
||||
}
|
||||
|
||||
filter = File.join(PATH, '*.{rb,rbs}' )
|
||||
@existing_files = Dir.glob( filter ).map! { |file|
|
||||
File.basename( file )
|
||||
}
|
||||
@unexpected_files = @existing_files - @expected_files
|
||||
end
|
||||
|
||||
def ok?
|
||||
@missing_files.empty? && @unexpected_files.empty?
|
||||
end
|
||||
|
||||
def missing_files?
|
||||
!@missing_files.empty?
|
||||
end
|
||||
|
||||
def unexpected_files?
|
||||
!@unexpected_files.empty?
|
||||
end
|
||||
|
||||
end # class IntegrityCheck
|
||||
|
||||
|
||||
# Called upon startup. If it's the first time this library is loaded a file
|
||||
# integrity check is run to ensure all requires files are present, and that
|
||||
# there are no old remains in case of an update.
|
||||
#
|
||||
# @private
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.integrity_check
|
||||
version = "VerifiedIntegrity-#{self::VERSION}"
|
||||
verified = ::Sketchup.read_default( PREF_KEY, version )
|
||||
return true if verified
|
||||
|
||||
integrity = IntegrityCheck.new( self.integrity_check_file )
|
||||
|
||||
if integrity.ok?
|
||||
::Sketchup.write_default( PREF_KEY, version, true )
|
||||
true
|
||||
else
|
||||
message = <<MSG
|
||||
TT_Lib² appear to be incorrectly installed. Please remove TT_Lib² and then
|
||||
install it again. If this error message persist, contant the author for
|
||||
assistance.
|
||||
MSG
|
||||
message.gsub!( /\s+/, ' ' ) # Collapse whitespace.
|
||||
|
||||
if integrity.missing_files?
|
||||
missing_files = integrity.missing_files.join("\n")
|
||||
message << "\n\nMissing files:\n" << missing_files
|
||||
end
|
||||
|
||||
if integrity.unexpected_files?
|
||||
unexpected_files = integrity.unexpected_files.join("\n")
|
||||
message << "\n\nUnexpected files found:\n" << unexpected_files
|
||||
end
|
||||
|
||||
# Defer execution to allow remaining plugins to load.
|
||||
TT.defer { UI.messagebox( message, MB_OK ) }
|
||||
false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# Checks for VirtualStore conflict.
|
||||
#
|
||||
# @private
|
||||
# @return [Nil]
|
||||
# @since 2.9.0
|
||||
def self.virtualstore_check
|
||||
return nil unless TT::System.is_windows?
|
||||
require 'TT_Lib2/win32.rb'
|
||||
plugins_folder = Sketchup.find_support_file( 'Plugins' ) # rubocop:disable SketchupSuggestions/SketchupFindSupportFile
|
||||
virtual_folder = TT::System.get_virtual_file( plugins_folder )
|
||||
return nil if virtual_folder.nil?
|
||||
return nil if plugins_folder == virtual_folder
|
||||
return nil unless File.exist?( virtual_folder )
|
||||
if Dir.entries( virtual_folder ).to_a.size > 2
|
||||
message = <<MSG
|
||||
TT_Lib² detected that some of the files in SketchUp's plugin folder has ended up
|
||||
in Window's Virtual Store. It happens because of insufficint permissions for the
|
||||
plugin folder. Please move the files into the real Plugins folder.
|
||||
MSG
|
||||
message.gsub!( /\s+/, ' ' ) # Collapse whitespace.
|
||||
# Defer execution to allow remaining plugins to load.
|
||||
TT.defer {
|
||||
result = UI.messagebox( message, MB_OKCANCEL )
|
||||
if result == IDOK
|
||||
UI.openURL( virtual_folder )
|
||||
end
|
||||
}
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
# Returns the full path to the integrity file.
|
||||
#
|
||||
# @private
|
||||
# @return [Nil]
|
||||
# @since 2.5.0
|
||||
def self.integrity_check_file
|
||||
File.join( self.path, 'integrity_list.dat' )
|
||||
end
|
||||
|
||||
|
||||
# @return [String] The file path where the library is installed.
|
||||
# @since 2.0.0
|
||||
def self.path
|
||||
PATH.dup
|
||||
end
|
||||
|
||||
|
||||
# Debug method to reload the library modules.
|
||||
#
|
||||
# @param [Boolean] return_files Determines if the method should return
|
||||
# the number of files reloaded or an array of the files reloaded.
|
||||
#
|
||||
# @return [Integer, Array]
|
||||
# @since 2.0.0
|
||||
def self.reload( return_files=false )
|
||||
original_verbose = $VERBOSE
|
||||
$VERBOSE = nil # Mute warnings caused by constant redefining.
|
||||
x = Dir.glob( File.join(self.path, '*.{rb,rbs}') ).each { |file|
|
||||
load file
|
||||
}
|
||||
(return_files) ? x : x.length
|
||||
ensure
|
||||
$VERBOSE = original_verbose
|
||||
end
|
||||
|
||||
end # module TT::Lib
|
||||
|
||||
|
||||
### MENUS ### ----------------------------------------------------------------
|
||||
|
||||
# If TT's Menu is installed this method will return a custom Menu item
|
||||
# instead of the requested root menu.
|
||||
#
|
||||
# @param [String] name The prefered root menu in Sketchup.
|
||||
#
|
||||
# @return [Sketchup::Menu]
|
||||
# @since 2.0.0
|
||||
def self.menu( name )
|
||||
if global_variables.include?( :$tt_menu ) && $tt_menu
|
||||
$tt_menu
|
||||
else
|
||||
UI.menu( name )
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
### NAMESPACE ### ------------------------------------------------------------
|
||||
|
||||
# Namespace for plugins to be wrapped into.
|
||||
# Example:
|
||||
#
|
||||
# require 'TT_Lib2/core.rb'
|
||||
# module TT::Plugins::FooBar
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# Reserved for Thomas Thomassen.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module Plugins; end
|
||||
|
||||
|
||||
### MACROS ### ---------------------------------------------------------------
|
||||
|
||||
# @param [Integer] number
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.even?(number)
|
||||
number % 2 == 0
|
||||
end
|
||||
|
||||
|
||||
# @param [Integer] number
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.odd?(number)
|
||||
number % 2 > 0
|
||||
end
|
||||
|
||||
|
||||
# Format the given +time+ into a human readable string.
|
||||
#
|
||||
# @param [Numeric] time
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def self.format_time( time )
|
||||
time = (time.finite?) ? time : 0.0
|
||||
hours = (time / 3600).to_i
|
||||
minutes = (time/60 - hours * 60).to_i
|
||||
seconds = (time - (minutes * 60 + hours * 3600)).to_i
|
||||
if hours > 0 && minutes > 0
|
||||
"#{hours}h #{minutes}m #{seconds}s"
|
||||
elsif minutes > 0
|
||||
"#{minutes}m #{seconds}s"
|
||||
else
|
||||
"#{seconds}s"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns the given square meters +area_meters+ in square inches.
|
||||
#
|
||||
# @param [Numeric] area_meters
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.5.0
|
||||
def self.m2( area_meters )
|
||||
ratio = 1.m ** 2
|
||||
area_meters * ratio
|
||||
end
|
||||
|
||||
|
||||
# Returns the given square inches +area_inches+ in square meters.
|
||||
#
|
||||
# @param [Numeric] area_inches
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.5.0
|
||||
def self.to_m2( area_inches )
|
||||
ratio = 1.0 / self.m2(1)
|
||||
area_inches * ratio
|
||||
end
|
||||
|
||||
|
||||
# @param [Object] object
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.6.0
|
||||
def self.object_id_hex( object )
|
||||
"0x%x" % (object.object_id << 1)
|
||||
end
|
||||
|
||||
# @param [Array] array
|
||||
#
|
||||
# @return [Hash]
|
||||
# @since 2.6.0
|
||||
def self.array_to_hash( array )
|
||||
h = {}
|
||||
for key, value in array
|
||||
h[ key ] = value
|
||||
end
|
||||
h
|
||||
end
|
||||
|
||||
|
||||
### ENVIRONMENT ###-----------------------------------------------------------
|
||||
|
||||
# Set up load paths.
|
||||
#path = File.join( TT::Lib.path, 'libraries' )
|
||||
#$LOAD_PATH << path unless $LOAD_PATH.include?( path )
|
||||
|
||||
end # module TT
|
||||
end
|
||||
|
||||
if SpeckleConnector::TT::System.platform_supported?
|
||||
|
||||
# Check the integrity of the library.
|
||||
SpeckleConnector::TT::Lib.integrity_check
|
||||
#TT::Lib.virtualstore_check() # Disabled until a better guide can be made.
|
||||
|
||||
# Require remaining modules.
|
||||
Dir.glob( File.join(SpeckleConnector::TT::Lib.path, '*.{rb}') ).each { |filename|
|
||||
file = __FILE__.dup
|
||||
file.force_encoding( "UTF-8" ) if file.respond_to?(:force_encoding)
|
||||
unless File.basename( filename ) == File.basename( file )
|
||||
relative_file = File.join( SpeckleConnector::TT::Lib.path, File.basename( filename ) )
|
||||
require( relative_file )
|
||||
end
|
||||
}
|
||||
|
||||
else
|
||||
|
||||
# Disable the extension if it's not supported by the platform. This is done to
|
||||
# avoid potential crashes when loading the binaries.
|
||||
#if Sketchup.respond_to?(:extensions)
|
||||
# extension = Sketchup.extensions[TT::Lib::PLUGIN_NAME]
|
||||
# extension.uncheck
|
||||
#end
|
||||
# Alert the user that the extension is not compatible with the running system.
|
||||
message = "#{SpeckleConnector::TT::Lib::PLUGIN_NAME} is not supported for this platform."
|
||||
UI.messagebox(message)
|
||||
|
||||
end # if TT::System.platform_supported?
|
||||
@@ -1,269 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# @since 2.4.0
|
||||
module SpeckleConnector
|
||||
module TT::Cursor
|
||||
|
||||
# Path to the cursor resources.
|
||||
PATH = File.join( TT::Lib.path, 'cursors')
|
||||
|
||||
# Definitions of cursor resources.
|
||||
# :symbol_id => ['filename.png', x, y]
|
||||
@cursors = {
|
||||
:default => 0,
|
||||
:invalid => 663,
|
||||
:hand => 671,
|
||||
:hand_invalid => 918,
|
||||
:link => 670,
|
||||
:erase => 645,
|
||||
:pencil => 632,
|
||||
:freehand => 655,
|
||||
:arc_1 => 629,
|
||||
:arc_2 => 631,
|
||||
:arc_3 => 630,
|
||||
:man => 612,
|
||||
:position_camera => 653,
|
||||
:position_camera_3d => 902,
|
||||
:walk => 420,
|
||||
:walk_3d => 904,
|
||||
:look_around => 418,
|
||||
:look_around_3d => 903,
|
||||
:orbit => 419,
|
||||
:orbit_3d => 900,
|
||||
:pan => 1003,
|
||||
:pan_2d => 901,
|
||||
:zoom => 421,
|
||||
:zoom_region => 422,
|
||||
:zoom_3d => 905,
|
||||
:zoom_2d => 907,
|
||||
:zoom_2d_region => 906,
|
||||
:offset => 646,
|
||||
:offset_invalid => 679,
|
||||
:dropper => 651,
|
||||
:dropper_texture => 652,
|
||||
:dropper_invalid => ['dropper_invalid.png', 2, 29],
|
||||
:paint => 681, # 647
|
||||
:paint_same => 650,
|
||||
:paint_object => 649,
|
||||
:paint_connected => 648,
|
||||
:paint_invalid => 680,
|
||||
:text => 678,
|
||||
:follow_me => 640,
|
||||
:follow_me_invalid => 678,
|
||||
:pushpull => 639,
|
||||
:pushpull_add => 755,
|
||||
:pushpull_invalid => 707,
|
||||
:tape => 638,
|
||||
:tape_add => 731,
|
||||
:select => 633,
|
||||
:select_add => 634,
|
||||
:select_remove => 636,
|
||||
:select_toggle => 635,
|
||||
:select_step_1 => 924,
|
||||
:select_step_2 => 925,
|
||||
:select_invalid => 926,
|
||||
:vertex => ['Vertex.png', 12, 19],
|
||||
:vertex_add => ['Vertex_Add.png', 12, 19],
|
||||
:vertex_remove => ['Vertex_Remove.png', 12, 19],
|
||||
:vertex_toggle => ['Vertex_Toggle.png', 12, 19],
|
||||
:rectangle => 637,
|
||||
:move => 641,
|
||||
:move_copy => 642,
|
||||
:move_fold => 672,
|
||||
:move_invalid => 673,
|
||||
:position => 658,
|
||||
:position_invalid => 673,
|
||||
:scale => 736,
|
||||
:scale_invalid => 730,
|
||||
:scale_n_s => 659,
|
||||
:scale_n_ne => 666,
|
||||
:scale_ne => 661,
|
||||
:scale_ne_e => 667,
|
||||
:scale_w_e => 660,
|
||||
:scale_n_nw => 665,
|
||||
:scale_nw => 662,
|
||||
:scale_nw_w => 664,
|
||||
:rotate => 643,
|
||||
:rotate_copy => 644,
|
||||
:rotate_invalid => 713
|
||||
}
|
||||
|
||||
# Creates cursor ids for the requested cursor +id+. Cursors are created on demand and
|
||||
# reused to save resources.
|
||||
#
|
||||
# Valid +id+ arguments
|
||||
# * +:default+
|
||||
# * +:invalid+ (2.7.0)
|
||||
# * +:hand+ (2.7.0)
|
||||
# * +:hand_invalid+ (2.7.0)
|
||||
# * +:link+ (2.7.0)
|
||||
# * +:erase+ (2.7.0)
|
||||
# * +:pencil+ (2.7.0)
|
||||
# * +:freehand+ (2.7.0)
|
||||
# * +:arc_1+ (2.7.0)
|
||||
# * +:arc_2+ (2.7.0)
|
||||
# * +:arc_3+ (2.7.0)
|
||||
# * +:man+ (2.7.0)
|
||||
# * +:position_camera_3d+ (2.7.0)
|
||||
# * +:orbit+ (2.7.0)
|
||||
# * +:orbit_3d+ (2.7.0)
|
||||
# * +:pan_2d+ (2.7.0)
|
||||
# * +:pan+ (2.7.0)
|
||||
# * +:walk+ (2.7.0)
|
||||
# * +:walk_3d+ (2.7.0)
|
||||
# * +:look_around+ (2.7.0)
|
||||
# * +:look_around_903+ (2.7.0)
|
||||
# * +:zoom+ (2.7.0)
|
||||
# * +:zoom_region+ (2.7.0)
|
||||
# * +:zoom_3d+ (2.7.0)
|
||||
# * +:zoom_2d+ (2.7.0)
|
||||
# * +:zoom_2d_region+ (2.7.0)
|
||||
# * +:offset+
|
||||
# * +:offset_invalid+
|
||||
# * +:dropper+
|
||||
# * +:dropper_texture+ (2.7.0)
|
||||
# * +:dropper_invalid+
|
||||
# * +:paint+ (2.7.0)
|
||||
# * +:paint_same+ (2.7.0)
|
||||
# * +:paint_object+ (2.7.0)
|
||||
# * +:paint_connected+ (2.7.0)
|
||||
# * +:paint_invalid+ (2.7.0)
|
||||
# * +:text+ (2.7.0)
|
||||
# * +:follow+ (2.7.0)
|
||||
# * +:follow_me+ (2.7.0)
|
||||
# * +:pushpull+ (2.7.0)
|
||||
# * +:pushpull_add+ (2.7.0)
|
||||
# * +:pushpull_invalid+ (2.7.0)
|
||||
# * +:tape+ (2.7.0)
|
||||
# * +:tape_add+ (2.7.0)
|
||||
# * +:select+
|
||||
# * +:select_add+
|
||||
# * +:select_remove+
|
||||
# * +:select_toggle+
|
||||
# * +:select_step_1+ (2.7.0)
|
||||
# * +:select_step_2+ (2.7.0)
|
||||
# * +:select_invalid+ (2.7.0)
|
||||
# * +:vertex+ (2.5.0)
|
||||
# * +:vertex_add+ (2.5.0)
|
||||
# * +:vertex_remove+ (2.5.0)
|
||||
# * +:vertex_toggle+ (2.5.0)
|
||||
# * +:rectangle+ (2.6.0)
|
||||
# * +:move+ (2.6.0)
|
||||
# * +:move_copy+ (2.7.0)
|
||||
# * +:move_fold+ (2.7.0)
|
||||
# * +:move_invalid+ (2.7.0)
|
||||
# * +:position+ (2.7.0)
|
||||
# * +:position_invalid+ (2.7.0)
|
||||
# * +:rotate+ (2.6.0)
|
||||
# * +:rotate_copy+ (2.6.0)
|
||||
# * +:rotate_invalid+ (2.7.0)
|
||||
# * +:scale+ (2.6.0)
|
||||
# * +:scale_invalid+ (2.7.0)
|
||||
# * +:scale_n_s+ (2.7.0)
|
||||
# * +:scale_n_ne+ (2.7.0)
|
||||
# * +:scale_ne+ (2.7.0)
|
||||
# * +:scale_ne_e+ (2.7.0)
|
||||
# * +:scale_w_e+ (2.7.0)
|
||||
# * +:scale_n_nw+ (2.7.0)
|
||||
# * +:scale_nw+ (2.7.0)
|
||||
# * +:scale_nw_w+ (2.7.0)
|
||||
#
|
||||
# @param [Symbol] id
|
||||
#
|
||||
# @return [Integer, nil] +Integer+ of a cursor resource uon success, +nil+ upon failure.
|
||||
# @since 2.4.0
|
||||
def self.get_id(id)
|
||||
return nil unless @cursors.key?(id)
|
||||
# Load cursors on demand
|
||||
if @cursors[id].is_a?(Array)
|
||||
cursor_file, x, y = @cursors[id]
|
||||
filename = File.join( TT::Cursor::PATH, cursor_file )
|
||||
@cursors[id] = UI.create_cursor( filename, x, y )
|
||||
end
|
||||
return @cursors[id]
|
||||
end
|
||||
|
||||
# Returns a cursor ID to a scaling direction cursor based on a 2D vector in
|
||||
# screen space.
|
||||
#
|
||||
# @param [Geom::Vector3d] screen_vector
|
||||
# @param [Sketchup::View] view
|
||||
#
|
||||
# @return [Integer, nil] +Integer+ of a cursor resource uon success, +nil+ upon failure.
|
||||
# @since 2.7.0
|
||||
def self.get_vector2d_cursor( screen_vector, view )
|
||||
cursors = self.scale_handles
|
||||
cursor_id = nil
|
||||
nearest_angle = nil
|
||||
for vector, cursor in cursors
|
||||
a1 = vector.angle_between( screen_vector ).abs
|
||||
a2 = vector.angle_between( screen_vector.reverse ).abs
|
||||
angle = [ a1, a2 ].min
|
||||
if nearest_angle.nil? || angle < nearest_angle
|
||||
nearest_angle = angle
|
||||
cursor_id = cursor
|
||||
end
|
||||
end
|
||||
cursor_id
|
||||
end
|
||||
|
||||
# Returns a cursor ID to a scaling direction cursor based on a 3D vector in
|
||||
# model space.
|
||||
#
|
||||
# @param [Geom::Vector3d] vector
|
||||
# @param [Sketchup::View] view
|
||||
#
|
||||
# @return [Integer, nil] +Integer+ of a cursor resource uon success, +nil+ upon failure.
|
||||
# @since 2.7.0
|
||||
def self.get_vector3d_cursor( vector, view )
|
||||
pt1 = ORIGIN
|
||||
pt2 = ORIGIN.offset( vector )
|
||||
spt1 = view.screen_coords( pt1 )
|
||||
spt2 = view.screen_coords( pt2 )
|
||||
spt1.z = 0
|
||||
spt2.z = 0
|
||||
screen_vector = spt1.vector_to( spt2 )
|
||||
self.get_vector2d_cursor( screen_vector, view )
|
||||
end
|
||||
|
||||
# @return [Hash]
|
||||
# @since 2.7.0
|
||||
def self.scale_handles
|
||||
@scale_handles ||= self.compute_scale_handles
|
||||
@scale_handles
|
||||
end
|
||||
|
||||
# @private
|
||||
#
|
||||
# @return [Hash]
|
||||
# @since 2.7.0
|
||||
def self.compute_scale_handles
|
||||
cursor_ids = [
|
||||
TT::Cursor.get_id( :scale_nw_w ),
|
||||
TT::Cursor.get_id( :scale_nw ),
|
||||
TT::Cursor.get_id( :scale_n_nw ),
|
||||
TT::Cursor.get_id( :scale_n_s ),
|
||||
TT::Cursor.get_id( :scale_n_ne ),
|
||||
TT::Cursor.get_id( :scale_ne ),
|
||||
TT::Cursor.get_id( :scale_ne_e ),
|
||||
TT::Cursor.get_id( :scale_w_e )
|
||||
].reverse
|
||||
cursors = {}
|
||||
angle = ( 180.0 / cursor_ids.size ).degrees
|
||||
cursor_ids.each_with_index { |id, index|
|
||||
tr = Geom::Transformation.rotation( ORIGIN, Z_AXIS, -angle * index )
|
||||
vector = X_AXIS.transform( tr )
|
||||
cursors[ vector ] = id
|
||||
}
|
||||
cursors
|
||||
end
|
||||
|
||||
end # module TT::Cursor
|
||||
end
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,73 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'system.rb'
|
||||
require_relative 'win32.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
module TT
|
||||
|
||||
# Outputs debug data.
|
||||
#
|
||||
# Under Windows the data is sent to OutputDebugString and
|
||||
# requires a utility like DebugView to see the data. Without it the call
|
||||
# is muted.
|
||||
#
|
||||
# Under other platforms the data is sent to the console.
|
||||
#
|
||||
# @param [Mixed] data
|
||||
#
|
||||
# @return [Nil]
|
||||
# @since 2.5.0
|
||||
def self.debug(data)
|
||||
if data.is_a?( String )
|
||||
str = data
|
||||
else
|
||||
str = data.inspect
|
||||
end
|
||||
if TT::System.is_windows?
|
||||
if TT::Win32.respond_to?(:debug_output)
|
||||
TT::Win32.debug_output(str)
|
||||
else
|
||||
TT::Win32::OutputDebugString.call( "#{str}\n\0" )
|
||||
end
|
||||
else
|
||||
puts data
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
class Debug
|
||||
|
||||
# @param [String] object
|
||||
#
|
||||
# @return [Array]
|
||||
# @since 2.7.0
|
||||
def self.map_methods( object, ignore = [Kernel, Object] )
|
||||
klass = ( object.class == Class || object.class == Module ) ? object : object.class
|
||||
methods = klass.instance_methods
|
||||
klasses = {}
|
||||
ancestors = klass.ancestors
|
||||
puts "#{klass} - (#{klass.class})"
|
||||
puts "> Ancestors: #{ancestors.inspect}"
|
||||
for k in ancestors
|
||||
puts " > #{k} - ( #{k.class})"
|
||||
if ignore.include?( k )
|
||||
puts " (Ignored)"
|
||||
else
|
||||
puts " #{k.instance_methods(false).sort.join( "\n " )}"
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
end # class Debug
|
||||
|
||||
end # module TT
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Special Proc like object that limits the frequency it's executed. Designed to
|
||||
# be used with +change+ events for TT::GUI::Textbox.
|
||||
#
|
||||
# @since 2.7.0
|
||||
|
||||
module SpeckleConnector
|
||||
class TT::DeferredEvent
|
||||
|
||||
attr_accessor( :suppress_event_if_value_not_changed )
|
||||
|
||||
# @param [Float] delay Maximum frequency the event can be executed.
|
||||
# @param [Proc] block
|
||||
#
|
||||
# @since 2.7.0
|
||||
def initialize( delay = 0.2, &block )
|
||||
@proc = block
|
||||
@delay = delay
|
||||
@last_value = nil
|
||||
@timer = nil
|
||||
@suppress_event_if_value_not_changed = true
|
||||
end
|
||||
|
||||
# @param [Mixed] value Must be different from last call in order to trigger.
|
||||
#
|
||||
# @return [Boolean] True is the event was executed.
|
||||
# @since 2.7.0
|
||||
def call( value )
|
||||
return false if @suppress_event_if_value_not_changed && value == @last_value
|
||||
UI.stop_timer( @timer ) if @timer
|
||||
@timer = UI.start_timer( @delay, false ) {
|
||||
UI.stop_timer( @timer ) # Ensure it only runs once.
|
||||
@proc.call( value )
|
||||
}
|
||||
true
|
||||
end
|
||||
|
||||
end # class DeferredEvent
|
||||
end
|
||||
@@ -1,111 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of Face methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT::Definition
|
||||
|
||||
# Sets the origin of the +ComponentDefinition+ to a given 3d point.
|
||||
#
|
||||
# @param [Sketchup::ComponentDefinition] definition
|
||||
# @param [Geom::Point3d] origin
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.0.0
|
||||
def self.set_origin(definition, origin)
|
||||
return false if definition.image?
|
||||
# Set the origin - move the entities and counter-adjust the instances.
|
||||
t = Geom::Transformation.new( origin )
|
||||
definition.entities.transform_entities( t.inverse, definition.entities.to_a )
|
||||
definition.instances.each { |i|
|
||||
i.transformation = i.transformation * t
|
||||
}
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
# Sets the origin of the +ComponentDefinition+ to a given point on its bounds.
|
||||
#
|
||||
# +origin+ can be an integer of the following values:
|
||||
#
|
||||
# BB_LEFT_FRONT_BOTTOM = 0
|
||||
# BB_RIGHT_FRONT_BOTTOM = 1
|
||||
# BB_LEFT_BACK_BOTTOM = 2
|
||||
# BB_RIGHT_BACK_BOTTOM = 3
|
||||
# BB_LEFT_FRONT_TOP = 4
|
||||
# BB_RIGHT_FRONT_TOP = 5
|
||||
# BB_LEFT_BACK_TOP = 6
|
||||
# BB_RIGHT_BACK_TOP = 7
|
||||
# BB_BOTTOM_CENTER = 8
|
||||
# BB_TOP_CENTER = 9
|
||||
# BB_LEFT_CENTER = 10
|
||||
# BB_RIGHT_CENTER = 11
|
||||
# BB_FRONT_CENTER = 12
|
||||
# BB_BACK_CENTER = 13
|
||||
# BB_CENTER = 14
|
||||
#
|
||||
# All these constants are defined under +TT+.
|
||||
#
|
||||
# @param [Sketchup::ComponentDefinition] definition
|
||||
# @param [Integer] origin
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.0.0
|
||||
def self.set_origin_by_bounds(definition, origin)
|
||||
return false if definition.image?
|
||||
|
||||
bb = definition.bounds
|
||||
|
||||
# Compute the origin
|
||||
if origin.is_a?(Numeric)
|
||||
case origin
|
||||
when (0..7)
|
||||
new_origin = bb.corner(origin)
|
||||
when BB_CENTER
|
||||
new_origin = bb.center
|
||||
when BB_BOTTOM_CENTER
|
||||
p1 = bb.corner(BB_LEFT_FRONT_BOTTOM)
|
||||
p2 = bb.corner(BB_RIGHT_BACK_BOTTOM)
|
||||
new_origin = Geom::Point3d.linear_combination(0.5, p1, 0.5, p2)
|
||||
when BB_TOP_CENTER
|
||||
p1 = bb.corner(BB_LEFT_FRONT_TOP)
|
||||
p2 = bb.corner(BB_RIGHT_BACK_TOP)
|
||||
new_origin = Geom::Point3d.linear_combination(0.5, p1, 0.5, p2)
|
||||
when BB_LEFT_CENTER
|
||||
p1 = bb.corner(BB_LEFT_FRONT_BOTTOM)
|
||||
p2 = bb.corner(BB_LEFT_BACK_TOP)
|
||||
new_origin = Geom::Point3d.linear_combination(0.5, p1, 0.5, p2)
|
||||
when BB_RIGHT_CENTER
|
||||
p1 = bb.corner(BB_RIGHT_FRONT_BOTTOM)
|
||||
p2 = bb.corner(BB_RIGHT_BACK_TOP)
|
||||
new_origin = Geom::Point3d.linear_combination(0.5, p1, 0.5, p2)
|
||||
when BB_FRONT_CENTER
|
||||
p1 = bb.corner(BB_LEFT_FRONT_BOTTOM)
|
||||
p2 = bb.corner(BB_RIGHT_FRONT_TOP)
|
||||
new_origin = Geom::Point3d.linear_combination(0.5, p1, 0.5, p2)
|
||||
when BB_BACK_CENTER
|
||||
p1 = bb.corner(BB_LEFT_BACK_BOTTOM)
|
||||
p2 = bb.corner(BB_RIGHT_BACK_TOP)
|
||||
new_origin = Geom::Point3d.linear_combination(0.5, p1, 0.5, p2)
|
||||
else
|
||||
raise ArgumentError
|
||||
end
|
||||
elsif origin.is_a?(Geom::Point3d) || (origin.is_a?(Array) && origin.size = 3)
|
||||
new_origin = origin
|
||||
else
|
||||
raise ArgumentError
|
||||
end
|
||||
|
||||
self.set_origin(definition, bb.center)
|
||||
end
|
||||
|
||||
end # module TT::Definition
|
||||
end
|
||||
@@ -1,279 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# A 2d matrix Array that can be iterated in rows, columns or like a regular +Array+.
|
||||
#
|
||||
# *Note*: it does not behave exactly like a regular +Array+. Use the +.to_a+ method
|
||||
# to get a regular array.
|
||||
#
|
||||
# @since 2.5.0
|
||||
|
||||
module SpeckleConnector
|
||||
class TT::Dimension
|
||||
include Enumerable
|
||||
|
||||
# @since 2.5.0
|
||||
attr(:width)
|
||||
# @since 2.5.0
|
||||
attr(:height)
|
||||
|
||||
|
||||
# @overload new(array, width, height, obj = nil)
|
||||
# @param [Array] array creates an Dimension from an array
|
||||
# @param [Integer] width
|
||||
# @param [Integer] height
|
||||
# @param [Object] obj default value
|
||||
# @overload new(width, height, obj = nil)
|
||||
# @param [Integer] width
|
||||
# @param [Integer] height
|
||||
# @param [Object] obj default value
|
||||
#
|
||||
# @since 2.5.0
|
||||
def initialize(*args)
|
||||
# Check if the first aruments is an +Array+ - use that to populate the dataset.
|
||||
arr = args.first.is_a?(Array) ? args.shift : nil
|
||||
# Validate arguments
|
||||
if args.length < 2
|
||||
raise ArgumentError, 'Missing arguments. Requires width and height.'
|
||||
end
|
||||
# Extract remaining arguments
|
||||
@width, @height, obj = args
|
||||
# Create 2-dimensional array
|
||||
@d = Array.new(@width * @height, obj)
|
||||
# Populate with the given data, if any.
|
||||
unless arr.nil?
|
||||
unless arr.length == @width * @height
|
||||
raise ArgumentError, 'Array length does not match width and height.'
|
||||
end
|
||||
arr.each_index { |i| @d[i] = arr[i] }
|
||||
end
|
||||
end
|
||||
|
||||
# @example
|
||||
# dim = Dimension.new([6,7,8,9], 2, 2)
|
||||
# puts dim[2]
|
||||
# -> 8
|
||||
# puts dim[0,1]
|
||||
# -> 8
|
||||
#
|
||||
# @overload [](index)
|
||||
# @param [Integer] index an Integer between 0 and self.length-1
|
||||
# @overload [](row, column)
|
||||
# @param [Integer] row an Integer between 0 and self.width-1
|
||||
# @param [Integer] column an Integer between 0 and self.height-1
|
||||
#
|
||||
# @return [Object] value at the given index
|
||||
#
|
||||
# @since 2.5.0
|
||||
def [](*args)
|
||||
case args.length
|
||||
when 1
|
||||
return @d[ args.first ]
|
||||
when 2
|
||||
column, row = args
|
||||
return @d[ (row * @width) + column ]
|
||||
end
|
||||
end
|
||||
|
||||
# @overload []=(index)
|
||||
# @param [Integer] index an Integer between 0 and self.length-1
|
||||
# @overload []=(row, column)
|
||||
# @param [Integer] row an Integer between 0 and self.width-1
|
||||
# @param [Integer] column an Integer between 0 and self.height-1
|
||||
#
|
||||
# @since 2.5.0
|
||||
def []=(*args)
|
||||
value = args.pop
|
||||
case args.length
|
||||
when 1
|
||||
@d[args.first] = value
|
||||
when 2
|
||||
column, row = args
|
||||
@d[ (row * @width) + column ] = value
|
||||
end
|
||||
end
|
||||
|
||||
# @param [Integer] index an Integer between +0+ and +self.height-1+
|
||||
#
|
||||
# @return [Array] row at index
|
||||
#
|
||||
# @since 2.5.0
|
||||
def row(index)
|
||||
return @d[index * @width, @width]
|
||||
end
|
||||
|
||||
# Sets the row at the given index.
|
||||
#
|
||||
# @param [Integer] index an Integer between +0+ and +self.height-1+
|
||||
# @param [Array] new_row an +Array+ of the size +self.width+
|
||||
#
|
||||
# @return [Array] the new row
|
||||
#
|
||||
# @since 2.5.0
|
||||
def set_row(index, new_row)
|
||||
return @d[index * @width, @width] = new_row
|
||||
end
|
||||
|
||||
# @return [Array<Array>] an array containing all the rows
|
||||
#
|
||||
# @since 2.5.0
|
||||
def rows
|
||||
arr = []
|
||||
0.step(@d.length - 1, @width) { |i|
|
||||
arr << @d[i, @width]
|
||||
}
|
||||
return arr
|
||||
end
|
||||
|
||||
# @param [Integer] index an Integer between +0+ and +self.width-1+
|
||||
#
|
||||
# @return [Array] column at index
|
||||
#
|
||||
# @since 2.5.0
|
||||
def column(index)
|
||||
arr = []
|
||||
0.upto(@height - 1) { |j|
|
||||
arr << @d[index + (j * @width)]
|
||||
}
|
||||
return arr
|
||||
end
|
||||
|
||||
# Sets the column at the given index.
|
||||
#
|
||||
# @param [Integer] index an Integer between +0+ and +self.width-1+
|
||||
# @param [Array] new_column an +Array+ of the size +self.height+
|
||||
#
|
||||
# @return [Array] the new column
|
||||
#
|
||||
# @since 2.5.0
|
||||
def set_column(index, new_column)
|
||||
0.upto(@height - 1) { |j|
|
||||
@d[index + (j * @width)] = new_column[j]
|
||||
}
|
||||
end
|
||||
|
||||
# @return [Array<Array>] an +Array+ containing all the columns
|
||||
#
|
||||
# @since 2.5.0
|
||||
def columns
|
||||
arr = []
|
||||
0.upto(@width - 1) { |i|
|
||||
column = []
|
||||
0.upto(@height - 1) { |j|
|
||||
column << @d[i + (j * @width)]
|
||||
}
|
||||
arr << column
|
||||
}
|
||||
return arr
|
||||
end
|
||||
|
||||
# @yield [array, index] an +Array+ representing either a row or a column
|
||||
# depening on +column+
|
||||
# @yieldparam [Array] array
|
||||
# @yieldparam [optional, Integer] index
|
||||
#
|
||||
# @param [Boolean] column iterates columns when +true+ or rows
|
||||
# when +false+
|
||||
#
|
||||
# @since 2.5.0
|
||||
def each(column = false, &block)
|
||||
if column
|
||||
0.upto(@width - 1) { |i|
|
||||
0.upto(@height - 1) { |j|
|
||||
index = i + (j * @width)
|
||||
if block.arity > 1
|
||||
yield( @d[index], index )
|
||||
else
|
||||
yield( @d[index] )
|
||||
end
|
||||
}
|
||||
}
|
||||
else
|
||||
@d.each { |i| yield(i) }
|
||||
end
|
||||
end
|
||||
|
||||
# @yield [row, index]
|
||||
# @yieldparam [Array] row
|
||||
# @yieldparam [optional, Integer] index
|
||||
#
|
||||
# @since 2.5.0
|
||||
def each_row(&block)
|
||||
0.step(@d.length - 1, @width) { |i|
|
||||
if block.arity > 1
|
||||
yield( @d[i, @width], (i / @width) )
|
||||
else
|
||||
yield( @d[i, @width] )
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# @yield [column, index]
|
||||
# @yieldparam [Array] column
|
||||
# @yieldparam [optional, Integer] index
|
||||
#
|
||||
# @since 2.5.0
|
||||
def each_column(&block)
|
||||
0.upto(@width - 1) { |i|
|
||||
column = []
|
||||
0.upto(@height - 1) { |j|
|
||||
column << @d[i + (j * @width)]
|
||||
}
|
||||
if block.arity > 1
|
||||
yield(column, i)
|
||||
else
|
||||
yield(column)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# @return [Integer] the size of the +Dimension+
|
||||
#
|
||||
# @since 2.5.0
|
||||
def length
|
||||
return @width * @height
|
||||
end
|
||||
alias :size :length
|
||||
|
||||
# @return [Array] converts the Dimension to an array.
|
||||
#
|
||||
# @since 2.5.0
|
||||
def to_a(orient_by_columns = false)
|
||||
arr = []
|
||||
self.each(orient_by_columns) { |value|
|
||||
arr << value
|
||||
}
|
||||
return arr
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def inspect
|
||||
return "#<#{self.class}(#{@width}x#{@height})>"
|
||||
end
|
||||
|
||||
# @return [TT::Dimension]
|
||||
# @since 2.5.0
|
||||
def map
|
||||
new_dim = TT::Dimension.new( self.width, self.height )
|
||||
self.each_with_index { |value, index|
|
||||
new_dim[index] = yield( value )
|
||||
}
|
||||
new_dim
|
||||
end
|
||||
|
||||
# @return [TT::Dimension]
|
||||
# @since 2.5.0
|
||||
def map!
|
||||
@d.map! { |value| yield(value) }
|
||||
self
|
||||
end
|
||||
|
||||
end # class TT::Dimension
|
||||
end
|
||||
@@ -1,120 +0,0 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Caches drawing instructions so complex calculations for generating the
|
||||
# GL data can be reused.
|
||||
#
|
||||
# Redirect all Skethcup::View commands to a DrawCache object and call
|
||||
# #render in a Tool's #draw event.
|
||||
#
|
||||
# @example
|
||||
# class Example
|
||||
# def initialize( model )
|
||||
# @draw_cache = TT::DrawCache.new( model.active_view )
|
||||
# end
|
||||
# def deactivate( view )
|
||||
# @draw_cache.clear
|
||||
# end
|
||||
# def resume( view )
|
||||
# view.invalidate
|
||||
# end
|
||||
# def draw( view )
|
||||
# @draw_cache.render
|
||||
# end
|
||||
# def onLButtonUp( flags, x, y, view )
|
||||
# point = Geom::Point3d.new( x, y, 0 )
|
||||
# view.draw_points( point, 10, 1, 'red' )
|
||||
# view.invalidate
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @since 2.8.0
|
||||
module SpeckleConnector
|
||||
class TT::DrawCache
|
||||
|
||||
# @param [Sketchup::View] view
|
||||
#
|
||||
# @since 2.8.0
|
||||
def initialize( view )
|
||||
@view = view
|
||||
@commands = []
|
||||
end
|
||||
|
||||
# Clears the cache. All drawing instructions are removed.
|
||||
#
|
||||
# @return [Nil]
|
||||
# @since 2.8.0
|
||||
def clear
|
||||
@commands.clear
|
||||
nil
|
||||
end
|
||||
|
||||
# Draws the cached drawing instructions.
|
||||
#
|
||||
# @return [Sketchup::View]
|
||||
# @since 2.8.0
|
||||
def render
|
||||
view = @view
|
||||
for command in @commands
|
||||
view.send( *command )
|
||||
end
|
||||
view
|
||||
end
|
||||
|
||||
# Cache drawing commands and data. These methods received the finsihed
|
||||
# processed drawing data that will be executed when #render is called.
|
||||
[
|
||||
:draw,
|
||||
:draw2d,
|
||||
:draw_line,
|
||||
:draw_lines,
|
||||
:draw_points,
|
||||
:draw_polyline,
|
||||
:draw_text,
|
||||
:drawing_color=,
|
||||
:line_stipple=,
|
||||
:line_width=,
|
||||
:set_color_from_line
|
||||
].each { |symbol|
|
||||
define_method( symbol ) { |*args|
|
||||
@commands << args.unshift( this_method )
|
||||
@commands.size
|
||||
}
|
||||
}
|
||||
|
||||
# Pass through methods to Sketchup::View so that the drawing cache object
|
||||
# can easily replace Sketchup::View objects in existing codes.
|
||||
#
|
||||
# @since 2.8.0
|
||||
def method_missing( *args )
|
||||
view = @view
|
||||
method = args.first
|
||||
if view.respond_to?( method )
|
||||
view.send(*args)
|
||||
else
|
||||
raise NoMethodError, "undefined method `#{method}' for #{self.class.name}"
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.8.0
|
||||
def inspect
|
||||
hex_id = TT.object_id_hex( self )
|
||||
"#<#{self.class.name}:#{hex_id} Commands:#{@commands.size}>"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# http://www.ruby-forum.com/topic/75258#895569
|
||||
def this_method
|
||||
( caller[0] =~ /`([^']*)'/ and $1 ).intern
|
||||
end
|
||||
|
||||
end # class TT::DrawCache
|
||||
end
|
||||
@@ -1,516 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'point3d.rb'
|
||||
require_relative 'progressbar.rb'
|
||||
|
||||
# Collection of Edge methods.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module SpeckleConnector
|
||||
module TT::Edge
|
||||
|
||||
# @param [Array<Geom::Point3d, Geom::Vector3d>, Array<Geom::Point3d, Geom::Point3d>] line
|
||||
# @param [Sketchup::Edge] edge
|
||||
#
|
||||
# @return [Geom::Point3d|Nil]
|
||||
# @since 2.5.0
|
||||
def self.intersect_line_edge(line, edge)
|
||||
point = Geom.intersect_line_line(line, edge.line)
|
||||
return nil if point.nil?
|
||||
return point if self.point_on_edge?(point, edge)
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
# @param [Geom::Point3d] point
|
||||
# @param [Sketchup::Edge] edge
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.point_on_edge?(point, edge)
|
||||
a = edge.start.position
|
||||
b = edge.end.position
|
||||
TT::Point3d.between?(a, b, point, true)
|
||||
end
|
||||
|
||||
end # module TT::Edge
|
||||
|
||||
|
||||
# Collection of methods to manipulate sets of edges.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module TT::Edges
|
||||
|
||||
# @param [Sketchup::Edge] edge1
|
||||
# @param [Sketchup::Edge] edge2
|
||||
#
|
||||
# @return [Sketchup::Vertex|Nil]
|
||||
# @since 2.5.0
|
||||
def self.common_vertex(edge1, edge2)
|
||||
for v1 in edge1.vertices
|
||||
for v2 in edge2.vertices
|
||||
return v1 if v1 == v2
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
# Find continous sets of edges not part of faces. The result is an array of
|
||||
# more arrays. Each sub-array contains a set of sorted edges.
|
||||
#
|
||||
# Sub-arrays may contains arrays of only one edge.
|
||||
#
|
||||
# @param [Array<Sketchup::Entity>] entities
|
||||
#
|
||||
# @return [Array<Array<Sketchup::Edge>>] An +Array+ of Arrays.
|
||||
# @since 2.5.0
|
||||
def self.find_curves( entities, progress_in_ui=false )
|
||||
cache = {} # Use Hash for quick lookups
|
||||
entities.each { |e|
|
||||
next unless e.is_a?( Sketchup::Edge ) && e.faces.empty?
|
||||
cache[e] = e
|
||||
}
|
||||
if progress_in_ui && progress_in_ui.is_a?( TT::Progressbar )
|
||||
progress = progress_in_ui
|
||||
else
|
||||
progress = TT::Progressbar.new( cache, 'Finding curves' )
|
||||
end
|
||||
curves = []
|
||||
until cache.empty?
|
||||
curve = {} # Use Hash for quick lookups
|
||||
stack = [ cache.keys.first ]
|
||||
until stack.empty?
|
||||
# Fetch the next edges in the stack and add to the curve
|
||||
edge = stack.shift
|
||||
curve[edge] = edge
|
||||
cache.delete(edge)
|
||||
progress.next if progress_in_ui
|
||||
# Find next edges
|
||||
for v in edge.vertices
|
||||
vert_edges = v.edges
|
||||
next if vert_edges.size != 2
|
||||
for e in vert_edges
|
||||
stack << e unless curve.key?(e)
|
||||
end
|
||||
end
|
||||
end # until stack.empty?
|
||||
curves << self.sort( curve )
|
||||
end # until cache.empty?
|
||||
curves
|
||||
end
|
||||
|
||||
|
||||
# Finds all sets of edges forming a curve not connected to any other geometry.
|
||||
# Returns an array of sorted curves. (Curve = array of edges)
|
||||
#
|
||||
# @param [Array<Sketchup::Entity>] entities
|
||||
#
|
||||
# @return [Array<Array<Sketchup::Edge>>] An +Array+ of Arrays.
|
||||
# @since 2.5.0
|
||||
def self.find_isolated_curves(entities)
|
||||
source = entities.to_a
|
||||
curves = []
|
||||
until source.empty?
|
||||
entity = source.shift
|
||||
next unless entity.is_a?( Sketchup::Edge )
|
||||
connected = entity.all_connected
|
||||
source -= connected
|
||||
next unless connected.all? { |e| e.is_a?(Sketchup::Edge) }
|
||||
sorted_edges = self.sort( connected )
|
||||
curves << sorted_edges unless sorted_edges.nil?
|
||||
end
|
||||
return curves
|
||||
end
|
||||
|
||||
|
||||
# Attempts to merge colinear edges into one edge. Makes use of SketchUp's own
|
||||
# healing feature.
|
||||
# Based on repair_broken_lines.rb by Carlo Roosen 2004
|
||||
#
|
||||
# If +progress_in_ui+ is true then a +TT::Progressbar+ object is used to
|
||||
# give UI feedback to the user about the process.
|
||||
#
|
||||
# If +progress_in_ui+ is a +TT::Progressbar+ object then that is used instead
|
||||
# and +.next+ is called for each entity.
|
||||
#
|
||||
# @param [Array<Sketchup::Entity>] entities
|
||||
# @param [Boolean|TT::Progressbar] progress_in_ui
|
||||
#
|
||||
# @return [Integer] Number of splits repaired.
|
||||
# @since 2.5.0
|
||||
def self.repair_splits( entities, progress_in_ui=false )
|
||||
temp_edges = []
|
||||
return 0 if entities.length == 0
|
||||
|
||||
parent = entities[0].parent.entities
|
||||
|
||||
if progress_in_ui && progress_in_ui.is_a?( TT::Progressbar )
|
||||
progress = progress_in_ui
|
||||
else
|
||||
progress = TT::Progressbar.new( entities, 'Repairing split edges' )
|
||||
end
|
||||
for e in entities.to_a
|
||||
next unless e.is_a?(Sketchup::Edge)
|
||||
|
||||
progress.next if progress_in_ui
|
||||
|
||||
for vertex in e.vertices
|
||||
next unless vertex.edges.length == 2
|
||||
# (?) Like coplanar faces - can one compare vectors like this? Or do one
|
||||
# have to check if all vertices lie on the same line?
|
||||
v1 = vertex.edges[0].line[1]
|
||||
v2 = vertex.edges[1].line[1]
|
||||
next unless v1.parallel?(v2)
|
||||
# To repair a broken edge a temporary edge is placed at their shared vertex.
|
||||
# This temporary edge is then later erased which causes the two edges to
|
||||
# merge.
|
||||
pt1 = vertex.position
|
||||
pt2 = pt1.clone
|
||||
pt2.x += rand(1000) / 100.0
|
||||
pt2.y += rand(1000) / 100.0
|
||||
pt2.z += rand(1000) / 100.0
|
||||
temp_edge = parent.add_line( pt1, pt2 )
|
||||
temp_edges << temp_edge unless temp_edge.nil?
|
||||
end
|
||||
end
|
||||
|
||||
parent.erase_entities(temp_edges) unless temp_edges.empty?
|
||||
|
||||
temp_edges.size
|
||||
end
|
||||
|
||||
|
||||
# Sorts the given set of edges from start to end. If the edges form a loop
|
||||
# an arbitrary start is picked.
|
||||
#
|
||||
# @todo Comment source
|
||||
#
|
||||
# @param [Array<Sketchup::Edge>] edges
|
||||
#
|
||||
# @return [Array<Sketchup::Edge>] Sorted set of edges.
|
||||
# @since 2.5.0
|
||||
def self.sort( edges )
|
||||
if edges.is_a?( Hash )
|
||||
self.sort_from_hash( edges )
|
||||
elsif edges.is_a?( Enumerable )
|
||||
lookup = {}
|
||||
for edge in edges
|
||||
lookup[edge] = edge
|
||||
end
|
||||
self.sort_from_hash( lookup )
|
||||
else
|
||||
raise ArgumentError, '"edges" argument must be a collection of edges.'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Sorts the given set of edges from start to end. If the edges form a loop
|
||||
# an arbitrary start is picked.
|
||||
#
|
||||
# @param [Hash] edges Sketchup::Edge as keys
|
||||
#
|
||||
# @return [Array<Sketchup::Edge>] Sorted set of edges.
|
||||
# @since 2.5.0
|
||||
def self.sort_from_hash( edges )
|
||||
# Get starting edge - then trace the connected edges from either end.
|
||||
start_edge = edges.keys.first
|
||||
|
||||
# Find the next left and right edge
|
||||
vertices = start_edge.vertices
|
||||
|
||||
left = []
|
||||
for e in vertices.first.edges
|
||||
left << e if e != start_edge && edges[e]
|
||||
end
|
||||
|
||||
right = []
|
||||
for e in vertices.last.edges
|
||||
right << e if e != start_edge && edges[e]
|
||||
end
|
||||
|
||||
return nil if left.size > 1 || right.size > 1 # Check for forks
|
||||
left = left.first
|
||||
right = right.first
|
||||
|
||||
# Sort edges from start to end
|
||||
sorted = [start_edge]
|
||||
|
||||
# Right
|
||||
edge = right
|
||||
until edge.nil?
|
||||
sorted << edge
|
||||
connected = []
|
||||
for v in edge.vertices
|
||||
for e in v.edges
|
||||
connected << e if edges[e] && !sorted.include?(e)
|
||||
end
|
||||
end
|
||||
return nil if connected.size > 1 # Check for forks
|
||||
edge = connected.first
|
||||
end
|
||||
|
||||
# Left
|
||||
unless sorted.include?( left ) # Fix: 2.6.0
|
||||
edge = left
|
||||
until edge.nil?
|
||||
sorted.unshift( edge )
|
||||
connected = []
|
||||
for v in edge.vertices
|
||||
for e in v.edges
|
||||
connected << e if edges[e] && !sorted.include?(e)
|
||||
end
|
||||
end
|
||||
return nil if connected.size > 1 # Check for forks
|
||||
edge = connected.first
|
||||
end
|
||||
end
|
||||
|
||||
sorted
|
||||
end
|
||||
|
||||
|
||||
# @note The first vertex will also appear last if the curve forms a loop.
|
||||
#
|
||||
# Takes a sorted set of edges and returns a sorted set of vertices. Use
|
||||
# +TT::Edges.sort+ to sort a set of edges.
|
||||
#
|
||||
# @param [Array<Sketchup::Edge>] curve Set of sorted edge.
|
||||
#
|
||||
# @return [Array<Sketchup::Vertex>] Sorted set of vertices.
|
||||
# @since 2.5.0
|
||||
def self.sort_vertices(curve)
|
||||
return curve[0].vertices if curve.size <= 1
|
||||
vertices = []
|
||||
# Find the first vertex.
|
||||
common = self.common_vertex( curve[0], curve[1] ) # (?) Errorcheck?
|
||||
vertices << curve[0].other_vertex( common )
|
||||
# Now the rest can be added.
|
||||
curve.each { |edge|
|
||||
vertices << edge.other_vertex(vertices.last) # (?) Errorcheck?
|
||||
}
|
||||
return vertices
|
||||
end
|
||||
|
||||
end # module TT::Edges
|
||||
|
||||
|
||||
# Collection of methods to find and repair small gaps and stray edges.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module TT::Edges::Gaps
|
||||
|
||||
# Pair with the results of +TT::Edges::Gaps.find+.
|
||||
#
|
||||
# @param [Sketchup::Entities] entities +Entities+ collection where the vertex belong to.
|
||||
# @param [Sketchup::Vertex] vertex
|
||||
# @param [Hash] result The returned hash from +TT::Edges::Gaps.find+.
|
||||
# @param [Length] epsilon The max distance for which the gap can be closed.
|
||||
#
|
||||
# @return [Boolean] Returns +true+ if the gap was closed.
|
||||
# @since 2.5.0
|
||||
def self.close( entities, vertex, result, epsilon )
|
||||
# 1. Closest projected open end
|
||||
data = result[:vertex_projected]
|
||||
if data[:dist] && data[:dist][0] + data[:dist][1] < epsilon
|
||||
pt1 = data[:point]
|
||||
pt2 = data[:point2]
|
||||
entities.add_line( vertex.position, pt1 )
|
||||
entities.add_line( pt1, pt2 )
|
||||
return true
|
||||
end
|
||||
# 2. Closest edge
|
||||
data = result[:edge]
|
||||
if data[:dist] && data[:dist] < epsilon
|
||||
entities.add_line( vertex.position, data[:point] )
|
||||
return true
|
||||
end
|
||||
# 3. Closest open vertex
|
||||
data = result[:vertex]
|
||||
if data[:dist] && data[:dist] < epsilon
|
||||
entities.add_line( vertex.position, data[:point] )
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
|
||||
# Tries to connect all open ended edges to other edges if the distance is less
|
||||
# than +epsilon+. If an open-ended edge can't be connected it'll erase it if
|
||||
# its length is less than +epsilon+.
|
||||
#
|
||||
# If +progress_in_ui+ is true then a +TT::Progressbar+ object is used to
|
||||
# give UI feedback to the user about the process.
|
||||
#
|
||||
# If +progress_in_ui+ is a +TT::Progressbar+ object then that is used instead
|
||||
# and +.next+ is called for each entity.
|
||||
#
|
||||
# @param [Sketchup::Entities|Array<Sketchup::Entity>] entities Entities to process.
|
||||
# @param [Length] epsilon The max distance for which the gap can be closed.
|
||||
# @param [Boolean] erase_small_edges If +true+ it will erase all stray edges shorter than +epsilon+.
|
||||
# @param [Boolean|TT::Progressbar] progress_in_ui
|
||||
#
|
||||
# @return [Integer] Returns the number of fixes done.
|
||||
# @since 2.5.0
|
||||
def self.close_all( entities, epsilon, erase_small_edges=false, progress_in_ui=false )
|
||||
fixes = 0
|
||||
return fixes if entities.length == 0
|
||||
context = entities[0].parent.entities
|
||||
edges = entities.select { |e| e.is_a?( Sketchup::Edge ) }
|
||||
small_edges = []
|
||||
end_vertices = self.find_end_vertices( edges )
|
||||
if progress_in_ui && progress_in_ui.is_a?( TT::Progressbar )
|
||||
progress = progress_in_ui
|
||||
else
|
||||
progress = TT::Progressbar.new( end_vertices, 'Closing open ends' )
|
||||
end
|
||||
for v in end_vertices
|
||||
progress.next if progress_in_ui
|
||||
result = self.find(v, end_vertices, edges)
|
||||
closed = self.close( context, v, result, epsilon )
|
||||
fixes += 1 if closed
|
||||
if !closed && erase_small_edges
|
||||
edge = v.edges.first
|
||||
if edge.length < epsilon
|
||||
small_edges << edge
|
||||
fixes += 1
|
||||
end
|
||||
end
|
||||
end # for
|
||||
Sketchup.status_text = 'Erasing small edges...' if progress_in_ui
|
||||
if erase_small_edges && !small_edges.empty?
|
||||
context.erase_entities( small_edges )
|
||||
end
|
||||
fixes
|
||||
end
|
||||
|
||||
|
||||
# Finds possible connections for +vertex+ within the set of +open_ends+ and
|
||||
# +edges+.
|
||||
#
|
||||
# The returned hash contains three keys:
|
||||
# * +:vertex_projected+ - the closest connection by extending two edges towards each other.
|
||||
# * +:vertex+ - the closest open-end vertex
|
||||
# * +:edge+ - the closest edge
|
||||
#
|
||||
# Each value contains another hash with the following keys:
|
||||
# * +:dist+ the distance from +vertex+ to +:point+
|
||||
# * +:point+ the point which +vertex+ can be extended to.
|
||||
# * +:point2+ Only availible to +:vertex_projected+. The origin of the end vertex
|
||||
# of the other edge. In this case +:point+ is the intersecting point where
|
||||
# the two edges meet.
|
||||
#
|
||||
# @param [Sketchup::Vertex] vertex The open-end vertex to find connections for.
|
||||
# @param [Array<Sketchup::Vertex>] open_ends Set of availible open-end vertices.
|
||||
# @param [Array<Sketchup::Edge>] edges Set of edges to connect to.
|
||||
#
|
||||
# @return [Hash] Returns a hash with possible connection options.
|
||||
# @since 2.5.0
|
||||
def self.find( vertex, open_ends, edges )
|
||||
origin = vertex.position
|
||||
edge = vertex.edges.first
|
||||
other = edge.other_vertex( vertex ).position
|
||||
vector = origin.vector_to( other )
|
||||
line = edge.line
|
||||
|
||||
ends = open_ends - [ vertex, edge.other_vertex( vertex ) ]
|
||||
end_edges = ends.map { |v| v.edges.first }
|
||||
|
||||
distances = {}
|
||||
ends.each { |v|
|
||||
distances[v] = origin.distance( v.position )
|
||||
}
|
||||
|
||||
# 1. Closest projected open end
|
||||
projected_vertices = {}
|
||||
projected_vertices_pt = {}
|
||||
for v in ends
|
||||
e = v.edges.first
|
||||
pt = Geom::intersect_line_line( line, e.line )
|
||||
next if pt.nil?
|
||||
|
||||
direction = origin.vector_to(pt)
|
||||
next unless direction.valid?
|
||||
next if direction.samedirection?(vector)
|
||||
|
||||
projected_vertices[v] = [
|
||||
origin.distance( pt ),
|
||||
pt.distance( v.position )
|
||||
]
|
||||
projected_vertices_pt[v] = pt
|
||||
end
|
||||
closest_projected_vertex = projected_vertices.keys.min { |a,b|
|
||||
va = projected_vertices[a]
|
||||
vb = projected_vertices[b]
|
||||
if va[0] == vb[0]
|
||||
va[1] <=> vb[1]
|
||||
else
|
||||
va[0] <=> vb[0]
|
||||
end
|
||||
}
|
||||
pt = projected_vertices_pt[closest_projected_vertex]
|
||||
tmp = projected_vertices[closest_projected_vertex]
|
||||
dist = (tmp.nil?) ? nil : tmp[0]
|
||||
result = {}
|
||||
result[:vertex_projected] = { :dist => tmp, :point => pt, :point2 => closest_projected_vertex }
|
||||
|
||||
# 3. Closest open vertex
|
||||
closest_open_vertex = ends.min { |a,b|
|
||||
distances[a] <=> distances[b]
|
||||
}
|
||||
pt = (closest_open_vertex) ? closest_open_vertex.position : nil
|
||||
dist = distances[closest_open_vertex]
|
||||
result[:vertex] = { :dist => dist, :point => pt }
|
||||
|
||||
# 2. Closest edge
|
||||
edge_distances = {}
|
||||
edge_distances_pt = {}
|
||||
for e in edges
|
||||
next if e == edge
|
||||
pt = Geom::intersect_line_line( line, e.line )
|
||||
next if pt.nil?
|
||||
#next if pt == other
|
||||
pt1, pt2 = e.vertices.map { |v| v.position }
|
||||
next unless TT::Point3d.between?( pt1, pt2, pt, true )
|
||||
v = origin.vector_to(pt)
|
||||
next unless v.valid?
|
||||
next if v.samedirection?(vector)
|
||||
edge_distances[e] = origin.distance( pt )
|
||||
edge_distances_pt[e] = pt
|
||||
end
|
||||
closest_edge = edge_distances.keys.min { |a,b|
|
||||
edge_distances[a] <=> edge_distances[b]
|
||||
}
|
||||
pt = edge_distances_pt[closest_edge]
|
||||
dist = edge_distances[closest_edge]
|
||||
result[:edge] = { :dist => dist, :point => pt }
|
||||
|
||||
result
|
||||
end # def
|
||||
|
||||
|
||||
# Finds all open-ended edges in +entities+ and returns an array of vertices
|
||||
# for each open end.
|
||||
#
|
||||
# @param [Sketchup::Entities|Array<Sketchup::Entity>] entities
|
||||
#
|
||||
# @return [Array<Sketchup::Vertex>]
|
||||
# @since 2.5.0
|
||||
def self.find_end_vertices( entities )
|
||||
vertices = []
|
||||
for e in entities
|
||||
next unless e.is_a?( Sketchup::Edge )
|
||||
open_ends = e.vertices.select { |v| v.edges.length == 1 }
|
||||
vertices.concat( open_ends )
|
||||
end
|
||||
vertices
|
||||
end
|
||||
|
||||
end # module TT::Edges::Gaps
|
||||
end
|
||||
@@ -1,164 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'instance.rb'
|
||||
|
||||
# Collection of Entities methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT::Entities
|
||||
|
||||
|
||||
# Returns a boundingbox for all the given entities.
|
||||
#
|
||||
# @param [Enumerable] entities
|
||||
#
|
||||
# @return [Geom::BoundingBox]
|
||||
# @since 2.1.0
|
||||
def self.bounds(entities)
|
||||
bb = Geom::BoundingBox.new
|
||||
for e in entities
|
||||
next unless e.respond_to?(:bounds)
|
||||
bb.add( e.bounds )
|
||||
end
|
||||
return bb
|
||||
end
|
||||
|
||||
|
||||
# Counts all unique +Sketchup::Entities+ collections in the given context,
|
||||
# including sub-entities.
|
||||
#
|
||||
# @param [Enumerable] context
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @return [Integer]
|
||||
# @since 2.5.0
|
||||
def self.count_unique_entities( context, options={} )
|
||||
c = 0
|
||||
entities = nil # Init variables for speed
|
||||
self.each_entities( context, options={} ) { |entities| c = c.next }
|
||||
c
|
||||
end
|
||||
|
||||
|
||||
# Counts all unique entities in the given collection, including sub-entities.
|
||||
#
|
||||
# @param [Enumerable] context
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @return [Integer]
|
||||
# @since 2.5.0
|
||||
def self.count_unique_entity( context, options={} )
|
||||
c = context.length
|
||||
entities = nil # Init variables for speed
|
||||
self.each_entities( context, options={} ) { |entities|
|
||||
c += entities.length
|
||||
}
|
||||
c
|
||||
end
|
||||
|
||||
|
||||
# Yields each unique Entities collection recursivly.
|
||||
#
|
||||
# TT::Entities.each_entities { |entities|
|
||||
# processEntities( entities )
|
||||
# }
|
||||
#
|
||||
# If a number is returned to the processing block it will be used to add up a
|
||||
# total when +each_entities+ returns.
|
||||
#
|
||||
# TT::Entities.each_entities { |entities|
|
||||
# c = 0
|
||||
# for e in entities
|
||||
# c += 1 if e.is_a?( SketchUp::Edge )
|
||||
# end
|
||||
# c
|
||||
# }
|
||||
#
|
||||
# This example will return the total number of edges processed. Use to keep
|
||||
# statistic for the iteration.
|
||||
#
|
||||
# @param [Enumerable] context
|
||||
# @param [Hash] processed_definitions Hash index of processed entities.
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @yield [entities]
|
||||
# @yieldparam [Enumerable|Sketchup::Entities] entities
|
||||
#
|
||||
# @return [Integer] Returns
|
||||
# @since 2.5.0
|
||||
def self.each_entities( context, processed_definitions={}, options={}, &block )
|
||||
skip_locked = options[:locked] && options[:locked].is_a?( Hash )
|
||||
c = 0
|
||||
result = yield( context )
|
||||
c += result if result.is_a?( Numeric )
|
||||
# Process Groups and ComponentInstances
|
||||
for e in context.to_a
|
||||
next unless e.valid? && TT::Instance.is?( e )
|
||||
d = TT::Instance.definition( e )
|
||||
if processed_definitions[d].nil?
|
||||
processed_definitions[d] = true
|
||||
next if skip_locked && options[:locked].key?(d)
|
||||
result = self.each_entities( d.entities, processed_definitions, options, &block )
|
||||
c += result if result.is_a?( Numeric )
|
||||
end
|
||||
end
|
||||
c
|
||||
end
|
||||
|
||||
|
||||
# Yields each entity recursivly.
|
||||
def self.each_entity( entities, processed_definitions={}, options={}, &block )
|
||||
skip_locked = options[:locked] && options[:locked].is_a?( Hash )
|
||||
c = 0
|
||||
for e in entities.to_a
|
||||
next if skip_locked && e.respond_to?( :locked? ) && e.locked?
|
||||
c = c.next if yield( e )
|
||||
# Process Groups and ComponentInstances
|
||||
next unless e.valid? && TT::Instance.is?( e )
|
||||
d = TT::Instance.definition( e )
|
||||
if processed_definitions[d].nil?
|
||||
processed_definitions[d] = true
|
||||
next if skip_locked && options[:locked].key?(d)
|
||||
c += self.each_entity( d.entities, processed_definitions, options, &block )
|
||||
end
|
||||
end # for
|
||||
c
|
||||
end
|
||||
|
||||
|
||||
# Collects the 3d positions of the vertices in the given entities collection.
|
||||
# Processes child Groups and Components recursivly.
|
||||
#
|
||||
# @param [Enumerable] entities
|
||||
# @param [Geom::Transformation] parent_transformation
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.0.0
|
||||
def self.positions(entities, parent_transformation = Geom::Transformation.new)
|
||||
pts = []
|
||||
vertices = []
|
||||
for e in entities
|
||||
if e.respond_to?(:vertices)
|
||||
vertices << e.vertices
|
||||
elsif TT::Instance.is?( e )
|
||||
d = TT::Instance.definition(e)
|
||||
t = parent_transformation * e.transformation
|
||||
sub_pts = self.positions(d.entities, t)
|
||||
pts.concat( sub_pts )
|
||||
end
|
||||
end # for
|
||||
vertices.flatten!
|
||||
vertices.uniq!
|
||||
pts.concat( vertices.map { |v| v.position.transform( parent_transformation ) } )
|
||||
pts
|
||||
end
|
||||
|
||||
end # module TT::Entities
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
ID=c03a2b93-3365-4ef1-95f4-f35158757622
|
||||
VERSION_ID=0c8388c8-299b-44aa-9584-877446f65403
|
||||
@@ -1,130 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'geom3d.rb'
|
||||
require_relative 'uvq.rb'
|
||||
|
||||
# Collection of Face methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT::Face
|
||||
|
||||
# Returns all vertices in the face's outer loop that isn't connecting colinear
|
||||
# edges.
|
||||
#
|
||||
# @param [Sketchup::Face] face
|
||||
#
|
||||
# @return [Array<Sketchup::Vertex>]
|
||||
# @since 2.5.0
|
||||
def self.corners(face)
|
||||
corners = []
|
||||
# We only check the outer loop, ignoring interior lines.
|
||||
face.outer_loop.edgeuses.each { |eu|
|
||||
# Ignore vertices that's between co-linear edges.
|
||||
v1 = eu.edge.line[1]
|
||||
v2 = eu.next.edge.line[1]
|
||||
if v1.valid? && v2.valid? && !v1.parallel?( v2 )
|
||||
#unless v1.parallel?( v2 )
|
||||
#unless eu.edge.line[1].parallel?( eu.next.edge.line[1] )
|
||||
# Find which vertex is shared between the two edges.
|
||||
# (?) TT::Edges.common_vertex( eu.edge, eu.next.edge )
|
||||
if eu.edge.start.used_by?(eu.next.edge)
|
||||
corners << eu.edge.start
|
||||
else
|
||||
corners << eu.edge.end
|
||||
end
|
||||
end
|
||||
}
|
||||
return corners
|
||||
end
|
||||
|
||||
|
||||
# @param [Sketchup::Face] face
|
||||
#
|
||||
# @return [Boolean] Returns true if the face has four corners.
|
||||
# @since 2.5.0
|
||||
def self.is_quad?( face )
|
||||
face.is_a?( Sketchup::Face ) &&
|
||||
self.corners( face ).length == 4
|
||||
end
|
||||
|
||||
|
||||
# @param [Sketchup::Face] face
|
||||
# @param [Sketchup::TextureWriter] texture_writer
|
||||
# @param [Boolean] front_to_back
|
||||
#
|
||||
# @return [Boolean] Returns true if the face has four corners.
|
||||
# @since 2.5.0
|
||||
def self.mirror_material(face, texture_writer, front_to_back=true)
|
||||
# Get the material to mirror
|
||||
material = (front_to_back) ? face.material : face.back_material
|
||||
# Plain colour and Default is simply mirrored
|
||||
if material.nil? || material.materialType < 1
|
||||
if front_to_back
|
||||
face.back_material = material
|
||||
else
|
||||
face.material = material
|
||||
end
|
||||
return
|
||||
end
|
||||
# Get four Point3d samples from the face's plane.
|
||||
# We take one point from the face and offset that in X and Y
|
||||
# on the face's plane. This ensures we get enough data.
|
||||
# Previously I sampled data from vertices, which lead to problems
|
||||
# for triangles with distorted textures. Distorted textures require
|
||||
# four UV points.
|
||||
samples = []
|
||||
samples << face.vertices[0].position # 0,0 | Origin
|
||||
samples << samples[0].offset(face.normal.axes.x) # 1,0 | Offset Origin in X
|
||||
samples << samples[0].offset(face.normal.axes.y) # 0,1 | Offset Origin in Y
|
||||
samples << samples[1].offset(face.normal.axes.y) # 1,1 | Offset X in Y
|
||||
# Arrays containing 3D and UV points.
|
||||
xyz = []
|
||||
uv = []
|
||||
uvh = face.get_UVHelper(true, true, texture_writer)
|
||||
samples.each { |position|
|
||||
# XYZ 3D coordinates
|
||||
xyz << position
|
||||
# UV 2D coordinates
|
||||
if front_to_back
|
||||
uvq = uvh.get_front_UVQ(position)
|
||||
else
|
||||
uvq = uvh.get_back_UVQ(position)
|
||||
end
|
||||
uv << TT::UVQ.normalize(uvq)
|
||||
}
|
||||
# Position texture.
|
||||
pts = []
|
||||
(0..3).each { |i|
|
||||
pts << xyz[i]
|
||||
pts << uv[i]
|
||||
}
|
||||
face.position_material(material, pts, !front_to_back)
|
||||
nil
|
||||
end
|
||||
|
||||
end # module TT::Face
|
||||
|
||||
|
||||
# Collection of methods for sets of faces.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module TT::Faces
|
||||
|
||||
# @param [Sketchup::Face] face1
|
||||
# @param [Sketchup::Face] face2
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.coplanar?(face1, face2)
|
||||
TT::Geom3d.planar_points?( face1.vertices + face2.vertices )
|
||||
end
|
||||
|
||||
end # module TT::Faces
|
||||
end
|
||||
@@ -1,211 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'point3d.rb'
|
||||
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT::Geom3d
|
||||
|
||||
# Returns +plane+ in the format +[ point3d, vector3d ]+.
|
||||
#
|
||||
# @param [Array<Geom::Point3d, Geom::Vector3d>, Array<Number, Number, Number, Number>] plane
|
||||
#
|
||||
# @return [Array<Geom::Point3d, Geom::Vector3d>]
|
||||
# @since 2.0.0
|
||||
def self.normalize_plane(plane)
|
||||
return plane if plane.length == 2
|
||||
a, b, c, d = plane
|
||||
v = Geom::Vector3d.new(a,b,c)
|
||||
p = ORIGIN.offset(v.reverse, d)
|
||||
return [p, v]
|
||||
end
|
||||
|
||||
|
||||
# Check if all points in the array are on the same plane.
|
||||
#
|
||||
# @todo Ensure that the points are not co-linear. (Edge case)
|
||||
#
|
||||
# @param [Array<Geom::Point3d|Sketchup::Vertex>] points
|
||||
#
|
||||
# @since 2.0.0
|
||||
def self.planar_points?(points)
|
||||
points = TT::Point3d.extend_all( points )
|
||||
points.uniq!
|
||||
return false if points.size < 3
|
||||
plane = Geom.fit_plane_to_points( points )
|
||||
points.all? { |pt| pt.on_plane?( plane ) }
|
||||
end
|
||||
|
||||
|
||||
# Creates a set of +Geom::Point3d+ objects for an arc.
|
||||
#
|
||||
# @param [Geom::Point3d] center
|
||||
# @param [Geom::Vector3d] xaxis
|
||||
# @param [Geom::Vector3d] normal
|
||||
# @param [Number] radius
|
||||
# @param [Float] start_angle in radians
|
||||
# @param [Float] end_angle in radians
|
||||
# @param [Integer] num_segments
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.0.0
|
||||
def self.arc(center, xaxis, normal, radius, start_angle, end_angle, num_segments = 12)
|
||||
# Generate the first point.
|
||||
t = Geom::Transformation.rotation(center, normal, start_angle )
|
||||
points = []
|
||||
points << center.offset(xaxis, radius).transform(t)
|
||||
# Prepare a transformation we can repeat on the last entry in point to complete the arc.
|
||||
t = Geom::Transformation.rotation(center, normal, (end_angle - start_angle) / num_segments )
|
||||
1.upto(num_segments) { |i|
|
||||
points << points.last.transform(t)
|
||||
}
|
||||
return points
|
||||
end
|
||||
|
||||
|
||||
# @see http://en.wikipedia.org/wiki/Circle
|
||||
# @see http://en.wikipedia.org/wiki/Unit_Circle
|
||||
#
|
||||
# @param [Geom::Point3d] center
|
||||
# @param [Geom::Vector3d] xaxis
|
||||
# @param [Numeric] radius
|
||||
# @param [Numeric] start_angle
|
||||
# @param [Numeric] end_angle
|
||||
# @param [Integer] segments
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.7.0
|
||||
def self.arc2d( center, xaxis, radius, start_angle, end_angle, segments = 24 )
|
||||
full_angle = end_angle - start_angle
|
||||
segment_angle = full_angle / segments
|
||||
t = Geom::Transformation.axes( center, xaxis, xaxis * Z_AXIS, Z_AXIS )
|
||||
arc = []
|
||||
(0..segments).each { |i|
|
||||
angle = start_angle + (segment_angle * i)
|
||||
x = radius * Math.cos(angle)
|
||||
y = radius * Math.sin(angle)
|
||||
arc << Geom::Point3d.new(x,y,0).transform!(t)
|
||||
}
|
||||
arc
|
||||
end
|
||||
|
||||
|
||||
# Creates a set of +Geom::Point3d+ objects for an circle.
|
||||
#
|
||||
# @param [Geom::Point3d] center
|
||||
# @param [Geom::Vector3d] normal
|
||||
# @param [Number] radius
|
||||
# @param [Integer] num_segments
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.0.0
|
||||
def self.circle(center, normal, radius, num_segments)
|
||||
points = self.arc(center, normal.axes.x, normal, radius, 0.0, Math::PI * 2, num_segments)
|
||||
points.pop
|
||||
return points
|
||||
end
|
||||
|
||||
|
||||
# @param [Geom::Point3d] center
|
||||
# @param [Geom::Vector3d] xaxis
|
||||
# @param [Numeric] radius
|
||||
# @param [Integer] segments
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.7.0
|
||||
def self.circle2d( center, xaxis, radius, segments = 24 )
|
||||
segments = segments.to_i
|
||||
angle = 360.degrees - ( 360.degrees / segments )
|
||||
self.arc2d( center, xaxis, radius, 0, angle, segments - 1 )
|
||||
end
|
||||
|
||||
|
||||
# Calculates the number of segments in an arc given the segments of a full circle. This
|
||||
# will give a close visual quality of the arcs and circles.
|
||||
#
|
||||
# @param [Float] angle in radians
|
||||
# @param [Integer] full_circle_segments
|
||||
# @param [Boolean] force_even useful to ensure the segmented arc's
|
||||
# apex hits the apex of the real arc
|
||||
#
|
||||
# @return [Integer]
|
||||
# @since 2.0.0
|
||||
def self.arc_segments(angle, full_circle_segments, force_even = false)
|
||||
segments = (full_circle_segments * (angle.abs / (Math::PI * 2))).to_i
|
||||
segments += 1 if force_even && segments % 2 > 0 # if odd
|
||||
segments = 1 if segments < 1
|
||||
return segments
|
||||
end
|
||||
|
||||
|
||||
# Evenly distribute a fixed number of points on a sphere.
|
||||
# http://www.cgafaq.info/wiki/Evenly_distributed_points_on_sphere
|
||||
#
|
||||
# @param [Integer] number_of_points
|
||||
# @param [Geom::Point3d] origin
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.3.0
|
||||
def self.spiral_sphere( number_of_points, origin=ORIGIN )
|
||||
t = Geom::Transformation.new( origin )
|
||||
n = number_of_points
|
||||
node = Array.new( n )
|
||||
dlong = Math::PI * (3-Math.sqrt(5))
|
||||
dz = 2.0/n
|
||||
long = 0
|
||||
z = 1 - dz/2
|
||||
(0...n).each { |k|
|
||||
r = Math.sqrt( 1-z*z )
|
||||
pt = Geom::Point3d.new( Math.cos(long)*r, Math.sin(long)*r, z )
|
||||
node[k] = pt.transform( t )
|
||||
z = z - dz
|
||||
long = long + dlong
|
||||
}
|
||||
node
|
||||
end
|
||||
|
||||
|
||||
# @param [Geom::Point3d] point1
|
||||
# @param [Geom::Point3d] point2
|
||||
# @param [Integer] subdivs The number of resulting segments
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.5.0
|
||||
def self.interpolate_linear(point1, point2, subdivs)
|
||||
step = 1.0 / subdivs
|
||||
pts = []
|
||||
(0..subdivs).each { |i|
|
||||
r = step * i
|
||||
pts << Geom::linear_combination( r, point1, 1.0 - r, point2 )
|
||||
}
|
||||
pts
|
||||
end
|
||||
|
||||
|
||||
# @param [Array<Geom::Point3d>] points
|
||||
#
|
||||
# @return [Geom::Point3d]
|
||||
# @since 2.5.0
|
||||
def self.average_point(points)
|
||||
average = Geom::Point3d.new(0,0,0)
|
||||
return average if points.empty?
|
||||
number_of_points = points.length
|
||||
for pt in points
|
||||
average.x += pt.x
|
||||
average.y += pt.y
|
||||
average.z += pt.z
|
||||
end
|
||||
average.x = average.x / number_of_points
|
||||
average.y = average.y / number_of_points
|
||||
average.z = average.z / number_of_points
|
||||
average
|
||||
end
|
||||
|
||||
end # module TT::Geom3D
|
||||
end
|
||||
@@ -1,561 +0,0 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'bezier.rb'
|
||||
require_relative 'geom3d.rb'
|
||||
|
||||
# @note Alpha stage. Very likely to be subject to change!
|
||||
#
|
||||
# Read .gli file into an array of drawing instructions.
|
||||
# The instructions is an array of method name and arguments.
|
||||
#
|
||||
# POLYGON [10,0],[14,4],[6,4]
|
||||
#
|
||||
# becomes
|
||||
#
|
||||
# [ :draw2d, [ Point3d(10,0,0), Point3d(14,4,0), Point3d(6,4,0) ] ]
|
||||
#
|
||||
# This translate to arguments that is sent to the View object by using the
|
||||
# .send method.
|
||||
#
|
||||
# = GL Image Format
|
||||
#
|
||||
# == Data Types
|
||||
#
|
||||
# <color>
|
||||
# * red <int>
|
||||
# * green <int>
|
||||
# * blue <int>
|
||||
# * alpha <int> (Default = 255)
|
||||
#
|
||||
# <point>
|
||||
# * x <float>
|
||||
# * y <float>
|
||||
#
|
||||
# == Instructions
|
||||
#
|
||||
# COLOR
|
||||
# * color <color>
|
||||
#
|
||||
# WIDTH
|
||||
# * width <int>
|
||||
#
|
||||
# STIPPLE
|
||||
# * width <string>
|
||||
#
|
||||
# POINTS
|
||||
# * type <string>
|
||||
# TRIANGLE_OPEN
|
||||
# TRIANGLE_FILLED
|
||||
# SQUARE_OPEN
|
||||
# SQUARE_FILLED
|
||||
# CIRCLE, O
|
||||
# DISC, *
|
||||
# PLUS, +
|
||||
# CROSS, X
|
||||
# * size <int>
|
||||
# * points <point>+
|
||||
#
|
||||
# LINES
|
||||
# * points <point>+
|
||||
#
|
||||
# POLYLINE
|
||||
# * points <point>+
|
||||
#
|
||||
# BEZIER
|
||||
# * points <point>+
|
||||
#
|
||||
# LOOP
|
||||
# * points <point>+
|
||||
#
|
||||
# ARC
|
||||
# * center <point>
|
||||
# * radius <float>
|
||||
# * degrees <float>
|
||||
# * segments <int>
|
||||
# * start_angle <float> (Default = 0.0)
|
||||
#
|
||||
# CIRCLE
|
||||
# * center <point>
|
||||
# * radius <float>
|
||||
# * segments <int>
|
||||
#
|
||||
# RECT
|
||||
# * left <float>
|
||||
# * top <float>
|
||||
# * width <float>
|
||||
# * height <float>
|
||||
# * corner_radius <float> (Default = 0.0)
|
||||
#
|
||||
# POLYGON
|
||||
# * points <point>+
|
||||
#
|
||||
# PIE
|
||||
# * center <point>
|
||||
# * radius <float>
|
||||
# * degrees <float>
|
||||
# * segments <int>
|
||||
# * start_angle <float> (Default = 0.0)
|
||||
#
|
||||
# DISC
|
||||
# * center <point>
|
||||
# * radius <float>
|
||||
# * segments <int>
|
||||
#
|
||||
# GRADIENT
|
||||
# * left <float>
|
||||
# * top <float>
|
||||
# * width <float>
|
||||
# * height <float>
|
||||
# * steps <int>
|
||||
# * horizontal <bool>
|
||||
# * start_color <color>
|
||||
# * end_color <color>
|
||||
#
|
||||
# @since 2.7.0
|
||||
|
||||
module SpeckleConnector
|
||||
class TT::GL_Image
|
||||
|
||||
# http://www.songho.ca/opengl/index.html
|
||||
# http://www.songho.ca/opengl/gl_tessellation.html
|
||||
|
||||
# rescue TT::GL_Image::ParseError => e
|
||||
#
|
||||
# @since 2.7.0
|
||||
class ParseError < StandardError; end
|
||||
|
||||
# @param [String] gl_image_file
|
||||
#
|
||||
# @since 2.7.0
|
||||
def initialize( gl_image_file )
|
||||
unless File.exist?( gl_image_file )
|
||||
raise( ArgumentError, "GL Image '#{gl_image_file}' not found." )
|
||||
end
|
||||
@file = gl_image_file
|
||||
@instructions = read_file( gl_image_file )
|
||||
end
|
||||
|
||||
# @param [Sketchup::View] view
|
||||
# @param [Integer] x
|
||||
# @param [Integer] y
|
||||
#
|
||||
# @since 2.7.0
|
||||
def draw( view, x, y )
|
||||
# Reset viewport
|
||||
view.line_width = 1
|
||||
view.line_stipple = ''
|
||||
view.drawing_color = [0,0,0]
|
||||
# Process drawing instructions. Offset drawing by given coordinates.
|
||||
offset = Geom::Vector3d.new( x, y, 0 )
|
||||
for instruction in @instructions
|
||||
command, arguments = instruction
|
||||
positioned_arguments = arguments.map { |argument|
|
||||
if argument.is_a?( Geom::Point3d )
|
||||
argument.transform( offset )
|
||||
else
|
||||
argument
|
||||
end
|
||||
}
|
||||
begin
|
||||
view.send( command, *positioned_arguments )
|
||||
rescue => e
|
||||
p instruction
|
||||
raise( e )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def inspect
|
||||
hex_id = TT.object_id_hex( self )
|
||||
"#<#{self.class.name}:#{hex_id}>"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param [String] gl_image_file
|
||||
#
|
||||
# @return [Array]
|
||||
# @since 2.7.0
|
||||
def read_file( gl_image_file )
|
||||
offset = Geom::Point3d.new( 0, 0, 0 )
|
||||
odd_width = true # Default width = 1.0
|
||||
line_width = 1
|
||||
instructions = []
|
||||
File.open( gl_image_file, 'r' ) { |file|
|
||||
file.each_line { |line|
|
||||
#next if line[0,1] == '#'
|
||||
next if line[0] == 35 # Is it faster to check for single char like this?
|
||||
data = decode_line( line )
|
||||
command = data.shift
|
||||
case command
|
||||
when nil
|
||||
# Skip
|
||||
when 'OFFSET'
|
||||
x, y = data
|
||||
offset = Geom::Point3d.new( x, y, 0 )
|
||||
when 'COLOR'
|
||||
add_instruction( instructions, :drawing_color=, data )
|
||||
when 'WIDTH'
|
||||
line_width = data[0]
|
||||
odd_width = data[0] % 2 == 1
|
||||
add_instruction( instructions, :line_width=, data )
|
||||
when 'STIPPLE'
|
||||
add_instruction( instructions, :line_stipple=, data )
|
||||
when 'POINTS'
|
||||
style = data.shift
|
||||
size = data.shift
|
||||
r = size / 2
|
||||
points = []
|
||||
case style
|
||||
when 'SQUARE_OPEN'
|
||||
for point in data
|
||||
x1, y1 = point.to_a.map { |i| i - r }
|
||||
x2 = x1 + size
|
||||
y2 = y1 + size
|
||||
|
||||
points << Geom::Point3d.new( x1, y1, 0 )
|
||||
points << Geom::Point3d.new( x2, y1, 0 )
|
||||
|
||||
points << points.last
|
||||
points << Geom::Point3d.new( x2, y2, 0 )
|
||||
|
||||
points << points.last
|
||||
points << Geom::Point3d.new( x1, y2, 0 )
|
||||
|
||||
points << points.last
|
||||
points << points[-7]
|
||||
end
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINES )
|
||||
when 'SQUARE_FILLED'
|
||||
for point in data
|
||||
x1, y1 = point.to_a.map { |i| i - r }
|
||||
x2 = x1 + size
|
||||
y2 = y1 + size
|
||||
points << Geom::Point3d.new( x1, y1, 0 )
|
||||
points << Geom::Point3d.new( x2, y1, 0 )
|
||||
points << Geom::Point3d.new( x2, y2, 0 )
|
||||
points << Geom::Point3d.new( x1, y2, 0 )
|
||||
end
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_QUADS )
|
||||
when 'TRIANGLE_OPEN'
|
||||
for point in data
|
||||
x, y = point.to_a
|
||||
points << Geom::Point3d.new( x, y - r, 0 )
|
||||
points << Geom::Point3d.new( x - r, y + r, 0 )
|
||||
|
||||
points << points.last
|
||||
points << Geom::Point3d.new( x + r, y + r, 0 )
|
||||
|
||||
points << points.last
|
||||
points << points[-5]
|
||||
end
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINES )
|
||||
when 'TRIANGLE_FILLED'
|
||||
for point in data
|
||||
x, y = point.to_a
|
||||
points << Geom::Point3d.new( x, y - r, 0 )
|
||||
points << Geom::Point3d.new( x - r, y + r, 0 )
|
||||
points << Geom::Point3d.new( x + r, y + r, 0 )
|
||||
end
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_TRIANGLES )
|
||||
when 'PLUS', '+'
|
||||
for point in data
|
||||
x, y = point.to_a
|
||||
points << Geom::Point3d.new( x, y - r, 0 )
|
||||
points << Geom::Point3d.new( x, y + r, 0 )
|
||||
points << Geom::Point3d.new( x - r, y, 0 )
|
||||
points << Geom::Point3d.new( x + r, y, 0 )
|
||||
end
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINES )
|
||||
when 'CROSS', 'X'
|
||||
for point in data
|
||||
x, y = point.to_a
|
||||
points << Geom::Point3d.new( x - r, y - r, 0 )
|
||||
points << Geom::Point3d.new( x + r, y + r, 0 )
|
||||
points << Geom::Point3d.new( x + r, y - r, 0 )
|
||||
points << Geom::Point3d.new( x - r, y + r, 0 )
|
||||
end
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINES )
|
||||
when 'DISC', '*'
|
||||
for point in data
|
||||
points = TT::Geom3d.circle( point, Z_AXIS, r, 24 )
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_POLYGON )
|
||||
end
|
||||
when 'CIRCLE', 'O'
|
||||
for point in data
|
||||
points = TT::Geom3d.circle( point, Z_AXIS, r, 24 )
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_LOOP )
|
||||
end
|
||||
else
|
||||
raise( ParseError, "Invalid point style. (#{style})" )
|
||||
end
|
||||
when 'LINES'
|
||||
points = data
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINES )
|
||||
when 'POLYLINE'
|
||||
points = data
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_STRIP )
|
||||
when 'LOOP'
|
||||
points = data
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_LOOP )
|
||||
when 'FRAME'
|
||||
x, y, w, h, r = data # X, Y, Width, Height, Radius
|
||||
o = line_width / 2.0 # Because of this - don't use off_width.
|
||||
points = []
|
||||
if r
|
||||
x_axis = X_AXIS.reverse
|
||||
z = Z_AXIS
|
||||
# Account for line width.
|
||||
ro = r - o
|
||||
# Top Left
|
||||
c = Geom::Point3d.new( x + r, y + r, 0 )
|
||||
arc = TT::Geom3d.arc( c, x_axis, z, ro, 0, 90.degrees, 8 )
|
||||
points.concat( arc )
|
||||
# Top Right
|
||||
c = Geom::Point3d.new( x + w - r, y + r, 0 )
|
||||
arc = TT::Geom3d.arc( c, x_axis, z, ro, 90.degrees, 180.degrees, 8 )
|
||||
points.concat( arc )
|
||||
# Bottom Right
|
||||
c = Geom::Point3d.new( x + w - r, y + h - r, 0 )
|
||||
arc = TT::Geom3d.arc( c, x_axis, z, ro, 180.degrees, 270.degrees, 8 )
|
||||
points.concat( arc )
|
||||
# Bottom Right
|
||||
c = Geom::Point3d.new( x + r, y + h - r, 0 )
|
||||
arc = TT::Geom3d.arc( c, x_axis, z, ro, 270.degrees, 360.degrees, 8 )
|
||||
points.concat( arc )
|
||||
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_LOOP )
|
||||
else
|
||||
points << Geom::Point3d.new( x, y + o, 0 )
|
||||
points << Geom::Point3d.new( x + w, y + o, 0 )
|
||||
|
||||
points << Geom::Point3d.new( x + w - o, y, 0 )
|
||||
points << Geom::Point3d.new( x + w - o, y + h, 0 )
|
||||
|
||||
points << Geom::Point3d.new( x + w, y + h - o, 0 )
|
||||
points << Geom::Point3d.new( x, y + h - o, 0 )
|
||||
|
||||
points << Geom::Point3d.new( x + o, y + h, 0 )
|
||||
points << Geom::Point3d.new( x + o, y, 0 )
|
||||
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINES )
|
||||
end
|
||||
when 'BEZIER'
|
||||
points = TT::Geom3d::Bezier.points( data, 8 * data.size )
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_STRIP )
|
||||
when 'ARC'
|
||||
center, radius, degrees, segments, start_angle = data
|
||||
start_angle = (start_angle) ? start_angle.degrees : 0.0
|
||||
end_angle = start_angle + degrees.degrees
|
||||
points = TT::Geom3d.arc( center, X_AXIS.reverse, Z_AXIS, radius, start_angle, end_angle, segments )
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_STRIP )
|
||||
when 'CIRCLE'
|
||||
center, radius, segments = data
|
||||
points = TT::Geom3d.circle( center, Z_AXIS, radius, segments )
|
||||
adjust_points!( points, odd_width, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_LINE_LOOP )
|
||||
when 'RECT'
|
||||
x,y,w,h,r = data
|
||||
points = []
|
||||
if r
|
||||
x_axis = X_AXIS.reverse
|
||||
z = Z_AXIS
|
||||
# Top Left
|
||||
c = Geom::Point3d.new( x + r, y + r, 0 )
|
||||
points.concat( TT::Geom3d.arc( c, x_axis, z, r, 0, 90.degrees, 8 ) )
|
||||
# Top Right
|
||||
c = Geom::Point3d.new( x + w - r, y + r, 0 )
|
||||
points.concat( TT::Geom3d.arc( c, x_axis, z, r, 90.degrees, 180.degrees, 8 ) )
|
||||
# Bottom Right
|
||||
c = Geom::Point3d.new( x + w - r, y + h - r, 0 )
|
||||
points.concat( TT::Geom3d.arc( c, x_axis, z, r, 180.degrees, 270.degrees, 8 ) )
|
||||
# Bottom Right
|
||||
c = Geom::Point3d.new( x + r, y + h - r, 0 )
|
||||
points.concat( TT::Geom3d.arc( c, x_axis, z, r, 270.degrees, 360.degrees, 8 ) )
|
||||
#
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_POLYGON )
|
||||
else
|
||||
points << Geom::Point3d.new( x, y, 0 )
|
||||
points << Geom::Point3d.new( x + w, y, 0 )
|
||||
points << Geom::Point3d.new( x + w, y + h, 0 )
|
||||
points << Geom::Point3d.new( x, y + h, 0 )
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_QUADS )
|
||||
end
|
||||
when 'POLYGON'
|
||||
points = data
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_POLYGON )
|
||||
when 'PIE'
|
||||
center, radius, degrees, segments, start_angle = data
|
||||
start_angle = (start_angle) ? start_angle.degrees : 0.0
|
||||
end_angle = start_angle + degrees.degrees
|
||||
points = TT::Geom3d.arc( center, X_AXIS.reverse, Z_AXIS, radius, start_angle, end_angle, segments )
|
||||
points.unshift( center )
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_TRIANGLE_FAN )
|
||||
when 'DISC'
|
||||
center, radius, segments = data
|
||||
points = TT::Geom3d.circle( center, Z_AXIS, radius, segments )
|
||||
adjust_points!( points, false, offset )
|
||||
add_instruction( instructions, :draw2d, points, GL_POLYGON )
|
||||
when 'GRADIENT'
|
||||
x, y, w, h, steps, vertical, color1, color2 = data
|
||||
n = ( vertical ) ? w / steps : h / steps
|
||||
r = 1.0 / steps
|
||||
for i in ( 0...steps )
|
||||
points = []
|
||||
if vertical
|
||||
x1 = x + (n * i)
|
||||
y1 = y
|
||||
w1 = n
|
||||
h1 = h
|
||||
else
|
||||
x1 = x
|
||||
y1 = y + (n * i)
|
||||
w1 = w
|
||||
h1 = n
|
||||
end
|
||||
points << Geom::Point3d.new( x1, y1, 0 )
|
||||
points << Geom::Point3d.new( x1 + w1, y1, 0 )
|
||||
points << Geom::Point3d.new( x1 + w1, y1 + h1, 0 )
|
||||
points << Geom::Point3d.new( x1, y1 + h1, 0 )
|
||||
adjust_points!( points, false, offset )
|
||||
c = color2.blend( color1, r * i )
|
||||
add_instruction( instructions, :drawing_color=, [c] )
|
||||
add_instruction( instructions, :draw2d, points, GL_QUADS )
|
||||
end
|
||||
else
|
||||
raise( ParseError, "Unknown drawing instruction. (#{command})\n\t#{line.inspect}" )
|
||||
end
|
||||
}
|
||||
}
|
||||
instructions
|
||||
end
|
||||
|
||||
# On nVidia cards an odd width line must have the co-ordinates in the center
|
||||
# of the pixel - otherwise the line will be aliased between the pixels on
|
||||
# either side.
|
||||
#
|
||||
# @param [Array<Geom::Point3d>] points
|
||||
# @param [Boolean] odd_width
|
||||
# @param [Geom::Vector3d] offset
|
||||
#
|
||||
# @return [Array]
|
||||
# @since 2.7.0
|
||||
def adjust_points!( points, odd_width, offset )
|
||||
total_offset = offset.clone
|
||||
if odd_width
|
||||
pixel_grid = Geom::Vector3d.new( 0.5, 0.5, 0.0 )
|
||||
total_offset = total_offset + pixel_grid
|
||||
end
|
||||
tr = Geom::Transformation.new( total_offset )
|
||||
for point in points
|
||||
point.transform!( tr )
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @param [Array] instructions
|
||||
# @param [Symbol] command
|
||||
# @param [Array] arguments
|
||||
# @param [Integer] draw_operation
|
||||
#
|
||||
# @return [Array]
|
||||
# @since 2.7.0
|
||||
def add_instruction( instructions, command, arguments, draw_operation = nil )
|
||||
last_instruction = instructions.last
|
||||
# Prepend any drawing operation.
|
||||
if draw_operation
|
||||
arguments.unshift( draw_operation )
|
||||
end
|
||||
# If there where no previous command or the last command was different
|
||||
# there is no optimization to be made.
|
||||
if last_instruction.nil? || last_instruction[0] != command
|
||||
instructions << [ command, arguments ]
|
||||
return nil
|
||||
end
|
||||
# Merge commands
|
||||
case command
|
||||
when :drawing_color=, :line_width=
|
||||
# Remove command that override each other.
|
||||
instructions.pop
|
||||
instructions << [ command, arguments ]
|
||||
when :draw2d
|
||||
# Merge operations when possible.
|
||||
current_operation = arguments[0]
|
||||
case current_operation
|
||||
when GL_POINTS, GL_LINES, GL_TRIANGLES, GL_QUADS
|
||||
# These operations can safely be merged if they appear in sequence.
|
||||
last_operation = last_instruction[1][0]
|
||||
if current_operation == last_operation
|
||||
arguments.shift
|
||||
last_instruction[1].concat( arguments )
|
||||
else
|
||||
instructions << [ command, arguments ]
|
||||
end
|
||||
else
|
||||
# All other operations cannot be merged.
|
||||
instructions << [ command, arguments ]
|
||||
end
|
||||
end
|
||||
instructions
|
||||
end
|
||||
|
||||
# @param [String] line
|
||||
#
|
||||
# @return [Array]
|
||||
# @since 2.7.0
|
||||
def decode_line( line )
|
||||
parts = line.split(/\s/)
|
||||
args = []
|
||||
for part in parts
|
||||
next if part.empty?
|
||||
if r = part.match( /^\<(\d+),(\d+),(\d+)(?:,(\d+))?\>$/ )
|
||||
# Colour - <255,64,0> - <255,64,0,64>
|
||||
color = r.captures.compact.map { |str| str.to_i }
|
||||
args << Sketchup::Color.new( color )
|
||||
elsif r = part.match( /^\[(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\]$/ )
|
||||
# Point2d - [-20,30] - [20.5,-30.5]
|
||||
x, y = r.captures.map! { |str| str.to_f }
|
||||
args << Geom::Point3d.new( x, y, 0 )
|
||||
elsif part.match( /^(-?\d+(?:\.\d+)?)$/ )
|
||||
# Number
|
||||
args << part.to_f
|
||||
elsif part.match( /^true$/i )
|
||||
args << true
|
||||
elsif part.match( /^false$/i )
|
||||
args << false
|
||||
else
|
||||
args << part
|
||||
end
|
||||
end
|
||||
args
|
||||
end
|
||||
|
||||
end # class TT::GL_Image
|
||||
end
|
||||
@@ -1,874 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'debug.rb'
|
||||
require_relative 'json.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# Wrapper library for creating and manipulating WebDialogs and HTML content
|
||||
# via Ruby.
|
||||
#
|
||||
# @note Very likely to be subject to change!
|
||||
module TT::GUI
|
||||
|
||||
# http://juixe.com/techknow/index.php/2007/01/22/ruby-class-tutorial/
|
||||
# http://stackoverflow.com/questions/1645398/ruby-include-question
|
||||
|
||||
# http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/
|
||||
#
|
||||
# @since 2.6.0
|
||||
module ControlEvents
|
||||
|
||||
# @since 2.6.0
|
||||
def self.included( base )
|
||||
# Hash table of valid events for the class.
|
||||
# > Key: (Symbol)
|
||||
# > Value: (Symbol)
|
||||
base.instance_variable_set( '@control_events', {} )
|
||||
base.extend( ControlEventDefinitions )
|
||||
end
|
||||
|
||||
# @since 2.6.0
|
||||
module ControlEventDefinitions
|
||||
|
||||
# @since 2.6.0
|
||||
def inherited( subclass )
|
||||
instance_var = '@control_events'
|
||||
parent_events = instance_variable_get( instance_var ).dup
|
||||
subclass.instance_variable_set( instance_var, parent_events )
|
||||
end
|
||||
|
||||
end # module ControlEventDefinitions
|
||||
|
||||
end # module ControlEvents
|
||||
|
||||
|
||||
# All GUI elements inherit this class.
|
||||
#
|
||||
# @abstract
|
||||
# @since 2.4.0
|
||||
class Control
|
||||
include ControlEvents
|
||||
|
||||
attr_accessor( :ui_id ) # @since 2.4.0
|
||||
attr_accessor( :name ) # @since 2.7.0
|
||||
attr_accessor( :left, :right, :top, :bottom, :height, :width ) # @since 2.4.0
|
||||
attr_accessor( :parent, :window ) # @since 2.4.0
|
||||
attr_accessor( :tooltip ) # @since 2.5.0
|
||||
attr_accessor( :font_name, :font_size, :font_bold, :font_italic ) # @since 2.7.0
|
||||
|
||||
# (!) Tab Index
|
||||
|
||||
# Base Events:
|
||||
# * click, double-click (?)
|
||||
# * keydown, keypress, keyup (?)
|
||||
# * mousein, mouseout, mousemove
|
||||
# * mousebuttondown, mousebuttonup
|
||||
# * focus, blur
|
||||
|
||||
# @since 2.4.0
|
||||
def initialize( *args )
|
||||
# Unique identifier used in the WebDialog's HTML.
|
||||
@ui_id = 'UI_' + self.object_id.to_s
|
||||
# Hash with Symbols for keys idenitfying the event.
|
||||
# Each event is an array of Proc's.
|
||||
@events = {}
|
||||
|
||||
@disabled = false
|
||||
end
|
||||
|
||||
|
||||
# @return [Array<Symbol>]
|
||||
# @since 2.6.0
|
||||
def self.events
|
||||
@control_events.keys
|
||||
end
|
||||
|
||||
# @return [Array<Symbol>]
|
||||
# @since 2.6.0
|
||||
def self.has_event?( event )
|
||||
@control_events.key?( event )
|
||||
end
|
||||
|
||||
|
||||
# Adds an event to the stack.
|
||||
#
|
||||
# @param [Symbol] event
|
||||
# @param [Proc] block
|
||||
#
|
||||
# @return [nil]
|
||||
# @since 2.5.0
|
||||
def add_event_handler( event, &block )
|
||||
unless self.class.has_event?( event )
|
||||
raise( ArgumentError, "Event #{event} not defined for #{self.class}" )
|
||||
end
|
||||
@events[event] ||= []
|
||||
@events[event] << block
|
||||
nil
|
||||
end
|
||||
|
||||
# Triggers the given event. All attached procs for that event will be called.
|
||||
#
|
||||
# @param [Symbol] event
|
||||
# @param [Array] args Set of arguments to be passed to the called procs.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def call_event( event, args = nil )
|
||||
TT::debug "call_event(#{event.to_s})"
|
||||
TT::debug args.inspect
|
||||
if @events.key?( event )
|
||||
@events[event].each { |proc|
|
||||
next if proc.nil? # In case Button control where made without a block.
|
||||
# Add self to argument list so the called event can get the handle for
|
||||
# the control triggering it.
|
||||
if args.nil?
|
||||
proc.call( self )
|
||||
else
|
||||
args.unshift( self )
|
||||
proc.call( *args )
|
||||
end
|
||||
}
|
||||
true
|
||||
else
|
||||
# (?) Raise error?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def disabled?
|
||||
@disabled == true
|
||||
end
|
||||
|
||||
# @param [Boolean] value
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def disabled=( value )
|
||||
@disabled = value
|
||||
update_html_element( { :disabled => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Boolean] value
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def enabled=( value )
|
||||
self.disabled = !value
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def enabled?
|
||||
!@disabled
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.6.0
|
||||
def top=( value )
|
||||
@top = value
|
||||
update_html_element( { :top => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.6.0
|
||||
def left=( value )
|
||||
@left = value
|
||||
update_html_element( { :left => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.6.0
|
||||
def bottom=( value )
|
||||
@bottom = value
|
||||
update_html_element( { :bottom => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.6.0
|
||||
def right=( value )
|
||||
@right = value
|
||||
update_html_element( { :right => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.6.0
|
||||
def width=( value )
|
||||
@width = value
|
||||
update_html_element( { :width => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.6.0
|
||||
def height=( value )
|
||||
@height = value
|
||||
update_html_element( { :height => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] left
|
||||
# @param [Numeric] top
|
||||
#
|
||||
# @return [Array<Numeric,Numeric>]
|
||||
# @since 2.4.0
|
||||
def move( left, top )
|
||||
@left = left
|
||||
@top = top
|
||||
update_html_element( { :left => left, :top => top } )
|
||||
[ left, top ]
|
||||
end
|
||||
|
||||
# @param [Numeric] width
|
||||
# @param [Numeric] height
|
||||
#
|
||||
# @return [Array<Numeric,Numeric>]
|
||||
# @since 2.4.0
|
||||
def size( width, height )
|
||||
@width = width
|
||||
@height = height
|
||||
update_html_element( { :width => width, :height => height } )
|
||||
[ width, height ]
|
||||
end
|
||||
|
||||
# @param [Numeric] top
|
||||
# @param [Numeric] right
|
||||
# @param [Numeric] bottom
|
||||
# @param [Numeric] left
|
||||
#
|
||||
# @return [Array<Numeric,Numeric,Numeric,Numeric>]
|
||||
# @since 2.4.0
|
||||
def position( top, right, bottom, left )
|
||||
@top = top
|
||||
@right = right
|
||||
@bottom = bottom
|
||||
@left = left
|
||||
properties = {
|
||||
:top => top,
|
||||
:right => right,
|
||||
:bottom => bottom,
|
||||
:left => left
|
||||
}
|
||||
update_html_element( properties )
|
||||
[ top, right, bottom, left ]
|
||||
end
|
||||
|
||||
# (!) Need explicit :position property.
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def positioned?
|
||||
( @left || @right || @top || @bottom ) ? true : false
|
||||
end
|
||||
|
||||
# @param [String] value
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def font_name=( value )
|
||||
@font_name = value
|
||||
update_html_element( { :font_name => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [Numeric] value
|
||||
#
|
||||
# @return [Numeric]
|
||||
# @since 2.7.0
|
||||
def font_size=( value )
|
||||
@font_size = value
|
||||
update_html_element( { :font_size => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.5.0
|
||||
def properties
|
||||
options = TT::JSON.new
|
||||
options['id'] = @ui_id
|
||||
options['parent'] = @parent.ui_id if @parent.is_a?( Control )
|
||||
options['type'] = self.class.name
|
||||
if positioned?
|
||||
options['top'] = @top if @top
|
||||
options['bottom'] = @bottom if @bottom
|
||||
options['left'] = @left if @left
|
||||
options['right'] = @right if @right
|
||||
end
|
||||
options['width'] = @width if @width
|
||||
options['height'] = @height if @height
|
||||
options['font_name'] = @font_name if @font_name
|
||||
options['font_size'] = @font_size if @font_size
|
||||
options['disabled'] = @disabled if @disabled
|
||||
if self.respond_to?( :custom_properties )
|
||||
options.merge!( custom_properties() )
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.6.0
|
||||
def inspect
|
||||
"<#{self.class}:#{TT.object_id_hex(self)}>"
|
||||
end
|
||||
|
||||
# Release all references to other objects. Setting them to nil. So that
|
||||
# the GC can collect them.
|
||||
#
|
||||
# @return [Nil]
|
||||
# @since 2.6.0
|
||||
def release!
|
||||
@parent = nil
|
||||
@window = nil
|
||||
@events.clear
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param [Hash] properties
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.6.0
|
||||
def update_html_element( properties )
|
||||
if self.window && self.window.visible?
|
||||
properties['type'] = self.class.name # Required by JS UI.update_properties
|
||||
self.window.call_script( 'UI.update_properties', self.ui_id, properties )
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Defines an event for the control. If an event is not defines it cannot be
|
||||
# called.
|
||||
#
|
||||
# @overload set( event, ... )
|
||||
# @param [Symbol] event
|
||||
#
|
||||
# @return [Nil]
|
||||
# @since 2.6.0
|
||||
def self.define_event( *args )
|
||||
for event in args
|
||||
raise( ArgumentError, 'Expected a Symbol' ) unless event.is_a?( Symbol )
|
||||
@control_events[event] = event
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
end # class Control
|
||||
|
||||
|
||||
# @abstract +Container+ and +Window+ implements this.
|
||||
# @since 2.4.0
|
||||
module ContainerElement
|
||||
|
||||
# @since 2.4.0
|
||||
attr( :controls )
|
||||
|
||||
# @since 2.4.0
|
||||
def initialize( *args )
|
||||
super( *args )
|
||||
@controls = []
|
||||
end
|
||||
|
||||
# @param [Control] control
|
||||
#
|
||||
# @return [Boolean] +True+ if the webdialog was open and the control added.
|
||||
# @since 2.4.0
|
||||
def add_control( control )
|
||||
raise( ArgumentError, 'Expected Control' ) unless control.is_a?( Control )
|
||||
# Add to Ruby DOM tree
|
||||
@controls << control
|
||||
control.parent = self
|
||||
control.window = self.window
|
||||
# Add to Webdialog
|
||||
if self.window && self.window.visible?
|
||||
self.window.add_control_to_webdialog( control )
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# @param [Control] control
|
||||
#
|
||||
# @return [Boolean] +True+ if the webdialog was open and the control removed.
|
||||
# @since 2.9.0
|
||||
def remove_control( control )
|
||||
raise( ArgumentError, 'Expected Control' ) unless control.is_a?( Control )
|
||||
raise( IndexError, 'Control not found' ) unless controls.include?( control )
|
||||
@controls.delete( control )
|
||||
control_ui_id = control.ui_id
|
||||
control.release!
|
||||
if self.window && self.window.visible?
|
||||
self.window.call_script( 'UI.remove_control', control_ui_id )
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# While #add_control add the control to the Ruby class's internal list, this
|
||||
# method adds the control to the webdialog.
|
||||
#
|
||||
# @private
|
||||
# @return [Nil]
|
||||
# @since 2.5.0
|
||||
def add_controls_to_webdialog
|
||||
for control in @controls
|
||||
@window.add_control_to_webdialog( control )
|
||||
if control.is_a?( ContainerElement )
|
||||
control.add_controls_to_webdialog
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @param [String] ui_id
|
||||
#
|
||||
# @return [Control,Nil]
|
||||
# @since 2.5.0
|
||||
def get_control_by_ui_id( ui_id )
|
||||
for control in @controls
|
||||
return control if control.ui_id == ui_id
|
||||
if control.is_a?( ContainerElement )
|
||||
result = control.get_control_by_ui_id( ui_id )
|
||||
return result if result
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @param [Symbol] name
|
||||
#
|
||||
# @return [Control,Nil]
|
||||
# @since 2.5.0
|
||||
def get_control_by_name( name )
|
||||
for control in @controls
|
||||
return control if control.name == name
|
||||
if control.is_a?( ContainerElement )
|
||||
result = control.get_control_by_name( name )
|
||||
return result if result
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
alias :[] :get_control_by_name
|
||||
|
||||
# @see Control#release!
|
||||
# @return [Nil]
|
||||
# @since 2.6.0
|
||||
def release!
|
||||
for control in @controls
|
||||
control.release!
|
||||
end
|
||||
@controls.clear
|
||||
super
|
||||
end
|
||||
|
||||
end # module ContainerElement
|
||||
|
||||
|
||||
# @since 2.4.0
|
||||
class Container < Control
|
||||
include ContainerElement
|
||||
# (!) Background color. (Style)
|
||||
end # class Container
|
||||
|
||||
|
||||
# @since 2.7.0
|
||||
class Groupbox < Container
|
||||
|
||||
# @since 2.7.0
|
||||
attr_reader( :label )
|
||||
|
||||
# @param [String] label
|
||||
#
|
||||
# @since 2.7.0
|
||||
def initialize( label = '' )
|
||||
super
|
||||
@label = label
|
||||
end
|
||||
|
||||
# @param [String] value
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def label=( value )
|
||||
@label = value
|
||||
update_html_element( { :label => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.7.0
|
||||
def custom_properties
|
||||
prop = TT::JSON.new
|
||||
prop['label'] = @label
|
||||
prop
|
||||
end
|
||||
|
||||
end # Groupbox
|
||||
|
||||
# = Events
|
||||
# * +:click+
|
||||
#
|
||||
# @since 2.4.0
|
||||
class Button < Control
|
||||
|
||||
# @since 2.4.0
|
||||
attr_accessor( :caption )
|
||||
|
||||
# @since 2.6.0
|
||||
define_event( :click )
|
||||
|
||||
# @param [String] caption
|
||||
# @param [Proc] on_click
|
||||
#
|
||||
# @since 2.4.0
|
||||
def initialize( caption, &on_click )
|
||||
super
|
||||
# Defaults
|
||||
# http://msdn.microsoft.com/en-us/library/aa511279.aspx#controlsizing
|
||||
# http://msdn.microsoft.com/en-us/library/aa511453.aspx#sizing
|
||||
# Actual: 75x23
|
||||
# Visible: 73x21
|
||||
@width = 73
|
||||
@height = 21
|
||||
# User Properties
|
||||
@caption = caption
|
||||
add_event_handler( :click, &on_click )
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.5.0
|
||||
def custom_properties
|
||||
prop = TT::JSON.new
|
||||
prop['caption'] = @caption
|
||||
prop
|
||||
end
|
||||
|
||||
end # class Button
|
||||
|
||||
|
||||
# @since 2.6.0
|
||||
class ToolbarButton < Button
|
||||
|
||||
# @since 2.6.0
|
||||
attr_accessor( :icon )
|
||||
|
||||
# @param [String] caption
|
||||
# @param [Proc] on_click
|
||||
#
|
||||
# @since 2.6.0
|
||||
def initialize( caption, &on_click )
|
||||
super
|
||||
# Defaults
|
||||
@width = 28
|
||||
@height = 28
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.6.0
|
||||
def custom_properties
|
||||
# (!) Improve custom properties.
|
||||
prop = super
|
||||
#prop['caption'] = @caption
|
||||
prop['icon'] = @icon if @icon
|
||||
prop
|
||||
end
|
||||
|
||||
end # class ToolbarButton
|
||||
|
||||
|
||||
# @since 2.7.0
|
||||
class Checkbox < Control
|
||||
|
||||
# @since 2.7.0
|
||||
attr_reader( :label )
|
||||
|
||||
# @since 2.7.0
|
||||
define_event( :change )
|
||||
define_event( :click )
|
||||
|
||||
# @param [String] label
|
||||
#
|
||||
# @since 2.7.0
|
||||
def initialize( label, checked = false )
|
||||
super
|
||||
@label = label
|
||||
@checked = checked
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def check!
|
||||
checked = true
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def uncheck!
|
||||
checked = false
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def toggle!
|
||||
checked = !checked
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def checked
|
||||
@checked = self.window.get_checkbox_state( self.ui_id )
|
||||
end
|
||||
alias checked? checked
|
||||
|
||||
# @param [Boolean] value
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.7.0
|
||||
def checked=( value )
|
||||
@checked = value
|
||||
update_html_element( { :checked => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @param [String] value
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def label=( value )
|
||||
@label = value
|
||||
update_html_element( { :label => value } )
|
||||
value
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.7.0
|
||||
def custom_properties
|
||||
prop = TT::JSON.new
|
||||
prop['label'] = @label
|
||||
prop['checked'] = @checked
|
||||
prop
|
||||
end
|
||||
|
||||
end # class Checkbox
|
||||
|
||||
|
||||
# = Events
|
||||
# * +:change+
|
||||
#
|
||||
# @since 2.4.0
|
||||
class Listbox < Control
|
||||
|
||||
# @since 2.5.0
|
||||
attr_reader( :value, :items, :size, :multiple )
|
||||
|
||||
# @since 2.6.0
|
||||
define_event( :change )
|
||||
|
||||
# @param [Array<String>] list
|
||||
#
|
||||
# @since 2.5.0
|
||||
def initialize( list = nil )
|
||||
super
|
||||
@value = nil
|
||||
@items = ( list.is_a?(Array) ) ? list : [] # (?) Hash instead?
|
||||
@size = nil
|
||||
@multiple = false
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.5.0
|
||||
def custom_properties
|
||||
prop = TT::JSON.new
|
||||
prop['value'] = @value
|
||||
prop['items'] = @items
|
||||
prop['size'] = @size if @size
|
||||
prop['multiple'] = @multiple if @multiple
|
||||
prop
|
||||
end
|
||||
|
||||
# @overload add_item(string)
|
||||
# @param [String] string
|
||||
#
|
||||
# @overload add_item(string, ...)
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def add_item( *args )
|
||||
args = args[0] if args.size == 1 && args[0].is_a?( Array )
|
||||
if args.size == 1
|
||||
@items << args[0]
|
||||
self.window.call_script( 'UI.add_list_item', self.ui_id, args[0] )
|
||||
else
|
||||
@items.concat( args )
|
||||
self.window.call_script( 'UI.add_list_item', self.ui_id, args )
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def value
|
||||
@value = self.window.get_control_value( self.ui_id )
|
||||
end
|
||||
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def value=( string )
|
||||
unless @items.include?( string )
|
||||
raise ArgumentError, "'#{string}' not a valid value in list."
|
||||
end
|
||||
@value = string
|
||||
update_html_element( { :value => string } )
|
||||
string
|
||||
end
|
||||
|
||||
end # class Listbox
|
||||
|
||||
|
||||
# = Events
|
||||
# * +:change+
|
||||
# * +:keydown+
|
||||
# * +:keypress+
|
||||
# * +:keyup+
|
||||
# * +:focus+
|
||||
# * +:blur+
|
||||
#
|
||||
# @since 2.4.0
|
||||
class Textbox < Control
|
||||
|
||||
# @since 2.4.0
|
||||
attr_accessor( :value, :multiline )
|
||||
alias :multiline? :multiline
|
||||
|
||||
# @since 2.6.0
|
||||
define_event( :change )
|
||||
define_event( :textchange )
|
||||
define_event( :keydown, :keypress, :keyup )
|
||||
define_event( :focus, :blur )
|
||||
define_event( :copy, :cut, :paste )
|
||||
|
||||
# @param [String] value
|
||||
#
|
||||
# @since 2.4.0
|
||||
def initialize( value )
|
||||
super
|
||||
@value = value
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
# @since 2.6.0
|
||||
def value
|
||||
@value = self.window.get_control_value( self.ui_id )
|
||||
end
|
||||
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.6.0
|
||||
def value=( string )
|
||||
@value = string
|
||||
update_html_element( { :value => string } )
|
||||
string
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.6.0
|
||||
def custom_properties
|
||||
prop = TT::JSON.new
|
||||
prop['value'] = @value
|
||||
prop['multiline'] = @multiline
|
||||
prop
|
||||
end
|
||||
|
||||
end # class Textbox
|
||||
|
||||
|
||||
# @since 2.4.0
|
||||
class Label < Control
|
||||
|
||||
# @since 2.4.0
|
||||
attr_accessor( :caption )
|
||||
|
||||
# @since 2.7.0
|
||||
attr_accessor( :url )
|
||||
|
||||
# @since 2.7.0
|
||||
define_event( :open_url )
|
||||
|
||||
# @param [String] caption
|
||||
# @param [Control] control Control which receives focus when the Label is activated.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def initialize( caption, control=nil )
|
||||
super
|
||||
@caption = caption
|
||||
@control = control
|
||||
@url = nil
|
||||
add_event_handler( :open_url ) {
|
||||
UI.openURL( param )
|
||||
}
|
||||
# (!) Align
|
||||
end
|
||||
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.6.0
|
||||
def caption=( string )
|
||||
@caption = string
|
||||
update_html_element( { :caption => string } )
|
||||
string
|
||||
end
|
||||
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.7.0
|
||||
def url=( string )
|
||||
@url = string
|
||||
update_html_element( { :url => string } )
|
||||
string
|
||||
end
|
||||
|
||||
# @return [TT::JSON]
|
||||
# @since 2.6.0
|
||||
def custom_properties
|
||||
prop = TT::JSON.new
|
||||
prop['caption'] = @caption
|
||||
prop['control'] = @control.ui_id if @control
|
||||
prop['url'] = @url if @url
|
||||
prop
|
||||
end
|
||||
|
||||
end # class Label
|
||||
|
||||
end # module TT::GUI
|
||||
end
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'instance.rb'
|
||||
|
||||
# Collection of Face methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
|
||||
module SpeckleConnector
|
||||
module TT::Image
|
||||
|
||||
# Returns the material for the given +Image+.
|
||||
#
|
||||
# @param [Sketchup::Image] image
|
||||
#
|
||||
# @return [Sketchup::Material]
|
||||
# @since 2.0.0
|
||||
def self.material(image)
|
||||
definition = TT::Instance.definition(image)
|
||||
face = definition.entities.grep(Sketchup::Face).first
|
||||
face.material
|
||||
end
|
||||
|
||||
end # module TT::Image
|
||||
end
|
||||
@@ -1,315 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'gui.rb'
|
||||
require_relative 'json.rb'
|
||||
require_relative 'locale.rb'
|
||||
require_relative 'modal_wrapper.rb'
|
||||
require_relative 'toolwindow.rb'
|
||||
require_relative 'settings.rb'
|
||||
|
||||
# (i) Alpha stage. Very likely to be subject to change!
|
||||
#
|
||||
# @example
|
||||
# i = TT::GUI::Inputbox.new
|
||||
# i.add_control( {
|
||||
# :label => 'FooBar',
|
||||
# :value => 'Hello'
|
||||
# } )
|
||||
# i.prompt { |results|
|
||||
# p results
|
||||
# }
|
||||
#
|
||||
# @since 2.4.0
|
||||
module SpeckleConnector
|
||||
class TT::GUI::Inputbox < TT::GUI::ToolWindow
|
||||
|
||||
CT_LIST = 1
|
||||
CT_RADIOBOX = 2
|
||||
|
||||
# @since 2.5.5
|
||||
#attr_accessor( :controls )
|
||||
def controls; @ib_controls; end;
|
||||
|
||||
# Creates a new Inputbox instance.
|
||||
#
|
||||
# @todo Escape HTML data (?) Ok on Windows IE.
|
||||
# @todo Descriptions (Info, HTML)
|
||||
# @todo Slider
|
||||
# @todo PickList
|
||||
# @todo Events
|
||||
#
|
||||
# @param [Hash] options
|
||||
# @option options [String] :title ("Inputbox")
|
||||
# @option options [String] :pref_key If present the inputbox will remember
|
||||
# it's values and properties between sessions.
|
||||
# @option options [Boolean] :save_values (true) Set to false if you
|
||||
# do not want the values to be remembered between sessions, but want the
|
||||
# window size etc to be remembered. Requires +:pref_key+ to be +true+
|
||||
# @option options [Boolean] :resizable (true)
|
||||
# @option options [Integer] :width
|
||||
# @option options [Integer] :height
|
||||
# @option options [String] :accept_label ("Ok") The caption of the
|
||||
# accept button.
|
||||
# @option options [String] :cancel_label ("Cancel") The caption of the
|
||||
# cancel button.
|
||||
# @option options [Boolean] :modal (false) Set to true to prevent
|
||||
# the user from interacting with the model while the window is open. It also
|
||||
# prevents other modal windows (From TT_Lib) from opening.
|
||||
# @option options [Boolean] :true_modal (false) *Deprecated* Under
|
||||
# Windows the window can be made into a true modal window, but since it
|
||||
# can't under OSX this key is deprecated.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def initialize(options={})
|
||||
raise ArgumentError unless options.is_a?( Hash )
|
||||
|
||||
# Window options (Sent to Window < Webdialog instance)
|
||||
wnd_options = {
|
||||
:dialog_title => 'Inputbox',
|
||||
:scrollable => false,
|
||||
:resizable => true
|
||||
}
|
||||
wnd_options[:dialog_title] = options[:title] if options.key?(:title)
|
||||
wnd_options[:preferences_key] = options[:pref_key] if options.key?(:pref_key)
|
||||
wnd_options[:resizable] = options[:resizable] if options.key?(:resizable)
|
||||
wnd_options[:width] = options[:width] if options.key?(:width)
|
||||
wnd_options[:height] = options[:height] if options.key?(:height)
|
||||
wnd_options[:left] = options[:left] if options.key?(:left)
|
||||
wnd_options[:top] = options[:top] if options.key?(:top)
|
||||
wnd_options[:min_width] = 200
|
||||
wnd_options[:min_height] = 12
|
||||
# Window UI options (Sent to Javascript)
|
||||
@html_options = {
|
||||
:accept_label => 'Ok',
|
||||
:cancel_label => 'Cancel'
|
||||
}
|
||||
@html_options[:save_values] = options[:save_values] if options.key?(:save_values)
|
||||
@html_options.merge!(options)
|
||||
# Internal flags.
|
||||
@save_values = ( options.key?(:save_values) ) ? options[:save_values] : true
|
||||
@true_modal = ( options.key?(:true_modal) ) ? options[:true_modal] : false
|
||||
@modal = ( options.key?(:modal) ) ? options[:modal] : false
|
||||
# Array of the controls and their values.
|
||||
@ib_controls = []
|
||||
@values = nil
|
||||
# Flag indicating if the window is closing.
|
||||
@closing = false
|
||||
# Initate the ModalWrapper
|
||||
if !@true_modal && @modal
|
||||
@modal_window = TT::GUI::ModalWrapper.new( self )
|
||||
end
|
||||
# Control's label is the section key for settings
|
||||
if options.key?(:pref_key) && @save_values
|
||||
@defaults = TT::Settings.new( options[:pref_key] )
|
||||
else
|
||||
@defaults = nil
|
||||
end
|
||||
|
||||
# Initialize the parent Window class
|
||||
super( wnd_options )
|
||||
# Using relative paths, relying on the BASE element seem to fail on some
|
||||
# computers running various versions of Windows. Not sure why - maybe
|
||||
# different security settings..?
|
||||
wpath = File.join( TT::Lib.path, 'webdialog' )
|
||||
add_script( local_path( File.join(wpath, 'js', 'inputbox.js') ) )
|
||||
add_style( local_path( File.join(wpath, 'css', 'inputbox.css') ) )
|
||||
|
||||
add_action_callback( 'Inputbox_ready', &method(:event_inputbox_ready) )
|
||||
add_action_callback( 'Inputbox_accept', &method(:event_inputbox_accept) )
|
||||
add_action_callback( 'Inputbox_cancel', &method(:event_inputbox_cancel) )
|
||||
|
||||
set_on_close( &method(:event_inputbox_close) )
|
||||
end
|
||||
|
||||
|
||||
# When the webdialog reports it's ready, call function to start building the
|
||||
# UI with the given user options.
|
||||
def event_inputbox_ready( window, params )
|
||||
TT.debug '>> Input Ready'
|
||||
# Window settings
|
||||
o = TT::JSON.new( @html_options )
|
||||
window.execute_script("inputbox.init_html(#{o});")
|
||||
# Add controls
|
||||
@ib_controls.each { |control|
|
||||
c = control.clone
|
||||
if c[:value].is_a?( Float ) && !c[:value].is_a?( Length )
|
||||
c[:value] = TT::Locale.float_to_string( c[:value] )
|
||||
elsif c[:value].is_a?( Length )
|
||||
c[:value] = c[:value].to_s
|
||||
end
|
||||
window.execute_script("inputbox.add_control(#{c});")
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
# When the user accepts the values in the Inputbox, collect the data and
|
||||
# convert them back from string into their input types.
|
||||
# (!) Build Hash with keys and values.
|
||||
def event_inputbox_accept( window, params )
|
||||
TT.debug '>> Input Accept'
|
||||
@values = {}
|
||||
@ib_controls.each { |control|
|
||||
value = window.send( :get_value, control[:id] )
|
||||
type = ( control[:options] && control[:multiple] ) ? Array : control[:value]
|
||||
value = TT::Locale.cast_string(value, type, '||')
|
||||
@defaults[ control[:label] ] = value if @save_values
|
||||
@values[ control[:key] ] = value
|
||||
control[:value] = value
|
||||
}
|
||||
window.close
|
||||
end
|
||||
|
||||
|
||||
# User cancels the inputbox, either by using the Cancel button, the Close
|
||||
# button or the Right-Click menu.
|
||||
def event_inputbox_cancel( window, params )
|
||||
TT.debug '>> Input Cancel'
|
||||
window.close
|
||||
end
|
||||
|
||||
|
||||
# Flag the window as closing to avoid the Modal_Wrapper from also calling
|
||||
# close - something which will lead to multiple triggering of this event.
|
||||
def event_inputbox_close
|
||||
TT.debug '>> Window Closed'
|
||||
@closing = true
|
||||
@modal_window.close if @modal_window
|
||||
begin
|
||||
@block.call(@values) # (?) Delay with timer to ensure window closes?
|
||||
rescue => e
|
||||
puts e.message
|
||||
puts e.backtrace.join("\n")
|
||||
UI.messagebox("#{e.message}\n\n#{e.backtrace.join("\n")}", MB_MULTILINE)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Flag indicating if the webdialog is closing.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def closing?
|
||||
@closing
|
||||
end
|
||||
|
||||
|
||||
# Adds a new input control.
|
||||
#
|
||||
# @example Normal Textbox
|
||||
# i = TT::GUI::Inputbox.new( options )
|
||||
# i.add_control( {
|
||||
# :label => 'Hello',
|
||||
# :value => 'World'
|
||||
# } )
|
||||
#
|
||||
# @example Natrually Ordered Multi Select List
|
||||
# i = TT::GUI::Inputbox.new( options )
|
||||
# i.add_control( {
|
||||
# :label => 'My List',
|
||||
# :description => 'Lorem Ipsum Dolor Sit Amet.',
|
||||
# :value => ['Hello', 'World'],
|
||||
# :options => ['Foo', 'Hello', 'Bar', 'World', 'FooBar'],
|
||||
# :multiple => true,
|
||||
# :order => 1,
|
||||
# :natrual_order => true,
|
||||
# :size => 5
|
||||
# } )
|
||||
#
|
||||
# @param [Hash] options
|
||||
# @option options [String] :label *Required* Should be unique in order for
|
||||
# persistant values to function.
|
||||
# @option options [String|Array] :value *Required* Default value.
|
||||
# @option options [Symbol] :key Identifying key used in the result hash when
|
||||
# the dialog closes. This was introdused in 2.5.0. Before that the results
|
||||
# where Arrays.
|
||||
# @option options [String] :description Explainatory text accosiated to the
|
||||
# control.
|
||||
# @option options [Array] :options Array of valued, used for lists and
|
||||
# radiobox options.
|
||||
# @option options [Integer] :order -1, 0 or 1 - -1 means decending order, 0 no
|
||||
# order, 1 accending order.
|
||||
# @option options [Boolean] :natrual_order If the list is ordered, set this
|
||||
# +true+ for a natrual ordering.
|
||||
# @option options [Integer] :size Set this property for a scrollable list
|
||||
# instead of a drop down list.
|
||||
# @option options [Boolean] :multiple Allows mulitple items to be selected.
|
||||
# @option options [Integer] :type Possible constants:
|
||||
# * +TT::GUI::Inputbox::CT_LIST+
|
||||
# * +TT::GUI::Inputbox::CT_RADIOBOX+
|
||||
# @option options [Boolean] :no_save Prevents the value from being saved if
|
||||
# set to +true+. Added version 2.5.0.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def add_control(options)
|
||||
options = ( options.is_a?( Hash ) ) ? TT::JSON.new( options ) : options.dup
|
||||
raise ArgumentError unless options.is_a?( TT::JSON )
|
||||
# Process and prepare the options before sending it to the Webdialog.
|
||||
options[:id] = "Inputbox_control#{@ib_controls.size}"
|
||||
# Ensure the options are JSON object which can be sent to the Webdialog.
|
||||
# Hashes will aumatically be converted to JSON objects.
|
||||
if options.key?( :key )
|
||||
# Ensure key is unique.
|
||||
key = options[:key]
|
||||
@ib_controls.each { |control|
|
||||
raise ArgumentError, 'options[:key] must be uniqe' if control[:key] == key
|
||||
}
|
||||
else
|
||||
options[:key] = options[:id]
|
||||
end
|
||||
# Default value.
|
||||
if @defaults && !options[:no_save]
|
||||
options[:value] = @defaults[ options[:label], options[:value] ]
|
||||
end
|
||||
@ib_controls << options
|
||||
# If controls are added while the inputbox is open it needs to be notified.
|
||||
if self.visible?
|
||||
self.execute_script("inputbox.add_control(#{options});")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# *Note* As of 2.5.0 the returned argument of the block is a Hash instead of
|
||||
# an Array.
|
||||
#
|
||||
# @param [&block] block the callback that receives the resulting values as a
|
||||
# +Hash+ when the inputbox closes. Only used if the inputbox is not true modal.
|
||||
#
|
||||
# @return [Hash|nil] Hash of resulting values if Inputbox is true modal, nil otherwise.
|
||||
# @since 2.4.0
|
||||
def prompt(&block)
|
||||
@values = nil
|
||||
@block = block
|
||||
if @true_modal
|
||||
show_window(@true_modal)
|
||||
@values
|
||||
elsif @modal
|
||||
@modal_window.show
|
||||
nil
|
||||
else
|
||||
self.show_window
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
|
||||
# Returns the value of the given element identified by its index in +@controls+.
|
||||
#
|
||||
# @param [String] id
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.4.0
|
||||
def get_value(id)
|
||||
self.execute_script("inputbox.get_value('#{id}');")
|
||||
self.get_element_value('RUBY_Inputbox_get_value');
|
||||
end
|
||||
|
||||
end # module TT::GUI::Inputbox
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of Group, ComponentInstance and Image methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module SpeckleConnector
|
||||
module TT::Instance
|
||||
|
||||
# Returns the definition for a +Group+, +ComponentInstance+ or +Image+
|
||||
#
|
||||
# @param [:definition, Sketchup::Group, Sketchup::Image] instance
|
||||
#
|
||||
# @return [Sketchup::ComponentDefinition,Mixed]
|
||||
# @since 2.0.0
|
||||
def self.definition(instance)
|
||||
if instance.respond_to?(:definition)
|
||||
begin
|
||||
return instance.definition
|
||||
rescue
|
||||
# Previously this was the first check, but too many extensions modify
|
||||
# Sketchup::Group.definition with a method which is bugged so to avoid
|
||||
# all the complaints about extensions not working due to this the call
|
||||
# is trapped is a rescue block and any errors will make it fall back to
|
||||
# using the old way of finding the group definition.
|
||||
end
|
||||
end
|
||||
if instance.is_a?(Sketchup::Group)
|
||||
# (i) group.entities.parent should return the definition of a group.
|
||||
# But because of a SketchUp bug we must verify that group.entities.parent
|
||||
# returns the correct definition. If the returned definition doesn't
|
||||
# include our group instance then we must search through all the
|
||||
# definitions to locate it.
|
||||
if instance.entities.parent.instances.include?(instance)
|
||||
return instance.entities.parent
|
||||
else
|
||||
Sketchup.active_model.definitions.each { |definition|
|
||||
return definition if definition.instances.include?(instance)
|
||||
}
|
||||
end
|
||||
elsif instance.is_a?(Sketchup::Image)
|
||||
Sketchup.active_model.definitions.each { |definition|
|
||||
if definition.image? && definition.instances.include?(instance)
|
||||
return definition
|
||||
end
|
||||
}
|
||||
end
|
||||
return nil # Given entity was not an instance of an definition.
|
||||
end
|
||||
|
||||
|
||||
# Query to whether it's a Group or ComponentInstance
|
||||
#
|
||||
# @param [Sketchup::Entity] entity
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.0.0
|
||||
def self.is?(entity)
|
||||
entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
|
||||
end
|
||||
|
||||
end # module TT::Instance
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
arc.rb
|
||||
attributes.rb
|
||||
babelfish.rb
|
||||
bezier.rb
|
||||
binary.rb
|
||||
boolean_attributes.rb
|
||||
bounds.rb
|
||||
color.rb
|
||||
core.rb
|
||||
cursor.rb
|
||||
c_extension_manager.rb
|
||||
debug.rb
|
||||
deferred_event.rb
|
||||
definition.rb
|
||||
dimension.rb
|
||||
draw_cache.rb
|
||||
edges.rb
|
||||
entities.rb
|
||||
faces.rb
|
||||
geom3d.rb
|
||||
gizmo_manipulator.rb
|
||||
gl_image.rb
|
||||
gui.rb
|
||||
image.rb
|
||||
inputbox.rb
|
||||
instance.rb
|
||||
javascript.rb
|
||||
json.rb
|
||||
length.rb
|
||||
locale.rb
|
||||
materials.rb
|
||||
metaclass.rb
|
||||
modal_wrapper.rb
|
||||
model.rb
|
||||
point3d.rb
|
||||
point3d_ex.rb
|
||||
profiler.rb
|
||||
progressbar.rb
|
||||
ray.rb
|
||||
selection.rb
|
||||
settings.rb
|
||||
simpletask.rb
|
||||
sketchup.rb
|
||||
system.rb
|
||||
toolbarwindow.rb
|
||||
toolwindow.rb
|
||||
uvq.rb
|
||||
uv_plane.rb
|
||||
version.rb
|
||||
webdialog_patch.rb
|
||||
win32.rb
|
||||
win32_shim.rb
|
||||
window.rb
|
||||
@@ -1,47 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'json.rb'
|
||||
|
||||
# Javascript helper module.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module SpeckleConnector
|
||||
module TT::Javascript
|
||||
|
||||
# Query to whether it's a Group or ComponentInstance
|
||||
#
|
||||
# @param [Object] object
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.5.0
|
||||
def self.to_js(object, format=false)
|
||||
if object.is_a?( TT::JSON )
|
||||
object.to_s(format)
|
||||
elsif object.is_a?( Hash )
|
||||
TT::JSON.new(object).to_s(format)
|
||||
elsif object.is_a?( Symbol ) # 2.5.0
|
||||
object.inspect.inspect
|
||||
elsif object.nil?
|
||||
'null'
|
||||
elsif object.is_a?( Array ) # 2.7.0
|
||||
data = object.map { |x| self.to_js(x, format) }
|
||||
"[#{data.join(',')}]"
|
||||
elsif object.is_a?( Geom::Point3d ) # 2.7.0
|
||||
"new Point3d( #{object.to_a.join(', ')} )"
|
||||
elsif object.is_a?( Geom::Vector3d ) # 2.7.0
|
||||
"new Vector3d( #{object.to_a.join(', ')} )"
|
||||
else
|
||||
# (!) Filter out accepted objects.
|
||||
# (!) Convert unknown into strings - then inspect.
|
||||
object.inspect
|
||||
end
|
||||
end
|
||||
|
||||
end # module TT::Javascript
|
||||
end
|
||||
@@ -1,150 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'javascript.rb'
|
||||
|
||||
# Sortable Hash that preserves the insertion order.
|
||||
# Prints out JSON strings of the content.
|
||||
#
|
||||
# Based of Bill Kelly's InsertOrderPreservingHash
|
||||
#
|
||||
# @see http://www.ruby-forum.com/topic/166075#728764
|
||||
#
|
||||
# @since 2.4.0
|
||||
module SpeckleConnector
|
||||
class TT::JSON
|
||||
include Enumerable
|
||||
|
||||
# @since 2.4.0
|
||||
def initialize(*args, &block)
|
||||
if args.size == 1 && args[0].is_a?(Hash)
|
||||
@h = args[0].dup
|
||||
@ordered_keys = @h.keys
|
||||
else
|
||||
@h = Hash.new(*args, &block)
|
||||
@ordered_keys = []
|
||||
end
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def initialize_copy(source)
|
||||
super
|
||||
@h = @h.dup
|
||||
@ordered_keys = @ordered_keys.dup
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def []=(key, val)
|
||||
@ordered_keys << key unless @h.has_key? key
|
||||
@h[key] = val
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def each
|
||||
@ordered_keys.each {|k| yield(k, @h[k])}
|
||||
end
|
||||
alias :each_pair :each
|
||||
|
||||
# @since 2.4.0
|
||||
def each_value
|
||||
@ordered_keys.each {|k| yield(@h[k])}
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def each_key
|
||||
@ordered_keys.each {|k| yield k}
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def key?(key)
|
||||
@h.key?(key)
|
||||
end
|
||||
alias :has_key? :key?
|
||||
alias :include? :key?
|
||||
alias :member? :key?
|
||||
|
||||
# @since 2.4.0
|
||||
def keys
|
||||
@ordered_keys
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def values
|
||||
@ordered_keys.map {|k| @h[k]}
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def clear
|
||||
@ordered_keys.clear
|
||||
@h.clear
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def delete(k, &block)
|
||||
@ordered_keys.delete k
|
||||
@h.delete(k, &block)
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def reject!
|
||||
del = []
|
||||
each_pair {|k,v| del << k if yield k,v}
|
||||
del.each {|k| delete k}
|
||||
del.empty? ? nil : self
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def delete_if(&block)
|
||||
reject!(&block)
|
||||
self
|
||||
end
|
||||
|
||||
# @since 2.4.0
|
||||
def merge!(hash)
|
||||
hash.each { |key, value|
|
||||
if @h.key?(key)
|
||||
@h[key] = value
|
||||
else
|
||||
self[key] = value
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
#%w(merge!).each do |name|
|
||||
# define_method(name) do |*args|
|
||||
# raise NotImplementedError, "#{name} not implemented"
|
||||
# end
|
||||
#end
|
||||
|
||||
# @since 2.4.0
|
||||
def method_missing(*args)
|
||||
@h.send(*args)
|
||||
end
|
||||
|
||||
# @return [Hash]
|
||||
# @since 2.4.0
|
||||
def to_hash
|
||||
@h.dup
|
||||
end
|
||||
|
||||
# Compile JSON Hash into a string.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def to_s(format=false)
|
||||
arr = self.map { |k,v|
|
||||
key = ( k.is_a?(Symbol) ) ? k.to_s.inspect : k.inspect
|
||||
value = TT::Javascript.to_js(v, format)
|
||||
"#{key}: #{value}"
|
||||
}
|
||||
str = (format) ? arr.join(",\n\t") : arr.join(", ")
|
||||
return (format) ? "{\n\t#{str}\n}\n" : "{#{str}}"
|
||||
end
|
||||
alias :inspect :to_s
|
||||
|
||||
end # class TT::JSON
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
# Collection of Length methods.
|
||||
#
|
||||
# @since 2.7.0
|
||||
module SpeckleConnector
|
||||
module TT::Length
|
||||
|
||||
# @param [Length] length
|
||||
# @param [Length] snap
|
||||
#
|
||||
# @return [Length]
|
||||
# @since 2.7.0
|
||||
def self.snap( length, snap )
|
||||
return length.to_l if snap.zero?
|
||||
diff = length % snap
|
||||
if diff > snap / 2.0
|
||||
new_length = length - diff + snap
|
||||
else
|
||||
new_length = length - diff
|
||||
end
|
||||
new_length.to_l
|
||||
end
|
||||
|
||||
end # module TT::Length
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
The tt_api.so file in this folder is a recompiled version of api.so from the
|
||||
Win32Utils API library.
|
||||
|
||||
Original version: http://win32utils.rubyforge.org/ ( Artistic License 2.0 )
|
||||
|
||||
This modified version is simply rewrapped under my own namespace:
|
||||
TT::Win32::API
|
||||
|
||||
No other changes made. Please do not redistrubute, use the original version
|
||||
instead as this version would be of no use for anyone else.
|
||||
|
||||
-Thomas Thomassen (www.thomthom.net)
|
||||
@@ -1,124 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# Collection of Group, ComponentInstnace and Image methods.
|
||||
#
|
||||
# @since 2.4.0
|
||||
module TT::Locale
|
||||
|
||||
# Some locale settings uses the comma as decimal separator. .to_f does not
|
||||
# account for this, so all commas must be coverted to periods.
|
||||
#
|
||||
# @param [String] string
|
||||
#
|
||||
# @return [Float]
|
||||
# @since 2.4.0
|
||||
def self.string_to_float(string)
|
||||
# Dirty hack to get the current locale's decimal separator, which is then
|
||||
# replaced in the string to a period which is what the .to_f method expects.
|
||||
@decimal_separator ||= self.decimal_separator
|
||||
string.tr(@decimal_separator, '.').to_f
|
||||
end
|
||||
|
||||
|
||||
# Returns a string with the decimal separator in the user's locale and
|
||||
# in the precision of the model units.
|
||||
#
|
||||
# @param [Float] float
|
||||
# @param [Integer] precision (Added: 2.7.0)
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.4.0
|
||||
def self.float_to_string( float, precision = nil )
|
||||
@decimal_separator ||= self.decimal_separator
|
||||
if precision
|
||||
sprintf( "%.#{precision}f", float ).tr!( '.', @decimal_separator )
|
||||
else
|
||||
float.to_s.tr!( '.', @decimal_separator )
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Formats the given float to a string with the user's locale decimal delmitor
|
||||
# and with the precision given in the model's option for lengths.
|
||||
#
|
||||
# @param [Float] float
|
||||
# @param [Sketchup::Model] model
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.4.0
|
||||
def self.format_float(float, model)
|
||||
@decimal_separator ||= self.decimal_separator
|
||||
precision = model.options['UnitsOptions']['LengthPrecision']
|
||||
num = sprintf("%.#{precision}f", float)
|
||||
if num.to_f != float
|
||||
num = "~ #{num}"
|
||||
end
|
||||
num.tr!('.', @decimal_separator)
|
||||
num
|
||||
end
|
||||
|
||||
|
||||
# Casts +string+ to +type+s class.
|
||||
#
|
||||
# @param [String] string
|
||||
# @param [Mixed] type Class or an instance of an class.
|
||||
# @param [String] array_split
|
||||
#
|
||||
# @return [Mixed]
|
||||
# @since 2.4.0
|
||||
def self.cast_string(string, type, array_split = ',')
|
||||
type = type.new if type.class == Class
|
||||
if string == 'null'
|
||||
value = nil
|
||||
elsif type.is_a?( Integer )
|
||||
value = string.to_i
|
||||
elsif type.is_a?( Float ) && !type.is_a?( Length )
|
||||
value = self.string_to_float(string)
|
||||
elsif type.is_a?( Length )
|
||||
value = string.to_l
|
||||
elsif type.is_a?( TrueClass ) || type.is_a?( FalseClass )
|
||||
value = ( string.downcase == 'true' ) ? true : false
|
||||
elsif type.is_a?( Array )
|
||||
value = string.split( array_split )
|
||||
else
|
||||
value = string
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
|
||||
# Hack to determine if the user's locale uses comma or periodas decimal
|
||||
# separator. Makes the assumption that if it's not period, then is is
|
||||
# comma. Yields incorrect result if the user has some other obscure
|
||||
# separator.
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.4.0
|
||||
def self.decimal_separator
|
||||
# If this raises an error the decimal separator is not '.'
|
||||
'1.2'.to_l
|
||||
return '.'
|
||||
rescue
|
||||
return ','
|
||||
end
|
||||
|
||||
|
||||
# @note Currently makes the assumption that locales with comma as decimal
|
||||
# uses semi-colon and locales with period as decimal uses comma.
|
||||
#
|
||||
# @return [String]
|
||||
# @since 2.6.0
|
||||
def self.list_separator
|
||||
( self.decimal_separator == '.' ) ? ',' : ';'
|
||||
end
|
||||
|
||||
end # module TT::Locale
|
||||
end
|
||||
@@ -1,180 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'system.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# Collection of Material methods.
|
||||
#
|
||||
# @since 2.5.0
|
||||
module TT::Material
|
||||
|
||||
# When the user clicks on a material in the materials browser
|
||||
# +model.materials.current+ will return a material that does not exist in the
|
||||
# model. It is possible to apply this material to entities in the model, but
|
||||
# it will evetually BugSplat.
|
||||
#
|
||||
# This method checks if a given material exist in the model and is safe to use.
|
||||
#
|
||||
# @param [Sketchup::Material] material
|
||||
# @param [Sketchup::Model] model
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.in_model?( material, model = Sketchup.active_model )
|
||||
#model.materials.any? { |m| m == material }
|
||||
# This is probably just as good to write. Is this wrapper method needed?
|
||||
model.materials.include?( material )
|
||||
end
|
||||
|
||||
|
||||
# Because SketchUp will bugsplat if a material selected from the library is
|
||||
# used this method attempts to add the material to the model.
|
||||
#
|
||||
# @note Do not use within a start_operation block. This method uses a
|
||||
# temporary operation which will break any already initiated operations.
|
||||
#
|
||||
# @param [Sketchup::Model] model
|
||||
#
|
||||
# @return [Sketchup::Material]
|
||||
# @since 2.7.0
|
||||
def self.get_current( model = Sketchup.active_model )
|
||||
materials = model.materials
|
||||
m = materials.current
|
||||
return m if m.nil?
|
||||
# Check if material already exists. If it does - reuse it.
|
||||
name = m.name
|
||||
if x = materials[ name ]
|
||||
if x.color.to_i == m.color.to_i && x.alpha == m.alpha
|
||||
# No texture applied.
|
||||
if x.texture.nil? && m.texture.nil?
|
||||
materials.current = x
|
||||
return x
|
||||
end
|
||||
x_basename = File.basename( x.texture.filename )
|
||||
m_basename = File.basename( m.texture.filename )
|
||||
if m_basename == m.texture.filename
|
||||
# If the texture only have a filename - not a path.
|
||||
if x_basename == m.texture.filename &&
|
||||
x.texture.width == m.texture.width &&
|
||||
x.texture.height == m.texture.height
|
||||
materials.current = x
|
||||
return x
|
||||
end
|
||||
else
|
||||
# If the texture only have a filename with full path.
|
||||
if x.texture.filename == m.texture.filename &&
|
||||
x.texture.width == m.texture.width &&
|
||||
x.texture.height == m.texture.height
|
||||
materials.current = x
|
||||
return x
|
||||
end
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
# Transfer name and colour.
|
||||
new_material = materials.add( name )
|
||||
new_material.color = m.color
|
||||
new_material.alpha = m.alpha
|
||||
# Any textures require special attention. If the filename contains a valid
|
||||
# path to an existing file nothing special needs to be done.
|
||||
#
|
||||
# But if the filename refers to a non-existing file it needs to be written
|
||||
# out to a temp file. This is where things become a bit risky and hacky.
|
||||
# Because TextureWriter doesn't accept Material objects the orphan material
|
||||
# needs to be temporarily applied to the model (risky).
|
||||
#
|
||||
# Materials from SketchUp's default library only contains the name of the
|
||||
# file without any paths. This is because the texture is located only within
|
||||
# the.skm.
|
||||
if m.texture
|
||||
filename = m.texture.filename
|
||||
if File.exist?( filename )
|
||||
new_material.texture = filename
|
||||
else
|
||||
# Create temp file to write the texture to.
|
||||
temp_path = TT::System.temp_path
|
||||
temp_folder = File.join( temp_path, 'tt_su_tmp_mtl' )
|
||||
temp_filename = File.basename( filename )
|
||||
temp_file = File.join( temp_folder, temp_filename )
|
||||
unless File.exist?( temp_folder )
|
||||
Dir.mkdir( temp_folder )
|
||||
end
|
||||
# Create temp group with the orphan material and write it out.
|
||||
#
|
||||
# Wrap within start_operation and clean up with abort_operation so it
|
||||
# doesn't end up in the undo stack.
|
||||
#
|
||||
# (!) This means this method should not occur within any other
|
||||
# start_operation blocks - as operations cannot be nested.
|
||||
tw = Sketchup.create_texture_writer
|
||||
model.start_operation( 'Extract Orphan Material' ) # rubocop:disable SketchupPerformance/OperationDisableUI
|
||||
begin
|
||||
g = model.entities.add_group # rubocop:disable SketchupSuggestions/ModelEntities
|
||||
g.material = m
|
||||
tw.load( g )
|
||||
tw.write( g, temp_file )
|
||||
ensure
|
||||
model.abort_operation
|
||||
end
|
||||
# Load texture to material and clean up.
|
||||
new_material.texture = temp_file
|
||||
File.delete( temp_file )
|
||||
end
|
||||
new_material.texture.size = [ m.texture.width, m.texture.height ]
|
||||
end
|
||||
materials.current = new_material
|
||||
new_material
|
||||
end
|
||||
|
||||
|
||||
# Safely removes a material from a model.
|
||||
#
|
||||
# @param [Sketchup::Material] material
|
||||
# @param [Sketchup::Model] model
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.5.0
|
||||
def self.remove( material, model = Sketchup.active_model )
|
||||
# SketchUp 8.0M1 introduced model.materials.remove, which turned out to be
|
||||
# bugged. It didn't remove the material from the entities in the model -
|
||||
# leaving the model with rouge invalid materials.
|
||||
# To work around this all entities are processed before the method is called.
|
||||
# The workaround for older versions also require this to be done.
|
||||
for e in model.entities # rubocop:disable SketchupSuggestions/ModelEntities
|
||||
e.material = nil if e.respond_to?( :material ) && e.material == material
|
||||
e.back_material = nil if e.respond_to?( :back_material ) && e.back_material == material
|
||||
end
|
||||
for d in model.definitions
|
||||
next if d.image?
|
||||
for e in d.entities
|
||||
e.material = nil if e.respond_to?( :material ) && e.material == material
|
||||
e.back_material = nil if e.respond_to?( :back_material ) && e.back_material == material
|
||||
end
|
||||
end
|
||||
materials = model.materials
|
||||
if materials.respond_to?( :remove )
|
||||
materials.remove( material )
|
||||
else
|
||||
# Workaround for SketchUp versions older than 8.0M1. Add all materials
|
||||
# except the one to be removed to temporary groups and purge the materials.
|
||||
temp_group = model.entities.add_group # rubocop:disable SketchupSuggestions/ModelEntities
|
||||
for m in model.materials
|
||||
next if m == material
|
||||
g = temp_group.add_group
|
||||
g.material = material
|
||||
end
|
||||
materials.purge_unused
|
||||
temp_group.erase!
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
end # module TT::Material
|
||||
end
|
||||
@@ -1,78 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# @example
|
||||
# module Foo
|
||||
# extend TT::MetaClass
|
||||
# cattr :bar
|
||||
# end
|
||||
#
|
||||
# @since 2.7.0
|
||||
module TT::MetaClass
|
||||
|
||||
# @since 2.7.0
|
||||
def metaclass
|
||||
class << self
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
def cattr_accessor( *args )
|
||||
metaclass.class_eval {
|
||||
attr_accessor( *args )
|
||||
}
|
||||
end
|
||||
alias :cattr :cattr_accessor
|
||||
|
||||
# @since 2.7.0
|
||||
def cattr_reader( *args )
|
||||
metaclass.class_eval {
|
||||
attr_reader( *args )
|
||||
}
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
def cattr_writer( *args )
|
||||
metaclass.class_eval {
|
||||
attr_writer( *args )
|
||||
}
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
def cbattr_accessor( *args )
|
||||
metaclass.class_eval {
|
||||
attr_accessor( *args )
|
||||
for attribute in args
|
||||
question = "#{attribute}?".to_sym
|
||||
alias_method( question, attribute )
|
||||
remove_method( attribute )
|
||||
end
|
||||
}
|
||||
end
|
||||
alias :cbattr :cbattr_accessor
|
||||
|
||||
# @since 2.7.0
|
||||
def cbattr_reader( *args )
|
||||
metaclass.class_eval {
|
||||
attr_reader( *args )
|
||||
for attribute in args
|
||||
question = "#{attribute}?".to_sym
|
||||
alias_method( question, attribute )
|
||||
remove_method( attribute )
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# @since 2.7.0
|
||||
alias :cbattr_writer :cattr_writer
|
||||
|
||||
end # module TT::MetaClass
|
||||
end
|
||||
@@ -1,131 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'gui.rb'
|
||||
require_relative 'sketchup.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# Creates a wrapper class that simulates a modal environment for the window. It
|
||||
# is not truly modal because of limitations of the API under OSX, but uses a tool
|
||||
# class to prevent the user from manipulating the model while the window is open.
|
||||
# If the user should activate another tool it acts as if the user used the Close
|
||||
# or cancel button.
|
||||
#
|
||||
# @todo Prevent other Windows from opening (?)
|
||||
#
|
||||
# @since 2.4.0
|
||||
class TT::GUI::ModalWrapper
|
||||
|
||||
@@open_window = nil
|
||||
|
||||
# @param [TT::GUI::Window] window
|
||||
#
|
||||
# @since 2.4.0
|
||||
def initialize(window)
|
||||
@window = window
|
||||
end
|
||||
|
||||
# @private
|
||||
# @since 2.4.0
|
||||
def activate
|
||||
#puts "T:activate - #{@@open_window}"
|
||||
Sketchup.active_model.active_view.invalidate
|
||||
@@open_window = @window
|
||||
@window.show_window
|
||||
end
|
||||
|
||||
# @private
|
||||
# @since 2.4.0
|
||||
def deactivate(view)
|
||||
#puts 'T:deactivate'
|
||||
#puts "> visible? #{@window.visible?}"
|
||||
#puts "> closing? #{@window.closing?}"
|
||||
@window.close unless @window.closing?
|
||||
@@open_window = nil
|
||||
view.invalidate
|
||||
end
|
||||
|
||||
# @private
|
||||
# @since 2.5.0
|
||||
def resume(view)
|
||||
view.invalidate
|
||||
end
|
||||
|
||||
# @private
|
||||
# @since 2.4.0
|
||||
def onLButtonDown(flags, x, y, view)
|
||||
UI.beep
|
||||
@window.bring_to_front
|
||||
end
|
||||
|
||||
# @private
|
||||
# @since 2.4.0
|
||||
def getMenu(menu)
|
||||
# Suppress the context menu
|
||||
menu.add_item('Close Dialog') {
|
||||
@window.close
|
||||
}
|
||||
end
|
||||
|
||||
# @private
|
||||
# @since 2.4.0
|
||||
def onCancel(reason, view)
|
||||
#puts "T:onCancel: reason #{reason.to_s}"
|
||||
end
|
||||
|
||||
if TT::SketchUp::support?( TT::SketchUp::COLOR_ALPHA )
|
||||
# Dim the SketchUp viewport while the modal window is open.
|
||||
# @private
|
||||
# @since 2.5.0
|
||||
def draw(view)
|
||||
pts = [
|
||||
[0,0,0],
|
||||
[view.vpwidth,0,0],
|
||||
[view.vpwidth,view.vpheight,0],
|
||||
[0,view.vpheight,0]
|
||||
]
|
||||
view.drawing_color = Sketchup::Color.new(0,0,0,128)
|
||||
view.draw2d( GL_QUADS, pts )
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
### Public Methods ###
|
||||
|
||||
|
||||
# Displays the modal window as long as there are no other modal windows open.
|
||||
# It only handles +TT::GUI::Window+ objects that uses the +ModalWrapper+ class.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def show
|
||||
#puts 'T:show'
|
||||
@closing = false
|
||||
if @@open_window
|
||||
UI.beep
|
||||
@@open_window.bring_to_front
|
||||
else
|
||||
Sketchup.active_model.tools.push_tool( self )
|
||||
end
|
||||
end
|
||||
|
||||
# Closes the modal window.
|
||||
#
|
||||
# @since 2.4.0
|
||||
def close
|
||||
#puts 'T:close'
|
||||
#puts caller.join("\r\n")
|
||||
# Prevent popping the tool multiple times. This worked fine in older SketchUp
|
||||
# versions, but regressed in SU2014 where it would then cause a crash.
|
||||
return false if @closing
|
||||
@closing = true
|
||||
Sketchup.active_model.tools.pop_tool
|
||||
true
|
||||
end
|
||||
|
||||
end # class TT::GUI::ModalWrapper
|
||||
end
|
||||
@@ -1,163 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'sketchup.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# Collection of Model methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module TT::Model
|
||||
|
||||
# Version safe +model.start_operation+ wrapper.
|
||||
#
|
||||
# @param [String] name
|
||||
# @param [Sketchup::Model] model - defaults to active model.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.0.0
|
||||
def self.start_operation(name, model = Sketchup.active_model)
|
||||
if TT::SketchUp.version[0] >= 7
|
||||
model.start_operation(name, true)
|
||||
else
|
||||
model.start_operation(name) # rubocop:disable SketchupPerformance/OperationDisableUI
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Counts all unique +Sketchup::Entities+ collections in the given model,
|
||||
# excluding Image definitions.
|
||||
#
|
||||
# @param [Sketchup::Model] model
|
||||
# @param [Boolean] only_used_definitions
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @return [Integer]
|
||||
# @since 2.5.0
|
||||
def self.count_unique_entities( model, only_used_definitions=true, options={} )
|
||||
skip_locked = options[:locked] && options[:locked].is_a?( Hash )
|
||||
if only_used_definitions
|
||||
model.definitions.reject { |d|
|
||||
d.image? ||
|
||||
d.count_instances == 0 ||
|
||||
(skip_locked && options[:locked].key?(d))
|
||||
}.length + 1
|
||||
else
|
||||
model.definitions.reject { |d|
|
||||
d.image? ||
|
||||
(skip_locked && options[:locked].key?(d))
|
||||
}.length + 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Counts all unique entities in the given model, including sub-entities.
|
||||
#
|
||||
# @param [Sketchup::Model] model
|
||||
# @param [Boolean] only_used_definitions
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @return [Integer]
|
||||
# @since 2.5.0
|
||||
def self.count_unique_entity( model, only_used_definitions=true, options={} )
|
||||
skip_locked = options[:locked] && options[:locked].is_a?( Hash )
|
||||
c = model.entities.length # rubocop:disable SketchupSuggestions/ModelEntities
|
||||
for d in model.definitions
|
||||
next if d.image?
|
||||
next if only_used_definitions && d.count_instances == 0
|
||||
next if skip_locked && options[:locked].key?(d)
|
||||
c += d.entities.count
|
||||
end
|
||||
c
|
||||
end
|
||||
|
||||
|
||||
# Yields each unique Entities collection recursivly.
|
||||
#
|
||||
# TT::Model.each_entities { |entities|
|
||||
# processEntities( entities )
|
||||
# }
|
||||
#
|
||||
# If a number is returned to the processing block it will be used to add up a
|
||||
# total when +each_entities+ returns.
|
||||
#
|
||||
# TT::Model.each_entities { |entities|
|
||||
# c = 0
|
||||
# for e in entities
|
||||
# c += 1 if e.is_a?( SketchUp::Edge )
|
||||
# end
|
||||
# c
|
||||
# }
|
||||
#
|
||||
# This example will return the total number of edges processed. Use to keep
|
||||
# statistic for the iteration.
|
||||
#
|
||||
# @param [Sketchup::Model] model
|
||||
# @param [Boolean] only_used_definitions
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @yield [entities]
|
||||
# @yieldparam [Enumerable|Sketchup::Entities] entities
|
||||
#
|
||||
# @return [Integer] Returns
|
||||
# @since 2.5.0
|
||||
def self.each_entities( model, only_used_definitions=true, options={} )
|
||||
skip_locked = options[:locked] && options[:locked].is_a?( Hash )
|
||||
c = 0
|
||||
result = yield( model.entities ) # rubocop:disable SketchupSuggestions/ModelEntities
|
||||
c += result if result.is_a?( Numeric )
|
||||
for d in model.definitions
|
||||
next if d.image?
|
||||
next if only_used_definitions && d.count_instances == 0
|
||||
next if skip_locked && options[:locked].key?(d)
|
||||
result += yield( d.entities )
|
||||
c += result if result.is_a?( Numeric )
|
||||
end
|
||||
c
|
||||
end
|
||||
|
||||
|
||||
# Yields all the entities in the model. Note that it does not traverse the
|
||||
# model hierarchy.
|
||||
#
|
||||
# When +only_used_definitions+ is true, only entities that is used in the model
|
||||
# is yielded. When +only_used_definitions+ is false, also the entiites in
|
||||
# unused definitions are yielded.
|
||||
#
|
||||
# @param [Sketchup::Model] model
|
||||
# @param [Boolean] only_used_definitions
|
||||
# @param [Hash] options
|
||||
#
|
||||
# @yield [entity]
|
||||
# @yieldparam [Sketchup::Entity] entity
|
||||
#
|
||||
# @return [Integer] Number of entities which returned non-false.
|
||||
# @since 2.5.0
|
||||
def self.each_entity( model, only_used_definitions=true, options={} )
|
||||
skip_locked = options[:locked] && options[:locked].is_a?( Hash )
|
||||
# c = c.next is faster than c += 1
|
||||
# http://forums.sketchucation.com/viewtopic.php?f=180&t=25305&p=305810#p305810
|
||||
c = 0
|
||||
for e in model.entities.to_a # # rubocop:disable SketchupSuggestions/ModelEntities
|
||||
next if skip_locked && e.respond_to?( :locked? ) && e.locked?
|
||||
c = c.next if yield( e )
|
||||
end
|
||||
for d in model.definitions
|
||||
next if d.image?
|
||||
next if only_used_definitions && d.count_instances == 0
|
||||
next if skip_locked && options[:locked].key?(d)
|
||||
for e in d.entities.to_a
|
||||
next if skip_locked && e.respond_to?( :locked? ) && e.locked?
|
||||
c = c.next if yield( e )
|
||||
end
|
||||
end
|
||||
c
|
||||
end
|
||||
|
||||
end # module TT::Model
|
||||
end
|
||||
@@ -1,127 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Thomas Thomassen
|
||||
# thomas[at]thomthom[dot]net
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
require_relative 'core.rb'
|
||||
require_relative 'point3d_ex.rb'
|
||||
|
||||
module SpeckleConnector
|
||||
# Collection of Point3d methods.
|
||||
#
|
||||
# @since 2.0.0
|
||||
module TT::Point3d
|
||||
|
||||
# Checks if point +c+ is between point +a+ and +b+.
|
||||
#
|
||||
# Return true if +c+ is on +a+ or +b+.
|
||||
#
|
||||
# @param [Geom::Point3d] a
|
||||
# @param [Geom::Point3d] b
|
||||
# @param [Geom::Point3d] c
|
||||
# @param [Geom::Point3d] on_point - When +true+, if point +c+ is at the same
|
||||
# position as +a+ or +b+ it is considered to be in between.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @since 2.0.0
|
||||
def self.between?(a, b, c, on_point = true)
|
||||
return false unless c.on_line?([a,b])
|
||||
v1 = c.vector_to(a)
|
||||
v2 = c.vector_to(b)
|
||||
if on_point
|
||||
return true if !v1.valid? || !v2.valid?
|
||||
else
|
||||
return false if !v1.valid? || !v2.valid?
|
||||
end
|
||||
!v1.samedirection?(v2)
|
||||
end
|
||||
|
||||
|
||||
# Implementation of the Douglas-Peucker algorithm.
|
||||
#
|
||||
# http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
|
||||
# http://en.wiki.mcneel.com/default.aspx/McNeel/PolylineSimplification.html
|
||||
#
|
||||
# @param [Array<Geom::Point3d>] points Point set to be simplified. No Loop!
|
||||
# @param [Length] epsilon Maximin deviance from original curve.
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.5.0
|
||||
def self.douglas_peucker(points, epsilon)
|
||||
return points if points.length < 3
|
||||
# Find the point with the maximum distance
|
||||
dmax = 0
|
||||
index = 0
|
||||
line = [points.first, points.last]
|
||||
1.upto(points.length - 2) { |i|
|
||||
d = points[i].distance_to_line(line)
|
||||
if d > dmax
|
||||
index = i
|
||||
dmax = d
|
||||
end
|
||||
}
|
||||
# If max distance is greater than epsilon, recursively simplify
|
||||
result = []
|
||||
if dmax >= epsilon
|
||||
# Recursive call
|
||||
recResults1 = self.douglas_peucker(points[0..index], epsilon)
|
||||
recResults2 = self.douglas_peucker(points[index...points.length], epsilon)
|
||||
# Build the result list
|
||||
result = recResults1[0...-1] + recResults2
|
||||
else
|
||||
result = [points.first, points.last]
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
# Extends all the points (+Geom::Point3d+ and +Array+ in +points+) with the
|
||||
# +TT::Point3d_Ex+ mix-in module.
|
||||
#
|
||||
# All +Array+ objects that represent a 3d point will be converted into
|
||||
# +Geom::Point3d+ before being extended.
|
||||
#
|
||||
# @param [Array<Geom::Point3d>] points
|
||||
#
|
||||
# @return [Array<TT::Point3d_Ex>] Geom::Point3d objects extended by TT::Point3d_Ex
|
||||
# @since 2.5.0
|
||||
def self.extend_all( points )
|
||||
extended_points = []
|
||||
for point in points
|
||||
if point.is_a?( Geom::Point3d )
|
||||
point_ex = point
|
||||
elsif point.is_a?( Array )
|
||||
next unless point.size == 3 && point.all? { |n| n.is_a?( Numeric ) }
|
||||
point_ex = Geom::Point3d.new( point.x, point.y, point.z )
|
||||
elsif point.respond_to?( :position )
|
||||
position = point.position
|
||||
point_ex = position if position.is_a?( Geom::Point3d )
|
||||
end
|
||||
point_ex.extend( TT::Point3d_Ex ) unless point_ex.is_a?( TT::Point3d_Ex )
|
||||
extended_points << point_ex
|
||||
end
|
||||
extended_points
|
||||
end
|
||||
|
||||
|
||||
# Wrapper of the Douglas-Peucker algorithm. Handles looping curves.
|
||||
#
|
||||
# @param [Array<Geom::Point3d>] points Point set to be simplified. No Loop!
|
||||
# @param [Length] epsilon Maximin deviance from original curve.
|
||||
#
|
||||
# @return [Array<Geom::Point3d>]
|
||||
# @since 2.5.0
|
||||
def self.simplify_curve(points, epsilon)
|
||||
if points.first == points.last # Detect loop
|
||||
points.pop
|
||||
simplified_curve = self.douglas_peucker(points, epsilon)
|
||||
simplified_curve << points.first
|
||||
else
|
||||
simplified_curve = self.douglas_peucker(points, epsilon)
|
||||
end
|
||||
end
|
||||
|
||||
end # module TT::Point3d
|
||||
end
|
||||