Compare commits

..

26 Commits

Author SHA1 Message Date
Jedd Morgan b5b40c8a5c Update deploy.yml 2025-10-20 16:35:22 +01:00
Oğuzhan Koral 098ef3d112 Bump viewer for proxy fix (#210)
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
2025-10-20 10:44:54 +03:00
Oğuzhan Koral 94fdc7a2c3 bump viewer (#209)
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
2025-10-16 17:33:36 +03:00
Dogukan Karatas 525857bd26 adds version id suffix (#207)
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
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-10-09 22:24:40 +03:00
Dogukan Karatas 959bcaa671 added a env check (#208) 2025-10-09 22:22:03 +03:00
Dogukan Karatas 04b3aef829 Merge pull request #206 from specklesystems/oguzhan/objectloader2
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): objectloader2 integration
2025-10-01 14:10:40 +02:00
Dogukan Karatas 318dc6dbbe cleanup added 2025-10-01 13:46:54 +02:00
Dogukan Karatas 20577a1fdb version bump 2025-10-01 12:14:11 +02:00
Dogukan Karatas e74bad829e downloads missing objects 2025-10-01 11:59:44 +02:00
oguzhankoral dda04e49c2 get root object first 2025-09-30 14:03:29 +03:00
Dogukan Karatas 97983fb8aa Revert "loader integration"
This reverts commit 53e4cda456.
2025-09-25 12:02:26 +02:00
Mucahit Bilal GOKER 1cac02ae61 Merge pull request #205 from specklesystems/bilal/cnx-2596-auto-expand-properties
feat: Add Property Expansion Option
2025-09-25 12:29:58 +03:00
bimgeek 0a5001987e remove true from description 2025-09-25 11:01:26 +03:00
bimgeek 5ffb3ea1dd set default value to false 2025-09-25 10:49:40 +03:00
bimgeek 3461c48b11 try check 2025-09-25 10:26:24 +03:00
bimgeek 220946a611 property expansion option 2025-09-25 10:19:35 +03:00
Dogukan Karatas 53e4cda456 loader integration 2025-09-23 15:00:50 +02:00
oguzhankoral 4ca0ae0978 replace objectloader1 with 2 2025-09-18 15:46:46 +03:00
Dogukan Karatas 685a137531 Merge pull request #204 from specklesystems/dogukan/cnx-2103-filtering-from-other-visuals-doesnt-work-when-conditional
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
fix (visual): remove forcing conditional formatted objects always be visible
2025-09-17 20:30:53 +03:00
Dogukan Karatas 78af91f38a Merge pull request #202 from specklesystems/dogukan/cnx-2515-fallback-to-json-for-scheduled-refresh
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
fallback to json download
2025-09-11 15:02:39 +02:00
Dogukan Karatas 108a406bd5 clearer error message on visual 2025-09-11 15:00:00 +02:00
Mucahit Bilal GOKER d7ede2edcf Merge branch 'main' into dogukan/cnx-2515-fallback-to-json-for-scheduled-refresh 2025-09-11 07:13:22 +03:00
Mucahit Bilal GOKER a25d635ca1 Merge pull request #201 from specklesystems/bilal/cnx-2510-remove-internal-border
Bilal/cnx 2510 remove internal border
2025-09-11 06:57:59 +03:00
Dogukan Karatas 5a9add6d76 fallback to json download 2025-09-10 13:22:41 +02:00
Mucahit Bilal GOKER 89c8005dee remove border from main container 2025-09-09 19:19:21 +03:00
Mucahit Bilal GOKER a384370652 Merge branch 'dev' into bilal/cnx-2510-remove-internal-border 2025-09-09 19:07:56 +03:00
13 changed files with 4042 additions and 2158 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: |
TAG=${{ github.ref_name }}
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
TAG="v3.0.99.${{ github.run_number }}"
TAG="v3.0.99"
fi
SEMVER="${TAG#v}"
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
+7
View File
@@ -199,6 +199,13 @@ shared Speckle.GetByUrl = Value.ReplaceType(
Documentation.FieldDescription = "The URL of a model in a Speckle server project. You can copy it directly from your browser.",
Documentation.SampleValues = {"https://app.speckle.systems/projects/7902de1f57/models/7f890a65df"}
]
),
optional ExpandProperties as (
type logical meta [
Documentation.FieldCaption = "Expand Properties (may slow query)",
Documentation.FieldDescription = "Expand the properties column into individual columns for easier analysis. When checked, each property from the 'properties' record column will have its own column. This can slow down the query if you have a lot of properties.",
Documentation.AllowedValues = {true, false}
]
)
) as table meta [
Documentation.Name = "Speckle - Get Data by URL",
+35 -11
View File
@@ -1,5 +1,8 @@
(url as text) as table =>
(url as text, optional ExpandProperties as logical) as table =>
let
// set default value for ExpandProperties
shouldExpandProperties = if ExpandProperties = null then false else ExpandProperties,
// import required functions
GetStructuredData = Extension.LoadFunction("GetStructuredData.pqm"),
SendToServer = Extension.LoadFunction("SendToServer.pqm"),
@@ -73,10 +76,19 @@
combinedData = Table.Combine(allTables),
// replace the "Version Object ID" column with the combined root IDs
finalData = Table.TransformColumns(
combinedData,
transformedData = Table.TransformColumns(
combinedData,
{"Version Object ID", each combinedRootIds}
)
),
// expand properties column if requested and if it exists
finalData = if shouldExpandProperties and Table.HasColumns(transformedData, {"properties"}) then
try
Speckle.Utils.ExpandRecord(transformedData, "properties")
otherwise
transformedData // fallback to original data if expansion fails
else
transformedData
in
finalData
else
@@ -85,13 +97,22 @@
// get model name
modelInfo = GetModel(url),
modelName = modelInfo[modelName],
// get structured data
structuredData = GetStructuredData(url),
// rename column based on send status
newColumnName = "Version Object ID",
result = Table.RenameColumns(structuredData, {{"Version Object ID", newColumnName}})
renamedData = Table.RenameColumns(structuredData, {{"Version Object ID", newColumnName}}),
// expand properties column if requested and if it exists
result = if shouldExpandProperties and Table.HasColumns(renamedData, {"properties"}) then
try
Speckle.Utils.ExpandRecord(renamedData, "properties")
otherwise
renamedData // fallback to original data if expansion fails
else
renamedData
in
result
else
@@ -121,11 +142,14 @@
// get structured data
structuredData = GetStructuredData(singleModelUrl),
// add the model name as context
// add the model name as context - with version id if exists
result = Table.AddColumn(
structuredData,
"Source Model",
each modelName,
structuredData,
"Source Model",
each if versionId <> null then
Text.Combine({modelName, "-", versionId})
else
modelName,
type text
)
in
@@ -1,5 +1,5 @@
(url as text) as list =>
try let
let
// Import required functions
GetModel = Extension.LoadFunction("GetModel.pqm"),
Parser = Extension.LoadFunction("Parser.pqm"),
@@ -24,80 +24,145 @@
Detail = [File = fileName, Error = e]
],
// Get required information
modelInfo = GetModel(url),
parsedUrl = Parser(url),
userInfo = GetUser(url),
apiKey = userInfo[Token],
userEmail = userInfo[UserEmail],
// get version from Speckle.pq - look GetVersion.pqm
connectorVersion = GetVersion(),
workspaceInfo = GetWorkspace(url),
// exchange powerful token for weak token via ds
tokenExchangeData = Json.FromValue([
PowerfulToken = apiKey,
Scopes = {"profile:read", "streams:read", "users:read"},
ProjectId = parsedUrl[projectId],
ServerUrl = parsedUrl[baseUrl]
]),
tokenExchangeResponse = Web.Contents(
"http://127.0.0.1:29364/auth/exchange-token",
[
Headers = [
#"Content-Type" = "application/json",
#"Method" = "POST"
],
Content = tokenExchangeData,
ManualStatusHandling = {400, 401, 403, 404, 500}
]
),
tokenExchangeJson = Json.Document(tokenExchangeResponse),
weakToken = tokenExchangeJson[token],
// Function to check if Desktop Service is available
IsDesktopServiceAvailable = () =>
try
let
PingResponse = Web.Contents(
"http://127.0.0.1:29364/ping",
[
Headers = [#"Method" = "GET"],
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
Timeout = #duration(0, 0, 0, 2) // 2 second timeout for ping
]
),
StatusCode = Value.Metadata(PingResponse)[Response.Status]
in
StatusCode = 200
otherwise
false,
// prepare request data with weak token
requestData = Json.FromValue([
Url = url,
Server = parsedUrl[baseUrl],
Email = userEmail,
ProjectId = parsedUrl[projectId],
RootObjectId = modelInfo[rootObjectId],
SourceApplication = modelInfo[sourceApplication],
Token = weakToken,
Version = connectorVersion,
VersionId = parsedUrl[versionId],
WorkspaceId = workspaceInfo[workspaceId],
WorkspaceName = workspaceInfo[workspaceName],
WorkspaceLogo = workspaceInfo[workspaceLogo],
CanHideBranding = workspaceInfo[canHideBranding]
]),
// Function to use Desktop Service approach (only called if available)
UseDesktopService = () =>
let
// exchange powerful token for weak token via ds
tokenExchangeData = Json.FromValue([
PowerfulToken = apiKey,
Scopes = {"profile:read", "streams:read", "users:read"},
ProjectId = parsedUrl[projectId],
ServerUrl = parsedUrl[baseUrl]
]),
tokenExchangeResponse = Web.Contents(
"http://127.0.0.1:29364/auth/exchange-token",
[
Headers = [
#"Content-Type" = "application/json",
#"Method" = "POST"
],
Content = tokenExchangeData,
ManualStatusHandling = {400, 401, 403, 404, 500}
]
),
tokenExchangeJson = Json.Document(tokenExchangeResponse),
weakToken = tokenExchangeJson[token],
// prepare request data with weak token
requestData = Json.FromValue([
Url = url,
Server = parsedUrl[baseUrl],
Email = userEmail,
ProjectId = parsedUrl[projectId],
RootObjectId = modelInfo[rootObjectId],
SourceApplication = modelInfo[sourceApplication],
Token = weakToken,
Version = connectorVersion,
VersionId = parsedUrl[versionId],
WorkspaceId = workspaceInfo[workspaceId],
WorkspaceName = workspaceInfo[workspaceName],
WorkspaceLogo = workspaceInfo[workspaceLogo],
CanHideBranding = workspaceInfo[canHideBranding]
]),
// Send request to local server
Response = Web.Contents(
"http://127.0.0.1:29364/download",
[
Headers = [
#"Content-Type" = "application/json",
#"Method" = "POST"
],
Content = requestData,
ManualStatusHandling = {400, 401, 403, 404, 500}
]
),
// Parse response
JsonResponse = Json.Document(Response)
in
JsonResponse,
// Function to fallback to direct JSON download from Speckle server
FallbackToDirectDownload = () =>
let
// Construct the direct object URL: {baseUrl}/objects/{projectId}/{rootObjectId}
objectUrl = Text.Combine({
parsedUrl[baseUrl],
"/objects/",
parsedUrl[projectId],
"/",
modelInfo[rootObjectId]
}),
// Download JSON directly from Speckle server
Response = Web.Contents(
objectUrl,
[
Headers = [
#"Authorization" = "Bearer " & apiKey,
#"Accept" = "application/json"
],
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504}
]
),
// Check response status
StatusCode = Value.Metadata(Response)[Response.Status],
// Parse JSON response if successful
JsonResponse = if StatusCode >= 200 and StatusCode < 300 then
Json.Document(Response)
else
error [
Reason = "DirectDownloadFailed",
Message = "Failed to download model data directly from Speckle server",
Detail = [
StatusCode = StatusCode,
ObjectUrl = objectUrl,
ProjectId = parsedUrl[projectId],
RootObjectId = modelInfo[rootObjectId]
]
]
in
JsonResponse,
// Check Desktop Service availability and choose approach
DesktopServiceAvailable = IsDesktopServiceAvailable(),
// Send request to local server
Response = Web.Contents(
"http://127.0.0.1:29364/download",
[
Headers = [
#"Content-Type" = "application/json",
#"Method" = "POST"
],
Content = requestData,
ManualStatusHandling = {400, 401, 403, 404, 500}
]
),
// Parse response
JsonResponse = Json.Document(Response)
FinalResult = if DesktopServiceAvailable then
UseDesktopService()
else
FallbackToDirectDownload()
in
JsonResponse
otherwise
error [
Reason = "Desktop Service Not Available",
Message = "Cannot connect to Speckle Desktop Service. Please ensure the Desktop Service is running and try again.",
Detail = "The Speckle Desktop Service must be running to load data from Speckle. Please start the Desktop Service application and refresh your data connection."
]
FinalResult
+3571 -2020
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -17,11 +17,10 @@
"@babel/runtime-corejs3": "^7.21.5",
"@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.0.12",
"@speckle/objectloader": "^2.25.9",
"@speckle/objectloader2": "^2.25.9",
"@speckle/objectloader2": "2.26.5",
"@speckle/tailwind-theme": "2.23.2",
"@speckle/ui-components": "2.23.2",
"@speckle/viewer": "2.23.23",
"@speckle/viewer": "2.26.5",
"color-interpolate": "^1.0.5",
"core-js": "^3.30.2",
"lodash": "^4.17.21",
@@ -1,5 +1,5 @@
<template>
<div class="border">
<div>
<transition name="slide-fade">
<nav
v-show="!visualStore.isNavbarHidden"
@@ -29,7 +29,7 @@
<div class="flex items-center space-x-2">
<FormButton
v-if="visualStore.latestAvailableVersion && !visualStore.isConnectorUpToDate"
v-if="visualStore.latestAvailableVersion && !visualStore.isConnectorUpToDate && visualStore.isRunningInDesktop"
v-tippy="{
content: 'New connector version is available.<br>Click to download.',
allowHTML: true
@@ -165,7 +165,7 @@ onMounted(async () => {
// Set up event listener for object clicks from the FilteredSelectionExtension
viewerHandler.emitter.on('objectClicked', handleObjectClicked)
visualStore.setViewerEmitter(viewerHandler.emit)
})
@@ -34,11 +34,9 @@ import ViewModes from '../../global/icon/ViewModes.vue'
const viewModes = {
[ViewMode.DEFAULT]: 'Default',
[ViewMode.DEFAULT_EDGES]: 'Edges',
[ViewMode.SHADED]: 'Shaded',
[ViewMode.PEN]: 'Pen',
[ViewMode.ARCTIC]: 'Arctic',
[ViewMode.COLORS]: 'Colors'
[ViewMode.ARCTIC]: 'Arctic'
}
const visualStore = useVisualStore()
@@ -1,31 +1,116 @@
import ObjectLoader from '@speckle/objectloader'
import { ObjectLoader2Factory } from '@speckle/objectloader2'
import { SpeckleLoader, WorldTree } from '@speckle/viewer'
// Base type from objectloader2 (has id, speckle_type properties)
interface Base {
id: string
speckle_type: string
[key: string]: any
}
export class SpeckleObjectsOfflineLoader extends SpeckleLoader {
constructor(targetTree: WorldTree, resourceData: string, resourceId?: string) {
super(targetTree, resourceId || '', undefined, undefined, resourceData)
constructor(targetTree: WorldTree, resourceData: unknown, resourceId?: string) {
// Resource ID is not used for offline loading since we have objects in memory
// Pass empty string to avoid URL parsing issues
super(targetTree, '', undefined, undefined, resourceData)
}
protected initObjectLoader(
_resource: string,
_authToken?: string,
_enableCaching?: boolean,
resourceData?: string | ArrayBuffer
): ObjectLoader {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return ObjectLoader.createFromObjects(resourceData as unknown as [])
resource: string,
authToken?: string,
enableCaching?: boolean,
resourceData?: unknown
): ReturnType<SpeckleLoader['initObjectLoader']> {
// Use ObjectLoader2Factory.createFromObjects for offline/memory-based loading
// The objects array must contain ALL objects (root + all children)
// The first object in the array must be the root object
const objects = (resourceData ?? this._resourceData) as Base[]
if (!objects || objects.length === 0) {
throw new Error('SpeckleObjectsOfflineLoader: No objects provided')
}
// Ensure all objects have an 'id' property
const missingIds = objects.filter((obj) => !obj.id)
if (missingIds.length > 0) {
console.error('Objects missing id property:', missingIds.slice(0, 5))
throw new Error(
`SpeckleObjectsOfflineLoader: ${missingIds.length} objects are missing 'id' property`
)
}
console.log(`Creating offline loader with ${objects.length} objects, root: ${objects[0].id}`)
// Create a Set of all object IDs for quick lookup
const objectIds = new Set(objects.map((obj) => obj.id))
// Check for references to objects that aren't in the array
const missingReferences = new Set<string>()
objects.forEach((obj) => {
// Check all properties for references (objects that look like { referencedId: "xxx" })
Object.values(obj).forEach((value) => {
if (value && typeof value === 'object') {
if ('referencedId' in value && typeof value.referencedId === 'string') {
if (!objectIds.has(value.referencedId)) {
missingReferences.add(value.referencedId)
}
}
}
// Check arrays for references
if (Array.isArray(value)) {
value.forEach((item) => {
if (item && typeof item === 'object' && 'referencedId' in item) {
if (!objectIds.has(item.referencedId)) {
missingReferences.add(item.referencedId)
}
}
})
}
})
})
if (missingReferences.size > 0) {
console.warn(
`⚠️ Found ${missingReferences.size} missing object references:`,
Array.from(missingReferences).slice(0, 10)
)
} else {
console.log('✅ All object references are present')
}
// @ts-ignore - Type compatibility issue between local objectloader2 and viewer's objectloader2
return ObjectLoader2Factory.createFromObjects(objects)
}
public async load(): Promise<boolean> {
const rootObject = await this.loader.getRootObject()
if (!rootObject && this._resource) {
console.error('No root id set!')
if (!rootObject) {
console.error('No root object found!')
return false
}
/** If not id is provided, we make one up based on the root object id */
this._resource = this._resource || `/json/${rootObject.id as string}`
/** Set resource to a fake URL for logging purposes only */
this._resource = this._resource || `/json/${rootObject.baseId as string}`
console.log('Loading objects from memory (offline mode)')
// Call parent load() which will use our ObjectLoader2 to iterate through objects
// Since we're using MemoryDownloader, it won't actually download anything
return super.load()
}
/**
* Clean up the ObjectLoader2 resources
*/
public async dispose(): Promise<void> {
try {
if (this.loader && 'disposeAsync' in this.loader) {
// @ts-ignore - disposeAsync exists on ObjectLoader2
await this.loader.disposeAsync()
console.log('SpeckleObjectsOfflineLoader: ObjectLoader2 disposed')
}
} catch (error) {
console.warn('Error disposing ObjectLoader2 in offline loader:', error)
}
}
}
+110 -21
View File
@@ -1,5 +1,5 @@
import { useVisualStore } from '@src/store/visualStore'
import ObjectLoader from '@speckle/objectloader' // Default import for v1
import { ObjectLoader2Factory } from '@speckle/objectloader2'
interface SpeckleObject {
id: string
@@ -18,39 +18,40 @@ export class SpeckleApiLoader {
this.projectId = projectId
this.token = token
this.headers = {
'Authorization': `Bearer ${token}`,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
async downloadObjectsWithChildren(objectId: string, onProgress?: (loaded: number, total: number) => void): Promise<SpeckleObject[]> {
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')
console.log('Creating ObjectLoader v2 for Power BI environment')
// Create ObjectLoader v1 instance - use 'token' not 'authToken'
const loader = new ObjectLoader({
const loader = ObjectLoader2Factory.createFromUrl({
serverUrl: this.serverUrl,
streamId: this.projectId,
objectId: objectId,
objectId,
token: this.token,
options: {
enableCaching: false, // Disable caching for Power BI environment
}
attributeMask: { exclude: ['properties', 'encodedValue'] },
options: { useCache: false }
})
try {
// Get total count for progress tracking
const totalCount = await loader.getTotalObjectCount()
console.log(`Loading ${totalCount} objects using ObjectLoader v1`)
console.log(`Loading ${totalCount} objects using ObjectLoader v2`)
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
objects.push(obj as SpeckleObject) // Type assertion for SpeckleObject interface
loadedCount++
// Update progress
@@ -67,18 +68,107 @@ export class SpeckleApiLoader {
}
}
console.log(`Downloaded ${objects.length} objects using ObjectLoader v1`)
console.log(`Downloaded ${objects.length} objects using ObjectLoader v2`)
visualStore.setLoadingProgress('🔄 Finalizing object download...', 0.9)
// Recursively fetch all missing references until none remain
let iterationCount = 0
let totalFetched = 0
while (iterationCount < 10) {
// Safety limit: loop exits early when missingIds.size === 0 (line 108)
// This limit only prevents infinite loops if something goes wrong
iterationCount++
const objectIds = new Set(objects.map((obj) => obj.id))
const missingIds = new Set<string>()
// Check all objects for missing references
objects.forEach((obj) => {
Object.values(obj).forEach((value) => {
if (value && typeof value === 'object') {
if ('referencedId' in value && typeof value.referencedId === 'string') {
if (!objectIds.has(value.referencedId)) {
missingIds.add(value.referencedId)
}
}
}
if (Array.isArray(value)) {
value.forEach((item) => {
if (item && typeof item === 'object' && 'referencedId' in item) {
if (!objectIds.has(item.referencedId)) {
missingIds.add(item.referencedId)
}
}
})
}
})
})
if (missingIds.size === 0) {
console.log(
`✅ No more missing references. Complete after ${iterationCount} iteration(s)`
)
break
}
console.log(
`Iteration ${iterationCount}: Fetching ${missingIds.size} missing referenced objects...`
)
visualStore.setLoadingProgress(`🔄 Loading additional objects)`, 0.9)
// Fetch missing objects with progress tracking
const missingIdsArray = Array.from(missingIds)
let fetchedInIteration = 0
for (const missingId of missingIdsArray) {
try {
const missingObj = await loader.getObject({ id: missingId })
objects.push(missingObj as SpeckleObject)
totalFetched++
fetchedInIteration++
// Update progress within this iteration
const iterationProgress = fetchedInIteration / missingIdsArray.length
visualStore.setLoadingProgress(
`🔄 Loading objects (${objects.length} loaded)`,
0.9 + iterationProgress * 0.05 // Progress from 0.9 to 0.95
)
} catch (err) {
console.warn(`⚠️ Could not fetch missing object ${missingId}:`, err)
}
}
console.log(
`✅ Iteration ${iterationCount} complete. Fetched ${missingIdsArray.length} objects. Total: ${objects.length}`
)
}
if (iterationCount >= 10) {
console.warn(
'⚠️ Reached maximum iterations for fetching references. Some objects may still be missing.'
)
}
console.log(
`✅ Downloaded total of ${objects.length} objects (${totalFetched} additional references fetched)`
)
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()
// Clean up the loader resources
try {
await loader.disposeAsync()
console.log('ObjectLoader2 disposed successfully')
} catch (disposeError) {
console.warn('Error disposing ObjectLoader2:', disposeError)
}
}
}
@@ -91,13 +181,12 @@ export class SpeckleApiLoader {
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
}
}
}
+5
View File
@@ -231,6 +231,11 @@ export class ViewerHandler {
// Since you are setting another camera position, maybe you want the second argument to false
await this.viewer.loadObject(loader, true)
this.viewer.getRenderer().shadowcatcher.shadowcatcherMesh.visible = false // works fine only right after loadObjects
// Clean up loader resources after loading is complete
if (loader.dispose) {
await loader.dispose()
}
}
store.setSpeckleViews(speckleViews)
+41 -4
View File
@@ -111,6 +111,15 @@ export const useVisualStore = defineStore('visualStore', () => {
return false
})
// detecting the env to control the visibility of update button
// might use for different reasons in the future
const isRunningInDesktop = computed(() => {
// power bi hostEnv enum values:
// web = 1, desktop = 4
const hostEnv = host.value?.['hostEnv'] as number
return hostEnv === 4
})
/**
* Ideally one time set when onMounted of `ViewerWrapper.vue` component
* @param emit picky emit function to trigger events under `IViewerEvents` interface
@@ -139,6 +148,23 @@ export const useVisualStore = defineStore('visualStore', () => {
}
}
const filterColorByIdsForSelection = (colorByIds: ColorBy[] | null | undefined, selectedIds: string[]): ColorBy[] => {
return colorByIds?.filter(colorGroup => {
const filteredObjectIds = colorGroup.objectIds.filter(objId =>
selectedIds.includes(objId)
)
if (filteredObjectIds.length > 0) {
return { ...colorGroup, objectIds: filteredObjectIds }
}
return false
}).map(colorGroup => ({
...colorGroup,
objectIds: colorGroup.objectIds.filter(objId =>
selectedIds.includes(objId)
)
})) || []
}
const clearLoadingProgress = () => {
loadingProgress.value = undefined
}
@@ -195,6 +221,10 @@ export const useVisualStore = defineStore('visualStore', () => {
if (dataInput.value.selectedIds.length > 0) {
isFilterActive.value = true
viewerEmit.value('filterSelection', dataInput.value.selectedIds, isGhostActive.value, isZoomOnFilterActive.value)
// When filtering, only apply colors to the selected/isolated objects
const filteredColorByIds = filterColorByIdsForSelection(dataInput.value.colorByIds, dataInput.value.selectedIds)
viewerEmit.value('colorObjectsByGroup', filteredColorByIds)
} else {
isFilterActive.value = false
latestColorBy.value = dataInput.value.colorByIds
@@ -205,8 +235,9 @@ export const useVisualStore = defineStore('visualStore', () => {
// No object IDs provided - show all objects without any filtering
viewerEmit.value('unIsolateObjects')
}
// When not filtering, apply all colors including conditional formatting
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
const writeObjectsToFile = (modelObjects: object[][]) => {
@@ -452,6 +483,7 @@ export const useVisualStore = defineStore('visualStore', () => {
// No object IDs provided - show all objects without any filtering
viewerEmit.value('unIsolateObjects')
}
// When resetting filters, apply all colors including conditional formatting
if (latestColorBy.value !== null) {
viewerEmit.value('colorObjectsByGroup', latestColorBy.value)
}
@@ -477,6 +509,10 @@ export const useVisualStore = defineStore('visualStore', () => {
if (dataInput.value.selectedIds.length > 0) {
isFilterActive.value = true
viewerEmit.value('filterSelection', dataInput.value.selectedIds, isGhostActive.value, isZoomOnFilterActive.value)
// When filtering, only apply colors to the selected/isolated objects
const filteredColorByIds = filterColorByIdsForSelection(dataInput.value.colorByIds, dataInput.value.selectedIds)
viewerEmit.value('colorObjectsByGroup', filteredColorByIds)
} else {
isFilterActive.value = false
latestColorBy.value = dataInput.value.colorByIds
@@ -487,10 +523,10 @@ export const useVisualStore = defineStore('visualStore', () => {
// No object IDs provided - show all objects without any filtering
viewerEmit.value('unIsolateObjects')
}
// Restore color grouping for all objects when not filtering
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
// Restore color grouping
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
// Trigger host data refresh to synchronize with Power BI
@@ -532,6 +568,7 @@ export const useVisualStore = defineStore('visualStore', () => {
isZoomOnFilterActive,
latestAvailableVersion,
isConnectorUpToDate,
isRunningInDesktop,
commonError,
previousToggleState,
setCommonError,
+34 -10
View File
@@ -131,7 +131,8 @@ function processObjectNode(
console.log('⚠️ HAS objects', color)
if (color) {
res.color = color
res.shouldColor = true
// Don't override shouldColor for conditional formatting - keep the selection state
// res.shouldColor = true // REMOVED: This was overriding cross-filter selection state
}
}
return res
@@ -170,13 +171,14 @@ async function getReceiveInfo(id) {
const response = await fetch(`http://localhost:29364/user-info/${ids[0]}`)
if (!response.body) {
console.error('No response body')
return
return { desktopServiceError: true }
}
return await response.json()
} catch (error) {
console.log(error)
console.log("User info couldn't retrieved from local server.")
return { desktopServiceError: true }
}
}
@@ -231,7 +233,11 @@ export async function processMatrixView(
try {
if (hasColorFilter) {
if (!localMatrixView[0].children || localMatrixView[0].children.length === 0 || !localMatrixView[0].children[0].values) {
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
@@ -258,7 +264,9 @@ export async function processMatrixView(
// 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.`)
console.log(
`📁 Internalized data mismatch: stored=${internalizedRootId}, current=${id}. Using fresh data.`
)
internalizedModelObjects = undefined // Clear internalized data - use fresh data instead
} else {
console.log(
@@ -270,7 +278,6 @@ export async function processMatrixView(
}
if (internalizedModelObjects && internalizedModelObjects.length > 0) {
// Set dummy receiveInfo to prevent UI errors
if (!visualStore.receiveInfo) {
visualStore.setReceiveInfo({
@@ -288,7 +295,8 @@ export async function processMatrixView(
}
// Only reload if switching models or not already loaded
const needsReload = !visualStore.isViewerObjectsLoaded || visualStore.lastLoadedRootObjectId !== id
const needsReload =
!visualStore.isViewerObjectsLoaded || visualStore.lastLoadedRootObjectId !== id
if (needsReload) {
console.log('🔄 Forcing viewer reload for internalized data (model switch or first load)')
visualStore.setViewerReloadNeeded()
@@ -321,7 +329,9 @@ export async function processMatrixView(
// Get receive info from desktop service to populate visual store
const receiveInfo = await getReceiveInfo(id)
if (receiveInfo) {
let desktopServiceUnavailable = false
if (receiveInfo && !receiveInfo.desktopServiceError) {
visualStore.setReceiveInfo({
userEmail: receiveInfo.email || receiveInfo.Email,
serverUrl: receiveInfo.server || receiveInfo.Server,
@@ -337,6 +347,9 @@ export async function processMatrixView(
projectId: receiveInfo.projectId || receiveInfo.ProjectId
})
console.log(`Receive info retrieved from desktop service - credentials loaded`)
} else {
desktopServiceUnavailable = true
console.log('Desktop service unavailable - cannot retrieve credentials')
}
// Now get the data from visual store for Speckle API download
@@ -345,9 +358,15 @@ export async function processMatrixView(
const projectId = visualStore.receiveInfo?.projectId
if (!token || !serverUrl || !projectId) {
visualStore.setCommonError(
'Missing Speckle credentials. Please refresh the data from the data connector.'
)
if (desktopServiceUnavailable) {
visualStore.setCommonError(
'Speckle Desktop Service is not running. Please start Speckle Desktop Services and refresh data.'
)
} else {
visualStore.setCommonError(
'Missing Speckle credentials. Please refresh the data from the data connector.'
)
}
visualStore.setViewerReadyToLoad(false)
return {
modelObjects: [],
@@ -456,6 +475,7 @@ export async function processMatrixView(
localMatrixView.forEach((obj) => {
const processedObjectIdLevels = processObjectIdLevel(obj, host, matrixView)
// Apply conditional formatting color if present, regardless of selection state
if (processedObjectIdLevels.color) {
let group = colorByIds.find((g) => g.color === processedObjectIdLevels.color)
if (!group) {
@@ -465,7 +485,11 @@ export async function processMatrixView(
}
colorByIds.push(group)
}
// Always add to color group if color is specified (conditional formatting)
group.objectIds.push(processedObjectIdLevels.id)
} else if (processedObjectIdLevels.shouldColor) {
// Only use shouldColor flag when there's no conditional formatting
// This preserves the original cross-filter coloring behavior
}
objectIds.push(processedObjectIdLevels.id)