@istar-ts/core 0.15.0-beta.0 → 0.15.0-beta.2

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.d.cts CHANGED
@@ -625,6 +625,34 @@ export declare const ACTOR_PADDING = 10;
625
625
  * what it actually draws.
626
626
  */
627
627
  export declare function actorBounds<EK extends string, LK extends string>(model: IstarModel<EK, LK>, actorId: string, metamodel?: Metamodel<EK, LK>, defaultSizeOf?: (kind: EK) => Size): Box;
628
+ /** Space kept between a suggested box and any existing element (`suggestPosition`). */
629
+ export declare const SUGGEST_MARGIN = 20;
630
+ export interface SuggestPositionOptions<EK extends string> {
631
+ /** The kind of element being placed; its metamodel size is what must not overlap. */
632
+ readonly kind: EK;
633
+ /** Place inside this actor's boundary (`actorBounds`), growing it if full. */
634
+ readonly containerId?: string;
635
+ /** Prefer a spot adjacent to this element's id, or to this point. */
636
+ readonly near?: string | Point;
637
+ readonly metamodel?: Metamodel<EK, string>;
638
+ /** See `actorBounds`' `defaultSizeOf`. */
639
+ readonly defaultSizeOf?: (kind: EK) => Size;
640
+ }
641
+ /**
642
+ * A free spot for a new element of `kind`, overlapping no existing element (sizes from the
643
+ * metamodel, or `defaultSizeOf` for a host with its own size overrides, as in `actorBounds`).
644
+ *
645
+ * Scans a deterministic, fixed-order set of candidates — rings around `near` when given, else
646
+ * the container's interior, else rings around the origin — and returns the first whose padded
647
+ * box (`SUGGEST_MARGIN`) overlaps nothing; same input always yields the same output. However
648
+ * dense the model, the result never overlaps: once every ring candidate is taken, it falls back
649
+ * to a point past the right edge of every obstacle (`guaranteedFreePoint`).
650
+ *
651
+ * Inside `containerId`, the candidate stays within the actor's current boundary (`actorBounds`)
652
+ * when there is room, or goes just past its last child when the boundary is full — the boundary
653
+ * grows from there, same as any other inner element (`actorBounds`).
654
+ */
655
+ export declare function suggestPosition<EK extends string, LK extends string>(model: IstarModel<EK, LK>, options: SuggestPositionOptions<EK>): Point;
628
656
  //#endregion
629
657
  //#region src/file-metamodel.d.ts
630
658
  /** The top-level key of the block in piStar JSON. */
package/dist/index.d.ts CHANGED
@@ -625,6 +625,34 @@ export declare const ACTOR_PADDING = 10;
625
625
  * what it actually draws.
626
626
  */
627
627
  export declare function actorBounds<EK extends string, LK extends string>(model: IstarModel<EK, LK>, actorId: string, metamodel?: Metamodel<EK, LK>, defaultSizeOf?: (kind: EK) => Size): Box;
628
+ /** Space kept between a suggested box and any existing element (`suggestPosition`). */
629
+ export declare const SUGGEST_MARGIN = 20;
630
+ export interface SuggestPositionOptions<EK extends string> {
631
+ /** The kind of element being placed; its metamodel size is what must not overlap. */
632
+ readonly kind: EK;
633
+ /** Place inside this actor's boundary (`actorBounds`), growing it if full. */
634
+ readonly containerId?: string;
635
+ /** Prefer a spot adjacent to this element's id, or to this point. */
636
+ readonly near?: string | Point;
637
+ readonly metamodel?: Metamodel<EK, string>;
638
+ /** See `actorBounds`' `defaultSizeOf`. */
639
+ readonly defaultSizeOf?: (kind: EK) => Size;
640
+ }
641
+ /**
642
+ * A free spot for a new element of `kind`, overlapping no existing element (sizes from the
643
+ * metamodel, or `defaultSizeOf` for a host with its own size overrides, as in `actorBounds`).
644
+ *
645
+ * Scans a deterministic, fixed-order set of candidates — rings around `near` when given, else
646
+ * the container's interior, else rings around the origin — and returns the first whose padded
647
+ * box (`SUGGEST_MARGIN`) overlaps nothing; same input always yields the same output. However
648
+ * dense the model, the result never overlaps: once every ring candidate is taken, it falls back
649
+ * to a point past the right edge of every obstacle (`guaranteedFreePoint`).
650
+ *
651
+ * Inside `containerId`, the candidate stays within the actor's current boundary (`actorBounds`)
652
+ * when there is room, or goes just past its last child when the boundary is full — the boundary
653
+ * grows from there, same as any other inner element (`actorBounds`).
654
+ */
655
+ export declare function suggestPosition<EK extends string, LK extends string>(model: IstarModel<EK, LK>, options: SuggestPositionOptions<EK>): Point;
628
656
  //#endregion
629
657
  //#region src/file-metamodel.d.ts
630
658
  /** The top-level key of the block in piStar JSON. */
package/dist/index.js CHANGED
@@ -811,6 +811,146 @@ function actorBounds(model, actorId, metamodel = metamodelOf(model), defaultSize
811
811
  height: maxY - minY
812
812
  };
813
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
+ }
814
954
  //#endregion
815
955
  //#region src/constraints.ts
816
956
  const OK = { ok: true };
@@ -3152,6 +3292,6 @@ function buildDefaultPalette() {
3152
3292
  /** The palette data of iStar 2.0, as piStar's toolbar presents it. */
3153
3293
  const DEFAULT_PALETTE = buildDefaultPalette();
3154
3294
  //#endregion
3155
- 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, 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, targetsLinks, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel, worstDiagnosticSeverity };
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 };
3156
3296
 
3157
3297
  //# sourceMappingURL=index.js.map