1

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;
        });
    }
Hemal Patel
  • 114
  • 1
  • 11
  • already told ya to change node to object. it is misleading ;) – kolboc Nov 06 '18 at 12:18
  • Refer to this answer: https://stackoverflow.com/a/53076322/6797400 – Shubham Agrawal Nov 13 '18 at 11:04
  • @ShubhamAgrawal, Thanks for your reply. I have referred the link which you give and try that one. But it does not work and gives an exception which is as below. java.lang.IllegalStateException: TransformableNode must have an AnchorNode as a parent. – Hemal Patel Nov 14 '18 at 05:42
  • @HemalPatel Can you please put your main activity code so that we can help with your error – Shubham Agrawal Nov 14 '18 at 06:10
  • @ShubhamAgrawal Here I have added my main activity where I am trying to do all these things. Please check it out. And for adding same scale node I have tried in a node gesture detector single tap event which you have suggested. – Hemal Patel Nov 14 '18 at 07:21
  • You have not set the parent for the anchorNode anywhere in the code that's why the error is coming @HemalPatel. See my answer below – Shubham Agrawal Nov 14 '18 at 09:19
  • I have already set anchor node after creating the anchor. It is created and initializes in the last method that I have mentioned in BaseActivity. Method name is **`addNodeToScene()`**. Check-in above code snippet. – Hemal Patel Nov 14 '18 at 11:47

1 Answers1

0

After you create the anchor and anchorNode you have to set its parent:

Anchor anchor = hitResult.createAnchor();
AnchorNode anchorNode = new AnchorNode(anchor);
anchorNode.setParent(arFragment.getArSceneView().getScene());
TransformableNode transformableNode = new TransformableNode(arFragment.getTransformationSystem());
transformableNode.setParent(anchorNode);
transformableNode.setRenderable(heightRenderable);
transformableNode.select();
ScaleController scaleController = transformableNode.getScaleController();
scaleController.setMaxScale(10f);
scaleController.setMinScale(0.01f);
transformableNode.setOnTapListener(this);
arFragment.getArSceneView().getScene().addOnUpdateListener(this);
lastAnchorNode = anchorNode;

See my github link: https://github.com/shubh261096/measurementapp

Shubham Agrawal
  • 1,252
  • 12
  • 29