Compare commits
62 Commits
2.0.0-alpha2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 15d79e6606 | |||
| bc5d16dfb8 | |||
| e445d27b01 | |||
| 75c8a60cef | |||
| 0f034c17d0 | |||
| bded971ecf | |||
| 77b13c2d89 | |||
| e3855a71c1 | |||
| 90c22211a9 | |||
| b13eef0b18 | |||
| 31c75f5407 | |||
| cd75dca5d7 | |||
| 72f1a836cb | |||
| 0232c91d42 | |||
| 38b2c55166 | |||
| fac7d8e547 | |||
| b19986ec35 | |||
| 3430cba29a | |||
| 6b57415d10 | |||
| 653dfb9910 | |||
| 30dbd19c52 | |||
| cfc958f9fd | |||
| 9cf6786e52 | |||
| 7ad8cd7e24 | |||
| 71cbd55583 | |||
| c3ca9c80dd | |||
| 1966669a74 | |||
| 12c7fbdf64 | |||
| d7772ca558 | |||
| cc0113b8f9 | |||
| 0e05ad8584 | |||
| 1b55d39787 | |||
| c7066c2242 | |||
| 67cae270b6 | |||
| 1e417a6720 | |||
| 51f17d476a | |||
| 124e1f186c | |||
| de5154b41d | |||
| 895b9bf688 | |||
| 956a7f94ee | |||
| 569baecc3e | |||
| c491f298c5 | |||
| ec9f9c7cd8 | |||
| 7e35700cfa | |||
| 39cfa33baf | |||
| a1fbba71e4 | |||
| 10b8a68e32 | |||
| 9e00cc85a8 | |||
| 047f763465 | |||
| fa33719902 | |||
| d60f0ba2b6 | |||
| aac875664d | |||
| ae6231f5f1 | |||
| d839573d96 | |||
| 50c7118bff | |||
| d21033cd85 | |||
| 6240ec724f | |||
| 3248c5ad14 | |||
| 917c7ee8a6 | |||
| 17c1f1c3f2 | |||
| 3bcc8c34d6 | |||
| e9834636e6 |
@@ -0,0 +1,37 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
const config = {
|
||||
root: true,
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
requireConfigFile: false,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier'
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
commonjs: true
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'public',
|
||||
'events.json',
|
||||
'.*.{ts,js,vue,tsx,jsx}',
|
||||
'generated/**/*'
|
||||
],
|
||||
rules: {
|
||||
'no-var': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@@ -1,78 +0,0 @@
|
||||
name: Update issue Status
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
update_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
echo "$PROJECT_ID"
|
||||
echo "$STATUS_FIELD_ID"
|
||||
|
||||
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo "$DONE_ID"
|
||||
|
||||
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Update Status
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
|
||||
set_status: updateProjectNextItemField(
|
||||
input: {
|
||||
projectId: $project
|
||||
itemId: $id
|
||||
fieldId: $status
|
||||
value: $value
|
||||
}
|
||||
) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Move new issues into Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add Issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
+4
-1
@@ -3,4 +3,7 @@ dist/
|
||||
webpack.statistics.dev.html
|
||||
.tmp/
|
||||
webpack.statistics.prod.html
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.idea/
|
||||
webpack.statistics.html
|
||||
**/Thumbs.db
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"endOfLine": "auto",
|
||||
"bracketSpacing": true,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
}
|
||||
Vendored
+32
-37
@@ -1,40 +1,35 @@
|
||||
{
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
"files.eol": "\n",
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/node_modules/**": true,
|
||||
".tmp": true
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
"files.eol": "\n",
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/node_modules/**": true,
|
||||
".tmp": true
|
||||
},
|
||||
"files.exclude": {
|
||||
".tmp": true
|
||||
},
|
||||
"files.associations": {
|
||||
"*.resjson": "json"
|
||||
},
|
||||
"search.exclude": {
|
||||
".tmp": true,
|
||||
"typings": true
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/pbiviz.json"],
|
||||
"url": "./node_modules/powerbi-visuals-api/schema.pbiviz.json"
|
||||
},
|
||||
"files.exclude": {
|
||||
".tmp": true
|
||||
{
|
||||
"fileMatch": ["/capabilities.json"],
|
||||
"url": "./node_modules/powerbi-visuals-api/schema.capabilities.json"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.resjson": "json"
|
||||
},
|
||||
"search.exclude": {
|
||||
".tmp": true,
|
||||
"typings": true
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"/pbiviz.json"
|
||||
],
|
||||
"url": "./node_modules/powerbi-visuals-api/schema.pbiviz.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/capabilities.json"
|
||||
],
|
||||
"url": "./node_modules/powerbi-visuals-api/schema.capabilities.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/dependencies.json"
|
||||
],
|
||||
"url": "./node_modules/powerbi-visuals-api/schema.dependencies.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"fileMatch": ["/dependencies.json"],
|
||||
"url": "./node_modules/powerbi-visuals-api/schema.dependencies.json"
|
||||
}
|
||||
],
|
||||
"vue.codeActions.enabled": false
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
# ⚠️⚠️⚠️ Note: This repository is no longer maintained and has been merged with the [Speckle Power BI](https://github.com/specklesystems/speckle-powerbi) repository. ⚠️⚠️⚠️
|
||||
|
||||
The functionalities and features of Speckle Power BI Visuals have been consolidated into the main [Speckle Power BI](https://github.com/specklesystems/speckle-powerbi) repository. Please visit the Speckle Power BI repository for the latest updates, installation instructions, and continued development.
|
||||
|
||||
|
||||
|
||||
<h1 align="center">
|
||||
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
|
||||
Speckle | PowerBI Visuals
|
||||
</h1>
|
||||
<h3 align="center">
|
||||
3D Viewer for PowerBI and more...
|
||||
Expected use case is that the visual displays data pulled from Speckle via the Speckle Data Connector for PowerBI (https://github.com/specklesystems/speckle-powerbi)
|
||||
</h3>
|
||||
|
||||
> ⚠️ This repo is still in very early stages of development, use at your own risk!
|
||||
> This repo is still in very early stages of development, use at your own risk!
|
||||
|
||||
<p align="center"><b>Speckle</b> is data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
@@ -34,7 +41,7 @@ What is Speckle? Check our ](https://speckle.xyz) ⇒ creating an account at
|
||||
- [](https://app.speckle.systems) ⇒ creating an account
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
|
||||
### Resources
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+143
-63
@@ -1,29 +1,54 @@
|
||||
{
|
||||
"dataRoles": [
|
||||
{
|
||||
"displayName": "Stream URL",
|
||||
"name": "stream",
|
||||
"kind": "Grouping"
|
||||
"displayName": "Model URL",
|
||||
"kind": "Grouping",
|
||||
"name": "stream"
|
||||
},
|
||||
{
|
||||
"displayName": "Version Object ID",
|
||||
"kind": "Grouping",
|
||||
"name": "parentObject"
|
||||
},
|
||||
{
|
||||
"displayName": "Object ID",
|
||||
"name": "object",
|
||||
"kind": "GroupingOrMeasure"
|
||||
"kind": "Grouping",
|
||||
"name": "object"
|
||||
},
|
||||
{
|
||||
"displayName": "Object Data",
|
||||
"name": "objectData",
|
||||
"kind": "Measure"
|
||||
"displayName": "Color By",
|
||||
"kind": "Grouping",
|
||||
"name": "objectColorBy"
|
||||
},
|
||||
{
|
||||
"displayName": "Tooltip Data",
|
||||
"kind": "Measure",
|
||||
"name": "objectData"
|
||||
}
|
||||
],
|
||||
"dataViewMappings": [
|
||||
{
|
||||
"categorical": {
|
||||
"categories": {
|
||||
"matrix": {
|
||||
"rows": {
|
||||
"dataReductionAlgorithm": {
|
||||
"top": {
|
||||
"count": 30000
|
||||
}
|
||||
},
|
||||
"select": [
|
||||
{
|
||||
"for": {
|
||||
"in": "stream"
|
||||
"bind": {
|
||||
"to": "stream"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bind": {
|
||||
"to": "parentObject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bind": {
|
||||
"to": "objectColorBy"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -34,74 +59,84 @@
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"for": { "in": "objectData" }
|
||||
"select": [
|
||||
{
|
||||
"bind": {
|
||||
"to": "objectData"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"supportsHighlight": true,
|
||||
"supportsMultiVisualSelection": true,
|
||||
"suppressDefaultTitle": true,
|
||||
"supportsSynchronizingFilterState": true,
|
||||
"supportsKeyboardFocus": true,
|
||||
"tooltips": {
|
||||
"supportEnhancedTooltips": true
|
||||
},
|
||||
"drilldown": {
|
||||
"roles": ["stream", "object"]
|
||||
},
|
||||
"objects": {
|
||||
"camera": {
|
||||
"displayName": "Camera",
|
||||
"properties": {
|
||||
"orthoMode": {
|
||||
"displayName": "Ortho mode",
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"defaultView": {
|
||||
"displayName": "Default view",
|
||||
"type": {
|
||||
"enumeration": [
|
||||
{
|
||||
"displayName": "Perspective",
|
||||
"displayNameKey": "perspective",
|
||||
"value": "perspective"
|
||||
},
|
||||
{
|
||||
"displayName": "Top",
|
||||
"displayNameKey": "top",
|
||||
"value": "top"
|
||||
},
|
||||
{
|
||||
"displayName": "Front",
|
||||
"displayNameKey": "front",
|
||||
"value": "front"
|
||||
},
|
||||
{
|
||||
"displayName": "Left",
|
||||
"displayNameKey": "left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"displayName": "Back",
|
||||
"displayNameKey": "back",
|
||||
"value": "back"
|
||||
},
|
||||
{
|
||||
"displayName": "Right",
|
||||
"displayNameKey": "right",
|
||||
"value": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"allowCameraUnder": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"zoomOnDataChange": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"projection": {
|
||||
"type": {
|
||||
"enumeration": [
|
||||
{
|
||||
"displayName": "Perspective",
|
||||
"value": "perspective"
|
||||
},
|
||||
{
|
||||
"displayName": "Orthographic",
|
||||
"value": "orthographic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"displayName": "Color",
|
||||
"properties": {
|
||||
"startColor": {
|
||||
"displayName": "Start Color",
|
||||
"enabled": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"fill": {
|
||||
"type": {
|
||||
"fill": {
|
||||
"solid": {
|
||||
@@ -110,34 +145,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"midColor": {
|
||||
"displayName": "Middle Color",
|
||||
"context": {
|
||||
"type": {
|
||||
"fill": {
|
||||
"solid": {
|
||||
"color": true
|
||||
"enumeration": [
|
||||
{
|
||||
"displayName": "Hidden",
|
||||
"value": "hidden"
|
||||
},
|
||||
{
|
||||
"displayName": "Ghosted",
|
||||
"value": "ghosted"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lighting": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"endColor": {
|
||||
"displayName": "End Color",
|
||||
"intensity": {
|
||||
"type": {
|
||||
"fill": {
|
||||
"solid": {
|
||||
"color": true
|
||||
}
|
||||
}
|
||||
"numeric": true
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"displayName": "Background Color",
|
||||
"elevation": {
|
||||
"type": {
|
||||
"fill": {
|
||||
"solid": {
|
||||
"color": true
|
||||
}
|
||||
}
|
||||
"numeric": true
|
||||
}
|
||||
},
|
||||
"azimuth": {
|
||||
"type": {
|
||||
"numeric": true
|
||||
}
|
||||
},
|
||||
"indirect": {
|
||||
"type": {
|
||||
"numeric": true
|
||||
}
|
||||
},
|
||||
"shadows": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
},
|
||||
"shadowCatcher": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,19 +203,41 @@
|
||||
},
|
||||
"privileges": [
|
||||
{
|
||||
"name": "WebAccess",
|
||||
"essential": true,
|
||||
"name": "WebAccess",
|
||||
"parameters": [
|
||||
"https://speckle.xyz",
|
||||
"https://app.speckle.systems",
|
||||
"https://latest.speckle.systems",
|
||||
"https://*.speckle.xyz",
|
||||
"https://latest.speckle.dev",
|
||||
"https://*.speckle.dev",
|
||||
"https://analytics.speckle.systems",
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ExportContent",
|
||||
"essential": false
|
||||
"essential": false,
|
||||
"name": "ExportContent"
|
||||
},
|
||||
{
|
||||
"essential": true,
|
||||
"name": "LocalStorage",
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
],
|
||||
"sorting": {
|
||||
"default": {}
|
||||
},
|
||||
"supportsEmptyDataView": true,
|
||||
"supportsHighlight": true,
|
||||
"supportsKeyboardFocus": true,
|
||||
"supportsLandingPage": true,
|
||||
"keepAllMetadataColumns": true,
|
||||
"supportsMultiVisualSelection": true,
|
||||
"supportsSynchronizingFilterState": true,
|
||||
"suppressDefaultTitle": true,
|
||||
"tooltips": {
|
||||
"supportEnhancedTooltips": true
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+10760
-1560
File diff suppressed because it is too large
Load Diff
+66
-19
@@ -1,30 +1,77 @@
|
||||
{
|
||||
"name": "visual",
|
||||
"description": "default_template_value",
|
||||
"name": "@specklesystems/powerbi-visual",
|
||||
"description": "A 3D viewer for Speckle Object in PowerBI",
|
||||
"repository": {
|
||||
"type": "default_template_value",
|
||||
"url": "default_template_value"
|
||||
"type": "github",
|
||||
"url": "https://github.com/specklesystems/speckle-powerbi-visuals"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"pbiviz": "pbiviz",
|
||||
"start": "pbiviz start",
|
||||
"package": "pbiviz package",
|
||||
"lint": "tslint -c tslint.json -p tsconfig.json"
|
||||
"pack": "webpack --config webpack.config.ts",
|
||||
"build": "webpack --config webpack.config.dev.ts",
|
||||
"serve": "webpack-dev-server --config webpack.config.dev.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.6.0",
|
||||
"@babel/runtime-corejs2": "7.6.0",
|
||||
"@speckle/viewer": "^2.7.1",
|
||||
"core-js": "3.2.1",
|
||||
"powerbi-visuals-api": "~4.7.0",
|
||||
"powerbi-visuals-utils-dataviewutils": "2.4.1",
|
||||
"regenerator-runtime": "^0.13.9"
|
||||
"@babel/runtime": "^7.21.5",
|
||||
"@babel/runtime-corejs3": "^7.21.5",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/tailwind-theme": "2.14.7",
|
||||
"@speckle/ui-components": "2.14.7",
|
||||
"@speckle/viewer": "^2.18.14",
|
||||
"color-interpolate": "^1.0.5",
|
||||
"core-js": "^3.30.2",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss-loader": "^7.3.0",
|
||||
"postcss-preset-env": "^8.4.1",
|
||||
"powerbi-visuals-api": "~5.4.0",
|
||||
"powerbi-visuals-utils-colorutils": "^6.0.1",
|
||||
"powerbi-visuals-utils-dataviewutils": "^6.0.1",
|
||||
"powerbi-visuals-utils-formattingmodel": "^5.0.0",
|
||||
"powerbi-visuals-utils-interactivityutils": "^6.0.2",
|
||||
"powerbi-visuals-utils-tooltiputils": "^6.0.1",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-loader": "6.1.0",
|
||||
"tslint": "^5.18.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"typescript": "3.6.3"
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/eslint-parser": "^7.21.8",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^20.1.7",
|
||||
"@types/regenerator-runtime": "^0.13.1",
|
||||
"@types/three": "^0.152.0",
|
||||
"@types/webpack": "^5.28.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.2",
|
||||
"base64-inline-loader": "^2.0.1",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-vue": "^9.13.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"json-loader": "^0.5.7",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss-import": "^15.1.0",
|
||||
"powerbi-visuals-webpack-plugin": "^4.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"style-loader": "^3.3.2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.1",
|
||||
"typescript": "^5.0.4",
|
||||
"user-agent-data-types": "^0.3.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-loader": "^17.1.1",
|
||||
"vue-template-compiler": "^2.7.14",
|
||||
"webpack": "^5.83.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^5.1.1",
|
||||
"webpack-dev-server": "^4.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -4,16 +4,16 @@
|
||||
"displayName": "Speckle PowerBI Viewer",
|
||||
"guid": "powerbiSpeckleVisualAA98F06515D847E8ACB33BAB487244E0",
|
||||
"visualClassName": "Visual",
|
||||
"version": "2.0.0-alpha1",
|
||||
"version": "2.19.0",
|
||||
"description": "An interactive 3D viewer for Speckle Data",
|
||||
"supportUrl": "https://speckle.community",
|
||||
"gitHubUrl": "https://github.com/specklesystems/speckle-powerbi-visuals"
|
||||
},
|
||||
"apiVersion": "4.7.0",
|
||||
"apiVersion": "5.4.0",
|
||||
"author": { "name": "Speckle Systems", "email": "info@speckle.systems" },
|
||||
"assets": { "icon": "assets/logo.png" },
|
||||
"externalJS": null,
|
||||
"style": "style/visual.less",
|
||||
"externalJS": [],
|
||||
"style": "style/visual.css",
|
||||
"capabilities": "capabilities.json",
|
||||
"dependencies": null,
|
||||
"stringResources": []
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'postcss-nesting': {}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import HomeView from './views/HomeView.vue'
|
||||
import ViewerView from './views/ViewerView.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { storeKey } from 'src/injectionKeys'
|
||||
|
||||
let store = useStore(storeKey)
|
||||
let status = computed(() => {
|
||||
return store.state.status
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ViewerView v-if="status == 'valid'" />
|
||||
<HomeView v-else />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
VideoCameraIcon,
|
||||
CubeIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
PaintBrushIcon
|
||||
} from '@heroicons/vue/24/solid'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
import { CanonicalView, SpeckleView } from '@speckle/viewer'
|
||||
import ButtonToggle from 'src/components/controls/ButtonToggle.vue'
|
||||
import ButtonGroup from 'src/components/controls/ButtonGroup.vue'
|
||||
import ButtonSimple from 'src/components/controls/ButtonSimple.vue'
|
||||
import { inject, watch } from 'vue'
|
||||
import { hostKey, viewerHandlerKey } from 'src/injectionKeys'
|
||||
import { resetPalette } from 'src/utils/matrixViewUtils'
|
||||
|
||||
const emits = defineEmits(['update:sectionBox', 'view-clicked', 'clear-palette'])
|
||||
const props = withDefaults(defineProps<{ sectionBox: boolean; views: SpeckleView[] }>(), {
|
||||
sectionBox: false,
|
||||
views: () => []
|
||||
})
|
||||
const viewerHandler = inject(viewerHandlerKey)
|
||||
const canonicalViews = [
|
||||
{ name: 'Top' },
|
||||
{ name: 'Front' },
|
||||
{ name: 'Left' },
|
||||
{ name: 'Back' },
|
||||
{ name: 'Right' }
|
||||
]
|
||||
|
||||
const onZoomExtentsClicked = (ev: MouseEvent) => {
|
||||
console.log('Zoom extents clicked', viewerHandler)
|
||||
viewerHandler.zoomExtents()
|
||||
}
|
||||
const host = inject(hostKey)
|
||||
const onClearPalletteClicked = (ev: MouseEvent) => {
|
||||
console.log('Clear pallette clicked')
|
||||
resetPalette()
|
||||
emits('clear-palette')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ButtonGroup>
|
||||
<ButtonSimple flat secondary @click="onZoomExtentsClicked">
|
||||
<ArrowsPointingOutIcon class="h-5 w-5" />
|
||||
</ButtonSimple>
|
||||
<Menu as="div" class="relative z-30">
|
||||
<MenuButton v-slot="{ open }" as="template">
|
||||
<ButtonToggle flat secondary :active="open">
|
||||
<VideoCameraIcon class="h-5 w-5" />
|
||||
</ButtonToggle>
|
||||
</MenuButton>
|
||||
<Transition
|
||||
enter-active-class="transform ease-out duration-300 transition"
|
||||
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
|
||||
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
|
||||
leave-active-class="transition ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute w-60 left-2 -translate-y-8 bottom-2 bg-foundation max-h-64 simple-scrollbar overflow-y-auto outline outline-2 outline-primary-muted rounded-lg shadow-lg overflow-hidden flex flex-col"
|
||||
>
|
||||
<MenuItem
|
||||
v-for="view in canonicalViews"
|
||||
:key="view.name"
|
||||
v-slot="{ active }"
|
||||
as="template"
|
||||
>
|
||||
<button
|
||||
:class="{
|
||||
'bg-primary text-foreground-on-primary': active,
|
||||
'text-foreground': !active,
|
||||
'text-sm py-2 transition': true
|
||||
}"
|
||||
@click="$emit('view-clicked', view.name.toLowerCase() as CanonicalView)"
|
||||
>
|
||||
{{ view.name }}
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem v-for="view in views" :key="view.name" v-slot="{ active }" as="template">
|
||||
<button
|
||||
:class="{
|
||||
'bg-primary text-foreground-on-primary': active,
|
||||
'text-foreground': !active,
|
||||
'text-sm py-2 transition': true
|
||||
}"
|
||||
@click="$emit('view-clicked', view)"
|
||||
>
|
||||
{{ view.view.name ?? view.name }}
|
||||
</button>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<ButtonToggle
|
||||
flat
|
||||
secondary
|
||||
:active="sectionBox"
|
||||
@click="$emit('update:sectionBox', !sectionBox)"
|
||||
>
|
||||
<CubeIcon class="h-5 w-5" />
|
||||
</ButtonToggle>
|
||||
<ButtonSimple flat secondary @click="onClearPalletteClicked">
|
||||
<PaintBrushIcon class="h-5 w-5" />
|
||||
</ButtonSimple>
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script async setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
Ref,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import ViewerControls from 'src/components/ViewerControls.vue'
|
||||
import { CanonicalView, SpeckleView } from '@speckle/viewer'
|
||||
import { CommonLoadingBar } from '@speckle/ui-components'
|
||||
import ViewerHandler from 'src/handlers/viewerHandler'
|
||||
import { useClickDragged } from 'src/composables/useClickDragged'
|
||||
import { isMultiSelect } from 'src/utils/isMultiSelect'
|
||||
import {
|
||||
selectionHandlerKey,
|
||||
storeKey,
|
||||
tooltipHandlerKey,
|
||||
viewerHandlerKey
|
||||
} from 'src/injectionKeys'
|
||||
import { SpeckleDataInput } from 'src/types'
|
||||
import { debounce, throttle } from 'lodash'
|
||||
import { ContextOption } from 'src/settings/colorSettings'
|
||||
|
||||
const selectionHandler = inject(selectionHandlerKey)
|
||||
const tooltipHandler = inject(tooltipHandlerKey)
|
||||
const store = useStore(storeKey)
|
||||
const { dragged } = useClickDragged()
|
||||
|
||||
let viewerHandler: ViewerHandler = null
|
||||
let ac = new AbortController()
|
||||
|
||||
const container = ref<HTMLElement>()
|
||||
let bboxActive = ref(false)
|
||||
let views: Ref<SpeckleView[]> = ref([])
|
||||
let updateTask: Ref<Promise<void>> = ref(null)
|
||||
let setupTask: Promise<void> = null
|
||||
|
||||
const isLoading = computed(() => updateTask.value != null)
|
||||
const input = computed(() => store.state.input)
|
||||
const settings = computed(() => store.state.settings)
|
||||
|
||||
const onCameraMoved = throttle((_) => {
|
||||
const pos = tooltipHandler.currentTooltip?.worldPos
|
||||
if (!pos) return
|
||||
const screenPos = viewerHandler.getScreenPosition(pos)
|
||||
tooltipHandler.move(screenPos)
|
||||
}, 50)
|
||||
|
||||
onMounted(() => {
|
||||
viewerHandler = new ViewerHandler(container.value)
|
||||
provide<ViewerHandler>(viewerHandlerKey, viewerHandler)
|
||||
setupTask = viewerHandler
|
||||
.init()
|
||||
.then(() => viewerHandler.addCameraUpdateEventListener(onCameraMoved))
|
||||
.finally(async () => {
|
||||
if (input.value) await cancelAndHandleDataUpdate()
|
||||
viewerHandler.updateSettings(settings.value)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
await viewerHandler.dispose()
|
||||
})
|
||||
|
||||
const debounceUpdate = throttle(cancelAndHandleDataUpdate, 500)
|
||||
const debounceSettingsUpdate = throttle(() => viewerHandler.updateSettings(settings.value), 500)
|
||||
watch(input, debounceUpdate)
|
||||
watch(settings, debounceSettingsUpdate)
|
||||
|
||||
watchEffect(() => {
|
||||
if (!isLoading.value) viewerHandler?.setSectionBox(bboxActive.value, input.value.objectIds)
|
||||
})
|
||||
|
||||
function handleDataUpdate(input: Ref<SpeckleDataInput>, signal: AbortSignal) {
|
||||
updateTask.value = setupTask
|
||||
.then(async () => {
|
||||
signal.throwIfAborted()
|
||||
// Clear previous selection
|
||||
await viewerHandler.selectObjects(null)
|
||||
|
||||
// Load
|
||||
await viewerHandler.loadObjectsWithAutoUnload(
|
||||
input.value.objectsToLoad,
|
||||
console.log,
|
||||
console.error,
|
||||
signal
|
||||
)
|
||||
|
||||
// Color
|
||||
await viewerHandler.colorObjectsByGroup(input.value.colorByIds)
|
||||
|
||||
await viewerHandler.unIsolateObjects()
|
||||
const objectsToIsolate =
|
||||
input.value.selectedIds.length == 0 ? input.value.objectIds : input.value.selectedIds
|
||||
if (settings.value.color.context.value != ContextOption.show)
|
||||
await viewerHandler.isolateObjects(
|
||||
objectsToIsolate,
|
||||
settings.value.color.context.value === ContextOption.ghosted
|
||||
)
|
||||
if (settings.value.camera.zoomOnDataChange.value) viewerHandler.zoom(objectsToIsolate)
|
||||
|
||||
// Update available views
|
||||
views.value = viewerHandler.getViews()
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.log('Loading operation was aborted', e)
|
||||
})
|
||||
.finally(() => {
|
||||
updateTask.value = null
|
||||
})
|
||||
}
|
||||
|
||||
async function cancelAndHandleDataUpdate() {
|
||||
console.log('Input has changed', input.value)
|
||||
if (updateTask.value) {
|
||||
ac.abort('New input is available')
|
||||
console.log('Cancelling previous load job')
|
||||
await updateTask.value
|
||||
ac = new AbortController()
|
||||
}
|
||||
const signal = ac.signal
|
||||
handleDataUpdate(input, signal)
|
||||
}
|
||||
|
||||
async function onCanvasClick(ev: MouseEvent) {
|
||||
if (dragged.value) return
|
||||
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
|
||||
const multi = isMultiSelect(ev)
|
||||
const hit = intersectResult?.hit
|
||||
if (hit) {
|
||||
const id = hit.object.id as string
|
||||
if (multi || !selectionHandler.isSelected(id)) await selectionHandler.select(id, multi)
|
||||
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 {
|
||||
tooltipHandler.hide()
|
||||
if (!multi) {
|
||||
selectionHandler.clear()
|
||||
await viewerHandler.selectObjects(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onCanvasAuxClick(ev: MouseEvent) {
|
||||
if (ev.button != 2 || dragged.value) return
|
||||
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
|
||||
await selectionHandler.showContextMenu(ev, intersectResult?.hit)
|
||||
}
|
||||
|
||||
function onClearPalette() {
|
||||
cancelAndHandleDataUpdate()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div
|
||||
ref="container"
|
||||
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-if="!isLoading"
|
||||
v-model:section-box="bboxActive"
|
||||
:views="views"
|
||||
class="fixed bottom-6"
|
||||
@view-clicked="(view) => viewerHandler.setView(view)"
|
||||
@clearPalette="onClearPalette"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped></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>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<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 } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
active?: boolean
|
||||
flat?: boolean
|
||||
secondary?: boolean
|
||||
}>()
|
||||
|
||||
const shadowClasses = computed(() => (props.flat ? '' : 'shadow-md'))
|
||||
|
||||
const colorClasses = computed(() => {
|
||||
const parts = []
|
||||
if (props.active) {
|
||||
if (props.secondary) parts.push('bg-foundation text-primary')
|
||||
else parts.push('bg-primary text-foreground-on-primary')
|
||||
} else {
|
||||
parts.push('bg-foundation text-foreground')
|
||||
}
|
||||
return parts.join(' ')
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ref, onMounted, onUnmounted, Ref } from 'vue'
|
||||
|
||||
// by convention, composable function names start with "use"
|
||||
export function useClickDragged(threshold = 1) {
|
||||
// state encapsulated and managed by the composable
|
||||
const dragged = ref(false)
|
||||
const distance = ref(0)
|
||||
const start: Ref<{ x: number; y: number }> = ref(null)
|
||||
const current: Ref<{ x: number; y: number }> = ref(null)
|
||||
function onPointerMove(ev) {
|
||||
distance.value = Math.sqrt(
|
||||
Math.pow(ev.x - start.value.x, 2) * Math.pow(ev.y - start.value.y, 2)
|
||||
)
|
||||
if (distance.value > threshold) {
|
||||
dragged.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown(ev) {
|
||||
dragged.value = false
|
||||
start.value = { x: ev.x, y: ev.y }
|
||||
current.value = start.value
|
||||
distance.value = 0
|
||||
document.addEventListener('pointermove', onPointerMove)
|
||||
}
|
||||
|
||||
function onPointerUp(_) {
|
||||
if (dragged.value === false) reset()
|
||||
|
||||
document.removeEventListener('pointermove', onPointerMove)
|
||||
}
|
||||
function reset() {
|
||||
start.value = null
|
||||
current.value = null
|
||||
distance.value = 0
|
||||
}
|
||||
|
||||
// a composable can also hook into its owner component's
|
||||
// lifecycle to setup and teardown side effects.
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', onPointerDown)
|
||||
document.addEventListener('pointerup', onPointerUp)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('pointerdown', onPointerDown)
|
||||
document.removeEventListener('pointerup', onPointerUp)
|
||||
})
|
||||
|
||||
// expose managed state as return value
|
||||
return { dragged, distance }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
export default class SelectionHandler {
|
||||
private selectionIdMap: Map<string, powerbi.extensibility.ISelectionId>
|
||||
private currentSelection: Set<string>
|
||||
private selectionManager: powerbi.extensibility.ISelectionManager
|
||||
private host: powerbi.extensibility.visual.IVisualHost
|
||||
|
||||
public constructor(host: powerbi.extensibility.visual.IVisualHost) {
|
||||
this.host = host
|
||||
this.selectionManager = this.host.createSelectionManager()
|
||||
this.selectionIdMap = new Map<string, powerbi.extensibility.ISelectionId>()
|
||||
this.currentSelection = new Set<string>()
|
||||
}
|
||||
|
||||
public async showContextMenu(ev: MouseEvent, hit?) {
|
||||
const selectionId = !hit ? null : this.selectionIdMap.get(hit?.object?.id)
|
||||
|
||||
return this.selectionManager.showContextMenu(selectionId, {
|
||||
x: ev.clientX,
|
||||
y: ev.clientY
|
||||
})
|
||||
}
|
||||
|
||||
public set(url: string, data: powerbi.extensibility.ISelectionId) {
|
||||
this.selectionIdMap.set(url, data)
|
||||
}
|
||||
public async select(url: string, multi = false) {
|
||||
if (multi) {
|
||||
await this.selectionManager.select(this.selectionIdMap.get(url), true)
|
||||
if (this.currentSelection.has(url)) this.currentSelection.delete(url)
|
||||
else this.currentSelection.add(url)
|
||||
} else {
|
||||
await this.selectionManager.select(this.selectionIdMap.get(url), false)
|
||||
this.currentSelection.clear()
|
||||
this.currentSelection.add(url)
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentSelection(): { id: string; selectionId: powerbi.extensibility.ISelectionId }[] {
|
||||
return [...this.currentSelection].map((entry) => ({
|
||||
id: entry,
|
||||
selectionId: this.selectionIdMap.get(entry)
|
||||
}))
|
||||
}
|
||||
|
||||
public isSelected(id: string) {
|
||||
return this.currentSelection.has(id)
|
||||
}
|
||||
public clear() {
|
||||
this.selectionManager.clear()
|
||||
this.currentSelection.clear()
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.clear()
|
||||
this.selectionIdMap.clear()
|
||||
}
|
||||
|
||||
public has(url) {
|
||||
return this.selectionIdMap.has(url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import ITooltipService = powerbi.extensibility.ITooltipService
|
||||
import { IViewerTooltip } from '../types'
|
||||
import { SpeckleTooltip } from '../interfaces'
|
||||
|
||||
export default class TooltipHandler {
|
||||
private data: Map<string, IViewerTooltip>
|
||||
private tooltipService: ITooltipService
|
||||
public currentTooltip: SpeckleTooltip = null
|
||||
|
||||
constructor(tooltipService) {
|
||||
this.tooltipService = tooltipService
|
||||
this.data = new Map<string, IViewerTooltip>()
|
||||
}
|
||||
|
||||
public setup(data: Map<string, IViewerTooltip>) {
|
||||
this.data = data
|
||||
}
|
||||
|
||||
public show(hit: { guid: string; object?; point }, screenLoc) {
|
||||
const id = hit.object.id as string
|
||||
const objTooltipData: IViewerTooltip = this.data.get(id)
|
||||
if (!objTooltipData) return
|
||||
|
||||
const tooltipData = {
|
||||
coordinates: [screenLoc.x, screenLoc.y],
|
||||
dataItems: objTooltipData.data,
|
||||
identities: [objTooltipData.selectionId],
|
||||
isTouchEvent: false
|
||||
}
|
||||
|
||||
this.currentTooltip = {
|
||||
id: hit.object.id,
|
||||
worldPos: hit.point,
|
||||
screenPos: screenLoc,
|
||||
tooltip: tooltipData
|
||||
}
|
||||
|
||||
this.tooltipService.show(tooltipData)
|
||||
if (Object.keys(tooltipData.dataItems).length > 0) this.tooltipService.show(tooltipData)
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.tooltipService.hide({ immediately: true, isTouchEvent: false })
|
||||
this.currentTooltip = null
|
||||
}
|
||||
|
||||
public move(pos: { x: number; y: number }) {
|
||||
if (!this.currentTooltip) return
|
||||
this.currentTooltip.tooltip.coordinates = [pos.x, pos.y]
|
||||
this.tooltipService.move(this.currentTooltip.tooltip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
CanonicalView,
|
||||
FilteringState,
|
||||
LegacyViewer,
|
||||
IntersectionQuery,
|
||||
DefaultViewerParams,
|
||||
Box3,
|
||||
SpeckleView,
|
||||
CameraController
|
||||
} from '@speckle/viewer'
|
||||
import { pickViewableHit, projectToScreen } from '../utils/viewerUtils'
|
||||
import _ from 'lodash'
|
||||
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
|
||||
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
|
||||
}
|
||||
|
||||
this.viewer.getExtension(CameraController).controls.maxPolarAngle = settings.camera
|
||||
.allowCameraUnder.value
|
||||
? Math.PI
|
||||
: Math.PI / 2
|
||||
|
||||
// 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
|
||||
} else {
|
||||
const bbox = this.viewer.getCurrentSectionBox()
|
||||
if (bbox) this.currentSectionBox = bbox
|
||||
}
|
||||
this.viewer.sectionBoxOn()
|
||||
} else {
|
||||
this.viewer.sectionBoxOff()
|
||||
}
|
||||
this.viewer.requestRender()
|
||||
}
|
||||
|
||||
public addCameraUpdateEventListener(listener: (ev) => void) {
|
||||
this.viewer.getExtension(CameraController).controls.addEventListener('update', 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()
|
||||
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(
|
||||
objectUrls: string[],
|
||||
onLoad: (url: string, index: number) => void,
|
||||
onError: (url: string, error: Error) => void,
|
||||
signal: AbortSignal
|
||||
) {
|
||||
var objectsToUnload = _.difference([...this.loadedObjectsCache], objectUrls)
|
||||
await this.unloadObjects(objectsToUnload, signal)
|
||||
await this.loadObjects(objectUrls, onLoad, onError, signal)
|
||||
}
|
||||
|
||||
public async loadObjects(
|
||||
objectUrls: string[],
|
||||
onLoad: (url: string, index: number) => void,
|
||||
onError: (url: string, error: Error) => void,
|
||||
signal: AbortSignal
|
||||
) {
|
||||
try {
|
||||
let index = 0
|
||||
let promises = []
|
||||
for (const url of objectUrls) {
|
||||
signal.throwIfAborted()
|
||||
console.log('Attempting to load', url)
|
||||
if (!this.loadedObjectsCache.has(url)) {
|
||||
console.log('Object is not in cache')
|
||||
const promise = this.viewer
|
||||
.loadObjectAsync(url, this.config.authToken, false)
|
||||
.then(() => onLoad(url, index++))
|
||||
.catch((e: Error) => onError(url, e))
|
||||
.finally(() => {
|
||||
if (!this.loadedObjectsCache.has(url)) this.loadedObjectsCache.add(url)
|
||||
})
|
||||
promises.push(promise)
|
||||
if (promises.length == this.config.batchSize) {
|
||||
//this.promises.push(Promise.resolve(this.later(1000)))
|
||||
await Promise.all(promises)
|
||||
promises = []
|
||||
}
|
||||
} else {
|
||||
console.log('Object was already in cache')
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') return
|
||||
throw new Error(`Load objects failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
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).controls.camera,
|
||||
worldPosition
|
||||
)
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.viewer.getExtension(CameraController).controls.removeAllEventListeners()
|
||||
this.viewer.dispose()
|
||||
this.viewer = null
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.png' {
|
||||
const source: string
|
||||
export default source
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import SelectionHandler from 'src/handlers/selectionHandler'
|
||||
import TooltipHandler from 'src/handlers/tooltipHandler'
|
||||
import { Store } from 'vuex'
|
||||
import { SpeckleVisualState } from 'src/store'
|
||||
import ViewerHandler from 'src/handlers/viewerHandler'
|
||||
|
||||
export const selectionHandlerKey: InjectionKey<SelectionHandler> = Symbol()
|
||||
export const tooltipHandlerKey: InjectionKey<TooltipHandler> = Symbol()
|
||||
export const hostKey: InjectionKey<powerbi.extensibility.visual.IVisualHost> = Symbol()
|
||||
export const storeKey: InjectionKey<Store<SpeckleVisualState>> = Symbol()
|
||||
export const viewerHandlerKey: InjectionKey<ViewerHandler> = Symbol()
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IViewerTooltipData } from './types'
|
||||
|
||||
export interface SpeckleSelectionData {
|
||||
id: powerbi.extensibility.ISelectionId
|
||||
data: IViewerTooltipData[]
|
||||
}
|
||||
|
||||
export interface SpeckleTooltip {
|
||||
worldPos: {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
screenPos: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
tooltip
|
||||
id: string
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
"use strict"
|
||||
|
||||
import { dataViewObjectsParser } from "powerbi-visuals-utils-dataviewutils"
|
||||
import DataViewObjectsParser = dataViewObjectsParser.DataViewObjectsParser
|
||||
|
||||
export class SpeckleVisualSettings extends DataViewObjectsParser {
|
||||
public camera: CameraSettings = new CameraSettings()
|
||||
public color: ColorSettings = new ColorSettings()
|
||||
}
|
||||
|
||||
export class CameraSettings {
|
||||
// Default color
|
||||
public orthoMode: boolean = false
|
||||
public defaultView: string = "default"
|
||||
}
|
||||
|
||||
export class ColorSettings {
|
||||
public startColor: string = "#31c116"
|
||||
public midColor: string = "#fc8032"
|
||||
public endColor: string = "#e70000"
|
||||
public background: string = "#ffffff"
|
||||
|
||||
public getColorList() {
|
||||
return [this.startColor, this.midColor, this.endColor]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
|
||||
export class CameraSettings extends fs.Card {
|
||||
public defaultView: fs.SimpleSlice = new fs.AutoDropdown({
|
||||
name: 'defaultView',
|
||||
displayName: 'Default View',
|
||||
value: 'perspective'
|
||||
})
|
||||
|
||||
public projection = new fs.AutoDropdown({
|
||||
name: 'projection',
|
||||
displayName: 'Projection',
|
||||
value: 'perspective'
|
||||
})
|
||||
|
||||
public allowCameraUnder = new fs.ToggleSwitch({
|
||||
name: 'allowCameraUnder',
|
||||
displayName: 'Allow under model',
|
||||
value: false
|
||||
})
|
||||
|
||||
public zoomOnDataChange = new fs.ToggleSwitch({
|
||||
name: 'zoomOnDataChange',
|
||||
displayName: 'Zoom extent on change',
|
||||
value: true
|
||||
})
|
||||
name = 'camera'
|
||||
displayName = 'Camera'
|
||||
slices: fs.Slice[] = [
|
||||
this.defaultView,
|
||||
this.projection,
|
||||
this.allowCameraUnder,
|
||||
this.zoomOnDataChange
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import {
|
||||
createDataViewWildcardSelector,
|
||||
DataViewWildcardMatchingOption
|
||||
} from 'powerbi-visuals-utils-dataviewutils/lib/dataViewWildcard'
|
||||
import VisualEnumerationInstanceKinds = powerbi.VisualEnumerationInstanceKinds
|
||||
|
||||
export enum ContextOption {
|
||||
hidden = 'hidden',
|
||||
ghosted = 'ghosted',
|
||||
show = 'show'
|
||||
}
|
||||
export class ColorSettings extends fs.Card {
|
||||
public enabled = new fs.ToggleSwitch({
|
||||
name: 'enabled',
|
||||
displayName: 'Enabled',
|
||||
value: true,
|
||||
topLevelToggle: true
|
||||
})
|
||||
|
||||
public fill = new fs.ColorPicker({
|
||||
name: 'fill',
|
||||
displayName: 'Color override',
|
||||
description:
|
||||
'Allows to override the colors of each object based on user-defined rules. Default color does not affect visualization.',
|
||||
value: { value: '#c5c5c5' },
|
||||
defaultColor: { value: '#c5c5c5' },
|
||||
selector: createDataViewWildcardSelector(DataViewWildcardMatchingOption.InstancesAndTotals),
|
||||
altConstantSelector: {
|
||||
static: {}
|
||||
},
|
||||
instanceKind: VisualEnumerationInstanceKinds.ConstantOrRule
|
||||
})
|
||||
|
||||
public context = new fs.AutoDropdown({
|
||||
name: 'context',
|
||||
displayName: 'Context display',
|
||||
description: 'Determines how to display objects not present in the input data table.',
|
||||
value: ContextOption.ghosted
|
||||
})
|
||||
|
||||
name = 'color'
|
||||
displayName = 'Object Display'
|
||||
slices: fs.Slice[] = [this.context, this.fill]
|
||||
}
|
||||
|
||||
export class ColorSelectorSettings extends fs.Card {
|
||||
name = 'colorSelector'
|
||||
displayName = 'Color Selector'
|
||||
slices = []
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import ValidatorType = powerbi.visuals.ValidatorType
|
||||
import { SunLightConfiguration } from '@speckle/viewer'
|
||||
|
||||
export class LightingSettings extends fs.Card {
|
||||
name = 'lighting'
|
||||
displayName = 'Lighting'
|
||||
|
||||
public enabled = new fs.ToggleSwitch({
|
||||
name: 'enabled',
|
||||
displayName: 'Enabled',
|
||||
value: true,
|
||||
topLevelToggle: true
|
||||
})
|
||||
|
||||
public intensity = new fs.Slider({
|
||||
name: 'intensity',
|
||||
displayName: 'Intensity',
|
||||
options: {
|
||||
minValue: { type: ValidatorType.Min, value: 1 },
|
||||
maxValue: { type: ValidatorType.Max, value: 10 }
|
||||
},
|
||||
value: 5
|
||||
})
|
||||
public elevation = new fs.Slider({
|
||||
name: 'elevation',
|
||||
displayName: 'Elevation',
|
||||
options: {
|
||||
minValue: { type: ValidatorType.Min, value: 0 },
|
||||
maxValue: { type: ValidatorType.Max, value: Math.PI }
|
||||
},
|
||||
value: 1.33
|
||||
})
|
||||
public azimuth = new fs.Slider({
|
||||
name: 'azimuth',
|
||||
displayName: 'azimuth',
|
||||
options: {
|
||||
minValue: { type: ValidatorType.Min, value: -Math.PI * 0.5 },
|
||||
maxValue: { type: ValidatorType.Max, value: Math.PI * 0.5 }
|
||||
},
|
||||
value: 0.75
|
||||
})
|
||||
public indirect = new fs.Slider({
|
||||
name: 'indirect',
|
||||
displayName: 'indirect',
|
||||
options: {
|
||||
minValue: { type: ValidatorType.Min, value: 0.0 },
|
||||
maxValue: { type: ValidatorType.Max, value: 5.0 }
|
||||
},
|
||||
value: 1.2
|
||||
})
|
||||
|
||||
public shadows = new fs.ToggleSwitch({
|
||||
name: 'shadows',
|
||||
displayName: 'Cast shadows',
|
||||
value: true
|
||||
})
|
||||
|
||||
public shadowCatcher = new fs.ToggleSwitch({
|
||||
name: 'shadowCatcher',
|
||||
displayName: 'Catch Shadows',
|
||||
value: true
|
||||
})
|
||||
|
||||
slices: fs.Slice[] = [
|
||||
this.intensity,
|
||||
this.elevation,
|
||||
this.azimuth,
|
||||
this.indirect,
|
||||
this.shadows,
|
||||
this.shadowCatcher
|
||||
]
|
||||
|
||||
public getViewerConfiguration(): SunLightConfiguration {
|
||||
return {
|
||||
enabled: this.enabled.value,
|
||||
castShadow: this.shadows.value,
|
||||
intensity: this.intensity.value,
|
||||
elevation: this.elevation.value,
|
||||
azimuth: this.azimuth.value,
|
||||
indirectLightIntensity: this.intensity.value,
|
||||
shadowcatcher: this.shadowCatcher.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import { ColorSelectorSettings, ColorSettings } from 'src/settings/colorSettings'
|
||||
import { CameraSettings } from 'src/settings/cameraSettings'
|
||||
import { LightingSettings } from 'src/settings/lightingSettings'
|
||||
|
||||
export class SpeckleVisualSettingsModel extends fs.Model {
|
||||
// Building my visual formatting settings card
|
||||
public color: ColorSettings = new ColorSettings()
|
||||
|
||||
public colorSelector: ColorSelectorSettings = new ColorSelectorSettings()
|
||||
|
||||
public camera: CameraSettings = new CameraSettings()
|
||||
|
||||
public lighting: LightingSettings = new LightingSettings()
|
||||
|
||||
cards = [this.color, this.camera, this.lighting]
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createStore } from 'vuex'
|
||||
import { SpeckleDataInput } from 'src/types'
|
||||
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
|
||||
export type InputState = 'valid' | 'incomplete' | 'invalid'
|
||||
|
||||
export interface SpeckleVisualState {
|
||||
input?: SpeckleDataInput
|
||||
status: InputState
|
||||
settings: SpeckleVisualSettingsModel
|
||||
}
|
||||
|
||||
// Create a new store instance.
|
||||
export const store = createStore<SpeckleVisualState>({
|
||||
state() {
|
||||
return {
|
||||
input: null,
|
||||
status: 'incomplete',
|
||||
settings: null
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setInput(state, input?: SpeckleDataInput) {
|
||||
state.input = input
|
||||
},
|
||||
setStatus(state, status: InputState) {
|
||||
state.status = status ?? 'invalid'
|
||||
},
|
||||
setSettings(state, settings: SpeckleVisualSettingsModel) {
|
||||
state.settings = settings
|
||||
},
|
||||
clearInput(state) {
|
||||
state.input = null
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
update(context, status: InputState, input?: SpeckleDataInput) {
|
||||
context.commit('setInput', input)
|
||||
context.commit('setStatus', status)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
export interface IViewerTooltipData {
|
||||
displayName: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface IViewerTooltip {
|
||||
selectionId: powerbi.extensibility.ISelectionId
|
||||
data: IViewerTooltipData[]
|
||||
}
|
||||
|
||||
export interface SpeckleDataInput {
|
||||
objectsToLoad: string[]
|
||||
objectIds: string[]
|
||||
selectedIds: string[]
|
||||
colorByIds: { objectIds: string[]; slice: fs.ColorPicker; color: string }[]
|
||||
objectTooltipData: Map<string, IViewerTooltip>
|
||||
view: powerbi.DataViewMatrix
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Add data types to window.navigator for use in this file. See https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types- for more info.
|
||||
/// <reference types="user-agent-data-types" />
|
||||
export function getOS(): OS {
|
||||
const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
|
||||
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K', 'macOS'],
|
||||
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
|
||||
let os = null
|
||||
if (macosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'MacOS'
|
||||
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'Windows'
|
||||
} else if (/Linux/.test(platform)) {
|
||||
os = 'Linux'
|
||||
}
|
||||
return os
|
||||
}
|
||||
|
||||
export enum OS {
|
||||
Windows,
|
||||
MacOS,
|
||||
Linux
|
||||
}
|
||||
|
||||
export const currentOS = getOS()
|
||||
@@ -0,0 +1,7 @@
|
||||
import { currentOS, OS } from './detectOS'
|
||||
|
||||
export function isMultiSelect(e: MouseEvent) {
|
||||
if (!e) return false
|
||||
if (currentOS === OS.MacOS) return e.metaKey || e.shiftKey
|
||||
else return e.ctrlKey || e.shiftKey
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import { IViewerTooltip, IViewerTooltipData, SpeckleDataInput } from '../types'
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import {
|
||||
createDataViewWildcardSelector,
|
||||
DataViewWildcardMatchingOption
|
||||
} from 'powerbi-visuals-utils-dataviewutils/lib/dataViewWildcard'
|
||||
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions
|
||||
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
|
||||
|
||||
export function validateMatrixView(options: VisualUpdateOptions): {
|
||||
hasColorFilter: boolean
|
||||
view: powerbi.DataViewMatrix
|
||||
} {
|
||||
const matrixVew = options.dataViews[0].matrix
|
||||
if (!matrixVew) throw new Error('Data does not contain a matrix data view')
|
||||
|
||||
let hasStream = false,
|
||||
hasParentObject = false,
|
||||
hasObject = false,
|
||||
hasColorFilter = false
|
||||
|
||||
matrixVew.rows.levels.forEach((level) => {
|
||||
level.sources.forEach((source) => {
|
||||
if (!hasStream) hasStream = source.roles['stream'] != undefined
|
||||
if (!hasParentObject) hasParentObject = source.roles['parentObject'] != undefined
|
||||
if (!hasObject) hasObject = source.roles['object'] != undefined
|
||||
if (!hasColorFilter) hasColorFilter = source.roles['objectColorBy'] != undefined
|
||||
})
|
||||
})
|
||||
|
||||
if (!hasStream) throw new Error('Missing Stream ID input')
|
||||
if (!hasParentObject) throw new Error('Missing Commit Object ID input')
|
||||
if (!hasObject) throw new Error('Missing Object Id input')
|
||||
return {
|
||||
hasColorFilter,
|
||||
view: matrixVew
|
||||
}
|
||||
}
|
||||
|
||||
function processObjectValues(
|
||||
objectIdChild: powerbi.DataViewMatrixNode,
|
||||
matrixView: powerbi.DataViewMatrix
|
||||
) {
|
||||
const objectData: IViewerTooltipData[] = []
|
||||
let shouldColor = true,
|
||||
shouldSelect = false
|
||||
|
||||
if (objectIdChild.values)
|
||||
Object.keys(objectIdChild.values).forEach((key) => {
|
||||
const value: powerbi.DataViewMatrixNodeValue = objectIdChild.values[key]
|
||||
const k: unknown = key
|
||||
const colInfo = matrixView.valueSources[k as number]
|
||||
const highLightActive = value.highlight !== undefined
|
||||
if (highLightActive) {
|
||||
shouldColor = false
|
||||
}
|
||||
const isHighlighted = value.highlight !== null
|
||||
|
||||
if (highLightActive && isHighlighted) {
|
||||
shouldSelect = true
|
||||
shouldColor = true
|
||||
}
|
||||
const propData: IViewerTooltipData = {
|
||||
displayName: colInfo.displayName,
|
||||
value: value.value.toString()
|
||||
}
|
||||
objectData.push(propData)
|
||||
})
|
||||
return { data: objectData, shouldColor, shouldSelect }
|
||||
}
|
||||
|
||||
function processObjectNode(
|
||||
objectIdChild: powerbi.DataViewMatrixNode,
|
||||
host: powerbi.extensibility.visual.IVisualHost,
|
||||
matrixView: powerbi.DataViewMatrix
|
||||
): {
|
||||
data: IViewerTooltipData[]
|
||||
shouldColor: boolean
|
||||
shouldSelect: boolean
|
||||
id: string
|
||||
selectionId: powerbi.visuals.ISelectionId
|
||||
color?: string
|
||||
} {
|
||||
const objId = objectIdChild.value as string
|
||||
// Create selection IDs for each object
|
||||
const nodeSelection = host
|
||||
.createSelectionIdBuilder()
|
||||
.withMatrixNode(objectIdChild, matrixView.rows.levels)
|
||||
.createSelectionId()
|
||||
// Create value records for the tooltips
|
||||
const objectValues = processObjectValues(objectIdChild, matrixView)
|
||||
const res = { id: objId, selectionId: nodeSelection, color: undefined, ...objectValues }
|
||||
// Process node objects, if any.
|
||||
if (objectIdChild.objects) {
|
||||
//@ts-ignore
|
||||
const color = objectIdChild.objects.color.fill.solid.color as string
|
||||
console.log('⚠️ HAS objects', color)
|
||||
if (color) {
|
||||
res.color = color
|
||||
res.shouldColor = true
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function processObjectIdLevel(
|
||||
parentObjectIdChild: powerbi.DataViewMatrixNode,
|
||||
host: powerbi.extensibility.visual.IVisualHost,
|
||||
matrixView: powerbi.DataViewMatrix
|
||||
) {
|
||||
return parentObjectIdChild.children?.map((objectIdChild) =>
|
||||
processObjectNode(objectIdChild, host, matrixView)
|
||||
)
|
||||
}
|
||||
|
||||
export let previousPalette = null
|
||||
|
||||
export function resetPalette() {
|
||||
previousPalette = null
|
||||
}
|
||||
export function processMatrixView(
|
||||
matrixView: powerbi.DataViewMatrix,
|
||||
host: powerbi.extensibility.visual.IVisualHost,
|
||||
hasColorFilter: boolean,
|
||||
settings: SpeckleVisualSettingsModel,
|
||||
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void
|
||||
): SpeckleDataInput {
|
||||
const objectUrlsToLoad = [],
|
||||
objectIds = [],
|
||||
selectedIds = [],
|
||||
colorByIds = [],
|
||||
objectTooltipData = new Map<string, IViewerTooltip>()
|
||||
|
||||
matrixView.rows.root.children.forEach((streamUrlChild) => {
|
||||
const url = streamUrlChild.value
|
||||
|
||||
streamUrlChild.children?.forEach((parentObjectIdChild) => {
|
||||
const parentId = parentObjectIdChild.value
|
||||
objectUrlsToLoad.push(`${url}/objects/${parentId}`)
|
||||
|
||||
if (!hasColorFilter) {
|
||||
processObjectIdLevel(parentObjectIdChild, host, matrixView).forEach((objRes) => {
|
||||
objectIds.push(objRes.id)
|
||||
onSelectionPair(objRes.id, objRes.selectionId)
|
||||
if (objRes.shouldSelect) selectedIds.push(objRes.id)
|
||||
if (objRes.color) {
|
||||
let group = colorByIds.find((g) => g.color === objRes.color)
|
||||
if (!group) {
|
||||
group = {
|
||||
color: objRes.color,
|
||||
objectIds: []
|
||||
}
|
||||
colorByIds.push(group)
|
||||
}
|
||||
group.objectIds.push(objRes.id)
|
||||
}
|
||||
objectTooltipData.set(objRes.id, {
|
||||
selectionId: objRes.selectionId,
|
||||
data: objRes.data
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (previousPalette) host.colorPalette['colorPalette'] = previousPalette
|
||||
parentObjectIdChild.children?.forEach((colorByChild) => {
|
||||
const colorSelectionId = host
|
||||
.createSelectionIdBuilder()
|
||||
.withMatrixNode(colorByChild, matrixView.rows.levels)
|
||||
.createSelectionId()
|
||||
|
||||
const color = host.colorPalette.getColor(colorByChild.value as string)
|
||||
if (colorByChild.objects) {
|
||||
console.log(
|
||||
'⚠️COLOR NODE HAS objects',
|
||||
colorByChild.objects,
|
||||
colorByChild.objects.color?.fill
|
||||
)
|
||||
}
|
||||
|
||||
const colorSlice = new fs.ColorPicker({
|
||||
name: 'selectorFill',
|
||||
displayName: colorByChild.value.toString(),
|
||||
value: {
|
||||
value: color.value
|
||||
},
|
||||
selector: colorSelectionId.getSelector()
|
||||
})
|
||||
|
||||
const colorGroup = {
|
||||
color: color.value,
|
||||
slice: colorSlice,
|
||||
objectIds: []
|
||||
}
|
||||
|
||||
processObjectIdLevel(colorByChild, host, matrixView).forEach((objRes) => {
|
||||
objectIds.push(objRes.id)
|
||||
onSelectionPair(objRes.id, objRes.selectionId)
|
||||
if (objRes.shouldSelect) selectedIds.push(objRes.id)
|
||||
if (objRes.shouldColor) {
|
||||
colorGroup.objectIds.push(objRes.id)
|
||||
}
|
||||
objectTooltipData.set(objRes.id, {
|
||||
selectionId: objRes.selectionId,
|
||||
data: objRes.data
|
||||
})
|
||||
})
|
||||
if (colorGroup.objectIds.length > 0) colorByIds.push(colorGroup)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
previousPalette = host.colorPalette['colorPalette']
|
||||
|
||||
return {
|
||||
objectsToLoad: objectUrlsToLoad,
|
||||
objectIds,
|
||||
selectedIds,
|
||||
colorByIds: colorByIds.length > 0 ? colorByIds : null,
|
||||
objectTooltipData,
|
||||
view: matrixView
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
const TRACK_URL = 'https://analytics.speckle.systems/track?ip=1'
|
||||
const MIXPANEL_TOKEN = 'acd87c5a50b56df91a795e999812a3a4'
|
||||
const HOST_APP_NAME = 'powerbi-visual'
|
||||
|
||||
export enum Event {
|
||||
Create = 'Create',
|
||||
Reload = 'Reload',
|
||||
Settings = 'Settings'
|
||||
}
|
||||
|
||||
export enum SettingsChangedType {
|
||||
Gradient = 'Gradient',
|
||||
DefaultCamera = 'DefaultCamera',
|
||||
OrthoMode = 'OrthoMode'
|
||||
}
|
||||
|
||||
export class Tracker {
|
||||
public static async track(event: Event, properties: any = {}) {
|
||||
return this.trackEvents([
|
||||
{
|
||||
event,
|
||||
properties
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
private static async trackEvents(events: Array<{ event: Event; properties: any }>) {
|
||||
try {
|
||||
await fetch(TRACK_URL, {
|
||||
method: 'POST',
|
||||
body:
|
||||
'data=' +
|
||||
JSON.stringify(
|
||||
events.map((e) => {
|
||||
Object.assign(e.properties, {
|
||||
token: MIXPANEL_TOKEN,
|
||||
hostApp: HOST_APP_NAME
|
||||
})
|
||||
return e
|
||||
})
|
||||
)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Create track failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
public static loaded() {
|
||||
return this.track(Event.Create)
|
||||
}
|
||||
|
||||
public static dataReload() {
|
||||
return this.track(Event.Reload)
|
||||
}
|
||||
|
||||
public static settingsChanged(type: SettingsChangedType) {
|
||||
return this.track(Event.Settings, { type })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import powerbiVisualsApi from 'powerbi-visuals-api'
|
||||
import powerbi = powerbiVisualsApi
|
||||
|
||||
import DataViewObject = powerbi.DataViewObject
|
||||
import DataViewObjects = powerbi.DataViewObjects
|
||||
import DataViewCategoryColumn = powerbi.DataViewCategoryColumn
|
||||
|
||||
/**
|
||||
* Gets property value for a particular object.
|
||||
*
|
||||
* @function
|
||||
* @param {DataViewObjects} objects - Map of defined objects.
|
||||
* @param {string} objectName - Name of desired object.
|
||||
* @param {string} propertyName - Name of desired property.
|
||||
* @param {T} defaultValue - Default value of desired property.
|
||||
*/
|
||||
export function getValue<T>(
|
||||
objects: DataViewObjects,
|
||||
objectName: string,
|
||||
propertyName: string,
|
||||
defaultValue: T
|
||||
): T {
|
||||
if (objects) {
|
||||
const object = objects[objectName]
|
||||
if (object) {
|
||||
const property: T = <T>object[propertyName]
|
||||
if (property !== undefined) {
|
||||
return property
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets property value for a particular object in a category.
|
||||
*
|
||||
* @function
|
||||
* @param {DataViewCategoryColumn} category - List of category objects.
|
||||
* @param {number} index - Index of category object.
|
||||
* @param {string} objectName - Name of desired object.
|
||||
* @param {string} propertyName - Name of desired property.
|
||||
* @param {T} defaultValue - Default value of desired property.
|
||||
*/
|
||||
export function getCategoricalObjectValue<T>(
|
||||
category: DataViewCategoryColumn,
|
||||
index: number,
|
||||
objectName: string,
|
||||
propertyName: string,
|
||||
defaultValue: T
|
||||
): T {
|
||||
const categoryObjects = category.objects
|
||||
|
||||
if (categoryObjects) {
|
||||
const categoryObject: DataViewObject = categoryObjects[index]
|
||||
if (categoryObject) {
|
||||
const object = categoryObject[objectName]
|
||||
if (object) {
|
||||
const property: T = <T>object[propertyName]
|
||||
if (property !== undefined) {
|
||||
return property
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2022 Davide Aversa
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import * as _ from 'lodash'
|
||||
|
||||
export interface SignalBindingAsync<S, T> {
|
||||
listener?: string
|
||||
handler: (source: S, data: T) => Promise<void>
|
||||
}
|
||||
|
||||
export interface IAsyncSignal<S, T> {
|
||||
bind(listener: string, handler: (source: S, data: T) => Promise<void>): void
|
||||
unbind(listener: string): void
|
||||
}
|
||||
|
||||
export class AsyncSignal<S, T> implements IAsyncSignal<S, T> {
|
||||
private handlers: Array<SignalBindingAsync<S, T>> = []
|
||||
|
||||
public bind(listener: string, handler: (source: S, data: T) => Promise<void>): void {
|
||||
if (this.contains(listener)) {
|
||||
this.unbind(listener)
|
||||
}
|
||||
this.handlers.push({ listener, handler })
|
||||
}
|
||||
|
||||
public unbind(listener: string): void {
|
||||
this.handlers = this.handlers.filter((h) => h.listener !== listener)
|
||||
}
|
||||
|
||||
public async trigger(source: S, data: T): Promise<void> {
|
||||
// Duplicate the array to avoid side effects during iteration.
|
||||
this.handlers.slice(0).map((h) => h.handler(source, data))
|
||||
}
|
||||
|
||||
public async triggerAwait(source: S, data: T): Promise<void> {
|
||||
// Duplicate the array to avoid side effects during iteration.
|
||||
const promises = this.handlers.slice(0).map((h) => h.handler(source, data))
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
public contains(listener: string): boolean {
|
||||
return _.some(this.handlers, (h) => h.listener === listener)
|
||||
}
|
||||
|
||||
public expose(): IAsyncSignal<S, T> {
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { FilteringState } from '@speckle/viewer'
|
||||
|
||||
export function projectToScreen(cam, loc) {
|
||||
cam.updateProjectionMatrix()
|
||||
const copy = loc.clone()
|
||||
copy.project(cam)
|
||||
return {
|
||||
x: (copy.x * 0.5 + 0.5) * window.innerWidth - 10,
|
||||
y: (copy.y * -0.5 + 0.5) * window.innerHeight
|
||||
}
|
||||
}
|
||||
|
||||
export interface Hit {
|
||||
guid: string
|
||||
object?: Record<string, unknown>
|
||||
point: { x: number; y: number; z: number }
|
||||
}
|
||||
export function pickViewableHit(hits: Hit[], state: FilteringState): Hit | null {
|
||||
let hit = null
|
||||
if (state.isolatedObjects) {
|
||||
// Find the first hit contained in the isolated objects
|
||||
hit = hits.find((hit) => {
|
||||
const hitId = hit.object.id as string
|
||||
return state.isolatedObjects.includes(hitId)
|
||||
})
|
||||
}
|
||||
return hit
|
||||
}
|
||||
|
||||
export const createViewerContainerDiv = (parent: HTMLElement) => {
|
||||
const container = parent.appendChild(document.createElement('div'))
|
||||
container.style.backgroundColor = 'transparent'
|
||||
container.style.height = '100%'
|
||||
container.style.width = '100%'
|
||||
container.style.position = 'fixed'
|
||||
return container
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { FormButton } from '@speckle/ui-components'
|
||||
import { inject } from 'vue'
|
||||
import { hostKey } from 'src/injectionKeys'
|
||||
|
||||
const host = inject(hostKey)
|
||||
|
||||
function goToForum() {
|
||||
host.launchUrl('https://speckle.community/tag/powerbi')
|
||||
}
|
||||
|
||||
function goToGuide() {
|
||||
host.launchUrl('https://speckle.guide/user/powerbi')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="speckle-home-view"
|
||||
class="flex flex-col justify-center items-center h-full w-full bg-primary text-center text-foundation"
|
||||
>
|
||||
<div class="flex justify-center items-center">
|
||||
<img src="@assets/logo-white.png" alt="Logo" class="w-1/3" />
|
||||
</div>
|
||||
<p class="heading">Speckle PowerBI 3D Visual</p>
|
||||
<div class="flex justify-center mt-2 gap-1">
|
||||
<FormButton color="invert" @click="goToForum">Help</FormButton>
|
||||
<FormButton color="invert" @click="goToGuide">Getting started</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import ViewerWrapper from 'src/components/ViewerWrapper.vue'
|
||||
import { inject } from 'vue'
|
||||
import { hostKey } from 'src/injectionKeys'
|
||||
|
||||
const host = inject(hostKey)
|
||||
|
||||
const goToSpeckleWebsite = () => host.launchUrl('https://speckle.systems')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="overlay">
|
||||
<img class="watermark" src="@assets/powered-by-speckle.png" @click="goToSpeckleWebsite" />
|
||||
</div>
|
||||
<viewer-wrapper id="speckle-3d-view" class="h-full w-full"></viewer-wrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.watermark {
|
||||
height: auto;
|
||||
width: 60pt;
|
||||
margin-top: 3pt;
|
||||
margin-right: 3pt;
|
||||
}
|
||||
|
||||
.watermark:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
+98
-270
@@ -1,299 +1,127 @@
|
||||
"use strict"
|
||||
import 'core-js/stable'
|
||||
import 'regenerator-runtime/runtime'
|
||||
import '../style/visual.css'
|
||||
import * as _ from 'lodash'
|
||||
import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { store } from 'src/store'
|
||||
import { hostKey, selectionHandlerKey, tooltipHandlerKey, storeKey } from 'src/injectionKeys'
|
||||
|
||||
import { Tracker } from './utils/mixpanel'
|
||||
import { SpeckleDataInput } from './types'
|
||||
import { processMatrixView, validateMatrixView } from './utils/matrixViewUtils'
|
||||
import { SpeckleVisualSettingsModel } from './settings/visualSettingsModel'
|
||||
|
||||
import TooltipHandler from './handlers/tooltipHandler'
|
||||
import SelectionHandler from './handlers/selectionHandler'
|
||||
|
||||
import "core-js/stable"
|
||||
import "regenerator-runtime/runtime" /* <---- add this line */
|
||||
import "./../style/visual.less"
|
||||
import powerbi from "powerbi-visuals-api"
|
||||
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions
|
||||
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions
|
||||
import IVisual = powerbi.extensibility.visual.IVisual
|
||||
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions
|
||||
import VisualObjectInstance = powerbi.VisualObjectInstance
|
||||
import DataView = powerbi.DataView
|
||||
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject
|
||||
|
||||
import { SpeckleVisualSettings } from "./settings"
|
||||
import { Viewer, DefaultViewerParams } from "@speckle/viewer"
|
||||
import ITooltipService = powerbi.extensibility.ITooltipService
|
||||
import {
|
||||
createDataViewWildcardSelector,
|
||||
DataViewWildcardMatchingOption
|
||||
} from 'powerbi-visuals-utils-dataviewutils/lib/dataViewWildcard'
|
||||
import { ColorSelectorSettings } from 'src/settings/colorSettings'
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export class Visual implements IVisual {
|
||||
private target: HTMLElement
|
||||
private settings: SpeckleVisualSettings
|
||||
private host: powerbi.extensibility.IVisualHost
|
||||
private selectionManager: powerbi.extensibility.ISelectionManager
|
||||
private selectionIdMap: Map<string, any>
|
||||
private viewer: Viewer
|
||||
private readonly host: powerbi.extensibility.visual.IVisualHost
|
||||
private selectionHandler: SelectionHandler
|
||||
private tooltipHandler: TooltipHandler
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
console.log("Speckle 3D Visual constructor called", options)
|
||||
private formattingSettings: SpeckleVisualSettingsModel
|
||||
private formattingSettingsService: FormattingSettingsService
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public constructor(options: VisualConstructorOptions) {
|
||||
Tracker.loaded()
|
||||
this.host = options.host
|
||||
this.formattingSettingsService = new FormattingSettingsService()
|
||||
|
||||
this.selectionIdMap = new Map<string, any>()
|
||||
//@ts-ignore
|
||||
this.selectionManager = this.host.createSelectionManager()
|
||||
console.log('🚀 Init handlers')
|
||||
this.selectionHandler = new SelectionHandler(this.host)
|
||||
this.tooltipHandler = new TooltipHandler(this.host.tooltipService as ITooltipService)
|
||||
|
||||
this.target = options.element
|
||||
console.log('🚀 Init Vue App')
|
||||
createApp(App)
|
||||
.use(store, storeKey)
|
||||
.provide(selectionHandlerKey, this.selectionHandler)
|
||||
.provide(tooltipHandlerKey, this.tooltipHandler)
|
||||
.provide(hostKey, options.host)
|
||||
.mount(options.element)
|
||||
}
|
||||
|
||||
public async initViewer() {
|
||||
if (this.viewer) {
|
||||
console.log("Viewer was already initialized. Skipping init call...")
|
||||
private async clear() {
|
||||
this.selectionHandler.clear()
|
||||
}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
// @ts-ignore
|
||||
console.log('⤴️ Update type 👉', powerbi.VisualUpdateType[options.type])
|
||||
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(
|
||||
SpeckleVisualSettingsModel,
|
||||
options.dataViews
|
||||
)
|
||||
|
||||
console.log('Selector colors', this.formattingSettings.colorSelector)
|
||||
let validationResult: { hasColorFilter: boolean; view: powerbi.DataViewMatrix } = null
|
||||
try {
|
||||
console.log('🔍 Validating input...', options)
|
||||
validationResult = validateMatrixView(options)
|
||||
console.log('✅Input valid', validationResult)
|
||||
} catch (e) {
|
||||
console.log('❌Input not valid:', (e as Error).message)
|
||||
this.host.displayWarningIcon(
|
||||
`Incomplete data input.`,
|
||||
`"Model URL", "Version Object ID" and "Object ID" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
|
||||
)
|
||||
console.warn(
|
||||
`Incomplete data input. "Model URL", "Version Object ID" and "Object ID" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
|
||||
)
|
||||
store.commit('setStatus', 'incomplete')
|
||||
return
|
||||
}
|
||||
|
||||
var container = this.target.appendChild(document.createElement("div"))
|
||||
container.style.backgroundColor = "transparent"
|
||||
container.style.height = "100%"
|
||||
container.style.width = "100%"
|
||||
container.style.position = "fixed"
|
||||
|
||||
const params = DefaultViewerParams
|
||||
// Uncomment the line below to show stats
|
||||
params.showStats = true
|
||||
|
||||
const viewer = new Viewer(container, params)
|
||||
|
||||
return viewer.init().then(() => {
|
||||
viewer.onWindowResize()
|
||||
|
||||
viewer.on(
|
||||
"load-progress",
|
||||
(a: { progress: number; id: string; url: string }) => {
|
||||
this.loadedUrls[a.url] = a.progress
|
||||
if (a.progress >= 1) {
|
||||
viewer.onWindowResize()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
viewer.on("load-complete", () => {
|
||||
//console.log("Load complete")
|
||||
})
|
||||
|
||||
viewer.on("select", o => {
|
||||
if (o.location == null) return
|
||||
console.log("viewer object selected", o)
|
||||
//var ids = o.userData.map(data => this.selectionIdMap[data.id])
|
||||
// this.selectionManager.showContextMenu(ids[0] ?? {}, {
|
||||
// x: rect.top + o.location.x,
|
||||
// y: rect.left + o.location.y
|
||||
// })
|
||||
})
|
||||
|
||||
this.viewer = viewer
|
||||
})
|
||||
}
|
||||
|
||||
private loadedUrls = {}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
this.settings = Visual.parseSettings(
|
||||
options && options.dataViews && options.dataViews[0]
|
||||
)
|
||||
|
||||
console.log(
|
||||
`Update was called with update type ${options.type.toString()}`,
|
||||
options,
|
||||
this.settings
|
||||
)
|
||||
|
||||
// TODO: These cases are not being handled right now, we will skip the update logic.
|
||||
// Some are already handled by our viewer, such as resize, but others may require custom implementations in the future.
|
||||
switch (options.type) {
|
||||
case powerbi.VisualUpdateType.Resize:
|
||||
case powerbi.VisualUpdateType.ResizeEnd:
|
||||
case powerbi.VisualUpdateType.Style:
|
||||
case powerbi.VisualUpdateType.ViewMode:
|
||||
case powerbi.VisualUpdateType.Resize + powerbi.VisualUpdateType.ResizeEnd:
|
||||
// Ignore case, nothing will happen
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Data was updated, updating viewer...")
|
||||
this.initViewer().then(_ => {
|
||||
// Handle changes in the visual objects
|
||||
this.handleSettingsUpdate(options)
|
||||
// Handle the update in data passed to this visual
|
||||
return this.handleDataUpdate(options)
|
||||
})
|
||||
}
|
||||
private currentOrthoMode: boolean = undefined
|
||||
private currentDefaultView: string = undefined
|
||||
private handleSettingsUpdate(options: VisualUpdateOptions) {
|
||||
// Handle change in ortho mode
|
||||
if (this.currentOrthoMode != this.settings.camera.orthoMode) {
|
||||
if (this.settings.camera.orthoMode)
|
||||
this.viewer?.cameraHandler?.setOrthoCameraOn()
|
||||
else this.viewer?.cameraHandler?.setPerspectiveCameraOn()
|
||||
this.currentOrthoMode = this.settings.camera.orthoMode
|
||||
}
|
||||
|
||||
// Handle change in default view
|
||||
if (this.currentDefaultView != this.settings.camera.defaultView) {
|
||||
this.viewer.interactions.rotateTo(this.settings.camera.defaultView)
|
||||
this.currentDefaultView = this.settings.camera.defaultView
|
||||
}
|
||||
|
||||
// Update bg of viewer
|
||||
this.target.style.backgroundColor = this.settings.color.background
|
||||
}
|
||||
|
||||
private handleDataUpdate(options: VisualUpdateOptions) {
|
||||
var categoricalView = options.dataViews[0].categorical
|
||||
var streamCategory = categoricalView?.categories[0].values
|
||||
var objectIdCategory = categoricalView?.categories[1].values
|
||||
var highlightedValues = categoricalView?.values
|
||||
? categoricalView?.values[0].highlights
|
||||
: null
|
||||
|
||||
console.log("Viewer loading:", options)
|
||||
//@ts-ignore
|
||||
var selectionBuilder = this.host.createSelectionIdBuilder()
|
||||
|
||||
var objectUrls = streamCategory.map((stream, index) => {
|
||||
var url = `${stream}/objects/${objectIdCategory[index]}`
|
||||
return url
|
||||
})
|
||||
var objectsToUnload = []
|
||||
for (const key in this.selectionIdMap.keys()) {
|
||||
if (!objectUrls.find(url => url.split("/").slice(-1).pop() == key)) {
|
||||
objectsToUnload.push(key)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`Viewer loading ${objectUrls.length} and unloading ${objectsToUnload.length}`
|
||||
)
|
||||
var unloadPromises = objectsToUnload.map(url => {
|
||||
return this.viewer
|
||||
.unloadObject(url)
|
||||
.then(_ => {
|
||||
this.selectionIdMap.delete(url.split("/").slice(-1).pop())
|
||||
})
|
||||
.catch(e => console.warn("Viewer Unload error", url, e))
|
||||
})
|
||||
|
||||
var loadPromises = objectUrls.map((url, index) => {
|
||||
if (!this.selectionIdMap.has(url.split("/").slice(-1).pop())) {
|
||||
var selectionId = selectionBuilder.withCategory(
|
||||
categoricalView?.categories[1].values[index]
|
||||
)
|
||||
return this.viewer
|
||||
.loadObject(url, null, false)
|
||||
.then(_ => {
|
||||
this.selectionIdMap.set(
|
||||
categoricalView?.categories[1].values[index].toString(),
|
||||
selectionId
|
||||
)
|
||||
})
|
||||
.catch(e => {
|
||||
console.warn("Viewer Load error", url, e)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
var unloadRes = Promise.all(unloadPromises)
|
||||
var loadRes = Promise.all(loadPromises)
|
||||
|
||||
return unloadRes
|
||||
.then(_ => loadRes)
|
||||
.then(_ => {
|
||||
var colorList = this.settings.color.getColorList()
|
||||
// Once everything is loaded, run the filter
|
||||
var filter = null
|
||||
if (categoricalView?.values) {
|
||||
var name = categoricalView?.values[0].source.displayName
|
||||
var isNum =
|
||||
categoricalView?.values[0].source.type.numeric ||
|
||||
categoricalView?.values[0].source.type.integer
|
||||
var filterType = isNum ? "gradient" : "category"
|
||||
console.log("filter:", filterType, name)
|
||||
if (highlightedValues)
|
||||
filter = {
|
||||
filterBy: {
|
||||
id: highlightedValues
|
||||
.map((value, index) =>
|
||||
value ? objectIdCategory[index] : null
|
||||
)
|
||||
.filter(e => e != null)
|
||||
},
|
||||
ghostOthers: true,
|
||||
colorBy: {
|
||||
type: filterType,
|
||||
property: this.cleanupDataColumnName(name),
|
||||
gradientColors: isNum ? colorList : undefined,
|
||||
minValue: categoricalView?.values[0].minLocal,
|
||||
maxValue: categoricalView?.values[0].maxLocal
|
||||
}
|
||||
}
|
||||
else
|
||||
filter = {
|
||||
filterBy: {
|
||||
id: objectIdCategory
|
||||
},
|
||||
colorBy: {
|
||||
type: filterType,
|
||||
property: this.cleanupDataColumnName(name),
|
||||
gradientColors: isNum ? colorList : undefined,
|
||||
minValue: categoricalView?.values[0].minLocal,
|
||||
maxValue: categoricalView?.values[0].maxLocal
|
||||
}
|
||||
}
|
||||
default:
|
||||
try {
|
||||
const input = processMatrixView(
|
||||
validationResult.view,
|
||||
this.host,
|
||||
validationResult.hasColorFilter,
|
||||
this.formattingSettings,
|
||||
(obj, id) => this.selectionHandler.set(obj, id)
|
||||
)
|
||||
this.throttleUpdate(input)
|
||||
} catch (error) {
|
||||
console.error('Data update error', error ?? 'Unknown')
|
||||
}
|
||||
console.log("filter:", filter)
|
||||
return this.viewer
|
||||
.applyFilter(filter)
|
||||
.catch(e => {
|
||||
console.warn("Filter failed to be applied. Filter will be reset", e)
|
||||
return this.viewer.applyFilter(null)
|
||||
})
|
||||
.then(_ => this.viewer.zoomExtents())
|
||||
})
|
||||
}
|
||||
private cleanupDataColumnName(name: string) {
|
||||
var cleanName = name
|
||||
var simplePrefixes = ["First", "Last"]
|
||||
var compoundPrefixes = [
|
||||
"Count",
|
||||
"Sum",
|
||||
"Average",
|
||||
"Minimum",
|
||||
"Maximum",
|
||||
"Count",
|
||||
"Standard deviation",
|
||||
"Variance",
|
||||
"Median"
|
||||
].map(prefix => prefix + " of")
|
||||
|
||||
var prefixes = [...simplePrefixes, ...compoundPrefixes].map(
|
||||
prefix => prefix + " "
|
||||
)
|
||||
|
||||
for (let i = 0; i < prefixes.length; i++) {
|
||||
const prefix = prefixes[i]
|
||||
if (name.startsWith(prefix)) {
|
||||
cleanName = name.slice(prefix.length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanName.startsWith("data.")) cleanName = cleanName.split("data.")[0]
|
||||
console.log("clean name", cleanName)
|
||||
return cleanName
|
||||
}
|
||||
private static parseSettings(dataView: DataView): SpeckleVisualSettings {
|
||||
return <SpeckleVisualSettings>SpeckleVisualSettings.parse(dataView)
|
||||
public getFormattingModel(): powerbi.visuals.FormattingModel {
|
||||
console.log('Showing Formatting settings', this.formattingSettings)
|
||||
const model = this.formattingSettingsService.buildFormattingModel(this.formattingSettings)
|
||||
console.log('Formatting model was created', model)
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* This function gets called for each of the objects defined in the capabilities files and allows you to select which of the
|
||||
* objects and properties you want to expose to the users in the property pane.
|
||||
*
|
||||
*/
|
||||
public enumerateObjectInstances(
|
||||
options: EnumerateVisualObjectInstancesOptions
|
||||
): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
|
||||
return SpeckleVisualSettings.enumerateObjectInstances(
|
||||
this.settings || SpeckleVisualSettings.getDefault(),
|
||||
options
|
||||
)
|
||||
private throttleUpdate = _.throttle((input: SpeckleDataInput) => {
|
||||
this.tooltipHandler.setup(input.objectTooltipData)
|
||||
store.commit('setInput', input)
|
||||
store.commit('setStatus', 'valid')
|
||||
store.commit('setSettings', this.formattingSettings)
|
||||
}, 500)
|
||||
|
||||
public async destroy() {
|
||||
await this.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@import '@speckle/ui-components/style.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,9 +0,0 @@
|
||||
p {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
em {
|
||||
background: yellow;
|
||||
padding: 5px;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const speckleTheme = require("@speckle/tailwind-theme");
|
||||
const themeConfig = require("@speckle/tailwind-theme/tailwind-configure");
|
||||
const uiConfig = require("@speckle/ui-components/tailwind-configure");
|
||||
const formsPlugin = require("@tailwindcss/forms");
|
||||
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./src/**/*.{js,ts,vue}", themeConfig.tailwindContentEntry(require), uiConfig.tailwindContentEntry(require)],
|
||||
plugins: [speckleTheme.default, formsPlugin]
|
||||
};
|
||||
+28
-17
@@ -1,19 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2020",
|
||||
"sourceMap": true,
|
||||
"outDir": "./.tmp/build/",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"lib": ["es2020", "dom"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"types": [],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"],
|
||||
"@assets/*": ["assets/*"]
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"outDir": "./.tmp/build/",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"lib": [
|
||||
"es2015",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"./src/visual.ts"
|
||||
]
|
||||
}
|
||||
"module": "CommonJS",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
},
|
||||
"files": ["./src/visual.ts"],
|
||||
"include": ["./src/**/*.ts", "./src/**/*.vue"],
|
||||
"exclude": ["webpack.config.dev.ts"]
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "tslint-microsoft-contrib/recommended",
|
||||
"rulesDirectory": [
|
||||
"node_modules/tslint-microsoft-contrib"
|
||||
],
|
||||
"rules": {
|
||||
"no-relative-imports": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from 'path'
|
||||
|
||||
// api configuration
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'
|
||||
import { BundleAnalyzerPlugin as Visualizer } from 'webpack-bundle-analyzer'
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||
import { PowerBICustomVisualsWebpackPlugin } from 'powerbi-visuals-webpack-plugin'
|
||||
import webpack from 'webpack'
|
||||
import fs from 'fs'
|
||||
import { WebpackConfiguration } from 'webpack-cli'
|
||||
import { VueLoaderPlugin } from 'vue-loader'
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const powerbiApi: any = powerbi // Types for PowerBI seem to be off, so I'm instead forcing it to `any`
|
||||
|
||||
// visual configuration json path
|
||||
const pbivizPath = './pbiviz.json'
|
||||
const pbivizFile = require(path.join(__dirname, pbivizPath))
|
||||
|
||||
// the visual capabilities content
|
||||
const capabilitiesPath = './capabilities.json'
|
||||
const capabilitiesFile = require(path.join(__dirname, capabilitiesPath))
|
||||
|
||||
const pluginLocation = './.tmp/precompile/visualPlugin.ts' // path to visual plugin file, the file generates by the plugin
|
||||
|
||||
// string resources
|
||||
const resourcesFolder = path.join('.', 'stringResources')
|
||||
const localizationFolders = fs.existsSync(resourcesFolder) && fs.readdirSync(resourcesFolder)
|
||||
const statsLocation = '../../webpack.statistics.html'
|
||||
|
||||
// babel options to support IE11
|
||||
const babelOptions = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
ie: '11'
|
||||
},
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
],
|
||||
plugins: [],
|
||||
sourceType: 'unambiguous', // tell to babel that the project can contain different module types, not only es2015 modules
|
||||
cacheDirectory: path.join('.tmp', 'babelCache') // path for cache files
|
||||
}
|
||||
|
||||
const config: WebpackConfiguration = {
|
||||
entry: {
|
||||
visual: pluginLocation
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: false,
|
||||
minimize: false // enable minimization for create *.pbiviz file less than 2 Mb, can be disabled for dev mode
|
||||
},
|
||||
devtool: 'source-map',
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: ['vue-loader']
|
||||
},
|
||||
{
|
||||
parser: {
|
||||
amd: false
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /(\.ts)x|\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
// '@babel/react',
|
||||
'@babel/env'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: false,
|
||||
experimentalWatchApi: false,
|
||||
appendTsSuffixTo: [/\.vue$/]
|
||||
}
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/],
|
||||
include: /.tmp|powerbi-visuals-|src|precompile\\visualPlugin.ts/
|
||||
},
|
||||
{
|
||||
test: /(\.js)x|\.js$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: babelOptions
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)?$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(woff|ttf|ico|woff2|jpg|jpeg|png|webp|svg)$/i,
|
||||
use: ['base64-inline-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'],
|
||||
alias: {
|
||||
src: path.resolve(__dirname, 'src/'),
|
||||
assets: path.resolve(__dirname, 'assets/')
|
||||
},
|
||||
plugins: [new TsconfigPathsPlugin()]
|
||||
},
|
||||
output: {
|
||||
publicPath: '/assets',
|
||||
path: path.join(__dirname, '/.tmp', 'drop'),
|
||||
library: +powerbiApi.version.replace(/\./g, '') >= 320 ? pbivizFile.visual.guid : undefined,
|
||||
libraryTarget: +powerbiApi.version.replace(/\./g, '') >= 320 ? 'var' : undefined
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, '.tmp', 'drop'), // path with assets for dev server, they are generated by webpack plugin
|
||||
publicPath: '/assets'
|
||||
},
|
||||
compress: true,
|
||||
port: 8080, // dev server port
|
||||
hot: false,
|
||||
https: {},
|
||||
liveReload: false,
|
||||
webSocketServer: false,
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
'cache-control': 'public, max-age=0'
|
||||
}
|
||||
},
|
||||
externals:
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false'
|
||||
}
|
||||
: {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false',
|
||||
corePowerbiObject: "Function('return this.powerbi')()",
|
||||
realWindow: "Function('return this')()"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: JSON.stringify(true),
|
||||
__VUE_PROD_DEVTOOLS__: JSON.stringify(false)
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'visual.css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new Visualizer({
|
||||
reportFilename: statsLocation,
|
||||
openAnalyzer: false,
|
||||
analyzerMode: `static`
|
||||
}),
|
||||
// visual plugin regenerates with the visual source, but it does not require relaunching dev server
|
||||
new webpack.WatchIgnorePlugin({
|
||||
paths: [path.join(__dirname, pluginLocation), './.tmp/**/*.*']
|
||||
}),
|
||||
// custom visuals plugin instance with options
|
||||
new PowerBICustomVisualsWebpackPlugin({
|
||||
...pbivizFile,
|
||||
compression: 0,
|
||||
capabilities: capabilitiesFile,
|
||||
stringResources:
|
||||
localizationFolders &&
|
||||
localizationFolders.map((localization) =>
|
||||
path.join(resourcesFolder, localization, 'resources.resjson')
|
||||
),
|
||||
apiVersion: powerbiApi.version,
|
||||
capabilitiesSchema: powerbiApi.schemas.capabilities,
|
||||
pbivizSchema: powerbiApi.schemas.pbiviz,
|
||||
stringResourcesSchema: powerbiApi.schemas.stringResources,
|
||||
dependenciesSchema: powerbiApi.schemas.dependencies,
|
||||
devMode: false,
|
||||
generatePbiviz: false,
|
||||
generateResources: true,
|
||||
minifyJS: false,
|
||||
minify: false,
|
||||
modules: true,
|
||||
visualSourceLocation: '../../src/visual',
|
||||
pluginLocation: pluginLocation,
|
||||
packageOutPath: path.join(__dirname, 'dist')
|
||||
}),
|
||||
new ExtraWatchWebpackPlugin({
|
||||
files: [pbivizPath, capabilitiesPath]
|
||||
}),
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? new webpack.ProvidePlugin({
|
||||
define: 'fakeDefine'
|
||||
})
|
||||
: new webpack.ProvidePlugin({
|
||||
window: 'realWindow',
|
||||
define: 'fakeDefine',
|
||||
powerbi: 'corePowerbiObject'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,209 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from 'path'
|
||||
|
||||
// api configuration
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'
|
||||
import { BundleAnalyzerPlugin as Visualizer } from 'webpack-bundle-analyzer'
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||
import { PowerBICustomVisualsWebpackPlugin } from 'powerbi-visuals-webpack-plugin'
|
||||
import webpack from 'webpack'
|
||||
import fs from 'fs'
|
||||
import { WebpackConfiguration } from 'webpack-cli'
|
||||
import { VueLoaderPlugin } from 'vue-loader'
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const powerbiApi: any = powerbi // Types for PowerBI seem to be off, so I'm instead forcing it to `any`
|
||||
|
||||
// visual configuration json path
|
||||
const pbivizPath = './pbiviz.json'
|
||||
const pbivizFile = require(path.join(__dirname, pbivizPath))
|
||||
|
||||
// the visual capabilities content
|
||||
const capabilitiesPath = './capabilities.json'
|
||||
const capabilitiesFile = require(path.join(__dirname, capabilitiesPath))
|
||||
|
||||
const pluginLocation = './.tmp/precompile/visualPlugin.ts' // path to visual plugin file, the file generates by the plugin
|
||||
|
||||
// string resources
|
||||
const resourcesFolder = path.join('.', 'stringResources')
|
||||
const localizationFolders = fs.existsSync(resourcesFolder) && fs.readdirSync(resourcesFolder)
|
||||
const statsLocation = '../../webpack.statistics.html'
|
||||
|
||||
// babel options to support IE11
|
||||
const babelOptions = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
ie: '11'
|
||||
},
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
],
|
||||
plugins: [],
|
||||
sourceType: 'unambiguous', // tell to babel that the project can contain different module types, not only es2015 modules
|
||||
cacheDirectory: path.join('.tmp', 'babelCache') // path for cache files
|
||||
}
|
||||
|
||||
const config: WebpackConfiguration = {
|
||||
entry: {
|
||||
visual: pluginLocation
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: false,
|
||||
minimize: true // enable minimization for create *.pbiviz file less than 2 Mb, can be disabled for dev mode
|
||||
},
|
||||
devtool: 'source-map',
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: ['vue-loader']
|
||||
},
|
||||
{
|
||||
parser: {
|
||||
amd: false
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /(\.ts)x|\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
// '@babel/react',
|
||||
'@babel/env'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: false,
|
||||
experimentalWatchApi: false,
|
||||
appendTsSuffixTo: [/\.vue$/]
|
||||
}
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/],
|
||||
include: /.tmp|powerbi-visuals-|src|precompile\\visualPlugin.ts/
|
||||
},
|
||||
{
|
||||
test: /(\.js)x|\.js$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: babelOptions
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)?$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(woff|ttf|ico|woff2|jpg|jpeg|png|webp|svg)$/i,
|
||||
use: ['base64-inline-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'],
|
||||
alias: {
|
||||
src: path.resolve(__dirname, 'src/'),
|
||||
assets: path.resolve(__dirname, 'assets/')
|
||||
},
|
||||
plugins: [new TsconfigPathsPlugin()]
|
||||
},
|
||||
output: {
|
||||
publicPath: '/assets',
|
||||
path: path.join(__dirname, '/.tmp', 'drop'),
|
||||
library: +powerbiApi.version.replace(/\./g, '') >= 320 ? pbivizFile.visual.guid : undefined,
|
||||
libraryTarget: +powerbiApi.version.replace(/\./g, '') >= 320 ? 'var' : undefined
|
||||
},
|
||||
externals:
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false'
|
||||
}
|
||||
: {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false',
|
||||
corePowerbiObject: "Function('return this.powerbi')()",
|
||||
realWindow: "Function('return this')()"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: JSON.stringify(true),
|
||||
__VUE_PROD_DEVTOOLS__: JSON.stringify(false)
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'visual.css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new Visualizer({
|
||||
reportFilename: statsLocation,
|
||||
openAnalyzer: false,
|
||||
analyzerMode: `static`
|
||||
}),
|
||||
// visual plugin regenerates with the visual source, but it does not require relaunching dev server
|
||||
new webpack.WatchIgnorePlugin({
|
||||
paths: [path.join(__dirname, pluginLocation), './.tmp/**/*.*']
|
||||
}),
|
||||
// custom visuals plugin instance with options
|
||||
new PowerBICustomVisualsWebpackPlugin({
|
||||
...pbivizFile,
|
||||
compression: 9,
|
||||
capabilities: capabilitiesFile,
|
||||
stringResources:
|
||||
localizationFolders &&
|
||||
localizationFolders.map((localization) =>
|
||||
path.join(resourcesFolder, localization, 'resources.resjson')
|
||||
),
|
||||
apiVersion: powerbiApi.version,
|
||||
capabilitiesSchema: powerbiApi.schemas.capabilities,
|
||||
pbivizSchema: powerbiApi.schemas.pbiviz,
|
||||
stringResourcesSchema: powerbiApi.schemas.stringResources,
|
||||
dependenciesSchema: powerbiApi.schemas.dependencies,
|
||||
devMode: false,
|
||||
generatePbiviz: true,
|
||||
generateResources: true,
|
||||
minifyJS: true,
|
||||
minify: true,
|
||||
modules: true,
|
||||
visualSourceLocation: '../../src/visual',
|
||||
pluginLocation: pluginLocation,
|
||||
packageOutPath: path.join(__dirname, 'dist')
|
||||
}),
|
||||
new ExtraWatchWebpackPlugin({
|
||||
files: [pbivizPath, capabilitiesPath]
|
||||
}),
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? new webpack.ProvidePlugin({
|
||||
define: 'fakeDefine'
|
||||
})
|
||||
: new webpack.ProvidePlugin({
|
||||
window: 'realWindow',
|
||||
define: 'fakeDefine',
|
||||
powerbi: 'corePowerbiObject'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
||||
Reference in New Issue
Block a user