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.
- {visualdynamics-0.1.0a45/src/visualdynamics.egg-info → visualdynamics-0.1.0a46}/PKG-INFO +1 -1
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/__init__.py +1 -1
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/geometry.py +387 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/mesh.py +57 -100
- visualdynamics-0.1.0a46/src/visualdynamics/core/solids.py +1487 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/__init__.py +1 -1
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/barc.py +69 -65
- visualdynamics-0.1.0a46/src/visualdynamics/demo/frame_wing.py +378 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/icons.py +147 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/main_window.py +1046 -22
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/merge_panel.py +32 -5
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/mesh_panel.py +97 -18
- visualdynamics-0.1.0a46/src/visualdynamics/gui/mesh_solids_panel.py +97 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/panes.py +8 -0
- visualdynamics-0.1.0a46/src/visualdynamics/gui/pattern_panel.py +114 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/native.py +15 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/step.py +5 -63
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/project.py +388 -6
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/report/__init__.py +50 -7
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/report/page.py +4 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/theme.py +2 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/geometry.py +3 -38
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46/src/visualdynamics.egg-info}/PKG-INFO +1 -1
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/SOURCES.txt +4 -1
- visualdynamics-0.1.0a45/src/visualdynamics/demo/frame.py +0 -374
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/LICENSE +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/README.md +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/pyproject.toml +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/setup.cfg +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/__main__.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/compatibility.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/__init__.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/author.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/averaging.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/channel_table.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/compliance.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/correlate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/data.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/detect.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/entities.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/fem.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/filters.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/kurtosis.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/matches.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/merge.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/modal_fit.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/octave.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/photos.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/progress.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/replication.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/report.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/rigid.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/shapes.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/shocks.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/sine.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/sine_tracking.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/snr.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/srs.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/tables.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/transform.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/truncate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/unit_choices.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/validate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/core/wavelet.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/decimate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/deform.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/drone.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/demo/plate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/__init__.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/app_icon.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/ask.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/author_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/averaging_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/console.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/disclaimer.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/drawer_tab.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/editors.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/filter_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/glyphs.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/movie.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/object_tables.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/octave_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/preferences.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/project_tree.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/record_grid.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/report_editor.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/rigid_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/run_reports.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/settings_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/shock_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/sine_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/stage_drag.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/stream_window.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/tables.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/toolbars.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/truncate_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/updater.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/wavelet_panel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/gui/windows.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/__init__.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/adf.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_objects.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/excel.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/exodus.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/exporters.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/femap.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/matlab.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/nastran.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/notes.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/photo_files.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/punch.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/rattlesnake.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/report_template.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sdynpy_data.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sdynpy_npz.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sep005.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/sniffing.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/stl.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/threemf.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/io/unv.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/names.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/__init__.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/averaging.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/bands.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/bars.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/filtering.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/scalogram.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/shocks.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/sine_tracking.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/tracking_filter.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/plot/truncation.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/py.typed +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/report/markdown.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/rotate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/units.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/update.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/__init__.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/_quiet.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/animate.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/banded.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/mac_bars.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/marks.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/paired.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/pick.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/rigid.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/scalogram.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/sinespec.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics/viz/waterfall.py +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/entry_points.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/requires.txt +0 -0
- {visualdynamics-0.1.0a45 → visualdynamics-0.1.0a46}/src/visualdynamics.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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
|
|
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.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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=
|
|
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],
|