Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
53b463e
hellocodenameone: fix graphics screenshot tests for scale and perspec…
shai-almog May 7, 2026
b04af90
hellocodenameone: rewrite perspective/camera viewport math for visibl…
shai-almog May 7, 2026
0204a73
hellocodenameone: project perspective/camera corners via transformPoint
shai-almog May 7, 2026
39502b7
iOS port: invalidate clip caches on impl.setTransform
shai-almog May 7, 2026
ae52375
cn1ss: tag every emitted PNG with FNV-1a 64 hash and detect duplicates
shai-almog May 7, 2026
360ca72
cn1ss: avoid HashMap static-init that breaks iOS class loading
shai-almog May 7, 2026
3f12fc9
hellocodenameone: refresh Android goldens for the four updated tests
shai-almog May 7, 2026
f65c05d
cn1ss: use negative lookahead in INFO-line regexes to avoid prefix match
shai-almog May 7, 2026
1115e78
run-ios-ui-tests: include fallback-decoded tests in compare set
shai-almog May 7, 2026
4c19316
hellocodenameone: promote iOS Metal goldens for transform-perspective…
shai-almog May 7, 2026
277ce81
Transform: mark composed transform dirty after copyTransform
shai-almog May 7, 2026
26f9f6b
Graphics.setTransform: conjugate user matrix with xTranslate/yTranslate
shai-almog May 8, 2026
5d355ab
PostPrComment: retry preview push with rebase on race-condition reject
shai-almog May 8, 2026
59274ba
Gate setTransform xTranslate-conjugation behind impl opt-in
shai-almog May 8, 2026
dc6b18e
Revert "Gate setTransform xTranslate-conjugation behind impl opt-in"
shai-almog May 8, 2026
271ae25
Revert "Graphics.setTransform: conjugate user matrix with xTranslate/…
shai-almog May 8, 2026
45b7ee5
hellocodenameone: render Scale/AffineScale gradients via mutable Image
shai-almog May 8, 2026
5f4c9f9
Revert "hellocodenameone: render Scale/AffineScale gradients via muta…
shai-almog May 8, 2026
512eba0
Graphics.setTransform: conjugate user matrix uniformly across iOS/And…
shai-almog May 8, 2026
1d94caf
hellocodenameone: cover ChartComponent + opt JS port into setTransfor…
shai-almog May 9, 2026
0873b6b
hellocodenameone: fix two chart-test runtime errors flagged by Androi…
shai-almog May 9, 2026
19a2e2b
hellocodenameone: unstick Android/JS chart pipelines + promote 11 And…
shai-almog May 9, 2026
3ddad10
charts: drop xTranslate from Canvas.rotate centre
shai-almog May 9, 2026
2ecfff5
charts: replace em-dash with ASCII in Canvas.rotate comment
shai-almog May 9, 2026
b11d702
charts: add diagnostic logging + promote iOS round-chart goldens
shai-almog May 9, 2026
a718b5e
charts: add CN1SS:DBG enter/bounds/exit logging in XYChart.draw
shai-almog May 9, 2026
beb795c
DIAGNOSTIC: turn off labels/legend/grid/axes in chart-line
shai-almog May 9, 2026
e17c424
DIAGNOSTIC: empty dataset in chart-line to isolate drawSeries
shai-almog May 9, 2026
79a0155
charts: revert diagnostic logging + chart-line test config
shai-almog May 9, 2026
46c0dc7
charts: clip ChartComponent.paint to component bounds (fix iOS blank)
shai-almog May 9, 2026
d9952d4
charts: collapse redundant moveTos in drawPath to fix iOS blank
shai-almog May 9, 2026
a95fc70
DIAGNOSTIC: switch chart Paint default join from BEVEL to MITER
shai-almog May 9, 2026
006435b
Revert "DIAGNOSTIC: switch chart Paint default join from BEVEL to MITER"
shai-almog May 9, 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
9 changes: 6 additions & 3 deletions CodenameOne/src/com/codename1/charts/compat/Canvas.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,14 @@ public void drawLine(float x1, float y1, float x2, float y2, Paint paint) {
}

public void rotate(float angle, float x, float y) {
//Log.p("Rotating by angle "+angle);
// (x, y) is in chart-local coords; Graphics.setTransform now
// conjugates with the active xTranslate/yTranslate, so we must NOT
// bake `absoluteX - bounds.getX()` (= xTranslate) into the rotation
// centre here -- that would apply the conjugation twice and rotate
// the chart around a point well off-screen.
Transform t = g.getTransform();
t.rotate((float) (angle * Math.PI / 180.0), x + absoluteX - bounds.getX(), y + absoluteY - bounds.getY());
t.rotate((float) (angle * Math.PI / 180.0), x, y);
g.setTransform(t);

}

public void scale(float x, float y) {
Expand Down
50 changes: 48 additions & 2 deletions CodenameOne/src/com/codename1/charts/views/AbstractChart.java
Original file line number Diff line number Diff line change
Expand Up @@ -372,18 +372,47 @@ protected void drawPath(Canvas canvas, List<Float> points, Paint paint, boolean
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);

// Track the running endpoint of the open subpath so we can emit a
// moveTo only when the data actually skips a segment (an off-screen
// point was filtered out via `continue`). The original loop emitted
// `path.moveTo(tempDrawPoints[0..1])` before EVERY lineTo, which
// produces N disjoint single-segment subpaths for an N-point line.
// The iOS GL+Metal form-Graphics drawShape -> TextureAlphaMask path
// crashes the on-screen frame buffer when fed a multi-subpath stroke
// covering most of the form (the chart fills BorderLayout.CENTER) --
// every other port (Skia / JavaFX / mutable-image NativeGraphics)
// happily collapses the redundant moveTos but iOS form-Graphics
// ends up with the entire frame dropped (form title bar disappears
// along with the chart). Emit moveTo only when the previous segment
// was skipped or the next segment doesn't continue from the running
// endpoint, so an unfiltered line series renders as a single
// continuous polyline -- byte-equivalent to the historical output on
// every port that handled the multi-subpath form, and the form
// actually paints on iOS.
float lastEndX = tempDrawPoints[2];
float lastEndY = tempDrawPoints[3];
boolean haveOpenSubpath = true;

int length = points.size();
for (int i = 4; i < length; i += 2) {
if ((points.get(i - 1) < 0 && points.get(i + 1) < 0)
|| (points.get(i - 1) > height && points.get(i + 1) > height)) {
haveOpenSubpath = false;
continue;
}
tempDrawPoints = calculateDrawPoints(points.get(i - 2), points.get(i - 1), points.get(i),
points.get(i + 1), height, width);
if (!circular) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
if (!haveOpenSubpath
|| tempDrawPoints[0] != lastEndX
|| tempDrawPoints[1] != lastEndY) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
}
}
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);
lastEndX = tempDrawPoints[2];
lastEndY = tempDrawPoints[3];
haveOpenSubpath = true;
}
if (circular) {
path.lineTo(points.get(0), points.get(1));
Expand Down Expand Up @@ -415,18 +444,35 @@ protected void drawPath(Canvas canvas, float[] points, Paint paint, boolean circ
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);

// See the List<Float> overload above for the rationale: collapse
// redundant moveTos so an unfiltered line series renders as a
// single continuous polyline rather than N disjoint single-segment
// subpaths. iOS form-Graphics drawShape -> TextureAlphaMask drops
// the entire frame when fed the multi-subpath form.
float lastEndX = tempDrawPoints[2];
float lastEndY = tempDrawPoints[3];
boolean haveOpenSubpath = true;

int length = points.length;
for (int i = 4; i < length; i += 2) {
if ((points[i - 1] < 0 && points[i + 1] < 0)
|| (points[i - 1] > height && points[i + 1] > height)) {
haveOpenSubpath = false;
continue;
}
tempDrawPoints = calculateDrawPoints(points[i - 2], points[i - 1], points[i], points[i + 1],
height, width);
if (!circular) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
if (!haveOpenSubpath
|| tempDrawPoints[0] != lastEndX
|| tempDrawPoints[1] != lastEndY) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
}
}
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);
lastEndX = tempDrawPoints[2];
lastEndY = tempDrawPoints[3];
haveOpenSubpath = true;
}
if (circular) {
path.lineTo(points[0], points[1]);
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
Original file line number Diff line number Diff line change
Expand Up @@ -5788,6 +5788,19 @@ public boolean isTransformSupported() {
return true;
}

@Override
public boolean isSetTransformTranslationConjugationRequired() {
// The HTML5 render path mirrors iOS / Android: 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 canvas matrix outright. Conjugating the user matrix
// with T(xTranslate, yTranslate) in Graphics.setTransform yields
// the same "transform applies in local coordinates" contract as
// every other CN1 port.
return true;
}

@Override
public void concatenateTransform(Object t1, Object t2) {
((JSAffineTransform)t1).concatenate((JSAffineTransform)t2);
Expand Down
Loading
Loading