visualdynamics 0.1.0a67__tar.gz → 0.1.0a68__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 (226) hide show
  1. {visualdynamics-0.1.0a67/src/visualdynamics.egg-info → visualdynamics-0.1.0a68}/PKG-INFO +2 -1
  2. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/pyproject.toml +4 -0
  3. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/__init__.py +1 -1
  4. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/instrumentation.py +104 -10
  5. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/__init__.py +37 -0
  6. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/icons.py +29 -19
  7. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/instrumentation_panel.py +30 -12
  8. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/main_window.py +75 -17
  9. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/project.py +19 -4
  10. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68/src/visualdynamics.egg-info}/PKG-INFO +2 -1
  11. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics.egg-info/requires.txt +1 -0
  12. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/LICENSE +0 -0
  13. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/README.md +0 -0
  14. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/setup.cfg +0 -0
  15. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/__main__.py +0 -0
  16. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/compatibility.py +0 -0
  17. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/__init__.py +0 -0
  18. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/author.py +0 -0
  19. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/averaging.py +0 -0
  20. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/channel_table.py +0 -0
  21. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/classical_pulse.py +0 -0
  22. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/compliance.py +0 -0
  23. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/correlate.py +0 -0
  24. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/data.py +0 -0
  25. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/detect.py +0 -0
  26. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/dwell.py +0 -0
  27. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/entities.py +0 -0
  28. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/fem.py +0 -0
  29. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/filters.py +0 -0
  30. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/geometry.py +0 -0
  31. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/half_power.py +0 -0
  32. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/hexmesh.py +0 -0
  33. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/kurtosis.py +0 -0
  34. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/matches.py +0 -0
  35. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/mates.py +0 -0
  36. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/merge.py +0 -0
  37. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/mesh.py +0 -0
  38. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/modal_fit.py +0 -0
  39. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/octave.py +0 -0
  40. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/photos.py +0 -0
  41. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/progress.py +0 -0
  42. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/replication.py +0 -0
  43. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/replication_fit.py +0 -0
  44. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/replication_segments.py +0 -0
  45. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/report.py +0 -0
  46. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/rigid.py +0 -0
  47. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/shaker.py +0 -0
  48. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/shapes.py +0 -0
  49. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/shock_check.py +0 -0
  50. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/shocks.py +0 -0
  51. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/signature.py +0 -0
  52. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/sine.py +0 -0
  53. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/sine_tracking.py +0 -0
  54. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/snr.py +0 -0
  55. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/solids.py +0 -0
  56. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/sparse_solve.py +0 -0
  57. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/srs.py +0 -0
  58. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/stroke.py +0 -0
  59. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/sweep_rate.py +0 -0
  60. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/tables.py +0 -0
  61. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/tetmesh.py +0 -0
  62. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/time_statistics.py +0 -0
  63. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/tone_readout.py +0 -0
  64. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/transform.py +0 -0
  65. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/truncate.py +0 -0
  66. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/unit_choices.py +0 -0
  67. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/validate.py +0 -0
  68. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/core/wavelet.py +0 -0
  69. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/decimate.py +0 -0
  70. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/deform.py +0 -0
  71. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/demo/__init__.py +0 -0
  72. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/demo/barc.py +0 -0
  73. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/demo/drone.py +0 -0
  74. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/demo/frame_wing.py +0 -0
  75. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/demo/plate.py +0 -0
  76. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/demo/shapes.py +0 -0
  77. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/app_icon.py +0 -0
  78. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/ask.py +0 -0
  79. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/author_panel.py +0 -0
  80. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/averaging_panel.py +0 -0
  81. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/console.py +0 -0
  82. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/disclaimer.py +0 -0
  83. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/drawer_tab.py +0 -0
  84. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/dwell_panel.py +0 -0
  85. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/editors.py +0 -0
  86. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/filter_panel.py +0 -0
  87. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/glyphs.py +0 -0
  88. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  89. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  90. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/mate_panel.py +0 -0
  91. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/merge_panel.py +0 -0
  92. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/mesh_panel.py +0 -0
  93. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/mesh_solids_panel.py +0 -0
  94. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/message_bar.py +0 -0
  95. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/movie.py +0 -0
  96. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/object_tables.py +0 -0
  97. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/octave_panel.py +0 -0
  98. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/opengl.py +0 -0
  99. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/panes.py +0 -0
  100. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/pattern_panel.py +0 -0
  101. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/preferences.py +0 -0
  102. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/project_tree.py +0 -0
  103. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/pulse_panel.py +0 -0
  104. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/record_grid.py +0 -0
  105. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/replication_fit_panel.py +0 -0
  106. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/report_editor.py +0 -0
  107. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/rigid_panel.py +0 -0
  108. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/run_reports.py +0 -0
  109. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/segments_panel.py +0 -0
  110. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/settings_panel.py +0 -0
  111. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/shock_panel.py +0 -0
  112. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/signature_panel.py +0 -0
  113. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/sine_panel.py +0 -0
  114. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/srs_tolerance_panel.py +0 -0
  115. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/stage_drag.py +0 -0
  116. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/statistics_panel.py +0 -0
  117. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/stream_window.py +0 -0
  118. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/stroke_panel.py +0 -0
  119. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/sweep_rate_panel.py +0 -0
  120. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/tables.py +0 -0
  121. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/tone_panel.py +0 -0
  122. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/toolbars.py +0 -0
  123. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/tooltips.py +0 -0
  124. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/truncate_panel.py +0 -0
  125. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/updater.py +0 -0
  126. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  127. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/gui/windows.py +0 -0
  128. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/__init__.py +0 -0
  129. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/adf.py +0 -0
  130. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf.py +0 -0
  131. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_objects.py +0 -0
  132. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  133. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  134. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  135. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  136. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  137. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  138. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  139. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  140. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  141. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  142. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  143. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  144. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  145. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  146. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  147. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  148. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  149. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  150. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  151. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  152. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  153. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  154. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  155. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  156. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/excel.py +0 -0
  157. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/exodus.py +0 -0
  158. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/exporters.py +0 -0
  159. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/femap.py +0 -0
  160. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/matlab.py +0 -0
  161. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/nastran.py +0 -0
  162. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/native.py +0 -0
  163. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/notes.py +0 -0
  164. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/photo_files.py +0 -0
  165. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/punch.py +0 -0
  166. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/rattlesnake.py +0 -0
  167. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  168. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  169. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/report_template.py +0 -0
  170. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/sdynpy_data.py +0 -0
  171. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  172. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  173. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/sep005.py +0 -0
  174. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/sniffing.py +0 -0
  175. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/step.py +0 -0
  176. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/stl.py +0 -0
  177. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/threemf.py +0 -0
  178. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/io/unv.py +0 -0
  179. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/names.py +0 -0
  180. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/__init__.py +0 -0
  181. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/averaging.py +0 -0
  182. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/bands.py +0 -0
  183. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/bars.py +0 -0
  184. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/classical_pulse.py +0 -0
  185. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/dwell.py +0 -0
  186. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/filtering.py +0 -0
  187. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/half_power.py +0 -0
  188. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/replication_fit.py +0 -0
  189. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/replication_segments.py +0 -0
  190. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/scalogram.py +0 -0
  191. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/shock_check.py +0 -0
  192. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/shocks.py +0 -0
  193. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/signature.py +0 -0
  194. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/sine_tracking.py +0 -0
  195. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/srs_tolerance.py +0 -0
  196. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/stroke.py +0 -0
  197. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/sweep_rate.py +0 -0
  198. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/time_statistics.py +0 -0
  199. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/tone_readout.py +0 -0
  200. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/tracking_filter.py +0 -0
  201. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/plot/truncation.py +0 -0
  202. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/py.typed +0 -0
  203. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/report/__init__.py +0 -0
  204. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/report/markdown.py +0 -0
  205. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/report/page.py +0 -0
  206. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/rotate.py +0 -0
  207. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/theme.py +0 -0
  208. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/units.py +0 -0
  209. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/update.py +0 -0
  210. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/__init__.py +0 -0
  211. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/_quiet.py +0 -0
  212. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/animate.py +0 -0
  213. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/banded.py +0 -0
  214. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/geometry.py +0 -0
  215. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/mac_bars.py +0 -0
  216. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/marks.py +0 -0
  217. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/paired.py +0 -0
  218. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/pick.py +0 -0
  219. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/rigid.py +0 -0
  220. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/scalogram.py +0 -0
  221. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/sinespec.py +0 -0
  222. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics/viz/waterfall.py +0 -0
  223. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics.egg-info/SOURCES.txt +0 -0
  224. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  225. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  226. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a68}/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.0a67
3
+ Version: 0.1.0a68
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
@@ -32,6 +32,7 @@ Requires-Dist: pyvista
32
32
  Requires-Dist: pyvistaqt
33
33
  Requires-Dist: vtk
34
34
  Requires-Dist: PySide6
35
+ Requires-Dist: shiboken6
35
36
  Requires-Dist: pyqtgraph
36
37
  Requires-Dist: openpyxl
37
38
  Requires-Dist: pandas
@@ -79,6 +79,10 @@ dependencies = [
79
79
  # pyvista's own cap is what decides which VTK is installed.
80
80
  "vtk",
81
81
  "PySide6",
82
+ # PySide6's own binding layer (LGPL-3.0, the same terms), installed
83
+ # with it: imported by name for the exit's window release
84
+ # (gui.release_widgets), so declared rather than relied on arriving
85
+ "shiboken6",
82
86
  "pyqtgraph",
83
87
  "openpyxl",
84
88
  # A channel table is a spreadsheet, and a DataFrame is what carries a
@@ -97,7 +97,7 @@ from .units import (
97
97
  #: alpha, said in the version itself so the dialog, the About text, the
98
98
  #: installers and latest.json all say the same thing, and the first
99
99
  #: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
100
- __version__ = '0.1.0a67'
100
+ __version__ = '0.1.0a68'
101
101
 
102
102
 
103
103
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -23,7 +23,10 @@ surface allows. A node where surfaces meet at an edge or a corner is a
23
23
  candidate on each of them, so that a sensor sits flat on one; within a
24
24
  surface the faces' normals at the node are averaged. A plate's normal
25
25
  points away from the middle of the model. Beams contribute every node,
26
- measured in the global system.
26
+ measured in the global system. A place nearer than `edge_distance` to
27
+ its surface's edge is not a candidate, since a sensor's base needs flat
28
+ ground around the spot it is glued to; an edge here is wherever the
29
+ surface ends, at a sharp edge, a free edge, a hole or where parts touch.
27
30
 
28
31
  `local_shapes` turns a shape set's translations into each candidate's
29
32
  own directions, and `effective_independence` chooses the sensors:
@@ -52,7 +55,16 @@ import numpy as np
52
55
  #: neighboring faces whose outward normals differ by more than this, in
53
56
  #: degrees, are on separate surfaces: a block's edge is 90, a
54
57
  #: cylinder's facets a few degrees each
55
- EDGE_ANGLE = 40.0
58
+ # Brandon, 2026-10-22: 20 degrees, from 40
59
+ EDGE_ANGLE = 20.0
60
+
61
+ #: how far a sensor stays from its surface's edge, in meters: a quarter
62
+ #: of an inch (Brandon, 2026-10-22)
63
+ EDGE_DISTANCE = 0.25 * 0.0254
64
+
65
+ #: a surface of less than this share of the whole outside's area is
66
+ #: left out unless picked (`default_surfaces`)
67
+ SMALL_SURFACE = 0.01
56
68
 
57
69
  #: removal in batches stops once this many times the wanted count
58
70
  #: remain, and the rest is removed one at a time
@@ -231,8 +243,35 @@ def skin(geometry: Any, *, edge_angle: float = EDGE_ANGLE) -> list[Surface]:
231
243
  return found
232
244
 
233
245
 
246
+ def default_surfaces(found: list[Surface]) -> list[int]:
247
+ """The surfaces sensors go on unless told otherwise: every one of at
248
+ least `SMALL_SURFACE` of the whole outside's area. Sensors go on
249
+ the significant surfaces of a test article; a small one is most
250
+ often a sliver along an edge, a fillet or a hole's bore. When no
251
+ surface is that large, all of them.
252
+
253
+ Parameters
254
+ ----------
255
+ found : list of Surface
256
+ From `skin`.
257
+
258
+ Returns
259
+ -------
260
+ list of int
261
+ Their ids, in order.
262
+ """
263
+ # Brandon, 2026-10-22: at 20 degrees the cone split into 193
264
+ # surfaces, most of them a single element; "any surfaces less than
265
+ # 1% of the total surface area should be de-selected by default ...
266
+ # we just mount to significant surfaces usually"
267
+ total = sum(s.area for s in found)
268
+ kept = [s.id for s in found if s.area >= SMALL_SURFACE * total]
269
+ return kept or [s.id for s in found]
270
+
271
+
234
272
  def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
235
- edge_angle: float = EDGE_ANGLE, found: Any = None) -> Candidates:
273
+ edge_angle: float = EDGE_ANGLE, found: Any = None,
274
+ edge_distance: float = 0.0) -> Candidates:
236
275
  """Every place on `geometry` a sensor can be mounted, as an
237
276
  experimental geometry with a coordinate system per place.
238
277
 
@@ -244,13 +283,19 @@ def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
244
283
  The element groups sensors may go on. All of them when omitted.
245
284
  A face shared with a group that is not picked is still inside.
246
285
  surfaces : sequence of int, optional
247
- The surfaces sensors may go on, by `Surface.id`. All of those in
248
- `groups` when omitted.
286
+ The surfaces sensors may go on, by `Surface.id`. Those of
287
+ `groups` that `default_surfaces` keeps when omitted: every one
288
+ of at least `SMALL_SURFACE` of the outside's area.
249
289
  edge_angle : float, default EDGE_ANGLE
250
290
  Degrees between two neighboring faces' normals beyond which they
251
291
  are on separate surfaces.
252
292
  found : list of Surface, optional
253
293
  `skin(geometry, edge_angle=edge_angle)`, when already found.
294
+ edge_distance : float, default 0
295
+ How near a place may be to its surface's edge, in the
296
+ geometry's coordinates (meters once its units are defined),
297
+ measured in a straight line. Nothing is left out at 0. A
298
+ surface with no edge, such as a whole sphere, keeps every place.
254
299
 
255
300
  Returns
256
301
  -------
@@ -260,11 +305,12 @@ def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
260
305
 
261
306
  picked = ({int(g) for g in geometry.group_id} if groups is None
262
307
  else {int(g) for g in groups})
263
- wanted = None if surfaces is None else {int(s) for s in surfaces}
264
308
  xyz = geometry.node_xyz
265
309
  row_of = {int(n): k for k, n in enumerate(geometry.node_id)}
310
+ by_id = np.argsort(geometry.node_id)
266
311
  if found is None:
267
312
  found = skin(geometry, edge_angle=edge_angle)
313
+ wanted = set(default_surfaces(found) if surfaces is None else (int(s) for s in surfaces))
268
314
 
269
315
  # a place per node per surface, the surface's faces at the node
270
316
  # averaged by area; the largest first, so a node's own id goes to it
@@ -278,6 +324,11 @@ def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
278
324
  at, where = np.unique(corner_nodes, return_inverse=True)
279
325
  summed = np.zeros((len(at), 3))
280
326
  np.add.at(summed, where, np.repeat(weight, per_face, axis=0))
327
+ if edge_distance > 0:
328
+ rows = by_id[np.searchsorted(geometry.node_id, at, sorter=by_id)]
329
+ away = _away_from_edge(surface.faces, at, xyz[rows], xyz, by_id,
330
+ geometry.node_id, edge_distance)
331
+ at, summed = at[away], summed[away]
281
332
  nodes.append(at)
282
333
  sums.append(summed)
283
334
  kinds.extend([surface.kind] * len(at))
@@ -623,7 +674,8 @@ class Plan:
623
674
 
624
675
  def plan(shapes: Any, geometry: Any, triaxial: int, uniaxial: int = 0, *,
625
676
  modes: Any = None, groups: Any = None, surfaces: Any = None,
626
- edge_angle: float | None = None, axes: Any = (2,)) -> Plan:
677
+ edge_angle: float | None = None, axes: Any = (2,),
678
+ edge_distance: float | None = None) -> Plan:
627
679
  """Plan a test's accelerometers from a finite element model.
628
680
 
629
681
  Parameters
@@ -642,14 +694,20 @@ def plan(shapes: Any, geometry: Any, triaxial: int, uniaxial: int = 0, *,
642
694
  groups : sequence of int, optional
643
695
  The element groups sensors may go on. All when omitted.
644
696
  surfaces : sequence of int, optional
645
- The surfaces sensors may go on, by `Surface.id`. All of those in
646
- `groups` when omitted.
697
+ The surfaces sensors may go on, by `Surface.id`. Those of
698
+ `groups` that `default_surfaces` keeps when omitted.
647
699
  edge_angle : float, optional
648
700
  Degrees between neighboring faces' normals beyond which they are
649
701
  on separate surfaces; `EDGE_ANGLE` when omitted.
650
702
  axes : sequence of int, default (2,)
651
703
  The directions a uni-axial accelerometer may measure: 0, 1, 2
652
704
  for a place's X, Y, Z. Z, the surface normal, by default.
705
+ edge_distance : float, optional
706
+ How near a sensor may be to its surface's edge, in the
707
+ geometry's coordinates. `EDGE_DISTANCE`, a quarter of an inch,
708
+ when omitted and the geometry's units are defined; 0 when they
709
+ are not, since a distance means nothing against coordinates
710
+ with no unit.
653
711
 
654
712
  Returns
655
713
  -------
@@ -665,8 +723,11 @@ def plan(shapes: Any, geometry: Any, triaxial: int, uniaxial: int = 0, *,
665
723
  modes = [int(m) for m in modes]
666
724
  if not modes:
667
725
  raise ValueError('no modes to measure: every mode is a rigid-body mode at 0 Hz')
726
+ if edge_distance is None:
727
+ edge_distance = EDGE_DISTANCE if geometry.units_defined else 0.0
668
728
  places = candidates(geometry, groups, surfaces,
669
- edge_angle=EDGE_ANGLE if edge_angle is None else float(edge_angle))
729
+ edge_angle=EDGE_ANGLE if edge_angle is None else float(edge_angle),
730
+ edge_distance=float(edge_distance))
670
731
  blocks, measured = local_shapes(shapes, modes, places, geometry)
671
732
  placement = effective_independence(blocks, triaxial, uniaxial, axes=axes,
672
733
  allowed=measured, sources=places.source,
@@ -770,6 +831,39 @@ def assemble(shapes: Any, modes: Any, places: Candidates, blocks: Any,
770
831
  return Plan(kept_geometry, table, test_shapes, placement)
771
832
 
772
833
 
834
+ def _away_from_edge(faces: list, at: np.ndarray, points: np.ndarray, xyz: np.ndarray,
835
+ by_id: np.ndarray, node_id: np.ndarray, distance: float) -> np.ndarray:
836
+ """Which of the nodes `at`, at `points`, are at least `distance`
837
+ from the edge of the surface made of `faces`: the sides only one of
838
+ its faces has."""
839
+ from scipy.spatial import cKDTree
840
+
841
+ sides = np.array([sorted((f[k], f[(k + 1) % len(f)])) for f in faces
842
+ for k in range(len(f))], dtype=np.int64).reshape(-1, 2)
843
+ unique, count = np.unique(sides, axis=0, return_counts=True)
844
+ edge = unique[count == 1]
845
+ if not len(edge):
846
+ return np.ones(len(at), dtype=bool)
847
+ ends = xyz[by_id[np.searchsorted(node_id, edge.ravel(), sorter=by_id)]].reshape(-1, 2, 3)
848
+ start, along = ends[:, 0], ends[:, 1] - ends[:, 0]
849
+ middle = start + along / 2
850
+ half = float(np.linalg.norm(along, axis=1).max()) / 2
851
+ # every side whose middle is near enough that the side could be
852
+ # nearer than `distance`, then its exact distance
853
+ near = cKDTree(middle).query_ball_point(points, distance + half, return_sorted=False)
854
+ which = np.repeat(np.arange(len(at)), [len(n) for n in near])
855
+ side = np.fromiter((k for n in near for k in n), dtype=np.int64, count=len(which))
856
+ if not len(side):
857
+ return np.ones(len(at), dtype=bool)
858
+ span = np.einsum('ij,ij->i', along[side], along[side])
859
+ t = np.einsum('ij,ij->i', points[which] - start[side], along[side])
860
+ t = np.clip(np.divide(t, span, out=np.zeros_like(t), where=span > 0), 0.0, 1.0)
861
+ gap = np.linalg.norm(points[which] - start[side] - t[:, None] * along[side], axis=1)
862
+ away = np.ones(len(at), dtype=bool)
863
+ away[which[gap < distance]] = False
864
+ return away
865
+
866
+
773
867
  def _remove(kept: np.ndarray, loss: np.ndarray, target: int) -> np.ndarray:
774
868
  """`kept` less its weakest: a tenth of it while far more remain than
775
869
  are wanted, otherwise the single weakest."""
@@ -244,4 +244,41 @@ def main(argv: Sequence[str] | None = None) -> int:
244
244
  # while both still stand.
245
245
  app.clipboard().clear()
246
246
  app.processEvents()
247
+ release_widgets(app)
247
248
  return code
249
+
250
+
251
+ def release_widgets(app: Any) -> int:
252
+ """Delete the windows Python still owns, one by one, while the
253
+ application stands, and return how many there were.
254
+
255
+ Left to the interpreter's exit, PySide deletes every widget it still
256
+ owns in one sweep over its own table, in no particular order. A
257
+ window deleted there takes its children with it, and the sweep then
258
+ reaches a child it still lists and deletes it again: the process
259
+ segfaults on its way out. Deleting each window here, through its
260
+ wrapper, leaves the sweep nothing to do.
261
+ """
262
+ # a gate worker segfaulted at exit three times on 2026-10-22, in
263
+ # PySide's moduleShutdown sweep, on a QListWidget, a QComboBox and a
264
+ # QRadioButton of dialogs the tests had left open; PLAN.md, "Nothing
265
+ # of Python's left for PySide's sweep"
266
+ import gc
267
+
268
+ import shiboken6
269
+ from PySide6.QtCore import QCoreApplication, QEvent
270
+
271
+ # the cycles first: a window held by nothing but its own connections
272
+ # goes the way Python owns it, not the sweep's
273
+ gc.collect()
274
+ released = 0
275
+ for widget in app.topLevelWidgets():
276
+ # through shiboken, not deleteLater: a window Qt deletes leaves
277
+ # its children's wrappers standing, and the sweep still crashed
278
+ if (shiboken6.isValid(widget) and shiboken6.ownedByPython(widget)
279
+ and widget.parent() is None):
280
+ shiboken6.delete(widget)
281
+ released += 1
282
+ QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
283
+ app.processEvents()
284
+ return released
@@ -1867,40 +1867,50 @@ def _draw_coordinate_systems(painter, color):
1867
1867
  painter.drawEllipse(origin, 5, 5)
1868
1868
 
1869
1869
 
1870
+ def _depth(painter, color, *edges):
1871
+ """The edges a solid's face is extruded along, thinner than the face
1872
+ so they read as going back.
1873
+
1874
+ `_element_shape` leaves the pen off to fill its node dots, so these
1875
+ take a pen of their own.
1876
+ """
1877
+ # drawn with the dots' empty pen they were invisible: the tet wore
1878
+ # the triangle's icon and the brick the quad's (Brandon, 2026-10-22)
1879
+ painter.setPen(_pen(color, 5))
1880
+ for start, end in edges:
1881
+ painter.drawLine(QPointF(*start), QPointF(*end))
1882
+
1883
+
1870
1884
  def _draw_tet(painter, color):
1871
- """A tetrahedron: a triangle with its apex drawn to the far corner."""
1885
+ """A tetrahedron: a triangle with its far corner inside, joined to
1886
+ the three."""
1872
1887
  _element_shape(painter, color, [(32, 10), (54, 50), (10, 50)])
1873
- painter.drawLine(QPointF(32, 10), QPointF(34, 36))
1874
- painter.drawLine(QPointF(54, 50), QPointF(34, 36))
1875
- painter.drawLine(QPointF(10, 50), QPointF(34, 36))
1888
+ _depth(painter, color, ((32, 10), (34, 36)), ((54, 50), (34, 36)),
1889
+ ((10, 50), (34, 36)))
1876
1890
 
1877
1891
 
1878
1892
  def _draw_wedge(painter, color):
1879
- """A wedge: a triangle extruded sideways."""
1880
- _element_shape(painter, color, [(12, 20), (28, 48), (12, 48)])
1881
- painter.drawLine(QPointF(12, 20), QPointF(40, 14))
1882
- painter.drawLine(QPointF(28, 48), QPointF(54, 42))
1883
- painter.drawLine(QPointF(12, 48), QPointF(40, 42))
1884
- painter.drawLine(QPointF(40, 14), QPointF(54, 42))
1885
- painter.drawLine(QPointF(40, 14), QPointF(40, 42))
1886
- painter.drawLine(QPointF(40, 42), QPointF(54, 42))
1893
+ """A wedge: an upright triangle extruded back and to the right, a
1894
+ triangular prism, the hidden edges left out as the brick leaves
1895
+ them."""
1896
+ # Brandon, 2026-10-22: the old sideways ramp did not read as a wedge
1897
+ _element_shape(painter, color, [(24, 24), (40, 54), (8, 54)])
1898
+ _depth(painter, color, ((24, 24), (38, 10)), ((40, 54), (54, 40)),
1899
+ ((38, 10), (54, 40)))
1887
1900
 
1888
1901
 
1889
1902
  def _draw_hex(painter, color):
1890
1903
  """A brick: a square with its back face offset."""
1891
1904
  _element_shape(painter, color, [(10, 22), (42, 22), (42, 54), (10, 54)])
1892
- painter.drawLine(QPointF(10, 22), QPointF(22, 10))
1893
- painter.drawLine(QPointF(42, 22), QPointF(54, 10))
1894
- painter.drawLine(QPointF(42, 54), QPointF(54, 42))
1895
- painter.drawLine(QPointF(22, 10), QPointF(54, 10))
1896
- painter.drawLine(QPointF(54, 10), QPointF(54, 42))
1905
+ _depth(painter, color, ((10, 22), (22, 10)), ((42, 22), (54, 10)),
1906
+ ((42, 54), (54, 42)), ((22, 10), (54, 10)), ((54, 10), (54, 42)))
1897
1907
 
1898
1908
 
1899
1909
  def _draw_pyramid(painter, color):
1900
1910
  """A pyramid: a quad base with an apex."""
1901
1911
  _element_shape(painter, color, [(10, 50), (42, 50), (54, 40), (22, 40)])
1902
- for corner in ((10, 50), (42, 50), (54, 40), (22, 40)):
1903
- painter.drawLine(QPointF(*corner), QPointF(32, 10))
1912
+ _depth(painter, color, *(((corner), (32, 10))
1913
+ for corner in ((10, 50), (42, 50), (54, 40), (22, 40))))
1904
1914
 
1905
1915
 
1906
1916
  def _draw_elements(painter, color):
@@ -7,7 +7,8 @@ the selection does while the pane is open; the pane names them. The
7
7
  element groups sensors may go on are checked here, and the surfaces
8
8
  of the model are drawn in the view, where a click on one leaves it out
9
9
  or puts it back. The numbers of tri-axial and uni-axial accelerometers
10
- are typed, and the sensors Effective Independence chooses are drawn on
10
+ are typed, and so is how far a sensor stays from a surface's edge, in
11
+ the display unit system; the sensors Effective Independence chooses are drawn on
11
12
  the model as they change, each tri-axial one as a triad and each
12
13
  uni-axial one as an arrow, with how well they tell the modes apart: the
13
14
  worst off-diagonal term of the auto-MAC at the chosen channels and the
@@ -33,7 +34,7 @@ from PySide6.QtWidgets import (
33
34
  )
34
35
 
35
36
  from ..core.instrumentation import EDGE_ANGLE
36
- from .editors import DoubleSpinBox, SpinBox, commit_on_enter, number_box
37
+ from .editors import LENGTH_DECIMALS, DoubleSpinBox, SpinBox, commit_on_enter, number_box
37
38
  from .settings_panel import panel_grid
38
39
 
39
40
  # Brandon, 2026-10-21: plan the accelerometers of a modal or vibration
@@ -52,8 +53,9 @@ class InstrumentationPanel(QWidget):
52
53
  #: the counts or the uni-axial directions changed: the window chooses
53
54
  #: the sensors again
54
55
  changed = Signal()
55
- #: the element groups or the edge angle changed: the window finds the
56
- #: places and reads the modes again, then chooses
56
+ #: the element groups, the edge angle or the distance from the edges
57
+ #: changed: the window finds the places and reads the modes again,
58
+ #: then chooses
57
59
  places_changed = Signal()
58
60
  #: every surface put back, or every one left out
59
61
  all_asked = Signal()
@@ -97,6 +99,12 @@ class InstrumentationPanel(QWidget):
97
99
  tip='Two neighboring faces are on one surface while their normals are within '
98
100
  'this angle: a curved surface is one surface however far it turns. Raise it '
99
101
  'for a coarse curved mesh that splits into strips.')
102
+ # Brandon, 2026-10-22: stay a quarter of an inch from the edges by
103
+ # default, shown in the display unit system
104
+ self.edge_distance: DoubleSpinBox = number_box(
105
+ 0.0, 1e9, value=0.0, decimals=LENGTH_DECIMALS,
106
+ tip='How near a sensor may be to the edge of its surface: a sharp edge, a free '
107
+ 'edge, a hole or where parts touch. 0 allows a sensor on the edge.')
100
108
  self.triaxial: SpinBox = SpinBox()
101
109
  self.triaxial.setRange(0, 100000)
102
110
  self.triaxial.setToolTip('How many tri-axial accelerometers: placed first')
@@ -110,21 +118,22 @@ class InstrumentationPanel(QWidget):
110
118
  self.axes.setToolTip('Which way a uni-axial accelerometer may point, in its '
111
119
  "place's own system: Z is out of the surface")
112
120
  for row, (label, widget) in enumerate(
113
- (('Surface edge', self.edge_angle), ('Tri-axial', self.triaxial),
114
- ('Uni-axial', self.uniaxial), ('Uni-axial along', self.axes)), start=7):
121
+ (('Surface edge', self.edge_angle), ('Away from edges', self.edge_distance),
122
+ ('Tri-axial', self.triaxial), ('Uni-axial', self.uniaxial),
123
+ ('Uni-axial along', self.axes)), start=7):
115
124
  grid.addWidget(QLabel(label), row, 0)
116
125
  grid.addWidget(widget, row, 1)
117
126
  self.name_label: QLabel = QLabel('Name')
118
127
  self.name: QLineEdit = QLineEdit()
119
128
  self.name.setToolTip("The start of the three names Keep adds: '... Geometry', "
120
129
  "'... Channels' and '... Shapes'")
121
- grid.addWidget(self.name_label, 11, 0)
122
- grid.addWidget(self.name, 11, 1)
130
+ grid.addWidget(self.name_label, 12, 0)
131
+ grid.addWidget(self.name, 12, 1)
123
132
  self.reading_label: QLabel = QLabel()
124
133
  self.reading_label.setWordWrap(True)
125
134
  self.reading_label.setTextInteractionFlags(
126
135
  Qt.TextInteractionFlag.TextSelectableByMouse)
127
- grid.addWidget(self.reading_label, 12, 0, 1, 2)
136
+ grid.addWidget(self.reading_label, 13, 0, 1, 2)
128
137
  buttons = QHBoxLayout()
129
138
  self.keep_button: QPushButton = QPushButton('Keep')
130
139
  self.keep_button.setToolTip('Add the experimental geometry, the channel table '
@@ -134,13 +143,14 @@ class InstrumentationPanel(QWidget):
134
143
  self.close_button.setToolTip('Put the pane away; nothing is added')
135
144
  buttons.addWidget(self.keep_button)
136
145
  buttons.addWidget(self.close_button)
137
- grid.addLayout(buttons, 13, 0, 1, 2)
138
- grid.setRowStretch(14, 1)
139
- commit_on_enter(self.triaxial, self.uniaxial, self.edge_angle)
146
+ grid.addLayout(buttons, 14, 0, 1, 2)
147
+ grid.setRowStretch(15, 1)
148
+ commit_on_enter(self.triaxial, self.uniaxial, self.edge_angle, self.edge_distance)
140
149
  self.triaxial.valueChanged.connect(lambda _value: self.changed.emit())
141
150
  self.uniaxial.valueChanged.connect(lambda _value: self.changed.emit())
142
151
  self.axes.currentIndexChanged.connect(lambda _index: self.changed.emit())
143
152
  self.edge_angle.valueChanged.connect(lambda _value: self.places_changed.emit())
153
+ self.edge_distance.valueChanged.connect(lambda _value: self.places_changed.emit())
144
154
  self.all_button.clicked.connect(self.all_asked.emit)
145
155
  self.none_button.clicked.connect(self.none_asked.emit)
146
156
  self.groups.itemChanged.connect(lambda _item: self.places_changed.emit())
@@ -190,6 +200,14 @@ class InstrumentationPanel(QWidget):
190
200
  or 0, 1 and 2."""
191
201
  return tuple(self.axes.currentData())
192
202
 
203
+ def show_edge_distance(self, value: float, unit: str) -> None:
204
+ """Show the distance from the edges, `value` in `unit` ('' for a
205
+ geometry whose units are not defined), without reporting it."""
206
+ self.edge_distance.blockSignals(True)
207
+ self.edge_distance.setSuffix(f' {unit}' if unit else '')
208
+ self.edge_distance.setValue(value)
209
+ self.edge_distance.blockSignals(False)
210
+
193
211
  def show_modes(self, text: str) -> None:
194
212
  """Name the modes measured."""
195
213
  self.modes_label.setText(text)
@@ -9114,7 +9114,11 @@ class MainWindow(QMainWindow):
9114
9114
  return True
9115
9115
 
9116
9116
  def _units_changed(self, name):
9117
+ planning = self.__dict__.get('_planning')
9118
+ distance = self._planning_distance() if planning is not None else None
9117
9119
  self.unit_system = SYSTEMS[name]
9120
+ if planning is not None:
9121
+ self._show_plan_distance(distance) # the same distance, in the new unit
9118
9122
  if self._merging is not None:
9119
9123
  self._open_merge_panel() # its tolerance, in the new unit
9120
9124
  self.render_current()
@@ -14295,6 +14299,7 @@ class MainWindow(QMainWindow):
14295
14299
  triaxial = -(-len(modes) // 3) + 1
14296
14300
  panel = self.scene.instrumentation_panel
14297
14301
  panel.open_for(f'{name}, on {geometry_name}', groups, triaxial, 0, f'{name} Plan')
14302
+ self._show_plan_distance(self._plan_default_distance())
14298
14303
  panel.show()
14299
14304
  self._begin_plan_clicks()
14300
14305
  self._plan_now()
@@ -14319,7 +14324,7 @@ class MainWindow(QMainWindow):
14319
14324
 
14320
14325
  def _planning_skin(self):
14321
14326
  """The model's surfaces at the pane's edge angle, found once."""
14322
- from ..core.instrumentation import skin
14327
+ from ..core.instrumentation import default_surfaces, skin
14323
14328
 
14324
14329
  planning = self._planning
14325
14330
  angle = float(self.scene.instrumentation_panel.edge_angle.value())
@@ -14330,12 +14335,41 @@ class MainWindow(QMainWindow):
14330
14335
  finally:
14331
14336
  QApplication.restoreOverrideCursor()
14332
14337
  # a surface's number is its place in this angle's split: the
14333
- # ones left out at another angle mean nothing here
14334
- if planning['skins']:
14335
- planning['off'] = set()
14338
+ # ones left out at another angle mean nothing here, and each
14339
+ # split starts from the default, the small surfaces out
14340
+ planning['off'] = {s.id for s in found} - set(default_surfaces(found))
14336
14341
  planning['skins'] = {angle: found}
14337
14342
  return angle, planning['skins'][angle]
14338
14343
 
14344
+ def _plan_default_distance(self) -> float:
14345
+ """The distance sensors keep from the edges unless told: a
14346
+ quarter of an inch, or none on a geometry whose units are not
14347
+ defined (`core.instrumentation.plan`)."""
14348
+ from ..core.instrumentation import EDGE_DISTANCE
14349
+
14350
+ geometry = self.objects[self._planning['geometry']]
14351
+ return EDGE_DISTANCE if geometry.units_defined else 0.0
14352
+
14353
+ def _planning_distance(self) -> float:
14354
+ """The distance sensors keep from the surfaces' edges, in the
14355
+ geometry's coordinates: the pane's box, typed in the display unit
14356
+ system once the geometry's units are defined."""
14357
+ geometry = self.objects[self._planning['geometry']]
14358
+ typed = float(self.scene.instrumentation_panel.edge_distance.value())
14359
+ return (float(self.unit_system.to_si(typed, 'length')) if geometry.units_defined
14360
+ else typed)
14361
+
14362
+ def _show_plan_distance(self, distance: float) -> None:
14363
+ """Put `distance`, in the geometry's coordinates, in the pane's
14364
+ box in the display unit system."""
14365
+ geometry = self.objects[self._planning['geometry']]
14366
+ panel = self.scene.instrumentation_panel
14367
+ if geometry.units_defined:
14368
+ panel.show_edge_distance(float(self.unit_system.from_si(distance, 'length')),
14369
+ self.unit_system.label_text('length'))
14370
+ else:
14371
+ panel.show_edge_distance(distance, '')
14372
+
14339
14373
  def _planning_kept(self, found, groups) -> list[int]:
14340
14374
  """The surfaces sensors may go on: in a checked group and not
14341
14375
  clicked out."""
@@ -14353,10 +14387,11 @@ class MainWindow(QMainWindow):
14353
14387
  planning = self._planning
14354
14388
  angle, found = self._planning_skin()
14355
14389
  kept = self._planning_kept(found, groups)
14356
- key = (angle, tuple(groups), tuple(kept))
14390
+ distance = self._planning_distance()
14391
+ key = (angle, tuple(groups), tuple(kept), distance)
14357
14392
  if planning['places'] is None or planning['places'][0] != key:
14358
14393
  places = candidates(self.objects[planning['geometry']], groups, kept,
14359
- edge_angle=angle, found=found)
14394
+ edge_angle=angle, found=found, edge_distance=distance)
14360
14395
  planning['places'] = (key, places)
14361
14396
  planning['blocks'] = None
14362
14397
  places = planning['places'][1]
@@ -14558,16 +14593,29 @@ class MainWindow(QMainWindow):
14558
14593
  def _begin_plan_clicks(self) -> None:
14559
14594
  """Watch the view's clicks: a press and release in one place on a
14560
14595
  surface leaves it out or puts it back. The press goes on to the
14561
- camera, so a drag turns the view and toggles nothing."""
14596
+ camera, so a drag turns the view and toggles nothing.
14597
+
14598
+ The release is heard from the camera's style, as the end of its
14599
+ interaction, not from the interactor: the style takes the mouse
14600
+ to itself from the press (VTK's GrabFocus), so the interactor's
14601
+ observers hear neither the drag nor the release. Listening there,
14602
+ no click ever toggled a surface, and the press left behind ended
14603
+ the hover's light for good (Brandon, 2026-10-22)."""
14562
14604
  interactor = getattr(self.scene.plotter, 'iren', None)
14563
14605
  if interactor is None or self.__dict__.get('_plan_observers'):
14564
14606
  return
14565
14607
  raw = interactor.interactor
14566
14608
  self._plan_observers = [
14567
- raw.AddObserver('LeftButtonPressEvent', self._on_plan_press, 10.0),
14568
- raw.AddObserver('LeftButtonReleaseEvent', self._on_plan_release, 10.0),
14569
- raw.AddObserver('MouseMoveEvent', self._on_plan_move, 10.0),
14609
+ (raw, raw.AddObserver('LeftButtonPressEvent', self._on_plan_press, 10.0)),
14610
+ (raw, raw.AddObserver('MouseMoveEvent', self._on_plan_move, 10.0)),
14570
14611
  ]
14612
+ style = raw.GetInteractorStyle()
14613
+ if style is not None:
14614
+ self._plan_observers.append(
14615
+ (style, style.AddObserver('EndInteractionEvent', self._on_plan_release, 10.0)))
14616
+ else:
14617
+ self._plan_observers.append(
14618
+ (raw, raw.AddObserver('LeftButtonReleaseEvent', self._on_plan_release, 10.0)))
14571
14619
 
14572
14620
  def _on_plan_press(self, _caller, _event) -> None:
14573
14621
  planning = self.__dict__.get('_planning')
@@ -14601,7 +14649,12 @@ class MainWindow(QMainWindow):
14601
14649
  """Keep: the plan added to the project as the pane says it
14602
14650
  (`Project.plan_instrumentation`), its three objects in the tree,
14603
14651
  and the pane put away."""
14604
- from ..core.instrumentation import EDGE_ANGLE, FewerDirectionsWarning, elastic
14652
+ from ..core.instrumentation import (
14653
+ EDGE_ANGLE,
14654
+ FewerDirectionsWarning,
14655
+ default_surfaces,
14656
+ elastic,
14657
+ )
14605
14658
 
14606
14659
  planning = self.__dict__.get('_planning')
14607
14660
  if planning is None:
@@ -14621,10 +14674,17 @@ class MainWindow(QMainWindow):
14621
14674
  options['modes'] = modes
14622
14675
  if not panel.all_groups:
14623
14676
  options['groups'] = groups
14624
- if len(kept) < len([s for s in found if s.group in set(groups)]):
14677
+ # journaled only when the person changed them from the default
14678
+ offered = {s.id for s in found if s.group in set(groups)}
14679
+ if set(kept) != set(default_surfaces(found)) & offered:
14625
14680
  options['surfaces'] = kept
14626
14681
  if angle != EDGE_ANGLE:
14627
14682
  options['edge_angle'] = angle
14683
+ distance = self._planning_distance()
14684
+ if not np.isclose(distance, self._plan_default_distance(), rtol=1e-4, atol=0.0):
14685
+ # in the geometry's own coordinates, which are meters once
14686
+ # its units are defined: the default unit
14687
+ options['edge_distance'] = distance
14628
14688
  if panel.uniaxial_axes != (2,):
14629
14689
  options['axes'] = list(panel.uniaxial_axes)
14630
14690
  typed = panel.name.text().strip()
@@ -14656,11 +14716,9 @@ class MainWindow(QMainWindow):
14656
14716
  """Put Plan Instrumentation away; nothing is added."""
14657
14717
  self.scene.instrumentation_panel.hide()
14658
14718
  self._plan_timer.stop()
14659
- interactor = getattr(self.scene.plotter, 'iren', None)
14660
- for observer in self.__dict__.get('_plan_observers') or []:
14661
- if interactor is not None:
14662
- with contextlib.suppress(Exception):
14663
- interactor.interactor.RemoveObserver(observer)
14719
+ for watched, observer in self.__dict__.get('_plan_observers') or []:
14720
+ with contextlib.suppress(Exception):
14721
+ watched.RemoveObserver(observer)
14664
14722
  self._plan_observers = []
14665
14723
  if self.__dict__.get('_planning') is None:
14666
14724
  return