Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
43f7e6c
hellocodenameone: fix graphics screenshot tests for scale and perspec…
shai-almog May 7, 2026
1ff257e
hellocodenameone: rewrite perspective/camera viewport math for visibl…
shai-almog May 7, 2026
a6570dd
hellocodenameone: project perspective/camera corners via transformPoint
shai-almog May 7, 2026
feb65f8
iOS port: invalidate clip caches on impl.setTransform
shai-almog May 7, 2026
c3011a7
cn1ss: tag every emitted PNG with FNV-1a 64 hash and detect duplicates
shai-almog May 7, 2026
7552c40
cn1ss: avoid HashMap static-init that breaks iOS class loading
shai-almog May 7, 2026
51eec90
hellocodenameone: refresh Android goldens for the four updated tests
shai-almog May 7, 2026
62f286a
cn1ss: use negative lookahead in INFO-line regexes to avoid prefix match
shai-almog May 7, 2026
f511f84
run-ios-ui-tests: include fallback-decoded tests in compare set
shai-almog May 7, 2026
d7eb2ed
hellocodenameone: promote iOS Metal goldens for transform-perspective…
shai-almog May 7, 2026
292b980
Transform: mark composed transform dirty after copyTransform
shai-almog May 7, 2026
67ec8ff
Graphics.setTransform: conjugate user matrix with xTranslate/yTranslate
shai-almog May 8, 2026
4afc42a
PostPrComment: retry preview push with rebase on race-condition reject
shai-almog May 8, 2026
a54efa0
Gate setTransform xTranslate-conjugation behind impl opt-in
shai-almog May 8, 2026
579a9bc
Revert "Gate setTransform xTranslate-conjugation behind impl opt-in"
shai-almog May 8, 2026
8396b64
Revert "Graphics.setTransform: conjugate user matrix with xTranslate/…
shai-almog May 8, 2026
6b7e20a
hellocodenameone: render Scale/AffineScale gradients via mutable Image
shai-almog May 8, 2026
7f8e07f
Revert "hellocodenameone: render Scale/AffineScale gradients via muta…
shai-almog May 8, 2026
a58de8a
Graphics.setTransform: conjugate user matrix uniformly across iOS/And…
shai-almog May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CodenameOne/src/com/codename1/charts/ChartComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,17 @@ public void paint(Graphics g) {
g.getTransform(tmpTransform);

if (currentTransform == null) {
currentTransform = Transform.makeTranslation(getAbsoluteX(), getAbsoluteY());
currentTransform = Transform.makeIdentity();
} else {
currentTransform.setTranslation(getAbsoluteX(), getAbsoluteY());
currentTransform.setIdentity();
}
currentTransform.concatenate(transform);
currentTransform.translate(-getAbsoluteX(), -getAbsoluteY());
// Earlier this conjugated `transform` with T(absX, absY) to
// compensate for the xTranslate/yTranslate the platform was
// adding to vertex coords. Graphics.setTransform() now performs
// that conjugation uniformly across iOS / Android / JavaSE, so
// doing it manually here would shift the chart by 2*absX,
// 2*absY. Pass the user's transform through unchanged.

g.setTransform(currentTransform);
} else {
Expand Down
26 changes: 26 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,32 @@ public boolean isTranslationSupported() {
return false;
}

/// When `#isTranslationSupported()` returns false, Graphics.java keeps
/// `xTranslate`/`yTranslate` in its own state and bakes them into the
/// vertex coordinates passed to fill primitives. If the impl's render
/// path then applies the user's setTransform matrix on top of those
/// already-translated vertices (e.g. iOS Metal's
/// `projection * modelView * userTransform * pos` shader, or
/// AndroidGraphics's `canvas.concat(t); canvas.drawRect(x+xT, y+yT)`),
/// the translation is double-counted for any non-translation matrix and
/// the rendered output is shifted off-cell -- noticeable on Android
/// (small displacement) and catastrophic on iOS Metal at native pixel
/// resolution (output goes off-screen entirely). Override this and
/// return true so `Graphics.setTransform` conjugates the user's matrix
/// with `T(xTranslate, yTranslate)` before passing it to the impl,
/// restoring "transform applies in local coordinates" semantics across
/// every isTranslationSupported=false port.
///
/// Internal Graphics.java callers that historically baked
/// xTranslate/yTranslate into their own setTransform argument
/// (com.codename1.ui.scene.Node, com.codename1.charts.ChartComponent,
/// FlipTransition perspective branch, ...) must drop the manual
/// conjugation when this returns true so the platform-side conjugation
/// doesn't double up.
public boolean isSetTransformTranslationConjugationRequired() {
return false;
}

/// Translates the X/Y location for drawing on the underlying surface. Translation
/// is incremental so the new value will be added to the current translation and
/// in order to reset translation we have to invoke
Expand Down
53 changes: 52 additions & 1 deletion CodenameOne/src/com/codename1/ui/Graphics.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ public final class Graphics {
private int xTranslate;
private int yTranslate;
private Transform translation;
/// Last non-identity argument to setTransform(). When the impl returns
/// true from isSetTransformTranslationConjugationRequired(), the matrix
/// actually pushed to impl.setTransform is
/// `T(xTranslate) * userTransform * T(-xTranslate)`, so the user-visible
/// transform applies to local coordinates regardless of any prior
/// g.translate(). getTransform() returns this original (un-conjugated)
/// matrix.
private Transform userTransform;
private GeneralPath tmpClipShape;
/// A buffer shape to use when we need to transform a shape
private int color;
Expand Down Expand Up @@ -137,6 +145,17 @@ public void translate(int x, int y) {
} else {
xTranslate += x;
yTranslate += y;
// The conjugation in setTransform() depends on the current
// xTranslate/yTranslate. If the user accumulated more
// translation after setting a non-identity transform,
// re-conjugate so the impl-side matrix stays in sync.
if (userTransform != null
&& impl.isSetTransformTranslationConjugationRequired()) {
Transform composed = Transform.makeTranslation(xTranslate, yTranslate);
composed.concatenate(userTransform);
composed.translate(-xTranslate, -yTranslate);
impl.setTransform(nativeGraphics, composed);
}
}
}

Expand Down Expand Up @@ -1129,6 +1148,9 @@ public void transform(Transform transform) {
///
/// - #setTransform
public Transform getTransform() {
if (userTransform != null) {
return userTransform.copy();
}
return impl.getTransform(nativeGraphics);

}
Expand Down Expand Up @@ -1160,7 +1182,31 @@ public Transform getTransform() {
///
/// - #setTransform(com.codename1.ui.geom.Matrix, int, int)
public void setTransform(Transform transform) {
impl.setTransform(nativeGraphics, transform);
// On platforms where impl.isTranslationSupported() is false, this
// Graphics object accumulates xTranslate/yTranslate locally and bakes
// them into vertex coordinates passed to impl fill primitives. The
// user's setTransform matrix is then applied by the underlying
// platform on top of those already-translated vertices, which
// double-counts the cell origin for any non-translation matrix
// (rotate, scale, shear) -- the gradient ends up off-cell or
// off-screen. Conjugate the user's matrix with T(xTranslate,
// yTranslate) so its effect is independent of any prior g.translate
// call, matching the Android Skia / JavaSE Graphics2D semantics that
// the framework's own callers (LinearGradientPaint, etc.) work
// around manually today. Impls that don't need this opt out via
// isSetTransformTranslationConjugationRequired() returning false.
if (transform != null && !transform.isIdentity()
&& (xTranslate != 0 || yTranslate != 0)
&& impl.isSetTransformTranslationConjugationRequired()) {
userTransform = transform.copy();
Transform composed = Transform.makeTranslation(xTranslate, yTranslate);
composed.concatenate(transform);
composed.translate(-xTranslate, -yTranslate);
impl.setTransform(nativeGraphics, composed);
} else {
userTransform = null;
impl.setTransform(nativeGraphics, transform);
}
}

/// Loads the provided transform with the current transform applied to this graphics context.
Expand All @@ -1169,6 +1215,10 @@ public void setTransform(Transform transform) {
///
/// - `t`: An "out" parameter to be filled with the current transform.
public void getTransform(Transform t) {
if (userTransform != null) {
t.setTransform(userTransform);
return;
}
impl.getTransform(nativeGraphics, t);
}

Expand Down Expand Up @@ -1576,6 +1626,7 @@ public void resetAffine() {
impl.resetAffine(nativeGraphics);
scaleX = 1;
scaleY = 1;
userTransform = null;
}

/// Scales the coordinate system using the affine transform
Expand Down
10 changes: 10 additions & 0 deletions CodenameOne/src/com/codename1/ui/Transform.java
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,16 @@ public void setTransform(Transform t) {
initNativeTransform();
t.initNativeTransform();
impl.copyTransform(t.nativeTransform, nativeTransform);
// Mark the cached native matrix as dirty so subsequent
// getNativeTransform() calls re-run initNativeTransform.
// For TYPE_UNKNOWN this is a no-op for the matrix data
// itself, but it triggers any platform-side code that
// listens on initNativeTransform to refresh its cache --
// the iOS Metal port has shown that without this flag
// setTransform(composed) silently fails to apply on the
// form-Graphics screen encoder while the equivalent
// g.rotate / g.scale / g.translate path renders correctly.
dirty = true;
break;
}

Expand Down
20 changes: 17 additions & 3 deletions CodenameOne/src/com/codename1/ui/scene/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,14 @@ public Transform getLocalToScreenTransform() {
Transform newT = Transform.isPerspectiveSupported() && scene != null && scene.camera.get() != null ?
scene.camera.get().getTransform() : Transform.makeIdentity();
if (getScene() != null) {
newT.translate(getScene().getAbsoluteX(), getScene().getAbsoluteY());
// The screen-translate component is contributed by the Graphics
// object's xTranslate/yTranslate (the cumulative parent
// translates applied during paint) -- on platforms where
// Graphics.setTransform() conjugates the user matrix with that
// translation, it would be double-counted if we baked
// scene.absX/absY in here too. Stop at the local-to-scene
// transform; the platform places it at the scene's screen
// origin.
newT.concatenate(getLocalToSceneTransform());
}
return newT;
Expand All @@ -381,9 +388,16 @@ public void render(Graphics g) {
scene.camera.get().getTransform() :
Transform.makeIdentity();
if (getScene() != null) {
newT.translate(getScene().getAbsoluteX(), getScene().getAbsoluteY());
// Earlier this conjugated localToScene with T(scene.absX,
// absY) so that, when applied to the xTranslate-shifted vertex
// coords the platform passed to the GPU, the rendering landed
// back at the scene's screen origin. Graphics.setTransform()
// now performs that conjugation uniformly across iOS / Android
// / JavaSE, so applying it manually here would double the
// translation and push the spinner rows off-cell. Hand the
// platform the local transform; it places it at xTranslate/
// yTranslate, which is the scene's screen origin during paint.
newT.concatenate(getLocalToSceneTransform());
newT.translate(-scene.getAbsoluteX(), -scene.getAbsoluteY());
}
g.setTransform(newT);
int alpha = g.getAlpha();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10583,6 +10583,23 @@ public boolean isTransformSupported() {
return true;
}

@Override
public boolean isSetTransformTranslationConjugationRequired() {
// Android's render path mirrors iOS: xTranslate/yTranslate accumulate
// in Graphics.java (since isTranslationSupported() is false) and end
// up baked into vertex coordinates. AndroidGraphics applies the
// user's setTransform matrix on top via canvas.concat at draw time,
// which double-counts the cell origin for any non-translation
// matrix. The displacement is small at typical phone resolutions
// (which is why the visual effect on Android is "shifted a bit"
// rather than the off-screen rendering iOS Metal exhibits) but it
// still differs from the user's intent. Conjugating in
// Graphics.setTransform yields the same "transform applies in local
// coordinates" contract as iOS, so identical CN1 code produces
// identical output across both ports.
return true;
}

@Override
public boolean isPerspectiveTransformSupported() {

Expand Down
17 changes: 16 additions & 1 deletion Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -9257,7 +9257,22 @@ public void drawShape(Object graphics, com.codename1.ui.geom.Shape shape, com.co
public boolean isTransformSupported(){
return true;
}


@Override
public boolean isSetTransformTranslationConjugationRequired() {
// JavaSE's render path is identical in shape to Android's:
// xTranslate/yTranslate accumulate in Graphics.java (since
// isTranslationSupported() is false) and end up baked into the
// coordinates passed to fill primitives, while setTransform()
// replaces the AWT Graphics2D matrix outright. Applying the user
// matrix to xTranslate-shifted coordinates double-counts the cell
// origin -- visible as a slight shift at simulator resolution.
// Conjugating in Graphics.setTransform yields the same "transform
// applies in local coordinates" contract as iOS / Android.
return true;
}


/**
* Checks of the Transform class can be used on this platform to perform perspective transforms.
* This is similar to
Expand Down
38 changes: 32 additions & 6 deletions Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -2402,9 +2402,22 @@ public void setTransform(Object graphics, Transform transform) {
ng.transform = transform == null ? null : transform.copy();
}
ng.transformApplied = false;
// The cached clip / inverseClip / inverseTransform are derived from
// the current transform; replacing the transform leaves them
// pointing at the previous transform's space. Subsequent draw ops
// (e.g. fillRect or fillLinearGradient on the form Graphics) read
// those caches via loadClipBounds / inverseClip and end up clipped
// to the wrong region, which is why TransformRotation and
// Scale/AffineScale produced empty top cells on iOS Metal while
// the equivalent rotation via g.rotate (which DOES invalidate
// these flags, line 5513) rendered correctly. Match the
// rotate/scale/translate/resetAffine paths so the cache is rebuilt
// before the next draw.
ng.clipDirty = true;
ng.inverseClipDirty = true;
ng.inverseTransformDirty = true;
ng.checkControl();
ng.applyTransform();

}

public void setNativeTransformGlobal(Transform transform){
Expand Down Expand Up @@ -4213,15 +4226,28 @@ public void rotate(Object nativeGraphics, float angle, int x, int y) {
@Override
public boolean isTranslationSupported() {
//return true;
// We'll leave this as false until the next iteration...
// ES2 should allow us to do all of this using transforms but
// We'll leave this as false until the next iteration...
// ES2 should allow us to do all of this using transforms but
// let's take small steps first
return false;
}




@Override
public boolean isSetTransformTranslationConjugationRequired() {
// The iOS render path bakes xTranslate/yTranslate into vertex coords
// (since isTranslationSupported() is false) and the GPU vertex
// shader then applies the user's setTransform matrix on top
// (`projection * modelView * userTransform * pos`). For any
// non-translation matrix this double-counts the cell origin and
// throws output off-screen at native pixel resolution. Conjugation
// in Graphics.setTransform restores "transform applies in local
// coordinates" semantics on this port.
return true;
}




public void shear(Object nativeGraphics, float x, float y) {
((NativeGraphics)nativeGraphics).shear(x, y);
}
Expand Down
Binary file modified scripts/android/screenshots/graphics-affine-scale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/android/screenshots/graphics-scale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/android/screenshots/graphics-transform-camera.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/android/screenshots/graphics-transform-perspective.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading