diff --git a/CHANGELOG.md b/CHANGELOG.md index 3355791fb..ff5af3acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,13 @@ for this cycle. - The unused engine-internal `Font.adjustFontSizeToFit(...)` (and its `PdfFont` / `WordFont` implementations) has been removed; text auto-sizing is resolved by the layout compiler. +- The dormant Entity-Component-System engine internals have been removed: the + `EntityManager` / `SystemECS` runtime, the `Entity` component model with its + geometry / coordinator / renderable companions, the ECS render pipeline + (`engine.render.*` and the guide renderers under `engine.render.guides`), and + `LayoutSnapshotExtractor`. None were reachable from the live render path — + `DocumentSession` → layout compiler → fixed-layout backend — so document layout, + PDF output, and the public `guideLines(...)` overlay are unchanged. ### Public API diff --git a/qa/src/test/resources/logback.xml b/qa/src/test/resources/logback.xml index 2adb12613..f33257512 100644 --- a/qa/src/test/resources/logback.xml +++ b/qa/src/test/resources/logback.xml @@ -93,24 +93,8 @@ - - - - - - - - - - - - - - - - diff --git a/src/main/java/com/demcha/compose/document/layout/ParagraphWrapping.java b/src/main/java/com/demcha/compose/document/layout/ParagraphWrapping.java index fd770fa41..6916fa0dc 100644 --- a/src/main/java/com/demcha/compose/document/layout/ParagraphWrapping.java +++ b/src/main/java/com/demcha/compose/document/layout/ParagraphWrapping.java @@ -7,7 +7,7 @@ import com.demcha.compose.engine.components.content.text.TextDataBody; import com.demcha.compose.engine.components.content.text.TextIndentStrategy; import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.renderable.BlockText; +import com.demcha.compose.engine.text.TextControlSanitizer; import com.demcha.compose.engine.measurement.TextMeasurementSystem; import com.demcha.compose.engine.text.markdown.MarkDownParser; @@ -560,7 +560,7 @@ private static List> tokenizeInlineRuns(List continue; } TextStyle style = textRun.textStyle() == null ? defaultStyle : toTextStyle(textRun.textStyle()); - String normalized = BlockText.sanitizeText(textRun.text().replace("\r\n", "\n").replace('\r', '\n')); + String normalized = TextControlSanitizer.remove(textRun.text().replace("\r\n", "\n").replace('\r', '\n')); String[] parts = normalized.split("\n", -1); for (int partIndex = 0; partIndex < parts.length; partIndex++) { if (partIndex > 0) { @@ -592,7 +592,7 @@ private static List> tokenizeInlineRuns(List // run's outer edges — lead pad on the first word, trail pad on the // last — and toInlineParagraphLine coalesces the same-group tokens on // each visual line back into one rounded fill. - String normalized = BlockText.sanitizeText( + String normalized = TextControlSanitizer.remove( highlight.text().replace("\r\n", " ").replace('\r', ' ').replace('\n', ' ')); if (normalized.isEmpty()) { continue; diff --git a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java index 1db6c3e75..28b3ea341 100644 --- a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java +++ b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java @@ -8,7 +8,7 @@ import com.demcha.compose.document.style.DocumentTextStyle; import com.demcha.compose.engine.components.content.text.TextIndentStrategy; import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.renderable.BlockText; +import com.demcha.compose.engine.text.TextControlSanitizer; import com.demcha.compose.engine.components.style.Padding; import com.demcha.compose.engine.measurement.TextMeasurementSystem; @@ -736,7 +736,7 @@ private static List sanitizeLogicalLines(String rawText) { String[] logicalLines = safeText.split("\n", -1); List sanitized = new ArrayList<>(logicalLines.length); for (String logicalLine : logicalLines) { - sanitized.add(BlockText.sanitizeText(logicalLine)); + sanitized.add(TextControlSanitizer.remove(logicalLine)); } return List.copyOf(sanitized); } diff --git a/src/main/java/com/demcha/compose/engine/components/core/Entity.java b/src/main/java/com/demcha/compose/engine/components/core/Entity.java deleted file mode 100644 index 03e595e85..000000000 --- a/src/main/java/com/demcha/compose/engine/components/core/Entity.java +++ /dev/null @@ -1,505 +0,0 @@ -package com.demcha.compose.engine.components.core; - -import com.demcha.compose.engine.components.geometry.EntityBounds; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.pagination.Offset; -import com.demcha.compose.engine.pagination.ParentContainerUpdater; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; - -/** - * Core runtime node in the GraphCompose entity-component model. - *

- * An {@code Entity} is a lightweight container for components such as text, - * style, size, layout metadata, and render markers. Builders create entities - * and - * attach those components; systems later read and enrich the same entity as the - * document moves through layout, pagination, and rendering. - *

- * - *

- * In other words, builders describe intent on the entity, while systems turn - * that intent into resolved geometry and final output. - *

- */ -@Slf4j -@EqualsAndHashCode -public final class Entity { - @Getter - private final UUID uuid; - private final Map, Component> comps = new LinkedHashMap<>(); - @Getter - private final List children = new ArrayList<>(); - private EntityName name; - @Getter - private Render render; - @Setter - @Getter - private boolean guideLines; - - /** - * Creates a new entity with a fresh runtime UUID. - */ - public Entity() { - UUID uuid = UUID.randomUUID(); - log.debug("Creating entity {}", uuid); - this.uuid = uuid; - } - - /** - * Creates a shallow component copy of another entity. - * - *

- * The new entity gets its own UUID while reusing the source entity's current - * component references as the initial snapshot. - *

- * - * @param entity source entity to clone from - * @return new entity with copied component references - */ - public static Entity createFrom(Entity entity) { - log.debug("Creating a copy of entity {}", entity); - var newEntity = new Entity(); - var values = new HashSet<>(entity.comps.values()); - newEntity.populate(values); - return newEntity; - } - - /** - * Returns the current entity name. - * - * @return configured name value - */ - public String name() { - return name.value(); - } - - /** - * Adds a component only when the same concrete component type is not already - * present. - * - * @param c component to add if absent - * @param component type - * @return this entity for fluent chaining - */ - public Entity addComponentIfAbsent(T c) { - if (c == null) { - log.warn("addComponentIfAbsent: component is null"); - return this; - } - Class key = c.getClass().asSubclass(Component.class); - Component prev = comps.putIfAbsent(key, c); - if (prev != null) { - if (log.isDebugEnabled()) { - log.debug("addComponentIfAbsent: component {} already exists on {}", key.getName(), this); - } - return this; - } - if (c instanceof EntityName en) - this.name = en; - if (log.isDebugEnabled()) { - log.debug("Added absent component {} to {}", key.getSimpleName(), this); - } - return this; - } - - /** - * Adds or replaces a component by its concrete runtime class. - * - *

- * When the component is an {@link EntityName}, the cached entity name is - * updated. When it implements {@link Render}, the entity also caches it for - * fast render checks and dispatch. - *

- * - * @param c component to add - * @param component type - * @return this entity for fluent chaining - * @throws IllegalStateException if a second render component is attached - * through the strict add path - */ - public Entity addComponent(@NonNull T c) { - Class key = c.getClass().asSubclass(Component.class); - if (log.isDebugEnabled()) { - log.debug("Adding component {} to {}", c, this); - } - Component prev = comps.put(key, c); - if (c instanceof EntityName en) - this.name = en; - if (c instanceof Render) { - if (render == null) { - this.render = (Render) c; - } else { - log.warn("Render Component Already signed in {}", this); - throw new IllegalStateException("%s Render component already assigned in %s".formatted(c, this.render)); - } - - } - debugPutMethod(c, prev, "addComponent"); - return this; - } - - private void debugPutMethod(T c, Component prev, String methodName) { - if (log.isDebugEnabled()) { - if (prev == null) { - log.debug("Added component {} to {} through method {}", c, this, methodName); - } else { - log.debug("Replaced component {} on {} through method {}", c, this, methodName); - } - } - } - - /** - * Adds or replaces a component by its concrete runtime class, including - * overwriting an existing cached render component when necessary. - * - * @param c component to add - * @param component type - * @return this entity for fluent chaining - */ - public Entity forceAddComponent(@NonNull T c) { - Class key = c.getClass().asSubclass(Component.class); - Component prev = comps.put(key, c); - if (c instanceof EntityName en) - this.name = en; - if (c instanceof Render) { - if (render == null) { - this.render = (Render) c; - } else { - log.warn("Rendering Component Already signed in {}", this); - this.render = (Render) c; - } - - } - debugPutMethod(c, prev, "forceAddComponent"); - return this; - } - - /** - * Looks up a component by its exact concrete class key. - * - * @param type concrete component class - * @param component type - * @return the component wrapped in an {@link Optional}, or empty when absent - */ - public Optional getComponent(Class type) { - T value = type.cast(comps.get(type)); - if (value == null) { - return Optional.empty(); - } - return Optional.of(value); - } - - /** - * Looks up a component and fails fast when the entity does not have it. - * - * @param type concrete component class - * @param component type - * @return resolved component - * @throws NoSuchElementException when the component is missing - */ - public T require(Class type) { - T value = type.cast(comps.get(type)); - if (value != null) { - return value; - } - log.error("No component found for type {} for entity [{}]", type.getName(), uuid); - throw new NoSuchElementException("Missing " + type.getName() + " for " + uuid); - } - - /** - * Checks whether any attached component type is assignable to the given base - * class. - * - * @param baseClass base class or marker interface to test - * @return {@code true} when at least one attached component matches by - * assignability - */ - public boolean hasAssignable(Class baseClass) { - return comps.keySet().stream() - .anyMatch(c -> baseClass.isAssignableFrom(c)); - } - - /** - * Checks whether the entity contains a component of the exact given concrete - * type. - * - * @param type component class to check - * @return {@code true} when the component is present - */ - public boolean has(Class type) { - if (log.isDebugEnabled()) { - log.debug("Checking component {} {}", type, this); - } - return comps.containsKey(type); - } - - /** - * Convenience overload that checks the runtime type of the provided component - * instance. - * - * @param component component whose runtime type should be checked - * @param component type - * @return {@code true} when a component of that concrete type is attached - */ - public boolean has(T component) { - var type = component.getClass(); - return has(type); - } - - /** - * Returns whether the entity currently has a cached render marker. - * - *

- * This is intentionally O(1) and does not rescan the component map. - *

- * - * @return {@code true} when a render component is attached - */ - public boolean hasRender() { - return render != null; - } - - /** - * Compatibility wrapper for entity top-bound calculation. - * - * @return top outer line including top margin - * @deprecated use {@link EntityBounds#topLine(Entity)} directly in new code - */ - @Deprecated - public double boundingTopLine() { - return EntityBounds.topLine(this); - } - - /** - * Compatibility wrapper for entity bottom-bound calculation. - * - * @return bottom outer line including bottom margin - * @deprecated use {@link EntityBounds#bottomLine(Entity)} directly in new code - */ - @Deprecated - public double boundingBottomLine() { - return EntityBounds.bottomLine(this); - } - - /** - * Compatibility wrapper for entity right-bound calculation. - * - * @return right outer line including right margin - * @deprecated use {@link EntityBounds#rightLine(Entity)} directly in new code - */ - @Deprecated - public double boundingRightLine() { - return EntityBounds.rightLine(this); - } - - /** - * Compatibility wrapper for entity left-bound calculation. - * - * @return left outer line including left margin - * @deprecated use {@link EntityBounds#leftLine(Entity)} directly in new code - */ - @Deprecated - public double boundingLeftLine() { - return EntityBounds.leftLine(this); - } - - /** - * Populates the entity with the provided component set. - * - * @param components components to attach - * @param component type - * @return this entity for fluent chaining - */ - public Entity populate(Set components) { - if (log.isDebugEnabled()) { - log.debug("Creating and populating entity"); - log.debug("Populating entity UUID [{}] with\nComponents: {}", this, components); - } - for (Component component : components) { - addComponent(component); - } - if (log.isDebugEnabled()) { - log.debug("Created and populated entity {}", this); - } - return this; - - } - - /** - * Returns a read-only view of all components currently attached to the entity. - * - * @return an unmodifiable component map keyed by concrete component class - */ - public Map, Component> view() { - if (log.isDebugEnabled()) { - log.debug("Viewing component {} {}", uuid, this); - } - return Collections.unmodifiableMap(comps); - } - - /** - * Removes a component by its exact concrete class key. - * - * @param type component class to remove - * @return {@code true} when a component was removed - */ - public boolean remove(Class type) { - Component removed = comps.remove(type); - if (removed == null) { - return false; - } - if (removed == render) { - render = null; - } - if (removed == name) { - name = null; - } - return true; - } - - private String printChildren(EntityManager manager) { - StringBuilder childrenInfo = new StringBuilder(); - manager.getSetEntitiesFromUuids(new HashSet<>(children)).forEach(entity -> { - childrenInfo.append(entity.toString()).append("\n"); - }); - return childrenInfo.toString(); - - } - - /** - * Compatibility wrapper that propagates a child offset to the parent container. - * - * @param manager entity manager used to resolve parent entities - * @param offset propagated offset - * @return {@code true} when a parent update was applied - * @deprecated use {@link ParentContainerUpdater#updateParentContainer(Entity, EntityManager, Offset)} - * directly in new code - */ - @Deprecated - public boolean updateParentContainer(EntityManager manager, Offset offset) { - return ParentContainerUpdater.updateParentContainer(this, manager, offset); - } - - /** - * Compatibility wrapper that propagates a vertical delta to the parent - * container chain. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY vertical delta to propagate - * @return {@code true} when a parent update was applied - * @deprecated use - * {@link ParentContainerUpdater#updateParentContainer(Entity, EntityManager, double)} - * directly in new code - */ - @Deprecated - public boolean updateParentContainer(EntityManager manager, double offsetY) { - return ParentContainerUpdater.updateParentContainer(this, manager, offsetY); - } - - /** - * Compatibility wrapper that propagates a size-only delta to the parent - * container chain. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta to propagate - * @return {@code true} when a parent size update was applied - * @deprecated use - * {@link ParentContainerUpdater#updateParentContainerSize(Entity, EntityManager, double)} - * directly in new code - */ - @Deprecated - public boolean updateParentContainerSize(EntityManager manager, double offsetY) { - return ParentContainerUpdater.updateParentContainerSize(this, manager, offsetY); - } - - /** - * Compatibility wrapper that resizes and, when needed, repositions the target - * entity before propagating the change upward. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta or upward shift delta - * @param entity entity to mutate - * @return {@code true} when an update was applied - * @deprecated use - * {@link ParentContainerUpdater#updateEntitySizeAndPosition(EntityManager, double, Entity)} - * directly in new code - */ - @Deprecated - public boolean updateEntitySizeAndPosition(EntityManager manager, double offsetY, @NonNull Entity entity) { - return ParentContainerUpdater.updateEntitySizeAndPosition(manager, offsetY, entity); - } - - /** - * Compatibility wrapper that resizes the target entity and propagates the size - * change upward. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta to apply - * @param entity entity to resize - * @return {@code true} when an update was applied - * @deprecated use {@link ParentContainerUpdater#updateEntitySize(EntityManager, double, Entity)} - * directly in new code - */ - @Deprecated - public boolean updateEntitySize(EntityManager manager, double offsetY, @NonNull Entity entity) { - return ParentContainerUpdater.updateEntitySize(manager, offsetY, entity); - } - - /** - * Compatibility wrapper that resizes the current entity and propagates the - * size-only change upward. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta to apply - * @return {@code true} when an update was applied - * @deprecated use - * {@link ParentContainerUpdater#updateCurrentEntitySize(Entity, EntityManager, double)} - * directly in new code - */ - @Deprecated - public boolean updateEntitySize(EntityManager manager, double offsetY) { - return ParentContainerUpdater.updateCurrentEntitySize(this, manager, offsetY); - } - - /** - * Returns a multi-line debug representation of the entity and its attached - * components. - * - * @return debug text for this entity - */ - public String printInfo() { - StringBuilder info = new StringBuilder(this + "\n"); - - comps.forEach((k, e) -> { - info.append(e.toString()).append("\n"); - }); - return info.toString(); - } - - /** - * Returns debug text for this entity plus its currently resolved child - * entities. - * - * @param entityManager entity manager used to resolve child UUIDs - * @return debug text for this entity and its children - */ - public String printInfoWithChildren(EntityManager entityManager) { - return printInfo() + printChildren(entityManager); - } - - @Override - public String toString() { - return "Entity[" + name + - " id: " + uuid + - " renderType: " + (render != null ? render.getClass().getSimpleName() : "null") + - ']'; - } - -} diff --git a/src/main/java/com/demcha/compose/engine/components/core/EntityName.java b/src/main/java/com/demcha/compose/engine/components/core/EntityName.java deleted file mode 100644 index 558e9e8e8..000000000 --- a/src/main/java/com/demcha/compose/engine/components/core/EntityName.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.demcha.compose.engine.components.core; - -/** - * Human-readable name for an entity. - * - *

Attach this component to make logs, debugging, and inspector views clearer. - * The name is not an identifier and does not have to be unique.

- * - *

Constraints

- *
    - *
  • Non-null and non-blank.
  • - *
  • Trimmed; recommended max length is 100 characters.
  • - *
- * - *

Examples: - *

{@code
- * addComponent(entityId, new EntityName("Header → ContactRow"));
- * addComponent(entityId, new EntityName("Button[primary]/#submit"));
- * }
- *

- * - * @param value the human-readable name of the entity; must be non-blank - * @since 1.0 - */ -public record EntityName(String value) implements Component { -} diff --git a/src/main/java/com/demcha/compose/engine/components/geometry/EntityBounds.java b/src/main/java/com/demcha/compose/engine/components/geometry/EntityBounds.java deleted file mode 100644 index 3efff34e5..000000000 --- a/src/main/java/com/demcha/compose/engine/components/geometry/EntityBounds.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import lombok.experimental.UtilityClass; - -/** - * Geometry helper for reading an entity's outer bounds from resolved layout - * state. - * - *

- * Bounds are derived from {@link Placement}, {@link ContentSize}, and optional - * {@link Margin}. This helper keeps geometry reads out of {@code Entity} - * itself. - *

- */ -@UtilityClass -public class EntityBounds { - - /** - * Returns the entity's top outer line including top margin. - * - * @param entity entity with resolved placement and content size - * @return top line in document coordinates - */ - public static double topLine(Entity entity) { - Placement placement = entity.require(Placement.class); - ContentSize size = entity.require(ContentSize.class); - Margin margin = marginOf(entity); - return placement.y() + size.height() + margin.top(); - } - - /** - * Returns the entity's bottom outer line including bottom margin. - * - * @param entity entity with resolved placement - * @return bottom line in document coordinates - */ - public static double bottomLine(Entity entity) { - Placement placement = entity.require(Placement.class); - Margin margin = marginOf(entity); - return placement.y() - margin.bottom(); - } - - /** - * Returns the entity's right outer line including right margin. - * - * @param entity entity with resolved placement and content size - * @return right line in document coordinates - */ - public static double rightLine(Entity entity) { - Placement placement = entity.require(Placement.class); - ContentSize size = entity.require(ContentSize.class); - Margin margin = marginOf(entity); - return placement.x() + size.width() + margin.right(); - } - - /** - * Returns the entity's left outer line including left margin. - * - * @param entity entity with resolved placement - * @return left line in document coordinates - */ - public static double leftLine(Entity entity) { - Placement placement = entity.require(Placement.class); - Margin margin = marginOf(entity); - return placement.x() - margin.left(); - } - - private static Margin marginOf(Entity entity) { - return entity.getComponent(Margin.class).orElse(Margin.zero()); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/geometry/InnerBoxSize.java b/src/main/java/com/demcha/compose/engine/components/geometry/InnerBoxSize.java deleted file mode 100644 index 286f2f3fc..000000000 --- a/src/main/java/com/demcha/compose/engine/components/geometry/InnerBoxSize.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.exceptions.ContentSizeNotFoundException; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -import java.util.Objects; -import java.util.Optional; - -@Slf4j -public record InnerBoxSize(double width, double height) { - - public static Optional from(@NonNull Entity entity) { - log.debug("Starting calculation a OuterBoxSize for entity: {}", entity); - Optional sizeOpt = entity.getComponent(ContentSize.class); - var padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - var size = sizeOpt - .orElseThrow( - () -> { - log.debug("{} has no outer BoxSize.", entity); - return new ContentSizeNotFoundException(); - } - ); - - var innerBoxSizeOpt = from(size, padding); - return innerBoxSizeOpt; - } - - public static Optional from(ContentSize contentSize, Padding padding) { - Objects.requireNonNull(contentSize, () -> { - log.error("Can not calculate InnerBoxSize. All objects must have a ContentSize."); - throw new ContentSizeNotFoundException(); - }); - Objects.requireNonNull(padding, () -> { - log.error("Padding cannot be null."); - throw new NullPointerException("Padding cannot be null."); - }); - - var w = contentSize.width() - padding.horizontal(); - var h = contentSize.height() - padding.vertical(); - - var box = new InnerBoxSize(w, h); - log.debug("{}", box); - return Optional.of(box); - } - - - - private static Placement getPlacement(Entity entity) { - return entity.getComponent(Placement.class).orElseThrow(() -> { - log.error("Placement.class has not been found. {}", entity); - return new NullPointerException("Placement.class has not been found."); - }); - } - - public double getX(Placement placement, Padding padding) { - return placement.x() + padding.left(); - } - - public double getY(Placement placement, Padding padding) { - return placement.y() + padding.bottom(); - } - - public double getInnerX(Entity entity) { - Placement placement = getPlacement(entity); - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - return getX(placement, padding); - } - - public double getInnerY(Entity entity) { - Placement placement = getPlacement(entity); - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - return getY(placement, padding); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/geometry/OuterBoxSize.java b/src/main/java/com/demcha/compose/engine/components/geometry/OuterBoxSize.java deleted file mode 100644 index db0c59933..000000000 --- a/src/main/java/com/demcha/compose/engine/components/geometry/OuterBoxSize.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.exceptions.ContentSizeNotFoundException; -import lombok.Data; -import lombok.NonNull; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; - -import java.util.Objects; -import java.util.Optional; - -/** - * This is size of Box Component which including a Component size + margins - * - */ -@Slf4j -@Accessors(fluent = true) -@Data -public final class OuterBoxSize { - final private double width; - final private double height; - - public static Optional from(@NonNull Entity entity) { - log.debug("Starting calculation a OuterBoxSize for entity: {}", entity); - Optional sizeOpt = entity.getComponent(ContentSize.class); - Margin margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - var size = sizeOpt - .orElseThrow( - () -> { - log.debug("{} has no outer BoxSize.", entity); - return new ContentSizeNotFoundException(entity); - } - ); - Optional outerBoxSizeOpt = from(size, margin); - OuterBoxSize outerBoxSize = outerBoxSizeOpt.orElseThrow(); - log.debug("Calculated outer {} for entity: {}", outerBoxSize, entity); - return outerBoxSizeOpt; - } - - public static Optional from(@NonNull ContentSize contentSize, @NonNull Margin margin) { - Objects.requireNonNull(contentSize, () -> { - log.error("All objects must have a ContentSize."); - throw new ContentSizeNotFoundException(); - }); - - - var w = contentSize.width() + margin.horizontal(); - var h = contentSize.height() + margin.vertical(); - - var box = new OuterBoxSize(w, h); - log.debug("{}", box); - return Optional.of(box); - } - - private static Placement getPlacement(Entity entity) { - return entity.getComponent(Placement.class).orElseThrow(() -> { - log.error("Placement.class has not been found. {}", entity); - return new NullPointerException("Placement.class has not been found."); - }); - } - - public static double getX(Placement placement, @NonNull Margin margin) { - //TODO has to be placement.y() - margin.left() - return placement.x(); - } - - public static double getY(Placement placement, @NonNull Margin margin) { - return placement.y() - margin.bottom(); - } - - public static double getOuterX(Entity entity) { - Placement placement = getPlacement(entity); - Margin margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - return getX(placement, margin); - } - - public static double getOuterY(Entity entity) { - Placement placement = getPlacement(entity); - Margin margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - return getY(placement, margin); - } - - -} - diff --git a/src/main/java/com/demcha/compose/engine/components/layout/Anchor.java b/src/main/java/com/demcha/compose/engine/components/layout/Anchor.java index 09fae1c2f..67516c633 100644 --- a/src/main/java/com/demcha/compose/engine/components/layout/Anchor.java +++ b/src/main/java/com/demcha/compose/engine/components/layout/Anchor.java @@ -1,29 +1,12 @@ package com.demcha.compose.engine.components.layout; import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.components.layout.coordinator.RenderingPosition; -import com.demcha.compose.engine.components.style.Margin; -import lombok.extern.slf4j.Slf4j; - -import java.util.Objects; /** - * Per-entity placement anchor used during layout. - *

- * {@code Anchor} tells the layout system how an entity should be positioned - * inside its available area. Combined with {@code Position}, margin, padding, - * and parent inner size, it produces the entity's {@code ComputedPosition}. - *

- * - *

Builders attach anchors while describing the document; layout later - * consumes them to convert relative intent into absolute coordinates.

+ * Horizontal + vertical placement anchor — a plain value pairing an + * {@link HAnchor} and a {@link VAnchor}. Consumers read {@code h()} / {@code v()} + * and the static factories to describe how content aligns within its area. */ -@Slf4j public record Anchor(HAnchor h, VAnchor v) implements Component { public static Anchor topLeft() { return new Anchor(HAnchor.LEFT, VAnchor.TOP); @@ -68,87 +51,5 @@ public static Anchor bottomCenter() { public static Anchor defaultAnchor() { return new Anchor(HAnchor.DEFAULT, VAnchor.DEFAULT); } - - /** - * Converts a computed outer-box position into a rendering origin inside the - * entity's margin box. - */ - public static RenderingPosition renderingPosition(ComputedPosition computedPosition, Margin margin) { - Objects.requireNonNull(computedPosition, "computedPosition cannot be null"); - Objects.requireNonNull(margin, "margin cannot be null"); - - // computedPosition — это левый-нижний угол OUTER-box (учитывает anchor/align). - // Чтобы рисовать контент/рамку внутри margin, просто смещаемся внутрь. - double rx = computedPosition.x() + margin.left(); - double ry = computedPosition.y() + margin.bottom(); - - var renderingPosition = new RenderingPosition(rx, ry); - log.debug("Rendering position computed: {}", renderingPosition); - return renderingPosition; - } - - /** - * Computes a child's anchored position inside a parent inner box. - */ - public ComputedPosition getComputedPosition(Entity child, InnerBoxSize perrentInnerBoxSize) { - log.debug("Starting calculation of computed position for {} ", child); - var position = child.getComponent(Position.class).orElse(Position.zero()); - - var outerBoxSize = OuterBoxSize.from(child).get(); - var childMargin = child.getComponent(Margin.class).orElse(Margin.zero()); - - return getComputedPosition(position, childMargin, outerBoxSize, perrentInnerBoxSize); - } - - private ComputedPosition getComputedPosition(Position position, Margin childMargin, OuterBoxSize outerBoxSize, InnerBoxSize parentInnerBoxSize) { - Objects.requireNonNull(position, () -> { - log.error("Position cannot be null."); - throw new NullPointerException("Position cannot be null."); - }); - Objects.requireNonNull(outerBoxSize, () -> { - log.error("OuterBoxSize cannot be null."); - throw new NullPointerException("OuterBoxSize cannot be null."); - - }); - Objects.requireNonNull(parentInnerBoxSize, () -> { - log.error("ParentInnerBoxSize cannot be null."); - throw new NullPointerException("ParentInnerBoxSize cannot be null."); - - }); - double outerW = outerBoxSize.width(); - double outerH = outerBoxSize.height(); - - double areaW = parentInnerBoxSize.width(); - double areaH = parentInnerBoxSize.height(); - - double childBottomMargin = childMargin.bottom(); - double childLeftMargin = childMargin.left(); - - - return getComputedPosition(position, childBottomMargin, childLeftMargin, areaW, outerW, areaH, outerH); - - } - - private ComputedPosition getComputedPosition(Position position, double childBottomMargin, double childLeftMargin, double areaW, double outerW, double areaH, double outerH) { - double x = switch (this.h()) { - case LEFT -> position.x() + childLeftMargin; // x=0 at left - case CENTER -> (areaW - outerW) / 2.0 + position.x() + childLeftMargin; - case RIGHT -> areaW - outerW + position.x() + childLeftMargin; - case DEFAULT -> position.x(); - }; - - double y = switch (this.v()) { - case BOTTOM -> position.y() + childBottomMargin; // y=0 at bottom - case MIDDLE -> (areaH - outerH) / 2.0 + position.y()+ childBottomMargin; - case TOP -> areaH - outerH + position.y() + childBottomMargin; - case DEFAULT -> position.y(); - }; - - var computed = new ComputedPosition(x, y); - log.debug("Computed position with Anchor has been created: {}", computed); - return computed; - } - - } diff --git a/src/main/java/com/demcha/compose/engine/components/layout/Layer.java b/src/main/java/com/demcha/compose/engine/components/layout/Layer.java deleted file mode 100644 index 5399ff53f..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/Layer.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Component; - -/** - * Represents an object that exists on a specific layer. - * - *

In graphical systems, rendering engines, or UI frameworks, - * a layer is often used to determine the drawing order or - * stacking position of elements. Lower layer numbers are typically - * drawn first (behind others), while higher numbers appear on top.

- */ -public record Layer(int value) implements Component {} - - diff --git a/src/main/java/com/demcha/compose/engine/components/layout/ParentComponent.java b/src/main/java/com/demcha/compose/engine/components/layout/ParentComponent.java deleted file mode 100644 index 056f7be96..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/ParentComponent.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; - -import java.util.Objects; -import java.util.UUID; - -public record ParentComponent(UUID uuid) implements Component{ - public ParentComponent(Entity entity) { - this(Objects.requireNonNull(entity.getUuid(), "Entity ID cannot be null.") ); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/RenderCoordinate.java b/src/main/java/com/demcha/compose/engine/components/layout/RenderCoordinate.java deleted file mode 100644 index 7da482185..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/RenderCoordinate.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.render.RenderingSystemECS; - -import java.util.Optional; - -public interface RenderCoordinate { - Optional renderCoordinate(Entity entity, RenderingSystemECS renderingSystem); -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPosition.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPosition.java deleted file mode 100644 index 0f8ebf986..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPosition.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -/** - * Resolved position of an entity relative to its layout context. - *

- * {@code ComputedPosition} is produced by layout from builder-time metadata such - * as {@link Anchor}, {@code Position}, parent inner size, padding offsets, and - * canvas geometry. It is the bridge between declarative placement intent and the - * fully resolved coordinates later used to build {@link Placement}. - *

- */ -@Slf4j -public record ComputedPosition(double x, double y) implements Component { - /** - * Returns a zero coordinate. - */ - public static ComputedPosition zero() { - return new ComputedPosition(0, 0); - } - - /** - * Resolves a child's position inside a parent inner box and applies the - * parent's padding coordinate offset. - * - * @param child the child entity being placed - * @param parentInnerBoxSize the available inner box of the parent - * @param paddingParentCoordinate the drawing origin inside the parent's padding box - * @return the resolved position for the child - */ - public static ComputedPosition from(@NonNull Entity child, @NonNull InnerBoxSize parentInnerBoxSize, PaddingCoordinate paddingParentCoordinate) { - var anchorOptional = child.getComponent(Anchor.class); - var anchor = anchorOptional.orElseGet(() -> { - log.warn("No Anchor found for {}. Using default Anchor (top-left).", child); - Anchor defaultAnchor = Anchor.defaultAnchor(); - child.addComponent(defaultAnchor); - return defaultAnchor; - }); - ComputedPosition computedPosition = anchor.getComputedPosition(child, parentInnerBoxSize); - double x = computedPosition.x + paddingParentCoordinate.x(); - double y = computedPosition.y + paddingParentCoordinate.y(); - return new ComputedPosition(x, y); - } - - /** - * Resolves a child's position directly from a parent entity. - * - * @param child the child entity - * @param parent the parent entity supplying inner box and padding context - * @return the resolved position - * @throws java.util.NoSuchElementException if the parent has no {@link InnerBoxSize} - */ - public static ComputedPosition from(@NonNull Entity child, @NonNull Entity parent) { - var parentInnerBox = InnerBoxSize.from(parent).orElseThrow(); - var paddingParentCoordinate = PaddingCoordinate.from(parent); - return from(child, parentInnerBox, paddingParentCoordinate); - } - - /** - * Resolves the position of a root-level entity against a document canvas. - * - * @param childEntity the entity being positioned - * @param canvas the document canvas - * @return the resolved position - */ - public static ComputedPosition from(@NonNull Entity childEntity, @NonNull Canvas canvas) { - log.debug("Computing position using default Canvas."); - var margin = canvas.margin() != null ? canvas.margin() : Margin.zero(); - InnerBoxSize innerBoxSize = new InnerBoxSize(canvas.width()- margin.horizontal(), canvas.height() - margin.vertical()); - var paddingParentCoordinate = new PaddingCoordinate(margin.left(), margin.bottom()); - return from(childEntity, innerBoxSize, paddingParentCoordinate); - } - - /** - * Converts this resolved position into a padding-aware coordinate origin. - * - * @param padding the padding to apply - * @return a coordinate shifted inside the padding box - */ - public PaddingCoordinate paddingCoordinate(@NonNull Padding padding) { - - var x = this.x + padding.left(); - var y = this.y + padding.bottom(); - - log.debug("ComputedPosition is {}", this.toString()); - log.debug("Padding coordinate is {}", padding); - - return new PaddingCoordinate(x, y); - } -} - diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/PaddingCoordinate.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/PaddingCoordinate.java deleted file mode 100644 index 051649363..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/PaddingCoordinate.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.style.Padding; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -/** - * Represents a coordinate adjusted by padding relative to a computed position (x, y). - *

- * Typical usage: - *

    - *
  • Apply the parent's padding to a child's computed position (most common in layout engines).
  • - *
  • Apply an entity's own padding to its computed position (less common, but supported).
  • - *
- */ -@Slf4j -public record PaddingCoordinate(double x, double y) { - - /** - * Creates a {@link PaddingCoordinate} by using the entity's already-present {@link ComputedPosition} - * and applying the entity's own {@link Padding}. - *

- * ⚠️ This assumes the entity already has a {@link ComputedPosition} stored. - * If not, compute one first (e.g., {@link ComputedPosition#from(Entity, Entity)}). - * - * @param entity the entity that already stores its {@link ComputedPosition} - * @return padding-adjusted coordinate based on the entity's own padding - * @throws IllegalStateException if {@code entity} has no {@link ComputedPosition} - */ - public static PaddingCoordinate from(@NonNull Entity entity) { - - ComputedPosition position = entity.getComponent(ComputedPosition.class).orElseThrow(); - - var padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - var result = from(position, padding); - log.debug("PaddingCoordinate.from(entity={}) -> {}", entity, result); - return result; - } - - /** - * Low-level factory: apply the given {@link Padding} to a provided {@link ComputedPosition}. - *

- * This is useful when you have already computed the position, and you explicitly - * know which parrentPadding (parent's or entity's) you want to apply. - * - * @param position the base position - * @param parrentPadding the parrentPadding to apply - * @return parrentPadding-adjusted coordinate - */ - public static PaddingCoordinate from(@NonNull ComputedPosition position, @NonNull Padding parrentPadding) { - double px = position.x() + parrentPadding.left() ; - double py = position.y() + parrentPadding.bottom() ; - var result = new PaddingCoordinate(px, py); - log.debug("PaddingCoordinate.from(position={}, parrentPadding={}) -> {}", position, parrentPadding, result); - return result; - } - public static PaddingCoordinate zero(){ - return new PaddingCoordinate(0, 0); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/Placement.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/Placement.java deleted file mode 100644 index f72468d91..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/Placement.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.render.RenderingSystemECS; -import lombok.extern.slf4j.Slf4j; - -import java.awt.*; -import java.util.Objects; -import java.util.Optional; - -/** - * Final layout bounding box for an entity. - *

- * {@code Placement} is the layout-phase component closest to what renderers - * consume. It combines resolved position, outer size, and page span information - * into a single record that tells the rendering layer where the entity lives in - * document space. - *

- * - * @param x final horizontal position - * @param y final vertical position - * @param width final outer width - * @param height final outer height - * @param startPage first page containing this entity - * @param endPage last page containing this entity - */ -@Slf4j -public record Placement(double x, double y, double width, double height, int startPage, - int endPage) implements Component { - - - /** - * Creates a single-page placement from an already resolved outer size and position. - */ - private static Placement from(OuterBoxSize outerBoxSize, Position positionWithMargins, int pageNumber) { - Objects.requireNonNull(outerBoxSize); - Objects.requireNonNull(positionWithMargins); - return new Placement(positionWithMargins.x(), positionWithMargins.y(), outerBoxSize.width(), outerBoxSize.height(), pageNumber, pageNumber); - } - - public static Placement from(Entity entity, InnerBoxSize parrentInnerBoxSize, PaddingCoordinate paddingCoordinate, int pageNumber) { - var computedPosition = ComputedPosition.from(entity, parrentInnerBoxSize, paddingCoordinate); - var outBoxSize = OuterBoxSize.from(entity).orElseThrow(); - var padding = entity.getComponent(Padding.class).get(); - return new Placement(computedPosition.x(), computedPosition.y(), outBoxSize.width(), outBoxSize.height(), pageNumber, pageNumber); - - - } - - public static Placement fromWithDefault(Entity entity, InnerBoxSize parrentInnerBoxSize, PaddingCoordinate paddingCoordinate, int pageNumber) { - var position = entity.getComponent(Position.class).orElse(Position.zero()); - entity.addComponent(position); - var margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - entity.addComponent(margin); - var anchor = entity.getComponent(Anchor.class).orElse(Anchor.topLeft()); - entity.addComponent(anchor); - return from(entity, parrentInnerBoxSize, paddingCoordinate, pageNumber); - } - - public static Placement from(Entity child, Entity parent, int pageNumber) { - var parrentInnerBoxSize = InnerBoxSize.from(parent).orElseThrow(); - var paddingCoordinate = PaddingCoordinate.from(parent); - - return from(child, parrentInnerBoxSize, paddingCoordinate, pageNumber); - } - - - /** - * Converts this placement into guideline rendering coordinates for debugging. - */ - public Optional renderCoordinate(Entity entity, RenderingSystemECS renderingSystem) { - double x; - double y; - double width; - double height; - int startPage; - int endPage; - - startPage = startPage(); - endPage = endPage(); - - x = x(); - y = y(); - width = width(); - height = height(); - Color color = renderingSystem.guidLineSettings().BOX_COLOR(); - Stroke stroke = renderingSystem.guidLineSettings().BOX_STROKE(); - return Optional.of(new RenderCoordinateContext(x, y, width, height, startPage, endPage, stroke, color)); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/Position.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/Position.java deleted file mode 100644 index 45037b47e..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/Position.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.core.Component; - -/** - * Represents an object that has a two-dimensional position. - * - *

Coordinates are typically defined in a Cartesian coordinate system, - * where X represents the horizontal axis and Y represents - * the vertical axis.

- * - *
    - *
  • X – Horizontal position (increasing values move right).
  • - *
  • Y – Vertical position (increasing values move down, unless using - * a mathematical coordinate system where values increase upward).
  • - *
- */ -public record Position(double x, double y) implements Component { - public static Position zero(){ - return new Position(0, 0); - } -} - diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/RenderCoordinateContext.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/RenderCoordinateContext.java deleted file mode 100644 index 7f36a6155..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/RenderCoordinateContext.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.core.Entity; - -import java.awt.*; - -public record RenderCoordinateContext(double x, double y, double width, double height, int startPage, int endPage, Stroke stroke, Color color){ -public static RenderCoordinateContext createBox (Entity entity, Stroke stroke, Color color) { - Placement placement = entity.getComponent(Placement.class).orElseThrow(); - return new RenderCoordinateContext(placement.x(), placement.y(), placement.width(), placement.height(), placement.startPage(), placement.endPage(), stroke, color); -} -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/RenderingPosition.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/RenderingPosition.java deleted file mode 100644 index 6ba4ab018..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/RenderingPosition.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.style.Margin; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -import java.util.Optional; - -/** - * Position for rendering BoxElement - * - * @param x - * @param y - */ -@Slf4j -public record RenderingPosition(double x, double y) { - - public static RenderingPosition from(@NonNull ComputedPosition computed, @NonNull Margin margin) { - return new RenderingPosition(computed.x() + margin.left(), computed.y() + margin.bottom()); - } - - public static Optional from(Entity entity) { - log.debug("Computation a Rendering position for {}", entity); - var computedPosition = entity.getComponent(ComputedPosition.class); - if (computedPosition.isEmpty()) { - log.warn("TextComponent has no ComputedPosition; skipping: {}", entity); - return Optional.empty(); - } - var margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - RenderingPosition from = from(computedPosition.get(), margin); - log.debug("Rendering position {}", from); - return Optional.of(from); - } - - @Override - public String toString() { - return "RenderingPosition[" + "x: " + x() + ", " + "y: " + y() + "]"; - - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/package-info.java b/src/main/java/com/demcha/compose/engine/components/layout/coordinator/package-info.java deleted file mode 100644 index 1e822a6d6..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/coordinator/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Layout components that describe anchors, parent links, layers, transforms, and resolved coordinates. - * - *

Ownership: Owned by the shared engine layout pipeline.

- *

Extension rules: Extend with backend-neutral placement metadata only; renderers should consume this state, not define it.

- */ -package com.demcha.compose.engine.components.layout.coordinator; \ No newline at end of file diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/BlockText.java b/src/main/java/com/demcha/compose/engine/components/renderable/BlockText.java deleted file mode 100644 index b077a6475..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/BlockText.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.pagination.Breakable; -import com.demcha.compose.engine.text.TextControlSanitizer; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -import java.util.Optional; - -/** - * Render marker for breakable multi-line text blocks. - */ -@Slf4j -@Builder -@EqualsAndHashCode -@NoArgsConstructor -public class BlockText implements Render, Breakable { - - public static Optional validatedTextData(@NonNull Entity entity) { - var blockTextDataOpt = entity.getComponent(BlockTextData.class); - var styleOpt = entity.getComponent(TextStyle.class); - - if (blockTextDataOpt.isEmpty()) { - log.info("TextComponent has no BlockTextData.class; skipping: {}", entity); - return Optional.empty(); - } - - TextStyle style = styleOpt.orElse(TextStyle.DEFAULT_STYLE); - if (styleOpt.isEmpty()) { - log.info("TextComponent has no TextStyle; skipping: {}", entity); - } - - return Optional.of(new ValidatedTextData(style, blockTextDataOpt.get())); - } - - public static Optional validatedPlacement(Entity entity) { - if (!entity.hasAssignable(BlockTextData.class)) { - log.debug("Entity doesn't have TextComponent; skipping: {}", entity); - return Optional.empty(); - } - - var placementOpt = entity.getComponent(Placement.class); - if (placementOpt.isEmpty()) { - log.warn("TextComponent has no Placement.class Component, skipping: {}", entity); - return Optional.empty(); - } - return placementOpt; - } - - public static String sanitizeText(String rawText) { - return TextControlSanitizer.remove(rawText); - } - - public record ValidatedTextData(TextStyle style, BlockTextData textValue) { - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/TextComponent.java b/src/main/java/com/demcha/compose/engine/components/renderable/TextComponent.java deleted file mode 100644 index adeac8194..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/TextComponent.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.components.content.text.Text; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; - -@Slf4j -@Builder -@EqualsAndHashCode -@NoArgsConstructor -public class TextComponent implements Render { - - public static ContentSize autoMeasureText(Entity entity, EntityManager entityManager) throws IOException { - Text text = entity.getComponent(Text.class).orElseThrow(); - TextStyle style = entity.getComponent(TextStyle.class).orElseThrow(); - TextMeasurementSystem measurementSystem = entityManager.getSystems() - .textMeasurement() - .orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to auto-measure text.")); - ContentSize measured = measurementSystem.measure(style, text.value()); - double width = measured.width(); - double height = measured.height(); - log.debug("Auto-measured single-line text entity {} -> width={} height={} style={}", - entity, width, height, style); - return new ContentSize(width, height); - } - - public static ValidatedTextData validatedTextData(Entity e) { - var textValueOpt = e.getComponent(Text.class); - var styleOpt = e.getComponent(TextStyle.class); - - Text textValue; - TextStyle style; - if (textValueOpt.isEmpty()) { - log.info("TextComponent has no TextValue; skipping: {}", e); - textValue = textValueOpt.orElse(Text.empty()); - } else { - textValue = textValueOpt.get(); - } - if (styleOpt.isEmpty()) { - log.info("TextComponent has no TextStyle; skipping: {}", e); - style = styleOpt.orElse(TextStyle.DEFAULT_STYLE); - } else { - style = styleOpt.get(); - } - - return new ValidatedTextData(style, textValue); - } - - public record ValidatedTextData(TextStyle style, Text textValue) { - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/package-info.java b/src/main/java/com/demcha/compose/engine/components/renderable/package-info.java deleted file mode 100644 index aeee07fa5..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Backend-neutral render marker components for low-level engine entities. - * - *

Ownership: Owned by the shared engine render seam.

- *

Extension rules: Add markers only with matching render-handler dispatch and tests for unsupported backend behavior.

- */ -package com.demcha.compose.engine.components.renderable; \ No newline at end of file diff --git a/src/main/java/com/demcha/compose/engine/components/style/Margin.java b/src/main/java/com/demcha/compose/engine/components/style/Margin.java index 18d6ef6db..49e6e2cbb 100644 --- a/src/main/java/com/demcha/compose/engine/components/style/Margin.java +++ b/src/main/java/com/demcha/compose/engine/components/style/Margin.java @@ -1,20 +1,9 @@ package com.demcha.compose.engine.components.style; -import com.demcha.compose.engine.components.content.shape.Stroke; import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.RenderCoordinate; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.render.RenderingSystemECS; -import lombok.extern.slf4j.Slf4j; -import java.awt.*; -import java.util.Optional; - -@Slf4j -public record Margin(double top, double right, double bottom, double left) implements Component, RenderCoordinate { +public record Margin(double top, double right, double bottom, double left) implements Component { public static Margin of(double value) { return new Margin(value, value, value, value); @@ -40,33 +29,6 @@ public static Margin left(double value) { return new Margin(0, 0, 0, value); } - public Optional renderCoordinate(Entity entity, RenderingSystemECS renderingSystem) { - if (this.equals(Margin.zero())) { - log.info("Margin is zero, return empty"); - return Optional.empty(); - } - double x; - double y; - double width; - double height; - int startPage; - int endPage; - - var placement = entity.getComponent(Placement.class).orElseThrow(); - startPage = placement.startPage(); - endPage = placement.endPage(); - width = placement.width() + horizontal(); - height = placement.height() + vertical(); - x = placement.x() - left(); - y = placement.y() - bottom(); - Color color = renderingSystem.guidLineSettings().MARGIN_COLOR(); - Stroke stroke = renderingSystem.guidLineSettings().MARGIN_STROKE(); - - - return Optional.of(new RenderCoordinateContext(x, y, width, height, startPage, endPage, stroke, color)); - - } - public double horizontal() { return left + right; } diff --git a/src/main/java/com/demcha/compose/engine/components/style/Padding.java b/src/main/java/com/demcha/compose/engine/components/style/Padding.java index 8d526d802..7062c6284 100644 --- a/src/main/java/com/demcha/compose/engine/components/style/Padding.java +++ b/src/main/java/com/demcha/compose/engine/components/style/Padding.java @@ -1,21 +1,11 @@ package com.demcha.compose.engine.components.style; -import com.demcha.compose.engine.components.content.shape.Stroke; import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.RenderCoordinate; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.render.RenderingSystemECS; import lombok.extern.slf4j.Slf4j; -import java.awt.*; -import java.util.Optional; - @Slf4j -public record Padding(double top, double right, double bottom, double left) implements Component, RenderCoordinate { +public record Padding(double top, double right, double bottom, double left) implements Component { public static Padding zero() { log.debug("Getting zero padding"); return new Padding(0.0, 0.0, 0.0, 0.0); @@ -49,31 +39,4 @@ public int hashCode() { result = 31 * result + Double.hashCode(left); return result; } - - @Override - public Optional renderCoordinate(Entity entity, RenderingSystemECS renderingSystem) { - if (this.equals(zero())) { - log.info("Padding is zero, return empty"); - return Optional.empty(); - } - var inner = InnerBoxSize.from(entity).orElseThrow(); - var placement = entity.getComponent(Placement.class).orElseThrow(); - double x; - double y; - double width; - double height; - int startPage; - int endPage; - - startPage = placement.startPage(); - endPage = placement.endPage(); - - x = placement.x() + left(); - y = placement.y()+ bottom(); - width = inner.width(); - height = inner.height(); - Color color = renderingSystem.guidLineSettings().PADDING_COLOR(); - Stroke stroke = renderingSystem.guidLineSettings().PADDING_STROKE(); - return Optional.of(new RenderCoordinateContext(x, y, width, height, startPage, endPage,stroke, color)); - } } diff --git a/src/main/java/com/demcha/compose/engine/core/Canvas.java b/src/main/java/com/demcha/compose/engine/core/Canvas.java deleted file mode 100644 index b8b58cbfa..000000000 --- a/src/main/java/com/demcha/compose/engine/core/Canvas.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.engine.components.style.Margin; - -public interface Canvas { - float width(); - - float x(); - - float y(); - - float height(); - - Margin margin(); - - default float boundingTopLine() { - return height() - (float) margin().top(); - } - - default float boundingBottomLine() { - return (float) margin().bottom(); - } - - default float boundingRightLine() { - return width() - (float) margin().right(); - } - - default float boundingLeftLine() { - return (float) margin().left(); - } - - default double innerHeigh() { - return height() - margin().vertical(); - } - - default double innerWidth() { - return width() - margin().horizontal(); - } - - - void addMargin(Margin margin); - -} diff --git a/src/main/java/com/demcha/compose/engine/core/EntityManager.java b/src/main/java/com/demcha/compose/engine/core/EntityManager.java deleted file mode 100644 index 8fb2345fd..000000000 --- a/src/main/java/com/demcha/compose/engine/core/EntityManager.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.font.FontLibrary; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.core.SystemRegistry; -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.core.SystemECS; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; -import java.util.UUID; -import java.util.stream.Collectors; - -/** - * Central registry for entities, systems, and document-scoped engine state. - *

- * {@code EntityManager} is the runtime hub of GraphCompose. Builders register - * entities here, systems query and mutate those entities during layout and - * rendering, and composer implementations use the registry to orchestrate the - * full document pipeline. - *

- * - *

Besides storing entities, the manager also owns system registration, the - * active font library, layer/depth metadata used by layout, and document-wide - * flags such as markdown and guide-line rendering.

- * - *

Typical role in the pipeline

- *
    - *
  1. builders create entities and register them here
  2. - *
  3. layout systems compute sizes, positions, and pagination metadata
  4. - *
  5. rendering systems read renderable entities and draw the final output
  6. - *
- * - * @author Artem Demchyshyn - * @since 1.0.0 - */ -@Slf4j -@Getter -@Setter -public class EntityManager { - private final Map entities; - private final SystemRegistry systems; - private final FontLibrary fonts; - private Map> layers; - private Map depthById; - private long mutationVersion; - private boolean guideLines; - private boolean markdown = false; - - public EntityManager() { - this(false); - } - - public EntityManager(@NonNull List systems) { - this(systems, new FontLibrary(), true); - } - - public EntityManager(@NonNull List systems, FontLibrary fonts, boolean markdown) { - log.debug("Creating new EntityManager"); - this.fonts = fonts; - this.markdown = markdown; - this.systems = new SystemRegistry(); - this.systems.addAllSystems(systems); - this.entities = new LinkedHashMap<>(); - this.layers = new TreeMap<>(); - this.depthById = new LinkedHashMap<>(); - } - - public EntityManager(boolean markdown) { - this(new ArrayList<>(), new FontLibrary(), markdown); - } - - public EntityManager(FontLibrary fonts, boolean markdown) { - this(new ArrayList<>(), fonts, markdown); - } - - public Entity createEntity() { - return createEntity(null); - } - - /** - * Creates and registers a new entity, optionally assigning an {@link EntityName}. - * - * @param name optional logical name used for diagnostics and debugging - * @return the newly created entity - */ - public Entity createEntity(String name) { - var entity = new Entity(); - - putEntity(entity); - - if (name != null && !name.isBlank()) { - entity.addComponent(new EntityName(name)); - log.trace("Created {}", entity); - } else { - log.trace("Created entity with no EntityName {}", entity.getUuid()); - } - return entity; - } - - public Optional getEntity(UUID id) { - log.trace("Getting Entity id {}", id); - return Optional.ofNullable(entities.get(id)); - } - - public void setGuideLines(boolean guideLines) { - this.guideLines = guideLines; - touchMutation(); - entities.forEach((id, entity) -> { - entity.setGuideLines(guideLines); - }); - } - - public void setMarkdown(boolean markdown) { - this.markdown = markdown; - touchMutation(); - } - - public Entity putEntity(Entity entity) { - UUID uuid = entity.getUuid(); - log.trace("Putting Entity id {}", uuid); - - Optional existing = getEntity(uuid); - - if (existing.isPresent()) { - if (entity.equals(existing)) { - log.trace("Entity already exists and is identical"); - return existing.orElse(null); - } else { - log.warn("Entity conflict detected for id {}. Replacing old entity.", uuid); - touchMutation(); - return entities.put(uuid, entity); - } - } - entity.setGuideLines(guideLines); - touchMutation(); - return entities.put(uuid, entity); - } - - public String displayName(UUID id) { - var entity = getEntity(id); - - log.info("Displaying entity with id {}", id); - String name = this.entities.get(id).name(); - return name == null ? "Entity#" + id.toString().substring(0, 8) : name; - } - - public void addComponent(UUID entityId, Component component) { - Entity entity = entities.get(entityId); - entity.addComponent(component); - } - - public Optional getComponent(UUID entityId, Class componentType) { - Optional entity = getEntity(entityId); - return entity.flatMap(value -> value.getComponent(componentType)); - } - - public Optional> getEntitiesWithComponent(Class componentType) { - log.debug("Getting component with id {}", componentType.getName()); - Set entityIds = null; - for (Map.Entry entry : entities.entrySet()) { - var entityId = entry.getKey(); - log.debug("Checking entity with id {}", entry.getKey()); - var components = entry.getValue(); - if (components.has(componentType)) { - log.debug("Found entity id [{}] with component {}", entityId, componentType.getName()); - if (entityIds == null) { - entityIds = new LinkedHashSet<>(); - } - log.debug("Adding entity [{}], to list with component {}", entityId, componentType.getName()); - entityIds.add(entityId); - } - } - if (entityIds == null || entityIds.isEmpty()) { - log.warn("No component with id {} found", componentType.getName()); - } - log.debug("Found [{}] entities with component {}", entityIds == null ? 0 : entityIds.size(), - componentType.getName()); - return Optional.ofNullable(entityIds); - } - - public Set getEntitiesWithAnyRender() { - Set result = new LinkedHashSet<>(); - for (Map.Entry e : entities.entrySet()) { - boolean hasRenderable = e.getValue().view().values().stream() - .anyMatch(comp -> comp instanceof Render); - if (hasRenderable) { - result.add(e.getKey()); - } - } - return result; - } - - public boolean remove(UUID entityId) { - boolean removed = entities.remove(entityId) != null; - if (removed) { - touchMutation(); - } - return removed; - } - - public boolean remove(Entity entity) { - boolean removed = entities.remove(entity.getUuid()) != null; - if (removed) { - touchMutation(); - } - return removed; - } - - public Set getEntitiesWithRender(Class componentType) { - log.debug("Searching for entities with component type {}", componentType.getName()); - - Set result = new LinkedHashSet<>(); - for (Map.Entry e : entities.entrySet()) { - UUID entityId = e.getKey(); - Entity entity = e.getValue(); - - // перебираем компоненты у сущности - for (Component comp : entity.view().values()) { - // подходит ли по типу? - if (componentType.isInstance(comp)) { - result.add(entityId); - log.debug("Matched entity {} with {}", entityId, componentType.getSimpleName()); - break; // уже добавили — дальше компоненты этой сущности можно не проверять - } - } - } - - if (result.isEmpty()) { - log.warn("No entities found with {}", componentType.getName()); - } else { - log.debug("Found {} entities with {}", result.size(), componentType.getName()); - } - return result; - } - - /** - * Retrive Entities from UUUIDs Set - * - * @param uuids Set will be transformed - * @return Set Entities - */ - public Set getSetEntitiesFromUuids(Set uuids) { - return uuids - .stream() - .map(this::getEntity) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - } - - /** - * SystemECS - */ - public void processSystems() { - log.info("Processing Systems"); - for (SystemECS system : systems.systems().values()) { - log.info("Processing SystemECS {}", system); - system.process(this); // Передаём себя, чтобы система могла получить доступ к компонентам - } - log.info("Processed Systems"); - } - - public void printEntities() { - this.entities.forEach((uuid, entity) -> { - java.lang.System.out.println(entity.toString()); - entity.view().forEach((k, v) -> java.lang.System.out.printf("UUID[%s] : %s \n", uuid, entity.toString())); - }); - } - - public void printEntitiesWithInfo() { - this.entities.forEach((uuid, entity) -> { - java.lang.System.out.println(entity.toString()); - entity.view().forEach((k, v) -> java.lang.System.out.printf("UUID[%s] : %s \n", uuid, entity.printInfo())); - }); - } - - private void touchMutation() { - mutationVersion++; - } - -} diff --git a/src/main/java/com/demcha/compose/engine/core/LayoutTraversalContext.java b/src/main/java/com/demcha/compose/engine/core/LayoutTraversalContext.java deleted file mode 100644 index b46a5c4c2..000000000 --- a/src/main/java/com/demcha/compose/engine/core/LayoutTraversalContext.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.ParentComponent; -import lombok.extern.slf4j.Slf4j; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.UUID; - -/** - * Immutable, deterministic view of the current entity hierarchy for one layout pass. - * - *

This helper keeps traversal-specific structure out of {@link EntityManager} so - * the manager can remain the registry of entities and systems, while layout and - * pagination reuse one canonical hierarchy snapshot.

- * - *

The context is built from the existing entity graph using two inputs:

- *
    - *
  • {@link ParentComponent} as the authoritative parent reference
  • - *
  • {@link Entity#getChildren()} as the canonical sibling order when it is consistent
  • - *
- * - *

If those two sources disagree, the context appends the missing child in a - * deterministic fallback position and logs a warning so the inconsistency stays - * visible during maintenance and backend work.

- */ -@Slf4j -public final class LayoutTraversalContext { - private final Map parentById; - private final Map> childrenByParent; - private final Set roots; - - private LayoutTraversalContext(Map parentById, - Map> childrenByParent, - Set roots) { - this.parentById = parentById; - this.childrenByParent = childrenByParent; - this.roots = roots; - } - - /** - * Clears layout-owned traversal state on the manager before a new layout pass. - * - *

The resolved hierarchy is recomputed every pass, so stale layers and depth - * metadata must not survive between runs on the same composer.

- */ - public static void resetTraversalState(EntityManager entityManager) { - entityManager.setLayers(new TreeMap<>()); - entityManager.setDepthById(new LinkedHashMap<>()); - } - - /** - * Builds a deterministic hierarchy snapshot from the current entity graph. - */ - public static LayoutTraversalContext from(EntityManager entityManager) { - Map entities = entityManager.getEntities(); - Map parentById = buildParentById(entities); - Map> childrenByParent = buildChildrenByParent(entities, parentById); - Set roots = buildRoots(entities, parentById); - - return new LayoutTraversalContext( - Collections.unmodifiableMap(parentById), - Collections.unmodifiableMap(childrenByParent), - Collections.unmodifiableSet(roots)); - } - - /** - * Returns an unmodifiable map from each child entity ID to its parent entity ID. - * - *

Only entities that carry a {@link ParentComponent} appear as keys.

- * - * @return child → parent mapping, never {@code null} - */ - public Map parentById() { - return parentById; - } - - /** - * Returns an unmodifiable map from each parent entity ID to its ordered list of - * child entity IDs. - * - *

The child order follows {@link Entity#getChildren()} when consistent with - * {@link ParentComponent}. Inconsistencies are resolved deterministically and - * logged as warnings.

- * - * @return parent → ordered children mapping, never {@code null} - */ - public Map> childrenByParent() { - return childrenByParent; - } - - /** - * Returns the set of root entity IDs — entities that have no resolved parent. - * - *

If no roots can be computed (e.g. due to a cycle), the set falls back to all - * known entities so the cycle becomes visible at traversal time.

- * - * @return root entity IDs in insertion order, never {@code null} or empty - */ - public Set roots() { - return roots; - } - - /** - * Walks all entities and indexes child → parent relationships from - * {@link ParentComponent}. Insertion order mirrors the entity map. - */ - private static Map buildParentById(Map entities) { - Map parentById = new LinkedHashMap<>(); - - for (Map.Entry entry : entities.entrySet()) { - entry.getValue() - .getComponent(ParentComponent.class) - .ifPresent(parentComponent -> parentById.put(entry.getKey(), parentComponent.uuid())); - } - - return parentById; - } - - /** - * Builds the ordered children map by reconciling {@link Entity#getChildren()} - * with {@link ParentComponent}. Stale or inconsistent references are logged and - * handled deterministically. - */ - private static Map> buildChildrenByParent(Map entities, - Map parentById) { - Map> childrenByParent = new LinkedHashMap<>(); - Set assignedChildren = new LinkedHashSet<>(); - - for (Entity parent : entities.values()) { - List orderedChildren = new ArrayList<>(); - - for (UUID childId : parent.getChildren()) { - UUID actualParentId = parentById.get(childId); - if (actualParentId == null) { - log.warn("Traversal context skipped stale child reference {} from parent {} because the child has no ParentComponent.", - childId, parent.getUuid()); - continue; - } - if (!actualParentId.equals(parent.getUuid())) { - log.warn("Traversal context skipped stale child reference {} from parent {} because ParentComponent points to {}.", - childId, parent.getUuid(), actualParentId); - continue; - } - if (!entities.containsKey(childId)) { - log.warn("Traversal context skipped missing child {} referenced by parent {}.", childId, parent.getUuid()); - continue; - } - orderedChildren.add(childId); - assignedChildren.add(childId); - } - - if (!orderedChildren.isEmpty()) { - childrenByParent.put(parent.getUuid(), List.copyOf(orderedChildren)); - } - } - - for (Map.Entry entry : parentById.entrySet()) { - UUID childId = entry.getKey(); - UUID parentId = entry.getValue(); - - if (assignedChildren.contains(childId)) { - continue; - } - if (!entities.containsKey(childId) || !entities.containsKey(parentId)) { - continue; - } - - log.warn("Traversal context appended child {} to parent {} because ParentComponent exists but the parent's children list is missing it.", - childId, parentId); - childrenByParent - .computeIfAbsent(parentId, key -> new ArrayList<>()) - .add(childId); - } - - Map> immutableChildren = new LinkedHashMap<>(); - for (Map.Entry> entry : childrenByParent.entrySet()) { - immutableChildren.put(entry.getKey(), List.copyOf(entry.getValue())); - } - return immutableChildren; - } - - /** - * Identifies root entities — those with no parent or whose parent is missing - * from the entity set. Falls back to all entities if no roots are found. - */ - private static Set buildRoots(Map entities, Map parentById) { - Set roots = new LinkedHashSet<>(); - - for (UUID entityId : entities.keySet()) { - UUID parentId = parentById.get(entityId); - if (parentId == null) { - roots.add(entityId); - continue; - } - if (!entities.containsKey(parentId)) { - roots.add(entityId); - log.warn("Traversal context treats entity {} as root because parent {} is missing.", entityId, parentId); - } - } - - if (roots.isEmpty()) { - log.warn("Traversal context computed no roots; falling back to all entities to expose the cycle at traversal time."); - roots.addAll(entities.keySet()); - } - - return new LinkedHashSet<>(roots); - } -} diff --git a/src/main/java/com/demcha/compose/engine/core/SystemECS.java b/src/main/java/com/demcha/compose/engine/core/SystemECS.java deleted file mode 100644 index 9cec4fa55..000000000 --- a/src/main/java/com/demcha/compose/engine/core/SystemECS.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.engine.core.EntityManager; - -public interface SystemECS { - void process(EntityManager entityManager); - -} diff --git a/src/main/java/com/demcha/compose/engine/core/SystemRegistry.java b/src/main/java/com/demcha/compose/engine/core/SystemRegistry.java deleted file mode 100644 index f2e473be5..000000000 --- a/src/main/java/com/demcha/compose/engine/core/SystemRegistry.java +++ /dev/null @@ -1,114 +0,0 @@ -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; - -import java.util.*; -import java.util.stream.Stream; - -/** - * A registry for managing and retrieving Entity Component System (ECS) systems. - * Provides methods to add, retrieve, and stream registered systems. - */ -@Slf4j -@Getter -@Accessors(fluent = true) -public class SystemRegistry { - - /** - * A map storing registered systems, keyed by their class type. - * Uses a LinkedHashMap to maintain the order of insertion. - */ - private final Map, 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 textMeasurement() { - return Optional.ofNullable(textMeasurement); - } - - /** - * Adds a single system to the registry. - * - * @param system the system to add - * @param the type of the system, extending SystemECS - */ - public void addSystem(T system) { - log.info("Adding SystemECS {}", system.getClass().getName()); - systems.put(system.getClass(), system); - } - - /** - * Returns a stream of all registered systems. - * - * @return a stream of SystemECS instances - */ - public Stream getStream() { - return systems.values().stream(); - } - - /** - * Retrieves a system from the registry by its class type. - * If an exact match is not found, it searches for a subclass or implementation. - * - * @param systemClass the class of the system to retrieve - * @param the expected type of the system - * @return an Optional containing the system if found, or an empty Optional otherwise - */ - public Optional getSystem(Class systemClass) { - SystemECS system = systems.get(systemClass); - if (systemClass.isInstance(system)) { - return Optional.of(systemClass.cast(system)); - } - for (SystemECS candidate : systems.values()) { - if (systemClass.isInstance(candidate)) { - return Optional.of(systemClass.cast(candidate)); - } - } - return Optional.empty(); - } - - /** - * Adds a list of systems to the registry. - * - * @param systems the list of systems to add - * @param the type of the systems, extending SystemECS - */ - public void addAllSystems(List systems) { - systems.forEach(this::addSystem); - } - -} diff --git a/src/main/java/com/demcha/compose/engine/core/package-info.java b/src/main/java/com/demcha/compose/engine/core/package-info.java deleted file mode 100644 index 8cdf0263f..000000000 --- a/src/main/java/com/demcha/compose/engine/core/package-info.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Core primitives of the legacy ECS engine — entity/component - * graphs and shared traversal state for the original {@code Entity}-based - * layout / pagination / render pipeline. - * - *

This is not the engine behind the public API. The canonical - * pipeline ({@code GraphCompose.document() -> DocumentSession -> LayoutCompiler - * -> LayoutGraph -> PdfFixedLayoutBackend}) in {@code com.demcha.compose.document.*} - * imports nothing from this package directly, and the former - * {@code GraphCompose.pdf(...)} surface that drove the ECS has been removed. The - * ECS execution engine — the {@code EntityManager.processSystems()} loop - * and the dispatch contracts it drives — is dead code: nothing invokes it, in - * production or in tests.

- * - *

The genuinely shared engine packages are elsewhere and are not - * deprecated: {@code engine.components} (value types), {@code engine.measurement} - * (text-measurement contracts), {@code engine.font}, and - * {@code engine.render} (backend-neutral render-pass contracts) are all used by - * the canonical pipeline.

- * - * @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 survives only because - * the live {@code Entity} object model and the shared render / pagination / - * debug helpers still compile against these primitives. It goes away when the - * internal {@code Entity} model is retired (see - * {@code docs/roadmaps/post-2.0-engineering.md}). Do not extend it or spend - * optimization effort here. - */ -@Deprecated -package com.demcha.compose.engine.core; diff --git a/src/main/java/com/demcha/compose/engine/debug/LayoutInsetsSnapshot.java b/src/main/java/com/demcha/compose/engine/debug/LayoutInsetsSnapshot.java index ea87d913b..003d796c4 100644 --- a/src/main/java/com/demcha/compose/engine/debug/LayoutInsetsSnapshot.java +++ b/src/main/java/com/demcha/compose/engine/debug/LayoutInsetsSnapshot.java @@ -3,12 +3,14 @@ import com.demcha.compose.engine.components.style.Margin; import com.demcha.compose.engine.components.style.Padding; +import java.math.BigDecimal; +import java.math.RoundingMode; + /** * Snapshot-friendly inset model used for margins and padding. * - *

The values are normalized through - * {@link LayoutSnapshotExtractor#normalize(double)} so JSON baselines remain - * stable across insignificant floating-point noise.

+ *

The values are normalized through {@link #normalize(double)} so JSON + * baselines remain stable across insignificant floating-point noise.

* * @param top top inset * @param right right inset @@ -26,10 +28,10 @@ public record LayoutInsetsSnapshot(double top, double right, double bottom, doub public static LayoutInsetsSnapshot from(Margin margin) { Margin safeMargin = margin == null ? Margin.zero() : margin; return new LayoutInsetsSnapshot( - LayoutSnapshotExtractor.normalize(safeMargin.top()), - LayoutSnapshotExtractor.normalize(safeMargin.right()), - LayoutSnapshotExtractor.normalize(safeMargin.bottom()), - LayoutSnapshotExtractor.normalize(safeMargin.left())); + normalize(safeMargin.top()), + normalize(safeMargin.right()), + normalize(safeMargin.bottom()), + normalize(safeMargin.left())); } /** @@ -41,9 +43,18 @@ public static LayoutInsetsSnapshot from(Margin margin) { public static LayoutInsetsSnapshot from(Padding padding) { Padding safePadding = padding == null ? Padding.zero() : padding; return new LayoutInsetsSnapshot( - LayoutSnapshotExtractor.normalize(safePadding.top()), - LayoutSnapshotExtractor.normalize(safePadding.right()), - LayoutSnapshotExtractor.normalize(safePadding.bottom()), - LayoutSnapshotExtractor.normalize(safePadding.left())); + normalize(safePadding.top()), + normalize(safePadding.right()), + normalize(safePadding.bottom()), + normalize(safePadding.left())); + } + + private static double normalize(double value) { + if (Math.abs(value) < 0.0005d) { + return 0.0d; + } + return BigDecimal.valueOf(value) + .setScale(3, RoundingMode.HALF_UP) + .doubleValue(); } } diff --git a/src/main/java/com/demcha/compose/engine/debug/LayoutSnapshotExtractor.java b/src/main/java/com/demcha/compose/engine/debug/LayoutSnapshotExtractor.java deleted file mode 100644 index 017b1532e..000000000 --- a/src/main/java/com/demcha/compose/engine/debug/LayoutSnapshotExtractor.java +++ /dev/null @@ -1,264 +0,0 @@ -package com.demcha.compose.engine.debug; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Layer; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.core.LayoutTraversalContext; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.regex.Pattern; - -/** - * Extracts deterministic, renderer-agnostic layout snapshots from resolved ECS state. - * - *

The extractor walks the entity tree in deterministic depth-first order, - * reads post-layout components such as {@link ComputedPosition} and - * {@link Placement}, and projects them into stable snapshot records that are - * suitable for regression testing and debugging.

- * - *

The extractor is intentionally strict: if required post-layout components - * are missing after the layout pass, extraction fails fast with a descriptive - * error rather than silently producing an incomplete snapshot.

- */ -public final class LayoutSnapshotExtractor { - public static final String FORMAT_VERSION = "1.0"; - private static final String PATH_SEPARATOR = "/"; - private static final Pattern GENERATED_ENTITY_NAME = Pattern.compile("^[A-Za-z0-9$]+Builder__[0-9a-fA-F]{5}$"); - - private LayoutSnapshotExtractor() { - } - - /** - * Builds a deterministic snapshot from the current entity manager state. - * - *

This method expects layout resolution and pagination to have already - * happened. It does not run rendering and does not mutate renderer-owned - * output state.

- * - * @param entityManager entity graph and resolved post-layout components - * @param canvas resolved canvas used during layout - * @return snapshot of the current resolved layout tree - */ - public static LayoutSnapshot extract(EntityManager entityManager, Canvas canvas) { - Objects.requireNonNull(entityManager, "entityManager"); - Objects.requireNonNull(canvas, "canvas"); - - Map entities = entityManager.getEntities(); - if (entities == null || entities.isEmpty()) { - return new LayoutSnapshot( - FORMAT_VERSION, - snapshotCanvas(canvas), - 0, - List.of()); - } - - // Resolved-order features must always traverse through LayoutTraversalContext - // so snapshots, pagination, and renderer ordering agree on the same tree. - LayoutTraversalContext traversalContext = LayoutTraversalContext.from(entityManager); - List roots = resolveRoots(entities, traversalContext); - List nodes = new ArrayList<>(entities.size()); - - for (int rootIndex = 0; rootIndex < roots.size(); rootIndex++) { - visitNode( - entityManager, - entities, - traversalContext, - roots.get(rootIndex), - null, - rootIndex, - nodes, - new HashSet<>()); - } - - int totalPages = nodes.stream() - .mapToInt(node -> Math.max(node.startPage(), node.endPage()) + 1) - .max() - .orElse(0); - - return new LayoutSnapshot( - FORMAT_VERSION, - snapshotCanvas(canvas), - totalPages, - List.copyOf(nodes)); - } - - static double normalize(double value) { - if (Math.abs(value) < 0.0005d) { - return 0.0d; - } - return BigDecimal.valueOf(value) - .setScale(3, RoundingMode.HALF_UP) - .doubleValue(); - } - - private static void visitNode(EntityManager entityManager, - Map entities, - LayoutTraversalContext traversalContext, - Entity entity, - String parentPath, - int childIndex, - List snapshots, - Set activePath) { - if (!activePath.add(entity.getUuid())) { - throw new IllegalStateException("Cycle detected while extracting layout snapshot for entity " + describeEntity(entity)); - } - - String segment = nodeSegment(entity, childIndex); - String path = parentPath == null ? segment : parentPath + PATH_SEPARATOR + segment; - - snapshots.add(toNodeSnapshot(entityManager, entity, parentPath, path, childIndex)); - - List childIds = traversalContext.childrenByParent().getOrDefault(entity.getUuid(), List.of()); - for (int index = 0; index < childIds.size(); index++) { - UUID childId = childIds.get(index); - Entity child = entities.get(childId); - if (child == null) { - throw new IllegalStateException("Missing child entity " + childId + " referenced from " + describeEntity(entity)); - } - visitNode(entityManager, entities, traversalContext, child, path, index, snapshots, activePath); - } - - activePath.remove(entity.getUuid()); - } - - private static LayoutNodeSnapshot toNodeSnapshot(EntityManager entityManager, - Entity entity, - String parentPath, - String path, - int childIndex) { - String label = path + " (" + describeEntity(entity) + ")"; - ComputedPosition computedPosition = requireComponent(entity, ComputedPosition.class, label); - Placement placement = requireComponent(entity, Placement.class, label); - ContentSize contentSize = requireComponent(entity, ContentSize.class, label); - int layer = requireComponent(entity, Layer.class, label).value(); - int depth = entityManager.getDepthById() - .getOrDefault(entity.getUuid(), layer); - - return new LayoutNodeSnapshot( - path, - semanticEntityName(entity), - entityKind(entity), - parentPath, - childIndex, - depth, - layer, - normalize(computedPosition.x()), - normalize(computedPosition.y()), - normalize(placement.x()), - normalize(placement.y()), - normalize(placement.width()), - normalize(placement.height()), - placement.startPage(), - placement.endPage(), - normalize(contentSize.width()), - normalize(contentSize.height()), - LayoutInsetsSnapshot.from(entity.getComponent(Margin.class).orElse(Margin.zero())), - LayoutInsetsSnapshot.from(entity.getComponent(Padding.class).orElse(Padding.zero()))); - } - - private static LayoutCanvasSnapshot snapshotCanvas(Canvas canvas) { - return new LayoutCanvasSnapshot( - normalize(canvas.width()), - normalize(canvas.height()), - normalize(canvas.innerWidth()), - normalize(canvas.innerHeigh()), - LayoutInsetsSnapshot.from(canvas.margin())); - } - - private static List resolveRoots(Map entities, LayoutTraversalContext traversalContext) { - Comparator rootComparator = Comparator - .comparingInt(LayoutSnapshotExtractor::startPage) - .thenComparing(LayoutSnapshotExtractor::topToBottomY) - .thenComparing(LayoutSnapshotExtractor::leftToRightX) - .thenComparing(LayoutSnapshotExtractor::entitySortKey); - - return traversalContext.roots().stream() - .map(entities::get) - .filter(Objects::nonNull) - .sorted(rootComparator) - .collect(Collectors.toList()); - } - - private static int startPage(Entity entity) { - return requireComponent(entity, Placement.class, describeEntity(entity)).startPage(); - } - - private static double topToBottomY(Entity entity) { - return -requireComponent(entity, Placement.class, describeEntity(entity)).y(); - } - - private static double leftToRightX(Entity entity) { - return requireComponent(entity, Placement.class, describeEntity(entity)).x(); - } - - private static String entitySortKey(Entity entity) { - String semanticName = semanticEntityName(entity); - return semanticName == null ? entityKind(entity) : semanticName; - } - - private static String nodeSegment(Entity entity, int childIndex) { - String base = semanticEntityName(entity); - if (base != null) { - base = sanitizeSegment(base); - } - if (base == null || base.isBlank()) { - base = entityKind(entity); - } - - return base + "[" + childIndex + "]"; - } - - private static String sanitizeSegment(String value) { - return value.trim() - .replace('\\', '_') - .replace('/', '_'); - } - - private static String entityKind(Entity entity) { - if (entity.getRender() != null) { - return entity.getRender().getClass().getSimpleName(); - } - return entity.getClass().getSimpleName(); - } - - private static String semanticEntityName(Entity entity) { - return entity.getComponent(EntityName.class) - .map(EntityName::value) - .map(String::trim) - .filter(value -> !value.isBlank()) - .filter(value -> !GENERATED_ENTITY_NAME.matcher(value).matches()) - .orElse(null); - } - - private static String describeEntity(Entity entity) { - String semanticName = semanticEntityName(entity); - return semanticName != null - ? semanticName - : entityKind(entity) + "#" + entity.getUuid().toString().substring(0, 8); - } - - private static T requireComponent(Entity entity, Class componentType, String label) { - return entity.getComponent(componentType) - .orElseThrow(() -> new NoSuchElementException("Layout snapshot requires " + componentType.getSimpleName() - + " for " + label)); - } -} diff --git a/src/main/java/com/demcha/compose/engine/exceptions/ContentSizeNotFoundException.java b/src/main/java/com/demcha/compose/engine/exceptions/ContentSizeNotFoundException.java deleted file mode 100644 index fa280f6fd..000000000 --- a/src/main/java/com/demcha/compose/engine/exceptions/ContentSizeNotFoundException.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.demcha.compose.engine.exceptions; - -import com.demcha.compose.engine.components.core.Entity; - -public class ContentSizeNotFoundException extends RuntimeException { - public ContentSizeNotFoundException() { - super(" All objects must have a ContentSize."); - } - - public ContentSizeNotFoundException(String s) { - super(s); - } - - public ContentSizeNotFoundException(Entity entity) { - super(entity.toString() + " All objects must have a ContentSize."); - } -} diff --git a/src/main/java/com/demcha/compose/engine/exceptions/RenderGuideLinesException.java b/src/main/java/com/demcha/compose/engine/exceptions/RenderGuideLinesException.java deleted file mode 100644 index d0931b49a..000000000 --- a/src/main/java/com/demcha/compose/engine/exceptions/RenderGuideLinesException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.demcha.compose.engine.exceptions; - -public class RenderGuideLinesException extends RuntimeException { - - public RenderGuideLinesException(String info) { - super(info); - } - - public RenderGuideLinesException(String message, Throwable io) { - super(message, io); - } -} diff --git a/src/main/java/com/demcha/compose/engine/exceptions/package-info.java b/src/main/java/com/demcha/compose/engine/exceptions/package-info.java deleted file mode 100644 index 80ac1a143..000000000 --- a/src/main/java/com/demcha/compose/engine/exceptions/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Shared engine exceptions raised by low-level layout, pagination, and rendering contracts. - * - *

Ownership: Owned by the shared engine foundation.

- *

Extension rules: Prefer precise exception types when callers can recover or produce better diagnostics.

- */ -package com.demcha.compose.engine.exceptions; \ No newline at end of file diff --git a/src/main/java/com/demcha/compose/engine/pagination/Breakable.java b/src/main/java/com/demcha/compose/engine/pagination/Breakable.java deleted file mode 100644 index b4cc90a45..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/Breakable.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.demcha.compose.engine.pagination; - -/** - * Marker for renderables whose content may continue across page boundaries. - * - *

This marker is used by the page breaker. A breakable entity may start on one page - * and continue on the next page instead of being moved as a single indivisible block.

- * - *

It does not mean that the entity should resize to fit children. - * Parent expansion is handled separately by {@code Expendable}.

- */ -public interface Breakable { -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/Offset.java b/src/main/java/com/demcha/compose/engine/pagination/Offset.java deleted file mode 100644 index 8991f6117..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/Offset.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.atomic.DoubleAdder; - -@Slf4j -@Data -@Accessors(fluent = true) -@NoArgsConstructor -public class Offset { - // Use DoubleAdder for thread safety and performance - private final DoubleAdder y = new DoubleAdder(); - private final DoubleAdder x = new DoubleAdder(); - - // Custom getters because DoubleAdder doesn't return primitive double automatically - public double y() { - return y.sum(); - } - - public double x() { - return x.sum(); - } - - // --- Y Increments --- - public void incrementY(double increment) { - log.debug("{} increment Y({})", this, increment); - this.y.add(increment); - } - - // Overloads - public void incrementY(float increment) { incrementY((double) increment); } - public void incrementY(int increment) { incrementY((double) increment); } - - public void incrementY(Offset offset) { - log.debug("Increment offset Y from object {}", offset); - incrementY(offset.y()); - } - - // --- X Increments (FIXED BUGS HERE) --- - public void incrementX(double increment) { - log.debug("{} increment X ({})", this, increment); - this.x.add(increment); // Was adding to Y in your code - } - - // Overloads - public void incrementX(float increment) { incrementX((double) increment); } // Fixed call - public void incrementX(int increment) { incrementX((double) increment); } // Fixed call - - public void incrementX(Offset offset) { - log.debug("Increment offset X from object {}", offset); - incrementX(offset.x()); // Fixed call - } -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/PageOutOfBoundException.java b/src/main/java/com/demcha/compose/engine/pagination/PageOutOfBoundException.java deleted file mode 100644 index c7e32a8c8..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/PageOutOfBoundException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.demcha.compose.engine.pagination; - -public class PageOutOfBoundException extends RuntimeException { - private static String message = "Page out of bound, current page: %s"; - public PageOutOfBoundException(String s) { - super(s); - } - - public PageOutOfBoundException(String message, Throwable cause) { - super(message, cause); - } - public PageOutOfBoundException(int pageNumber) { - super(String.format(message, pageNumber) ); - } -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/ParentContainerUpdater.java b/src/main/java/com/demcha/compose/engine/pagination/ParentContainerUpdater.java deleted file mode 100644 index 16b231b01..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/ParentContainerUpdater.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.core.EntityManager; -import lombok.NonNull; -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; - -/** - * Pagination helper that propagates child-driven size and position changes to - * parent containers. - * - *

- * The page breaker uses these routines after a child moves or grows so parent - * boxes can reflect the same change before later layout/render stages inspect - * the tree. - *

- */ -@Slf4j -@UtilityClass -public class ParentContainerUpdater { - - /** - * Propagates a resolved offset from a child entity to its parent container. - * - * @param entity child entity whose parent should be updated - * @param manager entity manager used to resolve parent entities - * @param offset resolved offset object - * @return {@code true} when a parent update was applied - */ - public static boolean updateParentContainer(Entity entity, EntityManager manager, Offset offset) { - if (offset == null) { - log.error("Offset cannot be null"); - return false; - } - return updateParentContainer(entity, manager, offset.y()); - } - - /** - * Propagates a vertical delta from a child entity to its parent container - * chain. - * - * @param entity child entity whose parent should be updated - * @param manager entity manager used to resolve parent entities - * @param offsetY vertical delta to propagate - * @return {@code true} when a parent update was applied - */ - public static boolean updateParentContainer(Entity entity, EntityManager manager, double offsetY) { - if (offsetY == 0) { - if (log.isDebugEnabled()) { - log.debug("Skipping parent container position update because offset is zero for {}", entity.getUuid()); - } - return false; - } - - ParentComponent parentComponent = entity.getComponent(ParentComponent.class).orElse(null); - if (parentComponent == null) { - if (log.isDebugEnabled()) { - log.debug("Parent component missing for entity [{}]; parent position update skipped.", entity.getUuid()); - } - return false; - } - - Entity parent = manager.getEntity(parentComponent.uuid()).orElse(null); - if (parent == null) { - log.error("Parent entity not found in manager for UUID {}", parentComponent.uuid()); - return false; - } - return updateEntitySizeAndPosition(manager, offsetY, parent); - } - - /** - * Propagates a size-only delta from a child entity to its parent container - * chain. - * - * @param entity child entity whose parent size should be updated - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta to propagate - * @return {@code true} when a parent size update was applied - */ - public static boolean updateParentContainerSize(Entity entity, EntityManager manager, double offsetY) { - if (offsetY == 0) { - if (log.isDebugEnabled()) { - log.debug("Skipping parent container size update because offset is zero for {}", entity.getUuid()); - } - return false; - } - - ParentComponent parentComponent = entity.getComponent(ParentComponent.class).orElse(null); - if (parentComponent == null) { - if (log.isDebugEnabled()) { - log.debug("Parent component missing for entity [{}]; parent size update skipped.", entity.getUuid()); - } - return false; - } - - Entity parent = manager.getEntity(parentComponent.uuid()).orElse(null); - if (parent == null) { - log.error("Parent entity not found in manager for UUID {}", parentComponent.uuid()); - return false; - } - return updateEntitySize(manager, offsetY, parent); - } - - /** - * Resizes the target entity and, for negative offsets, also shifts its Y - * position before propagating the change upward. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta or upward shift delta - * @param entity entity to mutate - * @return {@code true} when propagation reached a parent container - */ - public static boolean updateEntitySizeAndPosition(EntityManager manager, double offsetY, @NonNull Entity entity) { - ComputedPosition computedPosition = entity.require(ComputedPosition.class); - ContentSize size = entity.require(ContentSize.class); - - if (offsetY < 0) { - double newY = computedPosition.y() + offsetY; - double newHeight = size.height() + Math.abs(offsetY); - entity.addComponent(new ComputedPosition(computedPosition.x(), newY)); - entity.addComponent(new ContentSize(size.width(), newHeight)); - } else { - double newHeight = size.height() + offsetY; - entity.addComponent(new ContentSize(size.width(), newHeight)); - } - - return updateParentContainer(entity, manager, offsetY); - } - - /** - * Resizes the target entity without changing its own Y coordinate and then - * propagates the size change upward. - * - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta to apply - * @param entity entity to resize - * @return {@code true} when propagation reached a parent container - */ - public static boolean updateEntitySize(EntityManager manager, double offsetY, @NonNull Entity entity) { - entity.require(ComputedPosition.class); - ContentSize size = entity.require(ContentSize.class); - - if (offsetY < 0) { - double newHeight = size.height() + Math.abs(offsetY); - entity.addComponent(new ContentSize(size.width(), newHeight)); - } else { - double newHeight = size.height() + offsetY; - entity.addComponent(new ContentSize(size.width(), newHeight)); - } - - return updateParentContainerSize(entity, manager, offsetY); - } - - /** - * Resizes the current entity and propagates the size-only change to the parent - * container chain. - * - * @param entity current entity to resize - * @param manager entity manager used to resolve parent entities - * @param offsetY height delta to apply - * @return {@code true} when propagation reached a parent container - */ - public static boolean updateCurrentEntitySize(Entity entity, EntityManager manager, double offsetY) { - entity.require(ComputedPosition.class); - ContentSize size = entity.require(ContentSize.class); - - if (offsetY < 0) { - double newHeight = size.height() + Math.abs(offsetY); - entity.addComponent(new ContentSize(size.width(), newHeight)); - } else { - double newHeight = size.height() + offsetY; - entity.addComponent(new ContentSize(size.width(), newHeight)); - } - - return updateParentContainerSize(entity, manager, offsetY); - } -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/package-info.java b/src/main/java/com/demcha/compose/engine/pagination/package-info.java deleted file mode 100644 index 07d256192..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/package-info.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Pagination and page-placement helpers of the legacy ECS engine. - * - *

This package holds the {@code Entity}-based pagination helpers and markers - * ({@code ParentContainerUpdater} for upward size/position propagation, the - * {@code Breakable} marker, and {@code Offset}). It is renderer-neutral; the - * canonical pipeline ({@code com.demcha.compose.document.layout}) does its own - * pagination.

- * - * @deprecated Part of the legacy {@code Entity} engine, superseded by the - * canonical {@code com.demcha.compose.document.layout} pipeline; a candidate - * for removal once the {@code Entity} model is retired. Do not spend - * optimization effort here. - */ -@Deprecated -package com.demcha.compose.engine.pagination; diff --git a/src/main/java/com/demcha/compose/engine/render/Render.java b/src/main/java/com/demcha/compose/engine/render/Render.java deleted file mode 100644 index 8d8472675..000000000 --- a/src/main/java/com/demcha/compose/engine/render/Render.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.components.core.Component; - -public interface Render extends Component { -} diff --git a/src/main/java/com/demcha/compose/engine/render/RenderHandler.java b/src/main/java/com/demcha/compose/engine/render/RenderHandler.java deleted file mode 100644 index d2fc6747a..000000000 --- a/src/main/java/com/demcha/compose/engine/render/RenderHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.render.RenderingSystemECS; - -import java.io.IOException; - -/** - * Backend-owned renderer for one render marker type. - */ -public interface RenderHandler> { - - Class renderType(); - - boolean render(EntityManager manager, - Entity entity, - R renderComponent, - RS renderingSystem, - boolean guideLines) throws IOException; -} diff --git a/src/main/java/com/demcha/compose/engine/render/RenderHandlerRegistry.java b/src/main/java/com/demcha/compose/engine/render/RenderHandlerRegistry.java deleted file mode 100644 index 04817c7f9..000000000 --- a/src/main/java/com/demcha/compose/engine/render/RenderHandlerRegistry.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.render.RenderingSystemECS; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -/** - * Registry for renderer-owned handlers keyed by render marker type. - */ -public final class RenderHandlerRegistry { - private final Map, RenderHandler> handlers = new LinkedHashMap<>(); - - public > void register(RenderHandler handler) { - Objects.requireNonNull(handler, "handler"); - handlers.put(handler.renderType(), handler); - } - - public Optional> find(Render render) { - if (render == null) { - return Optional.empty(); - } - - RenderHandler direct = handlers.get(render.getClass()); - if (direct != null) { - return Optional.of(direct); - } - - return handlers.entrySet().stream() - .filter(entry -> entry.getKey().isInstance(render)) - .map(Map.Entry::getValue) - .findFirst(); - } -} diff --git a/src/main/java/com/demcha/compose/engine/render/RenderPassSession.java b/src/main/java/com/demcha/compose/engine/render/RenderPassSession.java deleted file mode 100644 index 7167dee11..000000000 --- a/src/main/java/com/demcha/compose/engine/render/RenderPassSession.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; - -import java.io.IOException; -import java.util.Objects; - -/** - * Backend-neutral render-pass session. - * - *

The engine uses a session to model "one rendering pass over one document". - * Backends can reuse expensive page-local resources during the pass while the - * engine keeps its contracts PDF-free. Concrete backends remain responsible for - * what a page surface actually is and how it is opened or closed.

- * - *

Render handlers must treat the returned surfaces as session-owned. They may - * change graphics/text state for their own draw operation, but they must not - * close the surface directly.

- * - * @param backend-specific surface type - */ -public interface RenderPassSession extends AutoCloseable { - - /** - * Ensures that the target page exists and is ready to accept draw commands. - * - * @param pageIndex zero-based page index - * @throws IOException if the backend cannot materialize the page - */ - void ensurePage(int pageIndex) throws IOException; - - /** - * Returns the session-owned drawing surface for a page. - * - * @param pageIndex zero-based page index - * @return backend-specific page surface - * @throws IOException if the backend cannot provide the surface - */ - T pageSurface(int pageIndex) throws IOException; - - /** - * Convenience helper for single-page entities. - * - *

Entities spanning multiple pages must acquire surfaces explicitly per - * page fragment so their handler can manage page-local draw state.

- * - * @param entity renderable entity with resolved placement - * @return session-owned page surface for the entity's single page - * @throws IOException if the backend cannot provide the surface - */ - default T pageSurface(Entity entity) throws IOException { - Objects.requireNonNull(entity, "entity must not be null"); - Placement placement = entity.getComponent(Placement.class) - .orElseThrow(() -> new IllegalStateException("Entity " + entity + " is missing Placement")); - if (placement.startPage() != placement.endPage()) { - throw new IllegalStateException( - "Entity " + entity + " spans multiple pages and must request page surfaces per fragment"); - } - return pageSurface(placement.startPage()); - } - - @Override - void close() throws IOException; -} diff --git a/src/main/java/com/demcha/compose/engine/render/RenderStream.java b/src/main/java/com/demcha/compose/engine/render/RenderStream.java deleted file mode 100644 index 6913c8fe1..000000000 --- a/src/main/java/com/demcha/compose/engine/render/RenderStream.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.demcha.compose.engine.render; - -import java.io.IOException; - -/** - * Factory for backend-specific render-pass sessions. - * - *

The engine does not ask the backend to open a fresh surface per entity. - * Instead it opens one session per render pass, and the backend can decide how - * to reuse page-local resources inside that pass.

- * - * @param backend-specific surface type - */ -public interface RenderStream { - RenderPassSession openRenderPass() throws IOException; -} diff --git a/src/main/java/com/demcha/compose/engine/render/RenderingSystemECS.java b/src/main/java/com/demcha/compose/engine/render/RenderingSystemECS.java deleted file mode 100644 index fd72fd3a5..000000000 --- a/src/main/java/com/demcha/compose/engine/render/RenderingSystemECS.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.core.SystemECS; -import com.demcha.compose.engine.render.guides.GuidLineSettings; -import com.demcha.compose.engine.render.guides.GuidesRenderer; - -import java.awt.*; -import java.io.IOException; -import java.util.Optional; -import java.util.Set; - -/** - * Backend-neutral rendering system contract. - * - *

The shared engine knows how to orchestrate a render pass, but not how a - * concrete backend materializes page surfaces. Implementations expose the - * active {@link RenderPassSession} during {@code process(...)} so handlers can - * reuse backend-local page resources without leaking PDF/DOCX/PPTX details into - * engine/template code.

- */ -public interface RenderingSystemECS extends SystemECS { - T canvas(); - - GuidesRenderer guidesRenderer(); - RenderHandlerRegistry renderHandlers(); - - > T stream(); - Optional> activeRenderSession(); - - /** - * Returns the current render-pass session. - * - *

This is only valid while a rendering system is actively processing a - * document. Callers that need ad-hoc rendering outside a normal pass should - * open their own short-lived session from {@link #stream()}.

- */ - default RenderPassSession renderSession() { - return activeRenderSession() - .orElseThrow(() -> new IllegalStateException("No active render session is available")); - } - - boolean renderBorder(S stream, RenderCoordinateContext context, - boolean lineDash, - Set sides) throws IOException; - - GuidLineSettings guidLineSettings(); - - boolean renderRectangle(S stream, RenderCoordinateContext context, boolean lineDash) throws IOException; - - void fillCircle(S stream, float cx, float cy, float r, Color fill) throws IOException; - -} - diff --git a/src/main/java/com/demcha/compose/engine/render/guides/BoxRender.java b/src/main/java/com/demcha/compose/engine/render/guides/BoxRender.java deleted file mode 100644 index 6c77640da..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/BoxRender.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; - -import java.awt.*; -import java.util.Optional; - -/** - * An interface for rendering a box guide for an entity. - * - * @param The type of the stream used for rendering. - */ -public interface BoxRender extends GuideCoordinate { - - /** - * Creates a {@link RenderCoordinateContext} for the bounding box of an entity. - * - * @param entity The entity for which to create the context. - * @return An {@link Optional} containing the {@link RenderCoordinateContext}. - * @throws java.util.NoSuchElementException if the entity does not have a {@link ContentSize} or {@link Placement} component. - */ - default Optional box(Entity entity) { - var boxSize = entity.getComponent(ContentSize.class).orElseThrow(); - var placement = entity.getComponent(Placement.class).orElseThrow(); - double x = placement.x(); - double y = placement.y(); - double width = boxSize.width(); - double height = boxSize.height(); - Color color = renderingSystem().guidLineSettings().BOX_COLOR(); - Stroke stroke = renderingSystem().guidLineSettings().BOX_STROKE(); - return Optional.of(new RenderCoordinateContext(x, y, width, height, placement.startPage(), placement.endPage(), stroke, color)); - } -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/GuidLineSettings.java b/src/main/java/com/demcha/compose/engine/render/guides/GuidLineSettings.java deleted file mode 100644 index 9879c85a3..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/GuidLineSettings.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.components.content.shape.Stroke; -import lombok.Data; -import lombok.experimental.Accessors; - -import java.awt.Color; -@Data -@Accessors(fluent = true) -public class GuidLineSettings { - /** - * The opacity for all guide elements. Value between 0.0f (transparent) and 1.0f (opaque). - */ - float GUIDES_OPACITY = 0.8f; - // --- Margin Guide --- - Color MARGIN_COLOR = new Color(0, 110, 255); - final float MARKER_RADIUS = 3.5f; - Stroke MARGIN_STROKE = new Stroke(0.5); - // --- Padding Guide --- - Color PADDING_COLOR = new Color(255, 140, 0); - Stroke PADDING_STROKE = new Stroke(0.5); - // --- Content Box Guide --- - Color BOX_COLOR = new Color(150, 150, 150); // Using a slightly lighter gray - Stroke BOX_STROKE = new Stroke(1.0); - //viability - boolean showOnlySetGuide = true; -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/GuideCoordinate.java b/src/main/java/com/demcha/compose/engine/render/guides/GuideCoordinate.java deleted file mode 100644 index cece6434f..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/GuideCoordinate.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.exceptions.RenderGuideLinesException; -import com.demcha.compose.engine.render.RenderPassSession; -import com.demcha.compose.engine.render.RenderingSystemECS; -import lombok.NonNull; - -import java.io.IOException; -import java.util.Set; - -/** - * An interface for rendering coordinate guides for broken elements. - * - * @param The type of the stream used for rendering. - */ -public interface GuideCoordinate { - - RenderingSystemECS renderingSystem(); - - // --------------------------------------------------------- - // 2. The "Execute Around" Method (Handles the boilerplate) - // --------------------------------------------------------- - default boolean executeOnStream(@NonNull RenderCoordinateContext context, int pageNumber, StreamRenderer action) throws RenderGuideLinesException { - var activeSession = renderingSystem().activeRenderSession(); - if (activeSession.isPresent()) { - try { - return action.render(context, activeSession.get().pageSurface(pageNumber)); - } catch (RenderGuideLinesException e) { - throw e; - } catch (IOException e) { - throw new RenderGuideLinesException("Error occurred during guide rendering on the active session", e); - } catch (Exception e) { - throw new RenderGuideLinesException("Failed to process the active guide stream", e); - } - } - - try (RenderPassSession session = renderingSystem().stream().openRenderPass()) { - T stream = session.pageSurface(pageNumber); - return action.render(context, stream); - } catch (RenderGuideLinesException e) { - throw e; - } catch (IOException e) { - throw new RenderGuideLinesException("Error occurred during guide stream session handling", e); - } catch (Exception e) { - throw new RenderGuideLinesException("Failed to close or process the stream", e); - } - } - - default boolean start(@NonNull RenderCoordinateContext context, int pageNumber) throws RenderGuideLinesException { - // We pass the method reference 'this::startFromStream' - return executeOnStream(context, pageNumber, this::startFromStream); - } - - // --------------------------------------------------------- - // 3. Public API (Clean and simple) - // --------------------------------------------------------- - - default boolean middle(@NonNull RenderCoordinateContext context, int pageNumber) throws RenderGuideLinesException { - return executeOnStream(context, pageNumber, this::middleFromStream); - } - - default boolean end(@NonNull RenderCoordinateContext context, int pageNumber) throws RenderGuideLinesException { - return executeOnStream(context, pageNumber, this::endFromStream); - } - - default boolean startFromStream(@NonNull RenderCoordinateContext context, @NonNull T stream) throws RenderGuideLinesException, IOException { - var startHeight = context.y(); - RenderCoordinateContext coordinateContext = new RenderCoordinateContext(context.x(), startHeight, context.width(), context.height(), context.startPage(), context.endPage(), context.stroke(), context.color()); - - - Set sides = Set.of(Side.LEFT, Side.RIGHT, Side.BOTTOM); - - boolean lineDash = !context.stroke().equals( - renderingSystem().guidLineSettings().BOX_STROKE() - ); - - return renderingSystem().renderBorder(stream, coordinateContext, lineDash, sides); - } - - default void startMarkers(@NonNull RenderCoordinateContext context, @NonNull T stream) throws IOException { - final float radius = renderingSystem().guidLineSettings().MARKER_RADIUS(); - float cx = (float) context.x(); - float cy = (float) context.y(); - var color = context.color(); - float w = (float) context.width(); - - renderingSystem().fillCircle(stream, cx, cy, radius, color); - renderingSystem().fillCircle(stream, cx + w, cy, radius, color); - } - - // --------------------------------------------------------- - // 4. Logic Implementations (Focus only on drawing) - // --------------------------------------------------------- - - default boolean middleFromStream(@NonNull RenderCoordinateContext context, @NonNull T stream) throws RenderGuideLinesException, IOException { - Set sides = Set.of(Side.LEFT, Side.RIGHT); - boolean lineDash = !context.stroke().equals( - renderingSystem().guidLineSettings().BOX_STROKE() - ); - - renderingSystem().renderBorder(stream, context, lineDash, sides); - - return true; - } - - default boolean endFromStream(@NonNull RenderCoordinateContext context, @NonNull T stream) throws RenderGuideLinesException, IOException { - Set sides = Set.of(Side.LEFT, Side.RIGHT, Side.TOP); - boolean lineDash = !context.stroke().equals( - renderingSystem().guidLineSettings().BOX_STROKE() - ); - renderingSystem().renderBorder(stream, context, lineDash, sides); - return true; - } - - default void endMarkers(@NonNull RenderCoordinateContext context, @NonNull T stream) throws IOException { - final float radius = renderingSystem().guidLineSettings().MARKER_RADIUS(); - float cx = (float) context.x(); - float cy = (float) context.y(); - var color = context.color(); - float w = (float) context.width(); - float h = (float) context.height(); - - renderingSystem().fillCircle(stream, cx, cy + h, radius, color); - renderingSystem().fillCircle(stream, cx + w, cy + h, radius, color); - } - - // Existing methods kept as requested - boolean fromStream(@NonNull Entity entity, T stream); - - default void markers(T stream, RenderCoordinateContext context) throws IOException { - final float radius = renderingSystem().guidLineSettings().MARKER_RADIUS(); - float cx = (float) context.x(); - float cy = (float) context.y(); - var color = context.color(); - float w = (float) context.width(); - float h = (float) context.height(); - - renderingSystem().fillCircle(stream, cx, cy, radius, color); - renderingSystem().fillCircle(stream, cx, cy + h, radius, color); - renderingSystem().fillCircle(stream, cx + w, cy, radius, color); - renderingSystem().fillCircle(stream, cx + w, cy + h, radius, color); - } - - // --------------------------------------------------------- - // 1. The Functional Interface (Contract for the logic) - // --------------------------------------------------------- - @FunctionalInterface - interface StreamRenderer { - boolean render(RenderCoordinateContext context, T stream) throws Exception; - } -} - - diff --git a/src/main/java/com/demcha/compose/engine/render/guides/GuidesRenderer.java b/src/main/java/com/demcha/compose/engine/render/guides/GuidesRenderer.java deleted file mode 100644 index e4450b3ca..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/GuidesRenderer.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.RenderCoordinate; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.exceptions.RenderGuideLinesException; -import com.demcha.compose.engine.render.guides.GuidLineSettings; -import com.demcha.compose.engine.render.RenderPassSession; -import com.demcha.compose.engine.render.RenderingSystemECS; -import com.demcha.compose.engine.pagination.PageOutOfBoundException; -import lombok.Data; -import lombok.NonNull; -import lombok.ToString; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; - -import java.awt.Color; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; - -/** - * Renders margin, padding, and box guides for resolved entities. - * - *

The renderer prefers the currently active render session when guide drawing - * happens during a render pass. If guide rendering is invoked outside an active - * pass, it falls back to a short-lived backend session so the public guide API - * remains usable.

- */ -@Slf4j -@Data -@Accessors(fluent = true) -public abstract class GuidesRenderer { - @ToString.Exclude - protected final RenderingSystemECS renderingSystem; - @ToString.Exclude - protected final BoxRender box; - @ToString.Exclude - protected final MarginRender margin; - @ToString.Exclude - protected final PaddingRender padding; - - private static RenderCoordinateContext update(RenderCoordinateContext source, double y, double height, int page) { - return new RenderCoordinateContext( - source.x(), - y, - source.width(), - height, - page, - page, - source.stroke(), - source.color()); - } - - protected Optional resolveCoordinateContext( - Entity entity, - @NonNull GuidLineSettings guidLineSettings, - Class componentClass, - Supplier defaultSupplier - ) { - if (!renderingSystem.guidLineSettings().showOnlySetGuide()) { - return Optional.empty(); - } - - T context = entity.getComponent(componentClass) - .orElseGet(() -> { - log.info("{} is {} ", componentClass, defaultSupplier.get()); - return defaultSupplier.get(); - }); - - return context.renderCoordinate(entity, renderingSystem()); - } - - public boolean guidesRender(Entity entity, S stream, EnumSet guides) throws RenderGuideLinesException { - boolean any = false; - if (guides.contains(Guide.MARGIN)) { - any |= margin().fromStream(entity, stream); - } - if (guides.contains(Guide.PADDING)) { - any |= padding().fromStream(entity, stream); - } - if (guides.contains(Guide.BOX)) { - any |= box().fromStream(entity, stream); - } - return any; - } - - public boolean guidesRender(Entity entity, EnumSet guides) { - try { - return withSession(session -> guidesRender(entity, guides, session)); - } catch (Exception ex) { - log.error("Failed to render guides for entity {}", entity, ex); - throw new RuntimeException("Rendering failed", ex); - } - } - - private boolean guidesRender(Entity entity, - EnumSet guides, - RenderPassSession session) throws Exception { - Placement placement = entity.getComponent(Placement.class) - .orElseThrow(() -> new IllegalStateException("Entity missing Placement component")); - - int startPage = placement.startPage(); - int endPage = placement.endPage(); - - if (startPage == endPage || startPage == 0 && endPage == -1) { - return guidesRender(entity, session.pageSurface(startPage), guides); - } - - List boxFragments = breakCoordinate(Optional.of(boxCoordinate(entity)), startPage); - List marginFragments = breakCoordinate(margin().margin(entity), startPage); - List paddingFragments = breakCoordinate(padding().padding(entity), startPage); - - log.debug("Rendering spanned multiple pages for entity: {}. Start: {}, End: {}", entity, startPage, endPage); - int boxFragmentIndex = 0; - int marginFragmentIndex = 0; - int paddingFragmentIndex = 0; - - for (int renderingPage = startPage; renderingPage >= endPage; renderingPage--) { - if (renderingPage < 0) { - log.error("Rendering page is out of bound, negative number: {}", renderingPage); - throw new PageOutOfBoundException("Rendering page is out of bound, negative number: " + renderingPage); - } - - RenderCoordinateContext currentBox = - boxFragmentIndex < boxFragments.size() ? boxFragments.get(boxFragmentIndex) : null; - RenderCoordinateContext currentMargin = - marginFragmentIndex < marginFragments.size() ? marginFragments.get(marginFragmentIndex) : null; - RenderCoordinateContext currentPadding = - paddingFragmentIndex < paddingFragments.size() ? paddingFragments.get(paddingFragmentIndex) : null; - - try { - S stream = session.pageSurface(renderingPage); - if (renderingPage == startPage) { - startGuidesFromStream(stream, currentBox, currentMargin, currentPadding); - } else if (renderingPage == endPage) { - endGuidesFromStream(stream, currentBox, currentMargin, currentPadding); - } else { - middleGuidesFromStream(stream, currentBox, currentMargin, currentPadding); - } - } catch (Exception ex) { - log.error("Failed to render guides on multiple page for entity {} \n {}", entity, entity.printInfo(), ex); - throw new RuntimeException( - String.format("Failed to render guides on multiple page %s for entity \n %s ", - renderingPage, - entity.printInfo()), - ex); - } - - boxFragmentIndex++; - marginFragmentIndex++; - paddingFragmentIndex++; - } - - return true; - } - - /** - * Executes a guide-rendering action within a render session. - * - *

Prefers the currently active session from the rendering system. If no - * session is active (e.g. guide rendering triggered outside a normal render - * pass), a short-lived session is opened and closed automatically.

- * - * @param action the rendering action to execute within a session - * @return {@code true} if the action rendered any guides - * @throws Exception if the action or session lifecycle fails - */ - private boolean withSession(SessionAction action) throws Exception { - Optional> activeSession = renderingSystem.activeRenderSession(); - if (activeSession.isPresent()) { - return action.render(activeSession.get()); - } - try (RenderPassSession session = renderingSystem.stream().openRenderPass()) { - return action.render(session); - } - } - - private void middleGuidesFromStream(S stream, - RenderCoordinateContext boxContext, - RenderCoordinateContext marginContext, - RenderCoordinateContext paddingContext) throws IOException { - if (marginContext != null) { - margin().middleFromStream(marginContext, stream); - } - if (paddingContext != null) { - padding().middleFromStream(paddingContext, stream); - } - if (boxContext != null) { - box().middleFromStream(boxContext, stream); - } - } - - private void endGuidesFromStream(S stream, - RenderCoordinateContext boxContext, - RenderCoordinateContext marginContext, - RenderCoordinateContext paddingContext) throws IOException { - if (marginContext != null) { - margin().endFromStream(marginContext, stream); - margin().endMarkers(marginContext, stream); - } - if (paddingContext != null) { - padding().endFromStream(paddingContext, stream); - padding().endMarkers(paddingContext, stream); - } - if (boxContext != null) { - box().endFromStream(boxContext, stream); - } - } - - public List breakCoordinate(Optional contextOptional, int pages) { - if (contextOptional.isEmpty()) { - return Collections.emptyList(); - } - - RenderCoordinateContext sourceContext = contextOptional.get(); - List resultSegments = new ArrayList<>(); - float canvasHeight = renderingSystem().canvas().height(); - - double remainingHeight = sourceContext.height(); - double currentY = sourceContext.y(); - int currentPage = pages - 1; - - while (remainingHeight > 0) { - double heightOnThisPage = currentY + remainingHeight <= canvasHeight - ? remainingHeight - : canvasHeight - currentY; - - resultSegments.add(update(sourceContext, currentY, heightOnThisPage, currentPage)); - - remainingHeight -= heightOnThisPage; - currentY = 0; - currentPage--; - } - - return resultSegments; - } - - private void startGuidesFromStream(S stream, - RenderCoordinateContext boxContext, - RenderCoordinateContext marginContext, - RenderCoordinateContext paddingContext) throws IOException { - if (marginContext != null) { - margin().startFromStream(marginContext, stream); - margin().startMarkers(marginContext, stream); - } - if (paddingContext != null) { - padding().startFromStream(paddingContext, stream); - padding().startMarkers(paddingContext, stream); - } - if (boxContext != null) { - box().startFromStream(boxContext, stream); - } - } - - private RenderCoordinateContext boxCoordinate(Entity entity) { - Stroke stroke = renderingSystem().guidLineSettings().BOX_STROKE(); - Color color = renderingSystem().guidLineSettings().BOX_COLOR(); - return RenderCoordinateContext.createBox(entity, stroke, color); - } - - /** - * Functional callback for guide-rendering operations that require a render session. - * - * @param backend-specific surface type - */ - @FunctionalInterface - private interface SessionAction { - boolean render(RenderPassSession session) throws Exception; - } - - /** - * Types of guides that can be rendered around an entity. - */ - public enum Guide { - MARGIN, - PADDING, - BOX - } -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/MarginRender.java b/src/main/java/com/demcha/compose/engine/render/guides/MarginRender.java deleted file mode 100644 index 605979681..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/MarginRender.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.components.style.Margin; - -import java.util.Optional; -import java.util.function.Supplier; - -public interface MarginRender extends GuideCoordinate { - - default Optional margin(Entity entity) { - Optional coordinateOpt = - resolveCoordinateContext(entity, Margin.class, Margin::zero); - return coordinateOpt; - } - - default Optional resolveCoordinateContext(Entity entity, Class componentClass, Supplier zero) { - if (!renderingSystem().guidLineSettings().showOnlySetGuide()) { - return Optional.empty(); - } - - C context = entity.getComponent(componentClass) - .orElseGet(zero); - - return context.renderCoordinate(entity, renderingSystem()); - } - - -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/PaddingRender.java b/src/main/java/com/demcha/compose/engine/render/guides/PaddingRender.java deleted file mode 100644 index fc7e9790f..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/PaddingRender.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.components.style.Padding; - -import java.util.Optional; -import java.util.function.Supplier; - -/** - * An interface for rendering a padding guide for an entity. - * - * @param The type of the stream used for rendering. - */ -public interface PaddingRender extends GuideCoordinate { - - /** - * Creates a {@link RenderCoordinateContext} for the padding area of an entity. - *

- * Note: This default implementation relies on {@code resolveCoordinateContext} and {@code renderingSystemECS} methods - * which are expected to be provided by the implementing class. - * - * @param entity The entity for which to create the context. - * @return An {@link Optional} containing the {@link RenderCoordinateContext} for the padding, or an empty optional if it cannot be resolved. - */ - default Optional padding(Entity entity) { - RenderCoordinateContext coordinateContext; - Optional coordinateOpt = - resolveCoordinateContext(entity, Padding.class, Padding::zero); - - return coordinateOpt; - - } - - // The following methods are required by the default 'padding' method. - // They must be implemented by any class that implements this interface. - - default Optional resolveCoordinateContext(Entity entity, Class componentClass, Supplier zero) { - if (!renderingSystem().guidLineSettings().showOnlySetGuide()) { - return Optional.empty(); - } - - // Берём компонент или default (Margin.zero() / Padding.zero()) - C context = entity.getComponent(componentClass) - .orElseGet(zero); - - return context.renderCoordinate(entity, renderingSystem()); - } - - -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/package-info.java b/src/main/java/com/demcha/compose/engine/render/guides/package-info.java deleted file mode 100644 index 319b4e47e..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Debug guide-line rendering contracts and implementations. - * - *

Ownership: Owned by render diagnostics and used only when guide overlays are enabled.

- *

Extension rules: Extend per backend without changing document authoring APIs.

- */ -package com.demcha.compose.engine.render.guides; \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/components/geometry/EntityBoundsTest.java b/src/test/java/com/demcha/compose/engine/components/geometry/EntityBoundsTest.java deleted file mode 100644 index 951b8fb6b..000000000 --- a/src/test/java/com/demcha/compose/engine/components/geometry/EntityBoundsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class EntityBoundsTest { - - @Test - void shouldCalculateBoundsFromPlacementContentSizeAndMargin() { - Entity entity = new Entity(); - entity.addComponent(new Placement(20, 40, 100, 60, 0, 0)); - entity.addComponent(new ContentSize(100, 60)); - entity.addComponent(new Margin(7, 11, 13, 17)); - - assertThat(EntityBounds.topLine(entity)).isEqualTo(107.0); - assertThat(EntityBounds.bottomLine(entity)).isEqualTo(27.0); - assertThat(EntityBounds.rightLine(entity)).isEqualTo(131.0); - assertThat(EntityBounds.leftLine(entity)).isEqualTo(3.0); - } - - @SuppressWarnings("deprecation") - @Test - void shouldExposeSameBoundsThroughDeprecatedEntityWrappers() { - Entity entity = new Entity(); - entity.addComponent(new Placement(12, 25, 40, 30, 0, 0)); - entity.addComponent(new ContentSize(40, 30)); - entity.addComponent(new Margin(2, 3, 5, 7)); - - assertThat(entity.boundingTopLine()).isEqualTo(EntityBounds.topLine(entity)); - assertThat(entity.boundingBottomLine()).isEqualTo(EntityBounds.bottomLine(entity)); - assertThat(entity.boundingRightLine()).isEqualTo(EntityBounds.rightLine(entity)); - assertThat(entity.boundingLeftLine()).isEqualTo(EntityBounds.leftLine(entity)); - } -} diff --git a/src/test/java/com/demcha/compose/engine/components/geometry/InnerBoxSizeTest.java b/src/test/java/com/demcha/compose/engine/components/geometry/InnerBoxSizeTest.java deleted file mode 100644 index c201f3fda..000000000 --- a/src/test/java/com/demcha/compose/engine/components/geometry/InnerBoxSizeTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.exceptions.ContentSizeNotFoundException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - - -class InnerBoxSizeTest { - @Nested - @DisplayName("from(ContentSize, Padding)") - class FromContentAndPadding { - - @ParameterizedTest(name = "content=({0},{1}), padding=({2},{3},{4},{5}) → inner=({6},{7})") - @CsvSource({ - // width, height, top, right, bottom, left, expectedInnerW, expectedInnerH - "100, 200, 0, 0, 0, 0, 100, 200", - "100, 200, 5, 10, 15, 20, 70, 180", // W:100-(20+10)=70; H:200-(5+15)=180 - "50, 40, 10, 5, 10, 5, 40, 20", - "50.5, 40.25, 1.5, 2.5, 3.5, 4.5, 43.5, 35.25", - // padding larger than content → negative inner size (current behavior) - "10, 10, 6, 6, 6, 6, -2, -2" - }) - void computesInnerSizeCorrectly(double w, double h, - double top, double right, double bottom, double left, - double expectedInnerW, double expectedInnerH) { - ContentSize content = new ContentSize(w, h); - Padding padding = new Padding(top, right, bottom, left); - - Optional result = InnerBoxSize.from(content, padding); - - assertThat(result).isPresent(); - InnerBoxSize box = result.get(); - assertThat(box.width()).isEqualTo(expectedInnerW); - assertThat(box.height()).isEqualTo(expectedInnerH); - } - - @Test - void nullContent_throwsContentSizeNotFoundException() { - Padding padding = Padding.zero(); - assertThatThrownBy(() -> InnerBoxSize.from(null, padding)) - .isInstanceOf(ContentSizeNotFoundException.class); - } - - @Test - void nullPadding_throwsNullPointerWithMessage() { - ContentSize content = new ContentSize(10, 20); - assertThatThrownBy(() -> InnerBoxSize.from(content, null)) - .isInstanceOf(NullPointerException.class) - .hasMessage("Padding cannot be null."); - } - } - - @Nested - @DisplayName("from(Entity)") - class FromEntity { - - @Test - void returnsInnerSize_whenContentSizeAndPaddingPresent() { - Entity entity = mock(Entity.class); - - ContentSize content = new ContentSize(100, 200); - Padding padding = new Padding(5, 10, 15, 20); // top,right,bottom,left - - when(entity.getComponent(ContentSize.class)).thenReturn(Optional.of(content)); - when(entity.getComponent(Padding.class)).thenReturn(Optional.of(padding)); - - Optional result = InnerBoxSize.from(entity); - - assertThat(result).isPresent(); - InnerBoxSize box = result.get(); - // W = 100 - (left+right) = 100 - (20+10) = 70 - // H = 200 - (top+bottom) = 200 - (5+15) = 180 - assertThat(box.width()).isEqualTo(70.0); - assertThat(box.height()).isEqualTo(180.0); - } - - @Test - void usesZeroPadding_whenPaddingMissing() { - Entity entity = mock(Entity.class); - ContentSize content = new ContentSize(80, 60); - - when(entity.getComponent(ContentSize.class)).thenReturn(Optional.of(content)); - when(entity.getComponent(Padding.class)).thenReturn(Optional.empty()); // Padding.zero() - - Optional result = InnerBoxSize.from(entity); - - assertThat(result).isPresent(); - InnerBoxSize box = result.get(); - assertThat(box.width()).isEqualTo(80.0); - assertThat(box.height()).isEqualTo(60.0); - } - - @Test - void throwsContentSizeNotFound_whenContentSizeMissing() { - Entity entity = mock(Entity.class); - - when(entity.getComponent(ContentSize.class)).thenReturn(Optional.empty()); - when(entity.getComponent(Padding.class)).thenReturn(Optional.of(Padding.zero())); - - assertThatThrownBy(() -> InnerBoxSize.from(entity)) - .isInstanceOf(ContentSizeNotFoundException.class); - } - - - } -} \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/components/geometry/OuterBoxSizeTest.java b/src/test/java/com/demcha/compose/engine/components/geometry/OuterBoxSizeTest.java deleted file mode 100644 index 08ed98e28..000000000 --- a/src/test/java/com/demcha/compose/engine/components/geometry/OuterBoxSizeTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.exceptions.ContentSizeNotFoundException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -class OuterBoxSizeTest { - - @Nested - @DisplayName("from(ContentSize, Margin)") - class FromContentAndMargin { - - @ParameterizedTest(name = "content=({0},{1}), margin=({2},{3},{4},{5}) → outer=({6},{7})") - @CsvSource({ - // width, height, top, right, bottom, left, expectedW, expectedH - "100, 200, 0, 0, 0, 0, 100, 200", - "100, 200, 5, 10, 15, 20, 130, 220", // w + (left+right) = 100 + (20+10) = 130; h + (top+bottom) = 200 + (5+15) = 220 - "0, 0, 1, 2, 3, 4, 6, 4", - "50.5, 75.25, 0.5, 1.5, 2.5, 3.5, 55.5, 78.25" - }) - void sumsContentAndMarginCorrectly(double w, double h, - double top, double right, double bottom, double left, - double expectedW, double expectedH) { - ContentSize content = new ContentSize(w, h); - Margin margin = new Margin(top, right, bottom, left); - - Optional result = OuterBoxSize.from(content, margin); - - assertThat(result) - .isPresent() - .get() - .satisfies(box -> { - assertThat(box.width()).isEqualTo(expectedW); - assertThat(box.height()).isEqualTo(expectedH); - }); - } - - @Test - void nullContent_throwsNullPointerBecauseOfLombokNonNull() { - Margin margin = Margin.zero(); - assertThatThrownBy(() -> OuterBoxSize.from(null, margin)) - .isInstanceOf(ContentSizeNotFoundException.class); - } - - @Test - void nullMargin_throwsNullPointerBecauseOfLombokNonNull() { - ContentSize content = new ContentSize(10, 20); - assertThatThrownBy(() -> OuterBoxSize.from(content, null)) - .isInstanceOf(NullPointerException.class); - } - } - - @Nested - @DisplayName("from(Entity)") - class FromEntity { - - @Test - void returnsOuterSize_whenContentSizeAndMarginPresent() { - // Arrange - Entity entity = mock(Entity.class); - - ContentSize content = new ContentSize(100, 200); - Margin margin = new Margin(5, 10, 15, 20); // top,right,bottom,left - - when(entity.getComponent(ContentSize.class)).thenReturn(Optional.of(content)); - when(entity.getComponent(Margin.class)).thenReturn(Optional.of(margin)); - - // Act - Optional result = OuterBoxSize.from(entity); - - // Assert - assertThat(result).isPresent(); - OuterBoxSize box = result.get(); - // width = 100 + (left+right) = 100 + (20+10) = 130 - // height = 200 + (top+bottom) = 200 + (5+15) = 220 - assertThat(box.width()).isEqualTo(130.0); - assertThat(box.height()).isEqualTo(220.0); - } - - @Test - void usesZeroMargin_whenMarginMissing() { - Entity entity = mock(Entity.class); - ContentSize content = new ContentSize(80, 60); - - when(entity.getComponent(ContentSize.class)).thenReturn(Optional.of(content)); - when(entity.getComponent(Margin.class)).thenReturn(Optional.empty()); // Margin.zero() - - Optional result = OuterBoxSize.from(entity); - - assertThat(result).isPresent(); - OuterBoxSize box = result.get(); - assertThat(box.width()).isEqualTo(80.0); - assertThat(box.height()).isEqualTo(60.0); - } - - @Test - void throwsContentSizeNotFound_whenContentSizeMissing() { - Entity entity = mock(Entity.class); - - when(entity.getComponent(ContentSize.class)).thenReturn(Optional.empty()); - when(entity.getComponent(Margin.class)).thenReturn(Optional.of(Margin.zero())); - - assertThatThrownBy(() -> OuterBoxSize.from(entity)) - .isInstanceOf(ContentSizeNotFoundException.class); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/components/layout/ComputedPositionTest.java b/src/test/java/com/demcha/compose/engine/components/layout/ComputedPositionTest.java deleted file mode 100644 index 47621b345..000000000 --- a/src/test/java/com/demcha/compose/engine/components/layout/ComputedPositionTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.PaddingCoordinate; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.style.Margin; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.MockedStatic; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -class ComputedPositionTest { - - @Test - @DisplayName("zero() returns (0,0)") - void zero_returns00() { - ComputedPosition cp = ComputedPosition.zero(); - assertThat(cp.x()).isZero(); - assertThat(cp.y()).isZero(); - } - - @Test - @DisplayName("from(child, innerBox) uses existing Anchor.getComputedPosition(...)") - void from_child_inner_usesExistingAnchor() { - // Arrange - Entity child = mock(Entity.class); - Anchor anchor = mock(Anchor.class); - InnerBoxSize inner = new InnerBoxSize(100, 200); - ComputedPosition expected = new ComputedPosition(12.5, 34.5); - - when(child.getComponent(Anchor.class)).thenReturn(Optional.of(anchor)); - when(anchor.getComputedPosition(child, inner)).thenReturn(expected); - - - // Act - ComputedPosition actual = ComputedPosition.from(child, inner, PaddingCoordinate.zero()); - - // Assert - assertThat(actual).isEqualTo(expected); - verify(child, never()).addComponent(any()); // already had anchor - verify(anchor).getComputedPosition(child, inner); - } - - @Test - @DisplayName("from(child, innerBox) creates and adds default Anchor when missing, then delegates") - void from_child_inner_addsDefaultAnchorWhenMissing() { - // Arrange - Entity child = mock(Entity.class); - InnerBoxSize inner = new InnerBoxSize(80, 60); - Anchor defaultAnchor = mock(Anchor.class); - Position position = mock(Position.class); - ComputedPosition expected = new ComputedPosition(5, 7); - - when(child.getComponent(ContentSize.class)).thenReturn(Optional.of(new ContentSize(80,80))); - - // FIX: Simulate that the Anchor component is MISSING. - when(child.getComponent(Anchor.class)).thenReturn(Optional.empty()); - when (child.getComponent(Position.class)).thenReturn(Optional.of(new Position(5,7))); - - // Mock the static method that is *actually* used for the default anchor. - try (MockedStatic ms = mockStatic(Anchor.class)) { - ms.when(Anchor::defaultAnchor).thenReturn(defaultAnchor); - when(defaultAnchor.getComputedPosition(child, inner)).thenReturn(expected); - - - // Act - - ComputedPosition actual = ComputedPosition.from(child, inner, PaddingCoordinate.zero()); - - // Assert - assertThat(actual).isEqualTo(expected); - verify(child).addComponent(defaultAnchor); // This will now be called - verify(defaultAnchor).getComputedPosition(child, inner); // This will now be called - ms.verify(Anchor::defaultAnchor); // This will now be called - } - } - - @Test - @DisplayName("from(child, parent) calls InnerBoxSize.from(parent) and delegates to anchor") - void from_child_parent_usesInnerBoxFromParent() { - // Arrange - Entity child = mock(Entity.class); - Entity parent = mock(Entity.class); - Anchor anchor = mock(Anchor.class); - InnerBoxSize parentInner = new InnerBoxSize(120, 240); - ComputedPosition expected = new ComputedPosition(10, 20); - - // Anchor present on child - when(child.getComponent(Anchor.class)).thenReturn(Optional.of(anchor)); - when(anchor.getComputedPosition(eq(child), eq(parentInner))).thenReturn(expected); - - // If PaddingCoordinate.from(...) looks up ComputedPosition on the entity, stub it: - when(child.getComponent(ComputedPosition.class)).thenReturn(Optional.of(ComputedPosition.zero())); - - try (MockedStatic innerMs = mockStatic(InnerBoxSize.class); - MockedStatic padMs = mockStatic(PaddingCoordinate.class)) { - - innerMs.when(() -> InnerBoxSize.from(parent)).thenReturn(Optional.of(parentInner)); - - // Return a harmless padding so the code path is satisfied - padMs.when(() -> PaddingCoordinate.from(any())).thenReturn(new PaddingCoordinate(0, 0)); - - // Act - ComputedPosition actual = ComputedPosition.from(child, parent); - - // Assert - assertThat(actual).isEqualTo(expected); - innerMs.verify(() -> InnerBoxSize.from(parent)); - verify(anchor).getComputedPosition(child, parentInner); - } - } - - - @Test - @DisplayName("from(child, pageSize) wraps Canvas in InnerBoxSize(width,height) and delegates") - void from_child_pageSize_wrapsIntoInnerBox() { - // Arrange - Entity child = mock(Entity.class); - Anchor anchor = mock(Anchor.class); - Canvas pageSize = mock(Canvas.class); - when(pageSize.width()).thenReturn(595.0f); - when(pageSize.height()).thenReturn(842.0f); - when(pageSize.margin()).thenReturn(Margin.zero()); - - when(child.getComponent(Anchor.class)).thenReturn(Optional.of(anchor)); - ComputedPosition expected = new ComputedPosition(3, 4); - - // We want to capture the InnerBoxSize passed to anchor.getComputedPosition - ArgumentCaptor innerCaptor = ArgumentCaptor.forClass(InnerBoxSize.class); - when(anchor.getComputedPosition(eq(child), innerCaptor.capture())).thenReturn(expected); - - // Act - ComputedPosition actual = ComputedPosition.from(child, pageSize); - - // Assert - assertThat(actual).isEqualTo(expected); - - InnerBoxSize innerUsed = innerCaptor.getValue(); - assertThat(innerUsed.width()).isEqualTo(595.0); - assertThat(innerUsed.height()).isEqualTo(842.0); - - verify(anchor).getComputedPosition(eq(child), any(InnerBoxSize.class)); - } -} - diff --git a/src/test/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPositionTest.java b/src/test/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPositionTest.java deleted file mode 100644 index 149bf05c0..000000000 --- a/src/test/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPositionTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class ComputedPositionTest { - - @Test - void from() { - } - - @Test - void testFrom() { - } - - @Test - void testFrom1() { - } - - @Test - void paddingCoordinate() { - } -} \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/components/layout/coordinator/PlacementTest.java b/src/test/java/com/demcha/compose/engine/components/layout/coordinator/PlacementTest.java deleted file mode 100644 index 093bf0ad5..000000000 --- a/src/test/java/com/demcha/compose/engine/components/layout/coordinator/PlacementTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.layout.coordinator.Placement; - -import static org.mockito.Mockito.*; - -class PlacementTest { - - public static Placement mockPlacement() { - Placement placement = mock(Placement.class); - when(placement.x()).thenReturn(0.0); - return placement; - } - -} \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/components/layout/coordinator/RenderingPositionTest.java b/src/test/java/com/demcha/compose/engine/components/layout/coordinator/RenderingPositionTest.java deleted file mode 100644 index b15923c29..000000000 --- a/src/test/java/com/demcha/compose/engine/components/layout/coordinator/RenderingPositionTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.demcha.compose.engine.components.layout.coordinator; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.components.layout.coordinator.RenderingPosition; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static org.mockito.Mockito.*; - -class RenderingPositionTest { - - static Entity createEntity(RenderingPosition renderingPosition, ComputedPosition computedPosition) { - Entity e = mock(Entity.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); - doReturn(Optional.of(computedPosition)).when(e).getComponent(ComputedPosition.class); - doReturn(Optional.of(renderingPosition)).when(e).getComponent(Position.class); - return e; - } - - static Component createComponent(double x, double y) { - ComputedPosition position = mock(ComputedPosition.class); - when(position.x()).thenReturn(x); - when(position.y()).thenReturn(y); - return position; - } - - static RenderingPosition createRenderingPosition(double x, double y) { - RenderingPosition position = mock(RenderingPosition.class); - when(position.x()).thenReturn(x); - when(position.y()).thenReturn(y); - return position; - } - - @Test - void fromEntity() { - - } - - - -} \ No newline at end of file diff --git a/src/test/java/com/demcha/mock/FactoryClasses.java b/src/test/java/com/demcha/mock/FactoryClasses.java deleted file mode 100644 index 9b3790e47..000000000 --- a/src/test/java/com/demcha/mock/FactoryClasses.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.demcha.mock; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.mock.data.CanvasData; -import com.demcha.mock.data.MarginData; -import com.demcha.mock.data.OffsetData; -import com.demcha.mock.data.SizeData; -import com.demcha.compose.engine.pagination.Offset; - -import java.util.Optional; - -import static org.mockito.Mockito.*; - -public class FactoryClasses { - - - @SafeVarargs - public static Entity entityMock(T... components) { - Entity e = mock(Entity.class); - - for (T c : components) { - doReturn(Optional.of(c)) - .when(e) - .getComponent(c.getClass()); - } - - return e; - } - - - public static Offset offsetMock(OffsetData offsetData) { - Offset offset = mock(Offset.class); - when(offset.x()).thenReturn(offsetData.x()); - when(offset.y()).thenReturn(offsetData.y()); - return offset; - } - - public static Offset offsetReal(OffsetData offsetData) { - Offset offset = new Offset(); - offset.incrementX(offsetData.x()); - offset.incrementY(offsetData.y()); - return offset; - } - - - public static ContentSize mockContentSize(SizeData sizeData) { - ContentSize contentSize = mock(ContentSize.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); - - when(contentSize.width()).thenReturn(sizeData.width()); - when(contentSize.height()).thenReturn(sizeData.height()); - return contentSize; - } - - public static Margin margin(MarginData marginData) { - // It is safer to use standard mock() instead of CALLS_REAL_METHODS - // if you are going to stub all methods anyway. - Margin margin = mock(Margin.class); - - // NOTICE: .when(margin).top() <-- Parenthesis closes after 'margin' - doReturn(marginData.top()).when(margin).top(); - doReturn(marginData.bottom()).when(margin).bottom(); - doReturn(marginData.right()).when(margin).right(); - doReturn(marginData.left()).when(margin).left(); - - return margin; - } - - public static Canvas canvasMock(CanvasData canvasData) { - // 1. Prepare the sub-component (Margin) first - // We reuse your existing 'margin' method to create a fully configured Margin mock - Margin marginMock = margin(canvasData.margin()); - - // 2. Create the Canvas mock - Canvas canvas = mock(Canvas.class); - - // 3. Link the Margin mock to the Canvas mock - // Correct Syntax: .when(mock).method() - doReturn(marginMock).when(canvas).margin(); - - // 4. Stub simple properties - doReturn((float) canvasData.width()).when(canvas).width(); - doReturn((float) canvasData.height()).when(canvas).height(); - - // 5. Stub calculated properties - // Notice the parenthesis: .when(canvas) is closed before calling .innerHeigh() - doReturn(canvasData.height() - canvasData.margin().top() - canvasData.margin().bottom()) - .when(canvas).innerHeigh(); - - doReturn(canvasData.width() - canvasData.margin().left() - canvasData.margin().right()) - .when(canvas).innerWidth(); - - // 6. Stub bounding lines - doReturn((float) canvasData.margin().bottom()).when(canvas).boundingBottomLine(); - doReturn((float) (canvasData.height() - canvasData.margin().top())).when(canvas).boundingTopLine(); - doReturn((float) (canvasData.margin().left())).when(canvas).boundingLeftLine(); - doReturn((float) (canvasData.width() - canvasData.margin().right())).when(canvas).boundingRightLine(); - - return canvas; - } -} - diff --git a/src/test/java/com/demcha/mock/FactoryPresets.java b/src/test/java/com/demcha/mock/FactoryPresets.java deleted file mode 100644 index f86b7a105..000000000 --- a/src/test/java/com/demcha/mock/FactoryPresets.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.demcha.mock; - -import com.demcha.mock.data.CanvasData; -import com.demcha.mock.data.MarginData; -import com.demcha.mock.data.OffsetData; -import com.demcha.mock.data.SizeData; - -public class FactoryPresets { - - // --- Margins (General) --- - public static final MarginData MARGIN_ZERO = MarginData.all(0); - public static final MarginData MARGIN_STANDARD_20 = MarginData.all(20); - public static final MarginData MARGIN_ASYMMETRIC_10_20_30_40 = new MarginData(10, 20, 30, 40); - - // --- Margins (Test / Vertical Specific) --- - // Top=10, Bot=10 (Симметричные вертикальные) - public static final MarginData MARGIN_VERT_10 = new MarginData(10, 10, 0, 0); - public static final MarginData MARGIN_VERT_1 = new MarginData(1, 1, 0, 0); - // Top=5, Bot=5 - public static final MarginData MARGIN_VERT_5 = new MarginData(5, 5, 0, 0); - // Canvas Margins: Top=10, Bot=20 (Несимметричные) - public static final MarginData MARGIN_CANVAS_TOP10_BOT20 = new MarginData(10, 20, 0, 0); - // Canvas Margins: Top=10, Bot=10 - public static final MarginData MARGIN_CANVAS_TOP10_BOT10 = new MarginData(10, 10, 0, 0); - - - // --- Objects (Standard) --- - public static final SizeData OBJ_SMALL_BOX_100_50 = new SizeData(100, 50); - public static final SizeData OBJ_FULL_WIDTH_500_100 = new SizeData(500, 100); - - // --- Objects (Test / Low Height) --- - public static final SizeData OBJ_W100_H20 = new SizeData(100, 20); - public static final SizeData OBJ_W100_H60 = new SizeData(100, 60); - public static final SizeData OBJ_W100_H40 = new SizeData(100, 40); - - - // --- Canvases (Standard A4) --- - public static final CanvasData CANVAS_A4_STANDARD = new CanvasData(595, 842, MARGIN_STANDARD_20); - public static final CanvasData CANVAS_A4_NO_MARGINS = new CanvasData(595, 842, MARGIN_ZERO); - - // --- Canvases (Test / Small Area) --- - // 100x100, Margins: Top 10, Bot 20 - public static final CanvasData CANVAS_TEST_100_100_M_T10_B20 = new CanvasData(100, 100, MARGIN_CANVAS_TOP10_BOT20); - - // 100x200, Margins: Top 10, Bot 20 - public static final CanvasData CANVAS_TEST_100_200_M_T10_B20 = new CanvasData(100, 200, MARGIN_CANVAS_TOP10_BOT20); - - // 100x100, Margins: Top 10, Bot 10 - public static final CanvasData CANVAS_TEST_100_100_M_10 = new CanvasData(100, 100, MARGIN_CANVAS_TOP10_BOT10); - - - // --- Offsets --- - public static final OffsetData OFFSET_DATA_ZERO = OffsetData.zero(); - public static final OffsetData OFFSET_DATA_ALL_2 = OffsetData.all(2); - public static final OffsetData OFFSET_DATA_ALL_NEGATIVE_2 = OffsetData.all(-2); - public static final OffsetData OFFSET_DATA_ALL_15 = OffsetData.all(15); - public static final OffsetData OFFSET_DATA_NEGATIVE_ALL_40 = OffsetData.all(-40); - - // --- Objects (For Splitting Tests) --- - // Height 150: Will require split on a 100px canvas - public static final SizeData OBJ_TALL_100_150 = new SizeData(100, 150); - public static final SizeData OBJ_TALL_5_12 = new SizeData(5, 12); - public static final SizeData OBJ_TALL_5_6 = new SizeData(5, 6); - // Height 250: Might require 3 pages on a 100px canvas - public static final SizeData OBJ_VERY_TALL_100_250 = new SizeData(100, 250); - - - // --- Canvases (For Splitting Tests) --- - // 100x100, Margins: Top 10, Bot 10. Safe Area = 80px. - // Useful for easy math: Total 100, Safe 80. - public static final CanvasData CANVAS_SPLIT_100_M10 = new CanvasData(100, 100, MARGIN_VERT_10); - public static final CanvasData CANVAS_SPLIT_5_M1 = new CanvasData(5, 5, MARGIN_VERT_1); -} \ No newline at end of file diff --git a/src/test/java/com/demcha/mock/data/CanvasData.java b/src/test/java/com/demcha/mock/data/CanvasData.java deleted file mode 100644 index f7696ec1c..000000000 --- a/src/test/java/com/demcha/mock/data/CanvasData.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.demcha.mock.data; - -public record CanvasData(double width, double height, MarginData margin) { -} diff --git a/src/test/java/com/demcha/mock/data/MarginData.java b/src/test/java/com/demcha/mock/data/MarginData.java deleted file mode 100644 index 78b87ae92..000000000 --- a/src/test/java/com/demcha/mock/data/MarginData.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.demcha.mock.data; - -public record MarginData(double top, double bottom, double right, double left) { - public static MarginData all(double i) { - return new MarginData(i, i, i, i); - } -} diff --git a/src/test/java/com/demcha/mock/data/OffsetData.java b/src/test/java/com/demcha/mock/data/OffsetData.java deleted file mode 100644 index fb9842dcf..000000000 --- a/src/test/java/com/demcha/mock/data/OffsetData.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.demcha.mock.data; - -public record OffsetData(double x, double y) { - public static OffsetData zero() { - return new OffsetData(0, 0); - } - public static OffsetData all(double offset) { - return new OffsetData(offset, offset); - } -} diff --git a/src/test/java/com/demcha/mock/data/SizeData.java b/src/test/java/com/demcha/mock/data/SizeData.java deleted file mode 100644 index ac8320e37..000000000 --- a/src/test/java/com/demcha/mock/data/SizeData.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.demcha.mock.data; - -public record SizeData(double width, double height) { -} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 2adb12613..f33257512 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -93,24 +93,8 @@ - - - - - - - - - - - - - - - -