Extend it to display all available primitive shapes with labels,
documenting each shape and its parameters.
-** Use shaded polygons for valumetric actree demo
* Performance
:PROPERTIES:
:CUSTOM_ID: performance
:CUSTOM_ID: investigate-performance-optimizations
:END:
Focus on critical pixel fill loops:
-- Textured polygon
-- Flat polygon
-- Line
-- Billboard
+
+
+| Status | Class | Hot Method(s) | Buffer Type |
+|--------+------------------+--------------------------------------------------------------------------------------+--------------|
+| DONE | TextureBitmap | drawPixel(), fillColor() | int[] (ARGB) |
+| TODO | SolidPolygon | drawHorizontalLine() (scanline inner loop) | int[] |
+| TODO | TexturedPolygon | drawHorizontalLine() (texture sampling inner loop) | int[] |
+| TODO | Line | drawHorizontalLine(), drawSinglePixelHorizontalLine(), drawSinglePixelVerticalLine() | int[] |
+| TODO | Billboard | paint() (nested loop over pixels) | int[] |
+| TODO | GlowingPoint | createTexture() | int[] |
+| TODO | Texture | downscaleBitmap(), upscaleBitmap(), avg2(), avg4() | int[] |
+| TODO | RenderingContext | Constructor (extracts pixels from DataBufferInt) | int[] |
+
** Dynamically resize horizontal per-CPU core slices based on their complexity
:PROPERTIES:
:CUSTOM_ID: add-polygon-reduction-lod
:END:
-** Make it easy to copy current camera position in developer tools
-It would be nice to fly around and choose good viewpoint and then copy
-camera view position into application source code, so that next time
-application starts already at that pre-chosen location
** Add object fading based on view distance
:PROPERTIES:
:CUSTOM_ID: add-object-fading-view-distance
</style>
#+end_export
-[[file:index.org][Back to main documentation]]
+[[file:../index.org][Back to main documentation]]
* The problem
:PROPERTIES:
*/
package eu.svjatoslav.sixth.e3d.gui;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Quaternion;
+
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
+import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
* <p>Opens as a popup window when F12 is pressed. Provides:</p>
* <ul>
* <li>Checkboxes to toggle debug settings</li>
+ * <li>Camera position display with copy button</li>
* <li>A scrollable log viewer showing captured debug output</li>
* <li>A button to clear the log buffer</li>
* <li>Resizable window with native maximize support</li>
*/
public class DeveloperToolsPanel extends JFrame {
- private static final int LOG_UPDATE_INTERVAL_MS = 500;
+ private static final int UPDATE_INTERVAL_MS = 200;
+ /** The view panel whose camera is being displayed. */
+ private final ViewPanel viewPanel;
/** The developer tools being controlled. */
private final DeveloperTools developerTools;
/** The log buffer being displayed. */
private final DebugLogBuffer debugLogBuffer;
/** The text area showing log messages. */
private final JTextArea logArea;
- /** Timer for periodic log updates. */
+ /** The label showing camera position. */
+ private final JLabel cameraLabel;
+ /** Timer for periodic updates. */
private final Timer updateTimer;
- /** Flag to prevent concurrent log updates. */
+ /** Flag to prevent concurrent updates. */
private volatile boolean updating = false;
/**
* Creates and displays a developer tools panel.
*
* @param parent the parent frame (for centering)
+ * @param viewPanel the view panel whose camera to display
* @param developerTools the developer tools to control
* @param debugLogBuffer the log buffer to display
*/
- public DeveloperToolsPanel(final Frame parent, final DeveloperTools developerTools,
- final DebugLogBuffer debugLogBuffer) {
+ public DeveloperToolsPanel(final Frame parent, final ViewPanel viewPanel,
+ final DeveloperTools developerTools,
+ final DebugLogBuffer debugLogBuffer) {
super("Developer Tools");
+ this.viewPanel = viewPanel;
this.developerTools = developerTools;
this.debugLogBuffer = debugLogBuffer;
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setLayout(new BorderLayout(8, 8));
- final JPanel settingsPanel = createSettingsPanel();
- add(settingsPanel, BorderLayout.NORTH);
+ cameraLabel = new JLabel(" ");
+ cameraLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+
+ final JPanel topPanel = new JPanel();
+ topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
+ topPanel.add(createSettingsPanel());
+ topPanel.add(createCameraPanel());
+ add(topPanel, BorderLayout.NORTH);
- logArea = new JTextArea(20, 60);
+ logArea = new JTextArea(15, 60);
logArea.setEditable(false);
logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
logArea.setBackground(Color.BLACK);
pack();
setLocationRelativeTo(parent);
- updateTimer = new Timer(LOG_UPDATE_INTERVAL_MS, new ActionListener() {
+ updateTimer = new Timer(UPDATE_INTERVAL_MS, new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
- updateLogDisplay();
+ updateDisplay();
}
});
addWindowListener(new WindowAdapter() {
@Override
public void windowOpened(final WindowEvent e) {
- updateLogDisplay();
+ updateDisplay();
updateTimer.start();
}
private JPanel createSettingsPanel() {
final JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
- panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 8));
final JCheckBox showBordersCheckbox = new JCheckBox("Show polygon borders");
showBordersCheckbox.setSelected(developerTools.showPolygonBorders);
return panel;
}
+ private JPanel createCameraPanel() {
+ final JPanel panel = new JPanel(new BorderLayout(4, 4));
+ panel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createEmptyBorder(8, 8, 8, 8),
+ BorderFactory.createTitledBorder("Camera (x, y, z, yaw, pitch, roll)")
+ ));
+
+ panel.add(cameraLabel, BorderLayout.CENTER);
+
+ final JButton copyButton = new JButton("Copy");
+ copyButton.setToolTipText("Copy camera position to clipboard");
+ copyButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(final ActionEvent e) {
+ final String text = cameraLabel.getText();
+ if (text != null && !text.trim().isEmpty()) {
+ final StringSelection sel = new StringSelection(text);
+ Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, null);
+ }
+ }
+ });
+ panel.add(copyButton, BorderLayout.EAST);
+
+ return panel;
+ }
+
private JPanel createButtonPanel() {
final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
return panel;
}
- private void updateLogDisplay() {
+ private void updateDisplay() {
if (updating) {
return;
}
updating = true;
try {
- final List<String> entries = debugLogBuffer.getEntries();
- final StringBuilder sb = new StringBuilder();
- for (final String entry : entries) {
- sb.append(entry).append('\n');
- }
- logArea.setText(sb.toString());
-
- final JScrollBar vertical = ((JScrollPane) logArea.getParent().getParent())
- .getVerticalScrollBar();
- vertical.setValue(vertical.getMaximum());
+ updateCameraLabel();
+ updateLogDisplay();
} finally {
updating = false;
}
}
+
+ private void updateCameraLabel() {
+ if (viewPanel == null) {
+ return;
+ }
+
+ final Camera camera = viewPanel.getCamera();
+ final Point3D pos = camera.getTransform().getTranslation();
+ final double[] angles = camera.getTransform().getRotation().toAngles();
+
+ cameraLabel.setText(String.format("%.2f, %.2f, %.2f, %.2f, %.2f, %.2f",
+ pos.x, pos.y, pos.z, angles[0], angles[1], angles[2]));
+ }
+
+ private void updateLogDisplay() {
+ final List<String> entries = debugLogBuffer.getEntries();
+ final StringBuilder sb = new StringBuilder();
+ for (final String entry : entries) {
+ sb.append(entry).append('\n');
+ }
+ logArea.setText(sb.toString());
+
+ final JScrollBar vertical = ((JScrollPane) logArea.getParent().getParent())
+ .getVerticalScrollBar();
+ vertical.setValue(vertical.getMaximum());
+ }
}
\ No newline at end of file
setVisible(true);
validate();
- // Render thread will be started by ViewPanel's componentShown/componentResized listeners
- // after all frame listeners have been registered by the caller
-
addResizeListener();
addWindowListener(this);
}
@Override
public void componentResized(final ComponentEvent e) {
viewRepaintNeeded = true;
+ startRenderThreadIfReady();
}
@Override
public void componentShown(final ComponentEvent e) {
viewRepaintNeeded = true;
+ startRenderThreadIfReady();
}
});
}
- private void maybeStartRenderThread() {
+ private void startRenderThreadIfReady() {
if (isShowing() && getWidth() > 0 && getHeight() > 0)
startRenderThread();
}
- /**
- * Ensures the render thread is started. Called by ViewFrame after the window is visible.
- */
- public void ensureRenderThreadStarted() {
- maybeStartRenderThread();
- }
-
/**
* Returns the camera representing the viewer's position and orientation.
*
parent = parent.getParent();
}
- developerToolsPanel = new DeveloperToolsPanel(parentFrame, developerTools, debugLogBuffer);
+ developerToolsPanel = new DeveloperToolsPanel(parentFrame, this, developerTools, debugLogBuffer);
developerToolsPanel.setVisible(true);
}
private boolean mouseWithinWindow = false;
private double cameraYaw = 0;
private double cameraPitch = 0;
+ private double cameraRoll = 0;
/**
* Creates an input manager attached to the given view panel.
@Override
public void mousePressed(final java.awt.event.MouseEvent e) {
+ // Initialize camera rotation state from current camera orientation.
+ // This prevents a jump when the camera was programmatically positioned
+ // with a non-default rotation before the user started dragging.
+ final Camera camera = viewPanel.getCamera();
+ final double[] angles = camera.getTransform().getRotation().toAngles();
+ cameraYaw = angles[0];
+ cameraPitch = angles[1];
+ cameraRoll = angles[2];
}
@Override
Math.min( Math.PI / 2 - 0.001, cameraPitch));
final Camera camera = viewPanel.getCamera();
- camera.getTransform().getRotation().set(Quaternion.fromAngles(cameraYaw, cameraPitch));
+ camera.getTransform().getRotation().set(
+ Quaternion.fromAngles(cameraYaw, cameraPitch, cameraRoll));
mouseDelta.zero();
return true;
* Creates a quaternion from XZ (yaw) and YZ (pitch) Euler angles.
*
* <p>The rotation is composed as yaw (around Y axis) followed by
- * pitch (around X axis).</p>
+ * pitch (around X axis). No roll rotation is applied.</p>
+ *
+ * <p>For full 3-axis rotation, use {@link #fromAngles(double, double, double)}.</p>
*
* @param angleXZ the angle around the XZ axis (yaw) in radians
* @param angleYZ the angle around the YZ axis (pitch) in radians
* @return a quaternion representing the combined rotation
*/
public static Quaternion fromAngles(final double angleXZ, final double angleYZ) {
- final Quaternion yaw = fromAxisAngle(new Point3D(0, 1, 0), angleXZ);
- final Quaternion pitch = fromAxisAngle(new Point3D(1, 0, 0), -angleYZ);
- return pitch.multiply(yaw);
+ return fromAngles(angleXZ, angleYZ, 0);
+ }
+
+ /**
+ * Creates a quaternion from full Euler angles (yaw, pitch, roll).
+ *
+ * <p>Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard
+ * Y-X-Z Euler order commonly used for object placement in 3D scenes.</p>
+ *
+ * @param yaw rotation around Y axis (horizontal heading) in radians
+ * @param pitch rotation around X axis (vertical tilt) in radians;
+ * positive values tilt upward
+ * @param roll rotation around Z axis (bank/tilt) in radians;
+ * positive values rotate clockwise when looking along +Z
+ * @return a quaternion representing the combined rotation
+ */
+ public static Quaternion fromAngles(final double yaw, final double pitch, final double roll) {
+ final Quaternion qYaw = fromAxisAngle(new Point3D(0, 1, 0), yaw);
+ final Quaternion qPitch = fromAxisAngle(new Point3D(1, 0, 0), -pitch);
+ final Quaternion qRoll = fromAxisAngle(new Point3D(0, 0, 1), roll);
+ return qRoll.multiply(qPitch).multiply(qYaw);
}
/**
return toMatrix3x3();
}
+ /**
+ * Extracts Euler angles (yaw, pitch, roll) from this quaternion.
+ *
+ * <p>This is the inverse of {@link #fromAngles(double, double, double)}.
+ * Returns angles in the Y-X-Z Euler order used by this engine.</p>
+ *
+ * @return array of {yaw, pitch, roll} in radians
+ */
+ public double[] toAngles() {
+ final Matrix3x3 m = toMatrix3x3();
+
+ // For Rz(roll) * Rx(-pitch) * Ry(yaw) rotation order:
+ // m21 = sin(pitch)
+ // m20 = cos(pitch)*sin(yaw), m22 = cos(pitch)*cos(yaw)
+ // m01 = -sin(roll)*cos(pitch), m11 = cos(roll)*cos(pitch)
+
+ final double pitch = -Math.asin(Math.max(-1, Math.min(1, m.m21)));
+ final double yaw = -Math.atan2(m.m20, m.m22);
+ final double roll = -Math.atan2(m.m01, m.m11);
+
+ return new double[]{yaw, pitch, roll};
+ }
+
}
\ No newline at end of file
* Creates a transform with the specified translation and rotation from Euler angles.
*
* @param translation the translation
- * @param angleXZ the angle around the XZ axis (yaw) in radians
- * @param angleYZ the angle around the YZ axis (pitch) in radians
+ * @param yaw the angle around the Y axis (horizontal heading) in radians
+ * @param pitch the angle around the X axis (vertical tilt) in radians
* @return a new transform with the specified translation and rotation
*/
- public static Transform fromAngles(final Point3D translation, final double angleXZ, final double angleYZ) {
- final Transform t = new Transform(translation);
- t.rotation.set(Quaternion.fromAngles(angleXZ, angleYZ));
+ public static Transform fromAngles(final Point3D translation, final double yaw, final double pitch) {
+ return fromAngles(translation.x, translation.y, translation.z, yaw, pitch, 0);
+ }
+
+ /**
+ * Creates a transform with translation and full Euler rotation.
+ *
+ * <p>Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard
+ * Y-X-Z Euler order commonly used for object placement in 3D scenes.</p>
+ *
+ * @param x translation X coordinate
+ * @param y translation Y coordinate
+ * @param z translation Z coordinate
+ * @param yaw rotation around Y axis (horizontal heading) in radians
+ * @param pitch rotation around X axis (vertical tilt) in radians
+ * @param roll rotation around Z axis (bank/tilt) in radians
+ * @return a new transform with the specified translation and rotation
+ */
+ public static Transform fromAngles(final double x, final double y, final double z,
+ final double yaw, final double pitch, final double roll) {
+ final Transform t = new Transform(new Point3D(x, y, z));
+ t.rotation.set(Quaternion.fromAngles(yaw, pitch, roll));
return t;
}
this.translation.z = translation.z;
}
+ /**
+ * Sets both translation and rotation from Euler angles.
+ *
+ * <p>Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard
+ * Y-X-Z Euler order commonly used for object placement in 3D scenes.</p>
+ *
+ * @param x translation X coordinate
+ * @param y translation Y coordinate
+ * @param z translation Z coordinate
+ * @param yaw rotation around Y axis (horizontal heading) in radians
+ * @param pitch rotation around X axis (vertical tilt) in radians
+ * @param roll rotation around Z axis (bank/tilt) in radians
+ * @return this transform for chaining
+ */
+ public Transform set(final double x, final double y, final double z,
+ final double yaw, final double pitch, final double roll) {
+ translation.x = x;
+ translation.y = y;
+ translation.z = z;
+ rotation.set(Quaternion.fromAngles(yaw, pitch, roll));
+ return this;
+ }
+
}
\ No newline at end of file
final int destG = (dest >> 8) & 0xff;
final int destB = dest & 0xff;
- final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) / 256;
- final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) / 256;
- final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) / 256;
+ final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) >> 8;
+ final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) >> 8;
+ final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) >> 8;
pixels[offset++] = (newR << 16) | (newG << 8) | newB;
final int destG = (dest >> 8) & 0xff;
final int destB = dest & 0xff;
- final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
- final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
- final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
+ final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+ final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+ final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
pixels[offset] = (newR << 16) | (newG << 8) | newB;
}
final int destG = (dest >> 8) & 0xff;
final int destB = dest & 0xff;
- final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
- final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
- final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
+ final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+ final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+ final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
pixels[offset] = (newR << 16) | (newG << 8) | newB;
}
final int destG = (dest >> 8) & 0xff;
final int destB = dest & 0xff;
- final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
- final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
- final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
+ final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+ final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+ final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
pixels[offset++] = (newR << 16) | (newG << 8) | newB;
}
} else {
final int backgroundAlpha = 255 - srcAlpha;
- final int srcR = (srcPixel >> 16) & 0xff;
- final int srcG = (srcPixel >> 8) & 0xff;
- final int srcB = srcPixel & 0xff;
+ final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha;
+ final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha;
+ final int srcB = (srcPixel & 0xff) * srcAlpha;
final int destPixel = renderBufferPixels[renderBufferOffset];
final int destR = (destPixel >> 16) & 0xff;
final int destG = (destPixel >> 8) & 0xff;
final int destB = destPixel & 0xff;
- final int r = ((destR * backgroundAlpha) + (srcR * srcAlpha)) / 256;
- final int g = ((destG * backgroundAlpha) + (srcG * srcAlpha)) / 256;
- final int b = ((destB * backgroundAlpha) + (srcB * srcAlpha)) / 256;
+ final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+ final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+ final int b = ((destB * backgroundAlpha) + srcB) >> 8;
renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
}
* <p>This texture stores pixels in ARGB format. The target is RGB format (no alpha).
* Alpha blending is performed based on the source pixel's alpha value.</p>
*
+ * <p><b>Performance note:</b> Uses bit-shift instead of division for alpha blending,
+ * and pre-multiplies source colors to reduce per-pixel operations.</p>
+ *
* @param sourcePixelAddress Pixel index within current texture.
* @param targetBitmap Target RGB pixel array.
* @param targetPixelAddress Pixel index within target image.
final int backgroundAlpha = 255 - textureAlpha;
- final int srcR = (sourcePixel >> 16) & 0xff;
- final int srcG = (sourcePixel >> 8) & 0xff;
- final int srcB = sourcePixel & 0xff;
+ // Pre-multiply source colors by alpha to reduce operations in blend
+ final int srcR = ((sourcePixel >> 16) & 0xff) * textureAlpha;
+ final int srcG = ((sourcePixel >> 8) & 0xff) * textureAlpha;
+ final int srcB = (sourcePixel & 0xff) * textureAlpha;
final int destPixel = targetBitmap[targetPixelAddress];
final int destR = (destPixel >> 16) & 0xff;
final int destG = (destPixel >> 8) & 0xff;
final int destB = destPixel & 0xff;
- final int r = ((destR * backgroundAlpha) + (srcR * textureAlpha)) / 256;
- final int g = ((destG * backgroundAlpha) + (srcG * textureAlpha)) / 256;
- final int b = ((destB * backgroundAlpha) + (srcB * textureAlpha)) / 256;
+ // Use bit-shift instead of division for faster blending
+ final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+ final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+ final int b = ((destB * backgroundAlpha) + srcB) >> 8;
targetBitmap[targetPixelAddress] = (r << 16) | (g << 8) | b;
}
+ /**
+ * Renders a scanline using pre-computed source pixel addresses.
+ *
+ * <p>This variant is optimized for cases where source addresses are computed
+ * externally (e.g., by a caller that already has the stepping logic).
+ * The sourceAddresses array must contain valid indices into {@link #pixels}.</p>
+ *
+ * @param sourceAddresses array of source pixel addresses (indices into pixels array)
+ * @param targetBitmap target RGB pixel array
+ * @param targetStartAddress starting index in the target array
+ * @param pixelCount number of pixels to render
+ */
+ public void drawScanlineWithAddresses(final int[] sourceAddresses,
+ final int[] targetBitmap, final int targetStartAddress,
+ final int pixelCount) {
+
+ int targetOffset = targetStartAddress;
+
+ for (int i = 0; i < pixelCount; i++) {
+ final int sourcePixel = pixels[sourceAddresses[i]];
+ final int textureAlpha = (sourcePixel >> 24) & 0xff;
+
+ if (textureAlpha == 255) {
+ targetBitmap[targetOffset] = sourcePixel;
+ } else if (textureAlpha != 0) {
+ final int backgroundAlpha = 255 - textureAlpha;
+
+ final int srcR = ((sourcePixel >> 16) & 0xff) * textureAlpha;
+ final int srcG = ((sourcePixel >> 8) & 0xff) * textureAlpha;
+ final int srcB = (sourcePixel & 0xff) * textureAlpha;
+
+ final int destPixel = targetBitmap[targetOffset];
+ final int destR = (destPixel >> 16) & 0xff;
+ final int destG = (destPixel >> 8) & 0xff;
+ final int destB = destPixel & 0xff;
+
+ final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+ final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+ final int b = ((destB * backgroundAlpha) + srcB) >> 8;
+
+ targetBitmap[targetOffset] = (r << 16) | (g << 8) | b;
+ }
+
+ targetOffset++;
+ }
+ }
+
/**
* Draws a single pixel at the specified coordinates using the given color.
*
* The same applies to {@code y1} and {@code y2}. The rectangle is exclusive of the
* right and bottom edges.</p>
*
+ * <p><b>Performance:</b> Uses {@link java.util.Arrays#fill(int[], int, int, int)}
+ * per scanline for optimal JVM-optimized memory writes.</p>
+ *
* @param x1 the left x coordinate
* @param y1 the top y coordinate
* @param x2 the right x coordinate (exclusive)
y2 = tmp;
}
- for (int y = y1; y < y2; y++)
- for (int x = x1; x < x2; x++)
- drawPixel(x, y, color);
+ // Clamp to bitmap bounds
+ if (x1 < 0) x1 = 0;
+ if (y1 < 0) y1 = 0;
+ if (x2 > width) x2 = width;
+ if (y2 > height) y2 = height;
+
+ final int pixel = (color.a << 24) | (color.r << 16) | (color.g << 8) | color.b;
+ final int rowWidth = x2 - x1;
+
+ if (rowWidth <= 0)
+ return;
+
+ // Fill each scanline using Arrays.fill for optimal performance
+ for (int y = y1; y < y2; y++) {
+ final int rowStart = y * width + x1;
+ java.util.Arrays.fill(pixels, rowStart, rowStart + rowWidth, pixel);
+ }
}
/**