Lightweight SysML v2 graphical viewer/editor in Rust with egui. Renders the graphical notation prescribed by OMG SysML v2 (formal/25-09-03). Runs as a native desktop app (OpenBSD, Windows) and as a web app (WASM/WebGL) from the same codebase. Coordinates with external tools rather than reimplementing their functionality.
cargo build # debug build
cargo build --release # release build
cargo run # run debug
cargo clippy # lint
cargo test # run testsrustup target add wasm32-unknown-unknown # one-time setup
cargo install trunk # one-time setup
trunk serve # dev server with hot reload
trunk build --release # production build to dist/docker build -t sysmlv2-gui .
docker run -p 8080:8080 -v /path/to/models:/models sysmlv2-guiThis project targets three deployment modes. All share the same Rust rendering code.
- eframe with glow backend (OpenGL ES 2.0+ / OpenGL 3.2+)
- Direct filesystem access, CLI validator, file watching
- Primary development and testing platform: OpenBSD
- Same eframe/glow code compiles to WASM; glow maps to WebGL
- Two use cases:
- Demos on decisym.ai: prospects interact without installing
- Enterprise/DoD deployment: containerized web service for managed assets that prohibit software installation
- File access and validation via backend API (not local filesystem)
- Switch from glow to wgpu via Cargo feature flag for GPU-intensive views (large graphs, force-directed layout)
- wgpu targets DX12 (Windows), Vulkan (Linux), WebGPU (browsers)
- OpenBSD stays on glow (no Vulkan)
- Application code does not change; only the eframe backend feature
src/
main.rs -- entry point, eframe setup (native + web)
app.rs -- top-level App implementing eframe::App
model/ -- SysML v2 model representation
mod.rs -- element types, relationships, containment
parse.rs -- .sysml file parser (structural, not semantic)
render/ -- graphical notation rendering
mod.rs -- view rendering dispatch
elements.rs -- definition/usage rectangles, compartments
connectors.rs -- relationship lines, arrowheads, decorations
ports.rs -- port symbols on element boundaries
layout.rs -- automatic element positioning
frame.rs -- diagram frame with view type tab
panels/ -- UI panels
browser.rs -- model element tree (egui_ltreeview)
canvas.rs -- diagram canvas (egui Painter)
properties.rs -- element property editor
validation.rs -- validation output display
source.rs -- textual notation viewer
platform/ -- platform abstraction
mod.rs -- trait definitions for file I/O and validation
native.rs -- filesystem + CLI validator (cfg not wasm)
web.rs -- HTTP API client (cfg wasm)
validator.rs -- interface to validate-sysml CLI (native)
watcher.rs -- file system watching for live reload (native)
File I/O and validation differ between native and web. Abstract
behind traits and use #[cfg(target_arch = "wasm32")] to select
the implementation:
pub trait ModelSource {
fn load(&self, path: &str) -> Result<String>;
fn watch(&self, path: &str, callback: impl Fn());
}
pub trait Validator {
fn validate(&self, content: &str) -> Vec<Diagnostic>;
}- Native (
platform/native.rs): reads files from disk, callsvalidate-sysmlas a subprocess, usesnotifyfor file watching. - Web (
platform/web.rs): fetches model text from a backend API, posts content for server-side validation, no file watching (uses polling or WebSocket for updates).
-
Definitions get sharp corners, usages get rounded corners. This is the fundamental visual rule of SysML v2. Never mix them up.
-
The model representation is lightweight. It captures structure (elements, features, relationships, containment) sufficient for rendering. It does NOT implement the full KerML metamodel. Semantic analysis is delegated to the validator.
-
Parsing is structural, not semantic. The parser extracts element kinds, names, types, features, and nesting from
.sysmltext. It does not resolve references, check types, or validate semantics. -
Validation is external. On native, call
validate-sysmlas a subprocess. On web, call the backend validation API. Parse GNU-format output (file:line:col: severity: message). Never reimplement validation logic. -
Rendering follows the specification. Consult the GBNF in subclause 8.2.3 of formal/25-09-03 and the graphical notation intro PDF for rendering rules. When in doubt, check what SysON does.
-
Same rendering code for all platforms. The
render/module must not contain platform-specific code. All platform differences live inplatform/.
Compliance with the OMG SysML v2 graphical notation specification (formal/25-09-03) is an invariant of this project. No change may violate the specification. Specifically:
- Corner radii:
DEFINITION_CORNER_RADIUS(0.0) for definitions and packages;USAGE_CORNER_RADIUS(6.0) for usages. This applies at every LOD level and in every rendering path (full, compact, label, dot, overview). - Named constants only: every numeric value in
src/render/that controls drawing (stroke widths, font scales, spacing, sizes) must reference a named constant intheme.rs. No bare magic numbers in rendering code. - All stroke widths named: every
Stroke::new()call in rendering code must use atheme::constant for its width argument. - Keyword format: keywords are lowercase text rendered inside
<<>>guillemets. - Port geometry: ports are
PORT_SIZExPORT_SIZEsquares withCornerRadius::ZERO, centered on the parent element boundary. - Frame geometry: frames use
CornerRadius::ZEROwith the tab anchored at the top-left corner. - Connector styles: flow connectors have filled arrowheads; bind and allocate connectors are dashed; each relationship kind has a distinct color.
These rules are enforced by tests/spec_conformance.rs. That test
suite must pass before any change is merged. When adding new rendering
code, add corresponding spec conformance tests.
- Edition 2024. Use current idioms.
cargo clippymust pass with no warnings.- Prefer
&stroverStringin function signatures where possible. - Use
thiserrorfor error types, not string errors. - No
unwrap()in library code.unwrap()is acceptable only in tests and inmain()for fatal setup errors. - Keep functions short. Extract when a function exceeds ~40 lines.
- Minimize
pubsurface. Default to private; expose only what panels and renderer need.
- Use
#[cfg(not(target_arch = "wasm32"))]for native-only code. - Use
#[cfg(target_arch = "wasm32")]for web-only code. - Never use
std::fs,std::process,std::net, ornotifyin code that compiles for WASM. These do not exist onwasm32. - The
model/,render/, andpanels/modules must be platform-agnostic. Onlyplatform/,validator.rs, andwatcher.rsmay usecfggates.
- Use the glow backend. Feature flags:
eframe = { default-features = false, features = ["glow"] } - For WASM builds, eframe automatically uses WebGL via glow.
- Use
egui::Painterfor all diagram rendering. Do not drop to raw OpenGL/WebGL unless absolutely necessary. - Panels use
egui::SidePanel,egui::TopBottomPanel,egui::CentralPanel. Useegui_tilesfor user-rearrangeable docking. - Immediate mode: compute layout and draw every frame. Cache expensive computations (parsing, layout) but not draw calls.
- Colors and sizes should be defined as constants in a theme module, not hardcoded in rendering code.
These rendering rules come from the specification:
// Corner radius for usage elements (definitions use radius 0.0)
const USAGE_CORNER_RADIUS: f32 = 6.0;
// Port size (small square straddling element boundary)
const PORT_SIZE: f32 = 12.0;
// Compartment separator line
const COMPARTMENT_LINE_WIDTH: f32 = 1.0;
// Connector line widths
const CONNECTOR_LINE_WIDTH: f32 = 1.5;
const CONNECTOR_DASH_LENGTH: f32 = 6.0;
const CONNECTOR_DASH_GAP: f32 = 4.0;Seven principles govern connector routing. All routing distances
reference ROUTE_* constants in theme.rs. Zero magic numbers in
layout.rs.
-
Frame encompasses all content. The bounding box includes elements, ports, AND connector waypoints.
Layout::content_bounds()is the single source of truth for frame sizing. -
Dedicated routing corridors. Explicit space between the frame edge and the element field is reserved for connector detours. Elements start at
FRAME_PADDING + ROUTE_FRAME_MARGINvertically andFRAME_PADDING + ROUTE_FRAME_MARGIN + ROUTE_COLUMN_GAP_MARGINhorizontally. -
Minimum clearances. Routes maintain
ROUTE_ELEMENT_CLEARANCE(20px) from element edges andROUTE_FRAME_MARGIN(30px) from the frame edge. Based on professional tool standards (yEd, draw.io, Visio). -
Named constants only. Every routing distance references a
ROUTE_*constant. The constants and their values:ROUTE_STUB_LENGTH = 20.0 // port exit before first bend ROUTE_ELEMENT_CLEARANCE = 20.0 // min gap from route to element ROUTE_PARALLEL_SPACING = 8.0 // min y-separation for overlapping h-segments ROUTE_FRAME_MARGIN = 30.0 // min distance from route to frame edge ROUTE_CHANNEL_SPREAD = 8.0 // x-offset between parallel vertical channels ROUTE_COLUMN_MERGE = 10.0 // threshold for merging adjacent columns ROUTE_COLUMN_GAP_MARGIN = 25.0 // margin outside first/last column ROUTE_EDGE_TOLERANCE = 2.0 // threshold for matching point to edge ROUTE_BOUNDARY_MARGIN = 1.0 // inset for crossing checks
-
Parallel deconfliction. Horizontal segments from different connectors that overlap in x are offset by
ROUTE_PARALLEL_SPACING(8px, derived from PCB 3W rule: >=3x the 1.5px connector line width plus margin). Implemented viadeconflict_y()which tracksused_h_levelsacross all routed connectors. -
Drawing order. Connectors drawn BEFORE elements. Element fill rectangles visually occlude lines that pass through element areas. This is industry standard in yEd, Visio, and draw.io.
-
Visual hierarchy. Connector stroke (1.5px, colored per relationship type) is distinct from frame stroke (2.0px, dark blue-gray). Never confusable.
validate-sysml path/to/file.sysml- Expects
validate-sysmlon PATH or at~/.local/share/sysmlv2-validator/validate-sysml - Requires Java 21+
- Output: GNU format errors to stderr
- Exit code: 0 = valid, 1 = errors
The containerized deployment runs validate-sysml server-side. The
WASM client posts model content to a REST endpoint and receives
diagnostics as JSON. The API contract:
POST /api/validate
Content-Type: text/plain
Body: <.sysml file content>
Response: 200
Content-Type: application/json
Body: [{"file": "...", "line": 1, "col": 1, "severity": "error", "message": "..."}]
Watch .sysml files for changes from external editors (Emacs
sysml-mode). On change: re-parse, optionally re-validate, update
display. Use notify crate. Not available in WASM builds.
When working on this project with AI assistance, the sysmlv2-skill
provides authoritative syntax guidance. Key rules:
- Imports require visibility:
private import ScalarValues::*; - Attribute defs must specialize:
attribute def Region :> String; - Import ScalarValues for basic types (String, Real, Integer, Boolean)
- See
../sysmlv2-skill/references/SYNTAX.mdfor complete rules
- Unit tests: model parsing, rendering geometry calculations, layout algorithms
- Integration tests: parse
.sysmlfiles from the SysML v2 Release repository's examples and verify structural extraction - Visual tests: screenshot comparison (later milestone)
- WASM tests: verify the build compiles and runs under
wasm32-unknown-unknownin CI - Test
.sysmlfiles go intests/fixtures/
Keep dependencies minimal. Justified additions:
| Crate | Purpose | Platforms | Required |
|---|---|---|---|
eframe |
egui native + web app framework (glow backend) | All | Yes |
egui |
Immediate mode GUI | All | Yes |
egui_extras |
Additional egui widgets | All | Yes |
egui_tiles |
Panel docking/tiling | All | Yes |
notify |
File system watching | Native only | Yes |
thiserror |
Error type derivation | All | Yes |
reqwest |
HTTP client for backend API | WASM only | Yes (web) |
serde / serde_json |
Serialization for API responses | All | Yes |
Add dependencies only when they provide clear value over a simple
implementation. Prefer stdlib solutions. Mark native-only deps with
[target.'cfg(not(target_arch = "wasm32"))'.dependencies].