@foblex/flow 18.6.0 → 18.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, ElementRef, Injector, input, effect, untracked, Directive, signal, numberAttribute, model, booleanAttribute, ChangeDetectionStrategy, Component, viewChild, Input, computed, contentChildren, contentChild, NgZone, Renderer2, DestroyRef, output, ViewContainerRef, TemplateRef, runInInjectionContext, EventEmitter, Output, afterNextRender, NgModule } from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, ElementRef, DestroyRef, Injector, input, effect, untracked, Directive, signal, numberAttribute, model, booleanAttribute, ChangeDetectionStrategy, Component, viewChild, Input, computed, contentChildren, contentChild, NgZone, Renderer2, output, ViewContainerRef, TemplateRef, runInInjectionContext, EventEmitter, Output, afterNextRender, NgModule } from '@angular/core';
3
3
  import { TransformModelExtensions, PointExtensions, RectExtensions, GetIntersections, RoundedRect, Point, LineExtensions, SizeExtensions, setRectToElement, adjustRectToMinSize, setRectToViewBox } from '@foblex/2d';
4
4
  import { __decorate } from 'tslib';
5
5
  import { FExecutionRegister, FMediator } from '@foblex/mediator';
@@ -1320,6 +1320,40 @@ class FConnectionContentBase {
1320
1320
  * Used internally for positioning calculations.
1321
1321
  */
1322
1322
  hostElement = inject((ElementRef)).nativeElement;
1323
+ // ResizeObserver-backed size cache. Avoids `getBoundingClientRect`
1324
+ // inside the placement loop, where one DOM read per label per redraw
1325
+ // forced a synchronous layout flush and dominated drag-time at a few
1326
+ // hundred labelled connections (issue #304). The observer also keeps
1327
+ // the cache correct when the label content actually resizes.
1328
+ //
1329
+ // The initial size is populated by the observer's first asynchronous
1330
+ // delivery — calling `getBoundingClientRect()` from the constructor
1331
+ // re-introduces the thrashing during Angular CD when many labels
1332
+ // mount in the same tick. Until the first delivery, the size stays
1333
+ // at zero; the only visible effect is that the edge-guard does not
1334
+ // push labels away from path endpoints on the very first frame.
1335
+ // Labels at `position` ≠ 0 / 1 are unaffected.
1336
+ _cachedSize = { width: 0, height: 0 };
1337
+ _observer = null;
1338
+ constructor() {
1339
+ if (typeof ResizeObserver === 'undefined') {
1340
+ return;
1341
+ }
1342
+ this._observer = new ResizeObserver((entries) => {
1343
+ for (const entry of entries) {
1344
+ const rect = entry.contentRect;
1345
+ this._cachedSize = { width: rect.width, height: rect.height };
1346
+ }
1347
+ });
1348
+ this._observer.observe(this.hostElement);
1349
+ inject(DestroyRef).onDestroy(() => {
1350
+ this._observer?.disconnect();
1351
+ this._observer = null;
1352
+ });
1353
+ }
1354
+ measureSize() {
1355
+ return this._cachedSize;
1356
+ }
1323
1357
  }
1324
1358
 
1325
1359
  /**
@@ -1555,7 +1589,7 @@ class PolylineContentPlace {
1555
1589
  const x = point.x + normal.x * lateral;
1556
1590
  const y = point.y + normal.y * lateral;
1557
1591
  // Edge guard
1558
- const projectedSize = this._sizeAlongDirection(content.hostElement, tangent);
1592
+ const projectedSize = this._sizeAlongDirection(content, tangent);
1559
1593
  const halfExtent = projectedSize * 0.5;
1560
1594
  const distanceFromStart = progress * sampler.totalLength;
1561
1595
  const distanceFromEnd = sampler.totalLength - distanceFromStart;
@@ -1577,9 +1611,14 @@ class PolylineContentPlace {
1577
1611
  }
1578
1612
  return { x, y };
1579
1613
  }
1580
- _sizeAlongDirection(element, dir) {
1581
- const rect = RectExtensions.fromElement(element);
1582
- return Math.abs(dir.x) * rect.width + Math.abs(dir.y) * rect.height;
1614
+ // Reads the size via `content.measureSize()` — a ResizeObserver-backed
1615
+ // cache. This loop runs interleaved with `style.transform` writes for
1616
+ // every label, so a `getBoundingClientRect()` here would force a
1617
+ // synchronous layout flush per label and stall drag at a few hundred
1618
+ // labelled connections (issue #304).
1619
+ _sizeAlongDirection(content, dir) {
1620
+ const size = content.measureSize();
1621
+ return Math.abs(dir.x) * size.width + Math.abs(dir.y) * size.height;
1583
1622
  }
1584
1623
  _normalize180(angleDeg) {
1585
1624
  let a = (angleDeg + 180) % 360;
@@ -13415,10 +13454,22 @@ class FReflowOrchestrator {
13415
13454
  }
13416
13455
  return result;
13417
13456
  }
13457
+ // We only emit a shift plan for nodes whose model signal matches
13458
+ // `_position`. A mismatch means the node is mid-flight — either its
13459
+ // `positionChanges()` effect has not yet mirrored the latest signal
13460
+ // into `_position`, or an internal write (fit-to-children) advanced
13461
+ // `_position` ahead of the model. Planning against a half-applied
13462
+ // state poisons every candidate read from DOM and propagates one
13463
+ // measured rect onto many siblings (issue #305). Skipping the rect
13464
+ // makes the orchestrator wait until the next coherent tick.
13418
13465
  _safeGetRect(node) {
13419
13466
  if (!node.hostElement) {
13420
13467
  return null;
13421
13468
  }
13469
+ const modelPos = node.position();
13470
+ if (modelPos.x !== node._position.x || modelPos.y !== node._position.y) {
13471
+ return null;
13472
+ }
13422
13473
  try {
13423
13474
  return this._mediator.execute(new GetNormalizedElementRectRequest(node.hostElement));
13424
13475
  }