@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.cjs CHANGED
@@ -812,6 +812,146 @@ function actorBounds(model, actorId, metamodel = metamodelOf(model), defaultSize
812
812
  height: maxY - minY
813
813
  };
814
814
  }
815
+ /** Space kept between a suggested box and any existing element (`suggestPosition`). */
816
+ const SUGGEST_MARGIN = 20;
817
+ /** Step between candidate positions on the grid `suggestPosition` scans. */
818
+ const SUGGEST_STEP = 40;
819
+ /** How many rings out `suggestPosition` scans before falling back to `guaranteedFreePoint`. */
820
+ const SUGGEST_MAX_RING = 25;
821
+ function boxesOverlap(a, b) {
822
+ 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;
823
+ }
824
+ function padded(box, margin) {
825
+ return {
826
+ x: box.x - margin,
827
+ y: box.y - margin,
828
+ width: box.width + margin * 2,
829
+ height: box.height + margin * 2
830
+ };
831
+ }
832
+ function firstFreeCandidate(candidates, size, obstacles, fallback) {
833
+ for (const point of candidates) {
834
+ const box = {
835
+ x: point.x,
836
+ y: point.y,
837
+ width: size.width,
838
+ height: size.height
839
+ };
840
+ if (!obstacles.some((obstacle) => boxesOverlap(padded(box, 20), obstacle))) return point;
841
+ }
842
+ return fallback;
843
+ }
844
+ /**
845
+ * A point guaranteed to overlap no obstacle, however dense the model: past the right edge of
846
+ * every obstacle's box, so no obstacle can reach it regardless of its y (`suggestPosition`'s
847
+ * last-resort fallback, once every ring candidate is taken).
848
+ */
849
+ function guaranteedFreePoint(obstacles, referenceY) {
850
+ if (obstacles.length === 0) return {
851
+ x: 0,
852
+ y: referenceY
853
+ };
854
+ return {
855
+ x: Math.max(...obstacles.map((box) => box.x + box.width)) + 20,
856
+ y: referenceY
857
+ };
858
+ }
859
+ /** Candidate positions in expanding square rings around `center`, in a fixed scan order. */
860
+ function ringCandidates(center, maxRing) {
861
+ const points = [center];
862
+ for (let ring = 1; ring <= maxRing; ring++) {
863
+ const offset = ring * SUGGEST_STEP;
864
+ for (let dx = -offset; dx <= offset; dx += SUGGEST_STEP) {
865
+ points.push({
866
+ x: center.x + dx,
867
+ y: center.y - offset
868
+ });
869
+ points.push({
870
+ x: center.x + dx,
871
+ y: center.y + offset
872
+ });
873
+ }
874
+ for (let dy = -offset + SUGGEST_STEP; dy <= offset - SUGGEST_STEP; dy += SUGGEST_STEP) {
875
+ points.push({
876
+ x: center.x - offset,
877
+ y: center.y + dy
878
+ });
879
+ points.push({
880
+ x: center.x + offset,
881
+ y: center.y + dy
882
+ });
883
+ }
884
+ }
885
+ return points;
886
+ }
887
+ function modelBoxes(model, defaultSizeOf) {
888
+ const boxes = [];
889
+ for (const element of model.elements.values()) {
890
+ const base = defaultSizeOf(element.kind);
891
+ const width = element.display?.width ?? base.width;
892
+ const height = element.display?.height ?? base.height;
893
+ boxes.push({
894
+ x: element.x,
895
+ y: element.y,
896
+ width,
897
+ height
898
+ });
899
+ }
900
+ return boxes;
901
+ }
902
+ /**
903
+ * A free spot for a new element of `kind`, overlapping no existing element (sizes from the
904
+ * metamodel, or `defaultSizeOf` for a host with its own size overrides, as in `actorBounds`).
905
+ *
906
+ * Scans a deterministic, fixed-order set of candidates — rings around `near` when given, else
907
+ * the container's interior, else rings around the origin — and returns the first whose padded
908
+ * box (`SUGGEST_MARGIN`) overlaps nothing; same input always yields the same output. However
909
+ * dense the model, the result never overlaps: once every ring candidate is taken, it falls back
910
+ * to a point past the right edge of every obstacle (`guaranteedFreePoint`).
911
+ *
912
+ * Inside `containerId`, the candidate stays within the actor's current boundary (`actorBounds`)
913
+ * when there is room, or goes just past its last child when the boundary is full — the boundary
914
+ * grows from there, same as any other inner element (`actorBounds`).
915
+ */
916
+ function suggestPosition(model, options) {
917
+ const metamodel = options.metamodel ?? metamodelOf(model);
918
+ const defaultSizeOf = options.defaultSizeOf ?? ((kind) => metamodelSizeOf(metamodel, kind));
919
+ const size = defaultSizeOf(options.kind);
920
+ const obstacles = modelBoxes(model, defaultSizeOf);
921
+ const near = typeof options.near === "string" ? model.elements.get(options.near) : options.near;
922
+ const nearPoint = near ? "kind" in near ? {
923
+ x: near.x,
924
+ y: near.y
925
+ } : near : void 0;
926
+ if (options.containerId) {
927
+ const bounds = actorBounds(model, options.containerId, metamodel, defaultSizeOf);
928
+ const candidates = ringCandidates(nearPoint ?? {
929
+ x: bounds.x + 10,
930
+ y: bounds.y + 10
931
+ }, 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);
932
+ if (candidates.length > 0) {
933
+ const sentinel = {
934
+ x: NaN,
935
+ y: NaN
936
+ };
937
+ const found = firstFreeCandidate(candidates, size, obstacles, sentinel);
938
+ if (found !== sentinel) return found;
939
+ }
940
+ const pastLastChild = {
941
+ x: bounds.x + bounds.width + 10,
942
+ y: bounds.y + 10
943
+ };
944
+ return firstFreeCandidate(ringCandidates(pastLastChild, SUGGEST_MAX_RING), size, obstacles, guaranteedFreePoint(obstacles, pastLastChild.y));
945
+ }
946
+ const start = nearPoint ?? (obstacles.length > 0 ? {
947
+ x: Math.max(...obstacles.map((box) => box.x + box.width)) + 20,
948
+ y: 0
949
+ } : {
950
+ x: 0,
951
+ y: 0
952
+ });
953
+ return firstFreeCandidate(ringCandidates(start, SUGGEST_MAX_RING), size, obstacles, guaranteedFreePoint(obstacles, start.y));
954
+ }
815
955
  //#endregion
816
956
  //#region src/constraints.ts
817
957
  const OK = { ok: true };
@@ -3184,6 +3324,7 @@ exports.NODE_LINK_KINDS = NODE_LINK_KINDS;
3184
3324
  exports.PISTAR_TOOL = PISTAR_TOOL;
3185
3325
  exports.PistarParseError = PistarParseError;
3186
3326
  exports.PistarWriteError = PistarWriteError;
3327
+ exports.SUGGEST_MARGIN = SUGGEST_MARGIN;
3187
3328
  exports.actorBounds = actorBounds;
3188
3329
  exports.addDependency = addDependency;
3189
3330
  exports.addElement = addElement;
@@ -3262,6 +3403,7 @@ exports.removeLeg = removeLeg;
3262
3403
  exports.setHostMetamodel = setHostMetamodel;
3263
3404
  exports.shortKindName = shortKindName;
3264
3405
  exports.string = string;
3406
+ exports.suggestPosition = suggestPosition;
3265
3407
  exports.targetsLinks = targetsLinks;
3266
3408
  exports.toPistar = toPistar;
3267
3409
  exports.toPistarObject = toPistarObject;