Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68d6bf3d55 | |||
| 2a8925c8ef | |||
| f9b3d3db52 | |||
| bfd0c33373 | |||
| d155a4b165 | |||
| 2e9ece856f | |||
| 808e288848 | |||
| 701116c66c | |||
| e73d392013 | |||
| dd7f3fe95d | |||
| fdcc1f2cef | |||
| 5e2f108e49 | |||
| df334e95a2 | |||
| ce733d1ced | |||
| 8345258990 | |||
| dbd0f2f9ce | |||
| c9b4155660 | |||
| c4d094d722 | |||
| 5b003b182b | |||
| 7eabd47f6d | |||
| 822b999be9 | |||
| 2407b8758a | |||
| d6f5e65bd7 | |||
| 183cc36654 | |||
| a740272585 | |||
| 72128a9f4e | |||
| 677c663ef3 | |||
| a077857c66 | |||
| 5897a286bc | |||
| 5b49fb2a9a | |||
| 424404dd11 | |||
| 31312522a7 | |||
| 932198dccf | |||
| 3770502ca4 | |||
| 93e8fcdd9d | |||
| 370052b2be | |||
| aa4a137a0d | |||
| 4acdf30734 | |||
| b531446acd | |||
| de1b2ca39c |
@@ -1,4 +1,4 @@
|
||||
name: build_powerbi
|
||||
name: Build and deploy Connector and Visual
|
||||
on:
|
||||
push:
|
||||
branches: ["installer-test/**"]
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Test Build Connector and Visual
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
build-connector:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Build Data Connector
|
||||
working-directory: src/powerbi-data-connector
|
||||
run: |
|
||||
msbuild Speckle.proj /restore /consoleloggerparameters:NoSummary /property:GenerateFullPaths=true
|
||||
|
||||
build-visual:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- run: npm ci
|
||||
working-directory: src/powerbi-visual
|
||||
- run: npm run build
|
||||
working-directory: src/powerbi-visual
|
||||
@@ -75,6 +75,42 @@ shared Speckle.GetWorkspace = Value.ReplaceType(
|
||||
type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
shared Speckle.Objects.Properties = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Objects.Properties.pqm"),
|
||||
type function (inputRecord as record, optional filterKeys as list) as record
|
||||
);
|
||||
|
||||
|
||||
shared Speckle.Utils.ExpandRecord = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Utils.ExpandRecord.pqm"),
|
||||
type function (
|
||||
table as table,
|
||||
columnName as text,
|
||||
optional FieldNames as list,
|
||||
optional UseCombinedNames as logical
|
||||
) as table
|
||||
);
|
||||
|
||||
shared Speckle.Objects.Collections = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Objects.Collections.pqm"),
|
||||
type function (inputData as table) as table
|
||||
);
|
||||
|
||||
shared Speckle.Objects.CompositeStructure = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Objects.CompositeStructure.pqm"),
|
||||
type function (objectRecord as record, optional outputAsList as nullable logical) as any
|
||||
);
|
||||
|
||||
shared Speckle.Objects.MaterialQuantities = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Objects.MaterialQuantities.pqm"),
|
||||
type function (objectRecord as record, optional outputAsList as logical) as any
|
||||
);
|
||||
|
||||
shared Speckle.Models.Federate = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Models.Federate.pqm"),
|
||||
type function (tables as list, optional excludeData as logical) as table
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle", Publish="GetByUrl.Publish"]
|
||||
shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetByUrl.pqm"),
|
||||
|
||||
@@ -39,8 +39,9 @@
|
||||
),
|
||||
// fields to remove from data record
|
||||
FieldsToRemove = {"__closure", "totalChildrenCount", "renderMaterialProxies"},
|
||||
// create the final table with cleaned data records
|
||||
FinalTable = Table.FromRecords(
|
||||
|
||||
// create basic table with cleaned data records (no properties column yet)
|
||||
BasicTable = Table.FromRecords(
|
||||
List.Transform(
|
||||
TableFromList[Column1],
|
||||
each let
|
||||
@@ -48,18 +49,19 @@
|
||||
fieldsToRemoveForThisRecord = List.Select(
|
||||
FieldsToRemove,
|
||||
each Record.HasFields(record, {_})
|
||||
)
|
||||
),
|
||||
cleanedRecord = Record.RemoveFields(record, fieldsToRemoveForThisRecord)
|
||||
in
|
||||
[
|
||||
#"Object IDs" = record[id], // Object IDs
|
||||
#"Speckle Type" = record[speckle_type], // Speckle Type
|
||||
#"Version Object ID" = rootId,
|
||||
data = Record.RemoveFields(record, fieldsToRemoveForThisRecord) // Data
|
||||
data = cleanedRecord // Data
|
||||
]
|
||||
)
|
||||
),
|
||||
|
||||
// Function to check if a row should be excluded based on speckle type
|
||||
// function to check if a row should be excluded based on speckle type
|
||||
ShouldExcludeRow = (row as record) as logical =>
|
||||
let
|
||||
speckleType = Record.FieldOrDefault(row[data], "speckle_type", "")
|
||||
@@ -70,14 +72,38 @@
|
||||
// Filtering logic here
|
||||
// If model data contains any DataObject -> fetch only data objects (excluding unwanted types)
|
||||
// If there are no data objects in the data -> fetch everything but exclude DataChunks and RawEncoding
|
||||
// Check if model contains any DataObject
|
||||
HasDataObjects = Table.RowCount(
|
||||
Table.SelectRows(
|
||||
FinalTable,
|
||||
BasicTable,
|
||||
each Text.Contains(Record.FieldOrDefault([data], "speckle_type", ""), "DataObject")
|
||||
and not ShouldExcludeRow(_)
|
||||
)
|
||||
) > 0,
|
||||
|
||||
// load the Objects.Properties function only if we have DataObjects
|
||||
ObjectsProperties = if HasDataObjects then Extension.LoadFunction("Objects.Properties.pqm") else null,
|
||||
|
||||
// Add properties column only if model has DataObjects
|
||||
FinalTable = if HasDataObjects then
|
||||
Table.AddColumn(
|
||||
BasicTable,
|
||||
"properties",
|
||||
each let
|
||||
dataRecord = [data],
|
||||
isDataObject = Text.Contains(Record.FieldOrDefault(dataRecord, "speckle_type", ""), "DataObject"),
|
||||
hasProperties = Record.HasFields(dataRecord, {"properties"}),
|
||||
extractedProperties = if hasProperties and isDataObject then
|
||||
try ObjectsProperties(dataRecord) otherwise []
|
||||
else
|
||||
[]
|
||||
in
|
||||
if Record.FieldCount(extractedProperties) > 0 then extractedProperties else null
|
||||
)
|
||||
else
|
||||
BasicTable,
|
||||
|
||||
// Apply the same filtering logic as before
|
||||
FilteredTable = if HasDataObjects then
|
||||
Table.SelectRows(
|
||||
FinalTable,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// function for federating multiple tables by combining them and creating a concatenated Version Object ID
|
||||
(tables as list, optional excludeData as logical) as table =>
|
||||
let
|
||||
ViewerOnly = if excludeData = null then false else excludeData,
|
||||
|
||||
// filter columns from each table if excludeData is true
|
||||
ProcessedTables = List.Transform(
|
||||
tables,
|
||||
each
|
||||
if ViewerOnly then
|
||||
Table.SelectColumns(_, {"Version Object ID", "Object IDs"}, MissingField.Ignore)
|
||||
else
|
||||
_
|
||||
),
|
||||
|
||||
CombinedTable = Table.Combine(ProcessedTables),
|
||||
|
||||
DistinctVersionObjectIDs = List.Distinct(CombinedTable[Version Object ID]),
|
||||
ConcatenatedVersionObjectIDs = Text.Combine(DistinctVersionObjectIDs, ","),
|
||||
|
||||
// Replace all Version Object ID values with the concatenated string
|
||||
FederatedTable = Table.ReplaceValue(
|
||||
CombinedTable,
|
||||
each [Version Object ID],
|
||||
ConcatenatedVersionObjectIDs,
|
||||
Replacer.ReplaceText,
|
||||
{"Version Object ID"}
|
||||
)
|
||||
in
|
||||
FederatedTable
|
||||
@@ -0,0 +1,163 @@
|
||||
// function for mapping collection names to referenced elements in Speckle data
|
||||
(inputData as table) as table =>
|
||||
let
|
||||
// Helper function to safely get field value
|
||||
SafeFieldValue = (record as record, fieldName as text) as any =>
|
||||
if Record.HasFields(record, {fieldName}) then
|
||||
Record.Field(record, fieldName)
|
||||
else
|
||||
null,
|
||||
|
||||
// Helper function to safely get nested field value
|
||||
SafeNestedValue = (record as record, path as list) as any =>
|
||||
List.Accumulate(
|
||||
path,
|
||||
record,
|
||||
(current, fieldName) =>
|
||||
if current <> null and Value.Is(current, type record) and Record.HasFields(current, {fieldName}) then
|
||||
Record.Field(current, fieldName)
|
||||
else
|
||||
null
|
||||
),
|
||||
|
||||
// Step 1: Identify Collection Objects
|
||||
CollectionObjects = Table.SelectRows(
|
||||
inputData,
|
||||
each
|
||||
let
|
||||
speckleType = SafeFieldValue(_, "Speckle Type")
|
||||
in
|
||||
speckleType <> null and Text.Contains(speckleType, "Collection")
|
||||
),
|
||||
|
||||
// Step 2: Extract Collection Metadata
|
||||
CollectionMetadata = Table.AddColumn(
|
||||
CollectionObjects,
|
||||
"CollectionInfo",
|
||||
each
|
||||
let
|
||||
objectId = SafeFieldValue(_, "Object IDs"),
|
||||
collectionName = SafeNestedValue(_, {"data", "name"}),
|
||||
elements = SafeNestedValue(_, {"data", "elements"})
|
||||
in
|
||||
[
|
||||
ObjectId = objectId,
|
||||
CollectionName = if collectionName <> null then collectionName else "Unnamed Collection",
|
||||
Elements = if elements <> null and Value.Is(elements, type list) then elements else {}
|
||||
]
|
||||
),
|
||||
|
||||
// Step 3: Build Collection Hierarchy Mapping
|
||||
CollectionHierarchy = Table.AddColumn(
|
||||
CollectionMetadata,
|
||||
"CollectionReferences",
|
||||
each
|
||||
let
|
||||
info = [CollectionInfo],
|
||||
collectionName = info[CollectionName],
|
||||
elements = info[Elements]
|
||||
in
|
||||
List.Transform(
|
||||
elements,
|
||||
(element) =>
|
||||
let
|
||||
referencedId = if Value.Is(element, type record) and Record.HasFields(element, {"referencedId"}) then
|
||||
element[referencedId]
|
||||
else
|
||||
null
|
||||
in
|
||||
if referencedId <> null then
|
||||
[
|
||||
ReferencedId = referencedId,
|
||||
CollectionName = collectionName,
|
||||
ParentCollectionId = info[ObjectId]
|
||||
]
|
||||
else
|
||||
null
|
||||
)
|
||||
),
|
||||
|
||||
// Step 4: Flatten Reference Mapping
|
||||
FlattenedReferences = Table.SelectRows(
|
||||
Table.ExpandListColumn(
|
||||
Table.SelectColumns(CollectionHierarchy, {"CollectionReferences"}),
|
||||
"CollectionReferences"
|
||||
),
|
||||
each [CollectionReferences] <> null
|
||||
),
|
||||
|
||||
ReferenceTable = Table.ExpandRecordColumn(
|
||||
FlattenedReferences,
|
||||
"CollectionReferences",
|
||||
{"ReferencedId", "CollectionName", "ParentCollectionId"},
|
||||
{"ReferencedId", "CollectionName", "ParentCollectionId"}
|
||||
),
|
||||
|
||||
// Step 5: Build Hierarchical Collection Paths
|
||||
BuildCollectionPath = (objectId as text, visited as list) as text =>
|
||||
let
|
||||
// Prevent infinite loops
|
||||
_ = if List.Contains(visited, objectId) then
|
||||
error "Circular reference detected in collection hierarchy"
|
||||
else
|
||||
null,
|
||||
|
||||
newVisited = List.InsertRange(visited, 0, {objectId}),
|
||||
|
||||
// Find if this object is referenced by any collection
|
||||
parentReferences = Table.SelectRows(ReferenceTable, each [ReferencedId] = objectId),
|
||||
|
||||
result = if Table.RowCount(parentReferences) = 0 then
|
||||
// No parent collection found
|
||||
""
|
||||
else
|
||||
let
|
||||
parentRef = parentReferences{0},
|
||||
parentCollectionId = parentRef[ParentCollectionId],
|
||||
currentCollectionName = parentRef[CollectionName],
|
||||
|
||||
// Recursively get parent path
|
||||
parentPath = @BuildCollectionPath(parentCollectionId, newVisited),
|
||||
|
||||
// Build full path
|
||||
fullPath = if parentPath = "" then
|
||||
currentCollectionName
|
||||
else
|
||||
parentPath & "::" & currentCollectionName
|
||||
in
|
||||
fullPath
|
||||
in
|
||||
result,
|
||||
|
||||
// Step 6: Add Collection Paths to data field
|
||||
FinalData = Table.TransformColumns(
|
||||
inputData,
|
||||
{
|
||||
"data", each
|
||||
let
|
||||
currentData = _,
|
||||
currentRow = Table.SelectRows(inputData, each [data] = currentData){0},
|
||||
objectId = SafeFieldValue(currentRow, "Object IDs"),
|
||||
collectionPath = if objectId <> null then
|
||||
try
|
||||
BuildCollectionPath(objectId, {})
|
||||
otherwise
|
||||
""
|
||||
else
|
||||
"",
|
||||
|
||||
// Add CollectionPath field to the data record, set to null if empty
|
||||
enhancedData = if Value.Is(currentData, type record) then
|
||||
Record.AddField(
|
||||
currentData,
|
||||
"collectionPath",
|
||||
if collectionPath = "" then null else collectionPath
|
||||
)
|
||||
else
|
||||
currentData
|
||||
in
|
||||
enhancedData
|
||||
}
|
||||
)
|
||||
in
|
||||
FinalData
|
||||
@@ -0,0 +1,18 @@
|
||||
(objectRecord as record, optional outputAsList as nullable logical) as any =>
|
||||
let
|
||||
compositeStructure =
|
||||
if Record.HasFields(objectRecord[properties], "Composite Structure") then
|
||||
objectRecord[properties][Composite Structure]
|
||||
else if Record.HasFields(objectRecord[properties], "Parameters") and
|
||||
Record.HasFields(objectRecord[properties][Parameters], "Type Parameters") and
|
||||
Record.HasFields(objectRecord[properties][Parameters][Type Parameters], "Structure") then
|
||||
objectRecord[properties][Parameters][Type Parameters][Structure]
|
||||
else
|
||||
null,
|
||||
result =
|
||||
if outputAsList = true then
|
||||
if compositeStructure <> null then Record.ToList(compositeStructure) else null
|
||||
else
|
||||
compositeStructure
|
||||
in
|
||||
result
|
||||
@@ -0,0 +1,15 @@
|
||||
// Helper function to extract [properties][Material Quantities] and optionally output as list
|
||||
(objectRecord as record, optional outputAsList as logical) as any =>
|
||||
let
|
||||
// Ensure outputAsList is logical and defaults to false if not provided
|
||||
OutputAsList = if outputAsList = null then false else outputAsList,
|
||||
// Check if 'properties' and 'Material Quantities' exist
|
||||
HasMaterialQuantities = Record.HasFields(objectRecord, {"properties"}) and Record.HasFields(Record.Field(objectRecord, "properties"), {"Material Quantities"}),
|
||||
MaterialQuantities = if HasMaterialQuantities then Record.Field(Record.Field(objectRecord, "properties"), "Material Quantities") else null,
|
||||
Result = if MaterialQuantities = null then null else
|
||||
if OutputAsList then
|
||||
Record.ToList(MaterialQuantities)
|
||||
else
|
||||
MaterialQuantities
|
||||
in
|
||||
Result
|
||||
@@ -0,0 +1,196 @@
|
||||
// function for extracting and flattening properties from Speckle objects
|
||||
(inputRecord as record, optional filterKeys as list) as record =>
|
||||
let
|
||||
// auto-extract properties if the input has a "properties" field
|
||||
ActualInput = if Record.HasFields(inputRecord, {"properties"}) then
|
||||
inputRecord[properties]
|
||||
else
|
||||
inputRecord,
|
||||
|
||||
// helper function to check if a key should be included
|
||||
ShouldIncludeKey = (keyName as text) as logical =>
|
||||
if filterKeys = null then
|
||||
true
|
||||
else
|
||||
List.Contains(filterKeys, keyName),
|
||||
|
||||
// define excluded paths
|
||||
ExcludedPaths = {
|
||||
"Composite Structure",
|
||||
"Material Quantities",
|
||||
"Parameters.Type Parameters.Structure"
|
||||
},
|
||||
|
||||
IsExcludedPath = (path as text) as logical =>
|
||||
List.AnyTrue(
|
||||
List.Transform(
|
||||
ExcludedPaths,
|
||||
(excludedPath) => Text.StartsWith(path, excludedPath)
|
||||
)
|
||||
),
|
||||
|
||||
// helper function to handle duplicate keys by adding suffixes
|
||||
AddUniqueKey = (existingRecord as record, newKey as text, newValue as any) as record =>
|
||||
let
|
||||
originalKey = newKey,
|
||||
counter = 1,
|
||||
|
||||
// find a unique key by adding suffix if needed
|
||||
FindUniqueKey = (testKey as text, testCounter as number) as text =>
|
||||
if Record.HasFields(existingRecord, {testKey}) then
|
||||
@FindUniqueKey(originalKey & "_" & Text.From(testCounter), testCounter + 1)
|
||||
else
|
||||
testKey,
|
||||
|
||||
uniqueKey = FindUniqueKey(newKey, counter),
|
||||
result = Record.AddField(existingRecord, uniqueKey, newValue)
|
||||
in
|
||||
result,
|
||||
|
||||
// enhanced combine function that handles duplicates and filtering
|
||||
SafeRecordCombine = (records as list) as record =>
|
||||
List.Accumulate(
|
||||
records,
|
||||
[],
|
||||
(state, current) =>
|
||||
List.Accumulate(
|
||||
Record.FieldNames(current),
|
||||
state,
|
||||
(innerState, fieldName) =>
|
||||
if ShouldIncludeKey(fieldName) then
|
||||
AddUniqueKey(innerState, fieldName, Record.Field(current, fieldName))
|
||||
else
|
||||
innerState
|
||||
)
|
||||
),
|
||||
|
||||
// helper function to process name-value objects
|
||||
ProcessNameValueObject = (obj as record) as record =>
|
||||
let
|
||||
// Assert that the object has required fields
|
||||
_ = if not (Record.HasFields(obj, {"name"}) and Record.HasFields(obj, {"value"})) then
|
||||
error [
|
||||
Reason = "Invalid Name-Value Object",
|
||||
Message = "Object must have both 'name' and 'value' fields"
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
BaseName = obj[name],
|
||||
BaseValue = obj[value],
|
||||
|
||||
// only extract name and value if it should be included
|
||||
Result = if ShouldIncludeKey(BaseName) then
|
||||
Record.FromList({BaseValue}, {BaseName})
|
||||
else
|
||||
[]
|
||||
in
|
||||
Result,
|
||||
|
||||
// helper function to process direct key-value objects
|
||||
ProcessDirectKeyValueObject = (obj as record) as record =>
|
||||
let
|
||||
// assert that input is a record
|
||||
_ = if not Value.Is(obj, type record) then
|
||||
error [
|
||||
Reason = "Invalid Input Type",
|
||||
Message = "Expected record for direct key-value processing"
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
// extract all primitive key-value pairs
|
||||
PrimitiveFields = List.Transform(
|
||||
Record.FieldNames(obj),
|
||||
(fieldName) =>
|
||||
let
|
||||
fieldValue = Record.Field(obj, fieldName)
|
||||
in
|
||||
if not Type.Is(Value.Type(fieldValue), type record) and ShouldIncludeKey(fieldName) then
|
||||
Record.FromList({fieldValue}, {fieldName})
|
||||
else
|
||||
[]
|
||||
),
|
||||
|
||||
// filter out empty records and combine using safe combine
|
||||
FilteredFields = List.Select(PrimitiveFields, (rec) => Record.FieldCount(rec) > 0),
|
||||
Result = SafeRecordCombine(FilteredFields)
|
||||
in
|
||||
Result,
|
||||
|
||||
// helper functions for type checking
|
||||
HasDirectKeyValuePattern = (obj as record) as logical =>
|
||||
List.AllTrue(
|
||||
List.Transform(
|
||||
Record.FieldValues(obj),
|
||||
(value) => not Type.Is(Value.Type(value), type record)
|
||||
)
|
||||
),
|
||||
|
||||
IsNestedContainer = (obj as record) as logical =>
|
||||
List.AnyTrue(
|
||||
List.Transform(
|
||||
Record.FieldValues(obj),
|
||||
(value) => Type.Is(Value.Type(value), type record)
|
||||
)
|
||||
),
|
||||
|
||||
// main processing function with path tracking
|
||||
ProcessField = (fieldName as text, fieldValue as any, currentPath as text) as record =>
|
||||
let
|
||||
fieldPath = if currentPath = "" then fieldName else currentPath & "." & fieldName
|
||||
in
|
||||
if IsExcludedPath(fieldPath) then
|
||||
[]
|
||||
else if Type.Is(Value.Type(fieldValue), type record) then
|
||||
if Record.HasFields(fieldValue, {"name", "value"}) then
|
||||
ProcessNameValueObject(fieldValue)
|
||||
else if HasDirectKeyValuePattern(fieldValue) then
|
||||
ProcessDirectKeyValueObject(fieldValue)
|
||||
else if IsNestedContainer(fieldValue) then
|
||||
// recursive call for nested containers
|
||||
@ProcessRecord(fieldValue, fieldPath)
|
||||
else
|
||||
// unknown record type, skip
|
||||
[]
|
||||
else
|
||||
// primitive value - check if should be included
|
||||
if ShouldIncludeKey(fieldName) then
|
||||
Record.FromList({fieldValue}, {fieldName})
|
||||
else
|
||||
[],
|
||||
|
||||
// process entire record recursively
|
||||
ProcessRecord = (record as record, currentPath as text) as record =>
|
||||
let
|
||||
// assert that input is a record
|
||||
_ = if not Value.Is(record, type record) then
|
||||
error [
|
||||
Reason = "Invalid Record Type",
|
||||
Message = "Expected record for processing, but received: " & Text.From(Value.Type(record))
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
ProcessedFields = List.Transform(
|
||||
Record.FieldNames(record),
|
||||
(fieldName) => ProcessField(fieldName, Record.Field(record, fieldName), currentPath)
|
||||
),
|
||||
FilteredFields = List.Select(ProcessedFields, (rec) => Record.FieldCount(rec) > 0),
|
||||
CombinedRecord = SafeRecordCombine(FilteredFields)
|
||||
in
|
||||
CombinedRecord,
|
||||
|
||||
// assert that input is a record before processing
|
||||
_ = if not Value.Is(ActualInput, type record) then
|
||||
error [
|
||||
Reason = "Invalid Input Type",
|
||||
Message = "Input must be a record, but received: " & Text.From(Value.Type(ActualInput))
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
// start processing from root
|
||||
Result = ProcessRecord(ActualInput, "")
|
||||
in
|
||||
Result
|
||||
@@ -1,5 +1,5 @@
|
||||
(url as text) as list =>
|
||||
let
|
||||
try let
|
||||
// Import required functions
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
@@ -70,4 +70,10 @@
|
||||
JsonResponse = Json.Document(Response)
|
||||
|
||||
in
|
||||
JsonResponse
|
||||
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."
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
// Expands a record column in a table, adding new columns for each field in the record.
|
||||
// If UseCombinedNames is true, columns are named as ColumnName.FieldName, otherwise just FieldName.
|
||||
// If FieldNames is provided (list), only those fields are expanded.
|
||||
(table as table, columnName as text, optional FieldNames as list, optional UseCombinedNames as logical) as table =>
|
||||
let
|
||||
useCombined = if UseCombinedNames = null then false else UseCombinedNames,
|
||||
// Determine which field names to expand
|
||||
allFieldNames = if FieldNames <> null then FieldNames else List.Distinct(
|
||||
List.Combine(
|
||||
List.Transform(
|
||||
Table.Column(table, columnName),
|
||||
each if _ is record then Record.FieldNames(_) else {}
|
||||
)
|
||||
)
|
||||
),
|
||||
// Add each field as a new column
|
||||
addColumns = List.Accumulate(
|
||||
allFieldNames,
|
||||
table,
|
||||
(state, field) =>
|
||||
Table.AddColumn(
|
||||
state,
|
||||
if useCombined then columnName & "." & field else field,
|
||||
(row) =>
|
||||
if Record.HasFields(row, columnName) and Record.Field(row, columnName) is record and Record.HasFields(Record.Field(row, columnName), field)
|
||||
then Record.Field(Record.Field(row, columnName), field)
|
||||
else null
|
||||
)
|
||||
)
|
||||
in
|
||||
addColumns
|
||||
@@ -89,6 +89,9 @@
|
||||
"properties": {
|
||||
"defaultViewMode": {
|
||||
"type": { "text": true }
|
||||
},
|
||||
"navbarHidden": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -102,6 +105,9 @@
|
||||
},
|
||||
"isGhost": {
|
||||
"type": { "bool": true }
|
||||
},
|
||||
"zoomOnFilter": {
|
||||
"type": { "bool": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+5
-4
@@ -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",
|
||||
|
||||
@@ -5,6 +5,19 @@
|
||||
<ViewerControlsButtonToggle flat tooltip="Zoom extends" @click="onZoomExtentsClicked">
|
||||
<ArrowsPointingOutIcon class="h-4 w-4 md:h-5 md:w-5" />
|
||||
</ViewerControlsButtonToggle>
|
||||
<!-- Zoom on Filter -->
|
||||
<ViewerControlsButtonToggle
|
||||
:tooltip="
|
||||
visualStore.isZoomOnFilterActive
|
||||
? 'Move camera on filter'
|
||||
: 'Keep camera position on filter'
|
||||
"
|
||||
flat
|
||||
@click="toggleZoomOnFilter"
|
||||
>
|
||||
<ZoomToFit v-if="visualStore.isZoomOnFilterActive" class="h-5 w-5" />
|
||||
<ZoomToFit v-else class="h-5 w-5 opacity-30" />
|
||||
</ViewerControlsButtonToggle>
|
||||
<!-- Ghost / Hidden -->
|
||||
<ViewerControlsButtonToggle
|
||||
:tooltip="
|
||||
@@ -65,6 +78,7 @@ import Perspective from '../components/global/icon/Perspective.vue'
|
||||
import PerspectiveMore from '../components/global/icon/PerspectiveMore.vue'
|
||||
|
||||
import Ghost from '../components/global/icon/Ghost.vue'
|
||||
import ZoomToFit from '../components/global/icon/ZoomToFit.vue'
|
||||
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
@@ -111,6 +125,11 @@ const toggleGhostHidden = () => {
|
||||
visualStore.writeIsGhostToFile()
|
||||
}
|
||||
|
||||
const toggleZoomOnFilter = () => {
|
||||
visualStore.setIsZoomOnFilterActive(!visualStore.isZoomOnFilterActive)
|
||||
visualStore.writeZoomOnFilterToFile()
|
||||
}
|
||||
|
||||
const viewModesOpen = computed({
|
||||
get: () => activeControl.value === 'viewModes',
|
||||
set: (value) => {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.75 3.75V8.25M3.75 3.75H8.25M3.75 3.75L9 9M20.25 3.75H15.75M20.25 3.75V8.25M20.25 3.75L15 9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.75 15.4028L18.8093 12.3435C18.8772 12.2756 18.9638 12.2294 19.0581 12.2107C19.1523 12.1919 19.25 12.2016 19.3387 12.2383C19.4275 12.2751 19.5034 12.3373 19.5568 12.4172C19.6102 12.4971 19.6388 12.591 19.6389 12.687V20.063C19.6388 20.159 19.6102 20.2529 19.5568 20.3328C19.5034 20.4127 19.4275 20.4749 19.3387 20.5117C19.25 20.5484 19.1523 20.5581 19.0581 20.5393C18.9638 20.5206 18.8772 20.4744 18.8093 20.4065L15.75 17.3472M8.45833 20.75H14.2917C14.6784 20.75 15.0494 20.5964 15.3229 20.3229C15.5964 20.0494 15.75 19.6784 15.75 19.2917V13.4583C15.75 13.0716 15.5964 12.7006 15.3229 12.4271C15.0494 12.1536 14.6784 12 14.2917 12H8.45833C8.07156 12 7.70063 12.1536 7.42714 12.4271C7.15365 12.7006 7 13.0716 7 13.4583V19.2917C7 19.6784 7.15365 20.0494 7.42714 20.3229C7.70063 20.5964 8.07156 20.75 8.45833 20.75Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -37,8 +37,8 @@ export interface Hit {
|
||||
export interface IViewerEvents {
|
||||
ping: (message: string) => void
|
||||
setSelection: (objectIds: string[]) => void
|
||||
resetFilter: (objectIds: string[], ghost: boolean) => void
|
||||
filterSelection: (objectIds: string[], ghost: boolean) => void
|
||||
resetFilter: (objectIds: string[], ghost: boolean, zoom: boolean) => void
|
||||
filterSelection: (objectIds: string[], ghost: boolean, zoom: boolean) => void
|
||||
setViewMode: (viewMode: ViewMode) => void
|
||||
colorObjectsByGroup: (
|
||||
colorById: {
|
||||
@@ -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)
|
||||
}
|
||||
@@ -147,20 +149,24 @@ export class ViewerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public filterSelection = (objectIds: string[], ghost: boolean) => {
|
||||
public filterSelection = (objectIds: string[], ghost: boolean, zoom: boolean = true) => {
|
||||
console.log('🔗 Handling filterSelection inside ViewerHandler')
|
||||
if (objectIds) {
|
||||
this.unIsolateObjects()
|
||||
this.filteringState = this.filtering.isolateObjects(objectIds, 'powerbi', true, ghost)
|
||||
this.zoomObjects(objectIds, true)
|
||||
if (zoom) {
|
||||
this.zoomObjects(objectIds, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public resetFilter = (objectIds: string[], ghost: boolean) => {
|
||||
public resetFilter = (objectIds: string[], ghost: boolean, zoom: boolean = true) => {
|
||||
console.log('🔗 Handling filterSelection inside ViewerHandler')
|
||||
if (objectIds) {
|
||||
this.isolateObjects(objectIds, ghost)
|
||||
this.zoomObjects(objectIds, true)
|
||||
if (zoom) {
|
||||
this.zoomObjects(objectIds, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +225,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 +239,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) {
|
||||
@@ -241,7 +248,8 @@ export class ViewerHandler {
|
||||
|
||||
Tracker.dataLoaded({
|
||||
sourceHostApp: store.receiveInfo.sourceApplication,
|
||||
workspace_id: store.receiveInfo.workspaceId
|
||||
workspace_id: store.receiveInfo.workspaceId,
|
||||
core_version: store.receiveInfo.version
|
||||
})
|
||||
if (store.cameraPosition) {
|
||||
const position = new Vector3(
|
||||
@@ -256,12 +264,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
|
||||
|
||||
@@ -35,6 +35,8 @@ 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 isZoomOnFilterActive = ref<boolean>(true)
|
||||
|
||||
const commonError = ref<string>(undefined)
|
||||
|
||||
@@ -164,11 +166,17 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
|
||||
if (dataInput.value.selectedIds.length > 0) {
|
||||
isFilterActive.value = true
|
||||
viewerEmit.value('filterSelection', dataInput.value.selectedIds, isGhostActive.value)
|
||||
viewerEmit.value('filterSelection', dataInput.value.selectedIds, isGhostActive.value, isZoomOnFilterActive.value)
|
||||
} 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, isZoomOnFilterActive.value)
|
||||
} else {
|
||||
// No object IDs provided - show all objects without any filtering
|
||||
viewerEmit.value('unIsolateObjects')
|
||||
}
|
||||
}
|
||||
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
|
||||
}
|
||||
@@ -240,6 +248,22 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const writeZoomOnFilterToFile = () => {
|
||||
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
|
||||
postFileSaveSkipNeeded.value = true
|
||||
host.value.persistProperties({
|
||||
merge: [
|
||||
{
|
||||
objectName: 'camera',
|
||||
properties: {
|
||||
zoomOnFilter: isZoomOnFilterActive.value
|
||||
},
|
||||
selector: null
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const writeViewModeToFile = (viewMode: ViewMode) => {
|
||||
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
|
||||
postFileSaveSkipNeeded.value = true
|
||||
@@ -272,6 +296,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 +353,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
|
||||
}
|
||||
@@ -321,6 +370,10 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
isGhostActive.value = val
|
||||
}
|
||||
|
||||
const setIsZoomOnFilterActive = (val: boolean) => {
|
||||
isZoomOnFilterActive.value = val
|
||||
}
|
||||
|
||||
const setPostFileSaveSkipNeeded = (newValue: boolean) => (postFileSaveSkipNeeded.value = newValue)
|
||||
const setPostClickSkipNeeded = (newValue: boolean) => (postClickSkipNeeded.value = newValue)
|
||||
|
||||
@@ -332,7 +385,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, isZoomOnFilterActive.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 +406,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, isZoomOnFilterActive.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, isZoomOnFilterActive.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 +463,8 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
isBrandingHidden,
|
||||
isOrthoProjection,
|
||||
isGhostActive,
|
||||
isNavbarHidden,
|
||||
isZoomOnFilterActive,
|
||||
latestAvailableVersion,
|
||||
isConnectorUpToDate,
|
||||
commonError,
|
||||
@@ -380,8 +472,10 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
setLatestAvailableVersion,
|
||||
setIsOrthoProjection,
|
||||
setIsGhost,
|
||||
setIsZoomOnFilterActive,
|
||||
setFormattingSettings,
|
||||
setBrandingHidden,
|
||||
setNavbarHidden,
|
||||
setPostClickSkipNeeded,
|
||||
setPostFileSaveSkipNeeded,
|
||||
setCameraPositionInFile,
|
||||
@@ -395,11 +489,14 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
writeObjectsToFile,
|
||||
writeCameraViewToFile,
|
||||
writeIsGhostToFile,
|
||||
writeZoomOnFilterToFile,
|
||||
writeIsOrthoToFile,
|
||||
writeViewModeToFile,
|
||||
writeCameraPositionToFile,
|
||||
writeHideBrandingToFile,
|
||||
writeNavbarVisibilityToFile,
|
||||
toggleBranding,
|
||||
toggleNavbar,
|
||||
setViewerEmitter,
|
||||
setDataInput,
|
||||
setFieldInputState,
|
||||
@@ -409,6 +506,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
clearLoadingProgress,
|
||||
setIsLoadingFromFile,
|
||||
resetFilters,
|
||||
downloadLatestVersion
|
||||
downloadLatestVersion,
|
||||
handleObjectsLoadedComplete
|
||||
}
|
||||
})
|
||||
|
||||
@@ -296,9 +296,14 @@ async function fetchStreamedDataForModel(
|
||||
const endObjectCleanup = performance.now()
|
||||
console.log(`Objects cleaned up in: ${(endObjectCleanup - startObjectCleanup) / 1000} s`)
|
||||
|
||||
const sizeInBytes = new TextEncoder().encode(JSON.stringify(objects)).length
|
||||
const sizeInMB = sizeInBytes / (1024 * 1024)
|
||||
console.log(`Size of objects: ${sizeInMB} MB`)
|
||||
try {
|
||||
const sizeInBytes = new TextEncoder().encode(JSON.stringify(objects)).length
|
||||
const sizeInMB = sizeInBytes / (1024 * 1024)
|
||||
console.log(`Size of objects: ${sizeInMB} MB`)
|
||||
} catch (error) {
|
||||
console.log("Can't calculate the size of the model")
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
return objects
|
||||
} catch (error) {
|
||||
|
||||
@@ -31,7 +31,8 @@ export class Tracker {
|
||||
// eslint-disable-next-line camelcase
|
||||
server_id: hashedServer,
|
||||
email: receiveInfo.userEmail,
|
||||
isAnonymous: receiveInfo.userEmail === ''
|
||||
isAnonymous: receiveInfo.userEmail === '',
|
||||
core_version: receiveInfo.version
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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([
|
||||
@@ -184,6 +196,16 @@ export class Visual implements IVisual {
|
||||
)
|
||||
}
|
||||
|
||||
if (camera && 'zoomOnFilter' in camera) {
|
||||
console.log(
|
||||
`Zoom on filter?: ${options.dataViews[0].metadata.objects.camera?.zoomOnFilter as boolean}`
|
||||
)
|
||||
|
||||
visualStore.setIsZoomOnFilterActive(
|
||||
options.dataViews[0].metadata.objects.camera?.zoomOnFilter as boolean
|
||||
)
|
||||
}
|
||||
|
||||
// get receive info from file for mixpanel
|
||||
try {
|
||||
const receiveInfoFromFile = JSON.parse(
|
||||
|
||||
Reference in New Issue
Block a user