I am trying to make one demo application in which I want to add some option like copy/paste selected node, remove the node, etc...
I have completed removing a node part. Now, I am trying to duplicate / copy-paste added a node after scaling and resize the node, but, not get success for duplicate the edited node in the plane. The duplicate node will be same as added or edited (scaling or resized) node.
Can anyone tell how can I duplicate/copy-paste that node in the application? Is there any easy way to do that?
My main activity code is as below:
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.BottomSheetDialog;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.AppCompatImageView;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.Toast;
import com.ar.furniture.R;
import com.ar.furniture.adapter.FurnitureAdapter;
import com.ar.furniture.model.FurnitureModel;
import com.ar.furniture.utils.Logger;
import com.ar.furniture.utils.PhotoUtils;
import com.ar.furniture.utils.PointerDrawable;
import com.ar.furniture.utils.Utils;
import com.google.ar.core.Frame;
import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.core.Trackable;
import com.google.ar.core.TrackingState;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.ux.ArFragment;
import java.util.ArrayList;
import java.util.List;
public class FurnitureActivity extends BaseActivity implements
View.OnLongClickListener, // View Long Click Listener
View.OnClickListener, // View Click Listener
DialogInterface.OnDismissListener, // Dialog Dismiss Listener
TextWatcher, // Edit Text TextWatcher
FurnitureAdapter.OnItemClickListener, // Furniture Item Click Listener
Scene.OnUpdateListener { // ARScene Update Listener
//AR Fragment
private ArFragment fragment;
private ImageView ivAddAR, ivClear, ivScreenShot;
//For BottomSheet dialog
private BottomSheetDialog mBottomSheetDialog;
private BottomSheetBehavior behavior;
//Adapter For RecyclerView
private FurnitureAdapter adapter;
//For Pointer Drawable
private PointerDrawable pointer = new PointerDrawable();
//For indicating if ARCore is in tracking state.
private boolean isTracking;
//For indicating the user is looking at a plane.
private boolean isHitting;
//For Avoiding double click on view click
private long mLastClickTime;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_furniture);
init();
}
//region View Click Events
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ivAddAR:
//region To avoid double click evnets
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
return;
}
mLastClickTime = SystemClock.elapsedRealtime();
//endregion
//Open Bottom Sheet Dialog
openARBottomSheetDialog();
break;
case R.id.ivClear:
clearNodes(fragment);
//removePreviousAnchors();
break;
case R.id.ivScreenShot:
PhotoUtils.takePhoto(fragment);
break;
default:
break;
}
}
//endregion
//region View Long Click Listener
@Override
public boolean onLongClick(View v) {
String message = "";
switch (v.getId()) {
case R.id.ivAddAR:
message = "Add 3D Image";
break;
case R.id.ivScreenShot:
message = "Take snap shot of view";
break;
case R.id.ivClear:
message = "Clear view";
break;
default:
break;
}
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
return true;
}
//endregion
//region Furniture Adapter
@Override
public void onClick(View view, FurnitureModel furnitureModel) {
switch (view.getId()) {
case R.id.tvFurnitureName:
case R.id.llFurniture:
case R.id.ivFurniture:
//region To avoid double Click events
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
return;
}
mLastClickTime = SystemClock.elapsedRealtime();
//endregion
//Dismiss Bottom Sheet Dialog
if (mBottomSheetDialog != null) {
mBottomSheetDialog.dismiss();
}
//Add 3D Object in ARSceneView
addObject(Uri.parse(/*"furniture_models/" + */furnitureModel.getFurnitureModel() + ".sfb"));
break;
default:
break;
}
}
//endregion
//region Bottom Sheet Dialog Dismiss Event
@Override
public void onDismiss(DialogInterface dialog) {
//Dismiss Bottom Sheet Dialog
dialog.dismiss();
//On Dismiss of bottom sheet dialog null initialize bottom sheet dialog
mBottomSheetDialog = null;
//Hide Keyboard
Utils.hideKeyboard(this);
}
//endregion
//region TextWatcher
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//Filter List
if (adapter != null) {
adapter.getFilter().filter(s.toString());
}
}
@Override
public void afterTextChanged(Editable s) {
}
//endregion
//region ARScene View Update Listener
@Override
public void onUpdate(FrameTime frameTime) {
/***
* To update ARCore View scene view
* First, update the tracking state. If ARCore is not tracking, remove the pointer until tracking is restored.
* Next, if ARCore is tracking, check for the gaze of the user hitting a plane detected by ARCore and enable the pointer accordingly.
*/
boolean trackingChanged = updateTracking();
View contentView = findViewById(android.R.id.content);
if (trackingChanged) {
if (isTracking) {
contentView.getOverlay().add(pointer);
} else {
contentView.getOverlay().remove(pointer);
}
contentView.invalidate();
}
if (isTracking) {
boolean hitTestChanged = updateHitTest();
if (hitTestChanged) {
pointer.setEnabled(isHitting);
contentView.invalidate();
}
}
}
//endregion
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
/***
* Initialize Views and other things for first time
*/
private void init() {
//Initialize ARFragment With Views Id
fragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.sceneFormFragment);
//Initialize Listener to the ARSceneView scene which will get called before processing every frame.
//In this listener we can make ARCore API calls and update the status of pointer.
if (fragment != null) {
//Add Update Listener
fragment.getArSceneView().getScene().addOnUpdateListener(this);
//To change plane color
/*fragment
.getArSceneView()
.getPlaneRenderer()
.getMaterial()
.thenAccept(material -> material.setFloat3(PlaneRenderer.MATERIAL_COLOR, new Color(0.0f, 0.0f, 1.0f, 1.0f)));*/
// Build texture sampler
/*Texture.Sampler sampler = Texture.Sampler.builder()
.setMinFilter(Texture.Sampler.MinFilter.LINEAR)
.setMagFilter(Texture.Sampler.MagFilter.LINEAR)
.setWrapMode(Texture.Sampler.WrapMode.REPEAT)
.build();
// Build texture with sampler
CompletableFuture<Texture> trigrid = Texture.builder()
.setSource(this, R.drawable.image)
.setSampler(sampler)
.build();
// Set plane texture
this.fragment.getArSceneView()
.getPlaneRenderer()
.getMaterial()
.thenAcceptBoth(trigrid, (material, texture) -> {
material.setTexture(PlaneRenderer.MATERIAL_TEXTURE, texture);
});*/
}
//region Initialize View
ivAddAR = findViewById(R.id.ivAddAR);
ivClear = findViewById(R.id.ivClear);
ivScreenShot = findViewById(R.id.ivScreenShot);
//endregion
//region Set View Click Listener
ivAddAR.setOnClickListener(this);
ivClear.setOnClickListener(this);
ivScreenShot.setOnClickListener(this);
//endregion
//region Set View Long Click Listener
ivAddAR.setOnLongClickListener(this);
ivClear.setOnLongClickListener(this);
ivScreenShot.setOnLongClickListener(this);
//endregion
//List of furniture model to show in list
List<FurnitureModel> alFurnitureModel = new ArrayList<>();
alFurnitureModel.add(new FurnitureModel("Table 1", "table_model_1", R.drawable.table_1));
alFurnitureModel.add(new FurnitureModel("Table 2", "table_model_2", R.drawable.table_2));
alFurnitureModel.add(new FurnitureModel("Table 3", "table_model_3", R.drawable.table_3));
//Initialize Adapter With Furniture list and its item click event
adapter = new FurnitureAdapter(alFurnitureModel);
adapter.setOnItemClickListener(this);
//Initialize Bottom sheet view and its behavior
View bottomSheet = findViewById(R.id.bottom_sheet);
behavior = BottomSheetBehavior.from(bottomSheet);
//Initialize Bottom sheet callback event
behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
switch (newState) {
case BottomSheetBehavior.STATE_DRAGGING:
Logger.e("BottomSheetCallback: BottomSheetBehavior.STATE_DRAGGING");
break;
case BottomSheetBehavior.STATE_SETTLING:
Logger.e("BottomSheetCallback: BottomSheetBehavior.STATE_SETTLING");
break;
case BottomSheetBehavior.STATE_EXPANDED:
Logger.e("BottomSheetCallback: BottomSheetBehavior.STATE_EXPANDED");
break;
case BottomSheetBehavior.STATE_COLLAPSED:
Logger.e("BottomSheetCallback: BottomSheetBehavior.STATE_COLLAPSED");
break;
case BottomSheetBehavior.STATE_HIDDEN:
Logger.e("BottomSheetCallback: BottomSheetBehavior.STATE_HIDDEN");
break;
default:
break;
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
Logger.e("BottomSheetCallback: slideOffset: " + slideOffset);
}
});
}
/***
* To open BottomSheet dialog for selecting AR image
*/
private void openARBottomSheetDialog() {
/*if (behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}*/
if (behavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
} else {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
//Initialize Bottom Sheet Dialog
mBottomSheetDialog = new BottomSheetDialog(this);
mBottomSheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
//Initialize View of bottom sheet dialog with its child views
View view = getLayoutInflater().inflate(R.layout.bottom_sheet_furniture, null);
AppCompatImageView ivClose = view.findViewById(R.id.ivClose);
AppCompatEditText edtSearch = view.findViewById(R.id.edtSearch);
RecyclerView rvFurniture = view.findViewById(R.id.rvFurniture);
//Set Recycler view layout manager and adapter
rvFurniture.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
rvFurniture.setAdapter(adapter);
//Initialize Edit text changed listener
edtSearch.addTextChangedListener(this);
//Initialize Close Button Click Event
ivClose.setOnClickListener(v -> {
//Hide Keyboard
Utils.hideKeyboard(FurnitureActivity.this);
//Dismiss Bottom Sheet Dialog
if (mBottomSheetDialog != null) {
mBottomSheetDialog.dismiss();
}
});
//Set Bottom Sheet Content View
mBottomSheetDialog.setContentView(view);
//Show Bottom Sheet Dialog
mBottomSheetDialog.show();
//Set Bottom Sheet Dismiss Listener Event
mBottomSheetDialog.setOnDismissListener(this);
}
/***
* Uses ARCore's camera state and returns.
* @return - true if the tracking state has changed since last call
*/
private boolean updateTracking() {
//Get ARFrame from ARSceneView
Frame frame = fragment.getArSceneView().getArFrame();
boolean wasTracking = isTracking;
isTracking = frame.getCamera().getTrackingState() == TrackingState.TRACKING;
return isTracking != wasTracking;
}
/***
* Uses ARCore to call Frame.hitTest()
* @return - As soon as any hit is detected, the method returns
*/
private boolean updateHitTest() {
Frame frame = fragment.getArSceneView().getArFrame();
android.graphics.Point pt = getScreenCenter();
List<HitResult> hits;
boolean wasHitting = isHitting;
isHitting = false;
if (frame != null) {
hits = frame.hitTest(pt.x, pt.y);
for (HitResult hit : hits) {
Trackable trackable = hit.getTrackable();
if ((trackable instanceof Plane
&& ((Plane) trackable).isPoseInPolygon(hit.getHitPose()))) {
isHitting = true;
break;
}
}
}
/*Vector3 position = gesture.getPosition();
List<HitResult> hitResultList = frame.hitTest(position.x, position.y);
for (int i = 0; i < hitResultList.size(); ++i) {
HitResult hit = (HitResult) hitResultList.get(i);
Trackable trackable = hit.getTrackable();
Pose pose = hit.getHitPose();
Node parent = this.getTransformableNode().getParent();
this.desiredLocalPosition = new Vector3(pose.tx(),parent.getLocalPosition().y, pose.tz());
if (parent != null && this.desiredLocalPosition != null) {
this.desiredLocalPosition = parent.worldToLocalPoint(this.desiredLocalPosition);
}
this.lastArHitResult = hit;
break;
}*/
return wasHitting != isHitting;
}
/***
* To add 3d object on click from gallery layout button
* @param model - 3d model uri
*/
private void addObject(Uri model) {
//Get ARFrame from ARSceneView
Frame frame = fragment.getArSceneView().getArFrame();
//Get center point of screen
android.graphics.Point pt = getScreenCenter();
List<HitResult> hits;
if (frame != null) {
hits = frame.hitTest(pt.x, pt.y);
for (HitResult hit : hits) {
Trackable trackable = hit.getTrackable();
if ((trackable instanceof Plane
&& ((Plane) trackable).isPoseInPolygon(hit.getHitPose()))) {
placeObject(fragment, hit.createAnchor(), model);
break;
}
}
}
}
}
The code for place object is added on base activity which is as below:
/***
* Take the ARCore anchor from the hitTest result and builds the Sceneform nodes.
* It starts asynchronous loading of the 3D model using the ModelRenderable builder.
* Note: Here we are using small models, but larger models could take substantially longer to load.
* @param fragment - ArFragment
* @param anchor - anchor points
* @param model - 3d model uri
*/
public void placeObject(ArFragment fragment, Anchor anchor, Uri model) {
//Code For Run static object from app
ModelRenderable
.builder()
.setSource(fragment.getContext(), model)
.build()
.thenAccept(renderable -> addNodeToScene(fragment, anchor, renderable))
.exceptionally((throwable -> {
AlertDialog.Builder builder = new AlertDialog
.Builder(this)
.setMessage(throwable.getMessage())
.setTitle(getString(R.string.error));
AlertDialog dialog = builder.create();
dialog.show();
return null;
}));
}
/***
* To add two nodes and attaches them to the ArSceneView's scene object.
* 1. AnchorNode: Anchor nodes are positioned based on the pose of an ARCore Anchor.
* As a result, they stay positioned in the sample place relative to the real world.
* 2. TransformableNode: We could use the base class type, Node for the placing the objects,
* but Node does not have the interaction functionality to handle moving, scaling
* and rotating based on user gestures.
* @param fragment - ARFragment
* @param anchor - anchor
* @param renderable - renderable
*/
private void addNodeToScene(ArFragment fragment, Anchor anchor, Renderable renderable) {
//Build Anchor Node using anchor points
AnchorNode anchorNode = new AnchorNode(anchor);
//Build Transformable Node using ArFragment
TransformableNode node = new TransformableNode(fragment.getTransformationSystem());
//Set renderable to transformable node
node.setRenderable(renderable);
// Set the min and max scales of the ScaleController.
// Default min is 0.75, default max is 1.75.
node.getScaleController().setMinScale(0.4f);
node.getScaleController().setMaxScale(2.0f);
// Set the local scale of the node BEFORE setting its parent
node.setLocalScale(new Vector3(0.55f, 0.55f, 0.55f));
//Set parent to anchor node
node.setParent(anchorNode);
//Add anchor node to ArView
fragment.getArSceneView().getScene().addChild(anchorNode);
//Set transformable node to be selectable
node.select();
//Double tap on node.
GestureDetector nodeGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
//For removing added node
node.getParent().removeChild(node);
return true;
//return super.onDoubleTap(e);
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
showPopupWindow(fragment, anchorNode, renderable, node);
return true;
//return super.onSingleTapUp(e);
}
});
//For Setting double click on node
node.setOnTouchListener((hitTestResult, motionEvent) -> {
nodeGestureDetector.onTouchEvent(motionEvent);
return true;
//return false;
});
}