Setup symbols: wireframe glyphs per held degree of freedom, a constant size on screen

Boundary condition, load and interaction symbols are wireframes (thin lines,
no shaded cones), so they never hide the mesh or a contour plot:

- a crossed wire triangle per constrained translation, tip on the node,
  coming from outside the part where the set's faces say which side that is;
  a held rotation adds a smaller triangle behind it (the double mark); fixed
  shows all three axes, a symmetry plane only its normal;
- forces as open-headed arrows ending on the node, as long as four support
  units at one place and 2.5 over a set, so loads and supports stay in
  proportion (#78); moments as an axis line and a right-handed curved arrow
  (#79); contact and tie surfaces as small squares on their faces;
- a constant size on screen at any zoom (#80): the symbol unit is the size
  setting (View > Symbol size, `ui.set {symbol_size}`, 14 pt by default)
  times the model length a pixel spans, rounded to a quarter of a doubling;
- spread evenly on a grid a few symbol units across, so their density follows
  the screen, never more than 400 per definition;
- a layer of their own on the desktop (GPU and CPU): zooming rebuilds and
  uploads the symbols only, never the mesh's scene; framing leaves them out;
- colour per kind from the theme's tokens (a new contact colour), and the
  definition selected in the browser drawn in the selection colour, wider;
- depth-tested against the model with the line bias that keeps them on their
  own surface; a load at a reference point or a single node and a reference
  point's cross are drawn over the part (#78).

Headless pictures (MCP screenshot, `ui.render`) frame the camera first and
draw the symbols at the default size for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 23:14:40 -04:00
1 parent 1fcc7acf84
commit 52c12e2ad7
13 files changed
+1221 -436

No files matched your search

+20 -4
View File
@@ -80,6 +80,8 @@ pub struct Tokens {
pub glyph_load: Color32,
/// Reference points and couplings.
pub glyph_interaction: Color32,
/// Contact and tie surface symbols.
pub glyph_contact: Color32,
/// Temperatures, heat fluxes, convection and heat sources.
pub glyph_thermal: Color32,
/// Datum planes and axes.
@@ -160,6 +162,7 @@ impl Tokens {
glyph_constraint: Color32::from_rgb(0x00, 0x8a, 0xa8),
glyph_load: Color32::from_rgb(0xd0, 0x24, 0x24),
glyph_interaction: Color32::from_rgb(0x9a, 0x30, 0xc0),
glyph_contact: Color32::from_rgb(0x6a, 0x8a, 0x00),
glyph_thermal: Color32::from_rgb(0xe8, 0x4c, 0x00),
glyph_datum: Color32::from_rgb(0x1e, 0x8c, 0x6e),
pick_selected: Color32::from_rgb(0xd6, 0x00, 0x7f),
@@ -223,6 +226,7 @@ impl Tokens {
t.glyph_constraint = Color32::from_rgb(0x3c, 0xdc, 0xf0);
t.glyph_load = Color32::from_rgb(0xff, 0x5a, 0x4c);
t.glyph_interaction = Color32::from_rgb(0xd2, 0x82, 0xff);
t.glyph_contact = Color32::from_rgb(0xa0, 0xe6, 0x28);
t.glyph_thermal = Color32::from_rgb(0xff, 0x8c, 0x00);
t.glyph_datum = Color32::from_rgb(0x5a, 0xd8, 0xaa);
t.pick_selected = Color32::from_rgb(0xff, 0x3f, 0xb4);
@@ -235,12 +239,15 @@ impl Tokens {
self.contour.map(rgb)
}
/// The colours of the analysis setup's symbols; their names in the overlay text colour.
/// The colours of the analysis setup's symbols; their names in the overlay text colour, the
/// selected definition's in the selection colour.
pub fn glyph_colors(&self) -> GlyphColors {
GlyphColors {
constraint: rgb(self.glyph_constraint),
load: rgb(self.glyph_load),
interaction: rgb(self.glyph_interaction),
contact: rgb(self.glyph_contact),
highlight: rgb(self.pick_selected),
thermal: rgb(self.glyph_thermal),
label: rgb(self.overlay_text),
datum: rgb(self.glyph_datum),
@@ -368,7 +375,7 @@ mod tests {
};
for dark in [true, false] {
let t = Tokens::get(dark);
let symbols = [t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_thermal];
let symbols = [t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_contact, t.glyph_thermal];
for (i, s) in symbols.iter().enumerate() {
assert!((luma(*s) - luma(t.viewport_bottom)).abs() > 400 || far(*s, t.viewport_bottom) > 250, "{s:?} dark={dark}");
for k in ElementKind::ALL {
@@ -392,8 +399,17 @@ mod tests {
let t = Tokens::get(dark);
assert!(far(t.pick_selected, t.pick_hover) > 90, "dark={dark}");
for c in [t.pick_selected, t.pick_hover] {
let others =
[t.geometry, t.node, t.glyph_constraint, t.glyph_load, t.glyph_interaction, t.glyph_thermal, t.viewport_bottom, t.mesh_edge];
let others = [
t.geometry,
t.node,
t.glyph_constraint,
t.glyph_load,
t.glyph_interaction,
t.glyph_contact,
t.glyph_thermal,
t.viewport_bottom,
t.mesh_edge,
];
for o in others.into_iter().chain(ElementKind::ALL.map(|k| t.element(k))) {
assert!(far(c, o) > 100, "{c:?} vs {o:?} dark={dark}");
}