Compare commits

...

72 Commits

Author SHA1 Message Date
Mucahit Bilal GOKER d4ee1f2a55 feat(revit): add roomId and spaceId parameters (#1181)
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
* add roomId and spaceId parameters

* clean up
2025-11-11 10:30:27 +03:00
Jedd Morgan 4f960cc670 readme tweaks (#1179) 2025-11-10 13:52:43 +00:00
Jedd Morgan 1f63c1f8b3 Merge pull request #1165 from specklesystems/main
chore: Back merge main -> Dev
2025-11-10 13:48:30 +00:00
Jedd Morgan 2ed9ffbca7 Use Openheadless for unit support (#1178) 2025-11-10 12:53:55 +00:00
Jedd Morgan d87b862e2b add readme (#1173) 2025-11-10 12:49:39 +00:00
Mucahit Bilal GOKER 3ad3ad2f01 feat(rhino): use existing materials when loading (#1177)
* reuse existing materials

* comment out purge material

* fix: unnecessary using directives

* more details in exception message and remove material purge.

---------

Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
2025-11-10 15:43:04 +03:00
Björn Steinhagen 6db7e46401 fix(grasshopper): refresh parameter UI when toggling empty properties mode (#1174) 2025-11-10 15:07:57 +03:00
Björn Steinhagen 13fc24c7c7 fix(rhino): handles null active doc (#1180) 2025-11-10 14:51:06 +03:00
Jedd Morgan cf86158b83 Ignore (#1171) 2025-11-03 12:57:17 +00:00
Björn Steinhagen 25eb955636 feat(etabs): add volume calculation for frames and shells (#1167)
* feat: adds frame volume prop

* feat: adds shell volume prop

* refactor: simplify caching

* fix(etabs): use IsInfinity for etabs 21
2025-10-28 17:39:30 +02:00
dependabot[bot] 7862a858ae chore(deps): bump actions/upload-artifact from 4 to 5 (#1166)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 18:09:02 +00:00
Jedd Morgan bc18d3b494 fix(rhino-importer): Use main thread always for document creation (#1161)
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
* Use main thread

* format

* configure await false

* pass args only once
2025-10-27 11:11:56 +00:00
Björn Steinhagen fd34f22028 fix(grasshopper): applicationId for SpeckleDataObject on creation (#1159)
* chore: formatting

* fix: generate appId on object creation

* feat: validate uniqueness of objects within a collection
2025-10-25 20:46:20 +01:00
Jedd Morgan 958c9e5e94 Merge pull request #1158 from specklesystems/main
Main -> Dev back merge
2025-10-20 15:42:23 +01:00
Jedd Morgan 7c7260c603 Merge pull request #1157 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Update dev into main
2025-10-20 15:39:33 +01:00
Oğuzhan Koral bae9e3e0f1 Prevent crashes on unnamed files (#1154)
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2025-10-20 17:33:00 +03:00
Oğuzhan Koral 26b0394613 feat(revit): display value proxies (#1140)
* POC

* some fixes

* Handle autocad, rhino and sketchup receives

* Handle revit receive

* Fix transform issues

* fix: custom mesh id logic

* Hash function

* Merge pull request #1142 from specklesystems/jedd/cnx-2657-hashing-the-meshes

feat(revit)!: Use Hash function for mesh geometry instance ids

* Use v2 style transform

* extra comments

* experiment1

* correct transform logic and disposal

* corrected transform logic

* simplify (maybe) the transform combination

* refactor(revit): replace tuples with DisplayValueResult record for display values (#1145)

* Clear instance proxies per conversion

* fix: material assignment on revit receive (#1146)

* Fix: enable send caching (#1148)

* Enable caching without definition proxy noise

* Log element id relationship while creating cache to filter after conversion to attach root

* Update RevitRootObjectBuilder.cs

* Clear cache on document swap

* More clean up

* fix(revit): defer material proxy population to prevent duplicate instance mesh IDs (#1155)

* fix(revit): defer material proxy population to prevent duplicate instance mesh IDs

* chore: formatting

* chore: campsite

* refactor: throwing on cache error

* refactor: move material proxy population into cache singleton

* fix: di

* fix(rhino): match cleaned block names when purging instance definitions (#1156)

* fix(rhino): match cleaned block names when purging instance definitions

* refactor: simplification

* chore: comments

* refactor: use .Contains

---------

Co-authored-by: Björn <steinhagen.bjoern@gmail.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
2025-10-20 15:26:56 +01:00
Dogukan Karatas 689ef0bcbe Merge pull request #1149 from specklesystems/dogukan/cnx-2490-receive-property-sets-in-civil-3d
feat (civil): receive property sets
2025-10-17 18:09:25 +02:00
Dogukan Karatas 461585b782 adds additional cast 2025-10-17 17:58:01 +02:00
Claire Kuang ea33f35a7d removes unnecessary casting on send 2025-10-17 16:57:38 +01:00
Claire Kuang 7427f1a2f3 adds constants and better host object builder methods 2025-10-17 16:08:40 +01:00
Dogukan Karatas b7984bf97e add additional cleaning 2025-10-17 15:25:35 +02:00
Dogukan Karatas 9b24a45b6e property set pruge 2025-10-17 12:50:58 +02:00
Claire Kuang 4ace81a422 Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-17 10:02:40 +01:00
Björn Steinhagen a60790c92c fix(grasshopper): account selection not respected when url input connected (#1150)
* fix: account switching on urlInput
* chore: good ol' comments
* chore: adding server url to exception message
2025-10-17 10:02:12 +01:00
Dogukan Karatas fd0d00cac3 Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-16 18:48:01 +02:00
Dogukan Karatas 498396e611 Merge pull request #1153 from specklesystems/dogukan/build-package-version-bump
chore(build): bump Microsoft.Build version
2025-10-16 18:47:18 +02:00
Dogukan Karatas 5444377398 bump version 2025-10-16 18:29:20 +02:00
Dogukan Karatas 9d981f9800 ci check 2025-10-16 17:59:13 +02:00
Dogukan Karatas 14e17fb67d prefix and purge 2025-10-16 17:52:30 +02:00
Dogukan Karatas 0ffa7685fd rather cast than convert 2025-10-16 17:16:20 +02:00
Dogukan Karatas dc7d4671e4 default value simplification 2025-10-16 16:40:37 +02:00
Dogukan Karatas 10cb5cd66f removed the parser 2025-10-16 16:24:53 +02:00
Jedd Morgan cb15d9f77a Merge pull request #1152 from specklesystems/jrm/trigger-ci
Remove arcgis from readme
2025-10-16 15:05:53 +01:00
Jedd Morgan da74faef9b read me change to trigger ci 2025-10-16 15:02:58 +01:00
Claire Kuang 4368833c7e Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-16 09:56:57 +01:00
Dogukan Karatas a20df41316 readonly dict 2025-10-15 22:56:50 +02:00
Dogukan Karatas ccf48dbad1 process property set definitions from root object 2025-10-15 22:42:33 +02:00
Björn Steinhagen 6700aa27bc fix(revit): handle level extraction for face-based family instances (#1151)
* fix: get levels for face-based instances

* fix: level unpacker
2025-10-15 22:06:01 +02:00
Dogukan Karatas df525eab63 moved the baker to the connector 2025-10-15 21:25:12 +02:00
Dogukan Karatas 275901626f created an abstract class 2025-10-15 16:35:19 +02:00
Claire Kuang fac0dc31b2 Merge branch 'dev' into dogukan/cnx-2490-receive-property-sets-in-civil-3d 2025-10-15 12:48:26 +01:00
Dogukan Karatas 8696eca1f0 use active transaction 2025-10-15 13:12:41 +02:00
Dogukan Karatas d647c71cf5 baker added 2025-10-15 12:25:01 +02:00
Dogukan Karatas 9b218dd808 Revert "created PropertySetConverter"
This reverts commit 112093f914.
2025-10-15 12:06:04 +02:00
Björn Steinhagen 9f39dc521d fix(revit): clamp PBR material properties to avoid receive failures (#1147)
* fix: clamping roughness vaues

* chore: extending to other 0 - 1 values
2025-10-14 16:15:52 +02:00
Dogukan Karatas 112093f914 created PropertySetConverter 2025-10-13 14:08:35 +02:00
Oğuzhan Koral d174597770 Merge pull request #1141 from specklesystems/dev
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Update dev into main
2025-10-08 16:06:07 +03:00
Björn Steinhagen d9289787b7 fix(grasshopper): add auto-load persistence and reactive behavior to sync receive component (#1134)
* fix: poc `(Sync) Load` component reacting to new versions

* fix: persist setting between sessions

---------

Co-authored-by: Claire Kuang <kuang.claire@gmail.com>
2025-10-08 12:59:18 +02:00
Björn Steinhagen 77c1c3b511 fix(revit): handle null SelectedObjectIds in view filters on send (#1139) 2025-10-07 15:19:05 +02:00
Adam Hathcock 5a1c542832 Push doc to callers if possible (#1129)
* Push doc to callers if possible

* use check for active doc earlier.  Use doc instead of linked doc

* Use passed in current doc instead of getting active doc each time

* fmt

* Remove extra using
2025-10-07 07:25:36 +00:00
Adam Hathcock 091d7cc897 Stop exceptions we can't do anything about (#1138) 2025-10-06 17:19:30 +01:00
Adam Hathcock 21f4fb52a8 cache parameters by group and name instead of caching groups and assuming units are all the same (#1128)
* cache parameters by group and name

* fmt
2025-10-06 12:01:04 +01:00
Jedd Morgan 656ed709f3 Exit non-zero on fault (#1137)
.NET Build and Publish / build-windows (push) Has been cancelled
.NET Build and Publish / build-linux (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
2025-10-06 11:35:49 +01:00
Adam Hathcock 868ca8db66 Key for docs should fallback correctly and load when not found. (#1130)
* fix for revit 2024 and greater

* fallback to path name for 22/23

* adjust comment

---------

Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: Mucahit Bilal GOKER <51519350+bimgeek@users.noreply.github.com>
2025-10-06 10:22:19 +01:00
Björn Steinhagen a9360e5fac fix(grasshopper): optimize CreateSpeckleProperties for large data trees (#1135)
* perf: remove expensive tree flattening in CreateSpeckleProperties

* chore: format
2025-10-04 10:42:01 +01:00
Björn Steinhagen 3414599f72 fix(etabs): units not supported (#1132)
* fix: etabs units not supported

* chore: notes
2025-10-03 15:15:54 +00:00
Björn Steinhagen 84e92aa8a8 fix: prevent internal parameters from appearing in Params/Primitives tab (#1133) 2025-10-03 09:56:31 +01:00
Jedd Morgan edd842763f remove obsolete (#1131) 2025-10-02 16:44:26 +01:00
Björn Steinhagen 867ee0f928 feat(etabs): adds design data material props (#1111) 2025-10-02 16:50:39 +02:00
Jedd Morgan 29ee648d7f Swallow IOEx (#1125) 2025-10-01 22:38:56 +00:00
Jedd Morgan 5b9c610856 Only call IsConnected on Physical connectors (#1127) 2025-10-01 15:41:48 +00:00
Adam Hathcock 769ddf6407 merge update (#1121) 2025-10-01 15:37:35 +00:00
Claire Kuang dd7205f855 fix(rhino): adds area and volume props to breps (#1124)
* adds area and volume props to breps

* removes bbox props and adds area and volume to extrusion and mesh
2025-10-01 14:34:01 +01:00
Mucahit Bilal GOKER 30ee410309 fix(revit): Missing Type Mark parameter added (#1112)
* type mark fix

* fix: format

---------

Co-authored-by: Björn Steinhagen <steinhagen.bjoern@gmail.com>
2025-10-01 15:35:15 +03:00
Jedd Morgan 2d0b1a3a24 chore: Remove legacy .NET based IFC importer (#1118)
* remove IFC source files

* re-generate slnx files
2025-10-01 12:06:43 +01:00
Jedd Morgan 2cdf036172 Merge pull request #1123 from specklesystems/installer-test/linux-ci
chore(ci): Refactor out nuget publishing from CI, use linux for pr builds
2025-10-01 11:58:37 +01:00
Björn Steinhagen c14aa28e76 fix(grasshopper): type-specific outputs not refreshing in QuerySpeckleObjects component (#1114)
Co-authored-by: Claire Kuang <kuang.claire@gmail.com>
2025-09-29 16:50:27 +00:00
Björn Steinhagen 0e7d2554f8 fix(grasshopper): sync SpeckleGeometryWrapper Properties with Base["properties"] (#1113)
Co-authored-by: Claire Kuang <kuang.claire@gmail.com>
2025-09-29 18:45:27 +02:00
Adam Hathcock cdfc618bab Avoid a null points collection (#1116) 2025-09-29 07:55:29 +00:00
Adam Hathcock 1005edb609 feat(Navisworks) Expose send cache options to all (#1115) 2025-09-26 19:23:44 +02:00
Adam Hathcock 3b4da8de52 check connection before using? (#1109) 2025-09-25 08:23:08 +01:00
197 changed files with 3198 additions and 6289 deletions
+14 -28
View File
@@ -1,29 +1,11 @@
name: .NET Build
name: .NET Test
on: pull_request
on:
pull_request: {}
push:
branches: ["main"]
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.4xx # Align with global.json (including roll forward rules)
- name: Cache Nuget
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- name: ⚒️ Run build
run: ./build.ps1 test
test:
runs-on: ubuntu-latest
steps:
@@ -43,14 +25,18 @@ jobs:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- name: ⚒️ Run Build on Linux
run: ./build.sh build-linux
- name: ⚒️ Run tests
run: ./build.sh test-only
- name: ⚒️ Run Test
run: ./build.sh test-and-pack
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: Converters/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
# Disabling this code for now, since we no longer need to publish nugets from this repo
# But keeping it around incase we ever need in the future.
# Ideally, I'd also like to move the nuget token to be an environment secret, and to have tight package scopes
# - name: Push to nuget.org
# if: ${{ inputs.deployNugets }}
# run: dotnet nuget push output/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{ secrets.CONNECTORS_NUGET_TOKEN }}
+10 -44
View File
@@ -6,8 +6,8 @@ on:
tags: ["v3.*.*"] # Manual delivery on every 3.x tag
jobs:
build-windows:
runs-on: windows-latest
build-connectors:
runs-on: windows-latest # Keeping on windows for now, for cross platform building of exe projects, we need to use dotnet publish
env:
SEMVER: "unset"
FILE_VERSION: "unset"
@@ -31,11 +31,11 @@ jobs:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- name: ⚒️ Run build on Windows
- name: ⚒️ Run build and zip connectors
run: ./build.ps1 zip
- name: ⬆️ Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: output-${{ env.SEMVER }}
path: output/*.*
@@ -48,10 +48,10 @@ jobs:
run: |
echo "semver=${{ env.SEMVER }}" >> "$Env:GITHUB_OUTPUT"
echo "file_version=${{ env.FILE_VERSION }}" >> "$Env:GITHUB_OUTPUT"
deploy-installers:
runs-on: ubuntu-latest
needs: build-windows
needs: build-connectors
env:
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
steps:
@@ -63,8 +63,8 @@ jobs:
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
inputs: '{
"run_id": "${{ github.run_id }}",
"semver": "${{ needs.build-windows.outputs.semver }}",
"file_version": "${{ needs.build-windows.outputs.file_version }}",
"semver": "${{ needs.build-connectors.outputs.semver }}",
"file_version": "${{ needs.build-connectors.outputs.file_version }}",
"repo": "${{ github.repository }}",
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
}'
@@ -74,42 +74,8 @@ jobs:
wait-for-completion-timeout: 10m
display-workflow-run-url: true
display-workflow-run-url-interval: 10s
# Allows us to inspect the artifacts of failed builds, since this below step will be skipped if the above step fails
- uses: geekyeggo/delete-artifact@v5
with:
name: output-*
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.4xx # Align with global.json (including roll forward rules)
- name: Cache Nuget
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- name: ⚒️ Run tests on Linux
run: ./build.sh test-only
- name: ⚒️ Run Build and Pack on Linux
run: ./build.sh build-linux
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: Converters/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
- name: Push to nuget.org
if: (github.ref_type == 'tag')
run: dotnet nuget push output/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{secrets.CONNECTORS_NUGET_TOKEN }} --skip-duplicate
+17 -21
View File
@@ -7,9 +7,9 @@ using static SimpleExec.Command;
const string CLEAN = "clean";
const string RESTORE = "restore";
const string BUILD = "build";
const string BUILD_LINUX = "build-linux";
const string PACK = "pack";
const string TEST_AFFECTED = "test-affected";
const string TEST = "test";
const string TEST_ONLY = "test-only";
const string FORMAT = "format";
const string ZIP = "zip";
const string RESTORE_TOOLS = "restore-tools";
@@ -19,6 +19,7 @@ const string GEN_SOLUTIONS = "generate-solutions";
const string DEEP_CLEAN = "deep-clean";
const string DEEP_CLEAN_LOCAL = "deep-clean-local";
const string DETECT_AFFECTED = "detect-affected";
const string TEST_AND_PACK = "test-and-pack";
//need to pass arguments
/*var arguments = new List<string>();
@@ -150,7 +151,7 @@ Target(
Target(
RESTORE,
DependsOn(FORMAT, DETECT_AFFECTED),
DependsOn(FORMAT),
Consts.Solutions,
async s =>
{
@@ -181,8 +182,8 @@ Target(CHECK_SOLUTIONS, Solutions.CompareConnectorsToLocal);
Target(GEN_SOLUTIONS, Solutions.GenerateSolutions);
Target(
TEST,
DependsOn(BUILD, CHECK_SOLUTIONS),
TEST_AFFECTED,
DependsOn(DETECT_AFFECTED, BUILD, CHECK_SOLUTIONS),
async () =>
{
foreach (var s in await Affected.GetTestProjects())
@@ -192,14 +193,12 @@ Target(
}
);
//all tests on purpose
Target(
TEST_ONLY,
DependsOn(FORMAT),
TEST,
DependsOn(BUILD, CHECK_SOLUTIONS),
Glob.Files(".", "**/*.Tests.csproj"),
file =>
{
Run("dotnet", $"build \"{file}\" -c Release --no-incremental");
Run(
"dotnet",
$"test \"{file}\" -c Release --no-build --verbosity=minimal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage /p:AltCoverVerbosity=Warning"
@@ -207,31 +206,28 @@ Target(
}
);
Target(TEST_AND_PACK, DependsOn(TEST, PACK));
Target(
BUILD_LINUX,
DependsOn(FORMAT),
Glob.Files(".", "**/Speckle.Importers.Ifc.csproj"),
async file =>
PACK,
DependsOn(BUILD),
Consts.Solutions,
async solution =>
{
await RunAsync("dotnet", $"restore \"{file}\" --locked-mode");
var version = await Versions.ComputeVersion();
var fileVersion = await Versions.ComputeFileVersion();
Console.WriteLine($"Version: {version} & {fileVersion}");
await RunAsync(
"dotnet",
$"build \"{file}\" -c Release --no-restore -warnaserror -p:Version={version} -p:FileVersion={fileVersion} -v:m"
);
await RunAsync(
"dotnet",
$"pack \"{file}\" -c Release -o output --no-build -p:Version={version} -p:FileVersion={fileVersion} -v:m"
$"pack \"{solution}\" -c Release -o output --no-build -p:Version={version} -p:FileVersion={fileVersion} -v:m"
);
}
);
Target(
ZIP,
DependsOn(TEST),
DependsOn(TEST_AFFECTED),
async () =>
{
var version = await Versions.ComputeVersion();
@@ -282,6 +278,6 @@ Target(
}
);
Target("default", DependsOn(TEST), () => Console.WriteLine("Done!"));
Target("default", DependsOn(TEST_AFFECTED), () => Console.WriteLine("Done!"));
await RunTargetsAndExitAsync(args).ConfigureAwait(true);
+9 -9
View File
@@ -16,12 +16,12 @@
},
"Microsoft.Build": {
"type": "Direct",
"requested": "[17.11.4, )",
"resolved": "17.11.4",
"contentHash": "UMC7DfeFEHY2GGHHaghybUuUlLaByFHEFudR2PehMgDBuRuLAUePp1iaa4eFtVzepRzMtIbeSCVJCzzX3NV2Gg==",
"requested": "[17.11.48, )",
"resolved": "17.11.48",
"contentHash": "g8Kn575mNAKcuFotV3C7xvF+IbxuHennl67LH2shL2au1U6UqwReTDygCHyU04+koc2Yn7fHIbVQaC08HqEWow==",
"dependencies": {
"Microsoft.Build.Framework": "17.11.4",
"Microsoft.NET.StringTools": "17.11.4",
"Microsoft.Build.Framework": "17.11.48",
"Microsoft.NET.StringTools": "17.11.48",
"System.Collections.Immutable": "8.0.0",
"System.Configuration.ConfigurationManager": "8.0.0",
"System.Reflection.Metadata": "8.0.0",
@@ -82,8 +82,8 @@
},
"Microsoft.Build.Framework": {
"type": "Transitive",
"resolved": "17.11.4",
"contentHash": "u28uDihlqxtt8h2dL1ZJOZ7TRkxBK+HGr+3FgQpILVo7Q7gErkw8mYW9R+RM5PtxvZTdYb/4MWDL66vdIsANBQ=="
"resolved": "17.11.48",
"contentHash": "C3WIMt2wBl4++NX3jSEpTq5KXBhvAV154R4JrYHkfy9JSBcXWiL0mkgpspk5xSdOj+fS/uz7zluIy6bMM1fkkQ=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
@@ -97,8 +97,8 @@
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
"resolved": "17.11.48",
"contentHash": "0IQo089IGBEC4jgtishauZMVr9ZxOWNiGKeDvyzZlvw7p2r253lJh6IJCLLFWXvZnVrVO5mnsYIPamxFPzM08w=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
@@ -0,0 +1,296 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Converters.Common;
using Speckle.Sdk.Common;
using Speckle.Sdk.Dependencies;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Instances;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
namespace Speckle.Connectors.Autocad.Operations.Receive;
/// <summary>
/// <para>Base class for AutoCAD host object builders. Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public abstract class AutocadHostObjectBaseBuilder : IHostObjectBuilder
{
private readonly IRootToHostConverter _converter;
private readonly AutocadLayerBaker _layerBaker;
private readonly AutocadGroupBaker _groupBaker;
private readonly AutocadInstanceBaker _instanceBaker;
private readonly IAutocadMaterialBaker _materialBaker;
private readonly IAutocadColorBaker _colorBaker;
private readonly AutocadContext _autocadContext;
private readonly RootObjectUnpacker _rootObjectUnpacker;
private readonly IReceiveConversionHandler _conversionHandler;
protected AutocadHostObjectBaseBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler
)
{
_converter = converter;
_layerBaker = layerBaker;
_groupBaker = groupBaker;
_instanceBaker = instanceBaker;
_materialBaker = materialBaker;
_colorBaker = colorBaker;
_autocadContext = autocadContext;
_rootObjectUnpacker = rootObjectUnpacker;
_conversionHandler = conversionHandler;
}
public Task<HostObjectBuilderResult> Build(
Base rootObject,
string projectName,
string modelName,
IProgress<CardProgress> onOperationProgressed,
CancellationToken cancellationToken
)
{
// Prompt the UI conversion started. Progress bar will swoosh.
onOperationProgressed.Report(new("Converting", null));
// Layer filter for received commit with project and model name
_layerBaker.CreateLayerFilter(projectName, modelName);
// 0 - Clean then Rock n Roll!
string baseLayerPrefix = _autocadContext.RemoveInvalidChars($"SPK-{projectName}-{modelName}-");
PreReceiveDeepClean(baseLayerPrefix);
// 1 - Unpack objects and proxies from root commit object
var unpackedRoot = _rootObjectUnpacker.Unpack(rootObject);
// 2 - Split atomic objects and instance components with their path
var (atomicObjects, instanceComponents) = _rootObjectUnpacker.SplitAtomicObjectsAndInstances(
unpackedRoot.ObjectsToConvert
);
var atomicObjectsWithPath = _layerBaker.GetAtomicObjectsWithPath(atomicObjects);
var instanceComponentsWithPath = _layerBaker.GetInstanceComponentsWithPath(instanceComponents);
// POC: these are not captured by traversal, so we need to re-add them here
if (unpackedRoot.DefinitionProxies != null && unpackedRoot.DefinitionProxies.Count > 0)
{
var transformed = unpackedRoot.DefinitionProxies.Select(proxy =>
(Array.Empty<Collection>(), proxy as IInstanceComponent)
);
instanceComponentsWithPath.AddRange(transformed);
}
// 3 - Parse and bake proxies (materials and colors), as they are used later down the line by layers and objects
if (unpackedRoot.RenderMaterialProxies != null)
{
_materialBaker.ParseAndBakeRenderMaterials(
unpackedRoot.RenderMaterialProxies,
baseLayerPrefix,
onOperationProgressed
);
}
if (unpackedRoot.ColorProxies != null)
{
_colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed);
}
// 3.5 - Parse and bake additional proxies that are needed for conversion
ParseAndBakeAdditionalProxies(rootObject, baseLayerPrefix);
// 4 - Convert atomic objects
HashSet<ReceiveConversionResult> results = new();
HashSet<string> bakedObjectIds = new();
Dictionary<string, IReadOnlyCollection<Entity>> applicationIdMap = new();
var count = 0;
foreach (var (layerPath, atomicObject) in atomicObjectsWithPath)
{
onOperationProgressed.Report(new("Converting objects", (double)++count / atomicObjects.Count));
var ex = _conversionHandler.TryConvert(() =>
{
cancellationToken.ThrowIfCancellationRequested();
string objectId = atomicObject.applicationId ?? atomicObject.id.NotNull();
IReadOnlyCollection<Entity> convertedObjects = ConvertObject(atomicObject, layerPath, baseLayerPrefix);
applicationIdMap[objectId] = convertedObjects;
results.UnionWith(
convertedObjects.Select(e => new ReceiveConversionResult(
Status.SUCCESS,
atomicObject,
e.GetSpeckleApplicationId(),
e.GetType().ToString()
))
);
bakedObjectIds.UnionWith(convertedObjects.Select(e => e.GetSpeckleApplicationId()));
});
if (ex != null)
{
results.Add(new(Status.ERROR, atomicObject, null, null, ex));
}
}
// 5 - Convert instances
var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = _instanceBaker.BakeInstances(
instanceComponentsWithPath,
applicationIdMap,
baseLayerPrefix,
onOperationProgressed
);
bakedObjectIds.RemoveWhere(id => consumedObjectIds.Contains(id));
bakedObjectIds.UnionWith(createdInstanceIds);
results.RemoveWhere(result => result.ResultId != null && consumedObjectIds.Contains(result.ResultId));
results.UnionWith(instanceConversionResults);
// 6 - Create groups
if (unpackedRoot.GroupProxies != null)
{
IReadOnlyCollection<ReceiveConversionResult> groupResults = _groupBaker.CreateGroups(
unpackedRoot.GroupProxies,
applicationIdMap
);
results.UnionWith(groupResults);
}
return Task.FromResult(new HostObjectBuilderResult(bakedObjectIds, results));
}
protected void PreReceiveDeepClean(string baseLayerPrefix)
{
_layerBaker.DeleteAllLayersByPrefix(baseLayerPrefix);
_instanceBaker.PurgeInstances(baseLayerPrefix);
_materialBaker.PurgeMaterials(baseLayerPrefix);
PreReceiveAdditionalDeepClean(baseLayerPrefix);
}
/// <summary>
/// Method for adding app-specific additional deep clean of the document prior to receiving.
/// </summary>
protected virtual void PreReceiveAdditionalDeepClean(string baseLayerPrefix) { }
/// <summary>
/// Method for parsing and baking additional app-specific proxies on the root prior to converting and baking objects
/// </summary>
protected virtual void ParseAndBakeAdditionalProxies(Base rootObject, string baseLayerPrefix) { }
private IReadOnlyCollection<Entity> ConvertObject(Base obj, Collection[] layerPath, string baseLayerNamePrefix)
{
string layerName = _layerBaker.CreateLayerForReceive(layerPath, baseLayerNamePrefix);
var convertedEntities = new HashSet<Entity>();
using var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
// 1: convert
var converted = _converter.Convert(obj);
// 2: handle result
switch (converted)
{
case Entity entity:
var bakedEntity = BakeObject(entity, obj, layerName, tr);
convertedEntities.Add(bakedEntity);
break;
case List<(Entity, Base)> listConversionResult: // this is from fallback conversion for brep/brepx/subdx/extrusionx/polycurve
var bakedFallbackEntities = BakeObjectsAsGroup(listConversionResult, obj, layerName, baseLayerNamePrefix, tr);
convertedEntities.UnionWith(bakedFallbackEntities);
break;
default:
// TODO: capture defualt case with report object here? Same as in Rhino
break;
}
tr.Commit();
return convertedEntities.Freeze();
}
private Entity BakeObject(
Entity entity,
Base originalObject,
string layerName,
Transaction tr,
Base? parentObject = null
)
{
var objId = originalObject.applicationId ?? originalObject.id.NotNull();
if (_colorBaker.ObjectColorsIdMap.TryGetValue(objId, out AutocadColor? color))
{
entity.Color = color;
}
if (_materialBaker.TryGetMaterialId(originalObject, parentObject, out ObjectId matId))
{
entity.MaterialId = matId;
}
entity.AppendToDb(layerName);
// Hook for derived classes to perform additional operations after entity is added to database
PostBakeEntity(entity, originalObject, tr);
return entity;
}
/// <summary>
/// Method for additional app-specific operations on entities after the entity has been added to the document database.
/// Called after the entity is added to the database in an open transaction
/// </summary>
/// <param name="entity"></param>
/// <param name="originalObject"></param>
/// <param name="tr"></param>
protected virtual void PostBakeEntity(Entity entity, Base originalObject, Transaction tr)
{
// Default implementation does nothing - override in derived classes
}
private List<Entity> BakeObjectsAsGroup(
List<(Entity, Base)> fallbackConversionResult,
Base parentObject,
string layerName,
string baseLayerName,
Transaction tr
)
{
var ids = new ObjectIdCollection();
var entities = new List<Entity>();
foreach (var (conversionResult, originalObject) in fallbackConversionResult)
{
BakeObject(conversionResult, originalObject, layerName, tr, parentObject);
ids.Add(conversionResult.ObjectId);
entities.Add(conversionResult);
}
if (entities.Count <= 1) // return if empty list or only one, because we don't want to create empty or single item groups.
{
return entities;
}
var groupDictionary = (DBDictionary)
tr.GetObject(Application.DocumentManager.CurrentDocument.Database.GroupDictionaryId, OpenMode.ForWrite);
var groupName = _autocadContext.RemoveInvalidChars(
$@"{parentObject.speckle_type.Split('.').Last()} - {parentObject.applicationId ?? parentObject.id} ({baseLayerName})"
);
var newGroup = new Group(groupName, true);
newGroup.Append(ids);
groupDictionary.UpgradeOpen();
groupDictionary.SetAt(groupName, newGroup);
tr.AddNewlyCreatedDBObject(newGroup, true);
return entities;
}
}
@@ -1,238 +1,35 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Converters.Common;
using Speckle.Sdk.Common;
using Speckle.Sdk.Dependencies;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Instances;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
namespace Speckle.Connectors.Autocad.Operations.Receive;
/// <summary>
/// <para>Expects to be a scoped dependency per receive operation.</para>
/// <para>AutoCAD-specific host object builder. Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public class AutocadHostObjectBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler
) : IHostObjectBuilder
public sealed class AutocadHostObjectBuilder : AutocadHostObjectBaseBuilder
{
public Task<HostObjectBuilderResult> Build(
Base rootObject,
string projectName,
string modelName,
IProgress<CardProgress> onOperationProgressed,
CancellationToken cancellationToken
public AutocadHostObjectBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler
)
{
// Prompt the UI conversion started. Progress bar will swoosh.
onOperationProgressed.Report(new("Converting", null));
// Layer filter for received commit with project and model name
layerBaker.CreateLayerFilter(projectName, modelName);
// 0 - Clean then Rock n Roll!
string baseLayerPrefix = autocadContext.RemoveInvalidChars($"SPK-{projectName}-{modelName}-");
PreReceiveDeepClean(baseLayerPrefix);
// 1 - Unpack objects and proxies from root commit object
var unpackedRoot = rootObjectUnpacker.Unpack(rootObject);
// 2 - Split atomic objects and instance components with their path
var (atomicObjects, instanceComponents) = rootObjectUnpacker.SplitAtomicObjectsAndInstances(
unpackedRoot.ObjectsToConvert
);
var atomicObjectsWithPath = layerBaker.GetAtomicObjectsWithPath(atomicObjects);
var instanceComponentsWithPath = layerBaker.GetInstanceComponentsWithPath(instanceComponents);
// POC: these are not captured by traversal, so we need to re-add them here
if (unpackedRoot.DefinitionProxies != null && unpackedRoot.DefinitionProxies.Count > 0)
{
var transformed = unpackedRoot.DefinitionProxies.Select(proxy =>
(Array.Empty<Collection>(), proxy as IInstanceComponent)
);
instanceComponentsWithPath.AddRange(transformed);
}
// 3 - Bake materials and colors, as they are used later down the line by layers and objects
if (unpackedRoot.RenderMaterialProxies != null)
{
materialBaker.ParseAndBakeRenderMaterials(
unpackedRoot.RenderMaterialProxies,
baseLayerPrefix,
onOperationProgressed
);
}
if (unpackedRoot.ColorProxies != null)
{
colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed);
}
// 4 - Convert atomic objects
HashSet<ReceiveConversionResult> results = new();
HashSet<string> bakedObjectIds = new();
Dictionary<string, IReadOnlyCollection<Entity>> applicationIdMap = new();
var count = 0;
foreach (var (layerPath, atomicObject) in atomicObjectsWithPath)
{
onOperationProgressed.Report(new("Converting objects", (double)++count / atomicObjects.Count));
var ex = conversionHandler.TryConvert(() =>
{
cancellationToken.ThrowIfCancellationRequested();
string objectId = atomicObject.applicationId ?? atomicObject.id.NotNull();
IReadOnlyCollection<Entity> convertedObjects = ConvertObject(atomicObject, layerPath, baseLayerPrefix);
applicationIdMap[objectId] = convertedObjects;
results.UnionWith(
convertedObjects.Select(e => new ReceiveConversionResult(
Status.SUCCESS,
atomicObject,
e.GetSpeckleApplicationId(),
e.GetType().ToString()
))
);
bakedObjectIds.UnionWith(convertedObjects.Select(e => e.GetSpeckleApplicationId()));
});
if (ex != null)
{
results.Add(new(Status.ERROR, atomicObject, null, null, ex));
}
}
// 5 - Convert instances
var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = instanceBaker.BakeInstances(
instanceComponentsWithPath,
applicationIdMap,
baseLayerPrefix,
onOperationProgressed
);
bakedObjectIds.RemoveWhere(id => consumedObjectIds.Contains(id));
bakedObjectIds.UnionWith(createdInstanceIds);
results.RemoveWhere(result => result.ResultId != null && consumedObjectIds.Contains(result.ResultId));
results.UnionWith(instanceConversionResults);
// 6 - Create groups
if (unpackedRoot.GroupProxies != null)
{
IReadOnlyCollection<ReceiveConversionResult> groupResults = groupBaker.CreateGroups(
unpackedRoot.GroupProxies,
applicationIdMap
);
results.UnionWith(groupResults);
}
return Task.FromResult(new HostObjectBuilderResult(bakedObjectIds, results));
}
private void PreReceiveDeepClean(string baseLayerPrefix)
{
layerBaker.DeleteAllLayersByPrefix(baseLayerPrefix);
instanceBaker.PurgeInstances(baseLayerPrefix);
materialBaker.PurgeMaterials(baseLayerPrefix);
}
private IReadOnlyCollection<Entity> ConvertObject(Base obj, Collection[] layerPath, string baseLayerNamePrefix)
{
string layerName = layerBaker.CreateLayerForReceive(layerPath, baseLayerNamePrefix);
var convertedEntities = new HashSet<Entity>();
using var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
// 1: convert
var converted = converter.Convert(obj);
// 2: handle result
switch (converted)
{
case Entity entity:
var bakedEntity = BakeObject(entity, obj, layerName);
convertedEntities.Add(bakedEntity);
break;
case List<(Entity, Base)> listConversionResult: // this is from fallback conversion for brep/brepx/subdx/extrusionx/polycurve
var bakedFallbackEntities = BakeObjectsAsGroup(listConversionResult, obj, layerName, baseLayerNamePrefix);
convertedEntities.UnionWith(bakedFallbackEntities);
break;
default:
// TODO: capture defualt case with report object here? Same as in Rhino
break;
}
tr.Commit();
return convertedEntities.Freeze();
}
private Entity BakeObject(Entity entity, Base originalObject, string layerName, Base? parentObject = null)
{
var objId = originalObject.applicationId ?? originalObject.id.NotNull();
if (colorBaker.ObjectColorsIdMap.TryGetValue(objId, out AutocadColor? color))
{
entity.Color = color;
}
if (materialBaker.TryGetMaterialId(originalObject, parentObject, out ObjectId matId))
{
entity.MaterialId = matId;
}
entity.AppendToDb(layerName);
return entity;
}
private List<Entity> BakeObjectsAsGroup(
List<(Entity, Base)> fallbackConversionResult,
Base parentObject,
string layerName,
string baseLayerName
)
{
var ids = new ObjectIdCollection();
var entities = new List<Entity>();
foreach (var (conversionResult, originalObject) in fallbackConversionResult)
{
BakeObject(conversionResult, originalObject, layerName, parentObject);
ids.Add(conversionResult.ObjectId);
entities.Add(conversionResult);
}
if (entities.Count <= 1) // return if empty list or only one, because we don't want to create empty or single item groups.
{
return entities;
}
var tr = Application.DocumentManager.CurrentDocument.Database.TransactionManager.TopTransaction;
var groupDictionary = (DBDictionary)
tr.GetObject(Application.DocumentManager.CurrentDocument.Database.GroupDictionaryId, OpenMode.ForWrite);
var groupName = autocadContext.RemoveInvalidChars(
$@"{parentObject.speckle_type.Split('.').Last()} - {parentObject.applicationId ?? parentObject.id} ({baseLayerName})"
);
var newGroup = new Group(groupName, true);
newGroup.Append(ids);
groupDictionary.UpgradeOpen();
groupDictionary.SetAt(groupName, newGroup);
tr.AddNewlyCreatedDBObject(newGroup, true);
return entities;
}
: base(
converter,
layerBaker,
groupBaker,
instanceBaker,
materialBaker,
colorBaker,
autocadContext,
rootObjectUnpacker,
conversionHandler
) { }
}
@@ -38,6 +38,7 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\EntityExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\SpeckleApplicationIdExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\TransactionContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\AutocadHostObjectBaseBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\AutocadHostObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\AutocadRootObject.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\AutocadRootObjectBaseBuilder.cs" />
@@ -3,6 +3,8 @@ using Microsoft.Extensions.DependencyInjection;
using Speckle.Connectors.Autocad.DependencyInjection;
using Speckle.Connectors.Autocad.Operations.Send;
using Speckle.Connectors.Civil3dShared.Bindings;
using Speckle.Connectors.Civil3dShared.HostApp;
using Speckle.Connectors.Civil3dShared.Operations.Receive;
using Speckle.Connectors.Civil3dShared.Operations.Send;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.DUI.Bindings;
@@ -24,10 +26,12 @@ public static class Civil3dConnectorModule
// add receive
serviceCollection.LoadReceive();
serviceCollection.AddScoped<IHostObjectBuilder, Civil3dHostObjectBuilder>();
serviceCollection.AddSingleton<IBinding, Civil3dReceiveBinding>();
// additional classes
serviceCollection.AddScoped<PropertySetDefinitionHandler>();
serviceCollection.AddScoped<PropertySetBaker>();
// automatically detects the Class:IClass interface pattern to register all generated interfaces
serviceCollection.AddMatchingInterfacesAsTransient(Assembly.GetExecutingAssembly());
@@ -0,0 +1,404 @@
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Operations;
using Speckle.Converters.Civil3dShared;
using Speckle.Converters.Civil3dShared.Helpers;
using Speckle.Converters.Civil3dShared.ToSpeckle;
using Speckle.Converters.Common;
using Speckle.Sdk;
using Speckle.Sdk.Models;
using AAEC = Autodesk.Aec;
using AAECPDB = Autodesk.Aec.PropertyData.DatabaseServices;
using ADB = Autodesk.AutoCAD.DatabaseServices;
namespace Speckle.Connectors.Civil3dShared.HostApp;
/// <summary>
/// Helper class to bake property sets to entities on receive.
/// </summary>
public class PropertySetBaker
{
private const string PROP_SET_DEF_DICT_NAME = "AecPropertySetDefs";
private readonly IConverterSettingsStore<Civil3dConversionSettings> _settingsStore;
private readonly ILogger<PropertySetBaker> _logger;
private readonly PropertyHandler _propertyHandler;
/// <summary>
/// Map of property set definition name to its ObjectId. Populated during ParsePropertySetDefinitions.
/// </summary>
private readonly Dictionary<string, ADB.ObjectId> _propertySetDefinitionMap = new();
public PropertySetBaker(
IConverterSettingsStore<Civil3dConversionSettings> settingsStore,
ILogger<PropertySetBaker> logger
)
{
_settingsStore = settingsStore;
_logger = logger;
_propertyHandler = new PropertyHandler();
}
/// <summary>
/// Removes all property set definitions with a prefix before receive operation.
/// </summary>
public void PurgePropertySets(string namePrefix)
{
ADB.Database db = _settingsStore.Current.Document.Database;
using var tr = db.TransactionManager.StartTransaction();
List<ADB.ObjectId> definitionsToDelete = new();
// Access the property set definition dictionary from the named object dictionary
var nod = (ADB.DBDictionary)tr.GetObject(db.NamedObjectsDictionaryId, ADB.OpenMode.ForRead);
if (nod.Contains(PROP_SET_DEF_DICT_NAME))
{
ADB.ObjectId propSetDefsDictId = nod.GetAt(PROP_SET_DEF_DICT_NAME);
var propSetDefsDict = (ADB.DBDictionary)tr.GetObject(propSetDefsDictId, ADB.OpenMode.ForRead);
// Iterate through all property set definitions in the dictionary
foreach (ADB.DBDictionaryEntry entry in propSetDefsDict)
{
if (entry.Key.Contains(namePrefix))
{
definitionsToDelete.Add(entry.Value);
}
}
}
// Delete the matching definitions
foreach (ADB.ObjectId defId in definitionsToDelete)
{
try
{
var propSetDef = (AAECPDB.PropertySetDefinition)tr.GetObject(defId, ADB.OpenMode.ForWrite);
propSetDef.Erase();
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to purge property set definition");
}
}
tr.Commit();
}
/// <summary>
/// Parse and bake all property set definitions from the root object.
/// Should be called after purging and after materials/colors are parsed.
/// </summary>
public void ParseAndBakePropertySetDefinitions(Base rootObject, string namePrefix)
{
_propertySetDefinitionMap.Clear();
if (rootObject[ProxyKeys.PROPERTYSET_DEFINITIONS] is not Dictionary<string, object?> definitions)
{
return;
}
if (definitions.Count == 0)
{
return;
}
using var tr = _settingsStore.Current.Document.Database.TransactionManager.StartTransaction();
foreach (var definition in definitions)
{
string setName = definition.Key;
object? setDefObj = definition.Value;
if (setDefObj is not Dictionary<string, object?> setDefData)
{
_logger.LogWarning("Property set definition {SetName} has invalid data format", setName);
continue;
}
if (!setDefData.TryGetValue(PropertySetDefinitionHandler.PROP_SET_PROP_DEFS_KEY, out var propDefsObj))
{
_logger.LogWarning("Property set definition {SetName} missing propertyDefinitions", setName);
continue;
}
if (propDefsObj is not Dictionary<string, object?> propertyDefinitions)
{
_logger.LogWarning("Property set definition {SetName} propertyDefinitions has invalid format", setName);
continue;
}
ADB.ObjectId defId = CreatePropertySetDefinition(setName, propertyDefinitions, namePrefix, tr);
if (!defId.IsNull)
{
_propertySetDefinitionMap[setName] = defId;
}
}
tr.Commit();
}
/// <summary>
/// Try to bake property sets from a Speckle object to a Civil3D entity.
/// </summary>
public bool TryBakePropertySets(ADB.Entity entity, Base sourceObject, ADB.Transaction tr)
{
if (
sourceObject["properties"] is not Dictionary<string, object?> properties
|| !properties.TryGetValue("Property Sets", out var propertySetsObj)
|| propertySetsObj is not Dictionary<string, object?> propertySets
|| propertySets.Count == 0
)
{
return false;
}
try
{
foreach (var propertySet in propertySets)
{
string setName = propertySet.Key;
object? setDataObj = propertySet.Value;
if (setDataObj is not Dictionary<string, object?> setData)
{
_logger.LogWarning("Property set {SetName} has invalid data format", setName);
continue;
}
if (!TryBakePropertySet(entity, setName, setData, tr))
{
_logger.LogWarning("Failed to bake property set {SetName} onto entity", setName);
}
}
return true;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to bake property sets onto entity {Handle}", entity.Handle);
return false;
}
}
private bool TryBakePropertySet(
ADB.Entity entity,
string setName,
Dictionary<string, object?> setData,
ADB.Transaction tr
)
{
try
{
if (!_propertySetDefinitionMap.TryGetValue(setName, out ADB.ObjectId propertySetDefId))
{
_logger.LogWarning("Property set definition {SetName} not found in definition map", setName);
return false;
}
if (propertySetDefId.IsNull)
{
return false;
}
if (ObjectHasPropertySet(entity, propertySetDefId))
{
throw new SpeckleException($"Property set '{setName}' already exists on entity.");
}
return AddPropertySetToEntity(entity, propertySetDefId, setData, tr);
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to process property set {SetName}", setName);
return false;
}
}
private ADB.ObjectId CreatePropertySetDefinition(
string setName,
Dictionary<string, object?> propertyDefinitions,
string namePrefix,
ADB.Transaction tr
)
{
var db = _settingsStore.Current.Document.Database;
using AAECPDB.DictionaryPropertySetDefinitions propSetDefs = new(db);
string prefixedName = $"{setName}-{namePrefix}";
AAECPDB.PropertySetDefinition propSetDef = new();
propSetDef.SetToStandard(db);
propSetDef.SubSetDatabaseDefaults(db);
//propSetDef.Description = "Property Set Definition added by Speckle"; // POC: should use the description that was published. can this back in if needed
propSetDef.AppliesToAll = true;
foreach (var propertyDefinition in propertyDefinitions)
{
string propertyName = propertyDefinition.Key;
object? propertyDefObj = propertyDefinition.Value;
if (propertyDefObj is not Dictionary<string, object?> propertyDefDict)
{
continue;
}
if (
!propertyDefDict.TryGetValue(PropertySetDefinitionHandler.PROP_DEF_TYPE_KEY, out var dataTypeStr)
|| dataTypeStr is not string dataTypeString
)
{
_logger.LogError(
"Property set definition {SetName} is invalid: property {PropertyName} missing or invalid dataType",
setName,
propertyName
);
return ADB.ObjectId.Null;
}
if (!Enum.TryParse(dataTypeString, out AAEC.PropertyData.DataType dataType))
{
_logger.LogError(
"Property set definition {SetName} is invalid: unsupported data type {DataType} for property {PropertyName}",
setName,
dataTypeString,
propertyName
);
return ADB.ObjectId.Null;
}
AAECPDB.PropertyDefinition propDef = new() { DataType = dataType, Name = propertyName };
propDef.SetToStandard(db);
propDef.SubSetDatabaseDefaults(db);
if (
propertyDefDict.TryGetValue(PropertySetDefinitionHandler.PROP_DEF_DEFAULT_VALUE_KEY, out object? defaultValue)
&& defaultValue != null
)
{
try
{
// Cast numeric types to avoid bad numeric value errors
var convertedValue = dataType switch
{
AAEC.PropertyData.DataType.Integer => (int)(long)defaultValue,
AAEC.PropertyData.DataType.AutoIncrement => (int)(long)defaultValue,
_ => defaultValue
};
propDef.DefaultData = convertedValue;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(
ex,
"Failed to set default value for property {PropertyName}, continuing without default",
propertyName
);
}
}
propSetDef.Definitions.Add(propDef);
}
propSetDefs.AddNewRecord(prefixedName, propSetDef);
tr.AddNewlyCreatedDBObject(propSetDef, true);
return propSetDef.ObjectId;
}
private bool ObjectHasPropertySet(ADB.DBObject obj, ADB.ObjectId propertySetId)
{
try
{
ADB.ObjectId tempId = AAECPDB.PropertyDataServices.GetPropertySet(obj, propertySetId);
return !tempId.IsNull;
}
catch (Autodesk.AutoCAD.Runtime.Exception ex) when (!ex.IsFatal())
{
return false;
}
}
private bool AddPropertySetToEntity(
ADB.Entity entity,
ADB.ObjectId propertySetDefId,
Dictionary<string, object?> setData,
ADB.Transaction tr
)
{
try
{
if (!entity.IsWriteEnabled)
{
entity.UpgradeOpen();
}
AAECPDB.PropertyDataServices.AddPropertySet(entity, propertySetDefId);
return TrySetPropertyValues(entity, propertySetDefId, setData, tr);
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to add property set to entity");
return false;
}
}
private bool TrySetPropertyValues(
ADB.Entity entity,
ADB.ObjectId propertySetDefId,
Dictionary<string, object?> setData,
ADB.Transaction tr
)
{
try
{
ADB.ObjectId propertySetId = AAECPDB.PropertyDataServices.GetPropertySet(entity, propertySetDefId);
var propertySet = (AAECPDB.PropertySet)tr.GetObject(propertySetId, ADB.OpenMode.ForWrite);
var setDefinition = (AAECPDB.PropertySetDefinition)tr.GetObject(propertySetDefId, ADB.OpenMode.ForRead);
// Build a map of property names to definition IDs
Dictionary<string, int> propertyNameToId = new();
foreach (AAECPDB.PropertyDefinition propDef in setDefinition.Definitions)
{
propertyNameToId[propDef.Name] = propDef.Id;
}
foreach (var propertyEntry in setData)
{
string propertyName = propertyEntry.Key;
object? propertyDataObj = propertyEntry.Value;
if (propertyDataObj is not Dictionary<string, object?> propertyDataDict)
{
continue;
}
if (!propertyDataDict.TryGetValue("value", out var value) || value == null)
{
continue;
}
if (!propertyNameToId.TryGetValue(propertyName, out int propertyId))
{
continue;
}
_propertyHandler.TryGetValue(
() =>
{
propertySet.SetAt(propertyId, value);
return true;
},
out _
);
}
return true;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogWarning(ex, "Failed to update property set values");
return false;
}
}
}
@@ -0,0 +1,60 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp;
using Speckle.Connectors.Autocad.Operations.Receive;
using Speckle.Connectors.Civil3dShared.HostApp;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Converters.Common;
using Speckle.Sdk.Models;
namespace Speckle.Connectors.Civil3dShared.Operations.Receive;
/// <summary>
/// <para>Civil3D specific host object builder with property set support. Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public sealed class Civil3dHostObjectBuilder : AutocadHostObjectBaseBuilder
{
private readonly PropertySetBaker _propertySetBaker;
public Civil3dHostObjectBuilder(
IRootToHostConverter converter,
AutocadLayerBaker layerBaker,
AutocadGroupBaker groupBaker,
AutocadInstanceBaker instanceBaker,
IAutocadMaterialBaker materialBaker,
IAutocadColorBaker colorBaker,
AutocadContext autocadContext,
RootObjectUnpacker rootObjectUnpacker,
IReceiveConversionHandler conversionHandler,
PropertySetBaker propertySetBaker
)
: base(
converter,
layerBaker,
groupBaker,
instanceBaker,
materialBaker,
colorBaker,
autocadContext,
rootObjectUnpacker,
conversionHandler
)
{
_propertySetBaker = propertySetBaker;
}
protected override void PreReceiveAdditionalDeepClean(string baseLayerPrefix)
{
_propertySetBaker.PurgePropertySets(baseLayerPrefix);
}
protected override void ParseAndBakeAdditionalProxies(Base rootObject, string baseLayerPrefix)
{
_propertySetBaker.ParseAndBakePropertySetDefinitions(rootObject, baseLayerPrefix);
}
protected override void PostBakeEntity(Entity entity, Base originalObject, Transaction tr)
{
_propertySetBaker.TryBakePropertySets(entity, originalObject, tr);
}
}
@@ -11,11 +11,15 @@
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Bindings\Civil3dReceiveBinding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DependencyInjection\Civil3dConnectorModule.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\PropertySetBaker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\Civil3dHostObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Civil3dRootObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bindings\Civil3dSendBinding.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)DependencyInjection\" />
<Folder Include="$(MSBuildThisFileDirectory)HostApp\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Receive\" />
<Folder Include="$(MSBuildThisFileDirectory)Operations\Send\" />
</ItemGroup>
</Project>
@@ -11,7 +11,7 @@ namespace Speckle.Connectors.CSiShared.HostApp.Helpers;
/// Currently, all material property extraction can happen on a CsiShared level which simplifies things a lot.
/// Properties depend on the directional symmetry of the material, hence the switch statements.
/// </remarks>
public class CsiMaterialPropertyExtractor
public class CsiMaterialPropertyExtractor : IMaterialPropertyExtractor
{
/// <summary>
/// Property strings for all mechanical properties, used by numerous methods.
@@ -35,11 +35,11 @@ public class CsiMaterialPropertyExtractor
_settingsStore = settingsStore;
}
public void ExtractProperties(string materialName, Dictionary<string, object?> properties)
public void ExtractProperties(string name, Dictionary<string, object?> properties)
{
GetGeneralProperties(materialName, properties);
GetWeightAndMassProperties(materialName, properties); // TODO: Add units
GetMechanicalProperties(materialName, properties); // TODO: Add units
GetGeneralProperties(name, properties);
GetWeightAndMassProperties(name, properties);
GetMechanicalProperties(name, properties);
}
private void GetGeneralProperties(string materialName, Dictionary<string, object?> properties)
@@ -76,7 +76,7 @@ public class CsiMaterialPropertyExtractor
ref massPerUnitVolume
);
var weightAndMass = properties.EnsureNested("Weight and Mass");
var weightAndMass = properties.EnsureNested(SectionPropertyCategory.WEIGHT_AND_MASS);
weightAndMass["Weight per Unit Volume"] = weightPerUnitVolume;
weightAndMass["Mass per Unit Volume"] = massPerUnitVolume;
}
@@ -101,7 +101,7 @@ public class CsiMaterialPropertyExtractor
_ => throw new ArgumentException($"Unknown symmetry type: {materialDirectionalSymmetryKey}")
};
var mechanicalProperties = properties.EnsureNested("Mechanical Properties");
var mechanicalProperties = properties.EnsureNested(SectionPropertyCategory.MECHANICAL_DATA);
mechanicalProperties["Directional Symmetry Type"] = materialDirectionalSymmetryValue.ToString();
GetMechanicalPropertiesByType(materialName, materialDirectionalSymmetryValue, mechanicalProperties);
@@ -0,0 +1,13 @@
namespace Speckle.Connectors.CSiShared.HostApp.Helpers;
/// <summary>
/// Contract for host application specific material property extraction.
/// </summary>
/// <remarks>
/// Mirrors property extraction system pattern by composing with base extractor.
/// Enables both shared and application-specific property extraction in one call.
/// </remarks>
public interface IApplicationMaterialPropertyExtractor
{
void ExtractProperties(string name, Dictionary<string, object?> properties);
}
@@ -0,0 +1,9 @@
namespace Speckle.Connectors.CSiShared.HostApp.Helpers;
/// <summary>
/// Core contract for material property extraction common across CSi products.
/// </summary>
public interface IMaterialPropertyExtractor
{
void ExtractProperties(string name, Dictionary<string, object?> properties);
}
@@ -0,0 +1,10 @@
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Connectors.CSiShared.HostApp.Helpers;
// NOTE: Interface because Etabs and Sap2000 material unpacking and extraction is different.
// At ServiceRegistration, we inject the correct implementation of the IMaterialUnpacker
public interface IMaterialUnpacker
{
IEnumerable<IProxyCollection> UnpackMaterials();
}
@@ -1,51 +0,0 @@
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Connectors.CSiShared.HostApp;
/// <summary>
/// Creates material proxies based on stored entries from the materials cache
/// </summary>
public class MaterialUnpacker
{
private readonly CsiMaterialPropertyExtractor _propertyExtractor;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
public MaterialUnpacker(
CsiMaterialPropertyExtractor propertyExtractor,
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
)
{
_propertyExtractor = propertyExtractor;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
}
// Creates a list of material proxies from the csi materials cache
public IEnumerable<IProxyCollection> UnpackMaterials()
{
foreach (var kvp in _csiToSpeckleCacheSingleton.MaterialCache)
{
// get the cached entry
string materialName = kvp.Key;
List<string> sectionIds = kvp.Value;
// get the properties of the material
Dictionary<string, object?> properties = new(); // create empty dictionary
_propertyExtractor.ExtractProperties(materialName, properties); // dictionary mutated with respective properties
// create the material proxy
GroupProxy materialProxy =
new()
{
id = materialName,
name = materialName,
applicationId = materialName,
objects = sectionIds,
["properties"] = properties
};
yield return materialProxy;
}
}
}
@@ -34,7 +34,7 @@ public class CsiRootObjectBuilder : IRootObjectBuilder<ICsiWrapper>
private readonly IRootToSpeckleConverter _rootToSpeckleConverter;
private readonly IConverterSettingsStore<CsiConversionSettings> _converterSettings;
private readonly CsiSendCollectionManager _sendCollectionManager;
private readonly MaterialUnpacker _materialUnpacker;
private readonly IMaterialUnpacker _materialUnpacker;
private readonly ISectionUnpacker _sectionUnpacker;
private readonly ILogger<CsiRootObjectBuilder> _logger;
private readonly ISdkActivityFactory _activityFactory;
@@ -45,7 +45,7 @@ public class CsiRootObjectBuilder : IRootObjectBuilder<ICsiWrapper>
IRootToSpeckleConverter rootToSpeckleConverter,
IConverterSettingsStore<CsiConversionSettings> converterSettings,
CsiSendCollectionManager sendCollectionManager,
MaterialUnpacker materialUnpacker,
IMaterialUnpacker materialUnpacker,
ISectionUnpacker sectionUnpacker,
ILogger<CsiRootObjectBuilder> logger,
ISdkActivityFactory activityFactory,
@@ -83,8 +83,16 @@ public class CsiRootObjectBuilder : IRootObjectBuilder<ICsiWrapper>
using var activity = _activityFactory.Start("Build");
string modelFileName = _csiApplicationService.SapModel.GetModelFilename(false) ?? "Unnamed model";
(string forceUnit, string tempUnit) = GetForceAndTemperatureUnits();
Collection rootObjectCollection =
new() { name = modelFileName, ["units"] = _converterSettings.Current.SpeckleUnits };
new()
{
name = modelFileName,
["units"] = _converterSettings.Current.SpeckleUnits,
["forceUnits"] = forceUnit,
["temperatureUnits"] = tempUnit
};
List<SendConversionResult> results = new(csiObjects.Count);
int count = 0;
@@ -217,4 +225,20 @@ public class CsiRootObjectBuilder : IRootObjectBuilder<ICsiWrapper>
group => group.Key, // ModelObjectType (FRAME, JOINT, etc.)
group => group.Select(obj => obj.Name).ToList() // Extract Name from each ICsiWrapper and convert to List<string>
);
/// <summary>
/// Instantiates a Base object and pre-populates it with the models defined force units.
/// </summary>
/// <returns></returns>
/// <exception cref="SpeckleException"></exception>
private (string, string) GetForceAndTemperatureUnits()
{
var forceUnit = eForce.NotApplicable;
var lengthUnit = eLength.NotApplicable;
var temperatureUnit = eTemperature.NotApplicable;
_converterSettings.Current.SapModel.GetDatabaseUnits_2(ref forceUnit, ref lengthUnit, ref temperatureUnit);
return (forceUnit.ToString(), temperatureUnit.ToString());
}
}
@@ -47,7 +47,7 @@ public static class ServiceRegistration
services.AddScoped<CsiMaterialPropertyExtractor>();
services.AddScoped<CsiResultsExtractorFactory>();
services.AddScoped<MaterialUnpacker>();
services.AddScoped<IMaterialPropertyExtractor, CsiMaterialPropertyExtractor>();
services.AddScoped<IFrameSectionPropertyExtractor, CsiFrameSectionPropertyExtractor>();
services.AddScoped<IShellSectionPropertyExtractor, CsiShellSectionPropertyExtractor>();
services.AddScoped<AnalysisResultsExtractor>();
@@ -19,7 +19,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Bindings\CsiSharedSendBinding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Filters\CsiSharedSelectionFilter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\CsiResultsExtractorFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\MaterialUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\IApplicationMaterialPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\IMaterialPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\IMaterialUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\CsiSendCollectionManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\CsiFrameSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\CsiMaterialPropertyExtractor.cs" />
@@ -31,8 +31,8 @@ public class AnalysisResultsExtractor
Dictionary<ModelObjectType, List<string>> objectSelectionSummary
)
{
// Step 1: get analysis units
var analysisResults = CreateAnalysisResultsWithUnits();
// Step 1: create base object that will hold analysis results
var analysisResults = new Base();
// Step 2: configure and validate load cases
ConfigureAndValidateSelectedLoadCases(selectedCasesAndCombinations);
@@ -43,36 +43,6 @@ public class AnalysisResultsExtractor
return analysisResults;
}
/// <summary>
/// Instantiates a Base object and pre-populates it with the models defined force units.
/// </summary>
/// <returns></returns>
/// <exception cref="SpeckleException"></exception>
private Base CreateAnalysisResultsWithUnits()
{
var forceUnit = eForce.NotApplicable;
var lengthUnit = eLength.NotApplicable;
var temperatureUnit = eTemperature.NotApplicable;
int success = _converterSettingsStore.Current.SapModel.GetDatabaseUnits_2(
ref forceUnit,
ref lengthUnit,
ref temperatureUnit
);
if (success != 0)
{
throw new SpeckleException("Failed to retrieve units for analysis results");
}
return new Base
{
["forceUnit"] = forceUnit.ToString(),
["lengthUnit"] = lengthUnit.ToString(),
["temperatureUnit"] = temperatureUnit.ToString()
};
}
private void ExtractResults(
List<string> requestedResultTypes,
Dictionary<ModelObjectType, List<string>> objectSelectionSummary,
@@ -0,0 +1,50 @@
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Connectors.ETABSShared.HostApp;
public class EtabsMaterialUnpacker : IMaterialUnpacker
{
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly IMaterialPropertyExtractor _csiMaterialPropertyExtractor;
private readonly IApplicationMaterialPropertyExtractor _etabsMaterialPropertyExtractor;
public EtabsMaterialUnpacker(
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
IMaterialPropertyExtractor csiMaterialPropertyExtractor,
IApplicationMaterialPropertyExtractor etabsMaterialPropertyExtractor
)
{
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
_csiMaterialPropertyExtractor = csiMaterialPropertyExtractor;
_etabsMaterialPropertyExtractor = etabsMaterialPropertyExtractor;
}
public IEnumerable<IProxyCollection> UnpackMaterials()
{
foreach (var kvp in _csiToSpeckleCacheSingleton.MaterialCache)
{
string name = kvp.Key;
var sectionIds = kvp.Value;
// get the properties of the material
Dictionary<string, object?> properties = [];
_csiMaterialPropertyExtractor.ExtractProperties(name, properties);
_etabsMaterialPropertyExtractor.ExtractProperties(name, properties);
// create the material proxy
GroupProxy materialProxy =
new()
{
id = name,
name = name,
applicationId = name,
objects = sectionIds,
["properties"] = properties
};
yield return materialProxy;
}
}
}
@@ -0,0 +1,219 @@
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.Utils;
namespace Speckle.Connectors.ETABS22.HostApp.Helpers;
public class EtabsMaterialPropertyExtractor : IApplicationMaterialPropertyExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly Dictionary<int, string?> _ssTypeDict =
new()
{
{ 0, "User defined" },
{ 1, "Parametric - Simple" },
{ 2, "Parametric - Mander" }
};
private readonly Dictionary<int, string?> _ssHysTypeDict =
new()
{
{ 0, "Elastic" },
{ 1, "Kinematic" },
{ 2, "Takeda" },
{ 3, "Pivot" },
{ 4, "Concrete" },
{ 5, "BRB Hardening" },
{ 6, "Degrading" },
{ 7, "Isotropic" }
};
private const int TEMP = 0;
public EtabsMaterialPropertyExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
}
public void ExtractProperties(string name, Dictionary<string, object?> properties)
{
// we want to get some of the "other" material property data that is type specific
// csi extractor populates "type" string, but api query arguably simpler and more reliable than dict string access
int symType = 0;
eMatType matType = 0;
_settingsStore.Current.SapModel.PropMaterial.GetTypeOAPI(name, ref matType, ref symType);
// we don't have design data api queries for these, so early return to avoid creating that specific dictionary
if (matType is eMatType.NoDesign or eMatType.Aluminum or eMatType.ColdFormed or eMatType.Masonry)
{
return;
}
// ensure design data specific properties dictionary that will be mutated in switch expression
var designData = properties.EnsureNested(SectionPropertyCategory.DESIGN_DATA);
// can't do a switch expression here because not all enums have an api query (e.g. masonry, aluminium)
switch (matType)
{
case eMatType.Steel:
ExtractSteelProperties(name, designData);
break;
case eMatType.Concrete:
ExtractConcreteProperties(name, designData);
break;
case eMatType.Rebar:
ExtractRebarProperties(name, designData);
break;
case eMatType.Tendon:
ExtractTendonProperties(name, designData);
break;
}
}
private void ExtractSteelProperties(string name, Dictionary<string, object?> designData)
{
// step 1: stubs for api query
int ssType = 0,
ssHysType = 0;
double fy = 0,
fu = 0,
eFy = 0,
eFu = 0,
strainAtHardening = 0,
strainAtMaxStress = 0,
strainAtRupture = 0;
// step 2: api query
// NOTE: using the "older" method. Not sure if _1 is unsupported in etabs 21
// also, _1 doesn't give a lot more MEANINGFUL data
_settingsStore.Current.SapModel.PropMaterial.GetOSteel(
name,
ref fy,
ref fu,
ref eFy,
ref eFu,
ref ssType,
ref ssHysType,
ref strainAtHardening,
ref strainAtMaxStress,
ref strainAtRupture
);
// step 3: mutate properties dictionary
designData["Fy"] = fy;
designData["Fu"] = fu;
designData["EFy"] = eFy;
designData["EFu"] = eFu;
designData["SSType"] = _ssTypeDict.TryGetValue(ssType, out string? ssTypeValue) ? ssTypeValue : "";
designData["SSHysType"] = _ssHysTypeDict.TryGetValue(ssHysType, out string? ssHysTypeValue) ? ssHysTypeValue : "";
designData["StrainAtHardening"] = strainAtHardening;
designData["StrainAtMaxStress"] = strainAtMaxStress;
designData["StrainAtRupture"] = strainAtRupture;
designData["Temp"] = TEMP;
}
private void ExtractConcreteProperties(string name, Dictionary<string, object?> designData)
{
// step 1: stubs for api query
int ssType = 0,
ssHysType = 0;
bool isLightweight = false;
double fc = 0,
fcsFactor = 0,
strainAtFc = 0,
strainUltimate = 0,
frictionAngle = 0,
dilatationalAngle = 0;
// step 2: api query
// NOTE: using the "older" method. Not sure if _1 or _2 are unsupported in etabs 21
// also, _1 or _2 doesn't give a lot more MEANINGFUL data
_settingsStore.Current.SapModel.PropMaterial.GetOConcrete(
name,
ref fc,
ref isLightweight,
ref fcsFactor,
ref ssType,
ref ssHysType,
ref strainAtFc,
ref strainUltimate,
ref frictionAngle,
ref dilatationalAngle
);
// step 3: mutate properties dictionary
designData["Fc"] = fc;
designData["FcsFactor"] = fcsFactor;
designData["StrainAtFc"] = strainAtFc;
designData["StrainUltimate"] = strainUltimate;
designData["FrictionAngle"] = frictionAngle;
designData["DilatationalAngle"] = dilatationalAngle;
designData["IsLightweight"] = isLightweight.ToString();
designData["SSType"] = _ssTypeDict.TryGetValue(ssType, out string? ssTypeValue) ? ssTypeValue : "";
designData["SSHysType"] = _ssHysTypeDict.TryGetValue(ssHysType, out string? ssHysTypeValue) ? ssHysTypeValue : "";
designData["Temp"] = TEMP;
}
private void ExtractRebarProperties(string name, Dictionary<string, object?> designData)
{
// step 1: stubs for api query
bool useCaltransSsDefaults = false;
int ssType = 0,
ssHysType = 0;
double fy = 0,
fu = 0,
eFy = 0,
eFu = 0,
strainAtHardening = 0,
strainUltimate = 0;
// step 2: api query
// NOTE: using the "older" method. Not sure if _1 is unsupported in etabs 21
// also, _1 doesn't give a lot more MEANINGFUL data
_settingsStore.Current.SapModel.PropMaterial.GetORebar(
name,
ref fy,
ref fu,
ref eFy,
ref eFu,
ref ssType,
ref ssHysType,
ref strainAtHardening,
ref strainUltimate,
ref useCaltransSsDefaults
);
// step 3: mutate properties dictionary
designData["Fy"] = fy;
designData["Fu"] = fu;
designData["EFy"] = eFy;
designData["EFu"] = eFu;
designData["StrainAtHardening"] = strainAtHardening;
designData["StrainUltimate"] = strainUltimate;
designData["SSType"] = _ssTypeDict.TryGetValue(ssType, out string? ssTypeValue) ? ssTypeValue : "";
designData["SSHysType"] = _ssHysTypeDict.TryGetValue(ssHysType, out string? ssHysTypeValue) ? ssHysTypeValue : "";
designData["UseCaltransSsDefaults"] = useCaltransSsDefaults.ToString();
designData["Temp"] = TEMP;
}
private void ExtractTendonProperties(string name, Dictionary<string, object?> designData)
{
// step 1: stubs for api query
int ssType = 0,
ssHysType = 0;
double fy = 0,
fu = 0;
// step 2: api query
_settingsStore.Current.SapModel.PropMaterial.GetOTendon(name, ref fy, ref fu, ref ssType, ref ssHysType);
// step 3: mutate properties dictionary
designData["Fy"] = fy;
designData["Fu"] = fu;
designData["SSType"] = _ssTypeDict.TryGetValue(ssType, out string? ssTypeValue) ? ssTypeValue : "";
designData["SSHysType"] = _ssHysTypeDict.TryGetValue(ssHysType, out string? ssHysTypeValue) ? ssHysTypeValue : "";
designData["Temp"] = TEMP;
}
}
@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.Utils;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
using Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
@@ -11,54 +10,55 @@ namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
/// </summary>
public class EtabsShellSectionPropertyExtractor : IApplicationShellSectionPropertyExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly ILogger<EtabsShellSectionPropertyExtractor> _logger;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly EtabsShellSectionResolver _etabsShellSectionResolver;
public EtabsShellSectionPropertyExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
ILogger<EtabsShellSectionPropertyExtractor> logger,
EtabsShellSectionResolver etabsShellSectionResolver
EtabsShellSectionResolver etabsShellSectionResolver,
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
)
{
_settingsStore = settingsStore;
_logger = logger;
_etabsShellSectionResolver = etabsShellSectionResolver;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
}
/// <summary>
/// Extract shell section properties
/// Extract shell section properties from cache.
/// </summary>
/// <remarks>
/// sectionName is unique across all types (Wall, Slab and Deck)
/// There is no general query such as PropArea.GetShell() - rather we have to be specific on the type, for example
/// PropArea.GetWall() or PropArea.GetDeck() BUT we can't get the building type given a SectionName.
/// Hence the introduction of ResolveSection.
/// By the time this method is called during section unpacking, all sections should already be
/// resolved and cached by <see cref="EtabsShellPropertiesExtractor"/> during object conversion.
/// </remarks>
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
{
// Step 01: Finding the appropriate api query for the unknown section type (wall, deck or slab)
Dictionary<string, object?> resolvedProperties = _etabsShellSectionResolver.ResolveSection(sectionName);
var sectionProps = GetSectionProperties(sectionName);
// Step 02: Mutate properties dictionary with resolved properties
foreach (var nestedDictionary in resolvedProperties)
// shallow copy nested dictionaries into provided properties dict to mutate it (required by interface contract)
foreach (var kvp in sectionProps)
{
if (nestedDictionary.Value is not Dictionary<string, object?> nestedValues)
{
_logger.LogWarning(
"Unexpected value type for key {Key} in section {SectionName}. Expected Dictionary<string, object?>, got {ActualType}",
nestedDictionary.Key,
sectionName,
nestedDictionary.Value?.GetType().Name ?? "null"
);
continue;
}
var nestedProperties = properties.EnsureNested(nestedDictionary.Key);
foreach (var kvp in nestedValues)
{
nestedProperties[kvp.Key] = kvp.Value;
}
properties[kvp.Key] = kvp.Value;
}
}
private Dictionary<string, object?> GetSectionProperties(string sectionName)
{
// return cached properties directly
if (_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache.TryGetValue(sectionName, out var cachedProperties))
{
return cachedProperties;
}
// fallback - shouldn't happen because cached populated on the fly as sections appear in the extractor
_logger.LogWarning(
"Section {SectionName} not in cache during unpacking - resolving via API (expensive)",
sectionName
);
var resolved = _etabsShellSectionResolver.ResolveSection(sectionName);
_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache[sectionName] = resolved;
return resolved;
}
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Speckle.Connectors.CSiShared.HostApp;
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Connectors.ETABS22.HostApp.Helpers;
using Speckle.Connectors.ETABSShared.HostApp;
using Speckle.Connectors.ETABSShared.HostApp.Helpers;
using Speckle.Connectors.ETABSShared.HostApp.Services;
@@ -14,12 +15,17 @@ public static class ServiceRegistration
{
services.AddEtabsConverters();
services.AddScoped<CsiSendCollectionManager, EtabsSendCollectionManager>();
services.AddScoped<EtabsSectionPropertyDefinitionService>();
services.AddScoped<EtabsSectionPropertyExtractor>();
services.AddScoped<EtabsShellSectionResolver>();
// material services
services.AddScoped<IMaterialUnpacker, EtabsMaterialUnpacker>();
services.AddScoped<IApplicationMaterialPropertyExtractor, EtabsMaterialPropertyExtractor>();
// section services
services.AddScoped<ISectionUnpacker, EtabsSectionUnpacker>();
services.AddScoped<IApplicationFrameSectionPropertyExtractor, EtabsFrameSectionPropertyExtractor>();
services.AddScoped<IApplicationShellSectionPropertyExtractor, EtabsShellSectionPropertyExtractor>();
services.AddScoped<ISectionUnpacker, EtabsSectionUnpacker>();
services.AddScoped<EtabsSectionPropertyDefinitionService>();
services.AddScoped<EtabsSectionPropertyExtractor>();
return services;
}
@@ -9,13 +9,14 @@
<Import_RootNamespace>Speckle.Connectors.ETABSShared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)HostApp\EtabsMaterialUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\EtabsSectionUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\EtabsSendCollectionManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsFrameSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsMaterialPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyDefinitionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\EtabsPluginBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\EtabsSpeckleFormBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ServiceRegistration.cs" />
@@ -6,6 +6,7 @@ using Speckle.Connector.Navisworks.DependencyInjection;
using Speckle.Connector.Navisworks.HostApp;
using Speckle.Connector.Navisworks.Plugin.Tools;
using Speckle.Connectors.Common;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.DUI;
using Speckle.Connectors.DUI.WebView;
using Speckle.Converter.Navisworks.DependencyInjection;
@@ -46,6 +47,8 @@ internal sealed class Connector : NAV.Plugins.DockPanePlugin
Container = services.BuildServiceProvider();
Container.UseDUI();
Container.GetRequiredService<NavisworksDocumentEvents>();
Container.GetRequiredService<ISerializationOptions>().SkipCacheRead = true;
Container.GetRequiredService<ISerializationOptions>().SkipCacheWrite = true;
var u = Container.GetRequiredService<DUI3ControlWebView>();
@@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using System.Windows.Controls;
using System.Windows.Threading;
using Autodesk.Revit.UI;
@@ -31,6 +32,10 @@ public partial class CefSharpPanel : Page, Autodesk.Revit.UI.IDockablePaneProvid
DispatcherPriority.Background
);
}
catch (SEHException)
{
//do nothing as we can't control external components
}
catch (OperationCanceledException)
{
//do nothing, happens when closing Revit while a script is being executed
@@ -106,7 +106,7 @@ internal sealed class BasicConnectorBindingRevit : IBasicConnectorBinding
if (senderModelCard.SendFilter is RevitViewsFilter revitViewsFilter)
{
var view = revitViewsFilter.GetView();
var view = revitViewsFilter.GetView(activeUIDoc.Document);
if (view is not null)
{
await _revitTask
@@ -29,9 +29,11 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private readonly DocumentModelStore _store;
private readonly ICancellationManager _cancellationManager;
private readonly ISendConversionCache _sendConversionCache;
private readonly ToSpeckleSettingsManager _toSpeckleSettingsManager;
private readonly ElementUnpacker _elementUnpacker;
private readonly IRevitConversionSettingsFactory _revitConversionSettingsFactory;
private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly LinkedModelHandler _linkedModelHandler;
private readonly IThreadContext _threadContext;
@@ -55,6 +57,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
ToSpeckleSettingsManager toSpeckleSettingsManager,
ElementUnpacker elementUnpacker,
IRevitConversionSettingsFactory revitConversionSettingsFactory,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
ITopLevelExceptionHandler topLevelExceptionHandler,
LinkedModelHandler linkedModelHandler,
IThreadContext threadContext,
@@ -71,6 +74,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
_toSpeckleSettingsManager = toSpeckleSettingsManager;
_elementUnpacker = elementUnpacker;
_revitConversionSettingsFactory = revitConversionSettingsFactory;
_revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton;
_topLevelExceptionHandler = topLevelExceptionHandler;
_linkedModelHandler = linkedModelHandler;
_threadContext = threadContext;
@@ -110,6 +114,11 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
public async Task Send(string modelCardId)
{
var document = _revitContext.UIApplication?.ActiveUIDocument?.Document;
if (document == null)
{
throw new SpeckleException("No document is active for sending.");
}
using var manager = _sendOperationManagerFactory.Create();
await manager.Process<DocumentToConvert>(
@@ -120,24 +129,20 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
sp.GetRequiredService<IConverterSettingsStore<RevitConversionSettings>>()
.Initialize(
_revitConversionSettingsFactory.Create(
_toSpeckleSettingsManager.GetDetailLevelSetting(card),
_toSpeckleSettingsManager.GetReferencePointSetting(card),
_toSpeckleSettingsManager.GetSendParameterNullOrEmptyStringsSetting(card),
_toSpeckleSettingsManager.GetLinkedModelsSetting(card),
_toSpeckleSettingsManager.GetSendRebarsAsVolumetric(card)
_toSpeckleSettingsManager.GetDetailLevelSetting(document, card),
_toSpeckleSettingsManager.GetReferencePointSetting(document, card),
_toSpeckleSettingsManager.GetSendParameterNullOrEmptyStringsSetting(document, card),
_toSpeckleSettingsManager.GetLinkedModelsSetting(document, card),
_toSpeckleSettingsManager.GetSendRebarsAsVolumetric(document, card)
)
);
},
async x => await RefreshElementsIdsOnSender(x.NotNull())
async x => await RefreshElementsIdsOnSender(document, x.NotNull())
);
}
private async Task<List<DocumentToConvert>> RefreshElementsIdsOnSender(SenderModelCard modelCard)
private async Task<List<DocumentToConvert>> RefreshElementsIdsOnSender(Document document, SenderModelCard modelCard)
{
var activeUIDoc =
_revitContext.UIApplication?.ActiveUIDocument
?? throw new SpeckleException("Unable to retrieve active UI document");
if (modelCard.SendFilter.NotNull() is IRevitSendFilter viewFilter)
{
viewFilter.SetContext(_revitContext);
@@ -147,10 +152,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
() => Task.FromResult(modelCard.SendFilter.NotNull().RefreshObjectIds())
);
var allElements = selectedObjects
.Select(uid => activeUIDoc.Document.GetElement(uid))
.Where(el => el is not null)
.ToList();
var allElements = selectedObjects.Select(uid => document.GetElement(uid)).Where(el => el is not null).ToList();
// split elements between main model and linked models
var elementsOnMainModel = allElements.Where(el => el is not RevitLinkInstance).ToList();
@@ -158,14 +160,11 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
// should ideally reuse the initialized value from the scoped IConverterSettingsStore<RevitConversionSettings>.
// but, it's scoped and to avoid bigger scarier changes I'm re-fetching the setting here (inexpensive operation?)
Transform? mainModelTransform = _toSpeckleSettingsManager.GetReferencePointSetting(modelCard);
List<DocumentToConvert> documentElementContexts =
[
new(mainModelTransform, activeUIDoc.Document, elementsOnMainModel)
];
Transform? mainModelTransform = _toSpeckleSettingsManager.GetReferencePointSetting(document, modelCard);
List<DocumentToConvert> documentElementContexts = [new(mainModelTransform, document, elementsOnMainModel)];
// get the linked models setting - this decision belongs at this level
bool includeLinkedModels = _toSpeckleSettingsManager.GetLinkedModelsSetting(modelCard);
bool includeLinkedModels = _toSpeckleSettingsManager.GetLinkedModelsSetting(document, modelCard);
// ⚠️ process linked models - RevitSendBinding controls the flow based on settings!
// If setting not enabled, we won't unpack (see if-else block)
@@ -192,7 +191,12 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
if (includeLinkedModels)
{
// handler is only responsible for element collection mechanics
var linkedElements = _linkedModelHandler.GetLinkedModelElements(modelCard.SendFilter, linkedDoc, transform);
var linkedElements = _linkedModelHandler.GetLinkedModelElements(
document,
modelCard.SendFilter,
linkedDoc,
transform
);
linkedDocumentContexts.Add(new(transform, linkedDoc, linkedElements));
}
// ⚠️ when disabled, still adds empty contexts to maintain warning generation in RevitRootObjectBuilder
@@ -332,9 +336,14 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private async Task PostSetObjectIds()
{
var document = _revitContext.UIApplication?.ActiveUIDocument?.Document;
if (document == null)
{
return;
}
foreach (var sender in _store.GetSenders().ToList())
{
await RefreshElementsIdsOnSender(sender);
await RefreshElementsIdsOnSender(document, sender);
}
}
@@ -444,6 +453,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private async Task OnDocumentChanged()
{
_sendConversionCache.ClearCache();
_revitToSpeckleCacheSingleton.ClearCache();
if (_cancellationManager.NumberOfOperations > 0)
{
@@ -34,18 +34,22 @@ public class LevelUnpacker
Dictionary<string, LevelProxy> levelProxies = new();
foreach (var element in elements)
{
if (levelProxies.TryGetValue(element.LevelId.ToString(), out LevelProxy? levelProxy))
// NOTE: Use level.UniqueId (not element.LevelId) as key
// face-based instances don't have a valid element.LevelId, hence all the changes in the LevelExtractor
var level = _levelExtractor.GetLevel(element);
if (level is null)
{
continue;
}
string levelKey = level.UniqueId;
if (levelProxies.TryGetValue(levelKey, out LevelProxy? levelProxy))
{
levelProxy.objects.Add(element.UniqueId);
}
else
{
var level = _levelExtractor.GetLevel(element);
if (level is null)
{
continue;
}
var levelDataObject = new DataObject()
{
name = level.Name,
@@ -53,11 +57,11 @@ public class LevelUnpacker
properties = _propertiesExtractor.GetProperties(level)
};
var unitSettings = _converterSettings.Current.Document.GetUnits();
var lengthUnitType = unitSettings.GetFormatOptions(Autodesk.Revit.DB.SpecTypeId.Length).GetUnitTypeId();
var lengthUnitType = unitSettings.GetFormatOptions(SpecTypeId.Length).GetUnitTypeId();
levelDataObject["elevation"] = UnitUtils.ConvertFromInternalUnits(level.Elevation, lengthUnitType);
levelDataObject["units"] = _converterSettings.Current.SpeckleUnits;
levelProxies[element.LevelId.ToString()] = new LevelProxy()
levelProxies[levelKey] = new LevelProxy()
{
applicationId = level.UniqueId,
objects = [element.UniqueId],
@@ -3,7 +3,6 @@ using Autodesk.Revit.DB;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.RevitShared;
using Speckle.Connectors.RevitShared.Operations.Send.Filters;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Sdk;
using Speckle.Sdk.Common;
@@ -17,20 +16,19 @@ namespace Speckle.Connectors.Revit.HostApp;
/// </summary>
public class LinkedModelHandler
{
private readonly RevitContext _revitContext;
public Dictionary<string, string> LinkedModelDisplayNames { get; } = new();
public LinkedModelHandler(RevitContext revitContext)
{
_revitContext = revitContext;
}
/// <summary>
/// Gets elements from a linked document based on the provided send filter.
/// This method handles the specifics of element collection but doesn't make decisions
/// about whether the linked model should be processed - that's the caller's responsibility.
/// </summary>
public List<Element> GetLinkedModelElements(ISendFilter sendFilter, Document linkedDocument, Transform? transform)
public List<Element> GetLinkedModelElements(
Document currentDocument,
ISendFilter sendFilter,
Document linkedDocument,
Transform? transform
)
{
// send mode → Categories
if (sendFilter is RevitCategoriesFilter categoryFilter && categoryFilter.SelectedCategories is not null)
@@ -48,19 +46,15 @@ public class LinkedModelHandler
}
// send mode → Views (taken from the legacy code)
if (sendFilter is RevitViewsFilter viewFilter && viewFilter.GetView() != null)
if (sendFilter is RevitViewsFilter viewFilter && viewFilter.GetView(currentDocument) != null)
{
RevitLinkInstance linkInstance = FindLinkInstanceForDocument(
linkedDocument.PathName,
_revitContext.UIApplication.NotNull().ActiveUIDocument.Document,
transform
);
RevitLinkInstance linkInstance = FindLinkInstanceForDocument(linkedDocument.PathName, currentDocument, transform);
#if REVIT2024_OR_GREATER
// revit 2024 and 2025 we can use the three-parameter constructor to get only visible elements
using var viewCollector = new FilteredElementCollector(
_revitContext.UIApplication.ActiveUIDocument.Document,
viewFilter.GetView().NotNull().Id,
currentDocument,
viewFilter.GetView(currentDocument).NotNull().Id,
linkInstance.Id
);
@@ -70,7 +64,7 @@ public class LinkedModelHandler
// 🚨 LIMITATION: in Revit 2023 and below, we can only check if the entire linked model is visible,
// not individual elements within it. If the linked model is visible, all its elements will be included.
// constructor overload pertaining to searching and filtering visible elements from a revit link only added 2024.
if (linkInstance.IsHidden(viewFilter.GetView().NotNull()))
if (linkInstance.IsHidden(viewFilter.GetView(currentDocument).NotNull()))
{
return new List<Element>(); // if the linked model is hidden, return no elements
}
@@ -97,10 +97,12 @@ internal sealed class RevitDocumentStore : DocumentModelStore
try
{
var key = GetKeyForDocument(document);
if (key != null)
if (key is null)
{
_jsonCacheManager.UpdateObject(key, modelCardState);
LoadFromString(null);
return;
}
_jsonCacheManager.UpdateObject(key, modelCardState);
}
catch (Exception ex) when (!ex.IsFatal())
{
@@ -111,11 +113,17 @@ internal sealed class RevitDocumentStore : DocumentModelStore
private string? GetKeyForDocument(Document doc)
{
string? id = doc?.ProjectInformation?.UniqueId; //ProjectInformation Should only be null for family docs
#if REVIT_2024_OR_GREATER
id ??= doc.CreationGUID.ToString(); //fallback for family docs
#if REVIT2024_OR_GREATER
return doc.CreationGUID.ToString();
#else
//basically, no document state will ever be saved when it's a new document. It must be saved first for path name to be a valid value.
var x = doc.PathName;
if (string.IsNullOrEmpty(x))
{
return doc.Title;
}
return x;
#endif
return id;
}
protected override void LoadState()
@@ -128,14 +136,12 @@ internal sealed class RevitDocumentStore : DocumentModelStore
}
var key = GetKeyForDocument(document);
if (key != null)
if (key is null)
{
var state = _jsonCacheManager.GetObject(key);
if (state == null)
{
return;
}
LoadFromString(state);
LoadFromString(null);
return;
}
var state = _jsonCacheManager.GetObject(key);
LoadFromString(state);
}
}
@@ -120,9 +120,14 @@ public class RevitMaterialBaker
try
{
// all values assumed to be on the 0 - 1 scale need to pass through this validation and logging (if assumption wrong)
double roughness = ClampToUnitRange(speckleRenderMaterial.roughness, "roughness", speckleRenderMaterial.name);
double opacity = ClampToUnitRange(speckleRenderMaterial.opacity, "opacity", speckleRenderMaterial.name);
double metalness = ClampToUnitRange(speckleRenderMaterial.metalness, "metalness", speckleRenderMaterial.name);
var diffuse = System.Drawing.Color.FromArgb(speckleRenderMaterial.diffuse);
double transparency = 1 - speckleRenderMaterial.opacity;
double smoothness = 1 - speckleRenderMaterial.roughness;
double transparency = 1 - opacity;
double smoothness = 1 - roughness;
string materialId = speckleRenderMaterial.applicationId ?? speckleRenderMaterial.id.NotNull();
string matName = _revitUtils.RemoveInvalidChars($"{speckleRenderMaterial.name}-({materialId})-{baseLayerName}");
@@ -130,7 +135,7 @@ public class RevitMaterialBaker
var revitMaterial = (Material)_converterSettings.Current.Document.GetElement(newMaterialId);
revitMaterial.Color = new Color(diffuse.R, diffuse.G, diffuse.B);
revitMaterial.Transparency = (int)(transparency * 100);
revitMaterial.Shininess = (int)(speckleRenderMaterial.metalness * 128);
revitMaterial.Shininess = (int)(metalness * 128);
revitMaterial.Smoothness = (int)(smoothness * 128);
foreach (var objectId in proxy.objects)
@@ -163,4 +168,30 @@ public class RevitMaterialBaker
document.Delete(materialIds);
}
}
/// <summary>
/// After CNX-2661, we've seen some edge cases contradicting the expected 0 - 1 range for PRB properties.
/// Defensively, we'd rather clamp these values than throw.
/// </summary>
/// <remarks>
/// Created a method so that we can extend the checks to any numerical value potentially leading to a negative value,
/// which would throw an exception. Generalised method since Math.Clamp() only available since C# 8.0 and this method
/// handles logging (in the hope that we can get a better feel for these "weird" models, e.g. 0 - 100 scale??)
/// </remarks>
private double ClampToUnitRange(double value, string propertyName, string materialName)
{
if (value is < 0 or > 1)
{
_logger.LogWarning(
"Material '{MaterialName}' has an invalid {PropertyName} value of {Value} and was clamped to 0 - 1 range",
materialName,
propertyName,
value
);
value = Math.Min(Math.Max(0, value), 1);
}
return value;
}
}
@@ -110,7 +110,8 @@ public sealed class RevitHostObjectBuilder(
// TODO: TransformTo and material baking needs to be fixed in Revit!!
// create a mapping from original to modified IDs <- so that we can actually map ids in the proxies to the objects
Dictionary<string, string> originalToModifiedIds = new();
// as part of CNX-2677, we have a one-to-many problem. many instances share the same reference, so we use a list
Dictionary<string, List<string>> originalToModifiedIds = new();
// modify application IDs BEFORE material baking
foreach (LocalToGlobalMap localToGlobalMap in localToGlobalMaps)
@@ -139,7 +140,13 @@ public sealed class RevitHostObjectBuilder(
string modifiedAppId = $"{originalAppId}_{Guid.NewGuid().ToString("N")[..8]}";
if (originalAppId != null)
{
originalToModifiedIds[originalAppId] = modifiedAppId;
if (!originalToModifiedIds.TryGetValue(originalAppId, out List<string>? modifiedIds))
{
modifiedIds = new List<string>();
originalToModifiedIds[originalAppId] = modifiedIds;
}
modifiedIds.Add(modifiedAppId);
}
localToGlobalMap.AtomicObject.applicationId = modifiedAppId;
@@ -152,14 +159,20 @@ public sealed class RevitHostObjectBuilder(
{
foreach (var proxy in unpackedRoot.RenderMaterialProxies)
{
var updatedObjects = new List<string>();
var objectIdsToUse = new List<string>();
foreach (var objectId in proxy.objects)
{
// Use the modified ID if it exists, otherwise keep the original <- this SUCKS and we need to change
string idToUse = originalToModifiedIds.TryGetValue(objectId, out var modifiedId) ? modifiedId : objectId;
updatedObjects.Add(idToUse);
if (originalToModifiedIds.TryGetValue(objectId, out var modifiedIds))
{
objectIdsToUse.AddRange(modifiedIds);
}
else
{
objectIdsToUse.Add(objectId);
}
}
proxy.objects = updatedObjects;
proxy.objects = objectIdsToUse;
}
}
@@ -11,14 +11,13 @@ namespace Speckle.Connectors.RevitShared.Operations.Send.Filters;
public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilter
{
private RevitContext _revitContext;
private Document? _doc;
public string Id { get; set; } = "revitViews";
public string Name { get; set; } = "Views";
public string Type { get; set; } = "Custom";
public string? Summary { get; set; }
public bool IsDefault { get; set; }
public string? SelectedView { get; set; }
public List<string> SelectedObjectIds { get; set; }
public List<string> SelectedObjectIds { get; set; } = new();
public Dictionary<string, string>? IdMap { get; set; } = new();
public List<string>? AvailableViews { get; set; }
@@ -27,12 +26,14 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
public RevitViewsFilter(RevitContext revitContext)
{
_revitContext = revitContext;
_doc = _revitContext.UIApplication?.ActiveUIDocument?.Document;
GetViews();
var doc = _revitContext.UIApplication?.ActiveUIDocument?.Document;
if (doc is not null)
{
GetViews(doc);
}
}
public View? GetView()
public View? GetView(Document document)
{
if (SelectedView is null)
{
@@ -42,7 +43,7 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
var viewFamilyString = result[0];
var viewString = result[1];
using var collector = new FilteredElementCollector(_doc);
using var collector = new FilteredElementCollector(document);
return collector
.OfClass(typeof(View))
.Cast<View>()
@@ -56,7 +57,8 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
/// <exception cref="SpeckleSendFilterException">Whenever no view is found.</exception>
public List<string> RefreshObjectIds()
{
if (SelectedView is null)
var document = _revitContext.UIApplication?.ActiveUIDocument?.Document;
if (SelectedView is null || document is null)
{
return [];
}
@@ -66,7 +68,7 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
var viewFamilyString = result[0];
var viewString = result[1];
using var collector = new FilteredElementCollector(_doc);
using var collector = new FilteredElementCollector(document);
View? view = collector
.OfClass(typeof(View))
.Cast<View>()
@@ -78,7 +80,7 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
return [];
}
IEnumerable<Element> elementsInView = GetFilteredElementsForView(view);
IEnumerable<Element> elementsInView = GetFilteredElementsForView(document, view);
// NOTE: FilteredElementCollector() includes sweeps and reveals from a wall family's definition and includes them as additional objects
// on this return. displayValue for Wall already includes these, therefore we end up with duplicate elements on wall sweeps
@@ -94,9 +96,9 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
return objectIds;
}
private void GetViews()
private void GetViews(Document document)
{
using var collector = new FilteredElementCollector(_doc);
using var collector = new FilteredElementCollector(document);
var views = collector
.OfClass(typeof(View))
.Cast<View>()
@@ -125,7 +127,6 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
public void SetContext(RevitContext revitContext)
{
_revitContext = revitContext;
_doc = _revitContext.UIApplication?.ActiveUIDocument.Document;
}
// NOTE: Element collector returns parts and source elements even when Parts Visibility is set as "Show Parts" only.
@@ -161,9 +162,9 @@ public class RevitViewsFilter : DiscriminatedObject, ISendFilter, IRevitSendFilt
return elementsToExclude;
}
private IEnumerable<Element> GetFilteredElementsForView(View view)
private IEnumerable<Element> GetFilteredElementsForView(Document document, View view)
{
using var viewCollector = new FilteredElementCollector(_doc, view.Id);
using var viewCollector = new FilteredElementCollector(document, view.Id);
var allElements = viewCollector.ToElements();
// parts filtering when view is set to show Parts only (and overwrites allElements)
@@ -183,6 +183,7 @@ public class RevitRootObjectBuilder(
// non-transformed elements can safely rely on cache
// TODO: Potential here to transform cached objects and NOT reconvert,
// TODO: we wont do !hasTransform here, and re-set application id before this
if (!hasTransform && sendConversionCache.TryGetValue(projectId, applicationId, out ObjectReference? value))
{
converted = value;
@@ -248,6 +249,17 @@ public class RevitRootObjectBuilder(
var levelProxies = levelUnpacker.Unpack(flatElements);
rootObject[ProxyKeys.LEVEL] = levelProxies;
rootObject[ProxyKeys.INSTANCE_DEFINITION] = revitToSpeckleCacheSingleton.GetInstanceDefinitionProxiesForObjects(
idsAndSubElementIds
);
rootObject.elements.Add(
new Collection()
{
elements = revitToSpeckleCacheSingleton.GetBaseObjectsForObjects(idsAndSubElementIds),
name = "revitInstancedObjects"
}
);
// NOTE: these are currently not used anywhere, we'll skip them until someone calls for it back
// rootObject[ProxyKeys.PARAMETER_DEFINITIONS] = _parameterDefinitionHandler.Definitions;
@@ -1,25 +1,21 @@
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Caching;
using Speckle.Connectors.DUI.Models.Card;
using Speckle.Connectors.Revit.HostApp;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Settings;
using Speckle.InterfaceGenerator;
using Speckle.Sdk;
using Speckle.Sdk.Common;
namespace Speckle.Connectors.Revit.Operations.Send.Settings;
[GenerateAutoInterface]
public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
public class ToSpeckleSettingsManager(
ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
ILogger<ToSpeckleSettingsManager> logger
) : IToSpeckleSettingsManager
{
private readonly RevitContext _revitContext;
private readonly ISendConversionCache _sendConversionCache;
private readonly ElementUnpacker _elementUnpacker;
private readonly ILogger<ToSpeckleSettingsManager> _logger;
// cache invalidation process run with ModelCardId since the settings are model specific
private readonly Dictionary<string, DetailLevelType> _detailLevelCache = [];
private readonly Dictionary<string, Transform?> _referencePointCache = [];
@@ -27,20 +23,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
private readonly Dictionary<string, bool?> _sendLinkedModelsCache = [];
private readonly Dictionary<string, bool?> _sendRebarsAsVolumetricCache = [];
public ToSpeckleSettingsManager(
RevitContext revitContext,
ISendConversionCache sendConversionCache,
ElementUnpacker elementUnpacker,
ILogger<ToSpeckleSettingsManager> logger
)
{
_revitContext = revitContext;
_elementUnpacker = elementUnpacker;
_sendConversionCache = sendConversionCache;
_logger = logger;
}
public DetailLevelType GetDetailLevelSetting(SenderModelCard modelCard)
public DetailLevelType GetDetailLevelSetting(Document document, SenderModelCard modelCard)
{
var fidelityString =
modelCard.Settings?.FirstOrDefault(s => s.Id == DetailLevelSetting.SETTING_ID)?.Value as string;
@@ -53,7 +36,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
{
if (previousType != fidelity)
{
EvictCacheForModelCard(modelCard);
EvictCacheForModelCard(document, modelCard);
}
}
_detailLevelCache[modelCard.ModelCardId.NotNull()] = fidelity;
@@ -61,7 +44,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
}
// log the issue
_logger.LogWarning(
logger.LogWarning(
"Invalid detail level setting received: '{FidelityString}' for model {ModelCardId}, using default: {DefaultValue}",
fidelityString,
modelCard.ModelCardId,
@@ -74,7 +57,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
return defaultValue;
}
public Transform? GetReferencePointSetting(ModelCard modelCard)
public Transform? GetReferencePointSetting(Document document, ModelCard modelCard)
{
var referencePointString =
modelCard.Settings?.FirstOrDefault(s => s.Id == SendReferencePointSetting.SETTING_ID)?.Value as string;
@@ -88,14 +71,14 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
{
// get the current transform from setting first
// we are doing this because we can't track if reference points were changed between send operations.
Transform? currentTransform = GetTransform(referencePoint);
Transform? currentTransform = GetTransform(document, referencePoint);
if (_referencePointCache.TryGetValue(modelCard.ModelCardId.NotNull(), out Transform? previousTransform))
{
// invalidate conversion cache if the transform has changed
if (modelCard is SenderModelCard senderModelCard && previousTransform != currentTransform)
{
EvictCacheForModelCard(senderModelCard);
EvictCacheForModelCard(document, senderModelCard);
}
}
@@ -104,7 +87,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
}
// log the issue
_logger.LogWarning(
logger.LogWarning(
"Invalid reference point setting received: '{ReferencePointString}' for model {ModelCardId}, using default: {DefaultValue}",
referencePointString,
modelCard.ModelCardId,
@@ -116,8 +99,9 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
return null;
}
public bool GetSendParameterNullOrEmptyStringsSetting(SenderModelCard modelCard) =>
public bool GetSendParameterNullOrEmptyStringsSetting(Document document, SenderModelCard modelCard) =>
GetBooleanSettingWithCache(
document,
SendParameterNullOrEmptyStringsSetting.SETTING_ID,
SendParameterNullOrEmptyStringsSetting.DEFAULT_VALUE,
modelCard,
@@ -127,8 +111,9 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
// NOTE: Cache invalidation currently a placeholder until we have more understanding on the sends
// TODO: Evaluate cache invalidation for GetLinkedModelsSetting
public bool GetLinkedModelsSetting(SenderModelCard modelCard) =>
public bool GetLinkedModelsSetting(Document document, SenderModelCard modelCard) =>
GetBooleanSettingWithCache(
document,
LinkedModelsSetting.SETTING_ID,
LinkedModelsSetting.DEFAULT_VALUE,
modelCard,
@@ -136,8 +121,9 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
"Linked models"
);
public bool GetSendRebarsAsVolumetric(SenderModelCard modelCard) =>
public bool GetSendRebarsAsVolumetric(Document document, SenderModelCard modelCard) =>
GetBooleanSettingWithCache(
document,
SendRebarsAsVolumetricSetting.SETTING_ID,
SendRebarsAsVolumetricSetting.DEFAULT_VALUE,
modelCard,
@@ -149,6 +135,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
/// Helper method to handle boolean settings with caching and logging
/// </summary>
private bool GetBooleanSettingWithCache(
Document document,
string settingId,
bool defaultValue,
SenderModelCard modelCard,
@@ -163,7 +150,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
{
if (previousValue != returnValue)
{
EvictCacheForModelCard(modelCard);
EvictCacheForModelCard(document, modelCard);
}
}
@@ -173,7 +160,7 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
// respected (linked models disabled but still sent linked models), I think we should note this occurence so we know
if (settingValue == null)
{
_logger.LogWarning(
logger.LogWarning(
"{SettingName} setting was null for model {ModelCardId}, using default: {DefaultValue}",
settingName,
modelCard.ModelCardId,
@@ -184,71 +171,59 @@ public class ToSpeckleSettingsManager : IToSpeckleSettingsManager
return returnValue;
}
private void EvictCacheForModelCard(SenderModelCard modelCard)
private void EvictCacheForModelCard(Document document, SenderModelCard modelCard)
{
var doc = _revitContext.UIApplication?.ActiveUIDocument?.Document;
if (doc == null)
{
throw new SpeckleException("Unable to retrieve active UI document");
}
var objectIds = modelCard.SendFilter != null ? modelCard.SendFilter.NotNull().SelectedObjectIds : [];
var unpackedObjectIds = _elementUnpacker.GetUnpackedElementIds(objectIds, doc);
_sendConversionCache.EvictObjects(unpackedObjectIds);
var objectIds = modelCard.SendFilter?.SelectedObjectIds ?? [];
var unpackedObjectIds = elementUnpacker.GetUnpackedElementIds(objectIds, document);
sendConversionCache.EvictObjects(unpackedObjectIds);
}
private Transform? GetTransform(ReferencePointType referencePointType)
private Transform? GetTransform(Document document, ReferencePointType referencePointType)
{
Transform? referencePointTransform = null;
if (_revitContext.UIApplication is UIApplication uiApplication)
// first get the main doc base points and reference setting transform
using FilteredElementCollector filteredElementCollector = new(document);
var points = filteredElementCollector.OfClass(typeof(BasePoint)).Cast<BasePoint>().ToList();
BasePoint? projectPoint = points.FirstOrDefault(o => !o.IsShared);
BasePoint? surveyPoint = points.FirstOrDefault(o => o.IsShared);
switch (referencePointType)
{
// first get the main doc base points and reference setting transform
using FilteredElementCollector filteredElementCollector = new(uiApplication.ActiveUIDocument.Document);
var points = filteredElementCollector.OfClass(typeof(BasePoint)).Cast<BasePoint>().ToList();
BasePoint? projectPoint = points.FirstOrDefault(o => !o.IsShared);
BasePoint? surveyPoint = points.FirstOrDefault(o => o.IsShared);
// note that the project base (ui) rotation is registered on the survey pt, not on the base point
case ReferencePointType.ProjectBase:
if (projectPoint is not null)
{
referencePointTransform = Transform.CreateTranslation(projectPoint.Position);
}
else
{
throw new InvalidOperationException("Couldn't retrieve Project Point from document");
}
break;
switch (referencePointType)
{
// note that the project base (ui) rotation is registered on the survey pt, not on the base point
case ReferencePointType.ProjectBase:
if (projectPoint is not null)
{
referencePointTransform = Transform.CreateTranslation(projectPoint.Position);
}
else
{
throw new InvalidOperationException("Couldn't retrieve Project Point from document");
}
break;
// note that the project base (ui) rotation is registered on the survey pt, not on the base point
case ReferencePointType.Survey:
if (surveyPoint is not null && projectPoint is not null)
{
// POC: should a null angle resolve to 0?
// retrieve the survey point rotation from the project point
var angle = projectPoint.get_Parameter(BuiltInParameter.BASEPOINT_ANGLETON_PARAM)?.AsDouble() ?? 0;
// note that the project base (ui) rotation is registered on the survey pt, not on the base point
case ReferencePointType.Survey:
if (surveyPoint is not null && projectPoint is not null)
{
// POC: should a null angle resolve to 0?
// retrieve the survey point rotation from the project point
var angle = projectPoint.get_Parameter(BuiltInParameter.BASEPOINT_ANGLETON_PARAM)?.AsDouble() ?? 0;
// POC: following disposed incorrectly or early or maybe a false negative?
using Transform translation = Transform.CreateTranslation(surveyPoint.Position);
referencePointTransform = translation.Multiply(Transform.CreateRotation(XYZ.BasisZ, angle));
}
else
{
throw new InvalidOperationException("Couldn't retrieve Survey and Project Point from document");
}
break;
// POC: following disposed incorrectly or early or maybe a false negative?
using Transform translation = Transform.CreateTranslation(surveyPoint.Position);
referencePointTransform = translation.Multiply(Transform.CreateRotation(XYZ.BasisZ, angle));
}
else
{
throw new InvalidOperationException("Couldn't retrieve Survey and Project Point from document");
}
break;
case ReferencePointType.InternalOrigin:
break;
}
return referencePointTransform;
case ReferencePointType.InternalOrigin:
break;
}
throw new InvalidOperationException(
"Revit Context UI Application was null when retrieving reference point transform."
);
return referencePointTransform;
}
}
@@ -64,6 +64,13 @@ public class CreateCollection : VariableParameterComponentBase
}
}
// validate for duplicate application IDs across the entire collection hierarchy
if (HasDuplicateApplicationIds(rootCollection))
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "The same object(s) cannot appear in multiple collections");
return; // error already added in validation method
}
dataAccess.SetData(0, new SpeckleCollectionWrapperGoo(rootCollection));
}
@@ -182,6 +189,54 @@ public class CreateCollection : VariableParameterComponentBase
}
}
/// <summary>
/// Validates that all application IDs are unique across the entire collection hierarchy.
/// Shows an error if duplicates are found, indicating objects appear in multiple collections.
/// </summary>
/// <returns>True if duplicates exist, false if all IDs are unique</returns>
private bool HasDuplicateApplicationIds(SpeckleCollectionWrapper rootCollection)
{
// args to CheckForDuplicateApplicationIds passed in since the method can recursively check
var seenIds = new HashSet<string>();
var duplicateIds = new HashSet<string>();
// iterate, create hash set and check all application IDs
ProcessAndCheckForDuplicateApplicationIds(rootCollection, seenIds, duplicateIds);
return duplicateIds.Count > 0;
}
/// <summary>
/// Recursively collects application IDs from all in the collection hierarchy.
/// </summary>
/// <remarks>
/// Only checks the wrapper's ApplicationId, not for example geometries within DataObjects.
/// </remarks>
private void ProcessAndCheckForDuplicateApplicationIds(
SpeckleCollectionWrapper collection,
HashSet<string> seenIds,
HashSet<string> duplicateIds
)
{
foreach (var element in collection.Elements)
{
switch (element)
{
case SpeckleCollectionWrapper childCollection:
// recurse into child collections
ProcessAndCheckForDuplicateApplicationIds(childCollection, seenIds, duplicateIds);
break;
case SpeckleWrapper wrapper:
if (wrapper.ApplicationId != null && !seenIds.Add(wrapper.ApplicationId))
{
duplicateIds.Add(wrapper.ApplicationId);
}
break;
}
}
}
// IGH_VariableParameterComponent implementation
public override bool CanInsertParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Input;
@@ -34,8 +34,7 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
pManager.AddParameter(param);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
protected override void RegisterOutputParams(GH_OutputParamManager pManager) =>
pManager.AddParameter(
new SpecklePropertyGroupParam(),
"Properties",
@@ -43,7 +42,6 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
"Properties for Speckle Objects",
GH_ParamAccess.item
);
}
protected override void SolveInstance(IGH_DataAccess da)
{
@@ -67,8 +65,7 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
{
var paramName = Params.Input[i].NickName;
var data = Params.Input[i].VolatileData.AllData(true).ToList();
if (data.Count == 0)
if (Params.Input[i].VolatileDataCount == 0)
{
continue;
}
@@ -165,13 +162,12 @@ public class CreateSpeckleProperties : VariableParameterComponentBase
Params.RegisterInputParam(param);
}
Params.OnParametersChanged();
ExpireSolution(true);
}
protected override void WriteComponentSpecificData(GH_IWriter writer)
{
protected override void WriteComponentSpecificData(GH_IWriter writer) =>
writer.SetBoolean("CreateEmptyProperties", CreateEmptyProperties);
}
protected override void ReadComponentSpecificData(GH_IReader reader)
{
@@ -89,7 +89,7 @@ public class QuerySpeckleObjects : GH_Component, IGH_VariableParameterComponent
private List<int>? _outputFilterIndices;
// Caches the list of all objects by geometrybase type
private readonly Dictionary<ObjectType, List<SpeckleGeometryWrapper>> _filterDict = new();
private readonly Dictionary<ObjectType, List<SpeckleGeometryWrapper>> _filterDict = [];
protected override void SolveInstance(IGH_DataAccess dataAccess)
{
@@ -104,6 +104,9 @@ public class QuerySpeckleObjects : GH_Component, IGH_VariableParameterComponent
string path = "";
dataAccess.GetData(1, ref path);
// ensure fresh data for type-specific outputs
_filterDict.Clear();
// filter by collection path
// Note: the collection paths selector will omit the target collection from the path of nested collections.
// the discard ("_objects") will be used to indicate objects found directly in the target collection.
@@ -127,10 +130,7 @@ public class QuerySpeckleObjects : GH_Component, IGH_VariableParameterComponent
// sort geometry objects by filters
var geometryObjects = filteredObjects.OfType<SpeckleGeometryWrapper>().ToList();
if (_filterDict.Count == 0)
{
SortObjectsByGeometryBaseType(geometryObjects);
}
SortObjectsByGeometryBaseType(geometryObjects);
// Set output objects
for (int i = 0; i < Params.Output.Count; i++)
@@ -173,13 +173,13 @@ public class QuerySpeckleObjects : GH_Component, IGH_VariableParameterComponent
{
if (_filterDict.Count > 0)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Stored input objects are in an invalid state.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Stored input objects are in an invalid state");
return;
}
foreach (ObjectType filter in Filters)
{
_filterDict.Add(filter, new());
_filterDict.Add(filter, []);
}
foreach (var wrapper in objs)
@@ -233,7 +233,7 @@ public class QuerySpeckleObjects : GH_Component, IGH_VariableParameterComponent
// repopulate current output params if needed
if (_outputFilterIndices is null)
{
_outputFilterIndices = new();
_outputFilterIndices = [];
foreach (var output in Params.Output)
{
if (Enum.TryParse(output.Name, out ObjectType filter))
@@ -289,8 +289,5 @@ public class QuerySpeckleObjects : GH_Component, IGH_VariableParameterComponent
base.RemovedFromDocument(document);
}
private void OnParameterSourceChanged(object sender, GH_ParamServerEventArgs args) =>
// an empty filter dict will trigger the SortObjectsByGeometryBaseType method.
// we only want to re-sort objects if an input has changed, not on every trigger of solve instance.
_filterDict.Clear();
private void OnParameterSourceChanged(object sender, GH_ParamServerEventArgs args) { }
}
@@ -103,7 +103,7 @@ public class SpeckleDataObjectPassthrough()
List<SpeckleGeometryWrapperGoo> inputGeometry = new();
if (!da.GetDataList(1, inputGeometry) && result == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Pass in a Speckle DataObject or Geometries.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle DataObject or Geometries");
return;
}
@@ -111,7 +111,7 @@ public class SpeckleDataObjectPassthrough()
{
if (inputGeo.Value is SpeckleBlockInstanceWrapper)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"DataObjects cannot contain Block Instances.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "DataObjects cannot contain Block Instances");
return;
}
}
@@ -158,6 +158,10 @@ public class SpeckleDataObjectPassthrough()
result.Properties = inputProperties;
}
// generate application ID for new data objects. Unlike SpeckleGeometry, DataObject wrappers aren't created
// through casting (which auto-generates IDs), so we must explicitly ensure an ID exists here
result.ApplicationId ??= Guid.NewGuid().ToString();
// get the path
string? path =
result.Path.Count > 1 ? string.Join(Constants.LAYER_PATH_DELIMITER, result.Path) : result.Path.FirstOrDefault();
@@ -138,7 +138,7 @@ public class SpeckleGeometryPassthrough()
if (result == null && inputGeometry == null)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Pass in a Speckle Geometry or Geometry.");
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Pass in a Speckle Geometry or Geometry");
return;
}
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using GH_IO.Serialization;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
@@ -297,6 +298,32 @@ public class ReceiveAsyncComponent : GH_AsyncComponent<ReceiveAsyncComponent>
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, e.ToFormattedString());
}
}
public override bool Write(GH_IWriter writer)
{
// call base implementation first
var result = base.Write(writer);
// persist AutoReceive setting
writer.SetBoolean("AutoReceive", AutoReceive);
return result;
}
public override bool Read(GH_IReader reader)
{
// call base implementation first
var result = base.Read(reader);
// restore AutoReceive setting
bool autoReceive = false;
if (reader.TryGetBoolean("AutoReceive", ref autoReceive))
{
AutoReceive = autoReceive;
}
return result;
}
}
public sealed class ReceiveComponentWorker : WorkerInstance<ReceiveAsyncComponent>
@@ -1,5 +1,6 @@
using Grasshopper.Kernel;
using Microsoft.Extensions.DependencyInjection;
using Rhino;
using Speckle.Connectors.Common;
using Speckle.Connectors.Common.Analytics;
using Speckle.Connectors.Common.Operations;
@@ -12,6 +13,7 @@ using Speckle.Connectors.GrasshopperShared.Properties;
using Speckle.Connectors.GrasshopperShared.Registration;
using Speckle.Sdk;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Credentials;
using Speckle.Sdk.Models.Collections;
@@ -31,11 +33,20 @@ public class ReceiveComponentInput
public class ReceiveComponentOutput
{
public SpeckleCollectionWrapperGoo RootObject { get; set; }
/// <remarks>
/// Made nullable as output can be null when Run = false or on error
/// </remarks>
public SpeckleCollectionWrapperGoo? RootObject { get; set; }
}
public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInput, ReceiveComponentOutput>
{
private IClient? _apiClient;
private string? _lastVersionId;
private SpeckleUrlModelResource? _lastResource;
public override Guid ComponentGuid => new("74954F59-B1B7-41FD-97DE-4C6B005F2801");
protected override Bitmap Icon => Resources.speckle_operations_syncload;
public ReceiveComponent()
: base(
"(Sync) Load",
@@ -45,9 +56,6 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
ComponentCategories.DEVELOPER
) { }
public override Guid ComponentGuid => new("74954F59-B1B7-41FD-97DE-4C6B005F2801");
protected override Bitmap Icon => Resources.speckle_operations_syncload;
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddParameter(new SpeckleUrlModelResourceParam(GH_ParamAccess.item));
@@ -77,19 +85,28 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
bool run = false;
da.GetData(1, ref run);
return new(url, run);
if (run)
{
SetupSubscription(url);
}
else
{
CleanupSubscription();
}
return new ReceiveComponentInput(url, run);
}
protected override void SetOutput(IGH_DataAccess da, ReceiveComponentOutput result)
{
if (result.RootObject is null)
{
Message = "Not Loaded";
Message = _apiClient != null ? "Monitoring" : "Not Loaded";
}
else
{
da.SetData(0, result.RootObject);
Message = "Done";
Message = _apiClient != null ? "Loaded" : "Done";
}
}
@@ -107,11 +124,11 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
GH_RuntimeMessageLevel.Error,
"Only one model can be loaded at a time. To load to multiple models, please use different load components."
);
return new();
return new ReceiveComponentOutput();
}
if (!input.Run)
{
return new();
return new ReceiveComponentOutput();
}
using var scope = PriorityLoader.CreateScopeForActiveDocument();
@@ -129,6 +146,9 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
using var client = clientFactory.Create(account);
var receiveInfo = await input.Resource.GetReceiveInfo(client, cancellationToken).ConfigureAwait(false);
// store version id for tracking
_lastVersionId = receiveInfo.SelectedVersionId;
var progress = new Progress<CardProgress>(_ =>
{
// TODO: Progress only makes sense in non-blocking async receive, which is not supported yet.
@@ -195,4 +215,116 @@ public class ReceiveComponent : SpeckleTaskCapableComponent<ReceiveComponentInpu
var goo = new SpeckleCollectionWrapperGoo(collectionRebuilder.RootCollectionWrapper);
return new ReceiveComponentOutput { RootObject = goo };
}
private void SetupSubscription(SpeckleUrlModelResource resource)
{
// skip if already subscribed to this resource
if (_apiClient != null && _lastResource != null && _lastResource.Equals(resource))
{
return;
}
// only subscribe for Model URLs (not specific versions)
if (resource is SpeckleUrlModelVersionResource)
{
CleanupSubscription();
_lastResource = resource;
return;
}
try
{
CleanupSubscription(); // clean up old subscription first
using var scope = PriorityLoader.CreateScopeForActiveDocument();
var account = resource.Account.GetAccount(scope);
if (account == null)
{
return;
}
_apiClient = scope.Get<IClientFactory>().Create(account);
_apiClient.Subscription.CreateProjectVersionsUpdatedSubscription(resource.ProjectId).Listeners +=
OnVersionCreated;
_lastResource = resource;
Message = "Monitoring";
}
catch (Exception ex) when (!ex.IsFatal())
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, $"Could not setup monitoring: {ex.Message}");
}
}
private void OnVersionCreated(object? sender, ProjectVersionsUpdatedMessage e) =>
// new version detected - trigger reload
RhinoApp.InvokeOnUiThread(
(Action)
delegate
{
ExpireSolution(true);
}
);
private void CleanupSubscription()
{
if (_apiClient != null && _lastResource != null)
{
try
{
_apiClient.Subscription.CreateProjectVersionsUpdatedSubscription(_lastResource.ProjectId).Listeners -=
OnVersionCreated;
}
catch (Exception ex) when (!ex.IsFatal())
{
// ignore cleanup errors
}
_apiClient.Dispose();
_apiClient = null;
}
}
// Cleanup on removal
public override void RemovedFromDocument(GH_Document document)
{
CleanupSubscription();
base.RemovedFromDocument(document);
}
// Handle document context changes
public override void DocumentContextChanged(GH_Document document, GH_DocumentContext context)
{
if (context == GH_DocumentContext.Unloaded)
{
CleanupSubscription();
}
else if (context == GH_DocumentContext.Loaded && _lastResource != null && _apiClient != null)
{
// Check for version changes when document reopens
Task.Run(async () =>
{
try
{
var receiveInfo = await _lastResource.GetReceiveInfo(_apiClient);
if (receiveInfo.SelectedVersionId != _lastVersionId)
{
RhinoApp.InvokeOnUiThread(
(Action)
delegate
{
ExpireSolution(true);
}
);
}
}
catch (Exception ex) when (!ex.IsFatal())
{
// ignore errors during background check
}
});
}
base.DocumentContextChanged(document, context);
}
}
@@ -87,6 +87,7 @@ public class SpeckleSelectModelComponent : GH_Component
string? urlInput = null;
// SCENARIO 1: Component has input wire connected
if (da.GetData(0, ref urlInput))
{
UrlInput = urlInput;
@@ -99,6 +100,11 @@ public class SpeckleSelectModelComponent : GH_Component
return;
}
if (_justPastedIn)
{
RestoreAccountFromStoredState();
}
try
{
// NOTE: once we split the logic in Sender and Receiver components, we need to set flag correctly
@@ -132,22 +138,9 @@ public class SpeckleSelectModelComponent : GH_Component
_storedUserId = SpeckleOperationWizard.SelectedAccount?.id;
}
if (_justPastedIn && _storedUserId != null && !string.IsNullOrEmpty(_storedUserId))
if (_justPastedIn)
{
try
{
SpeckleOperationWizard.SetAccountFromId(_storedUserId);
}
catch (SpeckleAccountManagerException e)
{
// Swallow and move onto checking server.
Console.WriteLine(e);
}
if (_storedServer != null && SpeckleOperationWizard.SelectedAccount == null)
{
SpeckleOperationWizard.SetAccountFromIdAndUrl(_storedUserId, _storedServer);
}
RestoreAccountFromStoredState();
}
// Validate backing data
@@ -396,4 +389,39 @@ public class SpeckleSelectModelComponent : GH_Component
VersionContextMenuButton.ExpirePreview(redraw);
base.ExpirePreview(redraw);
}
/// <summary>
/// Restores the account from stored state when the component is pasted or loaded from file.
/// </summary>
/// <remarks>
/// Attempts to restore account in two stages:
/// <list type="number">
/// <item>First tries to get account by stored user ID</item>
/// <item>If that fails and server url is available, falls back to getting any account matching the server</item>
/// </list>
/// Only executes when <see cref="_justPastedIn"/> is true and <see cref="_storedUserId"/> is not empty.
/// </remarks>
private void RestoreAccountFromStoredState()
{
if (_storedUserId is null || string.IsNullOrEmpty(_storedUserId))
{
return;
}
try
{
SpeckleOperationWizard.SetAccountFromId(_storedUserId);
}
catch (SpeckleAccountManagerException e)
{
Console.WriteLine(e);
}
// Fallback: if account wasn't found by ID but we have a server URL,
// try to find any account matching that server
if (_storedServer != null && SpeckleOperationWizard.SelectedAccount == null)
{
SpeckleOperationWizard.SetAccountFromIdAndUrl(_storedUserId, _storedServer);
}
}
}
@@ -79,22 +79,40 @@ public class SpeckleOperationWizard
var resources = SpeckleResourceBuilder.FromUrlString(input, token);
if (resources.Length == 0)
{
throw new SpeckleException($"Input url string was empty");
throw new SpeckleException("Input url string was empty");
}
if (resources.Length > 1)
{
throw new SpeckleException($"Input multi-model url is not supported");
throw new SpeckleException("Input multi-model url is not supported");
}
var resource = resources.First();
using var scope = PriorityLoader.CreateScopeForActiveDocument();
var account = resource.Account.GetAccount(scope);
SetAccount(account, false);
var urlDerivedAccount = resource.Account.GetAccount(scope);
// if no account is selected, happily go through the url derived account approach
if (SelectedAccount == null)
{
throw new SpeckleException("No account found for server URL");
SetAccount(urlDerivedAccount, false);
}
// if we have an account from right-click context-menu, we rely on that and just validate that it's actually applicable to that server
else if (urlDerivedAccount != null && SelectedAccount.serverInfo.url != urlDerivedAccount.serverInfo.url)
{
throw new SpeckleException(
$"Selected account is for '{SelectedAccount.serverInfo.url}' "
+ $"but URL requires '{urlDerivedAccount.serverInfo.url}'"
);
}
// we have both scenarios covered
// Scenario #1 - default account from url
// Scenario #2 - triggered by account switch on right-click context (and validated)
if (SelectedAccount == null)
{
throw new SpeckleException(
$"No appropriate account found for the given '{urlDerivedAccount?.serverInfo.url}' server"
);
}
IClient client = _clientFactory.Create(SelectedAccount);
@@ -11,6 +11,7 @@ namespace Speckle.Connectors.GrasshopperShared.Parameters;
/// </summary>
public class SpeckleOutputParam : Param_GenericObject
{
public override GH_Exposure Exposure => GH_Exposure.hidden;
public override Guid ComponentGuid => new("D2B4713D-FE8B-4EF0-8445-B6096DB15B24");
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
@@ -52,6 +52,7 @@ public class SpeckleVariableParam : Param_GenericObject
}
}
public override GH_Exposure Exposure => GH_Exposure.hidden;
public override Guid ComponentGuid => new("A1B2C3D4-E5F6-7890-ABCD-123456789ABC");
public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
@@ -4,6 +4,7 @@ using Rhino;
using Rhino.Display;
using Rhino.DocObjects;
using Rhino.Geometry;
using Speckle.Connectors.GrasshopperShared.HostApp;
using Speckle.Sdk.Models;
namespace Speckle.Connectors.GrasshopperShared.Parameters;
@@ -27,7 +28,25 @@ public class SpeckleGeometryWrapper : SpeckleWrapper, ISpeckleCollectionObject
// The list of layer/collection names that forms the full path to this object
public List<string> Path { get; set; } = new();
public SpeckleCollectionWrapper? Parent { get; set; }
public SpecklePropertyGroupGoo Properties { get; set; } = new();
/// <summary>
/// Properties that stay synchronized with Base["properties"]
/// </summary>
public SpecklePropertyGroupGoo Properties
{
get
{
// always create from Base["properties"] like SpeckleDataObjectWrapper does
if (Base[Constants.PROPERTIES_PROP] is Dictionary<string, object?> baseProps)
{
return new SpecklePropertyGroupGoo(baseProps);
}
return new SpecklePropertyGroupGoo(); // return empty if no properties
}
set =>
// unwrap and store in Base["properties"] like SpeckleDataObjectWrapper does
Base[Constants.PROPERTIES_PROP] = value.Unwrap();
}
/// <summary>
/// The color of the <see cref="Base"/>
@@ -83,7 +83,7 @@ public sealed class RhinoSendBinding : ISendBinding
_sendOperationManagerFactory = sendOperationManagerFactory;
_rhinoLayerHelper = rhinoLayerHelper;
Commands = new SendBindingUICommands(parent); // POC: Commands are tightly coupled with their bindings, at least for now, saves us injecting a factory.
PreviousUnitSystem = RhinoDoc.ActiveDoc.ModelUnitSystem;
PreviousUnitSystem = RhinoDoc.ActiveDoc?.ModelUnitSystem ?? UnitSystem.None;
SubscribeToRhinoEvents();
}
@@ -104,10 +104,10 @@ public class RhinoInstanceBaker : IInstanceBaker<IReadOnlyCollection<string>>
attributes
);
// POC: check on defIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
// POC: check on defIndex -1, means we haven't created anything - this is most likely an unrecoverable error at this stage
if (defIndex == -1)
{
throw new ConversionException("Failed to create an instance defintion object.");
throw new ConversionException("Failed to create an instance definition object.");
}
if (definitionProxy.applicationId != null)
@@ -170,9 +170,13 @@ public class RhinoInstanceBaker : IInstanceBaker<IReadOnlyCollection<string>>
public void PurgeInstances(string namePrefix)
{
var currentDoc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around
// clean name prefix to match how block names are created
var cleanedPrefix = RhinoUtils.CleanBlockDefinitionName(namePrefix);
foreach (var definition in currentDoc.InstanceDefinitions)
{
if (!definition.IsDeleted && definition.Name.Contains(namePrefix))
if (!definition.IsDeleted && definition.Name.Contains(cleanedPrefix))
{
currentDoc.InstanceDefinitions.Delete(definition.Index, true, false);
}
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using Rhino;
using Speckle.Converters.Common;
using Speckle.Converters.Rhino;
using Speckle.Objects.Other;
@@ -44,35 +43,43 @@ public class RhinoMaterialBaker
string materialId = speckleRenderMaterial.applicationId ?? speckleRenderMaterial.id.NotNull();
string matName = $"{speckleRenderMaterial.name}-({materialId})-{baseLayerName}";
matName = matName.Replace("[", "").Replace("]", ""); // "Material" doesn't like square brackets if we create from here. Once they created from Rhino UI, all good..
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
double transparency = 1 - speckleRenderMaterial.opacity;
Material rhinoMaterial =
new()
{
Name = matName,
DiffuseColor = diffuse,
EmissionColor = emissive,
Transparency = transparency
};
// Check if material with this name already exists in the document
int matIndex = doc.Materials.Find(matName, ignoreDeletedMaterials: true);
// try to get additional properties
if (speckleRenderMaterial["ior"] is double ior)
{
rhinoMaterial.IndexOfRefraction = ior;
}
if (speckleRenderMaterial["shine"] is double shine)
{
rhinoMaterial.Shine = shine;
}
int matIndex = doc.Materials.Add(rhinoMaterial);
// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
// If material doesn't exist, create it
if (matIndex == -1)
{
throw new ConversionException("Failed to add a material to the document.");
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
double transparency = 1 - speckleRenderMaterial.opacity;
Material rhinoMaterial =
new()
{
Name = matName,
DiffuseColor = diffuse,
EmissionColor = emissive,
Transparency = transparency
};
// try to get additional properties
if (speckleRenderMaterial["ior"] is double ior)
{
rhinoMaterial.IndexOfRefraction = ior;
}
if (speckleRenderMaterial["shine"] is double shine)
{
rhinoMaterial.Shine = shine;
}
matIndex = doc.Materials.Add(rhinoMaterial);
// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
if (matIndex == -1)
{
throw new ConversionException($"Failed to add a material to the document: '{matName}' (ID: {materialId})");
}
}
// Create the object <> material index map
@@ -87,27 +94,4 @@ public class RhinoMaterialBaker
}
}
}
/// <summary>
/// Removes all materials with a name starting with <paramref name="namePrefix"/> from the active document
/// </summary>
/// <param name="namePrefix"></param>
public void PurgeMaterials(string namePrefix)
{
var currentDoc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around
foreach (Material material in currentDoc.Materials)
{
try
{
if (!material.IsDeleted && material.Name != null && material.Name.Contains(namePrefix))
{
currentDoc.Materials.Delete(material);
}
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to purge a material from the document");
}
}
}
}
@@ -218,13 +218,23 @@ public class RhinoMaterialUnpacker
? pbRenderMaterial.Material.EmissionColor
: pbRenderMaterial.Emission.AsSystemColor(); // pbRenderMaterial.emission gives wrong color for emission materials, and material.emissioncolor gives the wrong value for most others *shrug*
// NOTE: added after CNX-2661, without having file that caused issue hard to say what the issue is
// api bug / funny model (custom textures) / upgrade from old model (e.g. Rhino 6)? who knows.
// PBR standard is 0-1. Clamping to valid range. This may indicate texture data is in wrong scale.
double roughness = pbRenderMaterial.Roughness;
if (roughness < 0 || roughness > 1)
{
_logger.LogWarning("Material '{Name}' has invalid roughness value of {Value}", renderMaterial.Name, roughness);
roughness = Math.Min(Math.Max(0, roughness), 1); // Math.Clamp() only from C# 8.0
}
SpeckleRenderMaterial speckleRenderMaterial =
new()
{
name = renderMaterialName,
opacity = opacity,
metalness = pbRenderMaterial.Metallic,
roughness = pbRenderMaterial.Roughness,
roughness = roughness,
diffuse = diffuse.ToArgb(),
emissive = emissive.ToArgb(),
applicationId = renderMaterial.Id.ToString()
@@ -256,7 +256,8 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
.RunOnMain(() =>
{
_instanceBaker.PurgeInstances(baseLayerName);
_materialBaker.PurgeMaterials(baseLayerName);
// Materials are now reused across receives instead of being purged
// _materialBaker.PurgeMaterials(baseLayerName);
var doc = _converterSettings.Current.Document;
// Cleans up any previously received objects
@@ -5,7 +5,7 @@ namespace Speckle.Converters.CSiShared;
[GenerateAutoInterface]
public class CsiConversionSettingsFactory(
IHostToSpeckleUnitConverter<eUnits> unitsConverter,
IHostToSpeckleUnitConverter<eLength> unitsConverter,
IConverterSettingsStore<CsiConversionSettings> settingsStore
) : ICsiConversionSettingsFactory
{
@@ -15,11 +15,21 @@ public class CsiConversionSettingsFactory(
cSapModel document,
List<string>? selectedLoadCasesAndCombinations = null,
List<string>? selectedResultTypes = null
) =>
new(
)
{
// NOTE: only applicable to ETABS. If we bring in SAP2000 then we need to revert to GetPresentUnits
// NOTE: change from GetPresentUnits as this was linked to weird behaviour (see CNX-2621), returning "0" sometimes
// bug in the GetPresentUnits api call ...
eTemperature temperatureUnit = eTemperature.NotApplicable;
eLength lengthUnit = eLength.NotApplicable;
eForce forceUnit = eForce.NotApplicable;
document.GetPresentUnits_2(ref forceUnit, ref lengthUnit, ref temperatureUnit);
return new CsiConversionSettings(
document,
unitsConverter.ConvertOrThrow(document.GetPresentUnits()),
unitsConverter.ConvertOrThrow(lengthUnit),
selectedLoadCasesAndCombinations ?? [],
selectedResultTypes ?? []
);
}
}
@@ -5,37 +5,28 @@ using Speckle.Sdk.Common.Exceptions;
namespace Speckle.Converters.CSiShared;
/// <summary>
/// Convert CSi eUnits enumeration to Speckle units.
/// Convert CSi eLength enumeration to Speckle units.
/// </summary>
/// <remarks>
/// CSi GetPresentUnits() valid for both SAP 2000 and ETABS.
/// CSi GetPresentUnits_2() valid for ONLY ETABS. If we add SAP2000, this needs to be modified
/// Represents units transmitted through API calls and not necessarily those displayed in GUI.
/// </remarks>
public class CsiToSpeckleUnitConverter : IHostToSpeckleUnitConverter<eUnits>
public class CsiToSpeckleUnitConverter : IHostToSpeckleUnitConverter<eLength>
{
private readonly Dictionary<eUnits, string> _unitMapping = new Dictionary<eUnits, string>();
private readonly Dictionary<eLength, string> _unitMapping = [];
public CsiToSpeckleUnitConverter()
{
_unitMapping[eUnits.lb_in_F] = Units.Inches;
_unitMapping[eUnits.lb_ft_F] = Units.Feet;
_unitMapping[eUnits.kip_in_F] = Units.Inches;
_unitMapping[eUnits.kip_ft_F] = Units.Feet;
_unitMapping[eUnits.kN_mm_C] = Units.Millimeters;
_unitMapping[eUnits.kN_m_C] = Units.Meters;
_unitMapping[eUnits.kgf_mm_C] = Units.Millimeters;
_unitMapping[eUnits.kgf_m_C] = Units.Meters;
_unitMapping[eUnits.N_mm_C] = Units.Millimeters;
_unitMapping[eUnits.N_m_C] = Units.Meters;
_unitMapping[eUnits.Ton_mm_C] = Units.Millimeters;
_unitMapping[eUnits.Ton_m_C] = Units.Meters;
_unitMapping[eUnits.kN_cm_C] = Units.Centimeters;
_unitMapping[eUnits.kgf_cm_C] = Units.Centimeters;
_unitMapping[eUnits.N_cm_C] = Units.Centimeters;
_unitMapping[eUnits.Ton_cm_C] = Units.Centimeters;
_unitMapping[eLength.NotApplicable] = Units.None;
_unitMapping[eLength.inch] = Units.Inches;
_unitMapping[eLength.ft] = Units.Feet;
// _unitMapping[eLength.micron] = Units.None;
_unitMapping[eLength.mm] = Units.Millimeters;
_unitMapping[eLength.cm] = Units.Centimeters;
_unitMapping[eLength.m] = Units.Meters;
}
public string ConvertOrThrow(eUnits hostUnit) =>
public string ConvertOrThrow(eLength hostUnit) =>
_unitMapping.TryGetValue(hostUnit, out string? value)
? value
: throw new UnitNotSupportedException($"The Unit System \"{hostUnit}\" is unsupported.");
@@ -39,7 +39,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<CsiToSpeckleCacheSingleton>();
// Settings and unit conversions
serviceCollection.AddApplicationConverters<CsiToSpeckleUnitConverter, eUnits>(converterAssembly);
serviceCollection.AddApplicationConverters<CsiToSpeckleUnitConverter, eLength>(converterAssembly);
serviceCollection.AddScoped<
IConverterSettingsStore<CsiConversionSettings>,
ConverterSettingsStore<CsiConversionSettings>
@@ -8,25 +8,11 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
/// Extracts properties common to frame elements across CSi products (e.g., Etabs, Sap2000)
/// using the FrameObj API calls.
/// </summary>
/// <remarks>
/// Design Decisions:
/// <list type="bullet">
/// <item>
/// <description>
/// Individual methods preferred over batched calls due to:
/// <list type="bullet">
/// <item><description>Independent API calls with no performance gain from batching (?)</description></item>
/// <item><description>Easier debugging and error tracing</description></item>
/// <item><description>Simpler maintenance as each method maps to one API concept</description></item>
/// </list>
/// </description>
/// </item>
/// </list>
/// </remarks>
public sealed class CsiFramePropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly DatabaseTableExtractor _databaseTableExtractor;
private static readonly string[] s_releaseKeys =
[
@@ -36,15 +22,17 @@ public sealed class CsiFramePropertiesExtractor
"Torsion",
"Moment 22 (Minor)",
"Moment 33 (Major)"
]; // Note: caching keys for better performance
];
public CsiFramePropertiesExtractor(
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
IConverterSettingsStore<CsiConversionSettings> settingsStore
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor
)
{
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
_settingsStore = settingsStore;
_databaseTableExtractor = databaseTableExtractor;
}
public void ExtractProperties(CsiFrameWrapper frame, PropertyExtractionResult frameData)
@@ -61,13 +49,29 @@ public sealed class CsiFramePropertiesExtractor
assignments[CommonObjectProperty.PROPERTY_MODIFIERS] = GetModifiers(frame);
assignments["End Releases"] = GetReleases(frame);
// NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc.
// NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc. Strings are unique
// Assign sectionId to variable as this will be an argument for the GetMaterialName method
string sectionId = GetSectionName(frame);
string materialId = GetMaterialName(sectionId);
assignments[ObjectPropertyKey.SECTION_ID] = sectionId;
assignments[ObjectPropertyKey.MATERIAL_ID] = materialId;
// CNX-2725 adds more numeric props for dashboard-ing
double length = GetLength(frame);
double area = GetCrossSectionalArea(sectionId);
double volume = double.NaN;
if (!double.IsNaN(length) && !double.IsNaN(area) && length > 0 && area > 0)
{
// I am paranoid about what etabs could throw our way
double computedVolume = length * area;
volume = (!double.IsInfinity(computedVolume) && !double.IsNaN(computedVolume)) ? computedVolume : double.NaN;
}
geometry.AddWithUnits(ObjectPropertyKey.LENGTH, length, _settingsStore.Current.SpeckleUnits);
geometry.AddWithUnits(ObjectPropertyKey.CROSS_SECTIONAL_AREA, area, $"{_settingsStore.Current.SpeckleUnits}²");
geometry.AddWithUnits(ObjectPropertyKey.VOLUME, volume, $"{_settingsStore.Current.SpeckleUnits}³");
// store the object, section, and material id relationships in their corresponding caches to be accessed by the connector
if (!string.IsNullOrEmpty(sectionId))
{
@@ -196,4 +200,56 @@ public sealed class CsiFramePropertiesExtractor
_ = _settingsStore.Current.SapModel.PropFrame.GetMaterial(sectionName, ref materialName);
return materialName;
}
private double GetLength(CsiFrameWrapper frame)
{
// using the DatabaseTableExtractor fetch table with key "Frame Assignments - Summary"
// limit query size to "UniqueName" and "Length" fields
string length = _databaseTableExtractor
.GetTableData("Frame Assignments - Summary", requestedColumns: ["UniqueName", ObjectPropertyKey.LENGTH])
.GetRowValue(frame.Name, ObjectPropertyKey.LENGTH);
// all database data is returned as strings
return double.TryParse(length, out double result) ? result : double.NaN;
}
private double GetCrossSectionalArea(string sectionName)
{
if (_csiToSpeckleCacheSingleton.FrameSectionAreaCache.TryGetValue(sectionName, out double value))
{
return value;
}
double area = 0,
as2 = 0,
as3 = 0,
torsion = 0,
i22 = 0,
i33 = 0,
s22 = 0,
s33 = 0,
z22 = 0,
z33 = 0,
r22 = 0,
r33 = 0;
int result = _settingsStore.Current.SapModel.PropFrame.GetSectProps(
sectionName,
ref area,
ref as2,
ref as3,
ref torsion,
ref i22,
ref i33,
ref s22,
ref s33,
ref z22,
ref z33,
ref r22,
ref r33
);
double validatedArea = result == 0 ? area : double.NaN;
_csiToSpeckleCacheSingleton.FrameSectionAreaCache.Add(sectionName, validatedArea);
return validatedArea;
}
}
@@ -8,28 +8,18 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
/// Extracts properties common to shell elements across CSi products (e.g., Etabs, Sap2000)
/// using the AreaObj API calls.
/// </summary>
/// <remarks>
/// Design Decisions:
/// <list type="bullet">
/// <item>
/// <description>
/// Individual methods preferred over batched calls due to:
/// <list type="bullet">
/// <item><description>Independent API calls with no performance gain from batching (?)</description></item>
/// <item><description>Easier debugging and error tracing</description></item>
/// <item><description>Simpler maintenance as each method maps to one API concept</description></item>
/// </list>
/// </description>
/// </item>
/// </list>
/// </remarks>
public sealed class CsiShellPropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
public CsiShellPropertiesExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
public CsiShellPropertiesExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton
)
{
_settingsStore = settingsStore;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
}
public void ExtractProperties(CsiShellWrapper shell, PropertyExtractionResult shellData)
@@ -37,7 +27,7 @@ public sealed class CsiShellPropertiesExtractor
shellData.ApplicationId = shell.GetSpeckleApplicationId(_settingsStore.Current.SapModel);
var geometry = shellData.Properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
geometry["Joints"] = GetPointNames(shell); // TODO: 🪲 Viewer shows 4 but only displays 3
geometry["Joints"] = GetPointNames(shell);
var assignments = shellData.Properties.EnsureNested(ObjectPropertyCategory.ASSIGNMENTS);
assignments[CommonObjectProperty.GROUPS] = GetGroupAssigns(shell);
@@ -16,4 +16,16 @@ public class CsiToSpeckleCacheSingleton
/// A map of (section id, shell object id). Assumes the section id is the unique name of the section
/// </summary>
public Dictionary<string, List<string>> ShellSectionCache { get; set; } = [];
/// <summary>
/// A cache of cross-sectional areas used
/// </summary>
public Dictionary<string, double> FrameSectionAreaCache { get; set; } = [];
/// <summary>
/// A cache of resolved shell section properties populated by "EtabsShellPropertiesExtractor"
/// and consumed by "EtabsShellSectionPropertyExtractor".
/// This eliminates redundant section resolution API calls.
/// </summary>
public Dictionary<string, Dictionary<string, object?>> ShellSectionPropertiesCache { get; set; } = [];
}
@@ -39,7 +39,7 @@ public abstract class CsiObjectToSpeckleConverterBase : IToSpeckleTopLevelConver
public Base Convert(object target) => Convert((CsiWrapperBase)target);
public Base Convert(CsiWrapperBase wrapper)
private Base Convert(CsiWrapperBase wrapper)
{
var displayValue = _displayValueExtractor.GetDisplayValue(wrapper).ToList();
var objectData = _applicationPropertiesExtractor.ExtractProperties(wrapper);
@@ -21,8 +21,14 @@ public static class ObjectPropertyCategory
/// </summary>
public static class ObjectPropertyKey
{
public const string AREA = "Area";
public const string CROSS_SECTIONAL_AREA = "Cross-Sectional Area";
public const string DESIGN_PROCEDURE = "Design Procedure";
public const string LENGTH = "Length";
public const string MATERIAL_ID = "Material";
public const string SECTION_ID = "Section Property";
public const string THICKNESS = "Thickness";
public const string VOLUME = "Volume";
}
/// <summary>
@@ -30,11 +36,14 @@ public static class ObjectPropertyKey
/// </summary>
public static class SectionPropertyCategory
{
public const string DESIGN_DATA = "Design Data";
public const string GENERAL_DATA = "General Data";
public const string MECHANICAL_DATA = "Mechanical Data";
public const string MODIFIERS = "Modifiers";
public const string PROPERTY_DATA = "Property Data";
public const string SECTION_PROPERTIES = "Section Properties";
public const string SECTION_DIMENSIONS = "Section Dimensions";
public const string WEIGHT_AND_MASS = "Weight and Mass";
}
/// <summary>
@@ -20,6 +20,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<EtabsShellPropertiesExtractor>();
serviceCollection.AddScoped<IApplicationPropertiesExtractor, EtabsPropertiesExtractor>();
serviceCollection.AddScoped<CsiObjectToSpeckleConverterBase, EtabsObjectToSpeckleConverter>();
serviceCollection.AddScoped<EtabsShellSectionResolver>();
serviceCollection.AddMatchingInterfacesAsTransient(converterAssembly);
@@ -14,6 +14,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsFramePropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsJointPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\EtabsShellSectionResolver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\TopLevel\EtabsObjectToSpeckleConverter.cs" />
</ItemGroup>
</Project>
@@ -1,6 +1,5 @@
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
using Speckle.Converters.CSiShared.Utils;
namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
@@ -8,32 +7,13 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
/// <summary>
/// Extracts ETABS-specific properties from frame elements using the FrameObj API calls.
/// </summary>
/// <remarks>
/// Responsibilities:
/// <list type="bullet">
/// <item><description>Extracts properties only available in ETABS (e.g., Label, Level)</description></item>
/// <item><description>Complements <see cref="CsiFramePropertiesExtractor"/> by adding product-specific data</description></item>
/// <item><description>Follows same pattern of single-purpose methods for clear API mapping</description></item>
/// </list>
///
/// Design Decisions:
/// <list type="bullet">
/// <item><description>Maintains separate methods for each property following CSI API structure</description></item>
/// <item><description>Properties are organized by their functional groups (Object ID, Assignments, Design)</description></item>
/// </list>
/// </remarks>
public sealed class EtabsFramePropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly DatabaseTableExtractor _databaseTableExtractor;
public EtabsFramePropertiesExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor
)
public EtabsFramePropertiesExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
_databaseTableExtractor = databaseTableExtractor;
}
public void ExtractProperties(CsiFrameWrapper frame, Dictionary<string, object?> properties)
@@ -46,11 +26,7 @@ public sealed class EtabsFramePropertiesExtractor
assignments[CommonObjectProperty.SPRING_ASSIGNMENT] = GetSpringAssignmentName(frame);
var design = properties.EnsureNested(ObjectPropertyCategory.DESIGN);
design["Design Procedure"] = GetDesignProcedure(frame);
var geometry = properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
double length = GetLength(frame);
geometry.AddWithUnits("Length", length, _settingsStore.Current.SpeckleUnits);
design[ObjectPropertyKey.DESIGN_PROCEDURE] = GetDesignProcedure(frame);
}
private (string label, string level) GetLabelAndLevel(CsiFrameWrapper frame)
@@ -90,16 +66,4 @@ public sealed class EtabsFramePropertiesExtractor
_ = _settingsStore.Current.SapModel.FrameObj.GetSpringAssignment(frame.Name, ref springPropertyName);
return springPropertyName;
}
private double GetLength(CsiFrameWrapper frame)
{
// using the DatabaseTableExtractor fetch table with key "Frame Assignments - Summary"
// limit query size to "UniqueName" and "Length" fields
string length = _databaseTableExtractor
.GetTableData("Frame Assignments - Summary", requestedColumns: ["UniqueName", "Length"])
.GetRowValue(frame.Name, "Length");
// all database data is returned as strings
return double.TryParse(length, out double result) ? result : double.NaN;
}
}
@@ -9,35 +9,24 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
/// <summary>
/// Extracts ETABS-specific properties from shell elements using the AreaObj API calls.
/// </summary>
/// <remarks>
/// Responsibilities:
/// <list type="bullet">
/// <item><description>Extracts properties only available in ETABS (e.g., Label, Level)</description></item>
/// <item><description>Complements <see cref="CsiShellPropertiesExtractor"/> by adding product-specific data</description></item>
/// <item><description>Follows same pattern of single-purpose methods for clear API mapping</description></item>
/// </list>
///
/// Design Decisions:
/// <list type="bullet">
/// <item><description>Maintains separate methods for each property following CSI API structure</description></item>
/// <item><description>Properties are organized by their functional groups (Object ID, Assignments, Design)</description></item>
/// </list>
/// </remarks>
public sealed class EtabsShellPropertiesExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton;
private readonly DatabaseTableExtractor _databaseTableExtractor;
private readonly EtabsShellSectionResolver _etabsShellSectionResolver;
public EtabsShellPropertiesExtractor(
CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton,
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor
DatabaseTableExtractor databaseTableExtractor,
EtabsShellSectionResolver etabsShellSectionResolver
)
{
_settingsStore = settingsStore;
_csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton;
_databaseTableExtractor = databaseTableExtractor;
_etabsShellSectionResolver = etabsShellSectionResolver;
}
public void ExtractProperties(CsiShellWrapper shell, Dictionary<string, object?> properties)
@@ -62,9 +51,22 @@ public sealed class EtabsShellPropertiesExtractor
assignments[ObjectPropertyKey.SECTION_ID] = sectionId;
assignments[ObjectPropertyKey.MATERIAL_ID] = materialId;
// CNX-2725 adds more numeric props for dashboard-ing
var geometry = properties.EnsureNested(ObjectPropertyCategory.GEOMETRY);
double area = GetArea(shell, designOrientation);
geometry.AddWithUnits("Area", area, $"{_settingsStore.Current.SpeckleUnits}²");
double thickness = GetSectionThickness(sectionId);
double volume = double.NaN;
if (!double.IsNaN(area) && !double.IsNaN(thickness) && area > 0 && thickness > 0)
{
// I am paranoid about what etabs could throw our way
double computedVolume = area * thickness;
volume = (!double.IsInfinity(computedVolume) && !double.IsNaN(computedVolume)) ? computedVolume : double.NaN;
}
geometry.AddWithUnits(ObjectPropertyKey.THICKNESS, thickness, _settingsStore.Current.SpeckleUnits);
geometry.AddWithUnits(ObjectPropertyKey.AREA, area, $"{_settingsStore.Current.SpeckleUnits}²");
geometry.AddWithUnits(ObjectPropertyKey.VOLUME, volume, $"{_settingsStore.Current.SpeckleUnits}³");
// store the object, section, and material id relationships in their corresponding caches to be accessed by the connector
if (!string.IsNullOrEmpty(sectionId))
@@ -188,4 +190,66 @@ public sealed class EtabsShellPropertiesExtractor
// all database data is returned as strings
return double.TryParse(area, out var result) ? result : double.NaN;
}
/// <summary>
/// Gets section thickness, resolving and caching section properties on first encounter.
/// </summary>
/// <param name="sectionId">The section name to get thickness for</param>
/// <returns>Thickness value, or NaN if section is invalid or thickness cannot be determined</returns>
private double GetSectionThickness(string sectionId)
{
// Guard against invalid sections
if (string.IsNullOrEmpty(sectionId) || sectionId == "None")
{
return double.NaN;
}
// Check if section already resolved and cached
if (!_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache.TryGetValue(sectionId, out var sectionProperties))
{
// First encounter - resolve section and cache all properties
sectionProperties = _etabsShellSectionResolver.ResolveSection(sectionId);
_csiToSpeckleCacheSingleton.ShellSectionPropertiesCache[sectionId] = sectionProperties;
}
// Extract thickness from cached properties
return ExtractThicknessFromProperties(sectionProperties);
}
/// <summary>
/// Extracts thickness value from resolved section properties dictionary structure.
/// </summary>
/// <remarks>
/// Section properties have nested structure:
/// { "Property Data" -> { "Thickness" -> { "value" -> double, "units" -> string } } }
/// </remarks>
private static double ExtractThicknessFromProperties(Dictionary<string, object?> sectionProperties)
{
if (!sectionProperties.TryGetValue(SectionPropertyCategory.PROPERTY_DATA, out object? propertyDataObj))
{
return double.NaN;
}
if (propertyDataObj is not Dictionary<string, object?> propertyData)
{
return double.NaN;
}
if (!propertyData.TryGetValue(ObjectPropertyKey.THICKNESS, out object? thicknessObj))
{
return double.NaN;
}
if (thicknessObj is not Dictionary<string, object> thicknessDict)
{
return double.NaN;
}
if (!thicknessDict.TryGetValue("value", out object? valueObj))
{
return double.NaN;
}
return valueObj is double thickness ? thickness : double.NaN;
}
}
@@ -2,7 +2,7 @@ using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.Utils;
namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers;
/// <summary>
/// Attempts to resolve the section type and retrieve its properties by trying different section resolvers.
@@ -14,6 +14,17 @@ public class PropertySetDefinitionHandler
/// POC: We're storing these by property set def name atm. There is a decent change different property sets can have the same name, need to validate this.
public Dictionary<string, Dictionary<string, object?>> Definitions { get; } = new();
// Keys used for the dictionary representing a single property set definition
public const string PROP_SET_DEF_NAME_KEY = "name"; // name of the property set definition
public const string PROP_SET_PROP_DEFS_KEY = "propertyDefinitions"; // property definitions in this property set definition
// Keys used for inidividual property definitions within a single property set definition
public const string PROP_DEF_NAME_KEY = "name";
public const string PROP_DEF_DESCRIPTION_KEY = "description";
public const string PROP_DEF_ID_KEY = "id";
public const string PROP_DEF_TYPE_KEY = "dataType";
public const string PROP_DEF_DEFAULT_VALUE_KEY = "defaultValue";
/// <summary>
/// Extracts out and stores in <see cref="Definitions"/> the property set definition.
/// </summary>
@@ -29,12 +40,11 @@ public class PropertySetDefinitionHandler
propertyDefinitionNames[propertyDefinition.Id] = propertyName;
var propertyDict = new Dictionary<string, object?>()
{
["name"] = propertyName,
["description"] = propertyDefinition.Description,
["id"] = propertyDefinition.Id,
["isReadOnly"] = propertyDefinition.IsReadOnly,
["dataType"] = propertyDefinition.DataType.ToString(),
["defaultValue"] = propertyDefinition.DefaultData
[PROP_DEF_NAME_KEY] = propertyName,
[PROP_DEF_DESCRIPTION_KEY] = propertyDefinition.Description,
[PROP_DEF_ID_KEY] = propertyDefinition.Id,
[PROP_DEF_TYPE_KEY] = propertyDefinition.DataType.ToString(),
[PROP_DEF_DEFAULT_VALUE_KEY] = propertyDefinition.DefaultData
};
// accessing unit type prop can be expected to throw if it's not applicable to the definition
@@ -53,8 +63,8 @@ public class PropertySetDefinitionHandler
Definitions[name] = new Dictionary<string, object?>()
{
["name"] = name,
["propertyDefinitions"] = propertyDefinitionsDict
[PROP_SET_DEF_NAME_KEY] = name,
[PROP_SET_PROP_DEFS_KEY] = propertyDefinitionsDict
};
return propertyDefinitionNames;
@@ -91,7 +91,8 @@ public class PropertySetExtractor
? propertyDefinitionName
: data.FieldBucketId;
var value = GetValue(data);
// POC: not sure how to support graphic types atm
var value = data.DataType is AAEC.PropertyData.DataType.Graphic ? null : data.GetData(data.UnitType);
Dictionary<string, object?> propertyValueDict = new() { ["value"] = value, ["name"] = dataName };
PropertyHandler propHandler = new();
@@ -109,31 +110,4 @@ public class PropertySetExtractor
return null;
}
private object? GetValue(AAECPDB.PropertySetData data)
{
object fieldData = data.GetData(data.UnitType);
switch (data.DataType)
{
case AAEC.PropertyData.DataType.Integer:
return fieldData as int?;
case AAEC.PropertyData.DataType.Real:
return fieldData as double?;
case AAEC.PropertyData.DataType.TrueFalse:
return fieldData as bool?;
case AAEC.PropertyData.DataType.Graphic: // POC: not sure how to support atm
return null;
case AAEC.PropertyData.DataType.List:
return fieldData as List<object>;
case AAEC.PropertyData.DataType.AutoIncrement:
return fieldData as int?;
case AAEC.PropertyData.DataType.AlphaIncrement: // POC: not sure what this is
return fieldData;
case AAEC.PropertyData.DataType.Text:
return fieldData as string;
default:
return fieldData;
}
}
}
@@ -1,8 +1,10 @@
using Microsoft.Extensions.Logging;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Common.ToSpeckle;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects;
using Speckle.Sdk;
using Speckle.Sdk.Common;
@@ -18,11 +20,11 @@ public sealed class DisplayValueExtractor
List<SOG.Mesh>
> _meshByMaterialConverter;
private readonly IScalingServiceToSpeckle _toSpeckleScalingService;
private readonly ITypedConverter<DB.Curve, ICurve> _curveConverter;
private readonly ITypedConverter<DB.PolyLine, SOG.Polyline> _polylineConverter;
private readonly ITypedConverter<DB.Point, SOG.Point> _pointConverter;
private readonly ITypedConverter<DB.PointCloudInstance, SOG.Pointcloud> _pointcloudConverter;
private readonly ILogger<DisplayValueExtractor> _logger;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
public DisplayValueExtractor(
@@ -34,8 +36,8 @@ public sealed class DisplayValueExtractor
ITypedConverter<DB.PolyLine, SOG.Polyline> polylineConverter,
ITypedConverter<DB.Point, SOG.Point> pointConverter,
ITypedConverter<DB.PointCloudInstance, SOG.Pointcloud> pointcloudConverter,
ILogger<DisplayValueExtractor> logger,
IConverterSettingsStore<RevitConversionSettings> converterSettings
IConverterSettingsStore<RevitConversionSettings> converterSettings,
IScalingServiceToSpeckle toSpeckleScalingService
)
{
_meshByMaterialConverter = meshByMaterialConverter;
@@ -43,30 +45,30 @@ public sealed class DisplayValueExtractor
_polylineConverter = polylineConverter;
_pointConverter = pointConverter;
_pointcloudConverter = pointcloudConverter;
_logger = logger;
_converterSettings = converterSettings;
_toSpeckleScalingService = toSpeckleScalingService;
}
public List<Base> GetDisplayValue(DB.Element element)
public List<DisplayValueResult> GetDisplayValue(DB.Element element)
{
switch (element)
{
// get custom (anything not using element.get_geometry) display values
case DB.PointCloudInstance pointcloud:
return new() { _pointcloudConverter.Convert(pointcloud) };
return [DisplayValueResult.WithoutTransform(_pointcloudConverter.Convert(pointcloud))];
case DB.ModelCurve modelCurve:
return new() { GetCurveDisplayValue(modelCurve.GeometryCurve) };
return [DisplayValueResult.WithoutTransform(GetCurveDisplayValue(modelCurve.GeometryCurve))];
case DB.Grid grid:
return new() { GetCurveDisplayValue(grid.Curve) };
return [DisplayValueResult.WithoutTransform(GetCurveDisplayValue(grid.Curve))];
case DB.Area area:
List<Base> areaDisplay = new();
List<DisplayValueResult> areaDisplay = new();
using (var options = new DB.SpatialElementBoundaryOptions())
{
foreach (IList<DB.BoundarySegment> boundarySegmentGroup in area.GetBoundarySegments(options))
{
foreach (DB.BoundarySegment boundarySegment in boundarySegmentGroup)
{
areaDisplay.Add(GetCurveDisplayValue(boundarySegment.GetCurve()));
areaDisplay.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(boundarySegment.GetCurve())));
}
}
}
@@ -87,7 +89,7 @@ public sealed class DisplayValueExtractor
return wall.CurtainGrid is not null || wall.IsStackedWall ? new() : GetGeometryDisplayValue(element);
// railings should also include toprail which need to be retrieved separately
case DBA.Railing railing:
List<Base> railingDisplay = GetGeometryDisplayValue(railing);
List<DisplayValueResult> railingDisplay = GetGeometryDisplayValue(railing);
if (railing.TopRail != DB.ElementId.InvalidElementId)
{
var topRail = _converterSettings.Current.Document.GetElement(railing.TopRail);
@@ -105,10 +107,21 @@ public sealed class DisplayValueExtractor
private Base GetCurveDisplayValue(DB.Curve curve) => (Base)_curveConverter.Convert(curve);
private List<Base> GetGeometryDisplayValue(DB.Element element, DB.Options? options = null)
private List<DisplayValueResult> GetGeometryDisplayValue(DB.Element element, DB.Options? options = null)
{
var collections = GetSortedGeometryFromElement(element, options);
return ProcessGeometryCollections(element, collections);
using DB.Transform? localToDocument = GetTransform(element);
using DB.Transform? documentToLocal = localToDocument?.Inverse;
DB.Transform? documentToWorld = _converterSettings.Current.ReferencePointTransform?.Inverse;
using DB.Transform? compoundTransform =
localToDocument is not null && documentToWorld is not null
? documentToWorld.Multiply(localToDocument)
: localToDocument; // don't want to accidentally dispose of the ReferencePointTransform
DB.Transform? localToWorld = compoundTransform ?? documentToWorld;
var collections = GetSortedGeometryFromElement(element, options, documentToLocal);
return ProcessGeometryCollections(element, collections, localToWorld);
}
/// <summary>
@@ -119,7 +132,15 @@ public sealed class DisplayValueExtractor
/// Note: Some special element types (like Rebar) cannot use this method as their
/// get_Geometry() returns null, requiring specialized extraction methods.
/// </remarks>
private GeometryCollections GetSortedGeometryFromElement(DB.Element element, DB.Options? options)
/// <param name="element"></param>
/// <param name="options"></param>
/// <param name="worldToLocal"></param>
/// <returns></returns>
private GeometryCollections GetSortedGeometryFromElement(
DB.Element element,
DB.Options? options,
DB.Transform? worldToLocal
)
{
//options = ViewSpecificOptions ?? options ?? new Options() { DetailLevel = DetailLevelSetting };
options ??= new DB.Options { DetailLevel = _detailLevelMap[_converterSettings.Current.DetailLevel] };
@@ -142,7 +163,7 @@ public sealed class DisplayValueExtractor
if (geom != null && geom.Any())
{
// retrieves all meshes and solids from a geometry element
SortGeometry(element, collections, geom);
SortGeometry(element, collections, geom, worldToLocal);
}
return collections;
@@ -155,36 +176,83 @@ public sealed class DisplayValueExtractor
/// <remarks>
/// Essentially all the ensuing steps after the common get_Geometry element method
/// </remarks>
private List<Base> ProcessGeometryCollections(DB.Element element, GeometryCollections collections)
private List<DisplayValueResult> ProcessGeometryCollections(
DB.Element element,
GeometryCollections collections,
DB.Transform? localToWorld
)
{
List<Base> displayValue = new();
// handle all solids and meshes by their material
var meshesByMaterial = GetMeshesByMaterial(collections.Meshes, collections.Solids);
List<SOG.Mesh> displayMeshes = _meshByMaterialConverter.Convert(
(meshesByMaterial, element.Id, ShouldSetElementDisplayToTransparent(element))
);
displayValue.AddRange(displayMeshes);
// add rest of geometry
List<DisplayValueResult> displayValue = new(collections.TotalCount);
Matrix4x4? matrix = localToWorld is not null ? TransformToMatrix(localToWorld) : null;
foreach (SOG.Mesh mesh in displayMeshes)
{
displayValue.Add(
matrix.HasValue
? DisplayValueResult.WithTransform(mesh, matrix.Value)
: DisplayValueResult.WithoutTransform(mesh)
);
}
// add rest of geometry (always without transform)
foreach (var curve in collections.Curves)
{
displayValue.Add(GetCurveDisplayValue(curve));
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
foreach (var polyline in collections.Polylines)
{
displayValue.Add(_polylineConverter.Convert(polyline));
displayValue.Add(DisplayValueResult.WithoutTransform(_polylineConverter.Convert(polyline)));
}
foreach (var point in collections.Points)
{
displayValue.Add(_pointConverter.Convert(point));
displayValue.Add(DisplayValueResult.WithoutTransform(_pointConverter.Convert(point)));
}
return displayValue;
}
private Matrix4x4 TransformToMatrix(DB.Transform transform) =>
new()
{
M11 = transform.BasisX.X,
M21 = transform.BasisX.Y,
M31 = transform.BasisX.Z,
M41 = 0,
M12 = transform.BasisY.X,
M22 = transform.BasisY.Y,
M32 = transform.BasisY.Z,
M42 = 0,
M13 = transform.BasisZ.X,
M23 = transform.BasisZ.Y,
M33 = transform.BasisZ.Z,
M43 = 0,
M14 = _toSpeckleScalingService.ScaleLength(transform.Origin.X),
M24 = _toSpeckleScalingService.ScaleLength(transform.Origin.Y),
M34 = _toSpeckleScalingService.ScaleLength(transform.Origin.Z),
M44 = 1
};
private static DB.Transform? GetTransform(DB.Element element)
{
if (element is DB.Instance i)
{
return i.GetTotalTransform();
}
return null;
}
private static Dictionary<DB.ElementId, List<DB.Mesh>> GetMeshesByMaterial(
List<DB.Mesh> meshes,
List<DB.Solid> solids
@@ -249,7 +317,12 @@ public sealed class DisplayValueExtractor
///
/// Note: this is basically a geometry unpacker for all types of geometry
/// </summary>
private void SortGeometry(DB.Element element, GeometryCollections collections, DB.GeometryElement geom)
private void SortGeometry(
DB.Element element,
GeometryCollections collections,
DB.GeometryElement geom,
DB.Transform? worldToLocal
)
{
foreach (DB.GeometryObject geomObj in geom)
{
@@ -267,13 +340,22 @@ public sealed class DisplayValueExtractor
continue;
}
if (worldToLocal is not null)
{
solid = DB.SolidUtils.CreateTransformed(solid, worldToLocal);
}
collections.Solids.Add(solid);
break;
case DB.Mesh mesh:
if (worldToLocal is not null)
{
mesh = mesh.get_Transformed(worldToLocal);
}
collections.Meshes.Add(mesh);
break;
//Note, we're not applying transforms to curves/polylines/points because ProcessGeometryCollections expects them in world coordinates
case DB.Curve curve:
collections.Curves.Add(curve);
break;
@@ -288,12 +370,19 @@ public sealed class DisplayValueExtractor
case DB.GeometryInstance instance:
// element transforms should not be carried down into nested geometryInstances.
// Nested geomInstances should have their geom retreived with GetInstanceGeom, not GetSymbolGeom
SortGeometry(element, collections, instance.GetInstanceGeometry());
// Nested geomInstances should have their geom retrieved with GetInstanceGeom, not GetSymbolGeom
if (worldToLocal == null) //see remark on method for why this is safe to do...
{
SortGeometry(element, collections, instance.GetInstanceGeometry(), null);
}
else
{
SortGeometry(element, collections, instance.GetSymbolGeometry(), null);
}
break;
case DB.GeometryElement geometryElement:
SortGeometry(element, collections, geometryElement);
SortGeometry(element, collections, geometryElement, null);
break;
}
}
@@ -424,25 +513,23 @@ public sealed class DisplayValueExtractor
/// Instead, we use GetFullGeometryForView() to obtain the geometry and then process it
/// using the standard geometry sorting and conversion.
/// </remarks>
private List<Base> GetRebarVolumetricDisplayValue(DB.Structure.Rebar rebar)
private List<DisplayValueResult> GetRebarVolumetricDisplayValue(DB.Structure.Rebar rebar)
{
var collections = new GeometryCollections();
// Regular get_Geometry() returns null for rebar, so we need to use GetFullGeometryForView
// ❗NOTE: view detail level needs to be fine in order for this to work
// Same behaviour as sending structural frame though - consistent and therefore okay.
DB.GeometryElement geometryElements = rebar.GetFullGeometryForView(_converterSettings.Current.Document.ActiveView);
SortGeometry(rebar, collections, geometryElements);
DB.GeometryElement? geometryElements = rebar.GetFullGeometryForView(_converterSettings.Current.Document.ActiveView);
if (geometryElements != null)
{
SortGeometry(rebar, collections, geometryElements);
return ProcessGeometryCollections(rebar, collections);
SortGeometry(rebar, collections, geometryElements, null);
return ProcessGeometryCollections(rebar, collections, null);
}
// Return empty list if no geometry is found - imo not critical
return new List<Base>();
return new List<DisplayValueResult>();
}
/// <summary>
@@ -451,7 +538,7 @@ public sealed class DisplayValueExtractor
/// <remarks>
/// This method extracts the centerlines of rebar elements when a simplified representation is preferred.
/// </remarks>
private List<Base> GetRebarCenterlineDisplayValue(DB.Structure.Rebar rebar)
private List<DisplayValueResult> GetRebarCenterlineDisplayValue(DB.Structure.Rebar rebar)
{
bool isSingleLayout = rebar.LayoutRule == DB.Structure.RebarLayoutRule.Single;
int numberOfBarPositions = rebar.NumberOfBarPositions;
@@ -480,10 +567,10 @@ public sealed class DisplayValueExtractor
);
}
List<Base> displayValue = new();
List<DisplayValueResult> displayValue = new();
foreach (var curve in curves)
{
displayValue.Add(GetCurveDisplayValue(curve));
displayValue.Add(DisplayValueResult.WithoutTransform(GetCurveDisplayValue(curve)));
}
return displayValue;
@@ -494,6 +581,10 @@ public sealed class DisplayValueExtractor
/// Used to pass multiple geometry collections as a single parameter to improve code readability
/// and reduce the risk of parameter ordering errors.
/// </summary>
/// <remarks>
/// <see cref="Solids"/> and <see cref="Meshes"/> potentially in local coordinate space.
/// For now, <see cref="Curves"/>, <see cref="Polylines"/>, <see cref="Points"/> will always be in world space
/// </remarks>
private sealed record GeometryCollections
{
public List<DB.Solid> Solids { get; } = new();
@@ -501,5 +592,7 @@ public sealed class DisplayValueExtractor
public List<DB.Curve> Curves { get; } = new();
public List<DB.PolyLine> Polylines { get; } = new();
public List<DB.Point> Points { get; } = new();
public int TotalCount => Solids.Count + Meshes.Count + Curves.Count + Polylines.Count + Points.Count;
}
}
@@ -18,23 +18,74 @@ public sealed class LevelExtractor
return level.Name;
}
/// <summary>
/// Gets the level associated with an element. Handles face-based family instances and hosted elements.
/// </summary>
public DB.Level? GetLevel(DB.Element element)
{
// get level, if any
DB.ElementId? levelId = null;
// try direct LevelId first
if (element.LevelId != DB.ElementId.InvalidElementId)
{
if (_levelCache.TryGetValue(element.LevelId, out DB.Level? cachedLevel))
{
return cachedLevel;
}
levelId = element.LevelId;
}
// otherwise try FamilyInstance-specific sources
else if (element is DB.FamilyInstance familyInstance)
{
levelId = TryGetFamilyInstanceLevelId(familyInstance);
if (element.Document.GetElement(element.LevelId) is DB.Level level)
// couldn't find a direct level ID - recurse to host
if (levelId == null && familyInstance.Host != null)
{
_levelCache[element.LevelId] = level;
return level;
return GetLevel(familyInstance.Host);
}
}
// okay, no valid LevelId found and we've tried A LOT!
if (levelId == null || levelId == DB.ElementId.InvalidElementId)
{
return null;
}
// check if cache has seen this Level before
if (_levelCache.TryGetValue(levelId, out DB.Level? cached))
{
return cached;
}
// add to the cache if firs occurence of this level
if (element.Document.GetElement(levelId) is DB.Level level)
{
_levelCache[levelId] = level;
return level;
}
return null;
}
/// <summary>
/// Tries to get a level ID from a FamilyInstance via parameter or host.
/// Face-based instances store their level in INSTANCE_SCHEDULE_ONLY_LEVEL_PARAM.
/// </summary>
/// <remarks>
/// See: https://forums.autodesk.com/t5/revit-api-forum/newfamilyinstance-not-setting-level-of-family-instance/td-p/11405934
/// </remarks>
private DB.ElementId? TryGetFamilyInstanceLevelId(DB.FamilyInstance familyInstance)
{
// try parameter-based level first (face-based families)
var levelId = familyInstance.get_Parameter(DB.BuiltInParameter.INSTANCE_SCHEDULE_ONLY_LEVEL_PARAM)?.AsElementId();
if (levelId != null && levelId != DB.ElementId.InvalidElementId)
{
return levelId;
}
// try host if it's directly a level
if (familyInstance.Host is DB.Level hostLevel)
{
return hostLevel.Id;
}
return null;
}
}
@@ -1,4 +1,5 @@
using Autodesk.Revit.DB;
using Speckle.DoubleNumerics;
namespace Speckle.Converters.RevitShared.Helpers;
@@ -43,6 +44,30 @@ public static class ReferencePointHelper
};
}
public static Matrix4x4 TransformToMatrix(Transform transform) =>
new()
{
M11 = transform.BasisX.X,
M21 = transform.BasisX.Y,
M31 = transform.BasisX.Z,
M41 = 0,
M12 = transform.BasisY.X,
M22 = transform.BasisY.Y,
M32 = transform.BasisY.Z,
M42 = 0,
M13 = transform.BasisZ.X,
M23 = transform.BasisZ.Y,
M33 = transform.BasisZ.Z,
M43 = 0,
M14 = transform.Origin.X,
M24 = transform.Origin.Y,
M34 = transform.Origin.Z,
M44 = 1
};
/// <summary>
/// Extracts and reconstructs a transform from the matrix data stored on root object
/// </summary>
@@ -1,4 +1,9 @@
using Microsoft.Extensions.Logging;
using Speckle.Converters.Common.ToSpeckle;
using Speckle.Objects.Other;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.RevitShared.Helpers;
@@ -11,7 +16,7 @@ namespace Speckle.Converters.RevitShared.Helpers;
/// Ask dim for more and he might start crying.
/// </para>
/// </summary>
public class RevitToSpeckleCacheSingleton
public class RevitToSpeckleCacheSingleton(ILogger<RevitToSpeckleCacheSingleton> logger)
{
/// <summary>
/// (DB.Material id, RenderMaterial). This can be generated from converting render materials or material quantities.
@@ -24,11 +29,31 @@ public class RevitToSpeckleCacheSingleton
/// </summary>
public Dictionary<string, Dictionary<string, RenderMaterialProxy>> ObjectRenderMaterialProxiesMap { get; } = new();
public Dictionary<
string,
(List<string> elementIds, InstanceDefinitionProxy definitionProxy)
> InstanceDefinitionProxiesMap { get; } = new();
public Dictionary<string, (List<string> elementIds, Base baseObj)> InstancedObjects { get; } = new();
/// <summary>
/// Returns the merged material proxy list for the given object ids. Use this to get post conversion a correct list of material proxies for setting on the root commit object.
/// Maps mesh application IDs to their material IDs for later proxy population.
/// Dictionary: elementId -> (meshAppId -> materialId)
/// </summary>
/// <param name="elementIds"></param>
/// <returns></returns>
public Dictionary<string, Dictionary<string, string>> MeshToMaterialMap { get; } = new();
/// <summary>
/// Returns the merged material proxy list for the given object IDs.
/// Use this post-conversion to get a correct list of material proxies for the root commit object.
/// </summary>
/// <returns>A deduplicated list of <see cref="RenderMaterialProxy"/> objects for all specified elements.</returns>
/// <remarks>
/// <para>
/// Material proxy objects lists should already be correctly populated at this point (with definition mesh IDs for instances
/// and individual mesh IDs for non-instances), so the merging primarily handles cross-element scenarios rather than
/// fixing incorrect data.
/// </para>
/// </remarks>
public List<RenderMaterialProxy> GetRenderMaterialProxyListForObjects(List<string> elementIds)
{
var proxiesToMerge = ObjectRenderMaterialProxiesMap
@@ -42,17 +67,134 @@ public class RevitToSpeckleCacheSingleton
{
if (!mergeTarget.TryGetValue(kvp.Key, out RenderMaterialProxy? value))
{
value = kvp.Value;
mergeTarget[kvp.Key] = value;
continue;
// first time seeing this material - add it
mergeTarget[kvp.Key] = kvp.Value;
}
else
{
// merge objects lists (should already be mostly correct now)
value.objects.AddRange(kvp.Value.objects);
}
value.objects.AddRange(kvp.Value.objects);
}
}
// final deduplication (should be minimal now)
foreach (var renderMaterialProxy in mergeTarget.Values)
{
renderMaterialProxy.objects = renderMaterialProxy.objects.Distinct().ToList();
}
return mergeTarget.Values.ToList();
}
/// <summary>
/// Gets instance definition proxies from session cache for the given element ids.
/// This is necessary because send caching only check against DB.Element since it is the managed object in Revit UI.
/// We need to filter already existant definition proxies from cache with their element id relationship.
/// Otherwise, we will end up with incomplete data in root.
/// </summary>
/// <param name="elementIds">Ids to get corresponding definition proxies that cached before.</param>
public List<InstanceDefinitionProxy> GetInstanceDefinitionProxiesForObjects(List<string> elementIds) =>
InstanceDefinitionProxiesMap
.Values.Where(v => v.elementIds.Any(id => elementIds.Contains(id)))
.Select(v => v.definitionProxy)
.ToList();
/// <summary>
/// Gets atomic objects (Base) that extracted out from display value of RevitDataObject.
/// We need to filter already existant atomic objects from cache with their element id relationship.
/// Otherwise, we will end up with incomplete data in root.
/// </summary>
/// <param name="elementIds">Element ids to get corresponding atomic objects (Base) that cached before.</param>
/// <returns></returns>
public List<Base> GetBaseObjectsForObjects(List<string> elementIds) =>
InstancedObjects.Values.Where(v => v.elementIds.Any(id => elementIds.Contains(id))).Select(v => v.baseObj).ToList();
/// <summary>
/// Adds a mesh ID to the appropriate material proxy.
/// For instances: adds the definition mesh ID.
/// For non-instances: adds the mesh's own ID.
/// </summary>
/// <remarks>
/// Cache navigation logic is encapsulated here. Failures are logged but do not throw exceptions,
/// allowing conversion to continue even if material assignment fails.
/// </remarks>
public void AddMeshToMaterialProxy(string elementId, SOG.Mesh mesh, bool isInstance)
{
// get mesh-to-material mapping
if (!MeshToMaterialMap.TryGetValue(elementId, out var meshMatMap))
{
logger.LogWarning("No mesh-to-material mapping found for element {ElementId}", elementId);
return;
}
// get material ID for this mesh
if (!meshMatMap.TryGetValue(mesh.applicationId.NotNull(), out var materialId))
{
logger.LogError(
"Cache inconsistency: Mesh {MeshId} not found in material mapping for element {ElementId}",
mesh.applicationId,
elementId
);
return;
}
// get material proxy map
if (!ObjectRenderMaterialProxiesMap.TryGetValue(elementId, out var proxyMap))
{
logger.LogError("Cache inconsistency: Material proxy map not found for element {ElementId}", elementId);
return;
}
// get specific material proxy
if (!proxyMap.TryGetValue(materialId, out var materialProxy))
{
if (materialId != DB.ElementId.InvalidElementId.ToString())
{
logger.LogError(
"Cache inconsistency: Material proxy not found for material {MaterialId} in element {ElementId}",
materialId,
elementId
);
}
return;
}
// determine which mesh ID to add
string meshIdToAdd;
if (isInstance)
{
var instanceDefinitionId = MeshInstanceIdGenerator.GenerateUntransformedMeshId(mesh);
if (!InstancedObjects.TryGetValue(instanceDefinitionId, out var instancedObject))
{
throw new InvalidOperationException(
$"Instance definition '{instanceDefinitionId}' not found in cache for mesh '{mesh.applicationId}'"
);
}
meshIdToAdd = instancedObject.baseObj.applicationId.NotNull();
}
else
{
meshIdToAdd = mesh.applicationId.NotNull();
}
// add to proxy if not already present
if (!materialProxy.objects.Contains(meshIdToAdd))
{
materialProxy.objects.Add(meshIdToAdd);
}
}
public void ClearCache()
{
ObjectRenderMaterialProxiesMap.Clear();
SpeckleRenderMaterialCache.Clear();
InstanceDefinitionProxiesMap.Clear();
InstancedObjects.Clear();
MeshToMaterialMap.Clear();
}
}
@@ -2,7 +2,6 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Registration;
using Speckle.Converters.Revit2023.ToSpeckle.Properties;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
@@ -19,7 +18,8 @@ public static class ServiceRegistration
var converterAssembly = Assembly.GetExecutingAssembly();
//register types by default
serviceCollection.AddMatchingInterfacesAsTransient(converterAssembly);
// Register single root
// register single root
serviceCollection.AddRootCommon<RevitRootToSpeckleConverter>(converterAssembly);
// register all application converters
@@ -29,7 +29,7 @@ public static class ServiceRegistration
serviceCollection.AddSingleton(new RevitContext());
serviceCollection.AddSingleton(new RevitToHostCacheSingleton());
serviceCollection.AddSingleton(new RevitToSpeckleCacheSingleton());
serviceCollection.AddSingleton<RevitToSpeckleCacheSingleton>();
// POC: do we need ToSpeckleScalingService as is, do we need to interface it out?
serviceCollection.AddScoped<ScalingServiceToSpeckle>();
@@ -73,6 +73,28 @@ public class ClassPropertiesExtractor
elementProperties.Add("worksetName", worksetName);
}
if (element is DB.FamilyInstance familyInstance)
{
try
{
// get room id if applicable (only for FamilyInstance elements)
if (familyInstance.Room is not null)
{
elementProperties.Add("roomId", familyInstance.Room.Id.ToString());
}
// get space id if applicable (only for FamilyInstance elements)
if (familyInstance.Space is not null)
{
elementProperties.Add("spaceId", familyInstance.Space.Id.ToString());
}
}
catch (Exception e) when (!e.IsFatal())
{
// silently ignore - not critical
}
}
// get group name if applicable
// TODO: in in group proxies separate issue. Below comments from PR #1081
// We're using group proxies in Rhino etc. Groups should be handled similarly in Revit, unless there's a good
@@ -5,13 +5,15 @@ namespace Speckle.Converters.RevitShared.ToSpeckle;
/// </summary>
public class ParameterDefinitionHandler
{
private sealed record GroupDefinition(string Group, string? Units);
private sealed record ParameterDefinition(string GroupName, string? Units);
private sealed record ParameterKey(string InternalName, string Group);
/// <summary>
/// Keeps track of all parameter definitions used in the current send operation. This should be attached to the root commit object post conversion.
/// </summary>
/// POC: Note that we're abusing dictionaries in here because we've yet to have a simple way to serialize non-base derived classes (or structs?)
private readonly Dictionary<string, GroupDefinition> _groupDefinitions = new();
private readonly Dictionary<ParameterKey, ParameterDefinition> _parameterDefinitions = new();
public (string internalDefinitionName, string humanReadableName, string groupName, string? units) HandleDefinition(
DB.Parameter parameter
@@ -37,11 +39,12 @@ public class ParameterDefinitionHandler
internalDefinitionName = builtInParameter.ToString();
}
}
if (_groupDefinitions.TryGetValue(groupDefinitionId, out var def))
var key = new ParameterKey(internalDefinitionName, groupDefinitionId);
if (_parameterDefinitions.TryGetValue(key, out var parameterDefinition))
{
return (internalDefinitionName, humanReadableName, def.Group, def.Units);
return (internalDefinitionName, humanReadableName, parameterDefinition.GroupName, parameterDefinition.Units);
}
var group = DB.LabelUtils.GetLabelForGroup(definition.GetGroupTypeId());
string? units = null;
if (parameter.StorageType == DB.StorageType.Double)
@@ -49,10 +52,7 @@ public class ParameterDefinitionHandler
units = DB.LabelUtils.GetLabelForUnit(parameter.GetUnitTypeId());
}
var group = DB.LabelUtils.GetLabelForGroup(definition.GetGroupTypeId());
_groupDefinitions[groupDefinitionId] = new GroupDefinition(Group: group, Units: units);
_parameterDefinitions[key] = new ParameterDefinition(GroupName: group, Units: units);
return (internalDefinitionName, humanReadableName, group, units);
}
}
@@ -148,7 +148,7 @@ public class ParameterExtractor
{
foreach (DB.Connector conn in cm.Connectors)
{
if (conn.MEPSystem != null)
if (conn.ConnectorType == DB.ConnectorType.Physical && conn.IsConnected && conn.MEPSystem != null)
{
return conn.MEPSystem;
}
@@ -172,7 +172,8 @@ public class ParameterExtractor
// NOTE: general assumption is that ids don't really have much meaning. See [CNX-556: All ID Parameters are send as Name](https://linear.app/speckle/issue/CNX-556/all-id-parameters-are-send-as-name)
// NOTE: subsequent request resulting in certain IDs being brought back. See [CNX-1125](https://linear.app/speckle/issue/CNX-1125/publish-type-id-instead-of-name) in GetValue() method
// "Type ID" (associated with "SYMBOL_ID_PARAM") won't evaluate to true here which is intentional
if (internalDefinitionName.EndsWith("_ID") || internalDefinitionName.EndsWith("_PARAM_ID"))
// NOTE: It seems we are skipping Type Mark parameter because it is called WINDOW_TYPE_ID internally (WTF ADSK).
if (internalDefinitionName.EndsWith("_ID") && !internalDefinitionName.Equals("WINDOW_TYPE_ID"))
{
continue;
}
@@ -4,7 +4,7 @@ using Speckle.Converters.RevitShared.Settings;
using Speckle.Sdk;
using Speckle.Sdk.Common;
namespace Speckle.Converters.Revit2023.ToSpeckle.Properties;
namespace Speckle.Converters.RevitShared.ToSpeckle.Properties;
public readonly struct StructuralAssetProperties(
string name,
@@ -43,16 +43,22 @@ public class MeshByMaterialDictionaryToSpeckle
/// <summary>
/// Converts a dictionary of Revit meshes, where key is MaterialId, into a list of Speckle meshes.
/// </summary>
/// <param name="args">A tuple consisting of (1) a dictionary with DB.ElementId keys and List of DB.Mesh values and (2) the root element id (the one generating all the meshes).</param>
/// <returns>
/// Returns a list of <see cref="SOG.Mesh"/> objects where each mesh represents one unique material in the input dictionary.
/// </returns>
/// <remarks>
/// Be aware that this method internally creates a new instance of <see cref="SOG.Mesh"/> for each unique material in the input dictionary.
/// These meshes are created with an initial capacity based on the size of the vertex and face arrays to avoid unnecessary resizing.
/// Also note that, for each unique material, the method tries to retrieve the related DB.Material from the current document and convert it. If the conversion is successful,
/// the material is added to the corresponding Speckle mesh. If the conversion fails, the operation simply continues without the material.
/// TODO: update description
/// <para>
/// This method creates a new instance of <see cref="SOG.Mesh"/> for each unique material in the input dictionary.
/// </para>
/// <para>
/// For each unique material, the method retrieves the related DB.Material from the current document and converts it to a <see cref="RenderMaterial"/>.
/// Material proxies are created but their objects lists are NOT populated at this stage. The mesh-to-material relationship is stored
/// in <see cref="RevitToSpeckleCacheSingleton.MeshToMaterialMap"/> for later population during display value processing.
/// </para>
/// <para>
/// Deferred population of the object list to ensure that instance geometry references the definition mesh ID in material proxies,
/// rather than individual instance mesh IDs. We can only do this later, because proxification hasn't happened yet.
/// </para>
/// </remarks>
public List<SOG.Mesh> Convert(
(Dictionary<DB.ElementId, List<DB.Mesh>> target, DB.ElementId parentElementId, bool makeTransparent) args
@@ -62,7 +68,7 @@ public class MeshByMaterialDictionaryToSpeckle
var objectRenderMaterialProxiesMap = _revitToSpeckleCacheSingleton.ObjectRenderMaterialProxiesMap;
var materialProxyMap = new Dictionary<string, RenderMaterialProxy>();
var key = args.parentElementId.ToString().NotNull();
// ids are same in copy pasted linked models, otherwise we reset the materialProxyMap in cache and only one of the linked model is having the render materials
if (objectRenderMaterialProxiesMap.TryGetValue(key, out var cachedMaterialProxy))
{
materialProxyMap = cachedMaterialProxy;
@@ -85,32 +91,37 @@ public class MeshByMaterialDictionaryToSpeckle
: materialId.ToString().NotNull();
List<DB.Mesh> meshes = keyValuePair.Value;
// use the meshlist converter to convert the mesh values into a single speckle mesh
SOG.Mesh speckleMesh = _meshListConverter.Convert(meshes);
speckleMesh.applicationId = Guid.NewGuid().ToString(); // NOTE: as we are composing meshes out of multiple ones for the same material, we need to generate our own application id. c'est la vie.
speckleMesh.applicationId = Guid.NewGuid().ToString();
// store mesh-to-material mapping
if (!_revitToSpeckleCacheSingleton.MeshToMaterialMap.TryGetValue(key, out var meshMatMap))
{
meshMatMap = new Dictionary<string, string>();
_revitToSpeckleCacheSingleton.MeshToMaterialMap[key] = meshMatMap;
}
meshMatMap[speckleMesh.applicationId.NotNull()] = materialIdString;
// get the speckle render material
RenderMaterial? renderMaterial = args.makeTransparent
? _transparentMaterial
: _converterSettings.Current.Document.GetElement(materialId) is DB.Material material
? _speckleRenderMaterialConverter.Convert(material)
: null;
// get the render material if any
// Create proxy but DON'T populate objects list yet
if (renderMaterial is not null)
{
if (!materialProxyMap.TryGetValue(materialIdString, out RenderMaterialProxy? renderMaterialProxy))
if (!materialProxyMap.ContainsKey(materialIdString))
{
renderMaterialProxy = new RenderMaterialProxy()
{
value = renderMaterial,
applicationId = materialId.ToString(),
objects = []
};
RenderMaterialProxy? renderMaterialProxy =
new()
{
value = renderMaterial,
applicationId = materialId.ToString(),
objects = []
};
materialProxyMap[materialIdString] = renderMaterialProxy;
}
renderMaterialProxy.objects.Add(speckleMesh.applicationId);
}
result.Add(speckleMesh);
@@ -36,12 +36,9 @@ public class MeshListConversionToSpeckle : ITypedConverter<List<DB.Mesh>, SOG.Me
foreach (DB.XYZ vert in mesh.Vertices)
{
// We need this method to take into account reference point transforms
DB.XYZ extVert = _referencePointConverter.ConvertToExternalCoordinates(vert, true);
vertices.Add(_toSpeckleScalingService.ScaleLength(extVert.X));
vertices.Add(_toSpeckleScalingService.ScaleLength(extVert.Y));
vertices.Add(_toSpeckleScalingService.ScaleLength(extVert.Z));
vertices.Add(_toSpeckleScalingService.ScaleLength(vert.X));
vertices.Add(_toSpeckleScalingService.ScaleLength(vert.Y));
vertices.Add(_toSpeckleScalingService.ScaleLength(vert.Z));
}
for (int i = 0; i < mesh.NumTriangles; i++)
@@ -1,6 +1,7 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
namespace Speckle.Converters.RevitShared.ToSpeckle;
@@ -32,7 +33,10 @@ public sealed class PointcloudToSpeckleConverter : ITypedConverter<DB.PointCloud
{
var filter = DB.PointClouds.PointCloudFilterFactory.CreateMultiPlaneFilter(new List<DB.Plane>() { minPlane });
var points = target.GetPoints(filter, 0.0001, 999999); // max limit is 1 mil but 1000000 throws error
if (points is null)
{
throw new ConversionException("No points found");
}
var specklePointCloud = new SOG.Pointcloud
{
points = points
@@ -1,8 +1,8 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Revit2023.ToSpeckle.Properties;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Converters.RevitShared.ToSpeckle.Properties;
using Speckle.Sdk.Common.Exceptions;
using ApplicationException = Autodesk.Revit.Exceptions.ApplicationException;
@@ -1,12 +1,16 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Common.ToSpeckle;
using Speckle.Converters.RevitShared.Extensions;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Settings;
using Speckle.Converters.RevitShared.ToSpeckle.Properties;
using Speckle.DoubleNumerics;
using Speckle.Objects.Data;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Instances;
namespace Speckle.Converters.RevitShared.ToSpeckle;
@@ -18,9 +22,11 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
private readonly ITypedConverter<DB.Location, Base> _locationConverter;
private readonly LevelExtractor _levelExtractor;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly RevitToSpeckleCacheSingleton _revitToSpeckleCacheSingleton;
public ElementTopLevelConverterToSpeckle(
DisplayValueExtractor displayValueExtractor,
RevitToSpeckleCacheSingleton revitToSpeckleCacheSingleton,
PropertiesExtractor propertiesExtractor,
LevelExtractor levelExtractor,
ITypedConverter<DB.Location, Base> locationConverter,
@@ -28,6 +34,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
)
{
_displayValueExtractor = displayValueExtractor;
_revitToSpeckleCacheSingleton = revitToSpeckleCacheSingleton;
_propertiesExtractor = propertiesExtractor;
_levelExtractor = levelExtractor;
_locationConverter = locationConverter;
@@ -36,7 +43,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
public Base Convert(object target) => Convert((DB.Element)target);
public RevitObject Convert(DB.Element target)
private RevitObject Convert(DB.Element target)
{
string category = target.Category?.Name ?? "none";
@@ -95,7 +102,10 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
}
// get the display value
List<Base> displayValue = _displayValueExtractor.GetDisplayValue(target);
List<DisplayValueResult> displayValuesWithTransforms = _displayValueExtractor.GetDisplayValue(target);
// process display values and create instance proxies where applicable
List<Base> proxifiedDisplayValues = ProcessDisplayValues(target.Id.ToString(), displayValuesWithTransforms);
// get level
string? level = _levelExtractor.GetLevelName(target);
@@ -117,7 +127,7 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
category = category,
location = convertedLocation,
elements = children,
displayValue = displayValue.Cast<Base>().ToList(),
displayValue = proxifiedDisplayValues,
properties = properties,
units = _converterSettings.Current.SpeckleUnits
};
@@ -184,4 +194,128 @@ public class ElementTopLevelConverterToSpeckle : IToSpeckleTopLevelConverter
yield return Convert(_converterSettings.Current.Document.GetElement(childId));
}
}
/// <summary>
/// Processes display values with transforms and creates instance proxies for meshes that can be instanced.
/// Also populates material proxy objects lists with the appropriate mesh IDs based on whether geometry is instanced or not.
/// </summary>
/// <returns>
/// List of processed display values, with meshes replaced by instance proxies where applicable.
/// Non-instance geometry is returned as-is.
/// </returns>
/// <remarks>
/// <para>
/// This is a bit of a code smell. This method is doing to much, "this ... AND this...".
/// </para>
/// <para>
/// But, given a mesh:
/// - if it has a transform, mesh is converted to instance proxy, and the definition mesh ID is added to material proxies
/// - if it doesn't have a transform, it remains as a regular mesh, and its own ID is added to material proxies
/// - other geometry types pass through unchanged
/// </para>
/// <para>
/// This is where material proxy population occurs (deferred from <see cref="MeshByMaterialDictionaryToSpeckle.Convert"/>)
/// to ensure we use definition mesh IDs for instances rather than individual instance mesh IDs.
/// </para>
/// </remarks>
private List<Base> ProcessDisplayValues(string elementId, List<DisplayValueResult> displayValues)
{
List<Base> proxifiedDisplayValues = new();
foreach (var displayValue in displayValues)
{
// check if this is a mesh with a transform - potential instance scenario
if (displayValue.Geometry is SOG.Mesh mesh && displayValue.Transform is not null)
{
var instanceProxy = CreateOrGetInstanceProxy(elementId, mesh, displayValue.Transform.Value);
proxifiedDisplayValues.Add(instanceProxy);
// add the definition mesh ID to material proxy, not the instance mesh
// method technically is a "Try" but logs internally, so we don't have a return to check
_revitToSpeckleCacheSingleton.AddMeshToMaterialProxy(elementId, mesh, isInstance: true);
}
else if (displayValue.Geometry is SOG.Mesh nonInstanceMesh)
{
// non-instance mesh - add its own ID to material proxy
// method technically is a "Try" but logs internally, so we don't have a return to check
_revitToSpeckleCacheSingleton.AddMeshToMaterialProxy(elementId, nonInstanceMesh, isInstance: false);
proxifiedDisplayValues.Add(nonInstanceMesh);
}
else
{
proxifiedDisplayValues.Add(displayValue.Geometry);
}
}
return proxifiedDisplayValues;
}
/// <summary>
/// Creates or retrieves an instance proxy for a mesh, managing instance definitions and caching.
/// </summary>
/// <remarks>
/// <para>
/// This method generates a deterministic instance definition ID based on the untransformed mesh geometry using
/// <see cref="MeshInstanceIdGenerator.GenerateUntransformedMeshId"/>. Multiple instances with identical geometry
/// will share the same definition.
/// </para>
/// <para>
/// The method manages two caches:
/// - <see cref="RevitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap"/>: Tracks instance definitions and which elements use them
/// - <see cref="RevitToSpeckleCacheSingleton.InstancedObjects"/>: Stores the actual definition meshes for later serialization
/// </para>
/// </remarks>
private InstanceProxy CreateOrGetInstanceProxy(string elementId, SOG.Mesh mesh, Matrix4x4 transform)
{
var instanceDefinitionId = MeshInstanceIdGenerator.GenerateUntransformedMeshId(mesh);
// We need to attach element id relationship to proxy singleton for send caching.
// Send caching skips whole DB.Element that turn into RevitDataObject. since we have instance proxies in RevitDataObject but
// its definitions outside of caching mechanism, this elementId helps us to filter which definition proxies should be attached to the root
if (
_revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.TryGetValue(
instanceDefinitionId,
out var instanceDefinition
)
)
{
instanceDefinition.elementIds.Add(elementId);
}
else
{
var newInstanceDefinition = new InstanceDefinitionProxy
{
applicationId = instanceDefinitionId,
objects = new List<string> { mesh.applicationId.NotNull() },
maxDepth = 0,
name = instanceDefinitionId,
};
_revitToSpeckleCacheSingleton.InstanceDefinitionProxiesMap.Add(
instanceDefinitionId,
([elementId], newInstanceDefinition)
);
}
// some comment valid here as above if statement, since we store original meshes outside of RevitDataObject, we need to know which of them will be attached.
if (_revitToSpeckleCacheSingleton.InstancedObjects.TryGetValue(instanceDefinitionId, out var instancedObject))
{
instancedObject.elementIds.Add(elementId);
}
else
{
_revitToSpeckleCacheSingleton.InstancedObjects.Add(instanceDefinitionId, ([elementId], mesh));
}
// create and return instance proxy with transform
var instanceProxy = new InstanceProxy
{
applicationId = Guid.NewGuid().ToString(),
definitionId = instanceDefinitionId,
transform = transform,
maxDepth = 0,
units = mesh.units
};
return instanceProxy;
}
}
@@ -38,6 +38,8 @@ public class BrepToSpeckleConverter : ITypedConverter<RG.Brep, SOG.BrepX>
{
displayValue = displayValue,
encodedValue = brepEncoding,
volume = target.IsSolid ? target.GetVolume() : 0,
area = target.GetArea(),
units = _settingsStore.Current.SpeckleUnits
};
@@ -1,4 +1,4 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
@@ -6,17 +6,14 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
public class EllipseToSpeckleConverter : ITypedConverter<RG.Ellipse, SOG.Ellipse>
{
private readonly ITypedConverter<RG.Plane, SOG.Plane> _planeConverter;
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public EllipseToSpeckleConverter(
ITypedConverter<RG.Plane, SOG.Plane> planeConverter,
ITypedConverter<RG.Box, SOG.Box> boxConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_planeConverter = planeConverter;
_boxConverter = boxConverter;
_settingsStore = settingsStore;
}
@@ -39,8 +36,7 @@ public class EllipseToSpeckleConverter : ITypedConverter<RG.Ellipse, SOG.Ellipse
units = _settingsStore.Current.SpeckleUnits,
domain = SOP.Interval.UnitInterval,
length = nurbsCurve.GetLength(),
area = Math.PI * target.Radius1 * target.Radius2,
bbox = _boxConverter.Convert(new RG.Box(nurbsCurve.GetBoundingBox(true)))
area = Math.PI * target.Radius1 * target.Radius2
};
}
}
@@ -1,3 +1,4 @@
using Rhino.Geometry;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.Rhino.ToSpeckle.Encoding;
@@ -34,8 +35,19 @@ public class ExtrusionToSpeckleConverter : ITypedConverter<RG.Extrusion, SOG.Ext
_settingsStore.Current.Document
);
// get area and volume props
double area = AreaMassProperties.Compute(target).Area;
double volume = 0;
if (target.IsSolid)
{
var volProps = VolumeMassProperties.Compute(target);
volume = volProps.Volume;
}
var bx = new SOG.ExtrusionX()
{
area = area,
volume = volume,
displayValue = displayValue,
encodedValue = extrusionEncoding,
units = _settingsStore.Current.SpeckleUnits
@@ -1,4 +1,4 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
@@ -6,17 +6,14 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
public class LineToSpeckleConverter : ITypedConverter<RG.Line, SOG.Line>, ITypedConverter<RG.LineCurve, SOG.Line>
{
private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public LineToSpeckleConverter(
ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
ITypedConverter<RG.Box, SOG.Box> boxConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_pointConverter = pointConverter;
_boxConverter = boxConverter;
_settingsStore = settingsStore;
}
@@ -34,8 +31,7 @@ public class LineToSpeckleConverter : ITypedConverter<RG.Line, SOG.Line>, ITyped
start = _pointConverter.Convert(target.From),
end = _pointConverter.Convert(target.To),
units = _settingsStore.Current.SpeckleUnits,
domain = new SOP.Interval { start = 0, end = target.Length },
bbox = _boxConverter.Convert(new RG.Box(target.BoundingBox))
domain = new SOP.Interval { start = 0, end = target.Length }
};
public SOG.Line Convert(RG.LineCurve target) => Convert(target.Line);
@@ -1,3 +1,4 @@
using Rhino.Geometry;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Sdk.Common.Exceptions;
@@ -7,15 +8,10 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
[NameAndRankValue(typeof(RG.Mesh), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)]
public class MeshToSpeckleConverter : ITypedConverter<RG.Mesh, SOG.Mesh>
{
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public MeshToSpeckleConverter(
ITypedConverter<RG.Box, SOG.Box> boxConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
public MeshToSpeckleConverter(IConverterSettingsStore<RhinoConversionSettings> settingsStore)
{
_boxConverter = boxConverter;
_settingsStore = settingsStore;
}
@@ -103,8 +99,9 @@ public class MeshToSpeckleConverter : ITypedConverter<RG.Mesh, SOG.Mesh>
}
}
// get area and volume props
double area = AreaMassProperties.Compute(target).Area;
double volume = target.IsClosed ? target.Volume() : 0;
SOG.Box bbox = _boxConverter.Convert(new RG.Box(target.GetBoundingBox(false)));
return new SOG.Mesh
{
@@ -115,7 +112,7 @@ public class MeshToSpeckleConverter : ITypedConverter<RG.Mesh, SOG.Mesh>
vertexNormals = [.. vertexNormals], // this will be empty array when setting is false
units = _settingsStore.Current.SpeckleUnits,
volume = volume,
bbox = bbox
area = area
};
}
}
@@ -9,19 +9,16 @@ public class NurbsCurveConverter : ITypedConverter<RG.NurbsCurve, SOG.Curve>
{
private readonly ITypedConverter<RG.Polyline, SOG.Polyline> _polylineConverter;
private readonly ITypedConverter<RG.Interval, SOP.Interval> _intervalConverter;
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public NurbsCurveConverter(
ITypedConverter<RG.Polyline, SOG.Polyline> polylineConverter,
ITypedConverter<RG.Interval, SOP.Interval> intervalConverter,
ITypedConverter<RG.Box, SOG.Box> boxConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_polylineConverter = polylineConverter;
_intervalConverter = intervalConverter;
_boxConverter = boxConverter;
_settingsStore = settingsStore;
}
@@ -76,8 +73,7 @@ public class NurbsCurveConverter : ITypedConverter<RG.NurbsCurve, SOG.Curve>
rational = nurbsCurve.IsRational,
domain = _intervalConverter.Convert(nurbsCurve.Domain),
closed = nurbsCurve.IsClosed,
length = nurbsCurve.GetLength(),
bbox = _boxConverter.Convert(new RG.Box(nurbsCurve.GetBoundingBox(true)))
length = nurbsCurve.GetLength()
};
return myCurve;
@@ -1,4 +1,4 @@
using Rhino.Geometry.Collections;
using Rhino.Geometry.Collections;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
@@ -6,19 +6,16 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
public class NurbsSurfaceToSpeckleConverter : ITypedConverter<RG.NurbsSurface, SOG.Surface>
{
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly ITypedConverter<RG.Interval, SOP.Interval> _intervalConverter;
private readonly ITypedConverter<RG.ControlPoint, SOG.ControlPoint> _controlPointConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public NurbsSurfaceToSpeckleConverter(
ITypedConverter<RG.Box, SOG.Box> boxConverter,
ITypedConverter<RG.Interval, SOP.Interval> intervalConverter,
ITypedConverter<RG.ControlPoint, SOG.ControlPoint> controlPointConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_boxConverter = boxConverter;
_intervalConverter = intervalConverter;
_controlPointConverter = controlPointConverter;
_settingsStore = settingsStore;
@@ -43,8 +40,7 @@ public class NurbsSurfaceToSpeckleConverter : ITypedConverter<RG.NurbsSurface, S
domainV = _intervalConverter.Convert(target.Domain(1)),
knotsU = target.KnotsU.ToList(),
knotsV = target.KnotsV.ToList(),
units = _settingsStore.Current.SpeckleUnits,
bbox = _boxConverter.Convert(new RG.Box(target.GetBoundingBox(true))),
units = _settingsStore.Current.SpeckleUnits
};
return result;
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Objects;
@@ -9,18 +9,15 @@ public class PolyCurveToSpeckleConverter : ITypedConverter<RG.PolyCurve, SOG.Pol
{
private readonly IServiceProvider _serviceProvider;
private readonly ITypedConverter<RG.Interval, SOP.Interval> _intervalConverter;
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public PolyCurveToSpeckleConverter(
ITypedConverter<RG.Interval, SOP.Interval> intervalConverter,
ITypedConverter<RG.Box, SOG.Box> boxConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore,
IServiceProvider serviceProvider
)
{
_intervalConverter = intervalConverter;
_boxConverter = boxConverter;
_settingsStore = settingsStore;
_serviceProvider = serviceProvider;
}
@@ -44,7 +41,6 @@ public class PolyCurveToSpeckleConverter : ITypedConverter<RG.PolyCurve, SOG.Pol
closed = target.IsClosed,
domain = _intervalConverter.Convert(target.Domain),
length = target.GetLength(),
bbox = _boxConverter.Convert(new RG.Box(target.GetBoundingBox(true))),
segments = target.DuplicateSegments().Select(x => CurveConverter.Value.Convert(x)).ToList(),
units = _settingsStore.Current.SpeckleUnits
};
@@ -7,17 +7,14 @@ public class PolylineToSpeckleConverter
: ITypedConverter<RG.Polyline, SOG.Polyline>,
ITypedConverter<RG.PolylineCurve, SOG.Polyline>
{
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
private readonly ITypedConverter<RG.Interval, SOP.Interval> _intervalConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public PolylineToSpeckleConverter(
ITypedConverter<RG.Box, SOG.Box> boxConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore,
ITypedConverter<RG.Interval, SOP.Interval> intervalConverter
)
{
_boxConverter = boxConverter;
_settingsStore = settingsStore;
_intervalConverter = intervalConverter;
}
@@ -30,8 +27,6 @@ public class PolylineToSpeckleConverter
/// <remarks>⚠️ This conversion assumes domain interval is (0,LENGTH) as Rhino Polylines have no domain. If needed, you may want to use PolylineCurve conversion instead. </remarks>
public SOG.Polyline Convert(RG.Polyline target)
{
var box = _boxConverter.Convert(new RG.Box(target.BoundingBox));
var count = target.IsClosed ? target.Count - 1 : target.Count;
List<double> points = new(count * 3);
for (int i = 0; i < count; i++)
@@ -46,7 +41,6 @@ public class PolylineToSpeckleConverter
{
value = points,
units = _settingsStore.Current.SpeckleUnits,
bbox = box,
length = target.Length,
domain = new() { start = 0, end = target.Length },
closed = target.IsClosed
@@ -1,4 +1,4 @@
using Speckle.Converters.Common;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
@@ -6,15 +6,10 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
public class RawPointCloudToSpeckle : ITypedConverter<RG.PointCloud, SOG.Pointcloud>
{
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
private readonly ITypedConverter<RG.Box, SOG.Box> _boxConverter;
public RawPointCloudToSpeckle(
IConverterSettingsStore<RhinoConversionSettings> settingsStore,
ITypedConverter<RG.Box, SOG.Box> boxConverter
)
public RawPointCloudToSpeckle(IConverterSettingsStore<RhinoConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
_boxConverter = boxConverter;
}
/// <summary>
@@ -27,7 +22,6 @@ public class RawPointCloudToSpeckle : ITypedConverter<RG.PointCloud, SOG.Pointcl
{
points = target.GetPoints().SelectMany(pt => new[] { pt.X, pt.Y, pt.Z }).ToList(),
colors = target.GetColors().Select(o => o.ToArgb()).ToList(),
bbox = _boxConverter.Convert(new RG.Box(target.GetBoundingBox(true))),
units = _settingsStore.Current.SpeckleUnits
};
}
@@ -6,17 +6,14 @@ namespace Speckle.Converters.Rhino.ToSpeckle.Raw;
public class TextEntityToSpeckleConverter : ITypedConverter<RG.TextEntity, SA.Text>
{
private readonly ITypedConverter<RG.Point3d, SOG.Point> _pointConverter;
private readonly ITypedConverter<RG.Plane, SOG.Plane> _planeConverter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _settingsStore;
public TextEntityToSpeckleConverter(
ITypedConverter<RG.Point3d, SOG.Point> pointConverter,
ITypedConverter<RG.Plane, SOG.Plane> planeConverter,
IConverterSettingsStore<RhinoConversionSettings> settingsStore
)
{
_pointConverter = pointConverter;
_planeConverter = planeConverter;
_settingsStore = settingsStore;
}
+1 -1
View File
@@ -16,7 +16,7 @@
<PackageVersion Include="LibTessDotNet" Version="1.1.15" />
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="Microsoft.Build" Version="17.11.4" />
<PackageVersion Include="Microsoft.Build" Version="17.11.48" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="Npgsql" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.9" />
@@ -1,31 +0,0 @@
using Speckle.Sdk.SQLite;
namespace Speckle.Importers.Ifc.Tester;
public sealed class DummySendCacheManager(Dictionary<string, string> objects) : ISqLiteJsonCacheManager
{
public void Dispose() { }
public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException();
public void DeleteObject(string id) => throw new NotImplementedException();
public string? GetObject(string id) => null;
public void SaveObject(string id, string json) => throw new NotImplementedException();
public bool HasObject(string objectId) => false;
#pragma warning disable CA1065
public string Path => throw new NotImplementedException();
#pragma warning restore CA1065
public void SaveObjects(IEnumerable<(string id, string json)> items)
{
foreach (var (id, json) in items)
{
objects[id] = json;
}
}
public void UpdateObject(string id, string json) => throw new NotImplementedException();
}
@@ -1,43 +0,0 @@
using System.Text;
using Speckle.Sdk.Serialisation.V2;
using Speckle.Sdk.Serialisation.V2.Send;
using Speckle.Sdk.Transports;
namespace Speckle.Importers.Ifc.Tester;
public class DummyServerObjectManager : IServerObjectManager
{
public IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
public Task<string?> DownloadSingleObject(
string objectId,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
public Task<Dictionary<string, bool>> HasObjects(
IReadOnlyCollection<string> objectIds,
CancellationToken cancellationToken
) => Task.FromResult(objectIds.ToDictionary(id => id, id => false));
public Task UploadObjects(
IReadOnlyList<BaseItem> objects,
bool compressPayloads,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
)
{
long totalBytes = 0;
foreach (var item in objects)
{
totalBytes += Encoding.Default.GetByteCount(item.Json.Value);
}
progress?.Report(new(ProgressEvent.UploadBytes, totalBytes, totalBytes));
return Task.CompletedTask;
}
}
@@ -1,66 +0,0 @@
#pragma warning disable CA1506
using System.Diagnostics;
using Ara3D.Utils;
//using JetBrains.Profiler.SelfApi;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Importers.Ifc;
using Speckle.Importers.Ifc.Ara3D.IfcParser;
using Speckle.Importers.Ifc.Converters;
using Speckle.Importers.Ifc.Tester;
using Speckle.Importers.Ifc.Types;
using Speckle.Sdk.Serialisation.V2;
using Speckle.Sdk.Serialisation.V2.Send;
using Speckle.Sdk.SQLite;
var serviceProvider = Import.GetServiceProvider();
//DotMemory.Init();
var filePath = new FilePath(
//"C:\\Users\\adam\\Git\\speckle-server\\packages\\fileimport-service\\ifc-dotnet\\ifcs\\20210221PRIMARK.ifc"
//"C:\\Users\\adam\\Git\\speckle-server\\packages\\fileimport-service\\ifc-dotnet\\ifcs\\231110ADT-FZK-Haus-2005-2006.ifc"
//"C:\\Users\\adam\\Downloads\\T03PV06IMPMI01C.ifc"
"C:\\Users\\adam\\Downloads\\20231128_HW_Bouwkosten.ifc"
);
var ifcFactory = serviceProvider.GetRequiredService<IIfcFactory>();
var stopwatch = Stopwatch.StartNew();
Console.WriteLine($"Opening with WebIFC: {filePath}");
var model = ifcFactory.Open(filePath);
var ms = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Opened with WebIFC: {ms} ms");
var graph = IfcGraph.Load(new FilePath(filePath));
var ms2 = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Loaded with StepParser: {ms2 - ms} ms");
var converter = serviceProvider.GetRequiredService<IGraphConverter>();
var b = converter.Convert(model, graph);
ms = ms2;
ms2 = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Converted to Speckle Bases: {ms2 - ms} ms");
var factory = serviceProvider.GetRequiredService<ISerializeProcessFactory>();
var cache = $"C:\\Users\\adam\\Git\\temp\\{Guid.NewGuid()}.db";
using var sqlite = SqLiteJsonCacheManager.FromFilePath(cache, 2);
await using var process2 = factory.CreateSerializeProcess(
sqlite,
new DummyServerObjectManager(),
new Progress(true),
default,
new SerializeProcessOptions(SkipServer: true)
);
Console.WriteLine($"Caching to Speckle: {cache}");
/*var config = new DotMemory.Config();
config.OpenDotMemory();
config.SaveToDir("C:\\Users\\adam\\dotTraceSnapshots");
DotMemory.Attach(config);
DotMemory.GetSnapshot("Before");*/
var (rootId, _) = await process2.Serialize(b).ConfigureAwait(false);
Console.WriteLine(rootId);
ms2 = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Converted to JSON: {ms2 - ms} ms");
//DotMemory.GetSnapshot("After");
//DotMemory.Detach();
#pragma warning restore CA1506
@@ -1,36 +0,0 @@
using Speckle.Sdk.Transports;
namespace Speckle.Importers.Ifc.Tester;
public class Progress(bool write) : IProgress<ProgressArgs>
{
private readonly TimeSpan _debounce = TimeSpan.FromMilliseconds(1000);
private DateTime _lastTime = DateTime.UtcNow;
private long _totalBytes;
public void Report(ProgressArgs value)
{
if (write)
{
if (value.ProgressEvent == ProgressEvent.DownloadBytes)
{
Interlocked.Add(ref _totalBytes, value.Count);
}
var now = DateTime.UtcNow;
if (now - _lastTime >= _debounce)
{
if (value.ProgressEvent == ProgressEvent.DownloadBytes)
{
Console.WriteLine(value.ProgressEvent + " t " + _totalBytes);
}
else
{
Console.WriteLine(value.ProgressEvent + " c " + value.Count + " t " + value.Total);
}
_lastTime = now;
}
}
}
}
@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release;Local</Configurations>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Speckle.Importers.Ifc\Speckle.Importers.Ifc.csproj" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More