From 1723bd3e901cf6ff0e3e9256849743517b8cf4b9 Mon Sep 17 00:00:00 2001 From: Vaivaswat Dubey <113991324+Vaivaswat2244@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:45:44 +0530 Subject: [PATCH 1/2] pr05 Visual Regression Testing #1: Initial Visual Testing Framework (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * added the image comparator which is the pixel matching algorithm * added build.gradle file * added the test runner * added the simple test * Revise README for Jetpack Compose migration strategy Updated README to reflect migration to Jetpack Compose and strategy for replacing JEditTextArea with RSyntaxTextArea. Added insights on LSP-based editor research and the need for user feedback on Tweak Mode and autocompletion features. * fixing the build issues * added junit as dependency * removing custom class implementation * inclding visual-tests in settings * fixed the overlapping cmd * cleaning * adding packages * added updated screenshot structure * refactoring * added tests in suits * removed simple test * deleting earlier files * updated the core/gradle file * added the infrastructure * added some tests ported by p5js * removing test rendering suite and its test file * added screenshots * config files * fixed the pixeldensity to 1 * Revert "fixed the pixeldensity to 1" This reverts commit 66071ac191d24a4a25d8d8257f58cbeb957c6f35. * fixed pixeldensity to 1 * Configure dependencyUpdates task in build.gradle.kts Add configuration for dependencyUpdates task to manage non-stable versions. * removing rendering gradient screenshot * General cleanup of `Base` I started cleaning up some of `Base`'s startup sequence for clarity of what is being started when. Nowhere near completion and I think a lot of this class will need to be refactored in the future. Also removed some of the timing measurement comments Added some comments to the Processing CLI class * Move contributor list to CONTRIBUTORS.md (#1313) Created CONTRIBUTORS.md and updated .all-contributorsrc to reference the new file instead of README.md. This will reduce the size of the README and improve loading times. * Update BUILD.md with build failure troubleshooting Added troubleshooting steps for build failures related to permissions. * fixing the build issues * inclding visual-tests in settings * updated the core/gradle file * config files * Configure dependencyUpdates task in build.gradle.kts Add configuration for dependencyUpdates task to manage non-stable versions. * fix rebasing --------- Co-authored-by: Stef Tervelde Co-authored-by: Raphaël de Courville --- .all-contributorsrc | 2 +- BUILD.md | 13 + CONTRIBUTORS.md | 258 ++++++++++++ README.md | 261 +------------ app/src/processing/app/Base.java | 366 +++++++++--------- app/src/processing/app/Processing.kt | 33 +- app/src/processing/app/UpdateCheck.java | 3 + .../app/contrib/ContributionManager.java | 6 - .../processing/app/contrib/ManagerFrame.java | 12 - app/src/processing/app/syntax/README.md | 14 +- build.gradle.kts | 3 +- core/build.gradle.kts | 45 ++- .../shapes-3d/per-vertex-fills-linux.png | Bin 0 -> 661 bytes .../shapes-3d/per-vertex-strokes-linux.png | Bin 0 -> 696 bytes .../shapes-3d/vertex-coordinates-linux.png | Bin 0 -> 134 bytes .../shapes/bezier-curves-linux.png | Bin 0 -> 722 bytes .../shapes/closed-curves-linux.png | Bin 0 -> 723 bytes .../shapes/closed-polylines-linux.png | Bin 0 -> 602 bytes .../__screenshots__/shapes/contours-linux.png | Bin 0 -> 991 bytes .../__screenshots__/shapes/curves-linux.png | Bin 0 -> 702 bytes .../shapes/curves-tightness-linux.png | Bin 0 -> 761 bytes .../__screenshots__/shapes/lines-linux.png | Bin 0 -> 387 bytes .../__screenshots__/shapes/points-linux.png | Bin 0 -> 254 bytes .../shapes/polylines-linux.png | Bin 0 -> 583 bytes .../shapes/quad-strips-linux.png | Bin 0 -> 405 bytes .../shapes/quadratic-beziers-linux.png | Bin 0 -> 579 bytes .../__screenshots__/shapes/quads-linux.png | Bin 0 -> 188 bytes .../shapes/single-closed-contour-linux.png | Bin 0 -> 222 bytes .../shapes/single-unclosed-contour-linux.png | Bin 0 -> 222 bytes .../shapes/triangle-fans-linux.png | Bin 0 -> 968 bytes .../shapes/triangle-strips-linux.png | Bin 0 -> 692 bytes .../shapes/triangles-linux.png | Bin 0 -> 686 bytes .../visual/src/core/BaselineManager.java | 38 ++ .../visual/src/core/ProcessingSketch.java | 9 + .../visual/src/core/TestConfig.java | 33 ++ .../visual/src/core/TestResult.java | 45 +++ .../visual/src/core/VisualTestRunner.java | 264 +++++++++++++ .../visual/src/test/base/VisualTest.java | 61 +++ .../visual/src/test/shapes/Shape3DTest.java | 84 ++++ .../visual/src/test/shapes/ShapeTest.java | 356 +++++++++++++++++ .../visual/src/test/suites/ShapesSuite.java | 12 + gradle/libs.versions.toml | 4 + java/src/processing/mode/java/JavaEditor.java | 11 - settings.gradle.kts | 8 +- 44 files changed, 1443 insertions(+), 498 deletions(-) create mode 100644 CONTRIBUTORS.md create mode 100644 core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-fills-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes-3d/per-vertex-strokes-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes-3d/vertex-coordinates-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/bezier-curves-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/closed-curves-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/closed-polylines-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/contours-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/curves-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/curves-tightness-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/lines-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/points-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/polylines-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/quad-strips-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/quadratic-beziers-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/quads-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/single-closed-contour-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/single-unclosed-contour-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/triangle-fans-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/triangle-strips-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shapes/triangles-linux.png create mode 100644 core/test/processing/visual/src/core/BaselineManager.java create mode 100644 core/test/processing/visual/src/core/ProcessingSketch.java create mode 100644 core/test/processing/visual/src/core/TestConfig.java create mode 100644 core/test/processing/visual/src/core/TestResult.java create mode 100644 core/test/processing/visual/src/core/VisualTestRunner.java create mode 100644 core/test/processing/visual/src/test/base/VisualTest.java create mode 100644 core/test/processing/visual/src/test/shapes/Shape3DTest.java create mode 100644 core/test/processing/visual/src/test/shapes/ShapeTest.java create mode 100644 core/test/processing/visual/src/test/suites/ShapesSuite.java 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._ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ben Fry
Ben Fry

💻 🤔 🚇 🧑‍🏫 🚧 🖋 📢
Casey Reas
Casey Reas

💻 🤔 🚇 🧑‍🏫 🖋 📢
codeanticode
codeanticode

💻
Manindra Moharana
Manindra Moharana

💻
Jakub Valtar
Jakub Valtar

💻
A Samuel Pottinger
A Samuel Pottinger

💻
Gottfried Haider
Gottfried Haider

💻
Akarshit Wal
Akarshit Wal

💻
Peter Kalauskas
Peter Kalauskas

💻
Daniel Shiffman
Daniel Shiffman

💻
Joel Moniz
Joel Moniz

💻
Lonnen
Lonnen

💻
Florian Jenett
Florian Jenett

💻
Scott Murray
Scott Murray

💻
Federico Bond
Federico Bond

💻
pvrs12
pvrs12

💻
George Bateman
George Bateman

💻
Sean McKenna
Sean McKenna

💻
kfeuz
kfeuz

💻
David Wicks
David Wicks

💻
Wilm Thoben
Wilm Thoben

💻
Ana
Ana

💻
Amnon Owed
Amnon Owed

💻
Gal Sasson
Gal Sasson

💻
scollovati
scollovati

💻
Yong Joseph Bakos
Yong Joseph Bakos

💻
Kenichi Ito
Kenichi Ito

💻
Efratror
Efratror

💻
Alexis Engelke
Alexis Engelke

💻
tyfkda
tyfkda

💻
Simon Greenwold
Simon Greenwold

💻
Rune Skjoldborg Madsen
Rune Skjoldborg Madsen

💻
Leslie Watkins
Leslie Watkins

💻
Rostyslav Zatserkovnyi
Rostyslav Zatserkovnyi

💻
Dan
Dan

💻
Daniel Howe
Daniel Howe

💻
Josh Giesbrecht
Josh Giesbrecht

💻 🐛
liquidex
liquidex

💻
bgc
bgc

💻
Mohammad Umair
Mohammad Umair

💻
T Michail
T Michail

💻
ohommos
ohommos

💻
Jonathan Feinberg
Jonathan Feinberg

💻
David Fokkema
David Fokkema

💻
liquid
liquid

💻
Kisaru Liyanage
Kisaru Liyanage

💻
BouB
BouB

💻
atk
atk

💻
Xerxes Rånby
Xerxes Rånby

💻
Will Rabalais
Will Rabalais

💻
Utkarsh Tiwari
Utkarsh Tiwari

💻
Prince-Polka
Prince-Polka

💻
jamesjgrady
jamesjgrady

💻
Raphaël de Courville
Raphaël de Courville

💻
Satoshi Okita
Satoshi Okita

💻
Carlos Andrés Rocha
Carlos Andrés Rocha

💻
Vincent Vijn
Vincent Vijn

💻
dzaima
dzaima

💻
mingness
mingness

🚇
Dora Do
Dora Do

🚇
Stef Tervelde
Stef Tervelde

💻
allcontributors[bot]
allcontributors[bot]

💻
Dave
Dave

💻
TN8001
TN8001

💻
Sigmund Hansen
Sigmund Hansen

💻
Rodrigo Bonifácio
Rodrigo Bonifácio

💻
Aidan Pieper
Aidan Pieper

💻
Liam James
Liam James

💻
james gilles
james gilles

💻
Elie Zananiri
Elie Zananiri

💻
Cosimo Cecchi
Cosimo Cecchi

💻
Liam Middlebrook
Liam Middlebrook

💻
Martin Yrjölä
Martin Yrjölä

💻
Michał Urbański
Michał Urbański

💻
Paco
Paco

💻
Patrick Ryan
Patrick Ryan

💻
Paweł Goliński
Paweł Goliński

💻
Rupesh Kumar
Rupesh Kumar

💻
Suhaib Khan
Suhaib Khan

💻
Yves BLAKE
Yves BLAKE

💻
M. Ernestus
M. Ernestus

💻
Francis Li
Francis Li

💻
Parag Jain
Parag Jain

💻
roopa vasudevan
roopa vasudevan

💻
kiwistrongis
kiwistrongis

💻
Alessandro Ranellucci
Alessandro Ranellucci

💻
Alexandre B A Villares
Alexandre B A Villares

💻
Heracles
Heracles

💻
Arya Gupta
Arya Gupta

💻
Damien Quartz
Damien Quartz

💻
Shubham Rathore
Shubham Rathore

💻
Grigoriy Titaev
Grigoriy Titaev

💻
Guilherme Silveira
Guilherme Silveira

💻
Héctor López Carral
Héctor López Carral

💻
Jeremy Douglass
Jeremy Douglass

💻
Jett LaRue
Jett LaRue

💻
Jim
Jim

💻 🐛
Joan Perals
Joan Perals

💻
Josh Holinaty
Josh Holinaty

💻
Keito Takeda
Keito Takeda

💻
Victor Osório
Victor Osório

💻
Torben
Torben

💻
Tobias Pristupin
Tobias Pristupin

💻
Thomas Leplus
Thomas Leplus

💻
Arnoud van der Leer
Arnoud van der Leer

💻
Stanislas Marçais / Knupel
Stanislas Marçais / Knupel

💻
Sanchit Kapoor
Sanchit Kapoor

💻
Miles Fogle
Miles Fogle

💻
Miguel Valadas
Miguel Valadas

💻
Maximilien Tirard
Maximilien Tirard

💻
Matthew Russell
Matthew Russell

💻
dcuartielles
dcuartielles

💻
Jayson Haebich
Jayson Haebich

💻
jordirosa
jordirosa

💻
Justin Shrake
Justin Shrake

💻
Kevin
Kevin

💻
kgtkr
kgtkr

💻
Mark Luffel
Mark Luffel

💻
Никита Король
Никита Король

💻
raguenets
raguenets

💻
robog-two
robog-two

💻
teddywing
teddywing

💻
chikuwa
chikuwa

💻
ಠ_ಠ
ಠ_ಠ

💻
Abe Pazos
Abe Pazos

💻
Alex
Alex

💻
Alexander Hurst
Alexander Hurst

💻
Anıl
Anıl

💻
Barış
Barış

💻
Brian Sapozhnikov
Brian Sapozhnikov

💻
Carlos Mario Rodriguez Perdomo
Carlos Mario Rodriguez Perdomo

💻
CyberFlame
CyberFlame

💻
Dhruv Jawali
Dhruv Jawali

💻
FlorisVO
FlorisVO

💻
Frank Leon Rose
Frank Leon Rose

💻
Greg Borenstein
Greg Borenstein

💻
Guillermo Perez
Guillermo Perez

💻
Henning Kiel
Henning Kiel

💻
J David Eisenberg
J David Eisenberg

💻
Jordan Ephron
Jordan Ephron

💻
Jason Sigal
Jason Sigal

💻
Jordan Orelli
Jordan Orelli

💻
Kalle
Kalle

💻
Laureano López
Laureano López

💻
Lesley Wagner
Lesley Wagner

💻
Mark Slee
Mark Slee

💻
MARTIN LEOPOLD GROEDL
MARTIN LEOPOLD GROEDL

💻
Martin Prout
Martin Prout

💻
Mathias Herberts
Mathias Herberts

💻
Diya Solanki
Diya Solanki

🚇
Neil C Smith
Neil C Smith

🚇
kate hollenbach
kate hollenbach

💻 📦 🧑‍🏫 🐛
Rishabdev Tudu
Rishabdev Tudu

📖 💻
Pau
Pau

📖
Junology
Junology

💻
Jaap Meijers
Jaap Meijers

📖
Xin Xin
Xin Xin

📋 🤔
Benjamin Fox
Benjamin Fox

💻
e1dem
e1dem

💻
Aditya Chaudhary
Aditya Chaudhary

💻
Rishab Kumar Jha
Rishab Kumar Jha

💻
Yehia Rasheed
Yehia Rasheed

💻
Subhraman Sarkar
Subhraman Sarkar

💻 ️️️️♿️ 📖
SushantBansal-tech
SushantBansal-tech

🤔 💻
Konsl
Konsl

📖
Mario Guzman
Mario Guzman

📖
Aranya Dutta
Aranya Dutta

💻
ovalnine
ovalnine

💻
Joshua D. Boyd
Joshua D. Boyd

📖
Vaivaswat Dubey
Vaivaswat Dubey

💻
jSdCool
jSdCool

💻 📖
AhmedMaged
AhmedMaged

💻
Nico Mexis
Nico Mexis

💻
charlotte 🌸
charlotte 🌸

👀
Joackim de Bourqueney
Joackim de Bourqueney

💻
Tonz
Tonz

💻 📖
Andrew
Andrew

💻
Ngoc Doan
Ngoc Doan

💻
Manoel Ribeiro
Manoel Ribeiro

📖
Moon
Moon

💻
Nia Perez
Nia Perez

💻
SuganthiThomas
SuganthiThomas

💻
+ + + + + \ 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._ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Ben Fry
Ben Fry

💻 🤔 🚇 🧑‍🏫 🚧 🖋 📢
Casey Reas
Casey Reas

💻 🤔 🚇 🧑‍🏫 🖋 📢
codeanticode
codeanticode

💻
Manindra Moharana
Manindra Moharana

💻
Jakub Valtar
Jakub Valtar

💻
A Samuel Pottinger
A Samuel Pottinger

💻
Gottfried Haider
Gottfried Haider

💻
Akarshit Wal
Akarshit Wal

💻
Peter Kalauskas
Peter Kalauskas

💻
Daniel Shiffman
Daniel Shiffman

💻
Joel Moniz
Joel Moniz

💻
Lonnen
Lonnen

💻
Florian Jenett
Florian Jenett

💻
Scott Murray
Scott Murray

💻
Federico Bond
Federico Bond

💻
pvrs12
pvrs12

💻
George Bateman
George Bateman

💻
Sean McKenna
Sean McKenna

💻
kfeuz
kfeuz

💻
David Wicks
David Wicks

💻
Wilm Thoben
Wilm Thoben

💻
Ana
Ana

💻
Amnon Owed
Amnon Owed

💻
Gal Sasson
Gal Sasson

💻
scollovati
scollovati

💻
Yong Joseph Bakos
Yong Joseph Bakos

💻
Kenichi Ito
Kenichi Ito

💻
Efratror
Efratror

💻
Alexis Engelke
Alexis Engelke

💻
tyfkda
tyfkda

💻
Simon Greenwold
Simon Greenwold

💻
Rune Skjoldborg Madsen
Rune Skjoldborg Madsen

💻
Leslie Watkins
Leslie Watkins

💻
Rostyslav Zatserkovnyi
Rostyslav Zatserkovnyi

💻
Dan
Dan

💻
Daniel Howe
Daniel Howe

💻
Josh Giesbrecht
Josh Giesbrecht

💻 🐛
liquidex
liquidex

💻
bgc
bgc

💻
Mohammad Umair
Mohammad Umair

💻
T Michail
T Michail

💻
ohommos
ohommos

💻
Jonathan Feinberg
Jonathan Feinberg

💻
David Fokkema
David Fokkema

💻
liquid
liquid

💻
Kisaru Liyanage
Kisaru Liyanage

💻
BouB
BouB

💻
atk
atk

💻
Xerxes Rånby
Xerxes Rånby

💻
Will Rabalais
Will Rabalais

💻
Utkarsh Tiwari
Utkarsh Tiwari

💻
Prince-Polka
Prince-Polka

💻
jamesjgrady
jamesjgrady

💻
Raphaël de Courville
Raphaël de Courville

💻
Satoshi Okita
Satoshi Okita

💻
Carlos Andrés Rocha
Carlos Andrés Rocha

💻
Vincent Vijn
Vincent Vijn

💻
dzaima
dzaima

💻
mingness
mingness

🚇
Dora Do
Dora Do

🚇
Stef Tervelde
Stef Tervelde

💻
allcontributors[bot]
allcontributors[bot]

💻
Dave
Dave

💻
TN8001
TN8001

💻
Sigmund Hansen
Sigmund Hansen

💻
Rodrigo Bonifácio
Rodrigo Bonifácio

💻
Aidan Pieper
Aidan Pieper

💻
Liam James
Liam James

💻
james gilles
james gilles

💻
Elie Zananiri
Elie Zananiri

💻
Cosimo Cecchi
Cosimo Cecchi

💻
Liam Middlebrook
Liam Middlebrook

💻
Martin Yrjölä
Martin Yrjölä

💻
Michał Urbański
Michał Urbański

💻
Paco
Paco

💻
Patrick Ryan
Patrick Ryan

💻
Paweł Goliński
Paweł Goliński

💻
Rupesh Kumar
Rupesh Kumar

💻
Suhaib Khan
Suhaib Khan

💻
Yves BLAKE
Yves BLAKE

💻
M. Ernestus
M. Ernestus

💻
Francis Li
Francis Li

💻
Parag Jain
Parag Jain

💻
roopa vasudevan
roopa vasudevan

💻
kiwistrongis
kiwistrongis

💻
Alessandro Ranellucci
Alessandro Ranellucci

💻
Alexandre B A Villares
Alexandre B A Villares

💻
Heracles
Heracles

💻
Arya Gupta
Arya Gupta

💻
Damien Quartz
Damien Quartz

💻
Shubham Rathore
Shubham Rathore

💻
Grigoriy Titaev
Grigoriy Titaev

💻
Guilherme Silveira
Guilherme Silveira

💻
Héctor López Carral
Héctor López Carral

💻
Jeremy Douglass
Jeremy Douglass

💻
Jett LaRue
Jett LaRue

💻
Jim
Jim

💻 🐛
Joan Perals
Joan Perals

💻
Josh Holinaty
Josh Holinaty

💻
Keito Takeda
Keito Takeda

💻
Victor Osório
Victor Osório

💻
Torben
Torben

💻
Tobias Pristupin
Tobias Pristupin

💻
Thomas Leplus
Thomas Leplus

💻
Arnoud van der Leer
Arnoud van der Leer

💻
Stanislas Marçais / Knupel
Stanislas Marçais / Knupel

💻
Sanchit Kapoor
Sanchit Kapoor

💻
Miles Fogle
Miles Fogle

💻
Miguel Valadas
Miguel Valadas

💻
Maximilien Tirard
Maximilien Tirard

💻
Matthew Russell
Matthew Russell

💻
dcuartielles
dcuartielles

💻
Jayson Haebich
Jayson Haebich

💻
jordirosa
jordirosa

💻
Justin Shrake
Justin Shrake

💻
Kevin
Kevin

💻
kgtkr
kgtkr

💻
Mark Luffel
Mark Luffel

💻
Никита Король
Никита Король

💻
raguenets
raguenets

💻
robog-two
robog-two

💻
teddywing
teddywing

💻
chikuwa
chikuwa

💻
ಠ_ಠ
ಠ_ಠ

💻
Abe Pazos
Abe Pazos

💻
Alex
Alex

💻
Alexander Hurst
Alexander Hurst

💻
Anıl
Anıl

💻
Barış
Barış

💻
Brian Sapozhnikov
Brian Sapozhnikov

💻
Carlos Mario Rodriguez Perdomo
Carlos Mario Rodriguez Perdomo

💻
CyberFlame
CyberFlame

💻
Dhruv Jawali
Dhruv Jawali

💻
FlorisVO
FlorisVO

💻
Frank Leon Rose
Frank Leon Rose

💻
Greg Borenstein
Greg Borenstein

💻
Guillermo Perez
Guillermo Perez

💻
Henning Kiel
Henning Kiel

💻
J David Eisenberg
J David Eisenberg

💻
Jordan Ephron
Jordan Ephron

💻
Jason Sigal
Jason Sigal

💻
Jordan Orelli
Jordan Orelli

💻
Kalle
Kalle

💻
Laureano López
Laureano López

💻
Lesley Wagner
Lesley Wagner

💻
Mark Slee
Mark Slee

💻
MARTIN LEOPOLD GROEDL
MARTIN LEOPOLD GROEDL

💻
Martin Prout
Martin Prout

💻
Mathias Herberts
Mathias Herberts

💻
Diya Solanki
Diya Solanki

🚇
Neil C Smith
Neil C Smith

🚇
kate hollenbach
kate hollenbach

💻 📦 🧑‍🏫 🐛
Rishabdev Tudu
Rishabdev Tudu

📖 💻
Pau
Pau

📖
Junology
Junology

💻
Jaap Meijers
Jaap Meijers

📖
Xin Xin
Xin Xin

📋 🤔
Benjamin Fox
Benjamin Fox

💻
e1dem
e1dem

💻
Aditya Chaudhary
Aditya Chaudhary

💻
Rishab Kumar Jha
Rishab Kumar Jha

💻
Yehia Rasheed
Yehia Rasheed

💻
Subhraman Sarkar
Subhraman Sarkar

💻 ️️️️♿️ 📖
SushantBansal-tech
SushantBansal-tech

🤔 💻
Konsl
Konsl

📖
Mario Guzman
Mario Guzman

📖
Aranya Dutta
Aranya Dutta

💻
ovalnine
ovalnine

💻
Joshua D. Boyd
Joshua D. Boyd

📖
Vaivaswat Dubey
Vaivaswat Dubey

💻
jSdCool
jSdCool

💻 📖
AhmedMaged
AhmedMaged

💻
Nico Mexis
Nico Mexis

💻
charlotte 🌸
charlotte 🌸

👀
Joackim de Bourqueney
Joackim de Bourqueney

💻
Tonz
Tonz

💻 📖
Andrew
Andrew

💻
Ngoc Doan
Ngoc Doan

💻
Manoel Ribeiro
Manoel Ribeiro

📖
Moon
Moon

💻
Nia Perez
Nia Perez

💻
SuganthiThomas
SuganthiThomas

💻
- - - - - +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 0000000000000000000000000000000000000000..608b7ffe20076c329cff71d16b9875377f819580 GIT binary patch literal 661 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZV2bf{aSW-5dppI^TiQ{g?f8WZ z;j=4mxn)f4I5d^bL%}07Wpa6RrvAlx845m zcG0qevT1AA?o^p@?%liVZi^4Dzn0i~^G=Ke-_h_n=dWFRp0@eqd*6e%?%&_BMqElx{UBI)8*+*IS5uR)myWx7|OP>a55fdnBG zH_O^^PwudYbJ6Q3iLKc%XM3EgZbYJG{$8)B4Trd*?d|`U<-fmt_ijv)ds`q#VdAd4 zc>*j`Pi1Z0RZ;cp-^ai^DW*X4#I*fZUtM)IYiW?-=Z_|roYK~4JeKiS*6ryORX+?A zXwqJz5qA8t2Y24aM>9YY0*S5{<$?5sq@xq%fb<@oFp-B9-rny+E*}kD3e-C%q^3U1D#hK;bFaP>wl*f8n z=u1`pufN|ue!P-UFn>jAT6#L;tq3DVYia5G7Zt)-neyI=R0r^`R%A+JYp#CQ$udoj z(R%rE|1-O7u6_F^p4!yza@l2l$Fn={+uM!V6xzO;Mub%A986y%y2#!|{ENN(9s7It z>oXZD)@blCMyI4nwFL(?^J`}sezUq5T_ zPF>2%$nEnw{O{J%%`e_DqarzhC9G#66=<9N~(JKf`h{d(r)W#XBc1Y4usN z;Oje|*#GXQ}zXJ>zV`t)wb zUh_{Mg)TWo>P+@a->Q+>3sR63VHVmyeS%n4(#$FL_pHAsYv`IZrB1HhvygLXYuZMi zQ!?+foH8AmPpf^dSh!_DSL!4yIc41*hSP1fa@x8*LZ^>dgJ`+a$7I&u&;PxD$GNqM cCx5cLD(_}{_NsjYFhw$Wy85}Sb4q9e0K@4({r~^~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e07fe529c8ee84295a8d219564ef7954d4f5ca85 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~AzMd|QAr*0N&n;wRFyLv}@UQ;n zxmoMGS1Sk|-7`6@UHX<#Zs#_a=ta60r)79=QN_=-JNe?t>YUel7l8&dc)I$ztaD0e F0suSKG4B8X literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e628f405416f0c1608764e2352c817234fcbda87 GIT binary patch literal 722 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZV4CUa;uum9_ja1CM^T`}@!Kvg zAC*NG^3QQmWD>o2^q{7JifdGei%67|NDRx-g^I<21qFIXjyeiVj4CO(P^R7~;J876 zM`+fn?+eA_*7aZAdHgXq-*@xQIluq^e^4<0cg|O{NxFZIrzvDygd>LwmtKFBmzSUa z`q|&Vwe|J#(+d?cI*&fuzTJGzsWUZp;+-x#dYB!(7Y3Y3GX{!${=7Lv>u9PBN0ha- z_2P>h%Z-dBcub^t^&UH1Fq(b#`}gk_Q)h;)PBoD#lw@zZcJX51+OWeb&&0&XTg&jp z9BW)qT2@w8TG|>s(^GA7qXPrTy4bilwr0mUr+RL`^;>=!WZmkZl@dH`b561GIL-A7 z*P9M9D{TAi(3K&RPqL&NfB5l3WBO@DNswknz3H!?J!4BZ4$u&p99wC{@i)~`C*f4 zMP}&g(5+Et{}sygANN`+bgAH=(4H*~moHy-QEIH#;A}rE*U#==&mO<__U-5&J2utG z&p!L?+c&c(4_>@~u=?unD%C_DzvaO(Cz>A?aGd5)d2m5~!3CxVVsFZRzkaQ~?(fv) z%a#w&)t^M=db^m>P3kwTN%b&GtlwWMAicg#9;j;Sbwb!L< zHgM>?`B@{kOk~dTqE~Ade4U=OamV)U?BZPamp)DX|MV#<-wdB+ckjmjXq10;uq(=8 z`Q^r~$I~_+4CA}b2$i*)F$H}hIMP!eeK`9dw0-Emfyi9QoY9? v?~GY@G2?;rlr@g8>*1*anXzGER6WDBfW`0DPwz1Vrd0+{S3j3^P6(U7{_NTQ6oai*oln=O{SDA$ZGb;ia!APv6{)kb_UU;M6xr}(5B0n^wQyzr4H*27|oF@urnZrEYI;eLkOFudmnZ_%KlxAElDaMfa${V0hnD3Yivl*%ymNGMUt9G&Y;<`S}^=vLMqU zE&F=CE|<$LmrE*@!Z!#06yd}Z5NN@EhO)!S>Fw?ZS_o!>9QvcD1wAAAQ!OM=moV6-F{EeV8HqtRHcR`}F^2(3gS(d+f_ zss9jK#bPm^&*M}7A+(y!W-u81{2mFd!C+un7ITlw*KJt`-#aU7>!ub0bZ{DvG_ z3&xa6rN8#*b^@-~D_o<~>EQQ-&|0vXR;v{bhoAR|oD_${K|f@)7L>7CtriLew8xiD ziqUAK88TW6%9zXLhQpzIk8hk5njxdLpp2nV=yWR$XHrX#%{NJKA++Cd?$sskg>E>8DYlpc*J8VyoHRVrOJp0 zvhffymX<2x?RLWh*+@gi(z5Sm#M2~*LdMlnWyGT-h(gBIQf0)Wgz!Vg)q0ds6K2S` zTAF2a!wDHzOTUZ{I3eR|+1h2iTrTVNy5H~DYPDD_20#3Z#RAW`BhZqT(d_|Ofgtm+>Dx5CSbIV=9&M$pazKf~o#_AOu>#Znwin o{vgx(7)DEi(UM@aBz&pX4@Y3iAecJL&;S4c07*qoM6N<$g28DYmjD0& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..032c60a7538965af172c780806541a149a2616ee GIT binary patch literal 991 zcmV<510ei~P)FV-VrRjE?m|K&N=lNCvJwkq!NS7l=H??wu~8IBzLI1oHtZ=2 zA7!cc{bg!8J$LT8Gk4~`dF%cb)7&#>&iQrDxzF=V!u78tK{osj;#yAlKea9{E;cqc z#>dCo+S*D=N)i(j{eFK_Q`5-E$m;4UPvr|9q0?gGw6wI+($enk?)mxo+uPgs_jj(` z(^6bqoRX5VwY4Q*^MbHiIOXKzoSvTk2~RK>%*)H8k9^rR)>?L)3_3bG0)c>h-!&Fm z_MPDC>#L}!XliOoj^Ylfg-u3AhU&z9Qc_Za0zcIb_xJainVCmNM`9>9NUg=i#j2_* z)hRnWdueG&?ZZzRdGzt|VRUP6Zyz2WR{OX_YW4Q^j*gC+S}crkGN94t^DQha7~Rg# z&pG?DlMAF)d3pKq@v*7JmWd7~H8nNq>FH+A{QUgogq}vTSo*fla-Z~+S=OG)KsqAi>G=g z`2PN`uC68`iWkFSS4#uuiM`V}89Y8d673ET59Rw-c%a1@S4%Vv{1rMoJMAO`544y} zEzvYQKR>gs6%`e7h!8x`A{MA6nx6jteogu}iQFS1#3y2boIecwo&S-RC}{N%X!stM zLxkXgmMH%<7$yJy{=OU{1P`>t$C3v7A4nc(5wXZE#BpM#6j3k=B%6hX2U@VZyGw2% zjz2v;Nu3{X%F4>JUubxv1>`$&3v-KtjC)#GC;Lv<9%>n|N5pX|3*ytw%}q~F51%2e zt*r+K2ePXckF^Ytpo}K3b9E|Y5ECu;#I>9d*K$H!%L#EUC&aa!@E6i*VdEdxhmimP N002ovPDHLkV1ic0*?ufv)TB3KD}PwXf*I`Lio{|&1SpZ?)`(u$48}7 zSt^zAAQJe|TCdj!rvsD8+3613xKMDStgUgLob5|Eigb%2bRkv zm}RqBJmCU((1NmKqa*YAT%k~u%Vj+0C%9@c%YKr6zYk`lYm=)MD*H()m5NrY?e%(i z@_D#wvCDpvcsy>gSYUIN}VPw7w}j8yKuosS1SxPB{g= z){kXp1NDa&K)2h)SuE(aE-5>kP$=Z_cyJaAdaVn~j@WLu8jYq{EaJGYP-nxh%-ENX!2G{9LV8Z*Om~=dG zo+3xW;jq{1bvPVyxm+X?nM@|X-w*pZsm&*3S_j~K^6>BgODV`87K`Qc`BtliM^izg k^$!rVND#D05VT163!sr))f)NWK>z>%07*qoM6N<$f@;K21ONa4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..aecfee50eac36f1b58ad3c38476e9db5d17ffb4c GIT binary patch literal 761 zcmVsCmYl~g(zLZCDB106h#EVABzwKiqzCngMpym*~vj@iBbenQjwss zDFRUu!^VmxrSFI2;{2T6*XN!aFsZ>OiH+uPfAyPZ-_9IeO4$H`>! z{lmw{$K~ZE$VDQN`}=!5bT!1$>UO&`r(Z^+Q6Lb|YPI=%9*h*eYA`9VKL}f?xdcDof z&1SQSLm7l?k(M2?ySsaJb%jG21ZoK=I|5G0a5#)}83bwxD?6J^CgXHEaV~=pEt<0b zlFep$E?`2mXv+S}$;nAD7{t*GLbNE$K4X7>ANC>~%|NV0Uv}W-(^-&d(U<-G{oQCZrcx;!Jr9`{W!b;JzHBxd+(YAB z4rE&JL36nrUTrntK@EhgR%@%(!nqvCw2H+d++ITZa5%)v@&YRG%ncLU?KTcw1eq29 zXEi+0E0xN0I{o_kir41=b&SX33WehA>OOQ5b`#tpJr4 r8@LYrVW9k9@IG&Kr2Et}jXCAmtp)3CXp=W5rH2&O)st6aw-ntFh|<1-xH+On2f zC!IO5=gIN-cy5lh6+IR$*K`e=1ft51b~t3*l~~NVP*u_{@b!GK_b+Ubm?d^tJY;_S V@wSBadBDJB@O1TaS?83{1OQeUr{Dkp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d0aecf8d308801172759a216d5c9a5373da55412 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~AM?GB}Ln`9lPBr8^s=(tM`Oxmp z?Bp*IXFvUKVA-S^Z1aRyG;7H#)6z?yB7&Yt%PkRe&N5sgb~q@z=VJBxt;bBy75_Bw zUb{il{*lde{>@ji*8bfJVHv%hmbp#i$@>KtC)F4BPOGsqiCT8o=3jff}k2tuN|JJ}OAcZW4I z_w3my&R5R4$$T=uUm@gtBq2Qb46K#}t0lo|Nw8WH2(4r?x!rE@)I5Y%DwUc{CU|Nd zLaS6N_4|E1H4mZHYPFipCZ3vy&>9Q|<#HKMe_^#+;ZaZst@(VO&*$;<6^q3p7K=@% zQ~VGJLJMjbiA47MJ%0EJIK|^}xOh69-fTAbSq`BE78tDVnhG=J%& z7!HS;A)~dRjD!632plq&mMSBxDHe--Cq=N3v9wef@j@QKLdMckWyA~F1PB>R%PS*Z$VM76 zmX`A8smRCj{_#xwJ>6FnAGh|#X%`*Dogp8}DUq%m3$hcaL zb{Q|1%X+=;^?LPsJ&{O6qtWGZiFZ5@Xi3ZH_kf=i4u`YZY_(c#x7(x9=>4B{ffiXt z9S?*+>$!}7=z$PuK^Zfdj3Ez%Knu2-^FRo+fZc9~M`n;|{To(Gg4L2>wIqC~))x|j V=vi=ANS6Qr002ovPDHLkV1fs&1S0?d literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a0836a3a6d019352d0e4cdb251912f5ea60cc2f7 GIT binary patch literal 405 zcmV;G0c!q&4g4&>0f#w$6U_Cg3LEz|{tiz%IAKeK|HKJ$5JV`R$kD@~6 zK|jfZKlly2mISXQ!D~tIS`x0brfG`f_;Hsgip&H)6yr+E@Or(PrunyoXnj7PvMhNH7{f5+dCn)EBhYHww(B~+5W{@Lx~}=ea|BvNQ7p^C z7h*i0&%W>Z#B)%sZQGJ0IgW!b1laf8+(g!OyV+M8e@Pp87PO&ld~^@v;jXS)EQtDwWP=Gd%bS)9F+w6t-F|yzBx;7Dy(O9*+ml ze$@GVb~qf-XcVvci92gB7$_8qUayBYJfvE!%H{HYzmM1a#GM67rIJ>w-S78!3DI`D zg}JbllS{^)_csuDdqp-n{#3&i9=uN|~k% zl8TrUY95u5RXR;f>dMswr{$jE&75=9VtLg{W|f%ExR7}Xhv2Sxc9)%kt2cMROeXot_v8w%{@Myh7VkmbX{603FQW>FVdQ&MBb@0K0oz0RR91 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..401b9974d104fb6c160742813712fd19b64d37b2 GIT binary patch literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~AOFdm2Ln`9lp4-UFY{=sr_}2br z?o@`ADq?0G{EFcprkZGMo)@mgdrxt*%*>}JbllS{^)_csuDdqp-n{#3&i9=uN|~k% zl8TrUY95u5RXR;f>dMswr{$jE&75=9VtLg{W|f%ExR7}Xhv2Sxc9)%kt2cMROeXot_v8w%{@Myh7VkmbX{603FQW>FVdQ&MBb@0K0oz0RR91 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c7c2b87e6474ded14d75d91e6e02c3c0fbaed4e9 GIT binary patch literal 968 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 UYk7IszyJUM07*qoM6N<$g7s=T0RR91 literal 0 HcmV?d00001 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") + From ed3d1e47bf19d965a141b0e5fb96a81e0813944d Mon Sep 17 00:00:00 2001 From: Vaivaswat Dubey <113991324+Vaivaswat2244@users.noreply.github.com> Date: Wed, 28 Jan 2026 06:49:55 +0530 Subject: [PATCH 2/2] pr05 Visual Regression Testing #2: Adding More Tests (#1315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * added the image comparator which is the pixel matching algorithm * added build.gradle file * added the test runner * added the simple test * Revise README for Jetpack Compose migration strategy Updated README to reflect migration to Jetpack Compose and strategy for replacing JEditTextArea with RSyntaxTextArea. Added insights on LSP-based editor research and the need for user feedback on Tweak Mode and autocompletion features. * fixing the build issues * added junit as dependency * removing custom class implementation * inclding visual-tests in settings * fixed the overlapping cmd * cleaning * adding packages * added updated screenshot structure * refactoring * added tests in suits * removed simple test * deleting earlier files * updated the core/gradle file * added the infrastructure * added some tests ported by p5js * removing test rendering suite and its test file * added screenshots * config files * fixed the pixeldensity to 1 * Revert "fixed the pixeldensity to 1" This reverts commit 66071ac191d24a4a25d8d8257f58cbeb957c6f35. * fixed pixeldensity to 1 * Configure dependencyUpdates task in build.gradle.kts Add configuration for dependencyUpdates task to manage non-stable versions. * removing rendering gradient screenshot * added font alignment size leading and width tests * added complex pfont tests * added screenshots for the tests * added typography screenshots * shape-modes screenshots * added shapemodes cases * changed resolutions of some tests * trying to fix arc issue --------- Co-authored-by: Stef Tervelde Co-authored-by: Raphaël de Courville --- .../shape-modes/arc-center-linux.png | Bin 0 -> 639 bytes .../shape-modes/arc-corner-linux.png | Bin 0 -> 639 bytes .../shape-modes/arc-corners-linux.png | Bin 0 -> 639 bytes .../arc-negative-dimensions-linux.png | Bin 0 -> 446 bytes .../shape-modes/arc-radius-linux.png | Bin 0 -> 639 bytes .../shape-modes/ellipse-center-linux.png | Bin 0 -> 958 bytes .../shape-modes/ellipse-corner-linux.png | Bin 0 -> 958 bytes .../shape-modes/ellipse-corners-linux.png | Bin 0 -> 958 bytes .../ellipse-negative-dimensions-linux.png | Bin 0 -> 1010 bytes .../shape-modes/ellipse-radius-linux.png | Bin 0 -> 958 bytes .../shape-modes/rect-center-linux.png | Bin 0 -> 704 bytes .../shape-modes/rect-corner-linux.png | Bin 0 -> 704 bytes .../shape-modes/rect-corners-linux.png | Bin 0 -> 704 bytes .../rect-negative-dimensions-linux.png | Bin 0 -> 177 bytes .../shape-modes/rect-radius-linux.png | Bin 0 -> 704 bytes .../align/multi-line-center-bottom-linux.png | Bin 0 -> 1261 bytes .../align/multi-line-center-center-linux.png | Bin 0 -> 1260 bytes .../align/multi-line-center-top-linux.png | Bin 0 -> 1275 bytes .../align/multi-line-left-bottom-linux.png | Bin 0 -> 1214 bytes .../align/multi-line-left-center-linux.png | Bin 0 -> 1215 bytes .../align/multi-line-left-top-linux.png | Bin 0 -> 1233 bytes .../align/multi-line-right-bottom-linux.png | Bin 0 -> 1217 bytes .../align/multi-line-right-center-linux.png | Bin 0 -> 1215 bytes .../align/multi-line-right-top-linux.png | Bin 0 -> 1233 bytes .../align/single-word-center-bottom-linux.png | Bin 0 -> 5894 bytes .../align/single-word-center-center-linux.png | Bin 0 -> 5910 bytes .../align/single-word-center-top-linux.png | Bin 0 -> 5894 bytes .../align/single-word-left-bottom-linux.png | Bin 0 -> 5681 bytes .../align/single-word-left-center-linux.png | Bin 0 -> 5692 bytes .../align/single-word-left-top-linux.png | Bin 0 -> 5681 bytes .../align/single-word-right-bottom-linux.png | Bin 0 -> 5622 bytes .../align/single-word-right-center-linux.png | Bin 0 -> 5632 bytes .../align/single-word-right-top-linux.png | Bin 0 -> 5622 bytes .../typography/complex/colored-text-linux.png | Bin 0 -> 3866 bytes .../typography/complex/rotated-text-linux.png | Bin 0 -> 4263 bytes .../complex/transparent-text-linux.png | Bin 0 -> 7043 bytes .../typography/font/default-font-linux.png | Bin 0 -> 761 bytes .../typography/font/monospace-font-linux.png | Bin 0 -> 775 bytes .../typography/font/system-font-linux.png | Bin 0 -> 1401 bytes .../leading/different-values-linux.png | Bin 0 -> 5905 bytes .../typography/pfont/ascent-descent-linux.png | Bin 0 -> 350 bytes .../pfont/char-availability-linux.png | Bin 0 -> 3530 bytes .../size/sizes-comparison-linux.png | Bin 0 -> 8814 bytes .../typography/width/string-width-linux.png | Bin 0 -> 1664 bytes .../visual/src/core/ImageComparator.java | 417 +++++++++++++++ .../src/test/shapemodes/ShapeModeTest.java | 335 ++++++++++++ .../src/test/typography/TypographyTest.java | 484 ++++++++++++++++++ settings.gradle.kts | 3 - 48 files changed, 1236 insertions(+), 3 deletions(-) create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/arc-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/arc-corner-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/arc-corners-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/arc-negative-dimensions-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/arc-radius-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/ellipse-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/ellipse-corner-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/ellipse-corners-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/ellipse-negative-dimensions-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/ellipse-radius-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/rect-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/rect-corner-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/rect-corners-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/rect-negative-dimensions-linux.png create mode 100644 core/test/processing/visual/__screenshots__/shape-modes/rect-radius-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-center-bottom-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-center-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-center-top-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-left-bottom-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-left-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-left-top-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-right-bottom-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-right-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/multi-line-right-top-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-center-bottom-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-center-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-center-top-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-left-bottom-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-left-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-left-top-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-right-bottom-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-right-center-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/align/single-word-right-top-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/complex/colored-text-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/complex/rotated-text-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/complex/transparent-text-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/font/default-font-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/font/monospace-font-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/font/system-font-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/leading/different-values-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/pfont/ascent-descent-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/pfont/char-availability-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/size/sizes-comparison-linux.png create mode 100644 core/test/processing/visual/__screenshots__/typography/width/string-width-linux.png create mode 100644 core/test/processing/visual/src/core/ImageComparator.java create mode 100644 core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java create mode 100644 core/test/processing/visual/src/test/typography/TypographyTest.java diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-center-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..124d3c8db9c9e481eb21efa1e6106111f1c3e811 GIT binary patch literal 639 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`*3@>Eakt5%+eMVV;(uL`yN7 z1rw{P`vbizbDF#hxLo>O_M3FP>ulocoW!Q$;#zl<%Zu%|YE-|Qz}fK1xAzzO?|%JN ztS&olg87>Y)~-vNZ|0at@v=3?t#42E6`pG-!W!6?Y#%s(&cu1=pC5j>;r83UrwKr6xi%-wh2egFOV zW5v#xOMmL+mz0$p3lMRUw0ZR3+4Gz?^Xsow+i%O-b-2YOxkleoeZExlyE3<`OP#-JSG&{}1^$}3aSNCC>J1bSKcX*vWxwckMLm5< zr29X)MAgM69}85$b{d8?%;>qqd8+ZD&Xgy6OfJ03_?DkBea`e{clvdvq)buUSIJ-q zbYZrPg4*Je!@pE0|<$pq=(K>4nUDCo�CA5l`pim-17hMGGM}D@O1TaS?83{ F1OTblC^P^7 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-corner-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-corner-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..124d3c8db9c9e481eb21efa1e6106111f1c3e811 GIT binary patch literal 639 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`*3@>Eakt5%+eMVV;(uL`yN7 z1rw{P`vbizbDF#hxLo>O_M3FP>ulocoW!Q$;#zl<%Zu%|YE-|Qz}fK1xAzzO?|%JN ztS&olg87>Y)~-vNZ|0at@v=3?t#42E6`pG-!W!6?Y#%s(&cu1=pC5j>;r83UrwKr6xi%-wh2egFOV zW5v#xOMmL+mz0$p3lMRUw0ZR3+4Gz?^Xsow+i%O-b-2YOxkleoeZExlyE3<`OP#-JSG&{}1^$}3aSNCC>J1bSKcX*vWxwckMLm5< zr29X)MAgM69}85$b{d8?%;>qqd8+ZD&Xgy6OfJ03_?DkBea`e{clvdvq)buUSIJ-q zbYZrPg4*Je!@pE0|<$pq=(K>4nUDCo�CA5l`pim-17hMGGM}D@O1TaS?83{ F1OTblC^P^7 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-corners-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-corners-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..124d3c8db9c9e481eb21efa1e6106111f1c3e811 GIT binary patch literal 639 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`*3@>Eakt5%+eMVV;(uL`yN7 z1rw{P`vbizbDF#hxLo>O_M3FP>ulocoW!Q$;#zl<%Zu%|YE-|Qz}fK1xAzzO?|%JN ztS&olg87>Y)~-vNZ|0at@v=3?t#42E6`pG-!W!6?Y#%s(&cu1=pC5j>;r83UrwKr6xi%-wh2egFOV zW5v#xOMmL+mz0$p3lMRUw0ZR3+4Gz?^Xsow+i%O-b-2YOxkleoeZExlyE3<`OP#-JSG&{}1^$}3aSNCC>J1bSKcX*vWxwckMLm5< zr29X)MAgM69}85$b{d8?%;>qqd8+ZD&Xgy6OfJ03_?DkBea`e{clvdvq)buUSIJ-q zbYZrPg4*Je!@pE0|<$pq=(K>4nUDCo�CA5l`pim-17hMGGM}D@O1TaS?83{ F1OTblC^P^7 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-negative-dimensions-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-negative-dimensions-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a97455efbf08757a16dddd60b6797557ed8af7f1 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZU~KhtaSW-5dwWH9^`Qij10Pi* zCH&eQ*_GDlolMB&2%CLSUqRvzlUn3#ol`o8H)PD?FpHlQb9<6&L2Y33$0`Sw_sV_M zJNgZFJQsJ7JI_~tY}tpYo=nvfc&D^FMoM5OKAzuHwtMdT-+SZMU(C3YWf~~5`l^=o z7rFmy-j>~dTPEt7xi#wX$BL~{M+LOlKQ!>1^nU)0mq4+! z%`;c63KTJPEPqp?H7{~m=DFv^X3rbv`W-d)p0woi&oj^d9C})G(d2EV==n8Od*>Bx z4`Oj#@cP$hpyJ6Vm&EApuikbqa{cuW(<i6L53WAxNHm}(A{TQnB`5KX&rTF!3jS9i2fkvzMLvFZE;$Mqr& Rzku<=;OXk;vd$@?2>>=U&#?di literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/arc-radius-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/arc-radius-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..124d3c8db9c9e481eb21efa1e6106111f1c3e811 GIT binary patch literal 639 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`*3@>Eakt5%+eMVV;(uL`yN7 z1rw{P`vbizbDF#hxLo>O_M3FP>ulocoW!Q$;#zl<%Zu%|YE-|Qz}fK1xAzzO?|%JN ztS&olg87>Y)~-vNZ|0at@v=3?t#42E6`pG-!W!6?Y#%s(&cu1=pC5j>;r83UrwKr6xi%-wh2egFOV zW5v#xOMmL+mz0$p3lMRUw0ZR3+4Gz?^Xsow+i%O-b-2YOxkleoeZExlyE3<`OP#-JSG&{}1^$}3aSNCC>J1bSKcX*vWxwckMLm5< zr29X)MAgM69}85$b{d8?%;>qqd8+ZD&Xgy6OfJ03_?DkBea`e{clvdvq)buUSIJ-q zbYZrPg4*Je!@pE0|<$pq=(K>4nUDCo�CA5l`pim-17hMGGM}D@O1TaS?83{ F1OTblC^P^7 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-center-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fd115f3795309df11b9ce8e8425de9226e967f GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)$<>Eakt5%+fb#w;U85x2KZ z0v;}xT<#wTQ{+#WY~;#ODY$xzlY_v<&zx(VUri{JI%w`OUu}Zg(#RWXHO2pbbAR8k z=JY(Lri+IYZ_KnVyTp0vqPZWV{;$7(|Ni{>^W(>l&z_}O$jqBNx72Fxqf!G2AGOK9 zfB$~8|7iM`Y-@5-VU%veO`SS<+uJdQoHlIFy`e)6$MLeh0T%3LO z*{@$!`|qzloVxvXY;5e>>)9LsvYKRnsj#W5th~5h?qJz&*%|16iHQ^&UF2TwQ?MB%q7CEdJ)?9qB)s5k-6mOgYm{Y~EAae0Rb$_7H z+#R;_oM-mFV41s2t>b2&9_Kk-pc&okbaP8WP4ujG#L6_KSxZ^Y@DbQ7=-b;M1N72^ z4WdA&G+&ouG|4upXcPf)UMn`F^|H;k0Sc8o`c}c0YPLwYWY#Q&H0xa|7TbYl=`jaoAD&U$nlJui78W&22lIqFtF>gSwK!@4FYl2wKL2zm8$s!66(DD z*mBCv4!;YTSxqzEzL@mjP7%kfy)SNEatn6_`W)y~pxF;y*`H5yzL4XtY|aAqfB8nR zYuAQ@jQh7w0OZd7Prz0TrvP}osQLC$#f)u>CpMpCF_gY|h2zA+Gg7>K(ctJ=D+uzo kxCe+sOiDWK>Gq#VFVdQ&MBb@0L}5ld;kCd literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corner-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corner-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fd115f3795309df11b9ce8e8425de9226e967f GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)$<>Eakt5%+fb#w;U85x2KZ z0v;}xT<#wTQ{+#WY~;#ODY$xzlY_v<&zx(VUri{JI%w`OUu}Zg(#RWXHO2pbbAR8k z=JY(Lri+IYZ_KnVyTp0vqPZWV{;$7(|Ni{>^W(>l&z_}O$jqBNx72Fxqf!G2AGOK9 zfB$~8|7iM`Y-@5-VU%veO`SS<+uJdQoHlIFy`e)6$MLeh0T%3LO z*{@$!`|qzloVxvXY;5e>>)9LsvYKRnsj#W5th~5h?qJz&*%|16iHQ^&UF2TwQ?MB%q7CEdJ)?9qB)s5k-6mOgYm{Y~EAae0Rb$_7H z+#R;_oM-mFV41s2t>b2&9_Kk-pc&okbaP8WP4ujG#L6_KSxZ^Y@DbQ7=-b;M1N72^ z4WdA&G+&ouG|4upXcPf)UMn`F^|H;k0Sc8o`c}c0YPLwYWY#Q&H0xa|7TbYl=`jaoAD&U$nlJui78W&22lIqFtF>gSwK!@4FYl2wKL2zm8$s!66(DD z*mBCv4!;YTSxqzEzL@mjP7%kfy)SNEatn6_`W)y~pxF;y*`H5yzL4XtY|aAqfB8nR zYuAQ@jQh7w0OZd7Prz0TrvP}osQLC$#f)u>CpMpCF_gY|h2zA+Gg7>K(ctJ=D+uzo kxCe+sOiDWK>Gq#VFVdQ&MBb@0L}5ld;kCd literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corners-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-corners-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fd115f3795309df11b9ce8e8425de9226e967f GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)$<>Eakt5%+fb#w;U85x2KZ z0v;}xT<#wTQ{+#WY~;#ODY$xzlY_v<&zx(VUri{JI%w`OUu}Zg(#RWXHO2pbbAR8k z=JY(Lri+IYZ_KnVyTp0vqPZWV{;$7(|Ni{>^W(>l&z_}O$jqBNx72Fxqf!G2AGOK9 zfB$~8|7iM`Y-@5-VU%veO`SS<+uJdQoHlIFy`e)6$MLeh0T%3LO z*{@$!`|qzloVxvXY;5e>>)9LsvYKRnsj#W5th~5h?qJz&*%|16iHQ^&UF2TwQ?MB%q7CEdJ)?9qB)s5k-6mOgYm{Y~EAae0Rb$_7H z+#R;_oM-mFV41s2t>b2&9_Kk-pc&okbaP8WP4ujG#L6_KSxZ^Y@DbQ7=-b;M1N72^ z4WdA&G+&ouG|4upXcPf)UMn`F^|H;k0Sc8o`c}c0YPLwYWY#Q&H0xa|7TbYl=`jaoAD&U$nlJui78W&22lIqFtF>gSwK!@4FYl2wKL2zm8$s!66(DD z*mBCv4!;YTSxqzEzL@mjP7%kfy)SNEatn6_`W)y~pxF;y*`H5yzL4XtY|aAqfB8nR zYuAQ@jQh7w0OZd7Prz0TrvP}osQLC$#f)u>CpMpCF_gY|h2zA+Gg7>K(ctJ=D+uzo kxCe+sOiDWK>Gq#VFVdQ&MBb@0L}5ld;kCd literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-negative-dimensions-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-negative-dimensions-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..fe97057fab6ee863c6eec5ab79ce47310063beac GIT binary patch literal 1010 zcmV9ZC?gC@10&uyD|!xQcKT=~xg7;u4fD1?eq!4nmjP z1WUn9>m<54xd^%!Czp=iQ+w7+wK2KjdXO)V5PrUI^6~L|-xuh9NP)KSF=SyGk%eVM z7M2lNSVm-F8Igr$ME@by-CgJ8C{k^;G~0^uNRf(=E{ar*f3=5m|2yA4ZK+bo*_U;&ZBY7~mOT zwz!TL6MEQ*o= z?%!DThsRSRXu^~gJT=eJ(a}1F7KKp$-%FldEDD}lW_y@gdQ7Ts>d^SNENlPHc9 zCA+t`hg~mTcGpFUCZlztIK5DZ0$UT^x#mqw$ZwJp?aHqXz`amV1rpr}_jzGoOl6vcEpz0KR* zhSgTp49o5i)PP7b75O8n+wFEHGj}JG$#S`j`Nre%^Yim;Hp5qZk1M#2e?o@RQ&bMQ?32lRXEXEJFjsKMuQU0q!r z931rfeeL_-6`t?&`E+)oOi`wy2WlPF^X68oswi%^+uUkZt5u!V`c^Lc9S)e(^F)JRUEVN;orz!{IWwaOVvMgL=JQC={a64JX7(o3WO{ z-`dyL*Jo#E;c$3=f8TDmd_y7O^ literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/ellipse-radius-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/ellipse-radius-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fd115f3795309df11b9ce8e8425de9226e967f GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)$<>Eakt5%+fb#w;U85x2KZ z0v;}xT<#wTQ{+#WY~;#ODY$xzlY_v<&zx(VUri{JI%w`OUu}Zg(#RWXHO2pbbAR8k z=JY(Lri+IYZ_KnVyTp0vqPZWV{;$7(|Ni{>^W(>l&z_}O$jqBNx72Fxqf!G2AGOK9 zfB$~8|7iM`Y-@5-VU%veO`SS<+uJdQoHlIFy`e)6$MLeh0T%3LO z*{@$!`|qzloVxvXY;5e>>)9LsvYKRnsj#W5th~5h?qJz&*%|16iHQ^&UF2TwQ?MB%q7CEdJ)?9qB)s5k-6mOgYm{Y~EAae0Rb$_7H z+#R;_oM-mFV41s2t>b2&9_Kk-pc&okbaP8WP4ujG#L6_KSxZ^Y@DbQ7=-b;M1N72^ z4WdA&G+&ouG|4upXcPf)UMn`F^|H;k0Sc8o`c}c0YPLwYWY#Q&H0xa|7TbYl=`jaoAD&U$nlJui78W&22lIqFtF>gSwK!@4FYl2wKL2zm8$s!66(DD z*mBCv4!;YTSxqzEzL@mjP7%kfy)SNEatn6_`W)y~pxF;y*`H5yzL4XtY|aAqfB8nR zYuAQ@jQh7w0OZd7Prz0TrvP}osQLC$#f)u>CpMpCF_gY|h2zA+Gg7>K(ctJ=D+uzo kxCe+sOiDWK>Gq#VFVdQ&MBb@0L}5ld;kCd literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-center-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..f8b37838e356be6fa94d44fdf11672668d0d15cb GIT binary patch literal 704 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)e*>Eakt5%>0*r-!rHl_QU2Guz{wsC4x8Lg}Zk<`(o_ob6=J~t! z&f#QI^_Sq<{<>A~FZw@S{MWez$)EKcBB#!^n6z#(R`gx?#>XRA!&>+fcA+)Z3(xMU zy5V;AaP%*^zajP?Gxq;Kd?78)HfQm(hgRkGKG?NkF?^7lRP}W6f5zwf$MYA|a3um$ OCWEJ|pUXO@geCyJRZl4Z literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-corner-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-corner-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..f8b37838e356be6fa94d44fdf11672668d0d15cb GIT binary patch literal 704 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)e*>Eakt5%>0*r-!rHl_QU2Guz{wsC4x8Lg}Zk<`(o_ob6=J~t! z&f#QI^_Sq<{<>A~FZw@S{MWez$)EKcBB#!^n6z#(R`gx?#>XRA!&>+fcA+)Z3(xMU zy5V;AaP%*^zajP?Gxq;Kd?78)HfQm(hgRkGKG?NkF?^7lRP}W6f5zwf$MYA|a3um$ OCWEJ|pUXO@geCyJRZl4Z literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-corners-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-corners-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..f8b37838e356be6fa94d44fdf11672668d0d15cb GIT binary patch literal 704 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j@z`)e*>Eakt5%>0*r-!rHl_QU2Guz{wsC4x8Lg}Zk<`(o_ob6=J~t! z&f#QI^_Sq<{<>A~FZw@S{MWez$)EKcBB#!^n6z#(R`gx?#>XRA!&>+fcA+)Z3(xMU zy5V;AaP%*^zajP?Gxq;Kd?78)HfQm(hgRkGKG?NkF?^7lRP}W6f5zwf$MYA|a3um$ OCWEJ|pUXO@geCyJRZl4Z literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/shape-modes/rect-negative-dimensions-linux.png b/core/test/processing/visual/__screenshots__/shape-modes/rect-negative-dimensions-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..18a0a4a4676a10219588a419f764bbafddc926ab GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~A6`n4RAr*0N&pUE881S%Oy!Sul zp4Umet=!gkRSxmgPj31k6Mp*G?}hH0JP^=6v3Hl6o86-G_wsk4bU z%vV@hzEFSv#{ECOnMA69;gbBYP@rh-Bv%B?I#tC`^pv4R@t{cw&@z`)e*>Eakt5%>0*r-!rHl_QU2Guz{wsC4x8Lg}Zk<`(o_ob6=J~t! z&f#QI^_Sq<{<>A~FZw@S{MWez$)EKcBB#!^n6z#(R`gx?#>XRA!&>+fcA+)Z3(xMU zy5V;AaP%*^zajP?Gxq;Kd?78)HfQm(hgRkGKG?NkF?^7lRP}W6f5zwf$MYA|a3um$ OCWEJ|pUXO@geCyJRZl4Z literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-bottom-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..d7d33e10a1b0a3362787e6cb969e258a8bab32d5 GIT binary patch literal 1261 zcmb7EdobGv9RGQaU*Ryi<04w zxrpgV_uOPvJx6j`Rv)!`Az?z6w8O~@=@rb}6o2ni^q{xy(S?=sbuXUNr^*ev9Ezdo zJNb(y-R^<#{?3)T+4Te3T8>ypVtjIO%$V_YdiyXqG5VV3Jb;qS9el~OuK(&n#HH#q z-ZQNZ?bo+JC0A1%B&b;JJlZ4TI#b}#CGxOnhI=QP`FUg8&9kw>cq@%Cg$HtRG88iA!xc{ zM6cI(cl(!7a&mq%nbJ7S8k4EKMcvraGSJaM5@u9TA~C$lRXT&=dY8#$3Iqa~Om=j1 zG&nf8WKP3mWo6x_(k7Pq#B%LA$xs9BXmc@y_OJZl|h7aJR^kFOb?n23ps z`)OlC;I3O-1l9KT_A|4`BcSY;23yIi!^4}8i-OprqoX9LRGKbJsZltBciR%G;G|wp zX96!|pXK6%L_T7%#y5_gvDFKW=q|84gjig>H#5@^a;LCxr+gt_irVbSyocHv-k)>B z;c$=ZT|jJdG7JRAaOV(+r(*GuT>E#E{)~goO#)$2r#lHdenTT{z-FM#{*!=gx}u<@ zq!$A1$rI)3t#t`zI<)gN*LU|N`l@N9v-47mIv_C6OFnny)O~}2x&i_kVKkzyljxpV zSeP(Be;EdG8&k>Og?*qnvdGEJ{k#+4;-<0gHx!zctrXGEZ1&q^Gr-S#8zgVIf#0sK zuJ+%uf>ua;yAm8_#9w!eo5cgA+!HWgF3yuT^2&r#=!iI6U`UA1jc-1sM27R2rjhCC zX%@@HL=8$!P2D!P)Cg_P*4Y`{qOAM>hX^A>dF$tON~LmdZ*OgFjmP7uRM*IFK{uI$ z;Y6uYyFKeClFJ!XDwRxbh05LCvwh}JxdBw?7^(UBsUqykIUpPB0dJn!E-x#i1+n$c zvnc>1&YqEHc)3cYG8&DpwaVp!u^qIjkc+ENb`PI?-@)c;a4?(0f%Y~wHp&MFCz7Dk z$GzXo3yA#KXzl2u)oR0U|H-#dBW&SS`No>O+V|QYD4VCI&{jHc2*vnp(qsvP5rRZY zghF&833WUAb@VXMwoS9~ qM}JdxbRRK}>|}|Je&;ofa|bY7chdYwn>ho$F(4)~E<%h-|LiX*Q(Q&> literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-center-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..91f425290369035c574ff360d667f02d4d87c524 GIT binary patch literal 1260 zcmeAS@N?(olHy`uVBq!ia0vp^(||aIg9%8!wv1cOz`(NE)5S5QBJSG^W{iFfB-JDsmM zT_I_F@6eCAXOibVo)>R?{>+{GpJ!+9vRnOlPVxQNTKjW%&erNaJI?MrNB$RQo79>K zHl(7B+FvSc_Rq)==zQsH(sV<#Yu3NSMF*c2fp`_7S+kxcDJced#k4NiG>Io8Y)t^` zg*6cixfVojbP{#goYbn-kam+9|P`u+R+ckkHDJ0@FMTi?EX+1S`v`B?qBKT9@mF3!ze zyJ%69DAValoS`#j&8jLbRb|V}%nS_;RaRDRZf=&8lw@aTZ{*F*&F$^&y?ps{X=$mf ztn8<)j%&iSwOOXA3cD?oU@wcZ3D6_n;s4u2lYpkL z|M2FGjzQ-3+dwZKO-M{Um|`SauPDOiznn+=2+)|g#Douunm`BZX=+aV{$G8KK=d@9 zEnIu!^2^G;UAh#M@DCUcuXp8daLS1^a$5K!;m=Rcu&`_HDy>aZr%r8ZVk#8t?&?~# zdiDNyUwFehbhYdL)crqt^yuBYbL$@dXP6awNy|qAN6>4BFq&wGtzgOsV-bF&E}n=7lUqxUlaUQVqKqxad(U%zQnQB6(E zJQwD(U2RUx(fbxJUaYC9*|?ka#+^G;%oTuEIwa;4oHjSQKwMI5+g>9sE`Iy=?b)+u zx3{&)$;o{=&4BRd`em74zkV$)E`I&`H7_sk+O=zc$pWP(hGc}CF3-+Bon!Vooe}8f zl@}&Vd;j_K=DT^~^H=<0Ww@{=L%V0~)9R9vDf8#&8_moyi?%s`Wj{N}w}xA4{?`3} z{P^+7lb-qc`I(t7U%y_xrwbes3(qd=(7ms4D7C7pD#QPN!GcKDg@-=q#s_Y*XlmSe zWAeA-g))VA*aHWr=bED)^KR_^cv{KfBax=Z(qLq z?k`q}3u`8YB|0%ea~C3t5!vwQ>VV5z;(b(veqB$Rv0we6NR^G8xG*p0_tTj^O$WG6 e9wrfe)_Wo=<`suHeEzKT2(n&fUF$-6`j8~J9;cGWL5+ShGY4_cC441E7-MVq(!~YdUMM*|8 z7ya4&`}gm;bLVDfXWwGwS#;}1D2JB!vJD#wDk~%B&73i#qP~85`k|!!{PPJ0De39A zbIjgKv1Np*h(@iw_WXIep@g*b?mc^CBqd+IeS7!r-LLr{Dr_!az6@jr2L~tp`F+Cw z)~#D%VPU6Eor+N0pfl5<_2kKu;o;#)6DCaxii@)|F}ZU8{`_gv)bjfG@t!?p!JnsE>k5JsEGqXT+*WGt#&YWp&WxfCB>({T>3-2ei-q@7V;#8RY=lSHGo}T^y zE>>@E@5;)`M^QQ&8Y@<;_+fR~9H@ECi;q=%#l^+T%gb-I{%6hz<6Ld9t|9FxS17~T zF40vCvqZH+7)`XpRxo9RtqEYguqI+5*MdmFfE(AZmlqT~`2XT#g_gE`D zZ@zr*-n|<)GPXw5<{!T3uvuw=!3Wl86W#llF9$atG_bSF+kEp(TCux(dl@?eC=T3i ze)_t3-#$IRb#qyP7A`q7V@r*_-GAm)t5$8<%&;KxVwi;Q>FX~`UVZ)b_HFK4CZMJ# zqHfndi3S0EQ+rAjsAtci18Ko0rmesI)=aARQRAxE2F9~mjG}D$KkaO6X80_-crmb2 zh}T40iYxiXCs{c;KQFIObI%@s_H5Z6Mxf7_xRjft_bqIkHZ{Rvb5g4o4!^UA9NQCb zVQ+u`(xpomE*KaXeE9KWMJyZ8{)R&bwi{pk!u__a+Sm6iFcx0D%F4~n4Gx|R_2aQs z@jJF|U3&fXR*;V-hDc1VdwBYE_t7N5@({3(wg7#kZ)0O~=Iq(4FH6!kTh2;oHIhBC0n!iQ)?v{0c+2aJ?=2?XMEakt5%>14w?;{!#PPyO zK6@2aBWD(6dU@h7X0wVS6PC72o$9f3$rrUv);sl5?ilGM zAJaXv=l`7RdydaEFSdOCzkJ@u-)sDy&zZUGdA859NDmH%Le5vh4!lB>+FPC|Dpq=U z$PvL5uXgG`Ud(ebK{%K{kbk3k%KDe)E{C5Mt!!VxzLrzfv&hjR$LpByiI=HICD(0s zauVEej*HXvft9w3NaO1@69QOB5xg{O!{*J!_4W1@pWeP*yJ^#>XV2UUldsg+#ryl8 zfA?+?>kC%^$vexw7Wc z!vY>2o(;`6Z$?f&ney|15ZnE^ULF#b`#7IAHz++kbjT?&ap9+b|D6t<==6K>waVDY z=-KnX5my`?(Fof`{VW9U%q^C*wJ$4(xsrhJiTgtF%glFkdPXi`|3c$ z)^a9SN7n8C|L4!2Z{Nad75y@2hDO|<{9*Y+d~vcx?MiL@j;&j--n)12#*G&xR!`Rl zJ#JVi+*W73I5obas3>V`)bV4-ZruM`wtMg5#m=?tzDI}%w#4%I8E z@e&pPIajP&6*PZoXO@%hZ>R0IbIoR(o10%1&rq6Uu&q|HJ1QthNxh%HMYt_b>(D#y zy>aW$rX|a-c*Q;CXy~E!-&^hGuV1q!Cnd$D(uhyOc46P|Lkkx!JaWY4=gpMYuV0^% z?l3AjbR$meTdUQtc`f$5*CQ1da>S&!wkQ?M6&30_P#Wf;!GtV0^;*lW^)=6*J&THp zl9H18_51hx_wTPx&jcECLI1{^c-eLgcmfVeykD-Z6;`@NIg Vn;gx516VFGc)I$ztaD0e0suZsOM(CZ literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc6d88916113655d63ea0c9dc538a019fcf5596 GIT binary patch literal 1215 zcmeAS@N?(olHy`uVBq!ia0vp^(||aIg9%8!wv1cOz`)Yx>Eakt5%>14w?;{!#PPyO zK6@2aBWD(6dU@h7X0wVS6PC72otkkJ3sVD*T)$ot-{S zm|#N$Q~z5_-1_eviw>$SlUpR0ar3kwU2i;vgN>zQ@`{ri_M zZ{EBa7_V^p0_W#XpQcTlHs$&8NMhP5&hoc|^!+?z#4M z_A&!2tE##>I~$ujw{P#>x>a;fqwUJz;NaNU*vXS8zxi=r`~KFgTXpsIr%#)vl-Tmc zf5wz4C543t`6f-CeERfhc6RpPzkUS;2bY(ZKb4*}d-m&Buhi7kR;^wg9UVP&J@2NQ zIUW*`*!V0nwg)UFS`C8H>dJ(^M@x-cGg^a zSir->v!VIs&B)0oQ+^&0Vw>-$s?sBKzva`x*-8%&9db%cT=?nVf2TtyI{jXJ{%K@n z^z8Zbr=O=!pDw@tHw&loa%ZP+-5;;-{_^F^_5#kCM~@!8b0_ATj+lr@NJvP{pFRE) z42q(JkA2(5)OY|GTxZYr{&Jdg$!PV4ed-VW590`PL_{n#UHDG>{@Jr1yA9{5W~_RVy>IXGJ^TlvTUCxBuds)uDQ2$0y#X*x0QRIzM1v=}5>h^y6`ulHcYO1ob@~O#%2VQWN#{A^t? zZQHhGdm1&I)qCFNCO%!g_`TQ;57sSSrGLT2G%-i}{@=fU=gg4-M(6h1TWgj{aXoA?TvNl`cmOE=@L?h_=>!H| z{QPe!72SWZ8I}7KYskrwOIX1 z;Ztj4X9#>T{)SqDr3o87}Ir|N;dT)Xj8s(jt@<;&&z-D?|-bT@Oot`_=df|7|4 z5em+3Q?Ip5QjvAEX#1CKC}4S5^-I;>ypFbq|B9p}ScE3Ew>(i)tn~1ZBZB#zJ+@GC VZ|IV{M}Y+sgQu&X%Q~loCIIY^PLu!u literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-left-top-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..40d07d7ca3056d11fa1a5023b8c7f5e2ad118447 GIT binary patch literal 1233 zcmeAS@N?(olHy`uVBq!ia0vp^(||aIg9%8!wv1cOz`!!Y)5S5QBJS;7OAqa6nd2X| zmDuF8nmVR3in9o+w1%oExUp=!;C01sf`(GZ)CE&}6O%AR`F-b8OkCYe8t1MRMpM2(w9TIYK@7}(>d-EpeT>%c3Wy_ZB+Pzy{UH#_Q z`u;x`Zr$3oXHQI2R98#jCHusrq(@Jms_ypi^3u}QmX?uu^ZK>7r)Q#k|0P~uU*D}; zw+agjhlYmg>gs+v-WIVoOr=NYc*`55DGS%F>)We&^XS8eiEeJkPM_x9y>H*Xw3+YU zy-Vr;CobgHpMU7`<>1VV4-0sBcs6w3ycs$9WXOk3Ri3@pPbU~u)d+iuzu(lt&CT7~ z+WP7Df1nQk5~uz5=g*sGXK$~+Ha<4C*6tm@hlH)H&?f$4`|Xx2UAouZ>x7P;-m@o9 zHhfR2tE;Q5{Q3OzUq;Tw%S{*Vnl8%2cK-bN{QUf>(uH29*C_p(*J96mJyLNYM@)Ka zi&DW{QK7B_rC}Z#Ovr*$ueHR+#{T~Od*M+Bg@C}o!s24(-G*kjZ{N<%&E@6g^;;hN z%hZNX!Yc6xe@;|ijlZ9tj@aoNH)foEy6N^?AUI{6p)@Vv@x=TSw{Ar_IyM$fRZHS5 zwft23|A%-$WaP@2rJY$$y6*ep)?d#t%g@c#G|y0)6L5E;es@$*kdkUYe~WONyqT-b zr?0>NhOM4jyHLwqqkDBry!_95_wV1%**0;a;Lju(gDNN4{+jl-HX$LQCFzrMa&k`O zE1bH(d3CRj%+FUZs)hcU5b))$`Qqo!o;5W$zkdDt_wV2BZEQ5QXBw3p`VgPKWsdl+ zyYF7TdiC<<%c)bR&YCsr*fF=KvL=GwE3Q{QeD-YF&781xmt1rf>OF4-1~FfIbL}E6 z^NdBa0`68mJb2KttZduK6kupX?F;5@EpQTj-g+?M!^e*wU%XhceEISfD_(s48oJM0 z1j!WKqE@o|?tVof=l@$JI_5nT`c-4cf8y(KRX6A9TK dSVa70U-CyP?6*=xF|ep&@O1TaS?83{1OUN5L(c#J literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-bottom-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..f55c6d27cfcfc04698e0c8f79b29e514dc9956b2 GIT binary patch literal 1217 zcmeAS@N?(olHy`uVBq!ia0vp^(||aIg9%8!wv1cOz`)Ys>Eakt5%+fPZtv_!k@oc+ z5)vUSg4)cBIFvlNR(B}4v248Hb;WOj#-sQJ3K1GNJPx=o3~O33b*e_w`80u}dOuvA z91}n0?o-2JI&-gWa^AXhiTuQTeOj!KX#F(`Rrz z@_kWa6{{e0DCm=#N>${4LyuU+g{pp#W{`}XkDpyxlAuXr* z20$a9J#*8!bm`KnRjb_G+}POJjg5@>`1!XA-@0{6N?LmP^5w5zy|S{hI<%fQack7X z06{j+qfVz4GP1ISW(Vo``S}$U75%B(A94Tg-Oe-p{r!pI|9OR09kzIQ^k{3@g}m+j z{QTWV4;^YcnWFX2NrmU_xq=A+vd1{Lx*F&_{Pd~F)6?_E&;J}ktCVlG#H}~Cwyw_1 z^t7(4uKrzmH(z0)m5bBIV?U0oM@B{-^x$M&y?Qk(D{GRzn5bxIczFHYm*x`;ZgR3P zg@=a&?YTeyg5xVL&QJ$@gevEJ@ptdum6eum-Ll0(hHw9Uf1q;JU-{{eW{S_7KR;S$ z+R2liAM5v?O?&DebiT8>yOtP2t>jC=95YHir+9}ZXLChc-^ z^4!AN`@Gd|{`yUuifU?XN?NV}qf*FPUq)8e*VFUjyJd>^DqhAYCca6C`(fV%lw8$- zkMI(*`xF}!larsHpO+^mB~?{br37;RU-6ueJvDRv(rat~-nnz<*fBR}XXor}ZIDA> z2&-j1*}ZpfuGwsu*Oc#HzaGB(uHL;su*($^bCmBte*E~)pPI=hQ#Ri`lCA~yVd4{M z4i;dDoj-T3s=C_S+uPsYALv}LXK#oUK9P3X;^Xgs|IVEy#jP!qGK8i2Yg#2YvTogK zp>Av({3g%}n6^FNoG7oWv(wjKzh;e(O>{(r#x@Kg!%6>8Cf3}{}FFgLZ!@)%5uKw~y6-MH7IHWxIEdFni(8pW_PcdN8 O#Ng@b=d#Wzp$Pz`Mi)H* literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..8eefc1b37997227d95748f6d9f14eff275718d5a GIT binary patch literal 1215 zcmeAS@N?(olHy`uVBq!ia0vp^(||aIg9%8!wv1cOz`)Yx>Eakt5%>1)S?w*=GRHqg zPg*G)G-;~l#7UQQ*0_1Od5NuQnR{u%AJ!?G7UY;bO>CSh6c)Bfvw!yF6M3Izl=Ls} zX*YbFc<9W}zcI(}6z}$%Zp?o_{Jx<68 zZEA0MqeujEqxzSsz4nSyNgLNIt2w>lJbL|$y366WW%aU#f{%P(6z#n8L3U%%CpDE{ zoBkzxY@NU;E>qKOMn*;!7BePJEG#HkV7qYb+O-=u7FJd5TDNZ9;>F6JwkC$H7UdK*^w7}T!s8Si zJb8!a(W4(f7W(=5DF_H$-@bjj(&xj6osX>k-`4Uy((9x^%$;hdbS_uC7oJn6aoa^~in zyLRvP1V*Fhe1mh!_cxq9>-+K95@4(>@t-k4`Tm9D_~VST z&)&Yxy|i{hfRUfX?TPCK6K$TgS#H|ow)j-P%yG(O(m+a_ZF z_tq_=*=G@+n?7B9_g%fZMUy~5<9O&sUrm2s->1)?ebpwPO%vW9408NQi$osxeZUxa z`SRtLFI#|#V9S;-PoA{oX{$`>5q`x#d!OwmUSZwKmoJ-}nZ4Ks^KFu3A$QjGN0u{Z z&U_@d^fnjg(j#&|&z(R2{OQxFQ>S*G7ZVffik?=YtA-@C N!PC{xWt~$(698U2Qds~1 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/multi-line-right-top-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..d600d172b01ed59fdc4023c38dc8c528c2173349 GIT binary patch literal 1233 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 JdCHIc{{y^Rq@w@; literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-center-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6171360efc0f70b5bcb3614cf603739a191d3027 GIT binary patch literal 5910 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-center-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-center-top-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..c3f7f02b402e89ae1b53cf3b31493dd1f0f8b244 GIT binary patch literal 5894 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-left-bottom-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-bottom-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a9c2ba4cdd1018c90177ba73cd8096932a0fc1e1 GIT binary patch literal 5681 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-left-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-left-top-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..008db894f48824b68cef91531b815460e84c1fe2 GIT binary patch literal 5681 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 D??nWC literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-right-center-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-center-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a1b1fb011438b3e7e59b3d87612c8c9ad7030c46 GIT binary patch literal 5632 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/align/single-word-right-top-linux.png b/core/test/processing/visual/__screenshots__/typography/align/single-word-right-top-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6ae69540bc22f1b00c3b4a36d9da74743ca8d51b GIT binary patch literal 5622 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 DlrRjV literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/complex/colored-text-linux.png b/core/test/processing/visual/__screenshots__/typography/complex/colored-text-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..df8be566f8ca649715f5335bd4f8fa4df5392b81 GIT binary patch literal 3866 zcmaJ^cQhP8w-!7f%^b@d)Wl!RDi1rY>^-dh%7wG|}73RYVYL@zOhUG*mAcA0ZA#E zMU65rQUOS$eD&q#Z&SA^%v{%$--pMTkcvVo5-G~a;N{T!De&O%N6YSB{K=umaq3M! z__WVfUPE)T5IM}AkXd}6{q`B)^p;P0@6WWpnW~P?2sGy$qMW~KvETlOn9oMs-r5aMZaEL# z5?#907}gV@&+kY$7YW`wk)y){2s;uHR(-BK9oYZzp6FsENFD_&HL?Bu*rP&+`J-A? z%&vwP=;Pxy%_GSuNUUnuKo!5YSrw%{A;-%;=+H4wAVBT(*qgL9w{^67@{>tCh|NbB zV2iSb!nc)A=i*ONGoUzo&H%JZxoOxHhWfUwJ2wKP1-Ii@q6guqLuutxMg@fvV1{C6 za=;%HXPsN6B&#xnf{IU>K@Jy0gf#fd@BN}Q0H4U9k|wewn^*;mswtgu}m|&wviJD zEn)q*>T9vUckn+kjYlGBvT7~QPsG@G}Cv@Fws-3a<)2%E@WX)XVDl>}_$h+3sYXH+l|NTK&%J zpX$R@bb2@wXpM7*z-nQ-=VInwm{$5MmdD9G-sN)h@j9Q58(M3aIchS)Z>_?O)mjfx z#vd!?N?49f7kk>A+O3z0XP>|Ia}=DxCW{f>m_nF$tHi`gHA=1&xIR2ficlAQF;ROk z6QQ0^31H9CJ2ezT*K}bMVg0j`EuOVi^c;#6)oL3Kj*dbSgx_Nazrxf>U(*AedG{kM zV(tXibEJ)p0@RLh$jbo%OW&Yd2$6vIJL@+*GJ?fpQ$J4C(j4#4EoQqn%woc^>KkB( zprd8=+8UV|6PBnG!Y9O9?+)gEV{G1Z(;>$sN#Yq84QvY*xB4>XyZSrr`(@Lz1V-={ zoeaUaFJ)qw@ew8Zo3U|trjpkZc7isb?Y-ba!^Y&^=+$LW_2JqW;rby^Au=?`9zoDO9DL~2>P(0Rey5-ir+1S0jYM4L+MwR?u=4Lx{@F-LDXJY z9A|XpcN=D(gt_)ZhnkXMjsi`2Mm1M(6IquqEFK^gJj}dFet+O5%87F!At7?Gu}-~@ z^=&1k4#7s(UgnHG%D!Rde7X2Jue9dHt=pdhC_L&}Z$+r=tDfIkwx|U6fd%;HB|BuC zOBjkWCGri7RwrvOH{IP_(H>NxrF z=(^{1u8@w(sZGOos#OCSY{@B`^rp(~&L+X-p}<%B8a8IR;I(%<0Rm2gi}qR&ZsnH@ z-*dLFxtiCk#h;n@iPjGGI`Ox=d^v!L)s!T-4Ce`sx|(7NVh6f&=&hc}Xq`nGYwK{C zPPZxTUFN*ZbA97lm8rg;tV!M7=);N!=;G zZ>$~ky89DrXzN9$r_VjrOPaOj7i!cR93N|@46z}`R+VBpYWRwE6)qDT5+$CyWn`JXh8SsOHM+0 z@qFfBLzn#dr3%btwrrr8*t7rJUldIp3FSdgmh5=w>?4y;_O$BO!cdz^e8li)&XfAW z+B98lTJx+Q!-Li3^S@8nxC43XCC9mi8ff<}*==j}*u~}pG{(B+KAOdmOg`b>6#-~6 z@xo?tOA@t{TS6tUVZGnCDgR!iB-OaFxx~OS^fm6#D&7b5WLWjz82(>1_k#S6R5K4$ zyfUnz87V(xO`f2MIj!*cqlnA0p(`K!|EimrFH5x-nCIJ<} z^pOHtEB9l5y#+Y;JG}#&jf7*X4i6zkJ%c!3x3RkZTFTBn8bcsH$B#`f=9fI+LH5Gq z$1;eP)8vYJ;9v%BPXsE>5b)s@-v0Qgm)RDjM8l`)X@r(}c=>W8RC$l3OTX{y{i`u4 z68O#0=H_h;mTA3!_Q(sEYjxf-R@(kkoef%x#KvT^8L=Uax6-xckD37wYlnb#lyHru zle6Jr6bUTXd|-!v&4d@K3~t)yjTzKaJC8t}n|m(8-(=qW%RHB@6{iy5l?A~1d1`gw z-dp24*Iau?o2>#syf7si9!*bObZ6&D%L%KjNP;giIt1YCb@CO%(^+sR|(=L)XRGK-SG6m*Sb zPFXO{A@9`5ZtS3;Im)68`h6y}hYPDcf3u{FQ zui*_;6C_>ZOtsTj#b}kI_6FPiQQJzUgoJ3oTE&m(}c|ytw z*9+4w2azkSX0)3f)-i?e$f9zznZvbp;+{nCx3YY8E&J0ZnosH?l`Ot=4q8rqd;eGje1z%zJ4Jl26$zxlE&(Y)a-x=rN)w!j_uUcJ3OX~uffVJ z*5@kDWv(!wy=UNQV|(V7rLf0pTnJ{J5xiR&Bt`3`!lOf39PiBITLP0fj7}{u-e-a@ zy9%GaPhDGgeNyaJM|G1THvo_MWFWTjcI-_e=^uz`AdlZxmv;JQZxkgXjXs$u0`7fA zrdVRd$K`VtG*GYX-_Bm5y%Jl9T%)Wmx|;=Cy@O1Aa|Tzo#`f3z()Gz#wUZ(4J_C6$ zM}r^r`&YrJ=0vU0!d+Bjrbkyb(s;X4?f)7$8oBc3@6OY;cHLDvwA@mRUku-6aaF^wLm0^~NdmAzsh+d`FF4?!+8!2q{BXMrj4?-b8nhJ)SAC(%SiK)R z@Ob@tHeUb0CJ2m{3UUi{Op=2Xe5-OXmq_lCU-b2pH#p>$A}!2?L%ioXN_DlJ+pjY~F7e2tCF|}=tUupMKF3wcjpng`rzB0c zWV;R^ZLi-TdVN6rXJAVt&m_X*imCLDcoovDT9ciy@zqdt>QWHGL`Zgu66%|BOH0iF z$aLlA+&HP2>LL<=bHc7ikqD`Vr;%7bUxZ1X&=0g^Tfg%cu p5pnvWL;v(Y{C||_f0_PL%dDo%I0WN_&;Pt+_cZj>Kf-K6{{#ClYFhvR literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/complex/rotated-text-linux.png b/core/test/processing/visual/__screenshots__/typography/complex/rotated-text-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..b90e1aaca7157dabc6fca561eaca85fb95d30d5c GIT binary patch literal 4263 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/complex/transparent-text-linux.png b/core/test/processing/visual/__screenshots__/typography/complex/transparent-text-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a9118ac7c6cd2186d0b29ec1a959e57262eff819 GIT binary patch literal 7043 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/font/monospace-font-linux.png b/core/test/processing/visual/__screenshots__/typography/font/monospace-font-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..2964d7d80aa8c74ee3e9963b65c7256ae5296f78 GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZU^?gN;uum9_jc-8Z>>OyW9lj# z?;dUA>b%tY!l+l_P;8fiM2kk=SN?+JfZ0k~O3kaoS;R91);e!4)p&O&OHel_RM7Fh zi0U+>?>mi4CHfv8c_>x#@K4y|-!{ef($7tA`k^cM;?Vbo<4KJ$P3WwmSX-lbdl?fRd7 z{#obsqQolVX}V3LCNR#5^}Qw~pM(O37^p4<7VTnRNQ8$){3vv2EN-6x;U^%$Dr76 z`R6}>Vhk@?0}UzKsgT%y(TUy9&+pPplWkJ>iY#Qd+_~nxclDVcKP=X)U%!5fLf7Wa znpxEECv6fm63+PY}+roPbNyalsoOSk^` zF6F#t-#)vT4X1Y9xaO@b#wWE#ZE|K|p`qeJ9z%0;_Nz%7mjtc+`}c2+T|d`4!y6s$ z>#zU*`_D%$SVQF7w{JFb{BfMEPDdOTRAnALWd5+PZR_^!;S+Z3+ZQG^>Ex3G2O4xZ zFS*)VT1M(k?^G1vcpxCa{7h4Z&)(i%`&dQ$JJ;7e2^N18#k!ArE&cT2gTv`X9dCj}UF&fw|l=d#Wzp$Py8oqomu literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/font/system-font-linux.png b/core/test/processing/visual/__screenshots__/typography/font/system-font-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..82e2a82708551f1e5bddd497e93ecde5c250af9c GIT binary patch literal 1401 zcmc&!`BTyf6#t5t_fezfrV^B*30PjKiJOvHv}LSXMy^+)PNGguyH2&lkY|_c6*##9 zDr=L8u6T=LVkMNB5{jD&h_2<4#b_#vl>6Og|AYNue|YbGet7TAJ3jB-B7_|?H`!$Z z0Dw9Ect|*CN&kou1k_tJh&})q1mQ!1Bd^3Rm%l+#$=0H{RlDb0Gv{Z8Pi<@SSk$6P?BTT1alWp2@65!adIb&xnERva9UX$&@YDY==^?*cMZ{)^(v_ zEkn7H_%Y>hQ7GJ+qcVN^JA!C*+!j)rMV#6*2t)+@$vD~r%nladele6KwNkB%Rc=6)>va(gJ){RN>_VKxT?b?X~y`4MDg~H&>Z8p>O z5(Jq-(fMLRLqpMV6biN;P(QYDQKe{D2jY{H8Fadjw>Q)>?8+6t&B(~e(ap{7Kl^cb zyz%JJ&=AVaZGB^7zq>oXyW7#h;oSC~7LPf-$e;rBb`%Q5Sd)rOGAd~_V7@=y=iou4 zOttUM-MggSXbi??sl2@W!Gi~cmqSA~cY-co_Ok!5yiAuF#ORF%A8tM$9ZmH0@F*b< zy?oiksIRZr9OyqkaQn53MQ3ksehri9h^D;eSD|Q!=8xH8lem*g<#5~dLJNRLi=y2|ZganCHy1J@) zCpIJ)28s`1u_Vm7i7nmTkrBc{;0|9?mzxVc3&r7ZTCG;0P&h@+l^9p;x>nrA;~}W& zSAQG)Yk4_6BqXRV!nKNrT9lC4%u^37)4-{9h*(n$@c}bbHu(()CwNlUv3kzW| z*!x+t-MiVfwayE6N07AC`C!@Gw@9+TzkepixsM^Jt`?4um&b%~nh+j1=ZIHcxpO%%?t|7<9!psWt^6;Tw^nRHc$v!L^o6~=o6Hb&! zBxrJSGEylmEiLWeru+Ok!3!Z%W#&X6Dhm@ha9dMT(>LmsmF9EMAZS`dHOP4P22&Hr zLs~eI$z-BY#g&ymL`O%*#(wMV?PLLVaH{~;H$1!_i^aOS8pWooQCvST$BbH`fJNsx zI64l#c!6|rQ33bZ>|vIVUzu`h>gE$OCP>9PH1I03>BH??H)?A3EC6Xja%yUiKv3(2 zL?YFxlL`eR3&!y%s?~2h-CsV!WxS(dM{|)L4 Y+y_c_W$#Jp0ADJAKN=R&h>Oqu8@bJkL;wH) literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/leading/different-values-linux.png b/core/test/processing/visual/__screenshots__/typography/leading/different-values-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e7260fcc7a5813c62ff285ff5d230cc5c2ad646c GIT binary patch literal 5905 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 literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/pfont/ascent-descent-linux.png b/core/test/processing/visual/__screenshots__/typography/pfont/ascent-descent-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..85dab54b370603c1bd2d11293a609713ad6d0938 GIT binary patch literal 350 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGGg9%9bJb4krz`&^L>Eakt5%>1aLO})v0hW#V z&wlF*+RQ3FImhY!g9JX7XSNOUEv`-xDr6H$=ibdU{B3P6#+f)#fMjBZ?eBee41XJN yaCEn9@PHCWe&igRw0G%2{zFQNIzluj3K^}#Z*>}^_s!GZGi%LSJI2UBi;qgrj5^vdW8mxs!ie@N zkjHM9QGj}i4ovNNknMKPt7_9R?jFN`97QutV~jS6L~Sbic*IJDo-$)ioVe1B#WnlK zv4t{nA5CFy?{5eq+$`A~`NCNw@5_n~21Vo#t;}yXE&47jNzU%pWl;8eY>podt5D{5 z7Z%UADAN>1me}Pt?Q)`hi*M-lexv@6Aha2n>t#yXRJ)8GZO__7?bW4L;U9X<)rX$! zmA9Vu`Lj81&(vI2m{&Os8mgWjQ1T-hx4-Wll3uj^BHRQjvJs0RyDbt{we=FnVq>hX z8dK=ev@>qSd#!FUe8tLp3g6*h*dEk+`m=QS(f&Z5qHpW@krl}%Z=%#}IA1m5=lJI; z=i!UJekq&4RMm?=8tmSW5A$ZlpO=i_Y?_8uFXvhW?_N;Z zmsvMdnwD9H_IcLLlTRTkr|X~MBpXXSrYnyEfdpnt7OIiD~zC+b%D@U&<1KcN!>t@5Qya zSCRb^b@4okm4UkpQ9MZ6j2t<4mhP=?;Qa*KMn6}CyvGz?8s7atM9yuzy*HVKwB3zb zf1iN^5vyItR_TBtv~5-PzhmG;?zS8Q2&nU%t%cZaQSy9;Zma(Nv5-M{W(d`Ux7Ju! zTB3|b8tCE#c+jP>ZP#y;JOv4jo46(q5Yj0o{k{o3>Dui_c1;X-tzS~vlHYw?gXA1; zlG+X@t!lB?|FBL1lfboI1oPRJS=Ja}l&D?bb_Kdl;RUN4ifJ|{%iuZBdJ^b%<{I{+ z_`_Yer#%f7macOxzWl~mYg1y~AcMR2A&B6Q763KZxRX982YR`MqrTGI;)zZ?5n7D6 zY+3PNCzbOjS=P9{w>HR@d8=ofIftHTe&y>04=6P)>&vkdm}A!wdJAT{^cijU7hX%+ zV~3dkIIVmQ6O?4)f|-k`O}B&uc3UxMQvi?~VCa^+LHX!dI$`;RAkgt*;9{89Ct+8#=4ukF>8HmDFKxmO zCvYG>gc_U2XaRK-f(f;5(dqL^gy67~AG$}p!N{fkN7#`^)`h9j&nhapl4*8-e;+Gt z#UXEyBTIDc&hQ(QBSq_<9q;lZSWJaO?|LDqni556wPi&HU-HJl&QA}0S`NPJC1PN? zm_)WNTZ2`dB!GU=%%G`Jc1i0$o8@h>HW@V694+Y~_%fZTR=O5)YCKp0Q4ygvG^)_bA3A?)l(X6xOzTgv*)Y%vTXD^Wvvy}gT6j##S%*0428_0c<}vv;z|Yt;4}57^ajWq8duZo4A{ z2}%be8vAJ@^D>To_%!2S1(T$Im4+n%QWR|VV&bG)6{4pnkcbRE# zf##j&%4)O4vhgMT^`)+3MJamj8{e@5oWHM0kAiib2v&Km;1qC3MS@h@Uzx`$nv%{# zd7Oo8_Px=RTs_&$Q=V^_z=fE=uGl+BqCwwUJm)_w)BMS?4~b{-*piZH!ha53u}*rm z%=XOM;$V9Sf()$3^BMQ@26hgv$BBGkAqsI&Dh2p5)oVh6Y^m(Xg($I|;X@oQ7a3$h6RIQ1+Tr zIDVH^mYklA7w&m<8+wqujiTWXeIWX@(?Kfe;)If|Nw|0Oqe-c$F{|cKDQ!PQhFe7m zpNRO^E#0;y*rKdb$jn|kt2akS_emY2cEniGlXNUVLb~7YKlWQX8uPtcWv+7-Qae$n z$%}N^c(3>jx?Me1|Mzzf|C7j;3F{z~BSe1Qk2RydN~53%`oj6cE3z65ukg}CaeE@} zt`?$!TMR;_v*dun$m$^>V^xq>2cueRbMJrYpwyjd9{|$E;6OG(cg@u&!ccacY)O4c zo;#c;-_z3Y8B#BX%>D%rtDJq^)Vt{;PrK-BWUn9@pu@M$QQoxE*skQ3@@PP1y;!(Y zT@4P2*8%is0Ni|Wx;d}j7_hAiM?Pz~UMt1be`!G%_fwWO5dBb08@iS^5B^Rc_lE}! zv2C8JWmWGQ`git7Hkv)A==nyiK3GHxXp{Kf6Kyp$HKXRnjOC~FOL@Jv<_G9tYG!?m z(!`-7m4ye>Av=1Drb4Nk>e`KwtXNw~FyE}^Kn=+@t}FeGOJbtp$@-t)EB$DEYUE33 zKekYbEK`MF2wF)HxLEsGNFKfyx4m%M-IeOq@tA^HUOic%YRm+pVB`-NR@p#@=*;3f+eQx?d|oR zd4zD;Kgm<@DgTNRIP|C5uw?qcfz^@oC{zf1j)J+7aJ=&6LXcSA9s~4kf*TxA{BCU_ ze)Mi;*1b|sr%L9@FD9iN@{}K-djrH58B%0ohiklFykloHGg(4mG3;P{G(EeAKcd-u zYQ`w*@U?^A&Rll3jN|9Lc0r?t0Rc$mLt@QJcwq;muOR!OXO3qQ8@sgk(2YSm#mrbgwr39w~g+^z;d$xd2s$2yWHr%ne)(-3pO$8R}OfB!=DrI8U;zD z5g}CqcK`k|IBu*N=`=+d$c_Y`Bfq?83&$w=4>-oLOC-lmPs-5hEr??S<{uy3q;#QZ zOWh{EFs6>s?a$;-$A%0?r-MH;5bkLF*WsmC-0;@*<^4-KRuOGIk&B~Q?|X_+z?P*X z{^b!s+Ws!s%8;t8trf8K&3SzGIxh@Au0_vh6RDD{XcU-5bR%L$BvI zz(eV>Yb&Irc3)=+ND2oeZf8Ds%k)N7@ZrzTZC-YRy?E>34Q!%2F%=x@bK{>qQR$Eo z$>EAfehh4#EE9?6lK0|)wMbN2R5?BQ%yc3Bm^p)SRxv*eFtpG3a?_Hu19lgn0bAy* z$&BN{pF=jYq~XGE?&Qe2RyuUi2eWhzh!flvTcL54-ixg)EvT0$Y4$sJ8p`{*K2e)`e6Tc$PRwq( zZOe@CAD`qm)RVg{972+xZ|Fxd5Kb2fC4iGnJ=EGrC)cTk9Bm2Ce$3Dx6Ja?rWidx{ zC_-<^ifCEXdT=%wuUcs@;_tHNErcE0)IL4?CB~xEVAp}F4^MT`!>Ye7UWZe$v9sUe zltCv$?p*m&Dfav2+)%0qhx%ISW iSD%~5-u?d?7dH>QogiB!P7i@U+7%s50~k)-4)s3)aN98e literal 0 HcmV?d00001 diff --git a/core/test/processing/visual/__screenshots__/typography/size/sizes-comparison-linux.png b/core/test/processing/visual/__screenshots__/typography/size/sizes-comparison-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..177befc9312d91f7376b8f1bc983313f97f01730 GIT binary patch literal 8814 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 clusters; + + public ComparisonDetails(int totalDiffPixels, int significantDiffPixels, List clusters) { + this.totalDiffPixels = totalDiffPixels; + this.significantDiffPixels = significantDiffPixels; + this.clusters = clusters; + } + + public void printDetails() { + System.out.println(" Total diff pixels: " + totalDiffPixels); + System.out.println(" Significant diff pixels: " + significantDiffPixels); + System.out.println(" Clusters found: " + clusters.size()); + + long lineShiftClusters = clusters.stream().filter(c -> c.isLineShift).count(); + if (lineShiftClusters > 0) { + System.out.println(" Line shift clusters (ignored): " + lineShiftClusters); + } + + // Print cluster details + for (int i = 0; i < clusters.size(); i++) { + ClusterInfo cluster = clusters.get(i); + System.out.println(" Cluster " + (i+1) + ": size=" + cluster.size + + ", lineShift=" + cluster.isLineShift); + } + } +} + +// Individual cluster information +class ClusterInfo { + public int size; + public List pixels; + public boolean isLineShift; + + public ClusterInfo(int size, List pixels, boolean isLineShift) { + this.size = size; + this.pixels = pixels; + this.isLineShift = isLineShift; + } +} + +// Simple 2D point +class Point2D { + public int x, y; + + public Point2D(int x, int y) { + this.x = x; + this.y = y; + } +} + +// Interface for pixel matching algorithms +interface PixelMatchingAlgorithm { + ComparisonResult compare(PImage baseline, PImage actual, double threshold); +} + +// Your sophisticated pixel matching algorithm +public class ImageComparator implements PixelMatchingAlgorithm { + + // Algorithm constants + private static final int MAX_SIDE = 400; + private static final int BG_COLOR = 0xFFFFFFFF; // White background + private static final int MIN_CLUSTER_SIZE = 4; + private static final int MAX_TOTAL_DIFF_PIXELS = 40; + private static final double DEFAULT_THRESHOLD = 0.5; + private static final double ALPHA = 0.1; + + private PApplet p; // Reference to PApplet for PImage creation + + public ImageComparator(PApplet p) { + this.p = p; + } + + @Override + public ComparisonResult compare(PImage baseline, PImage actual, double threshold) { + if (baseline == null || actual == null) { + return new ComparisonResult(false, 1.0); + } + + try { + return performComparison(baseline, actual, threshold); + } catch (Exception e) { + System.err.println("Comparison failed: " + e.getMessage()); + return new ComparisonResult(false, 1.0); + } + } + + private ComparisonResult performComparison(PImage baseline, PImage actual, double threshold) { + // Calculate scaling + double scale = Math.min( + (double) MAX_SIDE / baseline.width, + (double) MAX_SIDE / baseline.height + ); + + double ratio = (double) baseline.width / baseline.height; + boolean narrow = ratio != 1.0; + if (narrow) { + scale *= 2; + } + + // Resize images + PImage scaledActual = resizeImage(actual, scale); + PImage scaledBaseline = resizeImage(baseline, scale); + + // Ensure both images have the same dimensions + int width = scaledBaseline.width; + int height = scaledBaseline.height; + + // Create canvases with background color + PImage actualCanvas = createCanvasWithBackground(scaledActual, width, height); + PImage baselineCanvas = createCanvasWithBackground(scaledBaseline, width, height); + + // Create diff output canvas + PImage diffCanvas = p.createImage(width, height, PImage.RGB); + + // Run pixelmatch equivalent + int diffCount = pixelmatch(actualCanvas, baselineCanvas, diffCanvas, width, height, DEFAULT_THRESHOLD); + + // If no differences, return early + if (diffCount == 0) { + return new ComparisonResult(true, diffCanvas, null); + } + + // Post-process to identify and filter out isolated differences + Set visited = new HashSet<>(); + List clusterSizes = new ArrayList<>(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pos = y * width + x; + + // If this is a diff pixel and not yet visited + if (isDiffPixel(diffCanvas, x, y) && !visited.contains(pos)) { + ClusterInfo clusterInfo = findClusterSize(diffCanvas, x, y, width, height, visited); + clusterSizes.add(clusterInfo); + } + } + } + + // Determine if the differences are significant + List nonLineShiftClusters = clusterSizes.stream() + .filter(cluster -> !cluster.isLineShift && cluster.size >= MIN_CLUSTER_SIZE) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + + // Calculate significant differences excluding line shifts + int significantDiffPixels = nonLineShiftClusters.stream() + .mapToInt(cluster -> cluster.size) + .sum(); + + // Determine test result + boolean passed = diffCount == 0 || + significantDiffPixels == 0 || + (significantDiffPixels <= MAX_TOTAL_DIFF_PIXELS && nonLineShiftClusters.size() <= 2); + + ComparisonDetails details = new ComparisonDetails(diffCount, significantDiffPixels, clusterSizes); + + return new ComparisonResult(passed, diffCanvas, details); + } + + private PImage resizeImage(PImage image, double scale) { + int newWidth = (int) Math.ceil(image.width * scale); + int newHeight = (int) Math.ceil(image.height * scale); + + PImage resized = p.createImage(newWidth, newHeight, PImage.RGB); + resized.copy(image, 0, 0, image.width, image.height, 0, 0, newWidth, newHeight); + + return resized; + } + + private PImage createCanvasWithBackground(PImage image, int width, int height) { + PImage canvas = p.createImage(width, height, PImage.RGB); + + // Fill with background color (white) + canvas.loadPixels(); + for (int i = 0; i < canvas.pixels.length; i++) { + canvas.pixels[i] = BG_COLOR; + } + canvas.updatePixels(); + + // Draw the image on top + canvas.copy(image, 0, 0, image.width, image.height, 0, 0, image.width, image.height); + + return canvas; + } + + private int pixelmatch(PImage actual, PImage expected, PImage diff, int width, int height, double threshold) { + int diffCount = 0; + + actual.loadPixels(); + expected.loadPixels(); + diff.loadPixels(); + + for (int i = 0; i < actual.pixels.length; i++) { + int actualColor = actual.pixels[i]; + int expectedColor = expected.pixels[i]; + + double delta = colorDelta(actualColor, expectedColor); + + if (delta > threshold) { + // Mark as different (bright red pixel) + diff.pixels[i] = 0xFFFF0000; // Red + diffCount++; + } else { + // Mark as same (dimmed version of actual image) + int dimColor = dimColor(actualColor, ALPHA); + diff.pixels[i] = dimColor; + } + } + + diff.updatePixels(); + return diffCount; + } + + private double colorDelta(int color1, int color2) { + int r1 = (color1 >> 16) & 0xFF; + int g1 = (color1 >> 8) & 0xFF; + int b1 = color1 & 0xFF; + int a1 = (color1 >> 24) & 0xFF; + + int r2 = (color2 >> 16) & 0xFF; + int g2 = (color2 >> 8) & 0xFF; + int b2 = color2 & 0xFF; + int a2 = (color2 >> 24) & 0xFF; + + int dr = r1 - r2; + int dg = g1 - g2; + int db = b1 - b2; + int da = a1 - a2; + + return Math.sqrt(dr * dr + dg * dg + db * db + da * da) / 255.0; + } + + private int dimColor(int color, double alpha) { + int r = (int) (((color >> 16) & 0xFF) * alpha); + int g = (int) (((color >> 8) & 0xFF) * alpha); + int b = (int) ((color & 0xFF) * alpha); + int a = (int) (255 * alpha); + + r = Math.max(0, Math.min(255, r)); + g = Math.max(0, Math.min(255, g)); + b = Math.max(0, Math.min(255, b)); + a = Math.max(0, Math.min(255, a)); + + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private boolean isDiffPixel(PImage image, int x, int y) { + if (x < 0 || x >= image.width || y < 0 || y >= image.height) return false; + + image.loadPixels(); + int color = image.pixels[y * image.width + x]; + + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + return r == 255 && g == 0 && b == 0; + } + + private ClusterInfo findClusterSize(PImage diffImage, int startX, int startY, int width, int height, Set visited) { + List queue = new ArrayList<>(); + queue.add(new Point2D(startX, startY)); + + int size = 0; + List clusterPixels = new ArrayList<>(); + + while (!queue.isEmpty()) { + Point2D point = queue.remove(0); + int pos = point.y * width + point.x; + + // Skip if already visited + if (visited.contains(pos)) continue; + + // Skip if not a diff pixel + if (!isDiffPixel(diffImage, point.x, point.y)) continue; + + // Mark as visited + visited.add(pos); + size++; + clusterPixels.add(point); + + // Add neighbors to queue + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + + int nx = point.x + dx; + int ny = point.y + dy; + + // Skip if out of bounds + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + + // Skip if already visited + int npos = ny * width + nx; + if (!visited.contains(npos)) { + queue.add(new Point2D(nx, ny)); + } + } + } + } + + // Determine if this is a line shift + boolean isLineShift = detectLineShift(clusterPixels, diffImage, width, height); + + return new ClusterInfo(size, clusterPixels, isLineShift); + } + + private boolean detectLineShift(List clusterPixels, PImage diffImage, int width, int height) { + if (clusterPixels.isEmpty()) return false; + + int linelikePixels = 0; + + for (Point2D pixel : clusterPixels) { + int neighbors = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; // Skip self + + int nx = pixel.x + dx; + int ny = pixel.y + dy; + + // Skip if out of bounds + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + + // Check if neighbor is a diff pixel + if (isDiffPixel(diffImage, nx, ny)) { + neighbors++; + } + } + } + + // Line-like pixels typically have 1-2 neighbors + if (neighbors <= 2) { + linelikePixels++; + } + } + + // If most pixels (>80%) in the cluster have ≤2 neighbors, it's likely a line shift + return (double) linelikePixels / clusterPixels.size() > 0.8; + } + + // Configuration methods + public ImageComparator setMaxSide(int maxSide) { + // For future configurability + return this; + } + + public ImageComparator setMinClusterSize(int minClusterSize) { + // For future configurability + return this; + } + + public ImageComparator setMaxTotalDiffPixels(int maxTotalDiffPixels) { + // For future configurability + return this; + } +} + +// Utility class for algorithm configuration +class ComparatorConfig { + public int maxSide = 400; + public int minClusterSize = 4; + public int maxTotalDiffPixels = 40; + public double threshold = 0.5; + public double alpha = 0.1; + public int backgroundColor = 0xFFFFFFFF; + + public ComparatorConfig() {} + + public ComparatorConfig(int maxSide, int minClusterSize, int maxTotalDiffPixels) { + this.maxSide = maxSide; + this.minClusterSize = minClusterSize; + this.maxTotalDiffPixels = maxTotalDiffPixels; + } +} \ No newline at end of file diff --git a/core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java b/core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java new file mode 100644 index 0000000000..b2c8c7efaa --- /dev/null +++ b/core/test/processing/visual/src/test/shapemodes/ShapeModeTest.java @@ -0,0 +1,335 @@ +package processing.visual.src.test.shapemodes; + +import org.junit.jupiter.api.*; +import processing.core.*; +import processing.visual.src.test.base.VisualTest; +import processing.visual.src.core.ProcessingSketch; +import processing.visual.src.core.TestConfig; + +@Tag("shapes") +@Tag("modes") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ShapeModeTest extends VisualTest { + + /** + * Helper function that draws a shape using the specified shape mode + * @param p The PApplet instance + * @param shape The shape to draw: "ellipse", "arc", or "rect" + * @param mode The mode constant (CORNERS, CORNER, CENTER, or RADIUS) + * @param x1 First x coordinate + * @param y1 First y coordinate + * @param x2 Second x/width coordinate + * @param y2 Second y/height coordinate + */ + private void shapeCorners(PApplet p, String shape, int mode, float x1, float y1, float x2, float y2) { + // Adjust coordinates for testing modes other than CORNERS + if (mode == PApplet.CORNER) { + // Find top left corner + float x = PApplet.min(x1, x2); + float y = PApplet.min(y1, y2); + // Calculate width and height + // Don't use abs(), so we get negative values as well + float w = x2 - x1; + float h = y2 - y1; + // For negative widths/heights, adjust position so shapes align consistently + // Rects flip/mirror, but ellipses/arcs should be positioned consistently + if (w < 0) { x += (-w); } // Move right + if (h < 0) { y += (-h); } // Move down + x1 = x; y1 = y; x2 = w; y2 = h; + } else if (mode == PApplet.CENTER) { + // Find center + float x = (x2 + x1) / 2f; + float y = (y2 + y1) / 2f; + // Calculate width and height + // Don't use abs(), so we get negative values as well + float w = x2 - x1; + float h = y2 - y1; + x1 = x; y1 = y; x2 = w; y2 = h; + } else if (mode == PApplet.RADIUS) { + // Find Center + float x = (x2 + x1) / 2f; + float y = (y2 + y1) / 2f; + // Calculate radii + // Don't use abs(), so we get negative values as well + float r1 = (x2 - x1) / 2f; + float r2 = (y2 - y1) / 2f; + x1 = x; y1 = y; x2 = r1; y2 = r2; + } + + if (shape.equals("ellipse")) { + p.ellipseMode(mode); + p.ellipse(x1, y1, x2, y2); + } else if (shape.equals("arc")) { + // Draw four arcs with gaps inbetween + final float GAP = PApplet.radians(20); + p.ellipseMode(mode); + p.arc(x1, y1, x2, y2, 0 + GAP, PApplet.HALF_PI - GAP); + p.arc(x1, y1, x2, y2, PApplet.HALF_PI + GAP, PApplet.PI - GAP); + p.arc(x1, y1, x2, y2, PApplet.PI + GAP, PApplet.PI + PApplet.HALF_PI - GAP); + p.arc(x1, y1, x2, y2, PApplet.PI + PApplet.HALF_PI + GAP, PApplet.TWO_PI - GAP); + } else if (shape.equals("rect")) { + p.rectMode(mode); + p.rect(x1, y1, x2, y2); + } + } + + /** + * Helper to draw shapes in all four quadrants with various coordinate configurations + */ + private void drawShapesInQuadrants(PApplet p, String shape, int mode) { + p.translate(p.width / 2f, p.height / 2f); + + // Quadrant I (Bottom Right) + // P1 P2 + shapeCorners(p, shape, mode, 5, 5, 25, 15); // P1 Top Left, P2 Bottom Right + shapeCorners(p, shape, mode, 5, 20, 25, 30); // P1 Bottom Left, P2 Top Right + shapeCorners(p, shape, mode, 25, 45, 5, 35); // P1 Bottom Right, P2 Top Left + shapeCorners(p, shape, mode, 25, 50, 5, 60); // P1 Top Right, P2 Bottom Left + + // Quadrant II (Bottom Left) + shapeCorners(p, shape, mode, -25, 5, -5, 15); + shapeCorners(p, shape, mode, -25, 20, -5, 30); + shapeCorners(p, shape, mode, -5, 45, -25, 35); + shapeCorners(p, shape, mode, -5, 50, -25, 60); + + // Quadrant III (Top Left) + shapeCorners(p, shape, mode, -25, -60, -5, -50); + shapeCorners(p, shape, mode, -25, -35, -5, -45); + shapeCorners(p, shape, mode, -5, -20, -25, -30); + shapeCorners(p, shape, mode, -5, -15, -25, -5); + + // Quadrant IV (Top Right) + shapeCorners(p, shape, mode, 5, -60, 25, -50); + shapeCorners(p, shape, mode, 5, -35, 25, -45); + shapeCorners(p, shape, mode, 25, -20, 5, -30); + shapeCorners(p, shape, mode, 25, -15, 5, -5); + } + + private ProcessingSketch createShapeModeTest(String shape, int mode) { + return new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.background(200); + p.fill(255); + p.stroke(0); + } + + @Override + public void draw(PApplet p) { + drawShapesInQuadrants(p, shape, mode); + } + }; + } + + // ========== Ellipse Mode Tests ========== + + @Test + @Order(1) + @Tag("ellipse") + @DisplayName("Ellipse with CORNERS mode") + public void testEllipseCorners() { + assertVisualMatch("shape-modes/ellipse-corners", + createShapeModeTest("ellipse", PApplet.CORNERS), + new TestConfig(60, 125)); + } + + @Test + @Order(2) + @Tag("ellipse") + @DisplayName("Ellipse with CORNER mode") + public void testEllipseCorner() { + assertVisualMatch("shape-modes/ellipse-corner", + createShapeModeTest("ellipse", PApplet.CORNER), + new TestConfig(60, 125)); + } + + @Test + @Order(3) + @Tag("ellipse") + @DisplayName("Ellipse with CENTER mode") + public void testEllipseCenter() { + assertVisualMatch("shape-modes/ellipse-center", + createShapeModeTest("ellipse", PApplet.CENTER), + new TestConfig(60, 125)); + } + + @Test + @Order(4) + @Tag("ellipse") + @DisplayName("Ellipse with RADIUS mode") + public void testEllipseRadius() { + assertVisualMatch("shape-modes/ellipse-radius", + createShapeModeTest("ellipse", PApplet.RADIUS), + new TestConfig(60, 125)); + } + + // ========== Arc Mode Tests ========== + + @Test + @Order(5) + @Tag("arc") + @DisplayName("Arc with CORNERS mode") + public void testArcCorners() { + assertVisualMatch("shape-modes/arc-corners", + createShapeModeTest("arc", PApplet.CORNERS), + new TestConfig(60, 125)); + } + + @Test + @Order(6) + @Tag("arc") + @DisplayName("Arc with CORNER mode") + public void testArcCorner() { + assertVisualMatch("shape-modes/arc-corner", + createShapeModeTest("arc", PApplet.CORNER), + new TestConfig(60, 125)); + } + + @Test + @Order(7) + @Tag("arc") + @DisplayName("Arc with CENTER mode") + public void testArcCenter() { + assertVisualMatch("shape-modes/arc-center", + createShapeModeTest("arc", PApplet.CENTER), + new TestConfig(60, 125)); + } + + @Test + @Order(8) + @Tag("arc") + @DisplayName("Arc with RADIUS mode") + public void testArcRadius() { + assertVisualMatch("shape-modes/arc-radius", + createShapeModeTest("arc", PApplet.RADIUS), + new TestConfig(60, 125)); + } + + // ========== Rect Mode Tests ========== + + @Test + @Order(9) + @Tag("rect") + @DisplayName("Rect with CORNERS mode") + public void testRectCorners() { + assertVisualMatch("shape-modes/rect-corners", + createShapeModeTest("rect", PApplet.CORNERS), + new TestConfig(60, 125)); + } + + @Test + @Order(10) + @Tag("rect") + @DisplayName("Rect with CORNER mode") + public void testRectCorner() { + assertVisualMatch("shape-modes/rect-corner", + createShapeModeTest("rect", PApplet.CORNER), + new TestConfig(60, 125)); + } + + @Test + @Order(11) + @Tag("rect") + @DisplayName("Rect with CENTER mode") + public void testRectCenter() { + assertVisualMatch("shape-modes/rect-center", + createShapeModeTest("rect", PApplet.CENTER), + new TestConfig(60, 125)); + } + + @Test + @Order(12) + @Tag("rect") + @DisplayName("Rect with RADIUS mode") + public void testRectRadius() { + assertVisualMatch("shape-modes/rect-radius", + createShapeModeTest("rect", PApplet.RADIUS), + new TestConfig(60, 125)); + } + + // ========== Negative Dimensions Tests ========== + + @Test + @Order(13) + @Tag("negative-dimensions") + @DisplayName("Rect with negative dimensions") + public void testRectNegativeDimensions() { + assertVisualMatch("shape-modes/rect-negative-dimensions", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.background(200); + p.fill(255); + p.stroke(0); + } + + @Override + public void draw(PApplet p) { + p.translate(p.width / 2f, p.height / 2f); + p.rectMode(PApplet.CORNER); + p.rect(0, 0, 20, 10); + p.fill(255, 0, 0); + p.rect(0, 0, -20, 10); + p.fill(0, 255, 0); + p.rect(0, 0, 20, -10); + p.fill(0, 0, 255); + p.rect(0, 0, -20, -10); + } + }, new TestConfig(50, 50)); + } + + @Test + @Order(14) + @Tag("negative-dimensions") + @DisplayName("Ellipse with negative dimensions") + public void testEllipseNegativeDimensions() { + assertVisualMatch("shape-modes/ellipse-negative-dimensions", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.background(200); + p.fill(255); + p.stroke(0); + } + + @Override + public void draw(PApplet p) { + p.translate(p.width / 2f, p.height / 2f); + p.ellipseMode(PApplet.CORNER); + p.ellipse(0, 0, 20, 10); + p.fill(255, 0, 0); + p.ellipse(0, 0, -20, 10); + p.fill(0, 255, 0); + p.ellipse(0, 0, 20, -10); + p.fill(0, 0, 255); + p.ellipse(0, 0, -20, -10); + } + }, new TestConfig(50, 50)); + } + + @Test + @Order(15) + @Tag("negative-dimensions") + @DisplayName("Arc with negative dimensions") + public void testArcNegativeDimensions() { + assertVisualMatch("shape-modes/arc-negative-dimensions", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.background(200); + p.fill(255); + p.stroke(0); + } + + @Override + public void draw(PApplet p) { + p.translate(p.width / 2f, p.height / 2f); + p.ellipseMode(PApplet.CORNER); + p.arc(0, 0, 20, 10, 0, PApplet.PI + PApplet.HALF_PI); + p.fill(255, 0, 0); + p.arc(0, 0, -20, 10, 0, PApplet.PI + PApplet.HALF_PI); + p.fill(0, 255, 0); + p.arc(0, 0, 20, -10, 0, PApplet.PI + PApplet.HALF_PI); + p.fill(0, 0, 255); + p.arc(0, 0, -20, -10, 0, PApplet.PI + PApplet.HALF_PI); + } + }, new TestConfig(50, 50)); + } +} \ No newline at end of file diff --git a/core/test/processing/visual/src/test/typography/TypographyTest.java b/core/test/processing/visual/src/test/typography/TypographyTest.java new file mode 100644 index 0000000000..06be6fffe0 --- /dev/null +++ b/core/test/processing/visual/src/test/typography/TypographyTest.java @@ -0,0 +1,484 @@ +package processing.visual.src.test.typography; + +import org.junit.jupiter.api.*; +import processing.core.*; +import processing.visual.src.test.base.VisualTest; +import processing.visual.src.core.ProcessingSketch; +import processing.visual.src.core.TestConfig; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.util.stream.Stream; + +@Tag("typography") +@Tag("text") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TypographyTest extends VisualTest { + + @Nested + @Tag("font") + @DisplayName("textFont Tests") + class TextFontTests { + + @Test + @Order(1) + @DisplayName("Default font rendering") + public void testDefaultFont() { + assertVisualMatch("typography/font/default-font", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + PFont font = p.createFont("SansSerif", 20); + p.textFont(font); + p.textSize(20); + p.textAlign(PApplet.LEFT, PApplet.BASELINE); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.fill(0); // ← Must be in draw(), not just setup() + p.text("test", 5, 25); // ← Move away from edge + } + }, new TestConfig(50, 50)); + } + + @Test + @Order(2) + @DisplayName("Monospace font rendering") + public void testMonospaceFont() { + assertVisualMatch("typography/font/monospace-font", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + PFont mono = p.createFont("Monospaced", 20); + p.textFont(mono); + p.textAlign(PApplet.LEFT, PApplet.BASELINE); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.fill(0); // ← Add this + p.text("test", 5, 25); + } + }, new TestConfig(50, 50)); + } + + @Test + @Order(3) + @DisplayName("System font rendering") + public void testSystemFont() { + assertVisualMatch("typography/font/system-font", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + PFont font = p.createFont("Serif", 32); + p.textFont(font); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.fill(0); // ← Add this + p.text("test", 10, 50); // ← Better positioning + } + }, new TestConfig(100, 100)); + } + } + + + @Nested + @Tag("alignment") + @DisplayName("textAlign Tests") + class TextAlignTests { + + @ParameterizedTest(name = "Alignment: {0}-{1}") + @MethodSource("alignmentProvider") + @DisplayName("All horizontal and vertical alignments with single word") + public void testAllAlignmentsSingleWord(int alignX, int alignY) { + final String alignName = getAlignmentName(alignX, alignY); + + assertVisualMatch("typography/align/single-word-" + alignName, + new ProcessingSketch() { + PFont font; + + @Override + public void setup(PApplet p) { + font = p.createFont("SansSerif", 60); + p.textFont(font); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.textAlign(alignX, alignY); + p.fill(0); + p.text("Single Line", p.width / 2, p.height / 2); + + // Draw bounding box + p.noFill(); + p.stroke(255, 0, 0); + p.strokeWeight(2); + + float tw = p.textWidth("Single Line"); + float th = p.textAscent() + p.textDescent(); + float x = calculateX(p, alignX, p.width / 2f, tw); + float y = calculateY(p, alignY, p.height / 2f, th); + p.rect(x, y, tw, th); + } + }, new TestConfig(600, 300)); + } + + @ParameterizedTest(name = "Multi-line alignment: {0}-{1}") + @MethodSource("alignmentProvider") + @DisplayName("Multi-line text with manual line breaks") + public void testMultiLineManualText(int alignX, int alignY) { + final String alignName = getAlignmentName(alignX, alignY); + + assertVisualMatch("typography/align/multi-line-" + alignName, + new ProcessingSketch() { + PFont font; + + @Override + public void setup(PApplet p) { + font = p.createFont("SansSerif", 12); + p.textFont(font); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + float xPos = 20; + float yPos = 20; + float boxWidth = 100; + float boxHeight = 60; + + // Draw box + p.noFill(); + p.stroke(200); + p.strokeWeight(2); + p.rect(xPos, yPos, boxWidth, boxHeight); + + // Draw text + p.fill(0); + p.noStroke(); + p.textAlign(alignX, alignY); + p.text("Line 1\nLine 2\nLine 3", xPos, yPos, boxWidth, boxHeight); + + // Draw bounding box + p.noFill(); + p.stroke(255, 0, 0); + p.strokeWeight(1); + } + }, new TestConfig(150, 100)); + } + + // Provide alignment combinations + static Stream alignmentProvider() { + return Stream.of( + Arguments.of(PApplet.LEFT, PApplet.TOP), + Arguments.of(PApplet.CENTER, PApplet.TOP), + Arguments.of(PApplet.RIGHT, PApplet.TOP), + Arguments.of(PApplet.LEFT, PApplet.CENTER), + Arguments.of(PApplet.CENTER, PApplet.CENTER), + Arguments.of(PApplet.RIGHT, PApplet.CENTER), + Arguments.of(PApplet.LEFT, PApplet.BOTTOM), + Arguments.of(PApplet.CENTER, PApplet.BOTTOM), + Arguments.of(PApplet.RIGHT, PApplet.BOTTOM) + ); + } + + // Helper methods + private String getAlignmentName(int alignX, int alignY) { + String x = alignX == PApplet.LEFT ? "left" : + alignX == PApplet.CENTER ? "center" : "right"; + String y = alignY == PApplet.TOP ? "top" : + alignY == PApplet.CENTER ? "center" : "bottom"; + return x + "-" + y; + } + + private float calculateX(PApplet p, int alignX, float x, float tw) { + if (alignX == PApplet.LEFT) return x; + if (alignX == PApplet.CENTER) return x - tw / 2; + return x - tw; + } + + private float calculateY(PApplet p, int alignY, float y, float th) { + if (alignY == PApplet.TOP) return y; + if (alignY == PApplet.CENTER) return y - th / 2; + return y - th; + } + } + + + @Nested + @Tag("size") + @DisplayName("textSize Tests") + class TextSizeTests { + + @Test + @DisplayName("Text sizes comparison") + public void testTextSizes() { + assertVisualMatch("typography/size/sizes-comparison", new ProcessingSketch() { + PFont font; + + @Override + public void setup(PApplet p) { + font = p.createFont("SansSerif", 12); + p.textFont(font); + p.textAlign(PApplet.LEFT, PApplet.BASELINE); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.fill(0); // ← Add this + + int[] sizes = {12, 16, 20, 24, 30}; + float yOffset = 20; + + for (int size : sizes) { + p.textSize(size); + p.text("Size: " + size + "px", 10, yOffset); + yOffset += size + 5; + } + } + }, new TestConfig(300, 200)); + } + } + + @Nested + @Tag("leading") + @DisplayName("textLeading Tests") + class TextLeadingTests { + + @Test + @DisplayName("Text leading with different values") + public void testTextLeading() { + assertVisualMatch("typography/leading/different-values", new ProcessingSketch() { + PFont font; + + @Override + public void setup(PApplet p) { + font = p.createFont("SansSerif", 16); + p.textFont(font); + p.textSize(16); + p.textAlign(PApplet.LEFT, PApplet.BASELINE); + } + + @Override + public void draw(PApplet p) { + p.background(255); + p.fill(0); // ← Add this + + int[] leadingValues = {10, 20, 30}; + float yOffset = 25; + + for (int leading : leadingValues) { + p.textLeading(leading); + p.text("Leading: " + leading, 10, yOffset); + yOffset += 25; + p.text("Line 1\nLine 2", 10, yOffset); + yOffset += leading * 2 + 15; + } + } + }, new TestConfig(300, 250)); + } + } + + + @Nested + @Tag("width") + @DisplayName("textWidth Tests") + class TextWidthTests { + + @Test + @DisplayName("Verify width of a string") + public void testTextWidth() { + assertVisualMatch("typography/width/string-width", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.textSize(20); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + String text = "Width Test"; + float width = p.textWidth(text); + + p.fill(0); + p.text(text, 0, 30); + + p.noFill(); + p.stroke(255, 0, 0); + p.rect(0, 10, width, 20); + } + }, new TestConfig(100, 100)); + } + } + + @Nested + @Tag("pfont") + @DisplayName("PFont Methods Tests") + class PFontMethodsTests { + + @Test + @DisplayName("Text ascent and descent") + public void testTextAscentDescent() { + assertVisualMatch("typography/pfont/ascent-descent", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.textSize(32); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + float baseline = 50; + p.text("Typography", 10, baseline); + + // Show baseline + p.stroke(0, 255, 0); + p.line(0, baseline, p.width, baseline); + + // Show ascent + p.stroke(255, 0, 0); + float ascent = p.textAscent(); + p.line(0, baseline - ascent, p.width, baseline - ascent); + + // Show descent + p.stroke(0, 0, 255); + float descent = p.textDescent(); + p.line(0, baseline + descent, p.width, baseline + descent); + } + }, new TestConfig(200, 100)); + } + + @Test + @DisplayName("Character availability check") + public void testCharacterAvailability() { + assertVisualMatch("typography/pfont/char-availability", new ProcessingSketch() { + PFont font; + + @Override + public void setup(PApplet p) { + font = p.createFont("SansSerif", 24); + p.textFont(font); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + String testChars = "ABCabc123!@#"; + float x = 10; + float y = 30; + + for (int i = 0; i < testChars.length(); i++) { + char c = testChars.charAt(i); + + if (font.getGlyph(c) != null) { + p.fill(0); + } else { + p.fill(255, 0, 0); + } + + p.text(c, x, y); + x += p.textWidth(c) + 2; + } + } + }, new TestConfig(200, 80)); + } + } + + @Nested + @Tag("complex") + @DisplayName("Complex Text Rendering") + class ComplexTextRenderingTests { + + @Test + @DisplayName("Text with rotation") + public void testRotatedText() { + assertVisualMatch("typography/complex/rotated-text", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.textSize(24); + p.textAlign(PApplet.CENTER, PApplet.CENTER); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + p.pushMatrix(); + p.translate(p.width / 2, p.height / 2); + + for (int i = 0; i < 12; i++) { + p.pushMatrix(); + p.rotate(PApplet.TWO_PI * i / 12); + p.translate(0, -40); + p.fill(0); + p.text(i, 0, 0); + p.popMatrix(); + } + + p.popMatrix(); + } + }, new TestConfig(150, 150)); + } + + @Test + @DisplayName("Text with transparency") + public void testTransparentText() { + assertVisualMatch("typography/complex/transparent-text", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.textSize(48); + p.textAlign(PApplet.CENTER, PApplet.CENTER); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + for (int i = 0; i < 5; i++) { + int alpha = 255 - (i * 50); + p.fill(0, 0, 255, alpha); + p.text("Layer " + i, p.width / 2 + i * 5, p.height / 2 + i * 5); + } + } + }, new TestConfig(200, 150)); + } + + @Test + @DisplayName("Text with different colors") + public void testColoredText() { + assertVisualMatch("typography/complex/colored-text", new ProcessingSketch() { + @Override + public void setup(PApplet p) { + p.textSize(20); + p.textAlign(PApplet.LEFT, PApplet.TOP); + } + + @Override + public void draw(PApplet p) { + p.background(255); + + p.fill(255, 0, 0); + p.text("Red Text", 10, 10); + + p.fill(0, 255, 0); + p.text("Green Text", 10, 35); + + p.fill(0, 0, 255); + p.text("Blue Text", 10, 60); + + p.fill(255, 0, 255); + p.text("Magenta Text", 10, 85); + } + }, new TestConfig(150, 120)); + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 285a190390..514ca25623 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,7 +13,4 @@ include( "java:libraries:serial", "java:libraries:svg" ) - include("app:utils") -include(":visual-tests") -