ea 0.3.1 → 0.4.0

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.diagrams/01-xmi-extension-elements-parsing.md +67 -0
  3. data/TODO.diagrams/02-diagram-element-rich-model.md +61 -0
  4. data/TODO.diagrams/03-ea-style-svg-emitter.md +70 -0
  5. data/TODO.diagrams/04-connector-routing-from-soid-eoid.md +55 -0
  6. data/TODO.diagrams/05-marker-rendering-per-relationship-type.md +49 -0
  7. data/TODO.diagrams/06-connector-labels.md +49 -0
  8. data/TODO.diagrams/07-canvas-size-and-viewbox.md +39 -0
  9. data/TODO.diagrams/08-visual-regression-harness.md +34 -0
  10. data/TODO.diagrams/09-cli-render-all-diagrams.md +25 -0
  11. data/TODO.diagrams/10-stereotype-color-config.md +29 -0
  12. data/TODO.diagrams/11-elkrb-auto-layout-integration.md +53 -0
  13. data/config/stereotype_colors.yml +15 -0
  14. data/lib/ea/cli/app.rb +7 -1
  15. data/lib/ea/cli/command/svg.rb +50 -6
  16. data/lib/ea/model/diagram_connector.rb +19 -1
  17. data/lib/ea/model/diagram_element.rb +28 -0
  18. data/lib/ea/sources/xmi/annotation_builder.rb +1 -1
  19. data/lib/ea/sources/xmi/diagram_builder.rb +164 -44
  20. data/lib/ea/sources/xmi/extension_elements.rb +74 -0
  21. data/lib/ea/sources/xmi/extension_geometry_parser.rb +107 -0
  22. data/lib/ea/sources/xmi/extension_style_parser.rb +78 -0
  23. data/lib/ea/sources/xmi.rb +3 -0
  24. data/lib/ea/svg/ea_emitter/background.rb +16 -0
  25. data/lib/ea/svg/ea_emitter/canvas.rb +65 -0
  26. data/lib/ea/svg/ea_emitter/connectors.rb +50 -0
  27. data/lib/ea/svg/ea_emitter/document.rb +44 -0
  28. data/lib/ea/svg/ea_emitter/elements.rb +169 -0
  29. data/lib/ea/svg/ea_emitter/labels.rb +64 -0
  30. data/lib/ea/svg/ea_emitter/markers.rb +117 -0
  31. data/lib/ea/svg/ea_emitter.rb +29 -0
  32. data/lib/ea/svg/stereotype_color_resolver.rb +50 -0
  33. data/lib/ea/svg.rb +2 -0
  34. data/lib/ea/version.rb +1 -1
  35. metadata +26 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf343d41c80cffbd277dd67bf172d0c947ddae898f51dacc1a2fffc3af871b0a
4
- data.tar.gz: b554c52026c03c0a431e31140f840fedd495fae447b738c4d9fda4fae30c9eda
3
+ metadata.gz: b401faad5f11e5b2504e50cabd3ee86dd4939bd8639c4c046b3fd9963408a116
4
+ data.tar.gz: fe36564c5f8d52281a35e1fa49c7f1231a42f941b92e8e5895623e18d6f32ddf
5
5
  SHA512:
6
- metadata.gz: bdec06da4470d865e75c52cbe8c5bec83ae4f242b223e98b436e5c244e8dcf8fd3c9a03bfe3f1ef3eedd9d2443fa52c21544755e8ff9cb3c5e698ee0a4b685f3
7
- data.tar.gz: 5db5b284986bd79b3c1165f491d95cb6a5b9cd88d0bcdcab116e77abd92ebd84b267c8f3614850fa669d544085eca6eba52a6eea45c992e42439aa0b38803194
6
+ metadata.gz: 7cd10052a2b16998cab3bde33d0874f605e6d8f767adefc710c7080ced3a39d76e3e0fb262b3931483d19cbfc35ff9715928f0d7d5f4dda22d95be693b0455a8
7
+ data.tar.gz: 87d9150c1840ca118fe264b5b1cf2c01bf09ba5d7eb2c5f5cf5a3aedd3c27bc4a07ea86ac9e7d1636bd2ebf835b012c049bc535e76edbdf412b28152ae7e4b68
@@ -0,0 +1,67 @@
1
+ # 01 - Parse XMI Extension Elements Block
2
+
3
+ ## Status: DONE (2026-07-21)
4
+
5
+ ## Problem
6
+
7
+ EA's XMI stores diagram placement data inside an `<xmi:Extension>`
8
+ block, under `<elements>/<element>` rows. Each row carries:
9
+
10
+ - `geometry="Left=X;Top=Y;Right=X2;Bottom=Y2;imgL=..;imgT=..;imgR=..;imgB=..;"`
11
+ (pixel bounds + image bounds with 25 px padding)
12
+ - `subject="EAID_..."` (the model element this placement refers to)
13
+ - `seqno="N"` (z-order)
14
+ - `style="BCol=<int>;font=<name>;fontsz=<int>;bold=0;italic=0;LWth=<int>;.."`
15
+ (EA packed style)
16
+
17
+ For connectors, the same row shape carries:
18
+ - `geometry="SX=..;SY=..;EX=..;EY=..;EDGE=N;$LLB=..;$LLT=..;LRT=..;LRB=..;Path=;"`
19
+ - `style="Mode=3;SOID=<source DUID>;EOID=<target DUID>;Color=-1;LWidth=2;"`
20
+
21
+ Our `Ea::Sources::Xmi::DiagramBuilder` currently reads only the
22
+ UML-DI `<owned_element>/<bounds>` tree, which doesn't include any
23
+ of this rich placement/style data. Result: SVG rendering from XMI
24
+ sources is missing colors, fonts, bend routing, label positions,
25
+ and proper element placement.
26
+
27
+ ## Approach
28
+
29
+ Extend the XMI source adapter to walk the `<xmi:Extension>`
30
+ block's `<elements>` rows for each diagram. Build a parallel
31
+ `Ea::Model::DiagramElement` per row, populating:
32
+
33
+ - `bounds` from `Left/Top/Right/Bottom`
34
+ - `model_element_ref` from `subject` (resolved to model id)
35
+ - `style` hash including `BCol`, `font`, `fontsz`, `bold`, `italic`,
36
+ `LWth`, `LCol` (line color)
37
+ - A new `image_bounds` field carrying the `imgL/imgT/imgR/imgB`
38
+ padded bounds (EA's visual box, vs. the logical bounds)
39
+
40
+ For connector rows, build `Ea::Model::DiagramConnector` with:
41
+ - `relationship_ref` from `subject`
42
+ - Source/target resolved via `SOID`/`EOID` → DUID → placed element
43
+ - Waypoints computed from the SX/SY/EX/EY deltas + the source and
44
+ target element edge centers (matching EA's rendering math)
45
+ - Style hash with `Color`, `LWidth`, `Mode`
46
+
47
+ ## Files
48
+
49
+ - `lib/ea/sources/xmi/extension_elements.rb` (new — walks the
50
+ extension block, returns typed rows)
51
+ - `lib/ea/sources/xmi/extension_geometry_parser.rb` (new — parses
52
+ `geometry="..."` packed string into a typed struct)
53
+ - `lib/ea/sources/xmi/extension_style_parser.rb` (new — parses
54
+ `style="..."` packed string into a typed struct)
55
+ - `lib/ea/sources/xmi/diagram_builder.rb` (updated — uses
56
+ extension data when present, falls back to umldi otherwise)
57
+ - `lib/ea/model/diagram_element.rb` (extended — add
58
+ `image_bounds` field)
59
+ - `spec/ea/sources/xmi/extension_*_spec.rb`
60
+
61
+ ## Verification
62
+
63
+ - Unit specs: parse a known geometry/style string, assert typed
64
+ struct matches.
65
+ - Integration: load `plateau_all_packages_export.xmi`, find diagram
66
+ `EAID_0016F797_...`, assert every placed element has bounds +
67
+ style populated.
@@ -0,0 +1,61 @@
1
+ # 02 - Rich DiagramElement / DiagramConnector Model
2
+
3
+ ## Status: DONE (2026-07-21, folded into TODO 03)
4
+
5
+ ## Problem
6
+
7
+ The current `Ea::Model::DiagramElement` carries only `bounds` and
8
+ an untyped `style` hash. EA's renderer needs:
9
+
10
+ - `image_bounds` (the imgL/imgT/imgR/imgB padded rect — distinct
11
+ from the logical Left/Top/Right/Bottom rect)
12
+ - `background_color` (decoded from `BCol` BGR integer → RGB hex)
13
+ - `font_family`, `font_size`, `font_bold`, `font_italic`
14
+ - `line_color`, `line_width`
15
+ - `z_order` (seqno)
16
+ - `label_inset` (the offset from the bounds origin where text
17
+ should start; EA convention is x+5, y+17 for the first line)
18
+
19
+ For `DiagramConnector`:
20
+ - `source_anchor`, `target_anchor` (which edge of the source/target
21
+ element the line attaches to — top/right/bottom/left)
22
+ - `source_duid`, `target_duid` (so the renderer can resolve back to
23
+ placed elements without re-walking the model)
24
+ - `label_boxes` (the four LRT/LRB/LLT/LLB label rectangles with
25
+ their text content decoded)
26
+
27
+ ## Approach
28
+
29
+ Extend the existing types (don't replace — backward compat for the
30
+ QEA adapter which doesn't populate these yet):
31
+
32
+ - `DiagramElement`: add `image_bounds`, `background_color`,
33
+ `font_family`, `font_size`, `font_bold`, `font_italic`,
34
+ `line_color`, `line_width`, `z_order`
35
+ - `DiagramConnector`: add `source_anchor`, `target_anchor`,
36
+ `source_duid`, `target_duid`, `label_boxes` (collection)
37
+
38
+ The QEA adapter populates these from its `objectstyle` packed
39
+ string (existing `DiagramStyleParser`); the XMI adapter populates
40
+ them from the `<xmi:Extension>/<elements>` style attribute.
41
+
42
+ The renderer reads the typed fields directly — no more parsing at
43
+ render time.
44
+
45
+ ## Files
46
+
47
+ - `lib/ea/model/diagram_element.rb`
48
+ - `lib/ea/model/diagram_connector.rb`
49
+ - `lib/ea/model/label_box.rb` (new)
50
+ - `lib/ea/sources/qea/diagram_builder.rb` (populate the new fields)
51
+ - `lib/ea/sources/qea/diagram_style_parser.rb` (extend to typed
52
+ accessors, not just a hash)
53
+ - `spec/ea/model/diagram_element_spec.rb`
54
+ - `spec/ea/model/diagram_connector_spec.rb`
55
+
56
+ ## Verification
57
+
58
+ - Round-trip: model JSON includes all new fields; deserialize
59
+ preserves them.
60
+ - QEA adapter spec: every placed element has a non-nil
61
+ `background_color`.
@@ -0,0 +1,70 @@
1
+ # 03 - EA-Style SVG Emitter
2
+
3
+ ## Status: DONE (2026-07-21) (depends on 02)
4
+
5
+ ## Problem
6
+
7
+ The current `Ea::Svg::Renderer` emits minimal SVG: header,
8
+ background rect, then per-element `<g class="element">` groups
9
+ with the box + label inside. EA's SVG output is structurally
10
+ different:
11
+
12
+ 1. DOCTYPE declaration (SVG 1.0)
13
+ 2. Root `<svg>` with `width="Xcm" height="Ycm"` and
14
+ `viewBox="0 0 W H"`
15
+ 3. `<title></title>` (empty)
16
+ 4. `<desc>Created with Enterprise Architect (Build: 1628) 2</desc>`
17
+ 5. Background `<g style="fill:#FFFFFF;..."><rect .../></g>`
18
+ 6. For each element, TWO `<g>` blocks:
19
+ a. Shape `<g>` with `style="...fill:COLOR;..."` containing the
20
+ `<rect>`
21
+ b. Text `<g>` with `style="fill:#000000;..."` containing all
22
+ `<text>` labels for that element
23
+ 7. Divider line as `<g><path d="M x y L x2 y2"/></g>`
24
+ 8. Connector paths as separate `<g>` blocks per path segment
25
+ 9. Arrow markers as `<g><polygon .../></g>`
26
+ 10. Connector labels as a final `<g>` block of `<text>`
27
+
28
+ The output is layered by visual category, not by element. Our
29
+ current emitter layers by element, mixing shapes and text in one
30
+ `<g>`.
31
+
32
+ ## Approach
33
+
34
+ Rewrite the emitter to follow EA's layering. Each render phase
35
+ emits one or more `<g>` blocks in the canonical order:
36
+
37
+ 1. `Ea::Svg::EaEmitter::Background`
38
+ 2. `Ea::Svg::EaEmitter::Elements` (one shape `<g>` + one text `<g>`
39
+ per element, in z-order)
40
+ 3. `Ea::Svg::EaEmitter::Dividers` (one `<g>` per divider line)
41
+ 4. `Ea::Svg::EaEmitter::Connectors` (one `<g>` per path)
42
+ 5. `Ea::Svg::EaEmitter::Markers` (filled / open polygons)
43
+ 6. `Ea::Svg::EaEmitter::Labels` (all text labels last)
44
+
45
+ Each emitter is a single-responsibility class. The orchestrator
46
+ calls them in order.
47
+
48
+ The existing `Ea::Svg::Renderer` stays as a thinner alternative
49
+ (useful for diagrams without full style data, e.g. from QEA
50
+ sources with sparse style). The new `EaEmitter` is the default
51
+ when full EA-style data is available.
52
+
53
+ ## Files
54
+
55
+ - `lib/ea/svg/ea_emitter.rb` (orchestrator)
56
+ - `lib/ea/svg/ea_emitter/background.rb`
57
+ - `lib/ea/svg/ea_emitter/elements.rb`
58
+ - `lib/ea/svg/ea_emitter/dividers.rb`
59
+ - `lib/ea/svg/ea_emitter/connectors.rb`
60
+ - `lib/ea/svg/ea_emitter/markers.rb`
61
+ - `lib/ea/svg/ea_emitter/labels.rb`
62
+ - `lib/ea/svg/ea_emitter/canvas.rb` (computes cm dimensions + viewBox)
63
+ - `spec/ea/svg/ea_emitter/*_spec.rb`
64
+
65
+ ## Verification
66
+
67
+ - Byte-level structural diff against EA's reference SVG for one
68
+ diagram. We don't require pixel-perfect match (font rendering
69
+ differences), but the element layering, group structure, and
70
+ attribute presence should match.
@@ -0,0 +1,55 @@
1
+ # 04 - Connector Routing via SOID/EOID Resolution
2
+
3
+ ## Status: DONE (2026-07-21, folded into TODO 03)
4
+
5
+ ## Problem
6
+
7
+ EA connector geometry stores only bend deltas (SX/SY/EX/EY) and
8
+ the EDGE code (which side of the element the line attaches to).
9
+ The actual line endpoints are computed by EA's renderer at draw
10
+ time, by looking up the source and target element placements via
11
+ their DUIDs (style `SOID` and `EOID`).
12
+
13
+ Our current QEA adapter computes waypoints from the connector's
14
+ Start_Object_ID/End_Object_ID via the model. That works but
15
+ ignores the EDGE hint, producing lines that always start at the
16
+ element center instead of the correct edge.
17
+
18
+ ## Approach
19
+
20
+ In both the QEA and XMI source adapters, when building a
21
+ DiagramConnector:
22
+
23
+ 1. Resolve source and target placements via SOID/EOID (XMI) or
24
+ Start_Object_ID/End_Object_ID (QEA).
25
+ 2. Read the `EDGE` field (1=top, 2=right, 3=bottom, 4=left,
26
+ 5..8 = diagonal variants).
27
+ 3. Compute the source connection point as the center of the
28
+ chosen edge on the source element.
29
+ 4. Apply the SX/SY delta to get the first bend.
30
+ 5. Apply the EX/EY delta from the target edge center to get the
31
+ second bend.
32
+ 6. Compute the target connection point as the center of the
33
+ chosen edge on the target element.
34
+
35
+ Emit exactly 2-4 waypoints: source edge → first bend → second
36
+ bend → target edge. This matches EA's typical "L" or "Z" routing.
37
+
38
+ For connectors without EDGE or SOID/EOID, fall back to straight
39
+ line between element centers.
40
+
41
+ ## Files
42
+
43
+ - `lib/ea/sources/qea/diagram_builder.rb`
44
+ - `lib/ea/sources/xmi/diagram_builder.rb`
45
+ - `lib/ea/svg/edge_anchor.rb` (computes edge center given bounds +
46
+ EDGE code)
47
+ - `spec/ea/svg/edge_anchor_spec.rb`
48
+
49
+ ## Verification
50
+
51
+ - Spec: source element with EDGE=1 produces a source point at the
52
+ top edge center.
53
+ - Spec: connector with both bends produces 4 waypoints.
54
+ - Visual: connector lines visually attach to element edges, not
55
+ centers.
@@ -0,0 +1,49 @@
1
+ # 05 - Marker Rendering Per Relationship Type
2
+
3
+ ## Status: DONE (2026-07-21, folded into TODO 03)
4
+
5
+ ## Problem
6
+
7
+ EA renders different arrow markers based on the connector's
8
+ semantic type:
9
+
10
+ - **Association** (navigable): filled triangle at target
11
+ - **Generalization**: open triangle at target (white fill, black
12
+ stroke)
13
+ - **Realization**: open triangle at target + dashed line
14
+ - **Dependency**: open arrow at target + dashed line
15
+ - **Aggregation** (source): open diamond at source
16
+ - **Composition** (source): filled diamond at source
17
+
18
+ Our current renderer emits a filled triangle at target regardless
19
+ of relationship type, plus a diamond for aggregations. The
20
+ distinction between open and filled markers matters visually.
21
+
22
+ ## Approach
23
+
24
+ Add a `MarkerResolver` that maps a relationship type + end
25
+ (source/target) to a marker shape:
26
+
27
+ - `:filled_triangle`, `:open_triangle`, `:filled_diamond`,
28
+ `:open_diamond`, `:open_arrow`, `:none`
29
+
30
+ Each shape is rendered as a 4-point `<polygon>` with the
31
+ appropriate fill color.
32
+
33
+ The marker anchor is the END of the polyline (target) or the
34
+ START (source). The marker is rotated to align with the line's
35
+ direction at that end.
36
+
37
+ ## Files
38
+
39
+ - `lib/ea/svg/marker_resolver.rb`
40
+ - `lib/ea/svg/marker_shapes.rb` (one class per shape)
41
+ - Update `lib/ea/svg/ea_emitter/markers.rb`
42
+ - `spec/ea/svg/marker_resolver_spec.rb`
43
+
44
+ ## Verification
45
+
46
+ - Spec: Association → filled triangle at target.
47
+ - Spec: Generalization → open triangle at target.
48
+ - Spec: Composition → filled diamond at source + filled triangle
49
+ at target.
@@ -0,0 +1,49 @@
1
+ # 06 - Connector Labels (Role Names + Multiplicities)
2
+
3
+ ## Status: DONE (2026-07-21, folded into TODO 03)
4
+
5
+ ## Problem
6
+
7
+ EA renders labels on connectors at four canonical positions:
8
+
9
+ - `LLB` (lower-left, near source) — source role name
10
+ - `LLT` (upper-left, near source) — source multiplicity
11
+ - `LRB` (lower-right, near target) — target role name
12
+ - `LRT` (upper-right, near target) — target multiplicity
13
+
14
+ Each label has CX/CY (size), OX/OY (offset from the connector
15
+ endpoint), plus font/style flags.
16
+
17
+ Our current renderer emits no connector labels at all. The
18
+ information is in the XMI geometry string but never extracted.
19
+
20
+ ## Approach
21
+
22
+ Extend the XMI adapter's geometry parser to also extract the four
23
+ label boxes, each as an `Ea::Model::LabelBox` with:
24
+
25
+ - `role` (:source_name, :source_multiplicity, :target_name,
26
+ :target_multiplicity)
27
+ - `text` (decoded from the model's relationship ends)
28
+ - `offset_x`, `offset_y` (from OX/OY)
29
+ - `width`, `height` (from CX/CY)
30
+
31
+ The emitter renders each label as `<text>` at the appropriate
32
+ offset from the connector endpoint.
33
+
34
+ For QEA sources, the same data is in the connector's `SourceRole`
35
+ and `DestRole` notes + cardinality fields.
36
+
37
+ ## Files
38
+
39
+ - `lib/ea/model/label_box.rb`
40
+ - `lib/ea/sources/xmi/extension_geometry_parser.rb` (extend)
41
+ - `lib/ea/svg/ea_emitter/labels.rb` (extend)
42
+ - `spec/ea/sources/xmi/extension_geometry_parser_spec.rb`
43
+ - `spec/ea/svg/ea_emitter/labels_spec.rb`
44
+
45
+ ## Verification
46
+
47
+ - Spec: known geometry string yields 4 label boxes with correct
48
+ offsets.
49
+ - Visual: connector labels appear at the same positions as EA.
@@ -0,0 +1,39 @@
1
+ # 07 - Canvas Size + viewBox Computation
2
+
3
+ ## Status: DONE (2026-07-21, folded into TODO 03)
4
+
5
+ ## Problem
6
+
7
+ EA's root `<svg>` element declares `width="Xcm" height="Ycm"` and
8
+ `viewBox="0 0 W H"`. The cm dimensions are computed from the
9
+ union of all element `image_bounds` (the padded bounds), with
10
+ ~25 px padding around the canvas. The viewBox is in pixel units.
11
+
12
+ Our current renderer computes a viewBox from the union of element
13
+ bounds + connectors, but doesn't emit cm dimensions.
14
+
15
+ ## Approach
16
+
17
+ `Ea::Svg::EaEmitter::Canvas` computes:
18
+
19
+ 1. Union of all element `image_bounds` (NOT logical bounds — EA's
20
+ canvas size is based on the visual padding).
21
+ 2. Adds 10 px outer margin.
22
+ 3. Emits viewBox = "min_x min_y width height".
23
+ 4. Emits width/height in cm as `width / 28.346` (72 DPI conversion).
24
+
25
+ The canvas module is also responsible for translating the
26
+ coordinate space if min_x/min_y are non-zero (EA's convention is
27
+ to keep the viewBox starting at 0,0 by translating all elements).
28
+
29
+ ## Files
30
+
31
+ - `lib/ea/svg/ea_emitter/canvas.rb`
32
+ - `spec/ea/svg/ea_emitter/canvas_spec.rb`
33
+
34
+ ## Verification
35
+
36
+ - Spec: known element set produces expected viewBox and cm
37
+ dimensions.
38
+ - Visual: SVG opens in a viewer at the same physical size as EA's
39
+ output.
@@ -0,0 +1,34 @@
1
+ # 08 - Visual Regression Harness
2
+
3
+ ## Status: DONE (2026-07-21)
4
+
5
+ ## Problem
6
+
7
+ "Matches EA's SVG" is a fuzzy target. We need an automated way to
8
+ detect regressions and measure fidelity.
9
+
10
+ ## Approach
11
+
12
+ A spec that:
13
+
14
+ 1. Loads `plateau_all_packages_export.xmi`.
15
+ 2. For each diagram in the XMI's extension block, locates the
16
+ matching EA reference SVG in
17
+ `mn-samples-plateau/sources/001-mds/xmi-images/`.
18
+ 3. Renders our SVG for the same diagram.
19
+ 4. Compares structurally (canonicalized XML diff ignoring
20
+ whitespace and attribute order).
21
+ 5. Reports: which diagrams match exactly, which have structural
22
+ differences, which have only attribute differences.
23
+
24
+ Not a hard-fail spec; produces a report we can track over time.
25
+ Once fidelity is high enough, tighten to a hard threshold.
26
+
27
+ ## Files
28
+
29
+ - `spec/ea/svg/visual_regression_spec.rb`
30
+ - `spec/support/svg_canonicalizer.rb`
31
+
32
+ ## Verification
33
+
34
+ - Run produces a report; not asserting 100% match initially.
@@ -0,0 +1,25 @@
1
+ # 09 - CLI: Render All Diagrams from a Source
2
+
3
+ ## Status: DONE (2026-07-21)
4
+
5
+ ## Problem
6
+
7
+ `ea svg NAME FILE` renders one named diagram. For batch
8
+ regeneration of all diagrams (e.g. plateau-model deploy), we need
9
+ a way to render every diagram in a source file.
10
+
11
+ ## Approach
12
+
13
+ Add `ea svg --all FILE [--output-dir=PATH]` mode. Iterates every
14
+ diagram in the model, names the output file by diagram id (e.g.
15
+ `EAID_0016F797_....svg`).
16
+
17
+ ## Files
18
+
19
+ - `lib/ea/cli/command/svg.rb` (extend)
20
+ - `spec/ea/cli/svg_command_spec.rb`
21
+
22
+ ## Verification
23
+
24
+ - `ea svg --all plateau.xmi --output-dir=/tmp/svgs` produces one
25
+ SVG per diagram.
@@ -0,0 +1,29 @@
1
+ # 10 - Stereotype Color Configuration (QEA fallback)
2
+
3
+ ## Status: DONE (2026-07-21)
4
+
5
+ ## Problem
6
+
7
+ When the source is QEA, the element's BCol is often the default
8
+ white. EA picks stereotype-based colors at render time when BCol
9
+ is not set. Our renderer currently hard-codes a stereotype →
10
+ color map, which is fragile.
11
+
12
+ ## Approach
13
+
14
+ Externalize the stereotype → color map to a YAML configuration
15
+ file (`config/stereotype_colors.yml`). Users can override per
16
+ project.
17
+
18
+ The XMI source carries BCol directly from EA, so the config only
19
+ applies when BCol is missing (QEA sources without style data).
20
+
21
+ ## Files
22
+
23
+ - `config/stereotype_colors.yml`
24
+ - `lib/ea/svg/stereotype_color_resolver.rb`
25
+ - `spec/ea/svg/stereotype_color_resolver_spec.rb`
26
+
27
+ ## Verification
28
+
29
+ - Custom YAML overrides default map.
@@ -0,0 +1,53 @@
1
+ # 11 - ElkRb Auto-Layout Integration
2
+
3
+ ## Status: TODO (post-PR #24)
4
+
5
+ ## Problem
6
+
7
+ The current Ea::Svg::EaEmitter renders diagrams from the placement
8
+ data captured by the source adapter (QEA's t_diagramobjects /
9
+ t_diagramlinks, XMI's <xmi:Extension>/<elements>). When the source
10
+ doesn't carry placement data — or when the user wants to re-layout
11
+ an existing diagram — there's no fallback.
12
+
13
+ Eclipse Layout Kernel (ELK) is the standard auto-layout library for
14
+ node-link diagrams (UML class diagrams, flowcharts, etc.). `elkrb`
15
+ is a pure-Ruby implementation in `/Users/mulgogi/src/claricle/elkrb/`.
16
+
17
+ ## Approach
18
+
19
+ Build an `Ea::Layout::ElkAdapter` that:
20
+
21
+ 1. Translates an `Ea::Model::Document` (or a subset — packages,
22
+ classifiers, relationships) into elkrb's `Graph` / `Node` /
23
+ `Edge` model.
24
+ 2. Runs the chosen elkrb algorithm (`Layered` for class diagrams
25
+ is the Sugiyama-style default; `Force` for organic; `Stress`
26
+ for high-quality).
27
+ 3. Reads back the computed positions and writes them into an
28
+ `Ea::Model::Diagram` (with DiagramElement bounds and
29
+ DiagramConnector waypoints computed by elkrb's edge router).
30
+
31
+ Then the standard `Ea::Svg::EaEmitter` renders the layout-posed
32
+ diagram just like an EA-authored one. No renderer changes needed.
33
+
34
+ CLI surface:
35
+ - `ea layout FILE [--algorithm=layered|force|stress]` — runs
36
+ auto-layout, emits a JSON of computed positions
37
+ - `ea svg NAME FILE --layout=auto` — runs layout then renders
38
+
39
+ ## Files
40
+
41
+ - `lib/ea/layout.rb` (namespace)
42
+ - `lib/ea/layout/elk_adapter.rb`
43
+ - `lib/ea/layout/document_to_graph.rb`
44
+ - `lib/ea/layout/graph_to_diagram.rb`
45
+ - `lib/ea/cli/command/layout.rb`
46
+ - `spec/ea/layout/*_spec.rb`
47
+
48
+ ## Verification
49
+
50
+ - Spec: known small document → elk graph → known layout output.
51
+ - Spec: QEA fixture with no placement data → auto-layout produces
52
+ non-overlapping positions.
53
+ - Spec: round-trip — auto-layout, render to SVG, no errors.
@@ -0,0 +1,15 @@
1
+ # Stereotype → fill color map. Used by Ea::Svg when an element's
2
+ # BCol is missing (e.g. QEA sources without style data, or XMI
3
+ # elements whose BCol defaulted).
4
+ #
5
+ # Format: <stereotype_lowercase>: "#RRGGBB"
6
+ # Hex colors must be CSS-compatible (uppercase).
7
+ featuretype: "#FFFFCC" # yellow
8
+ feature: "#FFFFCC"
9
+ type: "#CCFFCC" # green
10
+ datatype: "#FFCCFF" # pink
11
+ basictype: "#FFCCFF"
12
+ codelist: "#FFCCFF"
13
+ enumeration: "#FFCCFF"
14
+ union: "#F0E68C" # khaki
15
+ adeelement: "#F5F5DC" # beige
data/lib/ea/cli/app.rb CHANGED
@@ -88,7 +88,13 @@ module Ea
88
88
  desc "svg NAME FILE", "Render a diagram from QEA/XMI to standalone SVG"
89
89
  option :output, **OUTPUT_OPTION,
90
90
  desc: "Output path (default: <basename>.<diagram>.svg)"
91
- def svg(name, file)
91
+ option :mode, type: :string, default: "ea",
92
+ desc: "Output mode: ea (default, matches EA SVG structure) | thin (compact)"
93
+ option :all, type: :boolean, default: false,
94
+ desc: "Render every diagram in the source file (NAME ignored)"
95
+ option :output_dir, type: :string,
96
+ desc: "Directory for --all output (default: <basename>.svgs/)"
97
+ def svg(name = nil, file = nil)
92
98
  Command::Svg.new(name: name, file: file, **symbolize(options)).call
93
99
  end
94
100
 
@@ -5,15 +5,29 @@ require "fileutils"
5
5
  module Ea
6
6
  module Cli
7
7
  module Command
8
- # `ea svg NAME FILE [--output=PATH]`
8
+ # `ea svg NAME FILE [--output=PATH] [--mode=ea|thin]`
9
+ # `ea svg --all FILE [--output-dir=PATH] [--mode=ea|thin]`
9
10
  #
10
- # Renders a single diagram from a QEA or XMI file into a
11
- # standalone SVG using the umldi content (placed element
12
- # bounds + connector waypoints) captured in Ea::Model::Diagram.
11
+ # Renders one (NAME) or every (--all) diagram from a QEA or
12
+ # XMI file into standalone SVG using the umldi content
13
+ # (placed element bounds + connector waypoints) captured in
14
+ # Ea::Model::Diagram.
15
+ #
16
+ # Default emitter is EaEmitter::Document which mirrors EA's SVG
17
+ # structure (DOCTYPE, layered groups, EA-style markers). Pass
18
+ # --mode=thin for the simpler Renderer output.
13
19
  class Svg < Base
14
20
  def call
21
+ return render_all if options[:all]
22
+
23
+ render_one
24
+ end
25
+
26
+ private
27
+
28
+ def render_one
15
29
  diagram = find_diagram
16
- svg = Ea::Svg::Renderer.new(diagram,
30
+ svg = emitter_for_mode.new(diagram,
17
31
  model_index: document.index_by_id).render
18
32
 
19
33
  output_path = resolve_output_path(diagram)
@@ -23,7 +37,37 @@ module Ea
23
37
  formatter.render([[output_path]], columns: [:written_to])
24
38
  end
25
39
 
26
- private
40
+ def render_all
41
+ out_dir = options[:output_dir] || default_output_dir
42
+ FileUtils.mkdir_p(out_dir)
43
+ paths = document.diagrams.map do |diagram|
44
+ svg = emitter_for_mode.new(diagram,
45
+ model_index: document.index_by_id).render
46
+ path = File.join(out_dir, "#{diagram.id}.svg")
47
+ File.write(path, svg)
48
+ path
49
+ end
50
+
51
+ formatter.render(paths.map { |p| [p] }, columns: [:written_to])
52
+ end
53
+
54
+ def default_output_dir
55
+ base = File.basename(file_path, ".*")
56
+ dir = File.dirname(file_path)
57
+ File.join(dir, "#{base}.svgs")
58
+ end
59
+
60
+ def emitter_for_mode
61
+ case (options[:mode] || :ea).to_sym
62
+ when :ea
63
+ Ea::Svg::EaEmitter::Document
64
+ when :thin
65
+ Ea::Svg::Renderer
66
+ else
67
+ raise Ea::Cli::UnsupportedFormat,
68
+ "Unknown svg mode: #{options[:mode]}"
69
+ end
70
+ end
27
71
 
28
72
  def document
29
73
  @document ||= build_model_document