ea 0.6.140 → 0.6.142
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.
- checksums.yaml +4 -4
- data/config/themes/default.yml +7 -0
- data/lib/ea/cli/app.rb +10 -0
- data/lib/ea/cli/command/diagrams.rb +4 -1
- data/lib/ea/cli/command/render.rb +8 -1
- data/lib/ea/cli/command/spa.rb +2 -1
- data/lib/ea/cli/command/svg.rb +9 -6
- data/lib/ea/fonts/metrics.rb +44 -16
- data/lib/ea/model/diagram.rb +6 -0
- data/lib/ea/qea/services/database_loader.rb +4 -1
- data/lib/ea/sources/qea/diagram_builder/routing.rb +64 -22
- data/lib/ea/sources/qea/diagram_builder.rb +4 -1
- data/lib/ea/sources/xmi/adapter.rb +20 -0
- data/lib/ea/sources/xmi/connector_index.rb +30 -0
- data/lib/ea/sources/xmi/constraint_builder.rb +81 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +22 -11
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/extension_types.rb +9 -2
- data/lib/ea/sources/xmi/relationship_builder.rb +17 -11
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +10 -26
- data/lib/ea/sources/xmi.rb +2 -0
- data/lib/ea/spa/generator.rb +4 -2
- data/lib/ea/spa/projector.rb +4 -2
- data/lib/ea/svg/ea_emitter/bottom_margin.rb +16 -12
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +22 -14
- data/lib/ea/svg/ea_emitter/canvas.rb +43 -5
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +12 -4
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/header.rb +8 -1
- data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +6 -1
- data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +6 -1
- data/lib/ea/svg/ea_emitter/compartment/marker.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +7 -2
- data/lib/ea/svg/ea_emitter/compartment/note_text.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/operations.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +12 -1
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +14 -1
- data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +6 -0
- data/lib/ea/svg/ea_emitter/compartment/tagged_values.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment.rb +13 -0
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +18 -4
- data/lib/ea/svg/ea_emitter/connector_redock.rb +13 -6
- data/lib/ea/svg/ea_emitter/content_extent.rb +11 -2
- data/lib/ea/svg/ea_emitter/diagram_frame.rb +7 -0
- data/lib/ea/svg/ea_emitter/document.rb +22 -6
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +6 -1
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +9 -0
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +7 -0
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +18 -14
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +1 -8
- data/lib/ea/svg/ea_emitter/elements.rb +36 -12
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +113 -19
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +66 -0
- data/lib/ea/svg/ea_emitter/label/plain_source_seat.rb +25 -2
- data/lib/ea/svg/ea_emitter/label/plain_target_seat.rb +64 -1
- data/lib/ea/svg/ea_emitter/labels.rb +64 -35
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +14 -7
- data/lib/ea/svg/ea_emitter/markers.rb +1 -1
- data/lib/ea/svg/ea_emitter/paint_order.rb +5 -4
- data/lib/ea/svg/ea_emitter/pen.rb +19 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
- data/lib/ea/svg/ea_emitter/text_renderer.rb +24 -8
- data/lib/ea/svg/ea_emitter.rb +1 -0
- data/lib/ea/svg/parity/source.rb +1 -1
- data/lib/ea/version.rb +1 -1
- metadata +5 -2
|
@@ -197,9 +197,11 @@ module Ea
|
|
|
197
197
|
|
|
198
198
|
def build_connectors(placed_connectors, placed_elements, owner_id, context)
|
|
199
199
|
# Index elements by DUID so we can resolve SOID/EOID.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
# A DUID shared by several placements names none of them (QEA
|
|
201
|
+
# placement_order_reversed? needs a unique match too).
|
|
202
|
+
by_duid = placed_elements.select { |pe| pe.style.duid }.group_by { |pe| pe.style.duid }
|
|
203
|
+
.select { |_duid, placements| placements.one? }
|
|
204
|
+
.transform_values(&:first)
|
|
203
205
|
by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
|
|
204
206
|
|
|
205
207
|
placed_connectors.each_with_index.map do |placed, index|
|
|
@@ -368,14 +370,28 @@ module Ea
|
|
|
368
370
|
# The saved SOID/EOID can name the model connector's ends in reverse
|
|
369
371
|
# order; the model connector decides which placement is the source.
|
|
370
372
|
def placed_ends(style, by_duid, by_subject, edge_source, edge_target)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
named = [by_duid[style.soid], by_duid[style.eoid]]
|
|
374
|
+
source = named[0] || sole_placement(by_subject, edge_source)
|
|
375
|
+
target = named[1] || sole_placement(by_subject, edge_target)
|
|
376
|
+
if source.equal?(target) && named.one?
|
|
377
|
+
source, target = subject_placements(by_subject, named, edge_source, edge_target)
|
|
378
|
+
end
|
|
373
379
|
return [source, target] unless source && target &&
|
|
374
380
|
reversed_ends?(source, target, edge_source, edge_target)
|
|
375
381
|
|
|
376
382
|
[target, source]
|
|
377
383
|
end
|
|
378
384
|
|
|
385
|
+
# One stale DUID falls back to the relationship's own end, which can
|
|
386
|
+
# be the very placement the other DUID named; two distinct ends never
|
|
387
|
+
# share a box, so each end then takes its own subject's placement. A
|
|
388
|
+
# link whose SOID and EOID are one DUID is a genuine self link.
|
|
389
|
+
def subject_placements(by_subject, named, edge_source, edge_target)
|
|
390
|
+
[edge_source, edge_target].zip(named).map do |subject, placement|
|
|
391
|
+
sole_placement(by_subject, subject) || (placement if guid_key(placement&.subject) == guid_key(subject))
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
379
395
|
def reversed_ends?(source, target, edge_source, edge_target)
|
|
380
396
|
guid_key(edge_source) != guid_key(edge_target) &&
|
|
381
397
|
guid_key(source.subject) == guid_key(edge_target) &&
|
|
@@ -399,12 +415,7 @@ module Ea
|
|
|
399
415
|
end
|
|
400
416
|
|
|
401
417
|
def connector_index
|
|
402
|
-
@connector_index ||=
|
|
403
|
-
conns = root.extension&.connectors&.connector || []
|
|
404
|
-
conns.each_with_object({}) do |c, acc|
|
|
405
|
-
acc[c.idref] = c if c.idref
|
|
406
|
-
end
|
|
407
|
-
end
|
|
418
|
+
@connector_index ||= ConnectorIndex.build(root)
|
|
408
419
|
end
|
|
409
420
|
|
|
410
421
|
def umldi_extractor
|
|
@@ -26,6 +26,7 @@ module Ea
|
|
|
26
26
|
|
|
27
27
|
def initialize(document)
|
|
28
28
|
@attributes = index(document, "attribute", "properties")
|
|
29
|
+
@attribute_stereotypes = index(document, "attribute", "stereotype", "stereotype")
|
|
29
30
|
@returns = index(document, "operation", "type")
|
|
30
31
|
@parameters = index(document, "parameter", "properties")
|
|
31
32
|
end
|
|
@@ -34,6 +35,12 @@ module Ea
|
|
|
34
35
|
@attributes[id]
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Stereotype EA stores on the attribute row
|
|
39
|
+
# (<attribute><stereotype stereotype="voidable"/>).
|
|
40
|
+
def attribute_stereotype(id)
|
|
41
|
+
@attribute_stereotypes[id]
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
def return_type(operation_id)
|
|
38
45
|
@returns[operation_id]
|
|
39
46
|
end
|
|
@@ -44,11 +51,11 @@ module Ea
|
|
|
44
51
|
|
|
45
52
|
private
|
|
46
53
|
|
|
47
|
-
def index(document, owner, child)
|
|
54
|
+
def index(document, owner, child, attribute = "type")
|
|
48
55
|
return {} unless document
|
|
49
56
|
|
|
50
57
|
document.xpath(%(//*[local-name()="#{owner}"][@*[local-name()="idref"]])).each_with_object({}) do |node, acc|
|
|
51
|
-
type = node.at_xpath(%(*[local-name()="#{child}"]
|
|
58
|
+
type = node.at_xpath(%(*[local-name()="#{child}"]/@#{attribute}))&.value
|
|
52
59
|
idref = node.at_xpath("@*[local-name()='idref']").value
|
|
53
60
|
acc[idref] = type unless type.nil? || type.empty?
|
|
54
61
|
end
|
|
@@ -51,12 +51,21 @@ module Ea
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Extension connector type => handler; a nil-returning handler skips
|
|
55
|
+
# the connector (Dependency counts only when stereotyped import).
|
|
56
|
+
EXTENSION_IMPORTS = {
|
|
57
|
+
"Package" => :package_import_relationship,
|
|
58
|
+
"Nesting" => :package_import_relationship,
|
|
59
|
+
"Dependency" => :stereotyped_dependency_import
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
54
62
|
def extension_import_relationship(id, connector)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
handler = EXTENSION_IMPORTS[connector.properties&.ea_type]
|
|
64
|
+
handler && method(handler).call(id, connector)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stereotyped_dependency_import(id, connector)
|
|
68
|
+
dependency_import_relationship(id, connector) if connector_stereotype(connector) == "import"
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
# EA exports elementImport outside packagedElement, so the model walk
|
|
@@ -73,6 +82,8 @@ module Ea
|
|
|
73
82
|
connector&.xrefs&.value.to_s[CONNECTOR_STEREOTYPE]
|
|
74
83
|
end
|
|
75
84
|
|
|
85
|
+
# Nesting connectors, like package imports, exist only in the
|
|
86
|
+
# extension; QEA models both as an Association between the ends.
|
|
76
87
|
def package_import_relationship(id, connector)
|
|
77
88
|
Ea::Model::Association.new(
|
|
78
89
|
id: id,
|
|
@@ -245,12 +256,7 @@ module Ea
|
|
|
245
256
|
end
|
|
246
257
|
|
|
247
258
|
def connector_index
|
|
248
|
-
@connector_index ||=
|
|
249
|
-
connectors = root.extension&.connectors&.connector || []
|
|
250
|
-
connectors.each_with_object({}) do |connector, index|
|
|
251
|
-
index[connector.idref] = connector if connector.idref
|
|
252
|
-
end
|
|
253
|
-
end
|
|
259
|
+
@connector_index ||= ConnectorIndex.build(root)
|
|
254
260
|
end
|
|
255
261
|
|
|
256
262
|
def navigable?(id, endpoint)
|
|
@@ -18,21 +18,17 @@ module Ea
|
|
|
18
18
|
return {} unless reversed_tree_candidate?(model, link, bounds, offsets)
|
|
19
19
|
|
|
20
20
|
start, finish = bounds.values_at(:source, :target)
|
|
21
|
-
|
|
22
|
-
return {} unless
|
|
21
|
+
route = reversed_tree_points(start, finish, ray_geometry(offsets, edge), stored_bus_levels(link))
|
|
22
|
+
return {} unless route
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
first = reversed_tree_leg(start, geom[:sx], geom[:sy], start_edge, finish)
|
|
26
|
-
last = reversed_tree_leg(finish, geom[:ex], geom[:ey], finish_edge, start)
|
|
27
|
-
|
|
28
|
-
reversed_tree_fields(model, first, last, bus, first[1] != start_edge)
|
|
24
|
+
reversed_tree_fields(model, route[0], route[1])
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
def reversed_tree_fields(model,
|
|
27
|
+
def reversed_tree_fields(model, points, beside)
|
|
32
28
|
style = model.style.to_h.except(:umldi_route, :reversed_ends)
|
|
33
29
|
style[:left_seated_source] = true if beside
|
|
34
|
-
ends = [point_for(first), point_for(last)]
|
|
35
|
-
{ waypoints: points_to_waypoints(
|
|
30
|
+
ends = [point_for(points.first), point_for(points.last)]
|
|
31
|
+
{ waypoints: points_to_waypoints(points),
|
|
36
32
|
source_anchor: ends[0], target_anchor: ends[1], source_port: ends[0], target_port: ends[1],
|
|
37
33
|
style: style }
|
|
38
34
|
end
|
|
@@ -49,26 +45,14 @@ module Ea
|
|
|
49
45
|
def reversed_corner_metadata(model, link, bounds, offsets, edge)
|
|
50
46
|
return {} unless reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
ends = origins.zip(boxes).map { |origin, box| corner_exit(origin, bend, box) }
|
|
56
|
-
ends.all? ? corner_fields(origins, bend, ends) : {}
|
|
57
|
-
end
|
|
48
|
+
origins, points = reversed_corner_points(bounds.values_at(:source, :target), ray_geometry(offsets, edge),
|
|
49
|
+
intermediate_waypoints(link).first)
|
|
50
|
+
return {} unless points
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
{ waypoints: points_to_waypoints([ends[0], bend, ends[1]].map { |point| [point.x, point.y] }),
|
|
52
|
+
{ waypoints: points_to_waypoints(points.map { |point| [point.x, point.y] }),
|
|
61
53
|
source_anchor: origins[0], target_anchor: origins[1] }
|
|
62
54
|
end
|
|
63
55
|
|
|
64
|
-
def corner_origins(boxes, geom)
|
|
65
|
-
[stored_ray_origin(boxes[0], geom[:sx], geom[:sy]), stored_ray_origin(boxes[1], geom[:ex], geom[:ey])]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def corner_exit(origin, bend, box)
|
|
69
|
-
inside_box?(origin, box) ? diagonal_corner_exit(origin, bend, box) : origin
|
|
70
|
-
end
|
|
71
|
-
|
|
72
56
|
def reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
73
57
|
model.connector_type == "Association" && model.style[:reversed_ends] && edge == 2 &&
|
|
74
58
|
model.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil] &&
|
data/lib/ea/sources/xmi.rb
CHANGED
|
@@ -25,11 +25,13 @@ module Ea
|
|
|
25
25
|
autoload :RelationshipBuilder, "ea/sources/xmi/relationship_builder"
|
|
26
26
|
autoload :AnnotationBuilder, "ea/sources/xmi/annotation_builder"
|
|
27
27
|
autoload :TagBuilder, "ea/sources/xmi/tag_builder"
|
|
28
|
+
autoload :ConstraintBuilder, "ea/sources/xmi/constraint_builder"
|
|
28
29
|
autoload :LegendBuilder, "ea/sources/xmi/legend_builder"
|
|
29
30
|
autoload :NoteBuilder, "ea/sources/xmi/note_builder"
|
|
30
31
|
autoload :StereotypeBuilder, "ea/sources/xmi/stereotype_builder"
|
|
31
32
|
autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
|
|
32
33
|
autoload :StoredRoute, "ea/sources/xmi/stored_route"
|
|
34
|
+
autoload :ConnectorIndex, "ea/sources/xmi/connector_index"
|
|
33
35
|
autoload :ExtensionTypes, "ea/sources/xmi/extension_types"
|
|
34
36
|
autoload :ExtensionElements, "ea/sources/xmi/extension_elements"
|
|
35
37
|
autoload :ExtensionGeometryParser, "ea/sources/xmi/extension_geometry_parser"
|
data/lib/ea/spa/generator.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Ea
|
|
|
17
17
|
attr_reader :document, :mode, :output_path, :shard_url_for, :configuration
|
|
18
18
|
|
|
19
19
|
def initialize(document, output:, mode: nil, shard_url_for: nil,
|
|
20
|
-
configuration: nil)
|
|
20
|
+
configuration: nil, frame: false)
|
|
21
|
+
@frame = frame
|
|
21
22
|
@document = document
|
|
22
23
|
@output_path = output
|
|
23
24
|
@mode = (mode || DEFAULT_MODE).to_sym
|
|
@@ -32,7 +33,8 @@ module Ea
|
|
|
32
33
|
def projector
|
|
33
34
|
@projector ||= Projector.new(document,
|
|
34
35
|
shard_url_for: shard_url_for,
|
|
35
|
-
configuration: configuration
|
|
36
|
+
configuration: configuration,
|
|
37
|
+
frame: @frame)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def strategy_class
|
data/lib/ea/spa/projector.rb
CHANGED
|
@@ -15,8 +15,9 @@ module Ea
|
|
|
15
15
|
class Projector
|
|
16
16
|
attr_reader :document, :shard_url_for, :configuration
|
|
17
17
|
|
|
18
|
-
def initialize(document, shard_url_for: nil, configuration: nil)
|
|
18
|
+
def initialize(document, shard_url_for: nil, configuration: nil, frame: false)
|
|
19
19
|
@document = document
|
|
20
|
+
@frame = frame
|
|
20
21
|
@shard_url_for = shard_url_for || default_shard_url
|
|
21
22
|
@configuration = configuration
|
|
22
23
|
end
|
|
@@ -288,7 +289,8 @@ module Ea
|
|
|
288
289
|
payload["svg"] = Ea::Svg::EaEmitter::Document.new(
|
|
289
290
|
diagram,
|
|
290
291
|
model_index: document.index_by_id,
|
|
291
|
-
document: document
|
|
292
|
+
document: document,
|
|
293
|
+
frame: @frame
|
|
292
294
|
).render
|
|
293
295
|
payload
|
|
294
296
|
rescue StandardError => e
|
|
@@ -12,7 +12,7 @@ module Ea
|
|
|
12
12
|
# the 7pt default): 31 for fontsz 100, 30 for 90, 28 for the
|
|
13
13
|
# default. A plain Note, and a Package that draws no "(from
|
|
14
14
|
# Parent)" subtitle, keep the bare 10 margin, as does a classifier
|
|
15
|
-
# of the diagram's own package. Misses: 2 outliers.
|
|
15
|
+
# or instance of the diagram's own package. Misses: 2 outliers.
|
|
16
16
|
class BottomMargin
|
|
17
17
|
BARE = 10
|
|
18
18
|
LINE_ALLOWANCE = 21
|
|
@@ -41,7 +41,7 @@ module Ea
|
|
|
41
41
|
model = model_index && model_index[element.model_element_ref]
|
|
42
42
|
return home_text_margin(element) if home_text?(model, diagram)
|
|
43
43
|
return BARE if plain_note?(model) || bare_package?(model, diagram) ||
|
|
44
|
-
|
|
44
|
+
home_element?(model, diagram)
|
|
45
45
|
|
|
46
46
|
LINE_ALLOWANCE + points(element)
|
|
47
47
|
end
|
|
@@ -60,17 +60,21 @@ module Ea
|
|
|
60
60
|
model.is_a?(Ea::Model::Note) && model.note_type != "Text"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# on the 7 PLATEAU diagrams
|
|
67
|
-
# diagram's package (all natively
|
|
68
|
-
# foreign (all 28..31).
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
# An element of the diagram's own package (a classifier, nested
|
|
64
|
+
# in an owner class or not, or an instance) draws no "(from
|
|
65
|
+
# Package)" row under its name, so EA keeps only the bare margin
|
|
66
|
+
# below it. EMPIRICAL FIT, checked on: the 7 PLATEAU diagrams
|
|
67
|
+
# whose lowest class is in the diagram's package (all natively
|
|
68
|
+
# 10) against the 84 foreign ones (all 28..31); basic.qea
|
|
69
|
+
# 3B8BD087 and D14AA320 (lowest an own-package "Object B"
|
|
70
|
+
# instance) and 372F7F7B, 4FC1D352, 6D0349D5 (lowest a Klass
|
|
71
|
+
# nested in "Class A.x" / "Class B" of the own package), all
|
|
72
|
+
# natively 10; and the foreign basic instances, natively 28.
|
|
73
|
+
def self.home_element?(model, diagram)
|
|
74
|
+
(model.is_a?(Ea::Model::Classifier) ||
|
|
75
|
+
model.is_a?(Ea::Model::InstanceSpecification)) &&
|
|
71
76
|
!diagram.package_id.to_s.empty? &&
|
|
72
|
-
model.package_id == diagram.package_id
|
|
73
|
-
model.nested_parent_name.to_s.empty?
|
|
77
|
+
model.package_id == diagram.package_id
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
# A Text element of the diagram's own package keeps 13 + its
|
|
@@ -33,6 +33,13 @@ module Ea
|
|
|
33
33
|
|
|
34
34
|
attr_reader :diagram, :model_index
|
|
35
35
|
|
|
36
|
+
# Where the leftmost model x lands: 30 + min_x capped at 35. A
|
|
37
|
+
# negative min_x keeps the 30 of min_x 0 (the corpus has none),
|
|
38
|
+
# so content left of the origin is not drawn off the canvas.
|
|
39
|
+
def self.inset_left_for(min_x)
|
|
40
|
+
[[INSET_LEFT - 5 + min_x, INSET_LEFT - 5].max, INSET_LEFT].min
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
def initialize(diagram, model_index: nil)
|
|
37
44
|
@diagram = diagram
|
|
38
45
|
@model_index = model_index
|
|
@@ -53,7 +60,7 @@ module Ea
|
|
|
53
60
|
# Left inset scales from 30 up to the 35 cap with the
|
|
54
61
|
# stored leftmost x: leftmost drawn x = 30 + min_x capped
|
|
55
62
|
# at 35 (corpus: min_x 0 -> 30, 2 -> 32, 4 -> 34, >=5 -> 35).
|
|
56
|
-
@effective_inset_left =
|
|
63
|
+
@effective_inset_left = self.class.inset_left_for(min_x)
|
|
57
64
|
[
|
|
58
65
|
min_x,
|
|
59
66
|
min_y,
|
|
@@ -62,19 +69,7 @@ module Ea
|
|
|
62
69
|
]
|
|
63
70
|
end
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Element x-extent: logical bounds only (matches EA's canvas
|
|
68
|
-
# left/right exactly).
|
|
69
|
-
# Element y-extent: union of logical + image_bounds (image
|
|
70
|
-
# extends below bounds for shadow / image padding), plus the
|
|
71
|
-
# "(from Parent)" subtitle extent: EA draws the subtitle 15px
|
|
72
|
-
# below the body and its text descent adds ~4 more, reserving
|
|
73
|
-
# 19px of canvas below subtitle-bearing packages (the +19
|
|
74
|
-
# height family: 05DF5000 Main, Metadata All, package
|
|
75
|
-
# Clients/Suppliers contexts).
|
|
76
|
-
SUBTITLE_CANVAS_EXTRA = 19
|
|
77
|
-
|
|
72
|
+
# Canvas points reserved below "(from Parent)" subtitles.
|
|
78
73
|
def subtitle_points
|
|
79
74
|
pts = []
|
|
80
75
|
(diagram.elements || []).each do |e|
|
|
@@ -88,6 +83,19 @@ module Ea
|
|
|
88
83
|
pts
|
|
89
84
|
end
|
|
90
85
|
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Element x-extent: logical bounds only (matches EA's canvas
|
|
89
|
+
# left/right exactly).
|
|
90
|
+
# Element y-extent: union of logical + image_bounds (image
|
|
91
|
+
# extends below bounds for shadow / image padding), plus the
|
|
92
|
+
# "(from Parent)" subtitle extent: EA draws the subtitle 15px
|
|
93
|
+
# below the body and its text descent adds ~4 more, reserving
|
|
94
|
+
# 19px of canvas below subtitle-bearing packages (the +19
|
|
95
|
+
# height family: 05DF5000 Main, Metadata All, package
|
|
96
|
+
# Clients/Suppliers contexts).
|
|
97
|
+
SUBTITLE_CANVAS_EXTRA = 19
|
|
98
|
+
|
|
91
99
|
def element_points
|
|
92
100
|
pts = []
|
|
93
101
|
(diagram.elements || []).each do |e|
|
|
@@ -23,6 +23,8 @@ module Ea
|
|
|
23
23
|
FRAME_INSET_LEFT = 35
|
|
24
24
|
INSET_TOUCHING_ORIGIN = 30
|
|
25
25
|
FRAME_INSET_TOP = 40
|
|
26
|
+
# Where the diagram frame's border line sits from the canvas edge.
|
|
27
|
+
FRAME_BORDER_INSET = 6
|
|
26
28
|
|
|
27
29
|
attr_reader :min_x, :min_y, :width, :height, :inset_left, :inset_top
|
|
28
30
|
|
|
@@ -32,8 +34,8 @@ module Ea
|
|
|
32
34
|
insets: [FRAME_INSET_LEFT, FRAME_INSET_TOP])
|
|
33
35
|
@min_x = min_x
|
|
34
36
|
@min_y = min_y
|
|
35
|
-
@width = width
|
|
36
|
-
@height = height
|
|
37
|
+
@width = whole(width)
|
|
38
|
+
@height = whole(height)
|
|
37
39
|
@inset_left, @inset_top = insets
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -60,11 +62,23 @@ module Ea
|
|
|
60
62
|
bottom: BottomMargin.for(diagram, model_index))
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
return empty_framed(diagram, min_x, min_y) unless bc.max_x
|
|
66
|
+
|
|
63
67
|
new(min_x: min_x, min_y: min_y, width: width, height: height,
|
|
64
68
|
insets: [bc.effective_inset_left || FRAME_INSET_LEFT,
|
|
65
69
|
FRAME_INSET_TOP])
|
|
66
70
|
end
|
|
67
71
|
|
|
72
|
+
# No element to size on: the frame alone, wide enough for its
|
|
73
|
+
# name tab.
|
|
74
|
+
def self.empty_framed(diagram, min_x, min_y)
|
|
75
|
+
width = [INSET_TOUCHING_ORIGIN + BoundsCalculator::INSET_RIGHT,
|
|
76
|
+
DiagramFrame.new(canvas: nil).min_width(diagram)].max
|
|
77
|
+
new(min_x: min_x, min_y: min_y, width: width,
|
|
78
|
+
height: BoundsCalculator::INSET_TOP + BoundsCalculator::INSET_BOTTOM,
|
|
79
|
+
insets: [BoundsCalculator.inset_left_for(min_x), FRAME_INSET_TOP])
|
|
80
|
+
end
|
|
81
|
+
|
|
68
82
|
# Second-pass canvas over the DRAWN extents captured by the
|
|
69
83
|
# element pass (grown classifiers, autosized packages, plus
|
|
70
84
|
# BoundsCalculator's subtitle reservation). EA sizes the
|
|
@@ -79,7 +93,7 @@ module Ea
|
|
|
79
93
|
pts << [b.x, b.y]
|
|
80
94
|
pts << [b.x + b.width, b.y + b.height]
|
|
81
95
|
end
|
|
82
|
-
extra = base.
|
|
96
|
+
extra = base.subtitle_points
|
|
83
97
|
min_x = (pts + extra).map(&:first).min || 0
|
|
84
98
|
min_y = (pts + extra).map(&:last).min || 0
|
|
85
99
|
max_x = (pts + extra).map(&:first).max || 0
|
|
@@ -95,8 +109,7 @@ module Ea
|
|
|
95
109
|
|
|
96
110
|
# Same scaled rule as BoundsCalculator: drawn leftmost =
|
|
97
111
|
# min(35, 30 + min_x).
|
|
98
|
-
inset_left =
|
|
99
|
-
BoundsCalculator::INSET_LEFT].min
|
|
112
|
+
inset_left = BoundsCalculator.inset_left_for(min_x)
|
|
100
113
|
new(min_x: min_x, min_y: min_y,
|
|
101
114
|
width: (max_x - min_x) + inset_left +
|
|
102
115
|
BoundsCalculator::INSET_RIGHT,
|
|
@@ -105,6 +118,25 @@ module Ea
|
|
|
105
118
|
insets: [inset_left, FRAME_INSET_TOP])
|
|
106
119
|
end
|
|
107
120
|
|
|
121
|
+
# This canvas when every point of `shapes` ([min_x, min_y, max_x,
|
|
122
|
+
# max_y] in canvas coordinates) lies inside its frame border (the
|
|
123
|
+
# FRAME_BORDER_INSET line), otherwise a canvas grown so the shapes
|
|
124
|
+
# sit CONTENT_MARGIN inside the border on the overflowing sides.
|
|
125
|
+
def holding(shapes)
|
|
126
|
+
left, top, right, bottom = shapes
|
|
127
|
+
reach = CanvasAnchor::CONTENT_MARGIN
|
|
128
|
+
grow_left = left < FRAME_BORDER_INSET ? reach - left : 0
|
|
129
|
+
grow_top = top < FRAME_BORDER_INSET ? reach - top : 0
|
|
130
|
+
new_width = right > width - FRAME_BORDER_INSET ? right + reach : width
|
|
131
|
+
new_height = bottom > height - FRAME_BORDER_INSET ? bottom + reach : height
|
|
132
|
+
return self if [grow_left, grow_top].all?(&:zero?) &&
|
|
133
|
+
new_width == width && new_height == height
|
|
134
|
+
|
|
135
|
+
Canvas.new(min_x: min_x - grow_left, min_y: min_y - grow_top,
|
|
136
|
+
width: new_width + grow_left, height: new_height + grow_top,
|
|
137
|
+
insets: [inset_left, inset_top])
|
|
138
|
+
end
|
|
139
|
+
|
|
108
140
|
def view_box
|
|
109
141
|
"0 0 #{width} #{height}"
|
|
110
142
|
end
|
|
@@ -139,6 +171,12 @@ module Ea
|
|
|
139
171
|
|
|
140
172
|
private
|
|
141
173
|
|
|
174
|
+
# Measured extents arrive as Floats ("508.0"); EA writes whole
|
|
175
|
+
# pixels into the viewBox and background rect.
|
|
176
|
+
def whole(pixels)
|
|
177
|
+
pixels.is_a?(Float) && pixels == pixels.to_i ? pixels.to_i : pixels
|
|
178
|
+
end
|
|
179
|
+
|
|
142
180
|
def format_cm(px)
|
|
143
181
|
return "0cm" if px.nil? || px.zero?
|
|
144
182
|
|
|
@@ -25,6 +25,12 @@ module Ea
|
|
|
25
25
|
# 4881CEEE, 70C77F92, 846DD800, 88AF6BD7, AC2191E3). Below, the
|
|
26
26
|
# same box (its top is the first baseline less the font height,
|
|
27
27
|
# its height the stored CY) + 5 holds the canvas open on 95246DCF.
|
|
28
|
+
#
|
|
29
|
+
# Drawn text also holds the right and bottom edges open (text
|
|
30
|
+
# right/bottom + TEXT_MARGIN, symmetric with the origin rule), so
|
|
31
|
+
# a name wider than its box is not clipped. Checked on the 188
|
|
32
|
+
# PLATEAU natives: widths/heights unchanged (185/188, 188/188);
|
|
33
|
+
# 8DAB169F moves 13 closer; no native contradicts it.
|
|
28
34
|
module CanvasAnchor
|
|
29
35
|
CONTENT_MARGIN = 10
|
|
30
36
|
TEXT_MARGIN = 5
|
|
@@ -66,18 +72,20 @@ module Ea
|
|
|
66
72
|
origin_x = origin(shapes && shapes[0], texts && texts[0])
|
|
67
73
|
origin_y = origin(shapes && shapes[1], texts && texts[1])
|
|
68
74
|
Canvas.new(min_x: origin_x, min_y: origin_y,
|
|
69
|
-
width: right_edge(shapes, label_reach) - origin_x,
|
|
70
|
-
height: bottom_edge(shapes, label_reach, bottom) - origin_y,
|
|
75
|
+
width: right_edge(shapes, texts, label_reach) - origin_x,
|
|
76
|
+
height: bottom_edge(shapes, texts, label_reach, bottom) - origin_y,
|
|
71
77
|
insets: [0, 0])
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
def right_edge(shapes, label_reach)
|
|
80
|
+
def right_edge(shapes, texts, label_reach)
|
|
75
81
|
[shapes && shapes[2] + CONTENT_MARGIN,
|
|
82
|
+
texts && (texts[2] + TEXT_MARGIN).ceil,
|
|
76
83
|
label_reach && label_reach[0] + LABEL_BOX_MARGIN].compact.max
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
def bottom_edge(shapes, label_reach, bottom)
|
|
86
|
+
def bottom_edge(shapes, texts, label_reach, bottom)
|
|
80
87
|
[shapes[3] + bottom,
|
|
88
|
+
texts && (texts[3] + TEXT_MARGIN).ceil,
|
|
81
89
|
label_reach && label_reach[1] + LABEL_BOX_MARGIN].compact.max
|
|
82
90
|
end
|
|
83
91
|
|
|
@@ -6,6 +6,11 @@ module Ea
|
|
|
6
6
|
module Compartment
|
|
7
7
|
# Attribute compartment. Skipped when no attribute lines.
|
|
8
8
|
module Attributes
|
|
9
|
+
# The pen the first group of this compartment leaves behind.
|
|
10
|
+
def self.pen(_context)
|
|
11
|
+
Pen::ROUND_BEVEL
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
module_function
|
|
10
15
|
|
|
11
16
|
def render(context)
|
|
@@ -11,6 +11,11 @@ module Ea
|
|
|
11
11
|
# content and suppresses them when they would overflow the
|
|
12
12
|
# stored box height (never truncates rows).
|
|
13
13
|
module Constraints
|
|
14
|
+
# The pen the first group of this compartment leaves behind.
|
|
15
|
+
def self.pen(_context)
|
|
16
|
+
Pen::ROUND_BEVEL
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
module_function
|
|
15
20
|
|
|
16
21
|
def render(context)
|
|
@@ -8,6 +8,11 @@ module Ea
|
|
|
8
8
|
# italic header + each literal name. Skipped when the
|
|
9
9
|
# classifier has no literals or no content geometry.
|
|
10
10
|
module EnumLiterals
|
|
11
|
+
# The pen the first group of this compartment leaves behind.
|
|
12
|
+
def self.pen(_context)
|
|
13
|
+
Pen::ROUND_BEVEL
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
module_function
|
|
12
17
|
|
|
13
18
|
# Caption and literal rows share the same baseline spacing.
|
|
@@ -7,6 +7,11 @@ module Ea
|
|
|
7
7
|
# Header compartment: stereotype label + class name. Skips
|
|
8
8
|
# rendering when the classifier produces no header lines.
|
|
9
9
|
module Header
|
|
10
|
+
# The pen the first group of this compartment leaves behind.
|
|
11
|
+
def self.pen(_context)
|
|
12
|
+
Pen::ROUND_BEVEL
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
module_function
|
|
11
16
|
|
|
12
17
|
def render(context)
|
|
@@ -29,10 +34,12 @@ module Ea
|
|
|
29
34
|
)
|
|
30
35
|
end
|
|
31
36
|
|
|
37
|
+
# No element font size of its own: QEA marks it inherited, XMI
|
|
38
|
+
# records the missing size without the flag.
|
|
32
39
|
def default_signal_header?(context)
|
|
33
40
|
context.classifier.is_a?(Ea::Model::Signal) &&
|
|
34
41
|
context.classifier.stereotype_refs.empty? &&
|
|
35
|
-
context.element.
|
|
42
|
+
context.element.font_size.nil? &&
|
|
36
43
|
context.element.font_family.nil? &&
|
|
37
44
|
!context.element.font_bold && !context.element.font_italic
|
|
38
45
|
end
|
|
@@ -8,6 +8,11 @@ module Ea
|
|
|
8
8
|
# compartment (attributes / operations / literals / tagged
|
|
9
9
|
# values). Skipped when there is no content below.
|
|
10
10
|
module HeaderDivider
|
|
11
|
+
# The pen the first group of this compartment leaves behind.
|
|
12
|
+
def self.pen(_context)
|
|
13
|
+
Pen::ROUND_BEVEL
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
module_function
|
|
12
17
|
|
|
13
18
|
def render(context)
|
|
@@ -40,7 +45,7 @@ module Ea
|
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def object_diagram?(context)
|
|
43
|
-
context.diagram
|
|
48
|
+
context.diagram&.object_diagram?
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def hand_draw?(context)
|
|
@@ -12,6 +12,11 @@ module Ea
|
|
|
12
12
|
# InstanceSpecification or has neither slots nor a
|
|
13
13
|
# classifier.
|
|
14
14
|
module InstanceSlots
|
|
15
|
+
# The pen the first group of this compartment leaves behind.
|
|
16
|
+
def self.pen(_context)
|
|
17
|
+
Pen::ROUND_BEVEL
|
|
18
|
+
end
|
|
19
|
+
|
|
15
20
|
ROW_OFFSET = 13
|
|
16
21
|
SUBTITLE_OFFSET_FROM_BOTTOM = 15
|
|
17
22
|
|
|
@@ -8,6 +8,11 @@ module Ea
|
|
|
8
8
|
# short horizontal line below each instance name to denote
|
|
9
9
|
# that it is an instance (UML convention).
|
|
10
10
|
module InstanceUnderline
|
|
11
|
+
# The pen the first group of this compartment leaves behind.
|
|
12
|
+
def self.pen(_context)
|
|
13
|
+
Pen::SQUARE_MITER
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
Y_OFFSET = 1
|
|
12
17
|
|
|
13
18
|
module_function
|
|
@@ -36,7 +41,7 @@ module Ea
|
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def object_diagram?(context)
|
|
39
|
-
context.diagram
|
|
44
|
+
context.diagram&.object_diagram?
|
|
40
45
|
end
|
|
41
46
|
module_function :object_diagram?
|
|
42
47
|
|
|
@@ -10,6 +10,11 @@ module Ea
|
|
|
10
10
|
# Real OCL constraints render trailing, after the content —
|
|
11
11
|
# the two are distinct compartments.
|
|
12
12
|
module Marker
|
|
13
|
+
# The pen the first group of this compartment leaves behind.
|
|
14
|
+
def self.pen(_context)
|
|
15
|
+
Pen::ROUND_BEVEL
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
module_function
|
|
14
19
|
|
|
15
20
|
def render(context)
|