Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UMLOne

A UML class diagram editor that runs in a web browser. Load, view, edit, and save class diagrams, with a semantic model (classes, associations) kept separate from the diagram that shows it (positions, sizes, bend points).

UMLOne was generated by AI Sloperator with Claude Code. No license is given.

Running it

Live: https://www.aisloperator.com/umlone/ — nothing to install.

Or clone the repository and double-click index.html. That is the whole procedure: it runs the same either way, because there is nothing to serve.

There is no build step, no package manager, no dev server, and nothing to download. The editor is plain JavaScript and SVG loaded with classic <script> tags, which is deliberate: ES modules are blocked by CORS over file://, so using them would have meant running a local server just to open the editor — and the hosted copy is the same files, not a build of them.

To try it out, press Open and pick samples/example.uml.json, then press Fit.

Tests

Open tests.html, or the local copy. It runs the whole suite in that browser and prints a pass/fail list. Everything runs against the real DOM, which is the only place text measurement and SVG geometry mean anything.

Saving and opening

  • Open, or drag a .uml.json file onto the canvas.
  • Save writes the document to your Downloads folder.
  • SVG writes the diagram you are looking at as a standalone image.
  • Print puts that same image on paper, with the diagram's name above it.

The export is one diagram, not the document — what is on screen, cropped to the drawing with a small margin, with nothing that only exists to be clicked and no dependency on styles.css. Every colour, font, and stroke width is written onto the elements themselves, because a file that needs a stylesheet beside it is not an exported file. It is not a save: an SVG cannot be opened back into the editor, so the document is still unsaved afterwards.

Printing uses the same builder, so what the printer produces is what the file would have contained. Ctrl/Cmd+P and the browser's own print menu print the diagram too, not a screenshot of the editor: the print stylesheet hides the chrome, and the page is filled on beforeprint rather than only by the button, since a hidden editor plus an empty print area is a blank sheet.

A browser cannot write back to the file you opened, so there is no in-place save: Save downloads a fresh copy every time, and the status line says Downloaded rather than Saved so it is clear where the file went. The title bar shows while there are unsaved changes, and closing the tab with unsaved work prompts first.

Files are validated on load. Structural problems (a dangling relationship endpoint, a duplicate id, a version from a newer editor) refuse the load and say why. Recoverable problems (an unparseable multiplicity, an owner that is not a package, a diagram node for an element that no longer exists) are repaired, and the status line tells you how many repairs happened, with details in the browser console.

Controls

Action How
Select Click; Shift-click to add; drag on empty space to marquee
Move Drag a box. Dragging a package moves its contents with it
Resize Drag a handle on a selected box
Create element Pick a palette tool, then click the canvas
Create relationship Pick a relationship tool, click the source, then the target
Add a bend to a line Double-click the line
Remove a bend Double-click the bend handle
Edit text on the diagram Double-click a name, attribute, operation, literal, or note
Precise placement Hold Alt while dragging to bypass the 5px grid
Nudge Arrow keys; Shift+Arrow for a larger step
Pan Middle-drag, or Ctrl/Cmd-drag, or scroll
Zoom Ctrl/Cmd + scroll, or the toolbar buttons
Back to 100% The 1:1 button, or Ctrl/Cmd+0
Show the whole diagram The Fit button. It shrinks to fit and stops at 100% — it never magnifies
Arrange the diagram The Layout button. One undo step
Print this diagram Ctrl/Cmd+P, or the toolbar button
Undo / redo Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z
Select all Ctrl/Cmd+A
Delete from the model Delete
Remove from this diagram only Shift+Delete
Cancel the current tool Escape

Delete and Shift+Delete differ, and the difference matters. Delete removes the element from the model, so it disappears from every diagram along with its relationships. Shift+Delete removes only the view — the element stays in the model and on other diagrams, and you can put it back from the inspector's Not on this diagram list.

Editing text on the diagram

Double-click any line of text in a box to edit it where it sits — a single click selects the box and starts a drag, as it does anywhere else on it. Placing a new element opens the field immediately, so creating a class and naming it is one gesture.

Key Effect
Enter, on a member row with text Commit and open a new row below
Enter, on a row that is empty Finish — the blank row is dropped
Enter, on a name or note Commit and finish
Shift+Enter Commit and finish without adding a row
Escape Discard the edit
↑ / ↓, Tab / Shift+Tab Commit and move between rows
Backspace in an empty row Delete the row and move up

So a whole attribute list is typed straight down: type, Enter, type, Enter, and one last Enter on the empty row to stop. That final Enter tidies up after itself, because committing an empty row deletes it — which is also how a member is removed, along with Backspace in an already-empty row.

Arrow keys cross compartment boundaries, so ↓ walks from the class name through every attribute and on into the operations without touching the mouse.

Double-clicking the gap just above or below a compartment's members inserts one there rather than editing an existing row — before the first, after the last, or the first one in an empty compartment. Those gaps are only a few pixels tall, so they tint on hover, and the one below the last member borrows a little of the empty space beneath it. That borrow is capped at a single row: a strip covering all the slack in a tall box would light up on hover and read as a member that is already there.

A box grows when a new row would not fit. Since building a list means inserting a blank row on every Enter, and the last of those is thrown away when the list ends, the box hands that height back when editing stops — down to what the contents need, but never below the height the box had when editing began. A size set by dragging stays yours.

Members are edited as the exact string the diagram draws:

- /total: Money [0..*] = 0
+ move(dx: int, out ok: bool): Point

which is read back by model.parseAttribute / parseOperation. The parsers never fail — text that matches nothing becomes the member's name, because losing what someone typed is worse than storing something odd. Two properties they guarantee:

  • Round-trip. format(parse(format(x))) equals format(x), so opening a field and pressing Enter changes nothing.
  • Static and abstract survive. Those render as underline and italic, which the one edited line cannot express, so they are carried over from the member being replaced rather than read from the text. The same goes for the member's id, so editing a row modifies it rather than replacing it.

Notes are the exception to the Enter rule: their text is genuinely multi-line, so Shift+Enter inserts a line break there and the arrow keys move the caret.

How it works

SVG <text> cannot be typed into. Rather than a <foreignObject>, the editor positions a real HTML <input> over the line being edited, using the same rectangle the renderer used to draw it (render.lineBoxes). Caret, selection, double-click-to-select-word, clipboard, and IME are then simply the browser's, none of which would be free inside SVG. The line underneath is skipped while its field is open, so the text never shows through.

Double-clicks are recognised in interact.js at pointerdown rather than left to the browser's dblclick event. Drawing is a full rebuild — every render calls ns.clear(svg) — and selecting a node redraws, as does releasing the pointer, so across a double-click the element under the cursor is destroyed and replaced four times. dblclick only fires when both clicks share a target, and Firefox holds to that strictly, so the event never arrived at all. Reading the target during pointerdown, before anything can redraw, does not depend on the browser's click bookkeeping surviving a rebuild.

Automatic layout

Layout rearranges the diagram you are looking at. It is one undo step, and pressing it twice in a row does nothing the second time.

The arrangement is layered, in the Sugiyama shape. Class diagrams have one strong convention — a supertype is drawn above its subtypes — and generalization and realization are the only relationships that carry it, so those two decide the rows. Everything else (associations, aggregation, composition, dependencies, note links) only pulls related boxes towards each other within a row. Treating every relationship as a hierarchy would tip associations on their side and draw inheritance where there is none.

  • A box goes below its deepest supertype, so a chain never doubles back.
  • Boxes that inheritance says nothing about sit beside whatever they are related to; a group joined only by associations spreads over rows from its busiest member, rather than forming one long line.
  • Order within a row comes from barycentre sweeps — the standard heuristic for reducing crossings, since the exact problem is NP-hard.
  • Packages nest. A package's contents are arranged on their own, the package is sized to hold the result, and it then takes part in the layout around it as a single box. An edge that crosses a package boundary pulls on the package.
  • Parts of a diagram that are not connected to each other are packed side by side, biggest first, wrapping into rows.
  • Bends are dropped: they were placed against the old positions.

What it is not: optimal. There is no orthogonal routing, no overlap avoidance for edges, and no attempt to preserve the shape you already had — it is a fresh arrangement, which is why it is a button rather than something that happens as you edit.

File format

One file is one document: a single semantic model plus any number of diagrams over it. The format is bespoke JSON rather than XMI. XMI is the actual standard, but it is verbose and version-fragmented, tools disagree on it in practice, and its layout half (Diagram Interchange) is the least-supported part of the ecosystem — while layout is a first-class requirement here. A converter can be added later without changing how the editor works internally.

Files are written in a canonical form: fixed key order, two-space indent, unknown fields dropped. Loading a file and immediately downloading it produces identical bytes, so these files diff and merge sensibly in version control.

{
  "format": "umlone",
  "version": 1,
  "model": {
    "elements": [
      { "id": "c1", "kind": "class", "name": "Order",
        "isAbstract": false, "stereotypes": ["entity"],
        "attributes": [
          { "id": "a1", "name": "total", "type": "Money",
            "visibility": "private", "multiplicity": "", "defaultValue": "",
            "isStatic": false, "isDerived": true }
        ],
        "operations": [
          { "id": "o1", "name": "recalculate", "returnType": "void",
            "visibility": "public", "isStatic": false, "isAbstract": false,
            "parameters": [
              { "name": "policy", "type": "TaxPolicy", "direction": "in" }
            ] }
        ],
        "owner": "p1" }
    ],
    "relationships": [
      { "id": "r1", "kind": "composition", "name": "",
        "ends": [
          { "ref": "c1", "role": "order", "multiplicity": "1",
            "visibility": "public", "navigable": false },
          { "ref": "c2", "role": "lines", "multiplicity": "1..*",
            "visibility": "private", "navigable": true }
        ] }
    ]
  },
  "diagrams": [
    { "id": "d1", "name": "Domain",
      "nodes": [
        { "id": "n1", "ref": "c1", "x": 80, "y": 110, "w": 220, "h": 130,
          "show": { "attributes": true, "operations": true } }
      ],
      "edges": [
        { "id": "g1", "ref": "r1", "waypoints": [[300, 190]], "labels": {} }
      ] }
  ]
}

Rules

  • Element kinds: class, interface, enumeration, package, comment. Which compartments an element carries follows from its kind — a comment has body instead of name, an enumeration has literals, an interface has no attributes.
  • Relationship kinds: association, aggregation, composition, generalization, realization, dependency, annotation.
  • ends is always [source, target]. What that ordering means is per-kind: for generalization the source is the subtype; for realization the source is the implementation; for composition and aggregation the source is the whole; for annotation the source is the note. Encoding every relationship the same way keeps rendering and hit-testing generic.
  • Role, multiplicity, visibility, and navigability are only meaningful on association, aggregation, and composition. They are preserved but not shown on the others, so switching a relationship's kind and switching back loses nothing.
  • Multiplicity grammar: 1, 0, *, 0..1, 1..*, 2..7, or empty. Inverted ranges such as 5..2 are rejected.
  • Package containment lives in the model as owner, not in the geometry. A box that merely overlaps a package on screen does not belong to it.
  • An element appears at most once per diagram. That invariant is what lets an edge resolve its endpoints from element ids alone.
  • Diagram nodes and edges hold ids and geometry only — never copies of semantic data. Rename a class and every diagram showing it updates.
  • An edge is only drawable on a diagram where both of its endpoint elements have nodes.

Source layout

Script order in index.html is the dependency graph: each file uses only globals defined above it, and every file is an IIFE hanging one property off the UMLOne global.

File Responsibility
src/ns.js The namespace; ids, cloning, events, DOM helpers
src/geometry.js Rect and polyline math; clipping a line to a box border
src/measure.js SVG text measurement, cached
src/model.js The semantic half: elements, relationships, formatting, parsing
src/diagram.js The diagram half: nodes, edges, routing, integrity
src/validate.js Validation, normalization, canonical key order
src/io.js Serialize, deserialize, file read, download, drag-and-drop
src/render.js Layout, line rectangles, and SVG drawing
src/layout.js Automatic arrangement: layering, ordering, packing
src/export.js Standalone SVG for export and for printing
src/commands.js The document store, undo stack, composite operations
src/inlineedit.js In-place text editing on the diagram
src/interact.js Pointer and keyboard handling
src/panel.js The property inspector
src/app.js Wiring

Three design decisions worth knowing before changing anything:

  • A document is plain JSON data, never a graph of class instances. That is what makes snapshot undo and lossless round-tripping cheap.
  • Every mutation goes through store.apply, which snapshots first. Undo is therefore correct by construction rather than by remembering to add it each time. Continuous gestures use applyContinuous so one drag is one undo step.
  • render.lineBoxes is the only answer to "where is this line?" The drawn text, the invisible click targets, and the in-place editor all read from it, so what you see, what you click, and where the text box appears cannot drift apart.

Current limits

  • Rendering is a full redraw per change. Fine at class-diagram sizes; it would need incremental updates for thousands of elements.
  • Edges route as straight polylines through user-placed bends. There is no automatic orthogonal routing or overlap avoidance.
  • No export to XMI or PlantUML. The JSON format is designed so an exporter can be added without changing the internals.
  • Automatic layout is a button, not a mode. Nothing rearranges itself while you edit, and the arrangement does not try to preserve the shape you already had.

About

UML diagram and model editor in Web single page app that stores files offline

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages