diff --git a/.all-contributorsrc b/.all-contributorsrc
index 38341006fa..eea6f9e5e3 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1503,6 +1503,15 @@
"ideas",
"code"
]
+ },
+ {
+ "login": "Konsl",
+ "name": "Konsl",
+ "avatar_url": "https://avatars.githubusercontent.com/u/82901383?v=4",
+ "profile": "https://github.com/Konsl",
+ "contributions": [
+ "doc"
+ ]
}
],
"repoType": "github",
diff --git a/BUILD.md b/BUILD.md
index b73147e8bf..8172a5313d 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -8,6 +8,8 @@ First, [download the IntelliJ IDEA Community Edition](https://www.jetbrains.com/
1. Clone the Processing4 repository to your machine locally
1. Open the cloned repository in IntelliJ IDEA CE
+1. When prompted, select **Trust Project**. You can preview the project in Safe Mode but you won't be able to build Processing.
+1. IntelliJ may start loading Gradle dependencies automatically. Wait for this process to complete.
1. In the main menu, go to File > Project Structure > Project Settings > Project.
1. In the SDK Dropdown option, select a JDK version 17 or Download the jdk
1. Click the green Run Icon in the top right of the window. This is also where you can find the option to debug Processing.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d90e5fa58a..f542aeef4b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -69,22 +69,37 @@ If there hasn’t been any activity after two weeks, feel free to gently follow
Before opening a pull request, please make sure to discuss the related issue and get assigned to it first. This helps us stay aligned and avoid unnecessary work. Thank you!
-## New Features
+## Adding New Features to Processing
-In most cases, the best way to contribute a new feature is to create a library. The [Processing Library Template](https://github.com/processing/processing-library-template) is a great way to get started. For more instructions, see the [library template documentation](https://processing.github.io/processing-library-template/).
+If you have an idea for something Processing doesn’t yet support, **creating a library** is often the best way to contribute. The [Processing Library Template](https://github.com/processing/processing-library-template) provides a starting point for developing, packaging, and distributing Processing libraries. For more instructions, see the [library template documentation](https://processing.github.io/processing-library-template/).
+Once your library is complete, you can submit it to the official [Processing Contributions](https://github.com/processing/processing-contributions) repository. This makes it discoverable through the PDE’s Contribution Manager. Follow the guidelines in the [Processing Library Guidelines](https://github.com/processing/processing4/wiki/Library-Guidelines).
+
+### Libraries as the Starting Point for Features
Nearly all new features are first introduced as a Library or a Mode, or even as an example. The current [OpenGL renderer](http://glgraphics.sourceforge.net/) and Video library began as separate projects by Andrés Colubri, who needed a more performant, more sophisticated version of what we had in Processing for work that he was creating. The original `loadShape()` implementation came from the “Candy” library by Michael Chang (“mflux“). Similarly, Tweak Mode began as a [separate project](http://galsasson.com/tweakmode/) by Gal Sasson before being incorporated. PDE X was a Google Summer of code [project](https://github.com/processing/processing-experimental) by Manindra Moharana that updated the PDE to include basic refactoring and better error checking.
-Developing features separately from the main software has several benefits:
+### Why Develop Outside the Core?
+Working outside the main Processing codebase has several advantages:
* It’s easier for the contributor to develop the software without it needing to work for tens or hundreds of thousands of Processing users.
* It provides a way to get feedback on that code independently of everything else, and the ability to iterate on it rapidly.
* This feedback process also helps gauge the level of interest for the community, and how it should be prioritized for the software.
* We can delay the process of “normalizing” the features so that they’re consistent with the rest of Processing (function naming, structure, etc).
-A major consideration for any new feature is the level of maintenance that it might require in the future. If the original maintainer loses interest over time (which is normal) and the feature breaks (which happens more often than we'd like), it sits on the issues list unfixed, which isn’t good for anyone.
+### What Guides the Inclusion of New Features?
+We take maintenance seriously. A feature added to the core becomes a long-term responsibility. If it breaks, it needs fixing. Sometimes the original developer is no longer active (which is normal), and the burden falls on others.
+
+Processing is a massive project that has existed for more than 20 years. Part of its longevity comes from the effort that’s gone into keeping things as simple as we can, and in particular, making a lot of difficult decisions about *what to leave out*.
+
+Adding a new feature always has to be weighed against the potential confusion of one more thing—whether it’s a menu item, a dialog box, a function that needs to be added to the reference, etc. Adding a new graphics function means making it work across all the renderers that we ship (Java2D, OpenGL, JavaFX, PDF, etc) and across platforms (macOS, Windows, Linux).
+
+It may also mean new interface elements, updates to the reference, and more documentation.
+
+So when we consider a new feature, we ask ourselves:
+
+> Does this solve a problem for many users? Is it worth the added complexity and extra maintenance work?
-Processing is a massive project that has existed for more than 20 years. Part of its longevity comes from the effort that’s gone into keeping things as simple as we can, and in particular, making a lot of difficult decisions about *what to leave out*. Adding a new feature always has to be weighed against the potential confusion of one more thing—whether it’s a menu item, a dialog box, a function that needs to be added to the reference, etc. Adding a new graphics function means making it work across all the renderers that we ship (Java2D, OpenGL, JavaFX, PDF, etc) and across platforms (macOS, Windows, Linux). Does the feature help enough people that it's worth making the reference longer? Or the additional burden of maintaining that feature? It's no fun to say “no,” especially to people volunteering their time, but we often have to.
+These are not easy decisions, especially when volunteers are offering their time and ideas. But we have to make them carefully to keep Processing sustainable.
## Editor
diff --git a/README.md b/README.md
index 5e33148ca4..4ce76574e4 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,14 @@ Processing is a flexible software sketchbook and a programming language designed
This repository contains the source code for the [Processing](https://processing.org/) project for people who want to help improve the code.
-## Announcing Processing 4.3.1
+## Welcome to Processing 4.4!
-We’re excited to announce the release of Processing 4.3.1! This update brings tooling improvements and a friendlier experience for contributors. To learn more, read the [Processing 4.3.1 announcement](https://github.com/processing/processing4-carbon-aug-19/wiki/Announcing-Processing-4.3.1).
+We’re excited to announce the release of Processing 4.4! This update modernizes Processing under the hood to make future development easier. Key changes include switching the build system from Ant to Gradle, starting the transition from Swing to Jetpack Compose Multiplatform for the UI, and adding Kotlin support to the codebase. To learn more, check out [Changes in 4.4.0](https://github.com/processing/processing4/wiki/Changes-in-4.4).
-Processing was initiated in 2001 by Ben Fry and Casey Reas, who lead the development and maintenance of the project until 2023. We are grateful for their vision and dedication to the project. Processing is also indebted to over two decades of contributions from the broader Processing community.
+We hope these updates will make it easier for more people to contribute to Processing. If you'd like to get involved, have a look at our [Contributor Guide](CONTRIBUTING.md).
-> [!NOTE]
-> Due to platform limitations, the GitHub Contributors page for this repository does not show the complete list of contributors. However, the [git commit history](https://github.com/processing/processing4/commits/main/) provides a full record of the project's contributions. For contributor graphs before November 13th, refer to [this page](https://github.com/benfry/processing4/graphs/contributors). A comprehensive [list of all contributors](#contributors) is also included below. To see all commits by a contributor, click on the [💻](https://github.com/processing/processing4/commits?author=benfry) emoji below their name.
+## Acknowledgement
+Processing was initiated in 2001 by Ben Fry and Casey Reas, who led the development and maintenance of the project until 2023. We are grateful for their vision and dedication to the project. Processing is also indebted to over two decades of contributions from the broader Processing community.
## Using Processing
@@ -66,8 +66,9 @@ 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)!
-Add yourself to the contributors list [here](https://github.com/processing/processing4-carbon-aug-19/issues/839)!
+_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._
@@ -293,6 +294,7 @@ Add yourself to the contributors list [here](https://github.com/processing/proce
 Subhraman Sarkar 💻 ️️️️♿️ |
 SushantBansal-tech 🤔 💻 |
+  Konsl 📖 |
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 865296d135..72a922c60a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,9 +1,9 @@
-import org.gradle.kotlin.dsl.support.zipTo
+import org.gradle.internal.jvm.Jvm
+import org.gradle.internal.os.OperatingSystem
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download
-import org.jetbrains.kotlin.fir.scopes.impl.overrides
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -47,7 +47,7 @@ sourceSets{
compose.desktop {
application {
- mainClass = "processing.app.ui.Start"
+ mainClass = "processing.app.ProcessingKt"
jvmArgs(*listOf(
Pair("processing.version", rootProject.version),
@@ -59,7 +59,7 @@ compose.desktop {
).map { "-D${it.first}=${it.second}" }.toTypedArray())
nativeDistributions{
- modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi")
+ modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi", "java.scripting")
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Processing"
@@ -97,6 +97,7 @@ compose.desktop {
dependencies {
implementation(project(":core"))
+ runtimeOnly(project(":java"))
implementation(libs.flatlaf)
@@ -121,6 +122,8 @@ dependencies {
testImplementation(libs.mockitoKotlin)
testImplementation(libs.junitJupiter)
testImplementation(libs.junitJupiterParams)
+
+ implementation(libs.clikt)
}
tasks.test {
@@ -133,14 +136,35 @@ tasks.compileJava{
options.encoding = "UTF-8"
}
+tasks.register("lsp-develop"){
+ group = "processing"
+ // This task is used to run the LSP server when developing the LSP server itself
+ // to run the LSP server for end-users use `processing lsp` instead
+ dependencies.add("runtimeOnly", project(":java"))
+
+ // Usage: ./gradlew lsp-develop
+ // Make sure the cwd is set to the project directory
+ // or use -p to set the project directory
+
+ // Modify run configuration to start the LSP server rather than the Processing IDE
+ val run = tasks.named("run").get()
+ run.standardInput = System.`in`
+ run.standardOutput = System.out
+ dependsOn(run)
+
+ // TODO: Remove after command line is integrated, then add the `lsp` argument instead, `lsp-develop` can't be removed because we still need to pipe the input and output
+ run.jvmArgs("-Djava.awt.headless=true")
+ compose.desktop.application.mainClass = "processing.mode.java.lsp.PdeLanguageServer"
+}
+
val version = if(project.version == "unspecified") "1.0.0" else project.version
tasks.register("installCreateDmg") {
- onlyIf { org.gradle.internal.os.OperatingSystem.current().isMacOsX }
+ onlyIf { OperatingSystem.current().isMacOsX }
commandLine("arch", "-arm64", "brew", "install", "--quiet", "create-dmg")
}
tasks.register("packageCustomDmg"){
- onlyIf { org.gradle.internal.os.OperatingSystem.current().isMacOsX }
+ onlyIf { OperatingSystem.current().isMacOsX }
group = "compose desktop"
val distributable = tasks.named("createDistributable").get()
@@ -170,8 +194,6 @@ tasks.register("packageCustomDmg"){
extra.add("25")
}
- commandLine("brew", "install", "--quiet", "create-dmg")
-
commandLine("create-dmg",
"--volname", packageName,
"--volicon", file("macos/volume.icns"),
@@ -188,7 +210,7 @@ tasks.register("packageCustomDmg"){
}
tasks.register("packageCustomMsi"){
- onlyIf { org.gradle.internal.os.OperatingSystem.current().isWindows }
+ onlyIf { OperatingSystem.current().isWindows }
dependsOn("createDistributable")
workingDir = file("windows")
group = "compose desktop"
@@ -204,20 +226,22 @@ tasks.register("packageCustomMsi"){
)
}
-val snapname = findProperty("snapname") ?: rootProject.name
-val snaparch = when (System.getProperty("os.arch")) {
- "amd64", "x86_64" -> "amd64"
- "aarch64" -> "arm64"
- else -> System.getProperty("os.arch")
-}
+
tasks.register("generateSnapConfiguration"){
- onlyIf { org.gradle.internal.os.OperatingSystem.current().isLinux }
+ val name = findProperty("snapname") ?: rootProject.name
+ val arch = when (System.getProperty("os.arch")) {
+ "amd64", "x86_64" -> "amd64"
+ "aarch64" -> "arm64"
+ else -> System.getProperty("os.arch")
+ }
+
+ onlyIf { OperatingSystem.current().isLinux }
val distributable = tasks.named("createDistributable").get()
dependsOn(distributable)
val dir = distributable.destinationDir.get()
val content = """
- name: $snapname
+ name: $name
version: $version
base: core22
summary: A creative coding editor
@@ -240,24 +264,24 @@ tasks.register("generateSnapConfiguration"){
- network
- opengl
- home
+ - removable-media
parts:
processing:
plugin: dump
- source: deb/processing_$version-1_$snaparch.deb
+ source: deb/processing_$version-1_$arch.deb
source-type: deb
stage-packages:
- openjdk-17-jre
override-prime: |
snapcraftctl prime
- chmod -R +x opt/processing/lib/app/resources/jdk-*
rm -vf usr/lib/jvm/java-17-openjdk-*/lib/security/cacerts
""".trimIndent()
dir.file("../snapcraft.yaml").asFile.writeText(content)
}
tasks.register("packageSnap"){
- onlyIf { org.gradle.internal.os.OperatingSystem.current().isLinux }
+ onlyIf { OperatingSystem.current().isLinux }
dependsOn("packageDeb", "generateSnapConfiguration")
group = "compose desktop"
@@ -279,19 +303,20 @@ tasks.register("zipDistributable"){
}
afterEvaluate{
+ // Override the default DMG task to use our custom one
tasks.named("packageDmg").configure{
dependsOn("packageCustomDmg")
group = "compose desktop"
actions = emptyList()
}
-
+ // Override the default MSI task to use our custom one
tasks.named("packageMsi").configure{
dependsOn("packageCustomMsi")
group = "compose desktop"
actions = emptyList()
}
tasks.named("packageDistributionForCurrentOS").configure {
- if(org.gradle.internal.os.OperatingSystem.current().isMacOsX
+ if(OperatingSystem.current().isMacOsX
&& compose.desktop.application.nativeDistributions.macOS.notarization.appleID.isPresent
){
dependsOn("notarizeDmg")
@@ -322,40 +347,9 @@ tasks.register("includeJavaMode") {
into(composeResources("modes/java/mode"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
-tasks.register("includeJdk") {
- val os = DefaultNativePlatform.getCurrentOperatingSystem()
- val arch = when (System.getProperty("os.arch")) {
- "amd64", "x86_64" -> "x64"
- else -> System.getProperty("os.arch")
- }
- val platform = when {
- os.isWindows -> "windows"
- os.isMacOsX -> "mac"
- else -> "linux"
- }
-
- val javaVersion = System.getProperty("java.version").split(".")[0]
- val imageType = "jdk"
-
- src("https://api.adoptium.net/v3/binary/latest/" +
- "$javaVersion/ga/" +
- "$platform/" +
- "$arch/" +
- "$imageType/" +
- "hotspot/normal/eclipse?project=jdk")
-
- val extension = if (os.isWindows) "zip" else "tar.gz"
- val jdk = layout.buildDirectory.file("tmp/jdk-$platform-$arch.$extension")
- dest(jdk)
- overwrite(false)
- doLast {
- copy {
- val archive = if (os.isWindows) { zipTree(jdk) } else { tarTree(jdk) }
- from(archive){ eachFile{ permissions{ unix("755") } } }
- into(composeResources(""))
- }
- }
- finalizedBy("prepareAppResources")
+tasks.register("includeJdk") {
+ from(Jvm.current().javaHome.absolutePath)
+ destinationDir = composeResources("jdk").get().asFile
}
tasks.register("includeSharedAssets"){
from("../build/shared/")
@@ -401,6 +395,7 @@ tasks.register("includeJavaModeResources") {
from(java.layout.buildDirectory.dir("resources-bundled"))
into(composeResources("../"))
}
+// TODO: Move to java mode
tasks.register("renameWindres") {
dependsOn("includeSharedAssets","includeJavaModeResources")
val dir = composeResources("modes/java/application/launch4j/bin/")
@@ -417,29 +412,29 @@ tasks.register("renameWindres") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
into(dir)
}
-tasks.register("signResources"){
- onlyIf {
- org.gradle.internal.os.OperatingSystem.current().isMacOsX
- &&
- compose.desktop.application.nativeDistributions.macOS.signing.sign.get()
- }
- group = "compose desktop"
+tasks.register("includeProcessingResources"){
dependsOn(
+ "includeJdk",
"includeCore",
"includeJavaMode",
- "includeJdk",
"includeSharedAssets",
"includeProcessingExamples",
"includeProcessingWebsiteExamples",
"includeJavaModeResources",
"renameWindres"
)
- finalizedBy("prepareAppResources")
+ finalizedBy("signResources")
+}
+tasks.register("signResources"){
+ onlyIf {
+ OperatingSystem.current().isMacOsX
+ &&
+ compose.desktop.application.nativeDistributions.macOS.signing.sign.get()
+ }
+ group = "compose desktop"
val resourcesPath = composeResources("")
-
-
// find jars in the resources directory
val jars = mutableListOf()
doFirst{
@@ -472,7 +467,7 @@ tasks.register("signResources"){
include("**/*x86_64*")
include("**/*ffmpeg*")
include("**/ffmpeg*/**")
- exclude("jdk-*/**")
+ exclude("jdk/**")
exclude("*.jar")
exclude("*.so")
exclude("*.dll")
@@ -508,39 +503,32 @@ tasks.register("signResources"){
}
-afterEvaluate {
- tasks.named("prepareAppResources").configure {
- dependsOn(
- "includeCore",
- "includeJavaMode",
- "includeSharedAssets",
- "includeProcessingExamples",
- "includeProcessingWebsiteExamples",
- "includeJavaModeResources",
- "renameWindres"
- )
- }
- tasks.register("setExecutablePermissions") {
- description = "Sets executable permissions on binaries in Processing.app resources"
- group = "compose desktop"
+tasks.register("setExecutablePermissions") {
+ description = "Sets executable permissions on binaries in Processing.app resources"
+ group = "compose desktop"
- doLast {
- val resourcesPath = layout.buildDirectory.dir("compose/binaries")
- fileTree(resourcesPath) {
- include("**/resources/**/bin/**")
- include("**/resources/**/*.sh")
- include("**/resources/**/*.dylib")
- include("**/resources/**/*.so")
- include("**/resources/**/*.exe")
- }.forEach { file ->
- if (file.isFile) {
- file.setExecutable(true, false)
- }
+ doLast {
+ val resourcesPath = layout.buildDirectory.dir("compose/binaries")
+ fileTree(resourcesPath) {
+ include("**/resources/**/bin/**")
+ include("**/resources/**/lib/**")
+ include("**/resources/**/*.sh")
+ include("**/resources/**/*.dylib")
+ include("**/resources/**/*.so")
+ include("**/resources/**/*.exe")
+ }.forEach { file ->
+ if (file.isFile) {
+ file.setExecutable(true, false)
}
}
}
+}
+
+afterEvaluate {
+ tasks.named("prepareAppResources").configure {
+ dependsOn("includeProcessingResources")
+ }
tasks.named("createDistributable").configure {
- dependsOn("signResources", "includeJdk")
finalizedBy("setExecutablePermissions")
}
}
diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index 07e23d36b4..b5aa599b98 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -166,6 +166,20 @@ static public void main(final String[] args) {
static private void createAndShowGUI(String[] args) {
// these times are fairly negligible relative to Base.
// long t1 = System.currentTimeMillis();
+ var preferences = java.util.prefs.Preferences.userRoot().node("org/processing/app");
+ var installLocations = new ArrayList<>(List.of(preferences.get("installLocations", "").split(",")));
+ var installLocation = System.getProperty("user.dir") + "^" + Base.getVersionName();
+
+ // Check if the installLocation is already in the list
+ if (!installLocations.contains(installLocation)) {
+ // Add the installLocation to the list
+ installLocations.add(installLocation);
+
+ // Save the updated list back to preferences
+ preferences.put("installLocations", String.join(",", installLocations));
+ }
+ // 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()) {
@@ -1942,18 +1956,20 @@ public void rebuildSketchbook() {
public void populateSketchbookMenu(JMenu menu) {
- boolean found = false;
- try {
- found = addSketches(menu, sketchbookFolder);
- } catch (Exception e) {
- Messages.showWarning("Sketchbook Menu Error",
- "An error occurred while trying to list the sketchbook.", e);
- }
- if (!found) {
- JMenuItem empty = new JMenuItem(Language.text("menu.file.sketchbook.empty"));
- empty.setEnabled(false);
- menu.add(empty);
- }
+ new Thread(() -> {
+ boolean found = false;
+ try {
+ found = addSketches(menu, sketchbookFolder);
+ } catch (Exception e) {
+ Messages.showWarning("Sketchbook Menu Error",
+ "An error occurred while trying to list the sketchbook.", e);
+ }
+ if (!found) {
+ JMenuItem empty = new JMenuItem(Language.text("menu.file.sketchbook.empty"));
+ empty.setEnabled(false);
+ menu.add(empty);
+ }
+ }).start();
}
@@ -1964,11 +1980,17 @@ public void populateSketchbookMenu(JMenu menu) {
* sketch should open in a new window.
*/
protected boolean addSketches(JMenu menu, File folder) {
+ Messages.log("scanning " + folder.getAbsolutePath());
// skip .DS_Store files, etc. (this shouldn't actually be necessary)
if (!folder.isDirectory()) {
return false;
}
+ // Don't look inside the 'android' folders in the sketchbook
+ if (folder.getName().equals("android")) {
+ return false;
+ }
+
if (folder.getName().equals("libraries")) {
return false; // let's not go there
}
@@ -2054,6 +2076,7 @@ protected boolean addSketches(JMenu menu, File folder) {
*/
public boolean addSketches(DefaultMutableTreeNode node, File folder,
boolean examples) throws IOException {
+ Messages.log("scanning " + folder.getAbsolutePath());
// skip .DS_Store files, etc. (this shouldn't actually be necessary)
if (!folder.isDirectory()) {
return false;
@@ -2061,6 +2084,11 @@ public boolean addSketches(DefaultMutableTreeNode node, File folder,
final String folderName = folder.getName();
+ // Don't look inside the 'android' folders in the sketchbook
+ if (folderName.equals("android")) {
+ return false;
+ }
+
// Don't look inside the 'libraries' folders in the sketchbook
if (folderName.equals("libraries")) {
return false;
diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java
index 2af96bfd12..b911d7e0ae 100644
--- a/app/src/processing/app/Platform.java
+++ b/app/src/processing/app/Platform.java
@@ -391,17 +391,14 @@ static public File getContentFile(String name) {
static public File getJavaHome() {
var resourcesDir = System.getProperty("compose.application.resources.dir");
if(resourcesDir != null) {
- var jdkFolder = Arrays.stream(new File(resourcesDir).listFiles((dir, name) -> dir.isDirectory() && name.startsWith("jdk-")))
- .findFirst()
- .orElse(null);
- if(Platform.isMacOS()){
- return new File(jdkFolder, "Contents/Home");
- }
+ var jdkFolder = new File(resourcesDir,"jdk");
+ if(jdkFolder.exists()){
return jdkFolder;
+ }
}
var home = System.getProperty("java.home");
- if(home != null && new File(home, "bin/java").exists()){
+ if(home != null){
return new File(home);
}
if (Platform.isMacOS()) {
diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt
new file mode 100644
index 0000000000..11555edf53
--- /dev/null
+++ b/app/src/processing/app/Processing.kt
@@ -0,0 +1,90 @@
+package processing.app
+
+import com.github.ajalt.clikt.command.SuspendingCliktCommand
+import com.github.ajalt.clikt.command.main
+import com.github.ajalt.clikt.core.Context
+import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.parameters.arguments.argument
+import com.github.ajalt.clikt.parameters.arguments.help
+import com.github.ajalt.clikt.parameters.arguments.multiple
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.help
+import com.github.ajalt.clikt.parameters.options.option
+import processing.app.ui.Start
+
+class Processing: SuspendingCliktCommand("processing"){
+ val version by option("-v","--version")
+ .flag()
+ .help("Print version information")
+
+ val sketches by argument()
+ .multiple(default = emptyList())
+ .help("Sketches to open")
+
+ override fun help(context: Context) = "Start the Processing IDE"
+ override val invokeWithoutSubcommand = true
+ override suspend fun run() {
+ if(version){
+ println("processing-${Base.getVersionName()}-${Base.getRevision()}")
+ return
+ }
+
+ val subcommand = currentContext.invokedSubcommand
+ if (subcommand == null) {
+ Start.main(sketches.toTypedArray())
+ }
+ }
+}
+
+suspend fun main(args: Array){
+ Processing()
+ .subcommands(
+ LSP(),
+ LegacyCLI(args)
+ )
+ .main(args)
+}
+
+class LSP: SuspendingCliktCommand("lsp"){
+ override fun help(context: Context) = "Start the Processing Language Server"
+ override suspend fun run(){
+ try {
+ // Indirect invocation since app does not depend on java mode
+ Class.forName("processing.mode.java.lsp.PdeLanguageServer")
+ .getMethod("main", Array::class.java)
+ .invoke(null, *arrayOf(emptyList()))
+ } catch (e: Exception) {
+ throw InternalError("Failed to invoke main method", e)
+ }
+ }
+}
+
+class LegacyCLI(val args: Array): SuspendingCliktCommand( "cli"){
+ override fun help(context: Context) = "Legacy processing-java command line interface"
+
+ val help by option("--help").flag()
+ val build by option("--build").flag()
+ val run by option("--run").flag()
+ val present by option("--present").flag()
+ val sketch: String? by option("--sketch")
+ val force by option("--force").flag()
+ val output: String? by option("--output")
+ val export by option("--export").flag()
+ val noJava by option("--no-java").flag()
+ val variant: String? by option("--variant")
+
+ override suspend fun run(){
+ val cliArgs = args.filter { it != "cli" }
+ try {
+ if(build){
+ System.setProperty("java.awt.headless", "true")
+ }
+ // Indirect invocation since app does not depend on java mode
+ Class.forName("processing.mode.java.Commander")
+ .getMethod("main", Array::class.java)
+ .invoke(null, *arrayOf(cliArgs.toTypedArray()))
+ } catch (e: Exception) {
+ throw InternalError("Failed to invoke main method", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/tools/InstallCommander.java b/app/src/processing/app/tools/InstallCommander.java
index cd136c3621..33eabc6f68 100644
--- a/app/src/processing/app/tools/InstallCommander.java
+++ b/app/src/processing/app/tools/InstallCommander.java
@@ -86,30 +86,41 @@ public void run() {
PrintWriter writer = PApplet.createWriter(file);
writer.print("#!/bin/sh\n\n");
- writer.print("# Prevents processing-java from stealing focus, see:\n" +
- "# https://github.com/processing/processing/issues/3996.\n" +
- "OPTION_FOR_HEADLESS_RUN=\"\"\n" +
- "for ARG in \"$@\"\n" +
- "do\n" +
- " if [ \"$ARG\" = \"--build\" ]; then\n" +
- " OPTION_FOR_HEADLESS_RUN=\"-Djava.awt.headless=true\"\n" +
- " fi\n" +
- "done\n\n");
-
- String javaRoot = Platform.getContentFile(".").getCanonicalPath();
-
- StringList jarList = new StringList();
- addJarList(jarList, new File(javaRoot));
- addJarList(jarList, new File(javaRoot, "core/library"));
- addJarList(jarList, new File(javaRoot, "modes/java/mode"));
- String classPath = jarList.join(":").replaceAll(javaRoot + "\\/?", "");
-
- writer.println("cd \"" + javaRoot + "\" && " +
- Platform.getJavaPath().replaceAll(" ", "\\\\ ") +
- " -Djna.nosys=true" +
- " $OPTION_FOR_HEADLESS_RUN" +
- " -cp \"" + classPath + "\"" +
- " processing.mode.java.Commander \"$@\"");
+ var resourcesDir = System.getProperty("compose.application.resources.dir");
+ if(resourcesDir != null) {
+ // Gradle based distributable
+ var appBinary = (resourcesDir
+ .split("\\.app")[0] + ".app/Contents/MacOS/Processing")
+ .replaceAll(" ", "\\\\ ");
+ writer.print(appBinary + " cli $@");
+
+ } else {
+ // Ant based distributable
+ writer.print("# Prevents processing-java from stealing focus, see:\n" +
+ "# https://github.com/processing/processing/issues/3996.\n" +
+ "OPTION_FOR_HEADLESS_RUN=\"\"\n" +
+ "for ARG in \"$@\"\n" +
+ "do\n" +
+ " if [ \"$ARG\" = \"--build\" ]; then\n" +
+ " OPTION_FOR_HEADLESS_RUN=\"-Djava.awt.headless=true\"\n" +
+ " fi\n" +
+ "done\n\n");
+
+ String javaRoot = Platform.getContentFile(".").getCanonicalPath();
+
+ StringList jarList = new StringList();
+ addJarList(jarList, new File(javaRoot));
+ addJarList(jarList, new File(javaRoot, "core/library"));
+ addJarList(jarList, new File(javaRoot, "modes/java/mode"));
+ String classPath = jarList.join(":").replaceAll(javaRoot + "\\/?", "");
+
+ writer.println("cd \"" + javaRoot + "\" && " +
+ Platform.getJavaPath().replaceAll(" ", "\\\\ ") +
+ " -Djna.nosys=true" +
+ " $OPTION_FOR_HEADLESS_RUN" +
+ " -cp \"" + classPath + "\"" +
+ " processing.mode.java.Commander \"$@\"");
+ }
writer.flush();
writer.close();
file.setExecutable(true);
diff --git a/core/examples/src/main/java/Basic.java b/core/examples/src/main/java/Basic.java
index 379bb4b306..7c5a72cba2 100644
--- a/core/examples/src/main/java/Basic.java
+++ b/core/examples/src/main/java/Basic.java
@@ -1,12 +1,25 @@
import processing.core.PApplet;
+import java.io.IOException;
+
public class Basic extends PApplet {
public void settings(){
size(500, 500);
+
+ try {
+ Runtime.getRuntime().exec("echo Hello World");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
public void draw(){
- ellipse(width / 2f, height / 2f, 125f, 125f);
+ background(255);
+ fill(0);
+ ellipse(mouseX, mouseY, 125f, 125f);
+ println(frameRate);
+
+
}
diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java
index 9f3486a10d..d1297ec6fb 100644
--- a/core/src/processing/core/PApplet.java
+++ b/core/src/processing/core/PApplet.java
@@ -913,7 +913,7 @@ void handleSettings() {
* Processing Development Environment (PDE). For example, when
* using the Eclipse code editor, it's necessary to use
* settings() to define the size() and
- * smooth() values for a sketch..
+ * smooth() values for a sketch.
*
* The settings() method runs before the sketch has been
* set up, so other Processing functions cannot be used at that
@@ -10105,6 +10105,8 @@ static public void runSketch(final String[] args,
sketch.present = present;
sketch.fullScreen = fullScreen;
+ sketch.pixelDensity = sketch.displayDensity();
+
// For 3.0.1, moved this above handleSettings() so that loadImage() can be
// used inside settings(). Sets a terrible precedent, but the alternative
// of not being able to size a sketch to an image is driving people loopy.
diff --git a/core/src/processing/core/PGraphics.java b/core/src/processing/core/PGraphics.java
index 9e3205c69a..0b9f0d2ed4 100644
--- a/core/src/processing/core/PGraphics.java
+++ b/core/src/processing/core/PGraphics.java
@@ -2496,7 +2496,7 @@ protected void curveVertexSegment(float x1, float y1, float z1,
*
* Using point() with strokeWeight(1) or smaller may draw nothing to the screen,
* depending on the graphics settings of the computer. Workarounds include
- * setting the pixel using set() or drawing the point using either
+ * setting the pixel using set() or drawing the point using either
* circle() or square().
*
* @webref shape:2d primitives
@@ -3559,8 +3559,8 @@ public float curvePoint(float a, float b, float c, float d, float t) {
/**
* Calculates the tangent of a point on a curve. There's a good definition
- * of tangent on Wikipedia.
+ * of tangent on Wikipedia.
*
* Advanced
* Code thanks to Dave Bollinger (Bug #715)
@@ -6252,7 +6252,7 @@ public float modelZ(float x, float y, float z) {
*
* The style information controlled by the following functions are included
* in the style:
- * fill(), stroke(), tint(), strokeWeight(), strokeCap(),strokeJoin(),
+ * fill(), stroke(), tint(), strokeWeight(), strokeCap(),strokeJoin(),
* imageMode(), rectMode(), ellipseMode(), shapeMode(), colorMode(),
* textAlign(), textFont(), textMode(), textSize(), textLeading(),
* emissive(), specular(), shininess(), ambient()
@@ -6439,7 +6439,7 @@ public PStyle getStyle(PStyle s) { // ignore
*
* Using point() with strokeWeight(1) or smaller may draw nothing to the screen,
* depending on the graphics settings of the computer. Workarounds include
- * setting the pixel using set() or drawing the point using either
+ * setting the pixel using set() or drawing the point using either
* circle() or square().
*
* @webref shape:attributes
diff --git a/core/src/processing/opengl/PGraphicsOpenGL.java b/core/src/processing/opengl/PGraphicsOpenGL.java
index 2d6dca9914..88164f43e1 100644
--- a/core/src/processing/opengl/PGraphicsOpenGL.java
+++ b/core/src/processing/opengl/PGraphicsOpenGL.java
@@ -4481,7 +4481,7 @@ public void ortho(float left, float right,
// The minus sign is needed to invert the Y axis.
projection.set(x, 0, 0, tx,
- 0, -y, 0, -ty,
+ 0, -y, 0, ty,
0, 0, z, tz,
0, 0, 0, 1);
diff --git a/core/test/processing/core/PMatrix2DTest.java b/core/test/processing/core/PMatrix2DTest.java
new file mode 100644
index 0000000000..3f1f82f247
--- /dev/null
+++ b/core/test/processing/core/PMatrix2DTest.java
@@ -0,0 +1,186 @@
+package processing.core;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class PMatrix2DTest {
+
+ private PMatrix2D m;
+
+ @Before
+ public void setUp() {
+ m = new PMatrix2D();
+ }
+
+ @Test
+ public void testIdentity() {
+ assertTrue("New matrix should be identity", m.isIdentity());
+ float[] arr = m.get(null);
+ assertEquals(1, arr[0], 0.0001f); // m00
+ assertEquals(0, arr[1], 0.0001f); // m01
+ assertEquals(0, arr[2], 0.0001f); // m02
+ assertEquals(0, arr[3], 0.0001f); // m10
+ assertEquals(1, arr[4], 0.0001f); // m11
+ assertEquals(0, arr[5], 0.0001f); // m12
+ }
+
+ @Test
+ public void testTranslate() {
+ m.translate(10, 20);
+ assertEquals(10, m.m02, 0.0001f);
+ assertEquals(20, m.m12, 0.0001f);
+ }
+
+ @Test
+ public void testRotate() {
+ m.rotate(PConstants.HALF_PI);
+ assertEquals(0, m.m00, 0.0001f);
+ assertEquals(-1, m.m01, 0.0001f);
+ assertEquals(1, m.m10, 0.0001f);
+ assertEquals(0, m.m11, 0.0001f);
+ }
+
+
+ @Test
+ public void testScale() {
+ m.scale(2, 3);
+ assertEquals(2, m.m00, 0.0001f);
+ assertEquals(3, m.m11, 0.0001f);
+ assertEquals(0, m.m02, 0.0001f);
+ assertEquals(0, m.m12, 0.0001f);
+ }
+
+ @Test
+ public void testShear() {
+ float shearAngle = 0.2f;
+ m.shearX(shearAngle);
+ assertEquals(0, m.m01, 0.0001f);
+ assertEquals((float)Math.tan(shearAngle), m.m10, 0.0001f);
+ assertEquals(1, m.m02, 0.0001f);
+
+ m.reset();
+
+ m.shearY(shearAngle);
+ assertEquals(0, m.m01, 0.0001f);
+ assertEquals(0, m.m10, 0.0001f);
+ assertEquals((float)Math.tan(shearAngle), m.m11, 0.0001f);
+ assertEquals(1, m.m02, 0.0001f);
+ }
+
+ @Test
+ public void testApply() {
+ PMatrix2D m2 = new PMatrix2D(1, 2, 3, 4, 5, 6);
+ m.apply(m2);
+ assertEquals(m2.m00, m.m00, 0.0001f);
+ assertEquals(m2.m01, m.m01, 0.0001f);
+ assertEquals(m2.m02, m.m02, 0.0001f);
+ assertEquals(m2.m10, m.m10, 0.0001f);
+ assertEquals(m2.m11, m.m11, 0.0001f);
+ assertEquals(m2.m12, m.m12, 0.0001f);
+ }
+
+ @Test
+ public void testPreApply() {
+ PMatrix2D m1 = new PMatrix2D(1, 2, 3, 4, 5, 6);
+ m.reset(); // identity matrix
+ m.preApply(m1);
+ assertEquals(m1.m00, m.m00, 0.0001f);
+ assertEquals(m1.m01, m.m01, 0.0001f);
+ assertEquals(m1.m02, m.m02, 0.0001f);
+ assertEquals(m1.m10, m.m10, 0.0001f);
+ assertEquals(m1.m11, m.m11, 0.0001f);
+ assertEquals(m1.m12, m.m12, 0.0001f);
+ }
+
+ @Test
+ public void testMultPVector() {
+ PVector src = new PVector(1, 2, 0);
+ PVector result = m.mult(src, null);
+ assertEquals(src.x, result.x, 0.0001f);
+ assertEquals(src.y, result.y, 0.0001f);
+ }
+
+ @Test
+ public void testMultArray() {
+ float[] vec = { 1, 2 };
+ float[] out = m.mult(vec, null);
+ assertEquals(1, out[0], 0.0001f);
+ assertEquals(2, out[1], 0.0001f);
+ }
+
+ @Test
+ public void testMultXandY() {
+ float x = 10, y = 20;
+ float xOut = m.multX(x, y);
+ float yOut = m.multY(x, y);
+ assertEquals(x, xOut, 0.0001f);
+ assertEquals(y, yOut, 0.0001f);
+ }
+
+ @Test
+ public void testInvertAndDeterminant() {
+ m.set(2, 0, 5, 1, 3, 7);
+ float det = m.determinant();
+ assertEquals(6, det, 0.0001f);
+
+ boolean invertible = m.invert();
+ assertTrue("Matrix should be invertible", invertible);
+
+ PMatrix2D identity = new PMatrix2D(2, 0, 5, 1, 3, 7);
+ identity.apply(m);
+
+ assertEquals(1, identity.m00, 0.001f);
+ assertEquals(0, identity.m01, 0.001f);
+ assertEquals(0, identity.m10, 0.001f);
+ assertEquals(1, identity.m11, 0.001f);
+ }
+
+ @Test
+ public void testIdentityWarped() {
+ assertTrue(m.isIdentity());
+ assertFalse(m.isWarped());
+
+ m.translate(10, 20);
+ assertFalse(m.isIdentity());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testTranslate3DThrows() {
+ m.translate(1, 2, 3);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRotateXThrows() {
+ m.rotateX(1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRotateYThrows() {
+ m.rotateY(1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testScale3DThrows() {
+ m.scale(1, 2, 3);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testApplyPMatrix3DThrows() {
+ PMatrix3D m3d = new PMatrix3D(1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1);
+ m.apply(m3d);
+ }
+
+ @Test
+ public void testGetArray() {
+ m.set(new float[]{1, 2, 0, 0, 1, 0});
+ float[] arr = m.get(null);
+ assertEquals(1, arr[0], 0.0001f);
+ assertEquals(2, arr[1], 0.0001f);
+ assertEquals(0, arr[2], 0.0001f);
+ }
+}
diff --git a/core/test/processing/core/PVectorTest.java b/core/test/processing/core/PVectorTest.java
new file mode 100644
index 0000000000..9d97dc470a
--- /dev/null
+++ b/core/test/processing/core/PVectorTest.java
@@ -0,0 +1,294 @@
+package processing.core;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PVectorTest {
+
+ @Test
+ public void testConstructors() {
+ PVector v0 = new PVector();
+ Assert.assertEquals(0, v0.x, 0.0001f);
+ Assert.assertEquals(0, v0.y, 0.0001f);
+ Assert.assertEquals(0, v0.z, 0.0001f);
+
+ PVector v2 = new PVector(3, 4);
+ Assert.assertEquals(3, v2.x, 0.0001f);
+ Assert.assertEquals(4, v2.y, 0.0001f);
+ Assert.assertEquals(0, v2.z, 0.0001f);
+
+ PVector v3 = new PVector(1, 2, 3);
+ Assert.assertEquals(1, v3.x, 0.0001f);
+ Assert.assertEquals(2, v3.y, 0.0001f);
+ Assert.assertEquals(3, v3.z, 0.0001f);
+ }
+
+ @Test
+ public void testSetAndCopy() {
+ PVector v = new PVector(1, 2, 3);
+ PVector copy = v.copy();
+ Assert.assertEquals(v.x, copy.x, 0.0001f);
+ Assert.assertEquals(v.y, copy.y, 0.0001f);
+ Assert.assertEquals(v.z, copy.z, 0.0001f);
+
+ v.set(4, 5, 6);
+ Assert.assertEquals(4, v.x, 0.0001f);
+ Assert.assertEquals(5, v.y, 0.0001f);
+ Assert.assertEquals(6, v.z, 0.0001f);
+ }
+
+ @Test
+ public void testAdd() {
+ PVector v1 = new PVector(1, 1, 1);
+ PVector v2 = new PVector(2, 3, 4);
+ v1.add(v2);
+ Assert.assertEquals(3, v1.x, 0.0001f);
+ Assert.assertEquals(4, v1.y, 0.0001f);
+ Assert.assertEquals(5, v1.z, 0.0001f);
+
+ PVector v3 = new PVector(1, 2, 3);
+ PVector result = PVector.add(v3, new PVector(4, 5, 6));
+ Assert.assertEquals(5, result.x, 0.0001f);
+ Assert.assertEquals(7, result.y, 0.0001f);
+ Assert.assertEquals(9, result.z, 0.0001f);
+ }
+
+ @Test
+ public void testSub() {
+ PVector v1 = new PVector(5, 7, 9);
+ PVector v2 = new PVector(1, 2, 3);
+ v1.sub(v2);
+ Assert.assertEquals(4, v1.x, 0.0001f);
+ Assert.assertEquals(5, v1.y, 0.0001f);
+ Assert.assertEquals(6, v1.z, 0.0001f);
+
+ PVector v3 = new PVector(10, 10, 10);
+ PVector result = PVector.sub(v3, new PVector(3, 3, 3));
+ Assert.assertEquals(7, result.x, 0.0001f);
+ Assert.assertEquals(7, result.y, 0.0001f);
+ Assert.assertEquals(7, result.z, 0.0001f);
+ }
+
+ @Test
+ public void testMult() {
+ PVector v = new PVector(1, 2, 3);
+ v.mult(2);
+ Assert.assertEquals(2, v.x, 0.0001f);
+ Assert.assertEquals(4, v.y, 0.0001f);
+ Assert.assertEquals(6, v.z, 0.0001f);
+
+ PVector result = PVector.mult(new PVector(1, 1, 1), 5);
+ Assert.assertEquals(5, result.x, 0.0001f);
+ Assert.assertEquals(5, result.y, 0.0001f);
+ Assert.assertEquals(5, result.z, 0.0001f);
+ }
+
+ @Test
+ public void testDiv() {
+ PVector v1 = new PVector(10, 20, 30);
+ v1.div(2);
+ Assert.assertEquals(5, v1.x, 0.0001f);
+ Assert.assertEquals(10, v1.y, 0.0001f);
+ Assert.assertEquals(15, v1.z, 0.0001f);
+
+ PVector result = PVector.div(new PVector(10, 20, 30), 2);
+ Assert.assertEquals(5, result.x, 0.0001f);
+ Assert.assertEquals(10, result.y, 0.0001f);
+ Assert.assertEquals(15, result.z, 0.0001f);
+
+ // Division by zero
+ PVector v2 = new PVector(1, 2, 3);
+ v2.div(0);
+ Assert.assertTrue(Float.isInfinite(v2.x));
+ Assert.assertTrue(Float.isInfinite(v2.y));
+ Assert.assertTrue(Float.isInfinite(v2.z));
+ }
+
+ @Test
+ public void testMagnitude() {
+ PVector v = new PVector(3, 4, 0);
+ Assert.assertEquals(5, v.mag(), 0.0001f);
+ Assert.assertEquals(25, v.magSq(), 0.0001f);
+ }
+
+ @Test
+ public void testDot() {
+ PVector v1 = new PVector(1, 2, 3);
+ PVector v2 = new PVector(4, -5, 6);
+ float dot = v1.dot(v2);
+ Assert.assertEquals(12, dot, 0.0001f);
+
+ float dotStatic = PVector.dot(v1, v2);
+ Assert.assertEquals(12, dotStatic, 0.0001f);
+ }
+
+ @Test
+ public void testCross() {
+ PVector v1 = new PVector(1, 0, 0);
+ PVector v2 = new PVector(0, 1, 0);
+ PVector cross = v1.cross(v2);
+ Assert.assertEquals(0, cross.x, 0.0001f);
+ Assert.assertEquals(0, cross.y, 0.0001f);
+ Assert.assertEquals(1, cross.z, 0.0001f);
+ }
+
+ @Test
+ public void testNormalize() {
+ PVector v1 = new PVector(3, 4, 0);
+ v1.normalize();
+ Assert.assertEquals(1, v1.mag(), 0.0001f);
+
+ //with target
+ PVector v2 = new PVector(3, 4, 0);
+ PVector target = new PVector();
+ PVector result = v2.normalize(target);
+ Assert.assertSame(target, result);
+ Assert.assertEquals(0.6f, result.x, 0.0001f);
+ Assert.assertEquals(0.8f, result.y, 0.0001f);
+ Assert.assertEquals(0, result.z, 0.0001f);
+
+ // Normalize zero vector
+ PVector zero = new PVector(0, 0, 0);
+ zero.normalize();
+ Assert.assertEquals(0, zero.x, 0.0001f);
+ Assert.assertEquals(0, zero.y, 0.0001f);
+ Assert.assertEquals(0, zero.z, 0.0001f);
+
+ }
+
+ @Test
+ public void testLimit() {
+ PVector v = new PVector(10, 0, 0);
+ v.limit(5);
+ Assert.assertEquals(5, v.mag(), 0.0001f);
+ }
+
+ @Test
+ public void testSetMag() {
+ PVector v = new PVector(3, 4, 0);
+ v.setMag(10);
+ Assert.assertEquals(10, v.mag(), 0.0001f);
+ }
+
+ @Test
+ public void testHeading() {
+ PVector v = new PVector(0, 1);
+ float heading = v.heading();
+ Assert.assertEquals(PConstants.HALF_PI, heading, 0.0001f);
+ }
+
+ @Test
+ public void testRotate() {
+ PVector v = new PVector(1, 0);
+ v.rotate(PConstants.HALF_PI);
+ Assert.assertEquals(0, v.x, 0.0001f);
+ Assert.assertEquals(1, v.y, 0.0001f);
+ }
+
+ @Test
+ public void testLerp() {
+ PVector v1 = new PVector(0, 0, 0);
+ PVector v2 = new PVector(10, 10, 10);
+ v1.lerp(v2, 0.5f);
+ Assert.assertEquals(5, v1.x, 0.0001f);
+ Assert.assertEquals(5, v1.y, 0.0001f);
+ Assert.assertEquals(5, v1.z, 0.0001f);
+
+ PVector result = PVector.lerp(new PVector(0, 0, 0), new PVector(10, 10, 10), 0.5f);
+ Assert.assertEquals(5, result.x, 0.0001f);
+ Assert.assertEquals(5, result.y, 0.0001f);
+ Assert.assertEquals(5, result.z, 0.0001f);
+ }
+
+ @Test
+ public void testAngleBetween() {
+ PVector v1 = new PVector(1, 0, 0);
+ PVector v2 = new PVector(0, 1, 0);
+ float a1 = PVector.angleBetween(v1, v2);
+ Assert.assertEquals(PConstants.HALF_PI, a1, 0.0001f);
+
+ // angleBetween with zero vectors
+ float a2 = PVector.angleBetween(new PVector(0, 0, 0), new PVector(1, 0, 0));
+ Assert.assertEquals(0, a2, 0.0001f);
+
+ // angleBetween with parallel vectors
+ float a3 = PVector.angleBetween(new PVector(1, 0, 0), new PVector(2, 0, 0));
+ Assert.assertEquals(0, a3, 0.0001f);
+
+ // angleBetween with opposite vectors
+ float a4 = PVector.angleBetween(new PVector(1, 0, 0), new PVector(-1, 0, 0));
+ Assert.assertEquals(PConstants.PI, a4, 0.0001f);
+ }
+
+ @Test
+ public void testFromAngle() {
+ PVector v = PVector.fromAngle(0);
+ Assert.assertEquals(1, v.x, 0.0001f);
+ Assert.assertEquals(0, v.y, 0.0001f);
+ Assert.assertEquals(0, v.z, 0.0001f);
+
+ v = PVector.fromAngle(PConstants.HALF_PI);
+ Assert.assertEquals(0, v.x, 0.0001f);
+ Assert.assertEquals(1, v.y, 0.0001f);
+ Assert.assertEquals(0, v.z, 0.0001f);
+
+ PVector target = new PVector();
+ PVector result = PVector.fromAngle(PConstants.PI, target);
+ Assert.assertSame(target, result);
+ Assert.assertEquals(-1, result.x, 0.0001f);
+ Assert.assertEquals(0, result.y, 0.0001f);
+ }
+
+
+ @Test
+ public void testArray() {
+ PVector v = new PVector(3, 4, 5);
+ float[] arr = v.array();
+ Assert.assertEquals(3, arr[0], 0.0001f);
+ Assert.assertEquals(4, arr[1], 0.0001f);
+ Assert.assertEquals(5, arr[2], 0.0001f);
+ }
+
+ @Test
+ public void testRandom2D() {
+ PVector v = PVector.random2D();
+ Assert.assertEquals(1, v.mag(), 0.0001f);
+ Assert.assertEquals(0, v.z, 0.0001f);
+
+ PVector target = new PVector();
+ PVector result = PVector.random2D(target);
+ Assert.assertSame(target, result);
+ Assert.assertEquals(1, result.mag(), 0.0001f);
+ }
+
+ @Test
+ public void testRandom3D() {
+ PVector v = PVector.random3D();
+ Assert.assertEquals(1, v.mag(), 0.0001f);
+
+ PVector target = new PVector();
+ PVector result = PVector.random3D(target);
+ Assert.assertSame(target, result);
+ Assert.assertEquals(1, result.mag(), 0.0001f);
+ }
+
+
+ @Test
+ public void testEqualsAndHashCode() {
+ PVector v1 = new PVector(1, 2, 3);
+ PVector v2 = new PVector(1, 2, 3);
+ PVector v3 = new PVector(3, 2, 1);
+
+ Assert.assertTrue(v1.equals(v2));
+ Assert.assertFalse(v1.equals(v3));
+ Assert.assertEquals(v1.hashCode(), v2.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ PVector v = new PVector(1, 2, 3);
+ String expected = "[ 1.0, 2.0, 3.0 ]";
+ Assert.assertEquals(expected, v.toString());
+ }
+
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a9fe0b6e52..70f93aaff5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -27,6 +27,7 @@ lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version = "0.22.0" }
jsoup = { module = "org.jsoup:jsoup", version = "1.17.2" }
markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version = "0.31.0" }
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" }
+clikt = { module = "com.github.ajalt.clikt:clikt", version = "5.0.2" }
[plugins]
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
diff --git a/java/src/processing/mode/java/lsp/PdeLanguageServer.java b/java/src/processing/mode/java/lsp/PdeLanguageServer.java
index 3d865fcc7b..0673b48231 100644
--- a/java/src/processing/mode/java/lsp/PdeLanguageServer.java
+++ b/java/src/processing/mode/java/lsp/PdeLanguageServer.java
@@ -21,7 +21,7 @@
import org.eclipse.lsp4j.services.LanguageClient;
-class PdeLanguageServer implements LanguageServer, LanguageClientAware {
+public class PdeLanguageServer implements LanguageServer, LanguageClientAware {
Map adapters = new HashMap<>();
LanguageClient client = null;
PdeTextDocumentService textDocumentService = new PdeTextDocumentService(this);