visualdynamics 0.1.0a45__tar.gz → 0.1.0a46__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. {visualdynamics-0.1.0a45/src/visualdynamics.egg-info → visualdynamics-0.1.0a46}/PKG-INFO +1 -1
  2. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/__init__.py +1 -1
  3. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/geometry.py +387 -0
  4. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/mesh.py +57 -100
  5. visualdynamics-0.1.0a46/src/visualdynamics/core/solids.py +1487 -0
  6. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/__init__.py +1 -1
  7. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/barc.py +69 -65
  8. visualdynamics-0.1.0a46/src/visualdynamics/demo/frame_wing.py +378 -0
  9. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/icons.py +147 -0
  10. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/main_window.py +1046 -22
  11. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/merge_panel.py +32 -5
  12. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/mesh_panel.py +97 -18
  13. visualdynamics-0.1.0a46/src/visualdynamics/gui/mesh_solids_panel.py +97 -0
  14. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/panes.py +8 -0
  15. visualdynamics-0.1.0a46/src/visualdynamics/gui/pattern_panel.py +114 -0
  16. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/native.py +15 -0
  17. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/step.py +5 -63
  18. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/project.py +388 -6
  19. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/report/__init__.py +50 -7
  20. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/report/page.py +4 -0
  21. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/theme.py +2 -0
  22. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/geometry.py +3 -38
  23. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46/src/visualdynamics.egg-info}/PKG-INFO +1 -1
  24. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/SOURCES.txt +4 -1
  25. visualdynamics-0.1.0a45/src/visualdynamics/demo/frame.py +0 -374
  26. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/LICENSE +0 -0
  27. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/README.md +0 -0
  28. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/pyproject.toml +0 -0
  29. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/setup.cfg +0 -0
  30. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/__main__.py +0 -0
  31. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/compatibility.py +0 -0
  32. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/__init__.py +0 -0
  33. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/author.py +0 -0
  34. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/averaging.py +0 -0
  35. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/channel_table.py +0 -0
  36. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/compliance.py +0 -0
  37. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/correlate.py +0 -0
  38. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/data.py +0 -0
  39. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/detect.py +0 -0
  40. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/entities.py +0 -0
  41. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/fem.py +0 -0
  42. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/filters.py +0 -0
  43. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/kurtosis.py +0 -0
  44. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/matches.py +0 -0
  45. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/merge.py +0 -0
  46. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/modal_fit.py +0 -0
  47. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/octave.py +0 -0
  48. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/photos.py +0 -0
  49. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/progress.py +0 -0
  50. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/replication.py +0 -0
  51. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/report.py +0 -0
  52. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/rigid.py +0 -0
  53. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/shapes.py +0 -0
  54. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/shocks.py +0 -0
  55. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/sine.py +0 -0
  56. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/sine_tracking.py +0 -0
  57. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/snr.py +0 -0
  58. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/srs.py +0 -0
  59. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/tables.py +0 -0
  60. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/transform.py +0 -0
  61. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/truncate.py +0 -0
  62. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/unit_choices.py +0 -0
  63. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/validate.py +0 -0
  64. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/wavelet.py +0 -0
  65. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/decimate.py +0 -0
  66. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/deform.py +0 -0
  67. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/drone.py +0 -0
  68. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/plate.py +0 -0
  69. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/__init__.py +0 -0
  70. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/app_icon.py +0 -0
  71. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/ask.py +0 -0
  72. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/author_panel.py +0 -0
  73. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/averaging_panel.py +0 -0
  74. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/console.py +0 -0
  75. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/disclaimer.py +0 -0
  76. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/drawer_tab.py +0 -0
  77. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/editors.py +0 -0
  78. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/filter_panel.py +0 -0
  79. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/glyphs.py +0 -0
  80. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  81. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  82. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/movie.py +0 -0
  83. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/object_tables.py +0 -0
  84. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/octave_panel.py +0 -0
  85. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/preferences.py +0 -0
  86. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/project_tree.py +0 -0
  87. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/record_grid.py +0 -0
  88. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/report_editor.py +0 -0
  89. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/rigid_panel.py +0 -0
  90. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/run_reports.py +0 -0
  91. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/settings_panel.py +0 -0
  92. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/shock_panel.py +0 -0
  93. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/sine_panel.py +0 -0
  94. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/stage_drag.py +0 -0
  95. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/stream_window.py +0 -0
  96. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/tables.py +0 -0
  97. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/toolbars.py +0 -0
  98. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/truncate_panel.py +0 -0
  99. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/updater.py +0 -0
  100. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  101. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/windows.py +0 -0
  102. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/__init__.py +0 -0
  103. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/adf.py +0 -0
  104. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf.py +0 -0
  105. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_objects.py +0 -0
  106. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  107. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  108. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  109. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  110. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  111. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  112. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  113. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  114. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  115. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  116. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  117. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  118. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  119. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  120. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  121. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  122. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  123. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  124. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  125. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  126. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  127. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  128. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  129. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  130. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/excel.py +0 -0
  131. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/exodus.py +0 -0
  132. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/exporters.py +0 -0
  133. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/femap.py +0 -0
  134. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/matlab.py +0 -0
  135. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/nastran.py +0 -0
  136. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/notes.py +0 -0
  137. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/photo_files.py +0 -0
  138. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/punch.py +0 -0
  139. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/rattlesnake.py +0 -0
  140. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  141. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  142. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/report_template.py +0 -0
  143. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sdynpy_data.py +0 -0
  144. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  145. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  146. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sep005.py +0 -0
  147. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sniffing.py +0 -0
  148. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/stl.py +0 -0
  149. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/threemf.py +0 -0
  150. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/unv.py +0 -0
  151. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/names.py +0 -0
  152. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/__init__.py +0 -0
  153. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/averaging.py +0 -0
  154. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/bands.py +0 -0
  155. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/bars.py +0 -0
  156. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/filtering.py +0 -0
  157. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/scalogram.py +0 -0
  158. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/shocks.py +0 -0
  159. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/sine_tracking.py +0 -0
  160. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/tracking_filter.py +0 -0
  161. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/truncation.py +0 -0
  162. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/py.typed +0 -0
  163. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/report/markdown.py +0 -0
  164. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/rotate.py +0 -0
  165. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/units.py +0 -0
  166. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/update.py +0 -0
  167. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/__init__.py +0 -0
  168. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/_quiet.py +0 -0
  169. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/animate.py +0 -0
  170. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/banded.py +0 -0
  171. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/mac_bars.py +0 -0
  172. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/marks.py +0 -0
  173. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/paired.py +0 -0
  174. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/pick.py +0 -0
  175. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/rigid.py +0 -0
  176. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/scalogram.py +0 -0
  177. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/sinespec.py +0 -0
  178. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/waterfall.py +0 -0
  179. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  180. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  181. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/requires.txt +0 -0
  182. {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visualdynamics
3
- Version: 0.1.0a45
3
+ Version: 0.1.0a46
4
4
  Summary: Structural Dynamics Visualization: a units-aware structural dynamics analysis toolset
5
5
  Author-email: Brandon Zwink <bzwink@gmail.com>
6
6
  License-Expression: MIT
@@ -90,7 +90,7 @@ from .units import (
90
90
  #: alpha, said in the version itself so the dialog, the About text, the
91
91
  #: installers and latest.json all say the same thing, and the first
92
92
  #: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
93
- __version__ = '0.1.0a45'
93
+ __version__ = '0.1.0a46'
94
94
 
95
95
 
96
96
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -341,6 +341,83 @@ def face_corners(code: int) -> int:
341
341
  return 3 if name.startswith('tri') else 4
342
342
 
343
343
 
344
+ #: the faces of a solid cell by its node count, as corner positions in
345
+ #: the cell's own node order: the hexahedron's six, the wedge's two
346
+ #: triangles and three quads, the tetrahedron's four. Bottom face then
347
+ #: top, the same way round, as UFF 2412 and Nastran number them.
348
+ _SOLID_FACES = {
349
+ 8: ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5),
350
+ (2, 3, 7, 6), (3, 0, 4, 7)),
351
+ 6: ((0, 2, 1), (3, 4, 5), (0, 1, 4, 3), (1, 2, 5, 4), (2, 0, 3, 5)),
352
+ 4: ((0, 2, 1), (0, 1, 3), (1, 2, 3), (2, 0, 3)),
353
+ }
354
+
355
+
356
+ def solid_faces(node_count: int) -> tuple[tuple[int, ...], ...]:
357
+ """The faces of a solid cell of this many corners, each as
358
+ positions into the cell's node list; a higher-order cell draws by
359
+ its corners, which come first.
360
+
361
+ Parameters
362
+ ----------
363
+ node_count : int
364
+ How many nodes the cell names.
365
+
366
+ Returns
367
+ -------
368
+ tuple of tuple of int
369
+ """
370
+ if node_count in (8, 20, 27):
371
+ return _SOLID_FACES[8]
372
+ if node_count in (6, 15, 24):
373
+ return _SOLID_FACES[6]
374
+ if node_count in (4, 10):
375
+ return _SOLID_FACES[4]
376
+ if node_count in (5, 13): # a pyramid: a quad and four
377
+ return ((0, 3, 2, 1), (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
378
+ return ()
379
+
380
+
381
+ #: per corner of a solid, three corners it shares an edge with: the
382
+ #: three edges whose triple product is that corner's share of the volume,
383
+ #: whose sign says whether the cell is still the right way out
384
+ _CORNER_EDGES = {
385
+ 8: ((1, 3, 4), (2, 0, 5), (3, 1, 6), (0, 2, 7),
386
+ (7, 5, 0), (4, 6, 1), (5, 7, 2), (6, 4, 3)),
387
+ 6: ((1, 2, 3), (2, 0, 4), (0, 1, 5), (5, 4, 0), (3, 5, 1), (4, 3, 2)),
388
+ 4: ((1, 2, 3),),
389
+ 5: ((1, 3, 4), (2, 0, 4), (3, 1, 4), (0, 2, 4)),
390
+ }
391
+
392
+
393
+ def _turned(code: int, before: NDArray, after: NDArray) -> bool:
394
+ """Whether moving an element's nodes from `before` to `after` turns
395
+ it inside out: a solid's corner volume, or a face's normal at a
396
+ corner, changing sign. A line has no inside: one drawn to nothing is
397
+ a link across the gap the move closes — the Frame-Wing's screw ties,
398
+ 2026-10-11 — which the merge that follows removes
399
+ (`merge_collapses`)."""
400
+ family = element_family(code)
401
+ if family in SOLID_FAMILIES:
402
+ count = {'hexes': 8, 'wedges': 6, 'tetras': 4, 'pyramids': 5}[family]
403
+ for corner, (a, b, c) in enumerate(_CORNER_EDGES[count]):
404
+ then = np.linalg.det(before[[a, b, c]] - before[corner])
405
+ now = np.linalg.det(after[[a, b, c]] - after[corner])
406
+ if then * now <= 0.0:
407
+ return True
408
+ return False
409
+ if family in ('quads', 'triangles'):
410
+ corners = face_corners(code)
411
+ for i in range(corners):
412
+ j, k = (i + 1) % corners, (i - 1) % corners
413
+ then = np.cross(before[j] - before[i], before[k] - before[i])
414
+ now = np.cross(after[j] - after[i], after[k] - after[i])
415
+ if float(np.dot(then, now)) <= 0.0:
416
+ return True
417
+ return False
418
+ return False
419
+
420
+
344
421
  class View:
345
422
  """The way a geometry opens in 3-D: where the eye is, seen from the
346
423
  model's center, and which way is up on screen.
@@ -574,6 +651,10 @@ class Geometry:
574
651
  self.mass_properties: MassProperties | None = None
575
652
  #: how it opens in 3-D (`View`); None opens on `DEFAULT_VIEW`
576
653
  self.view: View | None = None
654
+ #: the shapes its parts are made of before they are meshed — blocks,
655
+ #: cylinders and booleans of them (`core.solids`, 2026-10-11); not
656
+ #: elements, and read by nothing that solves or draws a mesh
657
+ self.solids: list = []
577
658
  # one element group, one family (2026-09-30): a source that mixes them —
578
659
  # UNV 2412 and the sdynpy layout carry no element groups at all, so every
579
660
  # element arrives in one — is split on arrival rather than
@@ -779,6 +860,10 @@ class Geometry:
779
860
  raw_origins = (origins if self.length_unit is None
780
861
  else from_si(origins, self.length_unit, 'length'))
781
862
  self.cs_matrix[:, 3, :] = raw_origins * scale
863
+ # the solids' lengths follow the coordinates (their angles do not)
864
+ raw_scale = (1.0 if self.length_unit is None
865
+ else 1.0 / si_transform(self.length_unit, 'length')[0])
866
+ self.solids = [s.scaled(raw_scale * scale) for s in self.solids]
782
867
  self.length_unit = length_unit
783
868
  return self
784
869
 
@@ -789,6 +874,8 @@ class Geometry:
789
874
  self.node_xyz = from_si(self.node_xyz, self.length_unit, 'length')
790
875
  self.cs_matrix[:, 3, :] = from_si(self.cs_matrix[:, 3, :],
791
876
  self.length_unit, 'length')
877
+ self.solids = [s.scaled(1.0 / si_transform(self.length_unit, 'length')[0])
878
+ for s in self.solids]
792
879
  self.length_unit = None
793
880
  return self
794
881
 
@@ -1644,6 +1731,10 @@ class Geometry:
1644
1731
  rows = (range(len(self.elem_conn)) if groups is None
1645
1732
  else np.flatnonzero(np.isin(self.elem_group,
1646
1733
  [int(g) for g in groups])))
1734
+ return self._spacing(rows)
1735
+
1736
+ def _spacing(self, rows) -> float | None:
1737
+ """`node_spacing` over these element rows."""
1647
1738
  by_count: dict[int, list[np.ndarray]] = {}
1648
1739
  for k in rows:
1649
1740
  conn = self.elem_conn[k]
@@ -1830,6 +1921,302 @@ class Geometry:
1830
1921
  return {'merged': len(mapping), 'into': len(set(mapping.values())),
1831
1922
  'duplicates': duplicates, 'links': len(links)}
1832
1923
 
1924
+ # ---- Align Meshes (Brandon, 2026-10-11) -----------------------------
1925
+
1926
+ def _parts(self, groups: Ids | None) -> list[np.ndarray]:
1927
+ """The element rows of `groups` (every element when None) as the
1928
+ pieces their shared nodes join them into: two parts meshed apart
1929
+ are two pieces whichever element groups they are in, and one
1930
+ element group holding two loose pieces is two."""
1931
+ rows = (np.arange(len(self.elem_conn)) if groups is None
1932
+ else np.flatnonzero(np.isin(self.elem_group,
1933
+ [int(g) for g in groups])))
1934
+ parent: dict[int, int] = {}
1935
+
1936
+ def root(node: int) -> int:
1937
+ while parent.setdefault(node, node) != node:
1938
+ parent[node] = parent[parent[node]]
1939
+ node = parent[node]
1940
+ return node
1941
+
1942
+ for k in rows:
1943
+ nodes = [int(n) for n in self.elem_conn[k]]
1944
+ for node in nodes[1:]:
1945
+ parent[root(node)] = root(nodes[0])
1946
+ pieces: dict[int, list[int]] = {}
1947
+ for k in rows:
1948
+ pieces.setdefault(root(int(self.elem_conn[k][0])), []).append(int(k))
1949
+ return [np.asarray(piece) for piece in pieces.values()]
1950
+
1951
+ def _skin(self, rows) -> set[int]:
1952
+ """The nodes of these element rows that lie on their outside:
1953
+ every node of a plate, a line or a point, and of a solid the
1954
+ nodes of the faces no second solid among them shares."""
1955
+ faces: dict[tuple[int, ...], tuple[int, ...]] = {}
1956
+ skin: set[int] = set()
1957
+ for k in rows:
1958
+ conn = [int(n) for n in self.elem_conn[k]]
1959
+ if element_family(int(self.elem_type[k])) not in SOLID_FAMILIES:
1960
+ skin.update(conn)
1961
+ continue
1962
+ for face in solid_faces(len(conn)):
1963
+ corners = tuple(conn[i] for i in face)
1964
+ key = tuple(sorted(corners))
1965
+ if key in faces:
1966
+ del faces[key]
1967
+ else:
1968
+ faces[key] = corners
1969
+ for corners in faces.values():
1970
+ skin.update(corners)
1971
+ return skin
1972
+
1973
+ def _typical_spacing(self, rows) -> float | None:
1974
+ """A piece's mesh size, as Align Meshes reads it: the median over
1975
+ its elements of a brick's middle edge — its size across, for a
1976
+ layer one brick thin — and of any other element's shortest. Not
1977
+ the finest edge anywhere (`node_spacing`): the bricks bent onto a
1978
+ hole's circle have edges a fraction of the mesh's, and the
1979
+ Frame-Wing's wing, meshed at an eighth of an inch, read finer
1980
+ than its frame at a twelfth; and not a brick's shortest edge: the
1981
+ washers under the wing, 0.028 in thick on an eighth-inch grid,
1982
+ read finer than the frame and the frame moved onto them
1983
+ (2026-10-11)."""
1984
+ shortest = []
1985
+ by_count: dict[int, list[np.ndarray]] = {}
1986
+ for k in rows:
1987
+ conn = self.elem_conn[k]
1988
+ if len(conn) > 1:
1989
+ by_count.setdefault(len(conn), []).append(conn)
1990
+ for count, conns in by_count.items():
1991
+ xyz = self.node_xyz[self.node_index(np.concatenate(conns))]
1992
+ xyz = xyz.reshape(len(conns), count, 3)
1993
+ if count == 8:
1994
+ # a brick's three edges from its first corner, the middle
1995
+ edges = np.linalg.norm(xyz[:, [1, 3, 4]] - xyz[:, [0]], axis=-1)
1996
+ shortest.extend(np.sort(edges, axis=1)[:, 1].tolist())
1997
+ continue
1998
+ first, second = np.triu_indices(count, 1)
1999
+ gaps = np.linalg.norm(xyz[:, first] - xyz[:, second], axis=-1)
2000
+ gaps[gaps <= 0.0] = np.inf
2001
+ shortest.extend(gaps.min(axis=1).tolist())
2002
+ shortest = [g for g in shortest if np.isfinite(g) and g > 0.0]
2003
+ return float(np.median(shortest)) if shortest else None
2004
+
2005
+ def align_reach(self, groups: Ids | None = None) -> float | None:
2006
+ """How far Align Meshes reaches unless told: half the coarsest
2007
+ piece's mesh size (`_typical_spacing`), so no node moves further
2008
+ than half an element of the mesh it belongs to. Parts with a gap
2009
+ between them want more: the gap as well.
2010
+
2011
+ Parameters
2012
+ ----------
2013
+ groups : sequence of int, optional
2014
+ The element groups, by id; every element when omitted.
2015
+
2016
+ Returns
2017
+ -------
2018
+ float or None
2019
+ As the coordinates are held; None with no spacing to read.
2020
+ """
2021
+ spacings = [self._typical_spacing(rows) for rows in self._parts(groups)]
2022
+ spacings = [s for s in spacings if s]
2023
+ return 0.5 * max(spacings) if spacings else None
2024
+
2025
+ def align_moves(self, reach: float, groups: Ids | None = None,
2026
+ exclude: Ids | None = None) -> list[list[int]]:
2027
+ """The moves Align Meshes would make: each [node, onto], a node of
2028
+ a coarser piece and the node of a finer piece it would move onto,
2029
+ so the two are one point, ready for Merge Coincident Nodes.
2030
+
2031
+ Two pieces meshed at different sizes cannot be made to match
2032
+ node for node, since their counts differ, but the coarser one's
2033
+ nodes can each be put on the finer one's nearest: none moves more
2034
+ than half the finer spacing in-plane, plus whatever gap lies
2035
+ between them, and each becomes a place to merge (Brandon,
2036
+ 2026-10-11, for the Frame-Wing, whose wing is meshed at an eighth
2037
+ of an inch on a frame meshed at a twelfth). Only nodes on the
2038
+ pieces' outsides move or are moved onto, a node moves once and
2039
+ onto a node no other takes, a node already on another is a place
2040
+ to merge rather than a move, and the nearest pairs are taken
2041
+ first — so a washer's underside goes onto the frame under it, and
2042
+ its top, whose nearest frame node is the underside's, stays.
2043
+ Pieces of equal spacing: the later piece moves.
2044
+
2045
+ Parameters
2046
+ ----------
2047
+ reach : float
2048
+ How far a node may move, as the coordinates are held.
2049
+ groups : sequence of int, optional
2050
+ The element groups whose pieces are aligned, by id; every
2051
+ element when omitted.
2052
+ exclude : sequence of int, optional
2053
+ Nodes left out, by id: neither moved nor moved onto — the
2054
+ moves a person unpicked in the app.
2055
+
2056
+ Returns
2057
+ -------
2058
+ list of [int, int]
2059
+ In the order of the node moved.
2060
+ """
2061
+ from scipy.spatial import cKDTree
2062
+
2063
+ parts = self._parts(groups)
2064
+ if len(parts) < 2:
2065
+ return []
2066
+ left_out = {int(n) for n in exclude or ()}
2067
+ skins = [sorted(self._skin(rows) - left_out) for rows in parts]
2068
+ spacing = [self._typical_spacing(rows) or 0.0 for rows in parts]
2069
+ low, high = self.extent if self.num_nodes else (np.zeros(3), np.zeros(3))
2070
+ # already one point: a place to merge, not a move
2071
+ touching = 1e-9 * float(np.linalg.norm(high - low) or 1.0)
2072
+ found = []
2073
+ for a, movers in enumerate(skins):
2074
+ for b, targets in enumerate(skins):
2075
+ finer = spacing[b] < spacing[a] or (spacing[b] == spacing[a]
2076
+ and b < a)
2077
+ if a == b or not finer or not movers or not targets:
2078
+ continue
2079
+ tree = cKDTree(self.node_xyz[self.node_index(targets)])
2080
+ distance, nearest = tree.query(
2081
+ self.node_xyz[self.node_index(movers)],
2082
+ distance_upper_bound=float(reach))
2083
+ for mover, d, j in zip(movers, distance, nearest):
2084
+ if touching < d <= reach:
2085
+ found.append((float(d), mover, targets[int(j)]))
2086
+ moves, moving, onto = [], set(), set()
2087
+ for _d, mover, target in sorted(found):
2088
+ if (mover in moving or mover in onto or target in onto
2089
+ or target in moving):
2090
+ continue
2091
+ moves.append([mover, target])
2092
+ moving.add(mover)
2093
+ onto.add(target)
2094
+ return sorted(self._unfolded(moves))
2095
+
2096
+ def _unfolded(self, moves: list[list[int]]) -> list[list[int]]:
2097
+ """The moves, nearest first, each tried against those before it
2098
+ and left out when it turns an element inside out — the rule Mesh
2099
+ Solids rounds a hole by. A washer's rim, rounded onto its circle,
2100
+ has bricks one move would fold (the Frame-Wing, 2026-10-11), and
2101
+ a refusal of the whole Align over one of forty moves left a
2102
+ person nothing to do but unpick it by hand."""
2103
+ if not moves:
2104
+ return moves
2105
+ movers = [a for a, _b in moves]
2106
+ touching: dict[int, list[int]] = {}
2107
+ wanted = set(movers)
2108
+ for k, conn in enumerate(self.elem_conn):
2109
+ for n in conn:
2110
+ if int(n) in wanted:
2111
+ touching.setdefault(int(n), []).append(k)
2112
+ rows = dict(zip(movers, self.node_index(movers)))
2113
+ targets = dict(zip(movers, self.node_index([b for _a, b in moves])))
2114
+ grid = self.node_xyz.copy()
2115
+ after = self.node_xyz.copy()
2116
+ order = sorted(moves, key=lambda m: float(np.linalg.norm(
2117
+ grid[targets[m[0]]] - grid[rows[m[0]]])))
2118
+ kept = []
2119
+ for mover, target in order:
2120
+ after[rows[mover]] = grid[targets[mover]]
2121
+ elements = touching.get(mover, [])
2122
+ if any(_turned(int(self.elem_type[k]),
2123
+ grid[self.node_index(self.elem_conn[k])],
2124
+ after[self.node_index(self.elem_conn[k])])
2125
+ for k in elements):
2126
+ after[rows[mover]] = grid[rows[mover]]
2127
+ else:
2128
+ kept.append([mover, target])
2129
+ return kept
2130
+
2131
+ def align_folds(self, moves) -> str | None:
2132
+ """Why these moves would be refused, or None: the sentence
2133
+ `align_meshes` raises when one would turn an element inside out
2134
+ — a solid's corner volume or a plate's facing changing sign —
2135
+ asked ahead, so a pane can say it before its button is pressed.
2136
+
2137
+ Parameters
2138
+ ----------
2139
+ moves : sequence of [int, int]
2140
+ As `align_moves` gives them.
2141
+
2142
+ Returns
2143
+ -------
2144
+ str or None
2145
+ """
2146
+ if not moves:
2147
+ return None
2148
+ moved = {int(a): int(b) for a, b in moves}
2149
+ after = self.node_xyz.copy()
2150
+ rows = self.node_index(list(moved))
2151
+ after[rows] = self.node_xyz[self.node_index(list(moved.values()))]
2152
+ # the elements a move touches, found a node count at a time: a
2153
+ # loop over every element took a third of a second on the
2154
+ # Frame-Wing, on every click in the pane
2155
+ touched = []
2156
+ by_count: dict[int, list[int]] = {}
2157
+ for k, conn in enumerate(self.elem_conn):
2158
+ by_count.setdefault(len(conn), []).append(k)
2159
+ names = np.fromiter(moved, dtype=np.int64)
2160
+ for count, ks in by_count.items():
2161
+ conns = np.asarray([self.elem_conn[k] for k in ks]).reshape(len(ks), count)
2162
+ hit = np.isin(conns, names).any(axis=1)
2163
+ touched.extend(np.asarray(ks)[hit].tolist())
2164
+ touched.sort()
2165
+ # their rows in one lookup: `node_index` sorts every id each call
2166
+ flat = self.node_index(np.concatenate(
2167
+ [self.elem_conn[k] for k in touched])) if touched else []
2168
+ starts = np.cumsum([0] + [len(self.elem_conn[k]) for k in touched])
2169
+ for i, k in enumerate(touched):
2170
+ conn = self.elem_conn[k]
2171
+ index = flat[starts[i]:starts[i + 1]]
2172
+ if _turned(int(self.elem_type[k]), self.node_xyz[index],
2173
+ after[index]):
2174
+ return (f'moving node {next(int(n) for n in conn if int(n) in moved)}'
2175
+ f' turns element {int(self.elem_id[k])} inside out; '
2176
+ 'leave that move out or use a smaller reach')
2177
+ return None
2178
+
2179
+ def align_meshes(self, reach: float, groups: Ids | None = None,
2180
+ exclude: Ids | None = None) -> dict[str, float]:
2181
+ """Move the coarser piece's nodes onto the finer's where they
2182
+ meet (`align_moves`), so Merge Coincident Nodes can join them:
2183
+ parts meshed apart at sizes that do not match, made coincident at
2184
+ their mating faces or where they overlap. Nothing is merged, and
2185
+ no node is removed. A move that would turn an element inside out
2186
+ is not among the moves; refused, with the element named, only
2187
+ when the moves a person left out make one of the rest fold
2188
+ (`align_folds`).
2189
+
2190
+ Parameters
2191
+ ----------
2192
+ reach : float
2193
+ How far a node may move, as the coordinates are held:
2194
+ meters once units are defined.
2195
+ groups : sequence of int, optional
2196
+ The element groups whose pieces are aligned, by id; every
2197
+ element of the geometry when omitted.
2198
+ exclude : sequence of int, optional
2199
+ Nodes left out, by id: neither moved nor moved onto.
2200
+
2201
+ Returns
2202
+ -------
2203
+ dict
2204
+ 'moved', how many nodes moved; 'farthest', the longest move,
2205
+ as the coordinates are held.
2206
+ """
2207
+ moves = self.align_moves(reach, groups, exclude)
2208
+ if not moves:
2209
+ return {'moved': 0, 'farthest': 0.0}
2210
+ refusal = self.align_folds(moves)
2211
+ if refusal is not None:
2212
+ raise ValueError(refusal)
2213
+ rows = self.node_index([a for a, _b in moves])
2214
+ targets = self.node_xyz[self.node_index([b for _a, b in moves])]
2215
+ farthest = float(np.linalg.norm(targets - self.node_xyz[rows],
2216
+ axis=1).max())
2217
+ self.node_xyz[rows] = targets
2218
+ return {'moved': len(moves), 'farthest': farthest}
2219
+
1833
2220
  def delete_coordinate_systems(self, cs_ids: Ids) -> dict[str, int]:
1834
2221
  """Remove coordinate systems, reassigning any node that used them.
1835
2222
 
@@ -105,7 +105,7 @@ def block(corner: ArrayLike, edge_a: ArrayLike, edge_b: ArrayLike,
105
105
  """A rectangular block meshed into eight-node bricks, in one element
106
106
  group.
107
107
 
108
- `plane` one dimension up (2026-09-30, for the four-unit frame
108
+ `plane` one dimension up (2026-09-30, for the Frame-Wing
109
109
  example): a corner and three perpendicular edges, each divided
110
110
  evenly into the whole number of elements nearest `size`, so the
111
111
  bricks come out close to cubes. Blocks that meet over a face divide
@@ -123,16 +123,21 @@ def block(corner: ArrayLike, edge_a: ArrayLike, edge_b: ArrayLike,
123
123
  no fifth item takes `hole_name`. Holes apply in order, a later one
124
124
  over an earlier: a through hole, then an insert named to the same
125
125
  radius part way down, then a void of the insert's bore, is a
126
- threaded insert in a drilled hole. The nodes on a hole's rim — the
127
- ones its bricks share with the bricks beside them — are then moved
128
- radially onto the circle, a node on the block's own face sliding
129
- along that face to where the circle meets it, so a hole is round to
130
- within the polygon its rim nodes make rather than stair-stepped
131
- (a hole centered on the block's edge is a round-over and keeps its
132
- steps; its corner brick cannot follow an arc): a
133
- stair-stepped hole in a member six bricks wide was anywhere from
134
- three to four bricks across as its center fell, and the frame's
135
- modes wandered by ten percent with it (2026-09-30).
126
+ threaded insert in a drilled hole. Every hole runs along the same
127
+ edge; one along another is refused, since the bricks are a grid
128
+ swept along it.
129
+
130
+ A hole is round, not stair-stepped — a stair-stepped hole in a
131
+ member six bricks wide was anywhere from three to four bricks across
132
+ as its center fell, and the frame's modes wandered by ten percent
133
+ with it (2026-09-30). Mesh Solids' mesher does it (`solids._mesh_grid`,
134
+ one rule for both, 2026-10-11): about a hole with room the grid is
135
+ replaced by a ring mesh, the circle's nodes on it and evenly spaced,
136
+ a core where an insert fills it; a hole without room has the grid's
137
+ nodes on its rim pulled onto the circle, sliding along the block's
138
+ face where it breaks out. A hole centered on the block's edge is a
139
+ round-over and keeps its steps: its corner brick cannot follow an
140
+ arc, and its area is within a few percent of the fillet's already.
136
141
 
137
142
  Parameters
138
143
  ----------
@@ -160,6 +165,7 @@ def block(corner: ArrayLike, edge_a: ArrayLike, edge_b: ArrayLike,
160
165
  name the holes put bricks in.
161
166
  """
162
167
  from ..units import si_transform
168
+ from .solids import _mesh_grid
163
169
 
164
170
  scale = 1.0 if unit is None else si_transform(unit, 'length')[0]
165
171
  corner = np.asarray(corner, dtype=float)
@@ -178,104 +184,55 @@ def block(corner: ArrayLike, edge_a: ArrayLike, edge_b: ArrayLike,
178
184
  counts = [max(1, round(length / size)) + 1 for length in lengths]
179
185
  grids = [np.linspace(0.0, length, count)
180
186
  for length, count in zip(lengths, counts)]
181
- na, nb, _nc = counts
182
- local = np.array([(a, b, c) for c in grids[2] for b in grids[1]
183
- for a in grids[0]])
184
- xyz = corner + local @ np.array(axes)
185
- node = np.arange(1, len(xyz) + 1)
186
- conn, centers = [], []
187
- for k in range(counts[2] - 1):
188
- for j in range(nb - 1):
189
- for i in range(na - 1):
190
- first = k * na * nb + j * na + i
191
- cell = [first, first + 1, first + na + 1, first + na]
192
- conn.append(node[cell + [c + na * nb for c in cell]])
193
- centers.append(local[cell].mean(axis=0)
194
- + 0.5 * (grids[2][k + 1] - grids[2][k])
195
- * np.array([0.0, 0.0, 1.0]))
196
- centers = np.array(centers)
197
- # 1 is the element group, 0 a brick removed, 2 and up the named element groups
198
- groups = np.ones(len(conn), dtype=int)
187
+ along_axis = {int(hole[2]) for hole in holes}
188
+ if len(along_axis) > 1:
189
+ raise ValueError(f'{name or "a block"}: every hole must run along '
190
+ 'the same edge')
191
+ w = along_axis.pop() if along_axis else 2
192
+ order = [(w + 1) % 3, (w + 2) % 3, w]
193
+ # the holes in the block's own frame, the corner at the origin, each
194
+ # with the element group its bricks go to (0 for none)
199
195
  names = [name]
200
- rims: list[tuple[np.ndarray, np.ndarray, float, int]] = []
201
- conn_array = np.array(conn) - 1 if conn else np.empty((0, 8), int)
196
+ cut = []
202
197
  for hole in holes:
203
- center, radius, axis = hole[0], float(hole[1]), int(hole[2])
198
+ center = np.asarray(hole[0], dtype=float) - corner
199
+ center = np.array([float(center @ unit_axis) for unit_axis in axes])
204
200
  depth = float(hole[3]) if len(hole) > 3 and hole[3] is not None else None
205
201
  target = hole[4] if len(hole) > 4 else hole_name
206
- along = np.asarray(center, dtype=float) - corner
207
- along = np.array([float(along @ unit_axis) for unit_axis in axes])
208
- across = [d for d in range(3) if d != axis]
209
- distance = np.linalg.norm(centers[:, across] - along[across], axis=1)
210
- inside = distance <= radius
211
- if depth is not None:
202
+ if target is not None and target not in names:
203
+ names.append(target)
204
+ group = 0 if target is None else names.index(target) + 1
205
+ if depth is None:
206
+ low, high = 0.0, lengths[w]
207
+ elif center[w] > lengths[w] / 2.0:
212
208
  # blind from the face the axis enters at: the corner's own
213
209
  # face when the center sits on it, the far face otherwise
214
- far = along[axis] > lengths[axis] / 2.0
215
- reach = (lengths[axis] - centers[:, axis] if far
216
- else centers[:, axis])
217
- inside &= reach <= depth
218
- # a hole whose axis lies inside the block gets a round rim; one
219
- # centered on the block's own edge or corner is a round-over,
220
- # and its rim stays stair-stepped: the corner brick a fillet
221
- # keeps cannot be pulled onto the arc without folding, and its
222
- # area is within a few percent of the fillet's already
223
- within = all(0.0 < along[d] < lengths[d]
224
- for d in range(3) if d != axis)
225
- beyond = distance > radius
226
- if within and inside.any() and beyond.any():
227
- # the rim: nodes the hole's bricks share with bricks beyond
228
- # its radius — not with the bricks under a blind hole's
229
- # floor, which lie inside the circle and must stay put
230
- rim = np.intersect1d(np.unique(conn_array[inside]),
231
- np.unique(conn_array[beyond]))
232
- rims.append((rim, along, radius, axis))
233
- if target is None:
234
- groups[inside] = 0
210
+ low, high = lengths[w] - depth, lengths[w]
235
211
  else:
236
- if target not in names:
237
- names.append(target)
238
- groups[inside] = names.index(target) + 1
239
- for rim, along, radius, axis in rims:
240
- across = [d for d in range(3) if d != axis]
241
- for row in rim:
242
- offset = local[row, across] - along[across]
243
- distance = float(np.linalg.norm(offset))
244
- if distance == 0.0:
245
- continue
246
- target = along[across] + offset * (radius / distance)
247
- # a node on the block's face keeps to the face: it slides to
248
- # where the circle meets it, or stays if the circle does not
249
- on_face = [k for k, d in enumerate(across)
250
- if local[row, d] <= 1e-12 * lengths[d]
251
- or local[row, d] >= lengths[d] * (1 - 1e-12)]
252
- if len(on_face) == 2:
253
- continue
254
- if on_face:
255
- held, other = on_face[0], 1 - on_face[0]
256
- reach = radius ** 2 - offset[held] ** 2
257
- if reach < 0.0:
258
- continue
259
- target = target.copy()
260
- target[held] = local[row, across[held]]
261
- target[other] = along[across[other]] + (
262
- np.sign(offset[other]) or 1.0) * np.sqrt(reach)
263
- local[row, across] = target
264
- xyz = corner + local @ np.array(axes)
265
- if not (groups > 0).all():
266
- keep = groups > 0
267
- conn = [c for c, k in zip(conn, keep) if k]
268
- groups = groups[keep]
269
- used = np.unique(np.concatenate(conn)) if conn else np.array([], int)
270
- renumber = {int(old): new for new, old in enumerate(used, 1)}
271
- xyz = xyz[used - 1]
272
- conn = [np.array([renumber[int(n)] for n in c]) for c in conn]
273
- node = np.arange(1, len(xyz) + 1)
274
- if not conn:
212
+ low, high = 0.0, depth
213
+ cut.append((center, float(hole[1]), low, high, group))
214
+ across = order[:2]
215
+
216
+ def held(points):
217
+ group = np.ones(len(points), dtype=int)
218
+ for center, radius, low, high, to in cut:
219
+ inside = ((np.linalg.norm(points[:, across] - center[across], axis=1)
220
+ <= radius) & (points[:, w] >= low) & (points[:, w] <= high))
221
+ group[inside] = to
222
+ return group
223
+
224
+ # a hole centered on the block's edge or corner is a round-over and
225
+ # keeps its steps: no circle for it
226
+ circles = [(center, radius, low, high, True) for center, radius, low, high, _to in cut
227
+ if all(0.0 < center[d] < lengths[d] for d in across)]
228
+ local, conn, groups, _made = _mesh_grid([grids[k] for k in order], order, size,
229
+ held, circles)
230
+ if not len(conn):
275
231
  raise ValueError(f'{name or "a block"}: the holes leave nothing')
232
+ xyz = corner + local @ np.array(axes)
276
233
  present = [k for k in range(1, len(names) + 1) if (groups == k).any()]
277
- return Geometry(node_id=node, node_xyz=xyz * scale, elem_conn=conn,
278
- elem_type=[115] * len(conn),
234
+ return Geometry(node_id=np.arange(1, len(xyz) + 1), node_xyz=xyz * scale,
235
+ elem_conn=list(conn), elem_type=[115] * len(conn),
279
236
  elem_group=[int(b) for b in groups],
280
237
  group_id=present,
281
238
  group_name=[names[k - 1] for k in present],