Merge pull request #195 from specklesystems/dogukan/online-mode-with-object-loader
Build and deploy Connector and Visual / build-connector (push) Has been cancelled
Build and deploy Connector and Visual / build-visual (push) Has been cancelled
Build and deploy Connector and Visual / deploy-installers (push) Has been cancelled

feat (visual): dual-mode data loading
This commit is contained in:
Dogukan Karatas
2025-08-27 20:40:40 +02:00
committed by GitHub
10 changed files with 1003 additions and 223 deletions
@@ -37,15 +37,37 @@
workspaceInfo = GetWorkspace(url),
// Prepare request data
// exchange powerful token for weak token via ds
tokenExchangeData = Json.FromValue([
PowerfulToken = apiKey,
Scopes = {"profile:read", "streams:read", "users:read"},
ProjectId = parsedUrl[projectId]
]),
tokenExchangeResponse = Web.Contents(
"http://127.0.0.1:29364/auth/exchange-token",
[
Headers = [
#"Content-Type" = "application/json",
#"Method" = "POST"
],
Content = tokenExchangeData,
ManualStatusHandling = {400, 401, 403, 404, 500}
]
),
tokenExchangeJson = Json.Document(tokenExchangeResponse),
weakToken = tokenExchangeJson[token],
// prepare request data with weak token
requestData = Json.FromValue([
Url = url,
Server = parsedUrl[baseUrl],
Email = userEmail,
ProjectId = parsedUrl[projectId],
ObjectId = modelInfo[rootObjectId],
RootObjectId = modelInfo[rootObjectId],
SourceApplication = modelInfo[sourceApplication],
Token = apiKey,
Token = weakToken,
Version = connectorVersion,
VersionId = parsedUrl[versionId],
WorkspaceId = workspaceInfo[workspaceId],
+9
View File
@@ -133,6 +133,15 @@
}
}
},
"dataLoading": {
"properties": {
"internalizeData": {
"type": {
"bool": true
}
}
}
},
"color": {
"properties": {
"enabled": {
+334 -12
View File
@@ -13,7 +13,8 @@
"@babel/runtime-corejs3": "^7.21.5",
"@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.0.12",
"@speckle/objectloader": "^2.23.8",
"@speckle/objectloader": "^2.25.9",
"@speckle/objectloader2": "^2.25.9",
"@speckle/tailwind-theme": "2.23.2",
"@speckle/ui-components": "2.23.2",
"@speckle/viewer": "2.23.23",
@@ -2826,6 +2827,13 @@
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@ioredis/commands": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz",
"integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==",
"license": "MIT",
"peer": true
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2979,6 +2987,90 @@
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
"dev": true
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3052,12 +3144,13 @@
"peer": true
},
"node_modules/@speckle/objectloader": {
"version": "2.23.23",
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.23.23.tgz",
"integrity": "sha512-k0qxk5M0Q57h+fth6GQq8N7SjeJnWHxjDlMDYC56lOZkH8vI0Y2RHG33+DiBvC7iHnTIRuZc0SyTRrJ64Cuhrg==",
"version": "2.25.9",
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.25.9.tgz",
"integrity": "sha512-ZSMinqrHm4Hx3x6dth2kfnJO2O1zI0i4E0eE3PS9hg1HAtCx9opIT/eKIzj5fYY/cUyfXxP00k7qXmZ3KAUl7w==",
"license": "Apache-2.0",
"dependencies": {
"@babel/core": "^7.17.9",
"@speckle/shared": "^2.23.23",
"@speckle/shared": "^2.25.9",
"core-js": "^3.21.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
@@ -3067,11 +3160,25 @@
"node": ">=18.0.0"
}
},
"node_modules/@speckle/shared": {
"version": "2.23.23",
"resolved": "https://registry.npmjs.org/@speckle/shared/-/shared-2.23.23.tgz",
"integrity": "sha512-5laonEcP7FsG5CPn/EXq0tpkA135Bxq3TndZ+OJGzuaY9L6ZHLtSURltZ6YpjMo6CHmAiFEVG62laP6UHwIL4w==",
"node_modules/@speckle/objectloader2": {
"version": "2.25.9",
"resolved": "https://registry.npmjs.org/@speckle/objectloader2/-/objectloader2-2.25.9.tgz",
"integrity": "sha512-fZYkVGBCNUcVMMZnIljmtqiyXpSlyWiuW+qbpfls5lX6P9ZLP6DC88AUjyJQ6cM9jZ6RGNk3/Sa+MeReTeZIvg==",
"license": "Apache-2.0",
"dependencies": {
"@speckle/shared": "^2.25.9"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@speckle/shared": {
"version": "2.25.9",
"resolved": "https://registry.npmjs.org/@speckle/shared/-/shared-2.25.9.tgz",
"integrity": "sha512-7hK6v55tSu8OTxza9UakettBpzlbyIWq17jtzjf3ZSnqqjiasayhG3O0/uuRI2GoeingoOqUMLvybatjDu4ang==",
"license": "Apache-2.0",
"dependencies": {
"dayjs": "^1.11.13",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"nanoid": "^5.1.5",
@@ -3083,6 +3190,7 @@
},
"peerDependencies": {
"@tiptap/core": "^2.0.0-beta.176",
"bull": "*",
"knex": "*",
"mixpanel": "^0.17.0",
"pino": "^8.7.0",
@@ -5271,6 +5379,38 @@
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
"dev": true
},
"node_modules/bull": {
"version": "4.16.5",
"resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz",
"integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cron-parser": "^4.9.0",
"get-port": "^5.1.1",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"msgpackr": "^1.11.2",
"semver": "^7.5.2",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/bull/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
@@ -5526,6 +5666,16 @@
"node": ">=6"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -5845,6 +5995,19 @@
"license": "MIT",
"peer": true
},
"node_modules/cron-parser": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"luxon": "^3.2.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -6076,6 +6239,12 @@
"node": "*"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -6198,6 +6367,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -6235,6 +6414,17 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
@@ -7741,6 +7931,19 @@
"node": ">=8.0.0"
}
},
"node_modules/get-port": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -8485,6 +8688,31 @@
"node": ">= 0.10"
}
},
"node_modules/ioredis": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz",
"integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ioredis/commands": "^1.3.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -9262,8 +9490,14 @@
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT",
"peer": true
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
@@ -9327,6 +9561,16 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -9653,6 +9897,39 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/msgpackr": {
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
"license": "MIT",
"peer": true,
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/multicast-dns": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
@@ -9773,6 +10050,22 @@
"node": ">= 6.13.0"
}
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@@ -12547,6 +12840,29 @@
"node": ">=8"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"peer": true,
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -13383,6 +13699,13 @@
"node": ">= 10.x"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT",
"peer": true
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -14486,7 +14809,6 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
+2 -1
View File
@@ -17,7 +17,8 @@
"@babel/runtime-corejs3": "^7.21.5",
"@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.0.12",
"@speckle/objectloader": "^2.23.8",
"@speckle/objectloader": "^2.25.9",
"@speckle/objectloader2": "^2.25.9",
"@speckle/tailwind-theme": "2.23.2",
"@speckle/ui-components": "2.23.2",
"@speckle/viewer": "2.23.23",
@@ -0,0 +1,103 @@
import { useVisualStore } from '@src/store/visualStore'
import ObjectLoader from '@speckle/objectloader' // Default import for v1
interface SpeckleObject {
id: string
speckle_type?: string
[key: string]: any
}
export class SpeckleApiLoader {
private serverUrl: string
private token: string
private projectId: string
private headers: Record<string, string>
constructor(serverUrl: string, projectId: string, token: string) {
this.serverUrl = serverUrl.replace(/\/$/, '')
this.projectId = projectId
this.token = token
this.headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
async downloadObjectsWithChildren(objectId: string, onProgress?: (loaded: number, total: number) => void): Promise<SpeckleObject[]> {
const visualStore = useVisualStore()
visualStore.setLoadingProgress('Initializing object loader', 0)
console.log('Creating ObjectLoader v1 for Power BI environment')
// Create ObjectLoader v1 instance - use 'token' not 'authToken'
const loader = new ObjectLoader({
serverUrl: this.serverUrl,
streamId: this.projectId,
objectId: objectId,
token: this.token,
options: {
enableCaching: false, // Disable caching for Power BI environment
}
})
try {
// Get total count for progress tracking
const totalCount = await loader.getTotalObjectCount()
console.log(`Loading ${totalCount} objects using ObjectLoader v1`)
const objects: SpeckleObject[] = []
let loadedCount = 0
// Stream all objects using the async iterator
for await (const obj of loader.getObjectIterator()) {
objects.push(obj as SpeckleObject) // Type assertion since ObjectLoader v1 has different type
loadedCount++
// Update progress
if (onProgress) {
onProgress(loadedCount, totalCount)
}
const progress = totalCount > 0 ? loadedCount / totalCount : 0
visualStore.setLoadingProgress('🌍 Loading from Speckle', progress)
// Log progress every 100 objects
if (loadedCount % 100 === 0) {
console.log(`Loaded ${loadedCount}/${totalCount} objects`)
}
}
console.log(`Downloaded ${objects.length} objects using ObjectLoader v1`)
visualStore.setLoadingProgress('Download complete', 1)
return objects
} catch (error) {
console.error('Error loading objects:', error)
throw error
} finally {
// ObjectLoader v1 cleanup
if (loader.dispose) {
loader.dispose()
}
}
}
async downloadFromVersionId(versionId: string): Promise<SpeckleObject[]> {
// For version IDs, we can't avoid GraphQL entirely as we need to resolve the referenced object
// However, this method might not be used if we're getting object IDs directly from the data connector
throw new Error('Version ID downloads not supported with weak tokens. Use object IDs directly.')
}
async downloadMultipleModels(objectIds: string[]): Promise<SpeckleObject[][]> {
const allObjects: SpeckleObject[][] = []
for (const objectId of objectIds) {
const objects = await this.downloadObjectsWithChildren(objectId)
allObjects.push(objects)
}
return allObjects
}
}
@@ -0,0 +1,15 @@
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
export class DataLoadingSettings extends fs.SimpleCard {
name = 'dataLoading'
displayName = 'Data Management'
public internalizeData = new fs.ToggleSwitch({
name: 'internalizeData',
displayName: 'Internalize Data',
description: 'When enabled, objects are downloaded and stored in the Power BI file for offline access. When disabled, objects are loaded directly from Speckle servers (online mode).',
value: false
})
slices: fs.Slice[] = [this.internalizeData]
}
@@ -2,6 +2,7 @@ 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'
import { DataLoadingSettings } from 'src/settings/dataLoadingSettings'
export class SpeckleVisualSettingsModel extends fs.Model {
// Building my visual formatting settings card
@@ -9,9 +10,11 @@ export class SpeckleVisualSettingsModel extends fs.Model {
public colorSelector: ColorSelectorSettings = new ColorSelectorSettings()
public dataLoading: DataLoadingSettings = new DataLoadingSettings()
// public camera: CameraSettings = new CameraSettings()
// public lighting: LightingSettings = new LightingSettings()
cards = [this.color]
cards = [this.color, this.dataLoading]
}
+68 -6
View File
@@ -3,8 +3,8 @@ import { Version } from '@src/composables/useUpdateConnector'
import { ColorBy, IViewerEvents } from '@src/plugins/viewer'
import { SpeckleVisualSettingsModel } from '@src/settings/visualSettingsModel'
import { SpeckleDataInput } from '@src/types'
import { zipModelObjects } from '@src/utils/compression'
import { ReceiveInfo } from '@src/utils/matrixViewUtils'
import { zipModelObjects } from '@src/utils/compression'
import { defineStore } from 'pinia'
import { Vector3 } from 'three'
import { computed, ref, shallowRef } from 'vue'
@@ -28,6 +28,9 @@ export const useVisualStore = defineStore('visualStore', () => {
const loadingProgress = ref<LoadingProgress>(undefined)
const objectsFromStore = ref<object[]>(undefined)
// State tracking for toggle reset prevention
const previousToggleState = ref<boolean | undefined>(undefined)
const postFileSaveSkipNeeded = ref<boolean>(false)
const postClickSkipNeeded = ref<boolean>(false)
@@ -83,7 +86,13 @@ export const useVisualStore = defineStore('visualStore', () => {
host.value = hostToSet
}
const setReceiveInfo = (newReceiveInfo: ReceiveInfo) => (receiveInfo.value = newReceiveInfo)
const setReceiveInfo = (newReceiveInfo: ReceiveInfo) => {
receiveInfo.value = newReceiveInfo
// Only save receiveInfo to file in offline mode for persistence (contains token and metadata)
if (formattingSettings.value?.dataLoading.internalizeData.value) {
writeReceiveInfoToFile()
}
}
const setLatestAvailableVersion = (version: Version | null) => {
latestAvailableVersion.value = version
@@ -134,17 +143,24 @@ export const useVisualStore = defineStore('visualStore', () => {
}
const loadObjectsFromFile = async (objects: object[][]) => {
console.log('📁 loadObjectsFromFile called with:', objects.length, 'models')
const savedVersionObjectId = objects.map((o) => (o[0] as SpeckleObject).id).join(',')
lastLoadedRootObjectId.value = savedVersionObjectId
viewerReloadNeeded.value = false
console.log(`📦 Loading viewer from cached data with ${lastLoadedRootObjectId.value} id.`)
console.log('📁 About to call viewerEmit loadObjects...')
await viewerEmit.value('loadObjects', objects)
console.log('📁 viewerEmit loadObjects completed')
objectsFromStore.value = objects
isViewerObjectsLoaded.value = true
viewerReloadNeeded.value = false
setIsLoadingFromFile(false)
console.log('📁 loadObjectsFromFile completed successfully')
}
const setIsLoadingFromFile = (newValue: boolean) => (isLoadingFromFile.value = newValue)
/**
* Sets upcoming data input into store to be able to pass it through viewer by evaluating the data.
* @param newValue new data input that user dragged and dropped to the fields in visual
@@ -159,8 +175,14 @@ export const useVisualStore = defineStore('visualStore', () => {
await viewerEmit.value('loadObjects', dataInput.value.modelObjects)
viewerReloadNeeded.value = false
isViewerObjectsLoaded.value = true
setLoadingProgress('Storing objects into file', null)
writeObjectsToFile(dataInput.value.modelObjects)
// Store the model objects for potential internalization
if (dataInput.value.modelObjects && dataInput.value.modelObjects.length > 0) {
console.log('📦 Storing modelObjects in visualStore for internalization:', dataInput.value.modelObjects.length, 'models')
objectsFromStore.value = dataInput.value.modelObjects
}
// Note: Object internalization is now handled by toggle in visual.ts
loadingProgress.value = undefined
}
@@ -200,6 +222,23 @@ export const useVisualStore = defineStore('visualStore', () => {
})
}
const writeReceiveInfoToFile = () => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'storedData',
properties: {
receiveInfo: JSON.stringify(receiveInfo.value)
},
selector: null
}
]
})
}
const writeCameraViewToFile = (view: CanonicalView) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
@@ -312,6 +351,22 @@ export const useVisualStore = defineStore('visualStore', () => {
})
}
const writeDataLoadingModeToFile = (internalizeData: boolean) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'dataLoading',
properties: {
internalizeData: internalizeData
},
selector: null
}
]
})
}
const writeCameraPositionToFile = (position: Vector3, target: Vector3) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
@@ -338,7 +393,6 @@ export const useVisualStore = defineStore('visualStore', () => {
const clearDataInput = () => (dataInput.value = null)
const setIsLoadingFromFile = (newValue: boolean) => (isLoadingFromFile.value = newValue)
const setViewerReadyToLoad = (newValue: boolean) => (isViewerReadyToLoad.value = newValue)
@@ -437,6 +491,11 @@ export const useVisualStore = defineStore('visualStore', () => {
host.value.refreshHostData()
}
// Toggle state tracking functions
const setPreviousToggleState = (state: boolean) => {
previousToggleState.value = state
}
return {
host,
receiveInfo,
@@ -468,6 +527,7 @@ export const useVisualStore = defineStore('visualStore', () => {
latestAvailableVersion,
isConnectorUpToDate,
commonError,
previousToggleState,
setCommonError,
setLatestAvailableVersion,
setIsOrthoProjection,
@@ -495,6 +555,7 @@ export const useVisualStore = defineStore('visualStore', () => {
writeCameraPositionToFile,
writeHideBrandingToFile,
writeNavbarVisibilityToFile,
writeDataLoadingModeToFile,
toggleBranding,
toggleNavbar,
setViewerEmitter,
@@ -507,6 +568,7 @@ export const useVisualStore = defineStore('visualStore', () => {
setIsLoadingFromFile,
resetFilters,
downloadLatestVersion,
handleObjectsLoadedComplete
handleObjectsLoadedComplete,
setPreviousToggleState
}
})
+172 -175
View File
@@ -11,6 +11,8 @@ import { FieldInputState, useVisualStore } from '@src/store/visualStore'
import { delay } from 'lodash'
import { getSlugFromHostAppNameAndVersion } from './hostAppSlug'
import { useUpdateConnector } from '@src/composables/useUpdateConnector'
import { SpeckleApiLoader } from '@src/loader/SpeckleApiLoader'
import { unzipModelObjects } from './compression'
export class AsyncPause {
private lastPauseTime = 0
@@ -158,40 +160,8 @@ export type ReceiveInfo = {
workspaceName?: string
canHideBranding: boolean
version?: string
}
export type PreGetObjects = {
modelExists: boolean
objectCount?: number
}
async function getPreGetObjects(commaSeparatedModelIds: string): Promise<PreGetObjects[]> {
const modelIds = (commaSeparatedModelIds as string).split(',')
const preGetObjects = []
for await (const id of modelIds) {
const res = await getPreGetObjectsForModel(id)
preGetObjects.push(res)
}
return preGetObjects
}
async function getPreGetObjectsForModel(id: string): Promise<PreGetObjects> {
try {
const preGetObjectsRes = await fetch(`http://localhost:29364/pre-get-objects/${id}`)
if (!preGetObjectsRes.body) {
console.log('No response body for pre get objects')
return {
modelExists: false,
objectCount: null
} as PreGetObjects
}
return (await preGetObjectsRes.json()) as PreGetObjects
} catch (error) {
console.log(error)
}
token: string
projectId?: string
}
async function getReceiveInfo(id) {
@@ -206,120 +176,33 @@ async function getReceiveInfo(id) {
return await response.json()
} catch (error) {
console.log(error)
console.log("User infp couldn't retrieved from local server.")
console.log("User info couldn't retrieved from local server.")
}
}
async function fetchStreamedData(commaSeparatedModelIds: string, totalObjectCount: number) {
const modelIds = (commaSeparatedModelIds as string).split(',')
async function fetchFromSpeckleApi(
objectIds: string,
serverUrl: string,
projectId: string,
token: string
): Promise<object[][]> {
const ids = objectIds.split(',')
const modelObjects = []
let loadedObjectCount = 0
for await (const id of modelIds) {
const objects = await fetchStreamedDataForModel(id, totalObjectCount, loadedObjectCount)
modelObjects.push(objects)
loadedObjectCount += objects.length
}
return modelObjects
}
async function fetchStreamedDataForModel(
id: string,
totalObjectCount: number,
loadedObjectCount: number
) {
console.log(loadedObjectCount, totalObjectCount)
try {
const visualStore = useVisualStore()
const response = await fetch(`http://localhost:29364/get-objects/${id}`)
if (!response.body) {
console.error('No response body')
return
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
const objects = []
let buffer = ''
const start = performance.now()
console.log('Streaming started...')
for await (const chunk of readStream(reader)) {
// chucks.push(chuck)
buffer += decoder.decode(chunk, { stream: true })
let boundary
while ((boundary = buffer.indexOf('\n')) !== -1) {
const jsonString = buffer.slice(0, boundary)
buffer = buffer.slice(boundary + 1)
try {
const obj = JSON.parse(jsonString)
objects.push(obj)
visualStore.setLoadingProgress(
'Loading objects from storage',
(objects.length + loadedObjectCount) / totalObjectCount
)
// console.log('Loading', (objects.length + loadedObjectCount) / totalObjectCount)
// console.log('Received object:', jsonObject)
} catch (e) {
console.error('Invalid JSON chunk:', jsonString)
}
}
}
for (const objectId of ids) {
try {
const obj = JSON.parse(buffer)
objects.push(obj)
// console.log('Received object:', jsonObject)
} catch (e) {
console.error('Invalid JSON chunk:', buffer)
}
const end = performance.now()
console.log(`Objects streamed in: ${(end - start) / 1000} s`)
const startObjectCleanup = performance.now()
// Skips first element
for (let i = 1; i < objects.length; i++) {
const obj = objects[i]
if (obj.speckle_type) {
if (obj.speckle_type.includes('Objects.Data.DataObject')) {
delete obj.properties
}
}
delete obj.__closure
}
const endObjectCleanup = performance.now()
console.log(`Objects cleaned up in: ${(endObjectCleanup - startObjectCleanup) / 1000} s`)
try {
const sizeInBytes = new TextEncoder().encode(JSON.stringify(objects)).length
const sizeInMB = sizeInBytes / (1024 * 1024)
console.log(`Size of objects: ${sizeInMB} MB`)
console.log(`Downloading from Speckle API: ${objectId}`)
const loader = new SpeckleApiLoader(serverUrl, projectId, token)
const objects = await loader.downloadObjectsWithChildren(objectId)
modelObjects.push(objects)
console.log(`Downloaded ${objects.length} objects from Speckle`)
} catch (error) {
console.log("Can't calculate the size of the model")
console.log(error)
console.error(`Failed to download objects from Speckle:`, error)
throw error
}
return objects
} catch (error) {
console.log(error)
console.log("Objects couldn't retrieved from local server.")
} finally {
console.log('Streaming finished!')
}
}
async function* readStream(reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
yield value
}
return modelObjects
}
export async function processMatrixView(
@@ -327,7 +210,8 @@ export async function processMatrixView(
host: powerbi.extensibility.visual.IVisualHost,
hasColorFilter: boolean,
settings: SpeckleVisualSettingsModel,
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void,
internalizedData?: string
): Promise<SpeckleDataInput> {
const visualStore = useVisualStore()
const objectIds = [],
@@ -340,10 +224,86 @@ export async function processMatrixView(
const localMatrixView = matrixView.rows.root.children
let id = null
if (hasColorFilter) {
id = localMatrixView[0].children[0].values[0].value as unknown as string
} else {
id = localMatrixView[0].values[0].value as unknown as string
// Safety check for matrix data structure
if (!localMatrixView || localMatrixView.length === 0) {
throw new Error('Matrix view has no data rows')
}
try {
if (hasColorFilter) {
if (!localMatrixView[0].children || localMatrixView[0].children.length === 0 || !localMatrixView[0].children[0].values) {
throw new Error('Matrix view structure is incomplete for color filter mode')
}
id = localMatrixView[0].children[0].values[0].value as unknown as string
} else {
if (!localMatrixView[0].values || !localMatrixView[0].values[0]) {
throw new Error('Matrix view structure is incomplete for normal mode')
}
id = localMatrixView[0].values[0].value as unknown as string
}
} catch (error) {
console.error('Error accessing matrix data:', error)
throw new Error(`Failed to extract root object ID from matrix: ${error.message}`)
}
// Check for internalized data but ONLY if it matches current matrix data
let internalizedModelObjects: object[][] | undefined = undefined
if (settings.dataLoading.internalizeData.value && internalizedData) {
console.log('📁 Checking internalized data in processMatrixView')
try {
internalizedModelObjects = unzipModelObjects(internalizedData)
if (internalizedModelObjects && internalizedModelObjects.length > 0) {
// CRITICAL: Validate that internalized data matches current matrix data
const internalizedRootId = (internalizedModelObjects[0][0] as any).id
if (internalizedRootId !== id) {
console.log(`📁 Internalized data mismatch: stored=${internalizedRootId}, current=${id}. Using fresh data.`)
internalizedModelObjects = undefined // Clear internalized data - use fresh data instead
} else {
console.log(
'📁 Successfully validated internalized data matches current matrix:',
internalizedModelObjects.length,
'models'
)
}
}
if (internalizedModelObjects && internalizedModelObjects.length > 0) {
// Set dummy receiveInfo to prevent UI errors
if (!visualStore.receiveInfo) {
visualStore.setReceiveInfo({
userEmail: 'offline@speckle.systems',
serverUrl: 'offline',
sourceApplication: 'PowerBI Offline',
workspaceId: 'offline',
workspaceName: 'Offline Workspace',
workspaceLogo: '',
version: '1.0.0',
canHideBranding: false,
token: 'offline',
projectId: 'offline'
})
}
// Only reload if switching models or not already loaded
const needsReload = !visualStore.isViewerObjectsLoaded || visualStore.lastLoadedRootObjectId !== id
if (needsReload) {
console.log('🔄 Forcing viewer reload for internalized data (model switch or first load)')
visualStore.setViewerReloadNeeded()
visualStore.setViewerReadyToLoad(true)
visualStore.setLoadingProgress('📁 Loading from file', null)
} else {
console.log('📁 Internalized data already loaded, skipping reload')
}
visualStore.lastLoadedRootObjectId = id // Set to current ID to skip API calls
} else {
console.error('📁 Failed to unzip internalized data')
}
} catch (error) {
console.error('📁 Error processing internalized data:', error)
}
}
// const id = localMatrixView[0].values[0].value as unknown as string
@@ -352,48 +312,85 @@ export async function processMatrixView(
let modelObjects: object[][] = undefined
if (visualStore.isLoadingFromFile) {
console.log('The data is loading from file, skipping the streaming it.')
}
if (visualStore.lastLoadedRootObjectId !== id && !visualStore.isLoadingFromFile) {
if (
visualStore.lastLoadedRootObjectId !== id &&
!visualStore.isLoadingFromFile &&
!internalizedModelObjects
) {
const start = performance.now()
const getPreGetObjectsRes: PreGetObjects[] = await getPreGetObjects(id)
if (getPreGetObjectsRes.some((preGetObjects) => preGetObjects.modelExists === false)) {
visualStore.setCommonError(
'Version Object ID is not found in storage. Please make sure you placed correct field or consider refreshing your data via data connector.'
)
visualStore.setViewerReadyToLoad(false)
return
}
// Get receive info from desktop service to populate visual store
const receiveInfo = await getReceiveInfo(id)
if (receiveInfo) {
visualStore.setReceiveInfo({
userEmail: receiveInfo.email,
serverUrl: receiveInfo.server,
sourceApplication: getSlugFromHostAppNameAndVersion(receiveInfo.sourceApplication),
workspaceId: receiveInfo.workspaceId,
workspaceName: receiveInfo.workspaceName,
workspaceLogo: receiveInfo.workspaceLogo,
version: receiveInfo.version,
canHideBranding: receiveInfo.canHideBranding
userEmail: receiveInfo.email || receiveInfo.Email,
serverUrl: receiveInfo.server || receiveInfo.Server,
sourceApplication: getSlugFromHostAppNameAndVersion(
receiveInfo.sourceApplication || receiveInfo.SourceApplication
),
workspaceId: receiveInfo.workspaceId || receiveInfo.WorkspaceId,
workspaceName: receiveInfo.workspaceName || receiveInfo.WorkspaceName,
workspaceLogo: receiveInfo.workspaceLogo || receiveInfo.WorkspaceLogo,
version: receiveInfo.version || receiveInfo.Version,
canHideBranding: receiveInfo.canHideBranding ?? receiveInfo.CanHideBranding,
token: receiveInfo.weakToken || receiveInfo.WeakToken,
projectId: receiveInfo.projectId || receiveInfo.ProjectId
})
console.log(`Receive info retrieved from desktop service`, receiveInfo)
console.log(`Receive info retrieved from desktop service - credentials loaded`)
}
const totalObjectCount = getPreGetObjectsRes.reduce((sum, obj) => {
return sum + (obj.objectCount ?? 0)
}, 0)
// Now get the data from visual store for Speckle API download
const token = visualStore.receiveInfo?.token
const serverUrl = visualStore.receiveInfo?.serverUrl
const projectId = visualStore.receiveInfo?.projectId
if (!token || !serverUrl || !projectId) {
visualStore.setCommonError(
'Missing Speckle credentials. Please refresh the data from the data connector.'
)
visualStore.setViewerReadyToLoad(false)
return {
modelObjects: [],
objectIds: [],
selectedIds: [],
colorByIds: null,
objectTooltipData: new Map(),
isFromStore: false
}
}
visualStore.setViewerReadyToLoad(true)
// stream data
modelObjects = await fetchStreamedData(id, totalObjectCount)
console.log('Downloading objects directly from Speckle API...')
console.log(`Server: ${serverUrl}, Project: ${projectId}, Object: ${id}`)
try {
modelObjects = await fetchFromSpeckleApi(id, serverUrl, projectId, token)
console.log('Successfully downloaded from Speckle API')
// Debug: Check what we're passing to the viewer
if (modelObjects && modelObjects.length > 0 && modelObjects[0].length > 0) {
console.log('ModelObjects structure:', {
totalModels: modelObjects.length,
firstModelObjectCount: modelObjects[0].length,
firstObject: modelObjects[0][0]
})
}
} catch (error) {
console.error('Failed to download from Speckle API:', error)
visualStore.setCommonError(`Failed to download objects from Speckle: ${error.message}`)
visualStore.setViewerReadyToLoad(false)
return {
modelObjects: [],
objectIds: [],
selectedIds: [],
colorByIds: null,
objectTooltipData: new Map(),
isFromStore: false
}
}
visualStore.setViewerReloadNeeded() // they should be marked as deferred action bc of update function complexity.
visualStore.setLoadingProgress('Loading objects into viewer', null)
visualStore.setLoadingProgress('🌍 Loading objects into viewer', null)
console.log(`🚀 Upload is completed in ${(performance.now() - start) / 1000} s!`)
}
@@ -538,11 +535,11 @@ export async function processMatrixView(
previousPalette = host.colorPalette['colorPalette']
return {
modelObjects,
modelObjects: internalizedModelObjects || modelObjects, // Use internalized data if available
objectIds,
selectedIds,
colorByIds: colorByIds.length > 0 ? colorByIds : null,
objectTooltipData,
isFromStore: false
isFromStore: !!internalizedModelObjects // true if loaded from internalized data
}
}
+271 -25
View File
@@ -10,6 +10,7 @@ import { selectionHandlerKey, tooltipHandlerKey } from 'src/injectionKeys'
import { SpeckleDataInput } from './types'
import { processMatrixView, ReceiveInfo, validateMatrixView } from './utils/matrixViewUtils'
import { SpeckleVisualSettingsModel } from './settings/visualSettingsModel'
import { unzipModelObjects } from './utils/compression'
import TooltipHandler from './handlers/tooltipHandler'
import SelectionHandler from './handlers/selectionHandler'
@@ -21,7 +22,6 @@ import ITooltipService = powerbi.extensibility.ITooltipService
import { pinia } from './plugins/pinia'
import { useVisualStore } from './store/visualStore'
import { unzipModelObjects } from './utils/compression'
// noinspection JSUnusedGlobalSymbols
export class Visual implements IVisual {
@@ -88,6 +88,7 @@ export class Visual implements IVisual {
// @ts-ignore
console.log('⤴️ Update type 👉', powerbi.VisualUpdateType[options.type])
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(
SpeckleVisualSettingsModel,
options.dataViews[0]
@@ -95,6 +96,36 @@ export class Visual implements IVisual {
visualStore.setFormattingSettings(this.formattingSettings)
console.log('Selector colors', this.formattingSettings.colorSelector)
console.log(
'Data Loading - Internalize Data:',
this.formattingSettings.dataLoading.internalizeData.value
)
// Handle toggle state changes
const currentToggleState = this.formattingSettings.dataLoading.internalizeData.value
const previousToggleState = visualStore.previousToggleState
// Detect user toggle changes
if (previousToggleState !== undefined && currentToggleState !== previousToggleState) {
console.log('🔄 User changed toggle from', previousToggleState, 'to', currentToggleState)
if (currentToggleState) {
// Toggle switched ON - internalize via streaming
if (visualStore.isViewerObjectsLoaded && visualStore.lastLoadedRootObjectId) {
console.log('📁 Toggle ON - starting internalization')
await this.internalizeCurrentViewerData()
} else {
console.log('📁 Toggle ON - no active session to internalize')
}
} else {
// Toggle switched OFF - remove internalized data
console.log('🗑️ Toggle OFF - removing internalized data')
this.removeInternalizedData()
}
}
// CRITICAL: Always update the previous state for next comparison
visualStore.setPreviousToggleState(currentToggleState)
try {
const matrixView = options.dataViews[0].matrix
@@ -114,16 +145,10 @@ export class Visual implements IVisual {
return
case powerbi.VisualUpdateType.Data:
try {
// read saved data from file if any
if (
!visualStore.isViewerObjectsLoaded &&
this.isFirstViewerLoad &&
options.dataViews[0].metadata.objects
) {
const chunks = options.dataViews[0].metadata.objects.storedData
?.speckleObjects as string
const objectsFromFile = unzipModelObjects(chunks)
// read saved settings from file if any
console.log('🔍 Checking for other saved settings:')
if (!visualStore.isViewerObjectsLoaded && options.dataViews[0].metadata.objects) {
if (options.dataViews[0].metadata.objects.viewMode?.defaultViewMode as string) {
console.log(
`Default View Mode: ${
@@ -198,7 +223,9 @@ export class Visual implements IVisual {
if (camera && 'zoomOnFilter' in camera) {
console.log(
`Zoom on filter?: ${options.dataViews[0].metadata.objects.camera?.zoomOnFilter as boolean}`
`Zoom on filter?: ${
options.dataViews[0].metadata.objects.camera?.zoomOnFilter as boolean
}`
)
visualStore.setIsZoomOnFilterActive(
@@ -206,31 +233,59 @@ export class Visual implements IVisual {
)
}
// get receive info from file for mixpanel
// Log persisted data loading setting but don't force sync
if (
options.dataViews[0].metadata.objects.dataLoading?.internalizeData !== undefined
) {
console.log(
`Stored Data Loading - Internalize Data: ${
options.dataViews[0].metadata.objects.dataLoading?.internalizeData as boolean
}`
)
}
// get receive info from file for persistence
try {
const receiveInfoFromFile = JSON.parse(
options.dataViews[0].metadata.objects.storedData?.receiveInfo as string
) as ReceiveInfo
visualStore.setReceiveInfo(receiveInfoFromFile)
// Don't call setReceiveInfo here as it would trigger another save
visualStore.receiveInfo = receiveInfoFromFile
} catch (error) {
console.warn(error)
console.log('missing mixpanel info')
}
const savedVersionObjectId = objectsFromFile.map((o) => o[0].id).join(',')
if (visualStore.lastLoadedRootObjectId !== savedVersionObjectId) {
this.tryReadFromFile(objectsFromFile, visualStore)
console.log('missing stored receive info')
}
}
// Check for internalized data
const internalizedData = options.dataViews[0].metadata.objects?.storedData
?.speckleObjects as string
const input = await processMatrixView(
matrixView,
this.host,
validationResult.colorBy,
this.formattingSettings,
(obj, id) => this.selectionHandler.set(obj, id)
(obj, id) => this.selectionHandler.set(obj, id),
internalizedData
)
this.updateViewer(input)
// Auto-internalize new API data if toggle is ON and this is fresh data (not from store)
// Imagine that user has a visual and select internalizing data and changes the data source
// This will automatically internalize the new data
if (
this.formattingSettings.dataLoading.internalizeData.value &&
input.modelObjects &&
input.modelObjects.length > 0 &&
!input.isFromStore
) {
console.log('📦 Auto-internalizing new API data since toggle is ON')
// Trigger internalization after objects are loaded
setTimeout(() => {
this.internalizeCurrentViewerData()
}, 2000) // avoid a race condition (i know)
}
} catch (error) {
console.error('Data update error', error ?? 'Unknown')
}
@@ -258,9 +313,70 @@ export class Visual implements IVisual {
}
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)
console.log('🎨 getFormattingModel called')
// build the cards for the options
const model: powerbi.visuals.FormattingModel = {
cards: [
// Color card
{
displayName: 'Object Display',
name: 'color',
uid: 'color_card_uid',
groups: [
{
displayName: undefined,
uid: 'color_group_uid',
slices: [
{
displayName: 'Enabled',
uid: 'color_enabled_uid',
control: {
type: powerbi.visuals.FormattingComponent.ToggleSwitch,
properties: {
descriptor: {
objectName: 'color',
propertyName: 'enabled'
},
value: this.formattingSettings.color.enabled.value
}
}
}
]
}
]
},
// Data Management card
{
displayName: 'Data Management',
name: 'dataLoading',
uid: 'dataLoading_card_uid',
groups: [
{
displayName: undefined,
uid: 'dataLoading_group_uid',
slices: [
{
displayName: 'Internalize Data',
uid: 'dataLoading_internalizeData_uid',
control: {
type: powerbi.visuals.FormattingComponent.ToggleSwitch,
properties: {
descriptor: {
objectName: 'dataLoading',
propertyName: 'internalizeData'
},
value: this.formattingSettings.dataLoading.internalizeData.value
}
}
}
]
}
]
}
]
}
return model
}
@@ -276,14 +392,13 @@ export class Visual implements IVisual {
// we should give some time to Vue to render ViewerWrapper component to be able to have proper emitter setup. Happiness level 6/10
setTimeout(() => {
visualStore.setDataInput(input)
// visualStore.writeObjectsToFile(input.objects)
}, 250)
}
}
private tryReadFromFile(objectsFromFile: object[][], visualStore) {
visualStore.setViewerReadyToLoad(true)
visualStore.setIsLoadingFromFile(true) // to block unnecessary streaming data if bg service is running
visualStore.setIsLoadingFromFile(true)
setTimeout(() => {
visualStore.loadObjectsFromFile(objectsFromFile)
this.isFirstViewerLoad = false
@@ -291,6 +406,137 @@ export class Visual implements IVisual {
console.log(`${objectsFromFile.length} objects retrieved from persistent properties!`)
}
private async internalizeCurrentViewerData() {
const visualStore = useVisualStore()
// Get the current root object ID from the last loaded data
if (!visualStore.lastLoadedRootObjectId) {
console.log('📁 No root object ID to internalize')
return
}
try {
console.log('📁 Starting internalization via desktop service streaming...')
visualStore.setLoadingProgress('📦 Internalizing data...', null)
// Use desktop service for internalization
// TBD: getting objects from viewer caused two issue:
// - Data format -> we need to make an extra operation to match with the offline loader
// - Memory -> need to save data two times so sometimes causes memory issues
const rootObjectIds = visualStore.lastLoadedRootObjectId
const projectId = visualStore.receiveInfo?.projectId
// Handle federated models by processing each object ID separately
const objectIds = rootObjectIds.split(',')
let allStreamedObjects = []
for (const objectId of objectIds) {
console.log(`📁 Fetching objects for ID: ${objectId}`)
// For federated models, pass project ID explicitly to avoid "project id is not set" error
const url = projectId
? `http://localhost:29364/get-objects/${objectId}?projectId=${projectId}`
: `http://localhost:29364/get-objects/${objectId}`
const response = await fetch(url)
if (!response.body) {
console.error(`📁 No response body from desktop service for ${objectId}`)
continue
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let allObjectsData = ''
console.log(`📁 Streaming objects from desktop service for ${objectId}...`)
while (true) {
const { done, value } = await reader.read()
if (done) break
allObjectsData += decoder.decode(value, { stream: true })
}
// Parse NDJSON (newline-delimited JSON) format
const lines = allObjectsData.trim().split('\n')
const objectsForThisId = lines.map((line) => JSON.parse(line))
console.log(`📁 Streamed ${objectsForThisId.length} objects for ID ${objectId}`)
allStreamedObjects.push(...objectsForThisId)
}
const streamedObjects = allStreamedObjects
if (streamedObjects.length === 0) {
console.error('📁 No objects retrieved from desktop service')
visualStore.clearLoadingProgress()
return
}
console.log(`📁 Retrieved ${streamedObjects.length} total objects from desktop service`)
// Clean up objects to reduce file size (same as desktop service does)
const cleanedObjects = streamedObjects.map((obj: any, index: number) => {
// Skip first object (root), clean others
if (index === 0) return obj
const cleanedObj = { ...obj }
// Remove unnecessary properties
if (cleanedObj.speckle_type?.includes('Objects.Data.DataObject')) {
delete cleanedObj.properties
}
delete cleanedObj.__closure
return cleanedObj
})
console.log(`📁 Cleaned objects: ${cleanedObjects.length} total`)
// Wrap in array format expected by viewer (object[][])
const modelObjectsArray = [cleanedObjects]
// Use existing writeObjectsToFile method from visualStore
visualStore.writeObjectsToFile(modelObjectsArray)
// Clear loading message immediately when done
visualStore.clearLoadingProgress()
console.log('📁 Successfully internalized data via desktop service!')
} catch (error) {
console.error('📁 Failed to internalize via desktop service:', error)
// Clear loading message immediately on error
visualStore.clearLoadingProgress()
}
}
private removeInternalizedData() {
const visualStore = useVisualStore()
try {
// Clear stored data from PowerBI file
this.host.persistProperties({
merge: [
{
objectName: 'storedData',
properties: {
speckleObjects: null,
receiveInfo: null
},
selector: null
}
]
})
console.log('🗑️ Successfully removed internalized data from file!')
} catch (error) {
console.error('🗑️ Failed to remove internalized data:', error)
}
}
public async destroy() {
await this.clear()
}