Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7e7dbf2
Add cross-platform AR and VR support
shai-almog Jul 4, 2026
420a5cd
Fix CI failures: CLDC math, SpotBugs, guide prose, iOS test runner
shai-almog Jul 4, 2026
48f4202
Fix SpotBugs violations in the iOS AR impl
shai-almog Jul 4, 2026
7a46103
Fix forbidden PMD violations in the new AR/VR core code
shai-almog Jul 4, 2026
8062d38
Harden mac-native and javase-simulator harnesses against runner gaps
shai-almog Jul 4, 2026
dd6d299
Exclude the ARCore impl package from the Ant Android port build
shai-almog Jul 4, 2026
35cde46
Exclude the ARCore impl package from the Android jar target too
shai-almog Jul 4, 2026
11e180a
Add AR/VR screenshot tests, AR simulator scenario and guide images
shai-almog Jul 6, 2026
fa086ed
Seed AR/VR screenshot goldens from the CI capture
shai-almog Jul 6, 2026
1946774
Authenticate the JavaSE-sim skin release lookup to avoid rate-limit f…
shai-almog Jul 6, 2026
8360eae
Give the Android suite time to finish when it grows
shai-almog Jul 6, 2026
7326885
Capture VR/360 in landscape, skip stereo VR on tvOS, move to suite tail
shai-almog Jul 6, 2026
4bff04a
Stop ARApiTest from opening a live AR session (froze the Android suite)
shai-almog Jul 6, 2026
f20c993
Place VR/360 right after the orientation test and restore portrait
shai-almog Jul 6, 2026
0f270d3
Detach the VR/360 GPU peer after capture; seed landscape goldens
shai-almog Jul 6, 2026
583f281
Revert the VR/360 GPU-peer detach (it destabilized the iOS Metal capt…
shai-almog Jul 6, 2026
cfb0b1a
Detach only Media360Panorama's peer; widen its iOS Metal tolerance
shai-almog Jul 6, 2026
0dc919a
Move VR/360 to the suite tail so DesktopMode never grabs their frame
shai-almog Jul 6, 2026
24fd8ce
Force a fresh present before VR/360 capture (iOS Metal late-present r…
shai-almog Jul 6, 2026
e68c2c0
Give Media360Panorama's texture time to present on the iOS Metal simu…
shai-almog Jul 6, 2026
85ddeb4
Use createForm for Media360Panorama so the iOS Metal capture matches VR
shai-almog Jul 7, 2026
3131af6
Build 360 panorama as an immutable array-backed image
shai-almog Jul 7, 2026
a0cab3e
Regenerate android + JS Media360 goldens for array-backed panorama
shai-almog Jul 7, 2026
661ae98
Move AR/VR guide snippets into compiled demo sources
shai-almog Jul 7, 2026
aed276a
Use contraction in AR chapter to satisfy Vale prose gate
shai-almog Jul 7, 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
195 changes: 195 additions & 0 deletions CodenameOne/src/com/codename1/ar/AR.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.ar;

import com.codename1.impl.ARImpl;
import com.codename1.io.Log;
import com.codename1.ui.Display;
import com.codename1.util.AsyncResource;
import com.codename1.util.SuccessCallback;

import java.io.IOException;

/// Entry point for the cross-platform augmented reality API.
///
/// AR sessions track the device's pose in the real world through the camera,
/// detect surfaces, and composite virtual 3D content into the camera image.
/// The typical flow: check `#isSupported()`, `#open(ARSessionOptions)` a
/// session, add its `ARSession#createView()` to a form, then place content by
/// hit testing taps against detected planes and hanging `ARNode`s on the
/// resulting anchors.
///
/// **Permissions and dependencies**: simply referencing classes in this
/// package causes the build pipeline to inject the camera permission, the
/// `NSCameraUsageDescription` plist entry (iOS, override via the
/// `ios.NSCameraUsageDescription` build hint) and the ARCore dependency
/// (Android). Devices without AR support report `#isSupported()` false;
/// always guard AR functionality behind that check.
///
/// ```java
/// if (AR.isSupported()) {
/// ARSession s = AR.open(new ARSessionOptions());
/// Form f = new Form("AR", new BorderLayout());
/// f.add(BorderLayout.CENTER, s.createView());
/// f.show();
/// // on tap: hit test and place a model
/// s.hitTest(xNorm, yNorm).ready(hits -> {
/// if (hits.length > 0) {
/// ARAnchor a = hits[0].createAnchor();
/// a.setNode(new ARNode(ARModel.fromGltf(modelBytes)));
/// }
/// });
/// }
/// ```
public final class AR {
private static final Object ACTIVE_LOCK = new Object();
private static ARSession active;

private AR() {
}

/// True when the running platform has a working AR implementation. False
/// on platforms and devices without AR support.
public static boolean isSupported() {
return Display.getInstance().getARBackend() != null;
}

/// Returns which AR features this device supports. On unsupported
/// platforms every capability reads false.
public static ARCapabilities getCapabilities() {
ARImpl probe = newImpl();
if (probe == null) {
return ARCapabilities.UNSUPPORTED;
}
try {
ARCapabilities caps = probe.getCapabilities();
return caps == null ? ARCapabilities.UNSUPPORTED : caps;
} finally {
closeQuietly(probe);
}
}

/// Opens an AR session. Throws `IllegalStateException` when AR is
/// unsupported or a session is already open; close the old session first.
///
/// #### Parameters
///
/// - `opts`: the session configuration; null uses the defaults
///
/// #### Returns
///
/// the running session
public static ARSession open(ARSessionOptions opts) {
if (opts == null) {
opts = new ARSessionOptions();
}
// The check-and-set is atomic so the "one open session at a time"
// contract holds under concurrent open() calls; contention is
// essentially zero since opening AR is a foreground user action.
synchronized (ACTIVE_LOCK) {
if (active != null && !active.isClosed()) {
throw new IllegalStateException(
"Only one ARSession may be open at a time. Close the existing session first.");
}
ARImpl impl = newImpl();
if (impl == null) {
throw new IllegalStateException("AR is not supported on this platform.");
}
ARSession session = new ARSession(impl, opts);
try {
impl.open(opts);
} catch (IOException e) {
session.close();
throw new RuntimeException("Could not open the AR session", e);
}
active = session;
return session;
}
}

/// Requests the camera permission needed for AR. The callback receives
/// true when granted; it is invoked on the EDT.
///
/// #### Parameters
///
/// - `callback`: receives the grant result; may be null
public static void requestPermissions(final SuccessCallback<Boolean> callback) {
final ARImpl impl = newImpl();
if (impl == null) {
fireLater(callback, Boolean.FALSE);
return;
}
AsyncResource<Boolean> result = new AsyncResource<Boolean>();
result.ready(new SuccessCallback<Boolean>() {
@Override public void onSucess(Boolean value) {
closeQuietly(impl);
if (callback != null) {
callback.onSucess(Boolean.valueOf(value != null && value.booleanValue()));
}
}
});
result.except(new SuccessCallback<Throwable>() {
@Override public void onSucess(Throwable t) {
closeQuietly(impl);
if (callback != null) {
callback.onSucess(Boolean.FALSE);
}
}
});
impl.requestPermissions(result);
}

private static void fireLater(final SuccessCallback<Boolean> callback, final Boolean value) {
if (callback == null) {
return;
}
Display.getInstance().callSerially(new Runnable() {
@Override public void run() {
callback.onSucess(value);
}
});
}

private static void closeQuietly(ARImpl impl) {
try {
impl.close();
} catch (Throwable t) {
Log.e(t);
}
}

private static ARImpl newImpl() {
return Display.getInstance().getARBackend();
}

// Identity comparison is intentional: only the exact ARSession instance
// returned from open() may clear the active slot.
@SuppressWarnings("PMD.CompareObjectsWithEquals")
static void clearActive(ARSession s) {
synchronized (ACTIVE_LOCK) {
if (active == s) {
active = null;
}
}
}
}
147 changes: 147 additions & 0 deletions CodenameOne/src/com/codename1/ar/ARAnchor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.ar;

/// A fixed position and orientation in the real world that the AR session
/// keeps tracking as its understanding of the environment improves. Anchors
/// are the attachment points for virtual content: create one with
/// `ARSession#createAnchor(ARPose)` or `ARHitResult#createAnchor()` and hang
/// an `ARNode` on it with `#setNode(ARNode)`.
///
/// The session refines anchor poses over time; updates are delivered through
/// the session's `ARAnchorListener` and reflected by `#getPose()`.
public class ARAnchor {
private final String id;
private ARPose pose;
private ARTrackingState trackingState = ARTrackingState.TRACKING;
private ARNode node;
private boolean detached;
ARSession session;

/// Creates an anchor. Intended for platform implementations and tests;
/// applications create anchors through the session.
///
/// #### Parameters
///
/// - `id`: the stable identifier of this anchor
///
/// - `pose`: the initial world pose
public ARAnchor(String id, ARPose pose) {
if (id == null || id.length() == 0) {
throw new IllegalArgumentException("id is required");
}
this.id = id;
this.pose = pose == null ? ARPose.IDENTITY : pose;
}

/// The stable identifier of this anchor.
public String getId() {
return id;
}

/// The current world pose. Updated on the EDT as tracking refines.
public ARPose getPose() {
return pose;
}

/// The tracking quality of this anchor.
public ARTrackingState getTrackingState() {
return trackingState;
}

/// The content root rendered at this anchor, or null when none is
/// attached.
public ARNode getNode() {
return node;
}

/// Attaches (or replaces) the content root rendered at this anchor. Pass
/// null to remove the content while keeping the anchor.
///
/// #### Parameters
///
/// - `node`: the root node to render at this anchor, or null
// Identity comparison is intentional: re-attaching the exact same node
// instance must not clear its session wiring first.
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public void setNode(ARNode node) {
if (detached) {
throw new IllegalStateException("anchor is detached");
}
if (node != null && node.getParent() != null) {
throw new IllegalArgumentException("the anchor content root may not have a parent node");
}
ARNode old = this.node;
if (old != null && old != node) {
old.session = null;
old.anchorId = null;
}
this.node = node;
if (node != null) {
node.session = session;
node.anchorId = id;
}
if (session != null) {
session.anchorNodeChangedInternal(this, node);
}
}

/// Removes this anchor (and any attached content) from the session.
/// No-op when already detached.
public void detach() {
if (detached) {
return;
}
detached = true;
if (node != null) {
node.session = null;
node.anchorId = null;
}
if (session != null) {
session.anchorDetachedInternal(this);
}
}

/// True once `#detach()` has been called or the platform removed the
/// anchor.
public boolean isDetached() {
return detached;
}

/// Updates the pose and tracking state from the platform. Called by the
/// session on the EDT.
void update(ARPose pose, ARTrackingState state) {
if (pose != null) {
this.pose = pose;
}
if (state != null) {
this.trackingState = state;
}
}

/// Marks the anchor detached without calling back into the session. Used
/// when the platform removes the anchor.
void markDetached() {
detached = true;
}
}
Loading
Loading