Compare commits

...

100 Commits

Author SHA1 Message Date
oguzhankoral 5ec90095f0 Merge branch 'dev'
# Conflicts:
#	README.md
2025-09-04 23:29:20 +03:00
Oğuzhan Koral 20fad26fef Merge pull request #200 from specklesystems/oguzhan/cherry-pick-readme
Update README.md (#147)
2025-09-04 23:19:32 +03:00
Jonathon Broughton 03215f79c4 Update README.md (#147)
* Update README.md

* Update README.md

(cherry picked from commit 85f8f72335)

# Conflicts:
#	README.md
2025-09-04 23:15:19 +03:00
Dogukan Karatas 6d17377ca2 Merge pull request #198 from specklesystems/dogukan/remove-access-token-auth
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
feat (data): remove token and anonymous authentication
2025-09-03 10:09:43 +02:00
Dogukan Karatas 256abaed0c remove token and anon auth 2025-09-02 21:46:35 +02:00
Dogukan Karatas 26409b4ea6 storing logic changes 2025-09-02 14:22:03 +02:00
Dogukan Karatas 865c4c1608 Merge pull request #197 from specklesystems/dogukan/server-url-in-request-data
fix (data): adds server url to exchange data
2025-09-02 12:53:44 +02:00
Dogukan Karatas 67836c2a7f case sensitive change 2025-09-02 11:00:35 +02:00
Dogukan Karatas 95d819f7f3 adds server url to exchange request data 2025-09-02 10:31:01 +02:00
Dogukan Karatas dee3ee6c4d Merge pull request #196 from specklesystems/dogukan/fix-conditional-formatting-card
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
fix (visual): bring back the conditional formatting
2025-08-28 17:17:27 +02:00
Dogukan Karatas 7ed612ec14 revert back for conditional formatting 2025-08-28 16:30:15 +02:00
Dogukan Karatas 4bd7af4c31 Merge pull request #195 from specklesystems/dogukan/online-mode-with-object-loader
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
feat (visual): dual-mode data loading
2025-08-27 20:40:40 +02:00
Dogukan Karatas 3ed2e977df removed unused function 2025-08-27 16:48:06 +02:00
Dogukan Karatas 788fa1c532 auto internalizing logic 2025-08-27 16:33:09 +02:00
Dogukan Karatas bafb7df6ed integrate two options together 2025-08-26 22:28:11 +02:00
Dogukan Karatas be4e4df983 integreates object loader instead of rest api 2025-08-20 15:25:37 +02:00
Dogukan Karatas b4830c80ab downloader with rest api 2025-08-20 13:16:50 +02:00
Dogukan Karatas a2d97facc5 adds token to the matrix 2025-08-19 17:02:58 +02:00
Dogukan Karatas aea344a46a updated sendtoserver for token exchange 2025-08-18 17:18:29 +02:00
Mucahit Bilal GOKER 13aa65bc2e Merge pull request #191 from specklesystems/bilal/cnx-2200-specklemodelsmaterialquantities-helper-function
Add Models.MaterialQuantities helper function
2025-08-12 10:28:44 +03:00
Mucahit Bilal GOKER 0a307c28e0 Merge pull request #190 from specklesystems/bilal/cnx-2169-properties-add-parent-dict-name-as-a-suffix
Parameter Group as suffix
2025-08-12 10:28:27 +03:00
Dogukan Karatas e0f4a4c02c Merge pull request #192 from specklesystems/dogukan/send-versionId-to-server
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
feat (data): send `versionId` to the server
2025-07-29 10:14:11 +02:00
Dogukan Karatas 29773f9492 versionId added 2025-07-28 16:28:53 +02:00
bimgeek 8f67ef4c84 add optional prefix 2025-07-25 21:37:49 +03:00
bimgeek 2c5f192403 first attempt 2025-07-25 20:25:50 +03:00
bimgeek 0c58789dd6 rename target fields to filterKeys 2025-07-23 16:45:55 +03:00
bimgeek 82acce2abb fix conflict resolution logic 2025-07-23 13:40:07 +03:00
bimgeek d83472c30b add parent name as a suffix 2025-07-23 13:15:13 +03:00
Oğuzhan Koral 634df47a25 Revert "Merge pull request #186 from specklesystems/dogukan/cnx-2136-tag-as-markreceived-after-download-in-dataconnector" (#189)
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
This reverts commit ffc0d8ef5e, reversing
changes made to c8d858d575.

Co-authored-by: bimgeek <mucahitbgoker@gmail.com>
2025-07-22 08:02:27 +01:00
Mucahit Bilal GOKER 9ad59bf1d3 Bilal/cnx 2115 configure oauth2 for data gateway usage (#183)
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
* oauth2 implementation

* change code challenge to plain

* Reorder error checks

* implement proper PKCE security with SHA256
2025-07-21 16:56:44 +01:00
Mucahit Bilal GOKER ffc0d8ef5e Merge pull request #186 from specklesystems/dogukan/cnx-2136-tag-as-markreceived-after-download-in-dataconnector
fix: mark stream as received
2025-07-21 15:59:26 +03:00
Mucahit Bilal GOKER 94c80857a0 Merge branch 'dev' into dogukan/cnx-2136-tag-as-markreceived-after-download-in-dataconnector 2025-07-21 14:47:07 +03:00
Mucahit Bilal GOKER c8d858d575 Merge pull request #185 from specklesystems/dogukan/cnx-1933-disable-selection-of-ghosted-objects
fix (visual): disable selection of ghosted objects
2025-07-21 14:46:54 +03:00
Mucahit Bilal GOKER 36b9787b66 Merge branch 'dev' into dogukan/cnx-1933-disable-selection-of-ghosted-objects 2025-07-21 14:44:23 +03:00
Mucahit Bilal GOKER bde7a42c44 Merge branch 'dev' into dogukan/cnx-2136-tag-as-markreceived-after-download-in-dataconnector 2025-07-21 14:36:24 +03:00
Mucahit Bilal GOKER 1040f4622d Bilal/cnx 2033 update readme (#184)
* update readme

* remove button styling

* added code

* Update README.md

* Update README.md
2025-07-21 14:23:33 +03:00
Dogukan Karatas 91e799d006 mark received function added 2025-07-16 15:45:44 +02:00
Dogukan Karatas 8694666874 remove legacy implementation 2025-07-14 15:02:46 +02:00
Dogukan Karatas fa6ad8ec40 extended filtered selection 2025-07-11 17:04:01 +02:00
Dogukan Karatas 8e60249291 update viewer for ghosted objects 2025-07-11 15:27:29 +02:00
Mucahit Bilal GOKER 68d6bf3d55 Merge pull request #180 from specklesystems/dogukan/cnx-1978-control-camera-animation-on-change
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
feat (visual): toogle for animation on action
2025-07-08 16:14:31 +03:00
Dogukan Karatas 2a8925c8ef icon updated 2025-07-08 15:08:47 +02:00
Mucahit Bilal GOKER f9b3d3db52 Merge pull request #181 from specklesystems/dogukan/cnx-2089-auto-extract-properties
feat (data): extract properties column by default
2025-07-08 15:41:06 +03:00
Mucahit Bilal GOKER bfd0c33373 Merge pull request #179 from specklesystems/bilal/cnx-2106-federate-models-helper-function
Helper Function: Federate Models
2025-07-08 15:39:36 +03:00
Dogukan Karatas d155a4b165 registered to the capabilities 2025-07-08 13:37:17 +02:00
Dogukan Karatas 2e9ece856f adds properties extractor by default 2025-07-07 15:49:38 +02:00
Dogukan Karatas 808e288848 adds option for zoom on select 2025-07-07 14:18:18 +02:00
bimgeek 701116c66c helper function: federate models 2025-07-07 10:48:05 +03:00
Dogukan Karatas e73d392013 Merge pull request #178 from specklesystems/dogukan/cnx-1932-saving-visual-with-filtered-data
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
fix (visual): persistent visual filters
2025-07-04 12:51:52 +02:00
Dogukan Karatas dd7f3fe95d cover initial states 2025-07-03 15:54:09 +02:00
Dogukan Karatas fdcc1f2cef listen load event 2025-07-03 15:42:06 +02:00
Dogukan Karatas 5e2f108e49 Merge pull request #177 from specklesystems/dogukan/cnx-2032-persistent-hide-nav-bar-toggle
fix (visual): persistent navbar toogle
2025-07-01 15:01:48 +02:00
Dogukan Karatas df334e95a2 workspace to viewmode 2025-07-01 14:53:03 +02:00
Dogukan Karatas ce733d1ced adds persistent navbar 2025-07-01 11:15:59 +02:00
Dogukan Karatas 8345258990 Merge pull request #176 from specklesystems/dogukan/fix-function-registration
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
fix: helper function registration
2025-06-24 15:22:12 +02:00
Dogukan Karatas dbd0f2f9ce fix registration 2025-06-24 15:19:11 +02:00
Dogukan Karatas c9b4155660 Merge pull request #174 from specklesystems/bilal/object-compound-structure
added objects composite structure
2025-06-24 14:56:26 +02:00
Dogukan Karatas c4d094d722 Merge branch 'dev' into bilal/object-compound-structure 2025-06-24 14:55:06 +02:00
Dogukan Karatas 5b003b182b Merge pull request #173 from specklesystems/bilal/object-materialquantities
object material quantities added
2025-06-24 14:54:36 +02:00
Dogukan Karatas 7eabd47f6d Merge pull request #175 from specklesystems/dogukan/cnx-2035-loading-got-veeeeeeery-sloooooooow
performance fix: remove `displayValue` exclusion
2025-06-24 14:54:24 +02:00
Dogukan Karatas 822b999be9 remove displayValue exclusion 2025-06-24 13:47:30 +02:00
Dogukan Karatas 2407b8758a Merge pull request #171 from specklesystems/dogukan/exclude-display-value
feat(data): exclude displayValue in data
2025-06-24 11:09:48 +02:00
Mucahit Bilal GOKER d6f5e65bd7 added objects composite structure 2025-06-19 15:28:41 +03:00
Mucahit Bilal GOKER 183cc36654 object material quantities added 2025-06-19 15:15:05 +03:00
Mucahit Bilal GOKER a740272585 Merge pull request #170 from specklesystems/bilal/expand-record-util
expand record helper function
2025-06-19 15:11:51 +03:00
Dogukan Karatas 72128a9f4e excludes displayValues in data 2025-06-19 13:55:41 +02:00
Mucahit Bilal GOKER 677c663ef3 Merge branch 'dev' into bilal/expand-record-util 2025-06-19 14:55:21 +03:00
Mucahit Bilal GOKER a077857c66 add new line 2025-06-19 14:40:16 +03:00
Mucahit Bilal GOKER 5897a286bc expand record helper function 2025-06-19 14:23:40 +03:00
Dogukan Karatas 5b49fb2a9a Merge pull request #169 from specklesystems/dogukan/revit-helper-functions
feat (data): get properties function
2025-06-19 13:07:57 +02:00
Dogukan Karatas 424404dd11 Merge branch 'dogukan/revit-helper-functions' of https://github.com/specklesystems/speckle-powerbi into dogukan/revit-helper-functions 2025-06-19 13:04:18 +02:00
Dogukan Karatas 31312522a7 adds objects.collections 2025-06-19 12:47:18 +02:00
Mucahit Bilal GOKER 932198dccf rename GetProperties to Objects.Properties 2025-06-19 13:47:13 +03:00
Dogukan Karatas 3770502ca4 adds function overload 2025-06-18 16:30:36 +02:00
Dogukan Karatas 93e8fcdd9d gets duplicated props 2025-06-18 16:13:42 +02:00
Dogukan Karatas 370052b2be GetProperties added 2025-06-18 14:41:14 +02:00
Oğuzhan Koral aa4a137a0d handle size in MB separately (#168)
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled
2025-06-16 21:17:43 +03:00
Dogukan Karatas 4acdf30734 error handling (#167)
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-06-16 12:05:21 +00:00
Dogukan Karatas b531446acd feat (visual): send connector version to mixpanel (#165)
* connector version added

* updated props
2025-06-16 15:03:54 +03:00
Jedd Morgan de1b2ca39c Run CI on PR (#164) 2025-06-06 10:14:00 +01:00
Jedd Morgan 5642da1e57 Ensure bash (#163)
build_powerbi / build-connector (push) Has been cancelled
build_powerbi / build-visual (push) Has been cancelled
build_powerbi / deploy-installers (push) Has been cancelled
2025-05-30 18:38:18 +03:00
Jedd Morgan 276d0c3a76 removed last trace of gitversion (#162) 2025-05-30 18:34:21 +03:00
Jedd Morgan 2d92b85687 fix(ci): replace gitversion with tag parsing (#161)
* remove git version

* delete unused gitversion

* disable circle ci
2025-05-30 16:27:57 +01:00
Oğuzhan Koral 82bd109b85 Feat: visual revamp (#156)
* delete debugger

* No need tooltip data as part of interactivity

* Fix coloring

* delete logging

* Fix messaging on interactivity for tooltip data

* Delete unused code

* Navbar and cursors

* Revamp viewer actions

* Hide viewer actions

* not a css master commit

* Toggle projection/orthi

* Remove console log

* Fix saved objects

* Remove console log

* Sort performance logging

* fix view mode cache

* Fix initial isolate issue

* Update README.md (#147)

* Update README.md

* Update README.md

* fix typo on conditions

* capabilities for object ids

* Revert isolating every setDataInput

* Fix selection issues

* Fix tooltip fckp

* Reset filters

* Fix reset filter

* Ghost hidden on filter

* Bring conditional formatting back

* Remove ghost hidden context from color card

* Disable shadow catcher

* Disable camera position persistence for performance reasons

* feat (data): sending version and branding info (#157)

* get version

* adds workspace info

* adds hideBranding

* adds workspace info

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>

* Enrich receive info from desktop service

* test

* fix path

* fix path again

* file version

* correct the version with assembly for file version

* sanitize tag

* Use version from receive info

* Fix clipped zoom extend

* Workspace name logo

* Add can hide branding logic

* Fix tooltip for toggle

* Fix capabilities for storing branding

* Tooltips

* Store is ortho in file

* Store camera position and target into file according to selected view

* Fix loading bar reactivity

* Update connector flow

* Fix ghost state

* Store is ghost in file

* Consider ghost value on reset filter

* More

* excludes rawencoding (#159)

* adds null check for personal (#158)

* Progress update and error handling

* Call pre get before

---------

Co-authored-by: Jonathon Broughton <760691+jsdbroughton@users.noreply.github.com>
Co-authored-by: Dogukan Karatas <61163577+dogukankaratas@users.noreply.github.com>
2025-05-30 17:43:21 +03:00
Kristaps Fabians Geikins 38fb5f7c26 fix: make webpack prefer module builds (#155)
* fix: make webpack prefer module builds

* dev screen
2025-05-24 00:27:27 +03:00
Dogukan Karatas dee021f0ef Merge pull request #154 from specklesystems/dogukan/cnx-1801-can-view-can-edit-in-power-bi-data-connector
feat(data connector): server-side permisson check integration
2025-05-14 13:46:56 +02:00
Dogukan Karatas 620bd22387 integrates permisson checks 2025-05-14 13:26:21 +02:00
Dogukan Karatas d364e096a8 Merge pull request #153 from specklesystems/dogukan/cnx-1551-reaching-end-of-the-buffer-in-power-bi
build_powerbi / build-connector (push) Has been cancelled
build_powerbi / build-visual (push) Has been cancelled
build_powerbi / deploy-installers (push) Has been cancelled
fix(data connector): reaching the end of the buffer
2025-05-14 11:43:32 +02:00
Dogukan Karatas 8e03bcf201 fix typo 2025-05-14 11:08:23 +02:00
Dogukan Karatas 1825a5ad4c update the query 2025-05-14 10:08:20 +02:00
Dogukan Karatas a626cdd45d updates acessing the token 2025-05-14 10:04:40 +02:00
Oğuzhan Koral 91871f8615 handle chunks not chunk (#152)
build_powerbi / build-connector (push) Has been cancelled
build_powerbi / build-visual (push) Has been cancelled
build_powerbi / deploy-installers (push) Has been cancelled
2025-05-14 10:04:54 +03:00
Dogukan Karatas 2da1602986 Merge pull request #146 from specklesystems/dogukan/cnx-1502-merge-tables-of-federated-models
build_powerbi / build-connector (push) Has been cancelled
build_powerbi / build-visual (push) Has been cancelled
build_powerbi / deploy-installers (push) Has been cancelled
feat: federated models
2025-04-30 16:18:44 +02:00
Dogukan Karatas a0372e1970 Merge pull request #149 from specklesystems/alex/no-more-legacy-viewer
Alex/no more legacy viewer
2025-04-30 16:17:39 +02:00
oguzhankoral 98f10bb344 Sort the new viewer after Alex and compression for federated models 2025-04-29 17:08:27 +03:00
oguzhankoral ee11e47af3 Merge remote-tracking branch 'origin/alex/no-more-legacy-viewer' into dogukan/cnx-1502-merge-tables-of-federated-models 2025-04-29 14:25:04 +03:00
AlexandruPopovici f57697d929 Got rid of LegacyViewer and adjusted the wrapper accordingly. Blindly 2025-04-29 10:25:07 +03:00
Jedd Morgan ac0db18d24 refactor(ci): Update workflow to use new consolidated deployment workflow (#148)
build_powerbi / build-connector (push) Has been cancelled
build_powerbi / build-visual (push) Has been cancelled
build_powerbi / deploy-installers (push) Has been cancelled
* Update workflow

* target main
2025-04-24 15:09:32 +02:00
Jonathon Broughton 85f8f72335 Update README.md (#147)
* Update README.md

* Update README.md
2025-04-08 21:32:04 +03:00
Dogukan Karatas 24d26dc49f adds support for federated models 2025-03-27 14:02:20 +01:00
112 changed files with 6299 additions and 1298 deletions
-3
View File
@@ -1,3 +0,0 @@
for /f "tokens=1 delims=-" %%i in ("%CIRCLE_TAG%") do set "TAG=%%i.%WORKFLOW_NUM%"
for /f "tokens=1 delims=/" %%j in ("%CIRCLE_TAG%") do set "SEMVER=%%j"
tools\InnoSetup\ISCC.exe tools\powerbi.iss /Sbyparam=$p /DINFO_VERSION=%TAG% /DVERSION=%SEMVER% %*
+11 -166
View File
@@ -1,171 +1,16 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1
orbs:
win: circleci/windows@5.0
commands:
setup_digicert:
description: Set up Digicert Keylocker certificate for code-signing
steps:
- run:
name: "Digicert Signing Manager Setup"
command: |
cd C:\
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
- run:
name: Setup Digicert ONE Client Cert
command: |
cd C:\
echo $env:SM_CLIENT_CERT_FILE_B64 > certificate.txt
certutil -decode certificate.txt certificate.p12
- run:
name: Sync Certs
command: |
& $env:SSM\smksp_cert_sync.exe
# Define the jobs we want to run for this project
jobs:
build-visual:
docker:
- image: cimg/node:18.20.3
steps:
- checkout
- run: node --version
- run:
name: "npm install"
command: "npm i"
working_directory: src/powerbi-visual
- run:
name: Set version
command: |
npm version ${CIRCLE_TAG:-2.0.0} --allow-same-version
working_directory: src/powerbi-visual
- run:
name: "npm run build"
command: "npm run build"
working_directory: src/powerbi-visual
- store_artifacts:
path: dist/*.pbiviz
- persist_to_workspace:
root: ./
paths:
- src/powerbi-visual/dist/*.pbiviz
build-connector:
executor:
name: win/default
shell: powershell.exe
environment:
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
steps:
- checkout
- run:
name: "Set connector internal version"
command: |
$env:VERSION = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "2.0.0" } else { $env:CIRCLE_TAG }
(Get-Content ./Speckle.pq).replace('[Version = "2.0.0"]', '[Version = "'+$($env:VERSION)+'"]') | Set-Content ./Speckle.pq
working_directory: src/powerbi-data-connector
- run:
name: "Build Data Connector"
command: "msbuild Speckle.proj /restore /consoleloggerparameters:NoSummary /property:GenerateFullPaths=true"
working_directory: src/powerbi-data-connector
- run:
name: Create PQX file
command: .\tools\MakePQX\MakePQX.exe pack -mz src/powerbi-data-connector/bin/Speckle.mez -t src/powerbi-data-connector/bin/Speckle.pqx
- persist_to_workspace:
root: ./
paths:
- src/powerbi-data-connector/bin/Speckle.pqx
build-installer:
executor:
name: win/default
shell: powershell.exe
environment:
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
steps:
- checkout
- attach_workspace:
at: ./
- unless: # Build installers unsigned on non-tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: Build Installer
shell: cmd.exe #does not work in powershell
environment:
WORKFLOW_NUM: << pipeline.number >>
CIRCLE_TAG: 2.0.0
command: .circleci\build-installer.bat
- when: # Setup certificates and build installers signed for tagged builds
condition: << pipeline.git.tag >>
steps:
- setup_digicert
- run:
name: Build Installer
shell: cmd.exe #does not work in powershell
environment:
WORKFLOW_NUM: << pipeline.number >>
command: .circleci\build-installer.bat /DSIGN_INSTALLER /DCODE_SIGNING_CERT_FINGERPRINT=%SM_CODE_SIGNING_CERT_SHA1_HASH%
- store_artifacts:
path: ./installer
- persist_to_workspace:
root: ./
paths:
- installer/*.exe
deploy-connector-to-feed:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
steps:
- 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 "2.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/\/[a-zA-Z-]*//')
VER=$(echo "$SEMVER" | sed -e 's/-.*//')
VERSION=$(echo $VER.$WORKFLOW_NUM)
/root/.dotnet/tools/Speckle.Manager.Feed deploy -s powerbi -v ${SEMVER} -u https://releases.speckle.dev/installers/powerbi/powerbi-${SEMVER}.exe -o Win -a Any -f ./installer/powerbi-${SEMVER}.exe
environment:
WORKFLOW_NUM: << pipeline.number >>
workflows:
build:
docker:
- image: cimg/base:2023.03
steps:
- run: echo "so long and thanks for all the fish"
# Orchestrate our job run sequence
workflows:
build_and_test:
when: false
jobs:
- build-connector:
context: digicert-keylocker
- build-visual
- build-installer:
context: digicert-keylocker
requires:
- build-connector
- build-visual
deploy:
jobs:
- build-connector:
filters: &deploy_filter
branches:
ignore: /.*/
tags:
only: /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w{1,10})?$/
context: digicert-keylocker
- build-visual:
filters: *deploy_filter
- build-installer:
filters: *deploy_filter
context: digicert-keylocker
requires:
- build-connector
- build-visual
- deploy-connector-to-feed:
filters: *deploy_filter
requires:
- build-installer
context: do-spaces-speckle-releases
- build
@@ -1,30 +1,46 @@
name: build_powerbi
name: Build and deploy Connector and Visual
on:
push:
branches: ["dev"]
tags: ["v3.*"] # Manual delivery on every 3.x tag
branches: ["installer-test/**"]
tags: ["v3.*.*"] # Manual delivery on every 3.x tag
jobs:
build-connector:
runs-on: windows-latest
outputs:
semver: ${{ steps.set-version.outputs.semver }}
file-version: ${{ steps.set-info-version.outputs.file-version }}
file-version: ${{ steps.set-version.outputs.file-version }}
env:
CertFile: "./speckle.pfx"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v3.0.0
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
versionSpec: 6.0.5 # github actions doesnt like 6.1.0 onwards https://github.com/GitTools/actions/blob/main/docs/versions.md
python-version: "3.10"
- name: Determine Version
id: gitversion
uses: gittools/actions/gitversion/execute@v3.0.0
- 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.file-version}}
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
@@ -51,37 +67,35 @@ jobs:
if-no-files-found: error
retention-days: 1
- id: set-version
name: Set version to output
run: echo "semver=${{steps.gitversion.outputs.semVer}}" >> "$GITHUB_OUTPUT"
shell: bash
- id: set-info-version
name: Set version to output
run: echo "file-version=${{steps.gitversion.outputs.AssemblySemVer}}" >> "$GITHUB_OUTPUT"
shell: bash
build-visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v3.0.0
with:
versionSpec: 6.0.5 # github actions doesnt like 6.1.0 onwards https://github.com/GitTools/actions/blob/main/docs/versions.md
- 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 }}"
- name: Determine Version
id: gitversion
uses: gittools/actions/gitversion/execute@v3.0.0
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "file-version=$FILE_VERSION" >> "$GITHUB_OUTPUT"
echo $SEMVER
echo $FILE_VERSION
- run: npm ci
working-directory: src/powerbi-visual
- run: npm version ${{steps.gitversion.outputs.semVer}} --allow-same-version
- run: npm version ${{steps.set-version.outputs.semver}} --allow-same-version
working-directory: src/powerbi-visual
- run: npm run build
working-directory: src/powerbi-visual
@@ -120,20 +134,26 @@ jobs:
path: powerbi.zip
if-no-files-found: error
retention-days: 1
- name: 🔫 Trigger Build Installers
uses: ALEEF02/workflow-dispatch@v3.0.0
- name: 🔫 Trigger Build Installer(s)
uses: the-actions-org/workflow-dispatch@v4.0.0
with:
workflow: Build PowerBI
workflow: Build Installers
repo: specklesystems/connector-installers
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
inputs: '{ "run_id": "${{ github.run_id }}", "semver": "${{ needs.build-connector.outputs.semver }}", "file_version": "${{ needs.build-connector.outputs.file-version }}", "public_release": ${{ env.IS_TAG_BUILD }} }'
inputs: '{
"run_id": "${{ github.run_id }}",
"semver": "${{ needs.build-connector.outputs.semver }}",
"file_version": "${{ needs.build-connector.outputs.file-version }}",
"repo": "${{ github.repository }}",
"is_public_release": ${{ env.IS_TAG_BUILD }}
}'
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-*
+30
View File
@@ -0,0 +1,30 @@
name: Test Build Connector and Visual
on: pull_request
jobs:
build-connector:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Build Data Connector
working-directory: src/powerbi-data-connector
run: |
msbuild Speckle.proj /restore /consoleloggerparameters:NoSummary /property:GenerateFullPaths=true
build-visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
working-directory: src/powerbi-visual
- run: npm run build
working-directory: src/powerbi-visual
-11
View File
@@ -1,11 +0,0 @@
workflow: GitFlow/v1
next-version: 3.0.0
mode: ManualDeployment
branches:
main:
label: rc
develop:
regex: ^dev$
label: beta
unknown:
increment: None
+60 -26
View File
@@ -3,18 +3,26 @@
Speckle | Power BI
</h1>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://docs.speckle.systems/"><img src="https://img.shields.io/badge/docs-speckle.systems-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
<h3 align="center">
Speckle Connector and 3D Viewer Visual for Power BI
Speckle Connector and 3D Visual for Power BI
</h3>
# Features
## Features
Speckle Power BI Data Connector lets you easily get data from Speckle into Power BI reports and visualizations. You can access and analyze data from various AEC apps (like Revit, Archicad, Grasshopper, and more) and open-source files (IFC, STL, OBJ, etc.) into Power BI with ease.
<p align="center">
<div align="center">
<a href="https://app.speckle.systems/connectors/">
Download Power BI Connector
</a>
</div>
</p>
Speckles connection to Power BI consists of two parts:
- **Data Connector** fetches the data you uploaded from AEC apps to Speckle.
@@ -22,19 +30,19 @@ Speckles connection to Power BI consists of two parts:
![Desktop - 1 (1)](https://github.com/specklesystems/speckle-powerbi/assets/51519350/6d2c5224-965f-4eae-b869-be26cb48c6b2)
# Repo Structure
## Repository Structure
This repo is home to our Power BI connector. The Speckle Server provides all the web-facing functionality and can be found [here](https://github.com/specklesystems/Server).
This repository is home to our Power BI connector. The Speckle Server provides all the web-facing functionality and can be found [here](https://github.com/specklesystems/Server).
`src/powerbi-data-connector` contains all the code for the Data connector.
`src/powerbi-visual` contains all the code for 3D Visual.
# Installation
## Installation
Speckle connector can be installed directly from [Manager for Speckle](https://speckle.systems/download/). Full instructions for [installation](https://speckle.guide/user/powerbi/installation.html) and [configuration](https://speckle.guide/user/powerbi/configuration.html) can be found on our docs.
Power BI connector installer can be downloaded from the [connectors portal](https://app.speckle.systems/connectors/). Full instructions for [installation](https://docs.speckle.systems/connectors/power-bi#setup) and [configuration](https://docs.speckle.systems/connectors/power-bi#why-dont-i-see-speckle-as-a-data-source-in-power-bi) can be found on our docs.
# Using 3D Visual
### 3D Visual
3D Visual can be imported as any other Power BI custom visual.
@@ -43,34 +51,62 @@ Speckle connector can be installed directly from [Manager for Speckle](https://s
3. Go to `Documents/Power BI Desktop/Custom Visuals` and import `Speckle 3D Visual.pbiviz` file.
4. Speckle cube will appear in the Visualization pane.
For more on how to use the visual, [check our docs](https://speckle.guide/user/powerbi-visual/introduction.html).
For more on how to use the visual, [check our docs](https://docs.speckle.systems/connectors/power-bi).
# Usage
## Quick Start
To get started with Power BI connectors, please take a look at the [documentation](https://speckle.guide/user/powerbi/introduction.html) and extensive [tutorials](https://www.youtube.com/playlist?list=PLlI5Dyt2HaEsZHG2WJ75WIM0Brx6VHT2S) published.
To get started with Power BI connector, please take a look at the [documentation](https://docs.speckle.systems/connectors/power-bi) and extensive [tutorials](https://www.youtube.com/@SpeckleSystems) published.
# **Developing & Debugging**
## Development Setup
We encourage everyone interested to debug/hack/contribute/give feedback to this project.
### For local development of the 3D Visual
## **Setup**
1. **Clone the repository**:
```bash
git clone https://github.com/specklesystems/speckle-powerbi.git
cd speckle-powerbi
```
### **Install PowerQuery SDK**
2. **Navigate to the visual directory**:
```bash
cd src/powerbi-visual
```
Follow the instructions from the [official docs](https://docs.microsoft.com/en-us/power-query/installingsdk)
3. **Install dependencies**:
```bash
npm install
# or
yarn install
```
### **Build with Visual Studio**
4. **Start development server**:
```bash
npm run dev
```
Every time you build the connector, VisualStudio will copy the latest `.mez` connector file to the appropriate location. Just restart PowerBI to see the latest changes.
5. **Build the visual**:
```bash
# Development build
npm run build:dev
# Production build
npm run build
```
### **Debug**
### For local development of the Data Connector
You can start the PowerQuery connector in VisualStudio, this will open a standalone connector you can use for testing purposes.
1. **Install PowerQuery SDK**:
Follow the instructions from the [official docs](https://docs.microsoft.com/en-us/power-query/installingsdk)
We don't know of a way to debug the connector live in PowerBI, but we'd be happy to hear about it.
2. **Open the project in Visual Studio Code**:
- Open `src/powerbi-data-connector/Speckle.proj`
- Build the project to generate the `.mez` file
3. **Testing the connector**:
- Visual Studio will automatically copy the `.mez` file to the appropriate location
- Restart Power BI Desktop to see the latest changes
# About Speckle
## About Speckle
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
@@ -91,12 +127,10 @@ What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube
Give Speckle a try in no time by:
- [![app.speckle.systems](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
- [![app.speckle.systems](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
![Untitled](https://user-images.githubusercontent.com/2679513/132021739-15140299-624d-4410-98dc-b6ae6d9027ab.png)
- [![docs](https://img.shields.io/badge/docs-speckle.systems-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://docs.speckle.systems) reference on almost any end-user and developer functionality
+42
View File
@@ -0,0 +1,42 @@
import re
import sys
import os
def sanitize_version(tag):
"""Extracts the first three numeric segments from a tag string, because PowerBI is..."""
parts = re.findall(r"\d+", tag)
return ".".join(parts[:3]) if len(parts) >= 3 else tag
def patch_connector(tag):
"""Patches the connector version within the data connector file"""
sanitized_tag = sanitize_version(tag)
pq_file = os.path.join(os.path.dirname(__file__), "src", "powerbi-data-connector", "Speckle.pq")
with open(pq_file, "r") as file:
lines = file.readlines()
for (index, line) in enumerate(lines):
if '[Version = "3.0.0"]' in line:
lines[index] = f'[Version = "{sanitized_tag}"]\n'
print(f"Patched connector version number in {pq_file}")
break
with open(pq_file, "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
raise ValueError(f"Invalid tag provided: {tag}")
print(f"Patching version: {tag}")
patch_connector(tag)
if __name__ == "__main__":
main()
+188 -29
View File
@@ -4,6 +4,79 @@ section Speckle;
AuthAppId = "spklpwerbi";
AuthAppSecret = "spklpwerbi";
// PKCE helper functions for enhanced OAuth2 security
Base64UrlEncode = (binaryData as binary) =>
let
// Convert binary to base64
base64 = Binary.ToText(binaryData, BinaryEncoding.Base64),
// Convert to base64url by replacing characters and removing padding
base64url = Text.Replace(Text.Replace(Text.Replace(base64, "+", "-"), "/", "_"), "=", "")
in
base64url;
GeneratePKCEVerifier = () =>
let
// Generate cryptographically secure random string using allowed characters
// RFC 7636: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",
// Generate multiple GUIDs to create entropy
guid1 = Text.Replace(Text.Replace(Text.NewGuid(), "-", ""), "{", ""),
guid2 = Text.Replace(Text.Replace(Text.NewGuid(), "-", ""), "}", ""),
guid3 = Text.Replace(Text.Replace(Text.NewGuid(), "-", ""), "{", ""),
guid4 = Text.Replace(Text.Replace(Text.NewGuid(), "-", ""), "}", ""),
// Combine and convert to allowed characters
combined = guid1 & guid2 & guid3 & guid4,
// Map hex characters to allowed PKCE characters
mapped = Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(combined, "0", "A"),
"1", "B"),
"2", "C"),
"3", "D"),
"4", "E"),
"5", "F"),
// Continue mapping remaining hex chars to allowed chars
verifier = Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(
Text.Replace(mapped, "6", "G"),
"7", "H"),
"8", "I"),
"9", "J"),
"a", "K"),
"b", "L"),
"c", "M"),
"d", "N"),
"e", "O"),
"f", "P"),
// Ensure length is between 43-128 characters as per RFC 7636
finalVerifier = Text.Start(verifier, 43)
in
finalVerifier;
GeneratePKCEChallenge = (verifier as text) =>
let
// Create SHA256 hash of the verifier as required by RFC 7636
hash = Crypto.CreateHash(CryptoAlgorithm.SHA256, Text.ToBinary(verifier, TextEncoding.Ascii)),
// Convert to base64url encoding
challenge = Base64UrlEncode(hash)
in
challenge;
// function to load `pqm` files - this is essential and must be kept
shared Speckle.LoadFunction = (fileName as text) =>
let
@@ -34,6 +107,12 @@ shared Speckle.Api.Fetch = Value.ReplaceType(
type function (url as Uri.Type, optional query as text, optional variables as record) as record
);
[DataSource.Kind = "Speckle"]
shared Speckle.CheckPermissions = Value.ReplaceType(
Speckle.LoadFunction("CheckPermissions.pqm"),
type function (url as Uri.Type) as record
);
[DataSource.Kind = "Speckle"]
shared Speckle.GetUser = Value.ReplaceType(
Speckle.LoadFunction("GetUser.pqm"),
@@ -52,12 +131,64 @@ shared Speckle.GetStructuredData = Value.ReplaceType(
type function (url as Uri.Type) as table
);
shared Speckle.GetVersion = Value.ReplaceType(
Speckle.LoadFunction("GetVersion.pqm"),
type function () as text
);
[DataSource.Kind = "Speckle"]
shared Speckle.SendToServer = Value.ReplaceType(
Speckle.LoadFunction("SendToServer.pqm"),
type function (url as Uri.Type) as table
);
[DataSource.Kind = "Speckle"]
shared Speckle.GetWorkspace = Value.ReplaceType(
Speckle.LoadFunction("GetWorkspace.pqm"),
type function (url as Uri.Type) as record
);
shared Speckle.Objects.Properties = Value.ReplaceType(
Speckle.LoadFunction("Objects.Properties.pqm"),
type function (inputRecord as any, optional filterKeys as list, optional parentPath as text, optional existingFields as list) as record
);
shared Speckle.Utils.ExpandRecord = Value.ReplaceType(
Speckle.LoadFunction("Utils.ExpandRecord.pqm"),
type function (
table as table,
columnName as text,
optional FieldNames as list,
optional UseCombinedNames as logical
) as table
);
shared Speckle.Objects.Collections = Value.ReplaceType(
Speckle.LoadFunction("Objects.Collections.pqm"),
type function (inputData as table) as table
);
shared Speckle.Objects.CompositeStructure = Value.ReplaceType(
Speckle.LoadFunction("Objects.CompositeStructure.pqm"),
type function (objectRecord as record, optional outputAsList as nullable logical) as any
);
shared Speckle.Objects.MaterialQuantities = Value.ReplaceType(
Speckle.LoadFunction("Objects.MaterialQuantities.pqm"),
type function (objectRecord as record, optional outputAsList as logical) as any
);
shared Speckle.Models.Federate = Value.ReplaceType(
Speckle.LoadFunction("Models.Federate.pqm"),
type function (tables as list, optional excludeData as logical) as table
);
shared Speckle.Models.MaterialQuantities = Value.ReplaceType(
Speckle.LoadFunction("Models.MaterialQuantities.pqm"),
type function (inputTable as table, optional addPrefix as logical) as table
);
[DataSource.Kind = "Speckle", Publish="GetByUrl.Publish"]
shared Speckle.GetByUrl = Value.ReplaceType(
Speckle.LoadFunction("GetByUrl.pqm"),
@@ -116,7 +247,7 @@ GetByUrl.Icons = [
Speckle = [
// This is used when running the connector on an on-premises data gateway
TestConnection = (path) => {"Speckle.GetUser", path},
// Authentication strategy
// Authentication strategy - OAuth only
Authentication = [
OAuth = [
Label = "Speckle Account",
@@ -124,14 +255,21 @@ Speckle = [
let
server = Text.Combine(
{Uri.Parts(dataSourcePath)[Scheme], "://", Uri.Parts(dataSourcePath)[Host]}
)
),
// Generate PKCE parameters for enhanced security
codeVerifier = GeneratePKCEVerifier(),
codeChallenge = GeneratePKCEChallenge(codeVerifier),
// Build authorization URL with PKCE parameters
authUrl = Text.Combine({server, "authn", "verify", AuthAppId, state}, "/") &
"?code_challenge=" & codeChallenge &
"&code_challenge_method=S256"
in
[
LoginUri = Text.Combine({server, "authn", "verify", AuthAppId, state}, "/"),
LoginUri = authUrl,
CallbackUri = "https://oauth.powerbi.com/views/oauthredirect.html",
WindowHeight = 800,
WindowWidth = 600,
Context = null
Context = [code_verifier = codeVerifier]
],
FinishLogin = (clientApplication, dataSourcePath, context, callbackUri, state) =>
let
@@ -139,20 +277,22 @@ Speckle = [
{Uri.Parts(dataSourcePath)[Scheme], "://", Uri.Parts(dataSourcePath)[Host]}
),
Parts = Uri.Parts(callbackUri)[Query],
// Extract code verifier from context for PKCE
codeVerifier = if context <> null then context[code_verifier] else null,
// Build token request with PKCE parameters
tokenRequest = [
accessCode = Parts[access_code],
appId = AuthAppId,
appSecret = AuthAppSecret,
challenge = state
] & (if codeVerifier <> null then [code_verifier = codeVerifier] else []),
Source = Web.Contents(
Text.Combine({server, "auth", "token"}, "/"),
[
Headers = [
#"Content-Type" = "application/json"
],
Content = Json.FromValue(
[
accessCode = Parts[access_code],
appId = AuthAppId,
appSecret = AuthAppSecret,
challenge = state
]
)
Content = Json.FromValue(tokenRequest)
]
),
json = Json.Document(Source)
@@ -168,7 +308,8 @@ Speckle = [
server = Text.Combine(
{Uri.Parts(dataSourcePath)[Scheme], "://", Uri.Parts(dataSourcePath)[Host]}
),
Source = Web.Contents(
// Enhanced refresh with error handling for gateway compatibility
Source = try Web.Contents(
Text.Combine({server, "auth", "token"}, "/"),
[
Headers = [
@@ -180,24 +321,42 @@ Speckle = [
appId = AuthAppId,
appSecret = AuthAppSecret
]
)
),
ManualStatusHandling = {400, 401, 403, 500, 502, 503, 504}
]
) otherwise null,
// Check if request was successful
IsSuccess = Source <> null,
// If successful, parse the response
json = if IsSuccess then
try Json.Document(Source) otherwise null
else
null,
// Validate the response contains expected fields
IsValidResponse = json <> null and Record.HasFields(json, {"token"}),
// Return result with enhanced error handling
result = if IsValidResponse then
[
access_token = json[token],
scope = null,
token_type = "bearer",
refresh_token = json[refreshToken]
]
else
error [
Reason = "TokenRefreshFailed",
Message = "Failed to refresh OAuth token - please re-authenticate",
Detail = [
Server = server,
RefreshToken = if refreshToken = null then "null" else "present"
]
]
),
json = Json.Document(Source)
in
[
access_token = json[token],
scope = null,
token_type = "bearer",
refresh_token = json[refreshToken]
]
],
Key = [
KeyLabel = "Personal Access Token",
Label = "Private Project"
],
Implicit = [
Label = "Public Project"
result
]
],
Label = "Speckle"
+1 -1
View File
@@ -3,7 +3,7 @@
// NOTE! for tests, be make sure you put here a model that in private project to make sure all good.
let
result = Speckle.GetByUrl(
"https://latest.speckle.systems/projects/126cd4b7bb/models/85c44d39c6"
"https://app.speckle.systems/projects/b61ab234b0/models/a8166255b5"
)
in
result
+111 -10
View File
@@ -4,6 +4,8 @@
GetStructuredData = Extension.LoadFunction("GetStructuredData.pqm"),
SendToServer = Extension.LoadFunction("SendToServer.pqm"),
GetModel = Extension.LoadFunction("GetModel.pqm"),
Parser = Extension.LoadFunction("Parser.pqm"),
CheckPermissions = Extension.LoadFunction("CheckPermissions.pqm"),
// the logic for importing functions from other files
Extension.LoadFunction = (fileName as text) =>
@@ -22,15 +24,114 @@
Detail = [File = fileName, Error = e]
],
// get model name
modelInfo = GetModel(url),
modelName = modelInfo[modelName],
// parse the URL to determine if it's a federated model
parsedUrl = Parser(url),
// get structured data
StructuredData = GetStructuredData(url),
// rename column based on send status
NewColumnName = "Version Object ID",
Result = Table.RenameColumns(StructuredData, {{"Version Object ID", NewColumnName}})
// check if user has permission to load the model
permissionCheck = CheckPermissions(url),
// assert that permission check returned a valid result
permissionAssert = if not Record.HasFields(permissionCheck, {"authorized", "code", "message"}) then
error "Invalid permission check result"
else
null,
// if not authorized, throw an error with the message from the server
authCheck = if not permissionCheck[authorized] then
error Text.Format(
"Permission denied: #{0} (Error code: #{1})",
{permissionCheck[message], permissionCheck[code]}
)
else
null,
// only proceed if user has permisson to load
results = if permissionCheck[authorized] then
if parsedUrl[isFederated] = true then
// process each model in the federation
let
modelsData = List.Transform(
parsedUrl[federatedModels],
each ProcessSingleModel(
parsedUrl[baseUrl],
parsedUrl[projectId],
[modelId],
[versionId]
)
),
// extract all data tables
allTables = List.Transform(modelsData, each [Data]),
// extract all root object IDs
allRootIds = List.Transform(modelsData, each [RootObjectId]),
// combine all root object IDs into a comma-separated string
combinedRootIds = Text.Combine(allRootIds, ","),
// combine all data tables
combinedData = Table.Combine(allTables),
// replace the "Version Object ID" column with the combined root IDs
finalData = Table.TransformColumns(
combinedData,
{"Version Object ID", each combinedRootIds}
)
in
finalData
else
// use existing functionality for single models
let
// get model name
modelInfo = GetModel(url),
modelName = modelInfo[modelName],
// get structured data
structuredData = GetStructuredData(url),
// rename column based on send status
newColumnName = "Version Object ID",
result = Table.RenameColumns(structuredData, {{"Version Object ID", newColumnName}})
in
result
else
error Text.Format(
"Permission denied: #{0} (Error code: #{1})",
{permissionCheck[message], permissionCheck[code]}
),
// function to process a single model and get its data
ProcessSingleModel = (baseUrl, projectId, modelId, versionId) =>
let
// construct a standard URL for the model
singleModelUrl = Text.Combine({
baseUrl,
"/projects/",
projectId,
"/models/",
modelId,
if versionId <> null then Text.Combine({"@", versionId}) else ""
}),
// get model info
modelInfo = GetModel(singleModelUrl),
rootObjectId = modelInfo[rootObjectId],
modelName = modelInfo[modelName],
// get structured data
structuredData = GetStructuredData(singleModelUrl),
// add the model name as context
result = Table.AddColumn(
structuredData,
"Source Model",
each modelName,
type text
)
in
[
Data = result,
RootObjectId = rootObjectId
]
in
Result
results
@@ -1,6 +1,8 @@
(server as text, optional query as text, optional variables as record) as record =>
let
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
// Enhanced credential retrieval with OAuth2 support
apiKey = try Extension.CurrentCredential()[access_token] otherwise null,
defaultQuery = "query {
activeUser {
email
@@ -12,7 +14,9 @@
version
}
}",
Source = Web.Contents(
// Enhanced API call with comprehensive error handling
Source = try Web.Contents(
Text.Combine({server, "graphql"}, "/"),
[
Headers = [
@@ -20,14 +24,56 @@
#"Content-Type" = "application/json",
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
],
ManualStatusHandling = {400},
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
Content = Json.FromValue([query = Text.From(query ?? defaultQuery), variables = variables])
]
),
#"JSON" = Json.Document(Source)
in
// Check if response contains errors, if so, return first error.
if Record.HasFields(#"JSON", {"errors"}) then
error #"JSON"[errors]{0}[message]
) otherwise null,
// Check if the HTTP request was successful
IsHttpSuccess = Source <> null,
// Get HTTP status code for detailed error handling
StatusCode = if IsHttpSuccess then Value.Metadata(Source)[Response.Status] else null,
// Parse JSON response if HTTP request was successful
#"JSON" = if IsHttpSuccess then
try Json.Document(Source) otherwise null
else
#"JSON"[data]
null,
// Comprehensive error handling
// Comprehensive error handling
result = if not IsHttpSuccess then
error [
Reason = "HttpRequestFailed",
Message = "Failed to connect to Speckle server",
Detail = [Server = server, StatusCode = StatusCode]
]
else if StatusCode = 401 then
error [
Reason = "AuthenticationFailed",
Message = "Invalid or expired authentication token",
Detail = [Server = server, HasToken = apiKey <> null]
]
else if StatusCode = 403 then
error [
Reason = "AuthorizationFailed",
Message = "Insufficient permissions for this operation",
Detail = [Server = server]
]
else if #"JSON" = null then
error [
Reason = "InvalidJsonResponse",
Message = "Server returned invalid JSON response",
Detail = [Server = server, StatusCode = StatusCode]
]
else if Record.HasFields(#"JSON", {"errors"}) then
error [
Reason = "GraphQLError",
Message = #"JSON"[errors]{0}[message],
Detail = [Server = server, Errors = #"JSON"[errors]]
]
else
#"JSON"[data]
in
result
@@ -0,0 +1,66 @@
(url as text) as record =>
let
ApiFetch = Extension.LoadFunction("Api.Fetch.pqm"),
Parser = Extension.LoadFunction("Parser.pqm"),
Extension.LoadFunction = (fileName as text) =>
let
binary = Extension.Contents(fileName),
asText = Text.FromBinary(binary)
in
try
Expression.Evaluate(asText, #shared)
catch (e) =>
error
[
Reason = "Extension.LoadFunction Failure",
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
Message.Parameters = {fileName, e[Reason], e[Message]},
Detail = [File = fileName, Error = e]
],
// parse the URL to extract project id
parsedUrl = Parser(url),
server = parsedUrl[baseUrl],
projectId = parsedUrl[projectId],
// GraphQL query to check permissions
query = "query Project($projectId: String!) {
data:project(id: $projectId) {
data:permissions {
canLoad {
authorized
code
message
}
}
}
}",
// variables variable for api fetch (i know)
variables = [
projectId = projectId
],
result = ApiFetch(server, query, variables),
// check that the result contains the expected structure
// this will throw an error if the structure is not as expected
structureCheck = if not (Record.HasFields(result, {"data"}) and
Record.HasFields(result[data], {"data"}) and
Record.HasFields(result[data][data], {"canLoad"}) and
Record.HasFields(result[data][data][canLoad], {"authorized", "code", "message"})) then
error "Invalid response structure from permission check"
else
null,
canLoad = result[data][data][canLoad],
// return the permission result
permissionResult = [
authorized = canLoad[authorized],
code = canLoad[code],
message = canLoad[message]
]
in
permissionResult
@@ -29,7 +29,7 @@
versionId = parsedUrl[versionId],
// get API key if available
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
apiKey = try Extension.CurrentCredential()[access_token] otherwise null,
// graphql query to get model info including root object id
// includes specific version if provided
@@ -39,8 +39,9 @@
),
// fields to remove from data record
FieldsToRemove = {"__closure", "totalChildrenCount", "renderMaterialProxies"},
// create the final table with cleaned data records
FinalTable = Table.FromRecords(
// create basic table with cleaned data records (no properties column yet)
BasicTable = Table.FromRecords(
List.Transform(
TableFromList[Column1],
each let
@@ -48,27 +49,68 @@
fieldsToRemoveForThisRecord = List.Select(
FieldsToRemove,
each Record.HasFields(record, {_})
)
),
cleanedRecord = Record.RemoveFields(record, fieldsToRemoveForThisRecord)
in
[
#"Object IDs" = record[id], // Object IDs
#"Speckle Type" = record[speckle_type], // Speckle Type
#"Version Object ID" = rootId,
data = Record.RemoveFields(record, fieldsToRemoveForThisRecord) // Data
data = cleanedRecord // Data
]
)
),
// function to check if a row should be excluded based on speckle type
ShouldExcludeRow = (row as record) as logical =>
let
speckleType = Record.FieldOrDefault(row[data], "speckle_type", "")
in
speckleType = "Speckle.Core.Models.DataChunk" or
Text.Contains(speckleType, "Objects.Other.RawEncoding"),
// Filtering logic here
// If, model data contains any DataObject -> fetch only data objects
// If there are no data objects in the data -> fetch everything but DataChunks
// If model data contains any DataObject -> fetch only data objects (excluding unwanted types)
// If there are no data objects in the data -> fetch everything but exclude DataChunks and RawEncoding
// Check if model contains any DataObject
HasDataObjects = Table.RowCount(
Table.SelectRows(FinalTable, each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject"))
Table.SelectRows(
BasicTable,
each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject")
and not ShouldExcludeRow(_)
)
) > 0,
FilteredTable = if HasDataObjects then
Table.SelectRows(FinalTable, each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject"))
// load the Objects.Properties function only if we have DataObjects
ObjectsProperties = if HasDataObjects then Extension.LoadFunction("Objects.Properties.pqm") else null,
// Add properties column only if model has DataObjects
FinalTable = if HasDataObjects then
Table.AddColumn(
BasicTable,
"properties",
each let
dataRecord = [data],
isDataObject = Text.Contains(Record.FieldOrDefault(dataRecord, "speckle_type", ""), "DataObject"),
hasProperties = Record.HasFields(dataRecord, {"properties"}),
extractedProperties = if hasProperties and isDataObject then
try ObjectsProperties(dataRecord) otherwise []
else
[]
in
if Record.FieldCount(extractedProperties) > 0 then extractedProperties else null
)
else
Table.SelectRows(FinalTable, each Record.FieldOrDefault([data], "speckle_type", "") <> "Speckle.Core.Models.DataChunk")
BasicTable,
// Apply the same filtering logic as before
FilteredTable = if HasDataObjects then
Table.SelectRows(
FinalTable,
each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject")
and not ShouldExcludeRow(_)
)
else
Table.SelectRows(FinalTable, each not ShouldExcludeRow(_))
in
FilteredTable
@@ -26,7 +26,7 @@ in
parsedUrl = Parser(url),
server = parsedUrl[baseUrl],
apiKey = try Extension.CurrentCredential()[Key] otherwise "",
apiKey = try Extension.CurrentCredential()[access_token] otherwise "",
query = "query {
activeUser {
@@ -62,5 +62,5 @@ in
ServerName = JsonResponse[data][serverInfo][name],
ServerCompany = JsonResponse[data][serverInfo][company],
ServerVersion = JsonResponse[data][serverInfo][version],
Token = if apiKey = "" then null else apiKey[access_token]
Token = if apiKey = "" then null else apiKey
]
@@ -0,0 +1,46 @@
() as text =>
let
// read the Speckle.pq file
specklePqContent = try
Text.FromBinary(Extension.Contents("Speckle.pq"))
otherwise
error "Could not read Speckle.pq file",
lines = Text.Split(specklePqContent, "#(lf)"),
versionLine = List.First(
List.Select(
lines,
each Text.Contains(_, "[Version = ")
),
null
),
version = if versionLine <> null then
let
// find the start and end positions of the version string
startPos = Text.PositionOf(versionLine, """") + 1,
tempText = Text.Middle(versionLine, startPos),
endPos = Text.PositionOf(tempText, """"),
versionText = Text.Middle(tempText, 0, endPos)
in
versionText
else
// fallback version if parsing fails
"3.0.0",
// validate version format
isValidVersion =
let
parts = Text.Split(version, "."),
isValid = List.Count(parts) = 3 and
List.AllTrue(List.Transform(parts, each try Number.From(_) >= 0 otherwise false))
in
isValid,
result = if isValidVersion then
version
else
error "Invalid version format found: " & version
in
result
@@ -0,0 +1,76 @@
// function for getting workspace information
(url as text) as record =>
let
ApiFetch = Extension.LoadFunction("Api.Fetch.pqm"),
Parser = Extension.LoadFunction("Parser.pqm"),
// the logic for importing functions from other files
Extension.LoadFunction = (fileName as text) =>
let
binary = Extension.Contents(fileName),
asText = Text.FromBinary(binary)
in
try
Expression.Evaluate(asText, #shared)
catch (e) =>
error
[
Reason = "Extension.LoadFunction Failure",
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
Message.Parameters = {fileName, e[Reason], e[Message]},
Detail = [File = fileName, Error = e]
],
parsedUrl = Parser(url),
server = parsedUrl[baseUrl],
projectId = parsedUrl[projectId],
// query to get workspace ID from project
projectQuery = "query Project($projectId: String!) {
data:project(id: $projectId) {
workspaceId
}
}",
projectVariables = [
projectId = projectId
],
projectResult = ApiFetch(server, projectQuery, projectVariables),
workspaceId = projectResult[data][workspaceId],
// check if workspaceId is null (personal project)
workspaceInfo = if workspaceId = null then
[
workspaceId = null,
workspaceLogo = null,
workspaceName = null,
canHideBranding = false
]
else
// query workspace only if workspaceId exists
let
workspaceQuery = "query Workspace($workspaceId: String!, $featureName: WorkspaceFeatureName!) {
data:workspace(id: $workspaceId) {
logo
name
hasAccessToFeature(featureName: $featureName)
}
}",
workspaceVariables = [
workspaceId = workspaceId,
featureName = "hideSpeckleBranding"
],
workspaceResult = ApiFetch(server, workspaceQuery, workspaceVariables),
workspace = workspaceResult[data]
in
[
workspaceId = workspaceId,
workspaceLogo = workspace[logo],
workspaceName = workspace[name],
canHideBranding = workspace[hasAccessToFeature]
]
in
workspaceInfo
@@ -0,0 +1,30 @@
// function for federating multiple tables by combining them and creating a concatenated Version Object ID
(tables as list, optional excludeData as logical) as table =>
let
ViewerOnly = if excludeData = null then false else excludeData,
// filter columns from each table if excludeData is true
ProcessedTables = List.Transform(
tables,
each
if ViewerOnly then
Table.SelectColumns(_, {"Version Object ID", "Object IDs"}, MissingField.Ignore)
else
_
),
CombinedTable = Table.Combine(ProcessedTables),
DistinctVersionObjectIDs = List.Distinct(CombinedTable[Version Object ID]),
ConcatenatedVersionObjectIDs = Text.Combine(DistinctVersionObjectIDs, ","),
// Replace all Version Object ID values with the concatenated string
FederatedTable = Table.ReplaceValue(
CombinedTable,
each [Version Object ID],
ConcatenatedVersionObjectIDs,
Replacer.ReplaceText,
{"Version Object ID"}
)
in
FederatedTable
@@ -0,0 +1,22 @@
// function for transforming a table to extract and expand Material Quantities data
(inputTable as table, optional addPrefix as logical) as table =>
let
// Default addPrefix to false if not provided
UsePrefix = if addPrefix = null then false else addPrefix,
// Add mq column using existing MaterialQuantities function with list output
AddedMQ = Table.AddColumn(inputTable, "mq", each Speckle.Objects.MaterialQuantities([data], true)),
// Expand the mq list column
ExpandMQ = Table.ExpandListColumn(AddedMQ, "mq"),
// Add MQProperties column using Properties function with error handling
AddedMQProperties = Table.AddColumn(ExpandMQ, "MQ", each try Speckle.Objects.Properties([mq]) otherwise null),
// Expand the MQProperties record using Utils.ExpandRecord
ExpandMQProperties = Speckle.Utils.ExpandRecord(AddedMQProperties, "MQ", null, UsePrefix),
// Remove the temporary mq and MQProperties columns
FinalTable = Table.RemoveColumns(ExpandMQProperties, {"mq", "MQ"}, MissingField.Ignore)
in
FinalTable
@@ -0,0 +1,163 @@
// function for mapping collection names to referenced elements in Speckle data
(inputData as table) as table =>
let
// Helper function to safely get field value
SafeFieldValue = (record as record, fieldName as text) as any =>
if Record.HasFields(record, {fieldName}) then
Record.Field(record, fieldName)
else
null,
// Helper function to safely get nested field value
SafeNestedValue = (record as record, path as list) as any =>
List.Accumulate(
path,
record,
(current, fieldName) =>
if current <> null and Value.Is(current, type record) and Record.HasFields(current, {fieldName}) then
Record.Field(current, fieldName)
else
null
),
// Step 1: Identify Collection Objects
CollectionObjects = Table.SelectRows(
inputData,
each
let
speckleType = SafeFieldValue(_, "Speckle Type")
in
speckleType <> null and Text.Contains(speckleType, "Collection")
),
// Step 2: Extract Collection Metadata
CollectionMetadata = Table.AddColumn(
CollectionObjects,
"CollectionInfo",
each
let
objectId = SafeFieldValue(_, "Object IDs"),
collectionName = SafeNestedValue(_, {"data", "name"}),
elements = SafeNestedValue(_, {"data", "elements"})
in
[
ObjectId = objectId,
CollectionName = if collectionName <> null then collectionName else "Unnamed Collection",
Elements = if elements <> null and Value.Is(elements, type list) then elements else {}
]
),
// Step 3: Build Collection Hierarchy Mapping
CollectionHierarchy = Table.AddColumn(
CollectionMetadata,
"CollectionReferences",
each
let
info = [CollectionInfo],
collectionName = info[CollectionName],
elements = info[Elements]
in
List.Transform(
elements,
(element) =>
let
referencedId = if Value.Is(element, type record) and Record.HasFields(element, {"referencedId"}) then
element[referencedId]
else
null
in
if referencedId <> null then
[
ReferencedId = referencedId,
CollectionName = collectionName,
ParentCollectionId = info[ObjectId]
]
else
null
)
),
// Step 4: Flatten Reference Mapping
FlattenedReferences = Table.SelectRows(
Table.ExpandListColumn(
Table.SelectColumns(CollectionHierarchy, {"CollectionReferences"}),
"CollectionReferences"
),
each [CollectionReferences] <> null
),
ReferenceTable = Table.ExpandRecordColumn(
FlattenedReferences,
"CollectionReferences",
{"ReferencedId", "CollectionName", "ParentCollectionId"},
{"ReferencedId", "CollectionName", "ParentCollectionId"}
),
// Step 5: Build Hierarchical Collection Paths
BuildCollectionPath = (objectId as text, visited as list) as text =>
let
// Prevent infinite loops
_ = if List.Contains(visited, objectId) then
error "Circular reference detected in collection hierarchy"
else
null,
newVisited = List.InsertRange(visited, 0, {objectId}),
// Find if this object is referenced by any collection
parentReferences = Table.SelectRows(ReferenceTable, each [ReferencedId] = objectId),
result = if Table.RowCount(parentReferences) = 0 then
// No parent collection found
""
else
let
parentRef = parentReferences{0},
parentCollectionId = parentRef[ParentCollectionId],
currentCollectionName = parentRef[CollectionName],
// Recursively get parent path
parentPath = @BuildCollectionPath(parentCollectionId, newVisited),
// Build full path
fullPath = if parentPath = "" then
currentCollectionName
else
parentPath & "::" & currentCollectionName
in
fullPath
in
result,
// Step 6: Add Collection Paths to data field
FinalData = Table.TransformColumns(
inputData,
{
"data", each
let
currentData = _,
currentRow = Table.SelectRows(inputData, each [data] = currentData){0},
objectId = SafeFieldValue(currentRow, "Object IDs"),
collectionPath = if objectId <> null then
try
BuildCollectionPath(objectId, {})
otherwise
""
else
"",
// Add CollectionPath field to the data record, set to null if empty
enhancedData = if Value.Is(currentData, type record) then
Record.AddField(
currentData,
"collectionPath",
if collectionPath = "" then null else collectionPath
)
else
currentData
in
enhancedData
}
)
in
FinalData
@@ -0,0 +1,18 @@
(objectRecord as record, optional outputAsList as nullable logical) as any =>
let
compositeStructure =
if Record.HasFields(objectRecord[properties], "Composite Structure") then
objectRecord[properties][Composite Structure]
else if Record.HasFields(objectRecord[properties], "Parameters") and
Record.HasFields(objectRecord[properties][Parameters], "Type Parameters") and
Record.HasFields(objectRecord[properties][Parameters][Type Parameters], "Structure") then
objectRecord[properties][Parameters][Type Parameters][Structure]
else
null,
result =
if outputAsList = true then
if compositeStructure <> null then Record.ToList(compositeStructure) else null
else
compositeStructure
in
result
@@ -0,0 +1,15 @@
// Helper function to extract [properties][Material Quantities] and optionally output as list
(objectRecord as record, optional outputAsList as logical) as any =>
let
// Ensure outputAsList is logical and defaults to false if not provided
OutputAsList = if outputAsList = null then false else outputAsList,
// Check if 'properties' and 'Material Quantities' exist
HasMaterialQuantities = Record.HasFields(objectRecord, {"properties"}) and Record.HasFields(Record.Field(objectRecord, "properties"), {"Material Quantities"}),
MaterialQuantities = if HasMaterialQuantities then Record.Field(Record.Field(objectRecord, "properties"), "Material Quantities") else null,
Result = if MaterialQuantities = null then null else
if OutputAsList then
Record.ToList(MaterialQuantities)
else
MaterialQuantities
in
Result
@@ -0,0 +1,257 @@
// function for extracting and flattening properties from Speckle objects
(inputRecord as any, optional filterKeys as list, optional parentPath as text, optional existingFields as list) as record =>
let
// Define excluded paths
ExcludedPaths = {
"Composite Structure",
"Material Quantities",
"Parameters.Type Parameters.Structure"
},
// Helper function to check if a path should be excluded
IsPathExcluded = (currentPath as text) as logical =>
List.AnyTrue(List.Transform(ExcludedPaths, each Text.Contains(currentPath, _))),
// Helper function to resolve naming conflicts
ResolveFieldName = (fieldName as text, parentPathParam as nullable text, existingFieldsParam as nullable list) as text =>
let
// Ensure we have valid inputs
parentPath = if parentPathParam = null then "" else parentPathParam,
existingFields = if existingFieldsParam = null then {} else existingFieldsParam,
// Try original field name first
candidateName = fieldName,
// If no conflict, return original name
finalName = if not List.Contains(existingFields, candidateName) then
candidateName
else if parentPath = "" then
fieldName // No parent path available, keep original
else
let
// Split parent path and try adding parents one by one
pathParts = Text.Split(parentPath, "."),
reversedParts = List.Reverse(pathParts), // Start with immediate parent
// Use iteration instead of recursion
ResolveWithIteration = () =>
let
// Generate all possible candidates
candidates = List.Generate(
() => [depth = 1, candidate = fieldName & "." & List.First(reversedParts)],
each [depth] <= List.Count(reversedParts),
each [
depth = [depth] + 1,
candidate = fieldName & "." & Text.Combine(List.FirstN(reversedParts, [depth]), ".")
],
each [candidate]
),
// Find first non-conflicting candidate
firstNonConflicting = List.First(
List.Select(candidates, each not List.Contains(existingFields, _)),
// If all conflict, use full path
fieldName & "." & Text.Combine(reversedParts, ".")
)
in
firstNonConflicting,
resolvedName = ResolveWithIteration()
in
resolvedName
in
finalName,
// Create the main flattening function with self-reference capability
FlattenRecordImpl = (
flattenFn as function,
inputRecord as any,
filterKeys as nullable list,
parentPathParam as nullable text,
existingFieldsParam as nullable list
) as record =>
let
// Ensure non-null values for internal use
currentParentPath = if parentPathParam = null then "" else parentPathParam,
currentExistingFields = if existingFieldsParam = null then {} else existingFieldsParam,
currentfilterKeys = filterKeys,
// Check if record has "properties" field and use it instead of the root record
recordToProcess = if inputRecord = null then
null
else if Value.Is(inputRecord, type record) and Record.HasFields(inputRecord, {"properties"}) then
Record.Field(inputRecord, "properties")
else
inputRecord,
// Helper function to check if a field should be included
ShouldIncludeField = (fieldName as text) as logical =>
if currentfilterKeys = null then true
else List.Contains(currentfilterKeys, fieldName),
// Handle different input types
result = if recordToProcess = null then
[]
else if Value.Is(recordToProcess, type record) then
let
fieldNames = Record.FieldNames(recordToProcess),
// Process each field
processedFields = List.Accumulate(
fieldNames,
[FlattenedRecord = [], ExistingFieldsList = currentExistingFields],
(state, fieldName) =>
let
fieldValue = Record.Field(recordToProcess, fieldName),
newPath = if currentParentPath = "" then fieldName else currentParentPath & "." & fieldName,
// Skip if path is excluded
shouldProcess = not IsPathExcluded(newPath),
processResult = if not shouldProcess then
state
else
let
// Check if this is a name/value record
hasNameValue = Value.Is(fieldValue, type record) and
Record.HasFields(fieldValue, {"name", "value"}),
finalResult = if hasNameValue then
let
nameField = Record.Field(fieldValue, "name"),
valueField = Record.Field(fieldValue, "value"),
// Check if this name field should be included
shouldInclude = if nameField = null then false else ShouldIncludeField(nameField),
result = if shouldInclude and nameField <> null then
let
resolvedName = ResolveFieldName(nameField, currentParentPath, state[ExistingFieldsList]),
newRecord = Record.AddField(state[FlattenedRecord], resolvedName, valueField),
newFieldsList = state[ExistingFieldsList] & {resolvedName}
in
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
else
state
in
result
else if fieldValue = null then
let
shouldInclude = ShouldIncludeField(fieldName),
result = if shouldInclude then
let
resolvedName = ResolveFieldName(fieldName, currentParentPath, state[ExistingFieldsList]),
newRecord = Record.AddField(state[FlattenedRecord], resolvedName, null),
newFieldsList = state[ExistingFieldsList] & {resolvedName}
in
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
else
state
in
result
else if Value.Is(fieldValue, type record) then
let
// Skip empty records
fieldCount = Record.FieldCount(fieldValue),
recursiveResult = if fieldCount = 0 then
state
else
let
// Call the function through the passed reference
// IMPORTANT: Pass the current state's existing fields list
flattened = flattenFn(flattenFn, fieldValue, currentfilterKeys, newPath, state[ExistingFieldsList]),
// Get all field names from the flattened result
flattenedFieldNames = Record.FieldNames(flattened),
// Merge the flattened record with the current state
combinedRecord = flattened & state[FlattenedRecord],
// Update the existing fields list with ALL fields from both records
allFieldNames = List.Distinct(state[ExistingFieldsList] & flattenedFieldNames)
in
[FlattenedRecord = combinedRecord, ExistingFieldsList = allFieldNames]
in
recursiveResult
else if Value.Is(fieldValue, type list) then
let
listLength = List.Count(fieldValue),
// Skip empty lists
listResult = if listLength = 0 then
state
else
List.Accumulate(
List.Positions(fieldValue),
state,
(listState, index) =>
let
listItem = fieldValue{index},
indexSuffix = Text.From(index + 1), // 1-based indexing
listFieldName = fieldName & "." & indexSuffix,
listPath = if currentParentPath = "" then listFieldName else currentParentPath & "." & listFieldName,
itemResult = if Value.Is(listItem, type record) then
let
itemFieldCount = Record.FieldCount(listItem),
itemFlattened = if itemFieldCount = 0 then
listState
else
let
// Call the function through the passed reference
flattened = flattenFn(flattenFn, listItem, currentfilterKeys, listPath, listState[ExistingFieldsList]),
// Get all field names from the flattened result
flattenedFieldNames = Record.FieldNames(flattened),
// Merge the flattened record with the current state
combinedRecord = flattened & listState[FlattenedRecord],
// Update the existing fields list with ALL fields
allFieldNames = List.Distinct(listState[ExistingFieldsList] & flattenedFieldNames)
in
[FlattenedRecord = combinedRecord, ExistingFieldsList = allFieldNames]
in
itemFlattened
else
let
shouldInclude = ShouldIncludeField(listFieldName),
result = if shouldInclude then
let
resolvedName = ResolveFieldName(listFieldName, currentParentPath, listState[ExistingFieldsList]),
newRecord = Record.AddField(listState[FlattenedRecord], resolvedName, listItem),
newFieldsList = listState[ExistingFieldsList] & {resolvedName}
in
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
else
listState
in
result
in
itemResult
)
in
listResult
else
// Handle primitive values
let
shouldInclude = ShouldIncludeField(fieldName),
result = if shouldInclude then
let
resolvedName = ResolveFieldName(fieldName, currentParentPath, state[ExistingFieldsList]),
newRecord = Record.AddField(state[FlattenedRecord], resolvedName, fieldValue),
newFieldsList = state[ExistingFieldsList] & {resolvedName}
in
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
else
state
in
result
in
finalResult
in
processResult
)
in
processedFields[FlattenedRecord]
else
// If input is not a record, return it as is in a record wrapper
[Value = recordToProcess]
in
result,
// Call the implementation with self-reference
result = FlattenRecordImpl(FlattenRecordImpl, inputRecord, filterKeys, parentPath, existingFields)
in
result
@@ -11,29 +11,48 @@
then pathSegments{1} else null,
// extract model ID and version ID if they exist
modelAndVersion = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
then Text.Split(pathSegments{3}, "@") else {},
rawModelSegment = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
then pathSegments{3} else "",
// separate model ID from version ID
modelId = if List.Count(modelAndVersion) > 0
then modelAndVersion{0} else null,
// check if this is a federated model (contains commas)
isFederated = Text.Contains(rawModelSegment, ","),
// if federated, split by comma to get multiple model IDs
modelSegments = if isFederated
then Text.Split(rawModelSegment, ",")
else {rawModelSegment},
// get version ID if it exists
versionId = if List.Count(modelAndVersion) > 1
then modelAndVersion{1} else null,
// process each model segment (could be modelID or modelID@versionID)
processedModels = List.Transform(
modelSegments,
each [
modelId = if Text.Contains(_, "@")
then Text.Split(_, "@"){0}
else _,
versionId = if Text.Contains(_, "@")
then Text.Split(_, "@"){1}
else null
]
),
// extract model IDs and version IDs into separate lists
modelIds = List.Transform(processedModels, each [modelId]),
versionIds = List.Transform(processedModels, each [versionId]),
// validate URL structure
isValid = projectId <> null and modelId <> null
isValid = projectId <> null and List.Count(modelIds) > 0 and List.First(modelIds) <> ""
in
if not isValid then
error [
Reason = "Invalid URL",
Message = "The URL must be in the format 'https://server/projects/PROJECT_ID/models/MODEL_ID' or 'https://server/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID'"
Message = "The URL must be in the format 'https://server/projects/PROJECT_ID/models/MODEL_ID' or 'https://server/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID' or 'https://server/projects/PROJECT_ID/models/MODEL_ID1,MODEL_ID2'"
]
else
[
baseUrl = baseUrl,
projectId = projectId,
modelId = modelId,
versionId = versionId
]
modelId = if isFederated then null else processedModels{0}[modelId],
versionId = if isFederated then null else processedModels{0}[versionId],
isFederated = isFederated,
federatedModels = if isFederated then processedModels else null
]
@@ -1,9 +1,12 @@
(url as text) as list =>
let
try let
// Import required functions
GetModel = Extension.LoadFunction("GetModel.pqm"),
Parser = Extension.LoadFunction("Parser.pqm"),
GetUser = Extension.LoadFunction("GetUser.pqm"),
GetVersion = Extension.LoadFunction("GetVersion.pqm"),
GetWorkspace = Extension.LoadFunction("GetWorkspace.pqm"),
// the logic for importing functions from other files
Extension.LoadFunction = (fileName as text) =>
let
@@ -20,27 +23,58 @@
Message.Parameters = {fileName, e[Reason], e[Message]},
Detail = [File = fileName, Error = e]
],
// Get model info and parsed URL
modelInfo = GetModel(url),
parsedUrl = Parser(url),
userInfo = GetUser(url),
// Get API key if available
apiKey = userInfo[Token],
// Get user email from credentials
userEmail = userInfo[UserEmail],
// get version from Speckle.pq - look GetVersion.pqm
connectorVersion = GetVersion(),
workspaceInfo = GetWorkspace(url),
// exchange powerful token for weak token via ds
tokenExchangeData = Json.FromValue([
PowerfulToken = apiKey,
Scopes = {"profile:read", "streams:read", "users:read"},
ProjectId = parsedUrl[projectId],
ServerUrl = parsedUrl[baseUrl]
]),
// Prepare request data
tokenExchangeResponse = Web.Contents(
"http://127.0.0.1:29364/auth/exchange-token",
[
Headers = [
#"Content-Type" = "application/json",
#"Method" = "POST"
],
Content = tokenExchangeData,
ManualStatusHandling = {400, 401, 403, 404, 500}
]
),
tokenExchangeJson = Json.Document(tokenExchangeResponse),
weakToken = tokenExchangeJson[token],
// prepare request data with weak token
requestData = Json.FromValue([
Url = url,
Server = parsedUrl[baseUrl],
Email = userEmail,
ProjectId = parsedUrl[projectId],
ObjectId = modelInfo[rootObjectId],
RootObjectId = modelInfo[rootObjectId],
SourceApplication = modelInfo[sourceApplication],
Token = apiKey
Token = weakToken,
Version = connectorVersion,
VersionId = parsedUrl[versionId],
WorkspaceId = workspaceInfo[workspaceId],
WorkspaceName = workspaceInfo[workspaceName],
WorkspaceLogo = workspaceInfo[workspaceLogo],
CanHideBranding = workspaceInfo[canHideBranding]
]),
// Send request to local server
@@ -60,4 +94,10 @@
JsonResponse = Json.Document(Response)
in
JsonResponse
JsonResponse
otherwise
error [
Reason = "Desktop Service Not Available",
Message = "Cannot connect to Speckle Desktop Service. Please ensure the Desktop Service is running and try again.",
Detail = "The Speckle Desktop Service must be running to load data from Speckle. Please start the Desktop Service application and refresh your data connection."
]
@@ -0,0 +1,31 @@
// Expands a record column in a table, adding new columns for each field in the record.
// If UseCombinedNames is true, columns are named as ColumnName.FieldName, otherwise just FieldName.
// If FieldNames is provided (list), only those fields are expanded.
(table as table, columnName as text, optional FieldNames as list, optional UseCombinedNames as logical) as table =>
let
useCombined = if UseCombinedNames = null then false else UseCombinedNames,
// Determine which field names to expand
allFieldNames = if FieldNames <> null then FieldNames else List.Distinct(
List.Combine(
List.Transform(
Table.Column(table, columnName),
each if _ is record then Record.FieldNames(_) else {}
)
)
),
// Add each field as a new column
addColumns = List.Accumulate(
allFieldNames,
table,
(state, field) =>
Table.AddColumn(
state,
if useCombined then columnName & "." & field else field,
(row) =>
if Record.HasFields(row, columnName) and Record.Field(row, columnName) is record and Record.HasFields(Record.Field(row, columnName), field)
then Record.Field(Record.Field(row, columnName), field)
else null
)
)
in
addColumns
+41 -36
View File
@@ -2,7 +2,7 @@
"dataRoles": [
{
"displayName": "Version Object ID",
"kind": "Grouping",
"kind": "Measure",
"name": "rootObjectId"
},
{
@@ -11,14 +11,14 @@
"name": "objectIds"
},
{
"displayName": "Color By",
"kind": "Grouping",
"name": "objectColorBy"
},
{
"displayName": "Tooltip Data",
"displayName": "Object Data (Tooltip)",
"kind": "Measure",
"name": "tooltipData"
},
{
"displayName": "Color By",
"kind": "Grouping",
"name": "colorBy"
}
],
"dataViewMappings": [
@@ -33,12 +33,7 @@
"select": [
{
"bind": {
"to": "rootObjectId"
}
},
{
"bind": {
"to": "objectColorBy"
"to": "colorBy"
}
},
{
@@ -51,8 +46,13 @@
"values": {
"select": [
{
"bind": {
"to": "tooltipData"
"for": {
"in": "rootObjectId"
}
},
{
"for": {
"in": "tooltipData"
}
}
]
@@ -60,6 +60,7 @@
},
"conditions": [
{
"colorBy": { "max": 1 },
"objectIds": { "max": 1 },
"rootObjectId": { "max": 1 }
}
@@ -77,10 +78,20 @@
}
}
},
"workspace": {
"properties": {
"brandingHidden": {
"type": { "bool": true }
}
}
},
"viewMode": {
"properties": {
"defaultViewMode": {
"type": { "text": true }
},
"navbarHidden": {
"type": { "bool": true }
}
}
},
@@ -89,29 +100,14 @@
"defaultView": {
"type": { "text": true }
},
"allowCameraUnder": {
"type": {
"bool": true
}
"isOrtho": {
"type": { "bool": true }
},
"zoomOnDataChange": {
"type": {
"bool": true
}
"isGhost": {
"type": { "bool": true }
},
"projection": {
"type": {
"enumeration": [
{
"displayName": "Perspective",
"value": "perspective"
},
{
"displayName": "Orthographic",
"value": "orthographic"
}
]
}
"zoomOnFilter": {
"type": { "bool": true }
}
}
},
@@ -137,6 +133,15 @@
}
}
},
"dataLoading": {
"properties": {
"internalizeData": {
"type": {
"bool": true
}
}
}
},
"color": {
"properties": {
"enabled": {
+791 -47
View File
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -17,10 +17,11 @@
"@babel/runtime-corejs3": "^7.21.5",
"@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.0.12",
"@speckle/objectloader": "^2.23.8",
"@speckle/objectloader": "^2.25.9",
"@speckle/objectloader2": "^2.25.9",
"@speckle/tailwind-theme": "2.23.2",
"@speckle/ui-components": "2.23.2",
"@speckle/viewer": "2.23.8",
"@speckle/viewer": "2.23.23",
"color-interpolate": "^1.0.5",
"core-js": "^3.30.2",
"lodash": "^4.17.21",
@@ -35,7 +36,8 @@
"powerbi-visuals-utils-formattingmodel": "^6.0.4",
"powerbi-visuals-utils-interactivityutils": "^6.0.4",
"powerbi-visuals-utils-tooltiputils": "^6.0.4",
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.13.11",
"vue-tippy": "^6.7.1"
},
"devDependencies": {
"@babel/core": "^7.21.8",
@@ -49,6 +51,7 @@
"@types/webpack": "^5.28.1",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@vueuse/core": "^13.2.0",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"base64-inline-loader": "^2.0.1",
@@ -57,6 +60,7 @@
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^9.13.0",
"extra-watch-webpack-plugin": "^1.0.3",
"html-webpack-plugin": "^5.6.3",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^2.7.5",
"postcss": "^8.4.23",
@@ -82,5 +86,6 @@
"version": "3.0.0",
"engines": {
"node": "^20.17.0"
}
},
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538"
}
+25
View File
@@ -1,4 +1,19 @@
<template>
<div
v-if="visualStore.loadingProgress"
class="absolute top-1/2 left-1/2 w-1/2 -translate-x-1/2 z-50 text-center text-sm"
>
<!-- Progress Bar -->
<LoadingBar :progress="visualStore.loadingProgress"></LoadingBar>
</div>
<div
v-if="visualStore.commonError"
class="absolute top-11 left-1/2 -translate-x-1/2 z-100 bg-white bg-opacity-70 text-black text-center text-sm px-4 py-1 rounded shadow font-medium cursor-default"
>
{{ visualStore.commonError }}
</div>
<ViewerView v-if="visualStore.isViewerReadyToLoad" />
<HomeView v-else />
</template>
@@ -8,6 +23,7 @@ import HomeView from './views/HomeView.vue'
import ViewerView from './views/ViewerView.vue'
import { onMounted } from 'vue'
import { useVisualStore } from './store/visualStore'
import LoadingBar from '@src/components/loading/LoadingBar.vue'
const visualStore = useVisualStore()
@@ -15,3 +31,12 @@ onMounted(() => {
console.log('App mounted')
})
</script>
<style>
.tippy-box[data-theme~='custom'] {
font-size: 10px;
padding: 0px 0px;
border-radius: 4px;
text-align: center;
}
</style>
@@ -1,212 +1,146 @@
<template>
<ButtonGroup>
<ButtonSimple flat secondary @click="onZoomExtentsClicked">
<ArrowsPointingOutIcon class="h-5 w-5" />
</ButtonSimple>
<!-- Canonical Views -->
<Menu as="div" class="relative z-50">
<MenuButton v-slot="{ open }" as="template">
<ButtonToggle flat secondary :active="open">
<VideoCameraIcon class="h-5 w-5" />
</ButtonToggle>
</MenuButton>
<Transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
<div class="space-y-2">
<ViewerControlsButtonGroup>
<!-- Zoom extend -->
<ViewerControlsButtonToggle flat tooltip="Zoom extends" @click="onZoomExtentsClicked">
<ArrowsPointingOutIcon class="h-4 w-4 md:h-5 md:w-5" />
</ViewerControlsButtonToggle>
<!-- Zoom on Filter -->
<ViewerControlsButtonToggle
:tooltip="
visualStore.isZoomOnFilterActive
? 'Move camera on filter'
: 'Keep camera position on filter'
"
flat
@click="toggleZoomOnFilter"
>
<MenuItems
class="absolute w-20 left-2 mb-8 bottom-2 bg-foundation max-h-64 simple-scrollbar overflow-y-auto outline outline-2 outline-primary-muted rounded-lg shadow-lg overflow-hidden flex flex-col"
>
<MenuItem
v-for="view in canonicalViews"
:key="view.name"
v-slot="{ active }"
as="template"
>
<button
:class="{
'bg-primary text-foreground-on-primary': active,
'text-foreground': !active,
'text-sm py-1 transition': true
}"
@click="handleCameraViewChange(view.name.toLocaleLowerCase() as CanonicalView)"
>
{{ view.name }}
</button>
</MenuItem>
<MenuItem v-for="view in views" :key="view.name" v-slot="{ active }" as="template">
<button
:class="{
'bg-primary text-foreground-on-primary': active,
'text-foreground': !active,
'text-sm py-2 transition': true
}"
@click="handleCameraViewChange(view)"
>
{{ view.view.name ?? view.name }}
</button>
</MenuItem>
</MenuItems>
</Transition>
</Menu>
<!-- Speckle Custom Views -->
<Menu v-if="visualStore.speckleViews.length" as="div" class="relative z-40">
<MenuButton v-slot="{ open }" as="template">
<ButtonToggle flat secondary :active="open">
<ViewsIcon class="h-5 w-5" />
</ButtonToggle>
</MenuButton>
<Transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
<ZoomToFit v-if="visualStore.isZoomOnFilterActive" class="h-5 w-5" />
<ZoomToFit v-else class="h-5 w-5 opacity-30" />
</ViewerControlsButtonToggle>
<!-- Ghost / Hidden -->
<ViewerControlsButtonToggle
:tooltip="
visualStore.isGhostActive
? 'Hide ghosted objects on filter'
: 'Show ghosted objects on filter'
"
flat
@click="toggleGhostHidden"
>
<MenuItems
class="absolute w-24 left-2 mb-8 bottom-2 bg-foundation max-h-64 simple-scrollbar overflow-y-auto outline outline-2 outline-primary-muted rounded-lg shadow-lg overflow-hidden flex flex-col"
>
<MenuItem
v-for="view in visualStore.speckleViews"
:key="view.id"
v-slot="{ active }"
as="template"
>
<button
:class="{
'bg-primary text-foreground-on-primary': active,
'text-foreground': !active,
'text-sm py-2 transition': true
}"
@click="handleCameraViewChange(view)"
>
{{ view.name }}
</button>
</MenuItem>
</MenuItems>
</Transition>
</Menu>
<Menu as="div" class="relative z-30">
<MenuButton v-slot="{ open }" as="template">
<ButtonToggle flat secondary :active="open">
<ViewModesIcon class="h-5 w-5" />
</ButtonToggle>
</MenuButton>
<Transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
<Ghost v-if="visualStore.isGhostActive" class="h-5 w-5" />
<Ghost v-else class="h-5 w-5 opacity-30" />
</ViewerControlsButtonToggle>
</ViewerControlsButtonGroup>
<ViewerControlsButtonGroup>
<!-- View Modes -->
<ViewerViewModesMenu
:open="viewModesOpen"
@force-close-others="activeControl = 'none'"
@update:open="(value) => toggleActiveControl(value ? 'viewModes' : 'none')"
@view-mode-clicked="(value) => $emit('view-mode-clicked', value)"
/>
<!-- Views -->
<ViewerViewsMenu
:open="viewsOpen"
:views="views"
@force-close-others="activeControl = 'none'"
@update:open="(value) => toggleActiveControl(value ? 'views' : 'none')"
@view-clicked="(view) => $emit('view-clicked', view)"
/>
<!-- Perspective/Ortho -->
<ViewerControlsButtonToggle
flat
secondary
tooltip="Projection"
:active="visualStore.isOrthoProjection"
@click="toggleProjection"
>
<MenuItems
class="absolute w-20 left-2 mb-8 bottom-2 bg-foundation max-h-64 simple-scrollbar overflow-y-auto outline outline-2 outline-primary-muted rounded-lg shadow-lg overflow-hidden flex flex-col"
>
<MenuItem
v-for="(label, mode) in viewModes"
:key="mode"
v-slot="{ active }"
as="template"
>
<button
:class="{
'bg-primary text-foreground-on-primary': active,
'text-foreground': !active,
'text-sm py-1 transition': true
}"
@click="handleCameraViewModeChange(Number(mode))"
>
{{ label }}
</button>
</MenuItem>
</MenuItems>
</Transition>
</Menu>
<!--
<ButtonToggle
flat
secondary
:active="sectionBox"
@click="$emit('update:sectionBox', !sectionBox)"
>
<CubeIcon class="h-5 w-5" />
</ButtonToggle>
<ButtonSimple flat secondary @click="onClearPalletteClicked">
<PaintBrushIcon class="h-5 w-5" />
</ButtonSimple> -->
</ButtonGroup>
<Perspective v-if="visualStore.isOrthoProjection" class="h-3.5 md:h-4 w-4" />
<PerspectiveMore v-else class="h-3.5 md:h-4 w-4" />
</ViewerControlsButtonToggle>
</ViewerControlsButtonGroup>
</div>
</template>
<script setup lang="ts">
import {
VideoCameraIcon,
CubeIcon,
ArrowsPointingOutIcon,
PaintBrushIcon
} from '@heroicons/vue/24/solid'
import ViewModesIcon from 'src/components/icons/ViewModesIcon.vue'
import ViewsIcon from 'src/components/icons/ViewsIcon.vue'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
import { CanonicalView, SpeckleView, ViewMode } from '@speckle/viewer'
import ButtonToggle from 'src/components/controls/ButtonToggle.vue'
import ButtonGroup from 'src/components/controls/ButtonGroup.vue'
import ButtonSimple from 'src/components/controls/ButtonSimple.vue'
import { inject, watch } from 'vue'
import { resetPalette } from 'src/utils/matrixViewUtils'
import { ArrowsPointingOutIcon } from '@heroicons/vue/24/solid'
import { SpeckleView } from '@speckle/viewer'
import { computed, ref } from 'vue'
import { useVisualStore } from '@src/store/visualStore'
import ViewerControlsButtonGroup from './viewer/controls/ViewerControlsButtonGroup.vue'
import ViewerControlsButtonToggle from './viewer/controls/ViewerControlsButtonToggle.vue'
import ViewerViewModesMenu from './viewer/view-modes/ViewerViewModesMenu.vue'
import ViewerViewsMenu from './viewer/views/ViewerViewsMenu.vue'
import Perspective from '../components/global/icon/Perspective.vue'
import PerspectiveMore from '../components/global/icon/PerspectiveMore.vue'
import Ghost from '../components/global/icon/Ghost.vue'
import ZoomToFit from '../components/global/icon/ZoomToFit.vue'
const visualStore = useVisualStore()
const emits = defineEmits([
'update:sectionBox',
'view-clicked',
'toggle-projection',
'clear-palette',
'view-mode-clicked'
])
const props = withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
sectionBox: false
})
const canonicalViews = [
{ name: 'Top' },
{ name: 'Front' },
{ name: 'Left' },
{ name: 'Back' },
{ name: 'Right' }
]
type ActiveControl =
| 'none'
| 'viewModes'
| 'views'
| 'sun'
| 'projection'
| 'sectionBox'
| 'explode'
| 'settings'
const viewModes = {
[ViewMode.DEFAULT]: 'Default',
[ViewMode.DEFAULT_EDGES]: 'Edges',
[ViewMode.SHADED]: 'Shaded',
[ViewMode.PEN]: 'Pen',
[ViewMode.ARCTIC]: 'Arctic',
[ViewMode.COLORS]: 'Colors'
}
const handleCameraViewChange = (view: CanonicalView | SpeckleView) => {
emits('view-clicked', view)
// visualStore.writeCameraViewToFile(view)
}
const handleCameraViewModeChange = (viewMode: ViewMode) => {
emits('view-mode-clicked', viewMode)
visualStore.writeViewModeToFile(viewMode)
}
const activeControl = ref<ActiveControl>('none')
const onZoomExtentsClicked = (ev: MouseEvent) => {
visualStore.viewerEmit('zoomExtends')
}
const onClearPalletteClicked = (ev: MouseEvent) => {
console.log('Clear pallette clicked')
resetPalette()
emits('clear-palette')
const toggleActiveControl = (control: ActiveControl) => {
activeControl.value = activeControl.value === control ? 'none' : control
}
const toggleProjection = () => {
visualStore.viewerEmit('toggleProjection')
visualStore.setIsOrthoProjection(!visualStore.isOrthoProjection)
visualStore.writeIsOrthoToFile()
}
const toggleGhostHidden = () => {
visualStore.setIsGhost(!visualStore.isGhostActive)
visualStore.viewerEmit('toggleGhostHidden', visualStore.isGhostActive)
visualStore.writeIsGhostToFile()
}
const toggleZoomOnFilter = () => {
visualStore.setIsZoomOnFilterActive(!visualStore.isZoomOnFilterActive)
visualStore.writeZoomOnFilterToFile()
}
const viewModesOpen = computed({
get: () => activeControl.value === 'viewModes',
set: (value) => {
activeControl.value = value ? 'viewModes' : 'none'
}
})
const viewsOpen = computed({
get: () => activeControl.value === 'views',
set: (value) => {
activeControl.value = value ? 'views' : 'none'
}
})
</script>
@@ -1,35 +1,144 @@
<template>
<div class="flex flex-col justify-center items-center">
<div class="border">
<transition name="slide-fade">
<nav
v-show="!visualStore.isNavbarHidden"
class="fixed top-0 h-9 flex items-center bg-foundation border border-outline-2 w-full transition z-20 cursor-default"
>
<div class="flex items-center transition-all justify-between w-full">
<div
v-if="visualStore.receiveInfo.workspaceName"
class="flex items-center gap-2 p-0.5 pr-1.5 hover:bg-highlight-2 rounded ml-2"
>
<WorkspaceAvatar
:name="visualStore.receiveInfo.workspaceName"
:logo="visualStore.receiveInfo.workspaceLogo"
></WorkspaceAvatar>
<div class="min-w-0 truncate flex-grow text-left text-xs">
<span>{{ visualStore.receiveInfo.workspaceName }}</span>
</div>
</div>
<div v-else>
<div class="flex items-center hover:cursor-pointer" @click="goToSpeckleWebsite">
<div class="max-[200px]:hidden block ml-2">
<img class="w-6 h-auto ml-1 mr-2 my-1" src="@assets/logo-big.png" />
</div>
<div class="font-sans font-medium">Speckle</div>
</div>
</div>
<div class="flex items-center space-x-2">
<FormButton
v-if="visualStore.latestAvailableVersion && !visualStore.isConnectorUpToDate"
v-tippy="{
content: 'New connector version is available.<br>Click to download.',
allowHTML: true
}"
color="outline"
size="sm"
@click="visualStore.downloadLatestVersion"
>
Update
</FormButton>
<div class="font-thin text-xs text-gray-400">
v{{ visualStore.receiveInfo.version }}
</div>
<button
class="text-gray-400 hover:text-gray-700 transition"
title="Hide navbar"
@click="visualStore.toggleNavbar()"
>
<ChevronUpIcon class="w-4 h-4" />
</button>
</div>
</div>
</nav>
</transition>
<div
v-if="!isInteractive"
class="absolute left-1/2 -translate-x-1/2 z-20 bg-white bg-opacity-70 text-black text-center text-xs px-4 py-1 rounded shadow font-medium cursor-default transition-all duration-300"
:class="visualStore.isNavbarHidden ? 'top-1' : 'top-11'"
>
<strong>Object IDs</strong>
field is needed for interactivity with other visuals.
</div>
<div v-if="visualStore.isNavbarHidden" class="fixed top-0 right-0 z-20">
<button
class="transition opacity-50 hover:opacity-100"
title="Show navbar"
@click="visualStore.toggleNavbar()"
>
<ChevronDownIcon class="w-4 h-4 text-gray-400" />
</button>
</div>
<transition name="slide-left">
<ViewerControls
v-show="!visualStore.isNavbarHidden"
v-model:section-box="bboxActive"
:views="views"
class="fixed top-11 left-2 z-30"
@view-clicked="(view) => viewerHandler.setView(view)"
@view-mode-clicked="(viewMode) => viewerHandler.setViewMode(viewMode)"
/>
</transition>
<div v-if="visualStore.isFilterActive" class="absolute bottom-5 left-1/2 -translate-x-1/2 z-50">
<FormButton size="sm" @click="visualStore.resetFilters(), selectionHandler.reset()">
Reset filters
</FormButton>
</div>
<div
class="absolute z-10 flex items-center text-xs cursor-pointer"
:class="visualStore.isBrandingHidden ? 'bottom-0 right-0' : 'bottom-2 right-2'"
@click.stop="goToSpeckleWebsite"
>
<!-- TODO: fade bottom here as transition -->
<transition name="fade-bottom">
<div
v-if="!visualStore.isBrandingHidden"
class="flex items-center justify-center font-thin"
>
<div class="">Powered by</div>
<img class="w-4 h-auto mx-1" src="@assets/logo-big.png" />
<div class="font-medium">Speckle</div>
</div>
</transition>
<button
v-if="visualStore.receiveInfo && visualStore.receiveInfo.canHideBranding"
class="transition opacity-50 hover:opacity-100 ml-1"
:title="visualStore.isBrandingHidden ? '' : 'Hide branding'"
@click.stop="visualStore.toggleBranding()"
>
<ChevronUpIcon v-if="visualStore.isBrandingHidden" class="w-4 h-4 text-gray-400" />
<ChevronDownIcon v-else class="w-4 h-4" />
</button>
</div>
<div
ref="container"
class="fixed h-full w-full z-0"
class="fixed h-full w-full z-0 cursor-default"
@click="onCanvasClick"
@auxclick="onCanvasAuxClick"
/>
<!-- <div class="z-30 w-1/2 px-10">
<common-loading-bar :loading="isLoading" />
</div> -->
<viewer-controls
v-model:section-box="bboxActive"
:views="views"
class="fixed bottom-6"
@view-clicked="(view) => viewerHandler.setView(view)"
@view-mode-clicked="(viewMode) => viewerHandler.setViewMode(viewMode)"
/>
</div>
</template>
<script async setup lang="ts">
import { inject, onBeforeUnmount, onMounted, Ref, ref } from 'vue'
import FormButton from '@src/components/form/FormButton.vue'
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref } from 'vue'
import { currentOS, OS } from '../utils/detectOS'
import ViewerControls from 'src/components/ViewerControls.vue'
import ViewModeControls from 'src/components/ViewModeControls.vue'
import { CanonicalView, SpeckleView } from '@speckle/viewer'
import { SpeckleView } from '@speckle/viewer'
import { useClickDragged } from 'src/composables/useClickDragged'
import { ContextOption } from 'src/settings/colorSettings'
import { useVisualStore } from '@src/store/visualStore'
import { ViewerHandler } from '@src/plugins/viewer'
import { selectionHandlerKey, tooltipHandlerKey } from '@src/injectionKeys'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/outline'
import WorkspaceAvatar from './workspace/WorkspaceAvatar.vue'
const visualStore = useVisualStore()
const { dragged } = useClickDragged()
@@ -43,10 +152,20 @@ const container = ref<HTMLElement>()
let bboxActive = ref(false)
let views: Ref<SpeckleView[]> = ref([])
const isInteractive = computed(
() => visualStore.fieldInputState.rootObjectId && visualStore.fieldInputState.objectIds
)
const goToSpeckleWebsite = () => visualStore.host.launchUrl('https://speckle.systems')
onMounted(async () => {
console.log('Viewer Wrapper mounted')
viewerHandler = new ViewerHandler()
await viewerHandler.init(container.value)
// Set up event listener for object clicks from the FilteredSelectionExtension
viewerHandler.emitter.on('objectClicked', handleObjectClicked)
visualStore.setViewerEmitter(viewerHandler.emit)
})
@@ -54,40 +173,100 @@ onBeforeUnmount(async () => {
await viewerHandler.dispose()
})
function isMultiSelect(e: MouseEvent) {
if (!e) return false
if (currentOS === OS.MacOS) return e.metaKey || e.shiftKey
else return e.ctrlKey || e.shiftKey
}
async function onCanvasClick(ev: MouseEvent) {
async function handleObjectClicked(hit: any, isMultiSelect: boolean, mouseEvent?: PointerEvent) {
// Skip if dragging occurred
if (dragged.value) return
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
const multi = isMultiSelect(ev)
const hit = intersectResult?.hit
console.log('🎯 Object clicked in ViewerWrapper:', hit, isMultiSelect)
if (hit) {
visualStore.setPostClickSkipNeeded(true)
const id = hit.object.id as string
if (multi || !selectionHandler.isSelected(id)) {
await selectionHandler.select(id, multi)
if (isMultiSelect || !selectionHandler.isSelected(id)) {
await selectionHandler.select(id, isMultiSelect)
}
tooltipHandler.show(hit, { x: ev.clientX, y: ev.clientY })
// Show tooltip if we have mouse coordinates
if (mouseEvent) {
tooltipHandler.show(hit, { x: mouseEvent.clientX, y: mouseEvent.clientY })
}
const selection = selectionHandler.getCurrentSelection()
const ids = selection.map((s) => s.id)
await viewerHandler.selectObjects(ids)
} else {
visualStore.setPostClickSkipNeeded(false)
tooltipHandler.hide()
if (!multi) {
if (!isMultiSelect) {
selectionHandler.clear()
await viewerHandler.selectObjects(null)
}
}
}
function onCanvasClick(ev: MouseEvent) {
// This click handler allows the viewer's built-in input system to handle clicks
// The viewer will emit ViewerEvent.ObjectClicked events which the SelectionExtension handles
console.log('🖱️ Canvas click detected:', ev.clientX, ev.clientY)
// Let the event propagate to the viewer's input system
// The viewer should handle the click and emit ViewerEvent.ObjectClicked
}
async function onCanvasAuxClick(ev: MouseEvent) {
if (ev.button != 2 || dragged.value) return
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
await selectionHandler.showContextMenu(ev, intersectResult?.hit)
if (ev.button !== 2 || dragged.value) return
// For right-clicks, we need to get the object at the click position
// Since FilteredSelectionExtension doesn't handle right-clicks, we'll ask it for current selection
const selectedObjects = viewerHandler.selection.getSelectedObjects()
const hit = selectedObjects.length > 0 ? {
guid: selectedObjects[0].id,
object: selectedObjects[0],
point: { x: 0, y: 0, z: 0 } // We don't have exact point for context menu
} : null
await selectionHandler.showContextMenu(ev, hit)
}
</script>
<style scoped>
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.3s ease;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
opacity: 0;
transform: translateY(-100%);
}
.slide-fade-enter-to,
.slide-fade-leave-from {
opacity: 1;
transform: translateY(0);
}
.slide-left-enter-active,
.slide-left-leave-active {
transition: all 0.3s ease;
}
.slide-left-enter-from,
.slide-left-leave-to {
opacity: 0;
transform: translateX(-20px);
}
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: all 0.3s ease;
}
.fade-bottom-enter-from,
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10px);
}
.fade-bottom-enter-to,
.fade-bottom-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>
@@ -1,8 +0,0 @@
<template>
<button
class="bg-foundation text-foreground shadow-md rounded-lg h-10 flex justify-center space-x-2 px-1"
>
<slot></slot>
</button>
</template>
<script setup lang="ts"></script>
@@ -1,45 +0,0 @@
<template>
<button
ref="button"
:class="`transition rounded-lg w-10 h-10 flex items-center justify-center ${shadowClasses} ${colorClasses} active:scale-[0.9] outline-none`"
>
<slot></slot>
</button>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
let active = ref(false)
let button = ref<HTMLElement>()
const props = defineProps<{
flat?: boolean
secondary?: boolean
}>()
const shadowClasses = computed(() => (props.flat ? '' : 'shadow-md'))
const colorClasses = computed(() => {
const parts = []
if (active.value) {
if (props.secondary) parts.push('bg-foundation text-primary')
else parts.push('bg-primary text-foreground-on-primary')
} else {
parts.push('bg-foundation text-foreground')
}
return parts.join(' ')
})
const onPointerDown = () => (active.value = true)
const onPointerUp = () => (active.value = false)
onMounted(() => {
button.value.addEventListener('pointerdown', onPointerDown)
button.value.addEventListener('pointerup', onPointerUp)
})
onBeforeUnmount(() => {
button.value.removeEventListener('pointerdown', onPointerDown)
button.value.removeEventListener('pointerup', onPointerUp)
})
</script>
@@ -0,0 +1,294 @@
<template>
<Component
:is="to ? linkComponent : 'button'"
:href="to"
:to="to"
:type="buttonType"
:external="external"
:class="buttonClasses"
:disabled="isDisabled"
role="button"
:style="
color !== 'subtle' && !text
? `box-shadow: -1px 1px 4px 0px #0000000a inset; box-shadow: 0px 2px 2px 0px #0000000d;`
: ''
"
@click="onClick"
>
<Component :is="finalLeftIcon" v-if="finalLeftIcon" :class="iconClasses" />
<slot v-if="!hideText">Button</slot>
<Component :is="iconRight" v-if="iconRight || !loading" :class="iconClasses" />
</Component>
</template>
<script setup lang="ts">
import { isObjectLike } from 'lodash'
import type { PropAnyComponent } from '../../helpers/common/components'
import { computed, resolveDynamicComponent } from 'vue'
import type { Nullable } from '@speckle/shared'
import type { FormButtonStyle, FormButtonSize } from '../../helpers/form/button'
const emit = defineEmits<{
/**
* Emit MouseEvent on click
*/
(e: 'click', val: MouseEvent): void
}>()
const props = defineProps<{
/**
* URL to which to navigate - can be a relative (app) path or an absolute link for an external URL
*/
to?: string
/**
* Choose from one of 3 button sizes
*/
size?: FormButtonSize
/**
* If set, will make the button take up all available space horizontally
*/
fullWidth?: boolean
/**
* Similar to "link", but without an underline and possibly in different colors
*/
text?: boolean
/**
* Will remove paddings and background. Use for links.
*/
link?: boolean
/**
* color:
* primary: the default primary blue.
* outline: foundation background and outline
* subtle: no styling
*/
color?: FormButtonStyle
/**
* Should rounded-full be added?:
*/
rounded?: boolean
/**
* Whether the target location should be forcefully treated as an external URL
* (for relative paths this will likely cause a redirect)
*/
external?: boolean
/**
* Whether to disable the button so that it can't be pressed
*/
disabled?: boolean
/**
* If set, will have type set to "submit" to enable it to submit any parent forms
*/
submit?: boolean
/**
* Add icon to the left from the text
*/
iconLeft?: Nullable<PropAnyComponent>
/**
* Add icon to the right from the text
*/
iconRight?: Nullable<PropAnyComponent>
/**
* Hide default slot (when you want to show icons only)
*/
hideText?: boolean
/**
* Customize component to be used when rendering links.
*
* The component will try to dynamically resolve NuxtLink and RouterLink and use those, if this is set to null.
*/
linkComponent?: Nullable<PropAnyComponent>
/**
* Disables the button and shows a spinning loader
*/
loading?: boolean
}>()
const NuxtLink = resolveDynamicComponent('NuxtLink')
const RouterLink = resolveDynamicComponent('RouterLink')
const linkComponent = computed(() => {
if (props.linkComponent) return props.linkComponent
if (props.external) return 'a'
if (isObjectLike(NuxtLink)) return NuxtLink
if (isObjectLike(RouterLink)) return RouterLink
return 'a'
})
const buttonType = computed(() => {
if (props.to) return undefined
if (props.submit) return 'submit'
return 'button'
})
const isDisabled = computed(() => props.disabled || props.loading)
const finalLeftIcon = computed(() => props.iconLeft)
const bgAndBorderClasses = computed(() => {
const classParts: string[] = []
const colorsBgBorder = {
subtle: [
'bg-transparent border-transparent text-foreground font-medium',
'hover:bg-primary-muted disabled:hover:bg-transparent focus-visible:border-foundation'
],
outline: [
'bg-foundation border-outline-2 text-foreground font-medium',
'hover:bg-primary-muted disabled:hover:bg-foundation focus-visible:border-foundation'
],
danger: [
'bg-danger border-danger-darker text-foundation font-medium',
'hover:bg-danger-darker disabled:hover:bg-danger focus-visible:border-foundation'
],
primary: [
'bg-primary border-outline-1 text-foreground-on-primary font-semibold',
'hover:bg-primary-focus disabled:hover:bg-primary focus-visible:border-foundation'
]
}
if (props.rounded) {
classParts.push('!rounded-full')
}
if (props.text || props.link) {
switch (props.color) {
case 'subtle':
classParts.push('text-foreground')
break
case 'outline':
classParts.push('text-foreground')
break
case 'danger':
classParts.push('text-danger')
break
case 'primary':
default:
classParts.push('text-primary')
break
}
} else {
switch (props.color) {
case 'subtle':
classParts.push(...colorsBgBorder.subtle)
break
case 'outline':
classParts.push(...colorsBgBorder.outline)
break
case 'danger':
classParts.push(...colorsBgBorder.danger)
break
case 'primary':
default:
classParts.push(...colorsBgBorder.primary)
break
}
}
return classParts.join(' ')
})
const sizeClasses = computed(() => {
switch (props.size) {
case 'sm':
return 'h-6 text-body-2xs'
case 'lg':
return 'h-10 text-body-sm'
default:
case 'base':
return 'h-8 text-body-xs'
}
})
const paddingClasses = computed(() => {
if (props.text || props.link) {
return 'p-0'
}
const hasIconLeft = !!props.iconLeft
const hasIconRight = !!props.iconRight
const hideText = props.hideText
switch (props.size) {
case 'sm':
if (hideText) return 'w-6'
if (hasIconLeft) return 'py-1 pr-2 pl-1'
if (hasIconRight) return 'py-1 pl-2 pr-1'
return 'px-2 py-1'
case 'lg':
if (hideText) return 'w-10'
if (hasIconLeft) return 'py-2 pr-6 pl-4'
if (hasIconRight) return 'py-2 pl-6 pr-4'
return 'px-6 py-2'
case 'base':
default:
if (hideText) return 'w-8'
if (hasIconLeft) return 'py-0 pr-4 pl-2'
if (hasIconRight) return 'py-0 pl-4 pr-2'
return 'px-4 py-0'
}
})
const generalClasses = computed(() => {
const baseClasses = [
'inline-flex justify-center items-center',
'text-center select-none whitespace-nowrap',
'outline outline-2 outline-transparent',
'transition duration-200 ease-in-out focus-visible:outline-outline-4'
]
const additionalClasses = []
if (!props.text && !props.link) {
additionalClasses.push('rounded-md border')
}
if (props.fullWidth) {
additionalClasses.push('w-full')
} else if (!props.hideText) {
additionalClasses.push('max-w-max')
}
if (isDisabled.value) {
additionalClasses.push('cursor-not-allowed opacity-60')
}
return [...baseClasses, ...additionalClasses].join(' ')
})
const buttonClasses = computed(() => {
return [
generalClasses.value,
sizeClasses.value,
bgAndBorderClasses.value,
paddingClasses.value
].join(' ')
})
const iconClasses = computed(() => {
const classParts: string[] = ['shrink-0']
switch (props.size) {
case 'sm':
classParts.push('h-4 w-4 p-0.5')
break
case 'lg':
classParts.push('h-6 w-6 p-1')
break
case 'base':
default:
classParts.push('h-6 w-6 p-1')
break
}
return classParts.join(' ')
})
const onClick = (e: MouseEvent) => {
if (isDisabled.value) {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
return
}
emit('click', e)
}
</script>
@@ -0,0 +1,18 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 1C6.24288 1 4.81818 2.42334 4.81818 4.18004C4.81818 5.93674 6.24288 7.36008 8 7.36008C9.75712 7.36008 11.1818 5.93674 11.1818 4.18004C11.1818 2.42334 9.75712 1 8 1Z"
fill="currentColor"
/>
<path
d="M6.18182 9.17649C4.42465 9.17649 3 10.6005 3 12.3578V14.6281H13V12.3578C13 10.6005 11.5754 9.17649 9.81818 9.17649H6.18182Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,18 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
/>
</svg>
</template>
@@ -0,0 +1,14 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13 11.5H15.5V13H13V15.5H11.5V13H9V11.5H11.5V9H13V11.5ZM10.5 0.75C10.9142 0.75 11.25 1.08579 11.25 1.5V2H12C13.6569 2 15 3.34315 15 5V8H13.5V6.75H1.5V12C1.5 12.8284 2.17157 13.5 3 13.5H8V15H3C1.34315 15 0 13.6569 0 12V5C8.05333e-08 3.34315 1.34315 2 3 2H4.75V1.5C4.75 1.08579 5.08579 0.75 5.5 0.75C5.91421 0.75 6.25 1.08579 6.25 1.5V2H9.75V1.5C9.75 1.08579 10.0858 0.75 10.5 0.75ZM3 3.5C2.17157 3.5 1.5 4.17157 1.5 5V5.25H13.5V5C13.5 4.17157 12.8284 3.5 12 3.5H3Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8.75 3.75C8.75 3.33579 8.41421 3 8 3C7.58579 3 7.25 3.33579 7.25 3.75V8V8.31066L7.46967 8.53033L9.72358 10.7842C10.0165 11.0771 10.4913 11.0771 10.7842 10.7842C11.0771 10.4913 11.0771 10.0165 10.7842 9.72358L8.75 7.68934V3.75Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.8849 5.91851L7.25614 11.74L3.99951 8.48337L4.93388 7.549L7.24028 9.8554L11.9349 5L12.8849 5.91851Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,14 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 2C17.7652 1.99996 18.5015 2.29233 19.0583 2.81728C19.615 3.34224 19.9501 4.06011 19.995 4.824L20 5C20.7956 5 21.5587 5.31607 22.1213 5.87868C22.6839 6.44129 23 7.20435 23 8C23.0001 9.55238 22.3984 11.0444 21.3215 12.1625C20.2446 13.2806 18.7763 13.9378 17.225 13.996L17 14H13L13.15 14.005C13.6262 14.0408 14.0738 14.2458 14.412 14.5829C14.7502 14.92 14.9567 15.3669 14.994 15.843L15 16V20C15.0002 20.5046 14.8096 20.9906 14.4665 21.3605C14.1234 21.7305 13.6532 21.9572 13.15 21.995L13 22H11C10.4954 22.0002 10.0094 21.8096 9.63945 21.4665C9.26947 21.1234 9.04284 20.6532 9.005 20.15L9 20V16C8.99984 15.4954 9.19041 15.0094 9.5335 14.6395C9.87659 14.2695 10.3468 14.0428 10.85 14.005L11 14V13C11 12.7551 11.09 12.5187 11.2527 12.3356C11.4155 12.1526 11.6397 12.0357 11.883 12.007L12 12H17C18.0609 12 19.0783 11.5786 19.8284 10.8284C20.5786 10.0783 21 9.06087 21 8C21 7.75507 20.91 7.51866 20.7473 7.33563C20.5845 7.15259 20.3603 7.03566 20.117 7.007L20 7L19.995 7.176C19.9519 7.90959 19.6411 8.60186 19.1215 9.12148C18.6019 9.6411 17.9096 9.95193 17.176 9.995L17 10H7C6.23479 10 5.49849 9.70767 4.94174 9.18272C4.38499 8.65776 4.04989 7.93989 4.005 7.176L4 7V5C3.99996 4.23479 4.29233 3.49849 4.81728 2.94174C5.34224 2.38499 6.06011 2.04989 6.824 2.005L7 2H17Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,31 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 5C5 4.46957 5.21071 3.96086 5.58579 3.58579C5.96086 3.21071 6.46957 3 7 3H17C17.5304 3 18.0391 3.21071 18.4142 3.58579C18.7893 3.96086 19 4.46957 19 5V7C19 7.53043 18.7893 8.03914 18.4142 8.41421C18.0391 8.78929 17.5304 9 17 9H7C6.46957 9 5.96086 8.78929 5.58579 8.41421C5.21071 8.03914 5 7.53043 5 7V5Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 6H20C20.5304 6 21.0391 6.21071 21.4142 6.58579C21.7893 6.96086 22 7.46957 22 8C22 9.32608 21.4732 10.5979 20.5355 11.5355C19.5979 12.4732 18.3261 13 17 13H12V15"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10 16C10 15.7348 10.1054 15.4804 10.2929 15.2929C10.4804 15.1054 10.7348 15 11 15H13C13.2652 15 13.5196 15.1054 13.7071 15.2929C13.8946 15.4804 14 15.7348 14 16V20C14 20.2652 13.8946 20.5196 13.7071 20.7071C13.5196 20.8946 13.2652 21 13 21H11C10.7348 21 10.4804 20.8946 10.2929 20.7071C10.1054 20.5196 10 20.2652 10 20V16Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 14.5C8.23033 14.5 8.84266 14.2743 9.48679 12.986C9.79275 12.3741 10.0504 11.6156 10.2293 10.75H5.77067C5.94959 11.6156 6.20725 12.3741 6.51321 12.986C7.15734 14.2743 7.76967 14.5 8 14.5ZM5.55361 9.25C5.51859 8.84716 5.5 8.42956 5.5 8C5.5 7.57044 5.51859 7.15284 5.55361 6.75H10.4464C10.4814 7.15284 10.5 7.57044 10.5 8C10.5 8.42956 10.4814 8.84716 10.4464 9.25H5.55361ZM11.7574 10.75C11.5334 11.974 11.1641 13.0579 10.6914 13.9184C12.0984 13.2775 13.2369 12.1496 13.8913 10.75H11.7574ZM14.3799 9.25H11.9515C11.9834 8.84271 12 8.42523 12 8C12 7.57477 11.9834 7.15729 11.9515 6.75H14.3799C14.4587 7.15451 14.5 7.57243 14.5 8C14.5 8.42756 14.4587 8.84549 14.3799 9.25ZM4.04854 9.25H1.62008C1.54128 8.84549 1.5 8.42756 1.5 8C1.5 7.57243 1.54128 7.15451 1.62008 6.75H4.04854C4.01659 7.15729 4 7.57477 4 8C4 8.42523 4.01659 8.84271 4.04854 9.25ZM2.10868 10.75H4.2426C4.46661 11.974 4.83588 13.0579 5.30864 13.9184C3.90156 13.2775 2.7631 12.1496 2.10868 10.75ZM5.77067 5.25H10.2293C10.0504 4.38438 9.79275 3.6259 9.48679 3.01397C8.84266 1.72571 8.23033 1.5 8 1.5C7.76967 1.5 7.15734 1.72571 6.51321 3.01397C6.20725 3.6259 5.94959 4.38438 5.77067 5.25ZM11.7574 5.25H13.8913C13.2369 3.85044 12.0984 2.72251 10.6914 2.08162C11.1641 2.94207 11.5334 4.02603 11.7574 5.25ZM5.30864 2.08162C4.83588 2.94207 4.46661 4.02603 4.2426 5.25H2.10868C2.7631 3.85044 3.90156 2.72251 5.30864 2.08162ZM8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,38 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.06301 2.75L13.2511 9.93813L11.4539 11.7354C10.9854 12.2227 10.4243 12.6116 9.80367 12.8795C9.183 13.1473 8.51514 13.2887 7.83918 13.2953C7.16321 13.302 6.49271 13.1737 5.86691 12.9181C5.2411 12.6625 4.67257 12.2846 4.19456 11.8066C3.71656 11.3286 3.33869 10.76 3.08306 10.1342C2.82743 9.50843 2.69918 8.83793 2.70581 8.16196C2.71244 7.486 2.85382 6.81814 3.12167 6.19747C3.38953 5.5768 3.77848 5.01579 4.26576 4.54725L6.06301 2.75Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1 15L4.0625 11.9375"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.625 1L7.5625 4.0625"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15 5.375L11.9375 8.4375"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,5 @@
<template>
<svg width="18" height="19" viewBox="0 0 18 19" xmlns="http://www.w3.org/2000/svg">
<path d="M4.33333 17L1 1L16 9.25806L8.5 10.5L4.33333 17Z" stroke="#2563eb" />
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 2.5C2.17157 2.5 1.5 3.17157 1.5 4V10C1.5 10.8284 2.17157 11.5 3 11.5H3.75H4.5V12.25V13.1004L6.56675 11.6378L6.76146 11.5H7H13C13.8284 11.5 14.5 10.8284 14.5 10V4C14.5 3.17157 13.8284 2.5 13 2.5H3ZM0 4C0 2.34315 1.34315 1 3 1H13C14.6569 1 16 2.34315 16 4V10C16 11.6569 14.6569 13 13 13H7.23854L4.18325 15.1622L3 15.9996V14.55V13C1.34315 13 0 11.6569 0 10V4Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6 1C5.0335 1 4.25 1.7835 4.25 2.75V4H3C1.89543 4 1 4.89543 1 6V13C1 14.1046 1.89543 15 3 15H13C14.1046 15 15 14.1046 15 13V6C15 4.89543 14.1046 4 13 4H11.75V2.75C11.75 1.7835 10.9665 1 10 1H6ZM10.25 4V2.75C10.25 2.61193 10.1381 2.5 10 2.5H6C5.86193 2.5 5.75 2.61193 5.75 2.75V4H10.25ZM3 5.5H13C13.2761 5.5 13.5 5.72386 13.5 6V7H2.5V6C2.5 5.72386 2.72386 5.5 3 5.5ZM2.5 8.5V13C2.5 13.2761 2.72386 13.5 3 13.5H13C13.2761 13.5 13.5 13.2761 13.5 13V8.5H9V10H7V8.5H2.5Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,31 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.8335 7.83337H7.00016C6.55814 7.83337 6.13421 8.00897 5.82165 8.32153C5.50909 8.63409 5.3335 9.05801 5.3335 9.50004V17C5.3335 17.4421 5.50909 17.866 5.82165 18.1786C6.13421 18.4911 6.55814 18.6667 7.00016 18.6667H14.5002C14.9422 18.6667 15.3661 18.4911 15.6787 18.1786C15.9912 17.866 16.1668 17.4421 16.1668 17V16.1667"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M18.9875 7.48759C19.3157 7.15938 19.5001 6.71424 19.5001 6.25009C19.5001 5.78594 19.3157 5.34079 18.9875 5.01259C18.6593 4.68438 18.2142 4.5 17.75 4.5C17.2858 4.5 16.8407 4.68438 16.5125 5.01259L9.5 12.0001V14.5001H12L18.9875 7.48759Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.3335 6.16663L17.8335 8.66663"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,59 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.8438 11.1563L21.9062 7.21875V14.2187L17.9062 18.2188L17.8438 11.1563Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.19123 15.2186L2.15624 18.2188L2.09375 11.1563L6.15626 7.21875V11.7187"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.9062 7.71875L18.4062 3.21875H11.9062L7.90625 7.71875H14.9062Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M13.9062 11.7188H5.40625V20.7188H13.9062V11.7188Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.6562 8.20312V10.9687"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.9062 5.46875L20.1563 5.46875L20.1563 8.71875"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.6562 14.4531L17.0313 14.4686"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 2.5C2.17157 2.5 1.5 3.17157 1.5 4V10C1.5 10.8284 2.17157 11.5 3 11.5H3.75H4.5V12.25V13.1004L6.56675 11.6378L6.76146 11.5H7H13C13.8284 11.5 14.5 10.8284 14.5 10V4C14.5 3.17157 13.8284 2.5 13 2.5H3ZM0 4C0 2.34315 1.34315 1 3 1H13C14.6569 1 16 2.34315 16 4V10C16 11.6569 14.6569 13 13 13H7.23854L4.18325 15.1622L3 15.9996V14.55V13C1.34315 13 0 11.6569 0 10V4Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,14 @@
<template>
<svg
width="18"
height="16"
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
>
<path
d="M11.4998 3.75H12.2498V3V2.16667C12.2498 1.66177 12.6582 1.25 13.1665 1.25H15.6665C16.1714 1.25 16.5832 1.65832 16.5832 2.16667V5.5C16.5832 6.0049 16.1749 6.41667 15.6665 6.41667H13.1665C12.6641 6.41667 12.2498 6.00245 12.2498 5.5V4.66667V3.91667H11.4998H9.83317H9.08317V4.66667V10.5083C9.08317 11.3725 9.79396 12.0833 10.6582 12.0833H11.4998H12.2498V11.3333V10.5C12.2498 9.9951 12.6582 9.58333 13.1665 9.58333H15.6665C16.1714 9.58333 16.5832 9.99165 16.5832 10.5V13.8333C16.5832 14.3382 16.1749 14.75 15.6665 14.75H13.1665C12.6616 14.75 12.2498 14.3417 12.2498 13.8333V13V12.25H11.4998H10.6582C9.69738 12.25 8.9165 11.4691 8.9165 10.5083V4.66667V3.91667H8.1665H6.49984H5.74984V4.66667V5.5C5.74984 6.0049 5.34152 6.41667 4.83317 6.41667H2.33317C1.82827 6.41667 1.4165 6.00835 1.4165 5.5V2.16667C1.4165 1.66421 1.83072 1.25 2.33317 1.25H4.8415C5.3464 1.25 5.75817 1.65832 5.75817 2.16667V3V3.75H6.50817H11.4998Z"
/>
</svg>
</template>
@@ -0,0 +1,24 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 16H16V20"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19.458 11.042C20.318 8.67599 20.18 6.46199 18.858 5.14199C16.586 2.86799 11.673 4.09699 7.88503 7.88499C4.09703 11.673 2.86803 16.586 5.14103 18.859C7.36803 21.085 12.128 19.952 15.881 16.344"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="38"
height="37"
viewBox="0 0 38 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M30.5695 7.84167C30.4459 7.87905 30.4465 8.05327 30.5704 8.08974L30.6008 8.09869C32.0949 8.53857 33.2612 9.70436 33.6956 11.1923C33.7314 11.3148 33.9059 11.3148 33.9425 11.1925C34.386 9.71452 35.5462 8.55207 37.0303 8.10336L37.0652 8.09282C37.1885 8.05553 37.1877 7.88161 37.064 7.84544L37.0283 7.835C35.5338 7.39799 34.3679 6.23128 33.938 4.74249C33.9028 4.62035 33.7287 4.62043 33.6922 4.7422C33.249 6.21893 32.0893 7.38202 30.6065 7.83048L30.5695 7.84167ZM31.4317 23.8166L31.4317 14.0863H31.4318V10.3168H31.4317V10.3162L26.998 10.3162V10.3168H17.2469L11.255 16.849V26.5794H11.2549V30.349H11.255V30.3494H15.6888V30.349L25.4397 30.349L31.4317 23.8166ZM26.998 26.5794L26.998 14.0863H15.6888L15.6888 26.5794H26.998Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,28 @@
<template>
<svg
width="800px"
height="800px"
viewBox="0 0 36 36"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--twemoji"
preserveAspectRatio="xMidYMid meet"
>
<path
stroke="#000"
fill="none"
stroke-width="2"
d="M36 11a2 2 0 0 0-4 0s-.011 3.285-3 3.894V12c0-6.075-4.925-11-11-11S7 5.925 7 12v3.237C1.778 16.806 0 23.231 0 27a2 2 0 0 0 4 0s.002-3.54 3.336-3.958C7.838 27.883 8.954 33 11 33h1c4 0 3 2 7 2s3-2 6-2s2.395 2 6 2a3 3 0 0 0 3-3c0-.675-2.274-4.994-3.755-9.268C35.981 21.348 36 14.58 36 11z"
></path>
<circle fill="#000" stroke-width="1" cx="13" cy="12" r="2"></circle>
<circle fill="#000" cx="23" cy="12" r="3"></circle>
<path
stroke="#000"
stroke-width="2"
fill="none"
d="M22.192 19.491c2.65 1.987 3.591 5.211 2.1 7.199c-1.491 1.988-4.849 1.988-7.5 0c-2.65-1.987-3.591-5.211-2.1-7.199c1.492-1.989 4.849-1.988 7.5 0z"
></path>
</svg>
</template>
@@ -0,0 +1,52 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 13V4.5C8 4.10218 8.15804 3.72064 8.43934 3.43934C8.72064 3.15804 9.10218 3 9.5 3C9.89782 3 10.2794 3.15804 10.5607 3.43934C10.842 3.72064 11 4.10218 11 4.5V12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11 11.5V9.5C11 9.10218 11.158 8.72064 11.4393 8.43934C11.7206 8.15804 12.1022 8 12.5 8C12.8978 8 13.2794 8.15804 13.5607 8.43934C13.842 8.72064 14 9.10218 14 9.5V12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 10.5C14 10.1022 14.158 9.72064 14.4393 9.43934C14.7206 9.15804 15.1022 9 15.5 9C15.8978 9 16.2794 9.15804 16.5607 9.43934C16.842 9.72064 17 10.1022 17 10.5V12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.0002 11.5C17.0002 11.1022 17.1582 10.7206 17.4395 10.4393C17.7208 10.158 18.1024 10 18.5002 10C18.898 10 19.2795 10.158 19.5608 10.4393C19.8421 10.7206 20.0002 11.1022 20.0002 11.5V16C20.0002 17.5913 19.368 19.1174 18.2428 20.2426C17.1176 21.3679 15.5915 22 14.0002 22H12.0002H12.2082C11.2145 22.0002 10.2364 21.7535 9.36157 21.2823C8.48676 20.811 7.7427 20.1299 7.19618 19.3L7.00018 19C6.68818 18.521 5.59318 16.612 3.71418 13.272C3.52263 12.9315 3.47147 12.5298 3.57157 12.1522C3.67166 11.7745 3.91513 11.4509 4.25018 11.25C4.60706 11.0359 5.02526 10.9471 5.43834 10.9978C5.85143 11.0486 6.23572 11.2359 6.53018 11.53L8.00018 13"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2.54102 5.59497C3.30843 5.03394 4.13302 4.55561 5.00102 4.16797"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 3.45703C15.32 3.81103 16.558 4.35903 17.685 5.06903"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,66 @@
<template>
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.5 13V4.5C8.5 4.10218 8.65804 3.72064 8.93934 3.43934C9.22064 3.15804 9.60218 3 10 3C10.3978 3 10.7794 3.15804 11.0607 3.43934C11.342 3.72064 11.5 4.10218 11.5 4.5V12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.5 11.5V9.5C11.5 9.10218 11.658 8.72064 11.9393 8.43934C12.2206 8.15804 12.6022 8 13 8C13.3978 8 13.7794 8.15804 14.0607 8.43934C14.342 8.72064 14.5 9.10218 14.5 9.5V12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.5 10.5C14.5 10.1022 14.658 9.72064 14.9393 9.43934C15.2206 9.15804 15.6022 9 16 9C16.3978 9 16.7794 9.15804 17.0607 9.43934C17.342 9.72064 17.5 10.1022 17.5 10.5V12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.5002 11.5C17.5002 11.1022 17.6582 10.7206 17.9395 10.4393C18.2208 10.158 18.6024 10 19.0002 10C19.398 10 19.7795 10.158 20.0608 10.4393C20.3421 10.7206 20.5002 11.1022 20.5002 11.5V16C20.5002 17.5913 19.868 19.1174 18.7428 20.2426C17.6176 21.3679 16.0915 22 14.5002 22H12.5002H12.7082C11.7145 22.0002 10.7364 21.7535 9.86157 21.2823C8.98676 20.811 8.2427 20.1299 7.69618 19.3L7.50018 19C7.18818 18.521 6.09318 16.612 4.21418 13.272C4.02263 12.9315 3.97147 12.5298 4.07157 12.1522C4.17166 11.7745 4.41513 11.4509 4.75018 11.25C5.10706 11.0359 5.52526 10.9471 5.93834 10.9978C6.35143 11.0486 6.73572 11.2359 7.03018 11.53L8.50018 13"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.5 3L4.5 2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.5 7H3.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.5 3L15.5 2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.5 6H16.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,73 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_614_5022)">
<path
d="M11 14V5.5C11 5.10218 11.158 4.72064 11.4393 4.43934C11.7206 4.15804 12.1022 4 12.5 4C12.8978 4 13.2794 4.15804 13.5607 4.43934C13.842 4.72064 14 5.10218 14 5.5V13"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 12.5V10.5C14 10.303 14.0388 10.108 14.1142 9.92597C14.1896 9.74399 14.3001 9.57863 14.4393 9.43934C14.5786 9.30005 14.744 9.18956 14.926 9.11418C15.108 9.0388 15.303 9 15.5 9C15.697 9 15.892 9.0388 16.074 9.11418C16.256 9.18956 16.4214 9.30005 16.5607 9.43934C16.6999 9.57863 16.8104 9.74399 16.8858 9.92597C16.9612 10.108 17 10.303 17 10.5V13"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17 11.5C17 11.1022 17.158 10.7206 17.4393 10.4393C17.7206 10.158 18.1022 10 18.5 10C18.8978 10 19.2794 10.158 19.5607 10.4393C19.842 10.7206 20 11.1022 20 11.5V13"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M20.0002 12.5C20.0002 12.1022 20.1582 11.7206 20.4395 11.4393C20.7208 11.158 21.1024 11 21.5002 11C21.898 11 22.2795 11.158 22.5608 11.4393C22.8421 11.7206 23.0002 12.1022 23.0002 12.5V17C23.0002 18.5913 22.368 20.1174 21.2428 21.2426C20.1176 22.3679 18.5915 23 17.0002 23H15.0002H15.2082C14.2145 23.0002 13.2364 22.7535 12.3616 22.2823C11.4868 21.811 10.7427 21.1299 10.1962 20.3C10.1306 20.2002 10.0653 20.1002 10.0002 20C9.68818 19.521 8.59318 17.612 6.71418 14.272C6.52263 13.9315 6.47147 13.5298 6.57157 13.1522C6.67166 12.7745 6.91513 12.4509 7.25018 12.25C7.60706 12.0359 8.02526 11.9471 8.43834 11.9978C8.85143 12.0486 9.23572 12.2359 9.53018 12.53L11.0002 14"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M0.750039 6.36034L3.75004 6.36034L3.75004 9.36034"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3.75068 6.36101C1.75068 8.36101 -0.249322 8.89698 2.25068 11.361"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.24999 0.750039L5.24999 3.75004L8.24999 3.75004"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.24969 3.7497C7.24969 1.7497 7.78565 -0.250299 10.2497 2.2497"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_614_5022">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
</template>
@@ -0,0 +1,90 @@
<template>
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.5 23.9655V3.00003L10.5 10.8621V31.5L1.5 23.9655Z"
stroke="#CBD5E1"
stroke-linejoin="round"
/>
<path
d="M1.5 3.00003L22.5 1.50003"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1.5 24L22.5 22.5"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="2 2"
/>
<path
d="M10.5 31.5L31.5 30"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.5 11.25L31.5 9.75003"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M22.5 1.50003L31.5 9.34814V30"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M22.5 1.50003V22.5"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="2 2"
/>
<path
d="M22.5 22.5L31.5 30"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="2 2"
/>
<path
d="M6.04926 17.378L27.0493 15.878"
stroke="#3B82F6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.37823 19.3902L7.10927 15.3896"
stroke="#3B82F6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M25.6345 17.8902L28.3656 13.8896"
stroke="#3B82F6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.96887 14.093L7.03114 20.663"
stroke="#3B82F6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M26.2251 12.593L28.2874 19.163"
stroke="#3B82F6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,68 @@
<template>
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 25.4655V4.50003L12 12.3621V33L3 25.4655Z"
stroke="#CBD5E1"
stroke-linejoin="round"
/>
<path
d="M3 4.50003L24 3.00003"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 25.5L24 24"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="2 2"
/>
<path
d="M12 33L33 31.5"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 12.75L33 11.25"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M24 3.00003L33 10.8481V31.5"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M24 3.00003V24"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="2 2"
/>
<path
d="M24 24L33 31.5"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="2 2"
/>
<path
d="M3 25.5L33 10.5"
stroke="#3B82F6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="3" cy="25.5" r="2.25" fill="#3B82F6" />
<circle cx="33" cy="10.5" r="2.25" fill="#3B82F6" />
</svg>
</template>
@@ -0,0 +1,27 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-ruler-measure"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M19.875 12c.621 0 1.125 .512 1.125 1.143v5.714c0 .631 -.504 1.143 -1.125 1.143h-15.875a1 1 0 0 1 -1 -1v-5.857c0 -.631 .504 -1.143 1.125 -1.143h15.75z"
/>
<path d="M9 12v2" />
<path d="M6 12v3" />
<path d="M12 12v3" />
<path d="M18 12v3" />
<path d="M15 12v2" />
<path d="M3 3v4" />
<path d="M3 5h18" />
<path d="M21 3v4" />
</svg>
</template>
@@ -0,0 +1,18 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 11.5C19 17.299 17.5 22 12.5 22C7.5 22 6 17.299 6 11.5C6 5.70101 6 1 12.5 1C19 1 19 5.70101 19 11.5Z"
stroke="#94A3B8"
/>
<path d="M6 9H19" stroke="#94A3B8" />
<path d="M19 9C19 6.5 19 1 12.5 1" stroke="#334155" />
<path d="M19.5 9H12" stroke="#334155" />
<path d="M12.5 0.5V9.5" stroke="#334155" />
</svg>
</template>
@@ -0,0 +1,18 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 11.5C19 17.299 17.5 22 12.5 22C7.5 22 6 17.299 6 11.5C6 5.70101 6 1 12.5 1C19 1 19 5.70101 19 11.5Z"
stroke="currentColor"
/>
<path d="M6 9C6 6.5 6 1 12.5 1" stroke="#334155" />
<path d="M6 9H19" stroke="#94A3B8" />
<path d="M5.5 9H13" stroke="#334155" />
<path d="M12.5 0.5V9.5" stroke="#334155" />
</svg>
</template>
@@ -0,0 +1,24 @@
<template>
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.5 11.5C19.5 17.299 18 22 13 22C8 22 6.5 17.299 6.5 11.5C6.5 5.70101 6.5 1 13 1C19.5 1 19.5 5.70101 19.5 11.5Z"
stroke="currentColor"
/>
<path d="M6.5 9H19.5" stroke="#94A3B8" />
<rect
x="11.5"
y="2.5"
width="3"
height="5"
rx="1"
stroke="#334155"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,38 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M4.16699 4.16667C4.16699 3.72464 4.34259 3.30072 4.65515 2.98816C4.96771 2.67559 5.39163 2.5 5.83366 2.5H14.167C14.609 2.5 15.0329 2.67559 15.3455 2.98816C15.6581 3.30072 15.8337 3.72464 15.8337 4.16667V15.8333C15.8337 16.2754 15.6581 16.6993 15.3455 17.0118C15.0329 17.3244 14.609 17.5 14.167 17.5H5.83366C5.39163 17.5 4.96771 17.3244 4.65515 17.0118C4.34259 16.6993 4.16699 16.2754 4.16699 15.8333V4.16667Z"
stroke="#334155"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M7.5 5.8335H12.5"
stroke="#334155"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M7.5 9.1665H12.5"
stroke="#334155"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M7.5 12.5H10.8333"
stroke="#334155"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,13 @@
<template>
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.648 8.90476L13.784 15.381H2.224L4.352 8.90476H11.648ZM4 0L0.8 3.2381L4 6.47619V4.04762H7.2V2.42857H4V0ZM12 0V2.42857H8.8V4.04762H12V6.47619L15.2 3.2381L12 0ZM12.8 7.28571H3.2L0 17H16L12.8 7.28571Z"
/>
</svg>
</template>
@@ -0,0 +1,13 @@
<template>
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.648 8.90476L13.784 15.381H2.224L4.352 8.90476H11.648ZM12 0L8.8 3.2381L12 6.47619V4.04762H15.2V2.42857H12V0ZM4 0V2.42857H0.8V4.04762H4V6.47619L7.2 3.2381L4 0ZM12.8 7.28571H3.2L0 17H16L12.8 7.28571Z"
/>
</svg>
</template>
@@ -0,0 +1,22 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2965_95098)">
<path
d="M12.6611 7.51562C13.0107 7.71744 13.0326 8.20331 12.7266 8.44043L12.6611 8.48437L4.58887 13.1445C4.21624 13.3597 3.75025 13.0913 3.75 12.6611V3.33887C3.75023 2.93564 4.15941 2.67432 4.51758 2.82031L4.58887 2.85547L12.6611 7.51562Z"
stroke="currentColor"
stroke-width="1.5"
/>
</g>
<defs>
<clipPath id="clip0_2965_95098">
<rect width="16" height="16" fill="currentColor" />
</clipPath>
</defs>
</svg>
</template>
@@ -0,0 +1,11 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 3V13M3 8H13" stroke="currentColor" stroke-width="1.5" />
</svg>
</template>
@@ -0,0 +1,45 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.5 13H18.5L21.5 22H2.5L5.5 13Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2 7.5H9"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M6.5 5L9 7.5L6.5 10"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M22 7.5H15"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.5 10L15 7.5L17.5 5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 2.5H5C5.27614 2.5 5.5 2.72386 5.5 3V5C5.5 5.27614 5.27614 5.5 5 5.5H3C2.72386 5.5 2.5 5.27614 2.5 5V3C2.5 2.72386 2.72386 2.5 3 2.5ZM1 3C1 1.89543 1.89543 1 3 1H5C6.10457 1 7 1.89543 7 3V5C7 6.10457 6.10457 7 5 7H3C1.89543 7 1 6.10457 1 5V3ZM3 10.5H5C5.27614 10.5 5.5 10.7239 5.5 11V13C5.5 13.2761 5.27614 13.5 5 13.5H3C2.72386 13.5 2.5 13.2761 2.5 13V11C2.5 10.7239 2.72386 10.5 3 10.5ZM1 11C1 9.89543 1.89543 9 3 9H5C6.10457 9 7 9.89543 7 11V13C7 14.1046 6.10457 15 5 15H3C1.89543 15 1 14.1046 1 13V11ZM13 2.5H11C10.7239 2.5 10.5 2.72386 10.5 3V5C10.5 5.27614 10.7239 5.5 11 5.5H13C13.2761 5.5 13.5 5.27614 13.5 5V3C13.5 2.72386 13.2761 2.5 13 2.5ZM11 1C9.89543 1 9 1.89543 9 3V5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5V3C15 1.89543 14.1046 1 13 1H11ZM11 10.5H13C13.2761 10.5 13.5 10.7239 13.5 11V13C13.5 13.2761 13.2761 13.5 13 13.5H11C10.7239 13.5 10.5 13.2761 10.5 13V11C10.5 10.7239 10.7239 10.5 11 10.5ZM9 11C9 9.89543 9.89543 9 11 9H13C14.1046 9 15 9.89543 15 11V13C15 14.1046 14.1046 15 13 15H11C9.89543 15 9 14.1046 9 13V11Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 7H4C2.89543 7 2 6.10457 2 5C2 3.89543 2.896 3 4.00057 3H11.9994C13.104 3 14 3.89543 14 5C14 6.10457 13.1046 7 12 7ZM5 5C5 5.55228 4.55228 6 4 6C3.44772 6 3 5.55228 3 5C3 4.44772 3.44772 4 4 4C4.55228 4 5 4.44772 5 5ZM2 12V10C2 8.89543 2.896 8 4.00057 8H11.9994C13.104 8 14 8.89543 14 10V12C14 13.1046 13.1046 14 12 14H4C2.89543 14 2 13.1046 2 12ZM5 10C5 10.5523 4.55228 11 4 11C3.44772 11 3 10.5523 3 10C3 9.44772 3.44772 9 4 9C4.55228 9 5 9.44772 5 10Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,29 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 15V1H6.5V15H5Z"
fill="currentColor"
/>
<path
d="M10.9002 8.28183C11.0333 8.1154 11.0333 7.8846 10.9002 7.71817L8.87389 5.18369C8.59132 4.83026 8 5.02096 8 5.46552V10.5345C8 10.979 8.59132 11.1697 8.87389 10.8163L10.9002 8.28183Z"
fill="currentColor"
/>
<rect
x="0.75"
y="0.75"
width="14.5"
height="14.5"
rx="3.25"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
</template>
@@ -0,0 +1,30 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11 15V1H9.5V15H11Z"
fill="currentColor"
/>
<path
d="M5.0998 8.28183C4.96673 8.1154 4.96673 7.8846 5.0998 7.71817L7.12611 5.18369C7.40868 4.83026 8 5.02096 8 5.46552L8 10.5345C8 10.979 7.40868 11.1697 7.12611 10.8163L5.0998 8.28183Z"
fill="currentColor"
/>
<rect
x="-0.75"
y="0.75"
width="14.5"
height="14.5"
rx="3.25"
transform="matrix(-1 0 0 1 14.5 0)"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
</template>
@@ -0,0 +1,14 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.64645 8.35355C9.84171 8.15829 9.84171 7.84171 9.64645 7.64645L6.85355 4.85355C6.53857 4.53857 6 4.76165 6 5.20711V10.7929C6 11.2383 6.53857 11.4614 6.85355 11.1464L9.64645 8.35355Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,45 @@
<template>
<svg
width="18"
height="16"
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.5 13.8154C2.64014 13.1571 3.93347 12.8105 5.25 12.8105C6.56652 12.8105 7.85986 13.1571 9 13.8154C10.1401 13.1571 11.4335 12.8105 12.75 12.8105C14.0665 12.8105 15.3599 13.1571 16.5 13.8154"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1.5 2.98137C2.64014 2.32311 3.93347 1.97656 5.25 1.97656C6.56652 1.97656 7.85986 2.32311 9 2.98137C10.1401 2.32311 11.4335 1.97656 12.75 1.97656C14.0665 1.97656 15.3599 2.32311 16.5 2.98137"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1.5 2.98242V13.8158"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M9 2.98242V13.8158"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.5 2.98242V13.8158"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8.75 3.75C8.75 3.33579 8.41421 3 8 3C7.58579 3 7.25 3.33579 7.25 3.75V8V8.31066L7.46967 8.53033L9.72358 10.7842C10.0165 11.0771 10.4913 11.0771 10.7842 10.7842C11.0771 10.4913 11.0771 10.0165 10.7842 9.72358L8.75 7.68934V3.75Z"
fill="currentColor"
/>
</svg>
</template>
@@ -0,0 +1,45 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7H6L3 15V17"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16 7H18L21 15V17"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10 16H14"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 16.5C14 17.4283 14.3687 18.3185 15.0251 18.9749C15.6815 19.6313 16.5717 20 17.5 20C18.4283 20 19.3185 19.6313 19.9749 18.9749C20.6313 18.3185 21 17.4283 21 16.5C21 15.5717 20.6313 14.6815 19.9749 14.0251C19.3185 13.3687 18.4283 13 17.5 13C16.5717 13 15.6815 13.3687 15.0251 14.0251C14.3687 14.6815 14 15.5717 14 16.5Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 16.5C3 16.9596 3.09053 17.4148 3.26642 17.8394C3.44231 18.264 3.70012 18.6499 4.02513 18.9749C4.35013 19.2999 4.73597 19.5577 5.16061 19.7336C5.58525 19.9095 6.04037 20 6.5 20C6.95963 20 7.41475 19.9095 7.83939 19.7336C8.26403 19.5577 8.64987 19.2999 8.97487 18.9749C9.29988 18.6499 9.55769 18.264 9.73358 17.8394C9.90947 17.4148 10 16.9596 10 16.5C10 16.0404 9.90947 15.5852 9.73358 15.1606C9.55769 14.736 9.29988 14.3501 8.97487 14.0251C8.64987 13.7001 8.26403 13.4423 7.83939 13.2664C7.41475 13.0905 6.95963 13 6.5 13C6.04037 13 5.58525 13.0905 5.16061 13.2664C4.73597 13.4423 4.35013 13.7001 4.02513 14.0251C3.70012 14.3501 3.44231 14.736 3.26642 15.1606C3.09053 15.5852 3 16.0404 3 16.5Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,38 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.5 8.79167L12 12.5833M18.5 8.79167V15.2917L12 19.0833M18.5 8.79167L12 5L5.5 8.79167M12 12.5833L5.5 8.79167M12 12.5833V19.0833M12 19.0833L5.5 15.2917V8.79167"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.5 15.2917L1.5 17.6251"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M18.5 15.2957L22.5 17.629"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 5V1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -0,0 +1,38 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_197_13852)">
<path
d="M4.876 13.61C4.28624 13.9796 3.80311 14.4966 3.47436 15.1101C3.14561 15.7235 2.98261 16.4121 3.00147 17.1079C3.02033 17.8036 3.2204 18.4824 3.58191 19.0771C3.94341 19.6719 4.45385 20.162 5.06277 20.4991C5.67169 20.8361 6.35802 21.0085 7.05395 20.9991C7.74988 20.9897 8.43131 20.7989 9.03092 20.4455C9.63053 20.0922 10.1276 19.5885 10.4729 18.9842C10.8182 18.3799 10.9999 17.696 11 17H17"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.066 20.502C15.6003 20.7969 16.1949 20.9656 16.8045 20.9953C17.414 21.0249 18.0222 20.9147 18.5826 20.6731C19.143 20.4315 19.6406 20.0649 20.0375 19.6013C20.4344 19.1377 20.7199 18.5895 20.8722 17.9985C21.0246 17.4076 21.0397 16.7897 20.9164 16.192C20.7931 15.5943 20.5348 15.0328 20.161 14.5504C19.7873 14.0679 19.3082 13.6774 18.7603 13.4087C18.2124 13.14 17.6102 13.0002 17 13C16.294 13 15.576 13.179 15 13.5L12 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16 8C16 6.93913 15.5786 5.92172 14.8284 5.17157C14.0783 4.42143 13.0609 4 12 4C10.9391 4 9.92172 4.42143 9.17157 5.17157C8.42143 5.92172 8 6.93913 8 8C8 9.506 8.77 10.818 10 11.5L7 17"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_197_13852">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
</template>
@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M4.00065 8.66667C2.53398 8.66667 1.33398 9.86667 1.33398 11.3333C1.33398 12.8 2.53398 14 4.00065 14C5.46732 14 6.66732 12.8 6.66732 11.3333C6.66732 9.86667 5.46732 8.66667 4.00065 8.66667ZM8.00065 2C6.53398 2 5.33398 3.2 5.33398 4.66667C5.33398 6.13333 6.53398 7.33333 8.00065 7.33333C9.46732 7.33333 10.6673 6.13333 10.6673 4.66667C10.6673 3.2 9.46732 2 8.00065 2ZM12.0007 8.66667C10.534 8.66667 9.33398 9.86667 9.33398 11.3333C9.33398 12.8 10.534 14 12.0007 14C13.4673 14 14.6673 12.8 14.6673 11.3333C14.6673 9.86667 13.4673 8.66667 12.0007 8.66667Z"
fill="currentColor"
/>
</g>
</svg>
</template>
@@ -0,0 +1,24 @@
<template>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 3.75V8.25M3.75 3.75H8.25M3.75 3.75L9 9M20.25 3.75H15.75M20.25 3.75V8.25M20.25 3.75L15 9"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.75 15.4028L18.8093 12.3435C18.8772 12.2756 18.9638 12.2294 19.0581 12.2107C19.1523 12.1919 19.25 12.2016 19.3387 12.2383C19.4275 12.2751 19.5034 12.3373 19.5568 12.4172C19.6102 12.4971 19.6388 12.591 19.6389 12.687V20.063C19.6388 20.159 19.6102 20.2529 19.5568 20.3328C19.5034 20.4127 19.4275 20.4749 19.3387 20.5117C19.25 20.5484 19.1523 20.5581 19.0581 20.5393C18.9638 20.5206 18.8772 20.4744 18.8093 20.4065L15.75 17.3472M8.45833 20.75H14.2917C14.6784 20.75 15.0494 20.5964 15.3229 20.3229C15.5964 20.0494 15.75 19.6784 15.75 19.2917V13.4583C15.75 13.0716 15.5964 12.7006 15.3229 12.4271C15.0494 12.1536 14.6784 12 14.2917 12H8.45833C8.07156 12 7.70063 12.1536 7.42714 12.4271C7.15365 12.7006 7 13.0716 7 13.4583V19.2917C7 19.6784 7.15365 20.0494 7.42714 20.3229C7.70063 20.5964 8.07156 20.75 8.45833 20.75Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
@@ -1,22 +1,46 @@
<template>
<div
:class="[
'relative w-full h-1 bg-blue-500/30 text-xs text-foreground-on-primary overflow-hidden rounded-xl',
showBar ? 'opacity-100' : 'opacity-0'
]"
>
<div class="swoosher relative top-0 bg-blue-500/50"></div>
<div class="w-full text-xs text-foreground-on-primary space-y-1">
<!-- Bar container -->
<div
:class="[
'w-full h-1 overflow-hidden rounded-xl bg-blue-500/30',
showBar ? 'opacity-100' : 'opacity-0'
]"
>
<!-- Swooshing animation -->
<div v-if="isIndeterminate" class="swoosher top-0 left-0 h-full bg-blue-500/50"></div>
<!-- Determinate progress bar -->
<div
v-else
class="top-0 left-0 h-full bg-blue-500 transition-all duration-300 ease-linear"
:style="{ width: `${progressPercent + 20}%` }"
></div>
</div>
<!-- Progress text below -->
<div v-if="isIndeterminate" class="text-[13px] text-center text-foreground-2">
{{ props.progress.summary }}
</div>
<div v-else class="text-[13px] text-center text-foreground-2">
{{ progressPercent.toFixed(0) }}% ({{ props.progress.summary }})
</div>
</div>
</template>
<script setup lang="ts">
import { useMounted } from '@vueuse/core'
import { computed } from 'vue'
const props = defineProps<{ loading: boolean; clientOnly?: boolean }>()
<script setup lang="ts">
import { computed } from 'vue'
import { useMounted } from '@vueuse/core'
import { LoadingProgress } from '@src/store/visualStore'
const props = defineProps<{ progress: LoadingProgress; clientOnly?: boolean }>()
const mounted = useMounted()
const showBar = computed(() => (mounted.value || !props.clientOnly) && props.loading)
const showBar = computed(() => (mounted.value || !props.clientOnly) && !!props.progress)
const isIndeterminate = computed(() => props.progress.progress == null)
const progressPercent = computed(() => (props.progress.progress ?? 0) * 100)
</script>
<style scoped>
.swoosher {
width: 100%;
@@ -29,11 +53,9 @@ const showBar = computed(() => (mounted.value || !props.clientOnly) && props.loa
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
@@ -0,0 +1,8 @@
<template>
<div
class="bg-foundation text-foreground rounded-lg w-8 md:w-10 flex flex-col justify-center items-center md:gap-1 border border-outline-2 shadow"
>
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
@@ -1,10 +1,14 @@
<template>
<button
:class="`transition rounded-lg w-10 h-10 flex items-center justify-center ${shadowClasses} ${colorClasses} active:scale-[0.9] outline-none`"
:title="tooltip"
:class="`transition rounded-lg w-8 md:w-10 h-8 md:h-10 shrink-0 flex items-center justify-center ${colorClasses} outline-none ${
props.flat ? '!w-7 md:!w-9' : 'border border-outline-2 w-8 md:w-10 shadow'
}`"
>
<slot></slot>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
@@ -12,15 +16,14 @@ const props = defineProps<{
active?: boolean
flat?: boolean
secondary?: boolean
tooltip?: string
}>()
const shadowClasses = computed(() => (props.flat ? '' : 'shadow-md'))
const colorClasses = computed(() => {
const parts = []
if (props.active) {
if (props.secondary) parts.push('bg-foundation text-primary')
else parts.push('bg-primary text-foreground-on-primary')
else parts.push('bg-primary text-foreground-on-primary border-primary')
} else {
parts.push('bg-foundation text-foreground')
}
@@ -0,0 +1,69 @@
<template>
<div ref="menuWrapper" class="relative z-30">
<ViewerControlsButtonToggle
:v-tippy="tooltip"
flat
secondary
:active="open"
@click="toggleMenu"
>
<slot name="trigger-icon" />
</ViewerControlsButtonToggle>
<div
v-if="open"
ref="menuContent"
class="absolute left-10 sm:left-[46px] -top-0 bg-foundation rounded-md border border-outline-2 flex flex-col overflow-hidden shadow"
>
<div
v-if="$slots.title"
class="flex items-center py-2 px-2 border-b border-outline-2 sticky top-0 z-50 bg-foundation"
>
<div class="flex items-center text-body-2xs text-foreground font-medium">
<span class="truncate flex-1">
<slot name="title"></slot>
</span>
</div>
</div>
<div class="max-h-68 simple-scrollbar overflow-y-auto">
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { computed, ref } from 'vue'
import ViewerControlsButtonToggle from '../controls/ViewerControlsButtonToggle.vue'
const props = defineProps<{
tooltip?: string
open: boolean
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()
const open = computed({
get: () => props.open,
set: (val) => emit('update:open', val)
})
const menuContent = ref<HTMLElement | null>(null)
const menuWrapper = ref<HTMLElement | null>(null)
const toggleMenu = () => {
open.value = !open.value
}
onClickOutside(
menuContent,
(event) => {
if (!menuWrapper.value?.contains(event.target as Node)) {
open.value = false
}
},
{ ignore: [menuWrapper] }
)
</script>
@@ -0,0 +1,24 @@
<template>
<button
:v-tippy="description ? description : undefined"
class="flex items-center justify-between hover:bg-highlight-1 text-foreground w-full h-full text-body-2xs py-1.5 pr-2 pl-1 rounded-md"
:class="{ 'bg-highlight-1': active }"
>
<div v-if="!hideActiveTick" class="w-5">
<Check v-if="active" class="h-4 w-4 text-foreground-2" />
</div>
<div class="flex-1 text-left">{{ label }}</div>
<slot />
</button>
</template>
<script setup lang="ts">
import Check from '../../global/icon/Check.vue'
defineProps<{
label: string
description?: string
active?: boolean
hideActiveTick?: boolean
shortcut?: string
}>()
</script>
@@ -0,0 +1,85 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<ViewerMenu v-model:open="open" title="View modes">
<template #trigger-icon>
<ViewModes class="h-5 w-5" />
</template>
<template #title>View modes</template>
<div
class="p-1.5"
@mouseenter="cancelCloseTimer"
@mouseleave="isManuallyOpened ? undefined : startCloseTimer"
@focusin="cancelCloseTimer"
@focusout="isManuallyOpened ? undefined : startCloseTimer"
>
<div v-for="(label, mode) in viewModes" :key="mode">
<ViewerMenuItem
:label="label"
:active="mode.toString() === visualStore.defaultViewModeInFile"
@click="handleViewModeChange(Number(mode))"
/>
</div>
</div>
</ViewerMenu>
</template>
<script setup lang="ts">
import { useTimeoutFn } from '@vueuse/core'
import { ViewMode } from '@speckle/viewer'
import ViewerMenu from '../menu/ViewerMenu.vue'
import ViewerMenuItem from '../menu/ViewerMenuItem.vue'
import { onUnmounted, ref, computed, onMounted } from 'vue'
import { useVisualStore } from '@src/store/visualStore'
import ViewModes from '../../global/icon/ViewModes.vue'
const viewModes = {
[ViewMode.DEFAULT]: 'Default',
[ViewMode.DEFAULT_EDGES]: 'Edges',
[ViewMode.SHADED]: 'Shaded',
[ViewMode.PEN]: 'Pen',
[ViewMode.ARCTIC]: 'Arctic',
[ViewMode.COLORS]: 'Colors'
}
const visualStore = useVisualStore()
// Props
const props = defineProps<{
open: boolean
}>()
// Emits
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
(e: 'force-close-others'): void
(e: 'view-mode-clicked', value: ViewMode): void
}>()
// Computed v-model
const open = computed({
get: () => props.open,
set: (val) => emit('update:open', val)
})
// State
const isManuallyOpened = ref(false)
const { start: startCloseTimer, stop: cancelCloseTimer } = useTimeoutFn(
() => {
open.value = false
},
3000,
{ immediate: false }
)
const handleViewModeChange = (mode: ViewMode) => {
open.value = false
visualStore.setDefaultViewModeInFile(mode.toString())
visualStore.writeViewModeToFile(mode)
emit('view-mode-clicked', mode)
}
onUnmounted(() => {
cancelCloseTimer()
})
</script>
@@ -0,0 +1,88 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<ViewerMenu v-model:open="open" title="Views">
<template #trigger-icon>
<Views class="w-5 h-5" />
</template>
<template #title>Views</template>
<div
class="max-h-64 simple-scrollbar overflow-y-auto flex flex-col p-1.5"
@mouseenter="cancelCloseTimer"
@mouseleave="isManuallyOpened ? undefined : startCloseTimer"
@focusin="cancelCloseTimer"
@focusout="isManuallyOpened ? undefined : startCloseTimer"
>
<div v-for="shortcut in viewShortcuts" :key="shortcut.name">
<ViewerMenuItem
:label="shortcut.name"
hide-active-tick
:active="activeView === shortcut.name.toLowerCase()"
@click="handleViewChange(shortcut.name.toLowerCase() as CanonicalView)"
/>
</div>
<div v-if="views.length !== 0" class="w-full border-b my-1"></div>
<ViewerMenuItem
v-for="view in views"
:key="view.id"
hide-active-tick
:active="activeView === view.id"
:label="view.name ? view.name : view.id"
@click="handleViewChange(view)"
/>
</div>
</ViewerMenu>
</template>
<script setup lang="ts">
import { useTimeoutFn } from '@vueuse/core'
import type { CanonicalView, SpeckleView } from '@speckle/viewer'
import { onUnmounted, ref, computed } from 'vue'
import ViewerMenu from '../menu/ViewerMenu.vue'
import ViewerMenuItem from '../menu/ViewerMenuItem.vue'
import Views from '../../global/icon/Views.vue'
import { ViewShortcuts } from '../../../helpers/viewer/shortcuts/shortcuts'
// Props
const props = defineProps<{
views: SpeckleView[]
open: boolean
}>()
// Emits
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
(e: 'force-close-others'): void
(e: 'view-clicked', value: CanonicalView | SpeckleView)
}>()
// Computed open for v-model
const open = computed({
get: () => props.open,
set: (val) => emit('update:open', val)
})
// State
const isManuallyOpened = ref(false)
const activeView = ref<string | null>(null)
const { start: startCloseTimer, stop: cancelCloseTimer } = useTimeoutFn(
() => {
open.value = false
},
3000,
{ immediate: false }
)
const handleViewChange = (v: CanonicalView | SpeckleView) => {
open.value = false
emit('view-clicked', v)
}
const viewShortcuts = Object.values(ViewShortcuts)
onUnmounted(() => {
cancelCloseTimer()
})
</script>
@@ -0,0 +1,19 @@
<template>
<div class="flex shrink-0 overflow-hidden rounded-md border border-outline-2 bg-foundation-2">
<div
class="w-6 h-6 bg-center bg-contain bg-no-repeat flex items-center justify-center"
:style="logo ? { backgroundImage: `url('${logo}')` } : {}"
>
<span v-if="!logo" class="text-foreground-3 uppercase leading-none">
{{ name[0] }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
logo: string | undefined | null
name: string
}>()
</script>
@@ -0,0 +1,55 @@
import { useVisualStore } from '@src/store/visualStore'
import { ref } from 'vue'
type Versions = {
Versions: Version[]
}
export type Version = {
Number: string
Url: string
Os: number
Architecture: number
Date: string
Prerelease: boolean
}
export function useUpdateConnector() {
const versions = ref<Version[]>([])
const latestAvailableVersion = ref<Version | null>(null)
async function checkUpdate() {
try {
await getVersions()
} catch (e) {
console.error(e)
}
}
async function getVersions() {
const visualStore = useVisualStore()
const response = await fetch(`https://releases.speckle.dev/manager2/feeds/powerbi-v3.json`, {
method: 'GET'
})
if (!response.ok) {
throw new Error('Failed to fetch versions')
}
const data = (await response.json()) as unknown as Versions
const sortedVersions = data.Versions.sort(function (a: Version, b: Version) {
return new Date(b.Date).getTime() - new Date(a.Date).getTime()
})
versions.value = sortedVersions
const sanitizedVersion = sanitizeVersion(sortedVersions[0].Number)
latestAvailableVersion.value = { ...sortedVersions[0], Number: sanitizedVersion }
visualStore.setLatestAvailableVersion(latestAvailableVersion.value)
}
function sanitizeVersion(version: string): string {
const match = version.match(/\d+\.\d+\.\d+/)
return match ? match[0] : version // fallback to original version
}
return { checkUpdate }
}
@@ -0,0 +1,155 @@
import {
CameraController,
FilteringExtension,
NodeRenderView,
SelectionEvent,
SelectionExtension,
TreeNode,
ObjectLayers,
IViewer,
ExtendedIntersection
} from '@speckle/viewer'
import { Vector2, Vector3 } from 'three'
export enum FilteredSelectionEvent {
FilteredObjectClicked = 'filtered-object-clicked'
}
export interface FilteredSelectionEventPayload {
[FilteredSelectionEvent.FilteredObjectClicked]: SelectionEvent | null
}
export class FilteredSelectionExtension extends SelectionExtension {
// We're adding the Filtering Extension
public get inject(): Array<new (viewer: IViewer, ...args: any[]) => any> {
return [...super.inject, FilteringExtension]
}
public constructor(
viewer: IViewer,
protected cameraProvider: CameraController,
protected filtering: FilteringExtension
) {
super(viewer, cameraProvider)
}
public on<T extends FilteredSelectionEvent>(
eventType: T,
listener: (arg: FilteredSelectionEventPayload[T]) => void
): void {
super.on(eventType, listener)
}
protected isVisibleForSelection(id: string): boolean
protected isVisibleForSelection(rv: NodeRenderView): boolean
protected isVisibleForSelection(input: string | NodeRenderView): boolean {
if (input instanceof NodeRenderView) return this.isVisibleForSelectionRv(input)
else if (typeof input === 'string') return this.isVisibleForSelectionId(input)
return false
}
protected isVisibleForSelectionId(id: string): boolean {
// The current filtering state
const filteringState = this.filtering.filteringState
// If there are no isolated objects, all objects are visible for selection
if (!filteringState.isolatedObjects || filteringState.isolatedObjects.length === 0) {
return true
}
// If there are isolated objects, only those objects are visible for selection
return filteringState.isolatedObjects.includes(id)
}
protected isVisibleForSelectionRv(rv: NodeRenderView): boolean {
// The current filtering state
const filteringState = this.filtering.filteringState
// If there are no isolated objects, all objects are visible for selection
if (!filteringState.isolatedObjects || filteringState.isolatedObjects.length === 0) {
return true
}
// Check if this render view belongs to any of the isolated objects
for (let k = 0; k < filteringState.isolatedObjects.length; k++) {
const rvs = this.viewer
.getWorldTree()
.getRenderTree()
.getRenderViewsForNodeId(filteringState.isolatedObjects[k])
if (rvs.includes(rv)) return true
}
return false
}
protected onObjectClicked(selection: SelectionEvent | null) {
console.log('🎯 FilteredSelectionExtension.onObjectClicked called with:', selection)
if (!selection) {
console.log('🎯 No selection, calling super with null')
super.onObjectClicked(selection)
return
}
const filteredHits = []
const filteredSelection = selection
? {
event: selection.event,
hits: filteredHits,
multiple: selection.multiple
}
: null
if (filteredSelection) {
for (const hit of selection.hits) {
console.log('🎯 Checking hit:', hit.node.model.id, 'isVisible:', this.isVisibleForSelection(hit.node.model.id))
if (this.isVisibleForSelection(hit.node.model.id)) {
filteredHits.push(hit)
}
}
}
console.log('🎯 Filtered hits:', filteredHits.length)
// Call base class with the filtered selection
if (filteredSelection && filteredSelection.hits.length) {
super.onObjectClicked(filteredSelection)
this.emit(FilteredSelectionEvent.FilteredObjectClicked, filteredSelection)
} else {
// If no valid hits, treat as empty selection
super.onObjectClicked(null)
}
}
protected onPointerMove(e: Vector2 & { event: Event }) {
if (!this._enabled) return
const camera = this.viewer.getRenderer().renderingCamera
if (!camera) return
if (!this.options.hoverMaterialData) return
const result =
(this.viewer
.getRenderer()
.intersections.intersect(
this.viewer.getRenderer().scene,
camera,
e,
[
ObjectLayers.STREAM_CONTENT_MESH,
ObjectLayers.STREAM_CONTENT_POINT,
ObjectLayers.STREAM_CONTENT_LINE,
ObjectLayers.STREAM_CONTENT_TEXT
],
true,
this.viewer.getRenderer().clippingVolume
) as ExtendedIntersection[]) || []
let rv = null
for (let k = 0; k < result.length; k++) {
rv = this.viewer.getRenderer().renderViewFromIntersection(result[k])
if (this.isVisibleForSelection(rv)) break
else rv = null
}
this.applyHover(rv)
}
}
@@ -36,7 +36,7 @@ export default class TooltipHandler {
tooltip: tooltipData
}
this.tooltipService.show(tooltipData)
// this.tooltipService.show(tooltipData)
if (Object.keys(tooltipData.dataItems).length > 0) this.tooltipService.show(tooltipData)
}
@@ -1,209 +0,0 @@
import {
CanonicalView,
FilteringState,
LegacyViewer,
IntersectionQuery,
DefaultViewerParams,
SpeckleView,
CameraController,
CameraEvent,
SpeckleOfflineLoader
} from '@speckle/viewer'
import { pickViewableHit, projectToScreen } from '../utils/viewerUtils'
import _ from 'lodash'
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
import { PerspectiveCamera, OrthographicCamera, Box3 } from 'three'
/**
* @deprecated
*/
export default class ViewerHandler {
private viewer: LegacyViewer
private readonly parent: HTMLElement
private state: FilteringState
private loadedObjectsCache: Set<string> = new Set<string>()
private config = {
authToken: null,
batchSize: 25
}
private currentSectionBox: Box3 = null
private currentSettings: SpeckleVisualSettingsModel
public getViews() {
return this.viewer.getViews()
}
public updateSettings(settings: SpeckleVisualSettingsModel) {
// Camera settings
switch (settings.camera.projection.value) {
case 'perspective':
this.viewer.setPerspectiveCameraOn()
break
case 'orthographic':
this.viewer.setOrthoCameraOn()
break
}
var camController = this.viewer.getExtension(CameraController)
var angle = settings.camera.allowCameraUnder.value ? Math.PI : Math.PI / 2
camController.options = { maximumPolarAngle: angle }
// Lighting settings
const newConfig = settings.lighting.getViewerConfiguration()
this.viewer.setLightConfiguration(newConfig)
this.currentSettings = settings
}
public setView(view: SpeckleView | CanonicalView) {
this.viewer.setView(view)
}
public setSectionBox(active: boolean, objectIds: string[]) {
if (active) {
if (this.currentSectionBox === null) {
const bbox = this.viewer.getSectionBoxFromObjects(objectIds)
this.viewer.setSectionBox(bbox)
this.currentSectionBox = bbox as unknown as Box3
} else {
const bbox = this.viewer.getCurrentSectionBox()
if (bbox) this.currentSectionBox = bbox as unknown as Box3
}
this.viewer.sectionBoxOn()
} else {
this.viewer.sectionBoxOff()
}
this.viewer.requestRender()
}
public addCameraUpdateEventListener(listener: (ev) => void) {
this.viewer.getExtension(CameraController).on(CameraEvent.LateFrameUpdate, listener)
}
public constructor(parent: HTMLElement) {
this.parent = parent
}
public async init() {
if (this.viewer) return
const viewerSettings = DefaultViewerParams
viewerSettings.showStats = false
viewerSettings.verbose = false
const viewer = new LegacyViewer(this.parent, viewerSettings)
await viewer.init()
console.log('Viewer initialized', viewer)
this.viewer = viewer
}
public async unloadObjects(
objects: string[],
signal?: AbortSignal,
onObjectUnloaded?: (url: string) => void
) {
for (const url of objects) {
if (signal?.aborted) return
await this.viewer
.cancelLoad(url, true)
.catch((e) => console.warn('Viewer Unload error', url, e))
.finally(() => {
if (this.loadedObjectsCache.has(url)) this.loadedObjectsCache.delete(url)
if (onObjectUnloaded) onObjectUnloaded(url)
})
}
}
public async loadObjectsWithAutoUnload(
objects: object[],
onLoad: (url: string, index: number) => void,
onError: (url: string, error: Error) => void,
signal: AbortSignal
) {
// var objectsToUnload = _.difference([...this.loadedObjectsCache], rootObject)
// await this.unloadObjects(objectsToUnload, signal)
// await this.loadObjects(obj, onLoad, onError) // TODO: pass root object
await this.loadObjects(objects, onLoad, onError)
}
public async loadObjects(
objects: object[],
onLoad: (url: string, index: number) => void,
onError: (url: string, error: Error) => void
) {
const stringifiedObject = JSON.stringify(objects)
const loader = new SpeckleOfflineLoader(this.viewer.getWorldTree(), stringifiedObject)
void this.viewer.unloadAll()
void this.viewer.loadObject(loader, true)
}
public async intersect(coords: { x: number; y: number }) {
const point = this.viewer.Utils.screenToNDC(
coords.x,
coords.y,
this.parent.clientWidth,
this.parent.clientHeight
)
const intQuery: IntersectionQuery = {
operation: 'Pick',
point
}
const res = this.viewer.query(intQuery)
if (!res) return null
return {
hit: pickViewableHit(res.objects, this.state),
objects: res.objects
}
}
public zoom(objectIds?: string[]) {
this.viewer.zoom(objectIds)
}
public zoomExtents() {
this.viewer.zoom()
}
public async unIsolateObjects() {
if (this.state.isolatedObjects)
this.state = await this.viewer.unIsolateObjects(this.state.isolatedObjects, 'powerbi', true)
}
public async isolateObjects(objectIds, ghost = false) {
this.state = await this.viewer.isolateObjects(objectIds, 'powerbi', true, ghost)
}
public async colorObjectsByGroup(
groups?: {
objectIds: string[]
color: string
}[]
) {
this.state = await this.viewer.setUserObjectColors(groups ?? [])
}
public async clear() {
if (this.viewer) await this.viewer.unloadAll()
this.loadedObjectsCache.clear()
}
public async selectObjects(objectIds: string[] = null) {
if (!this.viewer) return
await this.viewer.resetHighlight()
const objIds = objectIds ?? []
this.state = await this.viewer.selectObjects(objIds)
}
public getScreenPosition(worldPosition): { x: number; y: number } {
return projectToScreen(
this.viewer.getExtension(CameraController).renderingCamera as unknown as
| PerspectiveCamera
| OrthographicCamera,
worldPosition
)
}
public dispose() {
this.viewer.getExtension(CameraController).dispose()
this.viewer.dispose()
this.viewer = null
}
}
@@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ConcreteComponent, FunctionalComponent, DefineComponent } from 'vue'
export type PropAnyComponent =
| ConcreteComponent<any, any, any, any, any>
| FunctionalComponent<any, any, any>
| DefineComponent
| string
export type HorizontalOrVertical = 'horizontal' | 'vertical'
export interface StepCoreType {
name: string
href?: string
onClick?: () => void
}
export type BulletStepType = StepCoreType
export interface NumberStepType extends BulletStepType {
description?: string
}
export type AlertColor = 'success' | 'danger' | 'warning' | 'info' | 'neutral'
export type AlertAction = {
title: string
url?: string
onClick?: () => void
externalUrl?: boolean
disabled?: boolean
}
@@ -0,0 +1,2 @@
export type FormButtonStyle = 'primary' | 'outline' | 'subtle' | 'danger'
export type FormButtonSize = 'sm' | 'base' | 'lg'
@@ -0,0 +1,152 @@
import { ViewMode } from '@speckle/viewer'
export enum ModifierKeys {
CtrlOrCmd = 'cmd-or-ctrl',
AltOrOpt = 'alt-or-opt',
Shift = 'shift'
}
export const PanelShortcuts = {
ToggleModels: {
name: 'Models',
description: 'Toggle models panel',
modifiers: [ModifierKeys.Shift],
key: 'M',
action: 'ToggleModels'
},
ToggleExplorer: {
name: 'Scene explorer',
description: 'Toggle scene explorer panel',
modifiers: [ModifierKeys.Shift],
key: 'E',
action: 'ToggleExplorer'
},
ToggleDiscussions: {
name: 'Discussions',
description: 'Toggle discussions panel',
modifiers: [ModifierKeys.Shift],
key: 'D',
action: 'ToggleDiscussions'
}
} as const
export const ToolShortcuts = {
ToggleMeasurements: {
name: 'Measure',
description: 'Toggle measurement mode',
modifiers: [ModifierKeys.Shift],
key: 'R',
action: 'ToggleMeasurements'
},
ToggleProjection: {
name: 'Projection',
description: 'Toggle between orthographic and perspective projection',
modifiers: [ModifierKeys.Shift],
key: 'P',
action: 'ToggleProjection'
},
ToggleSectionBox: {
name: 'Section',
description: 'Toggle section box',
modifiers: [ModifierKeys.Shift],
key: 'B',
action: 'ToggleSectionBox'
},
ZoomExtentsOrSelection: {
name: 'Fit',
description: 'Zoom to fit selection or entire model',
modifiers: [ModifierKeys.Shift],
key: 'space',
action: 'ZoomExtentsOrSelection'
}
} as const
export const ViewModeShortcuts = {
SetViewModeDefault: {
name: 'Rendered',
description: 'A realistic view of your model rendered with available materials for surfaces.',
modifiers: [ModifierKeys.Shift],
key: 'Digit1',
action: 'SetViewModeDefault',
viewMode: ViewMode.DEFAULT
},
SetViewModeShaded: {
name: 'Shaded',
description: 'A shaded view of your model using available colors for surfaces and curves.',
modifiers: [ModifierKeys.Shift],
key: 'Digit2',
action: 'SetViewModeShaded',
viewMode: ViewMode.SHADED
},
SetViewModeArctic: {
name: 'Arctic',
description: 'A white conceptual view of your model without any materials or colors.',
modifiers: [ModifierKeys.Shift],
key: 'Digit3',
action: 'SetViewModeArctic',
viewMode: ViewMode.ARCTIC
},
// SetViewModeSolid: {
// name: 'Solid',
// description:
// 'A basic shaded view of your model using our default material, with edges.',
// modifiers: [ModifierKeys.Shift],
// key: 'Digit4',
// action: 'SetViewModeSolid',
// viewMode: ViewMode.SOLID
// },
SetViewModePen: {
name: 'Pen',
description:
'A stylized black and white drawing view of your model, without any lighting or shadows.',
modifiers: [ModifierKeys.Shift],
key: 'Digit5',
action: 'SetViewModePen',
viewMode: ViewMode.PEN
}
} as const
export const ViewShortcuts = {
SetViewTop: {
name: 'Top',
description: 'Set view to Top',
modifiers: [ModifierKeys.AltOrOpt],
key: 'Digit1',
action: 'SetViewTop'
},
SetViewFront: {
name: 'Front',
description: 'Set view to Front',
modifiers: [ModifierKeys.AltOrOpt],
key: 'Digit2',
action: 'SetViewFront'
},
SetViewLeft: {
name: 'Left',
description: 'Set view to Left',
modifiers: [ModifierKeys.AltOrOpt],
key: 'Digit3',
action: 'SetViewLeft'
},
SetViewBack: {
name: 'Back',
description: 'Set view to Back',
modifiers: [ModifierKeys.AltOrOpt],
key: 'Digit4',
action: 'SetViewBack'
},
SetViewRight: {
name: 'Right',
description: 'Set view to Right',
modifiers: [ModifierKeys.AltOrOpt],
key: 'Digit5',
action: 'SetViewRight'
}
} as const
export const ViewerShortcuts = {
...ViewModeShortcuts,
...PanelShortcuts,
...ToolShortcuts,
...ViewShortcuts
} as const
@@ -0,0 +1,17 @@
import type { ViewMode } from '@speckle/viewer'
import type { ModifierKeys, ViewerShortcuts } from './shortcuts'
export type BaseShortcut = {
name: string
description: string
modifiers: readonly ModifierKeys[]
key: string
action: string
}
export type ViewModeShortcut = BaseShortcut & {
viewMode: ViewMode
}
export type ViewerShortcut = (typeof ViewerShortcuts)[keyof typeof ViewerShortcuts]
export type ViewerShortcutAction = keyof typeof ViewerShortcuts
@@ -0,0 +1,103 @@
import { useVisualStore } from '@src/store/visualStore'
import ObjectLoader from '@speckle/objectloader' // Default import for v1
interface SpeckleObject {
id: string
speckle_type?: string
[key: string]: any
}
export class SpeckleApiLoader {
private serverUrl: string
private token: string
private projectId: string
private headers: Record<string, string>
constructor(serverUrl: string, projectId: string, token: string) {
this.serverUrl = serverUrl.replace(/\/$/, '')
this.projectId = projectId
this.token = token
this.headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
async downloadObjectsWithChildren(objectId: string, onProgress?: (loaded: number, total: number) => void): Promise<SpeckleObject[]> {
const visualStore = useVisualStore()
visualStore.setLoadingProgress('Initializing object loader', 0)
console.log('Creating ObjectLoader v1 for Power BI environment')
// Create ObjectLoader v1 instance - use 'token' not 'authToken'
const loader = new ObjectLoader({
serverUrl: this.serverUrl,
streamId: this.projectId,
objectId: objectId,
token: this.token,
options: {
enableCaching: false, // Disable caching for Power BI environment
}
})
try {
// Get total count for progress tracking
const totalCount = await loader.getTotalObjectCount()
console.log(`Loading ${totalCount} objects using ObjectLoader v1`)
const objects: SpeckleObject[] = []
let loadedCount = 0
// Stream all objects using the async iterator
for await (const obj of loader.getObjectIterator()) {
objects.push(obj as SpeckleObject) // Type assertion since ObjectLoader v1 has different type
loadedCount++
// Update progress
if (onProgress) {
onProgress(loadedCount, totalCount)
}
const progress = totalCount > 0 ? loadedCount / totalCount : 0
visualStore.setLoadingProgress('🌍 Loading from Speckle', progress)
// Log progress every 100 objects
if (loadedCount % 100 === 0) {
console.log(`Loaded ${loadedCount}/${totalCount} objects`)
}
}
console.log(`Downloaded ${objects.length} objects using ObjectLoader v1`)
visualStore.setLoadingProgress('Download complete', 1)
return objects
} catch (error) {
console.error('Error loading objects:', error)
throw error
} finally {
// ObjectLoader v1 cleanup
if (loader.dispose) {
loader.dispose()
}
}
}
async downloadFromVersionId(versionId: string): Promise<SpeckleObject[]> {
// For version IDs, we can't avoid GraphQL entirely as we need to resolve the referenced object
// However, this method might not be used if we're getting object IDs directly from the data connector
throw new Error('Version ID downloads not supported with weak tokens. Use object IDs directly.')
}
async downloadMultipleModels(objectIds: string[]): Promise<SpeckleObject[][]> {
const allObjects: SpeckleObject[][] = []
for (const objectId of objectIds) {
const objects = await this.downloadObjectsWithChildren(objectId)
allObjects.push(objects)
}
return allObjects
}
}
+181 -112
View File
@@ -1,19 +1,25 @@
import {
LegacyViewer,
DefaultViewerParams,
FilteringState,
IntersectionQuery,
CameraController,
CanonicalView,
ViewModes,
CameraEvent,
SpeckleView
SpeckleView,
ViewMode,
Viewer,
HybridCameraController,
SelectionExtension,
FilteringExtension,
UpdateFlags,
ViewerEvent,
SelectionEvent
} from '@speckle/viewer'
import { FilteredSelectionExtension, FilteredSelectionEvent } from '@src/extensions/FilteredSelectionExtension'
import { SpeckleObjectsOfflineLoader } from '@src/laoder/SpeckleObjectsOfflineLoader'
import { useVisualStore } from '@src/store/visualStore'
import { Tracker } from '@src/utils/mixpanel'
import { createNanoEvents, Emitter } from 'nanoevents'
import { ColorPicker } from 'powerbi-visuals-utils-formattingmodel/lib/FormattingSettingsComponents'
import { Vector3 } from 'three'
export interface IViewer {
@@ -23,15 +29,6 @@ export interface IViewer {
on: <E extends keyof IViewerEvents>(event: E, callback: IViewerEvents[E]) => void
}
export declare enum ViewMode {
DEFAULT = 0,
DEFAULT_EDGES = 1,
SHADED = 2,
PEN = 3,
ARCTIC = 4,
COLORS = 5
}
export interface Hit {
guid: string
object?: Record<string, unknown>
@@ -41,32 +38,44 @@ export interface Hit {
export interface IViewerEvents {
ping: (message: string) => void
setSelection: (objectIds: string[]) => void
resetFilter: (objectIds: string[], ghost: boolean, zoom: boolean) => void
filterSelection: (objectIds: string[], ghost: boolean, zoom: boolean) => void
setViewMode: (viewMode: ViewMode) => void
colorObjectsByGroup: (
colorById: {
objectIds: string[]
slice: ColorPicker
color: string
}[]
) => void
isolateObjects: (objectIds: string[]) => void
forceViewerUpdate: () => void
unIsolateObjects: () => void
zoomExtends: () => void
toggleProjection: () => void
toggleGhostHidden: (ghost: boolean) => void
loadObjects: (objects: object[]) => void
objectsLoaded: () => void
objectClicked: (hit: Hit | null, isMultiSelect: boolean, mouseEvent?: PointerEvent) => void
}
export type ColorBy = {
objectIds: string[]
color: string
}
export class ViewerHandler {
public emitter: Emitter
public viewer: LegacyViewer
private _needsRender = false
private parent: HTMLElement
public viewer: Viewer
public cameraControls: CameraController
public filtering: FilteringExtension
public selection: FilteredSelectionExtension
private filteringState: FilteringState
constructor() {
this.emitter = createNanoEvents()
this.emit = this.emit.bind(this)
this.emitter.on('ping', this.handlePing)
this.emitter.on('filterSelection', this.filterSelection)
this.emitter.on('resetFilter', this.resetFilter)
this.emitter.on('setSelection', this.selectObjects)
this.emitter.on('setViewMode', this.setViewMode)
this.emitter.on('colorObjectsByGroup', this.colorObjectsByGroup)
@@ -75,53 +84,54 @@ export class ViewerHandler {
this.emitter.on('zoomExtends', this.zoomExtends)
this.emitter.on('zoomObjects', this.zoomObjects)
this.emitter.on('loadObjects', this.loadObjects)
this.emitter.on('objectsLoaded', this.handleObjectsLoaded)
this.emitter.on('toggleProjection', this.toggleProjection)
this.emitter.on('toggleGhostHidden', this.toggleGhostHidden)
}
async init(parent: HTMLElement) {
this.viewer = await createViewer(parent)
this.parent = parent
this.viewer.speckleRenderer.speckleCamera.on(
CameraEvent.FrameUpdate,
(needsUpdate: boolean) => {
this.needsRender = needsUpdate
}
)
}
this.cameraControls = this.viewer.getExtension(CameraController)
this.filtering = this.viewer.getExtension(FilteringExtension)
this.selection = this.viewer.getExtension(FilteredSelectionExtension)
get needsRender(): boolean {
return this._needsRender
}
set needsRender(value: boolean) {
if (this._needsRender !== value) {
this._needsRender = value
this.onNeedsRenderChanged(value)
const store = useVisualStore()
if (store.isOrthoProjection) {
this.cameraControls.toggleCameras()
}
}
private onNeedsRenderChanged(value: boolean) {
// whenever the render is settled means that user stopped interaction, so we will set the camera position
if (!value) {
console.log('🎬 Storing the camera position into file')
const cameraController = this.viewer.getExtension(CameraController)
const position = cameraController.getPosition()
const target = cameraController.getTarget()
const store = useVisualStore()
store.writeCameraPositionToFile(position, target)
}
this.viewer.on(ViewerEvent.LoadComplete, (arg: string) => {
store.clearLoadingProgress()
})
// Set up event listener for viewer's built-in object clicked events
this.viewer.on(ViewerEvent.ObjectClicked, (selection: SelectionEvent | null) => {
console.log('🎯 Viewer ObjectClicked event received:', selection)
})
// Set up event listener for filtered selection events
this.selection.on(FilteredSelectionEvent.FilteredObjectClicked, this.handleFilteredSelection)
}
emit<E extends keyof IViewerEvents>(event: E, ...payload: Parameters<IViewerEvents[E]>): void {
this.emitter.emit(event, ...payload)
}
public zoomObjects = (objectIds: string[]) => {
this.viewer.zoom(objectIds)
public zoomObjects = (objectIds: string[], animate = false) => {
/** Second argument here is for animating the camera movement. Default is false */
this.cameraControls.setCameraView(objectIds, animate)
}
public zoomExtends = () => this.viewer.zoom()
public zoomExtends = () => {
this.cameraControls.setCameraView(undefined, true)
this.viewer.requestRender(UpdateFlags.RENDER_RESET)
}
public toggleProjection = () => this.cameraControls.toggleCameras()
public setView = (view: CanonicalView) => this.viewer.setView(view)
public setView = (view: CanonicalView) => {
this.cameraControls.setCameraView(view, false)
this.snapshotCameraPositionAndStore()
}
public setSectionBox = (bboxActive: boolean, objectIds: string[]) => {
// TODO
@@ -133,30 +143,64 @@ export class ViewerHandler {
viewModes.setViewMode(viewMode)
}
public selectObjects = async (objectIds: string[]) => {
public snapshotCameraPositionAndStore = () => {
console.log('🎬 Storing the camera position into file')
const cameraController = this.viewer.getExtension(CameraController)
const position = cameraController.getPosition()
const target = cameraController.getTarget()
const store = useVisualStore()
store.writeCameraPositionToFile(position, target)
}
public selectObjects = (objectIds: string[]) => {
console.log('🔗 Handling setSelection inside ViewerHandler:', objectIds)
if (objectIds) {
await this.viewer.selectObjects(objectIds)
this.selection.selectObjects(objectIds)
}
}
public colorObjectsByGroup = async (
colorByIds: {
objectIds: string[]
color: string
}[]
) => {
this.filteringState = await this.viewer.setUserObjectColors(colorByIds ?? [])
public filterSelection = (objectIds: string[], ghost: boolean, zoom: boolean = true) => {
console.log('🔗 Handling filterSelection inside ViewerHandler')
if (objectIds) {
this.unIsolateObjects()
this.filteringState = this.filtering.isolateObjects(objectIds, 'powerbi', true, ghost)
if (zoom) {
this.zoomObjects(objectIds, true)
}
}
}
public isolateObjects = async (objectIds: string[], ghost: boolean) => {
await this.unIsolateObjects()
this.filteringState = await this.viewer.isolateObjects(objectIds, 'powerbi', true, ghost)
public resetFilter = (objectIds: string[], ghost: boolean, zoom: boolean = true) => {
console.log('🔗 Handling filterSelection inside ViewerHandler')
if (objectIds) {
this.isolateObjects(objectIds, ghost)
if (zoom) {
this.zoomObjects(objectIds, true)
}
}
}
public unIsolateObjects = async () => {
public colorObjectsByGroup = (colorByIds: ColorBy[]) => {
this.filteringState = this.filtering.setUserObjectColors(colorByIds ?? [])
}
public isolateObjects = (objectIds: string[], ghost: boolean) => {
this.unIsolateObjects()
this.filteringState = this.filtering.isolateObjects(objectIds, 'powerbi', true, ghost)
}
public toggleGhostHidden = (ghost: boolean) => {
this.filteringState = this.filtering.isolateObjects(
this.filteringState.isolatedObjects,
'powerbi',
true,
ghost
)
}
public unIsolateObjects = () => {
if (this.filteringState && this.filteringState.isolatedObjects) {
this.filteringState = await this.viewer.unIsolateObjects(
this.filteringState = this.filtering.unIsolateObjects(
this.filteringState.isolatedObjects,
'powerbi',
true
@@ -164,50 +208,41 @@ export class ViewerHandler {
}
}
public intersect = (coords: { x: number; y: number }) => {
const point = this.viewer.Utils.screenToNDC(
coords.x,
coords.y,
this.parent.clientWidth,
this.parent.clientHeight
)
const intQuery: IntersectionQuery = {
operation: 'Pick',
point
}
const res = this.viewer.query(intQuery)
// console.log(res, 'pick objects')
if (!res) {
this.viewer.selectObjects([])
return
}
return {
hit: this.pickViewableHit(res.objects),
objects: res.objects
}
}
public loadObjects = async (objects: object[]) => {
public loadObjects = async (modelObjects: object[][]) => {
await this.viewer.unloadAll()
// const stringifiedObject = JSON.stringify(objects)
//@ts-ignore
const loader = new SpeckleObjectsOfflineLoader(this.viewer.getWorldTree(), objects)
const store = useVisualStore()
const speckleViews = objects.filter(
const store = useVisualStore()
const speckleViews = []
// Use for...of loop to properly handle async operations
for (const objects of modelObjects) {
//@ts-ignore
(o) => o.speckle_type === 'Objects.BuiltElements.View:Objects.BuiltElements.View3D'
) as SpeckleView[]
const loader = new SpeckleObjectsOfflineLoader(this.viewer.getWorldTree(), objects)
const speckleViewsInModel = objects.filter(
//@ts-ignore
(o) => o.speckle_type === 'Objects.BuiltElements.View:Objects.BuiltElements.View3D'
) as SpeckleView[]
speckleViews.concat(speckleViewsInModel)
// Since you are setting another camera position, maybe you want the second argument to false
await this.viewer.loadObject(loader, true)
this.viewer.getRenderer().shadowcatcher.shadowcatcherMesh.visible = false // works fine only right after loadObjects
}
store.setSpeckleViews(speckleViews)
if (store.defaultViewModeInFile) {
this.setViewMode(Number(store.defaultViewModeInFile))
}
await this.viewer.loadObject(loader, true)
Tracker.dataLoaded({ sourceHostApp: store.receiveInfo.sourceApplication })
// camera need to be set after objects loaded
Tracker.dataLoaded({
sourceHostApp: store.receiveInfo.sourceApplication,
workspace_id: store.receiveInfo.workspaceId,
core_version: store.receiveInfo.version
})
if (store.cameraPosition) {
const position = new Vector3(
store.cameraPosition[0],
@@ -219,28 +254,53 @@ export class ViewerHandler {
store.cameraPosition[4],
store.cameraPosition[5]
)
const cameraController = this.viewer.getExtension(CameraController)
cameraController.setCameraView({ position, target }, true)
this.cameraControls.setCameraView({ position, target }, true)
}
// Emit objects loaded event to trigger update
this.emit('objectsLoaded')
}
private handlePing = (message: string) => {
console.log(message)
}
private pickViewableHit(hits: Hit[]): Hit | null {
// let hit = null
// if (this.filteringState.isolatedObjects) {
// // Find the first hit contained in the isolated objects
// hit = hits.find((hit) => {
// const hitId = hit.object.id as string
// return this.filteringState.isolatedObjects.includes(hitId)
// })
// }
const hit = hits.find((h) => this.filteringState.isolatedObjects.includes(h.guid))
return hit
private handleObjectsLoaded = () => {
console.log('🎯 Objects loaded - triggering update')
const store = useVisualStore()
// Handle state restoration after objects are loaded
store.handleObjectsLoadedComplete()
}
private handleFilteredSelection = (selection: SelectionEvent | null) => {
console.log('🎯 Filtered selection event received:', selection)
let hit: Hit | null = null
let isMultiSelect = false
let mouseEvent: PointerEvent | undefined = undefined
if (selection && selection.hits.length > 0) {
// Convert the first hit to the Hit format expected by ViewerWrapper
const firstHit = selection.hits[0]
hit = {
guid: firstHit.node.model.id,
object: firstHit.node.model.raw,
point: {
x: firstHit.point.x,
y: firstHit.point.y,
z: firstHit.point.z
}
}
isMultiSelect = selection.multiple
mouseEvent = selection.event
}
// Emit the objectClicked event for ViewerWrapper to handle
this.emit('objectClicked', hit, isMultiSelect, mouseEvent)
}
public dispose() {
this.viewer.getExtension(CameraController).dispose()
this.viewer.dispose()
@@ -248,12 +308,21 @@ export class ViewerHandler {
}
}
const createViewer = async (parent: HTMLElement): Promise<LegacyViewer> => {
const createViewer = async (parent: HTMLElement): Promise<Viewer> => {
const viewerSettings = DefaultViewerParams
viewerSettings.showStats = false
viewerSettings.verbose = false
const viewer = new LegacyViewer(parent, viewerSettings)
viewerSettings.verbose = true // Turning this on so we can see logs for now
const viewer = new Viewer(parent, viewerSettings)
await viewer.init()
viewer.createExtension(HybridCameraController) // camera controller
viewer.createExtension(FilteringExtension) // filtering - must be created before FilteredSelectionExtension
viewer.createExtension(FilteredSelectionExtension) // filtered selection helper - depends on FilteringExtension
// viewer.createExtension(SectionTool) // section tool, possibly not needed for now?
// viewer.createExtension(SectionOutlines) // section tool, possibly not needed for now?
// viewer.createExtension(MeasurementsExtension) // measurements, possibly not needed for now?
viewer.createExtension(ViewModes) // view modes
console.log('🎥 Viewer is created!')
return viewer
}
@@ -40,7 +40,7 @@ export class ColorSettings extends fs.SimpleCard {
name = 'color'
displayName = 'Object Display'
slices: fs.Slice[] = [this.context, this.fill]
slices: fs.Slice[] = [this.fill]
}
export class ColorSelectorSettings extends fs.SimpleCard {

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