@istar-ts/core 0.14.2 → 0.15.0-beta.1

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/dist/index.js CHANGED
@@ -762,14 +762,204 @@ function defaultHubPosition(model, source, target, size) {
762
762
  y: (a.y + b.y) / 2 - size.height / 2
763
763
  };
764
764
  }
765
+ /** Space kept between inner elements and an actor's boundary (piStar's `updateBoundary` adds 10px). */
766
+ const ACTOR_PADDING = 10;
767
+ /** A kind's default size, absent any per-host override (see `actorBounds`' `defaultSizeOf`). */
768
+ function metamodelSizeOf(metamodel, kind) {
769
+ return metamodel.elements.get(kind)?.size ?? {
770
+ width: 0,
771
+ height: 0
772
+ };
773
+ }
774
+ /**
775
+ * An actor's boundary: the kind's default-size rectangle at the actor's position, grown to
776
+ * include every inner element (each padded by `ACTOR_PADDING`), like piStar's `updateBoundary`
777
+ * (which floors on the kind's default size, not the actor's last expanded `display` size; an
778
+ * inner element's own `display` size does override, same as its rendered size elsewhere).
779
+ * Shared by `@istar-ts/react`'s node layout and `@istar-ts/glsp`'s GModel, so an actor draws the
780
+ * same box in both: children are looked up by category (`metamodelOf`), not `istar.*` names.
781
+ *
782
+ * `defaultSizeOf` resolves a kind's default size; it defaults to the metamodel's, but a host
783
+ * whose registry overrides a kind's size independently of the metamodel (`@istar-ts/react`'s
784
+ * `IstarRegistry`) must pass one that reads its own sizes, or its boundary would stop matching
785
+ * what it actually draws.
786
+ */
787
+ function actorBounds(model, actorId, metamodel = metamodelOf(model), defaultSizeOf = (kind) => metamodelSizeOf(metamodel, kind)) {
788
+ const actor = model.elements.get(actorId);
789
+ if (!actor) throw new Error(`no element "${actorId}" in the model`);
790
+ const meta = metamodel;
791
+ const ownSize = defaultSizeOf(actor.kind);
792
+ let minX = actor.x;
793
+ let minY = actor.y;
794
+ let maxX = actor.x + ownSize.width;
795
+ let maxY = actor.y + ownSize.height;
796
+ for (const child of model.elements.values()) {
797
+ if (child.parent !== actorId) continue;
798
+ if (meta.elements.get(child.kind)?.category === "actor") continue;
799
+ const base = defaultSizeOf(child.kind);
800
+ const width = child.display?.width ?? base.width;
801
+ const height = child.display?.height ?? base.height;
802
+ minX = Math.min(minX, child.x - 10);
803
+ minY = Math.min(minY, child.y - 10);
804
+ maxX = Math.max(maxX, child.x + width + 10);
805
+ maxY = Math.max(maxY, child.y + height + 10);
806
+ }
807
+ return {
808
+ x: minX,
809
+ y: minY,
810
+ width: maxX - minX,
811
+ height: maxY - minY
812
+ };
813
+ }
814
+ /** Space kept between a suggested box and any existing element (`suggestPosition`). */
815
+ const SUGGEST_MARGIN = 20;
816
+ /** Step between candidate positions on the grid `suggestPosition` scans. */
817
+ const SUGGEST_STEP = 40;
818
+ /** How many rings out `suggestPosition` scans before falling back to `guaranteedFreePoint`. */
819
+ const SUGGEST_MAX_RING = 25;
820
+ function boxesOverlap(a, b) {
821
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
822
+ }
823
+ function padded(box, margin) {
824
+ return {
825
+ x: box.x - margin,
826
+ y: box.y - margin,
827
+ width: box.width + margin * 2,
828
+ height: box.height + margin * 2
829
+ };
830
+ }
831
+ function firstFreeCandidate(candidates, size, obstacles, fallback) {
832
+ for (const point of candidates) {
833
+ const box = {
834
+ x: point.x,
835
+ y: point.y,
836
+ width: size.width,
837
+ height: size.height
838
+ };
839
+ if (!obstacles.some((obstacle) => boxesOverlap(padded(box, 20), obstacle))) return point;
840
+ }
841
+ return fallback;
842
+ }
843
+ /**
844
+ * A point guaranteed to overlap no obstacle, however dense the model: past the right edge of
845
+ * every obstacle's box, so no obstacle can reach it regardless of its y (`suggestPosition`'s
846
+ * last-resort fallback, once every ring candidate is taken).
847
+ */
848
+ function guaranteedFreePoint(obstacles, referenceY) {
849
+ if (obstacles.length === 0) return {
850
+ x: 0,
851
+ y: referenceY
852
+ };
853
+ return {
854
+ x: Math.max(...obstacles.map((box) => box.x + box.width)) + 20,
855
+ y: referenceY
856
+ };
857
+ }
858
+ /** Candidate positions in expanding square rings around `center`, in a fixed scan order. */
859
+ function ringCandidates(center, maxRing) {
860
+ const points = [center];
861
+ for (let ring = 1; ring <= maxRing; ring++) {
862
+ const offset = ring * SUGGEST_STEP;
863
+ for (let dx = -offset; dx <= offset; dx += SUGGEST_STEP) {
864
+ points.push({
865
+ x: center.x + dx,
866
+ y: center.y - offset
867
+ });
868
+ points.push({
869
+ x: center.x + dx,
870
+ y: center.y + offset
871
+ });
872
+ }
873
+ for (let dy = -offset + SUGGEST_STEP; dy <= offset - SUGGEST_STEP; dy += SUGGEST_STEP) {
874
+ points.push({
875
+ x: center.x - offset,
876
+ y: center.y + dy
877
+ });
878
+ points.push({
879
+ x: center.x + offset,
880
+ y: center.y + dy
881
+ });
882
+ }
883
+ }
884
+ return points;
885
+ }
886
+ function modelBoxes(model, defaultSizeOf) {
887
+ const boxes = [];
888
+ for (const element of model.elements.values()) {
889
+ const base = defaultSizeOf(element.kind);
890
+ const width = element.display?.width ?? base.width;
891
+ const height = element.display?.height ?? base.height;
892
+ boxes.push({
893
+ x: element.x,
894
+ y: element.y,
895
+ width,
896
+ height
897
+ });
898
+ }
899
+ return boxes;
900
+ }
901
+ /**
902
+ * A free spot for a new element of `kind`, overlapping no existing element (sizes from the
903
+ * metamodel, or `defaultSizeOf` for a host with its own size overrides, as in `actorBounds`).
904
+ *
905
+ * Scans a deterministic, fixed-order set of candidates — rings around `near` when given, else
906
+ * the container's interior, else rings around the origin — and returns the first whose padded
907
+ * box (`SUGGEST_MARGIN`) overlaps nothing; same input always yields the same output. However
908
+ * dense the model, the result never overlaps: once every ring candidate is taken, it falls back
909
+ * to a point past the right edge of every obstacle (`guaranteedFreePoint`).
910
+ *
911
+ * Inside `containerId`, the candidate stays within the actor's current boundary (`actorBounds`)
912
+ * when there is room, or goes just past its last child when the boundary is full — the boundary
913
+ * grows from there, same as any other inner element (`actorBounds`).
914
+ */
915
+ function suggestPosition(model, options) {
916
+ const metamodel = options.metamodel ?? metamodelOf(model);
917
+ const defaultSizeOf = options.defaultSizeOf ?? ((kind) => metamodelSizeOf(metamodel, kind));
918
+ const size = defaultSizeOf(options.kind);
919
+ const obstacles = modelBoxes(model, defaultSizeOf);
920
+ const near = typeof options.near === "string" ? model.elements.get(options.near) : options.near;
921
+ const nearPoint = near ? "kind" in near ? {
922
+ x: near.x,
923
+ y: near.y
924
+ } : near : void 0;
925
+ if (options.containerId) {
926
+ const bounds = actorBounds(model, options.containerId, metamodel, defaultSizeOf);
927
+ const candidates = ringCandidates(nearPoint ?? {
928
+ x: bounds.x + 10,
929
+ y: bounds.y + 10
930
+ }, SUGGEST_MAX_RING).filter((point) => point.x >= bounds.x + 10 && point.y >= bounds.y + 10 && point.x + size.width <= bounds.x + bounds.width - 10 && point.y + size.height <= bounds.y + bounds.height - 10);
931
+ if (candidates.length > 0) {
932
+ const sentinel = {
933
+ x: NaN,
934
+ y: NaN
935
+ };
936
+ const found = firstFreeCandidate(candidates, size, obstacles, sentinel);
937
+ if (found !== sentinel) return found;
938
+ }
939
+ const pastLastChild = {
940
+ x: bounds.x + bounds.width + 10,
941
+ y: bounds.y + 10
942
+ };
943
+ return firstFreeCandidate(ringCandidates(pastLastChild, SUGGEST_MAX_RING), size, obstacles, guaranteedFreePoint(obstacles, pastLastChild.y));
944
+ }
945
+ const start = nearPoint ?? (obstacles.length > 0 ? {
946
+ x: Math.max(...obstacles.map((box) => box.x + box.width)) + 20,
947
+ y: 0
948
+ } : {
949
+ x: 0,
950
+ y: 0
951
+ });
952
+ return firstFreeCandidate(ringCandidates(start, SUGGEST_MAX_RING), size, obstacles, guaranteedFreePoint(obstacles, start.y));
953
+ }
765
954
  //#endregion
766
955
  //#region src/constraints.ts
767
956
  const OK = { ok: true };
768
- function fail$1(code, reason) {
957
+ function fail$1(code, reason, end) {
769
958
  return {
770
959
  ok: false,
771
960
  code,
772
- reason
961
+ reason,
962
+ end
773
963
  };
774
964
  }
775
965
  /**
@@ -799,8 +989,8 @@ function canLink(model, source, target, kind, options = {}) {
799
989
  const metamodel = options.metamodel ?? metamodelOf(model);
800
990
  const s = resolve(any, source);
801
991
  const t = resolve(any, target) ?? any.links.get(refId(target));
802
- if (!s) return fail$1("unknown-element", `unknown source element "${refId(source)}"`);
803
- if (!t) return fail$1("unknown-element", `unknown target element "${refId(target)}"`);
992
+ if (!s) return fail$1("unknown-element", `unknown source element "${refId(source)}"`, "source");
993
+ if (!t) return fail$1("unknown-element", `unknown target element "${refId(target)}"`, "target");
804
994
  let definition = metamodel.links.get(kind);
805
995
  if (!definition) return fail$1("unknown-link-kind", `unknown link kind "${String(kind)}"`);
806
996
  if (definition.parts && !isLinkRef(t) && metamodel.elements.get(t.kind)?.of === definition.kind) definition = metamodel.links.get(definition.parts.leg) ?? definition;
@@ -851,7 +1041,7 @@ function hubCheck(graph, definition, source, target) {
851
1041
  }
852
1042
  /** The built-in (iStar 2.0) rules only know element targets. */
853
1043
  function elementTargetOnly(definition) {
854
- return fail$1("invalid-target", `the target of ${/^[aeiou]/i.test(definition.label) ? "an" : "a"} ${definition.label} link must be an element, not a link`);
1044
+ return fail$1("invalid-target", `the target of ${/^[aeiou]/i.test(definition.label) ? "an" : "a"} ${definition.label} link must be an element, not a link`, "target");
855
1045
  }
856
1046
  function refId(ref) {
857
1047
  return typeof ref === "string" ? ref : ref.id;
@@ -927,16 +1117,16 @@ const is = (graph, element, ...shortNames) => {
927
1117
  };
928
1118
  const isDependum = (element) => element.isDependum === true;
929
1119
  const isALink = (graph, source, target) => {
930
- if (!is(graph, source, "Actor", "Role")) return fail$1("invalid-source", "the source of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
1120
+ if (!is(graph, source, "Actor", "Role")) return fail$1("invalid-source", "the source of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).", "source");
931
1121
  if (source.kind !== target.kind) return fail$1("actor-types-differ", "the source and target of Is-A links must be of the same type - Actor and Actor, or Role and Role (iStar 2.0 Guide, Page 6).");
932
- if (!is(graph, target, "Actor", "Role")) return fail$1("invalid-target", "the target of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
1122
+ if (!is(graph, target, "Actor", "Role")) return fail$1("invalid-target", "the target of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).", "target");
933
1123
  if (source.id === target.id) return fail$1("self-link", "you cannot make Is-A links from an actor onto itself.");
934
1124
  if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
935
1125
  return OK;
936
1126
  };
937
1127
  const participatesInLink = (graph, source, target) => {
938
- if (!graph.isActor(source)) return fail$1("invalid-source", "the source of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
939
- if (!graph.isActor(target)) return fail$1("invalid-target", "the target of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
1128
+ if (!graph.isActor(source)) return fail$1("invalid-source", "the source of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)", "source");
1129
+ if (!graph.isActor(target)) return fail$1("invalid-target", "the target of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)", "target");
940
1130
  if (source.id === target.id) return fail$1("self-link", "you cannot make a Participates-In link from an actor onto itself.");
941
1131
  if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
942
1132
  return OK;
@@ -947,30 +1137,30 @@ function owningActorId(graph, element) {
947
1137
  }
948
1138
  const dependencyLink = (graph, source, target) => {
949
1139
  if (source.id === target.id) return fail$1("self-link", "a Dependency link cannot link an element onto itself");
950
- if (isDependum(source)) return fail$1("dependum-endpoint", "a Dependency link cannot start from a dependum");
951
- if (isDependum(target)) return fail$1("dependum-endpoint", "a Dependency link cannot end in a dependum");
1140
+ if (isDependum(source)) return fail$1("dependum-endpoint", "a Dependency link cannot start from a dependum", "source");
1141
+ if (isDependum(target)) return fail$1("dependum-endpoint", "a Dependency link cannot end in a dependum", "target");
952
1142
  if (owningActorId(graph, source) === owningActorId(graph, target)) return fail$1("same-actor", "a Dependency link must involve two different actors (iStar 2.0 Guide, Page 14)");
953
- if (graph.isTargetOfType(source, "istar.OrRefinementLink") || graph.isTargetOfType(source, "istar.AndRefinementLink")) return fail$1("refined-depender", "a refined element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
954
- if (graph.isTargetOfType(source, "istar.ContributionLink")) return fail$1("contributed-depender", "a contributed element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
1143
+ if (graph.isTargetOfType(source, "istar.OrRefinementLink") || graph.isTargetOfType(source, "istar.AndRefinementLink")) return fail$1("refined-depender", "a refined element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.", "source");
1144
+ if (graph.isTargetOfType(source, "istar.ContributionLink")) return fail$1("contributed-depender", "a contributed element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.", "source");
955
1145
  return OK;
956
1146
  };
957
1147
  function refinementLink(label) {
958
1148
  const other = label === "AND" ? "istar.OrRefinementLink" : "istar.AndRefinementLink";
959
1149
  return (graph, source, target) => {
960
- if (!is(graph, source, "Task", "Goal")) return fail$1("invalid-source", `the source of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
961
- if (!is(graph, target, "Task", "Goal")) return fail$1("invalid-target", `the target of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
1150
+ if (!is(graph, source, "Task", "Goal")) return fail$1("invalid-source", `the source of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`, "source");
1151
+ if (!is(graph, target, "Task", "Goal")) return fail$1("invalid-target", `the target of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`, "target");
962
1152
  if (source.id === target.id) return fail$1("self-link", `you cannot make an ${label}-refinement link from an element onto itself`);
963
1153
  if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", `you cannot make an ${label}-refinement link with a dependum (iStar 2.0 Guide, Page 14)`);
964
1154
  if (source.parent !== target.parent) return fail$1("different-actors", `the source and target of an ${label}-refinement link must pertain to the same actor (iStar 2.0 Guide, Page 14)`);
965
1155
  if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one refinement link between the same two elements");
966
- if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail$1("depender-target", "you cannot refine a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-element.");
967
- if (graph.isTargetOfType(target, other)) return fail$1("mixed-refinement", label === "AND" ? "you cannot mix AND-refinements with OR-refinements targeting the same element (iStar 2.0 Guide, Page 10)." : "you cannot mix OR-refinements with AND-refinements targeting the same element (iStar 2.0 Guide, Page 10).");
1156
+ if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail$1("depender-target", "you cannot refine a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-element.", "target");
1157
+ if (graph.isTargetOfType(target, other)) return fail$1("mixed-refinement", label === "AND" ? "you cannot mix AND-refinements with OR-refinements targeting the same element (iStar 2.0 Guide, Page 10)." : "you cannot mix OR-refinements with AND-refinements targeting the same element (iStar 2.0 Guide, Page 10).", "target");
968
1158
  return OK;
969
1159
  };
970
1160
  }
971
1161
  const neededByLink = (graph, source, target) => {
972
- if (!is(graph, source, "Resource")) return fail$1("invalid-source", "the source of a Needed-By link must be a Resource (iStar 2.0 Guide, Table 1)");
973
- if (!is(graph, target, "Task")) return fail$1("invalid-target", "the target of a Needed-By link must be a Task (iStar 2.0 Guide, Table 1)");
1162
+ if (!is(graph, source, "Resource")) return fail$1("invalid-source", "the source of a Needed-By link must be a Resource (iStar 2.0 Guide, Table 1)", "source");
1163
+ if (!is(graph, target, "Task")) return fail$1("invalid-target", "the target of a Needed-By link must be a Task (iStar 2.0 Guide, Table 1)", "target");
974
1164
  if (source.id === target.id) return fail$1("self-link", "you cannot make a Needed-By link from an element onto itself");
975
1165
  if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", "you cannot make a Needed-By link with a dependum (iStar 2.0 Guide, Page 14)");
976
1166
  if (source.parent !== target.parent) return fail$1("different-actors", "the source and target of a Needed-By link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
@@ -978,19 +1168,19 @@ const neededByLink = (graph, source, target) => {
978
1168
  return OK;
979
1169
  };
980
1170
  const contributionLink = (graph, source, target) => {
981
- if (!is(graph, source, "Goal", "Quality", "Task", "Resource")) return fail$1("invalid-source", "the source of a Contribution link must be a Goal, a Quality, a Task or a Resource (iStar 2.0 Guide, Table 1)");
982
- if (!is(graph, target, "Quality")) return fail$1("invalid-target", "the target of a Contribution link must be a Quality (iStar 2.0 Guide, Table 1)");
1171
+ if (!is(graph, source, "Goal", "Quality", "Task", "Resource")) return fail$1("invalid-source", "the source of a Contribution link must be a Goal, a Quality, a Task or a Resource (iStar 2.0 Guide, Table 1)", "source");
1172
+ if (!is(graph, target, "Quality")) return fail$1("invalid-target", "the target of a Contribution link must be a Quality (iStar 2.0 Guide, Table 1)", "target");
983
1173
  if (source.id === target.id) return fail$1("self-link", "you cannot make a Contribution link from an element onto itself (iStar 2.0 Guide, Page 15)");
984
1174
  if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", "you cannot make a Contribution link with a dependum (iStar 2.0 Guide, Page 14)");
985
1175
  if (source.parent !== target.parent) return fail$1("different-actors", "the source and target of a Contribution link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
986
1176
  if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail$1("duplicate-link", "there can only be one Contribution link between the same two elements");
987
1177
  if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail$1("contribution-and-qualification", "you cannot have Contribution and Qualification links between the same two elements (iStar 2.0 Guide, Page 15)");
988
- if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail$1("depender-target", "you cannot contribute to a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-quality.");
1178
+ if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail$1("depender-target", "you cannot contribute to a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-quality.", "target");
989
1179
  return OK;
990
1180
  };
991
1181
  const qualificationLink = (graph, source, target) => {
992
- if (!is(graph, source, "Quality")) return fail$1("invalid-source", "the source of a Qualification link must be a Quality (iStar 2.0 Guide, Table 1)");
993
- if (!is(graph, target, "Goal", "Task", "Resource")) return fail$1("invalid-target", "the target of a Qualification link must be a Goal, a Task or a Resource (iStar 2.0 Guide, Table 1)");
1182
+ if (!is(graph, source, "Quality")) return fail$1("invalid-source", "the source of a Qualification link must be a Quality (iStar 2.0 Guide, Table 1)", "source");
1183
+ if (!is(graph, target, "Goal", "Task", "Resource")) return fail$1("invalid-target", "the target of a Qualification link must be a Goal, a Task or a Resource (iStar 2.0 Guide, Table 1)", "target");
994
1184
  if (source.id === target.id) return fail$1("self-link", "you cannot make a Qualification link from an element onto itself");
995
1185
  if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", "you cannot make a Qualification link with a dependum (iStar 2.0 Guide, Page 14)");
996
1186
  if (source.parent !== target.parent) return fail$1("different-actors", "the source and target of a Qualification link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
@@ -1030,12 +1220,12 @@ function matchesSelector(graph, element, selector) {
1030
1220
  function declarativeCheck(graph, definition, rules, rulesKind, source, target) {
1031
1221
  const name = definition.label;
1032
1222
  const article = /^[aeiou]/i.test(name) ? "an" : "a";
1033
- if (!rules.sources.some((selector) => matchesSelector(graph, source, selector))) return fail$1("invalid-source", `the source of ${article} ${name} link must be ${describe(graph, rules.sources)}`);
1223
+ if (!rules.sources.some((selector) => matchesSelector(graph, source, selector))) return fail$1("invalid-source", `the source of ${article} ${name} link must be ${describe(graph, rules.sources)}`, "source");
1034
1224
  if (!rules.targets.some((selector) => matchesSelector(graph, target, selector))) {
1035
1225
  const targetKind = isLinkRef(target) ? void 0 : graph.metamodel.elements.get(target.kind);
1036
1226
  const hubOf = targetKind?.role === "hub" && targetKind.of !== void 0 ? graph.metamodel.links.get(targetKind.of) : void 0;
1037
- if (hubOf) return fail$1("hub-target", `you cannot make ${article} ${name} link towards the hub of ${/^[aeiou]/i.test(hubOf.label) ? "an" : "a"} ${hubOf.label} link`);
1038
- return fail$1("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`);
1227
+ if (hubOf) return fail$1("hub-target", `you cannot make ${article} ${name} link towards the hub of ${/^[aeiou]/i.test(hubOf.label) ? "an" : "a"} ${hubOf.label} link`, "target");
1228
+ return fail$1("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`, "target");
1039
1229
  }
1040
1230
  if (!rules.allowSelf && source.id === target.id) return fail$1("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
1041
1231
  if (definition.category === "node" && !isLinkRef(target)) {
@@ -1790,6 +1980,20 @@ function withFileMetamodel(model, block) {
1790
1980
  //#region src/operations.ts
1791
1981
  var ModelOperationError = class extends Error {
1792
1982
  name = "ModelOperationError";
1983
+ /** The element or link the error is about, when the operation named one. */
1984
+ elementId;
1985
+ constructor(message, elementId) {
1986
+ super(message);
1987
+ this.elementId = elementId;
1988
+ }
1989
+ /** As a `GoalDiagnostic`, anchored on `elementId` (empty string if the error names none). */
1990
+ toDiagnostic() {
1991
+ return {
1992
+ elementId: this.elementId ?? "",
1993
+ severity: "error",
1994
+ message: this.message
1995
+ };
1996
+ }
1793
1997
  };
1794
1998
  const defaultIdGenerator = () => {
1795
1999
  const c = globalThis.crypto;
@@ -1819,12 +2023,12 @@ function derive(from, changes) {
1819
2023
  }
1820
2024
  function requireElement(model, id) {
1821
2025
  const element = model.elements.get(id);
1822
- if (!element) throw new ModelOperationError(`unknown element "${id}"`);
2026
+ if (!element) throw new ModelOperationError(`unknown element "${id}"`, id);
1823
2027
  return element;
1824
2028
  }
1825
2029
  function requireLink(model, id) {
1826
2030
  const link = model.links.get(id);
1827
- if (!link) throw new ModelOperationError(`unknown link "${id}"`);
2031
+ if (!link) throw new ModelOperationError(`unknown link "${id}"`, id);
1828
2032
  return link;
1829
2033
  }
1830
2034
  function withoutUndefined(value) {
@@ -1838,10 +2042,10 @@ function addElement(model, input, ctx = {}) {
1838
2042
  const definition = metamodel.elements.get(input.kind);
1839
2043
  if (!definition) throw new ModelOperationError(`unknown element kind "${input.kind}" in ${metamodel.name}`);
1840
2044
  const id = input.id ?? (ctx.createId ?? defaultIdGenerator)();
1841
- if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
2045
+ if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`, id);
1842
2046
  if (input.parent !== void 0) {
1843
2047
  if (definition.category === "actor") throw new ModelOperationError("actors cannot be nested");
1844
- if (!isActorIn$1(m, requireElement(m, input.parent).kind)) throw new ModelOperationError(`parent "${input.parent}" is not an actor`);
2048
+ if (!isActorIn$1(m, requireElement(m, input.parent).kind)) throw new ModelOperationError(`parent "${input.parent}" is not an actor`, input.parent);
1845
2049
  }
1846
2050
  const element = withoutUndefined({
1847
2051
  id,
@@ -1910,10 +2114,10 @@ function moveElement(model, id, x, y) {
1910
2114
  function nestElement(model, id, parent) {
1911
2115
  const m = any(model);
1912
2116
  const current = requireElement(m, id);
1913
- if (isActorIn$1(m, current.kind)) throw new ModelOperationError("actors cannot be nested");
1914
- if (current.isDependum) throw new ModelOperationError("a dependum cannot be nested");
2117
+ if (isActorIn$1(m, current.kind)) throw new ModelOperationError("actors cannot be nested", id);
2118
+ if (current.isDependum) throw new ModelOperationError("a dependum cannot be nested", id);
1915
2119
  if ((current.parent ?? null) === parent) return model;
1916
- if (parent !== null && !isActorIn$1(m, requireElement(m, parent).kind)) throw new ModelOperationError(`parent "${parent}" is not an actor`);
2120
+ if (parent !== null && !isActorIn$1(m, requireElement(m, parent).kind)) throw new ModelOperationError(`parent "${parent}" is not an actor`, parent);
1917
2121
  const elements = new Map(m.elements);
1918
2122
  elements.delete(id);
1919
2123
  const { parent: _old, ...rest } = current;
@@ -1991,12 +2195,27 @@ function removeCascade(model, elementIds, linkIds) {
1991
2195
  });
1992
2196
  }
1993
2197
  /**
2198
+ * Maps a failing `connect` result to a `GoalDiagnostic`, anchored on the end named by the
2199
+ * `LinkCheck`'s `end` field (`target` when a check doesn't single out one end, such as
2200
+ * `self-link` or `actor-types-differ`). `undefined` for a successful result.
2201
+ */
2202
+ function connectResultToDiagnostic(result, endpoints) {
2203
+ if (result.ok) return void 0;
2204
+ return {
2205
+ elementId: (result.end ?? "target") === "source" ? endpoints.source : endpoints.target,
2206
+ severity: "error",
2207
+ message: result.reason,
2208
+ code: result.code
2209
+ };
2210
+ }
2211
+ /**
1994
2212
  * Adds a link between two elements if the iStar constraints allow it.
1995
2213
  *
1996
2214
  * With an n-ary kind (`arity: 'n'`), a `target` that is one of the kind's hubs gets a new leg
1997
- * from `source` (see `addLeg`). Any other target gets a new hub, centred between source and
1998
- * target, with a leg from `source` and an end to `target`. `input.id` and `customProperties`
1999
- * then go to the hub, and `label` and `name` to the end.
2215
+ * from `source` (see `addLeg`), taking `input.id` or `input.legId` (default a fresh id) as the
2216
+ * leg's id. Any other target gets a new hub, centred between source and target, with a leg from
2217
+ * `source` and an end to `target`. `input.id` and `customProperties` then go to the hub, `label`
2218
+ * and `name` to the end, and `legId`/`endId` (default fresh ids) to the leg and end.
2000
2219
  */
2001
2220
  function connect(model, input, options = {}) {
2002
2221
  const m = any(model);
@@ -2025,7 +2244,7 @@ function connect(model, input, options = {}) {
2025
2244
  if (!m.links.has(target)) requireElement(m, target);
2026
2245
  }
2027
2246
  const id = input.id ?? (options.createId ?? defaultIdGenerator)();
2028
- if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
2247
+ if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`, id);
2029
2248
  const link = withoutUndefined({
2030
2249
  id,
2031
2250
  kind,
@@ -2046,7 +2265,13 @@ function connectHub(m, definition, input, options) {
2046
2265
  const metamodel = metaOf(m);
2047
2266
  const parts = definition.parts;
2048
2267
  const { source, target } = input;
2049
- if (m.elements.get(target)?.kind === parts.hub) return addLeg(typed(m), target, source, options);
2268
+ if (m.elements.get(target)?.kind === parts.hub) {
2269
+ const legId = input.id ?? input.legId;
2270
+ return addLeg(typed(m), target, source, {
2271
+ ...options,
2272
+ ...legId === void 0 ? {} : { id: legId }
2273
+ });
2274
+ }
2050
2275
  if (!options.force) {
2051
2276
  const check = canLink(m, source, target, definition.kind);
2052
2277
  if (!check.ok) return check;
@@ -2055,8 +2280,8 @@ function connectHub(m, definition, input, options) {
2055
2280
  const to = requireElement(m, target);
2056
2281
  const createId = options.createId ?? defaultIdGenerator;
2057
2282
  const hubId = input.id ?? createId();
2058
- const legId = createId();
2059
- const endId = createId();
2283
+ const legId = input.legId ?? createId();
2284
+ const endId = input.endId ?? createId();
2060
2285
  for (const [i, id] of [
2061
2286
  hubId,
2062
2287
  legId,
@@ -2065,7 +2290,7 @@ function connectHub(m, definition, input, options) {
2065
2290
  hubId,
2066
2291
  legId,
2067
2292
  endId
2068
- ].indexOf(id) !== i) throw new ModelOperationError(`id "${id}" is already in use`);
2293
+ ].indexOf(id) !== i) throw new ModelOperationError(`id "${id}" is already in use`, id);
2069
2294
  const position = defaultHubPosition(m, from, to, metamodel.elements.get(parts.hub)?.size ?? {
2070
2295
  width: 20,
2071
2296
  height: 20
@@ -2116,14 +2341,14 @@ function addLeg(model, hubId, source, options = {}) {
2116
2341
  const metamodel = metaOf(m);
2117
2342
  const hub = requireElement(m, hubId);
2118
2343
  const base = metamodel.links.get(metamodel.elements.get(hub.kind)?.of ?? "");
2119
- if (metamodel.elements.get(hub.kind)?.role !== "hub" || !base?.parts) throw new ModelOperationError(`"${hubId}" is not the hub of an n-ary link`);
2344
+ if (metamodel.elements.get(hub.kind)?.role !== "hub" || !base?.parts) throw new ModelOperationError(`"${hubId}" is not the hub of an n-ary link`, hubId);
2120
2345
  const kind = base.parts.leg;
2121
2346
  if (!options.force) {
2122
2347
  const check = canLink(m, source, hubId, kind);
2123
2348
  if (!check.ok) return check;
2124
2349
  } else requireElement(m, source);
2125
2350
  const id = options.id ?? (options.createId ?? defaultIdGenerator)();
2126
- if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
2351
+ if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`, id);
2127
2352
  const leg = {
2128
2353
  id,
2129
2354
  kind,
@@ -2146,7 +2371,7 @@ function addLeg(model, hubId, source, options = {}) {
2146
2371
  function removeLeg(model, legId) {
2147
2372
  const m = any(model);
2148
2373
  const leg = requireLink(m, legId);
2149
- if (metaOf(m).links.get(leg.kind)?.role !== "leg") throw new ModelOperationError(`"${legId}" is not a leg of an n-ary link`);
2374
+ if (metaOf(m).links.get(leg.kind)?.role !== "leg") throw new ModelOperationError(`"${legId}" is not a leg of an n-ary link`, legId);
2150
2375
  return disconnect(model, legId);
2151
2376
  }
2152
2377
  /** Adds depender → dependum → dependee. */
@@ -2190,7 +2415,7 @@ function addDependency(model, input, options = {}) {
2190
2415
  source: dependum.id,
2191
2416
  target: dependee.id
2192
2417
  };
2193
- for (const id of [id1, id2]) if (added.model.elements.has(id) || added.model.links.has(id) || id1 === id2) throw new ModelOperationError(`id "${id}" is already in use`);
2418
+ for (const id of [id1, id2]) if (added.model.elements.has(id) || added.model.links.has(id) || id1 === id2) throw new ModelOperationError(`id "${id}" is already in use`, id);
2194
2419
  return {
2195
2420
  ok: true,
2196
2421
  model: typed(derive(m, {
@@ -2972,6 +3197,101 @@ function fromNodeIdDiagnostics(diagnostics) {
2972
3197
  return result;
2973
3198
  }
2974
3199
  //#endregion
2975
- export { ACTOR_KINDS, ACTOR_LINK_KINDS, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEPENDENCY_LINK_KINDS, DIAGNOSTIC_SEVERITIES, DIAGRAM_LAYOUT_KEY, DSM3_FILE_KEYS, DSM3_TOOL, ELEMENT_KINDS, FILE_METAMODEL_KEY, HUB_ENTRY_KEY, ISTAR_2_0, ISTAR_2_0_DECLARATION, ISTAR_PREFIX, ISTAR_VERSION, LINK_KINDS, LINK_KIND_INFO, LINK_SELECTOR, METAMODEL_VERSION, MetamodelError, ModelOperationError, NODE_KINDS, NODE_KIND_INFO, NODE_LINK_KINDS, PISTAR_TOOL, PistarParseError, PistarWriteError, addDependency, addElement, addLeg, boolean, canLink, childrenOf, compareSeverity, connect, createDiagnosticsStore, createEmptyModel, createModelStore, defaultHubPosition, defaultIdGenerator, defineMetamodel, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, detectFramework, detectPrefix, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementCenter, elementKindDefinition, enumOf, extendMetamodel, fileMetamodelExtension, fileMetamodelOf, fromLspDiagnostic, fromLspDiagnostics, fromNodeIdDiagnostic, fromNodeIdDiagnostics, getSourceLayout, groupDiagnostics, hostMetamodelOf, hubEntryLayoutKey, hubLinkOf, hubOfLink, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isHubElement, isLinkKind, isLinkRef, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkBehavesLike, linkKindDefinition, linkTargetOf, linksOf, mergeDiagnostics, metamodelOf, moveElement, nestElement, number, parsePistar, prop, reference, removeElement, removeElements, removeLeg, setHostMetamodel, shortKindName, string, targetsLinks, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel, worstDiagnosticSeverity };
3200
+ //#region src/palette.ts
3201
+ /**
3202
+ * The data part of each kind's palette entry: label, grouping, ordering, and the presets a
3203
+ * toolbar entry creates with (a link's `value`, a dependency's `dependum`, an element's
3204
+ * `properties`). React-free so a host can answer "what can I add here" (GLSP's
3205
+ * `RequestContextActions`) without the react package. Icons, titles and components are
3206
+ * presentation and stay in `packages/react/src/registry`, which derives its default entries
3207
+ * from `DEFAULT_PALETTE`.
3208
+ */
3209
+ function defaultElementEntry(kind, index) {
3210
+ const actor = isActorKind(kind);
3211
+ return {
3212
+ label: shortKindName(kind),
3213
+ order: actor ? index : 30 + index,
3214
+ section: actor ? "actors" : "elements",
3215
+ ...actor ? { group: "actors" } : {}
3216
+ };
3217
+ }
3218
+ const CONTRIBUTION_ENTRY_LABELS = {
3219
+ make: "Make (++)",
3220
+ help: "Help (+)",
3221
+ hurt: "Hurt (-)",
3222
+ break: "Break (--)"
3223
+ };
3224
+ const LINK_ENTRIES = {
3225
+ "istar.IsALink": [{
3226
+ label: "Is A",
3227
+ order: 10,
3228
+ group: "actor-links",
3229
+ section: "actors"
3230
+ }],
3231
+ "istar.ParticipatesInLink": [{
3232
+ label: "Participates-In",
3233
+ order: 11,
3234
+ group: "actor-links",
3235
+ section: "actors"
3236
+ }],
3237
+ "istar.DependencyLink": NODE_KINDS.map((dependum, i) => ({
3238
+ label: `${shortKindName(dependum)} dependency`,
3239
+ dependum,
3240
+ order: 20 + i,
3241
+ group: "dependencies",
3242
+ section: "actors"
3243
+ })),
3244
+ "istar.AndRefinementLink": [{
3245
+ label: "And",
3246
+ order: 40,
3247
+ section: "elements"
3248
+ }],
3249
+ "istar.OrRefinementLink": [{
3250
+ label: "Or",
3251
+ order: 41,
3252
+ section: "elements"
3253
+ }],
3254
+ "istar.NeededByLink": [{
3255
+ label: "Needed-By",
3256
+ order: 42,
3257
+ section: "elements"
3258
+ }],
3259
+ "istar.QualificationLink": [{
3260
+ label: "Qualification",
3261
+ order: 43,
3262
+ section: "elements"
3263
+ }],
3264
+ "istar.ContributionLink": CONTRIBUTION_LABELS.map((value, i) => ({
3265
+ label: CONTRIBUTION_ENTRY_LABELS[value],
3266
+ value,
3267
+ order: 50 + i,
3268
+ group: "contributions",
3269
+ section: "elements"
3270
+ }))
3271
+ };
3272
+ /** piStar's dropdown buttons. */
3273
+ const DEFAULT_PALETTE_GROUPS = {
3274
+ actors: { label: "Actor" },
3275
+ "actor-links": { label: "Actor links" },
3276
+ dependencies: { label: "Dependency" },
3277
+ contributions: { label: "Contribution" }
3278
+ };
3279
+ function buildDefaultPalette() {
3280
+ const elements = {};
3281
+ ELEMENT_KINDS.forEach((kind, i) => {
3282
+ elements[kind] = defaultElementEntry(kind, i);
3283
+ });
3284
+ const links = {};
3285
+ for (const kind of LINK_KINDS) links[kind] = LINK_ENTRIES[kind];
3286
+ return {
3287
+ elements,
3288
+ links,
3289
+ groups: DEFAULT_PALETTE_GROUPS
3290
+ };
3291
+ }
3292
+ /** The palette data of iStar 2.0, as piStar's toolbar presents it. */
3293
+ const DEFAULT_PALETTE = buildDefaultPalette();
3294
+ //#endregion
3295
+ export { ACTOR_KINDS, ACTOR_LINK_KINDS, ACTOR_PADDING, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEFAULT_PALETTE, DEPENDENCY_LINK_KINDS, DIAGNOSTIC_SEVERITIES, DIAGRAM_LAYOUT_KEY, DSM3_FILE_KEYS, DSM3_TOOL, ELEMENT_KINDS, FILE_METAMODEL_KEY, HUB_ENTRY_KEY, ISTAR_2_0, ISTAR_2_0_DECLARATION, ISTAR_PREFIX, ISTAR_VERSION, LINK_KINDS, LINK_KIND_INFO, LINK_SELECTOR, METAMODEL_VERSION, MetamodelError, ModelOperationError, NODE_KINDS, NODE_KIND_INFO, NODE_LINK_KINDS, PISTAR_TOOL, PistarParseError, PistarWriteError, SUGGEST_MARGIN, actorBounds, addDependency, addElement, addLeg, boolean, canLink, childrenOf, compareSeverity, connect, connectResultToDiagnostic, createDiagnosticsStore, createEmptyModel, createModelStore, defaultHubPosition, defaultIdGenerator, defineMetamodel, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, detectFramework, detectPrefix, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementCenter, elementKindDefinition, enumOf, extendMetamodel, fileMetamodelExtension, fileMetamodelOf, fromLspDiagnostic, fromLspDiagnostics, fromNodeIdDiagnostic, fromNodeIdDiagnostics, getSourceLayout, groupDiagnostics, hostMetamodelOf, hubEntryLayoutKey, hubLinkOf, hubOfLink, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isHubElement, isLinkKind, isLinkRef, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkBehavesLike, linkKindDefinition, linkTargetOf, linksOf, mergeDiagnostics, metamodelOf, moveElement, nestElement, number, parsePistar, prop, reference, removeElement, removeElements, removeLeg, setHostMetamodel, shortKindName, string, suggestPosition, targetsLinks, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel, worstDiagnosticSeverity };
2976
3296
 
2977
3297
  //# sourceMappingURL=index.js.map