diff --git a/.all-contributorsrc b/.all-contributorsrc
index d60fb8c632..bbea31f828 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2,7 +2,7 @@
"projectName": "processing4",
"projectOwner": "processing",
"files": [
- "README.md"
+ "CONTRIBUTORS.md"
],
"imageSize": 120,
"contributorsPerLine": 6,
diff --git a/BUILD.md b/BUILD.md
index a7176776a2..1216f2e952 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -163,3 +163,16 @@ You may see this warning in IntelliJ:
> `Duplicate content roots detected: '.../processing4/java/src'`
This happens because multiple modules reference the same source folder. Itβs safe to ignore.
+
+
+### Build Failed
+
+If the build fails with `Permission denied` or `Could not copy file` errors, try cleaning the project.
+
+Run:
+
+```bash
+./gradlew clean
+```
+
+Then, rebuild the project.
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000000..38efdebfc8
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,258 @@
+_Note: due to GitHub's limitations, this repository's [Contributors](https://github.com/processing/processing4/graphs/contributors) page only shows accurate contribution data starting from late 2024. Contributor graphs from before November 13th 2024 can be found on [this page](https://github.com/benfry/processing4/graphs/contributors). The [git commit history](https://github.com/processing/processing4/commits/main/) provides a full record of the project's contributions. To see all commits by a contributor, click on the [π»](https://github.com/processing/processing4/commits?author=benfry) emoji below their name._
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 7abe540901..c229dc16c8 100644
--- a/README.md
+++ b/README.md
@@ -66,263 +66,6 @@ For licensing information about the Processing website see the [processing-websi
Copyright (c) 2015-now The Processing Foundation
## Contributors
-The Processing project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification, recognizing all forms of contributions (not just code!). A list of all contributors is included below. You can add yourself to the contributors list [here](https://github.com/processing/processing4-carbon-aug-19/issues/839)!
+See [CONTRIBUTORS.md](./CONTRIBUTORS.md) for a list of all contributors to the project.
-_Note: due to GitHub's limitations, this repository's [Contributors](https://github.com/processing/processing4/graphs/contributors) page only shows accurate contribution data starting from late 2024. Contributor graphs from before November 13th 2024 can be found on [this page](https://github.com/benfry/processing4/graphs/contributors). The [git commit history](https://github.com/processing/processing4/commits/main/) provides a full record of the project's contributions. To see all commits by a contributor, click on the [π»](https://github.com/processing/processing4/commits?author=benfry) emoji below their name._
-
-
-
-
-
-
-
-
-
-
+This project follows the [all-contributors specification](https://github.com/all-contributors/all-contributors) and the [Emoji Key](https://all-contributors.github.io/emoji-key/) β¨ for contribution types. Detailed instructions on how to add yourself or add contribution emojis to your name are [here](https://github.com/processing/processing4/issues/839). You can also post an issue or comment on a pull request with the text: `@all-contributors please add @YOUR-USERNAME for THINGS` (where `THINGS` is a comma-separated list of entries from the [list of possible contribution types](https://all-contributors.github.io/emoji-key/)) and our nice bot will add you to [CONTRIBUTORS.md](./CONTRIBUTORS.md) automatically!
\ No newline at end of file
diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index 2551a54d64..41370918ba 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -51,11 +51,15 @@
* files and images, etc.) that comes from that.
*/
public class Base {
- // Added accessors for 0218 because the UpdateCheck class was not properly
- // updating the values, due to javac inlining the static final values.
+ /**
+ * Revision number, used for update checks and contribution compatibility.
+ */
static private final int REVISION = Integer.parseInt(System.getProperty("processing.revision", "1295"));
- /** This might be replaced by main() if there's a lib/version.txt file. */
- static private String VERSION_NAME = System.getProperty("processing.version", "1295"); //$NON-NLS-1$
+ /**
+ * This might be replaced by main() if there's a lib/version.txt file.
+ *
+ */
+ static private String VERSION_NAME = System.getProperty("processing.version", "1295");
static final public String SKETCH_BUNDLE_EXT = ".pdez";
static final public String CONTRIB_BUNDLE_EXT = ".pdex";
@@ -65,11 +69,12 @@ public class Base {
* if an empty file named 'debug' is found in the settings folder.
* See implementation in createAndShowGUI().
*/
-
static public boolean DEBUG = Boolean.parseBoolean(System.getenv().getOrDefault("DEBUG", "false"));
- /** True if running via Commander. */
+ /**
+ * is Processing being run from the command line (true) or from the GUI (false)?
+ */
static private boolean commandLine;
/**
@@ -128,105 +133,59 @@ public class Base {
static public void main(final String[] args) {
Messages.log("Starting Processing version" + VERSION_NAME + " revision "+ REVISION);
EventQueue.invokeLater(() -> {
- try {
- createAndShowGUI(args);
+ run(args);
+ });
+ }
- } catch (Throwable t) {
- // Windows Defender has been insisting on destroying each new
- // release by removing core.jar and other files. Yay!
- // https://github.com/processing/processing/issues/5537
- if (Platform.isWindows()) {
- String mess = t.getMessage();
- String missing = null;
- if (mess.contains("Could not initialize class com.sun.jna.Native")) {
- //noinspection SpellCheckingInspection
- missing = "jnidispatch.dll";
- } else if (t instanceof NoClassDefFoundError &&
- mess.contains("processing/core/PApplet")) {
- // Had to change how this was called
- // https://github.com/processing/processing4/issues/154
- missing = "core.jar";
- }
- if (missing != null) {
- Messages.showError("Necessary files are missing",
- "A file required by Processing (" + missing + ") is missing.\n\n" +
- "Make sure that you're not trying to run Processing from inside\n" +
- "the .zip file you downloaded, and check that Windows Defender\n" +
- "has not removed files from the Processing folder.\n\n" +
- "(Defender sometimes flags parts of Processing as malware.\n" +
- "It is not, but Microsoft has ignored our pleas for help.)", t);
- }
+ /**
+ * The main run() method, wrapped in a try/catch to
+ * provide a graceful error message if something goes wrong.
+ */
+ private static void run(String[] args) {
+ try {
+ createAndShowGUI(args);
+ } catch (Throwable t) {
+ // Windows Defender has been insisting on destroying each new
+ // release by removing core.jar and other files. Yay!
+ // https://github.com/processing/processing/issues/5537
+ if (Platform.isWindows()) {
+ String mess = t.getMessage();
+ String missing = null;
+ if (mess.contains("Could not initialize class com.sun.jna.Native")) {
+ //noinspection SpellCheckingInspection
+ missing = "jnidispatch.dll";
+ } else if (t instanceof NoClassDefFoundError &&
+ mess.contains("processing/core/PApplet")) {
+ // Had to change how this was called
+ // https://github.com/processing/processing4/issues/154
+ missing = "core.jar";
+ }
+ if (missing != null) {
+ Messages.showError("Necessary files are missing",
+ "A file required by Processing (" + missing + ") is missing.\n\n" +
+ "Make sure that you're not trying to run Processing from inside\n" +
+ "the .zip file you downloaded, and check that Windows Defender\n" +
+ "has not removed files from the Processing folder.\n\n" +
+ "(Defender sometimes flags parts of Processing as malware.\n" +
+ "It is not, but Microsoft has ignored our pleas for help.)", t);
}
- Messages.showTrace("Unknown Problem",
- "A serious error happened during startup. Please report:\n" +
- "http://github.com/processing/processing4/issues/new", t, true);
}
- });
+ Messages.showTrace("Unknown Problem",
+ "A serious error happened during startup. Please report:\n" +
+ "http://github.com/processing/processing4/issues/new", t, true);
+ }
}
static private void createAndShowGUI(String[] args) {
- // these times are fairly negligible relative to Base.
-// long t1 = System.currentTimeMillis();
- // TODO: Cleanup old locations if no longer installed
- // TODO: Cleanup old locations if current version is installed in the same location
-
- File versionFile = Platform.getContentFile("lib/version.txt");
- if (versionFile != null && versionFile.exists()) {
- String[] lines = PApplet.loadStrings(versionFile);
- if (lines != null && lines.length > 0) {
- if (!VERSION_NAME.equals(lines[0])) {
- VERSION_NAME = lines[0];
- }
- }
- }
+ checkVersion();
- // Detect settings.txt in the lib folder for portable versions
- File settingsFile = Platform.getContentFile("lib/settings.txt");
- if (settingsFile != null && settingsFile.exists()) {
- try {
- Settings portable = new Settings(settingsFile);
- String path = portable.get("settings.path");
- File folder = new File(path);
- boolean success = true;
- if (!folder.exists()) {
- success = folder.mkdirs();
- if (!success) {
- Messages.err("Could not create " + folder + " to store settings.");
- }
- }
- if (success) {
- if (!folder.canRead()) {
- Messages.err("Cannot read from " + folder);
- } else if (!folder.canWrite()) {
- Messages.err("Cannot write to " + folder);
- } else {
- settingsOverride = folder.getAbsoluteFile();
- }
- }
- } catch (IOException e) {
- Messages.err("Error while reading the settings.txt file", e);
- }
- }
+ checkPortable();
Platform.init();
// call after Platform.init() because we need the settings folder
Console.startup();
- // Set the debug flag based on a file being present in the settings folder
- File debugFile = getSettingsFile("debug");
-
- // If it's a directory, it's a leftover from much older releases
- // (2.x? 3.x?) that wrote DebugMode.log files into this directory.
- // Could remove the directory, but it's harmless enough that it's
- // not worth deleting files in case something could go wrong.
- if (debugFile.exists() && debugFile.isFile()) {
- DEBUG = true;
- }
-
- // Use native popups to avoid looking crappy on macOS
- JPopupMenu.setDefaultLightWeightPopupEnabled(false);
-
// Don't put anything above this line that might make GUI,
// because the platform has to be inited properly first.
@@ -239,8 +198,6 @@ static private void createAndShowGUI(String[] args) {
// run static initialization that grabs all the prefs
Preferences.init();
-// long t2 = System.currentTimeMillis();
-
// boolean flag indicating whether to create new server instance or not
boolean createNewInstance = DEBUG || !SingleInstance.alreadyRunning(args);
@@ -250,56 +207,26 @@ static private void createAndShowGUI(String[] args) {
return;
}
- if (createNewInstance) {
- // Set the look and feel before opening the window
- try {
- Platform.setLookAndFeel();
- Platform.setInterfaceZoom();
- } catch (Exception e) {
- Messages.err("Error while setting up the interface", e); //$NON-NLS-1$
- }
-// long t3 = System.currentTimeMillis();
+ // Set the look and feel before opening the window
+ setLookAndFeel();
- // Get the sketchbook path, and make sure it's set properly
- locateSketchbookFolder();
+ // Get the sketchbook path, and make sure it's set properly
+ locateSketchbookFolder();
-// long t4 = System.currentTimeMillis();
+ // Load colors for UI elements. This must happen after Preferences.init()
+ // (so that fonts are set) and locateSketchbookFolder() so that a
+ // theme.txt file in the user's sketchbook folder is picked up.
+ Theme.init();
- // Load colors for UI elements. This must happen after Preferences.init()
- // (so that fonts are set) and locateSketchbookFolder() so that a
- // theme.txt file in the user's sketchbook folder is picked up.
- Theme.init();
+ // Create a location for untitled sketches
+ setupUntitleSketches();
- // Create a location for untitled sketches
- try {
- // Users on a shared machine may also share a TEMP folder,
- // which can cause naming collisions; use a UUID as the name
- // for the subfolder to introduce another layer of indirection.
- // https://github.com/processing/processing4/issues/549
- // The UUID also prevents collisions when restarting the
- // software. Otherwise, after using up the a-z naming options
- // it was not possible for users to restart (without manually
- // finding and deleting the TEMP files).
- // https://github.com/processing/processing4/issues/582
- String uuid = UUID.randomUUID().toString();
- untitledFolder = new File(Util.getProcessingTemp(), uuid);
-
- } catch (IOException e) {
- Messages.showError("Trouble without a name",
- "Could not create a place to store untitled sketches.\n" +
- "That's gonna prevent us from continuing.", e);
- }
-
-// long t5 = System.currentTimeMillis();
-// long t6 = 0; // replaced below, just needs decl outside try { }
-
- Messages.log("About to create Base..."); //$NON-NLS-1$
- try {
+ Messages.log("About to create Base...");
+ try {
final Base base = new Base(args);
base.updateTheme();
Messages.log("Base() constructor succeeded");
-// t6 = System.currentTimeMillis();
// Prevent more than one copy of the PDE from running.
SingleInstance.startServer(base);
@@ -308,7 +235,7 @@ static private void createAndShowGUI(String[] args) {
handleCrustyDisplay();
handleTempCleaning();
- } catch (Throwable t) {
+ } catch (Throwable t) {
// Catch-all to pick up badness during startup.
Throwable err = t;
if (t.getCause() != null) {
@@ -317,24 +244,51 @@ static private void createAndShowGUI(String[] args) {
err = t.getCause();
}
Messages.showTrace("We're off on the wrong foot",
- "An error occurred during startup.", err, true);
- }
- Messages.log("Done creating Base..."); //$NON-NLS-1$
+ "An error occurred during startup.", err, true);
+ }
+ Messages.log("Done creating Base...");
+ }
+
+ private static void setupUntitleSketches() {
+ try {
+ // Users on a shared machine may also share a TEMP folder,
+ // which can cause naming collisions; use a UUID as the name
+ // for the subfolder to introduce another layer of indirection.
+ // https://github.com/processing/processing4/issues/549
+ // The UUID also prevents collisions when restarting the
+ // software. Otherwise, after using up the a-z naming options
+ // it was not possible for users to restart (without manually
+ // finding and deleting the TEMP files).
+ // https://github.com/processing/processing4/issues/582
+ String uuid = UUID.randomUUID().toString();
+ untitledFolder = new File(Util.getProcessingTemp(), uuid);
-// long t10 = System.currentTimeMillis();
-// System.out.println("startup took " + (t2-t1) + " " + (t3-t2) + " " + (t4-t3) + " " + (t5-t4) + " " + (t6-t5) + " " + (t10-t6) + " ms");
+ } catch (IOException e) {
+ Messages.showError("Trouble without a name",
+ "Could not create a place to store untitled sketches.\n" +
+ "That's gonna prevent us from continuing.", e);
}
}
+ private static void setLookAndFeel() {
+ try {
+ // Use native popups to avoid looking crappy on macOS
+ JPopupMenu.setDefaultLightWeightPopupEnabled(false);
+
+ Platform.setLookAndFeel();
+ Platform.setInterfaceZoom();
+ } catch (Exception e) {
+ Messages.err("Error while setting up the interface", e); //$NON-NLS-1$
+ }
+ }
+
public void updateTheme() {
try {
- //System.out.println("updating theme");
FlatLaf laf = "dark".equals(Theme.get("laf.mode")) ?
new FlatDarkLaf() : new FlatLightLaf();
laf.setExtraDefaults(Collections.singletonMap("@accentColor",
Theme.get("laf.accent.color")));
- //System.out.println(laf.getExtraDefaults());
//UIManager.setLookAndFeel(laf);
FlatLaf.setup(laf);
// updateUI() will wipe out our custom components
@@ -449,6 +403,54 @@ static public void cleanTempFolders() {
}
}
+ /**
+ * Check for a version.txt file in the lib folder to override
+ */
+ private static void checkVersion() {
+ File versionFile = Platform.getContentFile("lib/version.txt");
+ if (versionFile != null && versionFile.exists()) {
+ String[] lines = PApplet.loadStrings(versionFile);
+ if (lines != null && lines.length > 0) {
+ if (!VERSION_NAME.equals(lines[0])) {
+ VERSION_NAME = lines[0];
+ }
+ }
+ }
+ }
+
+ /**
+ * Check for portable settings.txt file in the lib folder
+ * to override the location of the settings folder.
+ */
+ static void checkPortable() {
+ // Detect settings.txt in the lib folder for portable versions
+ File settingsFile = Platform.getContentFile("lib/settings.txt");
+ if (settingsFile != null && settingsFile.exists()) {
+ try {
+ Settings portable = new Settings(settingsFile);
+ String path = portable.get("settings.path");
+ File folder = new File(path);
+ boolean success = true;
+ if (!folder.exists()) {
+ success = folder.mkdirs();
+ if (!success) {
+ Messages.err("Could not create " + folder + " to store settings.");
+ }
+ }
+ if (success) {
+ if (!folder.canRead()) {
+ Messages.err("Cannot read from " + folder);
+ } else if (!folder.canWrite()) {
+ Messages.err("Cannot write to " + folder);
+ } else {
+ settingsOverride = folder.getAbsoluteFile();
+ }
+ }
+ } catch (IOException e) {
+ Messages.err("Error while reading the settings.txt file", e);
+ }
+ }
+ }
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
@@ -484,44 +486,21 @@ static public boolean isCommandLine() {
public Base(String[] args) throws Exception {
- long t1 = System.currentTimeMillis();
ContributionManager.init(this);
- long t2 = System.currentTimeMillis();
buildCoreModes();
- long t2b = System.currentTimeMillis();
rebuildContribModes();
- long t2c = System.currentTimeMillis();
rebuildContribExamples();
- long t3 = System.currentTimeMillis();
// Needs to happen after the sketchbook folder has been located.
// Also relies on the modes to be loaded, so it knows what can be
// marked as an example.
Recent.init(this);
- long t4 = System.currentTimeMillis();
- String lastModeIdentifier = Preferences.get("mode.last"); //$NON-NLS-1$
- if (lastModeIdentifier == null) {
- nextMode = getDefaultMode();
- Messages.log("Nothing set for last.sketch.mode, using default."); //$NON-NLS-1$
- } else {
- for (Mode m : getModeList()) {
- if (m.getIdentifier().equals(lastModeIdentifier)) {
- Messages.logf("Setting next mode to %s.", lastModeIdentifier); //$NON-NLS-1$
- nextMode = m;
- }
- }
- if (nextMode == null) {
- nextMode = getDefaultMode();
- Messages.logf("Could not find mode %s, using default.", lastModeIdentifier); //$NON-NLS-1$
- }
- }
+ setupNextMode();
//contributionManagerFrame = new ContributionManagerDialog();
- long t5 = System.currentTimeMillis();
-
// Make sure ThinkDifferent has library examples too
nextMode.rebuildLibraryList();
@@ -529,10 +508,20 @@ public Base(String[] args) throws Exception {
// menu works on Mac OS X (since it needs examplesFolder to be set).
Platform.initBase(this);
- long t6 = System.currentTimeMillis();
+ // check for updates
+ UpdateCheck.doCheck(this);
+
-// // Check if there were previously opened sketches to be restored
-// boolean opened = restoreSketches();
+ ContributionListing cl = ContributionListing.getInstance();
+ cl.downloadAvailableList(this, new ContribProgress(null));
+
+ openFilesOrNew(args);
+
+ }
+
+ private void openFilesOrNew(String[] args) {
+ // Check if there were previously opened sketches to be restored
+ // boolean opened = restoreSketches();
boolean opened = false;
// Check if any files were passed in on the command line
@@ -558,8 +547,6 @@ public Base(String[] args) throws Exception {
}
}
- long t7 = System.currentTimeMillis();
-
// Create a new empty window (will be replaced with any files to be opened)
if (!opened) {
Messages.log("Calling handleNew() to open a new window");
@@ -567,22 +554,25 @@ public Base(String[] args) throws Exception {
} else {
Messages.log("No handleNew(), something passed on the command line");
}
+ }
- long t8 = System.currentTimeMillis();
-
- // check for updates
- new UpdateCheck(this);
-
- ContributionListing cl = ContributionListing.getInstance();
- cl.downloadAvailableList(this, new ContribProgress(null));
- long t9 = System.currentTimeMillis();
-
- Messages.log("core modes: " + (t2b-t2) +
- ", contrib modes: " + (t2c-t2b) +
- ", contrib ex: " + (t2c-t2b));
- Messages.log("base took " + (t2-t1) + " " + (t3-t2) + " " + (t4-t3) +
- " " + (t5-t4) + " t6-t5=" + (t6-t5) + " " + (t7-t6) +
- " handleNew=" + (t8-t7) + " " + (t9-t8) + " ms");
+ private void setupNextMode() {
+ String lastModeIdentifier = Preferences.get("mode.last"); //$NON-NLS-1$
+ if (lastModeIdentifier == null) {
+ nextMode = getDefaultMode();
+ Messages.log("Nothing set for last.sketch.mode, using default."); //$NON-NLS-1$
+ } else {
+ for (Mode m : getModeList()) {
+ if (m.getIdentifier().equals(lastModeIdentifier)) {
+ Messages.logf("Setting next mode to %s.", lastModeIdentifier); //$NON-NLS-1$
+ nextMode = m;
+ }
+ }
+ if (nextMode == null) {
+ nextMode = getDefaultMode();
+ Messages.logf("Could not find mode %s, using default.", lastModeIdentifier); //$NON-NLS-1$
+ }
+ }
}
diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt
index 6bc6b64a7e..08ad763775 100644
--- a/app/src/processing/app/Processing.kt
+++ b/app/src/processing/app/Processing.kt
@@ -19,7 +19,14 @@ import java.util.prefs.Preferences
import kotlin.concurrent.thread
-
+/**
+ * This function is the new modern entry point for Processing
+ * It uses Clikt to provide a command line interface with subcommands
+ *
+ * If you want to add new functionality to the CLI, create a new subcommand
+ * and add it to the list of subcommands below.
+ *
+ */
suspend fun main(args: Array){
Processing()
.subcommands(
@@ -32,6 +39,10 @@ suspend fun main(args: Array){
.main(args)
}
+/**
+ * The main Processing command, will open the ide if no subcommand is provided
+ * Will also launch the `updateInstallLocations` function in a separate thread
+ */
class Processing: SuspendingCliktCommand("processing"){
val version by option("-v","--version")
.flag()
@@ -61,7 +72,10 @@ class Processing: SuspendingCliktCommand("processing"){
}
}
-
+/**
+ * A command to start the Processing Language Server
+ * This is used by IDEs to provide language support for Processing sketches
+ */
class LSP: SuspendingCliktCommand("lsp"){
override fun help(context: Context) = "Start the Processing Language Server"
override suspend fun run(){
@@ -79,6 +93,11 @@ class LSP: SuspendingCliktCommand("lsp"){
}
}
+/**
+ * A command to invoke the legacy CLI of Processing
+ * This is mainly for backwards compatibility with existing scripts
+ * that use the old CLI interface
+ */
class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") {
override val treatUnknownOptionsAsArgs = true
@@ -99,6 +118,16 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") {
}
}
+/**
+ * Update the install locations in preferences
+ * The install locations are stored in the preferences as a comma separated list of paths
+ * Each path is followed by a caret (^) and the version of Processing at that location
+ * This is used by other programs to find all installed versions of Processing
+ * works from 4.4.6 onwards
+ *
+ * Example:
+ * /path/to/processing-4.0^4.0,/path/to/processing-3.5.4^3.5.4
+ */
fun updateInstallLocations(){
val preferences = Preferences.userRoot().node("org/processing/app")
val installLocations = preferences.get("installLocations", "")
diff --git a/app/src/processing/app/UpdateCheck.java b/app/src/processing/app/UpdateCheck.java
index e18daee3eb..20c91dd38c 100644
--- a/app/src/processing/app/UpdateCheck.java
+++ b/app/src/processing/app/UpdateCheck.java
@@ -63,6 +63,9 @@ public class UpdateCheck {
static private final long ONE_DAY = 24 * 60 * 60 * 1000;
+ public static void doCheck(Base base) {
+ new UpdateCheck(base);
+ }
public UpdateCheck(Base base) {
this.base = base;
diff --git a/app/src/processing/app/contrib/ContributionManager.java b/app/src/processing/app/contrib/ContributionManager.java
index c4d45f7d7d..79a7b54eb3 100644
--- a/app/src/processing/app/contrib/ContributionManager.java
+++ b/app/src/processing/app/contrib/ContributionManager.java
@@ -694,15 +694,9 @@ static private void clearRestartFlags(File root) {
static public void init(Base base) throws Exception {
-// long t1 = System.currentTimeMillis();
- // Moved here to make sure it runs on EDT [jv 170121]
contribListing = ContributionListing.getInstance();
-// long t2 = System.currentTimeMillis();
managerFrame = new ManagerFrame(base);
-// long t3 = System.currentTimeMillis();
cleanup(base);
-// long t4 = System.currentTimeMillis();
-// System.out.println("ContributionManager.init() " + (t2-t1) + " " + (t3-t2) + " " + (t4-t3));
}
diff --git a/app/src/processing/app/contrib/ManagerFrame.java b/app/src/processing/app/contrib/ManagerFrame.java
index ab68fd1db5..bc15a439e8 100644
--- a/app/src/processing/app/contrib/ManagerFrame.java
+++ b/app/src/processing/app/contrib/ManagerFrame.java
@@ -61,27 +61,15 @@ public class ManagerFrame {
public ManagerFrame(Base base) {
this.base = base;
- // TODO Optimize these inits... unfortunately it needs to run on the EDT,
- // and Swing is a piece of s*t, so it's gonna be slow with lots of contribs.
- // In particular, load everything and then fire the update events.
- // Also, don't pull all the colors over and over again.
-// long t1 = System.currentTimeMillis();
librariesTab = new ContributionTab(this, ContributionType.LIBRARY);
-// long t2 = System.currentTimeMillis();
modesTab = new ContributionTab(this, ContributionType.MODE);
-// long t3 = System.currentTimeMillis();
toolsTab = new ContributionTab(this, ContributionType.TOOL);
-// long t4 = System.currentTimeMillis();
examplesTab = new ContributionTab(this, ContributionType.EXAMPLES);
-// long t5 = System.currentTimeMillis();
updatesTab = new UpdateContributionTab(this);
-// long t6 = System.currentTimeMillis();
tabList = new ContributionTab[] {
librariesTab, modesTab, toolsTab, examplesTab, updatesTab
};
-
-// System.out.println("ManagerFrame. " + (t2-t1) + " " + (t3-t2) + " " + (t4-t3) + " " + (t5-t4) + " " + (t6-t5));
}
diff --git a/app/src/processing/app/syntax/README.md b/app/src/processing/app/syntax/README.md
index 04e7bdc328..aabe0c2e24 100644
--- a/app/src/processing/app/syntax/README.md
+++ b/app/src/processing/app/syntax/README.md
@@ -1,4 +1,16 @@
-# π Fixing this code: here be dragons. π
+# Replacing our custom version of JEditTextArea
+
+Since 2025 we have started a migration of Swing to Jetpack Compose and we will eventually need to replace the JEditTextArea as well.
+
+I think a good current strategy would be to start using `RSyntaxTextArea` for an upcoming p5.js mode. `RSyntaxTextArea` is a better maintained and well rounded library. As noted below, a lot of the current state management of the PDE is interetwined with the JEditTextArea implementation. This will force us to decouple the state management out of the `JEditTextArea` whilst also trying to keep backwards compatibility alive for Tweak Mode and the current implementation of autocomplete.
+
+I also did some more research into the potential of using a JS + LSP based editor within Jetpack Compose but as of writing (early 2025) the only way to do so would be to embed chromium into the PDE through something like [Java-CEF]([url](https://github.com/chromiumembedded/java-cef)) and it looks like a PoC for Jetpack Compose Desktop exists [here](https://github.com/JetBrains/compose-multiplatform/blob/9cd413a4ed125bee5b624550fbd40a05061e912a/experimental/cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/BrowserView.kt). Moving the entire PDE into an electron app would be essentially a rewrite which currrently is not the target.
+
+Considering the current direction of the build-in LSP within Processing, I would say that creating a LSP based editor would be a good strategy going forward.
+
+Research needs to be done on how much the Tweak Mode and autocompletion are _actually_ being used. Currently both these features are quite hidden and I suspect that most users actually move on to more advanced use-cases before they even discover such things. I would like to make both of these features much more prominent within the PDE to test if they are a good value add.
+
+### Ben Fry's notes
Every few years, we've looked at replacing this package with [RSyntaxArea](https://github.com/bobbylight/RSyntaxTextArea), most recently with two attempts during the course of developing [Processing 4](https://github.com/processing/processing4/wiki/Processing-4), but probably dating back to the mid-2000s.
diff --git a/build.gradle.kts b/build.gradle.kts
index 8e7ad44a7a..dd3df4f710 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,6 +10,7 @@ plugins {
// Set the build directory to not /build to prevent accidental deletion through the clean action
// Can be deleted after the migration to Gradle is complete
+
layout.buildDirectory = file(".build")
// Configure the dependencyUpdates task
@@ -27,4 +28,4 @@ tasks {
isNonStable(candidate.version) && !isNonStable(currentVersion)
}
}
-}
+}
\ No newline at end of file
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 6708e269dc..f4e1ceb607 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -11,18 +11,18 @@ repositories {
maven { url = uri("https://jogamp.org/deployment/maven") }
}
-sourceSets{
- main{
- java{
+sourceSets {
+ main {
+ java {
srcDirs("src")
}
- resources{
+ resources {
srcDirs("src")
exclude("**/*.java")
}
}
- test{
- java{
+ test {
+ java {
srcDirs("test")
}
}
@@ -33,13 +33,32 @@ dependencies {
implementation(libs.gluegen)
testImplementation(libs.junit)
+ testImplementation(libs.junitJupiter)
+ testImplementation(libs.junitJupiterParams)
+ testImplementation(libs.junitPlatformSuite)
+ testImplementation(libs.assertjCore)
}
-mavenPublishing{
+// Simple JUnit 5 configuration - let JUnit handle everything
+tasks.test {
+ useJUnitPlatform() // JUnit discovers and runs all tests
+
+ // Only configuration, not orchestration
+ outputs.upToDateWhen { false }
+ maxParallelForks = 1
+
+ testLogging {
+ events("passed", "skipped", "failed", "started")
+ showStandardStreams = true
+ }
+}
+
+mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
+
signAllPublications()
- pom{
+ pom {
name.set("Processing Core")
description.set("Processing Core")
url.set("https://processing.org")
@@ -59,7 +78,7 @@ mavenPublishing{
name.set("Ben Fry")
}
}
- scm{
+ scm {
url.set("https://github.com/processing/processing4")
connection.set("scm:git:git://github.com/processing/processing4.git")
developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git")
@@ -67,13 +86,9 @@ mavenPublishing{
}
}
-
-tasks.test {
- useJUnit()
-}
tasks.withType {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
-tasks.compileJava{
+tasks.compileJava {
options.encoding = "UTF-8"
-}
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-center-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-center-linux.png
new file mode 100644
index 0000000000..124d3c8db9
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/arc-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-corner-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-corner-linux.png
new file mode 100644
index 0000000000..124d3c8db9
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/arc-corner-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-corners-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-corners-linux.png
new file mode 100644
index 0000000000..124d3c8db9
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/arc-corners-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-negative-dimensions-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-negative-dimensions-linux.png
new file mode 100644
index 0000000000..a97455efbf
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/arc-negative-dimensions-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-radius-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-radius-linux.png
new file mode 100644
index 0000000000..124d3c8db9
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/arc-radius-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-center-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-center-linux.png
new file mode 100644
index 0000000000..e0fd115f37
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corner-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corner-linux.png
new file mode 100644
index 0000000000..e0fd115f37
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corner-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corners-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corners-linux.png
new file mode 100644
index 0000000000..e0fd115f37
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corners-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-negative-dimensions-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-negative-dimensions-linux.png
new file mode 100644
index 0000000000..fe97057fab
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-negative-dimensions-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-radius-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-radius-linux.png
new file mode 100644
index 0000000000..e0fd115f37
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-radius-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-center-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-center-linux.png
new file mode 100644
index 0000000000..f8b37838e3
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/rect-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-corner-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-corner-linux.png
new file mode 100644
index 0000000000..f8b37838e3
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/rect-corner-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-corners-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-corners-linux.png
new file mode 100644
index 0000000000..f8b37838e3
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/rect-corners-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-negative-dimensions-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-negative-dimensions-linux.png
new file mode 100644
index 0000000000..18a0a4a467
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/rect-negative-dimensions-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-radius-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-radius-linux.png
new file mode 100644
index 0000000000..f8b37838e3
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shape-modes/rect-radius-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-fills-linux.png b/core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-fills-linux.png
new file mode 100644
index 0000000000..608b7ffe20
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-fills-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-strokes-linux.png b/core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-strokes-linux.png
new file mode 100644
index 0000000000..7270c15323
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-strokes-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes-3d/vertex-coordinates-linux.png b/core/test/processing/visual/__screenshots__/shapes-3d/vertex-coordinates-linux.png
new file mode 100644
index 0000000000..e07fe529c8
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes-3d/vertex-coordinates-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/bezier-curves-linux.png b/core/test/processing/visual/__screenshots__/shapes/bezier-curves-linux.png
new file mode 100644
index 0000000000..e628f40541
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/bezier-curves-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/closed-curves-linux.png b/core/test/processing/visual/__screenshots__/shapes/closed-curves-linux.png
new file mode 100644
index 0000000000..2851f61f95
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/closed-curves-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/closed-polylines-linux.png b/core/test/processing/visual/__screenshots__/shapes/closed-polylines-linux.png
new file mode 100644
index 0000000000..43a6cc68ba
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/closed-polylines-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/contours-linux.png b/core/test/processing/visual/__screenshots__/shapes/contours-linux.png
new file mode 100644
index 0000000000..032c60a753
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/contours-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/curves-linux.png b/core/test/processing/visual/__screenshots__/shapes/curves-linux.png
new file mode 100644
index 0000000000..5a629e80fb
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/curves-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/curves-tightness-linux.png b/core/test/processing/visual/__screenshots__/shapes/curves-tightness-linux.png
new file mode 100644
index 0000000000..aecfee50ea
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/curves-tightness-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/lines-linux.png b/core/test/processing/visual/__screenshots__/shapes/lines-linux.png
new file mode 100644
index 0000000000..f2e42539e6
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/lines-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/points-linux.png b/core/test/processing/visual/__screenshots__/shapes/points-linux.png
new file mode 100644
index 0000000000..d0aecf8d30
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/points-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/polylines-linux.png b/core/test/processing/visual/__screenshots__/shapes/polylines-linux.png
new file mode 100644
index 0000000000..8f0a6ad363
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/polylines-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/quad-strips-linux.png b/core/test/processing/visual/__screenshots__/shapes/quad-strips-linux.png
new file mode 100644
index 0000000000..a0836a3a6d
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/quad-strips-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/quadratic-beziers-linux.png b/core/test/processing/visual/__screenshots__/shapes/quadratic-beziers-linux.png
new file mode 100644
index 0000000000..85416ec263
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/quadratic-beziers-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/quads-linux.png b/core/test/processing/visual/__screenshots__/shapes/quads-linux.png
new file mode 100644
index 0000000000..b7d2c80f9e
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/quads-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/single-closed-contour-linux.png b/core/test/processing/visual/__screenshots__/shapes/single-closed-contour-linux.png
new file mode 100644
index 0000000000..401b9974d1
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/single-closed-contour-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/single-unclosed-contour-linux.png b/core/test/processing/visual/__screenshots__/shapes/single-unclosed-contour-linux.png
new file mode 100644
index 0000000000..401b9974d1
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/single-unclosed-contour-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/triangle-fans-linux.png b/core/test/processing/visual/__screenshots__/shapes/triangle-fans-linux.png
new file mode 100644
index 0000000000..c7c2b87e64
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/triangle-fans-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/triangle-strips-linux.png b/core/test/processing/visual/__screenshots__/shapes/triangle-strips-linux.png
new file mode 100644
index 0000000000..14ee9cd38e
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/triangle-strips-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/shapes/triangles-linux.png b/core/test/processing/visual/__screenshots__/shapes/triangles-linux.png
new file mode 100644
index 0000000000..0e25fdbc03
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/shapes/triangles-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-bottom-linux.png
new file mode 100644
index 0000000000..d7d33e10a1
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-bottom-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-center-linux.png
new file mode 100644
index 0000000000..91f4252903
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-top-linux.png
new file mode 100644
index 0000000000..e8455adf5f
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-top-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-bottom-linux.png
new file mode 100644
index 0000000000..02d38070bf
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-bottom-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-center-linux.png
new file mode 100644
index 0000000000..cfc6d88916
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-top-linux.png
new file mode 100644
index 0000000000..40d07d7ca3
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-top-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-bottom-linux.png
new file mode 100644
index 0000000000..f55c6d27cf
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-bottom-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-center-linux.png
new file mode 100644
index 0000000000..8eefc1b379
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-top-linux.png
new file mode 100644
index 0000000000..d600d172b0
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-top-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-center-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-bottom-linux.png
new file mode 100644
index 0000000000..11937e1578
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-bottom-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-center-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-center-linux.png
new file mode 100644
index 0000000000..6171360efc
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-center-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-top-linux.png
new file mode 100644
index 0000000000..c3f7f02b40
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-top-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-left-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-bottom-linux.png
new file mode 100644
index 0000000000..a9c2ba4cdd
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-bottom-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-left-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-center-linux.png
new file mode 100644
index 0000000000..f736f6c20a
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-left-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-top-linux.png
new file mode 100644
index 0000000000..008db894f4
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-top-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-right-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-bottom-linux.png
new file mode 100644
index 0000000000..77db6984e9
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-bottom-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-right-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-center-linux.png
new file mode 100644
index 0000000000..a1b1fb0114
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-center-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-right-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-top-linux.png
new file mode 100644
index 0000000000..6ae69540bc
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-top-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/complex/colored-text-linux.png b/core/test/processing/visual/__screenshots__/typography/complex/colored-text-linux.png
new file mode 100644
index 0000000000..df8be566f8
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/complex/colored-text-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/complex/rotated-text-linux.png b/core/test/processing/visual/__screenshots__/typography/complex/rotated-text-linux.png
new file mode 100644
index 0000000000..b90e1aaca7
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/complex/rotated-text-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/complex/transparent-text-linux.png b/core/test/processing/visual/__screenshots__/typography/complex/transparent-text-linux.png
new file mode 100644
index 0000000000..a9118ac7c6
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/complex/transparent-text-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/font/default-font-linux.png b/core/test/processing/visual/__screenshots__/typography/font/default-font-linux.png
new file mode 100644
index 0000000000..fab815d503
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/font/default-font-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/font/monospace-font-linux.png b/core/test/processing/visual/__screenshots__/typography/font/monospace-font-linux.png
new file mode 100644
index 0000000000..2964d7d80a
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/font/monospace-font-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/font/system-font-linux.png b/core/test/processing/visual/__screenshots__/typography/font/system-font-linux.png
new file mode 100644
index 0000000000..82e2a82708
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/font/system-font-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/leading/different-values-linux.png b/core/test/processing/visual/__screenshots__/typography/leading/different-values-linux.png
new file mode 100644
index 0000000000..e7260fcc7a
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/leading/different-values-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/pfont/ascent-descent-linux.png b/core/test/processing/visual/__screenshots__/typography/pfont/ascent-descent-linux.png
new file mode 100644
index 0000000000..85dab54b37
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/pfont/ascent-descent-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/pfont/char-availability-linux.png b/core/test/processing/visual/__screenshots__/typography/pfont/char-availability-linux.png
new file mode 100644
index 0000000000..bc1f5ebf17
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/pfont/char-availability-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/size/sizes-comparison-linux.png b/core/test/processing/visual/__screenshots__/typography/size/sizes-comparison-linux.png
new file mode 100644
index 0000000000..177befc931
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/size/sizes-comparison-linux.png differ
diff --git a/core/test/processing/visual/__screenshots__/typography/width/string-width-linux.png b/core/test/processing/visual/__screenshots__/typography/width/string-width-linux.png
new file mode 100644
index 0000000000..d44f86015f
Binary files /dev/null and b/core/test/processing/visual/__screenshots__/typography/width/string-width-linux.png differ
diff --git a/core/test/processing/visual/src/core/BaselineManager.java b/core/test/processing/visual/src/core/BaselineManager.java
new file mode 100644
index 0000000000..93123ab13c
--- /dev/null
+++ b/core/test/processing/visual/src/core/BaselineManager.java
@@ -0,0 +1,38 @@
+package processing.visual.src.core;
+
+import processing.core.PImage;
+
+import java.util.List;
+
+// Baseline manager for updating reference images
+public class BaselineManager {
+ private VisualTestRunner tester;
+
+ public BaselineManager(VisualTestRunner tester) {
+ this.tester = tester;
+ }
+
+ public void updateBaseline(String testName, ProcessingSketch sketch, TestConfig config) {
+ System.out.println("Updating baseline for: " + testName);
+
+ // Capture new image
+ SketchRunner runner = new SketchRunner(sketch, config);
+ runner.run();
+ PImage newImage = runner.getImage();
+
+ // Save as baseline
+ String baselinePath = "__screenshots__/" +
+ testName.replaceAll("[^a-zA-Z0-9-_]", "-") +
+ "-" + detectPlatform() + ".png";
+ newImage.save(baselinePath);
+
+ System.out.println("Baseline updated: " + baselinePath);
+ }
+
+ private String detectPlatform() {
+ String os = System.getProperty("os.name").toLowerCase();
+ if (os.contains("mac")) return "darwin";
+ if (os.contains("win")) return "win32";
+ return "linux";
+ }
+}
diff --git a/core/test/processing/visual/src/core/ImageComparator.java b/core/test/processing/visual/src/core/ImageComparator.java
new file mode 100644
index 0000000000..2b368e8e4b
--- /dev/null
+++ b/core/test/processing/visual/src/core/ImageComparator.java
@@ -0,0 +1,417 @@
+package processing.visual.src.core;
+
+import processing.core.*;
+import java.util.*;
+
+class ComparisonResult {
+ public boolean passed;
+ public double mismatchRatio;
+ public boolean isFirstRun;
+ public PImage diffImage;
+ public ComparisonDetails details;
+
+ public ComparisonResult(boolean passed, double mismatchRatio) {
+ this.passed = passed;
+ this.mismatchRatio = mismatchRatio;
+ this.isFirstRun = false;
+ }
+
+ public ComparisonResult(boolean passed, PImage diffImage, ComparisonDetails details) {
+ this.passed = passed;
+ this.diffImage = diffImage;
+ this.details = details;
+ this.mismatchRatio = details != null ? (double) details.significantDiffPixels / (diffImage.width * diffImage.height) : 0.0;
+ this.isFirstRun = false;
+ }
+
+ public static ComparisonResult createFirstRun() {
+ ComparisonResult result = new ComparisonResult(false, 0.0);
+ result.isFirstRun = true;
+ return result;
+ }
+
+ public void saveDiffImage(String filePath) {
+ if (diffImage != null) {
+ diffImage.save(filePath);
+ System.out.println("Diff image saved: " + filePath);
+ }
+ }
+}
+
+class ComparisonDetails {
+ public int totalDiffPixels;
+ public int significantDiffPixels;
+ public List clusters;
+
+ public ComparisonDetails(int totalDiffPixels, int significantDiffPixels, List clusters) {
+ this.totalDiffPixels = totalDiffPixels;
+ this.significantDiffPixels = significantDiffPixels;
+ this.clusters = clusters;
+ }
+
+ public void printDetails() {
+ System.out.println(" Total diff pixels: " + totalDiffPixels);
+ System.out.println(" Significant diff pixels: " + significantDiffPixels);
+ System.out.println(" Clusters found: " + clusters.size());
+
+ long lineShiftClusters = clusters.stream().filter(c -> c.isLineShift).count();
+ if (lineShiftClusters > 0) {
+ System.out.println(" Line shift clusters (ignored): " + lineShiftClusters);
+ }
+
+ // Print cluster details
+ for (int i = 0; i < clusters.size(); i++) {
+ ClusterInfo cluster = clusters.get(i);
+ System.out.println(" Cluster " + (i+1) + ": size=" + cluster.size +
+ ", lineShift=" + cluster.isLineShift);
+ }
+ }
+}
+
+// Individual cluster information
+class ClusterInfo {
+ public int size;
+ public List pixels;
+ public boolean isLineShift;
+
+ public ClusterInfo(int size, List pixels, boolean isLineShift) {
+ this.size = size;
+ this.pixels = pixels;
+ this.isLineShift = isLineShift;
+ }
+}
+
+// Simple 2D point
+class Point2D {
+ public int x, y;
+
+ public Point2D(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+}
+
+// Interface for pixel matching algorithms
+interface PixelMatchingAlgorithm {
+ ComparisonResult compare(PImage baseline, PImage actual, double threshold);
+}
+
+// Your sophisticated pixel matching algorithm
+public class ImageComparator implements PixelMatchingAlgorithm {
+
+ // Algorithm constants
+ private static final int MAX_SIDE = 400;
+ private static final int BG_COLOR = 0xFFFFFFFF; // White background
+ private static final int MIN_CLUSTER_SIZE = 4;
+ private static final int MAX_TOTAL_DIFF_PIXELS = 40;
+ private static final double DEFAULT_THRESHOLD = 0.5;
+ private static final double ALPHA = 0.1;
+
+ private PApplet p; // Reference to PApplet for PImage creation
+
+ public ImageComparator(PApplet p) {
+ this.p = p;
+ }
+
+ @Override
+ public ComparisonResult compare(PImage baseline, PImage actual, double threshold) {
+ if (baseline == null || actual == null) {
+ return new ComparisonResult(false, 1.0);
+ }
+
+ try {
+ return performComparison(baseline, actual, threshold);
+ } catch (Exception e) {
+ System.err.println("Comparison failed: " + e.getMessage());
+ return new ComparisonResult(false, 1.0);
+ }
+ }
+
+ private ComparisonResult performComparison(PImage baseline, PImage actual, double threshold) {
+ // Calculate scaling
+ double scale = Math.min(
+ (double) MAX_SIDE / baseline.width,
+ (double) MAX_SIDE / baseline.height
+ );
+
+ double ratio = (double) baseline.width / baseline.height;
+ boolean narrow = ratio != 1.0;
+ if (narrow) {
+ scale *= 2;
+ }
+
+ // Resize images
+ PImage scaledActual = resizeImage(actual, scale);
+ PImage scaledBaseline = resizeImage(baseline, scale);
+
+ // Ensure both images have the same dimensions
+ int width = scaledBaseline.width;
+ int height = scaledBaseline.height;
+
+ // Create canvases with background color
+ PImage actualCanvas = createCanvasWithBackground(scaledActual, width, height);
+ PImage baselineCanvas = createCanvasWithBackground(scaledBaseline, width, height);
+
+ // Create diff output canvas
+ PImage diffCanvas = p.createImage(width, height, PImage.RGB);
+
+ // Run pixelmatch equivalent
+ int diffCount = pixelmatch(actualCanvas, baselineCanvas, diffCanvas, width, height, DEFAULT_THRESHOLD);
+
+ // If no differences, return early
+ if (diffCount == 0) {
+ return new ComparisonResult(true, diffCanvas, null);
+ }
+
+ // Post-process to identify and filter out isolated differences
+ Set visited = new HashSet<>();
+ List clusterSizes = new ArrayList<>();
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int pos = y * width + x;
+
+ // If this is a diff pixel and not yet visited
+ if (isDiffPixel(diffCanvas, x, y) && !visited.contains(pos)) {
+ ClusterInfo clusterInfo = findClusterSize(diffCanvas, x, y, width, height, visited);
+ clusterSizes.add(clusterInfo);
+ }
+ }
+ }
+
+ // Determine if the differences are significant
+ List nonLineShiftClusters = clusterSizes.stream()
+ .filter(cluster -> !cluster.isLineShift && cluster.size >= MIN_CLUSTER_SIZE)
+ .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+
+ // Calculate significant differences excluding line shifts
+ int significantDiffPixels = nonLineShiftClusters.stream()
+ .mapToInt(cluster -> cluster.size)
+ .sum();
+
+ // Determine test result
+ boolean passed = diffCount == 0 ||
+ significantDiffPixels == 0 ||
+ (significantDiffPixels <= MAX_TOTAL_DIFF_PIXELS && nonLineShiftClusters.size() <= 2);
+
+ ComparisonDetails details = new ComparisonDetails(diffCount, significantDiffPixels, clusterSizes);
+
+ return new ComparisonResult(passed, diffCanvas, details);
+ }
+
+ private PImage resizeImage(PImage image, double scale) {
+ int newWidth = (int) Math.ceil(image.width * scale);
+ int newHeight = (int) Math.ceil(image.height * scale);
+
+ PImage resized = p.createImage(newWidth, newHeight, PImage.RGB);
+ resized.copy(image, 0, 0, image.width, image.height, 0, 0, newWidth, newHeight);
+
+ return resized;
+ }
+
+ private PImage createCanvasWithBackground(PImage image, int width, int height) {
+ PImage canvas = p.createImage(width, height, PImage.RGB);
+
+ // Fill with background color (white)
+ canvas.loadPixels();
+ for (int i = 0; i < canvas.pixels.length; i++) {
+ canvas.pixels[i] = BG_COLOR;
+ }
+ canvas.updatePixels();
+
+ // Draw the image on top
+ canvas.copy(image, 0, 0, image.width, image.height, 0, 0, image.width, image.height);
+
+ return canvas;
+ }
+
+ private int pixelmatch(PImage actual, PImage expected, PImage diff, int width, int height, double threshold) {
+ int diffCount = 0;
+
+ actual.loadPixels();
+ expected.loadPixels();
+ diff.loadPixels();
+
+ for (int i = 0; i < actual.pixels.length; i++) {
+ int actualColor = actual.pixels[i];
+ int expectedColor = expected.pixels[i];
+
+ double delta = colorDelta(actualColor, expectedColor);
+
+ if (delta > threshold) {
+ // Mark as different (bright red pixel)
+ diff.pixels[i] = 0xFFFF0000; // Red
+ diffCount++;
+ } else {
+ // Mark as same (dimmed version of actual image)
+ int dimColor = dimColor(actualColor, ALPHA);
+ diff.pixels[i] = dimColor;
+ }
+ }
+
+ diff.updatePixels();
+ return diffCount;
+ }
+
+ private double colorDelta(int color1, int color2) {
+ int r1 = (color1 >> 16) & 0xFF;
+ int g1 = (color1 >> 8) & 0xFF;
+ int b1 = color1 & 0xFF;
+ int a1 = (color1 >> 24) & 0xFF;
+
+ int r2 = (color2 >> 16) & 0xFF;
+ int g2 = (color2 >> 8) & 0xFF;
+ int b2 = color2 & 0xFF;
+ int a2 = (color2 >> 24) & 0xFF;
+
+ int dr = r1 - r2;
+ int dg = g1 - g2;
+ int db = b1 - b2;
+ int da = a1 - a2;
+
+ return Math.sqrt(dr * dr + dg * dg + db * db + da * da) / 255.0;
+ }
+
+ private int dimColor(int color, double alpha) {
+ int r = (int) (((color >> 16) & 0xFF) * alpha);
+ int g = (int) (((color >> 8) & 0xFF) * alpha);
+ int b = (int) ((color & 0xFF) * alpha);
+ int a = (int) (255 * alpha);
+
+ r = Math.max(0, Math.min(255, r));
+ g = Math.max(0, Math.min(255, g));
+ b = Math.max(0, Math.min(255, b));
+ a = Math.max(0, Math.min(255, a));
+
+ return (a << 24) | (r << 16) | (g << 8) | b;
+ }
+
+ private boolean isDiffPixel(PImage image, int x, int y) {
+ if (x < 0 || x >= image.width || y < 0 || y >= image.height) return false;
+
+ image.loadPixels();
+ int color = image.pixels[y * image.width + x];
+
+ int r = (color >> 16) & 0xFF;
+ int g = (color >> 8) & 0xFF;
+ int b = color & 0xFF;
+
+ return r == 255 && g == 0 && b == 0;
+ }
+
+ private ClusterInfo findClusterSize(PImage diffImage, int startX, int startY, int width, int height, Set visited) {
+ List queue = new ArrayList<>();
+ queue.add(new Point2D(startX, startY));
+
+ int size = 0;
+ List clusterPixels = new ArrayList<>();
+
+ while (!queue.isEmpty()) {
+ Point2D point = queue.remove(0);
+ int pos = point.y * width + point.x;
+
+ // Skip if already visited
+ if (visited.contains(pos)) continue;
+
+ // Skip if not a diff pixel
+ if (!isDiffPixel(diffImage, point.x, point.y)) continue;
+
+ // Mark as visited
+ visited.add(pos);
+ size++;
+ clusterPixels.add(point);
+
+ // Add neighbors to queue
+ for (int dy = -1; dy <= 1; dy++) {
+ for (int dx = -1; dx <= 1; dx++) {
+ if (dx == 0 && dy == 0) continue;
+
+ int nx = point.x + dx;
+ int ny = point.y + dy;
+
+ // Skip if out of bounds
+ if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
+
+ // Skip if already visited
+ int npos = ny * width + nx;
+ if (!visited.contains(npos)) {
+ queue.add(new Point2D(nx, ny));
+ }
+ }
+ }
+ }
+
+ // Determine if this is a line shift
+ boolean isLineShift = detectLineShift(clusterPixels, diffImage, width, height);
+
+ return new ClusterInfo(size, clusterPixels, isLineShift);
+ }
+
+ private boolean detectLineShift(List clusterPixels, PImage diffImage, int width, int height) {
+ if (clusterPixels.isEmpty()) return false;
+
+ int linelikePixels = 0;
+
+ for (Point2D pixel : clusterPixels) {
+ int neighbors = 0;
+ for (int dy = -1; dy <= 1; dy++) {
+ for (int dx = -1; dx <= 1; dx++) {
+ if (dx == 0 && dy == 0) continue; // Skip self
+
+ int nx = pixel.x + dx;
+ int ny = pixel.y + dy;
+
+ // Skip if out of bounds
+ if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
+
+ // Check if neighbor is a diff pixel
+ if (isDiffPixel(diffImage, nx, ny)) {
+ neighbors++;
+ }
+ }
+ }
+
+ // Line-like pixels typically have 1-2 neighbors
+ if (neighbors <= 2) {
+ linelikePixels++;
+ }
+ }
+
+ // If most pixels (>80%) in the cluster have β€2 neighbors, it's likely a line shift
+ return (double) linelikePixels / clusterPixels.size() > 0.8;
+ }
+
+ // Configuration methods
+ public ImageComparator setMaxSide(int maxSide) {
+ // For future configurability
+ return this;
+ }
+
+ public ImageComparator setMinClusterSize(int minClusterSize) {
+ // For future configurability
+ return this;
+ }
+
+ public ImageComparator setMaxTotalDiffPixels(int maxTotalDiffPixels) {
+ // For future configurability
+ return this;
+ }
+}
+
+// Utility class for algorithm configuration
+class ComparatorConfig {
+ public int maxSide = 400;
+ public int minClusterSize = 4;
+ public int maxTotalDiffPixels = 40;
+ public double threshold = 0.5;
+ public double alpha = 0.1;
+ public int backgroundColor = 0xFFFFFFFF;
+
+ public ComparatorConfig() {}
+
+ public ComparatorConfig(int maxSide, int minClusterSize, int maxTotalDiffPixels) {
+ this.maxSide = maxSide;
+ this.minClusterSize = minClusterSize;
+ this.maxTotalDiffPixels = maxTotalDiffPixels;
+ }
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/src/core/ProcessingSketch.java b/core/test/processing/visual/src/core/ProcessingSketch.java
new file mode 100644
index 0000000000..e4750490b6
--- /dev/null
+++ b/core/test/processing/visual/src/core/ProcessingSketch.java
@@ -0,0 +1,9 @@
+package processing.visual.src.core;
+
+import processing.core.PApplet;
+
+// Interface for user sketches
+public interface ProcessingSketch {
+ void setup(PApplet p);
+ void draw(PApplet p);
+}
diff --git a/core/test/processing/visual/src/core/TestConfig.java b/core/test/processing/visual/src/core/TestConfig.java
new file mode 100644
index 0000000000..fd39bb91e7
--- /dev/null
+++ b/core/test/processing/visual/src/core/TestConfig.java
@@ -0,0 +1,33 @@
+package processing.visual.src.core;
+
+// Test configuration class
+public class TestConfig {
+ public int width = 800;
+ public int height = 600;
+ public int[] backgroundColor = {255, 255, 255}; // RGB
+ public long renderWaitTime = 100; // milliseconds
+ public double threshold = 0.1;
+
+ public TestConfig() {}
+
+ public TestConfig(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public TestConfig(int width, int height, int[] backgroundColor) {
+ this.width = width;
+ this.height = height;
+ this.backgroundColor = backgroundColor;
+ }
+
+ public TestConfig setThreshold(double threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public TestConfig setRenderWaitTime(long waitTime) {
+ this.renderWaitTime = waitTime;
+ return this;
+ }
+}
diff --git a/core/test/processing/visual/src/core/TestResult.java b/core/test/processing/visual/src/core/TestResult.java
new file mode 100644
index 0000000000..6ff7c57ac7
--- /dev/null
+++ b/core/test/processing/visual/src/core/TestResult.java
@@ -0,0 +1,45 @@
+package processing.visual.src.core;
+
+// Enhanced test result with detailed information
+public class TestResult {
+ public String testName;
+ public boolean passed;
+ public double mismatchRatio;
+ public String error;
+ public boolean isFirstRun;
+ public ComparisonDetails details;
+
+ public TestResult(String testName, ComparisonResult comparison) {
+ this.testName = testName;
+ this.passed = comparison.passed;
+ this.mismatchRatio = comparison.mismatchRatio;
+ this.isFirstRun = comparison.isFirstRun;
+ this.details = comparison.details;
+ }
+
+ public static TestResult createError(String testName, String error) {
+ TestResult result = new TestResult();
+ result.testName = testName;
+ result.passed = false;
+ result.error = error;
+ return result;
+ }
+
+ private TestResult() {} // For error constructor
+
+ public void printResult() {
+ System.out.print(testName + ": ");
+ if (error != null) {
+ System.out.println("ERROR - " + error);
+ } else if (isFirstRun) {
+ System.out.println("BASELINE CREATED");
+ } else if (passed) {
+ System.out.println("PASSED");
+ } else {
+ System.out.println("FAILED (mismatch: " + String.format("%.4f", mismatchRatio * 100) + "%)");
+ if (details != null) {
+ details.printDetails();
+ }
+ }
+ }
+}
diff --git a/core/test/processing/visual/src/core/VisualTestRunner.java b/core/test/processing/visual/src/core/VisualTestRunner.java
new file mode 100644
index 0000000000..758ff0ec30
--- /dev/null
+++ b/core/test/processing/visual/src/core/VisualTestRunner.java
@@ -0,0 +1,264 @@
+package processing.visual.src.core;
+
+import processing.core.*;
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+
+// Core visual tester class
+public class VisualTestRunner {
+
+ private String screenshotDir;
+ private PixelMatchingAlgorithm pixelMatcher;
+ private String platform;
+
+ public VisualTestRunner(PixelMatchingAlgorithm pixelMatcher) {
+ this.pixelMatcher = pixelMatcher;
+ this.screenshotDir = "test/processing/visual/__screenshots__";
+ this.platform = detectPlatform();
+ createDirectoryIfNotExists(screenshotDir);
+ }
+
+ public VisualTestRunner(PixelMatchingAlgorithm pixelMatcher, String screenshotDir) {
+ this.pixelMatcher = pixelMatcher;
+ this.screenshotDir = screenshotDir;
+ this.platform = detectPlatform();
+ createDirectoryIfNotExists(screenshotDir);
+ }
+
+ // Main test execution method
+ public TestResult runVisualTest(String testName, ProcessingSketch sketch) {
+ return runVisualTest(testName, sketch, new TestConfig());
+ }
+
+ public TestResult runVisualTest(String testName, ProcessingSketch sketch, TestConfig config) {
+ try {
+ System.out.println("Running visual test: " + testName);
+
+ // Capture screenshot from sketch
+ PImage actualImage = captureSketch(sketch, config);
+
+ // Compare with baseline
+ ComparisonResult comparison = compareWithBaseline(testName, actualImage, config);
+
+ return new TestResult(testName, comparison);
+
+ } catch (Exception e) {
+ return TestResult.createError(testName, e.getMessage());
+ }
+ }
+
+ // Capture PImage from Processing sketch
+ private PImage captureSketch(ProcessingSketch sketch, TestConfig config) {
+ SketchRunner runner = new SketchRunner(sketch, config);
+ runner.run();
+ return runner.getImage();
+ }
+
+ // Compare actual image with baseline
+ private ComparisonResult compareWithBaseline(String testName, PImage actualImage, TestConfig config) {
+ String baselinePath = getBaselinePath(testName);
+
+ PImage baselineImage = loadBaseline(baselinePath);
+
+ if (baselineImage == null) {
+ // First run - save as baseline
+ saveBaseline(testName, actualImage);
+ return ComparisonResult.createFirstRun();
+ }
+
+ // Use your sophisticated pixel matching algorithm
+ ComparisonResult result = pixelMatcher.compare(baselineImage, actualImage, config.threshold);
+
+ // Save diff images if test failed
+ if (!result.passed && result.diffImage != null) {
+ saveDiffImage(testName, result.diffImage);
+ }
+
+ return result;
+ }
+
+ // Save diff image for debugging
+ private void saveDiffImage(String testName, PImage diffImage) {
+ String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_]", "-");
+ String diffPath;
+ if (sanitizedName.contains("/")) {
+ diffPath = "test/processing/visual/diff_" + sanitizedName.replace("/", "_") + "-" + platform + ".png";
+ } else {
+ diffPath = "test/processing/visual/diff_" + sanitizedName + "-" + platform + ".png";
+ }
+
+ File diffFile = new File(diffPath);
+ diffFile.getParentFile().mkdirs();
+
+ diffImage.save(diffPath);
+ System.out.println("Diff image saved: " + diffPath);
+ }
+
+ // Utility methods
+ private String detectPlatform() {
+ String os = System.getProperty("os.name").toLowerCase();
+ if (os.contains("mac")) return "darwin";
+ if (os.contains("win")) return "win32";
+ return "linux";
+ }
+
+ private void createDirectoryIfNotExists(String dir) {
+ try {
+ Files.createDirectories(Paths.get(dir));
+ } catch (IOException e) {
+ System.err.println("Failed to create directory: " + dir);
+ }
+ }
+
+ private String getBaselinePath(String testName) {
+ String sanitizedName = testName.replaceAll("[^a-zA-Z0-9-_/]", "-");
+
+ return screenshotDir + "/" + sanitizedName + "-" + platform + ".png";
+ }
+
+ // Replace loadBaseline method:
+ private PImage loadBaseline(String path) {
+ File file = new File(path);
+ if (!file.exists()) {
+ System.out.println("loadBaseline: File doesn't exist: " + file.getAbsolutePath());
+ return null;
+ }
+
+ try {
+ System.out.println("loadBaseline: Loading from " + file.getAbsolutePath());
+
+ // Use Java ImageIO instead of PApplet
+ BufferedImage img = ImageIO.read(file);
+
+ if (img == null) {
+ System.out.println("loadBaseline: ImageIO returned null");
+ return null;
+ }
+
+ // Convert BufferedImage to PImage
+ PImage pImg = new PImage(img.getWidth(), img.getHeight(), PImage.RGB);
+ img.getRGB(0, 0, pImg.width, pImg.height, pImg.pixels, 0, pImg.width);
+ pImg.updatePixels();
+
+ System.out.println("loadBaseline: β Loaded " + pImg.width + "x" + pImg.height);
+ return pImg;
+
+ } catch (Exception e) {
+ System.err.println("loadBaseline: Error loading image: " + e.getMessage());
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ // Replace saveBaseline method:
+ private void saveBaseline(String testName, PImage image) {
+ String path = getBaselinePath(testName);
+
+ if (image == null) {
+ System.out.println("saveBaseline: β Image is null!");
+ return;
+ }
+
+ try {
+ // Convert PImage to BufferedImage
+ BufferedImage bImg = new BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB);
+ image.loadPixels();
+ bImg.setRGB(0, 0, image.width, image.height, image.pixels, 0, image.width);
+
+ // Create File object and ensure parent directories exist
+ File outputFile = new File(path);
+ outputFile.getParentFile().mkdirs(); // This creates nested directories
+
+ // Use Java ImageIO to save
+ ImageIO.write(bImg, "PNG", outputFile);
+
+ System.out.println("Baseline saved: " + path);
+
+ } catch (Exception e) {
+ System.err.println("Failed to save baseline: " + path);
+ e.printStackTrace();
+ }
+ }
+}
+class SketchRunner extends PApplet {
+
+ private ProcessingSketch userSketch;
+ private TestConfig config;
+ private PImage capturedImage;
+ private volatile boolean rendered = false;
+
+ public SketchRunner(ProcessingSketch userSketch, TestConfig config) {
+ this.userSketch = userSketch;
+ this.config = config;
+ }
+
+ public void settings() {
+ size(config.width, config.height);
+ pixelDensity(1);
+ }
+
+ public void setup() {
+ noLoop();
+
+ // Set background if specified
+ if (config.backgroundColor != null) {
+ background(config.backgroundColor[0], config.backgroundColor[1], config.backgroundColor[2]);
+ }
+
+ // Call user setup
+ userSketch.setup(this);
+ }
+
+ public void draw() {
+ if (!rendered) {
+ userSketch.draw(this);
+ capturedImage = get();
+ rendered = true;
+ noLoop();
+ }
+ }
+
+ public void run() {
+ String[] args = {"SketchRunner"};
+ PApplet.runSketch(args, this);
+
+ // Simple polling with timeout
+ int maxWait = 100; // 10 seconds max
+ int waited = 0;
+
+ while (!rendered && waited < maxWait) {
+ try {
+ Thread.sleep(100);
+ waited++;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ // Additional wait time
+ try {
+ Thread.sleep(config.renderWaitTime);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ if (surface != null) {
+ surface.setVisible(false);
+ }
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public PImage getImage() {
+ return capturedImage;
+ }
+}
diff --git a/core/test/processing/visual/src/test/base/VisualTest.java b/core/test/processing/visual/src/test/base/VisualTest.java
new file mode 100644
index 0000000000..55804b4acb
--- /dev/null
+++ b/core/test/processing/visual/src/test/base/VisualTest.java
@@ -0,0 +1,61 @@
+package processing.visual.src.test.base;
+
+import org.junit.jupiter.api.*;
+import processing.core.*;
+import static org.junit.jupiter.api.Assertions.*;
+import processing.visual.src.core.*;
+import java.nio.file.*;
+import java.io.File;
+
+/**
+ * Base class for Processing visual tests using JUnit 5
+ */
+public abstract class VisualTest {
+
+ protected static VisualTestRunner testRunner;
+ protected static ImageComparator comparator;
+
+ @BeforeAll
+ public static void setupTestRunner() {
+ PApplet tempApplet = new PApplet();
+ comparator = new ImageComparator(tempApplet);
+ testRunner = new VisualTestRunner(comparator);
+
+ System.out.println("Visual test runner initialized");
+ }
+
+ /**
+ * Helper method to run a visual test
+ */
+ protected void assertVisualMatch(String testName, ProcessingSketch sketch) {
+ assertVisualMatch(testName, sketch, new TestConfig());
+ }
+
+ protected void assertVisualMatch(String testName, ProcessingSketch sketch, TestConfig config) {
+ TestResult result = testRunner.runVisualTest(testName, sketch, config);
+
+ // Print result for debugging
+ result.printResult();
+
+ // Handle different result types
+ if (result.isFirstRun) {
+ // First run - baseline created, mark as skipped
+ Assumptions.assumeTrue(false, "Baseline created for " + testName + ". Run tests again to verify.");
+ } else if (result.error != null) {
+ fail("Test error: " + result.error);
+ } else {
+ // Assert that the test passed
+ Assertions.assertTrue(result.passed,
+ String.format("Visual test '%s' failed with mismatch ratio: %.4f%%",
+ testName, result.mismatchRatio * 100));
+ }
+ }
+
+ /**
+ * Update baseline for a specific test (useful for maintenance)
+ */
+ protected void updateBaseline(String testName, ProcessingSketch sketch, TestConfig config) {
+ BaselineManager manager = new BaselineManager(testRunner);
+ manager.updateBaseline(testName, sketch, config);
+ }
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java b/core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java
new file mode 100644
index 0000000000..b2c8c7efaa
--- /dev/null
+++ b/core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java
@@ -0,0 +1,335 @@
+package processing.visual.src.test.shapemodes;
+
+import org.junit.jupiter.api.*;
+import processing.core.*;
+import processing.visual.src.test.base.VisualTest;
+import processing.visual.src.core.ProcessingSketch;
+import processing.visual.src.core.TestConfig;
+
+@Tag("shapes")
+@Tag("modes")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class ShapeModeTest extends VisualTest {
+
+ /**
+ * Helper function that draws a shape using the specified shape mode
+ * @param p The PApplet instance
+ * @param shape The shape to draw: "ellipse", "arc", or "rect"
+ * @param mode The mode constant (CORNERS, CORNER, CENTER, or RADIUS)
+ * @param x1 First x coordinate
+ * @param y1 First y coordinate
+ * @param x2 Second x/width coordinate
+ * @param y2 Second y/height coordinate
+ */
+ private void shapeCorners(PApplet p, String shape, int mode, float x1, float y1, float x2, float y2) {
+ // Adjust coordinates for testing modes other than CORNERS
+ if (mode == PApplet.CORNER) {
+ // Find top left corner
+ float x = PApplet.min(x1, x2);
+ float y = PApplet.min(y1, y2);
+ // Calculate width and height
+ // Don't use abs(), so we get negative values as well
+ float w = x2 - x1;
+ float h = y2 - y1;
+ // For negative widths/heights, adjust position so shapes align consistently
+ // Rects flip/mirror, but ellipses/arcs should be positioned consistently
+ if (w < 0) { x += (-w); } // Move right
+ if (h < 0) { y += (-h); } // Move down
+ x1 = x; y1 = y; x2 = w; y2 = h;
+ } else if (mode == PApplet.CENTER) {
+ // Find center
+ float x = (x2 + x1) / 2f;
+ float y = (y2 + y1) / 2f;
+ // Calculate width and height
+ // Don't use abs(), so we get negative values as well
+ float w = x2 - x1;
+ float h = y2 - y1;
+ x1 = x; y1 = y; x2 = w; y2 = h;
+ } else if (mode == PApplet.RADIUS) {
+ // Find Center
+ float x = (x2 + x1) / 2f;
+ float y = (y2 + y1) / 2f;
+ // Calculate radii
+ // Don't use abs(), so we get negative values as well
+ float r1 = (x2 - x1) / 2f;
+ float r2 = (y2 - y1) / 2f;
+ x1 = x; y1 = y; x2 = r1; y2 = r2;
+ }
+
+ if (shape.equals("ellipse")) {
+ p.ellipseMode(mode);
+ p.ellipse(x1, y1, x2, y2);
+ } else if (shape.equals("arc")) {
+ // Draw four arcs with gaps inbetween
+ final float GAP = PApplet.radians(20);
+ p.ellipseMode(mode);
+ p.arc(x1, y1, x2, y2, 0 + GAP, PApplet.HALF_PI - GAP);
+ p.arc(x1, y1, x2, y2, PApplet.HALF_PI + GAP, PApplet.PI - GAP);
+ p.arc(x1, y1, x2, y2, PApplet.PI + GAP, PApplet.PI + PApplet.HALF_PI - GAP);
+ p.arc(x1, y1, x2, y2, PApplet.PI + PApplet.HALF_PI + GAP, PApplet.TWO_PI - GAP);
+ } else if (shape.equals("rect")) {
+ p.rectMode(mode);
+ p.rect(x1, y1, x2, y2);
+ }
+ }
+
+ /**
+ * Helper to draw shapes in all four quadrants with various coordinate configurations
+ */
+ private void drawShapesInQuadrants(PApplet p, String shape, int mode) {
+ p.translate(p.width / 2f, p.height / 2f);
+
+ // Quadrant I (Bottom Right)
+ // P1 P2
+ shapeCorners(p, shape, mode, 5, 5, 25, 15); // P1 Top Left, P2 Bottom Right
+ shapeCorners(p, shape, mode, 5, 20, 25, 30); // P1 Bottom Left, P2 Top Right
+ shapeCorners(p, shape, mode, 25, 45, 5, 35); // P1 Bottom Right, P2 Top Left
+ shapeCorners(p, shape, mode, 25, 50, 5, 60); // P1 Top Right, P2 Bottom Left
+
+ // Quadrant II (Bottom Left)
+ shapeCorners(p, shape, mode, -25, 5, -5, 15);
+ shapeCorners(p, shape, mode, -25, 20, -5, 30);
+ shapeCorners(p, shape, mode, -5, 45, -25, 35);
+ shapeCorners(p, shape, mode, -5, 50, -25, 60);
+
+ // Quadrant III (Top Left)
+ shapeCorners(p, shape, mode, -25, -60, -5, -50);
+ shapeCorners(p, shape, mode, -25, -35, -5, -45);
+ shapeCorners(p, shape, mode, -5, -20, -25, -30);
+ shapeCorners(p, shape, mode, -5, -15, -25, -5);
+
+ // Quadrant IV (Top Right)
+ shapeCorners(p, shape, mode, 5, -60, 25, -50);
+ shapeCorners(p, shape, mode, 5, -35, 25, -45);
+ shapeCorners(p, shape, mode, 25, -20, 5, -30);
+ shapeCorners(p, shape, mode, 25, -15, 5, -5);
+ }
+
+ private ProcessingSketch createShapeModeTest(String shape, int mode) {
+ return new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.background(200);
+ p.fill(255);
+ p.stroke(0);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ drawShapesInQuadrants(p, shape, mode);
+ }
+ };
+ }
+
+ // ========== Ellipse Mode Tests ==========
+
+ @Test
+ @Order(1)
+ @Tag("ellipse")
+ @DisplayName("Ellipse with CORNERS mode")
+ public void testEllipseCorners() {
+ assertVisualMatch("shape-modes/ellipse-corners",
+ createShapeModeTest("ellipse", PApplet.CORNERS),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(2)
+ @Tag("ellipse")
+ @DisplayName("Ellipse with CORNER mode")
+ public void testEllipseCorner() {
+ assertVisualMatch("shape-modes/ellipse-corner",
+ createShapeModeTest("ellipse", PApplet.CORNER),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(3)
+ @Tag("ellipse")
+ @DisplayName("Ellipse with CENTER mode")
+ public void testEllipseCenter() {
+ assertVisualMatch("shape-modes/ellipse-center",
+ createShapeModeTest("ellipse", PApplet.CENTER),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(4)
+ @Tag("ellipse")
+ @DisplayName("Ellipse with RADIUS mode")
+ public void testEllipseRadius() {
+ assertVisualMatch("shape-modes/ellipse-radius",
+ createShapeModeTest("ellipse", PApplet.RADIUS),
+ new TestConfig(60, 125));
+ }
+
+ // ========== Arc Mode Tests ==========
+
+ @Test
+ @Order(5)
+ @Tag("arc")
+ @DisplayName("Arc with CORNERS mode")
+ public void testArcCorners() {
+ assertVisualMatch("shape-modes/arc-corners",
+ createShapeModeTest("arc", PApplet.CORNERS),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(6)
+ @Tag("arc")
+ @DisplayName("Arc with CORNER mode")
+ public void testArcCorner() {
+ assertVisualMatch("shape-modes/arc-corner",
+ createShapeModeTest("arc", PApplet.CORNER),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(7)
+ @Tag("arc")
+ @DisplayName("Arc with CENTER mode")
+ public void testArcCenter() {
+ assertVisualMatch("shape-modes/arc-center",
+ createShapeModeTest("arc", PApplet.CENTER),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(8)
+ @Tag("arc")
+ @DisplayName("Arc with RADIUS mode")
+ public void testArcRadius() {
+ assertVisualMatch("shape-modes/arc-radius",
+ createShapeModeTest("arc", PApplet.RADIUS),
+ new TestConfig(60, 125));
+ }
+
+ // ========== Rect Mode Tests ==========
+
+ @Test
+ @Order(9)
+ @Tag("rect")
+ @DisplayName("Rect with CORNERS mode")
+ public void testRectCorners() {
+ assertVisualMatch("shape-modes/rect-corners",
+ createShapeModeTest("rect", PApplet.CORNERS),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(10)
+ @Tag("rect")
+ @DisplayName("Rect with CORNER mode")
+ public void testRectCorner() {
+ assertVisualMatch("shape-modes/rect-corner",
+ createShapeModeTest("rect", PApplet.CORNER),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(11)
+ @Tag("rect")
+ @DisplayName("Rect with CENTER mode")
+ public void testRectCenter() {
+ assertVisualMatch("shape-modes/rect-center",
+ createShapeModeTest("rect", PApplet.CENTER),
+ new TestConfig(60, 125));
+ }
+
+ @Test
+ @Order(12)
+ @Tag("rect")
+ @DisplayName("Rect with RADIUS mode")
+ public void testRectRadius() {
+ assertVisualMatch("shape-modes/rect-radius",
+ createShapeModeTest("rect", PApplet.RADIUS),
+ new TestConfig(60, 125));
+ }
+
+ // ========== Negative Dimensions Tests ==========
+
+ @Test
+ @Order(13)
+ @Tag("negative-dimensions")
+ @DisplayName("Rect with negative dimensions")
+ public void testRectNegativeDimensions() {
+ assertVisualMatch("shape-modes/rect-negative-dimensions", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.background(200);
+ p.fill(255);
+ p.stroke(0);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.translate(p.width / 2f, p.height / 2f);
+ p.rectMode(PApplet.CORNER);
+ p.rect(0, 0, 20, 10);
+ p.fill(255, 0, 0);
+ p.rect(0, 0, -20, 10);
+ p.fill(0, 255, 0);
+ p.rect(0, 0, 20, -10);
+ p.fill(0, 0, 255);
+ p.rect(0, 0, -20, -10);
+ }
+ }, new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(14)
+ @Tag("negative-dimensions")
+ @DisplayName("Ellipse with negative dimensions")
+ public void testEllipseNegativeDimensions() {
+ assertVisualMatch("shape-modes/ellipse-negative-dimensions", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.background(200);
+ p.fill(255);
+ p.stroke(0);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.translate(p.width / 2f, p.height / 2f);
+ p.ellipseMode(PApplet.CORNER);
+ p.ellipse(0, 0, 20, 10);
+ p.fill(255, 0, 0);
+ p.ellipse(0, 0, -20, 10);
+ p.fill(0, 255, 0);
+ p.ellipse(0, 0, 20, -10);
+ p.fill(0, 0, 255);
+ p.ellipse(0, 0, -20, -10);
+ }
+ }, new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(15)
+ @Tag("negative-dimensions")
+ @DisplayName("Arc with negative dimensions")
+ public void testArcNegativeDimensions() {
+ assertVisualMatch("shape-modes/arc-negative-dimensions", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.background(200);
+ p.fill(255);
+ p.stroke(0);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.translate(p.width / 2f, p.height / 2f);
+ p.ellipseMode(PApplet.CORNER);
+ p.arc(0, 0, 20, 10, 0, PApplet.PI + PApplet.HALF_PI);
+ p.fill(255, 0, 0);
+ p.arc(0, 0, -20, 10, 0, PApplet.PI + PApplet.HALF_PI);
+ p.fill(0, 255, 0);
+ p.arc(0, 0, 20, -10, 0, PApplet.PI + PApplet.HALF_PI);
+ p.fill(0, 0, 255);
+ p.arc(0, 0, -20, -10, 0, PApplet.PI + PApplet.HALF_PI);
+ }
+ }, new TestConfig(50, 50));
+ }
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/src/test/shapes/Shape3DTest.java b/core/test/processing/visual/src/test/shapes/Shape3DTest.java
new file mode 100644
index 0000000000..7006cf329b
--- /dev/null
+++ b/core/test/processing/visual/src/test/shapes/Shape3DTest.java
@@ -0,0 +1,84 @@
+package processing.visual.src.test.shapes;
+
+import org.junit.jupiter.api.*;
+import processing.core.*;
+import processing.visual.src.test.base.VisualTest;
+import processing.visual.src.core.ProcessingSketch;
+import processing.visual.src.core.TestConfig;
+
+@Tag("shapes")
+@Tag("3d")
+@Tag("p3d")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class Shape3DTest extends VisualTest {
+
+ private ProcessingSketch create3DTest(Shape3DCallback callback) {
+ return new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ // P3D mode setup would go here if supported
+ p.background(200);
+ p.fill(255);
+ p.stroke(0);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ callback.draw(p);
+ }
+ };
+ }
+
+ @FunctionalInterface
+ interface Shape3DCallback {
+ void draw(PApplet p);
+ }
+
+ @Test
+ @DisplayName("3D vertex coordinates")
+ public void test3DVertexCoordinates() {
+ assertVisualMatch("shapes-3d/vertex-coordinates", create3DTest(p -> {
+ p.beginShape(PApplet.QUAD_STRIP);
+ p.vertex(10, 10, 0);
+ p.vertex(10, 40, -150);
+ p.vertex(40, 10, 150);
+ p.vertex(40, 40, 200);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @DisplayName("Per-vertex fills")
+ public void testPerVertexFills() {
+ assertVisualMatch("shapes-3d/per-vertex-fills", create3DTest(p -> {
+ p.beginShape(PApplet.QUAD_STRIP);
+ p.fill(0);
+ p.vertex(10, 10);
+ p.fill(255, 0, 0);
+ p.vertex(45, 5);
+ p.fill(0, 255, 0);
+ p.vertex(15, 35);
+ p.fill(255, 255, 0);
+ p.vertex(40, 45);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @DisplayName("Per-vertex strokes")
+ public void testPerVertexStrokes() {
+ assertVisualMatch("shapes-3d/per-vertex-strokes", create3DTest(p -> {
+ p.strokeWeight(5);
+ p.beginShape(PApplet.QUAD_STRIP);
+ p.stroke(0);
+ p.vertex(10, 10);
+ p.stroke(255, 0, 0);
+ p.vertex(45, 5);
+ p.stroke(0, 255, 0);
+ p.vertex(15, 35);
+ p.stroke(255, 255, 0);
+ p.vertex(40, 45);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/src/test/shapes/ShapeTest.java b/core/test/processing/visual/src/test/shapes/ShapeTest.java
new file mode 100644
index 0000000000..47ae08b5f3
--- /dev/null
+++ b/core/test/processing/visual/src/test/shapes/ShapeTest.java
@@ -0,0 +1,356 @@
+package processing.visual.src.test.shapes;
+
+import org.junit.jupiter.api.*;
+import processing.core.*;
+import processing.visual.src.test.base.VisualTest;
+import processing.visual.src.core.ProcessingSketch;
+import processing.visual.src.core.TestConfig;
+
+@Tag("shapes")
+@Tag("rendering")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class ShapeTest extends VisualTest {
+
+ // Helper method for common setup
+ private ProcessingSketch createShapeTest(ShapeDrawingCallback callback) {
+ return new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.background(200);
+ p.fill(255);
+ p.stroke(0);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ callback.draw(p);
+ }
+ };
+ }
+
+ @FunctionalInterface
+ interface ShapeDrawingCallback {
+ void draw(PApplet p);
+ }
+
+ // ========== Polylines ==========
+
+ @Test
+ @Order(1)
+ @Tag("polylines")
+ @DisplayName("Drawing polylines")
+ public void testPolylines() {
+ assertVisualMatch("shapes/polylines", createShapeTest(p -> {
+ p.beginShape();
+ p.vertex(10, 10);
+ p.vertex(15, 40);
+ p.vertex(40, 35);
+ p.vertex(25, 15);
+ p.vertex(15, 25);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(2)
+ @Tag("polylines")
+ @DisplayName("Drawing closed polylines")
+ public void testClosedPolylines() {
+ assertVisualMatch("shapes/closed-polylines", createShapeTest(p -> {
+ p.beginShape();
+ p.vertex(10, 10);
+ p.vertex(15, 40);
+ p.vertex(40, 35);
+ p.vertex(25, 15);
+ p.vertex(15, 25);
+ p.endShape(PApplet.CLOSE);
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Contours ==========
+
+ @Test
+ @Order(3)
+ @Tag("contours")
+ @DisplayName("Drawing with contours")
+ public void testContours() {
+ assertVisualMatch("shapes/contours", createShapeTest(p -> {
+ p.beginShape();
+ // Outer circle
+ vertexCircle(p, 15, 15, 10, 1);
+
+ // Inner cutout
+ p.beginContour();
+ vertexCircle(p, 15, 15, 5, -1);
+ p.endContour();
+
+ // Second outer shape
+ p.beginContour();
+ vertexCircle(p, 30, 30, 8, -1);
+ p.endContour();
+
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(4)
+ @Tag("contours")
+ @DisplayName("Drawing with a single closed contour")
+ public void testSingleClosedContour() {
+ assertVisualMatch("shapes/single-closed-contour", createShapeTest(p -> {
+ p.beginShape();
+ p.vertex(10, 10);
+ p.vertex(40, 10);
+ p.vertex(40, 40);
+ p.vertex(10, 40);
+
+ p.beginContour();
+ p.vertex(20, 20);
+ p.vertex(20, 30);
+ p.vertex(30, 30);
+ p.vertex(30, 20);
+ p.endContour();
+
+ p.endShape(PApplet.CLOSE);
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(5)
+ @Tag("contours")
+ @DisplayName("Drawing with a single unclosed contour")
+ public void testSingleUnclosedContour() {
+ assertVisualMatch("shapes/single-unclosed-contour", createShapeTest(p -> {
+ p.beginShape();
+ p.vertex(10, 10);
+ p.vertex(40, 10);
+ p.vertex(40, 40);
+ p.vertex(10, 40);
+
+ p.beginContour();
+ p.vertex(20, 20);
+ p.vertex(20, 30);
+ p.vertex(30, 30);
+ p.vertex(30, 20);
+ p.endContour();
+
+ p.endShape(PApplet.CLOSE);
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Triangle Shapes ==========
+
+ @Test
+ @Order(6)
+ @Tag("triangles")
+ @DisplayName("Drawing triangle fans")
+ public void testTriangleFans() {
+ assertVisualMatch("shapes/triangle-fans", createShapeTest(p -> {
+ p.beginShape(PApplet.TRIANGLE_FAN);
+ p.vertex(25, 25);
+ for (int i = 0; i <= 12; i++) {
+ float angle = PApplet.map(i, 0, 12, 0, PApplet.TWO_PI);
+ p.vertex(25 + 10 * PApplet.cos(angle), 25 + 10 * PApplet.sin(angle));
+ }
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(7)
+ @Tag("triangles")
+ @DisplayName("Drawing triangle strips")
+ public void testTriangleStrips() {
+ assertVisualMatch("shapes/triangle-strips", createShapeTest(p -> {
+ p.beginShape(PApplet.TRIANGLE_STRIP);
+ p.vertex(10, 10);
+ p.vertex(30, 10);
+ p.vertex(15, 20);
+ p.vertex(35, 20);
+ p.vertex(10, 40);
+ p.vertex(30, 40);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(8)
+ @Tag("triangles")
+ @DisplayName("Drawing with triangles")
+ public void testTriangles() {
+ assertVisualMatch("shapes/triangles", createShapeTest(p -> {
+ p.beginShape(PApplet.TRIANGLES);
+ p.vertex(10, 10);
+ p.vertex(15, 40);
+ p.vertex(40, 35);
+ p.vertex(25, 15);
+ p.vertex(10, 10);
+ p.vertex(15, 25);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Quad Shapes ==========
+
+ @Test
+ @Order(9)
+ @Tag("quads")
+ @DisplayName("Drawing quad strips")
+ public void testQuadStrips() {
+ assertVisualMatch("shapes/quad-strips", createShapeTest(p -> {
+ p.beginShape(PApplet.QUAD_STRIP);
+ p.vertex(10, 10);
+ p.vertex(30, 10);
+ p.vertex(15, 20);
+ p.vertex(35, 20);
+ p.vertex(10, 40);
+ p.vertex(30, 40);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(10)
+ @Tag("quads")
+ @DisplayName("Drawing with quads")
+ public void testQuads() {
+ assertVisualMatch("shapes/quads", createShapeTest(p -> {
+ p.beginShape(PApplet.QUADS);
+ p.vertex(10, 10);
+ p.vertex(15, 10);
+ p.vertex(15, 15);
+ p.vertex(10, 15);
+ p.vertex(25, 25);
+ p.vertex(30, 25);
+ p.vertex(30, 30);
+ p.vertex(25, 30);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Curves ==========
+
+ @Test
+ @Order(11)
+ @Tag("curves")
+ @DisplayName("Drawing with curves")
+ public void testCurves() {
+ assertVisualMatch("shapes/curves", createShapeTest(p -> {
+ p.beginShape();
+ p.curveVertex(10, 10);
+ p.curveVertex(15, 40);
+ p.curveVertex(40, 35);
+ p.curveVertex(25, 15);
+ p.curveVertex(15, 25);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(12)
+ @Tag("curves")
+ @DisplayName("Drawing closed curves")
+ public void testClosedCurves() {
+ assertVisualMatch("shapes/closed-curves", createShapeTest(p -> {
+ p.beginShape();
+ p.curveVertex(10, 10);
+ p.curveVertex(15, 40);
+ p.curveVertex(40, 35);
+ p.curveVertex(25, 15);
+ p.curveVertex(15, 25);
+ p.endShape(PApplet.CLOSE);
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(13)
+ @Tag("curves")
+ @DisplayName("Drawing with curves with tightness")
+ public void testCurvesWithTightness() {
+ assertVisualMatch("shapes/curves-tightness", createShapeTest(p -> {
+ p.curveTightness(-1);
+ p.beginShape();
+ p.curveVertex(10, 10);
+ p.curveVertex(15, 40);
+ p.curveVertex(40, 35);
+ p.curveVertex(25, 15);
+ p.curveVertex(15, 25);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Bezier Curves ==========
+
+ @Test
+ @Order(14)
+ @Tag("bezier")
+ @DisplayName("Drawing with bezier curves")
+ public void testBezierCurves() {
+ assertVisualMatch("shapes/bezier-curves", createShapeTest(p -> {
+ p.beginShape();
+ p.vertex(10, 10);
+ p.bezierVertex(10, 40, 40, 40, 40, 10);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(15)
+ @Tag("bezier")
+ @DisplayName("Drawing with quadratic beziers")
+ public void testQuadraticBeziers() {
+ assertVisualMatch("shapes/quadratic-beziers", createShapeTest(p -> {
+ p.beginShape();
+ p.vertex(10, 10);
+ p.quadraticVertex(25, 40, 40, 10);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Points and Lines ==========
+
+ @Test
+ @Order(16)
+ @Tag("primitives")
+ @DisplayName("Drawing with points")
+ public void testPoints() {
+ assertVisualMatch("shapes/points", createShapeTest(p -> {
+ p.strokeWeight(5);
+ p.beginShape(PApplet.POINTS);
+ p.vertex(10, 10);
+ p.vertex(15, 40);
+ p.vertex(40, 35);
+ p.vertex(25, 15);
+ p.vertex(15, 25);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(17)
+ @Tag("primitives")
+ @DisplayName("Drawing with lines")
+ public void testLines() {
+ assertVisualMatch("shapes/lines", createShapeTest(p -> {
+ p.beginShape(PApplet.LINES);
+ p.vertex(10, 10);
+ p.vertex(15, 40);
+ p.vertex(40, 35);
+ p.vertex(25, 15);
+ p.endShape();
+ }), new TestConfig(50, 50));
+ }
+
+ // ========== Helper Methods ==========
+
+ /**
+ * Helper method to create a circle using vertices
+ */
+ private void vertexCircle(PApplet p, float x, float y, float r, int direction) {
+ for (int i = 0; i <= 12; i++) {
+ float angle = PApplet.map(i, 0, 12, 0, PApplet.TWO_PI) * direction;
+ p.vertex(x + r * PApplet.cos(angle), y + r * PApplet.sin(angle));
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/src/test/suites/ShapesSuite.java b/core/test/processing/visual/src/test/suites/ShapesSuite.java
new file mode 100644
index 0000000000..f45e472826
--- /dev/null
+++ b/core/test/processing/visual/src/test/suites/ShapesSuite.java
@@ -0,0 +1,12 @@
+package processing.visual.src.test.suites;
+
+import org.junit.platform.suite.api.*;
+
+@Suite
+@SuiteDisplayName("Basic Shapes Visual Tests")
+@SelectPackages("processing.visual.src.test.shapes")
+@ExcludePackages("processing.visual.src.test.suites")
+@IncludeTags("shapes")
+public class ShapesSuite {
+ // Empty class - just holds annotations
+}
\ No newline at end of file
diff --git a/core/test/processing/visual/src/test/typography/TypographyTest.java b/core/test/processing/visual/src/test/typography/TypographyTest.java
new file mode 100644
index 0000000000..06be6fffe0
--- /dev/null
+++ b/core/test/processing/visual/src/test/typography/TypographyTest.java
@@ -0,0 +1,484 @@
+package processing.visual.src.test.typography;
+
+import org.junit.jupiter.api.*;
+import processing.core.*;
+import processing.visual.src.test.base.VisualTest;
+import processing.visual.src.core.ProcessingSketch;
+import processing.visual.src.core.TestConfig;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import java.util.stream.Stream;
+
+@Tag("typography")
+@Tag("text")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class TypographyTest extends VisualTest {
+
+ @Nested
+ @Tag("font")
+ @DisplayName("textFont Tests")
+ class TextFontTests {
+
+ @Test
+ @Order(1)
+ @DisplayName("Default font rendering")
+ public void testDefaultFont() {
+ assertVisualMatch("typography/font/default-font", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ PFont font = p.createFont("SansSerif", 20);
+ p.textFont(font);
+ p.textSize(20);
+ p.textAlign(PApplet.LEFT, PApplet.BASELINE);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+ p.fill(0); // β Must be in draw(), not just setup()
+ p.text("test", 5, 25); // β Move away from edge
+ }
+ }, new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("Monospace font rendering")
+ public void testMonospaceFont() {
+ assertVisualMatch("typography/font/monospace-font", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ PFont mono = p.createFont("Monospaced", 20);
+ p.textFont(mono);
+ p.textAlign(PApplet.LEFT, PApplet.BASELINE);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+ p.fill(0); // β Add this
+ p.text("test", 5, 25);
+ }
+ }, new TestConfig(50, 50));
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("System font rendering")
+ public void testSystemFont() {
+ assertVisualMatch("typography/font/system-font", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ PFont font = p.createFont("Serif", 32);
+ p.textFont(font);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+ p.fill(0); // β Add this
+ p.text("test", 10, 50); // β Better positioning
+ }
+ }, new TestConfig(100, 100));
+ }
+ }
+
+
+ @Nested
+ @Tag("alignment")
+ @DisplayName("textAlign Tests")
+ class TextAlignTests {
+
+ @ParameterizedTest(name = "Alignment: {0}-{1}")
+ @MethodSource("alignmentProvider")
+ @DisplayName("All horizontal and vertical alignments with single word")
+ public void testAllAlignmentsSingleWord(int alignX, int alignY) {
+ final String alignName = getAlignmentName(alignX, alignY);
+
+ assertVisualMatch("typography/align/single-word-" + alignName,
+ new ProcessingSketch() {
+ PFont font;
+
+ @Override
+ public void setup(PApplet p) {
+ font = p.createFont("SansSerif", 60);
+ p.textFont(font);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+ p.textAlign(alignX, alignY);
+ p.fill(0);
+ p.text("Single Line", p.width / 2, p.height / 2);
+
+ // Draw bounding box
+ p.noFill();
+ p.stroke(255, 0, 0);
+ p.strokeWeight(2);
+
+ float tw = p.textWidth("Single Line");
+ float th = p.textAscent() + p.textDescent();
+ float x = calculateX(p, alignX, p.width / 2f, tw);
+ float y = calculateY(p, alignY, p.height / 2f, th);
+ p.rect(x, y, tw, th);
+ }
+ }, new TestConfig(600, 300));
+ }
+
+ @ParameterizedTest(name = "Multi-line alignment: {0}-{1}")
+ @MethodSource("alignmentProvider")
+ @DisplayName("Multi-line text with manual line breaks")
+ public void testMultiLineManualText(int alignX, int alignY) {
+ final String alignName = getAlignmentName(alignX, alignY);
+
+ assertVisualMatch("typography/align/multi-line-" + alignName,
+ new ProcessingSketch() {
+ PFont font;
+
+ @Override
+ public void setup(PApplet p) {
+ font = p.createFont("SansSerif", 12);
+ p.textFont(font);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ float xPos = 20;
+ float yPos = 20;
+ float boxWidth = 100;
+ float boxHeight = 60;
+
+ // Draw box
+ p.noFill();
+ p.stroke(200);
+ p.strokeWeight(2);
+ p.rect(xPos, yPos, boxWidth, boxHeight);
+
+ // Draw text
+ p.fill(0);
+ p.noStroke();
+ p.textAlign(alignX, alignY);
+ p.text("Line 1\nLine 2\nLine 3", xPos, yPos, boxWidth, boxHeight);
+
+ // Draw bounding box
+ p.noFill();
+ p.stroke(255, 0, 0);
+ p.strokeWeight(1);
+ }
+ }, new TestConfig(150, 100));
+ }
+
+ // Provide alignment combinations
+ static Stream alignmentProvider() {
+ return Stream.of(
+ Arguments.of(PApplet.LEFT, PApplet.TOP),
+ Arguments.of(PApplet.CENTER, PApplet.TOP),
+ Arguments.of(PApplet.RIGHT, PApplet.TOP),
+ Arguments.of(PApplet.LEFT, PApplet.CENTER),
+ Arguments.of(PApplet.CENTER, PApplet.CENTER),
+ Arguments.of(PApplet.RIGHT, PApplet.CENTER),
+ Arguments.of(PApplet.LEFT, PApplet.BOTTOM),
+ Arguments.of(PApplet.CENTER, PApplet.BOTTOM),
+ Arguments.of(PApplet.RIGHT, PApplet.BOTTOM)
+ );
+ }
+
+ // Helper methods
+ private String getAlignmentName(int alignX, int alignY) {
+ String x = alignX == PApplet.LEFT ? "left" :
+ alignX == PApplet.CENTER ? "center" : "right";
+ String y = alignY == PApplet.TOP ? "top" :
+ alignY == PApplet.CENTER ? "center" : "bottom";
+ return x + "-" + y;
+ }
+
+ private float calculateX(PApplet p, int alignX, float x, float tw) {
+ if (alignX == PApplet.LEFT) return x;
+ if (alignX == PApplet.CENTER) return x - tw / 2;
+ return x - tw;
+ }
+
+ private float calculateY(PApplet p, int alignY, float y, float th) {
+ if (alignY == PApplet.TOP) return y;
+ if (alignY == PApplet.CENTER) return y - th / 2;
+ return y - th;
+ }
+ }
+
+
+ @Nested
+ @Tag("size")
+ @DisplayName("textSize Tests")
+ class TextSizeTests {
+
+ @Test
+ @DisplayName("Text sizes comparison")
+ public void testTextSizes() {
+ assertVisualMatch("typography/size/sizes-comparison", new ProcessingSketch() {
+ PFont font;
+
+ @Override
+ public void setup(PApplet p) {
+ font = p.createFont("SansSerif", 12);
+ p.textFont(font);
+ p.textAlign(PApplet.LEFT, PApplet.BASELINE);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+ p.fill(0); // β Add this
+
+ int[] sizes = {12, 16, 20, 24, 30};
+ float yOffset = 20;
+
+ for (int size : sizes) {
+ p.textSize(size);
+ p.text("Size: " + size + "px", 10, yOffset);
+ yOffset += size + 5;
+ }
+ }
+ }, new TestConfig(300, 200));
+ }
+ }
+
+ @Nested
+ @Tag("leading")
+ @DisplayName("textLeading Tests")
+ class TextLeadingTests {
+
+ @Test
+ @DisplayName("Text leading with different values")
+ public void testTextLeading() {
+ assertVisualMatch("typography/leading/different-values", new ProcessingSketch() {
+ PFont font;
+
+ @Override
+ public void setup(PApplet p) {
+ font = p.createFont("SansSerif", 16);
+ p.textFont(font);
+ p.textSize(16);
+ p.textAlign(PApplet.LEFT, PApplet.BASELINE);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+ p.fill(0); // β Add this
+
+ int[] leadingValues = {10, 20, 30};
+ float yOffset = 25;
+
+ for (int leading : leadingValues) {
+ p.textLeading(leading);
+ p.text("Leading: " + leading, 10, yOffset);
+ yOffset += 25;
+ p.text("Line 1\nLine 2", 10, yOffset);
+ yOffset += leading * 2 + 15;
+ }
+ }
+ }, new TestConfig(300, 250));
+ }
+ }
+
+
+ @Nested
+ @Tag("width")
+ @DisplayName("textWidth Tests")
+ class TextWidthTests {
+
+ @Test
+ @DisplayName("Verify width of a string")
+ public void testTextWidth() {
+ assertVisualMatch("typography/width/string-width", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.textSize(20);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ String text = "Width Test";
+ float width = p.textWidth(text);
+
+ p.fill(0);
+ p.text(text, 0, 30);
+
+ p.noFill();
+ p.stroke(255, 0, 0);
+ p.rect(0, 10, width, 20);
+ }
+ }, new TestConfig(100, 100));
+ }
+ }
+
+ @Nested
+ @Tag("pfont")
+ @DisplayName("PFont Methods Tests")
+ class PFontMethodsTests {
+
+ @Test
+ @DisplayName("Text ascent and descent")
+ public void testTextAscentDescent() {
+ assertVisualMatch("typography/pfont/ascent-descent", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.textSize(32);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ float baseline = 50;
+ p.text("Typography", 10, baseline);
+
+ // Show baseline
+ p.stroke(0, 255, 0);
+ p.line(0, baseline, p.width, baseline);
+
+ // Show ascent
+ p.stroke(255, 0, 0);
+ float ascent = p.textAscent();
+ p.line(0, baseline - ascent, p.width, baseline - ascent);
+
+ // Show descent
+ p.stroke(0, 0, 255);
+ float descent = p.textDescent();
+ p.line(0, baseline + descent, p.width, baseline + descent);
+ }
+ }, new TestConfig(200, 100));
+ }
+
+ @Test
+ @DisplayName("Character availability check")
+ public void testCharacterAvailability() {
+ assertVisualMatch("typography/pfont/char-availability", new ProcessingSketch() {
+ PFont font;
+
+ @Override
+ public void setup(PApplet p) {
+ font = p.createFont("SansSerif", 24);
+ p.textFont(font);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ String testChars = "ABCabc123!@#";
+ float x = 10;
+ float y = 30;
+
+ for (int i = 0; i < testChars.length(); i++) {
+ char c = testChars.charAt(i);
+
+ if (font.getGlyph(c) != null) {
+ p.fill(0);
+ } else {
+ p.fill(255, 0, 0);
+ }
+
+ p.text(c, x, y);
+ x += p.textWidth(c) + 2;
+ }
+ }
+ }, new TestConfig(200, 80));
+ }
+ }
+
+ @Nested
+ @Tag("complex")
+ @DisplayName("Complex Text Rendering")
+ class ComplexTextRenderingTests {
+
+ @Test
+ @DisplayName("Text with rotation")
+ public void testRotatedText() {
+ assertVisualMatch("typography/complex/rotated-text", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.textSize(24);
+ p.textAlign(PApplet.CENTER, PApplet.CENTER);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ p.pushMatrix();
+ p.translate(p.width / 2, p.height / 2);
+
+ for (int i = 0; i < 12; i++) {
+ p.pushMatrix();
+ p.rotate(PApplet.TWO_PI * i / 12);
+ p.translate(0, -40);
+ p.fill(0);
+ p.text(i, 0, 0);
+ p.popMatrix();
+ }
+
+ p.popMatrix();
+ }
+ }, new TestConfig(150, 150));
+ }
+
+ @Test
+ @DisplayName("Text with transparency")
+ public void testTransparentText() {
+ assertVisualMatch("typography/complex/transparent-text", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.textSize(48);
+ p.textAlign(PApplet.CENTER, PApplet.CENTER);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ for (int i = 0; i < 5; i++) {
+ int alpha = 255 - (i * 50);
+ p.fill(0, 0, 255, alpha);
+ p.text("Layer " + i, p.width / 2 + i * 5, p.height / 2 + i * 5);
+ }
+ }
+ }, new TestConfig(200, 150));
+ }
+
+ @Test
+ @DisplayName("Text with different colors")
+ public void testColoredText() {
+ assertVisualMatch("typography/complex/colored-text", new ProcessingSketch() {
+ @Override
+ public void setup(PApplet p) {
+ p.textSize(20);
+ p.textAlign(PApplet.LEFT, PApplet.TOP);
+ }
+
+ @Override
+ public void draw(PApplet p) {
+ p.background(255);
+
+ p.fill(255, 0, 0);
+ p.text("Red Text", 10, 10);
+
+ p.fill(0, 255, 0);
+ p.text("Green Text", 10, 35);
+
+ p.fill(0, 0, 255);
+ p.text("Blue Text", 10, 60);
+
+ p.fill(255, 0, 255);
+ p.text("Magenta Text", 10, 85);
+ }
+ }, new TestConfig(150, 120));
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 050502f4ca..4aae1c5a8b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,6 +4,8 @@ compose-plugin = "1.7.1"
jogl = "2.5.0"
antlr = "4.13.2"
jupiter = "5.12.0"
+junitPlatform = "1.12.0"
+assertj = "3.24.2"
[libraries]
jogl = { module = "org.jogamp.jogl:jogl-all-main", version.ref = "jogl" }
@@ -35,6 +37,8 @@ markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" }
clikt = { module = "com.github.ajalt.clikt:clikt", version = "5.0.2" }
kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" }
+junitPlatformSuite = { module = "org.junit.platform:junit-platform-suite", version.ref = "junitPlatform" }
+assertjCore = { module = "org.assertj:assertj-core", version.ref = "assertj" }
[plugins]
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java
index 3fab2c8b17..8e4d023b9c 100644
--- a/java/src/processing/mode/java/JavaEditor.java
+++ b/java/src/processing/mode/java/JavaEditor.java
@@ -110,8 +110,6 @@ protected JavaEditor(Base base, String path, EditorState state,
Mode mode) throws EditorException {
super(base, path, state, mode);
-// long t1 = System.currentTimeMillis();
-
jmode = (JavaMode) mode;
debugger = new Debugger(this);
@@ -127,8 +125,6 @@ protected JavaEditor(Base base, String path, EditorState state,
preprocService = new PreprocService(this.jmode, this.sketch);
-// long t5 = System.currentTimeMillis();
-
usage = new ShowUsage(this, preprocService);
inspect = new InspectMode(this, preprocService, usage);
rename = new Rename(this, preprocService, usage);
@@ -139,16 +135,12 @@ protected JavaEditor(Base base, String path, EditorState state,
errorChecker = new ErrorChecker(this::setProblemList, preprocService);
-// long t7 = System.currentTimeMillis();
-
for (SketchCode code : getSketch().getCode()) {
Document document = code.getDocument();
addDocumentListener(document);
}
sketchChanged();
-// long t9 = System.currentTimeMillis();
-
Toolkit.setMenuMnemonics(textarea.getRightClickPopup());
// ensure completion is hidden when editor loses focus
@@ -159,9 +151,6 @@ public void windowLostFocus(WindowEvent e) {
public void windowGainedFocus(WindowEvent e) { }
});
-
-// long t10 = System.currentTimeMillis();
-// System.out.println("java editor was " + (t10-t9) + " " + (t9-t7) + " " + (t7-t5) + " " + (t5-t1));
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 7eacb06877..514ca25623 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -11,5 +11,6 @@ include(
"java:libraries:net",
"java:libraries:pdf",
"java:libraries:serial",
- "java:libraries:svg",
-)
\ No newline at end of file
+ "java:libraries:svg"
+)
+include("app:utils")