Compare commits

...

6 Commits

Author SHA1 Message Date
Dogukan Karatas e73d392013 Merge pull request #178 from specklesystems/dogukan/cnx-1932-saving-visual-with-filtered-data
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): persistent visual filters
2025-07-04 12:51:52 +02:00
Dogukan Karatas dd7f3fe95d cover initial states 2025-07-03 15:54:09 +02:00
Dogukan Karatas fdcc1f2cef listen load event 2025-07-03 15:42:06 +02:00
Dogukan Karatas 5e2f108e49 Merge pull request #177 from specklesystems/dogukan/cnx-2032-persistent-hide-nav-bar-toggle
fix (visual): persistent navbar toogle
2025-07-01 15:01:48 +02:00
Dogukan Karatas df334e95a2 workspace to viewmode 2025-07-01 14:53:03 +02:00
Dogukan Karatas ce733d1ced adds persistent navbar 2025-07-01 11:15:59 +02:00
6 changed files with 118 additions and 17 deletions
+3
View File
@@ -89,6 +89,9 @@
"properties": {
"defaultViewMode": {
"type": { "text": true }
},
"navbarHidden": {
"type": { "bool": true }
}
}
},
+5 -4
View File
@@ -5367,9 +5367,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001689",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
"integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
"version": "1.0.30001726",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
"funding": [
{
"type": "opencollective",
@@ -5383,7 +5383,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.1.2",
@@ -2,7 +2,7 @@
<div class="border">
<transition name="slide-fade">
<nav
v-show="!isNavbarCollapsed"
v-show="!visualStore.isNavbarHidden"
class="fixed top-0 h-9 flex items-center bg-foundation border border-outline-2 w-full transition z-20 cursor-default"
>
<div class="flex items-center transition-all justify-between w-full">
@@ -46,7 +46,7 @@
<button
class="text-gray-400 hover:text-gray-700 transition"
title="Hide navbar"
@click="isNavbarCollapsed = true"
@click="visualStore.toggleNavbar()"
>
<ChevronUpIcon class="w-4 h-4" />
</button>
@@ -58,17 +58,17 @@
<div
v-if="!isInteractive"
class="absolute left-1/2 -translate-x-1/2 z-20 bg-white bg-opacity-70 text-black text-center text-xs px-4 py-1 rounded shadow font-medium cursor-default transition-all duration-300"
:class="isNavbarCollapsed ? 'top-1' : 'top-11'"
:class="visualStore.isNavbarHidden ? 'top-1' : 'top-11'"
>
<strong>Object IDs</strong>
field is needed for interactivity with other visuals.
</div>
<div v-if="isNavbarCollapsed" class="fixed top-0 right-0 z-20">
<div v-if="visualStore.isNavbarHidden" class="fixed top-0 right-0 z-20">
<button
class="transition opacity-50 hover:opacity-100"
title="Show navbar"
@click="isNavbarCollapsed = false"
@click="visualStore.toggleNavbar()"
>
<ChevronDownIcon class="w-4 h-4 text-gray-400" />
</button>
@@ -76,7 +76,7 @@
<transition name="slide-left">
<ViewerControls
v-show="!isNavbarCollapsed"
v-show="!visualStore.isNavbarHidden"
v-model:section-box="bboxActive"
:views="views"
class="fixed top-11 left-2 z-30"
@@ -152,8 +152,6 @@ const container = ref<HTMLElement>()
let bboxActive = ref(false)
let views: Ref<SpeckleView[]> = ref([])
const isNavbarCollapsed = ref(false)
const isInteractive = computed(
() => visualStore.fieldInputState.rootObjectId && visualStore.fieldInputState.objectIds
)
+15 -2
View File
@@ -52,6 +52,7 @@ export interface IViewerEvents {
toggleProjection: () => void
toggleGhostHidden: (ghost: boolean) => void
loadObjects: (objects: object[]) => void
objectsLoaded: () => void
}
export type ColorBy = {
@@ -81,6 +82,7 @@ export class ViewerHandler {
this.emitter.on('zoomExtends', this.zoomExtends)
this.emitter.on('zoomObjects', this.zoomObjects)
this.emitter.on('loadObjects', this.loadObjects)
this.emitter.on('objectsLoaded', this.handleObjectsLoaded)
this.emitter.on('toggleProjection', this.toggleProjection)
this.emitter.on('toggleGhostHidden', this.toggleGhostHidden)
}
@@ -219,7 +221,8 @@ export class ViewerHandler {
const store = useVisualStore()
const speckleViews = []
modelObjects.forEach(async (objects) => {
// Use for...of loop to properly handle async operations
for (const objects of modelObjects) {
//@ts-ignore
const loader = new SpeckleObjectsOfflineLoader(this.viewer.getWorldTree(), objects)
@@ -232,7 +235,7 @@ 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
})
}
store.setSpeckleViews(speckleViews)
if (store.defaultViewModeInFile) {
@@ -257,12 +260,22 @@ export class ViewerHandler {
)
this.cameraControls.setCameraView({ position, target }, true)
}
// Emit objects loaded event to trigger update
this.emit('objectsLoaded')
}
private handlePing = (message: string) => {
console.log(message)
}
private handleObjectsLoaded = () => {
console.log('🎯 Objects loaded - triggering update')
const store = useVisualStore()
// Handle state restoration after objects are loaded
store.handleObjectsLoadedComplete()
}
private pickViewableHit(hits: Hit[]): Hit | null {
// The current filtering state
const filteringState = this.filtering.filteringState
+77 -3
View File
@@ -35,6 +35,7 @@ export const useVisualStore = defineStore('visualStore', () => {
const isBrandingHidden = ref<boolean>(false)
const isOrthoProjection = ref<boolean>(false)
const isGhostActive = ref<boolean>(true)
const isNavbarHidden = ref<boolean>(false)
const commonError = ref<string>(undefined)
@@ -168,7 +169,13 @@ export const useVisualStore = defineStore('visualStore', () => {
} else {
isFilterActive.value = false
latestColorBy.value = dataInput.value.colorByIds
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
// Only apply filtering if object IDs are available, otherwise show all objects normally
if (fieldInputState.value.objectIds && dataInput.value.objectIds && dataInput.value.objectIds.length > 0) {
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
} else {
// No object IDs provided - show all objects without any filtering
viewerEmit.value('unIsolateObjects')
}
}
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
@@ -272,6 +279,22 @@ export const useVisualStore = defineStore('visualStore', () => {
})
}
const writeNavbarVisibilityToFile = (navbarHidden: boolean) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
host.value.persistProperties({
merge: [
{
objectName: 'viewMode',
properties: {
navbarHidden: navbarHidden
},
selector: null
}
]
})
}
const writeCameraPositionToFile = (position: Vector3, target: Vector3) => {
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
postFileSaveSkipNeeded.value = true
@@ -313,6 +336,15 @@ export const useVisualStore = defineStore('visualStore', () => {
isBrandingHidden.value = val
}
const setNavbarHidden = (val: boolean) => {
isNavbarHidden.value = val
}
const toggleNavbar = () => {
isNavbarHidden.value = !isNavbarHidden.value
writeNavbarVisibilityToFile(isNavbarHidden.value)
}
const setIsOrthoProjection = (val: boolean) => {
isOrthoProjection.value = val
}
@@ -332,7 +364,13 @@ export const useVisualStore = defineStore('visualStore', () => {
(formattingSettings.value = newFormattingSettings)
const resetFilters = () => {
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
// Only apply filtering if object IDs are available, otherwise show all objects normally
if (fieldInputState.value.objectIds && dataInput.value && dataInput.value.objectIds && dataInput.value.objectIds.length > 0) {
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
} else {
// No object IDs provided - show all objects without any filtering
viewerEmit.value('unIsolateObjects')
}
if (latestColorBy.value !== null) {
viewerEmit.value('colorObjectsByGroup', latestColorBy.value)
}
@@ -347,6 +385,37 @@ export const useVisualStore = defineStore('visualStore', () => {
commonError.value = error
}
const handleObjectsLoadedComplete = () => {
console.log('🔄 Objects loaded - handling state restoration')
// If we have current data input with selections, restore them
if (dataInput.value) {
console.log('🔄 Restoring selection state after object load')
// Restore selection filters if they exist
if (dataInput.value.selectedIds.length > 0) {
isFilterActive.value = true
viewerEmit.value('filterSelection', dataInput.value.selectedIds, isGhostActive.value)
} else {
isFilterActive.value = false
latestColorBy.value = dataInput.value.colorByIds
// Only apply filtering if object IDs are available, otherwise show all objects normally
if (fieldInputState.value.objectIds && dataInput.value.objectIds && dataInput.value.objectIds.length > 0) {
viewerEmit.value('resetFilter', dataInput.value.objectIds, isGhostActive.value)
} else {
// No object IDs provided - show all objects without any filtering
viewerEmit.value('unIsolateObjects')
}
}
// Restore color grouping
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
// Trigger host data refresh to synchronize with Power BI
host.value.refreshHostData()
}
return {
host,
receiveInfo,
@@ -373,6 +442,7 @@ export const useVisualStore = defineStore('visualStore', () => {
isBrandingHidden,
isOrthoProjection,
isGhostActive,
isNavbarHidden,
latestAvailableVersion,
isConnectorUpToDate,
commonError,
@@ -382,6 +452,7 @@ export const useVisualStore = defineStore('visualStore', () => {
setIsGhost,
setFormattingSettings,
setBrandingHidden,
setNavbarHidden,
setPostClickSkipNeeded,
setPostFileSaveSkipNeeded,
setCameraPositionInFile,
@@ -399,7 +470,9 @@ export const useVisualStore = defineStore('visualStore', () => {
writeViewModeToFile,
writeCameraPositionToFile,
writeHideBrandingToFile,
writeNavbarVisibilityToFile,
toggleBranding,
toggleNavbar,
setViewerEmitter,
setDataInput,
setFieldInputState,
@@ -409,6 +482,7 @@ export const useVisualStore = defineStore('visualStore', () => {
clearLoadingProgress,
setIsLoadingFromFile,
resetFilters,
downloadLatestVersion
downloadLatestVersion,
handleObjectsLoadedComplete
}
})
+12
View File
@@ -148,6 +148,18 @@ export class Visual implements IVisual {
)
}
if (options.dataViews[0].metadata.objects.viewMode?.navbarHidden as boolean) {
console.log(
`Navbar Hidden: ${
options.dataViews[0].metadata.objects.viewMode?.navbarHidden as boolean
}`
)
visualStore.setNavbarHidden(
options.dataViews[0].metadata.objects.viewMode?.navbarHidden as boolean
)
}
if (options.dataViews[0].metadata.objects.cameraPosition?.positionX as string) {
console.log(`Stored camera position is found`)
visualStore.setCameraPositionInFile([