mirror of
https://github.com/Dark98/SliceBeam.git
synced 2026-07-03 00:38:53 +00:00
Basic G-Code Thumbnail Generation Support
This commit is contained in:
@@ -51,6 +51,7 @@ import ru.ytkab0bp.slicebeam.events.SlicingProgressEvent;
|
|||||||
import ru.ytkab0bp.slicebeam.navigation.Fragment;
|
import ru.ytkab0bp.slicebeam.navigation.Fragment;
|
||||||
import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
|
import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
|
||||||
import ru.ytkab0bp.slicebeam.slic3r.GCodeProcessorResult;
|
import ru.ytkab0bp.slicebeam.slic3r.GCodeProcessorResult;
|
||||||
|
import ru.ytkab0bp.slicebeam.slic3r.GCodeThumbnailer;
|
||||||
import ru.ytkab0bp.slicebeam.slic3r.Model;
|
import ru.ytkab0bp.slicebeam.slic3r.Model;
|
||||||
import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError;
|
import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError;
|
||||||
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
||||||
@@ -412,12 +413,14 @@ public class BedFragment extends Fragment {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DEBUG_VIEWER) {
|
if (!DEBUG_VIEWER) {
|
||||||
gCodeResult = glView.getRenderer().getModel().slice(cfg.getAbsolutePath(), gcode.getAbsolutePath(), (progress, text) -> SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(progress, text)));
|
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, ""));
|
GCodeThumbnailer.addThumbnailsToGcode(gcode, SliceBeam.buildCurrentConfigObject(), glView);
|
||||||
} else {
|
SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, ""));
|
||||||
gCodeResult = new GCodeProcessorResult(gcode);
|
} else {
|
||||||
}
|
gCodeResult = new GCodeProcessorResult(gcode);
|
||||||
|
GCodeThumbnailer.addThumbnailsToGcode(gcode, SliceBeam.buildCurrentConfigObject(), glView);
|
||||||
|
}
|
||||||
ViewUtils.postOnMainThread(()-> {
|
ViewUtils.postOnMainThread(()-> {
|
||||||
glView.queueEvent(()->{
|
glView.queueEvent(()->{
|
||||||
glView.getRenderer().setGCodeViewer(gCodeResult);
|
glView.getRenderer().setGCodeViewer(gCodeResult);
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ public class PrinterConfigFragment extends ProfileListFragment {
|
|||||||
|
|
||||||
new OptionElement(new SubHeader("Firmware")),
|
new OptionElement(new SubHeader("Firmware")),
|
||||||
new OptionElement(def.options.get("gcode_flavor")),
|
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("silent_mode")),
|
||||||
new OptionElement(def.options.get("remaining_times")),
|
new OptionElement(def.options.get("remaining_times")),
|
||||||
new OptionElement(def.options.get("binary_gcode")),
|
new OptionElement(def.options.get("binary_gcode")),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ru.ytkab0bp.slicebeam.render;
|
|||||||
import static android.opengl.GLES30.*;
|
import static android.opengl.GLES30.*;
|
||||||
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
|
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.opengl.GLSurfaceView;
|
import android.opengl.GLSurfaceView;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -12,6 +13,8 @@ import androidx.core.graphics.ColorUtils;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
import javax.microedition.khronos.egl.EGLConfig;
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
import javax.microedition.khronos.opengles.GL10;
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ public class GLRenderer implements GLSurfaceView.Renderer {
|
|||||||
|
|
||||||
// Instance values, should be released
|
// Instance values, should be released
|
||||||
private Bed3D bed;
|
private Bed3D bed;
|
||||||
|
private boolean bedVisible = true;
|
||||||
private int lastConfigUid;
|
private int lastConfigUid;
|
||||||
private GLShadersManager shadersManager;
|
private GLShadersManager shadersManager;
|
||||||
private GLModel backgroundModel;
|
private GLModel backgroundModel;
|
||||||
@@ -77,6 +81,7 @@ public class GLRenderer implements GLSurfaceView.Renderer {
|
|||||||
private Vec3d bbMin = new Vec3d(), bbMax = new Vec3d();
|
private Vec3d bbMin = new Vec3d(), bbMax = new Vec3d();
|
||||||
private boolean isInFlattenMode;
|
private boolean isInFlattenMode;
|
||||||
private ArrayList<GLModel> flattenPlanes = new ArrayList<>();
|
private ArrayList<GLModel> flattenPlanes = new ArrayList<>();
|
||||||
|
private static final double TOP_VIEW_MARGIN = 1.1;
|
||||||
|
|
||||||
public Camera getCamera() {
|
public Camera getCamera() {
|
||||||
return camera;
|
return camera;
|
||||||
@@ -86,6 +91,204 @@ public class GLRenderer implements GLSurfaceView.Renderer {
|
|||||||
return bed;
|
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() {
|
public double[] getProjectionMatrix() {
|
||||||
return projectionMatrix;
|
return projectionMatrix;
|
||||||
}
|
}
|
||||||
@@ -225,7 +428,7 @@ public class GLRenderer implements GLSurfaceView.Renderer {
|
|||||||
if (lastConfigUid != SliceBeam.CONFIG_UID) {
|
if (lastConfigUid != SliceBeam.CONFIG_UID) {
|
||||||
configureBed();
|
configureBed();
|
||||||
}
|
}
|
||||||
if (bed.isValid()) {
|
if (bed.isValid() && bedVisible) {
|
||||||
bed.render(shadersManager, bottom, camera.getViewModelMatrix(), projectionMatrix, 1f / camera.getZoom());
|
bed.render(shadersManager, bottom, camera.getViewModelMatrix(), projectionMatrix, 1f / camera.getZoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<ThumbnailSpec> 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<ThumbnailSpec> parseThumbnailSpecs(String thumbnails, String defaultFormat) {
|
||||||
|
List<ThumbnailSpec> 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<ThumbnailSpec> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ public class PrintConfigDef {
|
|||||||
"tilt_up_finish_speed",
|
"tilt_up_finish_speed",
|
||||||
"tilt_down_initial_speed",
|
"tilt_down_initial_speed",
|
||||||
"tilt_down_finish_speed",
|
"tilt_down_finish_speed",
|
||||||
"tower_speed"
|
"tower_speed",
|
||||||
|
"thumbnails_format"
|
||||||
);
|
);
|
||||||
|
|
||||||
private static PrintConfigDef instance;
|
private static PrintConfigDef instance;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class Slic3rConfigWrapper {
|
|||||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "multimaterial_purging",
|
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "multimaterial_purging",
|
||||||
"max_print_height", "default_print_profile", "inherits",
|
"max_print_height", "default_print_profile", "inherits",
|
||||||
"remaining_times", "silent_mode",
|
"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_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_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",
|
"machine_max_feedrate_x", "machine_max_feedrate_y", "machine_max_feedrate_z", "machine_max_feedrate_e",
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import android.view.ViewConfiguration;
|
|||||||
import java.nio.IntBuffer;
|
import java.nio.IntBuffer;
|
||||||
import java.util.ArrayList;
|
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.R;
|
||||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||||
import ru.ytkab0bp.slicebeam.events.LongClickTranslationEvent;
|
import ru.ytkab0bp.slicebeam.events.LongClickTranslationEvent;
|
||||||
@@ -85,6 +89,7 @@ public class GLView extends GLSurfaceView implements IThemeView {
|
|||||||
xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||||
|
|
||||||
setEGLContextClientVersion(3);
|
setEGLContextClientVersion(3);
|
||||||
|
setEGLConfigChooser(new MultisampleConfigChooser());
|
||||||
renderer = new GLRenderer(this);
|
renderer = new GLRenderer(this);
|
||||||
|
|
||||||
setRenderer(renderer);
|
setRenderer(renderer);
|
||||||
@@ -242,6 +247,89 @@ public class GLView extends GLSurfaceView implements IThemeView {
|
|||||||
return Bitmap.createBitmap(bitmapSource, w, h, Bitmap.Config.ARGB_8888);
|
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
|
@Override
|
||||||
protected void onConfigurationChanged(Configuration newConfig) {
|
protected void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|||||||
@@ -1021,7 +1021,9 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail
|
|||||||
|
|
||||||
if (!export_to_binary_gcode)
|
if (!export_to_binary_gcode)
|
||||||
// Write information on the generator.
|
// 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 (! export_to_binary_gcode) {
|
||||||
// if exporting gcode in ascii format, generate the thumbnails here
|
// 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<coord_t>(pt);
|
return scaled<coord_t>(pt);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
Reference in New Issue
Block a user