From 261ba81e06eec089067ec040eab48367bde13e40 Mon Sep 17 00:00:00 2001 From: Dark98 Date: Thu, 22 Jan 2026 01:40:36 +0000 Subject: [PATCH] Basic G-Code Thumbnail Generation Support --- .../slicebeam/fragment/BedFragment.java | 15 +- .../fragment/PrinterConfigFragment.java | 3 +- .../ytkab0bp/slicebeam/render/GLRenderer.java | 205 ++++++++++- .../slicebeam/slic3r/GCodeThumbnailer.java | 318 ++++++++++++++++++ .../slicebeam/slic3r/PrintConfigDef.java | 3 +- .../slicebeam/slic3r/Slic3rConfigWrapper.java | 2 +- .../ru/ytkab0bp/slicebeam/view/GLView.java | 88 +++++ app/src/main/jni/libslic3r/GCode.cpp | 6 +- 8 files changed, 627 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/GCodeThumbnailer.java diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java index d202d73..5649e1e 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java @@ -51,6 +51,7 @@ import ru.ytkab0bp.slicebeam.events.SlicingProgressEvent; import ru.ytkab0bp.slicebeam.navigation.Fragment; import ru.ytkab0bp.slicebeam.slic3r.Bed3D; import ru.ytkab0bp.slicebeam.slic3r.GCodeProcessorResult; +import ru.ytkab0bp.slicebeam.slic3r.GCodeThumbnailer; import ru.ytkab0bp.slicebeam.slic3r.Model; import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError; import ru.ytkab0bp.slicebeam.theme.ThemesRepo; @@ -412,12 +413,14 @@ public class BedFragment extends Fragment { }); } - if (!DEBUG_VIEWER) { - gCodeResult = glView.getRenderer().getModel().slice(cfg.getAbsolutePath(), gcode.getAbsolutePath(), (progress, text) -> SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(progress, text))); - SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, "")); - } else { - gCodeResult = new GCodeProcessorResult(gcode); - } + if (!DEBUG_VIEWER) { + gCodeResult = glView.getRenderer().getModel().slice(cfg.getAbsolutePath(), gcode.getAbsolutePath(), (progress, text) -> SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(progress, text))); + GCodeThumbnailer.addThumbnailsToGcode(gcode, SliceBeam.buildCurrentConfigObject(), glView); + SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, "")); + } else { + gCodeResult = new GCodeProcessorResult(gcode); + GCodeThumbnailer.addThumbnailsToGcode(gcode, SliceBeam.buildCurrentConfigObject(), glView); + } ViewUtils.postOnMainThread(()-> { glView.queueEvent(()->{ glView.getRenderer().setGCodeViewer(gCodeResult); diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java index be3b761..fcdb43a 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java @@ -45,8 +45,7 @@ public class PrinterConfigFragment extends ProfileListFragment { new OptionElement(new SubHeader("Firmware")), new OptionElement(def.options.get("gcode_flavor")), - // TODO: Thumbnails are not working *yet* -// new OptionElement(def.options.get("thumbnails")), + new OptionElement(def.options.get("thumbnails")), new OptionElement(def.options.get("silent_mode")), new OptionElement(def.options.get("remaining_times")), new OptionElement(def.options.get("binary_gcode")), diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/render/GLRenderer.java b/app/src/main/java/ru/ytkab0bp/slicebeam/render/GLRenderer.java index 707a8c5..25f9ebd 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/render/GLRenderer.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/render/GLRenderer.java @@ -3,6 +3,7 @@ package ru.ytkab0bp.slicebeam.render; import static android.opengl.GLES30.*; import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue; +import android.graphics.Bitmap; import android.graphics.Color; import android.opengl.GLSurfaceView; import android.util.Log; @@ -12,6 +13,8 @@ import androidx.core.graphics.ColorUtils; import java.util.ArrayList; import java.util.List; +import java.nio.IntBuffer; + import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -51,6 +54,7 @@ public class GLRenderer implements GLSurfaceView.Renderer { // Instance values, should be released private Bed3D bed; + private boolean bedVisible = true; private int lastConfigUid; private GLShadersManager shadersManager; private GLModel backgroundModel; @@ -77,6 +81,7 @@ public class GLRenderer implements GLSurfaceView.Renderer { private Vec3d bbMin = new Vec3d(), bbMax = new Vec3d(); private boolean isInFlattenMode; private ArrayList flattenPlanes = new ArrayList<>(); + private static final double TOP_VIEW_MARGIN = 1.1; public Camera getCamera() { return camera; @@ -86,6 +91,204 @@ public class GLRenderer implements GLSurfaceView.Renderer { return bed; } + public void setBedVisible(boolean visible) { + bedVisible = visible; + } + + public boolean isBedVisible() { + return bedVisible; + } + + public Bitmap renderToBitmap(int width, int height, boolean hideBed) { + return renderToBitmap(width, height, hideBed, false); + } + + public Bitmap renderToBitmap(int width, int height, boolean hideBed, boolean topView) { + if (width <= 0 || height <= 0) { + return null; + } + + int[] fbo = new int[1]; + int[] texture = new int[1]; + int[] depth = new int[1]; + int[] fboMsaa = new int[1]; + int[] colorMsaa = new int[1]; + int[] depthMsaa = new int[1]; + + glGenFramebuffers(1, fbo, 0); + glGenTextures(1, texture, 0); + glGenRenderbuffers(1, depth, 0); + + glBindTexture(GL_TEXTURE_2D, texture[0]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null); + + glBindRenderbuffer(GL_RENDERBUFFER, depth[0]); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height); + + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture[0], 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth[0]); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteRenderbuffers(1, depth, 0); + glDeleteTextures(1, texture, 0); + glDeleteFramebuffers(1, fbo, 0); + return null; + } + + boolean useMsaa = true; + if (useMsaa) { + glGenFramebuffers(1, fboMsaa, 0); + glGenRenderbuffers(1, colorMsaa, 0); + glGenRenderbuffers(1, depthMsaa, 0); + + glBindRenderbuffer(GL_RENDERBUFFER, colorMsaa[0]); + glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_RGBA8, width, height); + + glBindRenderbuffer(GL_RENDERBUFFER, depthMsaa[0]); + glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT16, width, height); + + glBindFramebuffer(GL_FRAMEBUFFER, fboMsaa[0]); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorMsaa[0]); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthMsaa[0]); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + useMsaa = false; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteRenderbuffers(1, depthMsaa, 0); + glDeleteRenderbuffers(1, colorMsaa, 0); + glDeleteFramebuffers(1, fboMsaa, 0); + } + } + + int prevWidth = viewportWidth; + int prevHeight = viewportHeight; + boolean prevBed = bedVisible; + CameraState prevCamera = null; + + viewportWidth = width; + viewportHeight = height; + if (useMsaa) { + glBindFramebuffer(GL_FRAMEBUFFER, fboMsaa[0]); + } else { + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + } + glViewport(0, 0, width, height); + bedVisible = !hideBed; + if (topView) { + prevCamera = applyTopViewCamera(); + } + updateProjection(); + + onDrawFrame(null); + + if (useMsaa) { + glBindFramebuffer(GL_READ_FRAMEBUFFER, fboMsaa[0]); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]); + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + } + + Bitmap bitmap = readPixelsToBitmap(width, height); + + if (prevCamera != null) { + restoreCamera(prevCamera); + } + bedVisible = prevBed; + viewportWidth = prevWidth; + viewportHeight = prevHeight; + glViewport(0, 0, prevWidth, prevHeight); + updateProjection(); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (useMsaa) { + glDeleteRenderbuffers(1, depthMsaa, 0); + glDeleteRenderbuffers(1, colorMsaa, 0); + glDeleteFramebuffers(1, fboMsaa, 0); + } + glDeleteRenderbuffers(1, depth, 0); + glDeleteTextures(1, texture, 0); + glDeleteFramebuffers(1, fbo, 0); + + return bitmap; + } + + private static Bitmap readPixelsToBitmap(int width, int height) { + int[] buffer = new int[width * height]; + int[] source = new int[width * height]; + IntBuffer intBuffer = IntBuffer.wrap(buffer); + intBuffer.position(0); + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, intBuffer); + int offset1, offset2; + for (int i = 0; i < height; i++) { + offset1 = i * width; + offset2 = (height - i - 1) * width; + for (int j = 0; j < width; j++) { + int texturePixel = buffer[offset1 + j]; + int blue = (texturePixel >> 16) & 0xff; + int red = (texturePixel << 16) & 0x00ff0000; + source[offset2 + j] = (texturePixel & 0xff00ff00) | red | blue; + } + } + return Bitmap.createBitmap(source, width, height, Bitmap.Config.ARGB_8888); + } + + private CameraState applyTopViewCamera() { + if (bed == null || !bed.isValid()) { + return null; + } + Vec3d min; + Vec3d max; + if (model != null && model.getObjectsCount() > 0) { + min = model.getBoundingBoxApproxMin(); + max = model.getBoundingBoxApproxMax(); + } else { + min = bed.getVolumeMin(); + max = bed.getVolumeMax(); + } + Vec3d center = min.center(max); + double size = Math.max(max.x - min.x, max.y - min.y); + if (size <= 0) { + size = 1; + } + double fov = Math.toRadians(FOV); + double distance = (size / 2.0) / Math.tan(fov / 2.0); + distance *= TOP_VIEW_MARGIN; + + CameraState state = new CameraState(camera); + camera.origin.set(center); + camera.position.set(center.x, center.y, max.z + distance); + camera.up.set(0, 1, 0); + camera.setZoom(1f); + return state; + } + + private void restoreCamera(CameraState state) { + camera.position.set(state.position); + camera.origin.set(state.origin); + camera.up.set(state.up); + camera.setZoom(state.zoom); + } + + private static final class CameraState { + final Vec3d position; + final Vec3d origin; + final Vec3d up; + final float zoom; + + CameraState(Camera camera) { + position = new Vec3d(camera.position); + origin = new Vec3d(camera.origin); + up = new Vec3d(camera.up); + zoom = camera.getZoom(); + } + } + public double[] getProjectionMatrix() { return projectionMatrix; } @@ -225,7 +428,7 @@ public class GLRenderer implements GLSurfaceView.Renderer { if (lastConfigUid != SliceBeam.CONFIG_UID) { configureBed(); } - if (bed.isValid()) { + if (bed.isValid() && bedVisible) { bed.render(shadersManager, bottom, camera.getViewModelMatrix(), projectionMatrix, 1f / camera.getZoom()); } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/GCodeThumbnailer.java b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/GCodeThumbnailer.java new file mode 100644 index 0000000..3b6ba07 --- /dev/null +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/GCodeThumbnailer.java @@ -0,0 +1,318 @@ +package ru.ytkab0bp.slicebeam.slic3r; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.Base64; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ru.ytkab0bp.slicebeam.config.ConfigObject; +import ru.ytkab0bp.slicebeam.view.GLView; + +public final class GCodeThumbnailer { + private static final String TAG = "GCodeThumbnailer"; + private static final int MAX_ROW_LENGTH = 78; + private static final int THUMBNAIL_SUPERSAMPLE = 16; + private static final int MAX_SUPERSAMPLE_PIXELS = 16_000_000; + private static final int MAX_SUPERSAMPLE_DIM = 4096; + private static final Pattern SIZE_PATTERN = Pattern.compile("^(\\d+)\\s*[xX]\\s*(\\d+)$"); + + private GCodeThumbnailer() {} + + public static boolean addThumbnailsToGcode(File gcodeFile, ConfigObject config, GLView glView) { + if (gcodeFile == null || config == null || glView == null) { + return false; + } + if (!gcodeFile.exists()) { + return false; + } + String binaryGcode = config.get("binary_gcode"); + if ("1".equals(binaryGcode)) { + return false; + } + if (gcodeHasThumbnail(gcodeFile)) { + return false; + } + String thumbnails = config.get("thumbnails"); + if (thumbnails == null || thumbnails.trim().isEmpty()) { + return false; + } + String defaultFormat = "PNG"; + + List specs = parseThumbnailSpecs(thumbnails, defaultFormat); + if (specs.isEmpty()) { + return false; + } + + String header = buildHeader(specs, glView); + if (header.isEmpty()) { + return false; + } + + try { + return prependToFile(gcodeFile, header); + } catch (IOException e) { + Log.e(TAG, "Failed to add thumbnails to gcode", e); + return false; + } + } + + private static List parseThumbnailSpecs(String thumbnails, String defaultFormat) { + List specs = new ArrayList<>(); + for (String raw : thumbnails.split(",")) { + String token = raw.trim(); + if (token.isEmpty()) { + continue; + } + if (token.contains("COLPIC")) { + continue; + } + String format = defaultFormat; + int slash = token.indexOf('/'); + if (slash >= 0) { + format = token.substring(slash + 1).trim(); + token = token.substring(0, slash).trim(); + } + Matcher matcher = SIZE_PATTERN.matcher(token); + if (!matcher.matches()) { + continue; + } + int width = Integer.parseInt(matcher.group(1)); + int height = Integer.parseInt(matcher.group(2)); + if (width <= 0 || height <= 0) { + continue; + } + specs.add(new ThumbnailSpec(width, height, normalizeFormat(format))); + } + return specs; + } + + private static ThumbnailFormat normalizeFormat(String format) { + if (format == null) { + return ThumbnailFormat.PNG; + } + switch (format.trim().toUpperCase(Locale.US)) { + case "JPG": + case "JPEG": + return ThumbnailFormat.JPG; + case "QOI": + return ThumbnailFormat.QOI; + default: + return ThumbnailFormat.PNG; + } + } + + private static Bitmap captureSnapshot(GLView glView, int targetWidth, int targetHeight) { + CountDownLatch latch = new CountDownLatch(1); + Bitmap[] ref = new Bitmap[1]; + int scale = computeSupersampleScale(targetWidth, targetHeight); + int width = targetWidth * scale; + int height = targetHeight * scale; + glView.queueEvent(() -> { + try { + ref[0] = glView.snapshotBitmap(width, height, true, true); + } catch (OutOfMemoryError e) { + Log.e(TAG, "Thumbnail snapshot OOM", e); + } catch (Exception e) { + Log.e(TAG, "Failed to capture GL snapshot", e); + } finally { + latch.countDown(); + } + }); + try { + if (!latch.await(3, TimeUnit.SECONDS)) { + Log.w(TAG, "Timed out waiting for GL snapshot"); + return null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + Bitmap snapshot = ref[0]; + if (snapshot == null) { + return null; + } + if (scale <= 1) { + return snapshot; + } + Bitmap downscaled = downscaleBitmap(snapshot, targetWidth, targetHeight); + if (downscaled != snapshot) { + snapshot.recycle(); + } + return downscaled; + } + + private static int computeSupersampleScale(int w, int h) { + if (w <= 0 || h <= 0) { + return 1; + } + int scale = THUMBNAIL_SUPERSAMPLE; + while (scale > 1) { + long pixels = (long) w * h * scale * scale; + if (pixels <= MAX_SUPERSAMPLE_PIXELS && w * scale <= MAX_SUPERSAMPLE_DIM && h * scale <= MAX_SUPERSAMPLE_DIM) { + break; + } + scale--; + } + return Math.max(1, scale); + } + + private static String buildHeader(List specs, GLView glView) { + StringBuilder header = new StringBuilder(); + boolean wroteThumbnail = false; + for (ThumbnailSpec spec : specs) { + Bitmap snapshot = captureSnapshot(glView, spec.width, spec.height); + if (snapshot == null) { + continue; + } + ThumbnailFormat format = spec.format; + if (format == ThumbnailFormat.QOI) { + Log.w(TAG, "QOI thumbnails not supported, falling back to PNG"); + format = ThumbnailFormat.PNG; + } + Bitmap scaled = snapshot; + if (snapshot.getWidth() != spec.width || snapshot.getHeight() != spec.height) { + scaled = downscaleBitmap(snapshot, spec.width, spec.height); + snapshot.recycle(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Bitmap.CompressFormat compressFormat = format == ThumbnailFormat.JPG + ? Bitmap.CompressFormat.JPEG + : Bitmap.CompressFormat.PNG; + int quality = format == ThumbnailFormat.JPG ? 90 : 100; + if (!scaled.compress(compressFormat, quality, out)) { + scaled.recycle(); + continue; + } + scaled.recycle(); + byte[] data = out.toByteArray(); + if (data.length == 0) { + continue; + } + String tag = format == ThumbnailFormat.JPG ? "thumbnail_JPG" : "thumbnail"; + if (!wroteThumbnail) { + header.append("; THUMBNAIL_BLOCK_START\n"); + wroteThumbnail = true; + } + String encoded = Base64.encodeToString(data, Base64.NO_WRAP); + header.append("\n;\n; ").append(tag) + .append(" begin ").append(spec.width).append("x").append(spec.height) + .append(" ").append(encoded.length()).append("\n"); + int offset = 0; + while (offset < encoded.length()) { + int end = Math.min(offset + MAX_ROW_LENGTH, encoded.length()); + header.append("; ").append(encoded, offset, end).append("\n"); + offset = end; + } + header.append("; ").append(tag).append(" end\n;\n"); + } + if (wroteThumbnail) { + header.append("; THUMBNAIL_BLOCK_END\n"); + } + return header.toString(); + } + + private static Bitmap downscaleBitmap(Bitmap src, int targetWidth, int targetHeight) { + if (src.getWidth() == targetWidth && src.getHeight() == targetHeight) { + return src; + } + Bitmap current = src; + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + while (current.getWidth() / 2 >= targetWidth && current.getHeight() / 2 >= targetHeight) { + int nextWidth = Math.max(targetWidth, current.getWidth() / 2); + int nextHeight = Math.max(targetHeight, current.getHeight() / 2); + Bitmap next = Bitmap.createBitmap(nextWidth, nextHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(next); + canvas.drawBitmap(current, null, new Rect(0, 0, nextWidth, nextHeight), paint); + if (current != src) { + current.recycle(); + } + current = next; + } + if (current.getWidth() != targetWidth || current.getHeight() != targetHeight) { + Bitmap finalBmp = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(finalBmp); + canvas.drawBitmap(current, null, new Rect(0, 0, targetWidth, targetHeight), paint); + if (current != src) { + current.recycle(); + } + current = finalBmp; + } + return current; + } + + private static boolean prependToFile(File gcodeFile, String header) throws IOException { + File tmp = new File(gcodeFile.getParentFile(), gcodeFile.getName() + ".thumbtmp"); + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(gcodeFile)); + BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tmp))) { + out.write(header.getBytes(StandardCharsets.US_ASCII)); + byte[] buffer = new byte[8192]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } + if (!gcodeFile.delete()) { + tmp.delete(); + return false; + } + if (!tmp.renameTo(gcodeFile)) { + tmp.delete(); + return false; + } + return true; + } + + private static boolean gcodeHasThumbnail(File gcodeFile) { + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(gcodeFile))) { + byte[] buffer = new byte[8192]; + StringBuilder sb = new StringBuilder(); + int total = 0; + int read; + while ((read = in.read(buffer)) != -1 && total < 262144) { + sb.append(new String(buffer, 0, read, StandardCharsets.US_ASCII)); + if (sb.indexOf("thumbnail begin") != -1 || sb.indexOf("thumbnail_JPG begin") != -1) { + return true; + } + total += read; + } + } catch (IOException ignored) { + } + return false; + } + + private static final class ThumbnailSpec { + final int width; + final int height; + final ThumbnailFormat format; + + ThumbnailSpec(int width, int height, ThumbnailFormat format) { + this.width = width; + this.height = height; + this.format = format; + } + } + + private enum ThumbnailFormat { + PNG, + JPG, + QOI + } +} diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/PrintConfigDef.java b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/PrintConfigDef.java index 5f179d6..a9e8393 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/PrintConfigDef.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/PrintConfigDef.java @@ -16,7 +16,8 @@ public class PrintConfigDef { "tilt_up_finish_speed", "tilt_down_initial_speed", "tilt_down_finish_speed", - "tower_speed" + "tower_speed", + "thumbnails_format" ); private static PrintConfigDef instance; diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java index 66a504e..4369a6c 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java @@ -90,7 +90,7 @@ public class Slic3rConfigWrapper { "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "multimaterial_purging", "max_print_height", "default_print_profile", "inherits", "remaining_times", "silent_mode", - "machine_limits_usage", "thumbnails", "thumbnails_format", + "machine_limits_usage", "thumbnails", "machine_max_acceleration_extruding", "machine_max_acceleration_retracting", "machine_max_acceleration_travel", "machine_max_acceleration_x", "machine_max_acceleration_y", "machine_max_acceleration_z", "machine_max_acceleration_e", "machine_max_feedrate_x", "machine_max_feedrate_y", "machine_max_feedrate_z", "machine_max_feedrate_e", diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/view/GLView.java b/app/src/main/java/ru/ytkab0bp/slicebeam/view/GLView.java index 8ebe531..851272f 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/view/GLView.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/view/GLView.java @@ -25,6 +25,10 @@ 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; @@ -85,6 +89,7 @@ public class GLView extends GLSurfaceView implements IThemeView { xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); setEGLContextClientVersion(3); + setEGLConfigChooser(new MultisampleConfigChooser()); renderer = new GLRenderer(this); setRenderer(renderer); @@ -242,6 +247,89 @@ public class GLView extends GLSurfaceView implements IThemeView { 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); diff --git a/app/src/main/jni/libslic3r/GCode.cpp b/app/src/main/jni/libslic3r/GCode.cpp index 9d541ac..734a402 100644 --- a/app/src/main/jni/libslic3r/GCode.cpp +++ b/app/src/main/jni/libslic3r/GCode.cpp @@ -1021,7 +1021,9 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail if (!export_to_binary_gcode) // Write information on the generator. - file.write_format("; %s\n\n", Slic3r::header_slic3r_generated().c_str()); + file.write("; HEADER_BLOCK_START\n"); + file.write_format("; %s\n", Slic3r::header_slic3r_generated().c_str()); + file.write("; HEADER_BLOCK_END\n\n"); if (! export_to_binary_gcode) { // if exporting gcode in ascii format, generate the thumbnails here @@ -3889,4 +3891,4 @@ Point GCodeGenerator::gcode_to_point(const Vec2d &point) const return scaled(pt); } -} // namespace Slic3r \ No newline at end of file +} // namespace Slic3r