umlimator 0.21.0 → 0.23.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.
- package/README.md +73 -14
- package/lib/umlimator-all.js +1222 -71
- package/lib/umlimator.d.ts +77 -1
- package/lib/umlimator.js +26 -18
- package/lib/umlimator.schema.json +190 -18
- package/package.json +1 -1
package/lib/umlimator.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface MapNode {
|
|
|
44
44
|
/** A typed element in schema version 2's envelope (docs/diagrams/model.md §1): a node, a member, a typed edge, a
|
|
45
45
|
* fragment. Nodes, edges, members and fragments share one id space; an id never contains "->". A typed edge is an
|
|
46
46
|
* Element with `from` and `to`, which may name a member. A typed view is a MapView with `diagram` (uml.class), and may
|
|
47
|
-
* have `subject`, `direction` ('right' | 'down'), `notation`, `present`, `fragments` and `timeline`. umlimator/all draws `uml.class` (0.18.0), `uml.object` and `uml.package` (0.19.0), `uml.sequence` and `uml.communication` (0.20.0), `uml.use-case`, `uml.activity`, `uml.state-machine` and `uml.interaction-overview` (0.21.0); other types draw as plain cards.
|
|
47
|
+
* have `subject`, `direction` ('right' | 'down'), `notation`, `present`, `fragments` and `timeline`. umlimator/all draws `uml.class` (0.18.0), `uml.object` and `uml.package` (0.19.0), `uml.sequence` and `uml.communication` (0.20.0), `uml.use-case`, `uml.activity`, `uml.state-machine` and `uml.interaction-overview` (0.21.0), `uml.component`, `uml.deployment` and `uml.composite-structure` (0.22.0), `uml.profile` and `uml.timing` (0.23.0); other types draw as plain cards.
|
|
48
48
|
* From 0.19.0 the class, object and package types draw a shown package that owns something the view shows as a box holding it (`parent`), and
|
|
49
49
|
* qualify names (`sales::Order`) from the nearest shown owner, the view's `subject`, or the top. */
|
|
50
50
|
export interface Element {
|
|
@@ -157,6 +157,82 @@ export interface UmlTransitionProps { trigger?: string; guard?: string; effect?:
|
|
|
157
157
|
export type UmlInteractionOverviewElementType = 'uml.InitialNode' | 'uml.ActivityFinalNode' | 'uml.FlowFinalNode' | 'uml.DecisionNode'
|
|
158
158
|
| 'uml.MergeNode' | 'uml.ForkNode' | 'uml.JoinNode' | 'uml.InteractionUse' | 'uml.Interaction' | 'uml.Comment';
|
|
159
159
|
export type UmlInteractionOverviewLineType = 'uml.ControlFlow' | 'uml.CommentLink';
|
|
160
|
+
/** The component diagram's vocabulary (a view with `diagram: 'uml.component'`, umlimator/all from 0.22.0;
|
|
161
|
+
* docs/conformance/uml-component.md). A component's `parent` is a component (drawn nested, a box) or a package; its
|
|
162
|
+
* ports (a class's too) are members, drawn on its border. A provided interface is always an `uml.Interface` node, joined
|
|
163
|
+
* to its provider (a component, a class or a port) by a realization and to its requirers by usages; `present.<id>.as:
|
|
164
|
+
* 'ball'` draws it as a ball, a usage to it ending in a socket. */
|
|
165
|
+
export type UmlComponentElementType = 'uml.Component' | 'uml.Interface' | 'uml.Artifact' | 'uml.Package' | 'uml.Comment';
|
|
166
|
+
export type UmlComponentMemberType = 'uml.Port';
|
|
167
|
+
export type UmlComponentLineType = 'uml.InterfaceRealization' | 'uml.ComponentRealization' | 'uml.Usage' | 'uml.Connector' | 'uml.Dependency'
|
|
168
|
+
| 'uml.Manifestation' | 'uml.CommentLink';
|
|
169
|
+
/** A port's props: its type, drawn `name : Type` beside it. */
|
|
170
|
+
export interface UmlPortProps { valueType?: string; stereotypes?: string[]; [key: string]: unknown }
|
|
171
|
+
/** A connector's props: in a component view its kind (an assembly when left out); in a composite structure view, an end
|
|
172
|
+
* through a part (UML's partWithPort): `ends.from.part` or `ends.to.part` names the part, `from` or `to` the port of its type. */
|
|
173
|
+
export interface UmlConnectorProps { kind?: 'assembly' | 'delegation'; ends?: { from?: { part?: string }; to?: { part?: string } }; stereotypes?: string[]; [key: string]: unknown }
|
|
174
|
+
/** What a component or composite structure view shows of one element, beside UmlPresent: an interface as a ball; a port's
|
|
175
|
+
* side; on a part, each of its type's ports' sides. */
|
|
176
|
+
export interface UmlStructurePresent { as?: 'ball'; side?: 'left' | 'right' | 'top' | 'bottom'; ports?: Record<string, { side?: 'left' | 'right' | 'top' | 'bottom' }> }
|
|
177
|
+
/** The deployment diagram's vocabulary (a view with `diagram: 'uml.deployment'`, umlimator/all from 0.22.0;
|
|
178
|
+
* docs/conformance/uml-deployment.md). A node, device or execution environment that holds something shown is a box drawn
|
|
179
|
+
* as a cube; an artifact or deployment specification whose `parent` is one is deployed on it, a `uml.Deployment` line
|
|
180
|
+
* from it to one is the other form. */
|
|
181
|
+
export type UmlDeploymentElementType = 'uml.Node' | 'uml.Device' | 'uml.ExecutionEnvironment' | 'uml.Artifact' | 'uml.DeploymentSpecification'
|
|
182
|
+
| 'uml.Component' | 'uml.Comment';
|
|
183
|
+
export type UmlDeploymentLineType = 'uml.Deployment' | 'uml.Manifestation' | 'uml.CommunicationPath' | 'uml.Dependency' | 'uml.CommentLink';
|
|
184
|
+
/** The composite structure diagram's vocabulary (a view with `diagram: 'uml.composite-structure'`, umlimator/all from
|
|
185
|
+
* 0.22.0; docs/conformance/uml-composite-structure.md). The view's `subject` (a class, component or collaboration, listed
|
|
186
|
+
* in `nodeIds`) is drawn as a box holding its parts (`uml.Property` members) and collaboration uses (`parent` the
|
|
187
|
+
* subject); a part shows the ports of the class or component its `valueType` names by label, each drawn under the id
|
|
188
|
+
* `<part>-><port>`. An open collaboration (listed in `nodeIds`) is a dashed ellipse round its roles. A role binding runs
|
|
189
|
+
* from a part to a collaboration use, its `label` the role. */
|
|
190
|
+
export type UmlCompositeStructureElementType = 'uml.Class' | 'uml.Component' | 'uml.Collaboration' | 'uml.CollaborationUse' | 'uml.Comment';
|
|
191
|
+
export type UmlCompositeStructureMemberType = 'uml.Property' | 'uml.Port';
|
|
192
|
+
export type UmlCompositeStructureLineType = 'uml.Connector' | 'uml.RoleBinding' | 'uml.CommentLink';
|
|
193
|
+
/** A part's (or role's) props: the class or component it is, by label; its multiplicity; shared parts are drawn dashed. */
|
|
194
|
+
export interface UmlPartProps { valueType?: string; multiplicity?: string; aggregation?: 'composite' | 'shared'; stereotypes?: string[]; [key: string]: unknown }
|
|
195
|
+
/** A collaboration use's props: the collaboration it uses, by label. */
|
|
196
|
+
export interface UmlCollaborationUseProps { collaboration?: string; stereotypes?: string[]; [key: string]: unknown }
|
|
197
|
+
/** Any UML element's tagged values (0.23.0): by stereotype (as written in `stereotypes`, `Profile::name` when qualified, or
|
|
198
|
+
* a required stereotype's name), then by tag name, each value as text. Checked against the profiles applied to the
|
|
199
|
+
* element's packages (docs/conformance/uml-profile.md); edited in the properties panel on every typed view. */
|
|
200
|
+
export type UmlTags = Record<string, Record<string, string>>;
|
|
201
|
+
/** The props any UML element may hold from 0.23.0: its stereotypes (a name, or `Profile::name`) and their tags. */
|
|
202
|
+
export interface UmlStereotypedProps { stereotypes?: string[]; tags?: UmlTags; [key: string]: unknown }
|
|
203
|
+
/** The profile diagram's vocabulary (a view with `diagram: 'uml.profile'`, umlimator/all from 0.23.0;
|
|
204
|
+
* docs/conformance/uml-profile.md). A stereotype's and a metaclass reference's `parent` is a profile; an enumeration's
|
|
205
|
+
* (a tag's type) may be. A metaclass reference's `label` names a UML metaclass (Class, Property, Classifier, …). A
|
|
206
|
+
* stereotype's tag definitions are `uml.Property` members (`name : Type [multiplicity] = default`). Any element that
|
|
207
|
+
* applies a stereotype may be shown, with a compartment of tags for each stereotype it has. */
|
|
208
|
+
export type UmlProfileElementType = 'uml.Profile' | 'uml.Stereotype' | 'uml.MetaclassReference' | 'uml.Enumeration' | 'uml.Package' | 'uml.Comment';
|
|
209
|
+
export type UmlProfileMemberType = 'uml.Property' | 'uml.EnumerationLiteral';
|
|
210
|
+
/** An extension from a stereotype to a metaclass reference; a profile application from a package to a profile; a
|
|
211
|
+
* generalization between stereotypes. */
|
|
212
|
+
export type UmlProfileLineType = 'uml.Extension' | 'uml.ProfileApplication' | 'uml.Generalization' | 'uml.CommentLink';
|
|
213
|
+
/** A stereotype's props: abstract (applied through a specialization). */
|
|
214
|
+
export interface UmlStereotypeProps { isAbstract?: boolean; [key: string]: unknown }
|
|
215
|
+
/** An extension's props: required (the stereotype is applied to every element of the metaclass in its profile's scope). */
|
|
216
|
+
export interface UmlExtensionProps { isRequired?: boolean; [key: string]: unknown }
|
|
217
|
+
/** The timing diagram's vocabulary (a view with `diagram: 'uml.timing'`, umlimator/all from 0.23.0;
|
|
218
|
+
* docs/conformance/uml-timing.md). Lifelines are model nodes the view lists in `nodeIds`, top to bottom; messages are inline
|
|
219
|
+
* in the view's `edges`; changes and constraints are in its `timeline`. `present.<lifeline>.as: 'values'` draws a value
|
|
220
|
+
* lifeline. The scale is derived, never stored. */
|
|
221
|
+
export type UmlTimingElementType = 'uml.Lifeline';
|
|
222
|
+
export type UmlTimingLineType = 'uml.Message';
|
|
223
|
+
/** A timing lifeline's props: what it represents, and its states, top to bottom (ignored by the sequence and
|
|
224
|
+
* communication types). */
|
|
225
|
+
export interface UmlTimingLifelineProps { represents?: string; states?: string[]; stereotypes?: string[]; tags?: UmlTags; [key: string]: unknown }
|
|
226
|
+
/** A timing message's props: a sort of call, async or reply, and when it is sent and received, in the view's unit. */
|
|
227
|
+
export interface UmlTimingMessageProps { sort?: 'call' | 'async' | 'reply'; replyTo?: string; sentAt?: number; receivedAt?: number; [key: string]: unknown }
|
|
228
|
+
/** A change of state on a lifeline at a time; an element of the model's id space, drawn by `at`. */
|
|
229
|
+
export interface UmlTimingChange { id: string; lifeline: string; at: number; state: string }
|
|
230
|
+
/** A duration constraint from one change to a later one, or a time constraint on one change. */
|
|
231
|
+
export interface UmlTimingConstraint { id: string; type: 'uml.DurationConstraint' | 'uml.TimeConstraint'; from: string; to?: string; label?: string }
|
|
232
|
+
/** A timing view's `timeline`: the ruler's unit (optional), its changes in any order, its constraints. */
|
|
233
|
+
export interface UmlTimeline { unit?: string; changes?: UmlTimingChange[]; constraints?: UmlTimingConstraint[] }
|
|
234
|
+
/** What a timing view shows of a lifeline, in its `present`: drawn as a value lifeline. */
|
|
235
|
+
export interface UmlTimingPresent { as?: 'values' }
|
|
160
236
|
/** What a line means, drawn as its colour: calls (the default), feeds (hands work on), triggers (suggests, offers, fires),
|
|
161
237
|
* state (reads or writes a file, board or config: a thinner line, a muted label, no flow) and guard (a hook steps in).
|
|
162
238
|
* Colours are --um-w-<kind>. An edge's `style` (emphasis, dashed, security) wins over its kind's colour. */
|
package/lib/umlimator.js
CHANGED
|
@@ -123,7 +123,7 @@ function registerDiagramType(name, def = {}) {
|
|
|
123
123
|
reroute is no registry field: reroute: false goes in the dg the type's own layout passes to layoutTyped (src/typed/layout.js).
|
|
124
124
|
Stage 4's (notes/stage-4.md §12.1, §15): edits.rename(model, view, id, text); a hook's { pick, then }; name: false and
|
|
125
125
|
opens on a palette entry; a line's cut, its label's width (150); rows(model, view, id) and word(model, id), the PDF's.
|
|
126
|
-
Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, a view's labelGaps (src/typed/layout.js);
|
|
126
|
+
Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, head, a view's labelGaps (src/typed/layout.js);
|
|
127
127
|
edits.member, edits.key (src/typed/edit.js). */
|
|
128
128
|
|
|
129
129
|
/* A view's type: the map for a view with no diagram, the registered type, or null for one this release does not draw */
|
|
@@ -131,7 +131,9 @@ const diagramOf = view => TYPES.get(view && view.diagram != null ? view.diagram
|
|
|
131
131
|
|
|
132
132
|
/* What only a registered type's geometry uses, installed at load time by the files under src/typed/, which only
|
|
133
133
|
lib/umlimator-all.js holds (tools/build.mjs): rows, rooms and routes in layoutPass, a typed line's ends in svgOf, the
|
|
134
|
-
gestures of edit.js it takes over (edit)
|
|
134
|
+
gestures of edit.js it takes over (edit), diff()'s words for typed models (diff, order: src/uml/diff.js). Empty in
|
|
135
|
+
umlimator.js, where no view is laid out but as a map (risks.md C5).
|
|
136
|
+
Stage 6's: across(model), validate()'s rules on the whole model; fields(model, x), more panel fields (notes/stage-6.md §5) */
|
|
135
137
|
const TYPED = {};
|
|
136
138
|
|
|
137
139
|
const propRefs = type => Object.assign({}, ...[...TYPES.values()].map(T => T.refs && Object.hasOwn(T.refs, type) && T.refs[type]));
|
|
@@ -248,6 +250,7 @@ function refs(m, ids, only) {
|
|
|
248
250
|
arr(v.nodes).forEach((n, j) => element(n, [...at, 'nodes', j], false));
|
|
249
251
|
arr(v.edges).forEach((e, j) => { if (has(e)) say([...at, 'edges', j], 'view'); else element(e, [...at, 'edges', j], true); });
|
|
250
252
|
arr(v.fragments).forEach((f, j) => element(f, [...at, 'fragments', j], false));
|
|
253
|
+
['changes', 'constraints'].forEach(k => arr(isObj(v.timeline) && v.timeline[k]).forEach((c, j) => isObj(c) && ['lifeline', 'from', 'to'].forEach(f => { if (has(c[f])) say([...at, 'timeline', k, j, f], 'end'); })));
|
|
251
254
|
[['layout', 'pin'], ['present', 'present'], ['nodeOverrides', 'override']].forEach(([k, kind]) => Object.keys(isObj(v[k]) ? v[k] : {}).forEach(id => { if (has(id)) say([...at, k, id], kind); }));
|
|
252
255
|
if (has(v.subject)) say([...at, 'subject'], 'subject');
|
|
253
256
|
if (!only) ['parent', 'node'].forEach(k => { if (has(v[k])) say([...at, k], 'parent'); }); // a view opened from a card
|
|
@@ -374,9 +377,9 @@ function run(m, c, gone) {
|
|
|
374
377
|
const take = id => { const r = at.get(id); if (!r || now.has(id)) return; now.add(id);
|
|
375
378
|
arr(r.x.members).forEach(y => isObj(y) && take(y.id)); arr(kids.get(id)).forEach(take); }; // what it owns goes with it
|
|
376
379
|
take(c.id);
|
|
377
|
-
// lines to what is gone go too, lines to those lines, and what a type's refs cascade (a reply with its call)
|
|
380
|
+
// lines to what is gone go too, lines to those lines, a timeline's items, and what a type's refs cascade (a reply with its call)
|
|
378
381
|
const cascade = x => Object.entries(propRefs(x.type)).some(([p, how]) => how === 'cascade' && [].concat(dig(x, p)).some(id => now.has(id)));
|
|
379
|
-
for (let more = true; more;) { more = false; for (const [k, o] of at) if (!now.has(k) && (o.kind === 'edge' && (now.has(o.x.from) || now.has(o.x.to)) || cascade(o.x))) { take(k); more = true; } }
|
|
382
|
+
for (let more = true; more;) { more = false; for (const [k, o] of at) if (!now.has(k) && (o.kind === 'edge' && (now.has(o.x.from) || now.has(o.x.to)) || o.kind === 'time' && [o.x.lifeline, o.x.from, o.x.to].some(i => now.has(i)) || cascade(o.x))) { take(k); more = true; } }
|
|
380
383
|
// what stays in something gone goes to its nearest owner left; an operand's messages to its fragment's parent
|
|
381
384
|
const above = id => { const r = at.get(id); return r && (r.x.parent ?? (r.owner && above(r.owner.id))); };
|
|
382
385
|
for (const [k, o] of at) if (!now.has(k) && now.has(o.x.parent)) { let p = o.x.parent;
|
|
@@ -1035,7 +1038,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1035
1038
|
// A typed view's boxes (notes/stage-2.md §3.2) take the same code as a map's: their title is the type's (`title`), and a box
|
|
1036
1039
|
// grows to hold it; laid out turned (direction down), the 24 px header room goes left, the side that becomes the top
|
|
1037
1040
|
const familyDefs = dg.containers || [], tagged = n => map && (n.author || n.k === 'm-ext'), TB = !map && dg.turned; // tagged: a name tag hangs under the card
|
|
1038
|
-
const headW = f => textW(!map && f.title || f.label || f.id, 11) + 16; // a box's title, as the audit measures it
|
|
1041
|
+
const headW = f => textW(!map && f.title || f.label || f.id, !map && f.head ? 12 : 11) + 16; // a box's title, as the audit measures it (two lines: at 12 px)
|
|
1039
1042
|
const framesNow = () => { const cache = Object.create(null); // a box may be named 'constructor'
|
|
1040
1043
|
const frameOf = (f, trail = new Set()) => {
|
|
1041
1044
|
if (cache[f.id]) return cache[f.id];
|
|
@@ -1048,7 +1051,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1048
1051
|
const p = 12, x = Math.min(...rects.map(r => r.x)) - p - (TB ? 24 : 0), y = Math.min(...rects.map(r => r.y)) - p - (TB ? 0 : 24);
|
|
1049
1052
|
const b = {...f, x, y, w:Math.max(...rects.map(r => r.x+r.w)) + p - x, h:Math.max(...rects.map(r => r.y+r.h)) + p - y};
|
|
1050
1053
|
if (!map) b[TB ? 'h' : 'w'] = Math.max(b[TB ? 'h' : 'w'], headW(f) + 16);
|
|
1051
|
-
return cache[f.id] = dg.typed ? TYPED.frame(b) : b;
|
|
1054
|
+
return cache[f.id] = dg.typed ? TYPED.frame(b, TB) : b;
|
|
1052
1055
|
};
|
|
1053
1056
|
const out = familyDefs.map(f => frameOf(f)).filter(Boolean);
|
|
1054
1057
|
if (map) out.forEach(f => { // the box fits its title and its badges, which are tabs on its top border, at the right
|
|
@@ -1450,7 +1453,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
|
|
|
1450
1453
|
const runRouting = order => {
|
|
1451
1454
|
routed.length = 0; reserved.length = 0; placed.length = 0; stats = {};
|
|
1452
1455
|
(dg.blocks || []).forEach(b => { reserved.push(b); placed.push(b); });
|
|
1453
|
-
families.forEach(f => { const h = TB ? { x: f.x + 3, y: f.y + 8, w:
|
|
1456
|
+
families.forEach(f => { const t = 22 + (!map && f.head || 0), h = TB ? { x: f.x + 3, y: f.y + 8, w: t, h: headOf(f) } : { x: f.x + 8, y: f.y + 3, w: headOf(f), h: t }; reserved.push(h); placed.push(h);
|
|
1454
1457
|
placed.push(...[{ x: f.x, y: f.y - 3, w: f.w, h: 6 }, { x: f.x, y: f.y + f.h - 3, w: f.w, h: 6 }, { x: f.x - 3, y: f.y, w: 6, h: f.h }, { x: f.x + f.w - 3, y: f.y, w: 6, h: f.h }].map(r => ({ ...r, band: true }))); }); // lane headers and arrows stay clear
|
|
1455
1458
|
edges.filter(e => e.fixed).forEach(e => { routed.push(e); if (e.under || e[OWN_ROUTE]) { placeLabel(e); stats[e.how] = (stats[e.how] || 0) + 1; return; } if (e.lbl) { placed.push(e.lbl); reserved.push({ x: e.lbl.x - 3, y: e.lbl.y - 3, w: e.lbl.w + 6, h: e.lbl.h + 6 }); } stats.comb = (stats.comb || 0) + 1; });
|
|
1456
1459
|
order.forEach(e => {
|
|
@@ -1687,7 +1690,7 @@ function routeAudit(geo, where, only, refs) { // only: a set of edges; counts ju
|
|
|
1687
1690
|
if (f.parent && (!parent || !inside(parent,f))) say(`${where}: family "${f.label}" sticks out of its parent`, f);
|
|
1688
1691
|
(f.nodeIds || []).forEach(id => { const n = geo.byId[id]; if (n && !inside(f,n)) say(`${where}: family "${f.label}" does not contain "${n.label}"`, f, n); });
|
|
1689
1692
|
families.filter(g => g !== f && g.parent === f.parent && g.id > f.id).forEach(g => { if (hit(f,g,-1)) say(`${where}: families "${f.label}" and "${g.label}" overlap`, f, g); });
|
|
1690
|
-
geo.edges.forEach((e,i) => { if (segHit({x:f.x+8,y:f.y+3,w:Math.min(f.w-16,textW(f.label || f.id,11)+16),h:22},i)) say(`${where}: family "${f.label}": header crossed by edge ${name(e)}`, f, e); });
|
|
1693
|
+
geo.edges.forEach((e,i) => { if (segHit({x:f.x+8,y:f.y+3,w:Math.min(f.w-16,textW(f.label || f.id,11)+16),h:22+(!geo.map&&f.head||0)},i)) say(`${where}: family "${f.label}": header crossed by edge ${name(e)}`, f, e); });
|
|
1691
1694
|
geo.edges.forEach((e,i) => { if (segs[i].some(([a,b]) => { const v = a[0] === b[0], c = v ? a[0] : a[1], p = v ? [a[1], b[1]] : [a[0], b[0]];
|
|
1692
1695
|
const lo = Math.max(Math.min(...p), v ? f.y : f.x), hi = Math.min(Math.max(...p), v ? f.y + f.h : f.x + f.w);
|
|
1693
1696
|
return hi - lo > 40 && (v ? [f.x, f.x + f.w] : [f.y, f.y + f.h]).some(u => Math.abs(c - u) < 8); })) say(`${where}: edge ${name(e)} runs along the border of family "${f.label}"`, e, f); });
|
|
@@ -1702,7 +1705,7 @@ const box = n => ({ x: n.x, y: n.y, w: n.w, h: n.h });
|
|
|
1702
1705
|
function overlapIssues(geo, where, refs) {
|
|
1703
1706
|
const issues = [], nodes = geo.nodes, say = teller(issues, refs);
|
|
1704
1707
|
const inside = (a, b) => a.x >= b.x - 1 && a.y >= b.y - 1 && a.x + a.w <= b.x + b.w + 1 && a.y + a.h <= b.y + b.h + 1;
|
|
1705
|
-
const holds = (a, b) => (a.holder && inside(b, a)) || (b.holder && inside(a, b)); // a holder (a sequence's frame) may hold what it meets, not cross it
|
|
1708
|
+
const holds = (a, b) => (a.holder && inside(b, a)) || (b.holder && inside(a, b)) || !geo.map && ((a.owner || 0) === b.id || (b.owner || 0) === a.id); // a holder (a sequence's frame) may hold what it meets, not cross it; a port its owner
|
|
1706
1709
|
for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) if (hit(box(nodes[i]), box(nodes[j]), -1) && !holds(nodes[i], nodes[j])) say(`${where}: nodes "${nodes[i].label}" and "${nodes[j].label}" overlap`, nodes[i], nodes[j]);
|
|
1707
1710
|
return issues;
|
|
1708
1711
|
}
|
|
@@ -2614,9 +2617,10 @@ export function validate(model) {
|
|
|
2614
2617
|
out.push(...typedProblems(m).filter(p => !(p.level === 'error' && said.has(p.path))));
|
|
2615
2618
|
views.forEach((v, i) => { const at = `views[${i}].diagram`; // a well-formed name of a type this release does not draw (M13)
|
|
2616
2619
|
if (typedView(v) && !diagramOf(v) && !out.some(p => p.path === at)) warn(at, `diagram "${v.diagram}" is not drawn by this release: its nodes are drawn as plain cards`); });
|
|
2617
|
-
const once = new Set()
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
+
const once = new Set(), say = p => { const k = JSON.stringify([p.level, p.path, p.message]); if (!once.has(k)) { once.add(k); out.push(p); } };
|
|
2621
|
+
// each view's type's rules, a problem once; a validate(model) that takes no view, once a type
|
|
2622
|
+
views.filter(typedView).forEach(v => { const T = diagramOf(v); if (!T || !T.validate || once.has(T)) return; if (T.validate.length < 2) once.add(T); T.validate(m, v).forEach(say); });
|
|
2623
|
+
if (TYPED.across) arrOf(TYPED.across(m)).forEach(say);
|
|
2620
2624
|
}
|
|
2621
2625
|
return out;
|
|
2622
2626
|
}
|
|
@@ -2773,14 +2777,16 @@ export function diff(before, after) {
|
|
|
2773
2777
|
if (h === 'removed') said.push(`removes ${name(n.id)}`);
|
|
2774
2778
|
if (h === 'changed') { const [o, x] = was.get(n), keys = diffKeys([o, x]), rest = keys.filter(k => k !== 'label'), p0 = o.parent ?? null, p1 = x.parent ?? null;
|
|
2775
2779
|
if (keys.join() === 'parent' && p0 !== p1 && A.schemaVersion === 2 && out.schemaVersion === 2) return said.push(`moves ${name(n.id)} ${p1 != null ? 'into ' + name(p1) : 'out of ' + name(p0)}`); // a re-own (schema version 2; absent or null is no owner)
|
|
2780
|
+
const t = TYPED.diff && TYPED.diff(o, x, name, [A, out]); if (t) return said.push(...t); // a typed element's own words (src/uml/diff.js)
|
|
2776
2781
|
said.push((keys.includes('label') ? `renames ${o.label || o.id} to ${x.label || x.id}` : `changes ${name(n.id)}`) + (rest.length ? ` (${rest.join(', ')})` : '')); } });
|
|
2777
2782
|
lines.forEach(e => { const h = how.get(e); if (!h || cardHow.get(e.from) === h && h !== 'changed' || cardHow.get(e.to) === h && h !== 'changed') return;
|
|
2778
2783
|
const ends = `${name(e.from)} to ${name(e.to)}`, lab = e.label ? ` (${e.label})` : '';
|
|
2779
2784
|
if (h === 'added') said.push(`adds a line from ${ends}${lab}`);
|
|
2780
2785
|
if (h === 'removed') said.push(`removes the line from ${ends}${lab}`);
|
|
2781
|
-
if (h === 'changed') { const [o, x] = was.get(e);
|
|
2786
|
+
if (h === 'changed') { const [o, x] = was.get(e), t = TYPED.diff && TYPED.diff(o, x, name, [A, out]); if (t) return said.push(...t);
|
|
2782
2787
|
said.push(`changes the line from ${ends} (${diffKeys([o, x]).map(k => k === 'label' ? `label ${o.label ?? '-'} → ${x.label ?? '-'}` : k).join(', ')})`); } });
|
|
2783
2788
|
moved.forEach(([verb, id, v]) => said.push(`${verb} ${name(id)} ${verb === 'puts' ? 'in' : 'out of'} the view ${v.title || v.id}`));
|
|
2789
|
+
if (TYPED.order) TYPED.order(A, out, views, V0, said, touched, name);
|
|
2784
2790
|
|
|
2785
2791
|
// the views that show a change: a card or line the diff marks, or a card it put in or took out
|
|
2786
2792
|
const changed = views.filter(v => v && (touched.has(v.id) || arr(v.nodeIds).some(id => cardHow.has(id)) || linesOf(out, v).some(e => how.has(e)))).map(v => v.id);
|
|
@@ -2834,9 +2840,11 @@ const walkFrom = (edges, start, back) => { const seen = new Set([start]); // bre
|
|
|
2834
2840
|
for (let q = [start]; q.length;) { const u = q.shift(); edges.forEach(e => { const [a, b] = back ? [e.to, e.from] : [e.from, e.to]; if (a === u && !seen.has(b)) { seen.add(b); q.push(b); } }); }
|
|
2835
2841
|
return seen; };
|
|
2836
2842
|
|
|
2843
|
+
const viewType = (model, viewId) => model && model.schemaVersion === 2 && diagramOf(arrOf(model.views).find(v => v && v.id === viewId));
|
|
2837
2844
|
/** Every card and line on a path from one card to another, following lines forward: the cards both reachable from `from`
|
|
2838
2845
|
* and leading to `to`, and the lines between them. [] when no path joins them. */
|
|
2839
2846
|
export function paths(model, viewId, from, to) {
|
|
2847
|
+
const T = viewType(model, viewId); if (T && T.paths) return T.paths(model, viewId, from, to); // a type's own walk (a sequence's, in time)
|
|
2840
2848
|
const { ids, edges } = lineGraph(model, viewId);
|
|
2841
2849
|
if (!ids.includes(from) || !ids.includes(to)) return [];
|
|
2842
2850
|
const F = walkFrom(edges, from), B = walkFrom(edges, to, true), on = id => F.has(id) && B.has(id);
|
|
@@ -2861,7 +2869,7 @@ export function reach(model, viewId, from, options = {}) {
|
|
|
2861
2869
|
/** A view in plain sentences: its cards and their kinds, its boxes, what each card's lines do (in run order on a sequence
|
|
2862
2870
|
* view), and the cards with no line. The same model gives the same words: a text alternative, docs, or an AI's context. */
|
|
2863
2871
|
export function describe(model, viewId) {
|
|
2864
|
-
const T =
|
|
2872
|
+
const T = viewType(model, viewId); if (T && T.describe) return T.describe(model, viewId); // a type's own words (1.9)
|
|
2865
2873
|
const { ctx, v, ids, edges } = lineGraph(model, viewId), boxes = (v.containers || []).filter(c => c && c.id);
|
|
2866
2874
|
const name = id => ctx.N[id] ? ctx.nodeIn(id, viewId).label || id : (boxes.find(c => c.id === id) || {}).label || id;
|
|
2867
2875
|
const list = xs => xs.length < 3 ? xs.join(' and ') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
|
|
@@ -3642,7 +3650,7 @@ function typedEditor(api) {
|
|
|
3642
3650
|
function props() {
|
|
3643
3651
|
const at = elementsOf(model()), id = selId(at), e = id && at.get(id);
|
|
3644
3652
|
if (!e) return '<h3>Properties</h3><p class="um-note">Select a card or a line</p>';
|
|
3645
|
-
const x = e.x, P = isRec(x.props) ? x.props : {},
|
|
3653
|
+
const x = e.x, P = isRec(x.props) ? x.props : {}, F = a => arrOf(a).filter(f => isRec(f) && typeof f.key === 'string'), own = F(isRec(def().props) ? def().props[x.type] : null), schema = [...own, ...F(TYPED.fields && TYPED.fields(model(), x))];
|
|
3646
3654
|
const off = can('rename') ? '' : ' disabled'; // the editable flags: a field the host does not offer is shown, to read
|
|
3647
3655
|
const text = (f, v, name, more = '') => `<label>${esc(name)}<input data-f="${esc(f)}" data-was="${esc(v)}" value="${esc(v)}"${more}${off}></label>`;
|
|
3648
3656
|
let h = `<h3>Properties${x.type != null ? ': ' + esc(x.type) : ''}</h3>` + text('label', x.label ?? '', 'Name');
|
|
@@ -3657,7 +3665,7 @@ function typedEditor(api) {
|
|
|
3657
3665
|
const offer = Array.isArray(os) && [{ id: '', label: 'none' }, ...os.filter(o => isRec(o) && o.id != null && !under(o.id))];
|
|
3658
3666
|
if (offer && x.parent != null && !offer.some(o => o.id === x.parent)) offer.push({ id: x.parent, label: (at.get(x.parent) || { x: {} }).x.label });
|
|
3659
3667
|
if (offer) h += `<label>Owner<select data-f="owner" data-owner${can('move') ? '' : ' disabled'}>${offer.map(o => `<option value="${esc(o.id)}"${o.id === (x.parent ?? '') ? ' selected' : ''}>${esc(o.label ?? o.id)}</option>`).join('')}</select></label>`;
|
|
3660
|
-
const rest =
|
|
3668
|
+
const rest = own.length ? [] : Object.entries(P); // no schema: what it holds, to read
|
|
3661
3669
|
if (rest.length) h += '<dl>' + rest.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(typeof v === 'string' ? v : JSON.stringify(v))}</dd>`).join('') + '</dl>';
|
|
3662
3670
|
const M = def().member;
|
|
3663
3671
|
if (e.kind === 'node' && !(M && M.holds && !M.holds(x))) { // a comment holds none
|
|
@@ -3843,7 +3851,7 @@ export function render(container, model, options = {}) {
|
|
|
3843
3851
|
const emitSel = (again = false) => {
|
|
3844
3852
|
const now = JSON.stringify(picked()); if (now === said && !again) return; said = now;
|
|
3845
3853
|
say(!sel ? 'Nothing selected' : (sel.type === 'node' ? nameOf(sel.id) : sel.type === 'edge' ? (e => e ? `Line from ${nameOf(e.from)} to ${nameOf(e.to)}` : 'Line')(geo.edges[edgeIndex(sel)])
|
|
3846
|
-
: `Box ${
|
|
3854
|
+
: `Box ${[...svg.querySelectorAll(`.family[data-family="${CSS.escape(sel.id)}"] .family-title`)].map(t => t.textContent).join(' ') || sel.id}`) + (more.length ? `, ${more.length + 1} ${picked().every(r => r.type === 'node') ? 'cards ' : ''}selected` : ', selected'));
|
|
3847
3855
|
emit('select', { target: copyRef(sel), targets: picked() });
|
|
3848
3856
|
};
|
|
3849
3857
|
const edgeRef = e => ({ type: 'edge', ids: edgeRefIds(e), view: cur });
|
|
@@ -4288,7 +4296,7 @@ export function render(container, model, options = {}) {
|
|
|
4288
4296
|
if (ref.type === 'node') { const n = geo.byId[ref.id]; return n ? { x: n.x + 50, y: n.y + n.h / 2 - 13, w: n.w - 58, h: 26 } : null; }
|
|
4289
4297
|
if (ref.type === 'edge') { const e = geo.edges[edgeIndex(ref)]; if (!e) return null; const L = e.lbl, m = e.pts[Math.floor(e.pts.length / 2)];
|
|
4290
4298
|
return L ? { x: L.x - 8, y: L.y - 2, w: L.w + 16, h: L.h + 4 } : { x: m[0] - 60, y: m[1] - 12, w: 120, h: 24 }; }
|
|
4291
|
-
const f = ref.type === 'container' && (geo.families || []).find(x => x.id === ref.id); return f ? { x: f.x + 8, y: f.y + 2, w: Math.min(f.w - 16, 220), h: 24 } : null; };
|
|
4299
|
+
const f = ref.type === 'container' && (geo.families || []).find(x => x.id === ref.id); return f ? { x: f.x + 8, y: f.y + 2 + (f.head || 0), w: Math.min(f.w - 16, 220), h: 24 } : null; };
|
|
4292
4300
|
function editor(where, text, done, ref = null) { // where: a box in map units (a new card's), or ref names what the field sits over
|
|
4293
4301
|
if (naming) naming.close(false);
|
|
4294
4302
|
const inp = document.createElement('input');
|