diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d9d5124..edcfb24 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,8 +14,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- python: ['3.8', '3.9', '3.10']
+ os: [windows-latest]
+ python: ['3.9', '3.10', '3.11']
steps:
- uses: compas-dev/compas-actions.build@v3
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 967f740..b005a12 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,8 +10,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- python: ['3.8', '3.9', '3.10']
+ os: [windows-latest]
+ python: ['3.9', '3.10', '3.11']
steps:
- uses: compas-dev/compas-actions.build@v3
diff --git a/.gitignore b/.gitignore
index a751b83..723ee34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,6 @@ __pycache__/
# Distribution / packaging
.Python
env/
-build/
develop-eggs/
dist/
downloads/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7648fa6..e86526b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
+- class Model_3dec inherited from compas_model.
### Changed
### Removed
+- big cleaning, update documentation
diff --git a/docs/_images/.$roadmap.drawio.dtmp b/docs/_images/.$roadmap.drawio.dtmp
new file mode 100644
index 0000000..da5fe67
--- /dev/null
+++ b/docs/_images/.$roadmap.drawio.dtmp
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/_images/roadmap.drawio b/docs/_images/roadmap.drawio
new file mode 100644
index 0000000..a3ad0e7
--- /dev/null
+++ b/docs/_images/roadmap.drawio
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/api.rst b/docs/api.rst
index 091df9d..0b86d34 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -4,5 +4,7 @@ API Reference
.. toctree::
:maxdepth: 1
+ :titlesonly:
+
+ api/compas_3dec.datastructure
- api/compas_3dec
diff --git a/docs/api/compas_3dec.datastructure.rst b/docs/api/compas_3dec.datastructure.rst
new file mode 100644
index 0000000..0ef54a8
--- /dev/null
+++ b/docs/api/compas_3dec.datastructure.rst
@@ -0,0 +1,15 @@
+********************************************************************************
+datastructure
+********************************************************************************
+
+
+.. currentmodule:: compas_3dec.datastructure
+
+Classes
+=======
+
+.. autosummary::
+ :toctree: generated/
+ :nosignatures:
+
+ Problem3dec
\ No newline at end of file
diff --git a/docs/api/compas_3dec.rst b/docs/api/compas_3dec.rst
index b1f9946..a01a4b5 100644
--- a/docs/api/compas_3dec.rst
+++ b/docs/api/compas_3dec.rst
@@ -2,8 +2,11 @@
compas_3dec
********************************************************************************
+
.. currentmodule:: compas_3dec
.. toctree::
:maxdepth: 1
+
+ compas_3dec.datastructure
diff --git a/docs/examples/Arch/00_create_block_model.py b/docs/examples/Arch/00_create_block_model.py
new file mode 100644
index 0000000..5e41580
--- /dev/null
+++ b/docs/examples/Arch/00_create_block_model.py
@@ -0,0 +1,40 @@
+#! python 3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas.colors import Color
+from compas.geometry import Point, Line
+from compas_dem.templates import ArchTemplate
+from compas_dem.models import BlockModel
+from compas.scene import Scene
+
+
+# =============================================================================
+# Create Arch
+# =============================================================================
+rise = 2
+span = 5
+thickness = 0.3
+depth = 0.3
+n = 20
+arch = BlockModel.from_template(ArchTemplate(rise,span,thickness,depth,n))
+
+# =============================================================================
+# Save Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_O = os.path.join(HERE, 'arch_00.json')
+compas.json_dump(arch, FILE_O)
+
+# =============================================================================
+# View in Rhino
+# =============================================================================
+scene = Scene()
+scene.clear_context()
+import rhinoscriptsyntax as rs
+for block in arch.blocks():
+ scene.add(block._geometry)
+ rs.AddTextDot(block.graphnode,block._geometry.centroid())
+scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Arch/01_add_supports.py b/docs/examples/Arch/01_add_supports.py
new file mode 100644
index 0000000..2ec28db
--- /dev/null
+++ b/docs/examples/Arch/01_add_supports.py
@@ -0,0 +1,41 @@
+#! python 3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas.scene import Scene
+from compas_dem.models.blockmodel import BlockModel
+
+# =============================================================================
+# Load Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE = os.path.join(HERE, 'arch_00.json')
+arch = compas.json_load(FILE)
+
+# =============================================================================
+# Select supports
+# =============================================================================
+supports = [0,19]
+for block in arch.blocks():
+ if block.graphnode in supports:
+ block.is_support = True
+
+# =============================================================================
+# Save Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_O = os.path.join(HERE, 'arch_01.json')
+compas.json_dump(arch, FILE_O)
+
+# =============================================================================
+# View in Rhino
+# =============================================================================
+scene = Scene()
+scene.clear_context()
+for block in arch.blocks():
+ scene.add(block._geometry)
+for support in arch.supports():
+ scene.add(support._geometry, color= (255,0,0))
+scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Arch/02_to_3dec.py b/docs/examples/Arch/02_to_3dec.py
new file mode 100644
index 0000000..3fedfc2
--- /dev/null
+++ b/docs/examples/Arch/02_to_3dec.py
@@ -0,0 +1,74 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas.colors import Color
+from compas.scene import Scene
+from compas_dem.models import BlockModel
+from compas_3dec.datastructure import Problem3dec, Group, MohrCoulomb
+
+# =============================================================================
+# Load Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE = os.path.join(HERE, 'arch_01.json')
+arch: BlockModel= compas.json_load(FILE)
+
+# =============================================================================
+# Convert to 3DEC model
+# =============================================================================
+problem: Problem3dec = Problem3dec.from_blockmodel(arch, working_path=HERE)
+
+# =============================================================================
+# add/assign groups
+# =============================================================================
+block_group = Group(name="Blocks")
+support_group = Group(name="Supports")
+problem.add_group(block_group)
+problem.add_group(support_group)
+
+for b in problem.blocks:
+ if not b.is_support:
+ b.group = block_group.name
+ else:
+ b.group = support_group.name
+ b.color = Color.from_rgb255(89,154,255)
+
+# =============================================================================
+# add material
+# =============================================================================
+marble = problem.add_material(name="Marble", E=2.5e10, poisson=0.2, rho=2500, group = [block_group.name, support_group.name])
+
+# =============================================================================
+# add contact_properties
+# =============================================================================
+stiffness = problem.set_joint_stiffness_one_material(
+ block_height=0.5,
+ reduction_factor=1,
+ block_length=None,
+ material=marble)
+
+failure_criteria = MohrCoulomb(friction=35)
+contact_property = problem.add_contact_property(stiffness, failure_criteria, [block_group.name, support_group.name])
+
+# =============================================================================
+# Save problem_init
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_init.json')
+compas.json_dump(problem, FILE_O)
+end = time.time()
+print('time', end - start,'s')
+
+# =============================================================================
+# View
+# =============================================================================
+scene = Scene()
+scene.clear_context()
+for block in problem.blocks:
+ scene.add(block.mesh, color=block.color)
+scene.draw()
+
diff --git a/docs/examples/Arch/03_gravity.py b/docs/examples/Arch/03_gravity.py
new file mode 100644
index 0000000..62d2e48
--- /dev/null
+++ b/docs/examples/Arch/03_gravity.py
@@ -0,0 +1,50 @@
+#! python 3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas_3dec.datastructure.problem_3dec import Problem3dec
+
+# =============================================================================
+# Input problem_init
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I = os.path.join(HERE, 'problem_init.json')
+problem: Problem3dec = compas.json_load(FILE_I)
+
+# =============================================================================
+# To 3DEC geometry
+# =============================================================================
+problem.to_geometry_3dec()
+
+# =============================================================================
+# create gravity.dat
+# =============================================================================
+gravity_file = problem.gravity()
+
+# =============================================================================
+# run 3DEC
+# =============================================================================
+problem.run([gravity_file])
+
+
+# # =============================================================================
+# # read results blocks and interactions
+# # =============================================================================
+init_dict = problem.from_3dec_blocks("init_state.txt")
+mapping_dict = problem.mapping(init_dict)
+grav_dict = problem.from_3dec_blocks("grav_state.txt")
+problem.solve_ratio_check("grav_state.txt")
+problem.update_blocks(grav_dict,mapping_dict)
+output_3dec_per_vertex = problem.from_3dec_contacts("contact_grav.txt")
+
+# =============================================================================
+# save problem gravity
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_gravity.json')
+compas.json_dump(problem, FILE_O)
+end = time.time()
+print('analysis_3dec time', end - start, 's')
\ No newline at end of file
diff --git a/docs/examples/Arch/04_view_results.py b/docs/examples/Arch/04_view_results.py
new file mode 100644
index 0000000..be723d4
--- /dev/null
+++ b/docs/examples/Arch/04_view_results.py
@@ -0,0 +1,59 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas_3dec.datastructure import Problem3dec, Interaction3dec
+from compas.colors import Color
+
+# =============================================================================
+# Input problem init
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I1 = os.path.join(HERE, 'problem_init.json')
+problem_init: Problem3dec = compas.json_load(FILE_I1)
+
+# =============================================================================
+# Input problem gravity
+# =============================================================================
+FILE_I2 = os.path.join(HERE, 'problem_gravity.json')
+problem_gravity: Problem3dec = compas.json_load(FILE_I2)
+
+# =============================================================================
+# View
+# =============================================================================
+from compas.scene import Scene
+scene = Scene()
+scene.clear_context()
+
+# =============================================================================
+# show blocks
+# =============================================================================
+for block in problem_gravity.blocks:
+ scene.add(block.mesh, color = block.color_equilibrium)
+
+# =============================================================================
+# show contact forces
+# =============================================================================
+for interaction in problem_gravity.interactions:
+ interaction: Interaction3dec
+ if interaction.forces_per_contact:
+ application_point = interaction.display_resultant_application_point()
+ resultant_force = interaction.display_resultant_force(0.02)
+ scene.add(application_point)
+ for line in resultant_force:
+ scene.add(line, color=(0, 81, 12), width=20.0)
+
+# =============================================================================
+# show support reaction magnitudes
+# =============================================================================
+resultants, magnitudes, components, resultant_points = problem_gravity.support_resultants(0.02)
+import rhinoscriptsyntax as rs
+for magnitude, point in zip(magnitudes, resultant_points):
+ rs.AddTextDot(str(magnitude), point)
+
+# =============================================================================
+# draw geometry
+# =============================================================================
+scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Arch/arch.py b/docs/examples/Arch/arch.py
new file mode 100644
index 0000000..bcd161d
--- /dev/null
+++ b/docs/examples/Arch/arch.py
@@ -0,0 +1,159 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas.colors import Color
+from compas.geometry import Point, Line
+from compas_dem.templates import ArchTemplate
+from compas_dem.models import BlockModel
+from compas_3dec.datastructure import Problem3dec, Group, MohrCoulomb, Interaction3dec
+
+# =============================================================================
+# Create Arch
+# =============================================================================
+rise = 2
+span = 5
+thickness = 0.3
+depth = 0.3
+n = 20
+arch = BlockModel.from_template(ArchTemplate(rise,span,thickness,depth,n))
+
+# =============================================================================
+# Select supports
+# =============================================================================
+supports = [0,19]
+for block in arch.blocks():
+ if block.graphnode in supports:
+ block.is_support = True
+
+# =============================================================================
+# Convert to 3DEC model
+# =============================================================================
+HERE = os.path.dirname(__file__)
+problem: Problem3dec = Problem3dec.from_blockmodel(arch, working_path=HERE)
+
+# =============================================================================
+# add/assign groups
+# =============================================================================
+block_group = Group(name="Blocks")
+support_group = Group(name="Supports")
+problem.add_group(block_group)
+problem.add_group(support_group)
+
+for b in problem.blocks:
+ if not b.is_support:
+ b.group = block_group.name
+ else:
+ b.group = support_group.name
+ b.color = Color.from_rgb255(89,154,255)
+
+# =============================================================================
+# add material
+# =============================================================================
+marble = problem.add_material(name="Marble", E=2.5e10, poisson=0.2, rho=2500, group = [block_group.name, support_group.name])
+
+# =============================================================================
+# add contact_properties
+# =============================================================================
+stiffness = problem.set_joint_stiffness_one_material(
+ block_height=0.5,
+ reduction_factor=1,
+ block_length=None,
+ material=marble)
+
+failure_criteria = MohrCoulomb(friction=35)
+contact_property = problem.add_contact_property(stiffness, failure_criteria, [block_group.name, support_group.name])
+
+# =============================================================================
+# Save problem_init
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_init.json')
+compas.json_dump(problem, FILE_O)
+
+# =============================================================================
+# To 3DEC geometry
+# =============================================================================
+problem.to_geometry_3dec()
+
+# =============================================================================
+# create gravity.dat
+# =============================================================================
+gravity_file = problem.gravity()
+
+# =============================================================================
+# run 3DEC
+# =============================================================================
+problem.run([gravity_file])
+
+# # =============================================================================
+# # read results blocks and interactions
+# # =============================================================================
+init_dict = problem.from_3dec_blocks("init_state.txt")
+mapping_dict = problem.mapping(init_dict)
+grav_dict = problem.from_3dec_blocks("grav_state.txt")
+problem.solve_ratio_check("grav_state.txt")
+problem.update_blocks(grav_dict,mapping_dict)
+output_3dec_per_vertex = problem.from_3dec_contacts("contact_grav.txt")
+
+# =============================================================================
+# save problem gravity
+# =============================================================================
+FILE_Og = os.path.join(HERE, 'problem_gravity.json')
+compas.json_dump(problem, FILE_Og)
+
+# =============================================================================
+# Input problem init
+# =============================================================================
+FILE_I1 = os.path.join(HERE, 'problem_init.json')
+problem_init: Problem3dec = compas.json_load(FILE_I1)
+
+# =============================================================================
+# Input problem gravity
+# =============================================================================
+FILE_I2 = os.path.join(HERE, 'problem_gravity.json')
+problem_gravity: Problem3dec = compas.json_load(FILE_I2)
+
+# =============================================================================
+# View
+# =============================================================================
+from compas.scene import Scene
+scene = Scene()
+scene.clear_context()
+
+# =============================================================================
+# show blocks
+# =============================================================================
+for block in problem_gravity.blocks:
+ scene.add(block.mesh, color = block.color_equilibrium)
+
+# =============================================================================
+# show contact forces
+# =============================================================================
+for interaction in problem_gravity.interactions:
+ interaction: Interaction3dec
+ if interaction.forces_per_contact:
+ application_point = interaction.display_resultant_application_point()
+ resultant_force = interaction.display_resultant_force(0.02)
+ scene.add(application_point)
+ for line in resultant_force:
+ scene.add(line, color=(0, 81, 12), width=20.0)
+
+# =============================================================================
+# show support reaction magnitudes
+# =============================================================================
+resultants, magnitudes, components, resultant_points = problem_gravity.support_resultants(0.02)
+import rhinoscriptsyntax as rs
+for magnitude, point in zip(magnitudes, resultant_points):
+ rs.AddTextDot(str(magnitude), point)
+
+# =============================================================================
+# draw geometry
+# =============================================================================
+scene.draw()
+
+end = time.time()
+print('analysis_3dec time', end - start, 's')
\ No newline at end of file
diff --git a/docs/examples/Barrel_Vault/00_create_block_model.py b/docs/examples/Barrel_Vault/00_create_block_model.py
new file mode 100644
index 0000000..63c8066
--- /dev/null
+++ b/docs/examples/Barrel_Vault/00_create_block_model.py
@@ -0,0 +1,43 @@
+#! python 3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas.colors import Color
+from compas.geometry import Point, Line
+from compas_dem.templates import BarrelVaultTemplate
+from compas_dem.models import BlockModel
+from compas.scene import Scene
+
+
+# =============================================================================
+# Create Barrel Vault
+# =============================================================================
+rise = 2
+span = 5
+length = 6
+thickness = 0.3
+vou_span = 10
+vou_length = 5
+barrel_vault = BlockModel.from_template(BarrelVaultTemplate(span, length, thickness, rise, vou_span, vou_length, False))
+
+# =============================================================================
+# Save Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_O = os.path.join(HERE, 'barrel_vault_00.json')
+compas.json_dump(barrel_vault, FILE_O)
+
+# =============================================================================
+# View in Rhino
+# =============================================================================
+# scene = Scene()
+# scene.clear_context()
+# # import rhinoscriptsyntax as rs
+# for block in barrel_vault.blocks():
+# scene.add(block._geometry)
+# scene.add(span_line, color = (255,0,0))
+# scene.add(rise_line, color=(0,255,0))
+# # rs.AddTextDot(block.graphnode,block._geometry.centroid())
+# scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Barrel_Vault/01_add_supports.py b/docs/examples/Barrel_Vault/01_add_supports.py
new file mode 100644
index 0000000..eb15a8b
--- /dev/null
+++ b/docs/examples/Barrel_Vault/01_add_supports.py
@@ -0,0 +1,41 @@
+#! python 3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas.scene import Scene
+from compas_dem.models.blockmodel import BlockModel
+
+# =============================================================================
+# Load Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE = os.path.join(HERE, 'barrel_vault_00.json')
+barrel_vault = compas.json_load(FILE)
+
+# =============================================================================
+# Select supports
+# =============================================================================
+supports = [0,10,20,30,40,53,9,19,29,39,53,54]
+for block in barrel_vault.blocks():
+ if block.graphnode in supports:
+ block.is_support = True
+
+# =============================================================================
+# Save Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_O = os.path.join(HERE, 'barrel_vault_01.json')
+compas.json_dump(barrel_vault, FILE_O)
+
+# =============================================================================
+# View in Rhino
+# =============================================================================
+# scene = Scene()
+# scene.clear_context()
+# for block in barrel_vault.blocks():
+# scene.add(block._geometry)
+# for support in barrel_vault.supports():
+# scene.add(support._geometry, color= (255,0,0))
+# scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Barrel_Vault/02_to_3dec.py b/docs/examples/Barrel_Vault/02_to_3dec.py
new file mode 100644
index 0000000..bed5569
--- /dev/null
+++ b/docs/examples/Barrel_Vault/02_to_3dec.py
@@ -0,0 +1,74 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas.colors import Color
+from compas.scene import Scene
+from compas_dem.models import BlockModel
+from compas_3dec.datastructure import Problem3dec, Group, MohrCoulomb
+
+# =============================================================================
+# Load Blockmodel
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE = os.path.join(HERE, 'barrel_vault_01.json')
+barrel_vault: BlockModel= compas.json_load(FILE)
+
+# =============================================================================
+# Convert to 3DEC model
+# =============================================================================
+problem: Problem3dec = Problem3dec.from_blockmodel(barrel_vault, working_path=HERE)
+
+# =============================================================================
+# add/assign groups
+# =============================================================================
+block_group = Group(name="Blocks")
+support_group = Group(name="Supports")
+problem.add_group(block_group)
+problem.add_group(support_group)
+
+for b in problem.blocks:
+ if not b.is_support:
+ b.group = block_group.name
+ else:
+ b.group = support_group.name
+ b.color = Color.from_rgb255(89,154,255)
+
+# =============================================================================
+# add material
+# =============================================================================
+marble = problem.add_material(name="Marble", E=2.5e10, poisson=0.2, rho=2500, group = [block_group.name, support_group.name])
+
+# =============================================================================
+# add contact_properties
+# =============================================================================
+stiffness = problem.set_joint_stiffness_one_material(
+ block_height=0.5,
+ reduction_factor=1,
+ block_length=None,
+ material=marble)
+
+failure_criteria = MohrCoulomb(friction=35)
+contact_property = problem.add_contact_property(stiffness, failure_criteria, [block_group.name, support_group.name])
+
+# =============================================================================
+# Save problem_init
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_init.json')
+compas.json_dump(problem, FILE_O)
+end = time.time()
+print('time', end - start,'s')
+
+# =============================================================================
+# View
+# =============================================================================
+# scene = Scene()
+# scene.clear_context()
+# for block in problem.blocks:
+# scene.add(block.mesh, color=block.color)
+# scene.draw()
+
diff --git a/docs/examples/Barrel_Vault/03_gravity.py b/docs/examples/Barrel_Vault/03_gravity.py
new file mode 100644
index 0000000..62d2e48
--- /dev/null
+++ b/docs/examples/Barrel_Vault/03_gravity.py
@@ -0,0 +1,50 @@
+#! python 3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas_3dec.datastructure.problem_3dec import Problem3dec
+
+# =============================================================================
+# Input problem_init
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I = os.path.join(HERE, 'problem_init.json')
+problem: Problem3dec = compas.json_load(FILE_I)
+
+# =============================================================================
+# To 3DEC geometry
+# =============================================================================
+problem.to_geometry_3dec()
+
+# =============================================================================
+# create gravity.dat
+# =============================================================================
+gravity_file = problem.gravity()
+
+# =============================================================================
+# run 3DEC
+# =============================================================================
+problem.run([gravity_file])
+
+
+# # =============================================================================
+# # read results blocks and interactions
+# # =============================================================================
+init_dict = problem.from_3dec_blocks("init_state.txt")
+mapping_dict = problem.mapping(init_dict)
+grav_dict = problem.from_3dec_blocks("grav_state.txt")
+problem.solve_ratio_check("grav_state.txt")
+problem.update_blocks(grav_dict,mapping_dict)
+output_3dec_per_vertex = problem.from_3dec_contacts("contact_grav.txt")
+
+# =============================================================================
+# save problem gravity
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_gravity.json')
+compas.json_dump(problem, FILE_O)
+end = time.time()
+print('analysis_3dec time', end - start, 's')
\ No newline at end of file
diff --git a/docs/examples/Barrel_Vault/04_view_results.py b/docs/examples/Barrel_Vault/04_view_results.py
new file mode 100644
index 0000000..1b3d780
--- /dev/null
+++ b/docs/examples/Barrel_Vault/04_view_results.py
@@ -0,0 +1,59 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas_3dec.datastructure import Problem3dec, Interaction3dec
+from compas.colors import Color
+
+# =============================================================================
+# Input problem init
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I1 = os.path.join(HERE, 'problem_init.json')
+problem_init: Problem3dec = compas.json_load(FILE_I1)
+
+# =============================================================================
+# Input problem gravity
+# =============================================================================
+FILE_I2 = os.path.join(HERE, 'problem_gravity.json')
+problem_gravity: Problem3dec = compas.json_load(FILE_I2)
+
+# =============================================================================
+# View
+# =============================================================================
+from compas.scene import Scene
+scene = Scene()
+scene.clear_context()
+
+# =============================================================================
+# show blocks
+# =============================================================================
+for block in problem_gravity.blocks:
+ scene.add(block.mesh, color = block.color_equilibrium)
+
+# =============================================================================
+# show contact forces
+# =============================================================================
+for interaction in problem_gravity.interactions:
+ interaction: Interaction3dec
+ if interaction.forces_per_contact:
+ application_point = interaction.display_resultant_application_point()
+ resultant_force = interaction.display_resultant_force(0.002)
+ scene.add(application_point)
+ for line in resultant_force:
+ scene.add(line, color=(0, 81, 12), width=20.0)
+
+# =============================================================================
+# show support reaction magnitudes
+# =============================================================================
+resultants, magnitudes, components, resultant_points = problem_gravity.support_resultants(0.002)
+import rhinoscriptsyntax as rs
+for magnitude, point in zip(magnitudes, resultant_points):
+ rs.AddTextDot(str(magnitude), point)
+
+# =============================================================================
+# draw geometry
+# =============================================================================
+scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Barrel_Vault/barrel_vault.py b/docs/examples/Barrel_Vault/barrel_vault.py
new file mode 100644
index 0000000..f2a7ba7
--- /dev/null
+++ b/docs/examples/Barrel_Vault/barrel_vault.py
@@ -0,0 +1,160 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas.colors import Color
+from compas.geometry import Point, Line
+from compas_dem.templates import BarrelVaultTemplate
+from compas_dem.models import BlockModel
+from compas_3dec.datastructure import Problem3dec, Group, MohrCoulomb, Interaction3dec
+
+# =============================================================================
+# Create Barrel Vault
+# =============================================================================
+rise = 2
+span = 5
+length = 6
+thickness = 0.3
+vou_span = 10
+vou_length = 5
+barrel_vault = BlockModel.from_template(BarrelVaultTemplate(span, length, thickness, rise, vou_span, vou_length, False))
+
+# =============================================================================
+# Select supports
+# =============================================================================
+supports = [0,10,20,30,40,53,9,19,29,39,53,54]
+for block in barrel_vault.blocks():
+ if block.graphnode in supports:
+ block.is_support = True
+
+# =============================================================================
+# Convert to 3DEC model
+# =============================================================================
+HERE = os.path.dirname(__file__)
+problem: Problem3dec = Problem3dec.from_blockmodel(barrel_vault, working_path=HERE)
+
+# =============================================================================
+# add/assign groups
+# =============================================================================
+block_group = Group(name="Blocks")
+support_group = Group(name="Supports")
+problem.add_group(block_group)
+problem.add_group(support_group)
+
+for b in problem.blocks:
+ if not b.is_support:
+ b.group = block_group.name
+ else:
+ b.group = support_group.name
+ b.color = Color.from_rgb255(89,154,255)
+
+# =============================================================================
+# add material
+# =============================================================================
+marble = problem.add_material(name="Marble", E=2.5e10, poisson=0.2, rho=2500, group = [block_group.name, support_group.name])
+
+# =============================================================================
+# add contact_properties
+# =============================================================================
+stiffness = problem.set_joint_stiffness_one_material(
+ block_height=0.5,
+ reduction_factor=1,
+ block_length=None,
+ material=marble)
+
+failure_criteria = MohrCoulomb(friction=35)
+contact_property = problem.add_contact_property(stiffness, failure_criteria, [block_group.name, support_group.name])
+
+# =============================================================================
+# Save problem_init
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_init.json')
+compas.json_dump(problem, FILE_O)
+
+# =============================================================================
+# To 3DEC geometry
+# =============================================================================
+problem.to_geometry_3dec()
+
+# =============================================================================
+# create gravity.dat
+# =============================================================================
+gravity_file = problem.gravity()
+
+# =============================================================================
+# run 3DEC
+# =============================================================================
+problem.run([gravity_file])
+
+# # =============================================================================
+# # read results blocks and interactions
+# # =============================================================================
+init_dict = problem.from_3dec_blocks("init_state.txt")
+mapping_dict = problem.mapping(init_dict)
+grav_dict = problem.from_3dec_blocks("grav_state.txt")
+problem.solve_ratio_check("grav_state.txt")
+problem.update_blocks(grav_dict,mapping_dict)
+output_3dec_per_vertex = problem.from_3dec_contacts("contact_grav.txt")
+
+# =============================================================================
+# save problem gravity
+# =============================================================================
+FILE_Og = os.path.join(HERE, 'problem_gravity.json')
+compas.json_dump(problem, FILE_Og)
+
+# =============================================================================
+# Input problem init
+# =============================================================================
+FILE_I1 = os.path.join(HERE, 'problem_init.json')
+problem_init: Problem3dec = compas.json_load(FILE_I1)
+
+# =============================================================================
+# Input problem gravity
+# =============================================================================
+FILE_I2 = os.path.join(HERE, 'problem_gravity.json')
+problem_gravity: Problem3dec = compas.json_load(FILE_I2)
+
+# =============================================================================
+# View
+# =============================================================================
+from compas.scene import Scene
+scene = Scene()
+scene.clear_context()
+
+# =============================================================================
+# show blocks
+# =============================================================================
+for block in problem_gravity.blocks:
+ scene.add(block.mesh, color = block.color_equilibrium)
+
+# =============================================================================
+# show contact forces
+# =============================================================================
+for interaction in problem_gravity.interactions:
+ interaction: Interaction3dec
+ if interaction.forces_per_contact:
+ application_point = interaction.display_resultant_application_point()
+ resultant_force = interaction.display_resultant_force(0.002)
+ scene.add(application_point)
+ for line in resultant_force:
+ scene.add(line, color=(0, 81, 12), width=20.0)
+
+# =============================================================================
+# show support reaction magnitudes
+# =============================================================================
+resultants, magnitudes, components, resultant_points = problem_gravity.support_resultants(0.002)
+import rhinoscriptsyntax as rs
+for magnitude, point in zip(magnitudes, resultant_points):
+ rs.AddTextDot(str(magnitude), point)
+
+# =============================================================================
+# draw geometry
+# =============================================================================
+scene.draw()
+
+end = time.time()
+print('analysis_3dec time', end - start, 's')
\ No newline at end of file
diff --git a/docs/examples/Rhino_meshes/00_import_rhino_meshes.py b/docs/examples/Rhino_meshes/00_import_rhino_meshes.py
new file mode 100644
index 0000000..ec57f94
--- /dev/null
+++ b/docs/examples/Rhino_meshes/00_import_rhino_meshes.py
@@ -0,0 +1,25 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+import compas_rhino
+import compas_rhino.objects
+import compas_rhino.conversions
+
+# =============================================================================
+# Input
+# =============================================================================
+guids = compas_rhino.objects.select_meshes('Select blocks')
+meshes = []
+for guid in guids:
+ mesh = compas_rhino.conversions.meshobject_to_compas(guid)
+ meshes.append(mesh)
+
+# =============================================================================
+# Save meshes to json
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE = os.path.join(HERE, 'meshes.json')
+compas.json_dump(meshes, FILE)
\ No newline at end of file
diff --git a/docs/examples/Rhino_meshes/01_to_3dec.py b/docs/examples/Rhino_meshes/01_to_3dec.py
new file mode 100644
index 0000000..e658fdd
--- /dev/null
+++ b/docs/examples/Rhino_meshes/01_to_3dec.py
@@ -0,0 +1,84 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas.colors import Color
+from compas.scene import Scene
+from compas_3dec.datastructure import Problem3dec, Group, MohrCoulomb
+
+# =============================================================================
+# Input meshes
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I = os.path.join(HERE, 'meshes.json')
+meshes = compas.json_load(FILE_I)
+
+# =============================================================================
+# Init Problem3dec
+# =============================================================================
+problem = Problem3dec(working_path=HERE)
+
+# =============================================================================
+# Add blocks
+# =============================================================================
+problem.add_blocks(meshes)
+
+# =============================================================================
+# Define supports based on z coordinate
+# =============================================================================
+for block in problem.blocks:
+ z_coord = block.mesh.vertices_attribute('z')
+ for i in z_coord:
+ if -0.005 <= i <= 0.005:
+ block.is_support = True
+
+# =============================================================================
+# add/assign groups
+# =============================================================================
+block_group = Group(name="Blocks")
+support_group = Group(name="Supports")
+problem.add_group(block_group)
+problem.add_group(support_group)
+
+for b in problem.blocks:
+ if not b.is_support:
+ b.group = block_group.name
+ else:
+ b.group = support_group.name
+ b.color = Color.from_rgb255(89,154,255)
+
+
+# =============================================================================
+# Add material
+# =============================================================================
+marble = problem.add_material(name="Marble", E=2.5e10, poisson=0.2, rho=2500, group = [block_group.name, support_group.name])
+
+# =============================================================================
+# Add contact properties
+# =============================================================================
+stiffness = problem.set_joint_stiffness_one_material(
+ block_height=0.211,
+ reduction_factor=1,
+ block_length=None,
+ material = marble)
+
+failure_criteria = MohrCoulomb(friction=35)
+contact_property = problem.add_contact_property(stiffness, failure_criteria, [block_group.name, support_group.name])
+
+# =============================================================================
+# Save problem init
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_init.json')
+compas.json_dump(problem, FILE_O)
+
+
+# =============================================================================
+# View
+# =============================================================================
+scene = Scene()
+scene.clear_context()
+for block in problem.blocks:
+ scene.add(block.mesh, color=block.color)
+scene.draw()
diff --git a/docs/examples/Rhino_meshes/02_gravity.py b/docs/examples/Rhino_meshes/02_gravity.py
new file mode 100644
index 0000000..b6b6623
--- /dev/null
+++ b/docs/examples/Rhino_meshes/02_gravity.py
@@ -0,0 +1,56 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import time
+start = time.time()
+import os
+import compas
+from compas_3dec.datastructure import Problem3dec
+
+# =============================================================================
+# Input problem_init
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I = os.path.join(HERE, 'problem_init.json')
+problem: Problem3dec = compas.json_load(FILE_I)
+
+for block in problem.blocks:
+ if block.is_support:
+ print(block.name, block.index)
+ else:
+ print(block.index)
+
+# =============================================================================
+# To 3DEC geometry
+# =============================================================================
+problem.to_geometry_3dec()
+
+# =============================================================================
+# create gravity.dat
+# =============================================================================
+gravity_file = problem.gravity()
+
+# =============================================================================
+# run 3DEC
+# =============================================================================
+problem.run([gravity_file])
+
+
+# # =============================================================================
+# # read results blocks and interactions
+# # =============================================================================
+init_dict = problem.from_3dec_blocks("init_state.txt")
+mapping_dict = problem.mapping(init_dict)
+grav_dict = problem.from_3dec_blocks("grav_state.txt")
+problem.solve_ratio_check("grav_state.txt")
+problem.update_blocks(grav_dict,mapping_dict)
+output_3dec_per_vertex = problem.from_3dec_contacts("contact_grav.txt")
+
+# =============================================================================
+# save problem gravity
+# =============================================================================
+FILE_O = os.path.join(HERE, 'problem_gravity.json')
+compas.json_dump(problem, FILE_O)
+end = time.time()
+print('analysis_3dec time', end - start, 's')
\ No newline at end of file
diff --git a/docs/examples/Rhino_meshes/03_view.py b/docs/examples/Rhino_meshes/03_view.py
new file mode 100644
index 0000000..1b3d780
--- /dev/null
+++ b/docs/examples/Rhino_meshes/03_view.py
@@ -0,0 +1,59 @@
+#! python3
+# r: compas, tessagon
+# venv: himass1
+
+import os
+import compas
+from compas_3dec.datastructure import Problem3dec, Interaction3dec
+from compas.colors import Color
+
+# =============================================================================
+# Input problem init
+# =============================================================================
+HERE = os.path.dirname(__file__)
+FILE_I1 = os.path.join(HERE, 'problem_init.json')
+problem_init: Problem3dec = compas.json_load(FILE_I1)
+
+# =============================================================================
+# Input problem gravity
+# =============================================================================
+FILE_I2 = os.path.join(HERE, 'problem_gravity.json')
+problem_gravity: Problem3dec = compas.json_load(FILE_I2)
+
+# =============================================================================
+# View
+# =============================================================================
+from compas.scene import Scene
+scene = Scene()
+scene.clear_context()
+
+# =============================================================================
+# show blocks
+# =============================================================================
+for block in problem_gravity.blocks:
+ scene.add(block.mesh, color = block.color_equilibrium)
+
+# =============================================================================
+# show contact forces
+# =============================================================================
+for interaction in problem_gravity.interactions:
+ interaction: Interaction3dec
+ if interaction.forces_per_contact:
+ application_point = interaction.display_resultant_application_point()
+ resultant_force = interaction.display_resultant_force(0.002)
+ scene.add(application_point)
+ for line in resultant_force:
+ scene.add(line, color=(0, 81, 12), width=20.0)
+
+# =============================================================================
+# show support reaction magnitudes
+# =============================================================================
+resultants, magnitudes, components, resultant_points = problem_gravity.support_resultants(0.002)
+import rhinoscriptsyntax as rs
+for magnitude, point in zip(magnitudes, resultant_points):
+ rs.AddTextDot(str(magnitude), point)
+
+# =============================================================================
+# draw geometry
+# =============================================================================
+scene.draw()
\ No newline at end of file
diff --git a/docs/examples/Rhino_meshes/contact_grav.txt b/docs/examples/Rhino_meshes/contact_grav.txt
new file mode 100644
index 0000000..b47f0ed
--- /dev/null
+++ b/docs/examples/Rhino_meshes/contact_grav.txt
@@ -0,0 +1,11 @@
+**********************************************
+* Logging started at Fri Sep 5 17:51:33 2025
+* By 3dec Version 7.00 Release 161
+*
+*
+* Job Title:
+**********************************************
+3dec>log-file 'init_state.txt'
+**********************************************
+* Logging ended at Fri Sep 5 17:51:33 2025
+**********************************************
diff --git a/docs/installation.rst b/docs/installation.rst
index a2d3ef9..e041b0c 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -1,3 +1,12 @@
********************************************************************************
Installation
********************************************************************************
+
+.. code-block:: bash
+
+ git clone https://github.com/BRG-research/compas_3dec.git
+ cd compas_3dec
+ git fetch --all
+ conda create -n compas_3dec_3_9_10 -c conda-forge python=3.9.10
+ conda activate compas_3dec_3_9_10
+ pip install -r requirements.txt
diff --git a/docs/source/conftest.rst b/docs/source/conftest.rst
new file mode 100644
index 0000000..369e6d9
--- /dev/null
+++ b/docs/source/conftest.rst
@@ -0,0 +1,7 @@
+conftest module
+===============
+
+.. automodule:: conftest
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
new file mode 100644
index 0000000..85c174e
--- /dev/null
+++ b/docs/source/modules.rst
@@ -0,0 +1,9 @@
+compas_3dec
+===========
+
+.. toctree::
+ :maxdepth: 4
+
+ conftest
+ setup
+ tasks
diff --git a/docs/source/setup.rst b/docs/source/setup.rst
new file mode 100644
index 0000000..552eb49
--- /dev/null
+++ b/docs/source/setup.rst
@@ -0,0 +1,7 @@
+setup module
+============
+
+.. automodule:: setup
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/source/tasks.rst b/docs/source/tasks.rst
new file mode 100644
index 0000000..86f6039
--- /dev/null
+++ b/docs/source/tasks.rst
@@ -0,0 +1,7 @@
+tasks module
+============
+
+.. automodule:: tasks
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 164431b..6324e70 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,6 @@
attrs >=17.4
black ==22.12.0
+mistune ==3.0.2
bump2version >=1.0.1
check-manifest >=0.36
compas_invocations
@@ -10,4 +11,14 @@ isort
sphinx_compas2_theme
twine
wheel
+compas >=2.0
+compas_notebook >=0.5
+compas_model >= 0.3
+compas_viewer
+jupyter
-e .
+
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index e69de29..7ec8052 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+compas >=2.0
+compas_notebook >=0.5
+compas_model >= 0.3
+compas_viewer
+jupyter
+
diff --git a/setup.cfg b/setup.cfg
index a443ffd..513dab6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,11 +2,11 @@
universal = 1
[flake8]
-max-line-length = 120
+max-line-length = 400
exclude = */migrations/*
[doc8]
-max-line-length = 120
+max-line-length = 400
ignore = D001
[pydocstyle]
diff --git a/src/compas_3dec/__init__.py b/src/compas_3dec/__init__.py
index 6583453..7722998 100644
--- a/src/compas_3dec/__init__.py
+++ b/src/compas_3dec/__init__.py
@@ -16,6 +16,12 @@
DATA = os.path.abspath(os.path.join(HOME, "data"))
DOCS = os.path.abspath(os.path.join(HOME, "docs"))
TEMP = os.path.abspath(os.path.join(HOME, "temp"))
-
+log = "compas_3dec: 0.1.0"
__all__ = ["HOME", "DATA", "DOCS", "TEMP"]
+
+__all_plugins__ = [
+ "compas_3dec.scene",
+ "compas_3dec.notebook.scene",
+ "compas_3dec.rhino.scene",
+]
diff --git a/docs/examples/PLACEHOLDER b/src/compas_3dec/data/__init__.py
similarity index 100%
rename from docs/examples/PLACEHOLDER
rename to src/compas_3dec/data/__init__.py
diff --git a/src/compas_3dec/datastructure/__init__.py b/src/compas_3dec/datastructure/__init__.py
new file mode 100644
index 0000000..1cfe0b9
--- /dev/null
+++ b/src/compas_3dec/datastructure/__init__.py
@@ -0,0 +1,22 @@
+from .block import Block
+from .group import Group
+from .material import Material
+from .contact_property import ContactProperty
+from .failure_criteria import MohrCoulomb
+from .interaction_3dec import Interaction3dec
+from .rigid_interaction import RigidInteraction
+from .problem_3dec import Problem3dec
+from .boundary_condition import BoundaryCondition
+
+
+all = [
+ "Block",
+ "Group",
+ "Material",
+ "ContactProperty",
+ "MohrCoulomb",
+ "Interaction3dec",
+ "RigidInteraction",
+ "Problem3dec",
+ "BoundaryCondition",
+]
diff --git a/src/compas_3dec/datastructure/block.py b/src/compas_3dec/datastructure/block.py
new file mode 100644
index 0000000..82b117d
--- /dev/null
+++ b/src/compas_3dec/datastructure/block.py
@@ -0,0 +1,108 @@
+from compas.colors import Color
+from compas.data import Data
+from compas.datastructures import Mesh
+
+
+class Block(Data):
+ """
+ Represents a block in a 3DEC model, including geometry, group assignment, and visualization properties.
+
+ Parameters
+ ----------
+ index : int, optional
+ Unique index of the block.
+ mesh : Mesh, optional
+ Mesh object representing the block geometry.
+ is_support : bool, optional
+ Indicates if the block is a support block (default is False).
+ group : str, optional
+ Name of the group to which the block belongs.
+ unbalanced_force_ratio : float, optional
+ Ratio of unbalanced force for the block.
+ color_equilibrium : Color, optional
+ Color used to visualize equilibrium state.
+ color : Color, optional
+ Color used for block visualization.
+ name : str, optional
+ Name of the block.
+
+ Attributes
+ ----------
+ index : int
+ Unique index of the block.
+ mesh : Mesh
+ Mesh object representing the block geometry.
+ is_support : bool
+ Indicates if the block is a support block.
+ group : str
+ Name of the group to which the block belongs.
+ unbalanced_force_ratio : float
+ Ratio of unbalanced force for the block.
+ color_equilibrium : Color
+ Color used to visualize equilibrium state.
+ color : Color
+ Color used for block visualization.
+ name : str
+ Name of the block.
+
+ Examples
+ --------
+ >>> block = Block(index=1, mesh=my_mesh, is_support=True, group="Supports", name="Block1")
+ >>> print(block)
+ Block index: 1, Mesh: , is_support: True, Group: Supports, Unbalanced_Force_Ratio: None, Color_Equilibrium: None, Color: None, Name: Block1
+ """
+
+ def __init__(
+ self,
+ index=None, # type: int
+ mesh=None, # type: Mesh
+ is_support=False, # type: bool
+ group=None, # type: str | None
+ unbalanced_force_ratio=None, # type: float
+ color_equilibrium=None, # type: Color
+ color=None, # type: Color
+ name=None, # type: str | None
+ ):
+ super().__init__()
+ self.index = index
+ self.mesh = mesh
+ self.is_support = is_support
+ self.group = group
+ self.unbalanced_force_ratio = unbalanced_force_ratio
+ self.color_equilibrium = color_equilibrium
+ self.color = color
+ self.name = name
+
+ #
+
+ @property
+ def __data__(self):
+ return {
+ "index": self.index,
+ "mesh": self.mesh,
+ "is_support": self.is_support,
+ "group": self.group,
+ "unbalanced_force_ratio": self.unbalanced_force_ratio,
+ "color_equilibrium": self.color_equilibrium,
+ "color": self.color,
+ "name": self.name,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ mesh = data["mesh"] if Mesh else None
+ color = data["color"] if data["color"] else None
+ color_equilibrium = data["color_equilibrium"] if data["color_equilibrium"] else None
+ return cls(
+ index=data["index"],
+ mesh=mesh,
+ is_support=data["is_support"],
+ group=data["group"],
+ color=color,
+ unbalanced_force_ratio=data["unbalanced_force_ratio"],
+ color_equilibrium=color_equilibrium,
+ name=data["name"],
+ )
+
+ def __str__(self):
+ return f"Block index: {self.index}, Mesh: {self.mesh}, is_support: {self.is_support}, Group: {self.group}, Unbalanced_Force_Ratio: {self.unbalanced_force_ratio}, Color_Equilibrium: {self.color_equilibrium}, Color: {self.color}, Name: {self.name}"
diff --git a/src/compas_3dec/datastructure/boundary_condition.py b/src/compas_3dec/datastructure/boundary_condition.py
new file mode 100644
index 0000000..6aa072c
--- /dev/null
+++ b/src/compas_3dec/datastructure/boundary_condition.py
@@ -0,0 +1,79 @@
+from compas.data import Data
+from compas.geometry import Vector
+
+
+class BoundaryCondition(Data):
+ """
+ Represents a boundary condition for blocks or regions in a 3DEC model.
+
+ Parameters
+ ----------
+ type : str, optional
+ Type of boundary condition (e.g., "displacement", "force", "velocity").
+ point_of_application : str, optional
+ Identifier or description of the point where the boundary condition is applied.
+ region : int, optional
+ Region index to which the boundary condition is applied.
+ magnitude : float, optional
+ Magnitude of the boundary condition (e.g., displacement, force).
+ direction : Vector, optional
+ Direction vector for the boundary condition.
+
+ Attributes
+ ----------
+ type : str
+ Type of boundary condition.
+ point_of_application : str
+ Point where the boundary condition is applied.
+ region : int
+ Region index.
+ magnitude : float
+ Magnitude of the boundary condition.
+ direction : Vector
+ Direction vector.
+
+ Examples
+ --------
+ >>> bc = BoundaryCondition(type="displacement", region=1, magnitude=0.01, direction=[0, 0, -1])
+ >>> print(bc)
+ BoundaryCondition(type=displacement, point_of_application=None, region=1, magnitude=0.01, direction=[0, 0, -1])
+ """
+
+ def __init__(
+ self,
+ type=None, # type: str
+ point_of_application=None, # type: str | None
+ region=None, # type: int | None
+ magnitude=None, # type: float | None
+ direction=None, # type: Vector | None
+ ):
+ super().__init__()
+ self.type = type
+ self.point_of_application = point_of_application
+ self.region = region
+ self.magnitude = magnitude
+ self.direction = direction
+
+ @property
+ def __data__(self):
+ # type: () -> dict
+ return {
+ "type": self.type,
+ "point_of_application": self.point_of_application,
+ "region": self.region,
+ "magnitude": self.magnitude,
+ "direction": self.direction,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ return cls(
+ type=data["type"],
+ point_of_application=data["point_of_application"] if data["point_of_application"] else None,
+ region=data["region"] if data["region"] else None,
+ magnitude=data["magnitude"] if data["magnitude"] else None,
+ direction=data["direction"] if data["direction"] else None,
+ )
+
+ def __str__(self):
+ return f"BoundaryCondition(type={self.type}, point_of_application={self.point_of_application}, region={self.region}, magnitude={self.magnitude}, direction={self.direction})"
diff --git a/src/compas_3dec/datastructure/contact_property.py b/src/compas_3dec/datastructure/contact_property.py
new file mode 100644
index 0000000..f31feea
--- /dev/null
+++ b/src/compas_3dec/datastructure/contact_property.py
@@ -0,0 +1,72 @@
+from compas.data import Data
+from compas_3dec.datastructure.failure_criteria import MohrCoulomb
+
+
+class ContactProperty(Data):
+ """
+ Represents contact properties for block interactions in a 3DEC model.
+
+ Parameters
+ ----------
+ stiffness : tuple[float, float], optional
+ Tuple containing normal and shear stiffness values.
+ failure_criteria : MohrCoulomb, optional
+ Failure criterion assigned to the contact (e.g., Mohr-Coulomb).
+ group : list, optional
+ List of group names or indices associated with the contact property.
+ name : str, optional
+ Name of the contact property.
+
+ Attributes
+ ----------
+ stiffness : tuple[float, float]
+ Normal and shear stiffness values.
+ failure_criteria : MohrCoulomb
+ Failure criterion assigned to the contact.
+ group : list
+ Associated groups.
+ name : str
+ Name of the contact property.
+
+ Examples
+ --------
+ >>> fc = MohrCoulomb(friction=30, cohesion=1000)
+ >>> cp = ContactProperty(stiffness=(1e7, 5e6), failure_criteria=fc, group=["Blocks"], name="DryJoint")
+ >>> print(cp)
+ [Stiffness: (10000000.0, 5000000.0), failure_criteria: [name: MohrCoulomb, friction: 30, cohesion: 1000, dilation: 0, tension: 0], group: ['Blocks'], Name: DryJoint]
+ """
+
+ def __init__(
+ self,
+ stiffness=None, # type: tuple[float, float]
+ failure_criteria=None, # type: MohrCoulomb
+ group=None, # type: list
+ name=None, # type: str
+ ):
+ super().__init__(name)
+ self.stiffness = stiffness
+ self.failure_criteria = failure_criteria
+ self.group = group if group is not None else []
+
+ @property
+ def __data__(self):
+ # type: () -> dict
+ return {
+ "stiffness": self.stiffness,
+ "failure_criteria": self.failure_criteria,
+ "group": self.group,
+ "name": self.name,
+ }
+
+ @classmethod
+ def from_data(cls, data):
+ return cls(
+ stiffness=data["stiffness"],
+ failure_criteria=MohrCoulomb.__from_data__(data["failure_criteria"]) if data["failure_criteria"] else None,
+ group=data["group"] if data["group"] else None,
+ name=data.get("name"),
+ )
+
+ def __repr__(self):
+ # return "[Stiffness: " + str(self.stiffness) + ", failure_criteria: " + str(self.failure_criteria) + ", group: " + str(self.group) + "]"
+ return f"[Stiffness: {self.stiffness}, failure_criteria: {self.failure_criteria}, group: {self.group}, Name: {self.name}]"
diff --git a/src/compas_3dec/datastructure/failure_criteria.py b/src/compas_3dec/datastructure/failure_criteria.py
new file mode 100644
index 0000000..a5eae6c
--- /dev/null
+++ b/src/compas_3dec/datastructure/failure_criteria.py
@@ -0,0 +1,78 @@
+from compas.data import Data
+
+
+class MohrCoulomb(Data):
+ """
+ Represents a Mohr-Coulomb failure criterion for use in 3DEC models.
+
+ Parameters
+ ----------
+ name : str, optional
+ Name of the failure criterion (default is "MohrCoulomb").
+ friction : float, optional
+ Friction angle or coefficient.
+ cohesion : float, optional
+ Cohesion value (default is 0).
+ dilation : float, optional
+ Dilation angle or coefficient (default is 0).
+ tension : float, optional
+ Tensile strength (default is 0).
+
+ Attributes
+ ----------
+ name : str
+ Name of the failure criterion.
+ friction : float
+ Friction angle or coefficient.
+ cohesion : float
+ Cohesion value.
+ dilation : float
+ Dilation angle or coefficient.
+ tension : float
+ Tensile strength.
+
+ Examples
+ --------
+ >>> fc = MohrCoulomb(friction=30, cohesion=1000, dilation=5, tension=0)
+ >>> print(fc)
+ [name: MohrCoulomb, friction: 30, cohesion: 1000, dilation: 5, tension: 0]
+ """
+
+ def __init__(
+ self,
+ name="MohrCoulomb",
+ friction=None, # type: float
+ cohesion=0, # type: float
+ dilation=0, # type: float
+ tension=0, # type: float
+ ):
+ super().__init__(name)
+ self.name = name
+ self.friction = friction
+ self.cohesion = cohesion
+ self.dilation = dilation
+ self.tension = tension
+
+ @property
+ def __data__(self):
+ # type: () -> dict
+ return {
+ "name": self.name,
+ "friction": self.friction,
+ "cohesion": self.cohesion,
+ "dilation": self.dilation,
+ "tension": self.tension,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ return cls(
+ name=data["name"],
+ friction=data["friction"],
+ cohesion=data["cohesion"],
+ dilation=data["dilation"],
+ tension=data["tension"],
+ )
+
+ def __repr__(self):
+ return f"[name: {self.name}, friction: {self.friction}, cohesion: {self.cohesion}, dilation: {self.dilation}, tension: {self.tension}]"
diff --git a/src/compas_3dec/datastructure/group.py b/src/compas_3dec/datastructure/group.py
new file mode 100644
index 0000000..e30b7f4
--- /dev/null
+++ b/src/compas_3dec/datastructure/group.py
@@ -0,0 +1,65 @@
+from compas.data import Data
+
+
+class Group(Data):
+ """
+ Represents a group of blocks or elements in a 3DEC model, with associated material and contact properties.
+
+ Parameters
+ ----------
+ name : str, optional
+ Name of the group.
+ material : str or Material, optional
+ Material assigned to the group.
+ contact_property : str or ContactProperty, optional
+ Contact property assigned to the group.
+
+ Attributes
+ ----------
+ name : str
+ Name of the group.
+ material : str or Material
+ Material assigned to the group.
+ contact_property : str or ContactProperty
+ Contact property assigned to the group.
+
+ Examples
+ --------
+ >>> group = Group(name="Blocks", material="Stone", contact_property="DryJoint")
+ >>> print(group)
+ Group name: Blocks, Material: Stone, Contact_property: DryJoint
+ """
+
+ def __init__(
+ self,
+ name=None, # type: str
+ material=None, # type: str | None
+ contact_property=None, # type: str | None
+ ):
+ super().__init__(name)
+ self.name = name
+ self.material = material
+ self.contact_property = contact_property
+
+ @property
+ def __data__(self):
+ # type: () -> dict
+ return {
+ "name": self.name,
+ "material": self.material,
+ "contact_property": self.contact_property,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ return cls(
+ name=data["name"],
+ material=data["material"] if data["material"] else None,
+ contact_property=data["contact_property"] if data["contact_property"] else None,
+ )
+
+ def __repr__(self):
+ return f"Group(name={self.name})"
+
+ def __str__(self):
+ return f"Group name: {self.name}, Material: {self.material}, Contact_property: {self.contact_property}"
diff --git a/src/compas_3dec/datastructure/interaction_3dec.py b/src/compas_3dec/datastructure/interaction_3dec.py
new file mode 100644
index 0000000..9d6f6a3
--- /dev/null
+++ b/src/compas_3dec/datastructure/interaction_3dec.py
@@ -0,0 +1,379 @@
+from compas.colors import Color, ColorMap
+from compas.data import Data
+from compas.datastructures import Mesh
+from compas.geometry import Point, Vector, Line, Polygon
+
+
+class Interaction3dec(Data):
+ """
+ Represents a contact interaction between blocks in a 3DEC model.
+
+ Parameters
+ ----------
+ neighbours : list, optional
+ List of neighbouring block indices or identifiers involved in the interaction.
+ type : int or str, optional
+ Type or category of the interaction (e.g., contact type).
+ normal : Vector or list[float], optional
+ Normal vector at the contact interface.
+ position : Vector or list[float], optional
+ Position vector of the contact point.
+ contact_geometry : Polygon, Line, or Point, optional
+ Geometry object representing the contact area or line.
+ forces_per_vertices : list[dict], optional
+ List of dictionaries containing force and stress data per contact vertex.
+ forces_per_contact : dict, optional
+ Dictionary containing resultant forces, moments, and application points for the contact.
+
+ Attributes
+ ----------
+ neighbours : list
+ Neighbouring blocks involved in the interaction.
+ type : int or str
+ Type or category of the interaction.
+ normal : Vector or list[float]
+ Normal vector at the contact interface.
+ position : Vector or list[float]
+ Position vector of the contact point.
+ contact_geometry : Polygon, Line, or Point
+ Geometry object representing the contact area or line.
+ forces_per_vertices : list[dict]
+ Force and stress data per contact vertex.
+ forces_per_contact : dict
+ Resultant forces, moments, and application points for the contact.
+
+ Examples
+ --------
+ >>> interaction = Interaction3dec(
+ ... neighbours=[1, 2],
+ ... type=0,
+ ... normal=[0, 0, 1],
+ ... position=[1.0, 2.0, 3.0],
+ ... contact_geometry=Polygon(...),
+ ... forces_per_vertices=[{"position": [1,2,3], "normal_force": [0,0,1], ...}],
+ ... forces_per_contact={"resultant_force": [0,0,100], "resultant_point": [1,2,3]}
+ ... )
+ >>> print(interaction)
+ [type: 0, neighbours: [1, 2], normal: [0, 0, 1], position: [1.0, 2.0, 3.0], ...]
+ """
+
+ def __init__(
+ self,
+ neighbours=None, # type: list | None
+ type=None, # type: int | None
+ normal=None, # type: Vector | list | None
+ position=None, # type: Vector | list | None
+ contact_geometry=None, # type: Polygon | Line | Point | None
+ forces_per_vertices=None, # type: dict | None
+ forces_per_contact=None, # type: dict | None
+ ):
+ super().__init__()
+ self.type = type
+ self.neighbours = neighbours
+ self.normal = normal
+ self.position = position
+ self.contact_geometry = contact_geometry
+ self.forces_per_vertices = forces_per_vertices
+ self.forces_per_contact = forces_per_contact
+
+ @property
+ def __data__(self):
+ # type: () -> dict
+ return {
+ "type": self.type,
+ "neighbours": self.neighbours,
+ "normal": self.normal,
+ "position": self.position,
+ "contact_geometry": self.contact_geometry,
+ "forces_per_vertices": self.forces_per_vertices,
+ "forces_per_contact": self.forces_per_contact,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ return cls(
+ neighbours=data["neighbours"],
+ type=data["type"],
+ normal=data["normal"],
+ position=data["position"],
+ contact_geometry=data["contact_geometry"],
+ forces_per_vertices=data["forces_per_vertices"],
+ forces_per_contact=data["forces_per_contact"],
+ )
+
+ def __repr__(self):
+ return f"[type: {self.type}, neighbours: {self.neighbours}, normal: {self.normal}, position: {self.position}, contact_geometry: {self.contact_geometry}, forces_per_vertices: {self.forces_per_vertices}, forces_per_contact: {self.forces_per_contact}]"
+
+ def display_resultant_force(self, scale_factor=0.005):
+ # if not self.type == "null":
+ if self.forces_per_contact:
+ if "resultant_point" in self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point"])
+ resultant = self.create_lines(application_point, "resultant_force", scale_factor)
+ return resultant
+
+ def display_resultant_application_point(self):
+ # if not self.type == "null":
+ if self.forces_per_contact:
+ if "resultant_point" in self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point"])
+ return application_point
+
+ def display_resultant_shear_application_point(self):
+ # if not self.type == "null":
+ if self.forces_per_contact:
+ if "resultant_point_shear" in self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point_shear"])
+ return application_point
+
+ def display_resultant_normal(self, scale_factor=0.005):
+ if self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point"])
+ resultant = self.create_lines(application_point, "resultant_normal", scale_factor)
+ return resultant
+
+ def display_resultant_shear_transported(self, scale_factor=0.005):
+ if self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point"])
+ resultant = self.create_lines(application_point, "resultant_shear", scale_factor)
+ return resultant
+
+ def display_resultant_shear(self, scale_factor=0.005):
+ if self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point_shear"])
+ resultant = self.create_lines(application_point, "resultant_shear", scale_factor)
+ return resultant
+
+ def display_resultant_torque(self, scale_factor=0.005):
+ if self.forces_per_contact:
+ application_point = Point(*self.forces_per_contact["resultant_point"])
+ resultant = self.create_lines(application_point, "resultant_torque", scale_factor)
+ return resultant
+
+ def create_lines(self, point, force_key, scale_factor):
+ vector = Vector(*self.forces_per_contact[force_key]) * scale_factor
+ return Line.from_point_and_vector(point, vector), Line.from_point_and_vector(point, -vector)
+
+ def display_resultant_forces(
+ self,
+ scale_factor=0.005,
+ resultant=False,
+ application_point=False,
+ normal=False,
+ shear_and_torque=False,
+ shear=False,
+ application_point_shear=False,
+ ):
+ resultant_forces = []
+ application_points = []
+ application_points_shear = []
+ resultant_normal_forces = []
+ resultant_shear_forces = []
+ resultant_torque_forces = []
+ resultant_shear_forces_transported = []
+ return_list = []
+
+ if not self.type == "null":
+ resultant_point_application = Point(
+ self.forces_per_contact["resultant_point"][0],
+ self.forces_per_contact["resultant_point"][1],
+ self.forces_per_contact["resultant_point"][2],
+ )
+ application_points.append(resultant_point_application)
+ if application_point:
+ return_list.append(application_points)
+
+ resultant_point_application_shear = Point(
+ self.forces_per_contact["resultant_point_shear"][0],
+ self.forces_per_contact["resultant_point_shear"][1],
+ self.forces_per_contact["resultant_point_shear"][2],
+ )
+ application_points_shear.append(resultant_point_application_shear)
+ if application_point_shear:
+ return_list.append(application_points_shear)
+
+ if resultant:
+ resultant_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_force"]) * scale_factor
+ )
+ )
+ resultant_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application, -Vector(*self.forces_per_contact["resultant_force"]) * scale_factor
+ )
+ )
+ return_list.append(resultant_forces)
+
+ if normal:
+ resultant_normal_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_normal"]) * scale_factor
+ )
+ )
+ resultant_normal_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application,
+ -Vector(*self.forces_per_contact["resultant_normal"]) * scale_factor,
+ )
+ )
+ return_list.append(resultant_normal_forces)
+
+ if shear:
+ resultant_shear_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application_shear,
+ Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor,
+ )
+ )
+ resultant_shear_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application_shear,
+ -Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor,
+ )
+ )
+ return_list.append(resultant_shear_forces)
+
+ if shear_and_torque:
+ resultant_torque_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_torque"]) * scale_factor
+ )
+ )
+ resultant_torque_forces.append(
+ Line.from_point_and_vector(
+ resultant_point_application,
+ -Vector(*self.forces_per_contact["resultant_torque"]) * scale_factor,
+ )
+ )
+ return_list.append(resultant_torque_forces)
+
+ resultant_shear_forces_transported.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor
+ )
+ )
+ resultant_shear_forces_transported.append(
+ Line.from_point_and_vector(
+ resultant_point_application, -Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor
+ )
+ )
+ return_list.append(resultant_shear_forces_transported)
+
+ return return_list
+
+ def compute_force_display(self, scale_factor=0.005):
+ self.normal_force_lines = []
+ self.shear_force_lines = []
+ self.points = []
+ self.resultant_force = []
+ self.resultant_point = []
+ self.resultant_point_shear = []
+ self.resultant_shear = []
+ self.resultant_normal = []
+ self.resultant_torque = []
+ self.resultant_shear_transported = []
+
+ if "resultant_point" in self.forces_per_contact:
+ resultant_point_application = Point(
+ self.forces_per_contact["resultant_point"][0],
+ self.forces_per_contact["resultant_point"][1],
+ self.forces_per_contact["resultant_point"][2],
+ )
+ else:
+ resultant_point_application = Point(0, 0, 0) # or some other default value
+ # resultant_point_application = Point(self.forces_per_contact["resultant_point"][0],self.forces_per_contact["resultant_point"][1],self.forces_per_contact["resultant_point"][2])
+ self.resultant_point.append(resultant_point_application)
+
+ if "resultant_point_shear" in self.forces_per_contact:
+ resultant_point_application_shear = Point(
+ self.forces_per_contact["resultant_point_shear"][0],
+ self.forces_per_contact["resultant_point_shear"][1],
+ self.forces_per_contact["resultant_point_shear"][2],
+ )
+ else:
+ resultant_point_application_shear = Point(0, 0, 0) # or some other default value
+ # resultant_point_application_shear = Point(self.forces_per_contact["resultant_point_shear"][0],self.forces_per_contact["resultant_point_shear"][1],self.forces_per_contact["resultant_point_shear"][2])
+ self.resultant_point_shear.append(resultant_point_application_shear)
+
+ if "resultant_force" in self.forces_per_contact:
+ self.resultant_force.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_force"]) * scale_factor
+ )
+ )
+ self.resultant_force.append(
+ Line.from_point_and_vector(
+ resultant_point_application, -Vector(*self.forces_per_contact["resultant_force"]) * scale_factor
+ )
+ )
+
+ if "resultant_shear" in self.forces_per_contact:
+ self.resultant_shear.append(
+ Line.from_point_and_vector(
+ resultant_point_application_shear,
+ Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor,
+ )
+ )
+ self.resultant_shear.append(
+ Line.from_point_and_vector(
+ resultant_point_application_shear,
+ -Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor,
+ )
+ )
+
+ if "resultant_normal" in self.forces_per_contact:
+ self.resultant_normal.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_normal"]) * scale_factor
+ )
+ )
+ self.resultant_normal.append(
+ Line.from_point_and_vector(
+ resultant_point_application, -Vector(*self.forces_per_contact["resultant_normal"]) * scale_factor
+ )
+ )
+
+ if "resultant_torque" in self.forces_per_contact:
+ self.resultant_torque.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_torque"]) * scale_factor
+ )
+ )
+ self.resultant_torque.append(
+ Line.from_point_and_vector(
+ resultant_point_application, -Vector(*self.forces_per_contact["resultant_torque"]) * scale_factor
+ )
+ )
+
+ if "resultant_shear" in self.forces_per_contact:
+ self.resultant_shear_transported.append(
+ Line.from_point_and_vector(
+ resultant_point_application, Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor
+ )
+ )
+ self.resultant_shear_transported.append(
+ Line.from_point_and_vector(
+ resultant_point_application, -Vector(*self.forces_per_contact["resultant_shear"]) * scale_factor
+ )
+ )
+
+ cmap = ColorMap.from_two_colors(Color.white(), Color.red())
+ max_normal_stress = 0
+ max_shear_stress = 0
+ for forces_per_vertex in self.forces_per_vertices:
+ position = forces_per_vertex["position"]
+ self.points.append(Point(*position))
+ max_normal_stress = max(max_normal_stress, forces_per_vertex["normal_stress"])
+ max_shear_stress = max(max_shear_stress, forces_per_vertex["shear_stress"])
+
+ normal_force = Vector(*forces_per_vertex["normal_force"])
+ self.normal_force_lines.append(Line.from_point_and_vector(position, normal_force * scale_factor))
+ self.normal_force_lines.append(Line.from_point_and_vector(position, -normal_force * scale_factor))
+
+ shear_force = Vector(*forces_per_vertex["shear_force"])
+ self.shear_force_lines.append(Line.from_point_and_vector(position, shear_force * scale_factor))
+ self.shear_force_lines.append(Line.from_point_and_vector(position, -shear_force * scale_factor))
+
+ self.mesh_normal_stress = Mesh.from_vertices_and_faces(*self.contact_geometry.to_vertices_and_faces())
+ self.mesh_shear_stress = Mesh.from_vertices_and_faces(*self.contact_geometry.to_vertices_and_faces())
diff --git a/src/compas_3dec/datastructure/material.py b/src/compas_3dec/datastructure/material.py
new file mode 100644
index 0000000..4d37461
--- /dev/null
+++ b/src/compas_3dec/datastructure/material.py
@@ -0,0 +1,82 @@
+from compas.data import Data
+
+
+class Material(Data):
+ """
+ Represents a material with mechanical properties for use in 3DEC models.
+
+ Parameters
+ ----------
+ name : str, optional
+ Name of the material.
+ E : float, optional
+ Young's modulus of the material.
+ poisson : float, optional
+ Poisson's ratio of the material.
+ rho : float, optional
+ Density of the material.
+ group : list, optional
+ List of group names or indices associated with the material.
+
+ Attributes
+ ----------
+ name : str
+ Name of the material.
+ E : float
+ Young's modulus.
+ poisson : float
+ Poisson's ratio.
+ rho : float
+ Density.
+ group : list
+ Associated groups.
+
+ Examples
+ --------
+ >>> mat = Material(name="Stone", E=30e9, poisson=0.2, rho=2500)
+ >>> print(mat)
+ Stone E: 30000000000.0 poisson: 0.2 rho: 2500 group: []
+ """
+
+ def __init__(
+ self,
+ name=None, # type: str
+ E=None, # type: float
+ poisson=None, # type: float
+ rho=None, # type: float
+ group=None, # type: list
+ ):
+ super().__init__(name)
+ self.name = name
+ self.E = E
+ self.poisson = poisson
+ self.rho = rho
+ self.group = group if group is not None else []
+
+ @property
+ def __data__(self):
+ return {
+ "name": self.name,
+ "E": self.E,
+ "poisson": self.poisson,
+ "rho": self.rho,
+ "group": self.group,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ return cls(
+ name=data["name"],
+ E=data["E"],
+ poisson=data["poisson"],
+ rho=data["rho"],
+ group=data["group"] if data["group"] else None,
+ )
+
+ def __repr__(self):
+ # return self.name + " E: " + str(self.E) + " poisson: " + str(self.poisson) + " rho: " + str(self.rho) + " group: " + str(self.group)
+ return f"{self.name} E: {self.E} poisson: {self.poisson} rho: {self.rho} group: {self.group}"
+
+ @property
+ def G(self):
+ return self.E / (2 * (1 + self.poisson))
diff --git a/src/compas_3dec/datastructure/problem_3dec.py b/src/compas_3dec/datastructure/problem_3dec.py
new file mode 100644
index 0000000..213d16f
--- /dev/null
+++ b/src/compas_3dec/datastructure/problem_3dec.py
@@ -0,0 +1,2301 @@
+from operator import eq
+import os
+import time
+import inspect
+import compas
+from compas.data import Data
+from subprocess import call
+from compas.datastructures import Mesh
+from compas.geometry import Plane, Frame, Transformation, Line, Polygon, Vector, Point, convex_hull_xy, transform_points
+from compas.geometry import (
+ norm_vector,
+ centroid_points,
+ normalize_vector,
+ cross_vectors,
+ scale_vector,
+ sum_vectors,
+ dot_vectors,
+)
+from compas.colors import Color, ColorMap
+
+
+class Problem3dec(Data):
+ """Class representing a 3DEC problem encapsulating hierarchically organized elements and interactions.
+
+ Attributes
+ ----------
+ input : str, optional
+ Input data for the problem.
+ groups : list
+ Groups of elements in the model.
+ blocks : list
+ Blocks representing geometry.
+ rigid_interactions : list
+ Rigid interactions between blocks.
+ compounds : list
+ Compound structures derived from rigid interactions.
+ materials : list
+ Materials assigned to the model.
+ contact_properties : list
+ Properties defining contact behavior.
+ working_path : str
+ Path to working directory.
+ interactions : list
+ Interactions between elements.
+ boundary_conditions : list
+ Boundary conditions of the model.
+ executable_path : str
+ Path to the 3DEC executable.
+ block_map : dict
+ Mapping of blocks.
+ name : str
+ Name of the problem.
+
+ Notes
+ -----
+ - Model elements are structured in groups, blocks, and compounds.
+ - Interactions and hierarchical relationships are independent.
+ - The interactions define contact mechanics, while hierarchy defines organization.
+ """
+
+ def __init__(
+ self,
+ input=None,
+ groups=None,
+ blocks=None,
+ rigid_interactions=None,
+ compounds=None,
+ materials=None,
+ contact_properties=None,
+ working_path=None,
+ interactions=None,
+ boundary_conditions=None,
+ executable_path='"C:\\Program Files\\Itasca\\3DEC700\\exe64\\3dec700_console.exe"',
+ block_map=None,
+ name=None,
+ ):
+ """
+ Initializes a Problem3dec object.
+
+ Parameters
+ ----------
+ input : str, optional
+ Input data for the problem.
+ groups : list, optional
+ List of groups.
+ blocks : list, optional
+ List of blocks.
+ rigid_interactions : list, optional
+ List of rigid interactions.
+ compounds : list, optional
+ List of compounds.
+ materials : list, optional
+ List of materials.
+ contact_properties : list, optional
+ List of contact properties.
+ working_path : str, optional
+ Working directory path.
+ interactions : list, optional
+ List of interactions.
+ boundary_conditions : list, optional
+ List of boundary conditions.
+ executable_path : str, optional
+ Path to the 3DEC executable.
+ block_map : dict, optional
+ Mapping of blocks.
+ name : str, optional
+ Name of the problem.
+ """
+ super().__init__(name)
+ self.input = input
+ self.groups = groups if groups is not None else []
+ self.executable_path = executable_path
+ self.working_path = working_path
+ self.blocks = blocks if blocks is not None else []
+ self.rigid_interactions = rigid_interactions if rigid_interactions is not None else []
+ self.compounds = compounds if compounds is not None else []
+ self.materials = materials if materials is not None else []
+ self.contact_properties = contact_properties if contact_properties is not None else []
+ self.interactions = interactions if interactions is not None else []
+ self.boundary_conditions = boundary_conditions if boundary_conditions is not None else []
+ self.block_map = block_map if block_map is not None else {}
+ self.name = name
+
+ if not self.working_path:
+ caller_frame = inspect.stack()[-1]
+ caller_filename = caller_frame.filename
+ self.working_path = os.path.dirname(os.path.abspath(caller_filename))
+
+ else:
+ if self.working_path.startswith("file:"):
+ self.working_path = self.working_path[5:] # Remove the 'file:' prefix
+ if "C:\\Program Files\\Rhino 8\\System\\" in self.working_path:
+ self.working_path = self.working_path.split("C:\\Program Files\\Rhino 8\\System\\")[-1]
+ self.working_path = os.path.abspath(self.working_path)
+
+ @property
+ def __data__(self):
+ return {
+ "input": self.input,
+ "groups": [group.__data__ for group in self.groups],
+ "executable_path": self.executable_path,
+ "working_path": self.working_path,
+ "blocks": [block.__data__ for block in self.blocks],
+ "rigid_interactions": [interaction.__data__ for interaction in self.rigid_interactions],
+ "compounds": self.compounds,
+ "materials": [material.__data__ for material in self.materials],
+ "contact_properties": [contact_property.__data__ for contact_property in self.contact_properties],
+ "interactions": [interaction.__data__ for interaction in self.interactions],
+ "boundary_conditions": [boundary_condition.__data__ for boundary_condition in self.boundary_conditions],
+ "block_map": self.block_map,
+ "name": self.name,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ from .group import Group
+ from .block import Block
+ from .rigid_interaction import RigidInteraction
+ from .material import Material
+ from .contact_property import ContactProperty
+ from .interaction_3dec import Interaction3dec
+ from .boundary_condition import BoundaryCondition
+
+ problem = cls(
+ input=data.get("input"),
+ materials=[Material.__from_data__(material) for material in data["materials"]],
+ groups=[Group.__from_data__(group) for group in data["groups"]],
+ rigid_interactions=[
+ RigidInteraction.__from_data__(interaction) for interaction in data["rigid_interactions"]
+ ],
+ contact_properties=[
+ ContactProperty.__from_data__(contact_property) for contact_property in data["contact_properties"]
+ ],
+ blocks=[Block.__from_data__(block) for block in data["blocks"]],
+ interactions=[Interaction3dec.__from_data__(interaction) for interaction in data["interactions"]],
+ boundary_conditions=[
+ BoundaryCondition.__from_data__(boundary_condition)
+ for boundary_condition in data["boundary_conditions"]
+ ],
+ working_path=data["working_path"],
+ executable_path=data["executable_path"],
+ name=data["name"],
+ block_map=data["block_map"],
+ compounds=data["compounds"],
+ )
+ return problem
+
+ def __str__(self):
+ blocks_str = "\n".join(str(block) for block in self.blocks)
+ groups_str = "\n".join(str(group) for group in self.groups)
+ materials_str = "\n".join(str(material) for material in self.materials)
+ interactions_str = "\n".join(str(interaction) for interaction in self.interactions)
+ return f"Blocks:\n{blocks_str}\nGroups:\n{groups_str}\nMaterials:\n{materials_str}\nInteractions:\n{interactions_str}"
+
+ @classmethod
+ def from_blockmodel(cls, blockmodel, working_path=None):
+ """
+ Create a new Problem3dec instance from a compas_dem BlockModel.
+
+ Parameters
+ ----------
+ blockmodel : compas_dem.models.BlockModel
+ The block model containing block elements to be converted.
+ working_path : str, optional
+ The working directory for the new Problem3dec instance. If not provided,
+ the default path logic in the constructor will be used.
+
+ Returns
+ -------
+ Problem3dec
+ A new Problem3dec object populated with blocks from the blockmodel.
+
+ Notes
+ -----
+ - Each block element from the blockmodel is converted to a Block and added to the Problem3dec instance.
+ - The `is_support` attribute is set for blocks marked as support in the blockmodel.
+ - The working path can be set explicitly for consistent file output behavior, especially in environments like Rhino.
+
+ Examples
+ --------
+ >>> from compas_3dec.datastructure.problem_3dec import Problem3dec
+ >>> from compas_dem.models import BlockModel
+ >>> blockmodel = BlockModel.from_json('path/to/blockmodel.json')
+ >>> problem = Problem3dec.from_blockmodel(blockmodel, working_path='C:/my/project')
+ """
+ from .block import Block
+ from compas_dem.models import BlockModel
+
+ problem = cls(working_path=working_path)
+ blockmodel: BlockModel
+ for blockelement in blockmodel.elements():
+ block = Block(blockelement.graphnode, mesh=blockelement._geometry, name=blockelement.name)
+ problem.blocks.append(block)
+ if blockelement.is_support:
+ block.is_support = True
+ return problem
+
+ def add_group(self, group):
+ """
+ Add a group to the Problem3dec instance.
+
+ Parameters
+ ----------
+ group : Group
+ The group object to be added.
+
+ Raises
+ ------
+ ValueError
+ If a group with the same name already exists.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> group = Group(name="Supports")
+ >>> problem.add_group(group)
+ """
+ if group.name in [g.name for g in self.groups]:
+ raise ValueError(f"Group name '{group.name}' already exists.")
+ self.groups.append(group)
+ return True
+
+ def get_group_by_name(self, name):
+ """
+ Retrieve a group from the Problem3dec instance by its name.
+
+ Parameters
+ ----------
+ name : str
+ The name of the group to retrieve.
+
+ Returns
+ -------
+ Group or None
+ The group object with the specified name, or None if no such group exists.
+
+ Raises
+ ------
+ TypeError
+ If the provided name is not a string.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> group = problem.get_group_by_name("Supports")
+ >>> if group:
+ ... print("Group found:", group)
+ ... else:
+ ... print("Group not found.")
+ """
+ if not isinstance(name, str):
+ raise TypeError("Group name must be a string.")
+ for group in self.groups:
+ if group.name.lower() == name.lower():
+ return group
+ return None
+
+ def add_blocks(self, meshes, support_names=None):
+ """
+ Add multiple blocks to the Problem3dec instance from a list of meshes.
+
+ Parameters
+ ----------
+ meshes : list[Mesh]
+ A list of compas Mesh objects to be added as blocks.
+ support_names : list[str], optional
+ List of mesh names that should be marked as supports. Default is ["Supports"].
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> meshes = [mesh1, mesh2, mesh3]
+ >>> problem.add_blocks(meshes, support_names=["Supports", "Base"])
+ """
+ from .block import Block
+
+ if support_names is None:
+ support_names = ["Supports"]
+
+ for i, mesh in enumerate(meshes):
+ block_index = i # Sequential indices: 0, 1, 2, ...
+ block = Block(block_index, mesh, name=mesh.name)
+ if block.name in support_names:
+ block.is_support = True
+ self.blocks.append(block)
+
+ # def add_blocks(self, meshes, support_names=None):
+ # """
+ # Add multiple blocks to the Problem3dec instance from a list of meshes.
+
+ # Parameters
+ # ----------
+ # meshes : list[Mesh]
+ # A list of compas Mesh objects to be added as blocks.
+ # support_names : list[str], optional
+ # List of mesh names that should be marked as supports. Default is ["Supports"].
+
+ # Examples
+ # --------
+ # >>> problem = Problem3dec()
+ # >>> meshes = [mesh1, mesh2, mesh3]
+ # >>> problem.add_blocks(meshes, support_names=["Supports", "Base"])
+ # """
+ # from .block import Block
+
+ # if support_names is None:
+ # support_names = ["Supports"]
+
+ # for i, mesh in enumerate(meshes):
+ # block_index = len(self.blocks) + i
+ # block = Block(block_index, mesh, name=mesh.name)
+ # if block.name in support_names:
+ # block.is_support = True
+ # self.blocks.append(block)
+
+ def add_material(self, name, E, poisson, rho, group=None):
+ """
+ Add a material to the Problem3dec instance and optionally assign it to groups.
+
+ Parameters
+ ----------
+ name : str
+ Name of the material.
+ E : float
+ Young's modulus of the material.
+ poisson : float
+ Poisson's ratio of the material.
+ rho : float
+ Density of the material.
+ group : list[str], optional
+ List of group names to assign this material to. If None, the material is not assigned to any group.
+
+ Returns
+ -------
+ Material
+ The created Material object.
+
+ Raises
+ ------
+ TypeError
+ If group is not a list of strings.
+
+ Notes
+ -----
+ - The material is appended to the model's materials list.
+ - If `group` is provided, the material is assigned to all matching groups by name.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> mat = problem.add_material("Stone", 30e9, 0.2, 2500, group=["Blocks", "Supports"])
+ """
+ from .material import Material
+
+ material = Material(name, E, poisson, rho, group)
+ self.materials.append(material)
+ if group:
+ if not isinstance(group, (list, tuple)) or not all(isinstance(gr, str) for gr in group):
+ raise TypeError("group must be a list of strings.")
+ for gr in set(group):
+ found = False
+ for g in self.groups:
+ if g.name == gr:
+ g.material = material
+ found = True
+ if not found:
+ print(f"Warning: Group '{gr}' not found in Problem3dec.groups.")
+ return material
+
+ def add_contact_property(self, stiffness, failure_criteria, group=None):
+ """
+ Add a contact property to the Problem3dec instance and optionally assign it to groups.
+
+ Parameters
+ ----------
+ stiffness : tuple or list
+ The normal and shear stiffness values for the contact property.
+ failure_criteria : object
+ The failure criteria object (e.g., friction, cohesion) for the contact property.
+ group : list[str], optional
+ List of group names to assign this contact property to. If None, the contact property is not assigned to any group.
+
+ Returns
+ -------
+ ContactProperty
+ The created ContactProperty object.
+
+ Raises
+ ------
+ TypeError
+ If group is not a list of strings.
+
+ Notes
+ -----
+ - The contact property is appended to the model's contact_properties list.
+ - If `group` is provided, the contact property is assigned to all matching groups by name.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> cp = problem.add_contact_property((1e7, 1e6), failure_criteria, group=["Blocks", "Supports"])
+ """
+ from .contact_property import ContactProperty
+
+ contact_property = ContactProperty(stiffness, failure_criteria, group)
+ self.contact_properties.append(contact_property)
+ if group:
+ if not isinstance(group, (list, tuple)) or not all(isinstance(gr, str) for gr in group):
+ raise TypeError("group must be a list of strings.")
+ for gr in set(group):
+ found = False
+ for g in self.groups:
+ if g.name == gr:
+ g.contact_property = contact_property
+ found = True
+ if not found:
+ print(f"Warning: Group '{gr}' not found in Problem3dec.groups.")
+ return contact_property
+
+ def add_rigid_interactions(self, block_lists):
+ """
+ Add a rigid interaction to the Problem3dec instance.
+
+ Parameters
+ ----------
+ block_lists : list[list[int]]
+ A list of lists, where each inner list contains block indices that form a rigid interaction.
+
+ Returns
+ -------
+ RigidInteraction
+ The created RigidInteraction object.
+
+ Raises
+ ------
+ TypeError
+ If block_lists is not a list of lists of integers.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> problem.add_rigid_interactions([[0, 1, 2], [3, 4]])
+ """
+ from .rigid_interaction import RigidInteraction
+
+ if not (
+ isinstance(block_lists, list)
+ and all(isinstance(lst, list) and all(isinstance(i, int) for i in lst) for lst in block_lists)
+ ):
+ raise TypeError("block_lists must be a list of lists of integers.")
+
+ rigid = RigidInteraction(block_lists)
+ self.rigid_interactions.append(rigid)
+ return rigid
+
+ def add_interaction(self, interaction_data):
+ """
+ Add an interaction to the Problem3dec instance.
+
+ Parameters
+ ----------
+ interaction_data : Interaction3dec or dict
+ The interaction object or data to be added to the model.
+
+ Raises
+ ------
+ TypeError
+ If interaction_data is not an Interaction3dec or dict.
+
+ Notes
+ -----
+ - The interaction is appended to the model's interactions list.
+ - Interactions typically represent contact or mechanical relationships between blocks.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> interaction = Interaction3dec(...)
+ >>> problem.add_interaction(interaction)
+ """
+ # Example type check (adjust as needed for your actual interaction type)
+ # from .interaction_3dec import Interaction3dec
+ # if not isinstance(interaction_data, (Interaction3dec, dict)):
+ # raise TypeError("interaction_data must be an Interaction3dec or dict.")
+ self.interactions.append(interaction_data)
+ return True
+
+ def make_compounds(self):
+ """
+ Generate the list of compounds for the Problem3dec instance.
+
+ Compounds are groups of block indices that represent either rigidly joined blocks
+ (from rigid interactions) or individual blocks (if no rigid interactions exist).
+
+ Returns
+ -------
+ list[list[int]]
+ A sorted list of compounds, where each compound is a list of block indices.
+
+ Notes
+ -----
+ - If rigid interactions exist, compounds are extended with those from each RigidInteraction.
+ - Blocks not part of any rigid interaction are added as single-block compounds.
+ - If no rigid interactions exist, each block is added as a separate compound.
+ - The compounds list is sorted by the first block index in each compound.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> problem.make_compounds()
+ [[0], [1], [2], [3, 4]]
+ """
+ self.compounds = []
+ if self.rigid_interactions:
+ # Collect all indices in rigid compounds for fast lookup
+ rigid_indices = set()
+ for interaction in self.rigid_interactions:
+ self.compounds.extend(interaction.compounds)
+ for compound in interaction.compounds:
+ rigid_indices.update(compound)
+
+ # Add blocks that are not part of any interaction
+ for block in self.blocks:
+ if block.index not in rigid_indices:
+ self.compounds.append([block.index])
+
+ # Sort compounds
+ self.compounds = sorted(self.compounds, key=lambda x: x[0])
+ else:
+ # If no rigid interactions, add each block as a separate compound
+ for block in self.blocks:
+ self.compounds.append([block.index])
+
+ return list(self.compounds)
+
+ def to_geometry_3dec(self):
+ """
+ Export the geometry of blocks and supports to a 3DEC-compatible .dat file.
+
+ This method generates the geometry file for 3DEC by processing all block compounds,
+ grouping them as "Blocks" or "Supports" (or by their assigned group), and writing
+ their mesh data in the required format. Compounds of joined blocks are recognized,
+ enabling the creation of Master/Slave compounds in 3DEC.
+
+ Returns
+ -------
+ str
+ The path to the created geometry file.
+
+ Raises
+ ------
+ ValueError
+ If no blocks are available for export.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> geometry_file = problem.to_geometry_3dec()
+ >>> print("Geometry exported to:", geometry_file)
+ """
+ if not self.blocks:
+ raise ValueError("No blocks available for geometry export.")
+
+ self.make_compounds()
+ outputs = ""
+ for indices in self.compounds:
+ block = self.blocks[indices[0]]
+ if block.is_support:
+ group = "Supports"
+ elif block.group is None:
+ group = "Blocks"
+ else:
+ group = block.group
+ outputs += ";__create " + str(group) + "__" + "\n"
+
+ meshes = []
+ for index in indices:
+ meshes.append(self.blocks[index].mesh)
+ outputs += self._to_mesh_string_3dec(meshes, indices, group, precision=3)
+ geometry_path = os.path.join(self.working_path, "geometry.dat")
+ self._overwrite_file(geometry_path, outputs)
+ print(f"Geometry exported to {geometry_path}")
+ return geometry_path
+
+ def _to_mesh_string_3dec(self, meshes, indices, group, precision=10, unit_scale=1.0):
+ """
+ Convert a list of compas meshes to a string readable by 3DEC.
+
+ Parameters
+ ----------
+ meshes : list[compas.datastructures.Mesh]
+ List of compas Mesh objects representing block geometry.
+ indices : list[int]
+ List of indices corresponding to the meshes from model graph nodes.
+ group : str
+ 3DEC block's group name.
+ precision : int, optional
+ Number of decimal places for vertex coordinates (default is 10).
+ unit_scale : float, optional
+ Scale factor for vertex coordinates (default is 1.0).
+
+ Returns
+ -------
+ str
+ String containing 3DEC block creation and join commands for the given meshes.
+
+ Raises
+ ------
+ ValueError
+ If the lengths of meshes and indices do not match.
+
+ Examples
+ --------
+ >>> mesh1, mesh2 = ... # compas Mesh objects
+ >>> indices = [0, 1]
+ >>> group = "Blocks"
+ >>> s = problem._to_mesh_string_3dec([mesh1, mesh2], indices, group, precision=3)
+ >>> print(s)
+ """
+ if len(meshes) != len(indices):
+ raise ValueError("meshes and indices must have the same length.")
+ block_description = ""
+ for i, mesh in enumerate(meshes):
+ face_description = ""
+ for face in mesh.faces():
+ face_description += "face "
+ vertices = list(mesh.face_vertices(face))
+ vertices.reverse()
+ for vertex in vertices:
+ vertex_coordinates = mesh.vertex_coordinates(vertex)
+ face_description += "{0:.{3}f},{1:.{3}f},{2:.{3}f} ".format(
+ vertex_coordinates[0] / unit_scale,
+ vertex_coordinates[1] / unit_scale,
+ vertex_coordinates[2] / unit_scale,
+ precision,
+ )
+ sub_block_description = (
+ "block create group " + '"' + str(group) + '"' + " poly %s r=%i" % (face_description, indices[i])
+ )
+ block_description += sub_block_description + "\n"
+ if len(meshes) > 1:
+ str_indices = [str(num) for num in indices]
+ block_description += "block join range region " + " ".join(str_indices) + "\n"
+ return block_description
+
+ def _overwrite_file(self, file_path, replace_string):
+ """
+ Overwrite the file at file_path with replace_string.
+ If the file does not exist, it will be created.
+
+ Parameters
+ ----------
+ file_path : str
+ Path to the file to overwrite.
+ replace_string : str
+ Content to write to the file.
+
+ Raises
+ ------
+ PermissionError
+ If the file exists but is not writable.
+ """
+ # Clean up any unwanted prefix
+ file_path = file_path.replace("file:\\", "").replace("file:/", "").replace("file:", "")
+ file_path = os.path.abspath(file_path)
+
+ if os.path.exists(file_path):
+ if os.access(file_path, os.W_OK):
+ with open(file_path, "w") as f:
+ f.write(replace_string)
+ else:
+ raise PermissionError(f"File write access denied: {file_path}")
+ else:
+ with open(file_path, "w") as f:
+ f.write(replace_string)
+
+ # =============================================================================
+ # setup 3dec analysis
+ # =============================================================================
+ def set_joint_stiffness_one_material(self, block_height, reduction_factor, block_length=None, material=None):
+ """
+ Compute the joint stiffness values for a model with one joint material (dry assembled).
+
+ Parameters
+ ----------
+ block_height : float
+ Height of the block.
+ reduction_factor : float
+ Reduction factor for the joint stiffness.
+ block_length : float, optional
+ Length of the block. If not provided, only block_height is used.
+ material : Material, optional
+ Material object with attributes E (Young's modulus) and G (shear modulus).
+
+ Returns
+ -------
+ tuple (float, float)
+ The computed normal (jkn) and shear (jks) joint stiffness values.
+
+ Raises
+ ------
+ ValueError
+ If material is not provided or does not have E and G attributes.
+
+ Notes
+ -----
+ - If block_length is provided, the stiffness is averaged using both block_height and block_length.
+ - The reduction_factor is applied to both normal and shear stiffness.
+ - This function assumes dry joints (no mortar).
+
+ Examples
+ --------
+ >>> mat = Material(E=30e9, G=12e9)
+ >>> problem.set_joint_stiffness_one_material(0.2, 10, block_length=0.4, material=mat)
+ (750000000.0, 300000000.0)
+ """
+ if material is None or not hasattr(material, "E") or not hasattr(material, "G"):
+ raise ValueError("A valid material with 'E' and 'G' attributes must be provided.")
+
+ E = material.E
+ G = material.G
+
+ if block_length is None:
+ jkn = E / block_height
+ jks = G / block_height
+ else:
+ jkn = ((E / block_height) + (E / block_length)) / 2
+ jks = ((G / block_height) + (G / block_length)) / 2
+
+ jkn = jkn / reduction_factor
+ jks = jks / reduction_factor
+
+ return (jkn, jks)
+
+ def set_joint_stiffness_two_materials(
+ self, block_height, interface_thickness, reduction_factor, material0_name=None, material1_name=None
+ ):
+ """
+ Compute the joint stiffness values for a model with two joint materials (e.g., stone and mortar).
+
+ Parameters
+ ----------
+ block_height : float
+ Height of the block.
+ interface_thickness : float
+ Thickness of the interface material (e.g., mortar).
+ reduction_factor : float
+ Reduction factor for the joint stiffness.
+ material0_name : str, optional
+ Name of the first material (e.g., stone) stored in self.materials.
+ material1_name : str, optional
+ Name of the second material (e.g., mortar) stored in self.materials.
+
+ Returns
+ -------
+ tuple (float, float)
+ The computed normal (jkn) and shear (jks) joint stiffness values.
+
+ Raises
+ ------
+ KeyError
+ If either material name is not found in self.materials.
+ AttributeError
+ If the material does not have E or G attributes.
+
+ Notes
+ -----
+ - The stiffness is computed using the properties of both materials and the interface thickness.
+ - The reduction_factor is applied to both normal and shear stiffness.
+ - This function assumes two distinct joint materials.
+
+ Examples
+ --------
+ >>> problem.set_joint_stiffness_two_materials(0.2, 0.01, 10, material0_name="Stone", material1_name="Mortar")
+ (jkn_value, jks_value)
+ """
+ try:
+ E1 = self.materials[material0_name].E
+ G1 = self.materials[material0_name].G
+ E2 = self.materials[material1_name].E
+ G2 = self.materials[material1_name].G
+ except KeyError as e:
+ raise KeyError(f"Material '{e.args[0]}' not found in self.materials.")
+ except AttributeError as e:
+ raise AttributeError("Material must have 'E' and 'G' attributes.")
+
+ jkn = (E1 * E2) / ((block_height * E2) + (interface_thickness * E1))
+ jks = (G1 * G2) / ((block_height * G2) + (interface_thickness * G1))
+
+ self.jkn = jkn / reduction_factor
+ self.jks = jks / reduction_factor
+
+ return (jkn, jks)
+
+ # =============================================================================
+ # gravity
+ # =============================================================================
+ def gravity_equilibrium(
+ self, steps=10, keyword="ratio-local", ratio=1e-06, time=0.02, final_ratio=1e-06, time_final_step=1
+ ):
+ """
+ Generate the gravity loading sequence for a 3DEC analysis.
+
+ This method creates a string of 3DEC commands to incrementally apply gravity in multiple steps,
+ solving for equilibrium at each step, and then performing a final solve with specified criteria.
+
+ Parameters
+ ----------
+ steps : int, optional
+ Number of gravity loading steps (default is 10).
+ keyword : str, optional
+ Equilibrium criterion keyword for the 3DEC solver (default is "ratio-local").
+ ratio : float, optional
+ Solver ratio for each gravity step (default is 1e-06).
+ time : float, optional
+ Solver time for each gravity step (default is 0.02).
+ final_ratio : float, optional
+ Solver ratio for the final step (default is 1e-06).
+ time_final_step : float, optional
+ Solver time for the final step (default is 1).
+
+ Returns
+ -------
+ str
+ String containing the gravity loading and solve commands for 3DEC.
+
+ Examples
+ --------
+ >>> problem = Problem3dec()
+ >>> gravity_string = problem.gravity_equilibrium(steps=5)
+ >>> print(gravity_string)
+ """
+ g = -9.806 / steps
+ g = round(g, 3)
+ text = ";===========================================================================" + "\n"
+ text += ";GRAVITY APPLIED IN " + str(steps) + " STEPS\n"
+ text += ";===========================================================================" + "\n"
+ for i in range(steps):
+ gr = g * (i + 1)
+ header = ";======================================================================" + "\n"
+ header += ";_____GRAVITY_____ step " + str(i + 1) + "\n"
+ header += ";======================================================================" + "\n"
+ header += "model gravity 0 0 " + str(gr) + "\n"
+ header += "model solve " + str(keyword) + " " + str(ratio) + " time " + str(time) + "\n"
+ text += header
+ text += "model solve " + str(keyword) + " " + str(final_ratio) + " time " + str(time_final_step) + "\n"
+ return text
+
+ def gravity(
+ self,
+ steps=10,
+ keyword="ratio-local",
+ ratio=1e-06,
+ time_step=0.02,
+ final_ratio=1e-05,
+ time_final_step=1,
+ ):
+ self._check_and_delete_gravity_files(self.working_path)
+ if not all(
+ hasattr(group, "contact_property")
+ and hasattr(group.contact_property, "stiffness")
+ and group.contact_property.stiffness
+ for group in self.groups
+ ):
+ raise ValueError("Missing Joint Stiffness values in one or more groups.")
+
+ main_string = f";{time.strftime('%d/%m/%Y')} {time.strftime('%H:%M:%S')}"
+ main_string += """
+ model new
+ model large-strain on
+ program call 'geometry.dat'
+ block contact generate-subcontacts
+ """
+ for group in self.groups:
+ group_header = (
+ f"\nblock property density {group.material.rho} range group '{group.name}'\n"
+ f"block contact property stiffness-normal {group.contact_property.stiffness[0]} "
+ f"stiffness-shear {group.contact_property.stiffness[1]} "
+ f"friction {group.contact_property.failure_criteria.friction} range group '{group.name}'\n"
+ f"block contact material-table default property stiffness-normal {group.contact_property.stiffness[0]} "
+ f"stiffness-shear {group.contact_property.stiffness[1]}\n"
+ )
+ if group.name == "Supports":
+ group_header += "block fix range group 'Supports'\n"
+ main_string += group_header
+
+ main_string += self.set_damping_global()
+ main_string += self.blocks_output()
+ main_string += self.contacts_output()
+ main_string += self.save_blocks_output("init_state")
+ main_string += self.save_analysis("init")
+ main_string += self.restore_analysis("init")
+ main_string += "\n"
+ main_string += self.gravity_equilibrium(steps, keyword, ratio, time_step, final_ratio, time_final_step)
+ main_string += self.save_blocks_output("grav_state")
+ main_string += self.save_contacts_output("contact_grav")
+ main_string += self.save_analysis("grav")
+ main_string += "exit()"
+ output_path = self.working_path
+ filename = "gravity.dat"
+ with open(os.path.join(output_path, filename), "w") as file:
+ file.write(main_string)
+ return filename
+
+ def _check_and_delete_gravity_files(self, current_directory, verbose=False):
+ """
+ Check for and delete gravity-related files in the specified directory.
+
+ Parameters
+ ----------
+ current_directory : str
+ The directory in which to check and delete files.
+ verbose : bool, optional
+ If True, print messages about deleted or missing files (default is False).
+
+ Returns
+ -------
+ list[str]
+ List of deleted file names.
+
+ Raises
+ ------
+ OSError
+ If a file cannot be deleted due to permission issues.
+ """
+ files_to_check = ["init_state.txt", "grav_state.txt", "contact_grav.txt"]
+ deleted_files = []
+
+ for file_name in files_to_check:
+ full_path = os.path.join(current_directory, file_name)
+ if os.path.exists(full_path):
+ try:
+ os.remove(full_path)
+ deleted_files.append(file_name)
+ if verbose:
+ print(f"Deleted {file_name}")
+ except OSError as e:
+ print(f"Error deleting {file_name}: {e}")
+ else:
+ if verbose:
+ print(f"{file_name} does not exist in {current_directory} and was not deleted")
+ return deleted_files
+
+ # =============================================================================
+ # load.dat
+ # =============================================================================
+ def _load_box(self, point, precision, decimals=3):
+ """
+ Create a bounding box range string around a 3D point for use in 3DEC boundary load commands.
+
+ Parameters
+ ----------
+ point : list[float] or tuple[float]
+ 3D coordinates (x, y, z) where the load is applied.
+ precision : float
+ Distance to add and subtract in each direction to create the bounding box.
+ decimals : int, optional
+ Number of decimal places for output (default is 3).
+
+ Returns
+ -------
+ str
+ A string specifying the range in x, y, z directions for 3DEC.
+
+ Raises
+ ------
+ ValueError
+ If point is not a sequence of three numbers.
+ TypeError
+ If precision is not a float or int.
+
+ Examples
+ --------
+ >>> problem._load_box([1.0, 2.0, 3.0], 0.1)
+ 'range x 0.900 ,1.100 y 1.900 ,2.100 z 2.900 ,3.100'
+ """
+ if not (isinstance(point, (list, tuple)) and len(point) == 3):
+ raise ValueError("point must be a list or tuple of three numbers.")
+ if not isinstance(precision, (float, int)):
+ raise TypeError("precision must be a float or int.")
+
+ x1 = point[0] - precision
+ x2 = point[0] + precision
+ y1 = point[1] - precision
+ y2 = point[1] + precision
+ z1 = point[2] - precision
+ z2 = point[2] + precision
+ pl = (
+ f"range x {x1:.{decimals}f} ,{x2:.{decimals}f} "
+ f"y {y1:.{decimals}f} ,{y2:.{decimals}f} "
+ f"z {z1:.{decimals}f} ,{z2:.{decimals}f}"
+ )
+ return pl
+
+ def _load_along_direction(self, pt1, pt2, load, decimals=3):
+ """
+ Compute the load components along a direction defined by two points for 3DEC boundary load commands.
+
+ Parameters
+ ----------
+ pt1 : list[float] or tuple[float]
+ Starting point of the direction vector (x, y, z).
+ pt2 : list[float] or tuple[float]
+ Ending point of the direction vector (x, y, z).
+ load : float
+ Magnitude of the load to be applied along the direction.
+ decimals : int, optional
+ Number of decimal places for output (default is 3).
+
+ Returns
+ -------
+ str
+ String specifying the x, y, z components of the load for 3DEC.
+
+ Raises
+ ------
+ ValueError
+ If the direction vector is zero-length.
+
+ Examples
+ --------
+ >>> problem._load_along_direction([0, 0, 0], [1, 0, 0], 100)
+ 'xload 100.000 yload 0.000 zload 0.000'
+ """
+ if not (isinstance(pt1, (list, tuple)) and isinstance(pt2, (list, tuple)) and len(pt1) == 3 and len(pt2) == 3):
+ raise ValueError("pt1 and pt2 must be lists or tuples of three numbers.")
+ if not isinstance(load, (float, int)):
+ raise TypeError("load must be a float or int.")
+
+ vec = Vector.from_start_end(pt1, pt2)
+ vec = normalize_vector(vec)
+ if all(abs(v) < 1e-12 for v in vec):
+ raise ValueError("Direction vector cannot be zero-length.")
+
+ load_components = (
+ f"xload {vec[0] * load:.{decimals}f} yload {vec[1] * load:.{decimals}f} zload {vec[2] * load:.{decimals}f}"
+ )
+ return load_components
+
+ def set_point_load(
+ self, application_point, direction_point, load_magnitude, radius, subcontacts_per_point, decimals=3
+ ):
+ """
+ Generate a 3DEC command string to apply a point load at a gridpoint in a specified direction.
+
+ Parameters
+ ----------
+ application_point : list[float] or tuple[float]
+ 3D coordinates (x, y, z) where the load is applied.
+ direction_point : list[float] or tuple[float]
+ 3D coordinates (x, y, z) defining the direction of the load.
+ load_magnitude : float
+ Total magnitude of the load to be applied.
+ radius : float
+ Radius of the sphere around the application point for the range command.
+ subcontacts_per_point : int
+ Number of subcontacts at the application point (used to divide the load).
+ decimals : int, optional
+ Number of decimal places for output (default is 3).
+
+ Returns
+ -------
+ str
+ 3DEC command string to apply the point load.
+
+ Raises
+ ------
+ ValueError
+ If application_point or direction_point are not sequences of three numbers.
+ TypeError
+ If load_magnitude, radius, or subcontacts_per_point are not numbers.
+
+ Examples
+ --------
+ >>> problem.set_point_load([1, 2, 3], [2, 2, 3], 1000, 0.1, 4)
+ 'block gridpoint apply force-x ... force-y ... force-z ... range sphere c 1 2 3 r 0.1'
+ """
+ if not (isinstance(application_point, (list, tuple)) and len(application_point) == 3):
+ raise ValueError("application_point must be a list or tuple of three numbers.")
+ if not (isinstance(direction_point, (list, tuple)) and len(direction_point) == 3):
+ raise ValueError("direction_point must be a list or tuple of three numbers.")
+ if not isinstance(load_magnitude, (float, int)):
+ raise TypeError("load_magnitude must be a float or int.")
+ if not isinstance(radius, (float, int)):
+ raise TypeError("radius must be a float or int.")
+ if not isinstance(subcontacts_per_point, int) or subcontacts_per_point <= 0:
+ raise ValueError("subcontacts_per_point must be a positive integer.")
+
+ magnitude_per_point = load_magnitude / subcontacts_per_point
+ load_vector = Vector.from_start_end(application_point, direction_point)
+ load_vector = normalize_vector(load_vector)
+ load_vector = scale_vector(load_vector, magnitude_per_point)
+ string = (
+ f"block gridpoint apply force-x {load_vector[0]:.{decimals}f} "
+ f"force-y {load_vector[1]:.{decimals}f} "
+ f"force-z {load_vector[2]:.{decimals}f} "
+ f"range sphere c {application_point[0]:.{decimals}f} {application_point[1]:.{decimals}f} {application_point[2]:.{decimals}f} r {radius:.{decimals}f}\n"
+ )
+ return string
+
+ def set_points_load(self, points_list, load_magnitude, load_vector, radius, subcontacts_per_point, decimals=3):
+ """
+ Generate 3DEC command strings to apply point loads at multiple gridpoints.
+
+ Returns
+ -------
+ str
+ Combined command string for all points.
+ """
+ magnitude_per_point = load_magnitude / subcontacts_per_point
+ result = ""
+ for point in points_list:
+ load_direction = normalize_vector(load_vector)
+ load = scale_vector(load_direction, magnitude_per_point)
+ result += (
+ f"block gridpoint apply force-x {load[0]:.{decimals}f} "
+ f"force-y {load[1]:.{decimals}f} "
+ f"force-z {load[2]:.{decimals}f} "
+ f"range sphere c {point[0]:.{decimals}f} {point[1]:.{decimals}f} {point[2]:.{decimals}f} r {radius:.{decimals}f}\n"
+ )
+ return result
+
+ def set_load_analysis(
+ self,
+ load_string,
+ total_load,
+ load_magnitude_per_step,
+ number_of_cycles=35000,
+ load_capacity=False,
+ solver_ratio=0.00001,
+ ):
+ """
+ Generate and export a 3DEC load analysis sequence to a .dat file.
+
+ Parameters
+ ----------
+ load_string : str
+ 3DEC command string to apply the load at each step.
+ total_load : float
+ Total load to be applied over all steps.
+ load_magnitude_per_step : float
+ Magnitude of load applied in each step.
+ number_of_cycles : int, optional
+ Number of solver cycles per step (default is 35000).
+ load_capacity : bool, optional
+ If True, use a fixed large number of steps (default is False).
+ solver_ratio : float, optional
+ Solver ratio for equilibrium check (default is 0.00001).
+
+ Returns
+ -------
+ str
+ The filename of the created load .dat file.
+
+ Raises
+ ------
+ ValueError
+ If the gravity file is missing or if input values are invalid.
+ """
+ gravity_file = os.path.join(self.working_path, "grav_state.txt")
+ if not os.path.exists(gravity_file):
+ raise ValueError("Missing gravity file: compute gravity first")
+
+ if not isinstance(total_load, (float, int)) or total_load <= 0:
+ raise ValueError("total_load must be a positive number.")
+ if not isinstance(load_magnitude_per_step, (float, int)) or load_magnitude_per_step <= 0:
+ raise ValueError("load_magnitude_per_step must be a positive number.")
+ if not isinstance(number_of_cycles, int) or number_of_cycles <= 0:
+ raise ValueError("number_of_cycles must be a positive integer.")
+ if not isinstance(solver_ratio, (float, int)) or solver_ratio <= 0:
+ raise ValueError("solver_ratio must be a positive number.")
+
+ main_string = f";{time.strftime('%d/%m/%Y')} {time.strftime('%H:%M:%S')}\n\n"
+ main_string += self.restore_analysis("grav")
+ main_string += self.set_damping_global()
+ main_string += "\n\n"
+ main_string += self.blocks_output()
+ main_string += self.contacts_output() + "\n"
+
+ load_steps = int(total_load / load_magnitude_per_step)
+ if load_capacity:
+ load_steps = 10000
+
+ for step in range(load_steps):
+ step_load = (step + 1) * load_magnitude_per_step
+ step_name = f"Load_step_{step + 1}_load_magnitude_{step_load} N"
+ main_string += ";===========================================================================" + "\n"
+ main_string += f"; {step_name}\n"
+ main_string += ";===========================================================================" + "\n"
+ main_string += load_string
+ main_string += f"model cycle {number_of_cycles}\n\n"
+ main_string += self.save_blocks_output(step_name)
+ step_name_contact = step_name + "_contacts"
+ main_string += self.save_contacts_output(step_name_contact)
+ main_string += self.save_analysis(step_name)
+ main_string += self.check_and_exit(solver_ratio)
+ main_string += "\n;exit()\n"
+
+ output_path = self.working_path
+ filename = "load.dat"
+ with open(os.path.join(output_path, filename), "w") as file:
+ file.write(main_string)
+ return filename
+
+ # =============================================================================
+ # run 3dec in the background
+ # =============================================================================
+ def run(self, sequence=None, suppress_output=True):
+ """
+ Run the 3DEC executable with the specified command sequence in the working directory.
+
+ Parameters
+ ----------
+ sequence : list[str], optional
+ List of additional command-line arguments or files to pass to the executable.
+ suppress_output : bool, optional
+ If True, suppress stdout and stderr output (default is True).
+
+ Returns
+ -------
+ int
+ The return code from the subprocess call.
+ """
+ import subprocess
+
+ if sequence is None:
+ sequence = []
+
+ # Remove quotes from executable_path if present
+ executable = self.executable_path.strip('"')
+ cmd = [executable] + sequence
+
+ # Prevent black shell window on Windows
+ creationflags = 0
+ if os.name == "nt":
+ creationflags = subprocess.CREATE_NO_WINDOW
+
+ if suppress_output:
+ result = subprocess.call(
+ cmd,
+ cwd=self.working_path,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ creationflags=creationflags,
+ )
+ else:
+ result = subprocess.call(cmd, cwd=self.working_path, creationflags=creationflags)
+ return result
+
+ # =============================================================================
+ # displacement.dat
+ # =============================================================================
+ def get_model_timestep(self):
+ """
+ Retrieve the model timestep value from the 'grav_state.txt' file.
+
+ Returns
+ -------
+ float
+ The timestep value found in the file.
+
+ Raises
+ ------
+ FileNotFoundError
+ If 'grav_state.txt' does not exist in the working directory.
+ ValueError
+ If the timestep value is not found or cannot be converted to float.
+
+ Examples
+ --------
+ >>> timestep = problem.get_model_timestep()
+ >>> print("Model timestep:", timestep)
+ """
+ grav_file = os.path.join(self.working_path, "grav_state.txt")
+ if not os.path.exists(grav_file):
+ raise FileNotFoundError(f"File not found: {grav_file}")
+
+ timestep = None
+ with open(grav_file, "r") as fo:
+ for line in fo:
+ line = line.strip()
+ if not line:
+ continue
+ parts = line.split()
+ if len(parts) >= 3 and parts[0] == "timestep":
+ try:
+ timestep = float(parts[2])
+ break
+ except ValueError:
+ raise ValueError(f"Could not convert timestep value '{parts[2]}' to float.")
+ if timestep is None:
+ raise ValueError("No timestep value found in grav_state.txt.")
+ return timestep
+
+ def set_block_displacement(self, region=0, displacement_direction=[0, 0, -1], displ_magnitude_per_step=0.001):
+ """
+ Generate 3DEC command strings to apply block displacement in a specified direction for a region.
+
+ Parameters
+ ----------
+ region : int, optional
+ Region index to which the displacement is applied (default is 0).
+ displacement_direction : list[float] or tuple[float], optional
+ Direction vector for the displacement (default is [0, 0, -1]).
+ displ_magnitude_per_step : float, optional
+ Magnitude of the displacement per step (default is 0.001).
+
+ Returns
+ -------
+ list[str]
+ List containing the command strings for applying and resetting displacement.
+
+ Raises
+ ------
+ ValueError
+ If displacement_direction is not a sequence of three numbers.
+ TypeError
+ If displ_magnitude_per_step is not a float or int.
+
+ Examples
+ --------
+ >>> problem.set_block_displacement(region=1, displacement_direction=[0, 0, -1], displ_magnitude_per_step=0.002)
+ [header_string, equilibrium_string]
+ """
+ if not (isinstance(displacement_direction, (list, tuple)) and len(displacement_direction) == 3):
+ raise ValueError("displacement_direction must be a list or tuple of three numbers.")
+ if not isinstance(displ_magnitude_per_step, (float, int)):
+ raise TypeError("displ_magnitude_per_step must be a float or int.")
+
+ displacement_direction = normalize_vector(displacement_direction)
+ single_displacement_vector = scale_vector(displacement_direction, displ_magnitude_per_step)
+ header = (
+ f"block apply velocity-x {single_displacement_vector[0]} range region {region}\n"
+ f"block apply velocity-y {single_displacement_vector[1]} range region {region}\n"
+ f"block apply velocity-z {single_displacement_vector[2]} range region {region}\n"
+ )
+
+ equilibrium = (
+ f"block apply velocity-x 0.0 range region {region}\n"
+ f"block apply velocity-y 0.0 range region {region}\n"
+ f"block apply velocity-z 0.0 range region {region}\n"
+ )
+
+ from compas_3dec.datastructure.boundary_condition import BoundaryCondition
+
+ boundary = BoundaryCondition()
+ boundary.region = region
+ boundary.type = "displacement"
+ boundary.direction = displacement_direction
+ boundary.magnitude = displ_magnitude_per_step
+ self.boundary_conditions.append(boundary)
+
+ displacement_data = [header, equilibrium]
+ return displacement_data
+
+ def set_blocks_displacement(self, regions, displacement_direction=[0, 0, -1], displ_magnitude_per_step=0.001):
+ """
+ Generate 3DEC command strings to apply block displacement in a specified direction for multiple regions.
+
+ Parameters
+ ----------
+ regions : list[int]
+ List of region indices to which the displacement is applied.
+ displacement_direction : list[float] or tuple[float], optional
+ Direction vector for the displacement (default is [0, 0, -1]).
+ displ_magnitude_per_step : float, optional
+ Magnitude of the displacement per step (default is 0.001).
+
+ Returns
+ -------
+ list[str]
+ List containing the command strings for applying and resetting displacement.
+
+ Raises
+ ------
+ ValueError
+ If displacement_direction is not a sequence of three numbers.
+ TypeError
+ If displ_magnitude_per_step is not a float or int.
+ If regions is not a list of integers.
+
+ Examples
+ --------
+ >>> problem.set_blocks_displacement([0, 1], displacement_direction=[0, 0, -1], displ_magnitude_per_step=0.002)
+ [header_string, equilibrium_string]
+ """
+ if not (isinstance(displacement_direction, (list, tuple)) and len(displacement_direction) == 3):
+ raise ValueError("displacement_direction must be a list or tuple of three numbers.")
+ if not isinstance(displ_magnitude_per_step, (float, int)):
+ raise TypeError("displ_magnitude_per_step must be a float or int.")
+ if not (isinstance(regions, (list, tuple)) and all(isinstance(r, int) for r in regions)):
+ raise TypeError("regions must be a list or tuple of integers.")
+
+ displacement_direction = normalize_vector(displacement_direction)
+ single_displacement_vector = scale_vector(displacement_direction, displ_magnitude_per_step)
+ regions_str = " ".join(str(r) for r in regions)
+
+ header = (
+ f"block apply velocity-x {single_displacement_vector[0]} range region {regions_str}\n"
+ f"block apply velocity-y {single_displacement_vector[1]} range region {regions_str}\n"
+ f"block apply velocity-z {single_displacement_vector[2]} range region {regions_str}\n"
+ )
+
+ equilibrium = (
+ f"block apply velocity-x 0.0 range region {regions_str}\n"
+ f"block apply velocity-y 0.0 range region {regions_str}\n"
+ f"block apply velocity-z 0.0 range region {regions_str}\n"
+ )
+
+ displacement_data = [header, equilibrium]
+ return displacement_data
+
+ def set_displacement_analysis(
+ self,
+ displacements_list,
+ init_dict,
+ mapping_dict,
+ HERE,
+ total_displacement=0.0,
+ displ_magnitude_per_step=0.001,
+ solver_ratio=0.00001,
+ solver_time=3,
+ displacement_capacity=False,
+ ):
+ # Get the model timestep calculated by 3DEC from the gravity file
+ timestep = self.get_model_timestep()
+ # Number of solver cycles to reach the total displacement
+ number_of_cycles = int(displ_magnitude_per_step / (displ_magnitude_per_step * timestep))
+
+ if not os.path.join(self.working_path, "grav_state.txt"):
+ raise ValueError("Missing gravity file: compute gravity first")
+
+ displacement_list = []
+ displacement_steps = int(total_displacement / displ_magnitude_per_step)
+ if displacement_capacity:
+ displacement_steps = 10000
+
+ filenames = []
+ step_names = []
+ equilibrium_steps = []
+ collapse_step = []
+ for step in range(displacement_steps):
+ step_name = (
+ "Displacement_step"
+ + "_"
+ + str(step + 1)
+ + "_distance_"
+ + "{:.4f}".format((step + 1) * displ_magnitude_per_step)
+ + "m"
+ )
+ step_names.append(step_name)
+ step_name_contact = step_name + "_contacts"
+
+ # Initialize the main string for the displacement file
+ main_string = ";" + time.strftime("%d/%m/%Y") + " " + time.strftime("%H:%M:%S") + "\n\n"
+
+ # Load the gravity output for the first step, or the previous step's output for subsequent steps
+ if step == 0:
+ main_string += self.restore_analysis("grav")
+ else:
+ previous_step_name = (
+ "Displacement_step"
+ + "_"
+ + str(step)
+ + "_distance_"
+ + "{:.4f}".format(step * displ_magnitude_per_step)
+ + "m"
+ )
+ main_string += self.restore_analysis(previous_step_name)
+
+ main_string += self.set_damping_local() + "\n\n"
+ main_string += self.blocks_output()
+ main_string += self.contacts_output() + "\n"
+
+ main_string += ";===========================================================================" + "\n"
+ main_string += "; " + str(step_name) + "\n"
+ main_string += ";===========================================================================" + "\n"
+
+ for displacement in displacements_list:
+ main_string += displacement[0]
+ main_string += "model cycle " + str(number_of_cycles) + "\n\n"
+
+ main_string += ";===========================================================================" + "\n"
+ main_string += "; Equilibrium calculation" + "\n"
+ main_string += ";===========================================================================" + "\n"
+ for displacement in displacements_list:
+ main_string += displacement[1]
+ main_string += "model solve unbalanced-maximum {} time".format(solver_ratio) + " " + str(solver_time) + "\n"
+ main_string += self.save_blocks_output(step_name)
+ main_string += self.save_contacts_output(step_name_contact)
+ main_string += self.save_analysis(step_name)
+ main_string += "\nexit()\n"
+
+ # Save the displacement file with the name of the current step
+ output_path = self.working_path
+ filename = f"{step_name}.dat"
+ filenames.append(filename)
+ with open(os.path.join(output_path, filename), "w") as file:
+ file.write(main_string)
+
+ for filename, step_name in zip(filenames, step_names):
+ step_name_contact = step_name + "_contacts"
+ # Run the displacement file
+ self.run([filename])
+
+ # =============================================================================
+ # Read results blocks and interactions
+ # =============================================================================
+ result = self.solve_ratio_check(step_name + ".txt")
+ # displ_dict = self.from_3dec_blocks(step_name + ".txt")
+ if result == "Equilibrium":
+ equilibrium_steps.append(step_name)
+ continue
+ else:
+ collapse_step.append(step_name)
+ break
+
+ # displ_dict = self.from_3dec_blocks(equilibrium_steps[-1] + ".txt")
+ # self.update_blocks(displ_dict, mapping_dict)
+ # output_3dec_per_vertex = self.from_3dec_contacts(equilibrium_steps[-1] + "_contacts")
+ # FILE_O = os.path.join(HERE, 'problem_' + equilibrium_steps[-1] + '.json')
+ # compas.json_dump(self, FILE_O)
+
+ # if result == "Equilibrium":
+ # displacement_list.append(step_name)
+ # self.update_blocks(displ_dict, mapping_dict)
+ # output_3dec_per_vertex = self.from_3dec_contacts(step_name_contact + ".txt")
+ # FILE_O = os.path.join(HERE, 'problem_' + step_name + '.json')
+ # compas.json_dump(self, FILE_O)
+ # else:
+ # print("Not in equilibrium at {}".format(step_name))
+ # self.update_blocks(displ_dict, mapping_dict)
+ # output_3dec_per_vertex = self.from_3dec_contacts(step_name_contact + ".txt")
+ # FILE_O = os.path.join(HERE, 'problem_collapse' + step_name + '.json')
+ # compas.json_dump(self, FILE_O)
+ # break
+
+ return filename, displacement_steps, displ_magnitude_per_step, self, equilibrium_steps, collapse_step
+
+ def show_displacement(self, mapping_dict, equilibrium_steps, collapse_step, HERE):
+ """
+ Update block geometry and export the current state to a JSON file after equilibrium is reached.
+
+ Parameters
+ ----------
+ mapping_dict : dict
+ Mapping between model vertices and 3DEC output vertices.
+ equilibrium_steps : list[str]
+ List of step names where equilibrium was reached.
+ collapse_step : list[str]
+ List of step names where collapse occurred (not used in this function).
+ HERE : str
+ Directory path where the output JSON file will be saved.
+
+ Returns
+ -------
+ None
+
+ Notes
+ -----
+ - Updates block geometry using the last equilibrium step's output.
+ - Exports the updated Problem3dec object to a JSON file.
+ - Also processes contact data for the last equilibrium step.
+
+ Examples
+ --------
+ >>> problem.show_displacement(mapping_dict, equilibrium_steps, collapse_step, HERE)
+ """
+ if not equilibrium_steps:
+ raise ValueError("No equilibrium steps provided.")
+ step_name = equilibrium_steps[-1]
+ displ_dict = self.from_3dec_blocks(f"{step_name}.txt")
+ self.update_blocks(displ_dict, mapping_dict)
+ output_3dec_per_vertex = self.from_3dec_contacts(f"{step_name}_contacts.txt")
+ file_o = os.path.join(HERE, f"problem_{step_name}.json")
+ compas.json_dump(self, file_o)
+ return
+
+ # =============================================================================
+ # get and process BLOCK data from 3dec
+ # =============================================================================
+ def from_3dec_blocks(self, filename):
+ blocks = {}
+ with open(os.path.join(self.working_path, filename), "r") as fo:
+ for line in fo:
+ line = line.strip()
+ if not line:
+ continue
+ parts = line.split()
+ if not len(parts):
+ continue
+ if parts[0] == "block":
+ id = int(parts[1]) - 1
+ blocks[id] = {
+ "centroid": None,
+ "vertices": [],
+ "force": None,
+ "velocity": None,
+ "mass": None,
+ "region": None,
+ "moment": None,
+ "load": None,
+ }
+ continue
+ if parts[0] == "centroid":
+ blocks[id]["centroid"] = [float(c) for c in parts[2].split(",")]
+ continue
+ if parts[0] == "vertex":
+ xyz = [float(c) for c in parts[2][1:-1].split(",")]
+ blocks[id]["vertices"].append(xyz)
+ continue
+ if parts[0] == "forces":
+ xyz = [float(c) for c in parts[2].split(",")]
+ blocks[id]["force"] = xyz
+ continue
+ if parts[0] == "loads":
+ xyz = [float(c) for c in parts[2].split(",")]
+ blocks[id]["load"] = xyz
+ continue
+ if parts[0] == "moment":
+ xyz = [float(c) for c in parts[2].split(",")]
+ blocks[id]["moment"] = xyz
+ continue
+ if parts[0] == "velocity":
+ xyz = [float(c) for c in parts[2][1:-1].split(",")]
+ blocks[id]["velocity"] = xyz
+ continue
+ if parts[0] == "region":
+ region = int(parts[1])
+ blocks[id]["region"] = region
+ if parts[0] == "mass":
+ mass = float(parts[1])
+ blocks[id]["mass"] = mass
+ continue
+ return blocks
+
+ def mapping(self, init_dict_3dec):
+ block_map = {}
+ for bkey, block in init_dict_3dec.items():
+ region = block["region"]
+ block_gkey_index = {}
+ for index, xyz in enumerate(block["vertices"]):
+ gkey = self.geometric_key(xyz)
+ block_gkey_index[gkey] = index
+
+ block_map[int(region)] = {}
+ # block_map[region] = {}
+ for vkey in self.blocks[region].mesh.vertices():
+ # for vkey in self.elementlist[region].geometry.vertices():
+ xyz = self.blocks[region].mesh.vertex_coordinates(vkey)
+ gkey = self.geometric_key(xyz)
+ v_index = block_gkey_index[gkey]
+ block_map[int(region)][int(vkey)] = int(v_index)
+ # block_map[region][vkey] = v_index
+ self.block_map = block_map
+ return block_map
+
+ def update_blocks(self, step_dict, mapping_dict):
+ for index, block_element in enumerate(self.blocks):
+ for vkey, attr in block_element.mesh.vertices(True):
+ vertex_3dec = mapping_dict[index][vkey]
+ xyz = step_dict[index]["vertices"][vertex_3dec]
+ attr["x"] = xyz[0]
+ attr["y"] = xyz[1]
+ attr["z"] = xyz[2]
+
+ if not block_element.is_support:
+ unbal_force_ratio = norm_vector(step_dict[index]["force"]) / (step_dict[index]["mass"] * 9.806)
+ block_element.unbalanced_force_ratio = unbal_force_ratio
+ if unbal_force_ratio <= 0.001:
+ block_element.color_equilibrium = Color.from_rgb255(190, 190, 190)
+ elif unbal_force_ratio > 0.001 and unbal_force_ratio <= 0.005:
+ block_element.color_equilibrium = Color.from_rgb255(255, 230, 231)
+ elif unbal_force_ratio > 0.005 and unbal_force_ratio <= 0.01:
+ block_element.color_equilibrium = Color.from_rgb255(253, 204, 207)
+ elif unbal_force_ratio > 0.01 and unbal_force_ratio <= 0.05:
+ block_element.color_equilibrium = Color.from_rgb255(250, 179, 183)
+ elif unbal_force_ratio > 0.05 and unbal_force_ratio <= 0.1:
+ block_element.color_equilibrium = Color.from_rgb255(245, 153, 159)
+ elif unbal_force_ratio > 0.1 and unbal_force_ratio <= 0.5:
+ block_element.color_equilibrium = Color.from_rgb255(239, 126, 136)
+ elif unbal_force_ratio > 0.5 and unbal_force_ratio <= 1:
+ block_element.color_equilibrium = Color.from_rgb255(231, 98, 113)
+ elif unbal_force_ratio > 1:
+ block_element.color_equilibrium = Color.from_rgb255(222, 66, 91)
+
+ # =============================================================================
+ # get and process CONTACT data from 3dec
+ # =============================================================================
+ def from_3dec_contacts(self, filename, precision="3f"):
+ if self.interactions:
+ self.interactions = []
+ from compas_3dec.datastructure.interaction_3dec import Interaction3dec
+
+ contacts = {}
+ with open(os.path.join(self.working_path, filename), "r") as fo:
+ for line in fo:
+ line = line.strip()
+ if not line:
+ continue
+ parts = line.split()
+ if not len(parts):
+ continue
+ if parts[0] == "contact" and (
+ (int(parts[3]) == 1)
+ or (int(parts[3]) == 3)
+ or (int(parts[3]) == 5)
+ or (int(parts[3]) == 4)
+ or (int(parts[3]) == 2)
+ or (int(parts[3]) == 0)
+ ):
+ id = int(parts[2])
+ contacts[id] = {
+ "type": int(parts[3]),
+ "neighbours": [int(parts[4]), int(parts[5])],
+ "position": [float(c) for c in parts[6][1:-1].split(",")],
+ "normal": [float(c) for c in parts[7][1:-1].split(",")],
+ "subcontacts": {},
+ }
+ continue
+ if parts[0] == "subcontact":
+ ids = int(parts[5])
+ contacts[id]["subcontacts"][ids] = {}
+ coordinates = [float(c) for c in parts[2][1:-1].split(",")]
+ normal_force = float(parts[3])
+ shear_force = [float(c) for c in parts[4][1:-1].split(",")]
+ normal_displ = float(parts[6])
+ shear_displ = [float(c) for c in parts[7][1:-1].split(",")]
+ normal_stress = float(parts[8])
+ shear_stress = float(parts[9])
+ area = float(parts[10])
+ contacts[id]["subcontacts"][ids]["coordinates"] = coordinates
+ contacts[id]["subcontacts"][ids]["normal_force"] = normal_force / 1000
+ contacts[id]["subcontacts"][ids]["shear_force"] = [
+ shear_force[0] / 1000,
+ shear_force[1] / 1000,
+ shear_force[2] / 1000,
+ ]
+ contacts[id]["subcontacts"][ids]["normal_displ"] = normal_displ
+ contacts[id]["subcontacts"][ids]["shear_displ"] = shear_displ
+ contacts[id]["subcontacts"][ids]["normal_stress"] = normal_stress
+ contacts[id]["subcontacts"][ids]["shear_stress"] = shear_stress
+ contacts[id]["subcontacts"][ids]["area"] = area
+ continue
+
+ for key, contact in contacts.items():
+ neighbours = contact["neighbours"]
+ contact_normal = normalize_vector(contact["normal"])
+ contact_position = contact["position"]
+ # get list of subcontacts coordinates
+ output_3dec_per_vertex = {}
+ for key, subcontact in contact["subcontacts"].items():
+ point = subcontact["coordinates"]
+ position = self.geometric_key(point, precision)
+ normal_force = scale_vector(contact_normal, subcontact["normal_force"])
+ shear_force = subcontact["shear_force"]
+ normal_displacement = scale_vector(contact_normal, subcontact["normal_displ"])
+ shear_displacement = subcontact["shear_displ"]
+ normal_stress = subcontact["normal_stress"]
+ shear_stress = subcontact["shear_stress"]
+ area = subcontact["area"]
+
+ if position in output_3dec_per_vertex:
+ # Correctly access and update the dictionary for the existing key
+ output_3dec_per_vertex[position]["normal_force"] = [
+ x + y for x, y in zip(output_3dec_per_vertex[position]["normal_force"], normal_force)
+ ]
+ output_3dec_per_vertex[position]["shear_force"] = [
+ x + y for x, y in zip(output_3dec_per_vertex[position]["shear_force"], shear_force)
+ ]
+ # output_3dec_per_vertex[position]["normal_displacement"] = [
+ # x + y
+ # for x, y in zip(output_3dec_per_vertex[position]["normal_displacement"], normal_displacement)
+ # ]
+ # output_3dec_per_vertex[position]["shear_displacement"] = [
+ # x + y
+ # for x, y in zip(output_3dec_per_vertex[position]["shear_displacement"], shear_displacement)
+ # ]
+ output_3dec_per_vertex[position]["normal_stress"] += normal_stress
+ output_3dec_per_vertex[position]["shear_stress"] += shear_stress
+ output_3dec_per_vertex[position]["area"] += area
+ output_3dec_per_vertex[position]["is_combined"] = True
+
+ else:
+ output_3dec_per_vertex[position] = {
+ "position": point,
+ "normal_force": normal_force,
+ "shear_force": shear_force,
+ "normal_displacement": normal_displacement,
+ "shear_displacement": shear_displacement,
+ "normal_stress": normal_stress,
+ "shear_stress": shear_stress,
+ "area": area,
+ "is_combined": False,
+ }
+ # post-processing
+ Mtorque_tot = [0, 0, 0]
+ Mtot = [0, 0, 0]
+ Ntot = 0
+ Stot = [0, 0, 0]
+
+ output_list = []
+ points = [value["position"] for value in output_3dec_per_vertex.values()]
+ if points:
+ centroid = centroid_points(points)
+
+ output_3dec_per_contact = {}
+ first_iteration = True
+ for key, value in output_3dec_per_vertex.items():
+ vertex = value["position"]
+ ri = [vertex[0] - centroid[0], vertex[1] - centroid[1], vertex[2] - centroid[2]]
+ if first_iteration:
+ e1_plane = normalize_vector(ri)
+ e2_plane = cross_vectors(contact_normal, e1_plane)
+ first_iteration = False
+ Ni = norm_vector(value["normal_force"])
+ Mi = cross_vectors(ri, value["normal_force"])
+ Mtot = sum_vectors([Mtot, Mi])
+ Ntot = Ntot + Ni
+ Si = value["shear_force"]
+ Stot = sum_vectors([Stot, Si])
+ Mtorque_i = cross_vectors(ri, Si)
+ Mtorque_tot = sum_vectors([Mtorque_tot, Mtorque_i])
+
+ # if Ntot:
+ if Ntot:
+ Ftot = sum_vectors([Stot, scale_vector(contact_normal, Ntot)])
+ NN = scale_vector(contact_normal, Ntot)
+ b1 = -1 * dot_vectors(Mtot, e2_plane) / Ntot
+ b2 = dot_vectors(Mtot, e1_plane) / Ntot
+ # point of application of the resultant normal force
+ po = sum_vectors([centroid, scale_vector(e1_plane, b1), scale_vector(e2_plane, b2)])
+ norm_Stot = norm_vector(Stot)
+ if norm_Stot != 0:
+ s1 = -1 * dot_vectors(Mtorque_tot, e2_plane) / norm_Stot
+ s2 = dot_vectors(Mtorque_tot, e1_plane) / norm_vector(Stot)
+ ps = sum_vectors([centroid, scale_vector(e1_plane, s1), scale_vector(e2_plane, s2)])
+ else:
+ s1 = 0
+ # s1 = -1 * dot_vectors(Mtorque_tot, e2_plane) / norm_vector(Stot)
+ # s2 = dot_vectors(Mtorque_tot, e1_plane) / norm_vector(Stot)
+ # one point along the line of action of the resultant shear force
+ # ps = sum_vectors([centroid, scale_vector(e1_plane, s1), scale_vector(e2_plane, s2)])
+ # Calculate the vector from po to the centroid
+ r_po_centroid = sum_vectors([centroid, scale_vector(po, -1)])
+ # Calculate the moment generated by the total shear force at a distance from po
+ M_shear_po = cross_vectors(r_po_centroid, Stot)
+
+ output_3dec_per_contact["resultant_force"] = Ftot
+ output_3dec_per_contact["resultant_point"] = po
+ output_3dec_per_contact["resultant_point_shear"] = ps
+ output_3dec_per_contact["resultant_normal"] = NN
+ output_3dec_per_contact["resultant_shear"] = Stot
+ output_3dec_per_contact["resultant_torque"] = M_shear_po
+
+ if len(points) > 2:
+ normal = contact["normal"]
+ position = contact["position"]
+ plane = Plane(position, normal)
+ frame = Frame.from_plane(plane)
+ transformation = Transformation.from_frame_to_frame(frame, Frame.worldXY())
+ points = transform_points(points, transformation)
+ # ToDo: to be verified based on contact conditions (hinge)
+ points = convex_hull_xy(points)
+ points = transform_points(points, transformation.inverse())
+ contact_geometry = Polygon(points)
+ for point in points:
+ gkey = self.geometric_key(point, precision)
+ # output_list.append(output_3dec_per_vertex[gkey])
+
+ elif len(points) == 2:
+ contact_geometry = Line(points[0], points[1])
+ # output_list = output_3dec_per_vertex.values() if output_3dec_per_vertex else None
+ else:
+ contact_geometry = points if points else None
+ # contact_geometry = "no contact"
+ # output_list = output_3dec_per_vertex.values() if output_3dec_per_vertex else None
+
+ contact_type = contact["type"]
+ if contact_type == 0:
+ type = "null"
+ elif contact_type == 1:
+ type = "face-face"
+ elif contact_type == 2:
+ type = "face-edge"
+ elif contact_type == 3:
+ type = "face-vertex"
+ elif contact_type == 4:
+ type = "edge-edge"
+ elif contact_type == 5:
+ type = "edge-vertex"
+ elif contact_type == 6:
+ type = "vertex-vertex"
+ elif contact_type == 7:
+ type = "joined"
+
+ interaction = Interaction3dec(
+ type=type,
+ normal=contact_normal,
+ position=contact_position,
+ neighbours=neighbours,
+ contact_geometry=contact_geometry,
+ forces_per_vertices=output_list,
+ forces_per_contact=output_3dec_per_contact,
+ )
+ self.add_interaction(interaction)
+ return output_3dec_per_vertex
+
+ def support_resultants(self, scale_factor):
+ from compas.geometry import scale_vector
+ resultants = []
+ magnitudes = []
+ components = []
+ resultant_points = []
+ for block in self.blocks:
+ if block.is_support:
+ id = block.index
+ for interaction in self.interactions:
+ if id in interaction.neighbours:
+ if "resultant_force" in interaction.forces_per_contact:
+ resultant_force = Vector(*interaction.forces_per_contact["resultant_force"])
+ # print("Resultant force: ", resultant_force, id)
+ scaled = scale_vector(resultant_force, scale_factor)
+ resultant_point = Vector(*interaction.forces_per_contact["resultant_point"])
+ resultant = Vector.sum_vectors([resultant_point, scaled])
+ resultant_line = Line(Point(*resultant_point), Point(*resultant))
+ resultants.append(resultant_line)
+ magnitudes.append(str(round(resultant_force.length, 1)) + " kN")
+ comps = [
+ round(resultant_force.x, 3),
+ round(resultant_force.y, 3),
+ round(resultant_force.z, 3),
+ ]
+ components.append(comps)
+ resultant_points.append(resultant_point)
+ return resultants, magnitudes, components, resultant_points
+
+ def check_resultant_points(self):
+ polygons = []
+ resultant_points = []
+ points_out = []
+ points_not_polygon = []
+ for interaction in self.interactions:
+ geometry = interaction.contact_geometry
+ if isinstance(geometry, Polygon):
+ polygons.append(geometry.copy())
+ normal = geometry.normal
+ centroid = geometry.centroid
+ plane = Plane(centroid, normal)
+ frame = Frame.from_plane(plane)
+ projection = Transformation.from_frame_to_frame(frame, Frame.worldXY())
+ geometry.transform(projection)
+ point = Point(*interaction.forces_per_contact["resultant_point"])
+ contact_point = point.copy()
+ point.transform(projection)
+ if point.in_polygon(geometry):
+ resultant_points.append(contact_point)
+ else:
+ print(contact_point, " is outside")
+ points_out.append(contact_point)
+ else:
+ print(
+ "The interface is not a polygon",
+ " contact point " + str(interaction.forces_per_contact["resultant_point"]),
+ )
+ points_not_polygon.append(interaction.forces_per_contact["resultant_point"])
+
+ return polygons, resultant_points, points_out, points_not_polygon
+
+ # =============================================================================
+ # utilities
+ # =============================================================================
+ def check_and_exit(self, solve_ratio):
+
+ check_and_exit = """
+;===========================================================================
+; check equilibrium
+;===========================================================================
+fish define check_and_exit
+ local ratio = mech.solve('ratio-local')
+ if ratio > {} then
+ system.command('exit')
+ endif
+end
+@check_and_exit
+ """.format(
+ solve_ratio
+ )
+ return check_and_exit
+
+ def geometric_key(self, xyz, precision="3f", sanitize=True):
+ """Convert XYZ coordinates to a string that can be used as a dict key.
+
+ Parameters
+ ----------
+ xyz : list[float]
+ The XYZ coordinates.
+ precision : str, optional
+ A formatting option that specifies the precision of the
+ individual numbers in the string.
+ Supported values are any float precision (e.g. ``'3f'``), or decimal integer (``'d'``).
+ Default is ``None``, in which case the global precision setting will be used (:attr:`compas.PRECISION`).
+ sanitize : bool, optional
+ If True, minus signs ("-") will be removed from values that are equal to zero up to the given precision.
+
+ Returns
+ -------
+ str
+ The string representation of the given coordinates.
+
+ See also
+ --------
+ geometric_key_xy
+
+ Examples
+ --------
+ >>> from math import pi
+ >>> geometric_key([pi, pi, pi])
+ '3.142,3.142,3.142'
+
+ """
+ x, y, z = xyz
+ if not precision:
+ precision = "3f"
+ if precision == "d":
+ return "{0},{1},{2}".format(int(x), int(y), int(z))
+ if sanitize:
+ minzero = "-{0:.{1}}".format(0.0, precision)
+ if "{0:.{1}}".format(x, precision) == minzero:
+ x = 0.0
+ if "{0:.{1}}".format(y, precision) == minzero:
+ y = 0.0
+ if "{0:.{1}}".format(z, precision) == minzero:
+ z = 0.0
+ return "{0:.{3}},{1:.{3}},{2:.{3}}".format(x, y, z, precision)
+
+ # =============================================================================
+ # FISH output functions
+ # =============================================================================
+ def blocks_output(self):
+ """FISH function: get blocks data from 3DEC analysis:
+
+ Returns
+ -------
+ per block:
+ region n in 3DEC
+ int
+ centroid
+ x,y,z (precision = 18)
+ mass
+ float [Kg]
+ volume
+ float [mc]
+ out of balance force
+ fx,fy,fz [N]
+ moments
+ mx,my,mz [Nm]
+ loads
+ lx,ly,lz [N]
+ velocity
+ (vx,vy,vz) [m/s]
+ list of vertices (coordinates)
+ x,y,z (precision = 18)
+ """
+
+ blocks_output = """
+
+;===========================================================================
+; get blocks data
+;===========================================================================
+fish define blocks_output
+ ii = io.out('solve ratio = '+' '+string(mech.solve('ratio-local')))
+ ii = io.out('timestep = '+' '+string(mech.timestep))
+ ii=io.out(' centr - result - veloc')
+ ic = block.contact.head
+ loop foreach ib block.list
+ bid = block.id(ib)
+ br=block.region(ib)
+ ii=io.out('block '+string(bid))
+ ii=io.out('region '+string(br))
+ ii=io.out('centroid'+' '+'='+' '+string(block.pos.x(ib))+','+string(block.pos.y(ib))+','+string(block.pos.z(ib))+' '+string(br))
+ ii=io.out('mass '+' '+string(block.mass(ib)))
+ ii=io.out('volume '+' '+string(block.vol(ib)))
+ vel=block.vel(ib)
+ rx=block.force.unbal.x(ib)
+ ry=block.force.unbal.y(ib)
+ rz=block.force.unbal.z(ib)
+ lx=block.force.app.x(ib)
+ ly=block.force.app.y(ib)
+ lz=block.force.app.z(ib)
+ ; if there is gravity, the block weight should be added
+ rz=block.force.unbal.z(ib)+block.mass(ib)*global.gravity.z
+ ii=io.out('forces'+' '+'='+' '+string(rx)+','+string(ry)+','+string(rz)+' '+string(br))
+ ii=io.out('moment'+' '+'='+' '+string(block.moment.x(ib))+','+string(block.moment.y(ib))+','+string(block.moment.z(ib))+' '+string(br))
+ ii=io.out('loads'+' '+'='+' '+string(lx)+','+string(ly)+','+string(lz)+' '+string(br))
+ ii=io.out('velocity'+' '+'='+' '+string(vel)+' '+string(br))
+ loop foreach vi block.gplist(ib)
+ ii = io.out('vertex'+' '+'='+' '+string(block.gp.pos(vi))+' '+string(block.region(block.gp.hostblock(vi))))
+ vi = block.gp.next(vi)
+ endloop
+ ib = block.next(ib)
+ endloop
+end
+ """
+
+ return blocks_output
+
+ def save_blocks_output(self, state):
+ save_blocks_output = """
+;===========================================================================
+; save blocks output
+;===========================================================================
+log on
+log-file '{}.txt'
+@blocks_output
+log off
+ """.format(
+ state
+ )
+ return save_blocks_output
+
+ def contacts_output(self):
+ """FISH function: get contacts data from 3DEC analysis:
+
+ Returns
+ -------
+
+ """
+ contacts_output = """
+;===========================================================================
+; get contacts data
+;===========================================================================
+fish define contacts_output
+loop foreach ic block.contact.list()
+ii=io.out('contact'+' '+'='+' '+string(ic)+' '+string(block.contact.type(ic))+' '+string(block.region(block.contact.b1(ic)))+' '+string(block.region(block.contact.b2(ic)))+' '+string(block.contact.pos(ic))+' '+string(block.contact.normal(ic)))
+ loop foreach si block.contact.subcontactlist(ic)
+ ii=io.out('subcontact'+' '+'='+' '+string(block.subcontact.pos(si))+' '+string(block.subcontact.force.norm(si))+' '+string(block.subcontact.force.shear(si))+' '+string(si)+' '+string(block.subcontact.disp.norm(si))+' '+string(block.subcontact.disp.shear(si))+' '+string(block.subcontact.stress.norm(si))+' '+string(block.subcontact.stress.shear(si))+' '+string(block.subcontact.area(si)))
+ fi = block.subcontact.face(si)
+; if fi then
+; fo = block.face.bface(fi)
+; ii = io.out('face centroid'+' '+'='+' '+string(block.face.pos(fo)))
+; endif
+ si = block.subcontact.next(si)
+ endloop
+ic = block.contact.next(ic)
+endloop
+end
+
+ """
+ return contacts_output
+
+ def save_contacts_output(self, state):
+ save_contacts_output = """
+;===========================================================================
+; save contacts output
+;===========================================================================
+log on
+log-file '{}.txt'
+@contacts_output
+log off
+ """.format(
+ state
+ )
+ return save_contacts_output
+
+ # =============================================================================
+ # analysis utilities
+ # =============================================================================
+ def save_analysis(self, stage):
+ """
+ Stages: init
+ grav
+ step
+ """
+ save_analysis = """
+;===========================================================================
+; save analysis
+;===========================================================================
+model save "./{}.sav" compress
+""".format(
+ stage
+ )
+ return save_analysis
+
+ def restore_analysis(self, stage):
+ """
+ Stages: init
+ grav
+ step
+ """
+ restore_analysis = """
+;===========================================================================
+; restore analysis
+;===========================================================================
+model restore "./{}.sav"
+""".format(
+ stage
+ )
+ return restore_analysis
+
+ def solve_ratio_check(self, filename):
+ with open(os.path.join(self.working_path, filename), "r") as fo:
+ for line in fo:
+ line = line.strip()
+ if not line:
+ continue
+ parts = line.split()
+ if not len(parts):
+ continue
+ if parts[0] == "solve":
+ solve_r = float(parts[3])
+ if solve_r <= 1.0000e-05:
+ print("Equilibrium reached")
+ print("solve ratio = " + str(solve_r))
+ result = "Equilibrium"
+ else:
+ print("Equilibrium NOT reached")
+ print("solve ratio = " + str(solve_r))
+ result = "Collapse"
+ return result
+
+ # =============================================================================
+ # damping
+ # =============================================================================
+ # refer to this link for documentation:
+ # https://docs.itascacg.com/3dec700/3dec/block/doc/manual/block_manual/block_commands/block/cmd_block.mechanical.html#block.mechanical
+
+ def set_damping_global(self, fac=False, f1=None, f2=None):
+ header = "block mech damping global"
+ if fac:
+ header = "block mech damping global" + " " + str(fac) + " " + str(f1) + " " + str(f2)
+ return header
+
+ def set_damping_local(self, custom=False, f=None):
+ header = "block mech damping local"
+
+ if custom:
+ header = "block mech damping local" + " " + str(f)
+ return header
+
+ def set_damping_contact(self, damping_value):
+ pass
+
+ def set_damping_combined(self, damping_value):
+ pass
+
+ def set_damping_maxwell(self, damping_value):
+ pass
+
+ def set_damping_rayleigh(self, f1, f2, keyword):
+ """This form of the command is normally used for dynamic calculations when a
+ certain fraction of critical damping is required over a given frequency range.
+ This type of damping is known as Rayleigh damping, where f1 = the fraction of
+ critical damping operating at the center frequency of f2. See below for further
+ discussion.
+ keywords:
+ mass = Restrict the damping to mass-proportional only.
+ stiffness = Restrict the damping to stiffness-proportional only.
+ """
+ header = "block mech damping rayleigh" + " " + str(f1) + " " + str(f2) + " " + str(keyword)
+ return header
diff --git a/src/compas_3dec/datastructure/rigid_interaction.py b/src/compas_3dec/datastructure/rigid_interaction.py
new file mode 100644
index 0000000..baab245
--- /dev/null
+++ b/src/compas_3dec/datastructure/rigid_interaction.py
@@ -0,0 +1,46 @@
+from compas.data import Data
+
+
+class RigidInteraction(Data):
+ """
+ Represents a collection of rigid interaction compounds for use in 3DEC models.
+
+ Parameters
+ ----------
+ compounds : list, optional
+ List of compounds representing rigid interactions. If not provided, an empty list is used.
+
+ Attributes
+ ----------
+ compounds : list
+ The list of rigid interaction compounds.
+
+ Examples
+ --------
+ >>> ri = RigidInteraction(compounds=[compound1, compound2])
+ >>> print(ri)
+ Rigid Interactions [compound1, compound2]
+ """
+
+ def __init__(
+ self,
+ compounds=None,
+ ):
+ super().__init__()
+ self.compounds = compounds if compounds is not None else []
+
+ @property
+ def __data__(self):
+ # type: () -> dict
+ return {
+ "compounds": self.compounds,
+ }
+
+ @classmethod
+ def __from_data__(cls, data):
+ return cls(
+ compounds=data["compounds"],
+ )
+
+ def __str__(self):
+ return f"Rigid Interactions {self.compounds}"
diff --git a/src/compas_3dec/notebook/__init__.py b/src/compas_3dec/notebook/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/compas_3dec/notebook/scene/__init__.py b/src/compas_3dec/notebook/scene/__init__.py
new file mode 100644
index 0000000..8aa820a
--- /dev/null
+++ b/src/compas_3dec/notebook/scene/__init__.py
@@ -0,0 +1,25 @@
+"""This package provides scene object plugins for visualising COMPAS Model objects in Rhino.
+When working in Rhino, :class:`compas.scene.SceneObject` will automatically use
+the corresponding Rhino scene object for each COMPAS model object type.
+
+"""
+
+from compas.plugins import plugin
+from compas.scene import register
+
+from compas_3dec.blockelement3dec import BlockElement
+from compas_3dec.interactions_3dec import Interaction3dec
+from compas_3dec.model_3dec import Model_3dec
+from .blockobject import ThreeBlockObject
+from .interactionobject import ThreeInteractionObject
+from .modelobject import ThreeModelObject
+
+
+@plugin(category="factories", requires=["pythreejs"])
+def register_scene_objects():
+ register(BlockElement, ThreeBlockObject, context="Notebook")
+ register(Interaction3dec, ThreeInteractionObject, context="Notebook")
+ register(Model_3dec, ThreeModelObject, context="Notebook")
+
+
+__all__ = ["ThreeBlockObject", "ThreeInteractionObject", "ThreeModelObject"]
diff --git a/src/compas_3dec/notebook/scene/blockobject.py b/src/compas_3dec/notebook/scene/blockobject.py
new file mode 100644
index 0000000..b29057d
--- /dev/null
+++ b/src/compas_3dec/notebook/scene/blockobject.py
@@ -0,0 +1,178 @@
+import compas.datastructures # noqa: F401
+import compas.geometry # noqa: F401
+import numpy
+import pythreejs as three
+from compas.geometry import Polygon
+from compas.geometry import earclip_polygon
+
+# from .elementobject import ThreeElementObject
+from compas_notebook.scene import ThreeSceneObject
+from compas_3dec.scene.blockobject import BlockObject
+
+
+class ThreeBlockObject(ThreeSceneObject, BlockObject):
+ """Scene object for drawing block objects."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def draw(self):
+ """Draw the mesh associated with the scene object.
+
+ Returns
+ -------
+ list[three.Mesh, three.LineSegments]
+ List of pythreejs objects created.
+
+ """
+ self._guids = []
+
+ mesh = self.element.geometry # type: ignore
+
+ vertices = list(mesh.vertices()) # type: ignore
+ faces = list(mesh.faces()) # type: ignore
+ edges = list(mesh.edges()) # type: ignore
+
+ transformation = self.element.worldtransformation
+
+ if transformation:
+ matrix = ( # noqa: F841 # type: ignore
+ numpy.array(transformation.matrix, dtype=numpy.float32).transpose().ravel().tolist()
+ )
+
+ vertex_xyz = {vertex: mesh.vertex_attributes(vertex, "xyz") for vertex in vertices} # type: ignore
+
+ # =============================================================================
+ # Vertices
+ # =============================================================================
+
+ if self.show_vertices:
+ if self.show_vertices is not True:
+ vertices = self.show_vertices
+
+ positions = [vertex_xyz[vertex] for vertex in vertices]
+ positions = numpy.array(positions, dtype=numpy.float32)
+
+ colors = [self.vertexcolor[vertex] for vertex in vertices] # type: ignore
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.PointsMaterial(
+ size=self.vertexsize,
+ vertexColors="VertexColors",
+ )
+
+ threeobject = three.Points(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ self._guids.append(threeobject)
+
+ # =============================================================================
+ # Edges
+ # =============================================================================
+
+ if self.show_edges:
+ if self.show_edges is not True:
+ edges = self.show_edges
+
+ positions = []
+ colors = []
+
+ for u, v in edges:
+ positions.append(vertex_xyz[u])
+ positions.append(vertex_xyz[v])
+ colors.append(self.edgecolor[u, v])
+ colors.append(self.edgecolor[u, v])
+
+ positions = numpy.array(positions, dtype=numpy.float32)
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.LineBasicMaterial(vertexColors="VertexColors")
+
+ threeobject = three.LineSegments(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ self._guids.append(threeobject)
+
+ # =============================================================================
+ # Faces
+ # =============================================================================
+
+ if self.show_faces:
+ if self.show_faces is not True:
+ faces = self.show_faces
+
+ positions = []
+ colors = []
+
+ for face in faces:
+ vertices = mesh.face_vertices(face) # type: ignore
+ c = self.facecolor[face] # type: ignore
+
+ if len(vertices) == 3:
+ positions.append(vertex_xyz[vertices[0]])
+ positions.append(vertex_xyz[vertices[1]])
+ positions.append(vertex_xyz[vertices[2]])
+ colors.append(c)
+ colors.append(c)
+ colors.append(c)
+
+ elif len(vertices) == 4:
+ positions.append(vertex_xyz[vertices[0]])
+ positions.append(vertex_xyz[vertices[1]])
+ positions.append(vertex_xyz[vertices[2]])
+ colors.append(c)
+ colors.append(c)
+ colors.append(c)
+ positions.append(vertex_xyz[vertices[0]])
+ positions.append(vertex_xyz[vertices[2]])
+ positions.append(vertex_xyz[vertices[3]])
+ colors.append(c)
+ colors.append(c)
+ colors.append(c)
+
+ else:
+ polygon = Polygon([vertex_xyz[v] for v in vertices])
+ ears = earclip_polygon(polygon)
+ for ear in ears: # type: ignore
+ positions.append(vertex_xyz[vertices[ear[0]]])
+ positions.append(vertex_xyz[vertices[ear[1]]])
+ positions.append(vertex_xyz[vertices[ear[2]]])
+ colors.append(c)
+ colors.append(c)
+ colors.append(c)
+
+ positions = numpy.array(positions, dtype=numpy.float32)
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.MeshBasicMaterial(
+ side="DoubleSide",
+ vertexColors="VertexColors",
+ )
+
+ threeobject = three.Mesh(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ self._guids.append(threeobject)
+
+ return self.guids
diff --git a/src/compas_3dec/notebook/scene/interactionobject.py b/src/compas_3dec/notebook/scene/interactionobject.py
new file mode 100644
index 0000000..3aaa5ab
--- /dev/null
+++ b/src/compas_3dec/notebook/scene/interactionobject.py
@@ -0,0 +1,265 @@
+import compas.datastructures # noqa: F401
+import compas.geometry # noqa: F401
+import numpy
+import pythreejs as three
+from compas.geometry import Polygon
+from compas.geometry import earclip_polygon
+
+# from .interactionobject import ThreeinteractionObject
+from compas_notebook.scene import ThreeSceneObject
+from compas_3dec.scene.interactionobject import InteractionObject
+
+
+class ThreeInteractionObject(ThreeSceneObject, InteractionObject):
+ """Scene object for drawing block objects."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def draw_mesh(self, mesh, color=None):
+ """Draw the mesh associated with the scene object.
+
+ Returns
+ -------
+ list[three.Mesh, three.LineSegments]
+ List of pythreejs objects created.
+
+ """
+ guids = []
+
+ vertices = list(mesh.vertices()) # type: ignore
+ faces = list(mesh.faces()) # type: ignore
+ edges = list(mesh.edges()) # type: ignore
+
+ # transformation = self.interaction.worldtransformation
+
+ # if transformation:
+ # matrix = ( # noqa: F841 # type: ignore
+ # numpy.array(transformation.matrix, dtype=numpy.float32).transpose().ravel().tolist()
+ # )
+
+ vertex_xyz = {vertex: mesh.vertex_attributes(vertex, "xyz") for vertex in vertices} # type: ignore
+
+ # =============================================================================
+ # Vertices
+ # =============================================================================
+
+ if self.show_vertices:
+ if self.show_vertices is not True:
+ vertices = self.show_vertices
+
+ positions = [vertex_xyz[vertex] for vertex in vertices]
+ positions = numpy.array(positions, dtype=numpy.float32)
+
+ colors = [self.vertexcolor[vertex] for vertex in vertices] # type: ignore
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.PointsMaterial(
+ size=self.vertexsize,
+ vertexColors="VertexColors",
+ )
+
+ threeobject = three.Points(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ guids.append(threeobject)
+
+ # =============================================================================
+ # Edges
+ # =============================================================================
+
+ if self.show_edges:
+ if self.show_edges is not True:
+ edges = self.show_edges
+
+ positions = []
+ colors = []
+
+ for u, v in edges:
+ positions.append(vertex_xyz[u])
+ positions.append(vertex_xyz[v])
+ colors.append(self.edgecolor[u, v])
+ colors.append(self.edgecolor[u, v])
+
+ positions = numpy.array(positions, dtype=numpy.float32)
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.LineBasicMaterial(vertexColors="VertexColors")
+
+ threeobject = three.LineSegments(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ guids.append(threeobject)
+
+ # =============================================================================
+ # Faces
+ # =============================================================================
+
+ if self.show_faces:
+ if self.show_faces is not True:
+ faces = self.show_faces
+
+ positions = []
+ colors = []
+
+ for face in faces:
+ vertices = mesh.face_vertices(face) # type: ignore
+ c = self.facecolor[face] # type: ignore
+
+ if len(vertices) == 3:
+ positions.append(vertex_xyz[vertices[0]])
+ positions.append(vertex_xyz[vertices[1]])
+ positions.append(vertex_xyz[vertices[2]])
+ colors.append(c)
+ colors.append(c)
+ colors.append(c)
+
+ elif len(vertices) == 4:
+
+ positions.append(vertex_xyz[vertices[0]])
+ positions.append(vertex_xyz[vertices[1]])
+ positions.append(vertex_xyz[vertices[2]])
+ colors.append(mesh.vertex_attribute(vertices[0], "color"))
+ colors.append(mesh.vertex_attribute(vertices[1], "color"))
+ colors.append(mesh.vertex_attribute(vertices[2], "color"))
+ positions.append(vertex_xyz[vertices[0]])
+ positions.append(vertex_xyz[vertices[2]])
+ positions.append(vertex_xyz[vertices[3]])
+ colors.append(mesh.vertex_attribute(vertices[0], "color"))
+ colors.append(mesh.vertex_attribute(vertices[2], "color"))
+ colors.append(mesh.vertex_attribute(vertices[3], "color"))
+
+ else:
+ polygon = Polygon([vertex_xyz[v] for v in vertices])
+ ears = earclip_polygon(polygon)
+ for ear in ears: # type: ignore
+ positions.append(vertex_xyz[vertices[ear[0]]])
+ positions.append(vertex_xyz[vertices[ear[1]]])
+ positions.append(vertex_xyz[vertices[ear[2]]])
+ colors.append(mesh.vertex_attribute(vertices[0], "color"))
+ colors.append(mesh.vertex_attribute(vertices[1], "color"))
+ colors.append(mesh.vertex_attribute(vertices[2], "color"))
+
+ positions = numpy.array(positions, dtype=numpy.float32)
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.MeshBasicMaterial(
+ side="DoubleSide",
+ vertexColors="VertexColors",
+ )
+
+ threeobject = three.Mesh(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ guids.append(threeobject)
+
+ return guids
+
+ def draw_lines(self, lines, color, scale_factor=1):
+ guids = []
+ positions = []
+ colors = []
+
+ for line in lines:
+ positions.append(line.start)
+ positions.append(line.end)
+ colors.append(color)
+ colors.append(color)
+
+ positions = numpy.array(positions, dtype=numpy.float32)
+ colors = numpy.array(colors, dtype=numpy.float32)
+
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(positions, normalized=False),
+ "color": three.BufferAttribute(colors, normalized=False, itemSize=3),
+ }
+ )
+ material = three.LineBasicMaterial(vertexColors="VertexColors", linewidth=self.thickness_lines)
+
+ threeobject = three.LineSegments(geometry, material)
+ # threeobject.matrix = matrix
+ # threeobject.matrixAutoUpdate = False
+
+ guids.append(threeobject)
+ return guids
+
+ def draw_points(self, points, color):
+ guids = []
+ geometry = three.BufferGeometry(
+ attributes={
+ "position": three.BufferAttribute(points, normalized=False),
+ "color": three.BufferAttribute(color, normalized=False, itemSize=3),
+ }
+ )
+ material = three.PointsMaterial(
+ size=self.vertexsize * 0.1,
+ vertexColors="VertexColors",
+ )
+
+ threeobject = three.Points(geometry, material)
+ guids.append(threeobject)
+
+ return guids
+
+ def draw(self):
+ """Draw the mesh associated with the scene object.
+
+ Returns
+ -------
+ list[three.Mesh, three.LineSegments]
+ List of pythreejs objects created.
+
+ """
+ self._guids = []
+
+ if self.show_normal_force_lines:
+ guids = self.draw_lines(self.interaction.normal_force_lines, self.color_normal_force_lines)
+ self._guids.extend(guids)
+
+ if self.show_shear_force_lines:
+ guids = self.draw_lines(self.interaction.shear_force_lines, self.color_shear_force_lines)
+ self._guids.extend(guids)
+
+ if self.show_points:
+ guids = self.draw_points(self.interaction.points, self.color_points)
+ self._guids.extend(guids)
+
+ if self.show_mesh_normal_stress:
+ guids = self.draw_mesh(self.interaction.mesh_normal_stress)
+ self._guids.extend(guids)
+
+ if self.show_mesh_shear_stress:
+ guids = self.draw_mesh(self.interaction.mesh_shear_stress)
+ self._guids.extend(guids)
+
+ if self.show_resultant_force:
+ guids = self.draw_lines(self.interaction.resultant_force, self.color_resultant_force)
+ self._guids.extend(guids)
+
+ if self.show_resultant_point:
+ guids = self.draw_points(self.interaction.resultant_point, self.color_points)
+ self._guids.extend(guids)
+
+ return self.guids
diff --git a/src/compas_3dec/notebook/scene/modelobject.py b/src/compas_3dec/notebook/scene/modelobject.py
new file mode 100644
index 0000000..0b94a12
--- /dev/null
+++ b/src/compas_3dec/notebook/scene/modelobject.py
@@ -0,0 +1,26 @@
+from compas_notebook.scene import ThreeSceneObject
+
+from compas_3dec.scene import BlockObject
+from compas_3dec.scene import ModelObject
+
+
+class ThreeModelObject(ThreeSceneObject, ModelObject):
+ """Scene object for drawing block objects."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def draw(self):
+ """Draw the mesh associated with the scene object.
+
+ Returns
+ -------
+ list[three.Mesh, three.LineSegments]
+ List of pythreejs objects created.
+
+ """
+ for child in self.children:
+ if isinstance(child, BlockObject):
+ child.show = self.show_elements
+
+ return self.guids
diff --git a/src/compas_3dec/rhino/__init__.py b/src/compas_3dec/rhino/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/compas_3dec/rhino/build/rh8/compas_3dec-0.1.29929.8914-rh8-any.yak b/src/compas_3dec/rhino/build/rh8/compas_3dec-0.1.29929.8914-rh8-any.yak
new file mode 100644
index 0000000..6764011
Binary files /dev/null and b/src/compas_3dec/rhino/build/rh8/compas_3dec-0.1.29929.8914-rh8-any.yak differ
diff --git a/src/compas_3dec/rhino/build/rh8/compas_3dec.rhp b/src/compas_3dec/rhino/build/rh8/compas_3dec.rhp
new file mode 100644
index 0000000..152e427
Binary files /dev/null and b/src/compas_3dec/rhino/build/rh8/compas_3dec.rhp differ
diff --git a/src/compas_3dec/rhino/build/rh8/compas_3dec.rui b/src/compas_3dec/rhino/build/rh8/compas_3dec.rui
new file mode 100644
index 0000000..6f20df9
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/compas_3dec.rui
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+ compas_3dec
+
+
+
+
+ compas_3dec
+
+ 574526bf-a929-42e4-9a79-29d79faaff6b
+
+
+
+
+
+
+ compas_3dec
+
+
+
+ select_blocks
+
+ de172756-286d-43d5-8c1f-5143e7bdff62
+
+
+
+
+
+
+
+ Macro.select_blocks
+
+
+ select_blocks
+
+
+ select_blocks
+
+
+ select_blocks
+
+
+ select_blocks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/compas_3dec/rhino/build/rh8/manifest.yml b/src/compas_3dec/rhino/build/rh8/manifest.yml
new file mode 100644
index 0000000..d0c5576
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/manifest.yml
@@ -0,0 +1,10 @@
+---
+name: compas_3dec
+version: 0.1.29929.8914
+authors:
+- Alessandro Dell'Endice
+description: ''
+license: ''
+url: ''
+keywords:
+- guid:574526bf-a929-42e4-9a79-29d79faaff6b
diff --git a/src/compas_3dec/rhino/build/rh8/src/compas_3dec.sln b/src/compas_3dec/rhino/build/rh8/src/compas_3dec.sln
new file mode 100644
index 0000000..28cf9a8
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/src/compas_3dec.sln
@@ -0,0 +1,17 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{574526bf-a929-42e4-9a79-29d79faaff6b}") = "compas_3dec", "compas_3dec\compas_3dec.csproj", "{93d8559a-e5ec-4df6-a1e5-4a62db7bd90f}"
+EndProject
+
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {93d8559a-e5ec-4df6-a1e5-4a62db7bd90f}.Debug|Any CPU.Build.0 = Debug|Any CPU
+{93d8559a-e5ec-4df6-a1e5-4a62db7bd90f}.Release|Any CPU.Build.0 = Release|Any CPU
+
+ EndGlobalSection
+EndGlobal
diff --git a/src/compas_3dec/rhino/build/rh8/src/compas_3dec/AssemblyInfo.cs b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/AssemblyInfo.cs
new file mode 100644
index 0000000..d56dc9b
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Reflection;
+using System.Runtime.Versioning;
+using System.Runtime.InteropServices;
+
+using Rhino.PlugIns;
+
+[assembly: PlugInDescription(DescriptionType.Organization, "BRG, ETHZ")]
+[assembly: PlugInDescription(DescriptionType.Email, "dellendice@arch.ethz.ch ")]
+[assembly: PlugInDescription(DescriptionType.Phone, "")]
+[assembly: PlugInDescription(DescriptionType.Address, "")]
+[assembly: PlugInDescription(DescriptionType.Country, "")]
+[assembly: PlugInDescription(DescriptionType.WebSite, "")]
+[assembly: PlugInDescription(DescriptionType.UpdateUrl, "")]
+[assembly: PlugInDescription(DescriptionType.Icon, "compas_3dec.Resources.projectIcon.ico")]
+
+[assembly: AssemblyTitle("compas_3dec")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyCompany("BRG, ETHZ")]
+[assembly: AssemblyProduct("compas_3dec")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+[assembly: Guid("574526bf-a929-42e4-9a79-29d79faaff6b")]
+[assembly: AssemblyVersion("0.1.29929.8914")]
+[assembly: AssemblyFileVersion("0.1.29929.8914")]
+[assembly: AssemblyInformationalVersion("0.1.29929.8914")]
+
+[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
diff --git a/src/compas_3dec/rhino/build/rh8/src/compas_3dec/ProjectCommand_select_blocks.cs b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/ProjectCommand_select_blocks.cs
new file mode 100644
index 0000000..955965c
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/ProjectCommand_select_blocks.cs
@@ -0,0 +1,32 @@
+using System;
+
+using Rhino;
+using Rhino.Commands;
+
+namespace RhinoCodePlatform.Rhino3D.Projects.Plugin
+{
+ [CommandStyle(Rhino.Commands.Style.ScriptRunner)]
+ public class ProjectCommand_select_blocks : Command
+ {
+ public Guid CommandId { get; } = new Guid("de172756-286d-43d5-8c1f-5143e7bdff62");
+
+ public ProjectCommand_select_blocks() { Instance = this; }
+
+ public static ProjectCommand_select_blocks Instance { get; private set; }
+
+ public override string EnglishName => "select_blocks";
+
+ protected override Rhino.Commands.Result RunCommand(RhinoDoc doc, RunMode mode)
+ {
+ // NOTE:
+ // Initialize() attempts to loads the core rhinocode plugin
+ // and prepare the scripting platform. This call can not be in any static
+ // ctors of Command or Plugin classes since plugins can not be loaded while
+ // rhino is loading this plugin. The call has an initialized check and is
+ // very fast after the first run.
+ ProjectPlugin.Initialize();
+
+ return ProjectPlugin.RunCode(this, CommandId, doc, mode);
+ }
+ }
+}
diff --git a/src/compas_3dec/rhino/build/rh8/src/compas_3dec/ProjectPlugin.cs b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/ProjectPlugin.cs
new file mode 100644
index 0000000..2c4efe6
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/ProjectPlugin.cs
@@ -0,0 +1,96 @@
+using System;
+
+using Rhino;
+using Rhino.Commands;
+using Rhino.PlugIns;
+
+using Rhino.Runtime.Code;
+using Rhino.Runtime.Code.Platform;
+
+namespace RhinoCodePlatform.Rhino3D.Projects.Plugin
+{
+ public class ProjectPlugin : PlugIn
+ {
+ static readonly Guid s_rhinocode = new Guid("c9cba87a-23ce-4f15-a918-97645c05cde7");
+ static readonly PlatformSpec s_rhino = new PlatformSpec("mcneel.rhino3d.rhino");
+
+ static readonly Guid s_projectId = new Guid("574526bf-a929-42e4-9a79-29d79faaff6b");
+ static readonly string s_projectData = "ew0KICAiaWQiOiAiNTc0NTI2YmYtYTkyOS00MmU0LTlhNzktMjlkNzlmYWFmZjZiIiwNCiAgImlkZW50aXR5Ijogew0KICAgICJuYW1lIjogImNvbXBhc18zZGVjIiwNCiAgICAidmVyc2lvbiI6ICIwLjEiLA0KICAgICJwdWJsaXNoZXIiOiB7DQogICAgICAiZW1haWwiOiAiZGVsbGVuZGljZUBhcmNoLmV0aHouY2ggIiwNCiAgICAgICJuYW1lIjogIkFsZXNzYW5kcm8gRGVsbFx1MDAyN0VuZGljZSIsDQogICAgICAiY29tcGFueSI6ICJCUkcsIEVUSFoiDQogICAgfQ0KICB9LA0KICAic2V0dGluZ3MiOiB7DQogICAgImJ1aWxkUGF0aCI6ICJmaWxlOi8vL0M6L1VzZXJzL2FkZWxsZW5kL0NvZGUyL2NvbXBhc18zZGVjL3NyYy9jb21wYXNfM2RlYy9yaGluby9idWlsZC9yaDgiLA0KICAgICJidWlsZFRhcmdldCI6IHsNCiAgICAgICJhcHBOYW1lIjogIlJoaW5vM0QiLA0KICAgICAgImFwcFZlcnNpb24iOiB7DQogICAgICAgICJtYWpvciI6IDgNCiAgICAgIH0sDQogICAgICAidGl0bGUiOiAiUmhpbm8zRCAoOC4qKSIsDQogICAgICAic2x1ZyI6ICJyaDgiDQogICAgfSwNCiAgICAicHVibGlzaFRhcmdldCI6IHsNCiAgICAgICJ0aXRsZSI6ICJNY05lZWwgWWFrIFNlcnZlciINCiAgICB9DQogIH0sDQogICJjb2RlcyI6IFsNCiAgICB7DQogICAgICAiaWQiOiAiZGUxNzI3NTYtMjg2ZC00M2Q1LThjMWYtNTE0M2U3YmRmZjYyIiwNCiAgICAgICJsYW5ndWFnZSI6IHsNCiAgICAgICAgImlkIjogIiouKi5weXRob24iLA0KICAgICAgICAidmVyc2lvbiI6ICIzLiouKiINCiAgICAgIH0sDQogICAgICAidGl0bGUiOiAic2VsZWN0X2Jsb2NrcyIsDQogICAgICAiaW1hZ2UiOiB7DQogICAgICAgICJsaWdodCI6IHsNCiAgICAgICAgICAidHlwZSI6ICJzdmciLA0KICAgICAgICAgICJkYXRhIjogIlBITjJaeUJwWkQwaVRHRjVaWEpmTVNJZ1pHRjBZUzF1WVcxbFBTSk1ZWGxsY2lBeElpQjRiV3h1Y3owaWFIUjBjRG92TDNkM2R5NTNNeTV2Y21jdk1qQXdNQzl6ZG1jaUlIWnBaWGRDYjNnOUlqQWdNQ0EwT0NBME9DSVx1MDAyQlBHUmxabk1cdTAwMkJQSE4wZVd4bFBpNWpiSE10TVh0bWFXeHNPaU0xWXpoa05tSTdmUzVqYkhNdE1Td3VZMnh6TFRKN2MzUnliMnRsT2lObVptWTdjM1J5YjJ0bExXeHBibVZqWVhBNmNtOTFibVE3YzNSeWIydGxMV3hwYm1WcWIybHVPbkp2ZFc1a08zMHVZMnh6TFRKN1ptbHNiRG9qWmpSbU5HWTFPMzA4TDNOMGVXeGxQand2WkdWbWN6NDhaeUJwWkQwaVVFOU1XVXhKVGtVaVBqeHdiMng1WjI5dUlHTnNZWE56UFNKamJITXRNU0lnY0c5cGJuUnpQU0kwTGpRMklETTFMalk0SURFMExqTTJJRFEyTGpVeUlETXhMak15SURRd0xqRTVJRE14TGpNeUlETXlMak0zSURRMExqYzFJREkzTGpNMUlEUTBMamMxSURFeUxqSTFJRE0wTGpnMElERXVOREVnTVRjdU9Ea2dOeTQzTlNBeE55NDRPU0F4TlM0MU55QTBMalEySURJd0xqVTVJRFF1TkRZZ016VXVOamdpTHo0OEwyY1x1MDAyQlBHY2dhV1E5SWxOUVRFbE9SU0lcdTAwMkJQSEJoZEdnZ1kyeGhjM005SW1Oc2N5MHlJaUJrUFNKTk5DNDBOaXd6TlM0Mk9HdzVMamtzTVRBdU9EUWlMejQ4TDJjXHUwMDJCUEdjZ2FXUTlJbE5RVEVsT1JTMHlJaUJrWVhSaExXNWhiV1U5SWxOUVRFbE9SU0lcdTAwMkJQSEJoZEdnZ1kyeGhjM005SW1Oc2N5MHlJaUJrUFNKTk5DNDBOaXd5TUM0MU9XdzVMamtzTVRBdU9EUWlMejQ4TDJjXHUwMDJCUEdjZ2FXUTlJbE5RVEVsT1JTMHpJaUJrWVhSaExXNWhiV1U5SWxOUVRFbE9SU0lcdTAwMkJQSEJoZEdnZ1kyeGhjM005SW1Oc2N5MHlJaUJrUFNKTk5DNDBOaXd6TlM0Mk9GWXlNQzQxT1NJdlBqd3ZaejQ4WnlCcFpEMGlVMUJNU1U1RkxUUWlJR1JoZEdFdGJtRnRaVDBpVTFCTVNVNUZJajQ4Y0dGMGFDQmpiR0Z6Y3owaVkyeHpMVElpSUdROUlrMHhOQzR6Tml3ME5pNDFNbXd4TnkwMkxqTXpJaThcdTAwMkJQQzluUGp4bklHbGtQU0pUVUV4SlRrVXROU0lnWkdGMFlTMXVZVzFsUFNKVFVFeEpUa1VpUGp4d1lYUm9JR05zWVhOelBTSmpiSE10TWlJZ1pEMGlUVE14TGpNeUxEUXdMakU1VmpNeUxqTTNJaThcdTAwMkJQQzluUGp4bklHbGtQU0pUVUV4SlRrVXROaUlnWkdGMFlTMXVZVzFsUFNKVFVFeEpUa1VpUGp4d1lYUm9JR05zWVhOelBTSmpiSE10TWlJZ1pEMGlUVEUwTGpNMkxEUTJMalV5VmpNeExqUXpJaThcdTAwMkJQQzluUGp4bklHbGtQU0pUVUV4SlRrVXROeUlnWkdGMFlTMXVZVzFsUFNKVFVFeEpUa1VpUGp4d1lYUm9JR05zWVhOelBTSmpiSE10TWlJZ1pEMGlUVEUwTGpNMkxETXhMalF6YkRndU5EZ3RNeTR4TnlJdlBqd3ZaejQ4WnlCcFpEMGlVMUJNU1U1RkxUZ2lJR1JoZEdFdGJtRnRaVDBpVTFCTVNVNUZJajQ4Y0dGMGFDQmpiR0Z6Y3owaVkyeHpMVElpSUdROUlrMHhOeTQ0T1N3M0xqYzFiRGt1T1N3eE1DNDROQ0l2UGp3dlp6NDhaeUJwWkQwaVUxQk1TVTVGTFRraUlHUmhkR0V0Ym1GdFpUMGlVMUJNU1U1RklqNDhjR0YwYUNCamJHRnpjejBpWTJ4ekxUSWlJR1E5SWswME5DNDNOU3d5Tnk0ek5WWXhNaTR5TlNJdlBqd3ZaejQ4WnlCcFpEMGlVMUJNU1U1RkxURXdJaUJrWVhSaExXNWhiV1U5SWxOUVRFbE9SU0lcdTAwMkJQSEJoZEdnZ1kyeGhjM005SW1Oc2N5MHlJaUJrUFNKTk16UXVPRFFzTVM0ME1Xd3RNVGNzTmk0ek5DSXZQand2Wno0OFp5QnBaRDBpVTFCTVNVNUZMVEV4SWlCa1lYUmhMVzVoYldVOUlsTlFURWxPUlNJXHUwMDJCUEhCaGRHZ2dZMnhoYzNNOUltTnNjeTB5SWlCa1BTSk5NamN1Tnprc01UZ3VOVGxzTVRjdE5pNHpOQ0l2UGp3dlp6NDhaeUJwWkQwaVUxQk1TVTVGTFRFeUlpQmtZWFJoTFc1aGJXVTlJbE5RVEVsT1JTSVx1MDAyQlBIQmhkR2dnWTJ4aGMzTTlJbU5zY3kweUlpQmtQU0pOTVRjdU9Ea3NNVFV1TlRkc0xURXpMalF6TERVaUx6NDhMMmNcdTAwMkJQR2NnYVdROUlsTlFURWxPUlMweE15SWdaR0YwWVMxdVlXMWxQU0pUVUV4SlRrVWlQanh3WVhSb0lHTnNZWE56UFNKamJITXRNaUlnWkQwaVRURTNMamc1TERJeUxqZzBiRGt1T1N3eE1DNDROQ0l2UGp3dlp6NDhaeUJwWkQwaVUxQk1TVTVGTFRFMElpQmtZWFJoTFc1aGJXVTlJbE5RVEVsT1JTSVx1MDAyQlBIQmhkR2dnWTJ4aGMzTTlJbU5zY3kweUlpQmtQU0pOTWpjdU56a3NNek11TmpoV01UZ3VOVGtpTHo0OEwyY1x1MDAyQlBHY2dhV1E5SWxOUVRFbE9SUzB4TlNJZ1pHRjBZUzF1WVcxbFBTSlRVRXhKVGtVaVBqeHdZWFJvSUdOc1lYTnpQU0pqYkhNdE1pSWdaRDBpVFRFM0xqZzVMREl5TGpnMFZqY3VOelVpTHo0OEwyY1x1MDAyQlBHY2dhV1E5SWxOUVRFbE9SUzB4TmlJZ1pHRjBZUzF1WVcxbFBTSlRVRXhKVGtVaVBqeHdZWFJvSUdOc1lYTnpQU0pqYkhNdE1pSWdaRDBpVFRJM0xqYzVMRE16TGpZNGJERTNMVFl1TXpNaUx6NDhMMmNcdTAwMkJQQzl6ZG1jXHUwMDJCIg0KICAgICAgICB9DQogICAgICB9LA0KICAgICAgInRleHQiOiAiU1dsSmFVUlJjRTlVTVZKR1QyY3dTMFJSYjNSSlJrcHNXbTFXZVZwWE5XcGFVMEl3WW5sQ1UyRkhiSFZpTUU1MllsY3hkR0l5TkhWYVIzaHpTVWRzZWtsSFJtdGFSMVpyU1VkS05VbEhVbXhhYlVZeFlraFJUa05uTUV0TVUwSmFZak5WWjFreVJuVkpTRTUzV2xkT2NGcHVhMmRsVnpreFkybENlbGt6U25CalNGRm5ZMjFXZUdSWGJIbGFWekZzWW01U2VrbEhlSEJoTWxVMlJGRnZUa05wUVdkSlEwRnFTVWhKTmtsRWVIZFpWMDV5V1Zka2JFeFlUbmRhVjA1d1dtMXNiR05xTkdkWGVYZG5VRWhDYUZreWRHaGFNbFYwWXpOQ2JGa3liRzFoVjFaNVVHd3dUa05wUVdkSlEwRnFTVWhLYkdOWVZuQmpiVlowV2xjMU1HTjZiMmRRU0VKb1dUSjBhRm95VlhSak0wSnNXVEpzYldGWFZubFFhVUppVEVOQk9HTkhSbXBoTWtadVdsTXhlbU5IVm1waFYxcHdXbGhKSzFoUk1FdEVVVzluU1VOQloxSnRPWGxKUjFZMFdWY3hkMkpIVldka1IyaHdZM2xDYzJGWE5XeEpTR1J3WWtkM1oxbFlUbkpKU0ZKdldsTkNlV1JYTlRCaFZ6RnNTVWhTZGtsSGJIVmpNMUpvWWtkM1RrTnBRV2RKUTBJd1lVZFZaMkpIYkhwa1IxWnJTVWhDYUZreWRHaGFNbFo2U1VkS2JGcHRPWGxhVTBKNVpGYzFkV0ZYTlc1SlNGSnZXbE5DZWxrelNuQmpTRkUyUkZGdlRrTnBRV2RKUTBGcVNVaEtiR05ZVm5CamJWWjBXbGMxTUdONmIyZGpTR3d3WWpJeGMweERRbkphV0Vwb1kzY3dTMFJSYjJkSlEwRm5WMWM1TVVsSFRtaGlhVUp3WW01T01GbFhlSE5KU0U1M1dsZE9jRnB0YkdwSlNGcHNZMjVPY0dJeU5YcEpSemx0U1VkRloyTkhSbXBoTWtadVdsRXdTMGxEUVdkSlNGWjZZVmMxYmtsSVFuQmpRekZ6WVZkMGJFbElRbWhaTW5Sb1dqSlZaMk16UW14Wk1teHRZVmRXZVdONmIwNURaekJMU1VOQlowbERUV2RqYW05blkwaHNNR0l5TVhOUVZEQjNUR3BGZDB4cVNYTkpSM1JzWTIxR2VsQnFNSGxNYWxsMVRVRXdTMFJSYjNSSlJsWjZXbE5DYkdKdVdXZGFSMng1V2xkT01HRllXbXhKU0ZKMlNVZEdhMXBEUW1oaWFVSnNZbTVhY0dOdE9YVmlWMVoxWkVOQ2QxbFlVbTlKU0ZKMlNVaE9OV041TlhkWldGSnZTVWRHTVdSSE9YUlpXRkp3V1RKR2MySklhMDVEYVVGblNVTkJha2xIVm5Wa2FtOW5URE5DYUdSSFozWmtSemgyWlZjNU1XTnBPWHBoV0ZKc1RGaENhRmt5ZEdoYU1sWjZUSGN3UzBscFNXbEVVVzlxU1ZOQ2QyVllVbTlpTWpSNlJGRnZUa050YkhSalJ6bDVaRU5DZVdGSGJIVmlNMDVxWTIxc2QyUklUalZpYmxKb1pVTkNhR041UW5samR6QkxZVmN4ZDJJelNqQkpTRTVxWTIxc2QyUkhUblppYmxKc1pVaFJaMWxZVFdkak1rMU9RMjFzZEdOSE9YbGtRMEowV1ZoU2IwUlJiMDVEYld4MFkwYzVlV1JEUWxSbFdFNHdXbGN3VGtOdGJIUmpSemw1WkVOQ1ZHVllUakJhVnpCMVVUSTVjMkpIVm1wa1IyeDJZbTVOZFZJeVZuVmFXRXB3V1hjd1MyRlhNWGRpTTBvd1NVWktiMkZYTlhaRVVXOU9RMjFzZEdOSE9YbGtRMEpxWWpJeGQxbFlUVTVEWnpCTFJGRnZUa05uTUV0YVIxWnRTVWRhZVdJeU1XWmpiV2h3WW0wNVptTXlWbk5hVjA0d1MwZGtlV0l6Vm5kWU0xSTFZMGRXZWxCV2MybGpNMVozWTBjNWVXUkRTWE5KUTBwMlpFZG9iR05zT1c1amJUa3hZME5LWkV4RFFtNWpiVGt4WTBZNWFtSXllSFpqYmswNVYzbG5lVTVVVlhOSlJFRnpTVVJCY0V4RFFXOU5hbFV4VEVOQmQweERRWGxPVkZWd1dGTnJOa1JSYjJkSlEwRm5TVU5CWjBsRFNXbEphMDUyWW01T01HTnVWbXBrUTBKb1NVZE9kbUpZUW1oak1UbDBZakpTYkdKRFFtbGxVMEowV1ZjMU1WbFhlSE5sVTBKNldsZDRiRmt6VW5CaWJXTm5WVzFvY0dKdE9HZFpNamwxV1RKR01scFRRblpqWnpCTFNVTkJaMGxEUVdkSlEwSnFZakkxTWxwWVoyZGlWMVo2WVVkV2VreHBRa0prUTBKeldsZEdlbVJEUW5aaWJWVm5ZbGRXZW1GRFFtaGplVUpvU1VoT01XTklRblpqYmxGbldWYzFhMGxIT1hWYVUwSjBXbGhPYjBsSFJucEpSMFZPUTJsQlowbERRV2RKUTBGbldXMTRkbGt5YzJkak1taDJaRmQ0YTBsSFNteEpTRTVzWWtkV2FtUkhWbXRNYVVKVllVZFZaMkpYVm5waFIxWjZTVWRzZFVsR1NtOWhWelYyU1VoT2IySXpWbk5hUTBKcFdsTkNhbUpIT1hwYVYxRk9RMmxCWjBsRFFXZEpRMEZuV1ZjMWEwbElaSEJrUjJkblpESldjMXBIVm10SlNGcHNZMjVTY0ZreVZucE1hVUpLV21sQ2VtSXlNV3hKUjBwellqSk9jbU41UW1oamJWVm5XVEk1ZFZreVJqSmFVM2RuV2xkR2FtRkRRblphWnpCTFNVTkJaMGxEUVdkSlEwSXdZVWRXZEVsSVRtOWlNMVp6V2tOQ2FWcFRRbnBrVjBwcllWaGFjRnBIVm10SlIyeDFaRWM0WjFreU9YVmtiVlkwU1VjeGJHTXlhR3hqZVVKb1ltMVJaMkZ0T1hCaWJWWnJURU5DYldJelNuUmhWelZ1UkZGdlowbERRV2RKUTBGblNVZEZaMWt5T1hSalJ6a3hZbTFSZFVSUmIyZEpRMEZuU1VOQlowbEdTbXhrU0ZaNVltNU5Ua05wUVdkSlEwRm5TVU5CWjB4VE1IUk1VekIwVEZFd1MwbERRV2RKUTBGblNVTkJObGt5ZUdoak0wMDJXVWN4ZGxwSFZuTlpRVEJMU1VOQlowbERRV2RKUTBGblNVTkJaMVpIYUd4SlIwWjZZekpXZEZsdGVEVkpSMUpvWkVkR2VtUklTakZaTTFJeFkyMVZaMlF5YkRCaFEwSlVaRmhDZDJJelNqQmplWGRuVVcxNGRsa3lkSHBKUjBaMVdrTkNhbUl5TVhkaU0xWjFXa0V3UzBsRFFXZEpRMEZuU1VOQlowbERRV2RhTTBwMlpGaENla2xIVW14YWJXeDFXbGRSZFVSUmIyZEpRMEZuU1VOQlowbERTV2xKWnpCTFJGRnZaMGxEUVdkSlEwRm5TVWRhZVdJeU1HZFpNamwwWTBkR2VsZ3pTbTloVnpWMlRHMDVhV0Z0Vm1wa1NFMW5ZVmN4ZDJJelNqQkpTRTVzWWtkV2FtUkdPWFJhV0U1dldsaE5Ua05wUVdkSlEwRm5TVU5CWjFwdVNuWmlVMEpxWWpJeGQxbFlUbVpqYldod1ltMDRkV0l5U25GYVYwNHdZM2xDY0dKWVFuWmpibEZuV2pKV01GZ3lNV3hqTW1obVpHMVdlV1JIYkdwYVdFNW1XVmMxYTFneVdtaFpNbFo2UkZGdlowbERRV2RKUTBGblNVZGFlV0l5TUdkWk1qbDBZMGRHZWt4dFVtaGtSMFo2WkVoS01Wa3pVakZqYlZaNlNVZHNkR05IT1hsa1EwSk9XbGhPYjBSUmIyZEpRMEZuU1VOQlowbEhXbmxpTWpCbldUSTVkR05IUm5wWU0wcHZZVmMxZGt4dFRuWmlibHBzWTI1T2NHSXlOWHBKUjJ4MFkwYzVlV1JEUW5SYVdFNXZXRE5TZGxneVRuWmlXRUpvWTNjd1MwbERRV2RKUTBGblNVTkNiV050T1hSSlIwNTJZbGhDYUdNeE9YbGhSMngxWW5rMWFtSXlOVEphV0VwNllWYzVkV041UW5CaVdFSjJZMjVSWjJKWFZucGhSamt3WWpFNWVXRkhiSFZpZHpCTFJGRnZUa05wUVdkSlEwRm5TVU5CWjBsNVFUbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlJNRXRKUTBGblNVTkJaMGxEUVdwSlJWSnNZa2RXTUZwVFFtaGlSM2RuWkVkb2JFbElRbmxhV0Zwd1lqTldlbUpJYTJkWlYxSnJXbGRSWjJSSFZuUmpSemxvWTI1cloxb3lWblppVjFZd1kyNXJUa05wUVdkSlEwRm5TVU5CWjBsNVFUbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlJNRXRKUTBGblNVTkJaMGxEUW5CYWFVSjFZak5SYjBsRFNqQmFWekYzWWpOS2FHTnViR1phTWxaMllsZFdNR051YTJsSlIyeDFTVWhPYWt4dVRqQmhWMDV5WlZNMWNscFliSHBMUTJ0d1QyY3dTMGxEUVdkSlEwRm5TVU5CWjBsRFFXZGpNazExWXpOU2NGa3lkRFZYZVVvd1dsY3hkMkl6U21oamJteG1XakpXZG1KWFZqQmpibXRwV0ZReFlsaFJNRXRFVVc5blNVTkJaMGxEUVdkSlIyeHRTVWhPYWt4dVRqQmhWMDV5WlZaemFXUkhWblJqUnpsNVdWaEtOVmd5Wkd4aU1qRnNaRWhLTlVsc01EWkVVVzluU1VOQlowbERRV2RKUTBGblNVTkNlV041TlVWYVYzaHNaRWRXVUZsdGNHeFpNMUo2UzBoT2FreHVUakJoVjA1eVpWWnphV1JIVm5SalJ6bDVXVmhLTlZneVpHeGlNakZzWkVoS05VbHNNSEJFVVc5blNVTkJaMGxEUVdkSlEwRm5TVU5DZWxsNU5YcGtSMnhxWVROc1lrbHVVbXhpV0VKMlkyMUdlV1ZXT1c1YVZ6bDBXbGhTZVdWVFNtUkpSREJuVnpFd1RrTnBRV2RKUTBGblNVTkJaMGxEUVdkSlNFcDZUR3hLYkZwSVNtaGtlV2R3UkZGdlRrTnBRV2RKUTBGblNVTkJaMGw1UVRsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCUk1FdEpRMEZuU1VOQlowbERRV3BKUms1c1lrZFdhbVJEUW1oaVIzZG5WRmRXZW1GSFZucEpTRkp2V1ZoUloyUXliSE5pUTBKcFdsTkNNV015Vm10SlIyeDFTVWhTYjFwVFFucGlNbmd5V2xoSmRVUlJiMmRKUTBGblNVTkJaMGxEVFdkUlYxSnJTVWRHTUdSSVNuQlpibFl3V2xOQ01HSjVRakJoUjFWbllqSktjVnBYVGpCSlNGSjJTVWMxZG1SSFZXZGtSMmhvWkVOQ2NHUkRRbWxhVjNoMlltMWtla2xJVW5aSlNGSnZXbE5DZW1JeWVESmFXRWwxUkZGdlowbERRV2RKUTBGblNVTk5aMUJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3VGtOcFFXZEpRMEZuU1VOQloySlhWbnBoUmpsNlpGaENkMkl6U2pCak1UbHVaRmRzYTJONVFUbEpTRXA2VEd0a2JHUkZPV2xoYlZacVpFaE5iMGx1VG14aVIxWnFaRU5DYW1KSE9YcGFWMUZuWkcxR2MyRlhVV2RpVjFaNllVZFdla2xIV25aamFVSXdZVWRWWjJJeldteGpiVVp6WWtOQ2VtRlhNVEZpUjBZd1lWYzVkVWxwZDJkalNFcHNZekpXYzFwWFRqQlFWbEo1WkZkVmMwbElUbXhpUjFacVpFUXhWV051Vm14TVEwSnVZMjA1TVdORU1VZFpWM2g2V2xOM1oxcHRiSE5rUjFaNVVGaEtla3h0V25CaVNGSnNZMmsxZEZwWVRtOUxVVEJMU1VOQlowbERRV2RKUTBKMFdsaE9iMWd5WkRGaFYxSjZTVVF3WjJNeVZuTmFWMDR3V0RJeGJHTXlhR3hqZVdkd1JGRnZaMGxEUVdkSlEwRm5TVWhLZWt4c1ZuVmpNbFp6V2xkT01GRlhlSE5VTWtweFdsZE9NR041WjNCRVVXOW5TVU5CWjBsRFFXZEpSekZzWXpKb1ptTnRhSEJpYlRsbVlqSktjVnBYVGpCamVVRTVTVVowWkVSUmIwNURhVUZuU1VOQlowbERRV2RoVjFsbllsZFdlbUZHT1hwa1dFSjNZak5LTUdNeE9XNWtWMnhyWTNwdlowbDVRakZqTWxaNVNVZE9hR0pwUW1oWk1rNXdXa2RXZFdSSFJuTmlTR3RuV2tjNFoySnRPVEJKU0U1c1lrZFdhbVJEUW1oaWJtd3dZVWRzZFZwM01FdEpRMEZuU1VOQlowbERRV2RKUTBGbldtMDVlVWxIYTJkaFZ6Um5ZMjFHZFZveVZXOWlSMVoxUzBjeGJHTXlhR1phTTFad1draE5jRXRVYjA1RGFVRm5TVU5CWjBsRFFXZEpRMEZuU1VOQlowbERRbmxoUjJ4MVlqRTVkbGx0Y0d4Wk0xRm5VRk5DVTJGSGJIVmllVFZUWVVkc2RXSXdVblpaZVRWQ1dUTlNjR1J0VmtWaU1rMTFWREpLY1ZwWFRqQmplVFZIWVZjMWEwdEhNV3hqTW1obVdqTldjRnBJVG1KaFZqQndSRkZ2WjBsRFFXZEpRMEZuU1VOQlowbERRV2RKUTBGblkyNU5kVlF5U25GYVYwNHdVVEk1YzJJelNXOWlWMVo2WVVZNWJtUlhiR3RqTVhSd1dGTjNiMDFxUVhkTVJFbDNUVU4zZVUxRVFYQkxVVEJMU1VOQlowbERRV2RKUTBGblNVTkJaMGxEUVdkSlJ6RnNZekpvWm1OdGFIQmliVGxtWWpKS2NWcFhUakJqZVRWb1kwaENiR0p0VVc5amJXaHdZbTA1Wm1JeVNuRmFWMDR3UzFFd1MwbERRV2RKUTBGblNVTkJaMGxEUVdkSlEwRm5TVWhLYjJGWE5YWllNamxwWVcxV2FtUkROVUprU0ZKNVlWZEtNV1JIVm5wTWExSnNZa2RXTUZwVlJuTmlSbFo2V2xoS1ZHUklTbkJpYldSNlMwTnJUa05wUVdkSlEwRm5TVU5CWjBsRFFXZEpRMEZuU1VOQ2VXRkhiSFZpTVRsMldXMXdiRmt6VVhWUldGSXdZMjFzYVdSWVVteGplVFZVV2xoU1ZtTXlWbmxWTTFKNVlWYzFia3REU201amJUa3hZME5KYzBsRFNtbGlSemxxWVhsSmNFUlJiMDVEYVVGblNVTkJaMGxEUVdkSlEwRm5TVU5CWjBsRFFtcGFWelV3V2xoSloxQlRRbmxoUjJ4MVlqRTVkbGx0Y0d4Wk0xRjFVakpXZG1KWFZqQmpibXQxVWpKV01GRnRPVEZpYlZKd1ltMWtRMkl6WjI5VmJXaHdZbTA0ZFZJeVZuWmlWMVl3WTI1cmRWVkhlR2hpYlZWMVZqSTVlV0pIVWxsWFUydDFVVEpXZFdSSFZubEVVVzluU1VOQlowbERRV2RKUTBGblNVTkJaMGxEUVdka1IxWTBaRU5CT1VsSVRqQmphV2h3UzFFd1MwbERRV2RKUTBGblNVTkJaMGxEUVdkSlEwRm5TVWhTYkdWSVVtWmFSemt3U1VRd1oxVnRhSEJpYlRoMVVqSldkbUpYVmpCamJtdDFWa2RXTkdSRlVuWmtRMmd3V2xob01FeERRbXBhVnpVd1dsaEpjRVJSYjJkSlEwRm5TVU5CWjBsRFFXZEpRMEZuU1VOQloyTXlUWFZqTTFKd1dUSjBOVmQ1U2pCYVZ6RjNZak5LYUdOdWJHWmFNbFoyWWxkV01HTnVhMmxZVXpWb1kwaENiR0p0VVc5VmJXaHdZbTA0ZFZWdGFIQmliVGxGWWpKTmRWRlhUakJoV0Zwc1VrYzVha3hyT1dsaGJWWnFaRWhOZFZGWFVtdFdSMVkwWkVWU2RtUkRhREJhV0dnd1dESlNkbVJEYTNCRVVXOU9RMmN3UzBSUmIwNURhVUZuU1VOQlowbERRV2RhVjNoNldsUnZaMFJSYjJkSlEwRm5TVU5CWjBsRFFXZEpRMEZuU1VOQloyTnVUWFZVVjFaNll6SkdibHBWU25abFEyZHBWMWM1TVVsSFVuQmFRMEoxWWpOUloyTXlWbk5hVjA0d1dsZFJaMWxYTlRWSlJ6RnNZekpvYkdONVJXbExVVEJMU1VOQlowbERRV2RKUTBGblNVTkJaMGxEUVdkSlNFcHNaRWhXZVdKbk1FdEpRMEZuU1VOQlowbERRbmxqZVRWVFdsZFNlVmxZWTI5TFVUQkxTVU5CWjBsRFFXZEpRMEZxU1VRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVJGRnZaMGxEUVdkSlEwRm5TVU5OWjFSWFJubGhlVUo2V2xkNGJGa3pVbXhhUTBKMFdsaE9iMXBZVFdkWldFMW5Zek5XZDJOSE9YbGtTRTFPUTJsQlowbERRV2RKUTBGblNYbEJPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRkV3UzBSUmIyZEpRMEZuU1VOQlowbEhXblpqYVVKd1NVZHNkVWxJU21oaWJXUnNTMGQ0YkdKcGFHNWpiVGt4WTBZNU1HVllRbXhqZVd0d1QybEJha2xJVm5wYVdFbG5XVEpHZFVsSFJtcFpNbXhyV2xjMU1GbFhlSE5sVTBKcllubENkV0l6VVdkak1sWnpXbGRPTUVsSFJuVmxXRkp2WVZjMWJrUlJiMDVEYVVGblNVTkJaMGxEUVdkSlEwRm5TVWN4YkdNeWFHWmpNMVozWTBjNWVXUklUbVphTTFad1draE5aMUJUUW5samVUVklXbGhTVUZsdGNHeFpNMUo2UzBOS2VscFhlR3haTTFKbVNXbDBibU50T1RGalJqa3daVmhDYkdNeGRIQllVM2RuWTBoS2JHTXlWbk5hVjA0d1VGWlNlV1JYVlhOSlNFNXNZa2RXYW1SRU1WVmpibFpzVEVOQ2JtTnRPVEZqUkRGSFdWZDRlbHBUZDJkYWJXeHpaRWRXZVZCWVNucE1iVnB3WWtoU2JHTnBOWFJhV0U1dlMxRXdTMGxEUVdkSlEwRm5TVU5CWjBsRFFXZGpiazExVmxjMWVscFhlR3haTTFKQ1lrZDRVRmx0Y0d4Wk0xSjZTME5yVGtObk1FdEpRMEZuU1VOQlowbERRV2RKUTBGbllWZFpaMkpYVm5waFJqbDZaRmhDZDJJelNqQmpNVGx1WkZkc2EyTjZiMDVEYVVGblNVTkJaMGxEUVdkSlEwRm5TVU5CWjBsRFFtMWlNMGxuV2pOV2NGcERRbkJpYVVKMFdsaE9iMWd6VGpGalNFSjJZMjVTZWxneVpERmhWMUo2VDJjd1MwbERRV2RKUTBGblNVTkJaMGxEUVdkSlEwRm5TVU5CWjBsRFFubGhSMngxWWpFNWRsbHRjR3haTTFGblVGTkNVMkZIYkhWaWVUVlRZVWRzZFdJd1VuWlplVFZDV1ROU2NHUnRWa1ZpTWsxMVZESktjVnBYVGpCamVUVkhZVmMxYTB0SFpERmhWMUZ3UkZGdlowbERRV2RKUTBGblNVTkJaMGxEUVdkSlEwRm5TVU5CWjBsSVNtOWhWelYyV0RJNWFXRnRWbXBrUXpWQ1pFaFNlV0ZYU2pGa1IxWjZUR3hPYkdSR1ZucGFXRXBVWkVoS2NHSnRZMjlKYldSNVlqTldkMGxwZDJkYU0wcDJaRmhDWm1SSWJIZGFXRTVpWVZZd2NFUlJiMmRKUTBGblNVTkJaMGxEUVdkSlEwRm5TVU5CWjBsRFFXZEpTRXA2VEdzNWFXRnRWbXBrUlU1MllrYzVlVXRIWkRGaFYxRnpXak5LZG1SWVFtWlpNamx6WWpOS2VsY3liR1JMVVRCTFNVTkJaMGxEUVdkSlEwRm5TVU5CWjFwWGVIcGFWRzluUkZGdlowbERRV2RKUTBGblNVTkJaMGxEUVdkSlEwRm5ZMjVOZFZSWFZucGpNa1p1V2xWS2RtVkRaMmxWTWxaeldsZE9NRWxEU1dkTGVVSnVZMjA1TVdOR09UQmxXRUpzWXpGMGNGaFRRWEpKUTBsblNWTkpjRVJSYjJkSlEwRm5TVU5CWjBsRFFXZEpRMEZuU1VOQloyTnRWakJrV0VwMVJGRnZUa05wUVdkSlEwRm5TVU5CWjBsNVFUbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlJNRXRKUTBGblNVTkJaMGxEUVdwSlJVNTJZbTVhYkdOdVVXZGtXRTVzWTJsQ2VtUklTbkJpYldSNlNVZEdla2xJVW05YVUwRjZXa2RXYWtsSGJIVmpTRll3UkZGdlowbERRV2RKUTBGblNVTk5aMUJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3T1ZCVU1EbFFWREE1VUZRd09WQlVNRGxRVkRBNVVGUXdPVkJVTURsUVZEQTVVRlF3VGtOcFFXZEpRMEZuU1VOQloyRlhOWGRrV0ZKbVlsZFdlbUZIVm5wSlJEQm5WekV3VGtOcFFXZEpRMEZuU1VOQloyRllUbVpqTTFaM1kwYzVlV1JEUVRsSlJuUmtSRkZ2VGtOcFFXZEpRMEZuU1VOQloxcHRPWGxKUnpGc1l6Sm9abU50YUhCaWJUbG1ZakpLY1ZwWFRqQkpSMngxU1VjeGJHTXlhR1pqYldod1ltMDVabUl5U25GYVYwNHdZM3B2VGtOcFFXZEpRMEZuU1VOQlowbERRV2RKUjJ4MVkwaFdNRmd5TVd4ak1taHNZM2sxYUdOSVFteGliVkZ2WWxkV2VtRkdPVEJpTVRscVlqSXhkMWxZVFc5aVYxWjZZVVk1ZVdGSGJIVmlNVGwyV1cxd2JGa3pVWFZTTWxaMllsZFdNR051YTNCTFVUQkxTVU5CWjBsRFFXZEpRMEZuU1VOQloyRllUbVpqTTFaM1kwYzVlV1JETldoalNFSnNZbTFSYjJKWFZucGhSamw1WVVkc2RXSXhPWFpaYlhCc1dUTlJkVkZZVWpCamJXeHBaRmhTYkdONU5VaGFXRkpXWXpKV2VWVXpVbmxoVnpWdVMwTktibU50T1RGalEwbHdTVVF3T1VsRFNucGtXRUozWWpOS01FbHBhMDVEWnpCTFNVTkJaMGxEUVdkSlEwSjNZMjFzZFdSRGFIQmlia0l4WkVZNWRGcFlUbTlhV0Uxd1JGRnZaMGxEUVdkSlEwRm5TVWhDZVdGWE5UQkxSMng2V0ROT01XTklRblpqYmxGd1JGRnZUa05uTUV0RVVYQnJXbGRaWjFwdVNuWmlWamwyV1cxdmIwdFViMDVEYVVGblNVTkNkMWxZVG5wRVVXOU9RMjFTYkZwcFFtMWpiVGwwV0RKNGNGbHVTbWhqYm10dlMxUnZUa05wUVdkSlEwSjNXVmhPZWtSUmIwNURiVnA1WWpJeFptTnRhSEJpYlRsbVl6SldjMXBYVGpCTFEyczkiDQogICAgfQ0KICBdDQp9";
+
+ static bool s_initialized = false;
+ static IProjectServer s_projectServer = default;
+ static IProject s_project = default;
+
+ public static void Initialize()
+ {
+ if (s_initialized)
+ return;
+
+ Rhino.PlugIns.PlugIn.LoadPlugIn(s_rhinocode);
+
+ // get platforms registry into a dynamic type to avoid using
+ // the actual registry type. Otherwise when underlying api changes
+ // it will throw an exception.
+ dynamic projectRegistry = RhinoCode.Platforms;
+ // get project server
+ s_projectServer = projectRegistry.QueryLatest(s_rhino)?.ProjectServer;
+ if (s_projectServer is null)
+ {
+ RhinoApp.WriteLine($"Error loading plugin. Missing \"{s_rhino}\" platform");
+ return;
+ }
+
+ // get project
+ var dctx = new InvokeContext
+ {
+ Inputs =
+ {
+ ["projectAssembly"] = typeof(ProjectPlugin).Assembly,
+ ["projectId"] = s_projectId,
+ ["projectData"] = s_projectData,
+ }
+ };
+
+ if (s_projectServer.TryInvoke("plugins/v1/deserialize", dctx)
+ && dctx.Outputs.TryGet("project", out IProject project))
+ {
+ // server reports errors
+ s_project = project;
+ }
+
+ s_initialized = true;
+ }
+
+ public static ProjectPlugin Instance { get; private set; }
+
+ public static Rhino.Commands.Result RunCode(Command command, Guid id, RhinoDoc doc, RunMode mode)
+ {
+ if (s_project is null)
+ {
+ RhinoApp.WriteLine($"Error running command {id}. Project deserializiation failed.");
+ return Rhino.Commands.Result.Failure;
+ }
+
+ var rctx = new InvokeContext
+ {
+ Inputs =
+ {
+ ["command"] = command,
+ ["project"] = s_project,
+ ["projectId"] = id,
+ ["doc"] = doc,
+ ["runMode"] = mode,
+ }
+ };
+
+ if (s_projectServer.TryInvoke("plugins/v1/run", rctx))
+ return Rhino.Commands.Result.Success;
+
+ // server reports error
+ else
+ return Rhino.Commands.Result.Failure;
+ }
+
+ public ProjectPlugin() { Instance = this; }
+ }
+}
diff --git a/src/compas_3dec/rhino/build/rh8/src/compas_3dec/compas_3dec.csproj b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/compas_3dec.csproj
new file mode 100644
index 0000000..68dde28
--- /dev/null
+++ b/src/compas_3dec/rhino/build/rh8/src/compas_3dec/compas_3dec.csproj
@@ -0,0 +1,14 @@
+
+
+ net48
+ False
+ compas_3dec
+ 0.1.29929.8914
+
+
+
+
+
+
+
+
diff --git a/src/compas_3dec/rhino/commands/select_blocks.py b/src/compas_3dec/rhino/commands/select_blocks.py
new file mode 100644
index 0000000..9e22b60
--- /dev/null
+++ b/src/compas_3dec/rhino/commands/select_blocks.py
@@ -0,0 +1,142 @@
+"""
+NOTE:
+
+- Reference to RhinoCommmon.dll is added by default
+
+- You can specify your script requirements like:
+
+ # r: [, ]
+ # requirements: [, ]
+
+ For example this line will ask the runtime to install
+ the listed packages before running the script:
+
+ # requirements: pytoml, keras
+
+ You can install specific versions of a package
+ using pip-like package specifiers:
+
+ # r: pytoml==0.10.2, keras>=2.6.0
+
+- Use env directive to add an environment path to sys.path automatically
+ # env: /path/to/your/site-packages/
+"""
+#! python3
+
+import rhinoscriptsyntax as rs
+import scriptcontext as sc
+import math
+
+import System
+import System.Collections.Generic
+import Rhino
+
+import compas
+
+
+def from_rhino_select(group_types=["support", "other_group"], group_colors=[(255, 0, 0), (255, 0, 255)]):
+ """Construct a compas_model by manually selecting Rhino concave or
+ convex meshes. At least one mesh as a support and one mesh as a
+ block should be selected. The meshes in Rhino should be closed
+ and with welded vertices. If some blocks are concave, each of
+ them should be subdivided into convex meshes and joined, forming
+ a compound.
+ Returns
+ -------
+ :class:`model`
+ The assembly datastructure with Supports, Blocks and compound
+ groups defined.
+ """
+
+ from compas_rhino.objects import select_meshes
+ from compas_rhino.objects import get_mesh_vertices_and_faces
+ from compas.datastructures import Mesh
+ from compas_rhino.conversions import mesh_to_compas
+ from compas_rhino.conversions import mesh_to_rhino
+
+ # ==============================================================
+ # Delete all the previously added tempoary geometry
+ # ==============================================================
+ if not ("temporary_geometry" in sc.sticky.keys()):
+ sc.sticky["temporary_geometry"] = []
+
+ if sc.sticky["temporary_geometry"]:
+ rs.DeleteObjects(sc.sticky["temporary_geometry"])
+ sc.sticky["temporary_geometry"] = []
+ rs.Redraw()
+
+ # ==============================================================
+ # Select all Meshes that will be used in the solver.
+ # Add attribute to the object to note that it belongs to the solver.
+ # ==============================================================
+ mesh_supports_guids = rs.GetObjects(
+ "select closed valid meshes for the overall simulation",
+ preselect=True,
+ select=True,
+ group=False,
+ filter=rs.filter.mesh,
+ )
+ mesh_guids = select_meshes()
+ rs.UnselectAllObjects()
+ mesh_rhino_objects = []
+
+ if mesh_supports_guids: # user can accidentally do not select anything
+ for i in range(len(mesh_guids)):
+ rhino_object = Rhino.RhinoDoc.ActiveDoc.Objects.Find(mesh_guids[i])
+ rs.ObjectColor(mesh_guids[i], (200, 200, 200))
+ mesh_rhino_objects.append(rhino_object)
+ rhino_object.Attributes.DeleteAllUserStrings()
+ rhino_object.Attributes.SetUserString("group", "block")
+
+ center = rhino_object.Geometry.GetBoundingBox(Rhino.Geometry.Plane.WorldXY).Center
+ text = str(i)
+ text_dot = Rhino.Geometry.TextDot(text, center)
+ sc.sticky["temporary_geometry"].append(Rhino.RhinoDoc.ActiveDoc.Objects.AddTextDot(text_dot))
+
+ else:
+ rs.MessageBox("You did not selected any meshes!")
+ return
+ rs.Redraw()
+ # ==============================================================
+ # Mark selected meshes as supports
+ # ==============================================================
+
+ for i in range(len(group_types)): # user can accidentally do not select anything
+
+ mesh_supports_guids = rs.GetObjects(
+ "select_" + group_types[i], preselect=True, select=True, group=False, filter=rs.filter.mesh
+ )
+ rs.UnselectAllObjects()
+
+ if mesh_supports_guids:
+ for guid in mesh_supports_guids:
+ rhino_object = Rhino.RhinoDoc.ActiveDoc.Objects.Find(guid)
+ rhino_object.Attributes.SetUserString("group", group_types[i])
+ rs.ObjectColor(guid, group_colors[i])
+ else:
+ rs.MessageBox("Select " + group_types[i] + " !")
+ return
+
+ # ==============================================================
+ # Convert user strings as the 3dec input
+ # ==============================================================
+ input_meshes = []
+ is_support = []
+
+ for mesh_rhino_object in mesh_rhino_objects:
+ input_meshes.append(mesh_to_compas(mesh_rhino_object.Geometry))
+ is_support.append(mesh_rhino_object.Attributes.GetUserString("group") == "support")
+
+ print(input_meshes)
+ print(is_support)
+
+
+def from_obj():
+ pass
+
+
+def from_library():
+ pass
+
+
+from_rhino_select()
diff --git a/src/compas_3dec/rhino/compas_3dec.rhproj b/src/compas_3dec/rhino/compas_3dec.rhproj
new file mode 100644
index 0000000..d45e26a
--- /dev/null
+++ b/src/compas_3dec/rhino/compas_3dec.rhproj
@@ -0,0 +1,43 @@
+{
+ "id": "574526bf-a929-42e4-9a79-29d79faaff6b",
+ "identity": {
+ "name": "compas_3dec",
+ "version": "0.1",
+ "publisher": {
+ "email": "dellendice@arch.ethz.ch ",
+ "name": "Alessandro Dell\u0027Endice",
+ "company": "BRG, ETHZ"
+ }
+ },
+ "settings": {
+ "buildPath": "build/",
+ "buildTarget": {
+ "appName": "Rhino3D",
+ "appVersion": {
+ "major": 8
+ },
+ "title": "Rhino3D (8.*)",
+ "slug": "rh8"
+ },
+ "publishTarget": {
+ "title": "McNeel Yak Server"
+ }
+ },
+ "codes": [
+ {
+ "id": "de172756-286d-43d5-8c1f-5143e7bdff62",
+ "language": {
+ "id": "*.*.python",
+ "version": "3.*.*"
+ },
+ "title": "select_blocks",
+ "uri": "commands/select_blocks.py",
+ "image": {
+ "light": {
+ "type": "svg",
+ "data": "PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI\u002BPGRlZnM\u002BPHN0eWxlPi5jbHMtMXtmaWxsOiM1YzhkNmI7fS5jbHMtMSwuY2xzLTJ7c3Ryb2tlOiNmZmY7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO30uY2xzLTJ7ZmlsbDojZjRmNGY1O308L3N0eWxlPjwvZGVmcz48ZyBpZD0iUE9MWUxJTkUiPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI0LjQ2IDM1LjY4IDE0LjM2IDQ2LjUyIDMxLjMyIDQwLjE5IDMxLjMyIDMyLjM3IDQ0Ljc1IDI3LjM1IDQ0Ljc1IDEyLjI1IDM0Ljg0IDEuNDEgMTcuODkgNy43NSAxNy44OSAxNS41NyA0LjQ2IDIwLjU5IDQuNDYgMzUuNjgiLz48L2c\u002BPGcgaWQ9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNC40NiwzNS42OGw5LjksMTAuODQiLz48L2c\u002BPGcgaWQ9IlNQTElORS0yIiBkYXRhLW5hbWU9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNC40NiwyMC41OWw5LjksMTAuODQiLz48L2c\u002BPGcgaWQ9IlNQTElORS0zIiBkYXRhLW5hbWU9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNC40NiwzNS42OFYyMC41OSIvPjwvZz48ZyBpZD0iU1BMSU5FLTQiIGRhdGEtbmFtZT0iU1BMSU5FIj48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNC4zNiw0Ni41MmwxNy02LjMzIi8\u002BPC9nPjxnIGlkPSJTUExJTkUtNSIgZGF0YS1uYW1lPSJTUExJTkUiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxLjMyLDQwLjE5VjMyLjM3Ii8\u002BPC9nPjxnIGlkPSJTUExJTkUtNiIgZGF0YS1uYW1lPSJTUExJTkUiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE0LjM2LDQ2LjUyVjMxLjQzIi8\u002BPC9nPjxnIGlkPSJTUExJTkUtNyIgZGF0YS1uYW1lPSJTUExJTkUiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE0LjM2LDMxLjQzbDguNDgtMy4xNyIvPjwvZz48ZyBpZD0iU1BMSU5FLTgiIGRhdGEtbmFtZT0iU1BMSU5FIj48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNy44OSw3Ljc1bDkuOSwxMC44NCIvPjwvZz48ZyBpZD0iU1BMSU5FLTkiIGRhdGEtbmFtZT0iU1BMSU5FIj48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik00NC43NSwyNy4zNVYxMi4yNSIvPjwvZz48ZyBpZD0iU1BMSU5FLTEwIiBkYXRhLW5hbWU9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzQuODQsMS40MWwtMTcsNi4zNCIvPjwvZz48ZyBpZD0iU1BMSU5FLTExIiBkYXRhLW5hbWU9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjcuNzksMTguNTlsMTctNi4zNCIvPjwvZz48ZyBpZD0iU1BMSU5FLTEyIiBkYXRhLW5hbWU9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTcuODksMTUuNTdsLTEzLjQzLDUiLz48L2c\u002BPGcgaWQ9IlNQTElORS0xMyIgZGF0YS1uYW1lPSJTUExJTkUiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE3Ljg5LDIyLjg0bDkuOSwxMC44NCIvPjwvZz48ZyBpZD0iU1BMSU5FLTE0IiBkYXRhLW5hbWU9IlNQTElORSI\u002BPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjcuNzksMzMuNjhWMTguNTkiLz48L2c\u002BPGcgaWQ9IlNQTElORS0xNSIgZGF0YS1uYW1lPSJTUExJTkUiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE3Ljg5LDIyLjg0VjcuNzUiLz48L2c\u002BPGcgaWQ9IlNQTElORS0xNiIgZGF0YS1uYW1lPSJTUExJTkUiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTI3Ljc5LDMzLjY4bDE3LTYuMzMiLz48L2c\u002BPC9zdmc\u002B"
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/compas_3dec/rhino/icons.ai b/src/compas_3dec/rhino/icons.ai
new file mode 100644
index 0000000..0dd0fc7
--- /dev/null
+++ b/src/compas_3dec/rhino/icons.ai
@@ -0,0 +1,5772 @@
+%PDF-1.6
%âãÏÓ
+1 0 obj
<>/OCGs[31 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<>stream
+
+
+
+
+ application/pdf
+
+
+ icons
+
+
+ 2024-05-27T15:29:33+02:00
+ 2024-05-27T15:29:33+02:00
+ 2024-05-27T15:29:33+02:00
+ Adobe Illustrator 25.4 (Windows)
+
+
+
+ 256
+ 256
+ JPEG
+ /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7
FXYq7FUFqmt6RpUXq6jeRWqkVX1GAZv9VftN9AwE0xlIDmwnVvzq8u2xKafbzX7jo5/cxn6WBf8A
4TIHIGiWqiOW7EtR/OjzTcEi0it7JOxVDI/0lyV/4XInIWiWql0Y9d+ffOV2SZdXuVr1ET+iPui4
ZHiLUc0z1SmbUtRnNZ7qaU+LyM3t3OC2BkT1Q+BDsVREOpajAawXU0R8UkZfbscNpEiOqbWnn3zl
aEGLV7lqdBK/rD7peeHiLMZpjqyHTvzo8025Au4re9TuWQxv9BQhf+FyQyFtjqpdWW6T+dXl25IT
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
KuxV2KuxV2KuxV//2Q==
+
+
+
+ uuid:14dadf7d-72b1-46ca-8559-66a70ad7221b
+ xmp.did:7b8b3b57-ac02-dc49-8699-5c13d4c6a2e7
+ uuid:5D20892493BFDB11914A8590D31508C8
+ proof:pdf
+
+ xmp.iid:e73019d6-e1ce-3c4b-8317-7f42a6e24395
+ xmp.did:e73019d6-e1ce-3c4b-8317-7f42a6e24395
+ uuid:5D20892493BFDB11914A8590D31508C8
+ default
+
+
+
+
+ saved
+ xmp.iid:e73019d6-e1ce-3c4b-8317-7f42a6e24395
+ 2024-05-27T15:23:43+02:00
+ Adobe Illustrator 25.4 (Windows)
+ /
+
+
+ saved
+ xmp.iid:7b8b3b57-ac02-dc49-8699-5c13d4c6a2e7
+ 2024-05-27T15:28:59+02:00
+ Adobe Illustrator 25.4 (Windows)
+ /
+
+
+
+ Print
+ AIRobin
+ Document
+ False
+ False
+ 1
+
+ 48.000000
+ 48.000000
+ Pixels
+
+
+
+ Cyan
+ Magenta
+ Yellow
+ Black
+
+
+
+
+
+ Default Swatch Group
+ 0
+
+
+
+ White
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 0.000000
+
+
+ Black
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 100.000000
+
+
+ CMYK Red
+ CMYK
+ PROCESS
+ 0.000000
+ 100.000000
+ 100.000000
+ 0.000000
+
+
+ CMYK Yellow
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 100.000000
+ 0.000000
+
+
+ CMYK Green
+ CMYK
+ PROCESS
+ 100.000000
+ 0.000000
+ 100.000000
+ 0.000000
+
+
+ CMYK Cyan
+ CMYK
+ PROCESS
+ 100.000000
+ 0.000000
+ 0.000000
+ 0.000000
+
+
+ CMYK Blue
+ CMYK
+ PROCESS
+ 100.000000
+ 100.000000
+ 0.000000
+ 0.000000
+
+
+ CMYK Magenta
+ CMYK
+ PROCESS
+ 0.000000
+ 100.000000
+ 0.000000
+ 0.000000
+
+
+ C=15 M=100 Y=90 K=10
+ CMYK
+ PROCESS
+ 15.000000
+ 100.000000
+ 90.000000
+ 10.000000
+
+
+ C=0 M=90 Y=85 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 90.000000
+ 85.000000
+ 0.000000
+
+
+ C=0 M=80 Y=95 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 80.000000
+ 95.000000
+ 0.000000
+
+
+ C=0 M=50 Y=100 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 50.000000
+ 100.000000
+ 0.000000
+
+
+ C=0 M=35 Y=85 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 35.000000
+ 85.000000
+ 0.000000
+
+
+ C=5 M=0 Y=90 K=0
+ CMYK
+ PROCESS
+ 5.000000
+ 0.000000
+ 90.000000
+ 0.000000
+
+
+ C=20 M=0 Y=100 K=0
+ CMYK
+ PROCESS
+ 20.000000
+ 0.000000
+ 100.000000
+ 0.000000
+
+
+ C=50 M=0 Y=100 K=0
+ CMYK
+ PROCESS
+ 50.000000
+ 0.000000
+ 100.000000
+ 0.000000
+
+
+ C=75 M=0 Y=100 K=0
+ CMYK
+ PROCESS
+ 75.000000
+ 0.000000
+ 100.000000
+ 0.000000
+
+
+ C=85 M=10 Y=100 K=10
+ CMYK
+ PROCESS
+ 85.000000
+ 10.000000
+ 100.000000
+ 10.000000
+
+
+ C=90 M=30 Y=95 K=30
+ CMYK
+ PROCESS
+ 90.000000
+ 30.000000
+ 95.000000
+ 30.000000
+
+
+ C=75 M=0 Y=75 K=0
+ CMYK
+ PROCESS
+ 75.000000
+ 0.000000
+ 75.000000
+ 0.000000
+
+
+ C=80 M=10 Y=45 K=0
+ CMYK
+ PROCESS
+ 80.000000
+ 10.000000
+ 45.000000
+ 0.000000
+
+
+ C=70 M=15 Y=0 K=0
+ CMYK
+ PROCESS
+ 70.000000
+ 15.000000
+ 0.000000
+ 0.000000
+
+
+ C=85 M=50 Y=0 K=0
+ CMYK
+ PROCESS
+ 85.000000
+ 50.000000
+ 0.000000
+ 0.000000
+
+
+ C=100 M=95 Y=5 K=0
+ CMYK
+ PROCESS
+ 100.000000
+ 95.000000
+ 5.000000
+ 0.000000
+
+
+ C=100 M=100 Y=25 K=25
+ CMYK
+ PROCESS
+ 100.000000
+ 100.000000
+ 25.000000
+ 25.000000
+
+
+ C=75 M=100 Y=0 K=0
+ CMYK
+ PROCESS
+ 75.000000
+ 100.000000
+ 0.000000
+ 0.000000
+
+
+ C=50 M=100 Y=0 K=0
+ CMYK
+ PROCESS
+ 50.000000
+ 100.000000
+ 0.000000
+ 0.000000
+
+
+ C=35 M=100 Y=35 K=10
+ CMYK
+ PROCESS
+ 35.000000
+ 100.000000
+ 35.000000
+ 10.000000
+
+
+ C=10 M=100 Y=50 K=0
+ CMYK
+ PROCESS
+ 10.000000
+ 100.000000
+ 50.000000
+ 0.000000
+
+
+ C=0 M=95 Y=20 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 95.000000
+ 20.000000
+ 0.000000
+
+
+ C=25 M=25 Y=40 K=0
+ CMYK
+ PROCESS
+ 25.000000
+ 25.000000
+ 40.000000
+ 0.000000
+
+
+ C=40 M=45 Y=50 K=5
+ CMYK
+ PROCESS
+ 40.000000
+ 45.000000
+ 50.000000
+ 5.000000
+
+
+ C=50 M=50 Y=60 K=25
+ CMYK
+ PROCESS
+ 50.000000
+ 50.000000
+ 60.000000
+ 25.000000
+
+
+ C=55 M=60 Y=65 K=40
+ CMYK
+ PROCESS
+ 55.000000
+ 60.000000
+ 65.000000
+ 40.000000
+
+
+ C=25 M=40 Y=65 K=0
+ CMYK
+ PROCESS
+ 25.000000
+ 40.000000
+ 65.000000
+ 0.000000
+
+
+ C=30 M=50 Y=75 K=10
+ CMYK
+ PROCESS
+ 30.000000
+ 50.000000
+ 75.000000
+ 10.000000
+
+
+ C=35 M=60 Y=80 K=25
+ CMYK
+ PROCESS
+ 35.000000
+ 60.000000
+ 80.000000
+ 25.000000
+
+
+ C=40 M=65 Y=90 K=35
+ CMYK
+ PROCESS
+ 40.000000
+ 65.000000
+ 90.000000
+ 35.000000
+
+
+ C=40 M=70 Y=100 K=50
+ CMYK
+ PROCESS
+ 40.000000
+ 70.000000
+ 100.000000
+ 50.000000
+
+
+ C=50 M=70 Y=80 K=70
+ CMYK
+ PROCESS
+ 50.000000
+ 70.000000
+ 80.000000
+ 70.000000
+
+
+
+
+
+ Grays
+ 1
+
+
+
+ C=0 M=0 Y=0 K=100
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 100.000000
+
+
+ C=0 M=0 Y=0 K=90
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 89.999400
+
+
+ C=0 M=0 Y=0 K=80
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 79.998800
+
+
+ C=0 M=0 Y=0 K=70
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 69.999700
+
+
+ C=0 M=0 Y=0 K=60
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 59.999100
+
+
+ C=0 M=0 Y=0 K=50
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 50.000000
+
+
+ C=0 M=0 Y=0 K=40
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 39.999400
+
+
+ C=0 M=0 Y=0 K=30
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 29.998800
+
+
+ C=0 M=0 Y=0 K=20
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 19.999700
+
+
+ C=0 M=0 Y=0 K=10
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 9.999100
+
+
+ C=0 M=0 Y=0 K=5
+ CMYK
+ PROCESS
+ 0.000000
+ 0.000000
+ 0.000000
+ 4.998800
+
+
+
+
+
+ Brights
+ 1
+
+
+
+ C=0 M=100 Y=100 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 100.000000
+ 100.000000
+ 0.000000
+
+
+ C=0 M=75 Y=100 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 75.000000
+ 100.000000
+ 0.000000
+
+
+ C=0 M=10 Y=95 K=0
+ CMYK
+ PROCESS
+ 0.000000
+ 10.000000
+ 95.000000
+ 0.000000
+
+
+ C=85 M=10 Y=100 K=0
+ CMYK
+ PROCESS
+ 85.000000
+ 10.000000
+ 100.000000
+ 0.000000
+
+
+ C=100 M=90 Y=0 K=0
+ CMYK
+ PROCESS
+ 100.000000
+ 90.000000
+ 0.000000
+ 0.000000
+
+
+ C=60 M=90 Y=0 K=0
+ CMYK
+ PROCESS
+ 60.000000
+ 90.000000
+ 0.003100
+ 0.003100
+
+
+
+
+
+
+ Adobe PDF library 16.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+endstream
endobj
3 0 obj
<>
endobj
5 0 obj
<>/Resources<>/ExtGState<>/Properties<>>>/Thumb 36 0 R/TrimBox[0.0 0.0 48.0 48.0]/Type/Page>>
endobj
33 0 obj
<>stream
+H‰|WÍÎ%9
ÝOq_à«IìÄI¶+„ ¬@‚y‰ó“;ÝÃôÐ?÷^W¹R‰}||üß~|ýðÇÛëw¿ÿñõñïö›ÿ>ñÿ?ûøëë_?üø—öúòÓ«=µ>cégà³ùzýô>€Ï?~úø÷«¿þö×XOàÏ+ò9Ñêõ埼ÁÏÏÏêßíÉ6_Ÿði½~õýåã3ç³ð¾ŸïŒzZÆ×®Ýè{¿oÝGÿgͯKß—ßíý\ÓbÿøóŽ—‡ìçýéÃ~=dÖ3ûªã‰}âë!?ñ O†¯ò×\þêú:¾Å} –'iFâ«?qŠÖÀ]XrœkÑ3žSƒö^¸<ž‡.ÙšXrÐ;èZÏÚ~Ÿû| ž‘[Aï«Ó£û& #mÕ(ZwÝzŠ7kq$ºXŒÆÐcãéñÞ@Ã/¤ç5x~ÒNÕc9ýYØéÀiB1hØÔ@`–" O„íÐõ`Ãfòd_–\~ÇO® ̽_8±óSgÂ/ic¤ö8és¦Â~jÁ`´F>´R¥ð¦ÞÑgv`0²î3‚I
î#ˆƒßùl,Ä,$Ð`ÜmÜêªd⥽—ðÕRˆ_ׯŤiwÕ}ä7…òµP8.Š
+ë¹¾)zdE†´påÓXW=õ'¯§`Ë
+ܰ¶„•> »gSG0è„s6>¸±=€dòDiÚÏ>¿gЋ(GGÔÒ™BHt–ù†dN¢ŽpN"«ú$ï•hdë÷ðzx‡flqîƒ{@ b= jŽ0K‡6i0®¥‡ÎTŒVli”ʾpô
o²”Æv#>¦ O”ä$>[‚€.(lÆ-¦Ó…A8Ñ`I-V< [¥0Ÿ!éGÁ½18¨Ð Ô±Ùíâ%å“O•Ð0Ìg+ä×ÒÀ™)t˜äòû é›ôwÿ¶3^\¼7-³ˆ…¶p³u}èmcÒŠF(.Þfu*!$ùˆ9„¤d(æšÊOÞø!öÉ\¡¾¥QŒì™©;,õÖˆ½œ© ërxÁ¡
„r=k"m{q¹ô
†2°ýÌæÍC @Âö’ãSŽøÒeñ‚#» ø
+Ûüò|Wš0D¤i¤„š!C”è'@1Ì}ˆ‹Æå—^Šg‚§jЇ¸aîà UZkŒ"â߬%¹¸ãóD©Ý“t`¨!ÖÐCÝ©Ÿ@5i°3g“»_8:‹z,m©ÎvÖB€<©‚_2úâS©ÖNާ؆Ì×”q7®jÜDïb´Õ¦1·Ô/É3›R4Ѿ‡¹Á±dAÁ*‚o1Â.“-Kí—ðÑEÝeb
µ<àp)a¥+B.o'ԷỞ”nA&Ã3·šhLÅxs·€†CY¸YÛÝf×ÿv»^nÀç¬ë½Sïoi
µ0ÒºŸ&Žéžý66cRk£²§J€€âØza¶öîí¹¤)NsÌ%ØD!Ì9†^9ÍÁ7âÍ3´…2Å!Ë’§Ïå€<„ß2 ÉùHˆð-Lÿ¼Q–ãd#uLrìƒ!‡{»¸-Lxk ì¦ÁŠÝÎ
ŒL„t×ÏÍl'qXH‹£µñMke1x™‰Ëªgöv_é²™#:ŠpæË„"«&†å›N»žÇìß盂C¹M§
+¢wáë” sT=Œdím¹!6
±ý°
Ãâd‚*qˆ?‰?ÊØCwÙKh…-6‘R=Ë÷t5•…½ÑrÇékÍÝhös¡Ie@ú22› Çv/k ˜óäÛyK9Žeœ3Â4C\£is5W;z˜ásKÛPy’lÈø0î®Æb[S8Ú-Ò(oE:(]¡®ÿy+ˆyR^)U>½ù&ùvU¯ö|ë ‰üú>ß Ñì®™Üøü%ߘªÆª–ôn1šŠÔéõÎÚ.Fkø(-]WÚóÞW¥±Îˆ/»ñæíÒá¢oÇR÷©tøŽ.Ÿ°×±ÊIƒáGWÜêT{«õ¼1ÆL–G\¬åÈçæ–;“]…›Ò\BWkãe,«{ˆ{ƒ:ÔI‚FÈ®ÛDÊÂYê3úiŒ[¬œË0wfZ]è®<’%
+a8ÅR}ÄEóðTî½ÎÂ)cNÆZoÖO\Y ÊpL+¾Ï Šxlô°¯™§€½J›rË ¿£\U€á¨×ØnðW·ðÒV—w“.Ø‹„¬N/R=Û;-bìÇl“É7o TNû¢P>j«Š%„ŠÆäôÔ¦GÌ©)íוùÅ S1?\KÎ3¥~þ©A¤\€S¢T¸S3ai_v³¼šJG, Äê±`TÜ
J¯¶ô®†½-ìEÇ?(NÜéÝÓΡÙAµàqò¨g"–ÔòœàT}0R¥˜W†»ËQ"äœKK3L…—ì7êe
+íÉ:'ßýZoÄ•CoN¸ô˜ç°K ’<ÓN}»KèÁÈé"¶g\.=ËòEåÕÍaœ®£Û5¹…MJV>¬ Ž#y}Ù¯æ¾ÍLÊN¿?ÖpƸt
+%ãX¿);KKÏ]°
ݤ¾zsÀ
+1D›ÆúQÊO¿¨è¥UûTš\òlKI•RŒ³D#Ó9lk°´Iލ¾Ð8)P¤kúœi2¸SŒ¹½ß¬_mó=Õüâ‡HD=MΟñ‹Ä³½3N¡F¸VB/±.4æûÒš×¾3×°¥äÖºÖMýb¬—šGHpñGèNæLÛõy+âò¾›ôR–Y%5x–û†£1¢ÙX—¬Ú”¯R”–(Ë=ð´eÅ<½î²Ì47«Íð;Û{ÅëF¤ëí¨Øy~C¸õ…¸C¤¶Êó«¡RiuÃu¡ÝnÙÝ&¿í“n;¸!œ,¾
!cG‚1u ï–’®Ã*c‰mÉ@¹