Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f99041654a |
@@ -0,0 +1,3 @@
|
||||
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% %*
|
||||
+166
-11
@@ -1,16 +1,171 @@
|
||||
# Use the latest 2.1 version of CircleCI pipeline process engine.
|
||||
# See: https://circleci.com/docs/2.0/configuration-reference
|
||||
version: 2.1
|
||||
|
||||
# Define the jobs we want to run for this project
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/base:2023.03
|
||||
steps:
|
||||
- run: echo "so long and thanks for all the fish"
|
||||
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
|
||||
|
||||
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 >>
|
||||
|
||||
# Orchestrate our job run sequence
|
||||
workflows:
|
||||
build_and_test:
|
||||
when: false
|
||||
build:
|
||||
jobs:
|
||||
- build
|
||||
- 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
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||
name: Build and deploy Connector and Visual
|
||||
name: build_powerbi
|
||||
on:
|
||||
push:
|
||||
branches: ["installer-test/**"]
|
||||
tags: ["v3.*.*"] # Manual delivery on every 3.x tag
|
||||
branches: ["dev"]
|
||||
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-version.outputs.file-version }}
|
||||
file-version: ${{ steps.set-info-version.outputs.file-version }}
|
||||
env:
|
||||
CertFile: "./speckle.pfx"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
fetch-depth: 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 }}"
|
||||
- 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
|
||||
|
||||
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: Determine Version
|
||||
id: gitversion
|
||||
uses: gittools/actions/gitversion/execute@v3.0.0
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
@@ -67,35 +51,37 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
run: echo "semver=${{steps.gitversion.outputs.semVer}}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
- id: set-info-version
|
||||
name: Set version to output
|
||||
run: echo "file-version=${{steps.gitversion.outputs.AssemblySemVer}}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
build-visual:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- 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 }}"
|
||||
- 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
|
||||
|
||||
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
|
||||
echo "file-version=$FILE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo $SEMVER
|
||||
echo $FILE_VERSION
|
||||
- name: Determine Version
|
||||
id: gitversion
|
||||
uses: gittools/actions/gitversion/execute@v3.0.0
|
||||
|
||||
- run: npm ci
|
||||
working-directory: src/powerbi-visual
|
||||
- run: npm version ${{steps.set-version.outputs.semver}} --allow-same-version
|
||||
- run: npm version ${{steps.gitversion.outputs.semVer}} --allow-same-version
|
||||
working-directory: src/powerbi-visual
|
||||
- run: npm run build
|
||||
working-directory: src/powerbi-visual
|
||||
@@ -134,26 +120,20 @@ jobs:
|
||||
path: powerbi.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: 🔫 Trigger Build Installer(s)
|
||||
uses: the-actions-org/workflow-dispatch@v4.0.0
|
||||
- name: 🔫 Trigger Build Installers
|
||||
uses: ALEEF02/workflow-dispatch@v3.0.0
|
||||
with:
|
||||
workflow: Build Installers
|
||||
workflow: Build PowerBI
|
||||
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 }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"is_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 }}", "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-*
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
workflow: GitFlow/v1
|
||||
next-version: 3.0.0
|
||||
mode: ManualDeployment
|
||||
branches:
|
||||
main:
|
||||
label: rc
|
||||
develop:
|
||||
regex: ^dev$
|
||||
label: beta
|
||||
unknown:
|
||||
increment: None
|
||||
@@ -3,26 +3,18 @@
|
||||
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://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>
|
||||
<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>
|
||||
|
||||
> 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 Visual for Power BI
|
||||
Speckle Connector and 3D Viewer 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.
|
||||
@@ -30,19 +22,19 @@ Speckle’s connection to Power BI consists of two parts:
|
||||
|
||||

|
||||
|
||||
## Repository Structure
|
||||
# Repo Structure
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
`src/powerbi-data-connector` contains all the code for the Data connector.
|
||||
|
||||
`src/powerbi-visual` contains all the code for 3D Visual.
|
||||
|
||||
## Installation
|
||||
# Installation
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### 3D Visual
|
||||
# Using 3D Visual
|
||||
|
||||
3D Visual can be imported as any other Power BI custom visual.
|
||||
|
||||
@@ -51,62 +43,34 @@ Power BI connector installer can be downloaded from the [connectors portal](http
|
||||
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://docs.speckle.systems/connectors/power-bi).
|
||||
For more on how to use the visual, [check our docs](https://speckle.guide/user/powerbi-visual/introduction.html).
|
||||
|
||||
## Quick Start
|
||||
# Usage
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Development Setup
|
||||
# **Developing & Debugging**
|
||||
|
||||
### For local development of the 3D Visual
|
||||
We encourage everyone interested to debug/hack/contribute/give feedback to this project.
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone https://github.com/specklesystems/speckle-powerbi.git
|
||||
cd speckle-powerbi
|
||||
```
|
||||
## **Setup**
|
||||
|
||||
2. **Navigate to the visual directory**:
|
||||
```bash
|
||||
cd src/powerbi-visual
|
||||
```
|
||||
### **Install PowerQuery SDK**
|
||||
|
||||
3. **Install dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
Follow the instructions from the [official docs](https://docs.microsoft.com/en-us/power-query/installingsdk)
|
||||
|
||||
4. **Start development server**:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
### **Build with Visual Studio**
|
||||
|
||||
5. **Build the visual**:
|
||||
```bash
|
||||
# Development build
|
||||
npm run build:dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
```
|
||||
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.
|
||||
|
||||
### For local development of the Data Connector
|
||||
### **Debug**
|
||||
|
||||
1. **Install PowerQuery SDK**:
|
||||
Follow the instructions from the [official docs](https://docs.microsoft.com/en-us/power-query/installingsdk)
|
||||
You can start the PowerQuery connector in VisualStudio, this will open a standalone connector you can use for testing purposes.
|
||||
|
||||
2. **Open the project in Visual Studio Code**:
|
||||
- Open `src/powerbi-data-connector/Speckle.proj`
|
||||
- Build the project to generate the `.mez` file
|
||||
We don't know of a way to debug the connector live in PowerBI, but we'd be happy to hear about it.
|
||||
|
||||
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 
|
||||
|
||||
@@ -127,10 +91,12 @@ 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://docs.speckle.systems) reference on almost any end-user and developer functionality
|
||||
- [](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
|
||||
|
||||

|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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,79 +4,6 @@ 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.Utf8)),
|
||||
// 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
|
||||
@@ -107,12 +34,6 @@ shared Speckle.Api.Fetch = Value.ReplaceType(
|
||||
type function (url as Uri.Type, optional query as text, optional variables as record) as record
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.CheckPermissions = Value.ReplaceType(
|
||||
Speckle.LoadFunction("CheckPermissions.pqm"),
|
||||
type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.GetUser = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetUser.pqm"),
|
||||
@@ -131,70 +52,12 @@ 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"]
|
||||
shared Speckle.Project.Issues = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Project.Issues.pqm"),
|
||||
type function (url as Uri.Type, optional getReplies as logical) as table
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle", Publish="GetByUrl.Publish"]
|
||||
shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetByUrl.pqm"),
|
||||
@@ -205,13 +68,6 @@ 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",
|
||||
@@ -260,7 +116,7 @@ GetByUrl.Icons = [
|
||||
Speckle = [
|
||||
// This is used when running the connector on an on-premises data gateway
|
||||
TestConnection = (path) => {"Speckle.GetUser", path},
|
||||
// Authentication strategy - OAuth only
|
||||
// Authentication strategy
|
||||
Authentication = [
|
||||
OAuth = [
|
||||
Label = "Speckle Account",
|
||||
@@ -268,31 +124,14 @@ Speckle = [
|
||||
let
|
||||
server = Text.Combine(
|
||||
{Uri.Parts(dataSourcePath)[Scheme], "://", Uri.Parts(dataSourcePath)[Host]}
|
||||
),
|
||||
// Generate PKCE parameters for enhanced security
|
||||
codeVerifier = GeneratePKCEVerifier(),
|
||||
codeChallenge = GeneratePKCEChallenge(codeVerifier),
|
||||
// Detect if server supports /oauth/token
|
||||
oauthCheck = try Web.Contents(
|
||||
Text.Combine({server, "oauth", "token"}, "/"),
|
||||
[ManualStatusHandling = {400, 401, 403, 404, 405, 500}]
|
||||
) otherwise null,
|
||||
useNewOAuth = oauthCheck <> null and Value.Metadata(oauthCheck)[Response.Status] = 200,
|
||||
// Build auth URL based on server capabilities
|
||||
authUrl = if useNewOAuth then
|
||||
Text.Combine({server, "authn", "verify", AuthAppId, codeChallenge}, "/") &
|
||||
"?code_challenge_method=S256" &
|
||||
"&pbiNew=true"
|
||||
else
|
||||
// Legacy
|
||||
Text.Combine({server, "authn", "verify", AuthAppId, codeVerifier}, "/")
|
||||
)
|
||||
in
|
||||
[
|
||||
LoginUri = authUrl,
|
||||
LoginUri = Text.Combine({server, "authn", "verify", AuthAppId, state}, "/"),
|
||||
CallbackUri = "https://oauth.powerbi.com/views/oauthredirect.html",
|
||||
WindowHeight = 800,
|
||||
WindowWidth = 600,
|
||||
Context = [code_verifier = codeVerifier, use_new_oauth = useNewOAuth]
|
||||
Context = null
|
||||
],
|
||||
FinishLogin = (clientApplication, dataSourcePath, context, callbackUri, state) =>
|
||||
let
|
||||
@@ -300,35 +139,22 @@ Speckle = [
|
||||
{Uri.Parts(dataSourcePath)[Scheme], "://", Uri.Parts(dataSourcePath)[Host]}
|
||||
),
|
||||
Parts = Uri.Parts(callbackUri)[Query],
|
||||
codeVerifier = if context <> null then context[code_verifier] else null,
|
||||
useNewOAuth = if context <> null and Record.HasFields(context, "use_new_oauth") then context[use_new_oauth] else false,
|
||||
// Single token exchange call based on server capability
|
||||
Source = if useNewOAuth then
|
||||
Web.Contents(
|
||||
Text.Combine({server, "oauth", "token"}, "/"),
|
||||
[
|
||||
Headers = [#"Content-Type" = "application/json"],
|
||||
Content = Json.FromValue([
|
||||
appId = AuthAppId,
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "auth", "token"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json"
|
||||
],
|
||||
Content = Json.FromValue(
|
||||
[
|
||||
accessCode = Parts[access_code],
|
||||
codeVerifier = codeVerifier
|
||||
])
|
||||
]
|
||||
)
|
||||
else
|
||||
// Legacy
|
||||
Web.Contents(
|
||||
Text.Combine({server, "auth", "token"}, "/"),
|
||||
[
|
||||
Headers = [#"Content-Type" = "application/json"],
|
||||
Content = Json.FromValue([
|
||||
appId = AuthAppId,
|
||||
appSecret = AuthAppSecret,
|
||||
accessCode = Parts[access_code],
|
||||
challenge = codeVerifier
|
||||
])
|
||||
]
|
||||
),
|
||||
challenge = state
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
json = Json.Document(Source)
|
||||
in
|
||||
[
|
||||
@@ -342,8 +168,7 @@ Speckle = [
|
||||
server = Text.Combine(
|
||||
{Uri.Parts(dataSourcePath)[Scheme], "://", Uri.Parts(dataSourcePath)[Host]}
|
||||
),
|
||||
// Enhanced refresh with error handling for gateway compatibility
|
||||
Source = try Web.Contents(
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "auth", "token"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
@@ -355,42 +180,24 @@ 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
|
||||
result
|
||||
[
|
||||
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"
|
||||
]
|
||||
],
|
||||
Label = "Speckle"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// NOTE! for tests, be make sure you put here a model that in private project to make sure all good.
|
||||
let
|
||||
result = Speckle.GetByUrl(
|
||||
"https://app.speckle.systems/projects/b61ab234b0/models/a8166255b5"
|
||||
"https://latest.speckle.systems/projects/126cd4b7bb/models/85c44d39c6"
|
||||
)
|
||||
in
|
||||
result
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
(url as text, optional ExpandProperties as logical) as table =>
|
||||
(url as text) 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"),
|
||||
ExchangeToken = Extension.LoadFunction("ExchangeToken.pqm"),
|
||||
EncodeUserInfo = Extension.LoadFunction("EncodeUserInfo.pqm"),
|
||||
GetUser = Extension.LoadFunction("GetUser.pqm"),
|
||||
GetVersion = Extension.LoadFunction("GetVersion.pqm"),
|
||||
GetWorkspace = Extension.LoadFunction("GetWorkspace.pqm"),
|
||||
MarkReceived = Extension.LoadFunction("MarkReceived.pqm"),
|
||||
|
||||
// the logic for importing functions from other files
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
@@ -33,268 +22,15 @@
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
|
||||
// parse the URL to determine if it's a federated model
|
||||
parsedUrl = Parser(url),
|
||||
// get model name
|
||||
modelInfo = GetModel(url),
|
||||
modelName = modelInfo[modelName],
|
||||
|
||||
// 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,
|
||||
|
||||
// get user info, connector version, and workspace info for encoding
|
||||
userInfo = GetUser(url),
|
||||
powerfulToken = userInfo[Token],
|
||||
userEmail = userInfo[UserEmail],
|
||||
connectorVersion = GetVersion(),
|
||||
workspaceInfo = GetWorkspace(url),
|
||||
|
||||
// exchange powerful token for weak token with limited scopes
|
||||
tokenExchangeResult = ExchangeToken(
|
||||
powerfulToken,
|
||||
{"profile:read", "streams:read", "users:read"},
|
||||
parsedUrl[projectId],
|
||||
parsedUrl[baseUrl],
|
||||
parsedUrl[resourceIdString]
|
||||
),
|
||||
|
||||
// throw error if token exchange failed - do NOT use powerful token as fallback
|
||||
tokenToUse = if tokenExchangeResult[Success] then
|
||||
tokenExchangeResult[Token]
|
||||
else
|
||||
error [
|
||||
Reason = "TokenExchangeFailed",
|
||||
Message.Format = "Failed to exchange token for limited scope token: #{0}",
|
||||
Message.Parameters = {tokenExchangeResult[ErrorMessage]},
|
||||
Detail = [
|
||||
ErrorMessage = tokenExchangeResult[ErrorMessage],
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ServerUrl = parsedUrl[baseUrl]
|
||||
]
|
||||
],
|
||||
|
||||
// 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]),
|
||||
|
||||
// extract all encoded userInfo strings
|
||||
allEncodedUserInfos = List.Transform(modelsData, each [EncodedUserInfo]),
|
||||
|
||||
// combine all root object IDs into a comma-separated string
|
||||
combinedRootIds = Text.Combine(allRootIds, ","),
|
||||
|
||||
// combine all encoded userInfo strings with delimiter |||
|
||||
// (delimiter chosen to avoid conflicts with base64 characters)
|
||||
combinedEncodedUserInfos = Text.Combine(allEncodedUserInfos, "|||"),
|
||||
|
||||
// combine all data tables
|
||||
combinedData = Table.Combine(allTables),
|
||||
|
||||
// replace both columns with combined values
|
||||
transformedData = Table.TransformColumns(
|
||||
combinedData,
|
||||
{
|
||||
{"Version Object ID", each combinedRootIds},
|
||||
{"Model Info", each combinedEncodedUserInfos}
|
||||
}
|
||||
),
|
||||
|
||||
// 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 info
|
||||
modelInfo = GetModel(url),
|
||||
modelName = modelInfo[modelName],
|
||||
rootObjectId = modelInfo[rootObjectId],
|
||||
sourceApplication = modelInfo[sourceApplication],
|
||||
versionId = modelInfo[versionId],
|
||||
|
||||
// mark version as received
|
||||
markReceivedResult = MarkReceived(powerfulToken, versionId, parsedUrl[projectId], parsedUrl[baseUrl]),
|
||||
|
||||
// get structured data
|
||||
structuredData = GetStructuredData(url),
|
||||
|
||||
// build userInfoData record for this model
|
||||
userInfoData = [
|
||||
rootObjectId = rootObjectId,
|
||||
server = parsedUrl[baseUrl],
|
||||
email = userEmail,
|
||||
projectId = parsedUrl[projectId],
|
||||
token = tokenToUse,
|
||||
workspaceId = workspaceInfo[workspaceId],
|
||||
workspaceName = workspaceInfo[workspaceName],
|
||||
workspaceLogo = workspaceInfo[workspaceLogo],
|
||||
version = connectorVersion,
|
||||
sourceApplication = sourceApplication,
|
||||
canHideBranding = workspaceInfo[canHideBranding],
|
||||
versionId = versionId,
|
||||
url = url
|
||||
],
|
||||
|
||||
// try to send to desktop service for backward compatibility (non-blocking)
|
||||
// must be called BEFORE encoding to ensure it executes
|
||||
desktopServiceSent = TrySendToDesktopService(userInfoData),
|
||||
|
||||
// encode userInfoData as base64 JSON string
|
||||
encodedUserInfo = EncodeUserInfo(userInfoData),
|
||||
|
||||
// replace both columns with appropriate values
|
||||
transformedData = Table.TransformColumns(
|
||||
structuredData,
|
||||
{
|
||||
{"Version Object ID", each rootObjectId},
|
||||
{"Model Info", each if (desktopServiceSent or not desktopServiceSent) and (markReceivedResult or not markReceivedResult) then encodedUserInfo else encodedUserInfo}
|
||||
}
|
||||
),
|
||||
|
||||
// expand properties column if requested and if it exists
|
||||
result = 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
|
||||
result
|
||||
else
|
||||
error Text.Format(
|
||||
"Permission denied: #{0} (Error code: #{1})",
|
||||
{permissionCheck[message], permissionCheck[code]}
|
||||
),
|
||||
|
||||
// helper function to try sending user info to desktop service for backward compatibility
|
||||
// returns true if successful, false otherwise (non-blocking)
|
||||
TrySendToDesktopService = (userInfoData as record) =>
|
||||
try
|
||||
let
|
||||
userInfoJson = Json.FromValue(userInfoData),
|
||||
response = Web.Contents(
|
||||
"http://127.0.0.1:29364/store-user-info",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = userInfoJson,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Timeout = #duration(0, 0, 0, 2)
|
||||
]
|
||||
),
|
||||
statusCode = Value.Metadata(response)[Response.Status]
|
||||
in
|
||||
statusCode >= 200 and statusCode < 300
|
||||
otherwise
|
||||
false,
|
||||
|
||||
// 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],
|
||||
sourceApplication = modelInfo[sourceApplication],
|
||||
federatedVersionId = if versionId <> null then versionId else modelInfo[versionId],
|
||||
|
||||
// mark version as received (non-blocking, best-effort)
|
||||
markReceivedResult = MarkReceived(powerfulToken, federatedVersionId, projectId, baseUrl),
|
||||
|
||||
// get structured data
|
||||
structuredData = GetStructuredData(singleModelUrl),
|
||||
|
||||
// build userInfoData record for this model
|
||||
userInfoData = [
|
||||
rootObjectId = rootObjectId,
|
||||
server = baseUrl,
|
||||
email = userEmail,
|
||||
projectId = projectId,
|
||||
token = tokenToUse,
|
||||
workspaceId = workspaceInfo[workspaceId],
|
||||
workspaceName = workspaceInfo[workspaceName],
|
||||
workspaceLogo = workspaceInfo[workspaceLogo],
|
||||
version = connectorVersion,
|
||||
sourceApplication = sourceApplication,
|
||||
canHideBranding = workspaceInfo[canHideBranding],
|
||||
versionId = if versionId <> null then versionId else modelInfo[versionId],
|
||||
url = singleModelUrl
|
||||
],
|
||||
|
||||
// try to send to desktop service for backward compatibility (non-blocking)
|
||||
// must be called BEFORE encoding to ensure it executes
|
||||
desktopServiceSent = TrySendToDesktopService(userInfoData),
|
||||
|
||||
// encode userInfoData as base64 JSON string
|
||||
encodedUserInfo = EncodeUserInfo(userInfoData),
|
||||
|
||||
// add the model name as context - with version id if exists
|
||||
// reference desktopServiceSent and markReceivedResult to force evaluation
|
||||
result = Table.AddColumn(
|
||||
structuredData,
|
||||
"Source Model",
|
||||
each if versionId <> null then
|
||||
if (markReceivedResult or not markReceivedResult) then Text.Combine({modelName, "-", versionId}) else Text.Combine({modelName, "-", versionId})
|
||||
else if (desktopServiceSent or not desktopServiceSent) and (markReceivedResult or not markReceivedResult) then
|
||||
modelName
|
||||
else
|
||||
modelName,
|
||||
type text
|
||||
)
|
||||
in
|
||||
[
|
||||
Data = result,
|
||||
RootObjectId = rootObjectId,
|
||||
EncodedUserInfo = encodedUserInfo
|
||||
]
|
||||
// rename column based on send status
|
||||
NewColumnName = "Version Object ID",
|
||||
Result = Table.RenameColumns(StructuredData, {{"Version Object ID", NewColumnName}})
|
||||
in
|
||||
results
|
||||
Result
|
||||
@@ -1,8 +1,6 @@
|
||||
(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,
|
||||
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
defaultQuery = "query {
|
||||
activeUser {
|
||||
email
|
||||
@@ -14,9 +12,7 @@
|
||||
version
|
||||
}
|
||||
}",
|
||||
|
||||
// Enhanced API call with comprehensive error handling
|
||||
Source = try Web.Contents(
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
@@ -24,56 +20,14 @@
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
ManualStatusHandling = {400},
|
||||
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]
|
||||
),
|
||||
#"JSON" = Json.Document(Source)
|
||||
in
|
||||
result
|
||||
// Check if response contains errors, if so, return first error.
|
||||
if Record.HasFields(#"JSON", {"errors"}) then
|
||||
error #"JSON"[errors]{0}[message]
|
||||
else
|
||||
#"JSON"[data]
|
||||
@@ -1,66 +0,0 @@
|
||||
(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
|
||||
@@ -1,18 +0,0 @@
|
||||
// Function to encode userInfoData as base64-encoded JSON string
|
||||
(userInfoData as record) as text =>
|
||||
let
|
||||
JsonText = Text.FromBinary(
|
||||
Json.FromValue(userInfoData),
|
||||
TextEncoding.Utf8
|
||||
),
|
||||
|
||||
// Convert JSON text to binary
|
||||
JsonBinary = Text.ToBinary(JsonText, TextEncoding.Utf8),
|
||||
|
||||
// Encode binary as base64
|
||||
Base64Encoded = Binary.ToText(JsonBinary, BinaryEncoding.Base64),
|
||||
|
||||
// Return base64-encoded string
|
||||
Result = Base64Encoded
|
||||
in
|
||||
Result
|
||||
@@ -1,135 +0,0 @@
|
||||
// Function to exchange powerful token for weak limited token
|
||||
(powerfulToken as text, scopes as list, projectId as text, serverUrl as text, optional resourceIdString as text) as record =>
|
||||
let
|
||||
// Helper function to load .pqm modules dynamically
|
||||
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]
|
||||
],
|
||||
|
||||
// Validate inputs
|
||||
ValidationError = if Text.Length(powerfulToken) = 0 then
|
||||
"PowerfulToken is required"
|
||||
else if List.Count(scopes) = 0 then
|
||||
"Scopes are required"
|
||||
else if Text.Length(projectId) = 0 then
|
||||
"ProjectId is required"
|
||||
else if Text.Length(serverUrl) = 0 then
|
||||
"ServerUrl is required"
|
||||
else
|
||||
null,
|
||||
|
||||
// Ensure serverUrl ends with /
|
||||
NormalizedServerUrl = if Text.End(serverUrl, 1) = "/" then
|
||||
serverUrl
|
||||
else
|
||||
serverUrl & "/",
|
||||
|
||||
// New Share Token API mutation with variables
|
||||
NewGraphQLQuery = "mutation CreateEmbedShareToken($input: CreateEmbedShareTokenInput!) {
|
||||
sharingMutations {
|
||||
createEmbedShareToken(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
||||
}",
|
||||
NewGraphQLVariables = [
|
||||
input = [
|
||||
projectId = projectId,
|
||||
resourceIdString = resourceIdString
|
||||
]
|
||||
],
|
||||
|
||||
// Legacy apiTokenCreate mutation with variables
|
||||
TokenLifespanMs = 10 * 365 * 24 * 3600 * 1000,
|
||||
TokenName = "Limited Power BI Visual Token - " & DateTime.ToText(DateTime.LocalNow(), "yyyy-MM-dd HH:mm"),
|
||||
LegacyGraphQLQuery = "mutation CreateApiToken($token: ApiTokenCreateInput!) {
|
||||
apiTokenCreate(token: $token)
|
||||
}",
|
||||
LegacyGraphQLVariables = [
|
||||
token = [
|
||||
name = TokenName,
|
||||
scopes = scopes,
|
||||
lifespan = TokenLifespanMs,
|
||||
limitResources = {[
|
||||
type = "project",
|
||||
id = projectId
|
||||
]}
|
||||
]
|
||||
],
|
||||
|
||||
// Helper: execute a GraphQL query with variables and extract token
|
||||
ExecuteGraphQL = (query as text, variables as record, extractToken as function) =>
|
||||
let
|
||||
Response = Web.Contents(
|
||||
NormalizedServerUrl & "graphql",
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = "Bearer " & powerfulToken
|
||||
],
|
||||
Content = Json.FromValue([
|
||||
query = query,
|
||||
variables = variables
|
||||
]),
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Timeout = #duration(0, 0, 0, 10)
|
||||
]
|
||||
),
|
||||
StatusCode = Value.Metadata(Response)[Response.Status],
|
||||
JsonResponse = if StatusCode >= 200 and StatusCode < 300 then
|
||||
Json.Document(Response)
|
||||
else
|
||||
null,
|
||||
HasErrors = JsonResponse <> null and Record.HasFields(JsonResponse, {"errors"}),
|
||||
Token = if JsonResponse <> null and not HasErrors then
|
||||
try extractToken(JsonResponse) otherwise null
|
||||
else
|
||||
null,
|
||||
ErrorMsg = if HasErrors then
|
||||
try JsonResponse[errors]{0}[message] otherwise "GraphQL mutation failed"
|
||||
else if JsonResponse = null then
|
||||
"Request failed with status " & Number.ToText(StatusCode)
|
||||
else
|
||||
null
|
||||
in
|
||||
[Success = Token <> null, Token = Token, ErrorMessage = ErrorMsg],
|
||||
|
||||
// Try new API first, fall back to legacy
|
||||
Result = if ValidationError <> null then
|
||||
[Success = false, Token = null, ErrorMessage = ValidationError]
|
||||
else
|
||||
let
|
||||
newResult = if resourceIdString <> null then
|
||||
try ExecuteGraphQL(
|
||||
NewGraphQLQuery,
|
||||
NewGraphQLVariables,
|
||||
each [data][sharingMutations][createEmbedShareToken][token]
|
||||
) otherwise [Success = false, Token = null, ErrorMessage = "New API request failed"]
|
||||
else
|
||||
[Success = false, Token = null, ErrorMessage = null],
|
||||
|
||||
finalResult = if newResult[Success] then
|
||||
newResult
|
||||
else
|
||||
try ExecuteGraphQL(
|
||||
LegacyGraphQLQuery,
|
||||
LegacyGraphQLVariables,
|
||||
each [data][apiTokenCreate]
|
||||
) otherwise [Success = false, Token = null, ErrorMessage = "Token exchange request failed"]
|
||||
in
|
||||
finalResult
|
||||
in
|
||||
Result
|
||||
@@ -29,7 +29,7 @@
|
||||
versionId = parsedUrl[versionId],
|
||||
|
||||
// get API key if available
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise null,
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
|
||||
// graphql query to get model info including root object id
|
||||
// includes specific version if provided
|
||||
|
||||
@@ -39,9 +39,8 @@
|
||||
),
|
||||
// fields to remove from data record
|
||||
FieldsToRemove = {"__closure", "totalChildrenCount", "renderMaterialProxies"},
|
||||
|
||||
// create basic table with cleaned data records (no properties column yet)
|
||||
BasicTable = Table.FromRecords(
|
||||
// create the final table with cleaned data records
|
||||
FinalTable = Table.FromRecords(
|
||||
List.Transform(
|
||||
TableFromList[Column1],
|
||||
each let
|
||||
@@ -49,70 +48,32 @@
|
||||
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,
|
||||
#"Model Info" = rootId,
|
||||
#"Application ID" = Record.FieldOrDefault(record, "applicationId", null), // Application ID
|
||||
data = cleanedRecord // Data
|
||||
data = Record.RemoveFields(record, fieldsToRemoveForThisRecord) // 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(_)
|
||||
)
|
||||
// Check if data contains any DataObject
|
||||
HasDataObject = Table.RowCount(
|
||||
Table.SelectRows(FinalTable, each
|
||||
Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject") or
|
||||
Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "Objects.Data"))
|
||||
) > 0,
|
||||
|
||||
// load the Objects.Properties function only if we have DataObjects
|
||||
ObjectsProperties = if HasDataObjects then Extension.LoadFunction("Objects.Properties.pqm") else null,
|
||||
|
||||
// Add properties column only if model has DataObjects
|
||||
FinalTable = if HasDataObjects then
|
||||
Table.AddColumn(
|
||||
BasicTable,
|
||||
"properties",
|
||||
each let
|
||||
dataRecord = [data],
|
||||
isDataObject = Text.Contains(Record.FieldOrDefault(dataRecord, "speckle_type", ""), "DataObject"),
|
||||
hasProperties = Record.HasFields(dataRecord, {"properties"}),
|
||||
extractedProperties = if hasProperties and isDataObject then
|
||||
try ObjectsProperties(dataRecord) otherwise []
|
||||
else
|
||||
[]
|
||||
in
|
||||
if Record.FieldCount(extractedProperties) > 0 then extractedProperties else null
|
||||
)
|
||||
// Apply the filtering strategy
|
||||
FilteredTable = if HasDataObject then
|
||||
// If DataObjects exists, keep only those data objects
|
||||
Table.SelectRows(FinalTable, each
|
||||
Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject") or
|
||||
Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "Objects.Data"))
|
||||
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(_))
|
||||
// Otherwise, keep all but DataChunks
|
||||
Table.SelectRows(FinalTable, each Record.FieldOrDefault([data], "speckle_type", "") <> "Speckle.Core.Models.DataChunk")
|
||||
in
|
||||
FilteredTable
|
||||
@@ -26,7 +26,7 @@ in
|
||||
parsedUrl = Parser(url),
|
||||
server = parsedUrl[baseUrl],
|
||||
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise "",
|
||||
apiKey = Extension.CurrentCredential(),
|
||||
|
||||
query = "query {
|
||||
activeUser {
|
||||
@@ -45,7 +45,7 @@ in
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = "" then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400},
|
||||
Content = Json.FromValue([query = query])
|
||||
@@ -57,10 +57,10 @@ in
|
||||
error JsonResponse[errors]{0}[message]
|
||||
else
|
||||
[
|
||||
UserEmail = try JsonResponse[data][activeUser][email] otherwise "",
|
||||
UserName = try JsonResponse[data][activeUser][name] otherwise "",
|
||||
UserEmail = JsonResponse[data][activeUser][email],
|
||||
UserName = JsonResponse[data][activeUser][name],
|
||||
ServerName = JsonResponse[data][serverInfo][name],
|
||||
ServerCompany = JsonResponse[data][serverInfo][company],
|
||||
ServerVersion = JsonResponse[data][serverInfo][version],
|
||||
Token = if apiKey = "" then null else apiKey
|
||||
Token = apiKey[access_token]
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
() 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
|
||||
@@ -1,76 +0,0 @@
|
||||
// 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
|
||||
@@ -1,44 +0,0 @@
|
||||
// Function to mark a version as received via GraphQL mutation
|
||||
// Uses the powerful token
|
||||
(powerfulToken as text, versionId as text, projectId as text, serverUrl as text) as logical =>
|
||||
try
|
||||
let
|
||||
NormalizedServerUrl = if Text.End(serverUrl, 1) = "/" then
|
||||
serverUrl
|
||||
else
|
||||
serverUrl & "/",
|
||||
|
||||
// Build GraphQL
|
||||
GraphQLMutation = "mutation MarkVersionReceived($input: MarkReceivedVersionInput!) { versionMutations { markReceived(input: $input) } }",
|
||||
|
||||
Variables = [
|
||||
input = [
|
||||
versionId = versionId,
|
||||
projectId = projectId,
|
||||
sourceApplication = "powerbi-data"
|
||||
]
|
||||
],
|
||||
|
||||
// Make GraphQL request
|
||||
Response = Web.Contents(
|
||||
NormalizedServerUrl & "graphql",
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = "Bearer " & powerfulToken
|
||||
],
|
||||
Content = Json.FromValue([
|
||||
query = GraphQLMutation,
|
||||
variables = Variables
|
||||
]),
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Timeout = #duration(0, 0, 0, 5)
|
||||
]
|
||||
),
|
||||
|
||||
StatusCode = Value.Metadata(Response)[Response.Status]
|
||||
in
|
||||
StatusCode >= 200 and StatusCode < 300
|
||||
otherwise
|
||||
false
|
||||
@@ -1,42 +0,0 @@
|
||||
// function for federating multiple tables by combining them and creating concatenated Version Object ID and Model Info fields
|
||||
(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", "Model Info", "Object IDs"}, MissingField.Ignore)
|
||||
else
|
||||
_
|
||||
),
|
||||
|
||||
CombinedTable = Table.Combine(ProcessedTables),
|
||||
|
||||
DistinctVersionObjectIDs = List.Distinct(CombinedTable[Version Object ID]),
|
||||
ConcatenatedVersionObjectIDs = Text.Combine(DistinctVersionObjectIDs, ","),
|
||||
|
||||
DistinctModelInfo = List.Distinct(CombinedTable[Model Info]),
|
||||
ConcatenatedModelInfo = Text.Combine(DistinctModelInfo, "|||"),
|
||||
|
||||
// Replace all Version Object ID values with the concatenated string
|
||||
TableWithVersionObjectID = Table.ReplaceValue(
|
||||
CombinedTable,
|
||||
each [Version Object ID],
|
||||
ConcatenatedVersionObjectIDs,
|
||||
Replacer.ReplaceText,
|
||||
{"Version Object ID"}
|
||||
),
|
||||
|
||||
// Replace all Model Info values with the concatenated string
|
||||
FederatedTable = Table.ReplaceValue(
|
||||
TableWithVersionObjectID,
|
||||
each [Model Info],
|
||||
ConcatenatedModelInfo,
|
||||
Replacer.ReplaceText,
|
||||
{"Model Info"}
|
||||
)
|
||||
in
|
||||
FederatedTable
|
||||
@@ -1,22 +0,0 @@
|
||||
// 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
|
||||
@@ -1,163 +0,0 @@
|
||||
// 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
|
||||
@@ -1,18 +0,0 @@
|
||||
(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
|
||||
@@ -1,15 +0,0 @@
|
||||
// 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
|
||||
@@ -1,257 +0,0 @@
|
||||
// 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,49 +11,29 @@
|
||||
then pathSegments{1} else null,
|
||||
|
||||
// extract model ID and version ID if they exist
|
||||
rawModelSegment = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
|
||||
then pathSegments{3} else "",
|
||||
modelAndVersion = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
|
||||
then Text.Split(pathSegments{3}, "@") else {},
|
||||
|
||||
// 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},
|
||||
// separate model ID from version ID
|
||||
modelId = if List.Count(modelAndVersion) > 0
|
||||
then modelAndVersion{0} 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]),
|
||||
// get version ID if it exists
|
||||
versionId = if List.Count(modelAndVersion) > 1
|
||||
then modelAndVersion{1} else null,
|
||||
|
||||
// validate URL structure
|
||||
isValid = projectId <> null and List.Count(modelIds) > 0 and List.First(modelIds) <> ""
|
||||
isValid = projectId <> null and modelId <> null
|
||||
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' or 'https://server/projects/PROJECT_ID/models/MODEL_ID1,MODEL_ID2'"
|
||||
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'"
|
||||
]
|
||||
else
|
||||
[
|
||||
baseUrl = baseUrl,
|
||||
projectId = projectId,
|
||||
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,
|
||||
resourceIdString = rawModelSegment
|
||||
]
|
||||
modelId = modelId,
|
||||
versionId = versionId
|
||||
]
|
||||
@@ -1,173 +0,0 @@
|
||||
// Function for getting issues from Speckle projects, models, or versions
|
||||
(url as text, optional getReplies as logical) as table =>
|
||||
let
|
||||
// Import required functions
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
ApiFetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
|
||||
// Set default value for getReplies parameter
|
||||
getRepliesValue = if getReplies = null then false else getReplies,
|
||||
|
||||
// Extension.LoadFunction 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]
|
||||
],
|
||||
|
||||
// Parse the URL to get necessary components with fallback for project-only URLs
|
||||
parsedUrl = try Parser(url) otherwise
|
||||
// Custom parsing for project-only URLs
|
||||
let
|
||||
urlParts = Uri.Parts(url),
|
||||
baseUrl = Text.Combine({urlParts[Scheme], "://", urlParts[Host]}),
|
||||
pathSegments = List.Select(Text.Split(urlParts[Path], "/"), each _ <> ""),
|
||||
projectId = if List.Count(pathSegments) >= 2 and pathSegments{0} = "projects"
|
||||
then pathSegments{1} else null
|
||||
in
|
||||
if projectId = null then
|
||||
error [
|
||||
Reason = "Invalid URL",
|
||||
Message = "The URL must be a valid Speckle project URL in the format 'https://server/projects/PROJECT_ID' or include models/versions"
|
||||
]
|
||||
else
|
||||
[
|
||||
baseUrl = baseUrl,
|
||||
projectId = projectId,
|
||||
modelId = null,
|
||||
versionId = null
|
||||
],
|
||||
|
||||
server = parsedUrl[baseUrl],
|
||||
projectId = parsedUrl[projectId],
|
||||
modelId = parsedUrl[modelId],
|
||||
versionId = parsedUrl[versionId],
|
||||
|
||||
// Define the GraphQL query (single query for all scopes)
|
||||
issuesQuery = "query Project($projectId: String!, $input: ProjectIssuesInput" &
|
||||
(if getRepliesValue then ", $repliesInput2: IssueRepliesInput" else "") & ") {
|
||||
project(id: $projectId) {
|
||||
issues(input: $input) {
|
||||
items {
|
||||
identifier
|
||||
title
|
||||
rawDescription
|
||||
status
|
||||
priority
|
||||
assignee {
|
||||
user {
|
||||
name
|
||||
}
|
||||
}
|
||||
dueDate
|
||||
labels {
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
resourceIdString
|
||||
viewerState
|
||||
id" &
|
||||
(if getRepliesValue then "
|
||||
replies(input: $repliesInput2) {
|
||||
items {
|
||||
issueId
|
||||
id
|
||||
rawDescription
|
||||
createdAt
|
||||
author {
|
||||
user {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}" else "") & "
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
|
||||
// Build input variable dynamically based on URL scope
|
||||
inputVariable =
|
||||
if versionId <> null then
|
||||
// Version URL: resourceIdString = "MODEL_ID@VERSION_ID"
|
||||
[
|
||||
limit = 10000,
|
||||
resourceIdString = modelId & "@" & versionId
|
||||
]
|
||||
else if modelId <> null then
|
||||
// Model URL: resourceIdString = MODEL_ID
|
||||
[
|
||||
limit = 10000,
|
||||
resourceIdString = modelId
|
||||
]
|
||||
else
|
||||
// Project URL: no resourceIdString
|
||||
[
|
||||
limit = 10000
|
||||
],
|
||||
|
||||
// Build query variables
|
||||
queryVariables = if getRepliesValue then
|
||||
[
|
||||
projectId = projectId,
|
||||
input = inputVariable,
|
||||
repliesInput2 = [limit = 10000]
|
||||
]
|
||||
else
|
||||
[
|
||||
projectId = projectId,
|
||||
input = inputVariable
|
||||
],
|
||||
|
||||
// Make the API request using ApiFetch
|
||||
result = ApiFetch(server, issuesQuery, queryVariables),
|
||||
|
||||
// Extract issues from the response
|
||||
issues = result[project][issues][items],
|
||||
|
||||
// Transform to table structure with specified columns
|
||||
issuesTable = Table.FromRecords(
|
||||
List.Transform(issues, (issue) =>
|
||||
let
|
||||
// Extract selectedObjectApplicationIds from viewerState (already a record object)
|
||||
viewerState = try issue[viewerState] otherwise null,
|
||||
selectedObjectIds = try viewerState[ui][filters][selectedObjectApplicationIds] otherwise null,
|
||||
objectIds = try Record.FieldNames(selectedObjectIds) otherwise null,
|
||||
applicationIds = try Record.FieldValues(selectedObjectIds) otherwise null,
|
||||
|
||||
baseRecord = [
|
||||
ID = issue[identifier],
|
||||
Title = issue[title],
|
||||
Description = try issue[rawDescription] otherwise null,
|
||||
Status = try issue[status] otherwise null,
|
||||
Priority = try issue[priority] otherwise null,
|
||||
Assignee = try issue[assignee][user][name] otherwise null,
|
||||
#"Due Date" = try DateTime.From(issue[dueDate]) otherwise null,
|
||||
Labels = try List.Transform(issue[labels], each _[name]) otherwise {},
|
||||
#"Created at" = try DateTime.From(issue[createdAt]) otherwise null,
|
||||
#"Updated at" = try DateTime.From(issue[updatedAt]) otherwise null,
|
||||
URL = server & "/projects/" & projectId & "/models/" & issue[resourceIdString] & "#threadId=" & issue[id],
|
||||
#"Object IDs" = objectIds,
|
||||
#"Application IDs" = applicationIds
|
||||
],
|
||||
recordWithReplies = if getRepliesValue then
|
||||
baseRecord & [Replies = try issue[replies][items] otherwise null]
|
||||
else
|
||||
baseRecord
|
||||
in
|
||||
recordWithReplies
|
||||
)
|
||||
)
|
||||
in
|
||||
issuesTable
|
||||
@@ -1,11 +1,10 @@
|
||||
(url as text) as list =>
|
||||
let
|
||||
// Import required functions
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
GetUser = Extension.LoadFunction("GetUser.pqm"),
|
||||
ExchangeToken = Extension.LoadFunction("ExchangeToken.pqm"),
|
||||
|
||||
// helper function to load .pqm modules dynamically
|
||||
// the logic for importing functions from other files
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName),
|
||||
@@ -21,80 +20,44 @@
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
|
||||
|
||||
// Get model info and parsed URL
|
||||
modelInfo = GetModel(url),
|
||||
parsedUrl = Parser(url),
|
||||
userInfo = GetUser(url),
|
||||
powerfulToken = userInfo[Token],
|
||||
|
||||
// exchange powerful token for weak token using GraphQL
|
||||
// this replaces the desktop service token exchange
|
||||
tokenExchangeResult = ExchangeToken(
|
||||
powerfulToken,
|
||||
{"profile:read", "streams:read", "users:read"},
|
||||
parsedUrl[projectId],
|
||||
parsedUrl[baseUrl],
|
||||
parsedUrl[resourceIdString]
|
||||
|
||||
// Get API key if available
|
||||
apiKey = userInfo[Token],
|
||||
|
||||
// Get user email from credentials
|
||||
userEmail = userInfo[UserEmail],
|
||||
|
||||
// Prepare request data
|
||||
requestData = Json.FromValue([
|
||||
Url = url,
|
||||
Server = parsedUrl[baseUrl],
|
||||
Email = userEmail,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ObjectId = modelInfo[rootObjectId],
|
||||
SourceApplication = modelInfo[sourceApplication],
|
||||
Token = apiKey
|
||||
]),
|
||||
|
||||
// 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}
|
||||
]
|
||||
),
|
||||
|
||||
// throw error if token exchange failed - do NOT use powerful token as fallback
|
||||
tokenToUse = if tokenExchangeResult[Success] then
|
||||
tokenExchangeResult[Token]
|
||||
else
|
||||
error [
|
||||
Reason = "TokenExchangeFailed",
|
||||
Message.Format = "Failed to exchange token for limited scope token: #{0}",
|
||||
Message.Parameters = {tokenExchangeResult[ErrorMessage]},
|
||||
Detail = [
|
||||
ErrorMessage = tokenExchangeResult[ErrorMessage],
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ServerUrl = parsedUrl[baseUrl]
|
||||
]
|
||||
],
|
||||
|
||||
// downloads data directly from server
|
||||
DirectDownload = (token as text) =>
|
||||
let
|
||||
objectUrl = Text.Combine({
|
||||
parsedUrl[baseUrl],
|
||||
"/objects/",
|
||||
parsedUrl[projectId],
|
||||
"/",
|
||||
modelInfo[rootObjectId]
|
||||
}),
|
||||
|
||||
Response = Web.Contents(
|
||||
objectUrl,
|
||||
[
|
||||
Headers = [
|
||||
#"Authorization" = "Bearer " & token,
|
||||
#"Accept" = "application/json"
|
||||
],
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504}
|
||||
]
|
||||
),
|
||||
|
||||
StatusCode = Value.Metadata(Response)[Response.Status],
|
||||
|
||||
JsonResponse = if StatusCode >= 200 and StatusCode < 300 then
|
||||
Json.Document(Response)
|
||||
else
|
||||
error [
|
||||
Reason = "DirectDownloadFailed",
|
||||
Message.Format = "Failed to download model data from Speckle server (Status: #{0})",
|
||||
Message.Parameters = {Text.From(StatusCode)},
|
||||
Detail = [
|
||||
StatusCode = StatusCode,
|
||||
ObjectUrl = objectUrl,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
RootObjectId = modelInfo[rootObjectId]
|
||||
]
|
||||
]
|
||||
in
|
||||
JsonResponse,
|
||||
|
||||
// download data using the token (weak if exchange succeeded, powerful otherwise)
|
||||
FinalResult = DirectDownload(tokenToUse)
|
||||
|
||||
// Parse response
|
||||
JsonResponse = Json.Document(Response)
|
||||
|
||||
in
|
||||
FinalResult
|
||||
JsonResponse
|
||||
@@ -1,31 +0,0 @@
|
||||
// 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
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"dataRoles": [
|
||||
{
|
||||
"displayName": "Model Info",
|
||||
"kind": "Measure",
|
||||
"displayName": "Version Object ID",
|
||||
"kind": "Grouping",
|
||||
"name": "rootObjectId"
|
||||
},
|
||||
{
|
||||
@@ -10,15 +10,15 @@
|
||||
"kind": "Grouping",
|
||||
"name": "objectIds"
|
||||
},
|
||||
{
|
||||
"displayName": "Object Data (Tooltip)",
|
||||
"kind": "Measure",
|
||||
"name": "tooltipData"
|
||||
},
|
||||
{
|
||||
"displayName": "Color By",
|
||||
"kind": "Grouping",
|
||||
"name": "colorBy"
|
||||
"name": "objectColorBy"
|
||||
},
|
||||
{
|
||||
"displayName": "Tooltip Data",
|
||||
"kind": "Measure",
|
||||
"name": "tooltipData"
|
||||
}
|
||||
],
|
||||
"dataViewMappings": [
|
||||
@@ -33,7 +33,12 @@
|
||||
"select": [
|
||||
{
|
||||
"bind": {
|
||||
"to": "colorBy"
|
||||
"to": "rootObjectId"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bind": {
|
||||
"to": "objectColorBy"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -46,13 +51,8 @@
|
||||
"values": {
|
||||
"select": [
|
||||
{
|
||||
"for": {
|
||||
"in": "rootObjectId"
|
||||
}
|
||||
},
|
||||
{
|
||||
"for": {
|
||||
"in": "tooltipData"
|
||||
"bind": {
|
||||
"to": "tooltipData"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -60,7 +60,6 @@
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"colorBy": { "max": 1 },
|
||||
"objectIds": { "max": 1 },
|
||||
"rootObjectId": { "max": 1 }
|
||||
}
|
||||
@@ -78,83 +77,61 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"properties": {
|
||||
"brandingHidden": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewMode": {
|
||||
"properties": {
|
||||
"defaultViewMode": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"navbarHidden": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"edgesEnabled": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"edgesWeight": {
|
||||
"type": { "numeric": true }
|
||||
},
|
||||
"edgesColor": {
|
||||
"type": { "numeric": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"camera": {
|
||||
"properties": {
|
||||
"defaultView": {
|
||||
"type": { "text": true }
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"isOrtho": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"isGhost": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"zoomOnFilter": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
"sectionBox": {
|
||||
"properties": {
|
||||
"boxData": {
|
||||
"type": { "text": 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": {
|
||||
"allowCameraUnder": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"zoomOnDataChange": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"projection": {
|
||||
"type": {
|
||||
"enumeration": [
|
||||
{
|
||||
"displayName": "Perspective",
|
||||
"value": "perspective"
|
||||
},
|
||||
{
|
||||
"displayName": "Orthographic",
|
||||
"value": "orthographic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,7 +211,7 @@
|
||||
{
|
||||
"essential": true,
|
||||
"name": "WebAccess",
|
||||
"parameters": ["https://analytics.speckle.systems", "*"]
|
||||
"parameters": ["https://analytics.speckle.systems", "http://localhost:29364", "*"]
|
||||
},
|
||||
{
|
||||
"essential": false,
|
||||
|
||||
Generated
+1964
-4259
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/objectloader2": "2.26.7",
|
||||
"@speckle/objectloader": "^2.23.8",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.26.5",
|
||||
"@speckle/viewer": "2.23.8",
|
||||
"color-interpolate": "^1.0.5",
|
||||
"core-js": "^3.30.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -35,8 +35,7 @@
|
||||
"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",
|
||||
"vue-tippy": "^6.7.1"
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
@@ -50,7 +49,6 @@
|
||||
"@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",
|
||||
@@ -59,7 +57,6 @@
|
||||
"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",
|
||||
@@ -85,6 +82,5 @@
|
||||
"version": "3.0.0",
|
||||
"engines": {
|
||||
"node": "^20.17.0"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
<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>
|
||||
@@ -23,7 +8,6 @@ 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()
|
||||
|
||||
@@ -31,12 +15,3 @@ 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,99 +1,111 @@
|
||||
<template>
|
||||
<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>
|
||||
</ViewerControlsButtonGroup>
|
||||
<ViewerControlsButtonGroup>
|
||||
<!-- View Modes Toggle -->
|
||||
<div class="relative">
|
||||
<ViewerControlsButtonToggle
|
||||
flat
|
||||
tooltip="View modes"
|
||||
:active="viewModesOpen"
|
||||
@click="toggleActiveControl('viewModes')"
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<ViewModesIcon class="h-5 w-5" />
|
||||
</ViewerControlsButtonToggle>
|
||||
<!-- View Modes Panel (shown when glasses icon is clicked) -->
|
||||
<ViewerViewModesMenu
|
||||
v-if="viewModesOpen"
|
||||
@view-mode-clicked="(viewMode, options) => $emit('view-mode-clicked', viewMode, options)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Camera -->
|
||||
<ViewerCameraMenu
|
||||
:open="cameraOpen"
|
||||
:views="views"
|
||||
@update:open="(value) => toggleActiveControl(value ? 'camera' : 'none')"
|
||||
@view-clicked="(view) => $emit('view-clicked', view)"
|
||||
/>
|
||||
<!-- Section box -->
|
||||
<div class="relative">
|
||||
<ViewerControlsButtonToggle
|
||||
flat
|
||||
tooltip="Section box"
|
||||
@click="$emit('update:sectionBox')"
|
||||
>
|
||||
<ScissorsIcon class="h-4 w-4 md:h-5 md:w-5" />
|
||||
</ViewerControlsButtonToggle>
|
||||
<span
|
||||
v-if="sectionBox"
|
||||
class="absolute top-1 right-1 h-2 w-2 rounded-full bg-primary pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</ViewerControlsButtonGroup>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowsPointingOutIcon, ScissorsIcon } from '@heroicons/vue/24/solid'
|
||||
import { CanonicalView, SpeckleView, ViewMode } from '@speckle/viewer'
|
||||
import { computed, ref } from 'vue'
|
||||
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 { useVisualStore } from '@src/store/visualStore'
|
||||
import ViewerControlsButtonGroup from './viewer/controls/ViewerControlsButtonGroup.vue'
|
||||
import ViewerControlsButtonToggle from './viewer/controls/ViewerControlsButtonToggle.vue'
|
||||
|
||||
import ViewerCameraMenu from './viewer/camera/ViewerCameraMenu.vue'
|
||||
import ViewerViewModesMenu from './viewer/view-modes/ViewerViewModesMenu.vue'
|
||||
|
||||
import ViewModesIcon from '../components/global/icon/ViewModes.vue'
|
||||
import type { ViewModeOptions } from '@src/plugins/viewer'
|
||||
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:sectionBox', value: boolean): void
|
||||
(e: 'view-clicked', view: CanonicalView | SpeckleView): void
|
||||
(e: 'clear-palette'): void
|
||||
(e: 'view-mode-clicked', viewMode: ViewMode, options: ViewModeOptions): void
|
||||
}>()
|
||||
withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
|
||||
sectionBox: false
|
||||
const emits = defineEmits(['update:sectionBox', 'view-clicked', 'clear-palette'])
|
||||
const props = withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
|
||||
sectionBox: false,
|
||||
views: () => []
|
||||
})
|
||||
|
||||
type ActiveControl =
|
||||
| 'none'
|
||||
| 'viewModes'
|
||||
| 'camera'
|
||||
| 'sun'
|
||||
| 'projection'
|
||||
| 'sectionBox'
|
||||
| 'explode'
|
||||
| 'settings'
|
||||
|
||||
const activeControl = ref<ActiveControl>('none')
|
||||
const canonicalViews = [
|
||||
{ name: 'Top' },
|
||||
{ name: 'Front' },
|
||||
{ name: 'Left' },
|
||||
{ name: 'Back' },
|
||||
{ name: 'Right' }
|
||||
]
|
||||
|
||||
const onZoomExtentsClicked = (ev: MouseEvent) => {
|
||||
visualStore.viewerEmit('zoomExtends')
|
||||
}
|
||||
|
||||
const toggleActiveControl = (control: ActiveControl) => {
|
||||
activeControl.value = activeControl.value === control ? 'none' : control
|
||||
const onClearPalletteClicked = (ev: MouseEvent) => {
|
||||
console.log('Clear pallette clicked')
|
||||
resetPalette()
|
||||
emits('clear-palette')
|
||||
}
|
||||
|
||||
const viewModesOpen = computed(() => activeControl.value === 'viewModes')
|
||||
const cameraOpen = computed(() => activeControl.value === 'camera')
|
||||
</script>
|
||||
|
||||
@@ -1,150 +1,33 @@
|
||||
<template>
|
||||
<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-4 right-2 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"
|
||||
:section-box="sectionBoxEnabled"
|
||||
:views="views"
|
||||
class="fixed top-11 left-2 z-30"
|
||||
@update:section-box="onSectionBoxToggle"
|
||||
@view-clicked="(view) => viewerHandler.setView(view)"
|
||||
@view-mode-clicked="(viewMode, options) => viewerHandler.setViewMode(viewMode, options)"
|
||||
/>
|
||||
</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 v-if="sectionBoxVisible" class="absolute bottom-5 left-1/2 -translate-x-1/2 z-50 flex gap-2">
|
||||
<FormButton size="sm" color="outline" @click="onSectionBoxReset">Reset</FormButton>
|
||||
<FormButton size="sm" @click="onSectionBoxDone">Done</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 class="flex flex-col justify-center items-center">
|
||||
<div
|
||||
ref="container"
|
||||
class="fixed h-full w-full z-0 cursor-default"
|
||||
class="fixed h-full w-full z-0"
|
||||
@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 FormButton from '@src/components/form/FormButton.vue'
|
||||
import { computed, inject, onBeforeUnmount, onMounted, Ref, ref } from 'vue'
|
||||
import { inject, onBeforeUnmount, onMounted, Ref, ref } from 'vue'
|
||||
import { currentOS, OS } from '../utils/detectOS'
|
||||
import ViewerControls from 'src/components/ViewerControls.vue'
|
||||
import { SpeckleView } from '@speckle/viewer'
|
||||
import { CanonicalView, 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()
|
||||
@@ -155,68 +38,13 @@ const tooltipHandler = inject(tooltipHandlerKey)
|
||||
let viewerHandler: ViewerHandler = null
|
||||
|
||||
const container = ref<HTMLElement>()
|
||||
type SectionBoxState = 'inactive' | 'editing' | 'applied'
|
||||
const sectionBoxState = ref<SectionBoxState>('inactive')
|
||||
const sectionBoxEnabled = computed(() => sectionBoxState.value !== 'inactive')
|
||||
const sectionBoxVisible = computed(() => sectionBoxState.value === 'editing')
|
||||
const views: Ref<SpeckleView[]> = ref([])
|
||||
|
||||
const isInteractive = computed(
|
||||
() => visualStore.fieldInputState.rootObjectId && visualStore.fieldInputState.objectIds
|
||||
)
|
||||
|
||||
const goToSpeckleWebsite = () => visualStore.host.launchUrl('https://speckle.systems')
|
||||
|
||||
function disableSectionBox() {
|
||||
sectionBoxState.value = 'inactive'
|
||||
viewerHandler.toggleSectionBox(false)
|
||||
visualStore.writeSectionBoxToFile(null)
|
||||
visualStore.setSectionBoxData(null)
|
||||
}
|
||||
|
||||
function onSectionBoxToggle() {
|
||||
switch (sectionBoxState.value) {
|
||||
case 'inactive':
|
||||
sectionBoxState.value = 'editing'
|
||||
viewerHandler.toggleSectionBox(true)
|
||||
break
|
||||
case 'editing':
|
||||
onSectionBoxDone()
|
||||
break
|
||||
case 'applied':
|
||||
sectionBoxState.value = 'editing'
|
||||
viewerHandler.setSectionBoxVisible(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function onSectionBoxReset() {
|
||||
disableSectionBox()
|
||||
}
|
||||
|
||||
function onSectionBoxDone() {
|
||||
sectionBoxState.value = 'applied'
|
||||
viewerHandler.setSectionBoxVisible(false)
|
||||
const boxData = viewerHandler.getSectionBoxData()
|
||||
visualStore.setSectionBoxData(boxData)
|
||||
visualStore.writeSectionBoxToFile(boxData)
|
||||
}
|
||||
let bboxActive = ref(false)
|
||||
let views: Ref<SpeckleView[]> = ref([])
|
||||
|
||||
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)
|
||||
|
||||
// Sync section box UI state when restored from file
|
||||
viewerHandler.emitter.on('objectsLoaded', () => {
|
||||
if (visualStore.sectionBoxData) {
|
||||
sectionBoxState.value = 'applied'
|
||||
}
|
||||
})
|
||||
|
||||
visualStore.setViewerEmitter(viewerHandler.emit)
|
||||
})
|
||||
|
||||
@@ -224,100 +52,40 @@ onBeforeUnmount(async () => {
|
||||
await viewerHandler.dispose()
|
||||
})
|
||||
|
||||
async function handleObjectClicked(hit: any, isMultiSelect: boolean, mouseEvent?: PointerEvent) {
|
||||
// Skip if dragging occurred
|
||||
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) {
|
||||
if (dragged.value) return
|
||||
|
||||
console.log('🎯 Object clicked in ViewerWrapper:', hit, isMultiSelect)
|
||||
|
||||
|
||||
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
|
||||
|
||||
const multi = isMultiSelect(ev)
|
||||
const hit = intersectResult?.hit
|
||||
if (hit) {
|
||||
visualStore.setPostClickSkipNeeded(true)
|
||||
const id = hit.object.id as string
|
||||
if (isMultiSelect || !selectionHandler.isSelected(id)) {
|
||||
await selectionHandler.select(id, isMultiSelect)
|
||||
if (multi || !selectionHandler.isSelected(id)) {
|
||||
await selectionHandler.select(id, multi)
|
||||
}
|
||||
|
||||
// Show tooltip if we have mouse coordinates
|
||||
if (mouseEvent) {
|
||||
tooltipHandler.show(hit, { x: mouseEvent.clientX, y: mouseEvent.clientY })
|
||||
}
|
||||
|
||||
tooltipHandler.show(hit, { x: ev.clientX, y: ev.clientY })
|
||||
const selection = selectionHandler.getCurrentSelection()
|
||||
const ids = selection.map((s) => s.id)
|
||||
await viewerHandler.selectObjects(ids)
|
||||
} else {
|
||||
visualStore.setPostClickSkipNeeded(false)
|
||||
tooltipHandler.hide()
|
||||
if (!isMultiSelect) {
|
||||
if (!multi) {
|
||||
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
|
||||
|
||||
// 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)
|
||||
if (ev.button != 2 || dragged.value) return
|
||||
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
|
||||
await selectionHandler.showContextMenu(ev, intersectResult?.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>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
@@ -0,0 +1,45 @@
|
||||
<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>
|
||||
+4
-7
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<button
|
||||
: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'
|
||||
}`"
|
||||
: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 } from 'vue'
|
||||
|
||||
@@ -16,14 +12,15 @@ 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 border-primary')
|
||||
else parts.push('bg-primary text-foreground-on-primary')
|
||||
} else {
|
||||
parts.push('bg-foundation text-foreground')
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
<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>
|
||||
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label
|
||||
:for="name"
|
||||
class="block text-body-2xs text-foreground-2"
|
||||
>
|
||||
{{ label || name }}
|
||||
</label>
|
||||
<span class="text-body-2xs text-foreground-2">{{ displayValue }}</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
:id="name"
|
||||
:name="name"
|
||||
type="range"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:value="currentValue"
|
||||
:disabled="disabled"
|
||||
class="w-full h-1.5 outline-none slider"
|
||||
:class="{
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed': disabled
|
||||
}"
|
||||
:aria-label="label"
|
||||
:aria-valuemin="min"
|
||||
:aria-valuemax="max"
|
||||
:aria-valuenow="currentValue"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
name: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
modelValue?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const currentValue = ref(props.modelValue ?? props.min)
|
||||
|
||||
// Watch for external changes to modelValue
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal !== undefined && newVal !== currentValue.value) {
|
||||
currentValue.value = newVal
|
||||
}
|
||||
})
|
||||
|
||||
const displayValue = computed(() => {
|
||||
// Round to avoid floating point issues
|
||||
return Math.round(currentValue.value * 10) / 10
|
||||
})
|
||||
|
||||
const clampValue = (value: number): number => {
|
||||
return Math.max(props.min, Math.min(props.max, value))
|
||||
}
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = Number(target.value)
|
||||
const clampedValue = clampValue(value)
|
||||
currentValue.value = clampedValue
|
||||
emit('update:modelValue', clampedValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-runnable-track {
|
||||
@apply h-1.5 rounded-full bg-outline-3;
|
||||
}
|
||||
|
||||
.slider::-moz-range-track {
|
||||
@apply h-1.5 rounded-full bg-outline-3;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
@apply h-2.5 w-2.5 rounded-full cursor-pointer bg-foreground-2;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
@apply h-2.5 w-2.5 rounded-full cursor-pointer border-0 bg-foreground-2;
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
:id="name"
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="modelValue"
|
||||
:disabled="disabled"
|
||||
class="relative inline-flex flex-shrink-0 h-[18px] w-[30px] rounded-full transition-colors ease-in-out duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="modelValue ? 'bg-primary' : 'bg-foreground-3'"
|
||||
@click="toggle"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-3 w-3 rounded-full mt-[3px] ml-[3px] ring-0 transition ease-in-out duration-200 bg-foreground-on-primary"
|
||||
:class="modelValue ? 'translate-x-[12px]' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
<label v-if="showLabel" :for="name" class="block label-light">
|
||||
<span>{{ label || name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: boolean
|
||||
showLabel?: boolean
|
||||
name: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
showLabel: true,
|
||||
modelValue: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const toggle = () => {
|
||||
if (!props.disabled) {
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@@ -1,31 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,31 +0,0 @@
|
||||
<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>
|
||||
@@ -1,59 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,28 +0,0 @@
|
||||
<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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<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>
|
||||
@@ -1,66 +0,0 @@
|
||||
<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>
|
||||
@@ -1,73 +0,0 @@
|
||||
<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>
|
||||
@@ -1,90 +0,0 @@
|
||||
<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>
|
||||
@@ -1,68 +0,0 @@
|
||||
<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>
|
||||
@@ -1,27 +0,0 @@
|
||||
<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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<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>
|
||||
@@ -1,13 +0,0 @@
|
||||
<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>
|
||||
@@ -1,13 +0,0 @@
|
||||
<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>
|
||||
@@ -1,22 +0,0 @@
|
||||
<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>
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,29 +0,0 @@
|
||||
<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>
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.75 3.75V8.25M3.75 3.75H8.25M3.75 3.75L9 9M20.25 3.75H15.75M20.25 3.75V8.25M20.25 3.75L15 9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.75 15.4028L18.8093 12.3435C18.8772 12.2756 18.9638 12.2294 19.0581 12.2107C19.1523 12.1919 19.25 12.2016 19.3387 12.2383C19.4275 12.2751 19.5034 12.3373 19.5568 12.4172C19.6102 12.4971 19.6388 12.591 19.6389 12.687V20.063C19.6388 20.159 19.6102 20.2529 19.5568 20.3328C19.5034 20.4127 19.4275 20.4749 19.3387 20.5117C19.25 20.5484 19.1523 20.5581 19.0581 20.5393C18.9638 20.5206 18.8772 20.4744 18.8093 20.4065L15.75 17.3472M8.45833 20.75H14.2917C14.6784 20.75 15.0494 20.5964 15.3229 20.3229C15.5964 20.0494 15.75 19.6784 15.75 19.2917V13.4583C15.75 13.0716 15.5964 12.7006 15.3229 12.4271C15.0494 12.1536 14.6784 12 14.2917 12H8.45833C8.07156 12 7.70063 12.1536 7.42714 12.4271C7.15365 12.7006 7 13.0716 7 13.4583V19.2917C7 19.6784 7.15365 20.0494 7.42714 20.3229C7.70063 20.5964 8.07156 20.75 8.45833 20.75Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
<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>
|
||||
@@ -1,32 +0,0 @@
|
||||
<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,46 +1,22 @@
|
||||
<template>
|
||||
<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
|
||||
: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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useMounted } from '@vueuse/core'
|
||||
import { LoadingProgress } from '@src/store/visualStore'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ progress: LoadingProgress; clientOnly?: boolean }>()
|
||||
const props = defineProps<{ loading: boolean; clientOnly?: boolean }>()
|
||||
|
||||
const mounted = useMounted()
|
||||
const showBar = computed(() => (mounted.value || !props.clientOnly) && !!props.progress)
|
||||
const isIndeterminate = computed(() => props.progress.progress == null)
|
||||
const progressPercent = computed(() => (props.progress.progress ?? 0) * 100)
|
||||
const showBar = computed(() => (mounted.value || !props.clientOnly) && props.loading)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.swoosher {
|
||||
width: 100%;
|
||||
@@ -53,9 +29,11 @@ const progressPercent = computed(() => (props.progress.progress ?? 0) * 100)
|
||||
0% {
|
||||
transform: translateX(0) scaleX(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateX(0) scaleX(0.4);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%) scaleX(0.5);
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<ViewerMenu v-model:open="open">
|
||||
<template #trigger-icon>
|
||||
<VideoCameraIcon class="w-5 h-5" />
|
||||
</template>
|
||||
<template #title>Camera</template>
|
||||
<div class="flex flex-col p-1.5 min-w-[180px] space-y-0.5">
|
||||
<ViewerMenuItem
|
||||
label="Orthographic projection"
|
||||
:active="visualStore.isOrthoProjection"
|
||||
@click="toggleProjection"
|
||||
/>
|
||||
<ViewerMenuItem
|
||||
label="Move camera on filter"
|
||||
:active="visualStore.isZoomOnFilterActive"
|
||||
@click="toggleZoomOnFilter"
|
||||
/>
|
||||
<ViewerMenuItem
|
||||
label="Ghost filtered objects"
|
||||
:active="visualStore.isGhostActive"
|
||||
@click="toggleGhostHidden"
|
||||
/>
|
||||
|
||||
<div class="w-full border-b border-outline-2 my-1"></div>
|
||||
|
||||
<div class="text-body-2xs font-semibold text-foreground-2 px-2 py-1">Views</div>
|
||||
|
||||
<ViewerMenuItem
|
||||
v-for="shortcut in viewShortcuts"
|
||||
:key="shortcut.name"
|
||||
:label="shortcut.name"
|
||||
hide-active-tick
|
||||
:active="false"
|
||||
@click="handleViewChange(shortcut.name.toLowerCase() as CanonicalView)"
|
||||
/>
|
||||
|
||||
<div v-if="views.length !== 0" class="w-full border-b border-outline-2 my-1"></div>
|
||||
|
||||
<ViewerMenuItem
|
||||
v-for="view in views"
|
||||
:key="view.id"
|
||||
hide-active-tick
|
||||
:active="false"
|
||||
:label="view.name ? view.name : view.id"
|
||||
@click="handleViewChange(view)"
|
||||
/>
|
||||
</div>
|
||||
</ViewerMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { VideoCameraIcon } from '@heroicons/vue/24/outline'
|
||||
import type { CanonicalView, SpeckleView } from '@speckle/viewer'
|
||||
import { useVisualStore } from '@src/store/visualStore'
|
||||
import ViewerMenu from '../menu/ViewerMenu.vue'
|
||||
import ViewerMenuItem from '../menu/ViewerMenuItem.vue'
|
||||
import { ViewShortcuts } from '@src/helpers/viewer/shortcuts/shortcuts'
|
||||
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
views: SpeckleView[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'view-clicked', value: CanonicalView | SpeckleView): void
|
||||
}>()
|
||||
|
||||
const open = computed({
|
||||
get: () => props.open,
|
||||
set: (val) => emit('update:open', val)
|
||||
})
|
||||
|
||||
const viewShortcuts = Object.values(ViewShortcuts)
|
||||
|
||||
const handleViewChange = (v: CanonicalView | SpeckleView) => {
|
||||
emit('view-clicked', v)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-foundation text-foreground rounded-lg w-8 md:w-10 flex flex-col justify-center items-center md:gap-1 border border-outline-2 shadow"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
@@ -1,69 +0,0 @@
|
||||
<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>
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
@@ -1,204 +0,0 @@
|
||||
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
||||
<template>
|
||||
<div class="absolute left-10 sm:left-[46px] -top-0 bg-foundation rounded-md border border-outline-2 shadow min-w-[180px] z-30">
|
||||
<!-- Header -->
|
||||
<div class="px-2 py-1.5 border-b border-outline-2">
|
||||
<span class="text-body-2xs font-medium text-foreground">View modes</span>
|
||||
</div>
|
||||
|
||||
<!-- View Mode List -->
|
||||
<div class="py-0.5">
|
||||
<button
|
||||
v-for="item in viewModes"
|
||||
:key="item.mode"
|
||||
class="w-full px-2 py-1 flex items-center hover:bg-highlight-1 text-left"
|
||||
@click="handleViewModeChange(item.mode)"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<CheckIcon
|
||||
v-if="isActiveMode(item.mode)"
|
||||
class="w-3.5 h-3.5 text-foreground"
|
||||
/>
|
||||
<span v-else class="w-3.5 h-3.5" />
|
||||
<span class="text-body-2xs" :class="isActiveMode(item.mode) ? 'text-foreground font-medium' : 'text-foreground-2'">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Edges Section -->
|
||||
<div class="border-t border-outline-2 px-2 py-1.5 space-y-2">
|
||||
<!-- Edges Toggle -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-body-2xs text-foreground">Edges</span>
|
||||
<FormSwitch
|
||||
v-model="edgesEnabledLocal"
|
||||
:show-label="false"
|
||||
name="toggle-edges"
|
||||
:disabled="currentViewMode === ViewMode.PEN"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Weight Slider (only show when edges enabled) -->
|
||||
<div v-if="edgesEnabledLocal" class="py-1">
|
||||
<FormRange
|
||||
v-model="edgesWeightLocal"
|
||||
name="edge-weight"
|
||||
label="Weight"
|
||||
:min="0.5"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Color Selector (only show when edges enabled) -->
|
||||
<div v-if="edgesEnabledLocal" class="flex items-center justify-between">
|
||||
<span class="text-body-2xs text-foreground-2">Color</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
v-for="(color, index) in edgesColorOptions"
|
||||
:key="color === 'auto' ? 'auto' : color"
|
||||
class="flex items-center justify-center size-4 rounded-full"
|
||||
:class="edgesColorLocal === color && 'ring-2 ring-primary ring-offset-1'"
|
||||
@click="handleEdgesColorChange(color)"
|
||||
>
|
||||
<span
|
||||
class="size-3 rounded-full cursor-pointer"
|
||||
:style="{
|
||||
background:
|
||||
index === 0
|
||||
? 'linear-gradient(135deg, #1a1a1a 50%, #ffffff 50%)'
|
||||
: `#${(color as number).toString(16).padStart(6, '0')}`
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ViewMode } from '@speckle/viewer'
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { useVisualStore } from '@src/store/visualStore'
|
||||
import FormSwitch from '../../form/FormSwitch.vue'
|
||||
import FormRange from '../../form/FormRange.vue'
|
||||
import { CheckIcon } from '@heroicons/vue/24/solid'
|
||||
import type { ViewModeOptions } from '@src/plugins/viewer'
|
||||
|
||||
// Array to maintain proper display order (matching Speckle frontend)
|
||||
const viewModes = [
|
||||
{ mode: ViewMode.DEFAULT, label: 'Rendered' },
|
||||
{ mode: ViewMode.SHADED, label: 'Shaded' },
|
||||
{ mode: ViewMode.ARCTIC, label: 'Arctic' },
|
||||
{ mode: ViewMode.SOLID, label: 'Solid' },
|
||||
{ mode: ViewMode.PEN, label: 'Pen' }
|
||||
]
|
||||
|
||||
const edgesColorOptions = [
|
||||
'auto' as const,
|
||||
0x3b82f6, // blue-500
|
||||
0x8b5cf6, // violet-500
|
||||
0x65a30d, // lime-600
|
||||
0xf97316, // orange-500
|
||||
0xf43f5e // rose-500
|
||||
]
|
||||
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'view-mode-clicked', value: ViewMode, options: ViewModeOptions): void
|
||||
}>()
|
||||
|
||||
// Initialization flag
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// Local state synced with store (with safe defaults)
|
||||
const edgesEnabledLocal = ref(visualStore.edgesEnabled ?? true)
|
||||
const edgesWeightLocal = ref(visualStore.edgesWeight ?? 1)
|
||||
const edgesColorLocal = ref<number | 'auto'>(visualStore.edgesColor ?? 'auto')
|
||||
|
||||
// Mark as initialized after next tick to prevent watchers firing on mount
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
isInitialized.value = true
|
||||
})
|
||||
})
|
||||
|
||||
// Current view mode from store
|
||||
const currentViewMode = computed(() => {
|
||||
return visualStore.defaultViewModeInFile
|
||||
? Number(visualStore.defaultViewModeInFile) as ViewMode
|
||||
: ViewMode.DEFAULT
|
||||
})
|
||||
|
||||
const isActiveMode = (mode: ViewMode) => mode === currentViewMode.value
|
||||
|
||||
// Compute the actual edge color to use (auto resolves to dark)
|
||||
const finalEdgesColor = computed(() => {
|
||||
if (edgesColorLocal.value === 'auto') {
|
||||
return 0x1a1a1a // dark edges by default
|
||||
}
|
||||
return edgesColorLocal.value
|
||||
})
|
||||
|
||||
// Build view mode options
|
||||
const buildViewModeOptions = (mode: ViewMode): ViewModeOptions => {
|
||||
// PEN mode always has edges enabled and opacity 1
|
||||
const isPenMode = mode === ViewMode.PEN
|
||||
return {
|
||||
edges: isPenMode ? true : edgesEnabledLocal.value,
|
||||
outlineThickness: edgesWeightLocal.value,
|
||||
outlineOpacity: isPenMode ? 1 : 0.75,
|
||||
outlineColor: finalEdgesColor.value
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewModeChange = (mode: ViewMode) => {
|
||||
const options = buildViewModeOptions(mode)
|
||||
visualStore.setDefaultViewModeInFile(mode.toString())
|
||||
visualStore.writeViewModeToFile(mode)
|
||||
emit('view-mode-clicked', mode, options)
|
||||
}
|
||||
|
||||
const handleEdgesColorChange = (color: number | 'auto') => {
|
||||
edgesColorLocal.value = color
|
||||
}
|
||||
|
||||
// Apply edges changes to viewer when settings change
|
||||
const applyEdgesSettings = () => {
|
||||
// Don't apply during initialization
|
||||
if (!isInitialized.value) return
|
||||
|
||||
// Update store
|
||||
visualStore.setEdgesEnabled(edgesEnabledLocal.value)
|
||||
visualStore.setEdgesWeight(edgesWeightLocal.value)
|
||||
visualStore.setEdgesColor(edgesColorLocal.value)
|
||||
visualStore.writeEdgesSettingsToFile()
|
||||
|
||||
// Re-apply current view mode with new options
|
||||
const options = buildViewModeOptions(currentViewMode.value)
|
||||
emit('view-mode-clicked', currentViewMode.value, options)
|
||||
}
|
||||
|
||||
// Watch for edges settings changes and apply them
|
||||
watch([edgesEnabledLocal, edgesWeightLocal, edgesColorLocal], () => {
|
||||
applyEdgesSettings()
|
||||
})
|
||||
|
||||
// Sync local state with store when store changes (e.g., from file load)
|
||||
watch(() => visualStore.edgesEnabled, (val) => {
|
||||
edgesEnabledLocal.value = val
|
||||
})
|
||||
|
||||
watch(() => visualStore.edgesWeight, (val) => {
|
||||
edgesWeightLocal.value = val
|
||||
})
|
||||
|
||||
watch(() => visualStore.edgesColor, (val) => {
|
||||
edgesColorLocal.value = val
|
||||
})
|
||||
</script>
|
||||
@@ -1,19 +0,0 @@
|
||||
<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>
|
||||
@@ -1,59 +0,0 @@
|
||||
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
|
||||
|
||||
// Filter out prerelease versions
|
||||
const stableVersions = sortedVersions.filter((v) => !v.Prerelease)
|
||||
const latestVersion = stableVersions[0]
|
||||
const sanitizedVersion = sanitizeVersion(latestVersion.Number)
|
||||
latestAvailableVersion.value = { ...latestVersion, 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 }
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user