Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,26 @@ Open cycle — bug-fix / housekeeping. Entries land here as they merge.
(`GraphCompose.document() → DocumentSession → LayoutCompiler`) never runs; it
imports nothing from them directly, and the former `GraphCompose.pdf(...)`
entry point has already been removed. The ECS execution engine runs only under
the legacy engine regression tests. (One vestigial coupling remains, tracked as
a follow-up: the canonical `TextMeasurementSystem` still `extends
engine.core.SystemECS` via a no-op `process(...)`.) The
packages are now `@Deprecated` (package level, so no deprecation-warning cascade)
the legacy engine regression tests. The packages are now `@Deprecated` (package
level, so no deprecation-warning cascade)
with corrected package docs, to stop misdirecting contributors into optimizing a
dead engine. The genuinely shared engine packages (`engine.components`,
`engine.measurement`, `engine.font`, `engine.render`) are **not** deprecated.
No public API or behaviour change.

- **`TextMeasurementSystem` decoupled from `engine.core.SystemECS`.** The shared
text-measurement contract (`engine.measurement.TextMeasurementSystem`) dropped
its vestigial `extends SystemECS` and the no-op `process(EntityManager)` default
it carried — it was never consumed as an ECS system. The legacy ECS engine now
obtains the measurement service via `SystemRegistry.registerTextMeasurement(...)`
/ `textMeasurement()` instead of enrolling it as a `process()`-driven system,
completing the isolation of the deprecated `engine.core` from live and shared
code (only the legacy engine regression tests still reference it). Dropping the
super-interface is binary-incompatible on paper, so
`engine.measurement.TextMeasurementSystem` is excluded from the japicmp gate
until the baseline advances past this release. No canonical API or behaviour
change.

### Tests / tooling

- **Benchmark regression gate and measurement probe (benchmarks module, not part
Expand Down
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,19 @@
-->
<excludes>
<exclude>com.demcha.compose.document.layout.payloads</exclude>
<!--
engine.measurement.TextMeasurementSystem dropped its vestigial
SystemECS super-interface (and the no-op process(EntityManager)
default) — see CHANGELOG v1.7.1. japicmp reports this as
INTERFACE_REMOVED, but it is safe: nothing consumes a
TextMeasurementSystem as a SystemECS. The legacy ECS engine now
obtains the measurement service via
SystemRegistry.registerTextMeasurement(...) / textMeasurement()
instead of enrolling it as a process()-driven system, and the
canonical pipeline injects it directly. Drop this exclude once the
japicmp baseline advances to the release that lands the change.
-->
<exclude>com.demcha.compose.engine.measurement.TextMeasurementSystem</exclude>
</excludes>
</parameter>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public static double interLineGap(TextMeasurementSystem.LineMetrics previous,

private static TextMeasurementSystem measurementSystem(EntityManager entityManager) {
return entityManager.getSystems()
.getSystem(TextMeasurementSystem.class)
.textMeasurement()
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to resolve block text metrics."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static ContentSize autoMeasureText(Entity entity, EntityManager entityMan
Text text = entity.getComponent(Text.class).orElseThrow();
TextStyle style = entity.getComponent(TextStyle.class).orElseThrow();
TextMeasurementSystem measurementSystem = entityManager.getSystems()
.getSystem(TextMeasurementSystem.class)
.textMeasurement()
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to auto-measure text."));
ContentSize measured = measurementSystem.measure(style, text.value());
double width = measured.width();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.demcha.compose.engine.core;

import com.demcha.compose.engine.core.SystemECS;
import com.demcha.compose.engine.measurement.TextMeasurementSystem;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -23,6 +25,41 @@ public class SystemRegistry {
*/
private final Map<Class<? extends SystemECS>, SystemECS> systems = new LinkedHashMap<>();

/**
* The text-measurement service provider, held separately from the
* process()-driven {@link #systems} map. Measurement exposes font metrics to
* builders and layout helpers on demand; it is not a {@link SystemECS} and
* never participates in the {@code processSystems()} loop, so it is registered
* out-of-band via {@link #registerTextMeasurement(TextMeasurementSystem)}
* rather than {@link #addSystem(SystemECS)}.
*/
@Getter(AccessLevel.NONE)
private TextMeasurementSystem textMeasurement;

/**
* Registers the text-measurement service exposed to the legacy engine's text
* components and layout alignment. Unlike {@link #addSystem(SystemECS)} this
* does not enroll the measurement system in the {@code process()} loop — it is
* a service provider, not an ECS system.
*
* @param textMeasurement the measurement service to expose
* @since 1.7.1
*/
public void registerTextMeasurement(TextMeasurementSystem textMeasurement) {
this.textMeasurement = Objects.requireNonNull(textMeasurement, "textMeasurement");
}

/**
* Retrieves the registered text-measurement service, if any.
*
* @return an Optional containing the measurement service, or empty if none has
* been registered
* @since 1.7.1
*/
public Optional<TextMeasurementSystem> textMeasurement() {
return Optional.ofNullable(textMeasurement);
}

/**
* Adds a single system to the registry.
*
Expand Down
12 changes: 2 additions & 10 deletions src/main/java/com/demcha/compose/engine/core/package-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@
* and the layout / pagination / render systems it drives — is dead: it runs only
* under the legacy engine regression tests.</p>
*
* <p>One vestigial holdover keeps {@code SystemECS} and {@code EntityManager}
* referenced from live code: the canonical
* {@code engine.measurement.TextMeasurementSystem} still
* {@code extends SystemECS} with a no-op {@code process(EntityManager)}.
* Decoupling that base — so {@code engine.core} becomes genuinely unreferenced by
* the canonical pipeline — is a tracked follow-up.</p>
*
* <p>The genuinely shared engine packages are elsewhere and are <em>not</em>
* deprecated: {@code engine.components} (value types), {@code engine.measurement}
* (text-measurement contracts), {@code engine.font}, and
Expand All @@ -28,9 +21,8 @@
* @deprecated Legacy ECS engine, superseded by the canonical
* {@code com.demcha.compose.document.layout} pipeline. No public entry point
* runs it and it is not on the canonical hot path; it is retained only for the
* legacy engine regression tests (aside from the vestigial {@code SystemECS}
* base of {@code TextMeasurementSystem}, a tracked cleanup) — a candidate for
* removal. Do not extend it or spend optimization effort here.
* legacy engine regression tests — a candidate for removal. Do not extend it or
* spend optimization effort here.
*/
@Deprecated
package com.demcha.compose.engine.core;
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ private Entity alignBlockText(Entity blockTextBox, EntityManager entityManager)
if (Double.isNaN(lineWidth)) {
if (measurementSystem == null) {
measurementSystem = entityManager.getSystems()
.getSystem(TextMeasurementSystem.class)
.textMeasurement()
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to align block text."));
}
lineWidth = line.width(measurementSystem, style);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.demcha.compose.engine.components.content.text.TextStyle;
import com.demcha.compose.engine.components.geometry.ContentSize;
import com.demcha.compose.engine.core.EntityManager;
import com.demcha.compose.engine.core.SystemECS;

/**
* Engine-level text measurement contract used by builders and layout helpers.
Expand All @@ -12,7 +10,7 @@
* without forcing engine code to reach through the active rendering system.
* </p>
*/
public interface TextMeasurementSystem extends SystemECS {
public interface TextMeasurementSystem {

/**
* Vertical metrics for one resolved text style.
Expand Down Expand Up @@ -90,19 +88,4 @@ default void clearCaches() {
default double lineHeight(TextStyle style) {
return lineMetrics(style).lineHeight();
}

/**
* A no-op implementation of the standard ECS system processing method.
* <p>
* Measurement systems act as service providers that expose metrics to builders
* and layout calculations on demand. They do not actively mutate or participate
* in the runtime processing of entities within the main game loop / pipeline.
*
* @param entityManager The active entity manager. Ignored by this system.
*/
@Override
default void process(EntityManager entityManager) {
// Measurement systems expose services to builders/layout helpers and do
// not participate in the runtime processing pipeline.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.demcha.compose.engine.core;

import com.demcha.compose.engine.components.content.text.TextStyle;
import com.demcha.compose.engine.components.geometry.ContentSize;
import com.demcha.compose.engine.measurement.TextMeasurementSystem;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;

/**
* Unit coverage for the dedicated text-measurement service slot on
* {@link SystemRegistry}. This seam is what lets {@code TextMeasurementSystem}
* stay decoupled from {@link SystemECS}: the measurement system is a service
* provider exposed on demand, not a {@code process()}-driven ECS system, so it is
* held out-of-band from the {@code systems} map.
*/
class SystemRegistryTest {

@Test
void textMeasurementIsEmptyWhenNoneRegistered() {
assertThat(new SystemRegistry().textMeasurement()).isEmpty();
}

@Test
void registerTextMeasurementExposesTheSameInstance() {
SystemRegistry registry = new SystemRegistry();
TextMeasurementSystem service = new StubMeasurement();

registry.registerTextMeasurement(service);

assertThat(registry.textMeasurement()).containsSame(service);
}

@Test
void registerTextMeasurementRejectsNull() {
assertThatNullPointerException()
.isThrownBy(() -> new SystemRegistry().registerTextMeasurement(null))
.withMessageContaining("textMeasurement");
}

@Test
void measurementServiceIsNotEnrolledAsAProcessDrivenSystem() {
SystemRegistry registry = new SystemRegistry();

registry.registerTextMeasurement(new StubMeasurement());

// It must stay out of the SystemECS map so the processSystems() loop never
// calls process() on it — that is the whole point of the dedicated slot.
assertThat(registry.getStream().toList()).isEmpty();
}

private static final class StubMeasurement implements TextMeasurementSystem {
@Override
public ContentSize measure(TextStyle style, String text) {
return new ContentSize(0.0, 0.0);
}

@Override
public LineMetrics lineMetrics(TextStyle style) {
return new LineMetrics(0.0, 0.0, 0.0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ public void close() throws IOException {
}

private void setupSystems() {
entityManager.getSystems().addSystem(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.getSystems().addSystem(layoutSystem);
entityManager.getSystems().addSystem(renderingSystem);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ private boolean isStickyToNext(String text) {

private TextMeasurementSystem textMeasurementSystem() {
return entityManager.getSystems()
.getSystem(TextMeasurementSystem.class)
.textMeasurement()
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to build block text."));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class BlockTextBuilderTest {
@BeforeEach
void setUp() {
entityManager = new EntityManager();
entityManager.getSystems().addSystem(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.setMarkdown(true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ private ContentSize measureText(String text, TableCellLayoutStyle style) {

private TextMeasurementSystem textMeasurementSystem() {
return entityManager.getSystems()
.getSystem(TextMeasurementSystem.class)
.textMeasurement()
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to measure table text."));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ void shouldIncludeCellLineSpacingInMultilineCellHeight() throws Exception {
@Test
void shouldBuildWithoutLayoutSystemWhenTextMeasurementSystemIsRegistered() {
EntityManager entityManager = new EntityManager();
entityManager.getSystems().addSystem(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));

Entity table = new TableBuilder(entityManager)
.entityName("MeasurementOnlyTable")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class TextBuilderTest {
@BeforeEach
void setUp() {
entityManager = new EntityManager();
entityManager.getSystems().addSystem(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
}

@Test
Expand Down Expand Up @@ -79,7 +79,7 @@ void shouldAutosizeWithoutLayoutSystemWhenMeasurementSystemIsRegistered() {
void shouldExposeFullPdfLineMetricsThroughMeasurementSystem() {
TextStyle style = TextStyle.DEFAULT_STYLE;
var measurementSystem = entityManager.getSystems()
.getSystem(com.demcha.compose.engine.measurement.TextMeasurementSystem.class)
.textMeasurement()
.orElseThrow();

PdfFont font = (PdfFont) entityManager.getFonts().getFont(style.fontName(), PdfFont.class).orElseThrow();
Expand Down