/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.layout.process.alignment;
import org.pentaho.reporting.engine.classic.core.layout.model.InlineRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes;
import org.pentaho.reporting.engine.classic.core.layout.model.PageGrid;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.SpacerRenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.SplittableRenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.context.BoxDefinition;
import org.pentaho.reporting.engine.classic.core.layout.model.context.StaticBoxLayoutProperties;
import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData;
import org.pentaho.reporting.engine.classic.core.layout.process.layoutrules.EndSequenceElement;
import org.pentaho.reporting.engine.classic.core.layout.process.layoutrules.InlineSequenceElement;
import org.pentaho.reporting.engine.classic.core.layout.process.layoutrules.SequenceList;
import org.pentaho.reporting.engine.classic.core.layout.process.layoutrules.StartSequenceElement;
import org.pentaho.reporting.engine.classic.core.style.StyleSheet;
import org.pentaho.reporting.engine.classic.core.style.TextStyleKeys;
import org.pentaho.reporting.engine.classic.core.style.WhitespaceCollapse;
import org.pentaho.reporting.engine.classic.core.util.LongList;
import org.pentaho.reporting.libraries.base.util.FastStack;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Todo: The whole horizontal alignment is not suitable for spanned page breaks.
*
* @author Thomas Morgner
*/
public abstract class AbstractAlignmentProcessor implements TextAlignmentProcessor, LastLineTextAlignmentProcessor {
private static final InlineSequenceElement[] EMPTY_ELEMENTS = new InlineSequenceElement[0];
private static final RenderNode[] EMPTY_NODES = new RenderNode[0];
private long startOfLine;
private long endOfLine;
private long[] pagebreaks;
private int pagebreakCount;
private PageGrid pageGrid;
private InlineSequenceElement[] sequenceElements = AbstractAlignmentProcessor.EMPTY_ELEMENTS;
private RenderNode[] nodes = AbstractAlignmentProcessor.EMPTY_NODES;
private int sequenceFill;
/**
* A layouter hint, that indicates a possibly breakable element
*/
private int breakableIndex;
/**
* A layouter hint, that indicates where to continue on unbreakable elements.
*/
private int skipIndex;
/**
* A layouter hint, that shows up the maximum element's width to fit into line's limit. This value should be accessed
* only if {@code breakableIndex != -1}, as {@code breakableIndex} refers to the element
*/
private long breakableMaxAllowedWidth;
private long[] elementPositions;
private long[] elementDimensions;
private FastStack<RenderBox> contexts;
private ArrayList<RenderNode> pendingElements;
private static final long[] EMPTY = new long[0];
private boolean lastLineAlignment;
private LeftAlignmentProcessor leftAlignProcessor;
private boolean overflowX;
private LongList pageLongList;
protected AbstractAlignmentProcessor() {
this.contexts = new FastStack<RenderBox>( 50 );
this.pendingElements = new ArrayList<RenderNode>();
this.elementDimensions = AbstractAlignmentProcessor.EMPTY;
this.elementPositions = AbstractAlignmentProcessor.EMPTY;
}
public boolean isLastLineAlignment() {
return lastLineAlignment;
}
protected long getStartOfLine() {
return startOfLine;
}
protected PageGrid getPageGrid() {
return pageGrid;
}
protected InlineSequenceElement[] getSequenceElements() {
return sequenceElements;
}
protected RenderNode[] getNodes() {
return nodes;
}
protected long[] getElementPositions() {
return elementPositions;
}
protected long[] getElementDimensions() {
return elementDimensions;
}
protected long getEndOfLine() {
return endOfLine;
}
public int getPagebreakCount() {
return pagebreakCount;
}
protected long getPageBreak( final int pageIndex ) {
if ( pageIndex < 0 || pageIndex >= pagebreakCount ) {
throw new IndexOutOfBoundsException();
}
return pagebreaks[pageIndex];
}
protected long[] getPageBreaks() {
return pagebreaks;
}
protected void updatePageBreaks( final long[] pagebreaks, final int pageBreakCount ) {
this.pagebreakCount = pageBreakCount;
this.pagebreaks = pagebreaks;
}
protected int getBreakableIndex() {
return breakableIndex;
}
protected void setBreakableIndex( final int breakableIndex ) {
this.breakableIndex = breakableIndex;
}
protected int getSkipIndex() {
return skipIndex;
}
protected void setSkipIndex( final int skipIndex ) {
this.skipIndex = skipIndex;
}
protected long getBreakableMaxAllowedWidth() {
return breakableMaxAllowedWidth;
}
protected void setBreakableMaxAllowedWidth( long breakableMaxAllowedWidth ) {
this.breakableMaxAllowedWidth = breakableMaxAllowedWidth;
}
/**
* Processes the text and calls the layouting methods. This method returns the index of the last element that fits on
* the current line.
*
* @param elements
* @param maxPos
* @return
*/
protected int iterate( final InlineSequenceElement[] elements, final int maxPos ) {
breakableIndex = -1;
breakableMaxAllowedWidth = -1;
skipIndex = -1;
// The state transitions are as follows:
// ......From....START...CONTENT...END
// to...START....-.......X.........X
// ...CONTENT....-.......X.........X
// .......END....-.......-.........-
//
// Dash signals, that there is no break opportunity,
// while X means, that it is possible to break the inline flow at that
// position.
if ( maxPos == 0 ) {
// nothing to do ..
return 0;
}
int lastElementType = elements[0].getClassification();
int startIndex = 0;
boolean lastNodeWasSpacer =
( lastElementType == InlineSequenceElement.CONTENT && nodes[0].getNodeType() == LayoutNodeTypes.TYPE_NODE_SPACER );
for ( int i = 1; i < maxPos; i++ ) {
final InlineSequenceElement element = elements[i];
final int elementType = element.getClassification();
if ( lastNodeWasSpacer == false && lastElementType != InlineSequenceElement.START
&& elementType != InlineSequenceElement.END ) {
final int newIndex = handleElement( startIndex, i - startIndex );
if ( newIndex <= startIndex ) {
return startIndex;
}
startIndex = i;
}
lastNodeWasSpacer =
( elementType == InlineSequenceElement.CONTENT && nodes[i].getNodeType() == LayoutNodeTypes.TYPE_NODE_SPACER );
lastElementType = elementType;
}
return handleElement( startIndex, maxPos - startIndex );
}
/**
* Initializes the alignment process. The start and end parameters specify the line boundaries, and have been
* precomputed.
*
* @param sequence
* @param start
* @param end
* @param breaks
*/
public void initialize( final OutputProcessorMetaData metaData, final SequenceList sequence, final long start,
final long end, final PageGrid breaks, final boolean overflowX ) {
if ( sequence == null ) {
throw new NullPointerException();
}
if ( metaData == null ) {
throw new NullPointerException();
}
if ( breaks == null ) {
throw new NullPointerException();
}
if ( end < start ) {
// This is most certainly an error, treat it as such ..
throw new IllegalArgumentException( "Start is <= end; which is stupid!: " + end + ' ' + start );
}
this.overflowX = overflowX;
this.sequenceElements = sequence.getSequenceElements( this.sequenceElements );
this.nodes = sequence.getNodes( this.nodes );
this.sequenceFill = sequence.size();
this.pageGrid = breaks;
if ( elementPositions.length < sequenceFill ) {
this.elementPositions = new long[sequenceFill];
} else {
Arrays.fill( this.elementPositions, 0 );
}
if ( elementDimensions.length < sequenceFill ) {
this.elementDimensions = new long[sequenceFill];
} else {
Arrays.fill( this.elementDimensions, 0 );
}
updateLineSize( start, end );
}
public void updateLineSize( final long start, final long end ) {
// to be computed by the pagegrid ..
if ( startOfLine != start || endOfLine != end || pagebreaks == null ) {
this.startOfLine = start;
this.endOfLine = end;
updateBreaks();
}
}
public void deinitialize() {
this.pageGrid = null;
this.pendingElements.clear();
this.contexts.clear();
this.sequenceFill = 0;
}
private void updateBreaks() {
final long[] horizontalBreaks = pageGrid.getHorizontalBreaks();
final int breakCount = horizontalBreaks.length;
if ( pageLongList == null ) {
pageLongList = new LongList( breakCount );
} else {
pageLongList.clear();
}
for ( int i = 0; i < breakCount; i++ ) {
final long pos = horizontalBreaks[i];
if ( pos <= startOfLine ) {
// skip ..
continue;
}
if ( pos >= endOfLine ) {
break;
}
if ( overflowX == false || ( i < ( breakCount - 1 ) ) ) {
pageLongList.add( pos );
}
}
pageLongList.add( endOfLine );
this.pagebreaks = pageLongList.toArray( this.pagebreaks );
this.pagebreakCount = pageLongList.size();
}
public boolean hasNext() {
return sequenceFill > 0;
}
public RenderBox next() {
cleanFirstSpacers();
Arrays.fill( elementDimensions, 0 );
Arrays.fill( elementPositions, 0 );
int lastPosition = iterate( sequenceElements, sequenceFill );
if ( lastPosition == 0 ) {
if ( splitBreakableIfPossible() ) {
return next();
}
if ( getSkipIndex() >= 0 ) {
// This causes an overflow ..
performSkipAlignment( getSkipIndex() );
lastPosition = getSkipIndex();
} else {
// Skip the complete line. Oh, thats not good, really!
lastPosition = sequenceFill;
}
}
// now, build the line and update the array ..
pendingElements.clear();
contexts.clear();
RenderBox firstBox = null;
RenderBox box = null;
for ( int i = 0; i < lastPosition; i++ ) {
final RenderNode node = nodes[i];
final InlineSequenceElement element = sequenceElements[i];
if ( element instanceof EndSequenceElement ) {
contexts.pop();
final long boxX2 = ( elementPositions[i] + elementDimensions[i] );
box.setCachedWidth( boxX2 - box.getCachedX() );
if ( contexts.isEmpty() ) {
box = null;
} else {
final RenderNode tmpnode = box;
box = contexts.peek();
box.addGeneratedChild( tmpnode );
}
continue;
}
if ( element instanceof StartSequenceElement ) {
box = (RenderBox) node.derive( false );
box.setCachedX( elementPositions[i] );
contexts.push( box );
if ( firstBox == null ) {
firstBox = box;
}
continue;
}
if ( box == null ) {
throw new IllegalStateException( "Invalid sequence: " + "Cannot have elements before we open the box context." );
}
// Content element: Perform a deep-deriveForAdvance, so that we preserve the
// possibly existing sub-nodes.
final RenderNode child = node.derive( true );
child.setCachedX( elementPositions[i] );
child.setCachedWidth( elementDimensions[i] );
if ( box.getStaticBoxLayoutProperties().isPreserveSpace()
&& box.getStyleSheet().getBooleanStyleProperty( TextStyleKeys.TRIM_TEXT_CONTENT ) == false ) {
// Take a shortcut as we know that we will never have any pending elements if preserve is true and
// trim-content is false.
box.addGeneratedChild( child );
continue;
}
if ( child.isIgnorableForRendering() ) {
pendingElements.add( child );
} else {
for ( int j = 0; j < pendingElements.size(); j++ ) {
final RenderNode pendingNode = pendingElements.get( j );
box.addGeneratedChild( pendingNode );
}
pendingElements.clear();
box.addGeneratedChild( child );
}
}
// Remove all spacers and other non printable content that might
// look ugly at the beginning of a new line ..
for ( ; lastPosition < sequenceFill; lastPosition++ ) {
final RenderNode node = nodes[lastPosition];
final StyleSheet styleSheet = node.getStyleSheet();
if ( WhitespaceCollapse.PRESERVE.equals( styleSheet.getStyleProperty( TextStyleKeys.WHITE_SPACE_COLLAPSE ) )
&& styleSheet.getBooleanStyleProperty( TextStyleKeys.TRIM_TEXT_CONTENT ) == false ) {
break;
}
if ( node.isIgnorableForRendering() == false ) {
break;
}
}
// If there are open contexts, then add the split-result to the new line
// and update the width of the current line
RenderBox previousContext = null;
final int openContexts = contexts.size();
for ( int i = 0; i < openContexts; i++ ) {
final RenderBox renderBox = contexts.get( i );
final long cachedWidth = getEndOfLine() - renderBox.getCachedX();
renderBox.setCachedWidth( cachedWidth );
final InlineRenderBox rightBox = (InlineRenderBox) renderBox.split( RenderNode.HORIZONTAL_AXIS );
sequenceElements[i] = StartSequenceElement.INSTANCE;
nodes[i] = rightBox;
if ( previousContext != null ) {
previousContext.addGeneratedChild( renderBox );
}
previousContext = renderBox;
}
final int length = sequenceFill - lastPosition;
System.arraycopy( sequenceElements, lastPosition, sequenceElements, openContexts, length );
System.arraycopy( nodes, lastPosition, nodes, openContexts, length );
sequenceFill = openContexts + length;
Arrays.fill( sequenceElements, sequenceFill, sequenceElements.length, null );
Arrays.fill( nodes, sequenceFill, nodes.length, null );
return firstBox;
}
private void cleanFirstSpacers() {
InlineSequenceElement[] sequenceElements = this.sequenceElements;
RenderNode[] nodes = this.nodes;
int sequenceFill = this.sequenceFill;
boolean changed = false;
int targetIndex = 0;
for ( int i = 0; i < this.sequenceFill; i += 1 ) {
final InlineSequenceElement ise = this.sequenceElements[i];
final InlineSequenceElement.Classification type = ise.getType();
if ( type == InlineSequenceElement.Classification.CONTENT ) {
final RenderNode node = this.nodes[i];
if ( node instanceof SpacerRenderNode ) {
if ( changed == false ) {
// copy on demand ...
sequenceElements = this.sequenceElements.clone();
nodes = this.nodes.clone();
}
sequenceFill -= 1;
changed = true;
continue;
}
}
if ( changed ) {
// only copy if there is a change..
sequenceElements[targetIndex] = ise;
nodes[targetIndex] = this.nodes[i];
}
if ( type != InlineSequenceElement.Classification.START ) {
if ( !changed ) {
return;
}
System.arraycopy( this.sequenceElements, i, sequenceElements, targetIndex, this.sequenceFill - i );
System.arraycopy( this.nodes, i, nodes, targetIndex, this.sequenceFill - i );
Arrays.fill( nodes, sequenceFill, nodes.length, null );
Arrays.fill( sequenceElements, sequenceFill, sequenceElements.length, null );
this.sequenceElements = sequenceElements;
this.nodes = nodes;
this.sequenceFill = sequenceFill;
return;
}
targetIndex += 1;
}
}
/**
* Handle the next input chunk.
*
* @param start
* the start index
* @param count
* the number of elements in the sequence
* @return the processing position. Linebreaks will be inserted, if the returned value is equal or less the start
* index.
*/
protected abstract int handleElement( final int start, final int count );
protected void computeInlineBlock( final RenderBox box, final long position, final long itemElementWidth ) {
final StaticBoxLayoutProperties blp = box.getStaticBoxLayoutProperties();
box.setCachedX( position + blp.getMarginLeft() );
final long width = itemElementWidth - blp.getMarginLeft() - blp.getMarginRight();
if ( width == 0 ) {
// ModelPrinter.printParents(box);
throw new IllegalStateException( "A box without any width? "
+ Integer.toHexString( System.identityHashCode( box ) ) + ' ' + box.getClass() );
}
box.setCachedWidth( width );
final BoxDefinition bdef = box.getBoxDefinition();
final long leftInsets = bdef.getPaddingLeft() + blp.getBorderLeft();
final long rightInsets = bdef.getPaddingRight() + blp.getBorderRight();
box.setContentAreaX1( box.getCachedX() + leftInsets );
box.setContentAreaX2( box.getCachedX() + box.getCachedWidth() - rightInsets );
// final InfiniteMinorAxisLayoutStep layoutStep = new InfiniteMinorAxisLayoutStep(metaData);
// layoutStep.continueComputation(getPageGrid(), box);
}
protected int getSequenceFill() {
return sequenceFill;
}
public void performLastLineAlignment() {
if ( pagebreakCount == 0 ) {
throw new IllegalStateException( "Alignment processor has not been initialized correctly." );
}
Arrays.fill( elementDimensions, 0 );
Arrays.fill( elementPositions, 0 );
int lastPosition = iterate( sequenceElements, sequenceFill );
if ( lastPosition == 0 ) {
// This could evolve into an infinite loop. Thats evil.
// We have two choices to prevent that:
// (1) Try to break the element.
// if (getBreakableIndex() >= 0)
// {
// // Todo: Breaking is not yet implemented ..
// }
if ( getSkipIndex() >= 0 ) {
// This causes an overflow ..
performSkipAlignment( getSkipIndex() );
lastPosition = getSkipIndex();
} else {
// Skip the complete line. Oh, thats not good, really!
lastPosition = sequenceFill;
}
}
// the elements up to the 'lastPosition' are now aligned according to the alignment rules.
// now, update the element's positions and dimensions ..
if ( lastPosition == sequenceFill || lastLineAlignment ) {
// First, the simple case: The line's content did fully fit into the linebox. No linebreaks were necessary.
RenderBox firstBox = null;
for ( int i = 0; i < lastPosition; i++ ) {
final RenderNode node = nodes[i];
final InlineSequenceElement element = sequenceElements[i];
if ( element instanceof EndSequenceElement ) {
final long boxX2 = ( elementPositions[i] + elementDimensions[i] );
final RenderBox box = (RenderBox) node;
box.setCachedWidth( boxX2 - box.getCachedX() );
continue;
}
if ( element instanceof StartSequenceElement ) {
final RenderBox box = (RenderBox) node;
box.setCachedX( elementPositions[i] );
if ( firstBox == null ) {
firstBox = box;
}
continue;
}
// Content element: Perform a deep-deriveForAdvance, so that we preserve the
// possibly existing sub-nodes.
node.setCachedX( elementPositions[i] );
node.setCachedWidth( elementDimensions[i] );
}
return;
}
// The second case is more complicated. The text did not fit fully into the text-element.
// Left align all elements after the layouted content ..
if ( leftAlignProcessor == null ) {
leftAlignProcessor = new LeftAlignmentProcessor();
}
leftAlignProcessor.initializeForLastLineAlignment( this );
leftAlignProcessor.performLastLineAlignment();
leftAlignProcessor.deinitialize();
}
public void performSkipAlignment( final int endIndex ) {
// Left align all elements after the layouted content ..
if ( leftAlignProcessor == null ) {
leftAlignProcessor = new LeftAlignmentProcessor();
}
leftAlignProcessor.initializeForSkipAlignment( this, endIndex );
leftAlignProcessor.performLastLineAlignment();
leftAlignProcessor.deinitialize();
}
protected void initializeForSkipAlignment( final AbstractAlignmentProcessor proc, final int endIndex ) {
this.lastLineAlignment = true;
this.sequenceElements = proc.sequenceElements;
this.nodes = proc.nodes;
this.sequenceFill = endIndex;
this.pageGrid = proc.pageGrid;
this.elementDimensions = proc.elementDimensions;
this.elementPositions = proc.elementPositions;
Arrays.fill( this.elementPositions, 0 );
Arrays.fill( this.elementDimensions, 0 );
this.startOfLine = proc.startOfLine;
this.endOfLine = proc.endOfLine;
if ( this.pagebreaks == null || this.pagebreaks.length < proc.pagebreakCount ) {
this.pagebreaks = proc.pagebreaks.clone();
this.pagebreakCount = proc.pagebreakCount;
} else {
System.arraycopy( proc.pagebreaks, 0, this.pagebreaks, 0, proc.pagebreakCount );
this.pagebreakCount = proc.pagebreakCount;
}
}
protected void initializeForLastLineAlignment( final AbstractAlignmentProcessor proc ) {
this.lastLineAlignment = true;
this.sequenceElements = proc.sequenceElements;
this.nodes = proc.nodes;
this.sequenceFill = proc.sequenceFill;
this.pageGrid = proc.pageGrid;
this.elementDimensions = proc.elementDimensions;
this.elementPositions = proc.elementPositions;
Arrays.fill( this.elementPositions, 0 );
Arrays.fill( this.elementDimensions, 0 );
this.startOfLine = proc.startOfLine;
this.endOfLine = proc.endOfLine;
updateBreaksForLastLineAlignment();
}
protected void updateBreaksForLastLineAlignment() {
updateBreaks();
}
/**
* Returns {@code true} if {@code element} represents border type, namely if it is either
* {@linkplain StartSequenceElement} or {@linkplain EndSequenceElement}
*
* @param element
* element
* @return {@code true}, if element represents border type and {@code false} otherwise
*/
protected boolean isBorderMarker( InlineSequenceElement element ) {
return ( element == StartSequenceElement.INSTANCE ) || ( element == EndSequenceElement.INSTANCE );
}
/**
* Tries to split a breakable component if possible.
* <p/>
* First, checks whether each of the following conditions is true:
* <ol>
* <li>{@code getBreakableIndex() >= 0}</li>
* <li>{@code nodes[breakableIndex]} is an instance of {@linkplain SplittableRenderNode}</li>
* <li>{@code getBreakableMaxAllowedWidth() > 0}</li>
* </ol>
*
* Then asks the node to split limiting the first kid's width so, that it can be put inside the bounds. The node can
* be unable to do the separation and return {@code null}.
*
* If the separation was successful, then the method re-initialises the processors internal fields by invoking
* {@linkplain #reInitializeForHandlingComponentSplit(int, RenderNode[])}
*
* @return {@code true} if the split was done or {@code false} otherwise
*/
protected boolean splitBreakableIfPossible() {
int breakableIndex = getBreakableIndex();
if ( breakableIndex >= 0 ) {
RenderNode breakableNode = nodes[breakableIndex];
if ( breakableNode instanceof SplittableRenderNode ) {
SplittableRenderNode splittableNode = (SplittableRenderNode) breakableNode;
long widthExceeding = getBreakableMaxAllowedWidth();
if ( widthExceeding > 0 ) {
long widthByLayout = getElementDimensions()[breakableIndex];
long maxAllowedWidth;
if ( widthByLayout > 0 ) {
maxAllowedWidth = widthByLayout - widthExceeding;
} else {
// was not computed, use node's self estimation
maxAllowedWidth = splittableNode.getMinimumWidth() - widthExceeding;
}
if ( maxAllowedWidth > 0 ) {
RenderNode[] pair = splittableNode.splitBy( maxAllowedWidth );
if ( pair != null ) {
reInitializeForHandlingComponentSplit( breakableIndex, pair );
return true;
}
}
}
}
}
return false;
}
/**
* A utility method for shifting {@code array}'s part that starts at {@code startIndex} and contains {@code amount}
* elements by {@code offset}
* <p/>
* Note:<br/>
* org.apache.commons.lang.ArrayUtils.shift() solves the problem, but the version used now is too old and it does not
* contain this method
*
* @param array
* array
* @param startIndex
* start index of the block to be shifted (included)
* @param amount
* length of the block to be shifted
* @param offset
* indexes delta
*/
// package local visibility for testing purposes
static void shiftArray( Object[] array, int startIndex, int amount, int offset ) {
System.arraycopy( array, startIndex, array, startIndex + offset, amount );
}
/**
* For tests only!
*/
@Deprecated
void setNodes( RenderNode[] nodes ) {
this.nodes = nodes;
}
/**
* For tests only!
*/
@Deprecated
void setElementDimensions( long[] elementDimensions ) {
this.elementDimensions = elementDimensions;
}
protected void reInitializeForHandlingComponentSplit( int breakableIndex, RenderNode[] replacement ) {
// shift by (replacement.length - 1), because split component should be replaced
final int replacementAmountMinus1 = replacement.length - 1;
final int newSize = sequenceFill + replacementAmountMinus1;
if ( newSize > sequenceElements.length ) {
// need to create larger arrays
sequenceElements = Arrays.copyOf( sequenceElements, newSize );
nodes = Arrays.copyOf( nodes, newSize );
elementPositions = Arrays.copyOf( elementPositions, newSize );
elementDimensions = Arrays.copyOf( elementDimensions, newSize );
}
int shiftedPartStartIndex = breakableIndex + 1;
int shiftedPartLength = sequenceFill - shiftedPartStartIndex;
shiftArray( sequenceElements, shiftedPartStartIndex, shiftedPartLength, replacementAmountMinus1 );
// split elements derive their demiurge's type
InlineSequenceElement breakableNodeType = sequenceElements[breakableIndex];
Arrays.fill( sequenceElements, breakableIndex + 1, breakableIndex + replacement.length, breakableNodeType );
shiftArray( nodes, shiftedPartStartIndex, shiftedPartLength, replacementAmountMinus1 );
System.arraycopy( replacement, 0, nodes, breakableIndex, replacement.length );
// deliberately ignore elementPositions and elementDimensions,
// as they are reinitialised on each iterate() call
sequenceFill = newSize;
}
}