Compare commits

...

26 Commits

Author SHA1 Message Date
Dogukan Karatas 8345258990 Merge pull request #176 from specklesystems/dogukan/fix-function-registration
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: helper function registration
2025-06-24 15:22:12 +02:00
Dogukan Karatas dbd0f2f9ce fix registration 2025-06-24 15:19:11 +02:00
Dogukan Karatas c9b4155660 Merge pull request #174 from specklesystems/bilal/object-compound-structure
added objects composite structure
2025-06-24 14:56:26 +02:00
Dogukan Karatas c4d094d722 Merge branch 'dev' into bilal/object-compound-structure 2025-06-24 14:55:06 +02:00
Dogukan Karatas 5b003b182b Merge pull request #173 from specklesystems/bilal/object-materialquantities
object material quantities added
2025-06-24 14:54:36 +02:00
Dogukan Karatas 7eabd47f6d Merge pull request #175 from specklesystems/dogukan/cnx-2035-loading-got-veeeeeeery-sloooooooow
performance fix: remove `displayValue` exclusion
2025-06-24 14:54:24 +02:00
Dogukan Karatas 822b999be9 remove displayValue exclusion 2025-06-24 13:47:30 +02:00
Dogukan Karatas 2407b8758a Merge pull request #171 from specklesystems/dogukan/exclude-display-value
feat(data): exclude displayValue in data
2025-06-24 11:09:48 +02:00
Mucahit Bilal GOKER d6f5e65bd7 added objects composite structure 2025-06-19 15:28:41 +03:00
Mucahit Bilal GOKER 183cc36654 object material quantities added 2025-06-19 15:15:05 +03:00
Mucahit Bilal GOKER a740272585 Merge pull request #170 from specklesystems/bilal/expand-record-util
expand record helper function
2025-06-19 15:11:51 +03:00
Dogukan Karatas 72128a9f4e excludes displayValues in data 2025-06-19 13:55:41 +02:00
Mucahit Bilal GOKER 677c663ef3 Merge branch 'dev' into bilal/expand-record-util 2025-06-19 14:55:21 +03:00
Mucahit Bilal GOKER a077857c66 add new line 2025-06-19 14:40:16 +03:00
Mucahit Bilal GOKER 5897a286bc expand record helper function 2025-06-19 14:23:40 +03:00
Dogukan Karatas 5b49fb2a9a Merge pull request #169 from specklesystems/dogukan/revit-helper-functions
feat (data): get properties function
2025-06-19 13:07:57 +02:00
Dogukan Karatas 424404dd11 Merge branch 'dogukan/revit-helper-functions' of https://github.com/specklesystems/speckle-powerbi into dogukan/revit-helper-functions 2025-06-19 13:04:18 +02:00
Dogukan Karatas 31312522a7 adds objects.collections 2025-06-19 12:47:18 +02:00
Mucahit Bilal GOKER 932198dccf rename GetProperties to Objects.Properties 2025-06-19 13:47:13 +03:00
Dogukan Karatas 3770502ca4 adds function overload 2025-06-18 16:30:36 +02:00
Dogukan Karatas 93e8fcdd9d gets duplicated props 2025-06-18 16:13:42 +02:00
Dogukan Karatas 370052b2be GetProperties added 2025-06-18 14:41:14 +02:00
Oğuzhan Koral aa4a137a0d handle size in MB separately (#168)
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-06-16 21:17:43 +03:00
Dogukan Karatas 4acdf30734 error handling (#167)
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2025-06-16 12:05:21 +00:00
Dogukan Karatas b531446acd feat (visual): send connector version to mixpanel (#165)
* connector version added

* updated props
2025-06-16 15:03:54 +03:00
Jedd Morgan de1b2ca39c Run CI on PR (#164) 2025-06-06 10:14:00 +01:00
13 changed files with 506 additions and 9 deletions
@@ -1,4 +1,4 @@
name: build_powerbi
name: Build and deploy Connector and Visual
on:
push:
branches: ["installer-test/**"]
+30
View File
@@ -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
+31
View File
@@ -75,6 +75,37 @@ 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
);
[DataSource.Kind = "Speckle", Publish="GetByUrl.Publish"]
shared Speckle.GetByUrl = Value.ReplaceType(
Speckle.LoadFunction("GetByUrl.pqm"),
@@ -59,7 +59,7 @@
)
),
// 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", "")
@@ -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
+2 -1
View File
@@ -241,7 +241,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(
@@ -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) {
+2 -1
View File
@@ -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
}
}