Merge pull request #146 from specklesystems/dogukan/cnx-1502-merge-tables-of-federated-models
feat: federated models
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
GetStructuredData = Extension.LoadFunction("GetStructuredData.pqm"),
|
||||
SendToServer = Extension.LoadFunction("SendToServer.pqm"),
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
|
||||
// the logic for importing functions from other files
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
@@ -22,15 +23,90 @@
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
|
||||
// get model name
|
||||
modelInfo = GetModel(url),
|
||||
modelName = modelInfo[modelName],
|
||||
// parse the URL to determine if it's a federated model
|
||||
parsedUrl = Parser(url),
|
||||
|
||||
// get structured data
|
||||
StructuredData = GetStructuredData(url),
|
||||
// function to process a single model and get its data
|
||||
ProcessSingleModel = (baseUrl, projectId, modelId, versionId) =>
|
||||
let
|
||||
// construct a standard URL for the model
|
||||
singleModelUrl = Text.Combine({
|
||||
baseUrl,
|
||||
"/projects/",
|
||||
projectId,
|
||||
"/models/",
|
||||
modelId,
|
||||
if versionId <> null then Text.Combine({"@", versionId}) else ""
|
||||
}),
|
||||
|
||||
// get model info
|
||||
modelInfo = GetModel(singleModelUrl),
|
||||
rootObjectId = modelInfo[rootObjectId],
|
||||
modelName = modelInfo[modelName],
|
||||
|
||||
// get structured data
|
||||
structuredData = GetStructuredData(singleModelUrl),
|
||||
|
||||
// add the model name as context
|
||||
result = Table.AddColumn(
|
||||
structuredData,
|
||||
"Source Model",
|
||||
each modelName,
|
||||
type text
|
||||
)
|
||||
in
|
||||
[
|
||||
Data = result,
|
||||
RootObjectId = rootObjectId
|
||||
],
|
||||
|
||||
// check if this is a federated model
|
||||
results = if parsedUrl[isFederated] = true then
|
||||
// process each model in the federation
|
||||
let
|
||||
modelsData = List.Transform(
|
||||
parsedUrl[federatedModels],
|
||||
each ProcessSingleModel(
|
||||
parsedUrl[baseUrl],
|
||||
parsedUrl[projectId],
|
||||
[modelId],
|
||||
[versionId]
|
||||
)
|
||||
),
|
||||
|
||||
// extract all data tables
|
||||
allTables = List.Transform(modelsData, each [Data]),
|
||||
|
||||
// extract all root object IDs
|
||||
allRootIds = List.Transform(modelsData, each [RootObjectId]),
|
||||
|
||||
// combine all root object IDs into a comma-separated string
|
||||
combinedRootIds = Text.Combine(allRootIds, ","),
|
||||
|
||||
// combine all data tables
|
||||
combinedData = Table.Combine(allTables),
|
||||
|
||||
// replace the "Version Object ID" column with the combined root IDs
|
||||
finalData = Table.TransformColumns(
|
||||
combinedData,
|
||||
{"Version Object ID", each combinedRootIds}
|
||||
)
|
||||
in
|
||||
finalData
|
||||
else
|
||||
// use existing functionality for single models
|
||||
let
|
||||
// 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}})
|
||||
// rename column based on send status
|
||||
newColumnName = "Version Object ID",
|
||||
result = Table.RenameColumns(structuredData, {{"Version Object ID", newColumnName}})
|
||||
in
|
||||
result
|
||||
in
|
||||
Result
|
||||
results
|
||||
|
||||
@@ -11,29 +11,48 @@
|
||||
then pathSegments{1} else null,
|
||||
|
||||
// extract model ID and version ID if they exist
|
||||
modelAndVersion = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
|
||||
then Text.Split(pathSegments{3}, "@") else {},
|
||||
rawModelSegment = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
|
||||
then pathSegments{3} else "",
|
||||
|
||||
// separate model ID from version ID
|
||||
modelId = if List.Count(modelAndVersion) > 0
|
||||
then modelAndVersion{0} else null,
|
||||
// check if this is a federated model (contains commas)
|
||||
isFederated = Text.Contains(rawModelSegment, ","),
|
||||
|
||||
// if federated, split by comma to get multiple model IDs
|
||||
modelSegments = if isFederated
|
||||
then Text.Split(rawModelSegment, ",")
|
||||
else {rawModelSegment},
|
||||
|
||||
// get version ID if it exists
|
||||
versionId = if List.Count(modelAndVersion) > 1
|
||||
then modelAndVersion{1} else null,
|
||||
// process each model segment (could be modelID or modelID@versionID)
|
||||
processedModels = List.Transform(
|
||||
modelSegments,
|
||||
each [
|
||||
modelId = if Text.Contains(_, "@")
|
||||
then Text.Split(_, "@"){0}
|
||||
else _,
|
||||
versionId = if Text.Contains(_, "@")
|
||||
then Text.Split(_, "@"){1}
|
||||
else null
|
||||
]
|
||||
),
|
||||
|
||||
// extract model IDs and version IDs into separate lists
|
||||
modelIds = List.Transform(processedModels, each [modelId]),
|
||||
versionIds = List.Transform(processedModels, each [versionId]),
|
||||
|
||||
// validate URL structure
|
||||
isValid = projectId <> null and modelId <> null
|
||||
isValid = projectId <> null and List.Count(modelIds) > 0 and List.First(modelIds) <> ""
|
||||
in
|
||||
if not isValid then
|
||||
error [
|
||||
Reason = "Invalid URL",
|
||||
Message = "The URL must be in the format 'https://server/projects/PROJECT_ID/models/MODEL_ID' or 'https://server/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID'"
|
||||
Message = "The URL must be in the format 'https://server/projects/PROJECT_ID/models/MODEL_ID' or 'https://server/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID' or 'https://server/projects/PROJECT_ID/models/MODEL_ID1,MODEL_ID2'"
|
||||
]
|
||||
else
|
||||
[
|
||||
baseUrl = baseUrl,
|
||||
projectId = projectId,
|
||||
modelId = modelId,
|
||||
versionId = versionId
|
||||
]
|
||||
modelId = if isFederated then null else processedModels{0}[modelId],
|
||||
versionId = if isFederated then null else processedModels{0}[versionId],
|
||||
isFederated = isFederated,
|
||||
federatedModels = if isFederated then processedModels else null
|
||||
]
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
import { inject, onBeforeUnmount, onMounted, Ref, ref } from 'vue'
|
||||
import { currentOS, OS } from '../utils/detectOS'
|
||||
import ViewerControls from 'src/components/ViewerControls.vue'
|
||||
import ViewModeControls from 'src/components/ViewModeControls.vue'
|
||||
import { CanonicalView, SpeckleView } from '@speckle/viewer'
|
||||
import { SpeckleView } from '@speckle/viewer'
|
||||
import { useClickDragged } from 'src/composables/useClickDragged'
|
||||
import { ContextOption } from 'src/settings/colorSettings'
|
||||
import { useVisualStore } from '@src/store/visualStore'
|
||||
@@ -63,6 +62,9 @@ function isMultiSelect(e: MouseEvent) {
|
||||
async function onCanvasClick(ev: MouseEvent) {
|
||||
if (dragged.value) return
|
||||
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger
|
||||
|
||||
const intersectResult = await viewerHandler.intersect({ x: ev.clientX, y: ev.clientY })
|
||||
|
||||
const multi = isMultiSelect(ev)
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import {
|
||||
CanonicalView,
|
||||
FilteringState,
|
||||
LegacyViewer,
|
||||
IntersectionQuery,
|
||||
DefaultViewerParams,
|
||||
SpeckleView,
|
||||
CameraController,
|
||||
CameraEvent,
|
||||
SpeckleOfflineLoader
|
||||
} from '@speckle/viewer'
|
||||
import { pickViewableHit, projectToScreen } from '../utils/viewerUtils'
|
||||
import _ from 'lodash'
|
||||
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
|
||||
import { PerspectiveCamera, OrthographicCamera, Box3 } from 'three'
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export default class ViewerHandler {
|
||||
private viewer: LegacyViewer
|
||||
private readonly parent: HTMLElement
|
||||
private state: FilteringState
|
||||
private loadedObjectsCache: Set<string> = new Set<string>()
|
||||
private config = {
|
||||
authToken: null,
|
||||
batchSize: 25
|
||||
}
|
||||
private currentSectionBox: Box3 = null
|
||||
private currentSettings: SpeckleVisualSettingsModel
|
||||
|
||||
public getViews() {
|
||||
return this.viewer.getViews()
|
||||
}
|
||||
|
||||
public updateSettings(settings: SpeckleVisualSettingsModel) {
|
||||
// Camera settings
|
||||
switch (settings.camera.projection.value) {
|
||||
case 'perspective':
|
||||
this.viewer.setPerspectiveCameraOn()
|
||||
break
|
||||
case 'orthographic':
|
||||
this.viewer.setOrthoCameraOn()
|
||||
break
|
||||
}
|
||||
|
||||
var camController = this.viewer.getExtension(CameraController)
|
||||
var angle = settings.camera.allowCameraUnder.value ? Math.PI : Math.PI / 2
|
||||
camController.options = { maximumPolarAngle: angle }
|
||||
|
||||
// Lighting settings
|
||||
const newConfig = settings.lighting.getViewerConfiguration()
|
||||
this.viewer.setLightConfiguration(newConfig)
|
||||
|
||||
this.currentSettings = settings
|
||||
}
|
||||
|
||||
public setView(view: SpeckleView | CanonicalView) {
|
||||
this.viewer.setView(view)
|
||||
}
|
||||
|
||||
public setSectionBox(active: boolean, objectIds: string[]) {
|
||||
if (active) {
|
||||
if (this.currentSectionBox === null) {
|
||||
const bbox = this.viewer.getSectionBoxFromObjects(objectIds)
|
||||
this.viewer.setSectionBox(bbox)
|
||||
this.currentSectionBox = bbox as unknown as Box3
|
||||
} else {
|
||||
const bbox = this.viewer.getCurrentSectionBox()
|
||||
if (bbox) this.currentSectionBox = bbox as unknown as Box3
|
||||
}
|
||||
this.viewer.sectionBoxOn()
|
||||
} else {
|
||||
this.viewer.sectionBoxOff()
|
||||
}
|
||||
this.viewer.requestRender()
|
||||
}
|
||||
|
||||
public addCameraUpdateEventListener(listener: (ev) => void) {
|
||||
this.viewer.getExtension(CameraController).on(CameraEvent.LateFrameUpdate, listener)
|
||||
}
|
||||
|
||||
public constructor(parent: HTMLElement) {
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
public async init() {
|
||||
if (this.viewer) return
|
||||
const viewerSettings = DefaultViewerParams
|
||||
viewerSettings.showStats = false
|
||||
viewerSettings.verbose = false
|
||||
const viewer = new LegacyViewer(this.parent, viewerSettings)
|
||||
await viewer.init()
|
||||
console.log('Viewer initialized', viewer)
|
||||
this.viewer = viewer
|
||||
}
|
||||
|
||||
public async unloadObjects(
|
||||
objects: string[],
|
||||
signal?: AbortSignal,
|
||||
onObjectUnloaded?: (url: string) => void
|
||||
) {
|
||||
for (const url of objects) {
|
||||
if (signal?.aborted) return
|
||||
await this.viewer
|
||||
.cancelLoad(url, true)
|
||||
.catch((e) => console.warn('Viewer Unload error', url, e))
|
||||
.finally(() => {
|
||||
if (this.loadedObjectsCache.has(url)) this.loadedObjectsCache.delete(url)
|
||||
if (onObjectUnloaded) onObjectUnloaded(url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async loadObjectsWithAutoUnload(
|
||||
objects: object[],
|
||||
onLoad: (url: string, index: number) => void,
|
||||
onError: (url: string, error: Error) => void,
|
||||
signal: AbortSignal
|
||||
) {
|
||||
// var objectsToUnload = _.difference([...this.loadedObjectsCache], rootObject)
|
||||
// await this.unloadObjects(objectsToUnload, signal)
|
||||
// await this.loadObjects(obj, onLoad, onError) // TODO: pass root object
|
||||
|
||||
await this.loadObjects(objects, onLoad, onError)
|
||||
}
|
||||
|
||||
public async loadObjects(
|
||||
objects: object[],
|
||||
onLoad: (url: string, index: number) => void,
|
||||
onError: (url: string, error: Error) => void
|
||||
) {
|
||||
const stringifiedObject = JSON.stringify(objects)
|
||||
const loader = new SpeckleOfflineLoader(this.viewer.getWorldTree(), stringifiedObject)
|
||||
void this.viewer.unloadAll()
|
||||
void this.viewer.loadObject(loader, true)
|
||||
}
|
||||
|
||||
public async intersect(coords: { x: number; y: number }) {
|
||||
const point = this.viewer.Utils.screenToNDC(
|
||||
coords.x,
|
||||
coords.y,
|
||||
this.parent.clientWidth,
|
||||
this.parent.clientHeight
|
||||
)
|
||||
const intQuery: IntersectionQuery = {
|
||||
operation: 'Pick',
|
||||
point
|
||||
}
|
||||
|
||||
const res = this.viewer.query(intQuery)
|
||||
if (!res) return null
|
||||
return {
|
||||
hit: pickViewableHit(res.objects, this.state),
|
||||
objects: res.objects
|
||||
}
|
||||
}
|
||||
public zoom(objectIds?: string[]) {
|
||||
this.viewer.zoom(objectIds)
|
||||
}
|
||||
|
||||
public zoomExtents() {
|
||||
this.viewer.zoom()
|
||||
}
|
||||
public async unIsolateObjects() {
|
||||
if (this.state.isolatedObjects)
|
||||
this.state = await this.viewer.unIsolateObjects(this.state.isolatedObjects, 'powerbi', true)
|
||||
}
|
||||
|
||||
public async isolateObjects(objectIds, ghost = false) {
|
||||
this.state = await this.viewer.isolateObjects(objectIds, 'powerbi', true, ghost)
|
||||
}
|
||||
|
||||
public async colorObjectsByGroup(
|
||||
groups?: {
|
||||
objectIds: string[]
|
||||
color: string
|
||||
}[]
|
||||
) {
|
||||
this.state = await this.viewer.setUserObjectColors(groups ?? [])
|
||||
}
|
||||
|
||||
public async clear() {
|
||||
if (this.viewer) await this.viewer.unloadAll()
|
||||
this.loadedObjectsCache.clear()
|
||||
}
|
||||
|
||||
public async selectObjects(objectIds: string[] = null) {
|
||||
if (!this.viewer) return
|
||||
await this.viewer.resetHighlight()
|
||||
const objIds = objectIds ?? []
|
||||
this.state = await this.viewer.selectObjects(objIds)
|
||||
}
|
||||
|
||||
public getScreenPosition(worldPosition): { x: number; y: number } {
|
||||
return projectToScreen(
|
||||
this.viewer.getExtension(CameraController).renderingCamera as unknown as
|
||||
| PerspectiveCamera
|
||||
| OrthographicCamera,
|
||||
worldPosition
|
||||
)
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.viewer.getExtension(CameraController).dispose()
|
||||
this.viewer.dispose()
|
||||
this.viewer = null
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Viewer,
|
||||
HybridCameraController,
|
||||
SelectionExtension,
|
||||
FilteringExtension,
|
||||
FilteringExtension
|
||||
} from '@speckle/viewer'
|
||||
import { SpeckleObjectsOfflineLoader } from '@src/laoder/SpeckleObjectsOfflineLoader'
|
||||
import { useVisualStore } from '@src/store/visualStore'
|
||||
@@ -27,7 +27,6 @@ export interface IViewer {
|
||||
on: <E extends keyof IViewerEvents>(event: E, callback: IViewerEvents[E]) => void
|
||||
}
|
||||
|
||||
|
||||
export interface Hit {
|
||||
guid: string
|
||||
object?: Record<string, unknown>
|
||||
@@ -99,7 +98,7 @@ export class ViewerHandler {
|
||||
}
|
||||
|
||||
public zoomExtends = () => this.cameraControls.setCameraView(undefined, false)
|
||||
|
||||
|
||||
public setView = (view: CanonicalView) => this.cameraControls.setCameraView(view, false)
|
||||
|
||||
public setSectionBox = (bboxActive: boolean, objectIds: string[]) => {
|
||||
@@ -145,7 +144,7 @@ export class ViewerHandler {
|
||||
|
||||
public intersect = (coords: { x: number; y: number }) => {
|
||||
const point = this.viewer.Utils.screenToNDC(coords.x, coords.y)
|
||||
|
||||
|
||||
const intQuery: IntersectionQuery = {
|
||||
operation: 'Pick',
|
||||
point
|
||||
@@ -163,24 +162,32 @@ export class ViewerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public loadObjects = async (objects: object[]) => {
|
||||
public loadObjects = async (modelObjects: object[][]) => {
|
||||
await this.viewer.unloadAll()
|
||||
// const stringifiedObject = JSON.stringify(objects)
|
||||
//@ts-ignore
|
||||
const loader = new SpeckleObjectsOfflineLoader(this.viewer.getWorldTree(), objects)
|
||||
const store = useVisualStore()
|
||||
|
||||
const speckleViews = objects.filter(
|
||||
const store = useVisualStore()
|
||||
const speckleViews = []
|
||||
|
||||
modelObjects.forEach(async (objects) => {
|
||||
//@ts-ignore
|
||||
(o) => o.speckle_type === 'Objects.BuiltElements.View:Objects.BuiltElements.View3D'
|
||||
) as SpeckleView[]
|
||||
const loader = new SpeckleObjectsOfflineLoader(this.viewer.getWorldTree(), objects)
|
||||
|
||||
const speckleViewsInModel = objects.filter(
|
||||
//@ts-ignore
|
||||
(o) => o.speckle_type === 'Objects.BuiltElements.View:Objects.BuiltElements.View3D'
|
||||
) as SpeckleView[]
|
||||
speckleViews.concat(speckleViewsInModel)
|
||||
|
||||
// Since you are setting another camera position, maybe you want the second argument to false
|
||||
await this.viewer.loadObject(loader, true)
|
||||
})
|
||||
|
||||
store.setSpeckleViews(speckleViews)
|
||||
if (store.defaultViewModeInFile) {
|
||||
this.setViewMode(Number(store.defaultViewModeInFile))
|
||||
}
|
||||
// Since you are setting another camera position, maybe you want the second argument to false
|
||||
await this.viewer.loadObject(loader, true)
|
||||
|
||||
Tracker.dataLoaded({ sourceHostApp: store.receiveInfo.sourceApplication })
|
||||
// camera need to be set after objects loaded
|
||||
if (store.cameraPosition) {
|
||||
@@ -204,26 +211,28 @@ export class ViewerHandler {
|
||||
|
||||
private pickViewableHit(hits: Hit[]): Hit | null {
|
||||
// The current filtering state
|
||||
const filteringState = this.filtering.filteringState;
|
||||
const filteringState = this.filtering.filteringState
|
||||
// Are there any objects isolated?
|
||||
const hasIsolatedObjects =
|
||||
!!filteringState.isolatedObjects &&
|
||||
filteringState.isolatedObjects.length !== 0;
|
||||
!!filteringState.isolatedObjects && filteringState.isolatedObjects.length !== 0
|
||||
// Are there any objects hidden?
|
||||
const hasHiddenObjects =
|
||||
!!filteringState.hiddenObjects &&
|
||||
filteringState.hiddenObjects.length !== 0;
|
||||
const hasHiddenObjects =
|
||||
!!filteringState.hiddenObjects && filteringState.hiddenObjects.length !== 0
|
||||
// No isolated or hidden objects? Return the first hit
|
||||
if(!hasIsolatedObjects && !hasHiddenObjects)
|
||||
return hits[0]
|
||||
if (hasIsolatedObjects && !hasHiddenObjects) {
|
||||
return hits.find((h) => filteringState.isolatedObjects.includes(h.guid))
|
||||
}
|
||||
|
||||
for(let k = 0 ; k < hits.length ; k++){
|
||||
for (let k = 0; k < hits.length; k++) {
|
||||
/** Return the first one that's not hidden or isolated. */
|
||||
if((hasIsolatedObjects && filteringState.isolatedObjects?.includes(hits[k].guid)) &&
|
||||
(hasHiddenObjects && filteringState.hiddenObjects?.includes(hits[k].guid)))
|
||||
if (
|
||||
hasIsolatedObjects &&
|
||||
filteringState.isolatedObjects?.includes(hits[k].guid) &&
|
||||
hasHiddenObjects &&
|
||||
filteringState.hiddenObjects?.includes(hits[k].guid)
|
||||
)
|
||||
return hits[k]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
@@ -239,7 +248,7 @@ const createViewer = async (parent: HTMLElement): Promise<Viewer> => {
|
||||
viewerSettings.verbose = true // Turning this on so we can see logs for now
|
||||
const viewer = new Viewer(parent, viewerSettings)
|
||||
await viewer.init()
|
||||
|
||||
|
||||
viewer.createExtension(HybridCameraController) // camera controller
|
||||
viewer.createExtension(SelectionExtension) // selection helper
|
||||
// viewer.createExtension(SectionTool) // section tool, possibly not needed for now?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CanonicalView, SpeckleView, ViewMode } from '@speckle/viewer'
|
||||
import { IViewerEvents } from '@src/plugins/viewer'
|
||||
import { SpeckleDataInput } from '@src/types'
|
||||
import { zipJSONChunks } from '@src/utils/compression'
|
||||
import { zipJSONChunks, zipModelObjects } from '@src/utils/compression'
|
||||
import { ReceiveInfo } from '@src/utils/matrixViewUtils'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Vector3 } from 'three'
|
||||
@@ -101,7 +101,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
}
|
||||
|
||||
const loadObjectsFromFile = async (objects: object[]) => {
|
||||
lastLoadedRootObjectId.value = (objects[0] as SpeckleObject).id
|
||||
lastLoadedRootObjectId.value = (objects[0] as SpeckleObject).id // TODO fix
|
||||
viewerReloadNeeded.value = false
|
||||
console.log(`📦 Loading viewer from cached data with ${lastLoadedRootObjectId.value} id.`)
|
||||
await viewerEmit.value('loadObjects', objects)
|
||||
@@ -120,13 +120,14 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
dataInput.value = newValue
|
||||
|
||||
if (viewerReloadNeeded.value) {
|
||||
lastLoadedRootObjectId.value = (dataInput.value.objects[0] as SpeckleObject).id
|
||||
const modelIds = dataInput.value.modelObjects.map((o) => (o[0] as SpeckleObject).id).join(',')
|
||||
lastLoadedRootObjectId.value = modelIds
|
||||
console.log(`🔄 Forcing viewer re-render for new root object id.`)
|
||||
await viewerEmit.value('loadObjects', dataInput.value.objects)
|
||||
await viewerEmit.value('loadObjects', dataInput.value.modelObjects)
|
||||
clearLoadingProgress()
|
||||
viewerReloadNeeded.value = false
|
||||
isViewerObjectsLoaded.value = true
|
||||
writeObjectsToFile(dataInput.value.objects)
|
||||
writeObjectsToFile(dataInput.value.modelObjects)
|
||||
}
|
||||
|
||||
if (dataInput.value.selectedIds.length > 0) {
|
||||
@@ -137,8 +138,8 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
|
||||
}
|
||||
|
||||
const writeObjectsToFile = (objects: object[]) => {
|
||||
const compressedChunks = zipJSONChunks(objects, 10000) // Compress in chunks
|
||||
const writeObjectsToFile = (modelObjects: object[][]) => {
|
||||
const compressedChunks = zipModelObjects(modelObjects, 10000) // Compress in chunks
|
||||
|
||||
host.value.persistProperties({
|
||||
merge: [
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IViewerTooltip {
|
||||
}
|
||||
|
||||
export interface SpeckleDataInput {
|
||||
objects: object[]
|
||||
modelObjects: object[][]
|
||||
objectIds: string[]
|
||||
selectedIds: string[]
|
||||
colorByIds: { objectIds: string[]; slice: fs.ColorPicker; color: string }[]
|
||||
|
||||
@@ -29,11 +29,15 @@ function chunkArray(array, chunkSize) {
|
||||
return chunks
|
||||
}
|
||||
|
||||
export function zipModelObjects(modelObjects: object[][], chunkSize = 1000) {
|
||||
return modelObjects.map((objects) => zipJSONChunks(objects, chunkSize)).join('>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses JSON objects in chunks properly.
|
||||
*/
|
||||
export function zipJSONChunks(objects, chunkSize = 1000) {
|
||||
const chunks = chunkArray(objects, chunkSize)
|
||||
export function zipJSONChunks(objectsInModel: object[], chunkSize = 1000) {
|
||||
const chunks = chunkArray(objectsInModel, chunkSize)
|
||||
return chunks.map((chunk, index) => {
|
||||
const jsonString = JSON.stringify(chunk)
|
||||
const originalSize = new TextEncoder().encode(jsonString).length / (1024 * 1024) // Original size in bytes
|
||||
@@ -52,6 +56,14 @@ export function zipJSONChunks(objects, chunkSize = 1000) {
|
||||
})
|
||||
}
|
||||
|
||||
export function unzipModelObjects(compressedChunk: string) {
|
||||
const compressedModelObjects = compressedChunk.split('>')
|
||||
console.log(compressedModelObjects)
|
||||
return compressedModelObjects.map((compressedModelObjs) =>
|
||||
unzipJSONChunk(compressedModelObjs.split(','))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompresses JSON chunks properly.
|
||||
*/
|
||||
|
||||
@@ -149,7 +149,8 @@ export type ReceiveInfo = {
|
||||
|
||||
async function getReceiveInfo(id) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:29364/user-info/${id}`)
|
||||
const ids = (id as string).split(',')
|
||||
const response = await fetch(`http://localhost:29364/user-info/${ids[0]}`)
|
||||
if (!response.body) {
|
||||
console.error('No response body')
|
||||
return
|
||||
@@ -162,7 +163,18 @@ async function getReceiveInfo(id) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStreamedData(id) {
|
||||
async function fetchStreamedData(commaSeparatedModelIds: string) {
|
||||
const modelIds = (commaSeparatedModelIds as string).split(',')
|
||||
const modelObjects = []
|
||||
|
||||
for await (const id of modelIds) {
|
||||
const objects = await fetchStreamedDataForModel(id)
|
||||
modelObjects.push(objects)
|
||||
}
|
||||
return modelObjects
|
||||
}
|
||||
|
||||
async function fetchStreamedDataForModel(id) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:29364/get-objects/${id}`)
|
||||
|
||||
@@ -263,7 +275,7 @@ export async function processMatrixView(
|
||||
console.log('🗝️ Root Object Id: ', id)
|
||||
console.log('Last laoded root object id', visualStore.lastLoadedRootObjectId)
|
||||
|
||||
let objects: object[] = undefined
|
||||
let modelObjects: object[][] = undefined
|
||||
|
||||
if (visualStore.isLoadingFromFile) {
|
||||
console.log('The data is loading from file, skipping the streaming it.')
|
||||
@@ -275,7 +287,7 @@ export async function processMatrixView(
|
||||
visualStore.setLoadingProgress('Loading', null)
|
||||
|
||||
// stream data
|
||||
objects = await fetchStreamedData(id)
|
||||
modelObjects = await fetchStreamedData(id)
|
||||
|
||||
const receiveInfo = await getReceiveInfo(id)
|
||||
if (receiveInfo) {
|
||||
@@ -352,7 +364,7 @@ export async function processMatrixView(
|
||||
previousPalette = host.colorPalette['colorPalette']
|
||||
|
||||
return {
|
||||
objects,
|
||||
modelObjects,
|
||||
objectIds,
|
||||
selectedIds,
|
||||
colorByIds: colorByIds.length > 0 ? colorByIds : null,
|
||||
|
||||
@@ -4,10 +4,8 @@ import '../style/visual.css'
|
||||
import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
// import { store } from 'src/store'
|
||||
import { selectionHandlerKey, tooltipHandlerKey } from 'src/injectionKeys'
|
||||
|
||||
import { Tracker } from './utils/mixpanel'
|
||||
import { SpeckleDataInput } from './types'
|
||||
import { processMatrixView, ReceiveInfo, validateMatrixView } from './utils/matrixViewUtils'
|
||||
import { SpeckleVisualSettingsModel } from './settings/visualSettingsModel'
|
||||
@@ -19,15 +17,10 @@ import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructor
|
||||
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions
|
||||
import IVisual = powerbi.extensibility.visual.IVisual
|
||||
import ITooltipService = powerbi.extensibility.ITooltipService
|
||||
import {
|
||||
createDataViewWildcardSelector,
|
||||
DataViewWildcardMatchingOption
|
||||
} from 'powerbi-visuals-utils-dataviewutils/lib/dataViewWildcard'
|
||||
import { ColorSelectorSettings } from 'src/settings/colorSettings'
|
||||
|
||||
import { pinia } from './plugins/pinia'
|
||||
import { FieldInputState, useVisualStore } from './store/visualStore'
|
||||
import { unzipJSONChunk, unzipJSONChunks, zipJSONChunks } from './utils/compression'
|
||||
import { useVisualStore } from './store/visualStore'
|
||||
import { unzipModelObjects } from './utils/compression'
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export class Visual implements IVisual {
|
||||
@@ -103,10 +96,9 @@ export class Visual implements IVisual {
|
||||
this.isFirstViewerLoad &&
|
||||
options.dataViews[0].metadata.objects
|
||||
) {
|
||||
const chunks = (
|
||||
options.dataViews[0].metadata.objects.storedData?.speckleObjects as string
|
||||
).split(',')
|
||||
const objectsFromFile = unzipJSONChunks(chunks)
|
||||
const chunks = options.dataViews[0].metadata.objects.storedData
|
||||
?.speckleObjects as string
|
||||
const objectsFromFile = unzipModelObjects(chunks)
|
||||
|
||||
if (options.dataViews[0].metadata.objects.viewMode?.defaultViewMode as string) {
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user