Compare commits

...

18 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
11 changed files with 3845 additions and 2077 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
+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",
@@ -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)
@@ -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
@@ -559,6 +568,7 @@ export const useVisualStore = defineStore('visualStore', () => {
isZoomOnFilterActive,
latestAvailableVersion,
isConnectorUpToDate,
isRunningInDesktop,
commonError,
previousToggleState,
setCommonError,