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__/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/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/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/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/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..285a190390 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -11,5 +11,9 @@ 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")
+include(":visual-tests")
+