Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 525857bd26 | |||
| 959bcaa671 | |||
| 04b3aef829 | |||
| 318dc6dbbe | |||
| 20577a1fdb | |||
| e74bad829e | |||
| dda04e49c2 | |||
| 97983fb8aa | |||
| 1cac02ae61 | |||
| 0a5001987e | |||
| 5ffb3ea1dd | |||
| 3461c48b11 | |||
| 220946a611 | |||
| 53e4cda456 | |||
| 4ca0ae0978 | |||
| 685a137531 | |||
| 78af91f38a | |||
| 108a406bd5 | |||
| d7ede2edcf | |||
| a25d635ca1 | |||
| 5a9add6d76 | |||
| 89c8005dee | |||
| a384370652 |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Generated
+3571
-2020
File diff suppressed because it is too large
Load Diff
@@ -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.2",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.23.23",
|
||||
"@speckle/viewer": "2.26.1",
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user