Compare commits

..

38 Commits

Author SHA1 Message Date
oguzhankoral b9ff8ee5f7 sanitize tag
build_powerbi / build-connector (push) Has been cancelled
build_powerbi / build-visual (push) Has been cancelled
build_powerbi / deploy-installers (push) Has been cancelled
2025-05-28 14:49:11 +03:00
oguzhankoral 30c2a2002c correct the version with assembly for file version 2025-05-28 14:39:14 +03:00
oguzhankoral 4502a48098 file version 2025-05-28 14:27:43 +03:00
oguzhankoral f2ab186bd8 fix path again 2025-05-28 14:08:43 +03:00
oguzhankoral fbe9672f81 fix path 2025-05-28 14:06:25 +03:00
oguzhankoral 2691902533 test 2025-05-28 14:03:04 +03:00
oguzhankoral 95294c6e6f Enrich receive info from desktop service 2025-05-28 14:02:32 +03:00
Dogukan Karatas e70aa8ae4b feat (data): sending version and branding info (#157)
* get version

* adds workspace info

* adds hideBranding

* adds workspace info

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-05-27 22:12:20 +03:00
oguzhankoral e65bf83995 Disable camera position persistence for performance reasons 2025-05-27 21:58:50 +03:00
oguzhankoral 74ade84015 Disable shadow catcher 2025-05-27 06:52:09 +03:00
oguzhankoral abc4bf11fe Remove ghost hidden context from color card 2025-05-27 06:41:55 +03:00
oguzhankoral 0bd1218d49 Bring conditional formatting back 2025-05-27 06:40:07 +03:00
oguzhankoral e277ce686e Ghost hidden on filter 2025-05-26 03:43:04 +03:00
oguzhankoral f3f5eddb7b Fix reset filter 2025-05-26 02:53:00 +03:00
oguzhankoral 28ce6b6a76 Reset filters 2025-05-26 02:46:52 +03:00
oguzhankoral fe483b7b2b Fix tooltip fckp 2025-05-26 01:23:45 +03:00
oguzhankoral 72b4b9b589 Fix selection issues 2025-05-26 00:46:32 +03:00
oguzhankoral 57c0f198bd Revert isolating every setDataInput 2025-05-25 18:47:44 +03:00
oguzhankoral 439bf56f47 capabilities for object ids 2025-05-25 11:48:57 +03:00
oguzhankoral 3ecae7f493 fix typo on conditions 2025-05-25 00:21:26 +03:00
Jonathon Broughton 843174f5b6 Update README.md (#147)
* Update README.md

* Update README.md
2025-05-25 00:21:26 +03:00
oguzhankoral 83cfa39be0 Fix initial isolate issue 2025-05-24 21:48:26 +03:00
oguzhankoral e4401da357 fix view mode cache 2025-05-24 20:55:39 +03:00
oguzhankoral 222a6f8987 Sort performance logging 2025-05-24 19:50:43 +03:00
oguzhankoral c44b54616e Remove console log 2025-05-24 15:50:50 +03:00
oguzhankoral 7b22e929e0 Fix saved objects 2025-05-24 15:49:13 +03:00
oguzhankoral e1e6d4e640 Remove console log 2025-05-24 15:30:30 +03:00
oguzhankoral 7bdb80f801 Toggle projection/orthi 2025-05-24 05:59:33 +03:00
oguzhankoral da774b631a not a css master commit 2025-05-24 05:42:13 +03:00
oguzhankoral ca0765c862 Hide viewer actions 2025-05-24 05:32:41 +03:00
oguzhankoral 87b64b7a11 Revamp viewer actions 2025-05-24 05:19:34 +03:00
oguzhankoral 41c4e642fe Navbar and cursors 2025-05-24 02:02:35 +03:00
oguzhankoral 76febcfce6 Delete unused code 2025-05-24 01:14:14 +03:00
oguzhankoral 8fbb5a3c9d Fix messaging on interactivity for tooltip data 2025-05-24 01:07:52 +03:00
oguzhankoral eecde37f8b delete logging 2025-05-24 00:27:45 +03:00
oguzhankoral a21a960516 Fix coloring 2025-05-24 00:27:45 +03:00
oguzhankoral 9e76f62ad0 No need tooltip data as part of interactivity 2025-05-24 00:27:45 +03:00
oguzhankoral ab266fa410 delete debugger 2025-05-24 00:27:45 +03:00
23 changed files with 424 additions and 691 deletions
+3
View File
@@ -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
View File
@@ -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
+30 -35
View File
@@ -8,39 +8,32 @@ jobs:
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
with:
fetch-depth: 0
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- id: set-version
name: Set version to output
shell: bash
run: |
TAG=${{ github.ref_name }}
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
TAG="v3.0.99.${{ github.run_number }}"
fi
SEMVER="${TAG#v}"
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
- 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
- name: Set connector version
run: |
python patch_version.py ${{steps.set-version.outputs.file-version}}
python patch_version.py ${{steps.gitversion.outputs.AssemblySemVer}}
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
@@ -67,35 +60,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.${{ github.run_number }}"
fi
SEMVER="${TAG#v}"
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
- name: 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
+11
View File
@@ -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
@@ -59,32 +59,16 @@
)
),
// 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
// If, model data contains any DataObject -> fetch only data objects
// If there are no data objects in the data -> fetch everything but DataChunks
HasDataObjects = Table.RowCount(
Table.SelectRows(
FinalTable,
each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject")
and not ShouldExcludeRow(_)
)
Table.SelectRows(FinalTable, each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject"))
) > 0,
FilteredTable = if HasDataObjects then
Table.SelectRows(
FinalTable,
each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject")
and not ShouldExcludeRow(_)
)
Table.SelectRows(FinalTable, each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject"))
else
Table.SelectRows(FinalTable, each not ShouldExcludeRow(_))
Table.SelectRows(FinalTable, each Record.FieldOrDefault([data], "speckle_type", "") <> "Speckle.Core.Models.DataChunk")
in
FilteredTable
@@ -39,38 +39,29 @@
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)
}
}",
// query to get workspace
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),
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]
]
workspace = workspaceResult[data],
workspaceInfo = [
workspaceId = workspaceId,
workspaceLogo = workspace[logo],
workspaceName = workspace[name],
canHideBranding = workspace[hasAccessToFeature]
]
in
workspaceInfo
+22 -11
View File
@@ -78,13 +78,6 @@
}
}
},
"workspace": {
"properties": {
"brandingHidden": {
"type": { "bool": true }
}
}
},
"viewMode": {
"properties": {
"defaultViewMode": {
@@ -97,11 +90,29 @@
"defaultView": {
"type": { "text": true }
},
"isOrtho": {
"type": { "bool": true }
"allowCameraUnder": {
"type": {
"bool": true
}
},
"isGhost": {
"type": { "bool": true }
"zoomOnDataChange": {
"type": {
"bool": true
}
},
"projection": {
"type": {
"enumeration": [
{
"displayName": "Perspective",
"value": "perspective"
},
{
"displayName": "Orthographic",
"value": "orthographic"
}
]
}
}
}
},
+4 -6
View File
@@ -31,8 +31,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",
@@ -14715,10 +14714,9 @@
}
},
"node_modules/vue-tippy": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/vue-tippy/-/vue-tippy-6.7.1.tgz",
"integrity": "sha512-gdHbBV5/Vc8gH87hQHLA7TN1K4BlLco3MAPrTb70ZYGXxx+55rAU4a4mt0fIoP+gB3etu1khUZ6c29Br1n0CiA==",
"license": "MIT",
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/vue-tippy/-/vue-tippy-6.5.0.tgz",
"integrity": "sha512-U44UDETTLuZWZGosagslEwgimWQdt1JVSxfWStVPnVdeqo2jo9X5zW3SB04k7JaTFosdgrDhFsUDrd6n42Nh7Q==",
"dependencies": {
"tippy.js": "^6.3.7"
},
+1 -2
View File
@@ -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",
-25
View File
@@ -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>
@@ -2,20 +2,12 @@
<div class="space-y-2">
<ViewerControlsButtonGroup>
<!-- Zoom extend -->
<ViewerControlsButtonToggle flat tooltip="Zoom extends" @click="onZoomExtentsClicked">
<ViewerControlsButtonToggle v-tippy="'Zoom extends'" flat @click="onZoomExtentsClicked">
<ArrowsPointingOutIcon class="h-4 w-4 md:h-5 md:w-5" />
</ViewerControlsButtonToggle>
<!-- Ghost / Hidden -->
<ViewerControlsButtonToggle
:tooltip="
visualStore.isGhostActive
? 'Hide ghosted objects on filter'
: 'Show ghosted objects on filter'
"
flat
@click="toggleGhostHidden"
>
<Ghost v-if="visualStore.isGhostActive" class="h-5 w-5" />
<ViewerControlsButtonToggle flat @click="toggleGhostHidden">
<Ghost v-if="isGhost" class="h-5 w-5" />
<Ghost v-else class="h-5 w-5 opacity-30" />
</ViewerControlsButtonToggle>
</ViewerControlsButtonGroup>
@@ -39,11 +31,10 @@
<ViewerControlsButtonToggle
flat
secondary
tooltip="Projection"
:active="visualStore.isOrthoProjection"
:active="isOrthoProjection"
@click="toggleProjection"
>
<Perspective v-if="visualStore.isOrthoProjection" class="h-3.5 md:h-4 w-4" />
<Perspective v-if="isOrthoProjection" class="h-3.5 md:h-4 w-4" />
<PerspectiveMore v-else class="h-3.5 md:h-4 w-4" />
</ViewerControlsButtonToggle>
</ViewerControlsButtonGroup>
@@ -79,6 +70,9 @@ withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
sectionBox: false
})
const isOrthoProjection = ref(false)
const isGhost = ref(true)
type ActiveControl =
| 'none'
| 'viewModes'
@@ -100,15 +94,13 @@ const toggleActiveControl = (control: ActiveControl) => {
}
const toggleProjection = () => {
isOrthoProjection.value = !isOrthoProjection.value
visualStore.viewerEmit('toggleProjection')
visualStore.setIsOrthoProjection(!visualStore.isOrthoProjection)
visualStore.writeIsOrthoToFile()
}
const toggleGhostHidden = () => {
visualStore.setIsGhost(!visualStore.isGhostActive)
visualStore.viewerEmit('toggleGhostHidden', visualStore.isGhostActive)
visualStore.writeIsGhostToFile()
isGhost.value = !isGhost.value
visualStore.viewerEmit('toggleGhostHidden', isGhost.value)
}
const viewModesOpen = computed({
@@ -1,130 +1,76 @@
<template>
<div class="border">
<transition name="slide-fade">
<nav
v-show="!isNavbarCollapsed"
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"
<transition name="slide-fade">
<nav
v-show="!isNavbarCollapsed"
class="fixed top-0 h-9 flex items-center bg-foundation border-b border-outline-2 w-full transition z-20 shadow-sm hover:shadow cursor-default"
>
<div class="flex items-center transition-all justify-between w-full">
<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 class="flex items-center">
<div class="font-thin text-xs mr-2 text-gray-400">v1.0.0</div>
<button
class="text-gray-400 hover:text-gray-700 transition"
title="Hide navbar"
@click="isNavbarCollapsed = true"
>
<WorkspaceAvatar
:name="visualStore.receiveInfo.workspaceName"
:logo="visualStore.receiveInfo.workspaceLogo"
></WorkspaceAvatar>
<div class="min-w-0 truncate flex-grow text-left text-xs">
<span>{{ visualStore.receiveInfo.workspaceName }}</span>
</div>
</div>
<div v-else>
<div class="flex items-center hover:cursor-pointer" @click="goToSpeckleWebsite">
<div class="max-[200px]:hidden block ml-2">
<img class="w-6 h-auto ml-1 mr-2 my-1" src="@assets/logo-big.png" />
</div>
<div class="font-sans font-medium">Speckle</div>
</div>
</div>
<div class="flex items-center space-x-2">
<FormButton
v-if="visualStore.latestAvailableVersion && !visualStore.isConnectorUpToDate"
v-tippy="{
content: 'New connector version is available.<br>Click to download.',
allowHTML: true
}"
color="outline"
size="sm"
@click="visualStore.downloadLatestVersion"
>
Update
</FormButton>
<div class="font-thin text-xs text-gray-400">
v{{ visualStore.receiveInfo.version }}
</div>
<button
class="text-gray-400 hover:text-gray-700 transition"
title="Hide navbar"
@click="isNavbarCollapsed = true"
>
<ChevronUpIcon class="w-4 h-4" />
</button>
</div>
<ChevronUpIcon class="w-4 h-4" />
</button>
</div>
</nav>
</transition>
</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="isNavbarCollapsed ? 'top-1' : 'top-11'"
>
<strong>Object IDs</strong>
field is needed for interactivity with other visuals.
</div>
<!-- TODO: another transition here needed that below components - but this time it will move to left -->
<div v-if="isNavbarCollapsed" class="fixed top-0 right-0 z-20">
<button
class="transition opacity-50 hover:opacity-100"
title="Show navbar"
@click="isNavbarCollapsed = false"
>
<ChevronDownIcon class="w-4 h-4 text-gray-400" />
</button>
</div>
<transition name="slide-left">
<ViewerControls
v-show="!isNavbarCollapsed"
v-model:section-box="bboxActive"
:views="views"
class="fixed top-11 left-2 z-30"
@view-clicked="(view) => viewerHandler.setView(view)"
@view-mode-clicked="(viewMode) => viewerHandler.setViewMode(viewMode)"
/>
</transition>
<div v-if="visualStore.isFilterActive" class="absolute bottom-5 left-1/2 -translate-x-1/2 z-50">
<FormButton size="sm" @click="visualStore.resetFilters(), selectionHandler.reset()">
Reset filters
</FormButton>
</div>
<div
class="absolute z-10 flex items-center text-xs cursor-pointer"
:class="visualStore.isBrandingHidden ? 'bottom-0 right-0' : 'bottom-2 right-2'"
@click.stop="goToSpeckleWebsite"
>
<!-- TODO: fade bottom here as transition -->
<transition name="fade-bottom">
<div
v-if="!visualStore.isBrandingHidden"
class="flex items-center justify-center font-thin"
>
<div class="">Powered by</div>
<img class="w-4 h-auto mx-1" src="@assets/logo-big.png" />
<div class="font-medium">Speckle</div>
</div>
</transition>
<button
v-if="visualStore.receiveInfo && visualStore.receiveInfo.canHideBranding"
class="transition opacity-50 hover:opacity-100 ml-1"
:title="visualStore.isBrandingHidden ? '' : 'Hide branding'"
@click.stop="visualStore.toggleBranding()"
>
<ChevronUpIcon v-if="visualStore.isBrandingHidden" class="w-4 h-4 text-gray-400" />
<ChevronDownIcon v-else class="w-4 h-4" />
</button>
</div>
<div
ref="container"
class="fixed h-full w-full z-0 cursor-default"
@click="onCanvasClick"
@auxclick="onCanvasAuxClick"
/>
<div
v-if="!isInteractive"
class="absolute top-1 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"
>
<strong>Object IDs</strong>
field is needed for interactivity with other visuals.
</div>
<div v-if="isNavbarCollapsed" class="fixed top-2 right-0 z-20">
<button
class="transition opacity-50 hover:opacity-100"
title="Show navbar"
@click="isNavbarCollapsed = false"
>
<ChevronDownIcon class="w-4 h-4 text-gray-400" />
</button>
</div>
<!-- till here -->
<transition name="slide-left">
<ViewerControls
v-show="!isNavbarCollapsed"
v-model:section-box="bboxActive"
:views="views"
class="fixed top-11 left-1 z-30"
@view-clicked="(view) => viewerHandler.setView(view)"
@view-mode-clicked="(viewMode) => viewerHandler.setViewMode(viewMode)"
/>
</transition>
<div v-if="visualStore.isFilterActive" class="absolute bottom-5 left-1/2 -translate-x-1/2 z-50">
<FormButton size="sm" @click="visualStore.resetFilters(), selectionHandler.reset()">
Reset filters
</FormButton>
</div>
<div
ref="container"
class="fixed h-full w-full z-0"
@click="onCanvasClick"
@auxclick="onCanvasAuxClick"
/>
</template>
<script async setup lang="ts">
@@ -138,7 +84,6 @@ 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()
@@ -236,19 +181,4 @@ async function onCanvasAuxClick(ev: MouseEvent) {
opacity: 0;
transform: translateX(-20px);
}
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: all 0.3s ease;
}
.fade-bottom-enter-from,
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10px);
}
.fade-bottom-enter-to,
.fade-bottom-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>
@@ -1,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,6 +1,5 @@
<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'
}`"
@@ -16,7 +15,6 @@ const props = defineProps<{
active?: boolean
flat?: boolean
secondary?: boolean
tooltip?: string
}>()
const colorClasses = computed(() => {
@@ -1,6 +1,6 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<ViewerMenu v-model:open="open" title="View modes">
<ViewerMenu v-model:open="open" tooltip="View modes">
<template #trigger-icon>
<ViewModes class="h-5 w-5" />
</template>
@@ -1,6 +1,6 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<ViewerMenu v-model:open="open" title="Views">
<ViewerMenu v-model:open="open" tooltip="Views">
<template #trigger-icon>
<Views class="w-5 h-5" />
</template>
@@ -1,7 +1,7 @@
<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"
class="h-full w-full bg-cover bg-center bg-no-repeat flex items-center justify-center"
:style="logo ? { backgroundImage: `url('${logo}')` } : {}"
>
<span v-if="!logo" class="text-foreground-3 uppercase leading-none">
@@ -1,55 +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
const sanitizedVersion = sanitizeVersion(sortedVersions[0].Number)
latestAvailableVersion.value = { ...sortedVersions[0], Number: sanitizedVersion }
visualStore.setLatestAvailableVersion(latestAvailableVersion.value)
}
function sanitizeVersion(version: string): string {
const match = version.match(/\d+\.\d+\.\d+/)
return match ? match[0] : version // fallback to original version
}
return { checkUpdate }
}
+16 -31
View File
@@ -11,9 +11,7 @@ import {
Viewer,
HybridCameraController,
SelectionExtension,
FilteringExtension,
UpdateFlags,
ViewerEvent
FilteringExtension
} from '@speckle/viewer'
import { SpeckleObjectsOfflineLoader } from '@src/laoder/SpeckleObjectsOfflineLoader'
import { useVisualStore } from '@src/store/visualStore'
@@ -37,7 +35,7 @@ export interface Hit {
export interface IViewerEvents {
ping: (message: string) => void
setSelection: (objectIds: string[]) => void
resetFilter: (objectIds: string[], ghost: boolean) => void
resetFilter: (objectIds: string[]) => void
filterSelection: (objectIds: string[], ghost: boolean) => void
setViewMode: (viewMode: ViewMode) => void
colorObjectsByGroup: (
@@ -91,14 +89,15 @@ export class ViewerHandler {
this.filtering = this.viewer.getExtension(FilteringExtension)
this.selection = this.viewer.getExtension(SelectionExtension)
const store = useVisualStore()
if (store.isOrthoProjection) {
this.cameraControls.toggleCameras()
}
this.viewer.on(ViewerEvent.LoadComplete, (arg: string) => {
store.clearLoadingProgress()
})
// NOTE: storing camera position into file triggers `update` function. even if I early return according to flag - it slows down the usage a lot.
// this.cameraControls.on(CameraEvent.Stationary, () => {
// console.log('🎬 Storing the camera position into file')
// const cameraController = this.viewer.getExtension(CameraController)
// const position = cameraController.getPosition()
// const target = cameraController.getTarget()
// const store = useVisualStore()
// store.writeCameraPositionToFile(position, target)
// })
}
emit<E extends keyof IViewerEvents>(event: E, ...payload: Parameters<IViewerEvents[E]>): void {
@@ -110,16 +109,10 @@ export class ViewerHandler {
this.cameraControls.setCameraView(objectIds, animate)
}
public zoomExtends = () => {
this.cameraControls.setCameraView(undefined, true)
this.viewer.requestRender(UpdateFlags.RENDER_RESET)
}
public zoomExtends = () => this.cameraControls.setCameraView(undefined, false)
public toggleProjection = () => this.cameraControls.toggleCameras()
public setView = (view: CanonicalView) => {
this.cameraControls.setCameraView(view, false)
this.snapshotCameraPositionAndStore()
}
public setView = (view: CanonicalView) => this.cameraControls.setCameraView(view, false)
public setSectionBox = (bboxActive: boolean, objectIds: string[]) => {
// TODO
@@ -131,15 +124,6 @@ export class ViewerHandler {
viewModes.setViewMode(viewMode)
}
public snapshotCameraPositionAndStore = () => {
console.log('🎬 Storing the camera position into file')
const cameraController = this.viewer.getExtension(CameraController)
const position = cameraController.getPosition()
const target = cameraController.getTarget()
const store = useVisualStore()
store.writeCameraPositionToFile(position, target)
}
public selectObjects = (objectIds: string[]) => {
console.log('🔗 Handling setSelection inside ViewerHandler:', objectIds)
if (objectIds) {
@@ -156,10 +140,10 @@ export class ViewerHandler {
}
}
public resetFilter = (objectIds: string[], ghost: boolean) => {
public resetFilter = (objectIds: string[]) => {
console.log('🔗 Handling filterSelection inside ViewerHandler')
if (objectIds) {
this.isolateObjects(objectIds, ghost)
this.isolateObjects(objectIds, true)
this.zoomObjects(objectIds, true)
}
}
@@ -243,6 +227,7 @@ export class ViewerHandler {
sourceHostApp: store.receiveInfo.sourceApplication,
workspace_id: store.receiveInfo.workspaceId
})
// camera need to be set after objects loaded
if (store.cameraPosition) {
const position = new Vector3(
store.cameraPosition[0],
+27 -139
View File
@@ -1,5 +1,4 @@
import { CanonicalView, SpeckleView, ViewMode } from '@speckle/viewer'
import { Version } from '@src/composables/useUpdateConnector'
import { ColorBy, IViewerEvents } from '@src/plugins/viewer'
import { SpeckleVisualSettingsModel } from '@src/settings/visualSettingsModel'
import { SpeckleDataInput } from '@src/types'
@@ -7,7 +6,7 @@ import { zipModelObjects } from '@src/utils/compression'
import { ReceiveInfo } from '@src/utils/matrixViewUtils'
import { defineStore } from 'pinia'
import { Vector3 } from 'three'
import { computed, ref, shallowRef } from 'vue'
import { ref, shallowRef } from 'vue'
export type InputState = 'valid' | 'incomplete' | 'invalid'
@@ -18,25 +17,16 @@ export type FieldInputState = {
tooltipData: boolean
}
export type LoadingProgress = { summary: string; progress: number; step?: string }
export const useVisualStore = defineStore('visualStore', () => {
const latestAvailableVersion = ref<Version | null>(null)
const host = shallowRef<powerbi.extensibility.visual.IVisualHost>()
const formattingSettings = ref<SpeckleVisualSettingsModel>()
const loadingProgress = ref<LoadingProgress>(undefined)
const loadingProgress = ref<{ summary: string; progress: number }>(undefined)
const objectsFromStore = ref<object[]>(undefined)
const postFileSaveSkipNeeded = ref<boolean>(false)
const postClickSkipNeeded = ref<boolean>(false)
const isFilterActive = ref<boolean>(false)
const isBrandingHidden = ref<boolean>(false)
const isOrthoProjection = ref<boolean>(false)
const isGhostActive = ref<boolean>(true)
const commonError = ref<string>(undefined)
// once you see this shit, you might freak out and you are right. All of them needed because of "update" function trigger by API.
// most of the time we need to know what we are doing to treat operations accordingly. Ask for more to me (Ogu), but the answers will make both of us unhappy.
@@ -83,17 +73,6 @@ export const useVisualStore = defineStore('visualStore', () => {
const setReceiveInfo = (newReceiveInfo: ReceiveInfo) => (receiveInfo.value = newReceiveInfo)
const setLatestAvailableVersion = (version: Version | null) => {
latestAvailableVersion.value = version
}
const isConnectorUpToDate = computed(() => {
if (receiveInfo.value && receiveInfo.value.version) {
return receiveInfo.value.version === latestAvailableVersion.value?.Number
}
return false
})
/**
* Ideally one time set when onMounted of `ViewerWrapper.vue` component
* @param emit picky emit function to trigger events under `IViewerEvents` interface
@@ -122,9 +101,7 @@ export const useVisualStore = defineStore('visualStore', () => {
}
}
const clearLoadingProgress = () => {
loadingProgress.value = undefined
}
const clearLoadingProgress = () => (loadingProgress.value = undefined)
// MAKE TS HAPPY
type SpeckleObject = {
@@ -137,6 +114,7 @@ export const useVisualStore = defineStore('visualStore', () => {
viewerReloadNeeded.value = false
console.log(`📦 Loading viewer from cached data with ${lastLoadedRootObjectId.value} id.`)
await viewerEmit.value('loadObjects', objects)
clearLoadingProgress()
objectsFromStore.value = objects
isViewerObjectsLoaded.value = true
viewerReloadNeeded.value = false
@@ -155,20 +133,19 @@ export const useVisualStore = defineStore('visualStore', () => {
lastLoadedRootObjectId.value = modelIds
console.log(`🔄 Forcing viewer re-render for new root object id.`)
await viewerEmit.value('loadObjects', dataInput.value.modelObjects)
clearLoadingProgress()
viewerReloadNeeded.value = false
isViewerObjectsLoaded.value = true
setLoadingProgress('Storing objects into file', null)
writeObjectsToFile(dataInput.value.modelObjects)
loadingProgress.value = undefined
}
if (dataInput.value.selectedIds.length > 0) {
isFilterActive.value = true
viewerEmit.value('filterSelection', dataInput.value.selectedIds, isGhostActive.value)
viewerEmit.value('filterSelection', dataInput.value.selectedIds, true)
} else {
isFilterActive.value = false
latestColorBy.value = dataInput.value.colorByIds
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
viewerEmit.value('resetFilter', dataInput.value.objectIds)
}
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
@@ -208,38 +185,6 @@ export const useVisualStore = defineStore('visualStore', () => {
})
}
const writeIsOrthoToFile = () => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'camera',
properties: {
isOrtho: isOrthoProjection.value
},
selector: null
}
]
})
}
const writeIsGhostToFile = () => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'camera',
properties: {
isGhost: isGhostActive.value
},
selector: null
}
]
})
}
const writeViewModeToFile = (viewMode: ViewMode) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
@@ -256,41 +201,25 @@ export const useVisualStore = defineStore('visualStore', () => {
})
}
const writeHideBrandingToFile = (brandingHidden: boolean) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'workspace',
properties: {
brandingHidden: brandingHidden
},
selector: null
}
]
})
}
const writeCameraPositionToFile = (position: Vector3, target: Vector3) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'cameraPosition',
properties: {
positionX: position.x,
positionY: position.y,
positionZ: position.z,
targetX: target.x,
targetY: target.y,
targetZ: target.z
},
selector: null
}
]
})
// postFileSaveSkipNeeded.value = true
// host.value.persistProperties({
// merge: [
// {
// objectName: 'cameraPosition',
// properties: {
// positionX: position.x,
// positionY: position.y,
// positionZ: position.z,
// targetX: target.x,
// targetY: target.y,
// targetZ: target.z
// },
// selector: null
// }
// ]
// })
}
const setFieldInputState = (newFieldInputState: FieldInputState) =>
@@ -300,27 +229,10 @@ export const useVisualStore = defineStore('visualStore', () => {
const setIsLoadingFromFile = (newValue: boolean) => (isLoadingFromFile.value = newValue)
const setViewerReadyToLoad = (newValue: boolean) => (isViewerReadyToLoad.value = newValue)
const setViewerReadyToLoad = () => (isViewerReadyToLoad.value = true)
const setViewerReloadNeeded = () => (viewerReloadNeeded.value = true)
const toggleBranding = () => {
isBrandingHidden.value = !isBrandingHidden.value
writeHideBrandingToFile(isBrandingHidden.value)
}
const setBrandingHidden = (val: boolean) => {
isBrandingHidden.value = val
}
const setIsOrthoProjection = (val: boolean) => {
isOrthoProjection.value = val
}
const setIsGhost = (val: boolean) => {
isGhostActive.value = val
}
const setPostFileSaveSkipNeeded = (newValue: boolean) => (postFileSaveSkipNeeded.value = newValue)
const setPostClickSkipNeeded = (newValue: boolean) => (postClickSkipNeeded.value = newValue)
@@ -332,21 +244,13 @@ export const useVisualStore = defineStore('visualStore', () => {
(formattingSettings.value = newFormattingSettings)
const resetFilters = () => {
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
viewerEmit.value('resetFilter', dataInput.value.objectIds)
if (latestColorBy.value !== null) {
viewerEmit.value('colorObjectsByGroup', latestColorBy.value)
}
isFilterActive.value = false
}
const downloadLatestVersion = () => {
host.value.launchUrl(latestAvailableVersion.value?.Url as string)
}
const setCommonError = (error: string) => {
commonError.value = error
}
return {
host,
receiveInfo,
@@ -370,18 +274,7 @@ export const useVisualStore = defineStore('visualStore', () => {
isFilterActive,
latestColorBy,
formattingSettings,
isBrandingHidden,
isOrthoProjection,
isGhostActive,
latestAvailableVersion,
isConnectorUpToDate,
commonError,
setCommonError,
setLatestAvailableVersion,
setIsOrthoProjection,
setIsGhost,
setFormattingSettings,
setBrandingHidden,
setPostClickSkipNeeded,
setPostFileSaveSkipNeeded,
setCameraPositionInFile,
@@ -394,12 +287,8 @@ export const useVisualStore = defineStore('visualStore', () => {
setObjectsFromStore,
writeObjectsToFile,
writeCameraViewToFile,
writeIsGhostToFile,
writeIsOrthoToFile,
writeViewModeToFile,
writeCameraPositionToFile,
writeHideBrandingToFile,
toggleBranding,
setViewerEmitter,
setDataInput,
setFieldInputState,
@@ -408,7 +297,6 @@ export const useVisualStore = defineStore('visualStore', () => {
setLoadingProgress,
clearLoadingProgress,
setIsLoadingFromFile,
resetFilters,
downloadLatestVersion
resetFilters
}
})
@@ -10,7 +10,6 @@ import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
import { FieldInputState, useVisualStore } from '@src/store/visualStore'
import { delay } from 'lodash'
import { getSlugFromHostAppNameAndVersion } from './hostAppSlug'
import { useUpdateConnector } from '@src/composables/useUpdateConnector'
export class AsyncPause {
private lastPauseTime = 0
@@ -160,40 +159,6 @@ export type ReceiveInfo = {
version?: string
}
export type PreGetObjects = {
modelExists: boolean
objectCount?: number
}
async function getPreGetObjects(commaSeparatedModelIds: string): Promise<PreGetObjects[]> {
const modelIds = (commaSeparatedModelIds as string).split(',')
const preGetObjects = []
for await (const id of modelIds) {
const res = await getPreGetObjectsForModel(id)
preGetObjects.push(res)
}
return preGetObjects
}
async function getPreGetObjectsForModel(id: string): Promise<PreGetObjects> {
try {
const preGetObjectsRes = await fetch(`http://localhost:29364/pre-get-objects/${id}`)
if (!preGetObjectsRes.body) {
console.log('No response body for pre get objects')
return {
modelExists: false,
objectCount: null
} as PreGetObjects
}
return (await preGetObjectsRes.json()) as PreGetObjects
} catch (error) {
console.log(error)
}
}
async function getReceiveInfo(id) {
try {
const ids = (id as string).split(',')
@@ -210,29 +175,19 @@ async function getReceiveInfo(id) {
}
}
async function fetchStreamedData(commaSeparatedModelIds: string, totalObjectCount: number) {
async function fetchStreamedData(commaSeparatedModelIds: string) {
const modelIds = (commaSeparatedModelIds as string).split(',')
const modelObjects = []
let loadedObjectCount = 0
for await (const id of modelIds) {
const objects = await fetchStreamedDataForModel(id, totalObjectCount, loadedObjectCount)
const objects = await fetchStreamedDataForModel(id)
modelObjects.push(objects)
loadedObjectCount += objects.length
}
return modelObjects
}
async function fetchStreamedDataForModel(
id: string,
totalObjectCount: number,
loadedObjectCount: number
) {
console.log(loadedObjectCount, totalObjectCount)
async function fetchStreamedDataForModel(id) {
try {
const visualStore = useVisualStore()
const response = await fetch(`http://localhost:29364/get-objects/${id}`)
if (!response.body) {
@@ -259,11 +214,6 @@ async function fetchStreamedDataForModel(
try {
const obj = JSON.parse(jsonString)
objects.push(obj)
visualStore.setLoadingProgress(
'Loading objects from storage',
(objects.length + loadedObjectCount) / totalObjectCount
)
// console.log('Loading', (objects.length + loadedObjectCount) / totalObjectCount)
// console.log('Received object:', jsonObject)
} catch (e) {
@@ -353,16 +303,11 @@ export async function processMatrixView(
if (visualStore.lastLoadedRootObjectId !== id && !visualStore.isLoadingFromFile) {
const start = performance.now()
visualStore.setViewerReadyToLoad()
visualStore.setLoadingProgress('Loading', null)
const getPreGetObjectsRes: PreGetObjects[] = await getPreGetObjects(id)
if (getPreGetObjectsRes.some((preGetObjects) => preGetObjects.modelExists === false)) {
visualStore.setCommonError(
'Version Object ID is not found in storage. Please make sure you placed correct field or consider refreshing your data via data connector.'
)
visualStore.setViewerReadyToLoad(false)
return
}
// stream data
modelObjects = await fetchStreamedData(id)
const receiveInfo = await getReceiveInfo(id)
if (receiveInfo) {
@@ -376,25 +321,11 @@ export async function processMatrixView(
version: receiveInfo.version,
canHideBranding: receiveInfo.canHideBranding
})
console.log(`Receive info retrieved from desktop service`, receiveInfo)
}
const totalObjectCount = getPreGetObjectsRes.reduce((sum, obj) => {
return sum + (obj.objectCount ?? 0)
}, 0)
visualStore.setViewerReadyToLoad(true)
// stream data
modelObjects = await fetchStreamedData(id, totalObjectCount)
visualStore.setViewerReloadNeeded() // they should be marked as deferred action bc of update function complexity.
visualStore.setLoadingProgress('Loading objects into viewer', null)
console.log(`🚀 Upload is completed in ${(performance.now() - start) / 1000} s!`)
}
if (visualStore.receiveInfo && visualStore.receiveInfo.version) {
const { checkUpdate } = useUpdateConnector()
await checkUpdate()
console.log(`🚀 Upload is completed in ${(performance.now() - start) / 1000} s!`)
}
// If colors assigned, data arrives nested
@@ -1,7 +1,19 @@
<template>
<div
v-if="visualStore.loadingProgress"
class="absolute top-1/2 left-1/2 w-1/2 -translate-x-1/2 z-20 text-center text-sm"
>
<!-- Progress Bar -->
<LoadingBar :loading="!!visualStore.loadingProgress"></LoadingBar>
</div>
<viewer-wrapper id="speckle-3d-view" class="h-full w-full cursor-default"></viewer-wrapper>
</template>
<script setup lang="ts">
import ViewerWrapper from 'src/components/ViewerWrapper.vue'
import { useVisualStore } from '../store/visualStore'
import LoadingBar from '@src/components/loading/LoadingBar.vue'
const visualStore = useVisualStore()
</script>
+3 -51
View File
@@ -4,7 +4,6 @@ import '../style/visual.css'
import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel'
import { createApp } from 'vue'
import App from './App.vue'
import VueTippy from 'vue-tippy'
import { selectionHandlerKey, tooltipHandlerKey } from 'src/injectionKeys'
import { SpeckleDataInput } from './types'
@@ -47,11 +46,6 @@ export class Visual implements IVisual {
console.log('🚀 Init Vue App')
createApp(App)
.use(pinia)
.use(VueTippy, {
defaultProps: {
theme: 'custom'
}
})
// .use(store, storeKey)
.provide(selectionHandlerKey, this.selectionHandler)
.provide(tooltipHandlerKey, this.tooltipHandler)
@@ -69,10 +63,6 @@ export class Visual implements IVisual {
public async update(options: VisualUpdateOptions) {
const visualStore = useVisualStore()
if (visualStore.commonError) {
visualStore.setCommonError(undefined)
visualStore.setViewerReadyToLoad(false)
}
if (visualStore.postFileSaveSkipNeeded) {
visualStore.setPostFileSaveSkipNeeded(false)
@@ -136,20 +126,7 @@ export class Visual implements IVisual {
)
}
if (options.dataViews[0].metadata.objects.workspace?.brandingHidden as boolean) {
console.log(
`Branding Hidden: ${
options.dataViews[0].metadata.objects.workspace?.brandingHidden as boolean
}`
)
visualStore.setBrandingHidden(
options.dataViews[0].metadata.objects.workspace?.brandingHidden as boolean
)
}
if (options.dataViews[0].metadata.objects.cameraPosition?.positionX as string) {
console.log(`Stored camera position is found`)
visualStore.setCameraPositionInFile([
Number(options.dataViews[0].metadata.objects.cameraPosition?.positionX),
Number(options.dataViews[0].metadata.objects.cameraPosition?.positionY),
@@ -159,31 +136,6 @@ export class Visual implements IVisual {
Number(options.dataViews[0].metadata.objects.cameraPosition?.targetZ)
])
}
const camera = options.dataViews[0].metadata.objects.camera
if (camera && 'isOrtho' in camera) {
console.log(
`Projection is ortho?: ${
options.dataViews[0].metadata.objects.camera?.isOrtho as boolean
}`
)
visualStore.setIsOrthoProjection(
options.dataViews[0].metadata.objects.camera?.isOrtho as boolean
)
}
if (camera && 'isGhost' in camera) {
console.log(
`Is ghost?: ${options.dataViews[0].metadata.objects.camera?.isGhost as boolean}`
)
visualStore.setIsGhost(
options.dataViews[0].metadata.objects.camera?.isGhost as boolean
)
}
// get receive info from file for mixpanel
try {
const receiveInfoFromFile = JSON.parse(
@@ -194,8 +146,8 @@ export class Visual implements IVisual {
console.warn(error)
console.log('missing mixpanel info')
}
const savedVersionObjectId = objectsFromFile.map((o) => o[0].id).join(',')
if (visualStore.lastLoadedRootObjectId !== savedVersionObjectId) {
this.tryReadFromFile(objectsFromFile, visualStore)
}
@@ -246,7 +198,7 @@ export class Visual implements IVisual {
const visualStore = useVisualStore()
this.tooltipHandler.setup(input.objectTooltipData)
visualStore.setViewerReadyToLoad(true)
visualStore.setViewerReadyToLoad()
if (visualStore.isViewerInitialized && !visualStore.viewerReloadNeeded) {
visualStore.setDataInput(input)
@@ -260,7 +212,7 @@ export class Visual implements IVisual {
}
private tryReadFromFile(objectsFromFile: object[][], visualStore) {
visualStore.setViewerReadyToLoad(true)
visualStore.setViewerReadyToLoad()
visualStore.setIsLoadingFromFile(true) // to block unnecessary streaming data if bg service is running
setTimeout(() => {
visualStore.loadObjectsFromFile(objectsFromFile)