package ru.ytkab0bp.slicebeam.view; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Region; import android.graphics.Typeface; import android.opengl.GLES30; import android.opengl.GLException; import android.opengl.GLSurfaceView; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.ViewConfiguration; import java.nio.IntBuffer; import java.util.ArrayList; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLDisplay; import ru.ytkab0bp.slicebeam.R; import ru.ytkab0bp.slicebeam.SliceBeam; import ru.ytkab0bp.slicebeam.events.LongClickTranslationEvent; import ru.ytkab0bp.slicebeam.render.GLRenderer; import ru.ytkab0bp.slicebeam.slic3r.GLModel; import ru.ytkab0bp.slicebeam.theme.IThemeView; import ru.ytkab0bp.slicebeam.theme.ThemesRepo; import ru.ytkab0bp.slicebeam.utils.Prefs; import ru.ytkab0bp.slicebeam.utils.Vec3d; import ru.ytkab0bp.slicebeam.utils.ViewUtils; public class GLView extends GLSurfaceView implements IThemeView { private GLRenderer renderer; private float lastX, lastY; private float lastLength; private int touchSlop; private boolean fromTwoPointers; private boolean onePointerGesture; private boolean twoPointerGesture; private boolean longClickGesture; private boolean isScaling; private Vec3d tempVec = new Vec3d(); private Vec3d longClickOffset = new Vec3d(); private Vec3d longClickTranslation = new Vec3d(); private ArrayList longClickHitResults = new ArrayList<>(); private long lastActionTime = System.currentTimeMillis(); private Runnable longClick = () -> { getRenderer().getBed().getRaycaster().raycast(getRenderer(), longClickHitResults, lastX, lastY); if (!longClickHitResults.isEmpty()) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); longClickGesture = true; GLModel.HitResult result = longClickHitResults.get(0); getRenderer().getModel().getTranslation(getRenderer().getSelectedObject(), tempVec); longClickOffset.x = result.position.x - tempVec.x; longClickOffset.y = result.position.y - tempVec.y; } }; private Path path = new Path(); private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint xferPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private TextPaint invalidBedText = new TextPaint(Paint.ANTI_ALIAS_FLAG); private StaticLayout invalidBedDescriptionLayout; private float lastScale; private long lastDraw; private float invalidOffset; public GLView(Context context) { super(context); xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); setEGLContextClientVersion(3); setEGLConfigChooser(new MultisampleConfigChooser()); renderer = new GLRenderer(this); setRenderer(renderer); setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setWillNotDraw(false); } public void arrange() { if (renderer.getModel() == null) return; queueEvent(() -> { renderer.getBed().arrange(renderer.getModel()); renderer.resetGlModels(); requestRender(); }); } public GLRenderer getRenderer() { return renderer; } public void drawOverlay(Canvas canvas, boolean toBitmap) { long dt = Math.min(System.currentTimeMillis() - lastDraw, 16); lastDraw = System.currentTimeMillis(); int rad = ViewUtils.dp(16); float offsetX = getTranslationX(), offsetY = -getTranslationY(); if (toBitmap) { paint.setColor(ThemesRepo.getColor(android.R.attr.windowBackground)); canvas.drawRect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY, paint); canvas.drawRoundRect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY, rad, rad, xferPaint); } else { path.rewind(); path.addRoundRect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY, rad, rad, Path.Direction.CW); canvas.save(); canvas.clipPath(path, Region.Op.DIFFERENCE); canvas.drawColor(ThemesRepo.getColor(android.R.attr.windowBackground)); canvas.restore(); if (getRenderer().getBed() != null && !getRenderer().getBed().isValid()) { invalidOffset += dt / 10000f; paint.setColor(ThemesRepo.getColor(android.R.attr.windowBackground)); int size = ViewUtils.dp(200); canvas.drawRect(0, (getHeight() - size) / 2f, getWidth(), (getHeight() + size) / 2f, paint); double angle = Math.toRadians(60); int stableWidth = ViewUtils.dp(16); int lineHeight = ViewUtils.dp(16); int lineWidth = ViewUtils.dp(16 + (float) (32 * Math.sin(angle))); Path linePath = new Path(); linePath.moveTo(0, 0); linePath.lineTo(stableWidth, 0); linePath.lineTo(lineWidth, lineHeight); linePath.lineTo(lineWidth - stableWidth, lineHeight); linePath.lineTo(0, 0); linePath.close(); paint.setColor(ThemesRepo.getColor(android.R.attr.colorAccent)); int x = (int) (-lineWidth - invalidOffset * getWidth()); while (x < getWidth()) { canvas.save(); canvas.translate(x, (getHeight() - size) / 2f); canvas.drawPath(linePath, paint); canvas.translate(stableWidth, 0); int alpha = paint.getAlpha(); paint.setAlpha((int) (alpha * 0.5f)); canvas.drawPath(linePath, paint); canvas.restore(); paint.setAlpha(alpha); x += stableWidth * 2; } x = (int) (getWidth() + lineWidth + invalidOffset * getWidth()); while (x >= -lineWidth) { canvas.save(); canvas.translate(x, (getHeight() + size) / 2f - lineHeight); canvas.drawPath(linePath, paint); canvas.translate(-stableWidth, 0); int alpha = paint.getAlpha(); paint.setAlpha((int) (alpha * 0.5f)); canvas.drawPath(linePath, paint); canvas.restore(); paint.setAlpha(alpha); x -= stableWidth * 2; } if (invalidBedDescriptionLayout == null) { invalidBedText.setTextSize(ViewUtils.dp(16)); invalidBedText.setColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); invalidBedText.setTypeface(Typeface.DEFAULT); invalidBedDescriptionLayout = new StaticLayout(getContext().getString(R.string.BedConfigurationErrorDesc), invalidBedText, getWidth() - ViewUtils.dp(32), Layout.Alignment.ALIGN_CENTER, 1, 0, false); } int realTextSize = ViewUtils.dp(22); int padding = ViewUtils.dp(12); int totalHeight = realTextSize + invalidBedDescriptionLayout.getHeight(); invalidBedText.setTextSize(ViewUtils.dp(18)); invalidBedText.setColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); invalidBedText.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); String errString = getContext().getString(R.string.BedConfigurationError); canvas.drawText(errString, 0, errString.length(), (getWidth() - invalidBedText.measureText(errString)) / 2f, getHeight() / 2f - totalHeight / 2f + realTextSize - padding / 2f, invalidBedText); invalidBedText.setTextSize(ViewUtils.dp(16)); invalidBedText.setColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); invalidBedText.setTypeface(Typeface.DEFAULT); canvas.save(); canvas.translate((getWidth() - invalidBedDescriptionLayout.getWidth()) / 2f, getHeight() / 2f + totalHeight / 2f - invalidBedDescriptionLayout.getHeight() + padding / 2f); invalidBedDescriptionLayout.draw(canvas); canvas.restore(); invalidate(); } } } @Override public void draw(Canvas canvas) { super.draw(canvas); drawOverlay(canvas, false); } public Bitmap snapshotBitmap() { int w = getWidth(), h = getHeight(); int[] bitmapBuffer = new int[w * h]; int[] bitmapSource = new int[w * h]; IntBuffer intBuffer = IntBuffer.wrap(bitmapBuffer); intBuffer.position(0); try { GLES30.glReadPixels(0, 0, w, h, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, intBuffer); int offset1, offset2; for (int i = 0; i < h; i++) { offset1 = i * w; offset2 = (h - i - 1) * w; for (int j = 0; j < w; j++) { int texturePixel = bitmapBuffer[offset1 + j]; int blue = (texturePixel >> 16) & 0xff; int red = (texturePixel << 16) & 0x00ff0000; int pixel = (texturePixel & 0xff00ff00) | red | blue; bitmapSource[offset2 + j] = pixel; } } } catch (GLException e) { throw new RuntimeException(e); } return Bitmap.createBitmap(bitmapSource, w, h, Bitmap.Config.ARGB_8888); } public Bitmap snapshotBitmap(boolean hideBed) { if (!hideBed) { return snapshotBitmap(); } boolean prev = renderer.isBedVisible(); renderer.setBedVisible(false); renderer.onDrawFrame(null); Bitmap snapshot = snapshotBitmap(); renderer.setBedVisible(prev); return snapshot; } public Bitmap snapshotBitmap(boolean hideBed, int scaleFactor) { if (scaleFactor <= 1) { return snapshotBitmap(hideBed); } int w = getWidth(); int h = getHeight(); if (w <= 0 || h <= 0) { return null; } return renderer.renderToBitmap(w * scaleFactor, h * scaleFactor, hideBed, false); } public Bitmap snapshotBitmap(int width, int height, boolean hideBed) { return renderer.renderToBitmap(width, height, hideBed, false); } public Bitmap snapshotBitmap(int width, int height, boolean hideBed, boolean topView) { return renderer.renderToBitmap(width, height, hideBed, topView); } private static final class MultisampleConfigChooser implements EGLConfigChooser { private static final int EGL_OPENGL_ES2_BIT = 4; @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] configSpec = { EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8, EGL10.EGL_ALPHA_SIZE, 8, EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_SAMPLE_BUFFERS, 1, EGL10.EGL_SAMPLES, 4, EGL10.EGL_NONE }; EGLConfig config = chooseConfig(egl, display, configSpec); if (config != null) { return config; } int[] fallbackSpec = { EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8, EGL10.EGL_ALPHA_SIZE, 8, EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_NONE }; return chooseConfig(egl, display, fallbackSpec); } private EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, int[] configSpec) { int[] numConfigs = new int[1]; if (!egl.eglChooseConfig(display, configSpec, null, 0, numConfigs)) { return null; } int count = numConfigs[0]; if (count <= 0) { return null; } EGLConfig[] configs = new EGLConfig[count]; if (!egl.eglChooseConfig(display, configSpec, configs, count, numConfigs)) { return null; } return configs[0]; } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } private void calcStartFocus(MotionEvent e) { lastX = (e.getX(0) + e.getX(1)) / 2f; lastY = (e.getY(0) + e.getY(1)) / 2f; float x = e.getX(0) - e.getX(1), y = e.getY(0) - e.getY(1); lastLength = (float) Math.sqrt(x * x + y * y); } @Override public boolean onHoverEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT ? renderer.stopHover() : renderer.hover(event.getX() * Prefs.getRenderScale(), event.getY() * Prefs.getRenderScale())) { queueEvent(this::requestRender); } return super.onHoverEvent(event); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent e) { long deltaMs = System.currentTimeMillis() - lastActionTime; lastActionTime = System.currentTimeMillis(); int action = e.getActionMasked(); if (e.getPointerCount() > 2) { removeCallbacks(longClick); longClickGesture = false; return true; } if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { if (e.getPointerCount() == 2) { removeCallbacks(longClick); longClickGesture = false; calcStartFocus(e); fromTwoPointers = true; } else { lastX = e.getX(); lastY = e.getY(); int j = renderer.raycastObjectIndex(lastX, lastY); if (renderer.getGcodeResult() == null && j == renderer.getSelectedObject() && j != -1) { postDelayed(longClick, 300); } } return true; } if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_CANCEL) { removeCallbacks(longClick); if (longClickGesture) { queueEvent(()->{ int j = getRenderer().getSelectedObject(); getRenderer().getModel().getTranslation(j, tempVec); getRenderer().setSelectionTranslation(0, 0, 0); getRenderer().getModel().translate(j, longClickTranslation.x, longClickTranslation.y, 0); getRenderer().invalidateGlModel(j); requestRender(); SliceBeam.EVENT_BUS.fireEvent(new LongClickTranslationEvent(longClickTranslation.x, longClickTranslation.y, false)); }); } longClickGesture = false; if (fromTwoPointers) { if (e.getPointerCount() == 1) { fromTwoPointers = false; isScaling = false; twoPointerGesture = false; lastActionTime = 0; } return true; } if (e.getPointerCount() == 1) { if (!onePointerGesture && action != MotionEvent.ACTION_CANCEL) { if (renderer.onClick(e.getX() * Prefs.getRenderScale(), e.getY() * Prefs.getRenderScale())) { requestRender(); } } lastX = e.getX(0); lastY = e.getY(0); onePointerGesture = false; } // TODO: Rotate with inertia? return true; } if (action == MotionEvent.ACTION_MOVE) { if (e.getPointerCount() == 2) { float x = (e.getX(0) + e.getX(1)) / 2f; float y = (e.getY(0) + e.getY(1)) / 2f; float lenX = e.getX(0) - e.getX(1), lenY = e.getY(0) - e.getY(1); float len = (float) Math.sqrt(lenX * lenX + lenY * lenY); float distanceX = lastX - x, distanceY = lastY - y; if (deltaMs > 128) { isScaling = false; twoPointerGesture = false; } boolean startingGesture = false; if (!isScaling && !twoPointerGesture) { if (Math.abs(distanceX) < touchSlop && Math.abs(distanceY) < touchSlop && Math.abs(len - lastLength) > touchSlop * 1.5f) { isScaling = true; startingGesture = true; } else if (Math.sqrt(distanceX * distanceX + distanceY * distanceY) >= touchSlop) { twoPointerGesture = true; startingGesture = true; } } if (isScaling) { float delta = len - lastLength; lastLength = len; if (!startingGesture) { renderer.getCamera().zoom(delta / touchSlop * Prefs.getCameraSensitivity()); renderer.updateProjection(); requestRender(); } lastX = x; lastY = y; } else if (twoPointerGesture) { if (!startingGesture) { int mode = Prefs.getCameraControlMode(); if (mode == Prefs.CAMERA_CONTROL_MODE_ROTATE_MOVE || mode == Prefs.CAMERA_CONTROL_MODE_MOVE_ONLY) { renderer.getCamera().move(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity()); } else { renderer.getCamera().rotateAround(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity()); } requestRender(); } lastX = x; lastY = y; } } else if (!fromTwoPointers) { float distanceX = lastX - e.getX(), distanceY = lastY - e.getY(); boolean startingGesture = false; if (!onePointerGesture) { if (Math.sqrt(distanceX * distanceX + distanceY * distanceY) >= touchSlop) { onePointerGesture = true; startingGesture = true; removeCallbacks(longClick); } } if (onePointerGesture) { if (!startingGesture) { if (longClickGesture) { getRenderer().getModel().getTranslation(getRenderer().getSelectedObject(), tempVec); getRenderer().getBed().getRaycaster().raycast(getRenderer(), longClickHitResults, e.getX(), e.getY()); if (!longClickHitResults.isEmpty()) { GLModel.HitResult result = longClickHitResults.get(0); longClickTranslation.x = result.position.x - tempVec.x - longClickOffset.x; longClickTranslation.y = result.position.y - tempVec.y - longClickOffset.y; getRenderer().setSelectionTranslation(longClickTranslation.x, longClickTranslation.y, 0); SliceBeam.EVENT_BUS.fireEvent(new LongClickTranslationEvent(longClickTranslation.x, longClickTranslation.y, true)); } requestRender(); } else { int mode = Prefs.getCameraControlMode(); if (mode == Prefs.CAMERA_CONTROL_MODE_ROTATE_MOVE) { renderer.getCamera().rotateAround(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity()); } else { renderer.getCamera().move(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity()); } requestRender(); } } lastX = e.getX(); lastY = e.getY(); } } } return true; } private void applyScale() { int w = getWidth(), h = getHeight(); float realScale = Math.round(w * (lastScale = Prefs.getRenderScale())) / (float) w; getHolder().setFixedSize((int) (w * realScale), (int) (h * realScale)); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (w != 0 && h != 0) { applyScale(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (getWidth() > 0 && getHeight() > 0 && lastScale != Prefs.getRenderScale()) { applyScale(); } } @Override public void surfaceCreated(SurfaceHolder holder) { super.surfaceCreated(holder); requestRender(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { super.surfaceChanged(holder, format, w, h); requestRender(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { super.surfaceDestroyed(holder); renderer.onDestroy(); } @Override public void onApplyTheme() { requestRender(); } }