Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5b40c8a5c | |||
| 098ef3d112 | |||
| 94fdc7a2c3 | |||
| 525857bd26 | |||
| 959bcaa671 | |||
| 04b3aef829 | |||
| 318dc6dbbe | |||
| 20577a1fdb | |||
| e74bad829e | |||
| dda04e49c2 | |||
| 97983fb8aa | |||
| 1cac02ae61 | |||
| 0a5001987e | |||
| 5ffb3ea1dd | |||
| 3461c48b11 | |||
| 220946a611 | |||
| 53e4cda456 | |||
| 4ca0ae0978 | |||
| 685a137531 | |||
| 78af91f38a | |||
| 108a406bd5 | |||
| d7ede2edcf | |||
| a25d635ca1 | |||
| 5a9add6d76 | |||
| 89c8005dee | |||
| a384370652 | |||
| 5ec90095f0 | |||
| 20fad26fef | |||
| 03215f79c4 | |||
| 6d17377ca2 | |||
| 256abaed0c | |||
| 26409b4ea6 | |||
| 865c4c1608 | |||
| 67836c2a7f | |||
| 95d819f7f3 | |||
| dee3ee6c4d | |||
| 7ed612ec14 | |||
| 4bd7af4c31 | |||
| 3ed2e977df | |||
| 788fa1c532 | |||
| bafb7df6ed | |||
| be4e4df983 | |||
| b4830c80ab | |||
| a2d97facc5 | |||
| aea344a46a | |||
| 13aa65bc2e | |||
| 0a307c28e0 | |||
| e0f4a4c02c | |||
| 29773f9492 | |||
| 8f67ef4c84 | |||
| 2c5f192403 | |||
| 0c58789dd6 | |||
| 82acce2abb | |||
| d83472c30b | |||
| 634df47a25 | |||
| 9ad59bf1d3 | |||
| ffc0d8ef5e | |||
| 94c80857a0 | |||
| c8d858d575 | |||
| 36b9787b66 | |||
| bde7a42c44 | |||
| 1040f4622d | |||
| 91e799d006 | |||
| 8694666874 | |||
| fa6ad8ec40 | |||
| 8e60249291 | |||
| 68d6bf3d55 | |||
| 2a8925c8ef | |||
| f9b3d3db52 | |||
| bfd0c33373 | |||
| d155a4b165 | |||
| 2e9ece856f | |||
| 808e288848 | |||
| 701116c66c | |||
| e73d392013 | |||
| dd7f3fe95d | |||
| fdcc1f2cef | |||
| 5e2f108e49 | |||
| df334e95a2 | |||
| ce733d1ced | |||
| 8345258990 | |||
| dbd0f2f9ce | |||
| c9b4155660 | |||
| c4d094d722 | |||
| 5b003b182b | |||
| 7eabd47f6d | |||
| 822b999be9 | |||
| 2407b8758a | |||
| d6f5e65bd7 | |||
| 183cc36654 | |||
| a740272585 | |||
| 72128a9f4e | |||
| 677c663ef3 | |||
| a077857c66 | |||
| 5897a286bc | |||
| 5b49fb2a9a | |||
| 424404dd11 | |||
| 31312522a7 | |||
| 932198dccf | |||
| 3770502ca4 | |||
| 93e8fcdd9d | |||
| 370052b2be | |||
| aa4a137a0d | |||
| 4acdf30734 | |||
| b531446acd | |||
| de1b2ca39c | |||
| 5642da1e57 | |||
| 276d0c3a76 | |||
| 2d92b85687 | |||
| 82bd109b85 | |||
| 38fb5f7c26 | |||
| dee021f0ef | |||
| 620bd22387 | |||
| d364e096a8 | |||
| 8e03bcf201 | |||
| 1825a5ad4c | |||
| a626cdd45d | |||
| 91871f8615 | |||
| 2da1602986 | |||
| a0372e1970 | |||
| 98f10bb344 | |||
| ee11e47af3 | |||
| f57697d929 | |||
| ac0db18d24 | |||
| 85f8f72335 | |||
| 24d26dc49f | |||
| 1fff59de85 | |||
| 771e34f6b5 | |||
| f0b072d210 | |||
| 97a72c966a | |||
| 058c846770 | |||
| 3bb69f7aa8 | |||
| c1dac7e663 | |||
| 056815dad8 | |||
| 929f06dbb7 | |||
| 8cdfa97f41 | |||
| 28e9c43e5e | |||
| 380c60281b | |||
| c4f7c6082e | |||
| 51a5261f1d | |||
| 5f6a032bf8 | |||
| 8e1fe78af3 | |||
| 6dffa7dfa3 | |||
| f61fae4284 | |||
| f97946a426 | |||
| 8949917f21 | |||
| 0a0abd70df | |||
| 19c9252a4a |
@@ -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
@@ -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"
|
||||
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
|
||||
@@ -48,39 +64,38 @@ jobs:
|
||||
with:
|
||||
name: powerbi-connector
|
||||
path: src/powerbi-data-connector/bin/Speckle.pqx
|
||||
retention-days: 5
|
||||
|
||||
- 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
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
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
|
||||
@@ -88,6 +103,8 @@ jobs:
|
||||
with:
|
||||
name: powerbi-visual
|
||||
path: src/powerbi-visual/dist/*.pbiviz
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
deploy-installers:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -107,11 +124,6 @@ jobs:
|
||||
with:
|
||||
name: powerbi-visual
|
||||
path: artifacts/
|
||||
- name: pull powerbi-service
|
||||
run: |
|
||||
curl --location --output powerbi-service.zip https://releases.speckle.dev/services/powerbi-service/powerbi-service-0.7.0.zip # bump this version as needed
|
||||
unzip powerbi-service.zip -d artifacts
|
||||
|
||||
- name: Zip artifacts
|
||||
run: |
|
||||
cd artifacts && zip -r ../powerbi.zip .
|
||||
@@ -120,16 +132,28 @@ jobs:
|
||||
with:
|
||||
name: output-${{needs.build-connector.outputs.semver}}
|
||||
path: powerbi.zip
|
||||
- name: 🔫 Trigger Build Installers
|
||||
uses: ALEEF02/workflow-dispatch@v3.0.0
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- 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-*
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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&style=flat-square&logo=discourse&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&logo=read-the-docs&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&style=flat-square&logo=discourse&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&logo=read-the-docs&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>
|
||||
|
||||
Speckle’s connection to Power BI consists of two parts:
|
||||
|
||||
- **Data Connector** fetches the data you uploaded from AEC apps to Speckle.
|
||||
@@ -22,19 +30,19 @@ Speckle’s connection to Power BI consists of two parts:
|
||||
|
||||

|
||||
|
||||
# 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 
|
||||
|
||||
@@ -91,12 +127,10 @@ What is Speckle? Check our ](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||
- [](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||
|
||||
### Resources
|
||||
|
||||
- [](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
|
||||
|
||||

|
||||
- [](https://docs.speckle.systems) reference on almost any end-user and developer functionality
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -28,6 +101,18 @@ shared Speckle.Parser = Value.ReplaceType(
|
||||
type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.Api.Fetch = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Api.Fetch.pqm"),
|
||||
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"),
|
||||
@@ -46,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"),
|
||||
@@ -62,6 +199,13 @@ shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Documentation.FieldDescription = "The URL of a model in a Speckle server project. You can copy it directly from your browser.",
|
||||
Documentation.SampleValues = {"https://app.speckle.systems/projects/7902de1f57/models/7f890a65df"}
|
||||
]
|
||||
),
|
||||
optional ExpandProperties as (
|
||||
type logical meta [
|
||||
Documentation.FieldCaption = "Expand Properties (may slow query)",
|
||||
Documentation.FieldDescription = "Expand the properties column into individual columns for easier analysis. When checked, each property from the 'properties' record column will have its own column. This can slow down the query if you have a lot of properties.",
|
||||
Documentation.AllowedValues = {true, false}
|
||||
]
|
||||
)
|
||||
) as table meta [
|
||||
Documentation.Name = "Speckle - Get Data by URL",
|
||||
@@ -75,9 +219,24 @@ shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
]
|
||||
);
|
||||
|
||||
shared Speckle.Revit.Parameters.ToNameValueRecord = (r as record, optional exclude as list) as record =>
|
||||
let
|
||||
defaultExclude = {"id", "speckle_type", "applicationId", "totalChildrenCount"},
|
||||
fullExclusion = if exclude = null then defaultExclude else List.Union(defaultExclude, exclude),
|
||||
clean = Record.RemoveFields(r, fullExclusion, MissingField.Ignore),
|
||||
recTable = Record.ToTable(clean),
|
||||
cleanTable = Table.RemoveColumns(recTable, "Name"),
|
||||
expanded = Table.ExpandRecordColumn(
|
||||
cleanTable, "Value", {"name", "value", "applicationInternalName"}, {"Name", "Value", "UID"}
|
||||
),
|
||||
joined = Table.AddColumn(expanded, "Combo", each [Name] & " [" & [UID] & "]"),
|
||||
renamed = Table.RenameColumns(joined, {{"Name", "x"}, {"Combo", "Name"}}),
|
||||
result = Record.FromTable(renamed)
|
||||
in
|
||||
result;
|
||||
|
||||
// here we register the GetByUrl function to power bi ui
|
||||
GetByUrl.Publish = [
|
||||
Beta = true,
|
||||
Cateogry = "Other",
|
||||
ButtonText = {"Connect to Speckle"},
|
||||
LearnMoreUrl = "https://speckle.guide/user/powerbi/introduction.html",
|
||||
@@ -95,7 +254,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",
|
||||
@@ -103,14 +262,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
|
||||
@@ -118,20 +284,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)
|
||||
@@ -147,7 +315,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 = [
|
||||
@@ -159,24 +328,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,8 +1,9 @@
|
||||
// use this file to write queries to test your data connector
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
(url as text) as table =>
|
||||
(url as text, optional ExpandProperties as logical) as table =>
|
||||
let
|
||||
// set default value for ExpandProperties
|
||||
shouldExpandProperties = if ExpandProperties = null then false else ExpandProperties,
|
||||
|
||||
// import required functions
|
||||
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 +27,135 @@
|
||||
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),
|
||||
// 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
|
||||
transformedData = Table.TransformColumns(
|
||||
combinedData,
|
||||
{"Version Object ID", each combinedRootIds}
|
||||
),
|
||||
|
||||
// rename column based on send status
|
||||
NewColumnName = "Version Object ID",
|
||||
Result = Table.RenameColumns(StructuredData, {{"Version Object ID", NewColumnName}})
|
||||
// expand properties column if requested and if it exists
|
||||
finalData = if shouldExpandProperties and Table.HasColumns(transformedData, {"properties"}) then
|
||||
try
|
||||
Speckle.Utils.ExpandRecord(transformedData, "properties")
|
||||
otherwise
|
||||
transformedData // fallback to original data if expansion fails
|
||||
else
|
||||
transformedData
|
||||
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",
|
||||
renamedData = Table.RenameColumns(structuredData, {{"Version Object ID", newColumnName}}),
|
||||
|
||||
// expand properties column if requested and if it exists
|
||||
result = if shouldExpandProperties and Table.HasColumns(renamedData, {"properties"}) then
|
||||
try
|
||||
Speckle.Utils.ExpandRecord(renamedData, "properties")
|
||||
otherwise
|
||||
renamedData // fallback to original data if expansion fails
|
||||
else
|
||||
renamedData
|
||||
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 - with version id if exists
|
||||
result = Table.AddColumn(
|
||||
structuredData,
|
||||
"Source Model",
|
||||
each if versionId <> null then
|
||||
Text.Combine({modelName, "-", versionId})
|
||||
else
|
||||
modelName,
|
||||
type text
|
||||
)
|
||||
in
|
||||
[
|
||||
Data = result,
|
||||
RootObjectId = rootObjectId
|
||||
]
|
||||
in
|
||||
Result
|
||||
results
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
(server as text, optional query as text, optional variables as record) as record =>
|
||||
let
|
||||
// Enhanced credential retrieval with OAuth2 support
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise null,
|
||||
|
||||
defaultQuery = "query {
|
||||
activeUser {
|
||||
email
|
||||
name
|
||||
}
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
version
|
||||
}
|
||||
}",
|
||||
|
||||
// Enhanced API call with comprehensive error handling
|
||||
Source = try Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Content = Json.FromValue([query = Text.From(query ?? defaultQuery), variables = variables])
|
||||
]
|
||||
) 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
|
||||
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
|
||||
@@ -43,6 +43,7 @@
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +58,7 @@
|
||||
version(id: $versionId) {
|
||||
id
|
||||
referencedObject
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,14 +107,16 @@
|
||||
modelId = JsonResponse[data][project][model][id],
|
||||
modelName = JsonResponse[data][project][model][name],
|
||||
versionId = JsonResponse[data][project][model][versions][items]{0}[id],
|
||||
rootObjectId = JsonResponse[data][project][model][versions][items]{0}[referencedObject]
|
||||
rootObjectId = JsonResponse[data][project][model][versions][items]{0}[referencedObject],
|
||||
sourceApplication = JsonResponse[data][project][model][versions][items]{0}[sourceApplication]
|
||||
]
|
||||
else
|
||||
[
|
||||
modelId = JsonResponse[data][project][model][id],
|
||||
modelName = JsonResponse[data][project][model][name],
|
||||
versionId = JsonResponse[data][project][model][version][id],
|
||||
rootObjectId = JsonResponse[data][project][model][version][referencedObject]
|
||||
rootObjectId = JsonResponse[data][project][model][version][referencedObject],
|
||||
sourceApplication = JsonResponse[data][project][model][version][sourceApplication]
|
||||
]
|
||||
in
|
||||
result
|
||||
@@ -4,7 +4,7 @@
|
||||
// import the required functions
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
SendToServer = Extension.LoadFunction("SendToServer.pqm"),
|
||||
|
||||
|
||||
// the logic for importing functions from other files
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
@@ -21,28 +21,27 @@
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
|
||||
|
||||
// get model info and server data
|
||||
modelInfo = GetModel(url),
|
||||
rootId = modelInfo[rootObjectId],
|
||||
|
||||
|
||||
// Get the data from SendToServer - this is already a response from the service
|
||||
JsonResponse = SendToServer(url),
|
||||
|
||||
|
||||
// convert list to table with all columns expanded
|
||||
TableFromList = Table.FromList(
|
||||
JsonResponse,
|
||||
Splitter.SplitByNothing(),
|
||||
null,
|
||||
null,
|
||||
JsonResponse,
|
||||
Splitter.SplitByNothing(),
|
||||
null,
|
||||
null,
|
||||
ExtraValues.Error
|
||||
),
|
||||
|
||||
// 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
|
||||
@@ -50,19 +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 (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(
|
||||
BasicTable,
|
||||
each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject")
|
||||
and not ShouldExcludeRow(_)
|
||||
)
|
||||
) > 0,
|
||||
|
||||
// load the Objects.Properties function only if we have DataObjects
|
||||
ObjectsProperties = if HasDataObjects then Extension.LoadFunction("Objects.Properties.pqm") else null,
|
||||
|
||||
// filter out rows where applicationId is null
|
||||
// we can discuss this further
|
||||
FilteredTable = Table.SelectRows(FinalTable, each Record.FieldOrDefault(([data]), "applicationId", null) <> 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
|
||||
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 null,
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise "",
|
||||
|
||||
query = "query {
|
||||
activeUser {
|
||||
@@ -45,7 +45,7 @@ in
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
#"Authorization" = if apiKey = "" then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400},
|
||||
Content = Json.FromValue([query = query])
|
||||
@@ -57,9 +57,10 @@ in
|
||||
error JsonResponse[errors]{0}[message]
|
||||
else
|
||||
[
|
||||
UserEmail = JsonResponse[data][activeUser][email],
|
||||
UserName = JsonResponse[data][activeUser][name],
|
||||
UserEmail = try JsonResponse[data][activeUser][email] otherwise "",
|
||||
UserName = try JsonResponse[data][activeUser][name] otherwise "",
|
||||
ServerName = JsonResponse[data][serverInfo][name],
|
||||
ServerCompany = JsonResponse[data][serverInfo][company],
|
||||
ServerVersion = JsonResponse[data][serverInfo][version]
|
||||
ServerVersion = JsonResponse[data][serverInfo][version],
|
||||
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
|
||||
]
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
// 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
|
||||
@@ -19,37 +23,146 @@
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
|
||||
// Get model info and parsed URL
|
||||
|
||||
// Get required information
|
||||
modelInfo = GetModel(url),
|
||||
parsedUrl = Parser(url),
|
||||
userInfo = GetUser(url),
|
||||
apiKey = userInfo[Token],
|
||||
userEmail = userInfo[UserEmail],
|
||||
connectorVersion = GetVersion(),
|
||||
workspaceInfo = GetWorkspace(url),
|
||||
|
||||
// Function to check if Desktop Service is available
|
||||
IsDesktopServiceAvailable = () =>
|
||||
try
|
||||
let
|
||||
PingResponse = Web.Contents(
|
||||
"http://127.0.0.1:29364/ping",
|
||||
[
|
||||
Headers = [#"Method" = "GET"],
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Timeout = #duration(0, 0, 0, 2) // 2 second timeout for ping
|
||||
]
|
||||
),
|
||||
StatusCode = Value.Metadata(PingResponse)[Response.Status]
|
||||
in
|
||||
StatusCode = 200
|
||||
otherwise
|
||||
false,
|
||||
|
||||
// Function to use Desktop Service approach (only called if available)
|
||||
UseDesktopService = () =>
|
||||
let
|
||||
// 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]
|
||||
]),
|
||||
|
||||
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],
|
||||
RootObjectId = modelInfo[rootObjectId],
|
||||
SourceApplication = modelInfo[sourceApplication],
|
||||
Token = weakToken,
|
||||
Version = connectorVersion,
|
||||
VersionId = parsedUrl[versionId],
|
||||
WorkspaceId = workspaceInfo[workspaceId],
|
||||
WorkspaceName = workspaceInfo[workspaceName],
|
||||
WorkspaceLogo = workspaceInfo[workspaceLogo],
|
||||
CanHideBranding = workspaceInfo[canHideBranding]
|
||||
]),
|
||||
|
||||
// Send request to local server
|
||||
Response = Web.Contents(
|
||||
"http://127.0.0.1:29364/download",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = requestData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500}
|
||||
]
|
||||
),
|
||||
|
||||
// Parse response
|
||||
JsonResponse = Json.Document(Response)
|
||||
in
|
||||
JsonResponse,
|
||||
|
||||
// Function to fallback to direct JSON download from Speckle server
|
||||
FallbackToDirectDownload = () =>
|
||||
let
|
||||
// Construct the direct object URL: {baseUrl}/objects/{projectId}/{rootObjectId}
|
||||
objectUrl = Text.Combine({
|
||||
parsedUrl[baseUrl],
|
||||
"/objects/",
|
||||
parsedUrl[projectId],
|
||||
"/",
|
||||
modelInfo[rootObjectId]
|
||||
}),
|
||||
|
||||
// Download JSON directly from Speckle server
|
||||
Response = Web.Contents(
|
||||
objectUrl,
|
||||
[
|
||||
Headers = [
|
||||
#"Authorization" = "Bearer " & apiKey,
|
||||
#"Accept" = "application/json"
|
||||
],
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504}
|
||||
]
|
||||
),
|
||||
|
||||
// Check response status
|
||||
StatusCode = Value.Metadata(Response)[Response.Status],
|
||||
|
||||
// Parse JSON response if successful
|
||||
JsonResponse = if StatusCode >= 200 and StatusCode < 300 then
|
||||
Json.Document(Response)
|
||||
else
|
||||
error [
|
||||
Reason = "DirectDownloadFailed",
|
||||
Message = "Failed to download model data directly from Speckle server",
|
||||
Detail = [
|
||||
StatusCode = StatusCode,
|
||||
ObjectUrl = objectUrl,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
RootObjectId = modelInfo[rootObjectId]
|
||||
]
|
||||
]
|
||||
in
|
||||
JsonResponse,
|
||||
|
||||
// Check Desktop Service availability and choose approach
|
||||
DesktopServiceAvailable = IsDesktopServiceAvailable(),
|
||||
|
||||
// Get API key if available
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
|
||||
// Prepare request data
|
||||
requestData = Json.FromValue([
|
||||
Url = url,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ObjectId = modelInfo[rootObjectId],
|
||||
token = apiKey
|
||||
]),
|
||||
|
||||
// Send request to local server
|
||||
Response = Web.Contents(
|
||||
"http://127.0.0.1:49161/download",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = requestData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500}
|
||||
]
|
||||
),
|
||||
|
||||
// Parse response
|
||||
JsonResponse = Json.Document(Response)
|
||||
FinalResult = if DesktopServiceAvailable then
|
||||
UseDesktopService()
|
||||
else
|
||||
FallbackToDirectDownload()
|
||||
|
||||
in
|
||||
JsonResponse
|
||||
FinalResult
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -72,66 +73,72 @@
|
||||
"speckleObjects": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"userInfo": {
|
||||
"receiveInfo": {
|
||||
"type": { "text": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"properties": {
|
||||
"brandingHidden": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewMode": {
|
||||
"properties": {
|
||||
"defaultViewMode": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"navbarHidden": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"camera": {
|
||||
"properties": {
|
||||
"defaultView": {
|
||||
"type": {
|
||||
"enumeration": [
|
||||
{
|
||||
"displayName": "Perspective",
|
||||
"value": "perspective"
|
||||
},
|
||||
{
|
||||
"displayName": "Top",
|
||||
"value": "top"
|
||||
},
|
||||
{
|
||||
"displayName": "Front",
|
||||
"value": "front"
|
||||
},
|
||||
{
|
||||
"displayName": "Left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"displayName": "Back",
|
||||
"value": "back"
|
||||
},
|
||||
{
|
||||
"displayName": "Right",
|
||||
"value": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
"type": { "text": true }
|
||||
},
|
||||
"allowCameraUnder": {
|
||||
"isOrtho": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"isGhost": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"zoomOnFilter": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"cameraPosition": {
|
||||
"properties": {
|
||||
"positionX": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"positionY": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"positionZ": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"targetX": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"targetY": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"targetZ": {
|
||||
"type": { "text": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"dataLoading": {
|
||||
"properties": {
|
||||
"internalizeData": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"zoomOnDataChange": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"projection": {
|
||||
"type": {
|
||||
"enumeration": [
|
||||
{
|
||||
"displayName": "Perspective",
|
||||
"value": "perspective"
|
||||
},
|
||||
{
|
||||
"displayName": "Orthographic",
|
||||
"value": "orthographic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -211,7 +218,7 @@
|
||||
{
|
||||
"essential": true,
|
||||
"name": "WebAccess",
|
||||
"parameters": ["https://analytics.speckle.systems", "http://localhost:49161", "*"]
|
||||
"parameters": ["https://analytics.speckle.systems", "http://localhost:29364", "*"]
|
||||
},
|
||||
{
|
||||
"essential": false,
|
||||
|
||||
Generated
+4254
-1959
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,10 @@
|
||||
"@babel/runtime-corejs3": "^7.21.5",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/objectloader": "^2.23.7",
|
||||
"@speckle/objectloader2": "2.26.5",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.23.7",
|
||||
"@speckle/viewer": "2.26.5",
|
||||
"color-interpolate": "^1.0.5",
|
||||
"core-js": "^3.30.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -35,7 +35,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 +50,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 +59,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 +85,6 @@
|
||||
"version": "3.0.0",
|
||||
"engines": {
|
||||
"node": "^20.17.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538"
|
||||
}
|
||||
|
||||
@@ -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,111 +1,146 @@
|
||||
<template>
|
||||
<ButtonGroup>
|
||||
<ButtonSimple flat secondary @click="onZoomExtentsClicked">
|
||||
<ArrowsPointingOutIcon class="h-5 w-5" />
|
||||
</ButtonSimple>
|
||||
<Menu as="div" class="relative z-30">
|
||||
<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-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 canonicalViews"
|
||||
: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="$emit('view-clicked', view.name.toLowerCase() 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="$emit('view-clicked', view)"
|
||||
>
|
||||
{{ view.view.name ?? view.name }}
|
||||
</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>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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 { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
import { CanonicalView, SpeckleView } 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', 'clear-palette'])
|
||||
const props = withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
|
||||
sectionBox: false,
|
||||
views: () => []
|
||||
const emits = defineEmits([
|
||||
'update:sectionBox',
|
||||
'view-clicked',
|
||||
'toggle-projection',
|
||||
'clear-palette',
|
||||
'view-mode-clicked'
|
||||
])
|
||||
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 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,33 +1,144 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div>
|
||||
<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 && visualStore.isRunningInDesktop"
|
||||
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)"
|
||||
/>
|
||||
</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 { 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()
|
||||
@@ -41,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)
|
||||
})
|
||||
|
||||
@@ -52,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>
|
||||
@@ -0,0 +1,39 @@
|
||||
<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,32 @@
|
||||
<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>
|
||||
@@ -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>
|
||||
+7
-4
@@ -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,83 @@
|
||||
<!-- 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.SHADED]: 'Shaded',
|
||||
[ViewMode.PEN]: 'Pen',
|
||||
[ViewMode.ARCTIC]: 'Arctic'
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,31 +1,116 @@
|
||||
import ObjectLoader from '@speckle/objectloader'
|
||||
import { ObjectLoader2Factory } from '@speckle/objectloader2'
|
||||
import { SpeckleLoader, WorldTree } from '@speckle/viewer'
|
||||
|
||||
// Base type from objectloader2 (has id, speckle_type properties)
|
||||
interface Base {
|
||||
id: string
|
||||
speckle_type: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export class SpeckleObjectsOfflineLoader extends SpeckleLoader {
|
||||
constructor(targetTree: WorldTree, resourceData: string, resourceId?: string) {
|
||||
super(targetTree, resourceId || '', undefined, undefined, resourceData)
|
||||
constructor(targetTree: WorldTree, resourceData: unknown, resourceId?: string) {
|
||||
// Resource ID is not used for offline loading since we have objects in memory
|
||||
// Pass empty string to avoid URL parsing issues
|
||||
super(targetTree, '', undefined, undefined, resourceData)
|
||||
}
|
||||
|
||||
protected initObjectLoader(
|
||||
_resource: string,
|
||||
_authToken?: string,
|
||||
_enableCaching?: boolean,
|
||||
resourceData?: string | ArrayBuffer
|
||||
): ObjectLoader {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return ObjectLoader.createFromObjects(resourceData as unknown as [])
|
||||
resource: string,
|
||||
authToken?: string,
|
||||
enableCaching?: boolean,
|
||||
resourceData?: unknown
|
||||
): ReturnType<SpeckleLoader['initObjectLoader']> {
|
||||
// Use ObjectLoader2Factory.createFromObjects for offline/memory-based loading
|
||||
// The objects array must contain ALL objects (root + all children)
|
||||
// The first object in the array must be the root object
|
||||
const objects = (resourceData ?? this._resourceData) as Base[]
|
||||
|
||||
if (!objects || objects.length === 0) {
|
||||
throw new Error('SpeckleObjectsOfflineLoader: No objects provided')
|
||||
}
|
||||
|
||||
// Ensure all objects have an 'id' property
|
||||
const missingIds = objects.filter((obj) => !obj.id)
|
||||
if (missingIds.length > 0) {
|
||||
console.error('Objects missing id property:', missingIds.slice(0, 5))
|
||||
throw new Error(
|
||||
`SpeckleObjectsOfflineLoader: ${missingIds.length} objects are missing 'id' property`
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Creating offline loader with ${objects.length} objects, root: ${objects[0].id}`)
|
||||
|
||||
// Create a Set of all object IDs for quick lookup
|
||||
const objectIds = new Set(objects.map((obj) => obj.id))
|
||||
|
||||
// Check for references to objects that aren't in the array
|
||||
const missingReferences = new Set<string>()
|
||||
objects.forEach((obj) => {
|
||||
// Check all properties for references (objects that look like { referencedId: "xxx" })
|
||||
Object.values(obj).forEach((value) => {
|
||||
if (value && typeof value === 'object') {
|
||||
if ('referencedId' in value && typeof value.referencedId === 'string') {
|
||||
if (!objectIds.has(value.referencedId)) {
|
||||
missingReferences.add(value.referencedId)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check arrays for references
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
if (item && typeof item === 'object' && 'referencedId' in item) {
|
||||
if (!objectIds.has(item.referencedId)) {
|
||||
missingReferences.add(item.referencedId)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (missingReferences.size > 0) {
|
||||
console.warn(
|
||||
`⚠️ Found ${missingReferences.size} missing object references:`,
|
||||
Array.from(missingReferences).slice(0, 10)
|
||||
)
|
||||
} else {
|
||||
console.log('✅ All object references are present')
|
||||
}
|
||||
|
||||
// @ts-ignore - Type compatibility issue between local objectloader2 and viewer's objectloader2
|
||||
return ObjectLoader2Factory.createFromObjects(objects)
|
||||
}
|
||||
|
||||
public async load(): Promise<boolean> {
|
||||
const rootObject = await this.loader.getRootObject()
|
||||
if (!rootObject && this._resource) {
|
||||
console.error('No root id set!')
|
||||
if (!rootObject) {
|
||||
console.error('No root object found!')
|
||||
return false
|
||||
}
|
||||
/** If not id is provided, we make one up based on the root object id */
|
||||
this._resource = this._resource || `/json/${rootObject.id as string}`
|
||||
|
||||
/** Set resource to a fake URL for logging purposes only */
|
||||
this._resource = this._resource || `/json/${rootObject.baseId as string}`
|
||||
|
||||
console.log('Loading objects from memory (offline mode)')
|
||||
|
||||
// Call parent load() which will use our ObjectLoader2 to iterate through objects
|
||||
// Since we're using MemoryDownloader, it won't actually download anything
|
||||
return super.load()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the ObjectLoader2 resources
|
||||
*/
|
||||
public async dispose(): Promise<void> {
|
||||
try {
|
||||
if (this.loader && 'disposeAsync' in this.loader) {
|
||||
// @ts-ignore - disposeAsync exists on ObjectLoader2
|
||||
await this.loader.disposeAsync()
|
||||
console.log('SpeckleObjectsOfflineLoader: ObjectLoader2 disposed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error disposing ObjectLoader2 in offline loader:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user