Skip to content

Commit adb5301

Browse files
authored
Merge pull request #149 from DemchaAV/chore/decouple-text-measurement-from-ecs
chore(engine): decouple TextMeasurementSystem from legacy SystemECS
2 parents cf840be + 8405dd0 commit adb5301

15 files changed

Lines changed: 142 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,26 @@ Open cycle — bug-fix / housekeeping. Entries land here as they merge.
124124
(`GraphCompose.document() → DocumentSession → LayoutCompiler`) never runs; it
125125
imports nothing from them directly, and the former `GraphCompose.pdf(...)`
126126
entry point has already been removed. The ECS execution engine runs only under
127-
the legacy engine regression tests. (One vestigial coupling remains, tracked as
128-
a follow-up: the canonical `TextMeasurementSystem` still `extends
129-
engine.core.SystemECS` via a no-op `process(...)`.) The
130-
packages are now `@Deprecated` (package level, so no deprecation-warning cascade)
127+
the legacy engine regression tests. The packages are now `@Deprecated` (package
128+
level, so no deprecation-warning cascade)
131129
with corrected package docs, to stop misdirecting contributors into optimizing a
132130
dead engine. The genuinely shared engine packages (`engine.components`,
133131
`engine.measurement`, `engine.font`, `engine.render`) are **not** deprecated.
134132
No public API or behaviour change.
135133

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

138149
- **Benchmark regression gate and measurement probe (benchmarks module, not part

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,19 @@
692692
-->
693693
<excludes>
694694
<exclude>com.demcha.compose.document.layout.payloads</exclude>
695+
<!--
696+
engine.measurement.TextMeasurementSystem dropped its vestigial
697+
SystemECS super-interface (and the no-op process(EntityManager)
698+
default) — see CHANGELOG v1.7.1. japicmp reports this as
699+
INTERFACE_REMOVED, but it is safe: nothing consumes a
700+
TextMeasurementSystem as a SystemECS. The legacy ECS engine now
701+
obtains the measurement service via
702+
SystemRegistry.registerTextMeasurement(...) / textMeasurement()
703+
instead of enrolling it as a process()-driven system, and the
704+
canonical pipeline injects it directly. Drop this exclude once the
705+
japicmp baseline advances to the release that lands the change.
706+
-->
707+
<exclude>com.demcha.compose.engine.measurement.TextMeasurementSystem</exclude>
695708
</excludes>
696709
</parameter>
697710
</configuration>

src/main/java/com/demcha/compose/engine/components/content/text/BlockTextLineMetrics.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public static double interLineGap(TextMeasurementSystem.LineMetrics previous,
143143

144144
private static TextMeasurementSystem measurementSystem(EntityManager entityManager) {
145145
return entityManager.getSystems()
146-
.getSystem(TextMeasurementSystem.class)
146+
.textMeasurement()
147147
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to resolve block text metrics."));
148148
}
149149
}

src/main/java/com/demcha/compose/engine/components/renderable/TextComponent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static ContentSize autoMeasureText(Entity entity, EntityManager entityMan
2424
Text text = entity.getComponent(Text.class).orElseThrow();
2525
TextStyle style = entity.getComponent(TextStyle.class).orElseThrow();
2626
TextMeasurementSystem measurementSystem = entityManager.getSystems()
27-
.getSystem(TextMeasurementSystem.class)
27+
.textMeasurement()
2828
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to auto-measure text."));
2929
ContentSize measured = measurementSystem.measure(style, text.value());
3030
double width = measured.width();

src/main/java/com/demcha/compose/engine/core/SystemRegistry.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.demcha.compose.engine.core;
22

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

28+
/**
29+
* The text-measurement service provider, held separately from the
30+
* process()-driven {@link #systems} map. Measurement exposes font metrics to
31+
* builders and layout helpers on demand; it is not a {@link SystemECS} and
32+
* never participates in the {@code processSystems()} loop, so it is registered
33+
* out-of-band via {@link #registerTextMeasurement(TextMeasurementSystem)}
34+
* rather than {@link #addSystem(SystemECS)}.
35+
*/
36+
@Getter(AccessLevel.NONE)
37+
private TextMeasurementSystem textMeasurement;
38+
39+
/**
40+
* Registers the text-measurement service exposed to the legacy engine's text
41+
* components and layout alignment. Unlike {@link #addSystem(SystemECS)} this
42+
* does not enroll the measurement system in the {@code process()} loop — it is
43+
* a service provider, not an ECS system.
44+
*
45+
* @param textMeasurement the measurement service to expose
46+
* @since 1.7.1
47+
*/
48+
public void registerTextMeasurement(TextMeasurementSystem textMeasurement) {
49+
this.textMeasurement = Objects.requireNonNull(textMeasurement, "textMeasurement");
50+
}
51+
52+
/**
53+
* Retrieves the registered text-measurement service, if any.
54+
*
55+
* @return an Optional containing the measurement service, or empty if none has
56+
* been registered
57+
* @since 1.7.1
58+
*/
59+
public Optional<TextMeasurementSystem> textMeasurement() {
60+
return Optional.ofNullable(textMeasurement);
61+
}
62+
2663
/**
2764
* Adds a single system to the registry.
2865
*

src/main/java/com/demcha/compose/engine/core/package-info.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,6 @@
1212
* and the layout / pagination / render systems it drives — is dead: it runs only
1313
* under the legacy engine regression tests.</p>
1414
*
15-
* <p>One vestigial holdover keeps {@code SystemECS} and {@code EntityManager}
16-
* referenced from live code: the canonical
17-
* {@code engine.measurement.TextMeasurementSystem} still
18-
* {@code extends SystemECS} with a no-op {@code process(EntityManager)}.
19-
* Decoupling that base — so {@code engine.core} becomes genuinely unreferenced by
20-
* the canonical pipeline — is a tracked follow-up.</p>
21-
*
2215
* <p>The genuinely shared engine packages are elsewhere and are <em>not</em>
2316
* deprecated: {@code engine.components} (value types), {@code engine.measurement}
2417
* (text-measurement contracts), {@code engine.font}, and
@@ -28,9 +21,8 @@
2821
* @deprecated Legacy ECS engine, superseded by the canonical
2922
* {@code com.demcha.compose.document.layout} pipeline. No public entry point
3023
* runs it and it is not on the canonical hot path; it is retained only for the
31-
* legacy engine regression tests (aside from the vestigial {@code SystemECS}
32-
* base of {@code TextMeasurementSystem}, a tracked cleanup) — a candidate for
33-
* removal. Do not extend it or spend optimization effort here.
24+
* legacy engine regression tests — a candidate for removal. Do not extend it or
25+
* spend optimization effort here.
3426
*/
3527
@Deprecated
3628
package com.demcha.compose.engine.core;

src/main/java/com/demcha/compose/engine/layout/LayoutSystem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ private Entity alignBlockText(Entity blockTextBox, EntityManager entityManager)
388388
if (Double.isNaN(lineWidth)) {
389389
if (measurementSystem == null) {
390390
measurementSystem = entityManager.getSystems()
391-
.getSystem(TextMeasurementSystem.class)
391+
.textMeasurement()
392392
.orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to align block text."));
393393
}
394394
lineWidth = line.width(measurementSystem, style);

src/main/java/com/demcha/compose/engine/measurement/TextMeasurementSystem.java

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import com.demcha.compose.engine.components.content.text.TextStyle;
44
import com.demcha.compose.engine.components.geometry.ContentSize;
5-
import com.demcha.compose.engine.core.EntityManager;
6-
import com.demcha.compose.engine.core.SystemECS;
75

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

1715
/**
1816
* Vertical metrics for one resolved text style.
@@ -90,19 +88,4 @@ default void clearCaches() {
9088
default double lineHeight(TextStyle style) {
9189
return lineMetrics(style).lineHeight();
9290
}
93-
94-
/**
95-
* A no-op implementation of the standard ECS system processing method.
96-
* <p>
97-
* Measurement systems act as service providers that expose metrics to builders
98-
* and layout calculations on demand. They do not actively mutate or participate
99-
* in the runtime processing of entities within the main game loop / pipeline.
100-
*
101-
* @param entityManager The active entity manager. Ignored by this system.
102-
*/
103-
@Override
104-
default void process(EntityManager entityManager) {
105-
// Measurement systems expose services to builders/layout helpers and do
106-
// not participate in the runtime processing pipeline.
107-
}
10891
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.demcha.compose.engine.core;
2+
3+
import com.demcha.compose.engine.components.content.text.TextStyle;
4+
import com.demcha.compose.engine.components.geometry.ContentSize;
5+
import com.demcha.compose.engine.measurement.TextMeasurementSystem;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
10+
11+
/**
12+
* Unit coverage for the dedicated text-measurement service slot on
13+
* {@link SystemRegistry}. This seam is what lets {@code TextMeasurementSystem}
14+
* stay decoupled from {@link SystemECS}: the measurement system is a service
15+
* provider exposed on demand, not a {@code process()}-driven ECS system, so it is
16+
* held out-of-band from the {@code systems} map.
17+
*/
18+
class SystemRegistryTest {
19+
20+
@Test
21+
void textMeasurementIsEmptyWhenNoneRegistered() {
22+
assertThat(new SystemRegistry().textMeasurement()).isEmpty();
23+
}
24+
25+
@Test
26+
void registerTextMeasurementExposesTheSameInstance() {
27+
SystemRegistry registry = new SystemRegistry();
28+
TextMeasurementSystem service = new StubMeasurement();
29+
30+
registry.registerTextMeasurement(service);
31+
32+
assertThat(registry.textMeasurement()).containsSame(service);
33+
}
34+
35+
@Test
36+
void registerTextMeasurementRejectsNull() {
37+
assertThatNullPointerException()
38+
.isThrownBy(() -> new SystemRegistry().registerTextMeasurement(null))
39+
.withMessageContaining("textMeasurement");
40+
}
41+
42+
@Test
43+
void measurementServiceIsNotEnrolledAsAProcessDrivenSystem() {
44+
SystemRegistry registry = new SystemRegistry();
45+
46+
registry.registerTextMeasurement(new StubMeasurement());
47+
48+
// It must stay out of the SystemECS map so the processSystems() loop never
49+
// calls process() on it — that is the whole point of the dedicated slot.
50+
assertThat(registry.getStream().toList()).isEmpty();
51+
}
52+
53+
private static final class StubMeasurement implements TextMeasurementSystem {
54+
@Override
55+
public ContentSize measure(TextStyle style, String text) {
56+
return new ContentSize(0.0, 0.0);
57+
}
58+
59+
@Override
60+
public LineMetrics lineMetrics(TextStyle style) {
61+
return new LineMetrics(0.0, 0.0, 0.0);
62+
}
63+
}
64+
}

src/test/java/com/demcha/compose/testsupport/EngineComposerHarness.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ public void close() throws IOException {
349349
}
350350

351351
private void setupSystems() {
352-
entityManager.getSystems().addSystem(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
352+
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
353353
entityManager.getSystems().addSystem(layoutSystem);
354354
entityManager.getSystems().addSystem(renderingSystem);
355355
}

0 commit comments

Comments
 (0)