sysml-diagram 0.20.2 → 0.21.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/out/main.js CHANGED
@@ -162498,8 +162498,37 @@ function disjointTargets(node) {
162498
162498
  return out;
162499
162499
  }
162500
162500
  var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
162501
+ shared;
162501
162502
  perParseCache = /* @__PURE__ */ new WeakMap();
162502
162503
  featurePaths = new FeaturePathResolver();
162504
+ annotationFeaturePaths = /* @__PURE__ */ new Map();
162505
+ indexEpoch = 0;
162506
+ // REQ-210 — Annotation graphics
162507
+ // Shared services are optional so pure single-document tests stay cheap. In
162508
+ // production they provide both workspace documents and the linker's private
162509
+ // lazy library documents used by inherited members.
162510
+ constructor(shared) {
162511
+ this.shared = shared;
162512
+ shared?.workspace.DocumentBuilder.onBuildPhase(DocumentState.IndexedContent, () => {
162513
+ this.indexEpoch += 1;
162514
+ });
162515
+ }
162516
+ externalDocumentRoot(uri) {
162517
+ if (!this.shared)
162518
+ return void 0;
162519
+ const parsed = URI2.parse(uri);
162520
+ try {
162521
+ const services = this.shared.ServiceRegistry.getServices(parsed);
162522
+ const linker = services.references.Linker;
162523
+ const linked = linker.resolveDocumentRoot?.(parsed);
162524
+ if (linked)
162525
+ return linked;
162526
+ } catch {
162527
+ }
162528
+ if (!this.shared.workspace.LangiumDocuments.hasDocument(parsed))
162529
+ return void 0;
162530
+ return this.shared.workspace.LangiumDocuments.getDocument(parsed)?.parseResult?.value;
162531
+ }
162503
162532
  perParse(root4) {
162504
162533
  let entry = this.perParseCache.get(root4);
162505
162534
  if (!entry) {
@@ -162528,12 +162557,52 @@ var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
162528
162557
  const key = `${kind}\0${preferFileOverview ? "F" : "-"}\0${rootSymbol ?? ""}`;
162529
162558
  const cacheKey = kind === "grv" ? `${key}::${gridPreset}${gridPreset === "matrix" ? `::${matrixRelationship}` : ""}` : key;
162530
162559
  const hit = cache.models.get(cacheKey);
162560
+ if (hit && hit.indexEpoch === this.indexEpoch && this.externalRootsAreCurrent(hit.externalRoots))
162561
+ return hit.model;
162531
162562
  if (hit)
162532
- return hit;
162563
+ cache.models.delete(cacheKey);
162533
162564
  const model = this.computeDiagramModel(document2, root4, rootSymbol, kind, preferFileOverview, gridPreset, matrixRelationship);
162534
- cache.models.set(cacheKey, model);
162565
+ cache.models.set(cacheKey, {
162566
+ model,
162567
+ externalRoots: this.externalRootsOf(model, document2.uri.toString()),
162568
+ indexEpoch: this.indexEpoch
162569
+ });
162535
162570
  return model;
162536
162571
  }
162572
+ // REQ-210 — Annotation graphics
162573
+ // A model may project members and notes from another document. The anchor's
162574
+ // parse identity does not change when that source file is edited, so validate
162575
+ // those dependency roots before returning a warm per-anchor cache entry.
162576
+ externalRootsAreCurrent(roots) {
162577
+ for (const [uri, root4] of roots) {
162578
+ if (this.externalDocumentRoot(uri) !== root4)
162579
+ return false;
162580
+ }
162581
+ return true;
162582
+ }
162583
+ externalRootsOf(model, anchorUri) {
162584
+ const uris = /* @__PURE__ */ new Set();
162585
+ const add = (source) => {
162586
+ if (source?.uri && source.uri !== anchorUri)
162587
+ uris.add(source.uri);
162588
+ };
162589
+ for (const node of model.nodes) {
162590
+ add(node.source);
162591
+ for (const port of node.ports ?? [])
162592
+ add(port.source);
162593
+ }
162594
+ for (const frame2 of model.frames ?? []) {
162595
+ add(frame2.source);
162596
+ for (const port of frame2.ports ?? [])
162597
+ add(port.source);
162598
+ }
162599
+ add(model.ivBoundary?.source);
162600
+ for (const port of model.ivBoundary?.ports ?? [])
162601
+ add(port.source);
162602
+ for (const edge of model.edges)
162603
+ add(edge.source);
162604
+ return new Map([...uris].map((uri) => [uri, this.externalDocumentRoot(uri)]));
162605
+ }
162537
162606
  computeDiagramModel(document2, root4, rootSymbol, kind, preferFileOverview, gridPreset = "requirements", matrixRelationship = "allocation") {
162538
162607
  const uri = document2.uri.toString();
162539
162608
  const empty2 = (note) => ({
@@ -162549,28 +162618,126 @@ var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
162549
162618
  return empty2(`No anchor element found for the ${kind.toUpperCase()} view.`);
162550
162619
  const ctx = { uri, index: index2, anchor, gridPreset, matrixRelationship };
162551
162620
  const overview = isPackage(anchor) || isDocument(anchor);
162552
- switch (kind) {
162553
- case "gv":
162554
- return this.buildGeneralView(ctx);
162555
- case "iv":
162556
- return overview ? this.buildInterconnectionOverview(ctx) : this.buildInterconnectionView(ctx);
162557
- case "afv":
162558
- return overview ? this.buildActionFlowOverview(ctx) : this.buildActionFlowView(ctx);
162559
- case "stv":
162560
- return overview ? this.buildStateTransitionOverview(ctx) : this.buildStateTransitionView(ctx);
162561
- case "sv":
162562
- return overview ? this.buildSequenceOverview(ctx) : this.buildSequenceView(ctx);
162563
- case "cv":
162564
- return this.buildCaseView(ctx);
162565
- case "gev":
162566
- return overview ? this.buildGeometryOverview(ctx) : this.buildGeometryView(ctx);
162567
- case "grv":
162568
- return this.buildGridView(ctx);
162569
- case "bv":
162570
- return this.buildBrowserView(ctx);
162571
- default:
162572
- return empty2(`Unsupported diagram kind ${kind}.`);
162621
+ const build = () => {
162622
+ switch (kind) {
162623
+ case "gv":
162624
+ return this.buildGeneralView(ctx);
162625
+ case "iv":
162626
+ return overview ? this.buildInterconnectionOverview(ctx) : this.buildInterconnectionView(ctx);
162627
+ case "afv":
162628
+ return overview ? this.buildActionFlowOverview(ctx) : this.buildActionFlowView(ctx);
162629
+ case "stv":
162630
+ return overview ? this.buildStateTransitionOverview(ctx) : this.buildStateTransitionView(ctx);
162631
+ case "sv":
162632
+ return overview ? this.buildSequenceOverview(ctx) : this.buildSequenceView(ctx);
162633
+ case "cv":
162634
+ return this.buildCaseView(ctx);
162635
+ case "gev":
162636
+ return overview ? this.buildGeometryOverview(ctx) : this.buildGeometryView(ctx);
162637
+ case "grv":
162638
+ return this.buildGridView(ctx);
162639
+ case "bv":
162640
+ return this.buildBrowserView(ctx);
162641
+ default:
162642
+ return empty2(`Unsupported diagram kind ${kind}.`);
162643
+ }
162644
+ };
162645
+ return this.withNotes(build(), root4, index2, uri);
162646
+ }
162647
+ // REQ-210 — Annotation graphics
162648
+ // issue #229: attach the modeled notes of every DRAWN element to that
162649
+ // element, on every view.
162650
+ //
162651
+ // The General View builds its own richer annotation pass (comment + rep +
162652
+ // metadata, with package placement), the Browser View lists annotations as
162653
+ // tree rows, and the Grid View is a table, so all three are left untouched.
162654
+ // Every other view gets its notes here, once, after the builder (and its
162655
+ // overview tiling / id prefixing) has settled the final node ids.
162656
+ //
162657
+ // A `comment` is always a note. A `doc` documents its owner and already has
162658
+ // a textual home in that owner's `doc` compartment (issue #107), so it only
162659
+ // becomes a note on the views that draw no compartments at all.
162660
+ withNotes(model, root4, index2, uri) {
162661
+ if (model.kind === "gv" || model.kind === "bv" || model.kind === "grv")
162662
+ return model;
162663
+ const docAsNote = model.kind === "sv" || model.kind === "gev";
162664
+ const drawn = /* @__PURE__ */ new Map();
162665
+ const externalUris = /* @__PURE__ */ new Set();
162666
+ const key = (source) => source && `${source.uri}:${source.range.start.line}:${source.range.start.character}`;
162667
+ const claim = (source, target) => {
162668
+ const k = key(source);
162669
+ if (!k)
162670
+ return;
162671
+ if (source && source.uri !== uri)
162672
+ externalUris.add(source.uri);
162673
+ const list = drawn.get(k);
162674
+ if (list)
162675
+ list.push(target);
162676
+ else
162677
+ drawn.set(k, [target]);
162678
+ };
162679
+ for (const n2 of model.nodes) {
162680
+ claim(n2.source, { id: n2.id, parent: n2.parent, frame: n2.frame });
162681
+ for (const port of n2.ports ?? []) {
162682
+ claim(port.source, { id: port.id, parent: n2.parent, frame: n2.frame, port: true });
162683
+ }
162684
+ }
162685
+ for (const f of model.frames ?? []) {
162686
+ claim(f.source, { id: f.id, frame: f.id });
162687
+ for (const port of f.ports ?? []) {
162688
+ claim(port.source, { id: port.id, frame: f.id, port: true });
162689
+ }
162573
162690
  }
162691
+ if (model.ivBoundary) {
162692
+ claim(model.ivBoundary.source, { id: model.ivBoundary.id, frame: model.ivBoundary.id });
162693
+ for (const port of model.ivBoundary.ports ?? []) {
162694
+ claim(port.source, { id: port.id, frame: model.ivBoundary.id, port: true });
162695
+ }
162696
+ }
162697
+ if (!drawn.size)
162698
+ return model;
162699
+ const nodes = [...model.nodes];
162700
+ const edges = [...model.edges];
162701
+ let noteCount = 0;
162702
+ let edgeCount = edges.length;
162703
+ const note = (annotation) => {
162704
+ const targets = drawn.get(key(sourceOf2(annotation.owner, uri)) ?? "")?.filter((target) => annotation.keyword !== "doc" || docAsNote || target.port);
162705
+ if (!targets?.length)
162706
+ return;
162707
+ for (const target of targets) {
162708
+ const id2 = `__note_${noteCount++}__`;
162709
+ nodes.push({
162710
+ id: id2,
162711
+ name: annotation.label,
162712
+ keyword: annotation.keyword,
162713
+ isDef: false,
162714
+ shape: "annotation",
162715
+ type: annotation.body,
162716
+ // Sit in the same container as the element the note is about,
162717
+ // so the note travels with it instead of floating at the root.
162718
+ ...target.parent ? { parent: target.parent } : {},
162719
+ ...target.frame ? { frame: target.frame } : {},
162720
+ source: sourceOf2(annotation.annotation, uri)
162721
+ });
162722
+ edges.push({
162723
+ id: `e${edgeCount++}`,
162724
+ from: id2,
162725
+ to: target.id,
162726
+ kind: "annotation",
162727
+ source: sourceOf2(annotation.annotation, uri)
162728
+ });
162729
+ }
162730
+ };
162731
+ for (const annotation of this.semanticAnnotations(root4, index2))
162732
+ note(annotation);
162733
+ for (const externalUri of externalUris) {
162734
+ const externalRoot = this.externalDocumentRoot(externalUri);
162735
+ if (externalRoot) {
162736
+ for (const annotation of this.semanticAnnotations(externalRoot, this.localIndex(externalRoot)))
162737
+ note(annotation);
162738
+ }
162739
+ }
162740
+ return noteCount ? { ...model, nodes, edges } : model;
162574
162741
  }
162575
162742
  // REQ-224 — Detect the file/anchor-specific view kinds that will render
162576
162743
  // substantive diagram content. The extension uses this to limit the
@@ -162649,6 +162816,132 @@ var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
162649
162816
  cache.contents = contents;
162650
162817
  return contents;
162651
162818
  }
162819
+ // REQ-210 — Annotation graphics
162820
+ // Keep the whole-document filter out of each view build. Target resolution
162821
+ // remains live because the workspace index can become more complete without
162822
+ // reparsing the document.
162823
+ annotationMembers(root4) {
162824
+ const cache = this.perParse(root4);
162825
+ if (cache.annotations)
162826
+ return cache.annotations;
162827
+ const comments = [];
162828
+ const docs = [];
162829
+ for (const node of [root4, ...this.rootContents(root4)]) {
162830
+ if (node.$type === "CommentStmt")
162831
+ comments.push(node);
162832
+ else if (node.$type === "DocCommentMember")
162833
+ docs.push(node);
162834
+ }
162835
+ cache.annotations = { comments, docs };
162836
+ return cache.annotations;
162837
+ }
162838
+ // REQ-210 — Annotation graphics
162839
+ // Resolve a document's modeled annotations once for every workspace-index
162840
+ // epoch. Different diagram kinds then only match semantic owners to their
162841
+ // rendered source identities instead of walking and resolving the AST again.
162842
+ semanticAnnotations(root4, index2) {
162843
+ const cache = this.perParse(root4);
162844
+ if (cache.semanticAnnotations?.epoch === this.indexEpoch)
162845
+ return cache.semanticAnnotations.values;
162846
+ const members = this.annotationMembers(root4);
162847
+ const values2 = [];
162848
+ for (const comment of members.comments) {
162849
+ const rawTargets = comment.targets ?? [];
162850
+ const owners = rawTargets.length ? this.commentTargets(comment, index2) : [comment.$container];
162851
+ for (const owner of owners) {
162852
+ if (!owner)
162853
+ continue;
162854
+ values2.push({
162855
+ annotation: comment,
162856
+ owner,
162857
+ keyword: "comment",
162858
+ label: nameOf2(comment) ?? "comment",
162859
+ body: annotationText(trailingBlockBody(comment))
162860
+ });
162861
+ }
162862
+ }
162863
+ for (const doc of members.docs) {
162864
+ if (!doc.$container)
162865
+ continue;
162866
+ values2.push({
162867
+ annotation: doc,
162868
+ owner: doc.$container,
162869
+ keyword: "doc",
162870
+ label: nameOf2(doc) ?? "doc",
162871
+ body: annotationText(docCommentBody(doc))
162872
+ });
162873
+ }
162874
+ cache.semanticAnnotations = { epoch: this.indexEpoch, values: values2 };
162875
+ return values2;
162876
+ }
162877
+ annotationPathResolver(node) {
162878
+ if (!this.shared)
162879
+ return this.featurePaths;
162880
+ try {
162881
+ const services = this.shared.ServiceRegistry.getServices(ast_utils_exports.getDocument(node).uri);
162882
+ let resolver = this.annotationFeaturePaths.get(services);
162883
+ if (!resolver) {
162884
+ resolver = new FeaturePathResolver(services);
162885
+ this.annotationFeaturePaths.set(services, resolver);
162886
+ }
162887
+ return resolver;
162888
+ } catch {
162889
+ return this.featurePaths;
162890
+ }
162891
+ }
162892
+ annotationDescriptionNode(context, description) {
162893
+ if (!description)
162894
+ return void 0;
162895
+ if (!this.shared)
162896
+ return description.node;
162897
+ try {
162898
+ const services = this.shared.ServiceRegistry.getServices(ast_utils_exports.getDocument(context).uri);
162899
+ const linker = services.references.Linker;
162900
+ return linker.resolveIndexedNode?.(description) ?? description.node;
162901
+ } catch {
162902
+ return description.node;
162903
+ }
162904
+ }
162905
+ // REQ-210 — Annotation graphics
162906
+ /** Resolve every `comment about` target by semantic path identity. */
162907
+ commentTargets(comment, index2) {
162908
+ const rawTargets = comment.targets ?? [];
162909
+ if (!rawTargets.length)
162910
+ return [];
162911
+ const root4 = this.documentRootOf(comment);
162912
+ const all = [root4, ...this.rootContents(root4)];
162913
+ const resolver = this.annotationPathResolver(comment);
162914
+ const resolved = rawTargets.map((raw, occurrence) => {
162915
+ const segment = resolver.resolvePropertyPath(comment, "targets", occurrence).segments.at(-1);
162916
+ const linked = segment?.target ?? this.annotationDescriptionNode(comment, segment?.description);
162917
+ if (linked)
162918
+ return linked;
162919
+ const path10 = this.featurePath(raw)?.trim();
162920
+ if (!path10)
162921
+ return void 0;
162922
+ const unrooted = path10.replace(/^\s*\$\s*::\s*/u, "");
162923
+ if (!/::|\./u.test(unrooted)) {
162924
+ for (let scope = comment.$container; scope; scope = scope.$container) {
162925
+ const local = membersOf(scope).find((member) => member !== comment && nameOf2(member) === unrooted);
162926
+ if (local)
162927
+ return local;
162928
+ const declaringIndex = this.localIndex(this.documentRootOf(scope));
162929
+ for (const inherited of this.inheritedTypesOf(scope, declaringIndex)) {
162930
+ const inheritedMember = membersOf(inherited).find((member) => nameOf2(member) === unrooted);
162931
+ if (inheritedMember)
162932
+ return inheritedMember;
162933
+ }
162934
+ }
162935
+ return index2.get(unrooted);
162936
+ }
162937
+ const qualified = all.filter((candidate) => {
162938
+ const qname = qnameOf(candidate);
162939
+ return qname.length > 0 && pathMatchesQName(unrooted, qname);
162940
+ });
162941
+ return qualified.length === 1 ? qualified[0] : void 0;
162942
+ }).filter((target) => target !== void 0);
162943
+ return [...new Set(resolved)];
162944
+ }
162652
162945
  // REQ-192/REQ-193 — prefer Langium's linked typing target. The diagram's
162653
162946
  // local index deliberately contains only the active document, so consulting
162654
162947
  // it alone drops structure declared in an imported workspace/library file
@@ -163723,8 +164016,7 @@ var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
163723
164016
  }
163724
164017
  }
163725
164018
  let annotationCount = 0;
163726
- const annotate = (m, keyword, label, detail, target) => {
163727
- const owner = target ?? m.$container;
164019
+ const annotate = (m, keyword, label, detail, targets) => {
163728
164020
  const annId = `__annotation_${annotationCount++}__`;
163729
164021
  nodes.push({
163730
164022
  id: annId,
@@ -163736,17 +164028,33 @@ var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
163736
164028
  ...packageOf2(m),
163737
164029
  source: sourceOf2(m, uri)
163738
164030
  });
163739
- if (owner && ids.has(owner)) {
163740
- edges.push({ id: `e${e++}`, from: annId, to: ids.get(owner), kind: "annotation", source: sourceOf2(m, uri) });
164031
+ const owners = targets?.length ? targets : m.$container ? [m.$container] : [];
164032
+ for (const owner of new Set(owners)) {
164033
+ if (!ids.has(owner))
164034
+ continue;
164035
+ edges.push({
164036
+ id: `e${e++}`,
164037
+ from: annId,
164038
+ to: ids.get(owner),
164039
+ kind: "annotation",
164040
+ source: sourceOf2(m, uri)
164041
+ });
164042
+ }
164043
+ };
164044
+ const drawnAnnotationTarget = (target) => {
164045
+ for (let current2 = target; current2; current2 = current2.$container) {
164046
+ if (ids.has(current2))
164047
+ return current2;
163741
164048
  }
164049
+ return void 0;
163742
164050
  };
163743
164051
  for (const m of all) {
163744
164052
  if (m.$type === "CommentStmt") {
163745
- const targets = m.targets ?? [];
163746
- const target = targets.length ? byName.get(lastSeg(String(this.featurePath(targets[0]) ?? ""))) : void 0;
163747
- if (targets.length && !target)
164053
+ const rawTargets = m.targets ?? [];
164054
+ const targets = rawTargets.length ? this.commentTargets(m, index2).map(drawnAnnotationTarget).filter((target) => target !== void 0) : void 0;
164055
+ if (rawTargets.length && !targets?.length)
163748
164056
  continue;
163749
- annotate(m, "comment", nameOf2(m) ?? "comment", annotationText(trailingBlockBody(m)), target);
164057
+ annotate(m, "comment", nameOf2(m) ?? "comment", annotationText(trailingBlockBody(m)), targets);
163750
164058
  } else if (m.$type === "RepStmt") {
163751
164059
  const lang = `language "${String(m.language ?? "")}"`;
163752
164060
  const body = annotationText(trailingBlockBody(m));
@@ -163757,7 +164065,7 @@ var SysmlDiagramModelProvider = class _SysmlDiagramModelProvider {
163757
164065
  continue;
163758
164066
  const body = annotationText(docCommentBody(m));
163759
164067
  if (body)
163760
- annotate(m, "doc", nameOf2(m) ?? "doc", body, owner);
164068
+ annotate(m, "doc", nameOf2(m) ?? "doc", body, owner ? [owner] : void 0);
163761
164069
  } else if (isMetadataDecl(m) && m.isDef !== true && (nameOf2(m) || typeText(m)) && !ids.has(m)) {
163762
164070
  const attrs = metadataAttrsText(m);
163763
164071
  const detail = typeText(m) ? `${typeText(m)}${attrs ? ` { ${attrs} }` : ""}` : attrs;
@@ -170103,6 +170411,9 @@ function resolveEffectiveSubject(node) {
170103
170411
 
170104
170412
  // ../language-server/out/src/messages.js
170105
170413
  var DIAGNOSTIC_MESSAGES = {
170414
+ // REQ-006 - a broad diagram anchor cannot prove which typed occurrence owns
170415
+ // a connection whose projected ports resolve to their shared definition.
170416
+ EDIT_RELATION_OWNER_AMBIGUOUS_PROJECTED_USAGE: "The relation owner is ambiguous between a type definition and a projected usage. Select the concrete occurrence or explicitly synchronize the definition before creating the connection.",
170106
170417
  // REQ-317 — expression paths diagnose the exact missing feature segment.
170107
170418
  RES001_FEATURE_PATH_SEGMENT: (segment, owner) => `Feature path segment '${segment}' does not exist on '${owner}'.`,
170108
170419
  LEX001_UNTERMINATED_STRING: `Unterminated string literal - missing closing '"'.`,
@@ -178000,6 +178311,20 @@ var SysmlLinker = class extends DefaultLinker {
178000
178311
  resolveIndexedNode(nodeDescription) {
178001
178312
  return this.loadAstNode(nodeDescription);
178002
178313
  }
178314
+ // REQ-210 — Annotation graphics
178315
+ /**
178316
+ * Return the parsed root for a live workspace or lazily loaded library file.
178317
+ * Diagram projection uses this to collect annotations declared beside an
178318
+ * inherited member without adding standard-library files to the workspace.
178319
+ */
178320
+ resolveDocumentRoot(uri) {
178321
+ const live = this.langiumDocuments().getDocument(uri)?.parseResult?.value;
178322
+ if (live)
178323
+ return live;
178324
+ if (!isStandardLibraryUri(uri))
178325
+ return void 0;
178326
+ return this.parsedLibraryDocument(uri)?.parseResult?.value;
178327
+ }
178003
178328
  /** Parse (once) the standard-library file a precomputed description points at.
178004
178329
  * Prefers an already-live workspace document when one exists (runtime-loaded
178005
178330
  * library, or the user opened the file) so there is never a second copy. */
@@ -181911,19 +182236,15 @@ var OPPOSITE_DOCK_SIDE = {
181911
182236
  function portDockSides(side) {
181912
182237
  return [side, OPPOSITE_DOCK_SIDE[side]];
181913
182238
  }
181914
- var PORT_MOVE_STRIP_ACROSS = 10;
181915
182239
  var PORT_MOVE_STRIP_ALONG = 24;
181916
- var PORT_CONNECT_DOT_HIT = 18;
182240
+ var PORT_CONNECT_DOT_HIT = 10;
181917
182241
  function portInteractionStrip(rect, side) {
181918
182242
  const vertical = side === "left" || side === "right";
181919
- const dotHalf = PORT_CONNECT_DOT_HIT / 2;
181920
- const across = Math.max(PORT_MOVE_STRIP_ACROSS, rect.across - dotHalf);
181921
- const inward = -(rect.across - across) / 2;
181922
- const n2 = DOCK_NORMAL[side];
182243
+ const across = Math.max(1, rect.across - PORT_CONNECT_DOT_HIT);
181923
182244
  const run = Math.max(PORT_MOVE_STRIP_ALONG, rect.along);
181924
182245
  return {
181925
- cx: rect.cx + n2.x * inward,
181926
- cy: rect.cy + n2.y * inward,
182246
+ cx: rect.cx,
182247
+ cy: rect.cy,
181927
182248
  w: vertical ? across : run,
181928
182249
  h: vertical ? run : across
181929
182250
  };
@@ -182201,6 +182522,7 @@ function portLabelPlacement(side, x, y, label, half = PORT_GLYPH_HALF, alongHalf
182201
182522
  }
182202
182523
  var SEQ_MSG_TOP = 76;
182203
182524
  var SEQ_MSG_GAP = 46;
182525
+ var SEQ_NOTE_GAP = 40;
182204
182526
  function sequenceSlotBoundaryY(slot) {
182205
182527
  return SEQ_MSG_TOP + (slot - 0.5) * SEQ_MSG_GAP;
182206
182528
  }
@@ -182299,6 +182621,12 @@ function gvNodeCategory(node) {
182299
182621
  };
182300
182622
  return alias[k] ?? "other";
182301
182623
  }
182624
+ function withoutOrphanedAnnotations(original, nodes, edges) {
182625
+ const originallyAttached = new Set(original.edges.filter((edge) => edge.kind === "annotation").map((edge) => edge.from));
182626
+ if (originallyAttached.size === 0) return nodes;
182627
+ const stillAttached = new Set(edges.filter((edge) => edge.kind === "annotation").map((edge) => edge.from));
182628
+ return nodes.filter((node) => node.shape !== "annotation" || !originallyAttached.has(node.id) || stillAttached.has(node.id));
182629
+ }
182302
182630
  function applyGvFilters(model, hidden) {
182303
182631
  if (model.kind !== "gv" || hidden.size === 0) return model;
182304
182632
  const nodes = model.nodes.filter((n2) => n2.shape === "package" || !hidden.has(gvNodeCategory(n2)));
@@ -182309,7 +182637,7 @@ function applyGvFilters(model, hidden) {
182309
182637
  for (const p of n2.ports ?? []) keptIds.add(p.id);
182310
182638
  }
182311
182639
  const edges = model.edges.filter((e) => keptIds.has(e.from) && keptIds.has(e.to));
182312
- return { ...model, nodes, edges };
182640
+ return { ...model, nodes: withoutOrphanedAnnotations(model, nodes, edges), edges };
182313
182641
  }
182314
182642
  var PAD = 10;
182315
182643
  var NODE_W = 168;
@@ -182806,9 +183134,10 @@ function applyCaseDefs(model, show) {
182806
183134
  if (hidden.size === 0) return model;
182807
183135
  const edges = model.edges.filter((e) => !hidden.has(e.from) && !hidden.has(e.to));
182808
183136
  const retainedEndpoints = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
183137
+ const nodes = model.nodes.filter((n2) => !hidden.has(n2.id) && (n2.meta?.cvAuxiliary !== true || retainedEndpoints.has(n2.id)));
182809
183138
  return {
182810
183139
  ...model,
182811
- nodes: model.nodes.filter((n2) => !hidden.has(n2.id) && (n2.meta?.cvAuxiliary !== true || retainedEndpoints.has(n2.id))),
183140
+ nodes: withoutOrphanedAnnotations(model, nodes, edges),
182812
183141
  edges
182813
183142
  };
182814
183143
  }
@@ -182888,7 +183217,7 @@ function applyBehaviorFilters(model, filters) {
182888
183217
  ...nextFrames.flatMap((f) => (f.ports ?? []).map((p) => p.id))
182889
183218
  ]);
182890
183219
  const edges = model.edges.filter((e) => !(model.kind === "afv" && !filters.definedBy && e.kind === "definedBy") && !hiddenPortIds.has(e.from) && !hiddenPortIds.has(e.to) && liveIds.has(e.from) && liveIds.has(e.to));
182891
- return { ...model, frames: nextFrames, nodes, edges };
183220
+ return { ...model, frames: nextFrames, nodes: withoutOrphanedAnnotations(model, nodes, edges), edges };
182892
183221
  }
182893
183222
  function collapseIvModel(model, hiddenInternals) {
182894
183223
  const frames = model.frames ?? [];
@@ -183331,8 +183660,6 @@ function modelToFlow(model, opts) {
183331
183660
  for (const n2 of renderModelNodes) for (const p of n2.ports ?? []) portIds.add(p.id);
183332
183661
  for (const f of frames) for (const p of f.ports ?? []) portIds.add(p.id);
183333
183662
  for (const p of boundary?.ports ?? []) portIds.add(p.id);
183334
- const portSideById = /* @__PURE__ */ new Map();
183335
- for (const n2 of nodes) for (const ph of n2.data.ports ?? []) portSideById.set(ph.port.id, ph.side);
183336
183663
  const ownerOfPort = /* @__PURE__ */ new Map();
183337
183664
  for (const n2 of renderModelNodes) for (const p of n2.ports ?? []) ownerOfPort.set(p.id, n2.id);
183338
183665
  for (const f of frames) for (const p of f.ports ?? []) ownerOfPort.set(p.id, f.id);
@@ -183391,8 +183718,8 @@ function modelToFlow(model, opts) {
183391
183718
  id: e.id,
183392
183719
  source,
183393
183720
  target,
183394
- ...fromPort ? { sourceHandle: portAnchorHandleId(e.from, savedSourcePortSide ?? portSideById.get(e.from) ?? "right") } : savedSourceHandle ? { sourceHandle: savedSourceHandle } : {},
183395
- ...toPort ? { targetHandle: portAnchorHandleId(e.to, savedTargetPortSide ?? portSideById.get(e.to) ?? "left") } : savedTargetHandle ? { targetHandle: savedTargetHandle } : {},
183721
+ ...fromPort ? { sourceHandle: savedSourcePortSide ? portAnchorHandleId(e.from, savedSourcePortSide) : e.from } : savedSourceHandle ? { sourceHandle: savedSourceHandle } : {},
183722
+ ...toPort ? { targetHandle: savedTargetPortSide ? portAnchorHandleId(e.to, savedTargetPortSide) : e.to } : savedTargetHandle ? { targetHandle: savedTargetHandle } : {},
183396
183723
  type: "sysml",
183397
183724
  data: {
183398
183725
  kind: model.kind,
@@ -183411,38 +183738,6 @@ function modelToFlow(model, opts) {
183411
183738
  ...frames.length > 0 || boundary ? { zIndex: 4 } : {}
183412
183739
  });
183413
183740
  }
183414
- const frameIds = /* @__PURE__ */ new Set([...frames.map((f) => f.id), ...boundary ? [boundary.id] : []]);
183415
- const parentOf = /* @__PURE__ */ new Map();
183416
- for (const n2 of renderModelNodes) parentOf.set(
183417
- n2.id,
183418
- n2.frame ?? (model.kind === "gv" && n2.parent && gvPackageIds.has(n2.parent) ? n2.parent : void 0) ?? (boundary ? boundary.id : void 0)
183419
- );
183420
- for (const f of frames) parentOf.set(f.id, f.parent);
183421
- const isInsideFrame = (nodeId, frameId) => {
183422
- let cur = nodeId;
183423
- const seen = /* @__PURE__ */ new Set();
183424
- while (cur && !seen.has(cur)) {
183425
- if (cur === frameId) return true;
183426
- seen.add(cur);
183427
- cur = parentOf.get(cur);
183428
- }
183429
- return false;
183430
- };
183431
- const delegated = /* @__PURE__ */ new Set();
183432
- for (const e of model.edges) {
183433
- const fo = ownerOfPort.get(e.from);
183434
- const to = ownerOfPort.get(e.to);
183435
- const fromOther = to ?? e.to;
183436
- const toOther = fo ?? e.from;
183437
- if (fo && frameIds.has(fo) && fromOther !== fo && isInsideFrame(fromOther, fo)) delegated.add(e.from);
183438
- if (to && frameIds.has(to) && toOther !== to && isInsideFrame(toOther, to)) delegated.add(e.to);
183439
- }
183440
- if (delegated.size) {
183441
- for (const fn of nodes) {
183442
- const owned = (fn.data.ports ?? []).filter((ph) => delegated.has(ph.port.id)).map((ph) => ph.port.id);
183443
- if (owned.length) fn.data = { ...fn.data, delegatedPorts: owned };
183444
- }
183445
- }
183446
183741
  return { nodes, edges };
183447
183742
  }
183448
183743
 
@@ -183531,6 +183826,7 @@ var GEO_TARGET_H = 520;
183531
183826
  var GEO_DEF_W = 400;
183532
183827
  var GEO_DEF_H = 300;
183533
183828
  var GEO_STRIP_GAP = 16;
183829
+ var GEO_STRIP_INSET = 16;
183534
183830
  var GEO_FRAME_ID = "__geoframe__";
183535
183831
  var GEO_EMPTY_W = 560;
183536
183832
  var GEO_EMPTY_H = 320;
@@ -183599,7 +183895,7 @@ function layoutGeometry(nodes, edges, meta) {
183599
183895
  const frame2 = makeGeoFrameNode(kind, GEO_EMPTY_W, GEO_EMPTY_H, {
183600
183896
  geoEmpty: { hLabel: axes.h ?? "x", vLabel: axes.v ?? "z", unit }
183601
183897
  });
183602
- placeStrip(strip, GEO_MARGIN_LEFT + 16, GEO_EMPTY_H + GEO_STRIP_GAP);
183898
+ placeStrip(strip, GEO_MARGIN_LEFT + GEO_STRIP_INSET, GEO_EMPTY_H + GEO_STRIP_GAP);
183603
183899
  return { nodes: [frame2, ...strip], edges };
183604
183900
  }
183605
183901
  const is3d = placed.some((n2) => n2.data.node.geo.shape !== void 0 || n2.data.node.geo.z !== void 0);
@@ -193583,7 +193879,6 @@ function geometrySignature(nodes) {
193583
193879
  for (const p of n2.data.ports ?? []) {
193584
193880
  out += `|${p.port.id},${p.side},${p.offset},${p.along ?? ""},${p.shift?.along ?? ""},${p.shift?.across ?? ""}`;
193585
193881
  }
193586
- for (const id2 of [...n2.data.delegatedPorts ?? []].sort()) out += `|i:${id2}`;
193587
193882
  for (const d of n2.data.structuralDocks ?? []) out += `|d:${d.id},${d.side},${d.offset},${d.family}`;
193588
193883
  out += ";";
193589
193884
  }
@@ -195147,10 +195442,25 @@ function semanticPortHandle(node, handle) {
195147
195442
  if (parsed && portOnNode(node, parsed.portId)) return parsed.portId;
195148
195443
  return portOnNode(node, handle ?? void 0) ? handle ?? void 0 : void 0;
195149
195444
  }
195150
- function choosePortAnchorSide(node, portId) {
195445
+ function validConcretePortAnchor(node, handle) {
195446
+ const parsed = parsePortAnchorHandleId(handle);
195447
+ const ph = parsed ? portOnNode(node, parsed.portId) : void 0;
195448
+ return !!ph && portConnectHandleIds(ph.port.id, ph.side).includes(handle);
195449
+ }
195450
+ function isStrictDescendant(nodeId, ancestorId, byId) {
195451
+ let cur = byId.get(nodeId)?.parentId;
195452
+ const seen = /* @__PURE__ */ new Set();
195453
+ while (cur && !seen.has(cur)) {
195454
+ if (cur === ancestorId) return true;
195455
+ seen.add(cur);
195456
+ cur = byId.get(cur)?.parentId;
195457
+ }
195458
+ return false;
195459
+ }
195460
+ function inferredPortAnchorSide(node, other, portId, byId) {
195151
195461
  const ph = portOnNode(node, portId);
195152
195462
  if (!ph) return "right";
195153
- return node.data.delegatedPorts?.includes(portId) ? OPPOSITE[ph.side] : ph.side;
195463
+ return other.id === node.id || isStrictDescendant(other.id, node.id, byId) ? OPPOSITE[ph.side] : ph.side;
195154
195464
  }
195155
195465
  function assignPortAnchorSides(nodes, edges) {
195156
195466
  const byId = new Map(nodes.map((n2) => [n2.id, n2]));
@@ -195158,10 +195468,14 @@ function assignPortAnchorSides(nodes, edges) {
195158
195468
  const source = byId.get(e.source);
195159
195469
  const target = byId.get(e.target);
195160
195470
  if (!source || !target) continue;
195161
- const sourcePort = semanticPortHandle(source, e.sourceHandle);
195162
- const targetPort = semanticPortHandle(target, e.targetHandle);
195163
- if (sourcePort) e.sourceHandle = portAnchorHandleId(sourcePort, choosePortAnchorSide(source, sourcePort));
195164
- if (targetPort) e.targetHandle = portAnchorHandleId(targetPort, choosePortAnchorSide(target, targetPort));
195471
+ const sourcePort = semanticPortHandle(source, e.sourceHandle) ?? portOnNode(source, e.data?.edge.from)?.port.id;
195472
+ const targetPort = semanticPortHandle(target, e.targetHandle) ?? portOnNode(target, e.data?.edge.to)?.port.id;
195473
+ if (sourcePort && !validConcretePortAnchor(source, e.sourceHandle)) {
195474
+ e.sourceHandle = portAnchorHandleId(sourcePort, inferredPortAnchorSide(source, target, sourcePort, byId));
195475
+ }
195476
+ if (targetPort && !validConcretePortAnchor(target, e.targetHandle)) {
195477
+ e.targetHandle = portAnchorHandleId(targetPort, inferredPortAnchorSide(target, source, targetPort, byId));
195478
+ }
195165
195479
  }
195166
195480
  }
195167
195481
  function densifiedSideOffsets(base, needed) {
@@ -195507,13 +195821,18 @@ var SEQ_EVENT_GAP = 22;
195507
195821
  function layoutSequence(nodes, edges, overrides) {
195508
195822
  const ov = overrides ?? {};
195509
195823
  const lifelines = nodes.filter((n2) => n2.data.node?.shape === "lifeline").sort((a2, b) => Number(a2.data.node?.meta?.order ?? 0) - Number(b.data.node?.meta?.order ?? 0));
195824
+ const lifelineIds = new Set(lifelines.map((n2) => n2.id));
195825
+ const noteNodes = nodes.filter((n2) => n2.data.node?.shape === "annotation");
195826
+ const isMessage = (edge) => edge.data?.edge.kind === "message" && lifelineIds.has(edge.source) && lifelineIds.has(edge.target);
195827
+ const nonMessageEdges = edges.filter((edge) => !isMessage(edge));
195828
+ const msgs = edges.filter(isMessage);
195510
195829
  const slotFor = (e, i) => {
195511
195830
  const s = e.data?.edge.meta?.slot;
195512
195831
  return typeof s === "number" && Number.isFinite(s) && s >= 0 ? s : i;
195513
195832
  };
195514
- const msgY = edges.map((e, i) => MSG_TOP + slotFor(e, i) * MSG_GAP);
195515
- const lastMsgY = edges.length ? Math.max(...msgY) : MSG_TOP;
195516
- let maxSlot = edges.length ? Math.max(...edges.map(slotFor)) : -1;
195833
+ const msgY = msgs.map((e, i) => MSG_TOP + slotFor(e, i) * MSG_GAP);
195834
+ const lastMsgY = msgs.length ? Math.max(...msgY) : MSG_TOP;
195835
+ let maxSlot = msgs.length ? Math.max(...msgs.map(slotFor)) : -1;
195517
195836
  const eventBase = lastMsgY + 22;
195518
195837
  let maxEventY = 0;
195519
195838
  const eventsByLifeline = /* @__PURE__ */ new Map();
@@ -195536,7 +195855,7 @@ function layoutSequence(nodes, edges, overrides) {
195536
195855
  baseX.push(cursorX);
195537
195856
  cursorX += n2.data.w + LIFELINE_MIN_GAP;
195538
195857
  }
195539
- const msgHandles = edges.map((_edge, i) => ({ id: `m${i}`, y: msgY[i] }));
195858
+ const msgHandles = msgs.map((_edge, i) => ({ id: `m${i}`, y: msgY[i] }));
195540
195859
  const insertHandles = Array.from({ length: Math.max(maxSlot, -1) + 2 }, (_2, k) => ({
195541
195860
  id: `d${k}`,
195542
195861
  y: sequenceSlotBoundaryY(k),
@@ -195547,7 +195866,7 @@ function layoutSequence(nodes, edges, overrides) {
195547
195866
  const w = n2.data.w;
195548
195867
  const x = overrideFor(n2, ov)?.x ?? baseX[i];
195549
195868
  xOf.set(n2.id, x + w / 2);
195550
- const ys = edges.map((e, idx) => e.source === n2.id || e.target === n2.id ? msgY[idx] : void 0).filter((y) => y != null);
195869
+ const ys = msgs.map((e, idx) => e.source === n2.id || e.target === n2.id ? msgY[idx] : void 0).filter((y) => y != null);
195551
195870
  const activation = ys.length ? { from: Math.min(...ys) - 6, to: Math.max(...ys) + 6 } : void 0;
195552
195871
  return {
195553
195872
  ...n2,
@@ -195566,7 +195885,7 @@ function layoutSequence(nodes, edges, overrides) {
195566
195885
  draggable: true
195567
195886
  };
195568
195887
  });
195569
- const outEdges = edges.map((e, i) => {
195888
+ const outEdges = msgs.map((e, i) => {
195570
195889
  const y = msgY[i];
195571
195890
  return {
195572
195891
  ...e,
@@ -195603,7 +195922,21 @@ function layoutSequence(nodes, edges, overrides) {
195603
195922
  const h = Math.max(40, bottom - top);
195604
195923
  return { ...n2, position: { x: left, y: top }, width: w, height: h, data: { ...n2.data, w, h }, zIndex: 0 };
195605
195924
  });
195606
- return { nodes: [...fragmentNodes, ...out], edges: outEdges, height: bodyH };
195925
+ const noteX = out.length ? Math.max(...out.map((node) => node.position.x + (node.width ?? node.data.w))) + SEQ_NOTE_GAP : 20 + SEQ_NOTE_GAP;
195926
+ let noteY = MSG_TOP;
195927
+ const placedNotes = noteNodes.map((n2) => {
195928
+ const o = overrideFor(n2, ov);
195929
+ const y = o?.y ?? noteY;
195930
+ noteY = Math.max(noteY, y + n2.data.h + SEQ_NOTE_GAP);
195931
+ return { ...n2, position: { x: o?.x ?? noteX, y }, draggable: true };
195932
+ });
195933
+ const sequenceNodes = [...fragmentNodes, ...out, ...placedNotes];
195934
+ assignEdgeSides(sequenceNodes, nonMessageEdges);
195935
+ return {
195936
+ nodes: sequenceNodes,
195937
+ edges: [...outEdges, ...nonMessageEdges],
195938
+ height: Math.max(bodyH, noteY)
195939
+ };
195607
195940
  }
195608
195941
  var TILE_PAD = 22;
195609
195942
  var TILE_HEADER = 38;
@@ -195667,7 +196000,12 @@ async function layoutTiledOverview(model, nodes, edges, args) {
195667
196000
  }));
195668
196001
  const localOverrides = overrideSubset(members, args.overrides);
195669
196002
  const meta = tileMeta[frame2.id];
195670
- const laidPromise = model.kind === "sv" ? Promise.resolve(layoutSequence(detached, tileEdges, localOverrides)) : model.kind === "gev" ? Promise.resolve(layoutGeometry(detached, tileEdges, meta ?? model.meta)) : layoutFlow(detached, tileEdges, {
196003
+ const laidPromise = model.kind === "sv" ? Promise.resolve(layoutSequence(detached, tileEdges, localOverrides)) : model.kind === "gev" ? Promise.resolve(layoutGeometryWithAnnotationEdges(
196004
+ detached,
196005
+ tileEdges,
196006
+ meta ?? model.meta,
196007
+ args.connectPointSpacing
196008
+ )) : layoutFlow(detached, tileEdges, {
195671
196009
  ...args,
195672
196010
  overrides: localOverrides,
195673
196011
  kind: args.kind ?? model.kind,
@@ -196121,13 +196459,25 @@ async function layoutBands(nodes, edges, overrides, direction = "TB", connectPoi
196121
196459
  assignEdgeSides(all, edges, false, connectPointSpacing);
196122
196460
  return { nodes: all, edges };
196123
196461
  }
196462
+ function layoutGeometryWithAnnotationEdges(nodes, edges, meta, connectPointSpacing = CONNECT_POINT_SPACING_DEFAULT) {
196463
+ const laid = layoutGeometry(nodes, edges, meta);
196464
+ assignEdgeSides(
196465
+ laid.nodes,
196466
+ laid.edges.filter((edge) => edge.data?.edge.kind === "annotation"),
196467
+ false,
196468
+ connectPointSpacing
196469
+ );
196470
+ return laid;
196471
+ }
196124
196472
  async function layoutDiagram(model, nodes, edges, args) {
196125
196473
  const { kind, direction, overrides, connectPointSpacing } = args;
196126
196474
  if (model.meta?.groupMode === "tiled-h" || model.meta?.groupMode === "tiled-v") {
196127
196475
  return layoutTiledOverview(model, nodes, edges, { direction, overrides, kind, connectPointSpacing });
196128
196476
  }
196129
196477
  const profile2 = profileFor(kind);
196130
- if (profile2.layout === "geometry") return layoutGeometry(nodes, edges, model.meta);
196478
+ if (profile2.layout === "geometry") {
196479
+ return layoutGeometryWithAnnotationEdges(nodes, edges, model.meta, connectPointSpacing);
196480
+ }
196131
196481
  if (profile2.layout === "sequence") return layoutSequence(nodes, edges, overrides);
196132
196482
  if (kind === "gv" && (args.gvMode ?? "group") === "group") {
196133
196483
  return layoutBands(nodes, edges, overrides, direction, connectPointSpacing);
@@ -197891,7 +198241,12 @@ svg.react-flow__connectionline { z-index: 1; }
197891
198241
  .rf-frame .dport-hit { pointer-events: auto; }
197892
198242
  .rf-frame .react-flow__handle { pointer-events: auto; }
197893
198243
  /* REQ-370: a frame is click-through, but its stacked handles still obey React
197894
- * Flow's active end choice. An inactive twin must not cover an eligible end. */
198244
+ * Flow's active end choice. An inactive twin must not cover an eligible end.
198245
+ * This is also what fixes issue 232: the inactive twin is painted on top, so
198246
+ * while it was reachable elementFromPoint returned it, React Flow read no
198247
+ * connectableend, and the drop was refused across the whole port glyph. The
198248
+ * only dock left was the outer dot, which is why a connector from a contained
198249
+ * part could not land on the inner face of the container's own port. */
197895
198250
  .react-flow.sysml-connecting .rf-frame .react-flow__handle:not(.connectionindicator) { pointer-events: none; }
197896
198251
  .rf-frame .react-flow__resize-control { pointer-events: auto; }
197897
198252
  .rf-node .react-flow__resize-control.fork-join-resize {
@@ -197930,8 +198285,8 @@ svg.react-flow__connectionline { z-index: 1; }
197930
198285
  /* Connection handles are invisible at rest and become visible when their node,
197931
198286
  * lifeline, or port is hovered/selected. They create the view's default
197932
198287
  * relationship and remain targets for endpoint reconnection.
197933
- * Each port exposes four handles (top/right/bottom/left) so it behaves as a box
197934
- * with real docking points. Port handles are the PRIMARY connection target in the
198288
+ * Each port exposes two handles, one on its inner face and one on its outer face.
198289
+ * Port handles are the PRIMARY connection target in the
197935
198290
  * Interconnection View and sit ABOVE node-level side handles (which only serve
197936
198291
  * part\u2194part links), so a drop near a port snaps to the port, not the part body. */
197937
198292
  .react-flow__handle.rf-handle { min-width: 0; min-height: 0; border-radius: 50%;
@@ -197943,12 +198298,9 @@ svg.react-flow__connectionline { z-index: 1; }
197943
198298
  background: var(--cyan); border: 1px solid var(--bg-1);
197944
198299
  box-sizing: border-box;
197945
198300
  }
197946
- /* diagram-rules \u2014 Port: the visible connect DOT sits on the port glyph's outer edge
197947
- * (5px dot in an 18px invisible hit box) \u2014 the connect-START affordance and the outer
197948
- * dock point. A SEPARATE glyph-sized portdrop target (below) makes the whole port a
197949
- * reliable DROP landing zone during a drag without covering the dot or the select/move
197950
- * strip. All connection dots share ONE visible 5px size so the affordance reads the same
197951
- * everywhere. */
198301
+ /* diagram-rules - two visible port dots sit on the glyph's outer and inner faces.
198302
+ * Each 5px dot has a compact hit box and can start or end a connector. The chosen
198303
+ * handle is the chosen dock point, while the central body stays selectable. */
197952
198304
  .react-flow__handle.rf-handle-port { width: ${PORT_CONNECT_DOT_HIT}px; height: ${PORT_CONNECT_DOT_HIT}px; --rf-dot-size: 5px; z-index: 6; }
197953
198305
  .react-flow__handle.rf-handle-side { width: ${BODY_SNAP_POINT_HIT_SIZE}px; height: ${BODY_SNAP_POINT_HIT_SIZE}px; --rf-dot-size: 5px; z-index: 2; }
197954
198306
  /* REQ-370: body snap points are hollow circles. Port and pin endpoints are
@@ -197965,28 +198317,13 @@ svg.react-flow__connectionline { z-index: 1; }
197965
198317
  }
197966
198318
  .rf-circular-control-hit:active { cursor: grabbing; }
197967
198319
  .react-flow__handle.rf-handle-port.active { opacity: 0.95; }
197968
- /* The whole-glyph DROP target on a port's inner dock side. It exists ALWAYS (so a
197969
- * delegation connector docking on the inner side resolves and isn't dropped by React
197970
- * Flow), but is click-through AT REST so it never steals the port's click-to-select or
197971
- * the drag-to-move strip; while a connector is being dragged, React Flow's eligible
197972
- * ".connectionindicator" twins become pointer-active, so elementFromPoint returns a
197973
- * connectable end handle for a drop ANYWHERE on the glyph, inside or on an edge. It
197974
- * draws nothing (the drop cue is the port SQUARE highlight); its box is sized inline
197975
- * to PORT_GLYPH_SIZE by the renderer. */
197976
- .react-flow__handle.rf-handle-portdrop { --rf-dot-size: 0px; z-index: 5; border-radius: 3px; }
197977
- .react-flow__handle.rf-handle-portdrop::after { display: none; }
197978
- /* Both rules are (0,4,0), higher than React Flow's own .react-flow__handle.connectionindicator
197979
- * (0,2,0) which otherwise forces pointer-events on every connectable handle. During a drag
197980
- * we still restrict pointer events to ".connectionindicator". Each DualHandle renders a stacked
197981
- * target and source. Loose-mode reconnect can end on either twin, while semantic validation
197982
- * still rejects an incompatible model endpoint. */
197983
- .react-flow:not(.sysml-connecting) .react-flow__handle.rf-handle-portdrop { pointer-events: none; }
197984
- .react-flow.sysml-connecting .react-flow__handle.rf-handle-portdrop.connectionindicator { pointer-events: auto; }
197985
198320
  /* REQ-006 \u2014 graph nodes advertise their default drag relationship with modest
197986
198321
  * cyan side dots; all valid targets light up while a drag is in progress. */
197987
198322
  .rf-node-graph:hover .react-flow__handle.rf-handle-side { opacity: 0.85; }
197988
198323
  .react-flow.sysml-connecting .react-flow__handle.rf-handle-side.connectionindicator,
197989
198324
  .react-flow.sysml-connecting .react-flow__handle.rf-handle-lifeline.connectionindicator { opacity: 0.42; }
198325
+ .react-flow.sysml-connecting .react-flow__handle.rf-handle-port.connectionindicator { opacity: 0.42; }
198326
+ .react-flow.sysml-connecting .react-flow__handle.rf-handle-port.active { opacity: 1; }
197990
198327
  /* The nearest semantically valid body target is stronger than the candidate
197991
198328
  * points. Port and pin targets use the highlighted endpoint glyph instead. */
197992
198329
  .react-flow.sysml-connecting .react-flow__handle.rf-handle-side.connectingto.valid { opacity: 1; }
@@ -198017,13 +198354,14 @@ svg.react-flow__connectionline { z-index: 1; }
198017
198354
  * (diagram-rules \u2014 Nested port expansion control). Sized by the rule, so only its
198018
198355
  * colours and type live here; it reads as part of the port, not as chrome, until
198019
198356
  * hovered. Kept interactive inside a click-through container frame, like the port
198020
- * strip above, and click-through during a connection drag so it can never steal a
198021
- * drop aimed at the port it sits under. */
198357
+ * strip above, and raised above both the strip and face handles so expanding never
198358
+ * makes the collapse control unreachable. It is click-through during a connection
198359
+ * drag so it can never steal a drop aimed at the port it sits under. */
198022
198360
  .dport-toggle {
198023
198361
  display: flex; align-items: center; justify-content: center;
198024
198362
  padding: 0; border: 1px solid var(--fg-3); border-radius: 3px;
198025
198363
  background: var(--bg-1); color: var(--fg-2);
198026
- cursor: pointer; pointer-events: auto;
198364
+ cursor: pointer; pointer-events: auto; z-index: 7;
198027
198365
  }
198028
198366
  .dport-toggle svg { display: block; }
198029
198367
  .dport-toggle:hover { color: var(--accent); border-color: var(--accent); }
@@ -198033,9 +198371,9 @@ svg.react-flow__connectionline { z-index: 1; }
198033
198371
  /* While a connector is being dragged, the port-move strip must NOT intercept the
198034
198372
  * drop: it sits above the port's connect handle, so elementFromPoint would return
198035
198373
  * the strip (not a handle) and the drop would fall back to the nearest handle,
198036
- * which lets a competing part-side handle steal a drop aimed at the port centre.
198037
- * Making it click-through during a connection lets the full-glyph port handle take
198038
- * every drop \u2014 the port stays fully opaque to connectors. */
198374
+ * which lets a competing part-side handle steal a drop aimed at a face snap point.
198375
+ * Making it click-through during a connection leaves both exact port handles
198376
+ * reachable and lets the nearest one win. */
198039
198377
  .react-flow.sysml-connecting .port-drag-hit { pointer-events: none; }
198040
198378
  /* issue 189 \u2014 the in-place rename editor over a container-part FRAME header must
198041
198379
  be typeable even though the frame body is click-through (pointer-events:none). */
@@ -198639,7 +198977,7 @@ function getViewState(sc, anchor, kind) {
198639
198977
  }
198640
198978
 
198641
198979
  // src/node-markup.tsx
198642
- var import_react10 = __toESM(require_react(), 1);
198980
+ var import_react11 = __toESM(require_react(), 1);
198643
198981
  var import_server = __toESM(require_server_node(), 1);
198644
198982
 
198645
198983
  // ../extension/src/webview/diagram/flow/context.ts
@@ -198690,7 +199028,7 @@ function useDiagram() {
198690
199028
  }
198691
199029
 
198692
199030
  // ../extension/src/webview/diagram/flow/nodes.tsx
198693
- var import_react8 = __toESM(require_react());
199031
+ var import_react9 = __toESM(require_react());
198694
199032
 
198695
199033
  // ../extension/src/webview/diagram/derive.ts
198696
199034
  function edgeKindLabel(kind) {
@@ -199043,6 +199381,7 @@ var DIRECT_NAME_RE = new RegExp(`^${DIRECT_NAME_SEGMENT}$`);
199043
199381
  var DIRECT_PATH_RE = new RegExp(`^${DIRECT_NAME_SEGMENT}(?:\\.${DIRECT_NAME_SEGMENT})*$`);
199044
199382
  function isWritableRelationEndpoint(endpoint) {
199045
199383
  if (!endpoint) return false;
199384
+ if (endpoint.shape === "annotation") return false;
199046
199385
  if (pseudostateEndpointPath(endpoint.keyword)) return true;
199047
199386
  if (!endpoint.source || /^__flow_\d+__(?:\.|$)/u.test(endpoint.id)) return false;
199048
199387
  if (!endpoint.name.trim() && ["if", "while", "loop", "for"].includes(endpoint.keyword.trim().toLowerCase())) return true;
@@ -199063,6 +199402,7 @@ var TERMINAL_RELATION = "finish";
199063
199402
  function relationToolsForNode(keyword, viewKind, shape) {
199064
199403
  const { base, isDefinition, isDecision } = normalizedElementKeyword(keyword);
199065
199404
  const tools = TOOLBOX[viewKind]?.relations ?? [];
199405
+ if (shape === "annotation") return [];
199066
199406
  if (viewKind === "afv" && ["if", "while", "loop", "for"].includes(base)) {
199067
199407
  return tools.filter((tool) => tool.kind === "terminate");
199068
199408
  }
@@ -199139,6 +199479,12 @@ function relationToolsForNode(keyword, viewKind, shape) {
199139
199479
  var import_react7 = __toESM(require_react());
199140
199480
  var import_jsx_runtime3 = __toESM(require_jsx_runtime());
199141
199481
  var ptStr = (pts) => pts.map((p) => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ");
199482
+ var GEO_HANDLE_POSITION = {
199483
+ top: Position3.Top,
199484
+ right: Position3.Right,
199485
+ bottom: Position3.Bottom,
199486
+ left: Position3.Left
199487
+ };
199142
199488
  function niceStep(range, target = 5) {
199143
199489
  if (!(range > 0)) return 1;
199144
199490
  const raw = range / target;
@@ -199353,7 +199699,7 @@ function GeoShapeNode({ id: id2, data, selected: selected2 }) {
199353
199699
  const origin = data.origin;
199354
199700
  const iso = data.geo3d && node.geo && origin;
199355
199701
  const approx = node.geo?.approx;
199356
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
199702
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
199357
199703
  "div",
199358
199704
  {
199359
199705
  className: `rf-node rf-geo${selected2 ? " selected" : ""}${approx ? " dgeo-approx" : ""}`,
@@ -199363,19 +199709,38 @@ function GeoShapeNode({ id: id2, data, selected: selected2 }) {
199363
199709
  e.stopPropagation();
199364
199710
  ctx.onContextNode(id2, e.clientX, e.clientY);
199365
199711
  },
199366
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { className: "rf-node-svg", width: w, height: h, style: { overflow: "visible" }, children: [
199367
- iso ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
199368
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("g", { className: `dgeo3d-node${selected2 ? " selected" : ""}`, transform: `translate(${-origin.x},${-origin.y})`, children: geoShapeBody(node, data.geo3d) }),
199369
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("text", { className: "dgeo3d-name", x: w / 2, y: -3, textAnchor: "middle", children: node.name })
199370
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
199371
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { className: `dnode-rect${selected2 ? " selected" : ""}`, x: 0, y: 0, width: w, height: h, rx: node.isDef ? 0 : 12 }),
199372
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("text", { className: "dnode-label", x: w / 2, y: h / 2 + 3, textAnchor: "middle", children: node.name })
199712
+ children: [
199713
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { className: "rf-node-svg", width: w, height: h, style: { overflow: "visible" }, children: [
199714
+ iso ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
199715
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("g", { className: `dgeo3d-node${selected2 ? " selected" : ""}`, transform: `translate(${-origin.x},${-origin.y})`, children: geoShapeBody(node, data.geo3d) }),
199716
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("text", { className: "dgeo3d-name", x: w / 2, y: -3, textAnchor: "middle", children: node.name })
199717
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
199718
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { className: `dnode-rect${selected2 ? " selected" : ""}`, x: 0, y: 0, width: w, height: h, rx: node.isDef ? 0 : 12 }),
199719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("text", { className: "dnode-label", x: w / 2, y: h / 2 + 3, textAnchor: "middle", children: node.name })
199720
+ ] }),
199721
+ approx ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("text", { className: "dgeo-approx-mark", x: w - 2, y: -3, textAnchor: "end", children: [
199722
+ "\u26A0",
199723
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("title", { children: `${node.name}: ${approx}` })
199724
+ ] }) : null
199373
199725
  ] }),
199374
- approx ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("text", { className: "dgeo-approx-mark", x: w - 2, y: -3, textAnchor: "end", children: [
199375
- "\u26A0",
199376
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("title", { children: `${node.name}: ${approx}` })
199377
- ] }) : null
199378
- ] })
199726
+ PORT_DOCK_SIDES.flatMap((side) => (data.edgeSideAnchors?.[side] ?? []).map((offset2) => {
199727
+ const point = sidePoint(side, offset2, w, h);
199728
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
199729
+ Handle,
199730
+ {
199731
+ type: "target",
199732
+ id: sideAnchorHandleId(side, offset2),
199733
+ position: GEO_HANDLE_POSITION[side],
199734
+ style: { left: point.x, top: point.y, transform: "translate(-50%,-50%)" },
199735
+ className: "rf-handle rf-handle-side",
199736
+ isConnectable: true,
199737
+ isConnectableStart: false,
199738
+ isConnectableEnd: true
199739
+ },
199740
+ `${side}-${offset2}`
199741
+ );
199742
+ }))
199743
+ ]
199379
199744
  }
199380
199745
  );
199381
199746
  }
@@ -199504,8 +199869,8 @@ function mountedNodeSidePointOffsets(data, side, connectable, exposeAll) {
199504
199869
  function useMountedHandleRegistration(id2, sideOffsets) {
199505
199870
  const mountedKey = mountedHandleKey(sideOffsets);
199506
199871
  const updateNodeInternals2 = useUpdateNodeInternals();
199507
- const lastKeyRef = (0, import_react8.useRef)(mountedKey);
199508
- (0, import_react8.useEffect)(() => {
199872
+ const lastKeyRef = (0, import_react9.useRef)(mountedKey);
199873
+ (0, import_react9.useEffect)(() => {
199509
199874
  if (lastKeyRef.current === mountedKey) return;
199510
199875
  lastKeyRef.current = mountedKey;
199511
199876
  updateNodeInternals2(id2);
@@ -199522,9 +199887,9 @@ var SIDE_NORMAL = {
199522
199887
  };
199523
199888
  var clampOffset = (v) => Math.max(0.06, Math.min(0.94, v));
199524
199889
  function usePortDrag(containerRef, w, h, topReserve, onPortMove, onPortPreview, onSelect) {
199525
- const [dragId, setDragId] = (0, import_react8.useState)(void 0);
199526
- const gestureRef = (0, import_react8.useRef)(void 0);
199527
- (0, import_react8.useEffect)(() => () => gestureRef.current?.abort(), []);
199890
+ const [dragId, setDragId] = (0, import_react9.useState)(void 0);
199891
+ const gestureRef = (0, import_react9.useRef)(void 0);
199892
+ (0, import_react9.useEffect)(() => () => gestureRef.current?.abort(), []);
199528
199893
  const onPortPointerDown = (portId, e, origin, draggable = true) => {
199529
199894
  if (e.button !== 0) return;
199530
199895
  e.stopPropagation();
@@ -199966,7 +200331,7 @@ function LifelineBody({ node, w, lineBottom, activation, eventMarks }) {
199966
200331
  ] })
199967
200332
  ] });
199968
200333
  }
199969
- function PortGlyph({ ph, w, h, topReserve = 0, showLabels, selected: selected2, hovered, moving, connectTarget, connectStart, onSelect, onContext }) {
200334
+ function PortGlyph({ ph, w, h, topReserve = 0, showLabels, selected: selected2, hovered, moving, connectTargetSide, connectStart, onSelect, onContext }) {
199970
200335
  const rect = portGlyphRect(ph, w, h, topReserve);
199971
200336
  const x = rect.cx;
199972
200337
  const y = rect.cy;
@@ -200040,7 +200405,7 @@ function PortGlyph({ ph, w, h, topReserve = 0, showLabels, selected: selected2,
200040
200405
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200041
200406
  "rect",
200042
200407
  {
200043
- className: `dnode-port${ph.port.pin ? " pin" : ""}${ph.port.ref ? " ref" : ""}${rect.elongated ? " host" : ""}${selected2 ? " selected" : ""}${hovered ? " hovered" : ""}${moving ? " moving" : ""}${connectTarget ? " connect-target" : ""}`,
200408
+ className: `dnode-port${ph.port.pin ? " pin" : ""}${ph.port.ref ? " ref" : ""}${rect.elongated ? " host" : ""}${selected2 ? " selected" : ""}${hovered ? " hovered" : ""}${moving ? " moving" : ""}${connectTargetSide ? " connect-target" : ""}`,
200044
200409
  x: rect.x,
200045
200410
  y: rect.y,
200046
200411
  width: rect.width,
@@ -200053,7 +200418,7 @@ function PortGlyph({ ph, w, h, topReserve = 0, showLabels, selected: selected2,
200053
200418
  d === "out" || d === "inout" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { className: "dport-dir head", d: arrowHead(1) }) : "",
200054
200419
  d === "in" || d === "inout" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { className: "dport-dir head", d: arrowHead(-1) }) : ""
200055
200420
  ] }) : "",
200056
- selected2 && connectStart && !connectTarget ? (() => {
200421
+ selected2 && connectStart && !connectTargetSide ? (() => {
200057
200422
  const px = x + v.x * (half + PORT_PLUS_GAP);
200058
200423
  const py = y + v.y * (half + PORT_PLUS_GAP);
200059
200424
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: "dport-plus", children: [
@@ -200061,8 +200426,8 @@ function PortGlyph({ ph, w, h, topReserve = 0, showLabels, selected: selected2,
200061
200426
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: px, y1: py - PORT_PLUS_HALF, x2: px, y2: py + PORT_PLUS_HALF })
200062
200427
  ] });
200063
200428
  })() : "",
200064
- connectTarget ? (() => {
200065
- const at2 = portDockAt(rect, ph.side, ph.side);
200429
+ connectTargetSide ? (() => {
200430
+ const at2 = portDockAt(rect, ph.side, connectTargetSide);
200066
200431
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { className: "dport-anchor", cx: at2.x, cy: at2.y, r: 1.7 });
200067
200432
  })() : "",
200068
200433
  showLabels && hostLabel ? (
@@ -200199,8 +200564,8 @@ function ContainerControls({
200199
200564
  ] });
200200
200565
  }
200201
200566
  function InlineEditor({ initial, mode, elementKind, onCommit, onCancel, w }) {
200202
- const ref = (0, import_react8.useRef)(null);
200203
- (0, import_react8.useEffect)(() => {
200567
+ const ref = (0, import_react9.useRef)(null);
200568
+ (0, import_react9.useEffect)(() => {
200204
200569
  const el2 = ref.current;
200205
200570
  if (el2) {
200206
200571
  el2.focus();
@@ -200296,9 +200661,9 @@ function shapeBody(node, data) {
200296
200661
  }
200297
200662
  }
200298
200663
  var CLIP_EXEMPT = /* @__PURE__ */ new Set(["fork", "join", "dot", "initial", "final", "terminate", "lifeline", "actor"]);
200299
- function NodeBody({ node, data, selectedPortId, hoveredPortId, movingPortId, connectTargetPortId, showPortConnectStart, onPortSelect, onPortContext }) {
200664
+ function NodeBody({ node, data, selectedPortId, hoveredPortId, movingPortId, connectTargetPortId, connectTargetPortSide, showPortConnectStart, onPortSelect, onPortContext }) {
200300
200665
  const { w, h } = data;
200301
- const rawId = (0, import_react8.useId)();
200666
+ const rawId = (0, import_react9.useId)();
200302
200667
  const clipId = `dnode-clip-${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
200303
200668
  const clip = !CLIP_EXEMPT.has(node.shape) || data.kind === "gv" && node.shape === "actor";
200304
200669
  const elementColorClass = diagramElementColorClass(diagramElementColorTypeForNode(node.shape, node.keyword));
@@ -200316,7 +200681,7 @@ function NodeBody({ node, data, selectedPortId, hoveredPortId, movingPortId, con
200316
200681
  selected: selectedPortId === ph.port.id,
200317
200682
  hovered: hoveredPortId === ph.port.id,
200318
200683
  moving: movingPortId === ph.port.id,
200319
- connectTarget: connectTargetPortId === ph.port.id,
200684
+ connectTargetSide: connectTargetPortId === ph.port.id ? connectTargetPortSide : void 0,
200320
200685
  connectStart: showPortConnectStart && (data.kind === "iv" ? ph.port.pin !== true : data.kind === "afv"),
200321
200686
  onSelect: onPortSelect,
200322
200687
  onContext: onPortContext
@@ -200329,9 +200694,10 @@ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, heigh
200329
200694
  const ctx = useDiagram();
200330
200695
  const node = data.node;
200331
200696
  const myPortIds = node.ports?.map((p) => p.id) ?? [];
200332
- const connectTargetPortId = useConnection((c) => {
200333
- return validPortDropTarget(c, (portId) => myPortIds.includes(portId))?.portId;
200334
- });
200697
+ const connectTargetHandleId = useConnection((c) => validPortDropTarget(c, (portId) => myPortIds.includes(portId))?.handleId);
200698
+ const connectTargetPort = parsePortAnchorHandleId(connectTargetHandleId);
200699
+ const connectTargetPortId = connectTargetPort?.portId;
200700
+ const connectTargetPortSide = connectTargetPort?.side;
200335
200701
  const w = width ?? data.w;
200336
200702
  const h = height ?? data.h;
200337
200703
  const isLifeline = node.shape === "lifeline";
@@ -200344,7 +200710,7 @@ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, heigh
200344
200710
  const internalNoun = INTERNAL_NOUN[data.kind] ?? "internal parts";
200345
200711
  const featureCompartmentsVisible = node.meta?.featureCompartmentsVisible === true;
200346
200712
  const featureCompartmentHeight = featureBandTopReserve(data);
200347
- const containerRef = (0, import_react8.useRef)(null);
200713
+ const containerRef = (0, import_react9.useRef)(null);
200348
200714
  const portSource = (pid) => {
200349
200715
  const p = node.ports?.find((pp) => pp.id === pid);
200350
200716
  ctx.onPortSelected?.();
@@ -200371,8 +200737,8 @@ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, heigh
200371
200737
  const nodeSideStartable = directRelation !== void 0 && data.kind !== "sv" && node.shape !== "lifeline" && !isPackageEndpoint(node.shape, node.keyword);
200372
200738
  const selectedPortId = useInteraction(ctx.store, (s) => typeof s.selectedId === "string" && myPortIds.includes(s.selectedId) ? s.selectedId : void 0);
200373
200739
  const dropClass = useDropState(ctx.store, id2);
200374
- const [hoveredPortId, setHoveredPortId] = (0, import_react8.useState)(void 0);
200375
- const [sidePointsHovered, setSidePointsHovered] = (0, import_react8.useState)(false);
200740
+ const [hoveredPortId, setHoveredPortId] = (0, import_react9.useState)(void 0);
200741
+ const [sidePointsHovered, setSidePointsHovered] = (0, import_react9.useState)(false);
200376
200742
  const isPackageNode = isPackageEndpoint(node.shape, node.keyword);
200377
200743
  const sideConnectable = data.kind !== "sv" && !isPackageNode;
200378
200744
  const perimeterControl = keepsPerimeterSidePointsMounted(node.shape);
@@ -200455,6 +200821,7 @@ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, heigh
200455
200821
  hoveredPortId,
200456
200822
  movingPortId: dragId,
200457
200823
  connectTargetPortId,
200824
+ connectTargetPortSide,
200458
200825
  showPortConnectStart: data.kind === "iv" || data.kind === "afv",
200459
200826
  onPortSelect: !ctx.onPortMove && data.ports.length ? portSource : void 0,
200460
200827
  onPortContext: ctx.onContextNode
@@ -200501,7 +200868,6 @@ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, heigh
200501
200868
  node.shape === "initial" || node.shape === "final" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "rf-circular-control-hit", "aria-hidden": "true" }) : null,
200502
200869
  ports.flatMap((ph) => {
200503
200870
  const rect = portGlyphRect(ph, w, h, featureCompartmentHeight);
200504
- const c = { x: rect.cx, y: rect.cy };
200505
200871
  const startable = directRelationForHandle(data.kind, {
200506
200872
  id: ph.port.id,
200507
200873
  name: ph.port.name,
@@ -200509,18 +200875,38 @@ function SysmlNode({ id: id2, data, draggable, selected: selected2, width, heigh
200509
200875
  shape: ph.port.pin ? "pin" : "port",
200510
200876
  source: ph.port.source
200511
200877
  }) !== void 0;
200512
- const active = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId) || ph.port.id === connectTargetPortId;
200513
200878
  const [outerId, innerId] = portConnectHandleIds(ph.port.id, ph.side);
200514
- const dropSize = {
200515
- width: Math.max(rect.width, 16),
200516
- height: Math.max(rect.height, 16)
200517
- };
200518
200879
  const outer = portDockAt(rect, ph.side, ph.side);
200880
+ const innerSide = OPPOSITE_DOCK_SIDE[ph.side];
200881
+ const inner = portDockAt(rect, ph.side, innerSide);
200882
+ const revealBoth = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId);
200519
200883
  return [
200520
- // the DROP target stays centred over the glyph — it is a cover,
200521
- // not a dock point (the edge geometry is `dockPoint`'s business)
200522
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: innerId, side: OPPOSITE_DOCK_SIDE[ph.side], x: c.x, y: c.y, kind: "portdrop", size: dropSize, startable: false }, `${ph.port.id}-drop`),
200523
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: outerId, side: ph.side, x: outer.x, y: outer.y, kind: "port", active, startable }, `${ph.port.id}-out`)
200884
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200885
+ DualHandle,
200886
+ {
200887
+ id: outerId,
200888
+ side: ph.side,
200889
+ x: outer.x,
200890
+ y: outer.y,
200891
+ kind: "port",
200892
+ active: revealBoth || ph.port.id === connectTargetPortId && connectTargetPortSide === ph.side,
200893
+ startable
200894
+ },
200895
+ `${ph.port.id}-out`
200896
+ ),
200897
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
200898
+ DualHandle,
200899
+ {
200900
+ id: innerId,
200901
+ side: innerSide,
200902
+ x: inner.x,
200903
+ y: inner.y,
200904
+ kind: "port",
200905
+ active: revealBoth || ph.port.id === connectTargetPortId && connectTargetPortSide === innerSide,
200906
+ startable
200907
+ },
200908
+ `${ph.port.id}-in`
200909
+ )
200524
200910
  ];
200525
200911
  }),
200526
200912
  ctx.onPortMove ? ports.map((ph) => {
@@ -200655,14 +201041,15 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200655
201041
  const frameSemanticClass = frame2.keyword.startsWith("part") ? "part" : isStructuredControl || isActionElementKeyword(frame2.keyword) ? "action" : frame2.keyword.startsWith("state") ? "state" : void 0;
200656
201042
  const frameBottomHeight = loopUntil ? data.frameBottomHeight ?? LOOP_UNTIL_COMPARTMENT_HEIGHT : 0;
200657
201043
  const myPortIds = isPartitionLane ? [] : frame2.ports?.map((p) => p.id) ?? [];
200658
- const connectTargetPortId = useConnection((c) => {
200659
- return validPortDropTarget(c, (portId) => myPortIds.includes(portId))?.portId;
200660
- });
201044
+ const connectTargetHandleId = useConnection((c) => validPortDropTarget(c, (portId) => myPortIds.includes(portId))?.handleId);
201045
+ const connectTargetPort = parsePortAnchorHandleId(connectTargetHandleId);
201046
+ const connectTargetPortId = connectTargetPort?.portId;
201047
+ const connectTargetPortSide = connectTargetPort?.side;
200661
201048
  const w = width ?? data.w;
200662
201049
  const h = height ?? data.h;
200663
201050
  const partitionKindFull = isExhibitLane ? "\xABexhibited by\xBB" : "\xABperformer\xBB";
200664
201051
  const partitionKindShown = fitKind(partitionKindFull, Math.max(1, w - 20));
200665
- const rawClipId = (0, import_react8.useId)();
201052
+ const rawClipId = (0, import_react9.useId)();
200666
201053
  const clipId = `dframe-clip-${rawClipId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
200667
201054
  const structuredPrefix = `\xAB${structuredKeyword}\xBB`;
200668
201055
  const structuredConditionShown = structuredCondition ? fitText(structuredCondition, Math.max(1, w - 24 - structuredPrefix.length * 9 * 0.78), 11) : "";
@@ -200690,7 +201077,7 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200690
201077
  performer: frame2.meta?.performerLane === true,
200691
201078
  structured: frame2.meta?.structuredControl === true
200692
201079
  }));
200693
- const containerRef = (0, import_react8.useRef)(null);
201080
+ const containerRef = (0, import_react9.useRef)(null);
200694
201081
  const portSource = (pid) => {
200695
201082
  ctx.onPortSelected?.();
200696
201083
  ctx.onReveal(frame2.ports?.find((p) => p.id === pid)?.source, pid);
@@ -200718,8 +201105,8 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200718
201105
  const selectedPortId = useInteraction(ctx.store, (s) => typeof s.selectedId === "string" && myPortIds.includes(s.selectedId) ? s.selectedId : void 0);
200719
201106
  const dropClass = useDropState(ctx.store, id2);
200720
201107
  const editing = useInteraction(ctx.store, (s) => s.editing?.nodeId === id2 && !s.editing?.at && !s.editing?.edgeId ? s.editing : void 0);
200721
- const [hoveredPortId, setHoveredPortId] = (0, import_react8.useState)(void 0);
200722
- const [sidePointsHovered, setSidePointsHovered] = (0, import_react8.useState)(false);
201108
+ const [hoveredPortId, setHoveredPortId] = (0, import_react9.useState)(void 0);
201109
+ const [sidePointsHovered, setSidePointsHovered] = (0, import_react9.useState)(false);
200723
201110
  const sideConnectable = data.kind !== "sv" && !isPackageFrame && !isPartitionLane && !isOuterViewFrame;
200724
201111
  const sideOffsets = isPartitionLane ? SIDES.map(() => []) : SIDES.map((side) => mountedNodeSidePointOffsets(
200725
201112
  data,
@@ -200892,7 +201279,7 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200892
201279
  selected: selectedPortId === ph.port.id,
200893
201280
  hovered: hoveredPortId === ph.port.id,
200894
201281
  moving: dragId === ph.port.id,
200895
- connectTarget: connectTargetPortId === ph.port.id,
201282
+ connectTargetSide: connectTargetPortId === ph.port.id ? connectTargetPortSide : void 0,
200896
201283
  connectStart: data.kind === "iv" ? ph.port.pin !== true : data.kind === "afv",
200897
201284
  onSelect: ctx.onPortMove ? void 0 : portSource,
200898
201285
  onContext: ctx.onContextNode
@@ -200962,7 +201349,6 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200962
201349
  }),
200963
201350
  showPorts ? ports.flatMap((ph) => {
200964
201351
  const rect = portGlyphRect(ph, w, h, featureCompartmentHeight);
200965
- const c = { x: rect.cx, y: rect.cy };
200966
201352
  const startable = directRelationForHandle(data.kind, {
200967
201353
  id: ph.port.id,
200968
201354
  name: ph.port.name,
@@ -200970,18 +201356,38 @@ function FrameNode({ id: id2, data, selected: selected2, width, height }) {
200970
201356
  shape: ph.port.pin ? "pin" : "port",
200971
201357
  source: ph.port.source
200972
201358
  }) !== void 0;
200973
- const active = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId) || ph.port.id === connectTargetPortId;
200974
201359
  const [outerId, innerId] = portConnectHandleIds(ph.port.id, ph.side);
200975
- const dropSize = {
200976
- width: Math.max(rect.width, 16),
200977
- height: Math.max(rect.height, 16)
200978
- };
200979
201360
  const outer = portDockAt(rect, ph.side, ph.side);
201361
+ const innerSide = OPPOSITE_DOCK_SIDE[ph.side];
201362
+ const inner = portDockAt(rect, ph.side, innerSide);
201363
+ const revealBoth = startable && (ph.port.id === hoveredPortId || ph.port.id === selectedPortId);
200980
201364
  return [
200981
- // the DROP target stays centred over the glyph — it is a cover,
200982
- // not a dock point (the edge geometry is `dockPoint`'s business)
200983
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: innerId, side: OPPOSITE_DOCK_SIDE[ph.side], x: c.x, y: c.y, kind: "portdrop", size: dropSize, startable: false }, `${ph.port.id}-drop`),
200984
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DualHandle, { id: outerId, side: ph.side, x: outer.x, y: outer.y, kind: "port", active, startable }, `${ph.port.id}-out`)
201365
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
201366
+ DualHandle,
201367
+ {
201368
+ id: outerId,
201369
+ side: ph.side,
201370
+ x: outer.x,
201371
+ y: outer.y,
201372
+ kind: "port",
201373
+ active: revealBoth || ph.port.id === connectTargetPortId && connectTargetPortSide === ph.side,
201374
+ startable
201375
+ },
201376
+ `${ph.port.id}-out`
201377
+ ),
201378
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
201379
+ DualHandle,
201380
+ {
201381
+ id: innerId,
201382
+ side: innerSide,
201383
+ x: inner.x,
201384
+ y: inner.y,
201385
+ kind: "port",
201386
+ active: revealBoth || ph.port.id === connectTargetPortId && connectTargetPortSide === innerSide,
201387
+ startable
201388
+ },
201389
+ `${ph.port.id}-in`
201390
+ )
200985
201391
  ];
200986
201392
  }) : null,
200987
201393
  ctx.onPortMove ? ports.map((ph) => {
@@ -201088,12 +201494,12 @@ function SeqFragmentNode({ data, width, height }) {
201088
201494
  ] }) });
201089
201495
  }
201090
201496
  var nodeTypes = {
201091
- sysml: (0, import_react8.memo)(SysmlNode),
201092
- frame: (0, import_react8.memo)(FrameNode),
201093
- lane: (0, import_react8.memo)(LaneNode),
201094
- seqfragment: (0, import_react8.memo)(SeqFragmentNode),
201095
- geo: (0, import_react8.memo)(GeoShapeNode),
201096
- geoframe: (0, import_react8.memo)(GeoFrameNode)
201497
+ sysml: (0, import_react9.memo)(SysmlNode),
201498
+ frame: (0, import_react9.memo)(FrameNode),
201499
+ lane: (0, import_react9.memo)(LaneNode),
201500
+ seqfragment: (0, import_react9.memo)(SeqFragmentNode),
201501
+ geo: (0, import_react9.memo)(GeoShapeNode),
201502
+ geoframe: (0, import_react9.memo)(GeoFrameNode)
201097
201503
  };
201098
201504
 
201099
201505
  // src/node-markup.tsx
@@ -201157,13 +201563,13 @@ function renderNodeBody(node, lineStyle, index2) {
201157
201563
  width: node.width ?? node.measured?.width ?? node.data.w,
201158
201564
  height: node.height ?? node.measured?.height ?? node.data.h
201159
201565
  };
201160
- const markup = (0, import_server.renderToStaticMarkup)((0, import_react10.createElement)(
201566
+ const markup = (0, import_server.renderToStaticMarkup)((0, import_react11.createElement)(
201161
201567
  ReactFlowProvider,
201162
201568
  null,
201163
- (0, import_react10.createElement)(
201569
+ (0, import_react11.createElement)(
201164
201570
  DiagramContext.Provider,
201165
201571
  { value: exportContext(lineStyle) },
201166
- (0, import_react10.createElement)(Component, props)
201572
+ (0, import_react11.createElement)(Component, props)
201167
201573
  )
201168
201574
  ));
201169
201575
  const inner = extractNodeSvgInner(markup);
@@ -201343,7 +201749,7 @@ async function runExport(command) {
201343
201749
  const root4 = command.workspace ? path8.resolve(command.workspace) : findWorkspaceRoot(file);
201344
201750
  const settings = diagramSettingsFor(readProjectConfig(root4));
201345
201751
  const sideCar = command.autoLayout ? emptySideCar() : readSideCar(root4, file);
201346
- const provider = new SysmlDiagramModelProvider();
201752
+ const provider = new SysmlDiagramModelProvider(services.shared);
201347
201753
  const kinds = command.all ? CANVAS_KINDS.filter((kind) => provider.buildAvailableDiagramKinds(document2, command.anchor).some((available) => available.kind === kind)) : [command.view ?? settings.defaultKind];
201348
201754
  const written = [];
201349
201755
  const empty2 = [];
@@ -201371,7 +201777,7 @@ async function runExport(command) {
201371
201777
  }
201372
201778
 
201373
201779
  // src/main.ts
201374
- var VERSION2 = true ? "0.20.2" : "dev";
201780
+ var VERSION2 = true ? "0.21.0" : "dev";
201375
201781
  function display(file) {
201376
201782
  const rel2 = path9.relative(process.cwd(), file);
201377
201783
  return rel2 && !rel2.startsWith("..") ? rel2.split(path9.sep).join("/") : file;