YGauge.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.sequencediagram.teoz;
import net.sourceforge.plantuml.real.Real;
public final class YGauge {
private final Real min;
private final Real max;
// Contact line shared by the members of a parallel (&) group: the
// absolute Y of the arrow line. Null for tiles without a contact line.
private final Real contact;
// Chaining origin of the parallel group (the max of the gauge preceding
// the group head): no member of the group may start above it. It mirrors
// the legacy TileParallel behavior where the whole group is anchored at
// the group top and members are shifted DOWN to align contact points.
private final Real origin;
public YGauge(Real min, Real max) {
this(min, max, null, null);
}
public YGauge(Real min, Real max, Real contact, Real origin) {
this.min = min;
this.max = max;
this.contact = contact;
this.origin = origin;
}
@Override
public String toString() {
return min.getCurrentValue() + " " + max.getCurrentValue();
}
public static YGauge create(Real min, double height) {
if (height < 0)
throw new IllegalArgumentException();
// addAtLeast (not addFixed): see the note on chain anchoring below
return new YGauge(min, min.addAtLeast(height));
}
// CHAIN ANCHORING (performance-critical, do not "simplify" back to
// addFixed):
//
// RealDelta.addAtLeast() delegates DOWNWARD --
// `new RealDelta(delegated.addAtLeast(delta), diff)` -- so calling
// addAtLeast on a RealDelta recurses through every layer below it and
// rebuilds one RealDelta per layer. RealImpl.addAtLeast(), in contrast,
// creates a single new RealImpl in O(1).
//
// The Y gauge chain links each tile to the previous one, so the link
// carried from gauge to gauge (the `max`, which becomes the next tile's
// `origin`) must ALWAYS be an anchored moveable (RealImpl), never a
// RealDelta. Otherwise every tile stacks one more RealDelta layer and
// tile n pays O(n) to chain: O(n^2) objects, O(n) recursion depth, and
// the OutOfMemoryError / deep `RealDelta.addAtLeast` stack seen on large
// diagrams.
//
// addFixed remains fine for LEAF values that nothing chains onto (e.g.
// the `min` below, read only for drawing).
// For tiles owning a contact line (message arrows). The min FLOATS below
// the chaining origin: contact = origin.addAtLeast(contactRelative), so
// that a later parallel (&) member with a taller label can push this tile
// down through an ensureBiggerThan on the shared contact line. When no
// parallel member follows, the solver minimization gives
// contact = origin + contactRelative, that is min = origin: exactly the
// plain sequential chaining.
public static YGauge createWithContact(YGauge currentY, double contactRelative, double height) {
if (height < 0 || contactRelative < 0)
throw new IllegalArgumentException();
final Real origin = currentY.getMax();
final Real contact = origin.addAtLeast(contactRelative);
// min is a leaf (drawing only): addFixed is safe and keeps it exactly
// contactRelative above the contact line
final Real min = contact.addFixed(-contactRelative);
// max is the chain link: it MUST be an anchored moveable, so it is
// built with addAtLeast on the contact (a RealImpl), not with addFixed
// on min (a RealDelta)
final Real max = contact.addAtLeast(height - contactRelative);
return new YGauge(min, max, contact, origin);
}
// For a parallel (&) tile: aligns its contact line with the shared
// contact line of the group, and pushes that line down if this member's
// label is taller (contact >= groupTop + contactRelative). The max
// accumulates over the members so that the next sequential tile chains
// below the WHOLE group, like the legacy
// TileParallel.getPreferredHeight().
public static YGauge createParallel(YGauge currentY, double contactRelative, double height) {
final Real contact = currentY.getContact();
final Real origin = currentY.getOrigin();
if (contact == null || origin == null)
// The previous tile has no contact line: fall back to top alignment
return create(currentY.getMin(), height);
contact.ensureBiggerThan(origin.addFixed(contactRelative));
final Real min = contact.addFixed(-contactRelative);
// CAUTION: do NOT use RealUtils.max() here. RealMax is a post-compile,
// read-only combinator: addAtLeast/ensureBiggerThan are unsupported
// (would crash on the next chained tile) and its getCurrentValue is
// cached on first read, so using it as a PositiveForce source would
// freeze a stale value in the middle of RealLine.compile(). The group
// max is expressed instead as a moveable Real with two constraints:
// the solver minimization computes the same maximum.
// It is built from the contact (a RealImpl) so that the chain link
// stays anchored -- see the CHAIN ANCHORING note above.
final Real max = contact.addAtLeast(height - contactRelative);
max.ensureBiggerThan(currentY.getMax());
return new YGauge(min, max, contact, origin);
}
// For zero-side tiles (life events, ...) placed between the members of a
// parallel group: the contact line and origin are propagated so that a
// following & tile still finds them, mirroring the legacy
// moveRecentParallelTilesToPending which skips through LifeEventTiles.
public static YGauge createPropagating(YGauge currentY, double height) {
if (height < 0)
throw new IllegalArgumentException();
final Real min = currentY.getMax();
// addAtLeast: the max is a chain link (see CHAIN ANCHORING above)
return new YGauge(min, min.addAtLeast(height), currentY.getContact(), currentY.getOrigin());
}
public final Real getMin() {
return min;
}
public final Real getMax() {
return max;
}
public final Real getContact() {
return contact;
}
public final Real getOrigin() {
return origin;
}
}