mirror of
https://github.com/Dark98/SliceBeam.git
synced 2026-07-03 08:39:04 +00:00
Srat Refactoring To Santoku
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package com.dark98.santoku;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.loopj.android.http.AsyncHttpClient;
|
||||
import com.loopj.android.http.AsyncHttpResponseHandler;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import cz.msebera.android.httpclient.Header;
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
import com.dark98.santoku.events.BeamServerDataUpdatedEvent;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public class BeamServerData {
|
||||
private final static String TAG = "BeamServerData";
|
||||
private final static String DATA_URL = "https://beam3d.ru/slicebeam.php?act=get_data";
|
||||
private final static String RUSSIA_CHECK_URL = "https://beam3d.ru/check_russia.txt";
|
||||
private static AsyncHttpClient client = new AsyncHttpClient();
|
||||
|
||||
static {
|
||||
client.setUserAgent(String.format(Locale.ROOT, "Santoku/%s-%d", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
client.setEnableRedirects(true);
|
||||
client.setLoggingEnabled(false);
|
||||
}
|
||||
|
||||
public List<String> boostySubscribers = new ArrayList<>();
|
||||
|
||||
public BeamServerData(JSONObject obj) {
|
||||
JSONArray arr = obj.optJSONArray("boosty_subscribers");
|
||||
if (arr != null) {
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
boostySubscribers.add(arr.optString(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isBoostyAvailable() {
|
||||
return !BuildConfig.IS_GOOGLE_PLAY || Prefs.isRussianIP();
|
||||
}
|
||||
|
||||
public static boolean isCloudAvailable() {
|
||||
return isBoostyAvailable() && CloudController.hasAccountFeatures();
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
client.get(DATA_URL, new AsyncHttpResponseHandler() {
|
||||
@Override
|
||||
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
|
||||
String str = new String(responseBody, StandardCharsets.UTF_8);
|
||||
Prefs.setBeamServerData(str);
|
||||
Prefs.setLastCheckedInfo();
|
||||
|
||||
try {
|
||||
Santoku.SERVER_DATA = new BeamServerData(new JSONObject(str));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// Disable Boosty only for Google Play builds on non-Russian IP's
|
||||
if (BuildConfig.IS_GOOGLE_PLAY) {
|
||||
client.get(RUSSIA_CHECK_URL, new AsyncHttpResponseHandler() {
|
||||
@Override
|
||||
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
|
||||
setIsRussia(new String(responseBody).equals("true"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
|
||||
if (statusCode == 403) {
|
||||
setIsRussia(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setIsRussia(boolean v) {
|
||||
Prefs.setRussianIP(v);
|
||||
Santoku.EVENT_BUS.fireEvent(new BeamServerDataUpdatedEvent());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Santoku.EVENT_BUS.fireEvent(new BeamServerDataUpdatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
|
||||
Log.e(TAG, "Failed to update server data", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
package com.dark98.santoku;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Base64;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import ru.ytkab0bp.sapil.APICallback;
|
||||
import com.dark98.santoku.cloud.CloudAPI;
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.ChangeLogBottomSheet;
|
||||
import com.dark98.santoku.components.UnfoldMenu;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.events.NeedDismissAIGeneratorMenu;
|
||||
import com.dark98.santoku.events.NeedDismissSnackbarEvent;
|
||||
import com.dark98.santoku.events.NeedSnackbarEvent;
|
||||
import com.dark98.santoku.events.ObjectsListChangedEvent;
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.navigation.Fragment;
|
||||
import com.dark98.santoku.navigation.MobileNavigationDelegate;
|
||||
import com.dark98.santoku.navigation.NavigationDelegate;
|
||||
import com.dark98.santoku.slic3r.Model;
|
||||
import com.dark98.santoku.slic3r.Slic3rConfigWrapper;
|
||||
import com.dark98.santoku.slic3r.Slic3rRuntimeError;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.IOUtils;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.SnackbarsLayout;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
// Activity result
|
||||
public final static int REQUEST_CODE_OPEN_FILE = 1, REQUEST_CODE_EXPORT_GCODE = 2,
|
||||
REQUEST_CODE_IMPORT_PROFILES = 3, REQUEST_CODE_EXPORT_PROFILES = 4,
|
||||
REQUEST_CODE_EXPORT_3MF = 5,
|
||||
REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO = 6, REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO = 7;
|
||||
|
||||
private static MainActivity activeInstance;
|
||||
|
||||
public static List<ConfigObject> EXPORTING_PRINTS;
|
||||
public static List<ConfigObject> EXPORTING_FILAMENTS;
|
||||
public static List<ConfigObject> EXPORTING_PRINTERS;
|
||||
|
||||
public static boolean IS_GENERATING_AI_MODEL;
|
||||
|
||||
public static File aiTempFile;
|
||||
|
||||
private static SparseArray<NavigationDelegate> liveDelegate = new SparseArray<>();
|
||||
private static int lastId;
|
||||
|
||||
private int id;
|
||||
private NavigationDelegate delegate;
|
||||
private boolean landscape;
|
||||
private UnfoldMenu unfoldMenu;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (Prefs.getPrefs().contains("crash")) {
|
||||
startActivity(new Intent(this, SafeStartActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (Santoku.CONFIG == null) {
|
||||
Prefs.setLastCommit();
|
||||
startActivity(new Intent(this, SetupActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeInstance == null) {
|
||||
activeInstance = this;
|
||||
} else {
|
||||
Intent i = new Intent(this, MainActivity.class);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
if (getIntent() != null) {
|
||||
i.setAction(getIntent().getAction());
|
||||
i.putExtras(getIntent());
|
||||
i.setDataAndType(getIntent().getData(), getIntent().getType());
|
||||
}
|
||||
startActivity(i);
|
||||
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
id = savedInstanceState == null ? lastId++ : savedInstanceState.getInt("id");
|
||||
|
||||
if (delegate == null) {
|
||||
NavigationDelegate saved = liveDelegate.get(id);
|
||||
liveDelegate.remove(id);
|
||||
if (saved != null && isCompatible(saved)) {
|
||||
delegate = saved;
|
||||
} else {
|
||||
delegate = onCreateDelegate();
|
||||
}
|
||||
}
|
||||
delegate.setContext(this);
|
||||
|
||||
delegate.onCreate();
|
||||
View v = delegate.onCreateView(this);
|
||||
if (delegate.getContainerView() == null || delegate.getContainerView().getParent() == null) {
|
||||
throw new IllegalArgumentException("Delegate hasn't created container view!");
|
||||
}
|
||||
ViewCompat.setOnApplyWindowInsetsListener(v, (v2, insets) -> {
|
||||
Insets systemBars = insets.getSystemWindowInsets();
|
||||
v2.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets.consumeSystemWindowInsets();
|
||||
});
|
||||
setContentView(v);
|
||||
|
||||
if (getIntent() != null && getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_VIEW)) {
|
||||
loadFile(getIntent().getData());
|
||||
setIntent(null);
|
||||
}
|
||||
|
||||
DisplayMetrics dm = getResources().getDisplayMetrics();
|
||||
landscape = dm.widthPixels > dm.heightPixels;
|
||||
View decorView = getWindow().getDecorView();
|
||||
decorView.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
||||
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
if (landscape) {
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
|
||||
decorView.setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
||||
visibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
int finalVisibility = visibility;
|
||||
ViewUtils.postOnMainThread(() -> decorView.setSystemUiVisibility(finalVisibility), 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
getWindow().setStatusBarContrastEnforced(false);
|
||||
getWindow().setNavigationBarContrastEnforced(false);
|
||||
}
|
||||
if (ColorUtils.calculateLuminance(ThemesRepo.getColor(android.R.attr.windowBackground)) >= 0.9f) {
|
||||
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
} else {
|
||||
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Objects.equals(Prefs.getLastCommit(), BuildConfig.COMMIT) && Santoku.hasUpdateInfo) {
|
||||
Prefs.setLastCommit();
|
||||
BeamServerData.load();
|
||||
new ChangeLogBottomSheet(this).show();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public NavigationDelegate getNavigationDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(@NonNull Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
loadFile(intent.getData());
|
||||
setIntent(null);
|
||||
}
|
||||
|
||||
/** @noinspection ResultOfMethodCallIgnored*/
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == MainActivity.REQUEST_CODE_EXPORT_3MF) {
|
||||
Fragment fragment = getNavigationDelegate().getCurrentFragment();
|
||||
if (fragment instanceof BedFragment) {
|
||||
try {
|
||||
OutputStream out = getContentResolver().openOutputStream(data.getData());
|
||||
Model model = ((BedFragment) fragment).getGlView().getRenderer().getModel();
|
||||
File tempFile = File.createTempFile("temp_project", ".3mf");
|
||||
Santoku.genCurrentConfig();
|
||||
File cfg = Santoku.getCurrentConfigFile();
|
||||
model.export3mf(cfg.getAbsolutePath(), tempFile.getAbsolutePath());
|
||||
|
||||
InputStream in = new FileInputStream(tempFile);
|
||||
byte[] buffer = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, c);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
tempFile.delete();
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileExport3mfSuccess));
|
||||
} catch (IOException | Slic3rRuntimeError e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} else if (requestCode == MainActivity.REQUEST_CODE_EXPORT_GCODE) {
|
||||
try {
|
||||
OutputStream out = getContentResolver().openOutputStream(data.getData());
|
||||
InputStream in = new FileInputStream(BedFragment.getTempGCodePath());
|
||||
byte[] buffer = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, c);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else if (requestCode == MainActivity.REQUEST_CODE_OPEN_FILE) {
|
||||
loadFile(data.getData());
|
||||
} else if (requestCode == MainActivity.REQUEST_CODE_EXPORT_PROFILES) {
|
||||
try {
|
||||
Slic3rConfigWrapper w = new Slic3rConfigWrapper();
|
||||
w.printConfigs.addAll(EXPORTING_PRINTS);
|
||||
w.filamentConfigs.addAll(EXPORTING_FILAMENTS);
|
||||
w.printerConfigs.addAll(EXPORTING_PRINTERS);
|
||||
|
||||
EXPORTING_PRINTS = null;
|
||||
EXPORTING_FILAMENTS = null;
|
||||
EXPORTING_PRINTERS = null;
|
||||
|
||||
w.presets = new ConfigObject();
|
||||
if (w.findPrint(Santoku.CONFIG.presets.get("print")) != null) {
|
||||
w.presets.put("print", Santoku.CONFIG.presets.get("print"));
|
||||
}
|
||||
if (w.findFilament(Santoku.CONFIG.presets.get("filament")) != null) {
|
||||
w.presets.put("filament", Santoku.CONFIG.presets.get("filament"));
|
||||
}
|
||||
if (w.findPrinter(Santoku.CONFIG.presets.get("printer")) != null) {
|
||||
w.presets.put("printer", Santoku.CONFIG.presets.get("printer"));
|
||||
}
|
||||
|
||||
OutputStream out = getContentResolver().openOutputStream(data.getData());
|
||||
out.write(w.serialize().getBytes(StandardCharsets.UTF_8));
|
||||
out.close();
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileExportProfilesSuccess));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else if (requestCode == MainActivity.REQUEST_CODE_IMPORT_PROFILES) {
|
||||
Uri uri = data.getData();
|
||||
String fileName = IOUtils.getDisplayName(uri);
|
||||
|
||||
if (fileName == null) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(R.string.MenuFileOpenFileFailedNullName)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileName.endsWith(".orca_printer")) {
|
||||
loadConvertedProfile(uri);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.endsWith(".ini")) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(R.string.MenuFileImportProfilesFailedNotIni)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadIniForImport(getContentResolver().openInputStream(uri));
|
||||
} catch (FileNotFoundException e) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissAIGeneratorMenu());
|
||||
|
||||
Bitmap bm = BitmapFactory.decodeFile(aiTempFile.getAbsolutePath());
|
||||
generateAiModel(bm);
|
||||
aiTempFile.delete();
|
||||
aiTempFile = null;
|
||||
} else if (requestCode == REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissAIGeneratorMenu());
|
||||
|
||||
try {
|
||||
InputStream in = getContentResolver().openInputStream(data.getData());
|
||||
Bitmap bm = BitmapFactory.decodeStream(in);
|
||||
generateAiModel(bm);
|
||||
} catch (Exception e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadConvertedProfile(Uri uri) {
|
||||
String tag = UUID.randomUUID().toString();
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.OrcaConversionPleaseWait).tag(tag));
|
||||
File f = new File(Santoku.getModelCacheDir(), "orca_conv.zip");
|
||||
IOUtils.IO_POOL.submit(()->{
|
||||
try {
|
||||
InputStream in = getContentResolver().openInputStream(uri);
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
byte[] buffer = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, c);
|
||||
}
|
||||
fos.close();
|
||||
in.close();
|
||||
|
||||
ZipFile zf = new ZipFile(f);
|
||||
JSONObject bundle = new JSONObject(IOUtils.readString(zf.getInputStream(zf.getEntry("bundle_structure.json"))));
|
||||
if (!bundle.get("bundle_type").equals("printer config bundle")) {
|
||||
zf.close();
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(R.string.OrcaConversionNotAConfigBundle)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Slic3rConfigWrapper w = new Slic3rConfigWrapper();
|
||||
if (bundle.has("process_config")) {
|
||||
JSONArray arr = bundle.getJSONArray("process_config");
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> stripped = new ArrayList<>();
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
String v = arr.getString(i);
|
||||
names.add(v);
|
||||
stripped.add(v.substring(v.indexOf('/') + 1, v.length() - 5));
|
||||
}
|
||||
|
||||
for (String name : names) {
|
||||
w.printConfigs.add(IOUtils.configJsonToIni(new JSONObject(IOUtils.readString(zf.getInputStream(zf.getEntry(name)))), "process", Slic3rConfigWrapper.PRINT_CONFIG_KEYS, stripped));
|
||||
}
|
||||
for (ConfigObject obj : w.printConfigs) {
|
||||
String inherit = obj.get("inherits");
|
||||
while (inherit != null) {
|
||||
ConfigObject _obj = w.findPrint(inherit);
|
||||
if (_obj == null) throw new IOUtils.MissingProfileException(inherit);
|
||||
|
||||
obj.values.remove("inherits");
|
||||
HashMap<String, String> newMap = new HashMap<>();
|
||||
newMap.putAll(_obj.values);
|
||||
newMap.putAll(obj.values);
|
||||
obj.values = newMap;
|
||||
|
||||
inherit = obj.values.get("inherits");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bundle.has("filament_config")) {
|
||||
JSONArray arr = bundle.getJSONArray("filament_config");
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> stripped = new ArrayList<>();
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
String v = arr.getString(i);
|
||||
names.add(v);
|
||||
stripped.add(v.substring(v.indexOf('/') + 1, v.length() - 5));
|
||||
}
|
||||
|
||||
for (String name : names) {
|
||||
w.filamentConfigs.add(IOUtils.configJsonToIni(new JSONObject(IOUtils.readString(zf.getInputStream(zf.getEntry(name)))), "filament", Slic3rConfigWrapper.FILAMENT_CONFIG_KEYS, stripped));
|
||||
}
|
||||
for (ConfigObject obj : w.filamentConfigs) {
|
||||
String inherit = obj.get("inherits");
|
||||
while (inherit != null) {
|
||||
ConfigObject _obj = w.findFilament(inherit);
|
||||
if (_obj == null) throw new IOUtils.MissingProfileException(inherit);
|
||||
|
||||
obj.values.remove("inherits");
|
||||
HashMap<String, String> newMap = new HashMap<>();
|
||||
newMap.putAll(_obj.values);
|
||||
newMap.putAll(obj.values);
|
||||
obj.values = newMap;
|
||||
|
||||
inherit = obj.values.get("inherits");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bundle.has("printer_config")) {
|
||||
JSONArray arr = bundle.getJSONArray("printer_config");
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> stripped = new ArrayList<>();
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
String v = arr.getString(i);
|
||||
names.add(v);
|
||||
stripped.add(v.substring(v.indexOf('/') + 1));
|
||||
}
|
||||
|
||||
for (String name : names) {
|
||||
w.printerConfigs.add(IOUtils.configJsonToIni(new JSONObject(IOUtils.readString(zf.getInputStream(zf.getEntry(name)))), "machine", Slic3rConfigWrapper.PRINTER_CONFIG_KEYS, stripped));
|
||||
}
|
||||
for (ConfigObject obj : w.printerConfigs) {
|
||||
String inherit = obj.get("inherits");
|
||||
while (inherit != null) {
|
||||
ConfigObject _obj = w.findPrinter(inherit);
|
||||
if (_obj == null) throw new IOUtils.MissingProfileException(inherit);
|
||||
|
||||
obj.values.remove("inherits");
|
||||
HashMap<String, String> newMap = new HashMap<>();
|
||||
newMap.putAll(_obj.values);
|
||||
newMap.putAll(obj.values);
|
||||
obj.values = newMap;
|
||||
|
||||
inherit = obj.values.get("inherits");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zf.close();
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
loadIniForImport(new ByteArrayInputStream(w.serialize().getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (IOUtils.MissingProfileException ep) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(getString(R.string.MenuFileImportProfilesFailedBaseProfileNotFound, ep.profile))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
} catch (Exception e) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void generateAiModel(Bitmap bm) {
|
||||
IS_GENERATING_AI_MODEL = true;
|
||||
String uploadTag = UUID.randomUUID().toString();
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorUploading).tag(uploadTag));
|
||||
IOUtils.IO_POOL.submit(()->{
|
||||
Bitmap scaled;
|
||||
if (bm.getWidth() > 1024 || bm.getHeight() > 1024) {
|
||||
if (bm.getWidth() > bm.getHeight()) {
|
||||
int w = 1024;
|
||||
int h = (int) ((float) w * bm.getHeight() / bm.getWidth());
|
||||
scaled = Bitmap.createScaledBitmap(bm, w, h, true);
|
||||
} else {
|
||||
int h = 1024;
|
||||
int w = (int) ((float) h * bm.getWidth() / bm.getHeight());
|
||||
scaled = Bitmap.createScaledBitmap(bm, w, h, true);
|
||||
}
|
||||
bm.recycle();
|
||||
} else {
|
||||
scaled = bm;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
scaled.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||
scaled.recycle();
|
||||
|
||||
String processTag = UUID.randomUUID().toString();
|
||||
CloudAPI.INSTANCE.modelsGenerate(Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP), "image/png", new APICallback<InputStream>() {
|
||||
@Override
|
||||
public void onResponse(InputStream in) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(processTag));
|
||||
|
||||
String downloadTag = UUID.randomUUID().toString();
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorDownloading).tag(downloadTag));
|
||||
String fileName = "generated_" + UUID.randomUUID() + ".stl";
|
||||
|
||||
File f = new File(Santoku.getModelCacheDir(), fileName);
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "application/vnd.ms-pki.stl");
|
||||
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
|
||||
|
||||
Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
|
||||
|
||||
if (uri != null) {
|
||||
try {
|
||||
OutputStream out = getContentResolver().openOutputStream(uri);
|
||||
byte[] buf = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, c);
|
||||
fos.write(buf, 0, c);
|
||||
}
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
File downloadsDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
File file = new File(downloadsDirectory, fileName);
|
||||
|
||||
try {
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
byte[] buf = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, c);
|
||||
fos.write(buf, 0, c);
|
||||
}
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
}
|
||||
fos.close();
|
||||
} catch (Exception e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(downloadTag));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileAIGeneratorSavedAs, fileName));
|
||||
loadFile(f, true);
|
||||
CloudController.checkGeneratorRemaining();
|
||||
IS_GENERATING_AI_MODEL = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(processTag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(MainActivity.this)
|
||||
.setTitle(R.string.MenuFileAIGeneratorError)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
IS_GENERATING_AI_MODEL = false;
|
||||
}
|
||||
});
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(uploadTag));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorProcessing).tag(processTag));
|
||||
});
|
||||
}
|
||||
|
||||
private void loadIniForImport(InputStream in) {
|
||||
IOUtils.IO_POOL.submit(()->{
|
||||
try {
|
||||
Slic3rConfigWrapper w = new Slic3rConfigWrapper(in);
|
||||
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
CharSequence[] prints = new CharSequence[w.printConfigs.size()];
|
||||
boolean[] enabledPrints = new boolean[prints.length];
|
||||
for (int i = 0; i < prints.length; i++) {
|
||||
prints[i] = w.printConfigs.get(i).getTitle();
|
||||
enabledPrints[i] = true;
|
||||
}
|
||||
|
||||
CharSequence[] filaments = new CharSequence[w.filamentConfigs.size()];
|
||||
boolean[] enabledFilaments = new boolean[filaments.length];
|
||||
for (int i = 0; i < filaments.length; i++) {
|
||||
filaments[i] = w.filamentConfigs.get(i).getTitle();
|
||||
enabledFilaments[i] = true;
|
||||
}
|
||||
|
||||
CharSequence[] printers = new CharSequence[w.printerConfigs.size()];
|
||||
boolean[] enabledPrinters = new boolean[printers.length];
|
||||
for (int i = 0; i < printers.length; i++) {
|
||||
printers[i] = w.printerConfigs.get(i).getTitle();
|
||||
enabledPrinters[i] = true;
|
||||
}
|
||||
|
||||
if (prints.length == 0 && filaments.length == 0 && printers.length == 0) {
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(R.string.MenuFileImportProfilesFailedEmpty)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable finish = () -> {
|
||||
for (int i = 0; i < enabledPrints.length; i++) {
|
||||
if (enabledPrints[i]) {
|
||||
Santoku.CONFIG.importPrint(w.printConfigs.get(i));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < enabledFilaments.length; i++) {
|
||||
if (enabledFilaments[i]) {
|
||||
Santoku.CONFIG.importFilament(w.filamentConfigs.get(i));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < enabledPrinters.length; i++) {
|
||||
if (enabledPrinters[i]) {
|
||||
Santoku.CONFIG.importPrinter(w.printerConfigs.get(i));
|
||||
}
|
||||
}
|
||||
Santoku.saveConfig();
|
||||
};
|
||||
Runnable printersRun = () -> {
|
||||
if (printers.length == 0) {
|
||||
finish.run();
|
||||
return;
|
||||
}
|
||||
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileExportProfilesPrinters)
|
||||
.setMultiChoiceItems(printers, enabledPrinters, (dialog, which, isChecked) -> enabledPrinters[which] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (d3, w3) -> finish.run())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
};
|
||||
Runnable filamentsRun = () -> {
|
||||
if (filaments.length == 0) {
|
||||
printersRun.run();
|
||||
return;
|
||||
}
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileExportProfilesFilaments)
|
||||
.setMultiChoiceItems(filaments, enabledFilaments, (dialog, which, isChecked) -> enabledFilaments[which] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (d2, w2) -> printersRun.run())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
};
|
||||
if (prints.length == 0) {
|
||||
filamentsRun.run();
|
||||
} else {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileExportProfilesPrints)
|
||||
.setMultiChoiceItems(prints, enabledPrints, (dialog, which, isChecked) -> enabledPrints[which] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (d1, w1) -> filamentsRun.run())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("MainActivity", "Failed to read file", e);
|
||||
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadFile(File f, boolean autoorient) {
|
||||
String tag = UUID.randomUUID().toString();
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileOpenFileLoading).tag(tag));
|
||||
IOUtils.IO_POOL.submit(() -> {
|
||||
Process.setThreadPriority(-20);
|
||||
if (delegate.getCurrentFragment() instanceof BedFragment) {
|
||||
BedFragment fragment = (BedFragment) delegate.getCurrentFragment();
|
||||
try {
|
||||
boolean gcode = f.getName().endsWith(".gcode");
|
||||
if (gcode) {
|
||||
fragment.loadGCode(f);
|
||||
} else {
|
||||
fragment.loadModel(f);
|
||||
}
|
||||
fragment.getGlView().queueEvent(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
if (model == null || fragment.getGlView().getRenderer().getBed() == null) {
|
||||
fragment.getGlView().queueEvent(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gcode) {
|
||||
Santoku.EVENT_BUS.fireEvent(new ObjectsListChangedEvent());
|
||||
}
|
||||
int i = model.getObjectsCount() - 1;
|
||||
if (autoorient) {
|
||||
model.autoOrient(i);
|
||||
fragment.getGlView().getRenderer().invalidateGlModel(i);
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileOpenFileLoaded));
|
||||
if (model.isBigObject(i)) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.MenuFileOpenFileBigObject));
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Slic3rRuntimeError e) {
|
||||
Log.e("MainActivity", "Failed to load model", e);
|
||||
f.delete();
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileOpenFileFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadFile(Uri uri) {
|
||||
if (uri == null) return;
|
||||
|
||||
ContentResolver resolver = getContentResolver();
|
||||
String fileName = IOUtils.getDisplayName(uri);
|
||||
if (fileName == null) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileOpenFileFailed)
|
||||
.setMessage(R.string.MenuFileOpenFileFailedNullName)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
if (fileName.endsWith(".orca_printer")) {
|
||||
loadConvertedProfile(uri);
|
||||
return;
|
||||
}
|
||||
if (fileName.endsWith(".ini")) {
|
||||
try {
|
||||
loadIniForImport(resolver.openInputStream(uri));
|
||||
} catch (FileNotFoundException e) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
File f = new File(Santoku.getModelCacheDir(), fileName);
|
||||
// TODO: Check if file already exists
|
||||
IOUtils.IO_POOL.submit(()->{
|
||||
try {
|
||||
InputStream in = resolver.openInputStream(uri);
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
byte[] buffer = new byte[10240]; int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, c);
|
||||
}
|
||||
fos.close();
|
||||
in.close();
|
||||
loadFile(f, false);
|
||||
} catch (Exception e) {
|
||||
Log.e("MainActivity", "Failed to write cache file", e);
|
||||
|
||||
f.delete();
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileOpenFileFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if ((newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED) {
|
||||
ThemesRepo.resetSystemResolvedTheme();
|
||||
ThemesRepo.invalidate(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void onApplyTheme() {
|
||||
delegate.onApplyTheme();
|
||||
|
||||
View decorView = getWindow().getDecorView();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ColorUtils.calculateLuminance(ThemesRepo.getColor(android.R.attr.windowBackground)) >= 0.9f) {
|
||||
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
} else {
|
||||
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
}
|
||||
decorView.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (isChangingConfigurations()) {
|
||||
outState.putInt("id", id);
|
||||
liveDelegate.put(id, delegate);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCompatible(NavigationDelegate delegate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private NavigationDelegate onCreateDelegate() {
|
||||
return new MobileNavigationDelegate();
|
||||
}
|
||||
|
||||
public void showUnfoldMenu(UnfoldMenu menu, View v) {
|
||||
if (unfoldMenu != null) return;
|
||||
menu.setOnDismiss(() -> unfoldMenu = null);
|
||||
menu.show(v, delegate.getOverlayView());
|
||||
unfoldMenu = menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (unfoldMenu != null) {
|
||||
unfoldMenu.dismiss();
|
||||
return;
|
||||
}
|
||||
if (delegate.onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
delegate.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
delegate.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (activeInstance == this) {
|
||||
activeInstance = null;
|
||||
}
|
||||
if (delegate != null) {
|
||||
delegate.onDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.dark98.santoku;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.BeamButton;
|
||||
|
||||
public class SafeStartActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
getWindow().setStatusBarColor(Color.WHITE);
|
||||
View v = getWindow().getDecorView();
|
||||
v.setSystemUiVisibility(v.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
|
||||
LinearLayout ll = new LinearLayout(this);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setBackgroundColor(Color.WHITE);
|
||||
TextView title = new TextView(this);
|
||||
title.setTextColor(Color.BLACK);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
title.setText(R.string.AppCrashed);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
title.setPadding(ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12), 0);
|
||||
ll.addView(title);
|
||||
|
||||
ScrollView scroll = new ScrollView(this);
|
||||
TextView desc = new TextView(this);
|
||||
desc.setTextColor(0x99000000);
|
||||
desc.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
|
||||
String log = getString(R.string.AppCrashedDesc, Build.VERSION.RELEASE, Build.BRAND + " " + Build.MODEL, Prefs.getPrefs().getString("crash", ""));
|
||||
desc.setText(log);
|
||||
desc.setPadding(0, 0, 0, ViewUtils.dp(12));
|
||||
scroll.setPadding(ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12), 0);
|
||||
scroll.addView(desc);
|
||||
ll.addView(scroll, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
BeamButton share = new BeamButton(this);
|
||||
share.setText(R.string.AppCrashedShare);
|
||||
share.setOnClickListener(v -> {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, log);
|
||||
sendIntent.setType("text/plain");
|
||||
|
||||
Intent shareIntent = Intent.createChooser(sendIntent, null);
|
||||
startActivity(shareIntent);
|
||||
});
|
||||
ll.addView(share, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
|
||||
TextView restart = new TextView(this);
|
||||
restart.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
restart.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
restart.setGravity(Gravity.CENTER);
|
||||
restart.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
restart.setText(R.string.AppCrashedRestart);
|
||||
restart.setBackground(ViewUtils.createRipple(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorAccent), 0x21), 16));
|
||||
restart.setOnClickListener(v -> {
|
||||
Prefs.getPrefs().edit().remove("crash").apply();
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
finish();
|
||||
});
|
||||
ll.addView(restart, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
topMargin = ViewUtils.dp(8);
|
||||
bottomMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
|
||||
setContentView(ll);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.dark98.santoku;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.instacart.truetime.time.TrueTimeImpl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventBus;
|
||||
import com.dark98.santoku.boot.AppBoot;
|
||||
import com.dark98.santoku.boot.BeamServerDataTask;
|
||||
import com.dark98.santoku.boot.CheckUpdateJsonTask;
|
||||
import com.dark98.santoku.boot.ClearModelCacheTask;
|
||||
import com.dark98.santoku.boot.CloudInitTask;
|
||||
import com.dark98.santoku.boot.EventBusTask;
|
||||
import com.dark98.santoku.boot.LoadSlic3rConfigTask;
|
||||
import com.dark98.santoku.boot.PrefsTask;
|
||||
import com.dark98.santoku.boot.PrintConfigWarmupTask;
|
||||
import com.dark98.santoku.boot.TrueTimeTask;
|
||||
import com.dark98.santoku.boot.VibrationUtilsTask;
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.slic3r.ConfigOptionDef;
|
||||
import com.dark98.santoku.slic3r.PrintConfigDef;
|
||||
import com.dark98.santoku.slic3r.Slic3rConfigWrapper;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public class Santoku extends Application {
|
||||
public static Santoku INSTANCE;
|
||||
public static EventBus EVENT_BUS = EventBus.newBus("main");
|
||||
public static TrueTimeImpl TRUE_TIME;
|
||||
public static Slic3rConfigWrapper CONFIG;
|
||||
public static int CONFIG_UID = 0;
|
||||
public static BeamServerData SERVER_DATA;
|
||||
public static boolean hasUpdateInfo;
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
INSTANCE = this;
|
||||
AppBoot.run(Arrays.asList(
|
||||
new EventBusTask(),
|
||||
new PrefsTask(),
|
||||
new VibrationUtilsTask(),
|
||||
new TrueTimeTask(),
|
||||
new BeamServerDataTask(),
|
||||
new PrintConfigWarmupTask(),
|
||||
new CheckUpdateJsonTask(),
|
||||
new ClearModelCacheTask(),
|
||||
new LoadSlic3rConfigTask(),
|
||||
new CloudInitTask()
|
||||
));
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
|
||||
Prefs.getPrefs().edit().putString("crash", sw.toString()).commit();
|
||||
Intent intent = new Intent(this, SafeStartActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
Runtime.getRuntime().exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
public static void saveConfig() {
|
||||
Santoku.CONFIG_UID++;
|
||||
File f = getConfigFile();
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
fos.write(CONFIG.serialize().getBytes(StandardCharsets.UTF_8));
|
||||
fos.close();
|
||||
|
||||
getCurrentConfigFile().delete();
|
||||
} catch (Exception e) {
|
||||
Log.e("Config", "Failed to save config", e);
|
||||
}
|
||||
CloudController.notifyDataChanged();
|
||||
}
|
||||
|
||||
public static File getModelCacheDir() {
|
||||
File f = new File(INSTANCE.getCacheDir(), "model");
|
||||
if (!f.exists()) f.mkdirs();
|
||||
return f;
|
||||
}
|
||||
|
||||
public static File getConfigFile() {
|
||||
return new File(INSTANCE.getFilesDir(), "slic3r.ini");
|
||||
}
|
||||
|
||||
public static ConfigObject buildCurrentConfigObject() {
|
||||
ConfigObject singleObject = new ConfigObject();
|
||||
ConfigObject printerConfig = Santoku.CONFIG.findPrinter(Santoku.CONFIG.presets.get("printer"));
|
||||
if (printerConfig != null) {
|
||||
singleObject.values.putAll(printerConfig.values);
|
||||
}
|
||||
ConfigObject printConfig = Santoku.CONFIG.findPrint(Santoku.CONFIG.presets.get("print"));
|
||||
if (printConfig != null) {
|
||||
for (Map.Entry<String, String> en : printConfig.values.entrySet()) {
|
||||
if (!Slic3rConfigWrapper.PRINTER_CONFIG_KEYS.contains(en.getKey())) {
|
||||
singleObject.values.put(en.getKey(), en.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: MMU. Detect by printerConfig#getExtruderCount()
|
||||
ConfigObject filamentConfig = Santoku.CONFIG.findFilament(Santoku.CONFIG.presets.get("filament"));
|
||||
if (filamentConfig != null) {
|
||||
for (Map.Entry<String, String> en : filamentConfig.values.entrySet()) {
|
||||
if (!Slic3rConfigWrapper.PRINTER_CONFIG_KEYS.contains(en.getKey())) {
|
||||
singleObject.values.put(en.getKey(), en.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrintConfigDef def = PrintConfigDef.getInstance();
|
||||
for (Map.Entry<String, ConfigOptionDef> en : def.options.entrySet()) {
|
||||
if (singleObject.get(en.getKey()) == null && !PrintConfigDef.SKIP_DEFAULT_OPTIONS.contains(en.getKey()) && en.getValue().defaultValue != null) {
|
||||
singleObject.put(en.getKey(), en.getValue().defaultValue);
|
||||
}
|
||||
}
|
||||
return singleObject;
|
||||
}
|
||||
|
||||
public static void genCurrentConfig() throws IOException {
|
||||
File cfg = getCurrentConfigFile();
|
||||
FileOutputStream fos = new FileOutputStream(cfg);
|
||||
ConfigObject singleObject = buildCurrentConfigObject();
|
||||
fos.write(singleObject.serialize().getBytes(StandardCharsets.UTF_8));
|
||||
fos.close();
|
||||
}
|
||||
|
||||
public static File getCurrentConfigFile() {
|
||||
return new File(INSTANCE.getFilesDir(), "slic3r_current.ini");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
|
||||
public class AppBoot {
|
||||
private final static String TAG = "boot";
|
||||
|
||||
static ExecutorService executor = Executors.newCachedThreadPool();
|
||||
static List<BootTask> tasks;
|
||||
static List<Runnable> pendingMain = new ArrayList<>();
|
||||
static List<BootTask> pendingTasks = new ArrayList<>();
|
||||
static SparseBooleanArray completed = new SparseBooleanArray();
|
||||
static CountDownLatch latch;
|
||||
|
||||
public static void run(List<BootTask> tasks) {
|
||||
long start = System.currentTimeMillis();
|
||||
AppBoot.tasks = tasks;
|
||||
int size = tasks.size();
|
||||
for (int i = 0, s = tasks.size(); i < s; i++) {
|
||||
BootTask task = tasks.get(i);
|
||||
if (task.nonCritical) {
|
||||
if (!task.workerThread) {
|
||||
throw new IllegalArgumentException("Can't schedule non-critical task on main thread");
|
||||
}
|
||||
size--;
|
||||
}
|
||||
}
|
||||
AppBoot.latch = new CountDownLatch(size);
|
||||
|
||||
for (int i = 0, s = tasks.size(); i < s; i++) {
|
||||
BootTask task = tasks.get(i);
|
||||
task.index = i;
|
||||
tryRunTask(task, true, false);
|
||||
}
|
||||
try {
|
||||
while (!latch.await(10, TimeUnit.MILLISECONDS)) {
|
||||
if (!pendingMain.isEmpty()) {
|
||||
List<Runnable> clone = new ArrayList<>(pendingMain);
|
||||
for (Runnable r : clone) {
|
||||
r.run();
|
||||
}
|
||||
pendingMain.removeAll(clone);
|
||||
}
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Boot in " + (System.currentTimeMillis() - start) + "ms");
|
||||
}
|
||||
tryShutdown();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryShutdown() {
|
||||
if (completed.size() == tasks.size()) {
|
||||
executor.shutdown();
|
||||
executor = null;
|
||||
tasks = null;
|
||||
pendingMain = null;
|
||||
pendingTasks = null;
|
||||
completed = null;
|
||||
latch = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryRunTask(BootTask task, boolean fromMain, boolean isContinue) {
|
||||
if (checkDependencies(task.dependencies)) {
|
||||
Runnable r = () -> {
|
||||
try {
|
||||
task.run.run();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error while executing boot task", e);
|
||||
}
|
||||
completed.put(task.index, true);
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Finish " + task);
|
||||
}
|
||||
if (!task.nonCritical) {
|
||||
latch.countDown();
|
||||
} else {
|
||||
tryShutdown();
|
||||
}
|
||||
|
||||
if (!isContinue) {
|
||||
continueTasks(fromMain);
|
||||
}
|
||||
};
|
||||
if (task.workerThread) {
|
||||
executor.submit(r);
|
||||
} else {
|
||||
if (fromMain) {
|
||||
r.run();
|
||||
} else {
|
||||
pendingMain.add(r);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pendingTasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
private static void continueTasks(boolean fromMain) {
|
||||
for (Iterator<BootTask> it = pendingTasks.iterator(); it.hasNext();) {
|
||||
BootTask task = it.next();
|
||||
if (checkDependencies(task.dependencies)) {
|
||||
tryRunTask(task, fromMain, true);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkDependencies(List<Class<?>> clzs) {
|
||||
if (clzs.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0, s = tasks.size(); i < s; i++) {
|
||||
if (clzs.contains(tasks.get(i).getClass())) {
|
||||
if (!completed.get(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.dark98.santoku.BeamServerData;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class BeamServerDataTask extends BootTask {
|
||||
public BeamServerDataTask() {
|
||||
super(() -> {
|
||||
try {
|
||||
Santoku.SERVER_DATA = new BeamServerData(new JSONObject(Prefs.getBeamServerData()));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (System.currentTimeMillis() - Prefs.getLastCheckedInfo() >= 86400000L) {
|
||||
ViewUtils.postOnMainThread(BeamServerData::load);
|
||||
}
|
||||
});
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class BootTask {
|
||||
public final List<Class<?>> dependencies;
|
||||
public final Runnable run;
|
||||
public boolean workerThread;
|
||||
public int priority;
|
||||
public boolean nonCritical;
|
||||
|
||||
/* package */ int index;
|
||||
|
||||
public BootTask(Runnable run) {
|
||||
this.dependencies = Collections.emptyList();
|
||||
this.run = run;
|
||||
}
|
||||
|
||||
public BootTask(List<Class<?>> dependencies, Runnable run) {
|
||||
this.dependencies = dependencies;
|
||||
this.run = run;
|
||||
}
|
||||
|
||||
public BootTask onWorker() {
|
||||
return onWorker(-20);
|
||||
}
|
||||
|
||||
public BootTask onWorker(int priority) {
|
||||
this.workerThread = true;
|
||||
this.priority = priority;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
|
||||
public class CheckUpdateJsonTask extends BootTask {
|
||||
public CheckUpdateJsonTask() {
|
||||
super(() -> {
|
||||
try {
|
||||
Santoku.INSTANCE.getAssets().open("update.json").close();
|
||||
Santoku.hasUpdateInfo = true;
|
||||
} catch (IOException e) {
|
||||
Santoku.hasUpdateInfo = false;
|
||||
}
|
||||
});
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
|
||||
public class ClearModelCacheTask extends BootTask {
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public ClearModelCacheTask() {
|
||||
super(()->{
|
||||
File cache = Santoku.getModelCacheDir();
|
||||
if (cache.exists()) {
|
||||
for (File f : cache.listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
nonCritical = true;
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
|
||||
public class CloudCachedInitTask extends BootTask {
|
||||
public CloudCachedInitTask() {
|
||||
super(Collections.singletonList(PrefsTask.class), CloudController::initCached);
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
|
||||
public class CloudInitTask extends BootTask {
|
||||
public CloudInitTask() {
|
||||
super(Arrays.asList(PrefsTask.class, TrueTimeTask.class, LoadSlic3rConfigTask.class, CloudCachedInitTask.class), CloudController::init);
|
||||
onWorker();
|
||||
nonCritical = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventBus;
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
|
||||
public class EventBusTask extends BootTask {
|
||||
|
||||
public EventBusTask() {
|
||||
super(() -> EventBus.registerImpl(BuildConfig.APPLICATION_ID));
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.slic3r.Slic3rConfigWrapper;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public class LoadSlic3rConfigTask extends BootTask {
|
||||
public LoadSlic3rConfigTask() {
|
||||
super(() -> {
|
||||
File cfgFile = Santoku.getConfigFile();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
if (cfgFile.exists()) {
|
||||
try {
|
||||
Santoku.CONFIG = new Slic3rConfigWrapper(cfgFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public class PrefsTask extends BootTask {
|
||||
public PrefsTask() {
|
||||
super(()->Prefs.init(Santoku.INSTANCE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import com.dark98.santoku.slic3r.PrintConfigDef;
|
||||
|
||||
public class PrintConfigWarmupTask extends BootTask {
|
||||
public PrintConfigWarmupTask() {
|
||||
super(PrintConfigDef::getInstance);
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.instacart.truetime.TrueTimeEventListener;
|
||||
import com.instacart.truetime.time.TrueTimeImpl;
|
||||
import com.instacart.truetime.time.TrueTimeParameters;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import kotlinx.coroutines.Dispatchers;
|
||||
import com.dark98.santoku.Santoku;
|
||||
|
||||
public class TrueTimeTask extends BootTask {
|
||||
public TrueTimeTask() {
|
||||
super(() -> {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Santoku.TRUE_TIME = new TrueTimeImpl(new TrueTimeParameters.Builder().buildParams(), Dispatchers.getIO(), new TrueTimeEventListener() {
|
||||
@Override
|
||||
public void initialize(@NonNull TrueTimeParameters trueTimeParameters) {}
|
||||
|
||||
@Override
|
||||
public void initializeSuccess(@NonNull long[] longs) {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeFailed(@NonNull Exception e) {}
|
||||
|
||||
@Override
|
||||
public void nextInitializeIn(long l) {}
|
||||
|
||||
@Override
|
||||
public void resolvedNtpHostToIPs(@NonNull String s, @NonNull List<? extends InetAddress> list) {}
|
||||
|
||||
@Override
|
||||
public void lastSntpRequestAttempt(@NonNull InetAddress inetAddress) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequestFailed(@NonNull Exception e) {}
|
||||
|
||||
@Override
|
||||
public void syncDispatcherException(@NonNull Throwable throwable) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequest(@NonNull InetAddress inetAddress) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequestSuccessful(@NonNull InetAddress inetAddress) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequestFailed(@NonNull InetAddress inetAddress, @NonNull Exception e) {}
|
||||
|
||||
@Override
|
||||
public void storingTrueTime(@NonNull long[] longs) {}
|
||||
|
||||
@Override
|
||||
public void returningTrueTime(@NonNull Date date) {}
|
||||
|
||||
@Override
|
||||
public void returningDeviceTime() {}
|
||||
});
|
||||
Santoku.TRUE_TIME.sync();
|
||||
try {
|
||||
latch.await(300, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {}
|
||||
});
|
||||
onWorker();
|
||||
nonCritical = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.dark98.santoku.boot;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.utils.VibrationUtils;
|
||||
|
||||
public class VibrationUtilsTask extends BootTask {
|
||||
|
||||
public VibrationUtilsTask() {
|
||||
super(() -> VibrationUtils.init(Santoku.INSTANCE));
|
||||
onWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.dark98.santoku.cloud;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.ytkab0bp.sapil.APICallback;
|
||||
import ru.ytkab0bp.sapil.APILibrary;
|
||||
import ru.ytkab0bp.sapil.APIRequestHandle;
|
||||
import ru.ytkab0bp.sapil.APIRunner;
|
||||
import ru.ytkab0bp.sapil.Arg;
|
||||
import ru.ytkab0bp.sapil.Header;
|
||||
import ru.ytkab0bp.sapil.Method;
|
||||
import ru.ytkab0bp.sapil.RequestType;
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public interface CloudAPI extends APIRunner {
|
||||
CloudAPI INSTANCE = APILibrary.newRunner(CloudAPI.class, new RunnerConfig() {
|
||||
private final Map<String, String> headers = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public String getBaseURL() {
|
||||
return "https://api.beam3d.ru/v1/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultUserAgent() {
|
||||
return "Santoku v" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultHeaders() {
|
||||
headers.clear();
|
||||
if (Prefs.getCloudAPIToken() != null) {
|
||||
headers.put("Authorization", "Bearer " + Prefs.getCloudAPIToken());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Begins login flow, returns auth link
|
||||
*/
|
||||
@Method("login/begin")
|
||||
APIRequestHandle loginBegin(APICallback<LoginData> callback);
|
||||
|
||||
/**
|
||||
* Checks new login state by session id
|
||||
*/
|
||||
@Method("login/check")
|
||||
void loginCheck(@Arg("sessionId") String sessionId, APICallback<LoginState> callback);
|
||||
|
||||
/**
|
||||
* Cancels login flow
|
||||
*/
|
||||
@Method("login/cancel")
|
||||
void loginCancel(@Arg("sessionId") String sessionId, APICallback<Boolean> callback);
|
||||
|
||||
/**
|
||||
* Gets current user info
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("user/getInfo")
|
||||
void userGetInfo(APICallback<UserInfo> callback);
|
||||
|
||||
/**
|
||||
* Gets user features
|
||||
*/
|
||||
@Method("user/getFeatures")
|
||||
void userGetFeatures(APICallback<UserFeatures> callback);
|
||||
|
||||
/**
|
||||
* Fetches sync state
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("sync/getState")
|
||||
void syncGetState(APICallback<SyncState> callback);
|
||||
|
||||
/**
|
||||
* Uploads new data to the server
|
||||
* <p>
|
||||
* @param data New base64 encoded data
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method(requestType = RequestType.POST, value = "sync/upload")
|
||||
void syncUpload(@Arg("") String data, @Header("Content-Type") String type, APICallback<SyncState> callback);
|
||||
|
||||
/**
|
||||
* Downloads base64 data
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("sync/get")
|
||||
void syncGet(APICallback<String> callback);
|
||||
|
||||
/**
|
||||
* Generates 3D model from image
|
||||
* <p>
|
||||
* @param image Base64 encoded image
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method(requestType = RequestType.POST, value = "models/generate")
|
||||
void modelsGenerate(@Arg("") String image, @Header("Content-Type") String type, APICallback<InputStream> callback);
|
||||
|
||||
/**
|
||||
* Gets remaining model generations count
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("models/getRemainingCount")
|
||||
void modelsGetRemainingCount(APICallback<ModelsRemainingCount> callback);
|
||||
|
||||
/**
|
||||
* Destroys token
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("logout")
|
||||
void logout(APICallback<Boolean> callback);
|
||||
|
||||
final class LoginData {
|
||||
/**
|
||||
* Url that should be clicked by the user to authorize
|
||||
*/
|
||||
public String url;
|
||||
|
||||
/**
|
||||
* Session identifier
|
||||
*/
|
||||
public String sessionId;
|
||||
|
||||
/**
|
||||
* Time at which session should be considered expired if not logged in
|
||||
*/
|
||||
public long expiresAt;
|
||||
}
|
||||
|
||||
final class LoginState {
|
||||
/**
|
||||
* If user is now logged in
|
||||
*/
|
||||
public boolean loggedIn;
|
||||
|
||||
/**
|
||||
* Bearer token if auth was successful
|
||||
*/
|
||||
public String bearer;
|
||||
}
|
||||
|
||||
final class UserFeatures {
|
||||
/**
|
||||
* Which level is required for early access
|
||||
*/
|
||||
public int earlyAccessLevel;
|
||||
|
||||
/**
|
||||
* Which level is required for data sync
|
||||
*/
|
||||
public int syncRequiredLevel;
|
||||
|
||||
/**
|
||||
* Which level is required for AI model generator
|
||||
*/
|
||||
public int aiGeneratorRequiredLevel;
|
||||
|
||||
/**
|
||||
* Models per month max
|
||||
*/
|
||||
public int aiGeneratorModelsPerMonth;
|
||||
|
||||
/**
|
||||
* Url at which user should be redirected for info about how to restore a subscription
|
||||
*/
|
||||
public String alreadySubscribedInfoUrl;
|
||||
|
||||
/**
|
||||
* List of subscription levels
|
||||
*/
|
||||
public List<SubscriptionLevel> levels = new ArrayList<>();
|
||||
}
|
||||
|
||||
final class SubscriptionLevel {
|
||||
/**
|
||||
* Int representation
|
||||
*/
|
||||
public int level;
|
||||
|
||||
/**
|
||||
* Title of this level
|
||||
*/
|
||||
public String title;
|
||||
|
||||
/**
|
||||
* Price of this level
|
||||
*/
|
||||
public String price;
|
||||
|
||||
/**
|
||||
* Url at which user should be redirected for purchase
|
||||
*/
|
||||
public String subscribeOrUpgradeUrl;
|
||||
|
||||
/**
|
||||
* Url at which user should be redirected for managing the subscription
|
||||
*/
|
||||
public String manageUrl;
|
||||
}
|
||||
|
||||
|
||||
final class UserInfo {
|
||||
/**
|
||||
* User's id
|
||||
*/
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* User's display name
|
||||
*/
|
||||
public String displayName;
|
||||
|
||||
/**
|
||||
* User's avatar. Could be null
|
||||
*/
|
||||
@Nullable
|
||||
public String avatarUrl;
|
||||
|
||||
/**
|
||||
* Current subscription level
|
||||
*/
|
||||
public int currentLevel;
|
||||
}
|
||||
|
||||
|
||||
final class SyncState {
|
||||
/**
|
||||
* Cloud data last updated time
|
||||
*/
|
||||
public long lastUpdatedDate = 0;
|
||||
|
||||
/**
|
||||
* Used size of cloud storage
|
||||
*/
|
||||
public long usedSize;
|
||||
|
||||
/**
|
||||
* Max storage size
|
||||
*/
|
||||
public long maxSize;
|
||||
}
|
||||
|
||||
final class ModelsRemainingCount {
|
||||
/**
|
||||
* Used generations
|
||||
*/
|
||||
public int used;
|
||||
|
||||
/**
|
||||
* Max available generations
|
||||
*/
|
||||
public int max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package com.dark98.santoku.cloud;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import ru.ytkab0bp.sapil.APICallback;
|
||||
import ru.ytkab0bp.sapil.APIRequestHandle;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.events.CloudFeaturesUpdatedEvent;
|
||||
import com.dark98.santoku.events.CloudLoginStateUpdatedEvent;
|
||||
import com.dark98.santoku.events.CloudModelsRemainingCountUpdatedEvent;
|
||||
import com.dark98.santoku.events.CloudSyncFinishedEvent;
|
||||
import com.dark98.santoku.events.CloudUserInfoUpdatedEvent;
|
||||
import com.dark98.santoku.events.NeedDismissSnackbarEvent;
|
||||
import com.dark98.santoku.events.NeedSnackbarEvent;
|
||||
import com.dark98.santoku.slic3r.Slic3rConfigWrapper;
|
||||
import com.dark98.santoku.utils.IOUtils;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.SnackbarsLayout;
|
||||
|
||||
public class CloudController {
|
||||
public final static String USER_INFO_AI_GEN_TAG = "ai_gen_user_info";
|
||||
public final static String CLOUD_SYNC_TAG = "cloud_sync";
|
||||
|
||||
private final static String TAG = "cloud";
|
||||
private final static long MIN_SYNC_DELTA = 5 * 60 * 1000L; // Once in 5 minutes
|
||||
private final static long MIN_SYNC_FEATURES_DELTA = 12 * 60 * 60 * 1000L; // Once in 12 hours
|
||||
|
||||
private static boolean isSyncInProgress;
|
||||
private static CloudAPI.UserInfo userInfo;
|
||||
private static CloudAPI.UserFeatures userFeatures;
|
||||
|
||||
private static int modelsUsed;
|
||||
private static int modelsMaxGenerations;
|
||||
private static boolean isLoggingIn;
|
||||
private static APIRequestHandle beginLoginHandle;
|
||||
private static String loginSessionId;
|
||||
private static Runnable loginAutoCancel = () -> {
|
||||
loginSessionId = null;
|
||||
isLoggingIn = false;
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
};
|
||||
private static Runnable loginCheck = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
CloudAPI.INSTANCE.loginCheck(loginSessionId, new APICallback<CloudAPI.LoginState>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.LoginState response) {
|
||||
if (response.loggedIn) {
|
||||
Prefs.setCloudAPIToken(response.bearer);
|
||||
loadUserInfo();
|
||||
ViewUtils.removeCallbacks(loginAutoCancel);
|
||||
} else if (isLoggingIn) {
|
||||
ViewUtils.postOnMainThread(loginCheck, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to check login state", e);
|
||||
|
||||
if (isLoggingIn) {
|
||||
ViewUtils.postOnMainThread(loginCheck, 5000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private static Gson gson = new Gson();
|
||||
|
||||
public static void initCached() {
|
||||
if (Prefs.getCloudCachedUserFeatures() != null) {
|
||||
userFeatures = gson.fromJson(Prefs.getCloudCachedUserFeatures(), CloudAPI.UserFeatures.class);
|
||||
}
|
||||
if (Prefs.getCloudAPIToken() != null) {
|
||||
if (Prefs.getCloudCachedUserInfo() != null) {
|
||||
userInfo = gson.fromJson(Prefs.getCloudCachedUserInfo(), CloudAPI.UserInfo.class);
|
||||
modelsUsed = Prefs.getCloudCachedUsedModels();
|
||||
modelsMaxGenerations = Prefs.getCloudCachedMaxModels();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
long now = Santoku.TRUE_TIME.now().getTime();
|
||||
boolean needSyncInfo = userFeatures == null || now - Prefs.getCloudLastFeaturesSync() > MIN_SYNC_FEATURES_DELTA;
|
||||
if (needSyncInfo) {
|
||||
checkUserFeatures();
|
||||
}
|
||||
|
||||
if (Prefs.getCloudAPIToken() != null) {
|
||||
if (needSyncInfo || userInfo == null) {
|
||||
loadUserInfo();
|
||||
}
|
||||
|
||||
if (!needSyncInfo && userInfo != null && isSyncAvailable() && Prefs.isCloudProfileSyncEnabled()) {
|
||||
if (now - Prefs.getCloudLastSync() > MIN_SYNC_DELTA) {
|
||||
syncData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadUserInfo() {
|
||||
CloudAPI.INSTANCE.userGetInfo(new APICallback<CloudAPI.UserInfo>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.UserInfo response) {
|
||||
userInfo = response;
|
||||
|
||||
if (userInfo.id.equals("null")) {
|
||||
userInfo = null;
|
||||
Prefs.setCloudAPIToken(null);
|
||||
Prefs.setCloudCachedUserInfo(null);
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
|
||||
|
||||
if (isLoggingIn) {
|
||||
isLoggingIn = false;
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
}
|
||||
} else {
|
||||
Prefs.setCloudCachedUserInfo(gson.toJson(userInfo));
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(USER_INFO_AI_GEN_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
|
||||
|
||||
if (isLoggingIn) {
|
||||
isLoggingIn = false;
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
}
|
||||
|
||||
if (isSyncAvailable() && Prefs.isCloudProfileSyncEnabled()) {
|
||||
syncData();
|
||||
}
|
||||
checkGeneratorRemaining();
|
||||
}
|
||||
Prefs.setCloudLastFeaturesSync(Santoku.TRUE_TIME.now().getTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to get user info", e);
|
||||
ViewUtils.postOnMainThread(CloudController::init, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isLoggingIn() {
|
||||
return isLoggingIn;
|
||||
}
|
||||
|
||||
private static void beginLogin0() {
|
||||
beginLoginHandle = CloudAPI.INSTANCE.loginBegin(new APICallback<CloudAPI.LoginData>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.LoginData response) {
|
||||
loginSessionId = response.sessionId;
|
||||
|
||||
ViewUtils.postOnMainThread(loginAutoCancel, response.expiresAt * 1000L - Santoku.TRUE_TIME.now().getTime());
|
||||
ViewUtils.postOnMainThread(loginCheck, 5000);
|
||||
ViewUtils.postOnMainThread(() -> Santoku.INSTANCE.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(response.url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
ViewUtils.postOnMainThread(CloudController::beginLogin0, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void beginLogin() {
|
||||
isLoggingIn = true;
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
beginLogin0();
|
||||
}
|
||||
|
||||
public static void cancelLogin() {
|
||||
isLoggingIn = false;
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
if (loginSessionId != null) {
|
||||
CloudAPI.INSTANCE.loginCancel(loginSessionId, response -> {});
|
||||
}
|
||||
if (beginLoginHandle != null && beginLoginHandle.isRunning()) {
|
||||
beginLoginHandle.cancel();
|
||||
beginLoginHandle = null;
|
||||
}
|
||||
ViewUtils.removeCallbacks(loginCheck);
|
||||
ViewUtils.removeCallbacks(loginAutoCancel);
|
||||
loginSessionId = null;
|
||||
}
|
||||
|
||||
public static void logout() {
|
||||
Prefs.setCloudAPIToken(null);
|
||||
userInfo = null;
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
|
||||
CloudAPI.INSTANCE.logout(response -> {});
|
||||
}
|
||||
|
||||
public static void checkGeneratorRemaining() {
|
||||
CloudAPI.INSTANCE.modelsGetRemainingCount(new APICallback<CloudAPI.ModelsRemainingCount>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.ModelsRemainingCount response) {
|
||||
modelsUsed = response.used;
|
||||
modelsMaxGenerations = response.max;
|
||||
Prefs.setCloudCachedUsedMaxModels(modelsUsed, modelsMaxGenerations);
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudModelsRemainingCountUpdatedEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to check remaining models", e);
|
||||
ViewUtils.postOnMainThread(CloudController::checkGeneratorRemaining, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void checkUserFeatures() {
|
||||
CloudAPI.INSTANCE.userGetFeatures(new APICallback<CloudAPI.UserFeatures>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.UserFeatures response) {
|
||||
userFeatures = response;
|
||||
Prefs.setCloudCachedUserFeatures(gson.toJson(userFeatures));
|
||||
if (Prefs.getCloudAPIToken() == null) {
|
||||
Prefs.setCloudLastFeaturesSync(Santoku.TRUE_TIME.now().getTime());
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudFeaturesUpdatedEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to get user features", e);
|
||||
ViewUtils.postOnMainThread(CloudController::checkUserFeatures, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static CloudAPI.UserInfo getUserInfo() {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
public static CloudAPI.UserFeatures getUserFeatures() {
|
||||
return userFeatures;
|
||||
}
|
||||
|
||||
public static boolean hasAccountFeatures() {
|
||||
return userFeatures != null && userFeatures.levels != null && !userFeatures.levels.isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isSyncAvailable() {
|
||||
return Prefs.getCloudAPIToken() != null && userInfo != null && userFeatures != null && userInfo.currentLevel >= userFeatures.syncRequiredLevel;
|
||||
}
|
||||
|
||||
public static boolean needShowAIGenerator() {
|
||||
return userFeatures != null && userFeatures.aiGeneratorRequiredLevel >= 0;
|
||||
}
|
||||
|
||||
public static int getGeneratedModels() {
|
||||
return modelsUsed;
|
||||
}
|
||||
|
||||
public static int getMaxGeneratedModels() {
|
||||
return modelsMaxGenerations;
|
||||
}
|
||||
|
||||
private static void downloadData(long lastModified) {
|
||||
CloudAPI.INSTANCE.syncGet(new APICallback<String>() {
|
||||
@Override
|
||||
public void onResponse(String response) {
|
||||
IOUtils.IO_POOL.submit(() -> {
|
||||
try {
|
||||
File f = Santoku.getConfigFile();
|
||||
byte[] data = Base64.decode(response, 0);
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
fos.write(data);
|
||||
fos.close();
|
||||
|
||||
Santoku.CONFIG = new Slic3rConfigWrapper(f);
|
||||
|
||||
Prefs.setCloudLocalLastModified(lastModified);
|
||||
Prefs.setCloudLocalLastSentModified(lastModified);
|
||||
Prefs.setCloudRemoteLastModified(lastModified);
|
||||
Prefs.setCloudLastSync(Santoku.TRUE_TIME.now().getTime());
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.CloudSyncSuccess));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to write data", e);
|
||||
isSyncInProgress = false;
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to download data", e);
|
||||
isSyncInProgress = false;
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void syncData() {
|
||||
if (isSyncInProgress) {
|
||||
return;
|
||||
}
|
||||
long modified = Prefs.getCloudLocalLastModified();
|
||||
isSyncInProgress = true;
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.CloudSyncInProgress).tag(CLOUD_SYNC_TAG));
|
||||
|
||||
CloudAPI.INSTANCE.syncGetState(new APICallback<CloudAPI.SyncState>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.SyncState response) {
|
||||
if (Santoku.CONFIG == null && response.usedSize != 0) {
|
||||
// Setup screen, no config yet
|
||||
downloadData(response.lastUpdatedDate);
|
||||
} else if (response.usedSize == 0) {
|
||||
if (Santoku.CONFIG == null) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
return;
|
||||
}
|
||||
|
||||
// No data on server yet, send anyway
|
||||
uploadData(modified);
|
||||
} else if (response.lastUpdatedDate != Prefs.getCloudRemoteLastModified()) {
|
||||
if (Prefs.getCloudLocalLastSentModified() == modified) {
|
||||
// Modified only on server
|
||||
downloadData(response.lastUpdatedDate);
|
||||
} else {
|
||||
// Modified on client and on server
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.CloudSyncConflict).button(R.string.CloudSyncConflictResolve, v -> {
|
||||
SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault());
|
||||
new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.CloudSyncConflict)
|
||||
.setMessage(v.getContext().getString(R.string.CloudSyncConflictResolveMessage, format.format(new Date(response.lastUpdatedDate)), format.format(new Date(Prefs.getCloudLocalLastModified()))))
|
||||
.setPositiveButton(R.string.CloudSyncConflictChooseRemote, (dialog, which) -> downloadData(response.lastUpdatedDate))
|
||||
.setNegativeButton(R.string.CloudSyncConflictChooseLocal, (dialog, which) -> uploadData(modified))
|
||||
.show();
|
||||
}).tag(CLOUD_SYNC_TAG));
|
||||
}
|
||||
} else {
|
||||
if (Prefs.getCloudLocalLastSentModified() != modified) {
|
||||
// Modified only on client
|
||||
uploadData(modified);
|
||||
} else {
|
||||
// Not modified on server and on client
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to get sync state", e);
|
||||
isSyncInProgress = false;
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void uploadData(long modified) {
|
||||
IOUtils.IO_POOL.submit(() -> {
|
||||
try {
|
||||
File f = Santoku.getConfigFile();
|
||||
FileInputStream fis = new FileInputStream(f);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[10240];
|
||||
int c;
|
||||
while ((c = fis.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, c);
|
||||
}
|
||||
bos.close();
|
||||
fis.close();
|
||||
|
||||
CloudAPI.INSTANCE.syncUpload(Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP), "application/ini", new APICallback<CloudAPI.SyncState>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.SyncState response) {
|
||||
isSyncInProgress = false;
|
||||
if (Prefs.getCloudLocalLastModified() != modified) { // Re-send otherwise
|
||||
syncData();
|
||||
return;
|
||||
}
|
||||
Prefs.setCloudRemoteLastModified(response.lastUpdatedDate);
|
||||
Prefs.setCloudLocalLastSentModified(modified);
|
||||
Prefs.setCloudLastSync(Santoku.TRUE_TIME.now().getTime());
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.CloudSyncSuccess));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to upload sync data", e);
|
||||
isSyncInProgress = false;
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to read sync data", e);
|
||||
isSyncInProgress = false;
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
|
||||
Santoku.EVENT_BUS.fireEvent(new CloudSyncFinishedEvent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void notifyDataChanged() {
|
||||
long now = Santoku.TRUE_TIME.now().getTime();
|
||||
Prefs.setCloudLocalLastModified(now);
|
||||
if (!isSyncAvailable() || !Prefs.isCloudProfileSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (now - Prefs.getCloudLastSync() > MIN_SYNC_DELTA) {
|
||||
syncData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.database.DataSetObserver;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Build;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.AppCompatCheckedTextView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class BeamAlertDialogBuilder extends MaterialAlertDialogBuilder {
|
||||
public BeamAlertDialogBuilder(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public BeamAlertDialogBuilder(@NonNull Context context, int overrideThemeResId) {
|
||||
super(context, overrideThemeResId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AlertDialog create() {
|
||||
AlertDialog dialog = super.create();
|
||||
dialog.getWindow().setBackgroundDrawable(new GradientDrawable() {{
|
||||
setCornerRadius(ViewUtils.dp(32));
|
||||
setColor(ThemesRepo.getColor(R.attr.dialogBackground));
|
||||
}});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlertDialog show() {
|
||||
AlertDialog dialog = super.show();
|
||||
View v = dialog.getWindow().getDecorView();
|
||||
TextView v2;
|
||||
int id = getContext().getResources().getIdentifier("alertTitle", "id", getContext().getPackageName());
|
||||
if (id > 0) {
|
||||
v2 = v.findViewById(id);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
}
|
||||
}
|
||||
v2 = v.findViewById(android.R.id.message);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
}
|
||||
|
||||
v2 = v.findViewById(android.R.id.button1);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
}
|
||||
v2 = v.findViewById(android.R.id.button2);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
}
|
||||
v2 = v.findViewById(android.R.id.button3);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
}
|
||||
id = getContext().getResources().getIdentifier("select_dialog_listview", "id", getContext().getPackageName());
|
||||
if (id > 0) {
|
||||
ListView lv = v.findViewById(id);
|
||||
if (lv != null) {
|
||||
ListAdapter wrapped = lv.getAdapter();
|
||||
SparseBooleanArray checked = lv.getCheckedItemPositions() != null ? lv.getCheckedItemPositions().clone() : null;
|
||||
lv.setAdapter(new ListAdapter() {
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return wrapped.areAllItemsEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
return wrapped.isEnabled(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerDataSetObserver(DataSetObserver observer) {
|
||||
wrapped.registerDataSetObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterDataSetObserver(DataSetObserver observer) {
|
||||
wrapped.unregisterDataSetObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return wrapped.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return wrapped.getItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return wrapped.getItemId(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return wrapped.hasStableIds();
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View v = wrapped.getView(position, convertView, parent);
|
||||
TextView text = v.findViewById(android.R.id.text1);
|
||||
if (text != null) {
|
||||
text.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
if (text instanceof AppCompatCheckedTextView) {
|
||||
((AppCompatCheckedTextView) text).setSupportCompoundDrawablesTintList(new ColorStateList(new int[][]{
|
||||
{android.R.attr.state_enabled, android.R.attr.state_checked},
|
||||
{android.R.attr.state_enabled, -android.R.attr.state_checked}
|
||||
}, new int[]{
|
||||
ThemesRepo.getColor(android.R.attr.colorAccent),
|
||||
ThemesRepo.getColor(R.attr.dividerContrastColor)
|
||||
}));
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return wrapped.getItemViewType(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return wrapped.getViewTypeCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return wrapped.isEmpty();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence[] getAutofillOptions() {
|
||||
return wrapped.getAutofillOptions();
|
||||
}
|
||||
});
|
||||
if (checked != null) {
|
||||
for (int i = 0; i < checked.size(); i++) {
|
||||
lv.setItemChecked(checked.keyAt(i), checked.valueAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mrudultora.colorpicker.ColorPickerPopUp;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class BeamColorPickerPopUp extends ColorPickerPopUp {
|
||||
public BeamColorPickerPopUp(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
|
||||
Dialog dialog = getDialog();
|
||||
dialog.getWindow().setBackgroundDrawable(new GradientDrawable() {{
|
||||
setCornerRadius(ViewUtils.dp(32));
|
||||
setColor(ThemesRepo.getColor(R.attr.dialogBackground));
|
||||
}});
|
||||
View v = dialog.getWindow().getDecorView();
|
||||
TextView v2;
|
||||
int id = getResources().getIdentifier("alertTitle", "id", "android");
|
||||
if (id != -1) {
|
||||
v2 = v.findViewById(id);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
}
|
||||
}
|
||||
v2 = v.findViewById(android.R.id.button1);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
}
|
||||
v2 = v.findViewById(android.R.id.button2);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
}
|
||||
v2 = v.findViewById(android.R.id.button3);
|
||||
if (v2 != null) {
|
||||
v2.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Scroller;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.BeamServerData;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.events.BeamServerDataUpdatedEvent;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.BeamButton;
|
||||
import com.dark98.santoku.view.BoostySubsView;
|
||||
|
||||
public class ChangeLogBottomSheet extends BottomSheetDialog {
|
||||
private BoostySubsView subsView;
|
||||
private ScrollView scrollView;
|
||||
private ViewPager pager;
|
||||
|
||||
public ChangeLogBottomSheet(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
LinearLayout ll = new LinearLayout(context);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setCornerRadii(new float[] {
|
||||
ViewUtils.dp(28), ViewUtils.dp(28),
|
||||
ViewUtils.dp(28), ViewUtils.dp(28),
|
||||
0, 0,
|
||||
0, 0
|
||||
});
|
||||
gd.setColor(ThemesRepo.getColor(R.attr.dialogBackground));
|
||||
ll.setBackground(gd);
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, ViewUtils.dp(12));
|
||||
|
||||
FrameLayout fl = new FrameLayout(context);
|
||||
TextView titleA = new TextView(context);
|
||||
titleA.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
titleA.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
titleA.setText(R.string.Changelog);
|
||||
titleA.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
titleA.setGravity(Gravity.CENTER);
|
||||
titleA.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
fl.addView(titleA);
|
||||
|
||||
TextView titleB = new TextView(context);
|
||||
titleB.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
titleB.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
titleB.setText(R.string.ChangelogBoosty);
|
||||
titleB.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
titleB.setGravity(Gravity.CENTER);
|
||||
titleB.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
titleB.setAlpha(0f);
|
||||
fl.addView(titleB);
|
||||
|
||||
ll.addView(fl);
|
||||
|
||||
scrollView = new ScrollView(context);
|
||||
TextView text = new TextView(context);
|
||||
text.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
text.setPadding(ViewUtils.dp(16), ViewUtils.dp(12), ViewUtils.dp(16), ViewUtils.dp(12));
|
||||
|
||||
try {
|
||||
InputStream in = getContext().getAssets().open("update.json");
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[10240]; int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, c);
|
||||
}
|
||||
bos.close();
|
||||
in.close();
|
||||
|
||||
JSONObject obj = new JSONObject(bos.toString());
|
||||
String code = Locale.getDefault().getLanguage();
|
||||
if (obj.has(code)) {
|
||||
text.setText(obj.getString(code));
|
||||
} else {
|
||||
text.setText(obj.getString("en"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("Changelog", "Failed to open update file", e);
|
||||
}
|
||||
scrollView.addView(text);
|
||||
|
||||
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
|
||||
pager = new ViewPager(context) {{
|
||||
try {
|
||||
Field scroller = ViewPager.class.getDeclaredField("mScroller");
|
||||
scroller.setAccessible(true);
|
||||
|
||||
Scroller mScroller = new Scroller(getContext(), ViewUtils.CUBIC_INTERPOLATOR::getInterpolation);
|
||||
scroller.set(this, mScroller);
|
||||
} catch (Exception ignored) {}
|
||||
}};
|
||||
pager.setAdapter(new PagerAdapter() {
|
||||
@Override
|
||||
public int getCount() {
|
||||
return BeamServerData.isBoostyAvailable() ? 2 : 1;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View v;
|
||||
if (position == 0) {
|
||||
v = scrollView;
|
||||
} else {
|
||||
LinearLayout ll = new LinearLayout(context);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
TextView subtitle = new TextView(context);
|
||||
subtitle.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
subtitle.setText(R.string.ChangelogBoostyDescription);
|
||||
subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
subtitle.setGravity(Gravity.CENTER);
|
||||
subtitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
subtitle.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
ll.addView(subtitle);
|
||||
|
||||
subsView = new BoostySubsView(context);
|
||||
if (Santoku.SERVER_DATA != null) {
|
||||
List<String> list = new ArrayList<>(Santoku.SERVER_DATA.boostySubscribers);
|
||||
Collections.shuffle(list);
|
||||
subsView.setStrings(list);
|
||||
}
|
||||
ll.addView(subsView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
TextView subscribeButton = new TextView(context);
|
||||
subscribeButton.setText(R.string.IntroBoostySupport);
|
||||
subscribeButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
subscribeButton.setTextColor(ThemesRepo.getColor(R.attr.boostyColorTop));
|
||||
subscribeButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
subscribeButton.setGravity(Gravity.CENTER);
|
||||
subscribeButton.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8));
|
||||
subscribeButton.setOnClickListener(v2 -> context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://boosty.to/ytkab0bp"))));
|
||||
ll.addView(subscribeButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
v = ll;
|
||||
}
|
||||
|
||||
container.addView(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
container.removeView((View) object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
}
|
||||
});
|
||||
BeamButton btn = new BeamButton(context);
|
||||
pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
super.onPageSelected(position);
|
||||
if (position == pager.getAdapter().getCount() - 1) {
|
||||
btn.setText(R.string.ChangelogOK);
|
||||
} else {
|
||||
btn.setText(R.string.ChangelogNext);
|
||||
}
|
||||
}
|
||||
|
||||
private int[] colors = new int[2];
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
float pr = position == 0 ? positionOffset : 1f;
|
||||
colors[0] = ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.dialogBackground), ThemesRepo.getColor(R.attr.boostyColorTop), pr);
|
||||
colors[1] = ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.dialogBackground), ThemesRepo.getColor(R.attr.boostyColorBottom), pr);
|
||||
gd.setColors(colors);
|
||||
titleA.setAlpha(1f - pr);
|
||||
titleA.setTranslationX(-titleA.getWidth() * 0.25f * pr);
|
||||
titleB.setAlpha(pr);
|
||||
titleB.setTranslationX(titleB.getWidth() * 0.25f * (1f - pr));
|
||||
btn.setColor(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.colorAccent), ThemesRepo.getColor(R.attr.boostyColorTop), pr));
|
||||
}
|
||||
});
|
||||
ll.addView(pager, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) (dm.heightPixels * 0.45f)));
|
||||
|
||||
btn.setText(R.string.ChangelogNext);
|
||||
btn.setOnClickListener(v -> {
|
||||
if (pager.getCurrentItem() != pager.getAdapter().getCount() - 1) {
|
||||
pager.setCurrentItem(pager.getCurrentItem() + 1);
|
||||
} else {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
ll.addView(btn, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{
|
||||
leftMargin = topMargin = rightMargin = bottomMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
|
||||
ll.setFitsSystemWindows(true);
|
||||
setContentView(ll);
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
setOnDismissListener(dialog -> Santoku.EVENT_BUS.unregisterListener(this));
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDataUpdated(BeamServerDataUpdatedEvent e) {
|
||||
if (Santoku.SERVER_DATA != null) {
|
||||
List<String> list = new ArrayList<>(Santoku.SERVER_DATA.boostySubscribers);
|
||||
Collections.shuffle(list);
|
||||
subsView.setStrings(list);
|
||||
}
|
||||
pager.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.cloud.CloudAPI;
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
import com.dark98.santoku.events.NeedDismissSnackbarEvent;
|
||||
import com.dark98.santoku.recycler.PreferenceSwitchItem;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerAdapter;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.TextColorImageSpan;
|
||||
|
||||
public class CloudManageBottomSheet extends BottomSheetDialog {
|
||||
public CloudManageBottomSheet(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
LinearLayout ll = new LinearLayout(context);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setCornerRadii(new float[] {
|
||||
ViewUtils.dp(28), ViewUtils.dp(28),
|
||||
ViewUtils.dp(28), ViewUtils.dp(28),
|
||||
0, 0,
|
||||
0, 0
|
||||
});
|
||||
gd.setColor(ThemesRepo.getColor(R.attr.dialogBackground));
|
||||
ll.setBackground(gd);
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, ViewUtils.dp(12));
|
||||
|
||||
TextView title = new TextView(context);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setText(R.string.SettingsCloudManageButtonManage);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
ll.addView(title);
|
||||
|
||||
TextView description = new TextView(context);
|
||||
description.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
description.setText(context.getString(R.string.SettingsCloudManageLoggedInAs, CloudController.getUserInfo().displayName));
|
||||
description.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
description.setGravity(Gravity.CENTER);
|
||||
description.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
topMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
ll.addView(description);
|
||||
|
||||
int currentLevel = CloudController.getUserInfo().currentLevel;
|
||||
CloudAPI.SubscriptionLevel lvl = null;
|
||||
CloudAPI.UserFeatures features = CloudController.getUserFeatures();
|
||||
for (CloudAPI.SubscriptionLevel level : features.levels) {
|
||||
if (level.level != -1 && level.level <= currentLevel && (lvl == null || level.level > lvl.level)) {
|
||||
lvl = level;
|
||||
}
|
||||
}
|
||||
|
||||
if (lvl != null) {
|
||||
List<SimpleRecyclerItem> items = new ArrayList<>();
|
||||
if (currentLevel >= features.syncRequiredLevel) {
|
||||
items.add(new PreferenceSwitchItem()
|
||||
.setIcon(R.drawable.sync_outline_28)
|
||||
.setTitle(context.getString(R.string.SettingsCloudManageFeatureCloudSync))
|
||||
.setValueProvider(Prefs::isCloudProfileSyncEnabled)
|
||||
.setChangeListener((buttonView, isChecked) -> {
|
||||
Prefs.setCloudProfileSyncEnabled(isChecked);
|
||||
if (isChecked) {
|
||||
CloudController.notifyDataChanged();
|
||||
} else {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CloudController.CLOUD_SYNC_TAG));
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (!items.isEmpty()) {
|
||||
RecyclerView recyclerView = new RecyclerView(context);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
recyclerView.setBackground(ViewUtils.createRipple(0, ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10), 16));
|
||||
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
|
||||
adapter.setItems(items);
|
||||
recyclerView.setAdapter(adapter);
|
||||
ll.addView(recyclerView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
topMargin = ViewUtils.dp(16);
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
}});
|
||||
}
|
||||
|
||||
TextView manageButton = new TextView(context);
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(context.getString(R.string.SettingsCloudManageSubscription)).append(" ");
|
||||
Drawable dr = ContextCompat.getDrawable(context, R.drawable.external_link_outline_24);
|
||||
int size = ViewUtils.dp(16);
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.append("d", new TextColorImageSpan(dr, ViewUtils.dp(2f)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
manageButton.setText(sb);
|
||||
manageButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
manageButton.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
manageButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
manageButton.setGravity(Gravity.CENTER);
|
||||
manageButton.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8));
|
||||
manageButton.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
CloudAPI.SubscriptionLevel finalLvl = lvl;
|
||||
manageButton.setOnClickListener(v -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(finalLvl.manageUrl))));
|
||||
ll.addView(manageButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
topMargin = bottomMargin = ViewUtils.dp(6);
|
||||
}});
|
||||
} else {
|
||||
ll.addView(new Space(context), new LinearLayout.LayoutParams(0, ViewUtils.dp(16)));
|
||||
}
|
||||
|
||||
TextView buttonView = new TextView(context);
|
||||
buttonView.setText(R.string.SettingsCloudManageButtonLogOut);
|
||||
buttonView.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
buttonView.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
buttonView.setGravity(Gravity.CENTER);
|
||||
buttonView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(R.attr.textColorNegative), 16));
|
||||
buttonView.setOnClickListener(v-> {
|
||||
CloudController.logout();
|
||||
dismiss();
|
||||
});
|
||||
ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
setContentView(ll);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.events.SlicingProgressEvent;
|
||||
import com.dark98.santoku.slic3r.Slic3rLocalization;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class SliceProgressBottomSheet extends BottomSheetDialog {
|
||||
private RecyclerView recyclerView;
|
||||
private LinearProgressIndicator indicator;
|
||||
private List<String> lines = new ArrayList<>();
|
||||
|
||||
public SliceProgressBottomSheet(@NonNull Context context) {
|
||||
super(context);
|
||||
setCancelable(false);
|
||||
|
||||
LinearLayout ll = new LinearLayout(context);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setBackgroundResource(R.drawable.bottom_sheet_rounded_background);
|
||||
ll.setBackgroundTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.dialogBackground)));
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, ViewUtils.dp(12));
|
||||
|
||||
TextView title = new TextView(context);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setText(R.string.SliceInProgress);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
ll.addView(title);
|
||||
|
||||
indicator = new LinearProgressIndicator(context);
|
||||
indicator.setIndicatorColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
indicator.setTrackColor(ThemesRepo.getColor(R.attr.dividerColor));
|
||||
indicator.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1)) {{
|
||||
topMargin = ViewUtils.dp(8);
|
||||
bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
ll.addView(indicator);
|
||||
|
||||
recyclerView = new RecyclerView(context);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
|
||||
boolean portrait = dm.widthPixels < dm.heightPixels;
|
||||
recyclerView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) (dm.heightPixels * (portrait ? 0.4f : 0.6f))));
|
||||
recyclerView.setAdapter(new RecyclerView.Adapter() {
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
TextView v = new TextView(parent.getContext());
|
||||
v.setPadding(ViewUtils.dp(12), ViewUtils.dp(4), ViewUtils.dp(12), ViewUtils.dp(4));
|
||||
v.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
|
||||
v.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return new RecyclerView.ViewHolder(v) {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
TextView tv = (TextView) holder.itemView;
|
||||
tv.setText(lines.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return lines.size();
|
||||
}
|
||||
});
|
||||
ll.addView(recyclerView);
|
||||
|
||||
ll.setFitsSystemWindows(true);
|
||||
setContentView(ll);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onProgressChanged(SlicingProgressEvent e) {
|
||||
if (!e.message.isEmpty()) {
|
||||
int size = lines.size();
|
||||
lines.add(Slic3rLocalization.getString(e.message) + "...");
|
||||
recyclerView.getAdapter().notifyItemInserted(size);
|
||||
recyclerView.smoothScrollToPosition(size);
|
||||
}
|
||||
indicator.setProgressCompat(e.progress, true);
|
||||
|
||||
if (e.progress == 100) {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.MirrorView;
|
||||
|
||||
public abstract class UnfoldMenu {
|
||||
protected BedFragment fragment;
|
||||
|
||||
private boolean isVisible;
|
||||
private boolean isDismissing;
|
||||
private SpringAnimation spring;
|
||||
private DynamicAnimation.OnAnimationUpdateListener updateListener;
|
||||
private FrameLayout containerLayout;
|
||||
private View innerView;
|
||||
|
||||
private Runnable onDismiss;
|
||||
|
||||
private View dimmView;
|
||||
private FrameLayout rootView;
|
||||
private float progress;
|
||||
|
||||
private float fromTranslationX;
|
||||
private float fromTranslationY;
|
||||
private float toTranslationX;
|
||||
private float toTranslationY;
|
||||
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return (int) ((portrait ? into.getHeight() : into.getWidth()) * 0.6f);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected void onCreate() {}
|
||||
|
||||
@CallSuper
|
||||
protected void onDestroy() {
|
||||
if (onDismiss != null) {
|
||||
onDismiss.run();
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDismiss(Runnable onDismiss) {
|
||||
this.onDismiss = onDismiss;
|
||||
}
|
||||
|
||||
protected abstract View onCreateView(Context ctx, boolean portrait);
|
||||
|
||||
public void show(View from, BedFragment fragment) {
|
||||
show(from, fragment, fragment.getOverlayLayout());
|
||||
}
|
||||
|
||||
public void show(View from, FrameLayout into) {
|
||||
show(from, null, into);
|
||||
}
|
||||
|
||||
private void show(View from, BedFragment fragment, FrameLayout into) {
|
||||
if (isVisible) return;
|
||||
if (fragment != null) {
|
||||
this.fragment = fragment;
|
||||
}
|
||||
this.isVisible = true;
|
||||
this.containerLayout = into;
|
||||
|
||||
boolean portrait = into.getWidth() < into.getHeight();
|
||||
|
||||
Context ctx = into.getContext();
|
||||
MirrorView mirror = new MirrorView(ctx);
|
||||
mirror.setMirroredView(from);
|
||||
mirror.setLayoutParams(new FrameLayout.LayoutParams(from.getWidth(), from.getHeight()));
|
||||
|
||||
int[] pos = new int[2];
|
||||
from.getLocationInWindow(pos);
|
||||
int[] intoPos = new int[2];
|
||||
into.getLocationInWindow(intoPos);
|
||||
intoPos[0] += into.getPaddingLeft();
|
||||
intoPos[1] += into.getPaddingTop();
|
||||
|
||||
int side = getRequestedSize(into, portrait) + ((View) into.getParent().getParent()).getPaddingBottom();
|
||||
fromTranslationX = pos[0] - intoPos[0];
|
||||
fromTranslationY = pos[1] - intoPos[1];
|
||||
toTranslationX = 0;
|
||||
toTranslationY = portrait ? into.getHeight() - side - into.getPaddingTop() - into.getPaddingBottom() : 0;
|
||||
rootView = new FrameLayout(ctx) {
|
||||
{
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
private Path path = new Path();
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
canvas.save();
|
||||
path.rewind();
|
||||
float rad = ViewUtils.dp(16) * (1f - progress);
|
||||
path.addRoundRect(0, 0,
|
||||
ViewUtils.lerp(mirror.getWidth(), getWidth(), progress), ViewUtils.lerp(mirror.getHeight(), getHeight(), progress),
|
||||
rad, rad, Path.Direction.CW);
|
||||
canvas.clipPath(path);
|
||||
canvas.drawColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (isVisible) {
|
||||
onCreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (isVisible) {
|
||||
onDestroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
rootView.addView(mirror);
|
||||
rootView.addView(innerView = onCreateView(ctx, portrait));
|
||||
innerView.setAlpha(0f);
|
||||
|
||||
rootView.setTranslationX(fromTranslationX);
|
||||
rootView.setTranslationY(fromTranslationY);
|
||||
|
||||
dimmView = new View(ctx);
|
||||
dimmView.setBackgroundColor(0x40000000);
|
||||
dimmView.setTranslationX(toTranslationX);
|
||||
dimmView.setTranslationY(toTranslationY);
|
||||
dimmView.setAlpha(0f);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(portrait ? ViewGroup.LayoutParams.MATCH_PARENT : side, portrait ? side : ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
into.addView(dimmView, params);
|
||||
|
||||
into.addView(rootView, params);
|
||||
|
||||
float invY = into.getHeight() - ViewUtils.dp(80 * 2) - toTranslationY;
|
||||
spring = new SpringAnimation(new FloatValueHolder(0))
|
||||
.setMinimumVisibleChange(1 / 256f)
|
||||
.setSpring(new SpringForce(1f)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener(updateListener = (animation, value, velocity) -> {
|
||||
this.progress = value;
|
||||
rootView.invalidate();
|
||||
|
||||
dimmView.setAlpha(value);
|
||||
|
||||
rootView.setTranslationX(ViewUtils.lerp(fromTranslationX, toTranslationX, value));
|
||||
rootView.setTranslationY(ViewUtils.lerp(fromTranslationY, toTranslationY, value));
|
||||
|
||||
float mirrorValue = Math.min(0.75f, value) / 0.75f;
|
||||
mirror.setAlpha((1f - mirrorValue) * mirror.getMirroredView().getAlpha());
|
||||
mirror.setScaleX(1f + mirrorValue);
|
||||
mirror.setScaleY(1f + mirrorValue);
|
||||
mirror.setTranslationX((rootView.getWidth() - mirror.getWidth()) / 2f * mirrorValue);
|
||||
mirror.setTranslationY((rootView.getHeight() - mirror.getHeight()) / 2f * mirrorValue);
|
||||
|
||||
innerView.setTranslationX((mirror.getWidth() - innerView.getWidth()) * (1f - value));
|
||||
innerView.setTranslationY((mirror.getHeight() - innerView.getHeight()) * (1f - value));
|
||||
innerView.setPivotX(innerView.getWidth() / 2f);
|
||||
innerView.setPivotY(innerView.getHeight() / 2f);
|
||||
innerView.setScaleX(0.5f + value * 0.5f);
|
||||
innerView.setScaleY(0.5f + value * 0.5f);
|
||||
innerView.setAlpha(value);
|
||||
|
||||
if (fragment != null) {
|
||||
if (!portrait) {
|
||||
float tX = rootView.getWidth() - ViewUtils.dp(80 * 2);
|
||||
fragment.getGlView().setTranslationX(tX / 2f * value);
|
||||
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) fragment.getSnackbarsLayout().getLayoutParams();
|
||||
marginParams.leftMargin = (int) (ViewUtils.dp(80 * 2) + tX * value);
|
||||
fragment.getSnackbarsLayout().requestLayout();
|
||||
dimmView.setTranslationX(rootView.getTranslationX() - ViewUtils.lerp(rootView.getWidth() - mirror.getWidth(), 0, progress));
|
||||
} else {
|
||||
fragment.getGlView().setTranslationY(-invY / 2 * value);
|
||||
fragment.getSnackbarsLayout().setTranslationY(-invY * value);
|
||||
dimmView.setTranslationY(rootView.getTranslationY());
|
||||
}
|
||||
fragment.getGlView().invalidate();
|
||||
}
|
||||
})
|
||||
.addEndListener((animation, canceled, value, velocity) -> onShown());
|
||||
spring.start();
|
||||
}
|
||||
|
||||
protected void onShown() {}
|
||||
|
||||
public void relayout() {
|
||||
FrameLayout into = containerLayout;
|
||||
boolean portrait = into.getWidth() < into.getHeight();
|
||||
int side = rootView.getHeight();
|
||||
toTranslationY = portrait ? into.getHeight() - side : 0;
|
||||
|
||||
updateListener.onAnimationUpdate(spring, 1f, 0f);
|
||||
}
|
||||
|
||||
public boolean isAttached() {
|
||||
return rootView.getParent() != null && !isDismissing;
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
dismiss(false);
|
||||
}
|
||||
|
||||
public void dismiss(boolean alphaOnly) {
|
||||
if (!isVisible) return;
|
||||
this.isVisible = false;
|
||||
|
||||
isDismissing = true;
|
||||
onDestroy();
|
||||
isDismissing = false;
|
||||
|
||||
if (alphaOnly) {
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1).setDuration(150);
|
||||
anim.setInterpolator(ViewUtils.CUBIC_INTERPOLATOR);
|
||||
anim.addUpdateListener(animation -> {
|
||||
float val = (float) animation.getAnimatedValue();
|
||||
rootView.setAlpha(1f - val);
|
||||
dimmView.setAlpha(1f - val);
|
||||
|
||||
rootView.setTranslationY(val * ViewUtils.dp(64));
|
||||
dimmView.setTranslationY(val * ViewUtils.dp(64));
|
||||
});
|
||||
anim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
containerLayout.removeView(dimmView);
|
||||
containerLayout.removeView(rootView);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
} else {
|
||||
spring.getSpring().setFinalPosition(0f);
|
||||
spring.addEndListener((animation, canceled, value, velocity) -> {
|
||||
containerLayout.removeView(dimmView);
|
||||
containerLayout.removeView(rootView);
|
||||
});
|
||||
spring.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.dark98.santoku.components;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.events.NeedDismissCalibrationsMenu;
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
|
||||
public class WebViewMenu extends UnfoldMenu {
|
||||
private final Uri uri;
|
||||
private String javascript;
|
||||
private BedFragment fragment;
|
||||
|
||||
private FileOutputStream fileStream;
|
||||
private File cacheFile;
|
||||
|
||||
public WebViewMenu(Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public WebViewMenu(Uri uri, String javascript) {
|
||||
this(uri);
|
||||
this.javascript = javascript;
|
||||
}
|
||||
|
||||
public WebViewMenu setFragment(BedFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
WebView webView = new WebView(ctx) {
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
if (fileStream != null) {
|
||||
return true;
|
||||
}
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
};
|
||||
webView.addJavascriptInterface(new Bridge(), "Santoku");
|
||||
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setUserAgentString(String.format(Locale.ROOT, "Santoku/%s-%d", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setDatabaseEnabled(true);
|
||||
settings.setGeolocationEnabled(false);
|
||||
|
||||
webView.loadUrl(uri.toString());
|
||||
webView.setAlpha(0f);
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
super.onPageFinished(view, url);
|
||||
|
||||
if (javascript != null) {
|
||||
webView.evaluateJavascript(javascript, value -> ViewUtils.postOnMainThread(() -> webView.animate().alpha(1f).start()));
|
||||
} else {
|
||||
webView.animate().alpha(1f).start();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ll.addView(webView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return portrait ? into.getHeight() - into.getPaddingTop() - into.getPaddingBottom() : into.getWidth();
|
||||
}
|
||||
|
||||
private final class Bridge {
|
||||
|
||||
@JavascriptInterface
|
||||
public void beginDownload(String filename) {
|
||||
cacheFile = new File(Santoku.getModelCacheDir(), filename);
|
||||
try {
|
||||
fileStream = new FileOutputStream(cacheFile);
|
||||
} catch (Exception e) {
|
||||
Log.e("WebViewMenu", "Failed to begin download", e);
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void writeData(String data) {
|
||||
try {
|
||||
fileStream.write(Base64.decode(data, 0));
|
||||
} catch (Exception e) {
|
||||
Log.e("WebViewMenu", "Failed to write to stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void finishDownload() {
|
||||
try {
|
||||
fileStream.close();
|
||||
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
dismiss(true);
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissCalibrationsMenu());
|
||||
ViewUtils.postOnMainThread(() -> fragment.loadGCode(cacheFile), 200);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("WebViewMenu", "Failed to finish file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.slic3r.Bed3D;
|
||||
|
||||
public abstract class BedMenu {
|
||||
private View view;
|
||||
|
||||
public abstract View onCreateView(Context ctx, boolean portrait);
|
||||
|
||||
@CallSuper
|
||||
public void onViewCreated(View v) {
|
||||
view = v;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onViewDestroyed() {
|
||||
view = null;
|
||||
}
|
||||
|
||||
public View getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
public void onSetBed(BedFragment fragment) {}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.theme.IThemeView;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.RandomUtils;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolderView> {
|
||||
public final int titleRes;
|
||||
public final int iconRes;
|
||||
public boolean isSingleLine;
|
||||
|
||||
public boolean isEnabled = true;
|
||||
public boolean isChecked = false;
|
||||
public boolean isCheckable = false;
|
||||
public boolean isShiny = false;
|
||||
public View.OnClickListener clickListener;
|
||||
public CompoundButton.OnCheckedChangeListener checkedChangeListener;
|
||||
|
||||
public BedMenuItem(int titleRes, int iconRes) {
|
||||
this.titleRes = titleRes;
|
||||
this.iconRes = iconRes;
|
||||
}
|
||||
|
||||
public BedMenuItem onClick(View.OnClickListener listener) {
|
||||
clickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BedMenuItem setCheckable(CompoundButton.OnCheckedChangeListener checkedChangeListener, boolean checked) {
|
||||
this.checkedChangeListener = checkedChangeListener;
|
||||
isCheckable = true;
|
||||
isChecked = checked;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BedMenuItem setEnabled(boolean enabled) {
|
||||
isEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BedMenuItem setShiny(boolean shiny) {
|
||||
isShiny = shiny;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BedMenuItem setSingleLine(boolean singleLine) {
|
||||
isSingleLine = singleLine;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BedMenuItemHolderView onCreateView(Context ctx) {
|
||||
return new BedMenuItemHolderView(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(BedMenuItemHolderView view) {
|
||||
view.bind(this);
|
||||
}
|
||||
|
||||
public final static class BedMenuItemHolderView extends LinearLayout implements IThemeView {
|
||||
private final static float IN_BOUND = 0.05f;
|
||||
private final static float OUT_BOUND = 0.1f;
|
||||
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
|
||||
private Paint accentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
private Path path = new Path();
|
||||
private Path path2 = new Path();
|
||||
private float checkedProgress;
|
||||
private boolean enabled;
|
||||
private boolean shiny;
|
||||
private List<Sparkle> sparkles;
|
||||
private long lastDraw;
|
||||
private Drawable sparkleDrawable;
|
||||
|
||||
public BedMenuItemHolderView(Context context) {
|
||||
super(context);
|
||||
setOrientation(VERTICAL);
|
||||
setGravity(Gravity.CENTER);
|
||||
|
||||
icon = new ImageView(context);
|
||||
addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(24), ViewUtils.dp(24)));
|
||||
|
||||
title = new TextView(context);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setMaxLines(2);
|
||||
title.setEllipsize(TextUtils.TruncateAt.END);
|
||||
addView(title, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
topMargin = ViewUtils.dp(2);
|
||||
}});
|
||||
|
||||
setPadding(ViewUtils.dp(8), ViewUtils.dp(6), ViewUtils.dp(8), ViewUtils.dp(6));
|
||||
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT) {{
|
||||
leftMargin = topMargin = bottomMargin = ViewUtils.dp(6);
|
||||
}});
|
||||
setClipToPadding(false);
|
||||
setClipChildren(false);
|
||||
setWillNotDraw(false);
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
long dt = Math.min(System.currentTimeMillis() - lastDraw, 16);
|
||||
lastDraw = System.currentTimeMillis();
|
||||
|
||||
int rad = ViewUtils.dp(16);
|
||||
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), rad, rad, bgPaint);
|
||||
|
||||
if (enabled && checkedProgress != 0f) {
|
||||
if (checkedProgress == 1f) {
|
||||
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), rad, rad, accentPaint);
|
||||
} else {
|
||||
path.rewind();
|
||||
path.addRoundRect(0, 0, getWidth(), getHeight(), rad, rad, Path.Direction.CW);
|
||||
|
||||
canvas.save();
|
||||
canvas.clipPath(path);
|
||||
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, (float) (Math.sqrt(getWidth() * getWidth() + getHeight() * getHeight()) / 2f * checkedProgress), accentPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
super.draw(canvas);
|
||||
|
||||
if (shiny) {
|
||||
float side = Math.min(getWidth(), getHeight());
|
||||
canvas.save();
|
||||
if (sparkles == null) sparkles = new ArrayList<>();
|
||||
if (sparkleDrawable == null) {
|
||||
sparkleDrawable = ContextCompat.getDrawable(Santoku.INSTANCE, R.drawable.sparkle_28);
|
||||
sparkleDrawable.setColorFilter(new PorterDuffColorFilter(ThemesRepo.getColor(android.R.attr.colorAccent), PorterDuff.Mode.SRC_IN));
|
||||
}
|
||||
|
||||
float p = dt / 1000f;
|
||||
for (Iterator<Sparkle> iterator = sparkles.iterator(); iterator.hasNext(); ) {
|
||||
Sparkle sparkle = iterator.next();
|
||||
sparkle.position.x += sparkle.velocity.x * p;
|
||||
sparkle.position.y += sparkle.velocity.y * p;
|
||||
sparkle.velocity.x *= 0.9999f;
|
||||
sparkle.velocity.y *= 0.9999f;
|
||||
sparkle.living += dt;
|
||||
|
||||
int size = (int) (side * sparkle.size);
|
||||
|
||||
float fadems = 200;
|
||||
if ((sparkle.position.x - sparkle.size > 0 && sparkle.position.x + sparkle.size < 1f) &&
|
||||
sparkle.lifetime - sparkle.living > fadems) {
|
||||
sparkle.living = (long) (sparkle.lifetime - fadems);
|
||||
}
|
||||
if (sparkle.living >= sparkle.lifetime) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
float alpha = sparkle.living < fadems ? sparkle.living / fadems : sparkle.living > sparkle.lifetime - fadems ? (sparkle.lifetime - sparkle.living) / fadems : 1f;
|
||||
canvas.saveLayerAlpha(-OUT_BOUND * side, -OUT_BOUND * side, getWidth() + OUT_BOUND * side, getHeight() + OUT_BOUND * side, (int) (alpha * sparkle.alpha * 0xFF));
|
||||
canvas.translate(sparkle.position.x * side, sparkle.position.y * side);
|
||||
sparkleDrawable.setBounds(-size / 2, -size / 2, size / 2, size / 2);
|
||||
sparkleDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
if (sparkles.size() < 20) {
|
||||
int s = 20 - sparkles.size();
|
||||
for (int i = 0; i < s; i++) {
|
||||
if (RandomUtils.RANDOM.nextFloat() < 0.01f) {
|
||||
Sparkle sparkle = new Sparkle();
|
||||
boolean leftSide = RandomUtils.RANDOM.nextBoolean();
|
||||
sparkle.position = new PointF(leftSide ? RandomUtils.randomf(-OUT_BOUND, 0) : RandomUtils.randomf(1, 1 + OUT_BOUND), RandomUtils.randomf(-OUT_BOUND, 1 + OUT_BOUND));
|
||||
sparkle.velocity = new PointF(RandomUtils.randomf(-0.05f, 0.05f), RandomUtils.randomf(-0.05f, 0.05f));
|
||||
sparkle.size = RandomUtils.randomf(0.1f, 0.12f);
|
||||
sparkle.alpha = RandomUtils.randomf(0.5f, 1f);
|
||||
sparkle.lifetime = RandomUtils.randoml(4000, 10000);
|
||||
sparkles.add(sparkle);
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (getLayoutParams().width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.getMode(heightMeasureSpec)), heightMeasureSpec);
|
||||
}
|
||||
|
||||
public void bind(BedMenuItem item) {
|
||||
enabled = item.isEnabled;
|
||||
shiny = item.isShiny;
|
||||
title.setMaxLines(item.isSingleLine ? 1 : 2);
|
||||
title.setText(item.titleRes);
|
||||
icon.setImageResource(item.iconRes);
|
||||
checkedProgress = item.isCheckable && item.isChecked ? 1 : 0;
|
||||
onApplyTheme();
|
||||
title.setTextColor(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorPrimary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorSecondary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress)));
|
||||
|
||||
if (item.checkedChangeListener != null) {
|
||||
setOnClickListener(v -> {
|
||||
item.isChecked = !item.isChecked;
|
||||
new SpringAnimation(new FloatValueHolder(item.isChecked ? 0 : 1))
|
||||
.setMinimumVisibleChange(1 / 256f)
|
||||
.setSpring(new SpringForce(item.isChecked ? 1 : 0)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener((animation, value, velocity) -> {
|
||||
checkedProgress = value;
|
||||
title.setTextColor(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorPrimary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorSecondary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress)));
|
||||
|
||||
invalidate();
|
||||
})
|
||||
.start();
|
||||
|
||||
item.checkedChangeListener.onCheckedChanged(null, item.isChecked);
|
||||
});
|
||||
} else {
|
||||
setOnClickListener(item.clickListener);
|
||||
}
|
||||
setClickable(item.isEnabled);
|
||||
setAlpha(item.isEnabled ? 1f : 0.6f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
bgPaint.setColor(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10));
|
||||
accentPaint.setColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
}
|
||||
|
||||
private final static class Sparkle {
|
||||
private PointF position;
|
||||
private PointF velocity;
|
||||
private float size;
|
||||
private float alpha;
|
||||
private long lifetime;
|
||||
private long living;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.recycler.SpaceItem;
|
||||
import com.dark98.santoku.render.Camera;
|
||||
import com.dark98.santoku.slic3r.Bed3D;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.GLView;
|
||||
|
||||
public class CameraMenu extends ListBedMenu {
|
||||
private boolean checkInvalidBed() {
|
||||
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
|
||||
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
|
||||
return Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuCameraIsometric, R.drawable.camera_mode_0_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center.x - center.z * 2, center.y - center.z * 2, min.z + Math.sqrt(center.z * center.z * 8));
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(8) : 0, portrait ? 0 : ViewUtils.dp(8)),
|
||||
new BedMenuItem(R.string.MenuCameraTop, R.drawable.camera_mode_1_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center);
|
||||
toPosition.z = max.z + (max.z - min.z);
|
||||
toPosition.y -= 1f;
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuCameraBottom, R.drawable.camera_mode_2_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center);
|
||||
toPosition.z = min.z - (max.z - min.z);
|
||||
toPosition.y -= 1f;
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuCameraFront, R.drawable.camera_mode_3_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center);
|
||||
toPosition.y = min.y - (max.y - min.y);
|
||||
toPosition.z = 0;
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuCameraBack, R.drawable.camera_mode_4_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center);
|
||||
toPosition.y = max.y + (max.y - min.y);
|
||||
toPosition.z = 0;
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuCameraLeft, R.drawable.camera_mode_5_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center);
|
||||
toPosition.x = min.x - (max.x - min.x);
|
||||
toPosition.z = 0;
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuCameraRight, R.drawable.camera_mode_6_28).onClick(v -> {
|
||||
if (checkInvalidBed()) return;
|
||||
GLView glView = fragment.getGlView();
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
Vec3d toOrigin = new Vec3d(center).multiply(1, 1, 0);
|
||||
Vec3d toPosition = new Vec3d(center);
|
||||
toPosition.x = max.x + (max.x - min.x);
|
||||
toPosition.z = 0;
|
||||
animateTo(toOrigin, toPosition);
|
||||
}),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(8) : 0, portrait ? 0 : ViewUtils.dp(8)),
|
||||
new BedMenuItem(R.string.MenuCameraControlMode, R.drawable.rectangle_hand_point_up_28).onClick(v -> {
|
||||
Context ctx = v.getContext();
|
||||
new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.MenuCameraControlModeFull)
|
||||
.setSingleChoiceItems(new CharSequence[] {
|
||||
ctx.getString(R.string.MenuCameraControlModeOne),
|
||||
ctx.getString(R.string.MenuCameraControlModeTwo),
|
||||
ctx.getString(R.string.MenuCameraControlModeThree)
|
||||
}, Prefs.getCameraControlMode(), (dialog, which) -> {
|
||||
Prefs.setCameraControlMode(which);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.show();
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuCameraOrtho, R.drawable.image_format_32).setCheckable((buttonView, isChecked) -> {
|
||||
Prefs.setOrthoProjectionEnabled(isChecked);
|
||||
fragment.getGlView().getRenderer().updateProjection();
|
||||
fragment.getGlView().requestRender();
|
||||
}, Prefs.isOrthoProjectionEnabled()));
|
||||
}
|
||||
|
||||
private void animateTo(Vec3d toOrigin, Vec3d toPosition) {
|
||||
animateTo(toOrigin, null, toPosition);
|
||||
}
|
||||
|
||||
private void animateTo(Vec3d toOrigin, Vec3d middlePoint, Vec3d toPosition) {
|
||||
GLView glView = fragment.getGlView();
|
||||
Camera camera = glView.getRenderer().getCamera();
|
||||
|
||||
Vec3d fromOrigin = new Vec3d(camera.origin);
|
||||
Vec3d fromPosition = new Vec3d(camera.position);
|
||||
if (middlePoint == null) {
|
||||
middlePoint = fromPosition.center(toPosition);
|
||||
}
|
||||
|
||||
float zoom = camera.getZoom();
|
||||
Vec3d finalMiddlePoint = middlePoint;
|
||||
new SpringAnimation(new FloatValueHolder(0))
|
||||
.setMinimumVisibleChange(1 / 1000f)
|
||||
.setSpring(new SpringForce(1f)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener((animation, value, velocity) -> {
|
||||
camera.setZoom(ViewUtils.lerp(zoom, 1f, value));
|
||||
camera.position.set(
|
||||
ViewUtils.lerpd(fromPosition.x, Math.abs(toPosition.x - toOrigin.x) <= 5 ? finalMiddlePoint.x : fromPosition.x + (toPosition.x - fromPosition.x) / 2, toPosition.x, value),
|
||||
ViewUtils.lerpd(fromPosition.y, Math.abs(toPosition.y - toOrigin.y) <= 5 ? finalMiddlePoint.y : fromPosition.y + (toPosition.y - fromPosition.y) / 2, toPosition.y, value),
|
||||
ViewUtils.lerpd(fromPosition.z, Math.abs(toPosition.z - toOrigin.z) <= 5 ? finalMiddlePoint.z : fromPosition.z + (toPosition.z - fromPosition.z) / 2, toPosition.z, value)
|
||||
);
|
||||
camera.origin.set(
|
||||
ViewUtils.lerpd(fromOrigin.x, toOrigin.x, value),
|
||||
ViewUtils.lerpd(fromOrigin.y, toOrigin.y, value),
|
||||
ViewUtils.lerpd(fromOrigin.z, toOrigin.z, value)
|
||||
);
|
||||
glView.getRenderer().updateProjection();
|
||||
glView.requestRender();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.BeamServerData;
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.MainActivity;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.SetupActivity;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.cloud.CloudController;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.UnfoldMenu;
|
||||
import com.dark98.santoku.components.WebViewMenu;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.events.CloudFeaturesUpdatedEvent;
|
||||
import com.dark98.santoku.events.CloudModelsRemainingCountUpdatedEvent;
|
||||
import com.dark98.santoku.events.NeedDismissAIGeneratorMenu;
|
||||
import com.dark98.santoku.events.NeedDismissCalibrationsMenu;
|
||||
import com.dark98.santoku.events.NeedDismissSnackbarEvent;
|
||||
import com.dark98.santoku.events.NeedSnackbarEvent;
|
||||
import com.dark98.santoku.events.ObjectsListChangedEvent;
|
||||
import com.dark98.santoku.events.SelectedObjectChangedEvent;
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.recycler.PreferenceItem;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerAdapter;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.recycler.SpaceItem;
|
||||
import com.dark98.santoku.slic3r.Bed3D;
|
||||
import com.dark98.santoku.slic3r.Slic3rRuntimeError;
|
||||
import com.dark98.santoku.theme.BeamTheme;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
import com.dark98.santoku.view.FadeRecyclerView;
|
||||
import com.dark98.santoku.view.SegmentsView;
|
||||
import com.dark98.santoku.view.SnackbarsLayout;
|
||||
|
||||
public class FileMenu extends ListBedMenu {
|
||||
private final static List<String> K3D_SUPPORTED_LANGUAGES = Arrays.asList("en", "ru");
|
||||
|
||||
private boolean wasPortrait;
|
||||
|
||||
private String getK3DLanguage() {
|
||||
String lang = Locale.getDefault().getLanguage();
|
||||
return K3D_SUPPORTED_LANGUAGES.contains(lang) ? lang : "en";
|
||||
}
|
||||
|
||||
static String escapeStringForJs(String s) {
|
||||
if (s == null) return s;
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\t", "\\t")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\b", "\\b")
|
||||
.replace("\f", "\\f")
|
||||
.replace("\r", "\\r")
|
||||
.replace("'", "\\'")
|
||||
.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private boolean hasModel() {
|
||||
return fragment.getGlView().getRenderer().getModel() != null;
|
||||
}
|
||||
|
||||
private boolean hasSelection() {
|
||||
return hasModel() && fragment.getGlView().getRenderer().getSelectedObject() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
|
||||
wasPortrait = portrait;
|
||||
List<SimpleRecyclerItem> list = new ArrayList<>(Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuFileOpen, R.drawable.folder_simple_plus_outline_28).onClick(v -> {
|
||||
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
|
||||
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
i.setType("*/*");
|
||||
act.startActivityForResult(i, MainActivity.REQUEST_CODE_OPEN_FILE);
|
||||
}
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuFileDelete, R.drawable.delete_outline_android_28).setEnabled(hasSelection()).onClick(v -> {
|
||||
if (fragment.getGlView().getRenderer().getModel() == null) return;
|
||||
|
||||
if (fragment.getGlView().getRenderer().deleteObject(fragment.getGlView().getRenderer().getSelectedObject())) {
|
||||
fragment.getGlView().requestRender();
|
||||
fragment.updateModel();
|
||||
}
|
||||
}),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(3) : 0, portrait ? 0 : ViewUtils.dp(3))));
|
||||
if (BeamServerData.isBoostyAvailable() && CloudController.needShowAIGenerator()) {
|
||||
list.add(new BedMenuItem(R.string.MenuFileAIGenerator, R.drawable.picture_stack_outline_28).setShiny(true).onClick(view -> {
|
||||
if (Prefs.getCloudAPIToken() == null || CloudController.getUserInfo() != null && CloudController.getMaxGeneratedModels() == 0) {
|
||||
Context ctx = view.getContext();
|
||||
ctx.startActivity(new Intent(ctx, SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true));
|
||||
return;
|
||||
}
|
||||
if (CloudController.getUserInfo() == null) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorPleaseWaitSetup).tag(CloudController.USER_INFO_AI_GEN_TAG));
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
if (CloudController.getUserInfo() == null) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CloudController.USER_INFO_AI_GEN_TAG));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorErrorNotLoadedUserAccount));
|
||||
} else {
|
||||
fragment.showUnfoldMenu(new AIGeneratorMenu(), view);
|
||||
}
|
||||
}, 2500);
|
||||
return;
|
||||
}
|
||||
|
||||
fragment.showUnfoldMenu(new AIGeneratorMenu(), view);
|
||||
}));
|
||||
}
|
||||
list.addAll(Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuFileCalibrations, R.drawable.wrench_outline_28).setSingleLine(true).onClick(v -> {
|
||||
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
|
||||
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
fragment.showUnfoldMenu(new CalibrationsMenu(), v);
|
||||
}),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(3) : 0, portrait ? 0 : ViewUtils.dp(3)),
|
||||
new BedMenuItem(R.string.MenuFileImportProfiles, R.drawable.folder_simple_arrow_up_outline_28).onClick(v -> {
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
i.setType("*/*");
|
||||
act.startActivityForResult(i, MainActivity.REQUEST_CODE_IMPORT_PROFILES);
|
||||
}
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuFileExportProfiles, R.drawable.folder_simple_arrow_right_outline_28).onClick(v -> {
|
||||
CharSequence[] prints = new CharSequence[Santoku.CONFIG.printConfigs.size()];
|
||||
boolean[] enabledPrints = new boolean[prints.length];
|
||||
for (int i = 0; i < prints.length; i++) {
|
||||
prints[i] = Santoku.CONFIG.printConfigs.get(i).getTitle();
|
||||
enabledPrints[i] = true;
|
||||
}
|
||||
|
||||
CharSequence[] filaments = new CharSequence[Santoku.CONFIG.filamentConfigs.size()];
|
||||
boolean[] enabledFilaments = new boolean[filaments.length];
|
||||
for (int i = 0; i < filaments.length; i++) {
|
||||
filaments[i] = Santoku.CONFIG.filamentConfigs.get(i).getTitle();
|
||||
enabledFilaments[i] = true;
|
||||
}
|
||||
|
||||
CharSequence[] printers = new CharSequence[Santoku.CONFIG.printerConfigs.size()];
|
||||
boolean[] enabledPrinters = new boolean[printers.length];
|
||||
for (int i = 0; i < printers.length; i++) {
|
||||
printers[i] = Santoku.CONFIG.printerConfigs.get(i).getTitle();
|
||||
enabledPrinters[i] = true;
|
||||
}
|
||||
|
||||
new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.MenuFileExportProfilesPrints)
|
||||
.setMultiChoiceItems(prints, enabledPrints, (dialog, which, isChecked) -> enabledPrints[which] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (d1, w1) -> new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.MenuFileExportProfilesFilaments)
|
||||
.setMultiChoiceItems(filaments, enabledFilaments, (dialog, which, isChecked) -> enabledFilaments[which] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (d2, w2) -> new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.MenuFileExportProfilesPrinters)
|
||||
.setMultiChoiceItems(printers, enabledPrinters, (dialog, which, isChecked) -> enabledPrinters[which] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (d3, w3) -> {
|
||||
boolean hasEnabled = false;
|
||||
MainActivity.EXPORTING_PRINTS = new ArrayList<>();
|
||||
for (int i = 0; i < enabledPrints.length; i++) {
|
||||
if (enabledPrints[i]) {
|
||||
hasEnabled = true;
|
||||
MainActivity.EXPORTING_PRINTS.add(Santoku.CONFIG.printConfigs.get(i));
|
||||
}
|
||||
}
|
||||
MainActivity.EXPORTING_FILAMENTS = new ArrayList<>();
|
||||
for (int i = 0; i < enabledFilaments.length; i++) {
|
||||
if (enabledFilaments[i]) {
|
||||
hasEnabled = true;
|
||||
MainActivity.EXPORTING_FILAMENTS.add(Santoku.CONFIG.filamentConfigs.get(i));
|
||||
}
|
||||
}
|
||||
MainActivity.EXPORTING_PRINTERS = new ArrayList<>();
|
||||
for (int i = 0; i < enabledPrinters.length; i++) {
|
||||
if (enabledPrinters[i]) {
|
||||
hasEnabled = true;
|
||||
MainActivity.EXPORTING_PRINTERS.add(Santoku.CONFIG.printerConfigs.get(i));
|
||||
}
|
||||
}
|
||||
if (!hasEnabled) {
|
||||
new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.MenuFileExportProfiles)
|
||||
.setMessage(R.string.MenuFileExportProfilesNoProfiles)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
i.setType("application/ini");
|
||||
i.putExtra(Intent.EXTRA_TITLE, "SliceBeam_config_bundle.ini");
|
||||
act.startActivityForResult(i, MainActivity.REQUEST_CODE_EXPORT_PROFILES);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuFileExport3mf, R.drawable.arrow_down_to_square_outline_28).setEnabled(hasModel()).onClick(v -> {
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
i.setType("application/3mf");
|
||||
i.putExtra(Intent.EXTRA_TITLE, "SliceBeam_project.3mf");
|
||||
act.startActivityForResult(i, MainActivity.REQUEST_CODE_EXPORT_3MF);
|
||||
}
|
||||
})
|
||||
));
|
||||
return list;
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onObjectsChanged(ObjectsListChangedEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(1);
|
||||
|
||||
int i = 8 - (BeamServerData.isBoostyAvailable() && CloudController.needShowAIGenerator() ? 0 : 1);
|
||||
((BedMenuItem) adapter.getItems().get(i)).setEnabled(hasModel());
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onSelectionChanged(SelectedObjectChangedEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(1);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onFeaturedUpdated(CloudFeaturesUpdatedEvent e) {
|
||||
adapter.setItems(onCreateItems(wasPortrait));
|
||||
}
|
||||
|
||||
public final static class AIGeneratorMenu extends UnfoldMenu {
|
||||
private TextView remainingView;
|
||||
private SegmentsView segmentsView;
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return (int) (portrait ? ViewUtils.dp(52) + ViewUtils.dp(60) * 2 + ViewUtils.dp(28) + ViewUtils.dp(18) + ViewUtils.dp(2) : into.getWidth() * 0.6f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
RecyclerView rv = new FadeRecyclerView(ctx);
|
||||
rv.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
|
||||
adapter.setItems(Arrays.asList(
|
||||
new PreferenceItem().setIcon(R.drawable.camera_outline_28).setTitle(ctx.getString(R.string.MenuFileAIGeneratorFromCamera)).setOnClickListener(v -> {
|
||||
if (CloudController.getGeneratedModels() >= CloudController.getMaxGeneratedModels()) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft));
|
||||
return;
|
||||
}
|
||||
if (MainActivity.IS_GENERATING_AI_MODEL) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.MenuFileAIGeneratorAlreadyGenerating));
|
||||
return;
|
||||
}
|
||||
if (ctx instanceof MainActivity) {
|
||||
try {
|
||||
MainActivity.aiTempFile = File.createTempFile("ai_capture", ".jpg");
|
||||
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
i.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(ctx, BuildConfig.APPLICATION_ID + ".provider", MainActivity.aiTempFile));
|
||||
i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
((MainActivity) ctx).startActivityForResult(i, MainActivity.REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new PreferenceItem().setIcon(R.drawable.picture_outline_28).setTitle(ctx.getString(R.string.MenuFileAIGeneratorFromGallery)).setOnClickListener(v -> {
|
||||
if (CloudController.getGeneratedModels() >= CloudController.getMaxGeneratedModels()) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft));
|
||||
return;
|
||||
}
|
||||
if (MainActivity.IS_GENERATING_AI_MODEL) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.MenuFileAIGeneratorAlreadyGenerating));
|
||||
return;
|
||||
}
|
||||
if (ctx instanceof MainActivity) {
|
||||
Intent intent = new Intent();
|
||||
intent.setType("image/*");
|
||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||
((MainActivity) ctx).startActivityForResult(Intent.createChooser(intent, ""), MainActivity.REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO);
|
||||
}
|
||||
})
|
||||
));
|
||||
rv.setAdapter(adapter);
|
||||
ll.addView(rv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
remainingView = new TextView(ctx);
|
||||
remainingView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13);
|
||||
remainingView.setGravity(Gravity.CENTER);
|
||||
remainingView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
ll.addView(remainingView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(18)) {{
|
||||
topMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
segmentsView = new SegmentsView(ctx) {
|
||||
@Override
|
||||
protected int onGetColor(int i) {
|
||||
return i == 1 ? ThemesRepo.getColor(android.R.attr.textColorSecondary) : ThemesRepo.getColor(android.R.attr.colorAccent);
|
||||
}
|
||||
};
|
||||
ll.addView(segmentsView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(12)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
topMargin = bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
updateRemaining();
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
ViewUtils.postOnMainThread(() -> segmentsView.startAnimation(), 50);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDismiss(NeedDismissAIGeneratorMenu e) {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onRemainingUpdated(CloudModelsRemainingCountUpdatedEvent e) {
|
||||
updateRemaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
}
|
||||
|
||||
private void updateRemaining() {
|
||||
int rev = CloudController.getMaxGeneratedModels() - CloudController.getGeneratedModels();
|
||||
remainingView.setText(Santoku.INSTANCE.getString(R.string.MenuFileAIGeneratorRemaining, rev, CloudController.getMaxGeneratedModels()));
|
||||
segmentsView.setValues(new float[]{0, rev / (float) CloudController.getMaxGeneratedModels(), 1});
|
||||
}
|
||||
}
|
||||
|
||||
public final class CalibrationsMenu extends UnfoldMenu {
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return (int) (portrait ? into.getHeight() * 0.35f : into.getWidth() * 0.6f);
|
||||
}
|
||||
|
||||
private String loadJSLoader(String key) {
|
||||
try {
|
||||
InputStream in = Santoku.INSTANCE.getAssets().open("js_loader/" + key + ".js");
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[10240]; int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, c);
|
||||
}
|
||||
bos.close();
|
||||
in.close();
|
||||
|
||||
ConfigObject cfg = Santoku.buildCurrentConfigObject();
|
||||
Bed3D bed = FileMenu.this.fragment.getGlView().getRenderer().getBed();
|
||||
double bedX = bed.getVolumeMax().x - bed.getVolumeMin().x;
|
||||
double bedY = bed.getVolumeMax().y - bed.getVolumeMin().y;
|
||||
|
||||
String str = new String(bos.toByteArray(), StandardCharsets.UTF_8);
|
||||
StringBuilder sb = new StringBuilder(str);
|
||||
Pattern placeholderPattern = Pattern.compile("\\$\\['(\\w+?)(\\[\\d+]|)']");
|
||||
Matcher m = placeholderPattern.matcher(str);
|
||||
int offset = 0;
|
||||
while (m.find()) {
|
||||
String pKey = m.group(1);
|
||||
String pIndex = m.group(2);
|
||||
int index = pIndex.isEmpty() ? -1 : Integer.parseInt(pIndex.substring(1, pIndex.length() - 1));
|
||||
|
||||
String v;
|
||||
boolean quote = false;
|
||||
switch (pKey) {
|
||||
case "bed_x":
|
||||
v = String.format(Locale.ROOT, "%.1f", bedX);
|
||||
quote = true;
|
||||
break;
|
||||
case "bed_y":
|
||||
v = String.format(Locale.ROOT, "%.1f", bedY);
|
||||
quote = true;
|
||||
break;
|
||||
case "color_accent":
|
||||
v = String.format(Locale.ROOT, "#%06X", ThemesRepo.getColor(android.R.attr.colorAccent) & 0xFFFFFF);
|
||||
break;
|
||||
case "window_background_dark":
|
||||
v = String.format(Locale.ROOT, "#%06X", BeamTheme.DARK.colors.get(android.R.attr.windowBackground) & 0xFFFFFF);
|
||||
break;
|
||||
case "window_background_light":
|
||||
v = String.format(Locale.ROOT, "#%06X", BeamTheme.LIGHT.colors.get(android.R.attr.windowBackground) & 0xFFFFFF);
|
||||
break;
|
||||
case "is_dark_theme":
|
||||
v = String.valueOf(ColorUtils.calculateLuminance(ThemesRepo.getColor(android.R.attr.windowBackground)) >= 0.9f);
|
||||
break;
|
||||
default:
|
||||
v = cfg.get(pKey);
|
||||
quote = true;
|
||||
break;
|
||||
}
|
||||
if (v != null && index != -1) {
|
||||
try {
|
||||
v = v.split(",")[index];
|
||||
} catch (ArrayIndexOutOfBoundsException ex) {
|
||||
v = "";
|
||||
}
|
||||
}
|
||||
String newVal = escapeStringForJs(v);
|
||||
if (quote) {
|
||||
newVal = "'" + newVal + "'";
|
||||
}
|
||||
sb = sb.replace(m.start() + offset, m.end() + offset, newVal);
|
||||
offset += newVal.length() - (m.end() - m.start());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
RecyclerView rv = new FadeRecyclerView(ctx);
|
||||
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
|
||||
adapter.setItems(Arrays.asList(
|
||||
new PreferenceItem().setIcon(R.drawable.menu_calibrate_la_28).setTitle(ctx.getString(R.string.MenuFileCalibrationsLA)).setSubtitle(ctx.getString(R.string.MenuFileCalibrationsLADescription)).setOnClickListener(v -> {
|
||||
if (ctx instanceof MainActivity) {
|
||||
((MainActivity) ctx).showUnfoldMenu(new WebViewMenu(Uri.parse("https://k3d.tech/calibrations/la/calibrator/").buildUpon().appendQueryParameter("lang", getK3DLanguage()).build(), loadJSLoader("k3d_la")).setFragment(fragment), v);
|
||||
}
|
||||
}),
|
||||
new PreferenceItem().setIcon(R.drawable.menu_calibrate_retract_28).setTitle(ctx.getString(R.string.MenuFileCalibrationsRetract)).setSubtitle(ctx.getString(R.string.MenuFileCalibrationsRetractDescription)).setOnClickListener(v -> {
|
||||
if (ctx instanceof MainActivity) {
|
||||
((MainActivity) ctx).showUnfoldMenu(new WebViewMenu(Uri.parse("https://k3d.tech/calibrations/retractions/calibrator/").buildUpon().appendQueryParameter("lang", getK3DLanguage()).build(), loadJSLoader("k3d_rct")).setFragment(fragment), v);
|
||||
}
|
||||
}),
|
||||
new PreferenceItem().setIcon(R.drawable.deployed_code_24).setTitle(ctx.getString(R.string.MenuFileCalibrationsModels)).setSubtitle(ctx.getString(R.string.MenuFileCalibrationsModelsDescription)).setOnClickListener(v -> {
|
||||
if (ctx instanceof MainActivity) {
|
||||
((MainActivity) ctx).showUnfoldMenu(new CalibrationModelsMenu().setFragment(fragment), v);
|
||||
}
|
||||
})
|
||||
));
|
||||
rv.setAdapter(adapter);
|
||||
ll.addView(rv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
return ll;
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDismiss(NeedDismissCalibrationsMenu e) {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public final static class CalibrationModelsMenu extends UnfoldMenu {
|
||||
private void loadModel(String key) {
|
||||
BedFragment fragment = this.fragment;
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
File f = new File(Santoku.getModelCacheDir(), "calibration_" + key + ".stl");
|
||||
new Thread(()->{
|
||||
try {
|
||||
InputStream in = Santoku.INSTANCE.getAssets().open("models/" + key + ".stl");
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
byte[] buffer = new byte[10240]; int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, c);
|
||||
}
|
||||
fos.close();
|
||||
in.close();
|
||||
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
try {
|
||||
if (f.getName().endsWith(".gcode")) {
|
||||
fragment.loadGCode(f);
|
||||
} else {
|
||||
fragment.loadModel(f);
|
||||
Santoku.EVENT_BUS.fireEvent(new ObjectsListChangedEvent());
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileOpenFileLoaded));
|
||||
} catch (Slic3rRuntimeError e) {
|
||||
f.delete();
|
||||
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(fragment.getContext())
|
||||
.setTitle(R.string.MenuFileOpenFileFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
f.delete();
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(fragment.getContext())
|
||||
.setTitle(R.string.MenuFileOpenFileFailed)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
}).start();
|
||||
}, 200);
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissCalibrationsMenu());
|
||||
dismiss(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
RecyclerView rv = new FadeRecyclerView(ctx);
|
||||
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
|
||||
adapter.setItems(Arrays.asList(
|
||||
new PreferenceItem().setIcon(R.drawable.model_3dbenchy).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModels3DBenchy)).setOnClickListener(v -> loadModel("3dbenchy")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_xyz_cube).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsXYZCube)).setOnClickListener(v -> loadModel("xyz_cube")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_bunny).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsBunny)).setOnClickListener(v -> loadModel("bunny")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_fox).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsFox)).setOnClickListener(v -> loadModel("fox")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_box).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsBox)).setOnClickListener(v -> loadModel("box")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_cone).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsCone)).setOnClickListener(v -> loadModel("cone")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_cylinder).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsCylinder)).setOnClickListener(v -> loadModel("cylinder")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_pyramid).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsPyramid)).setOnClickListener(v -> loadModel("pyramid")),
|
||||
new PreferenceItem().setIcon(R.drawable.model_sphere).setNoTint(true).setRoundRadius(ViewUtils.dp(8)).setTitle(ctx.getString(R.string.MenuFileCalibrationsModelsSphere)).setOnClickListener(v -> loadModel("sphere"))
|
||||
));
|
||||
rv.setAdapter(adapter);
|
||||
ll.addView(rv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return portrait ? into.getHeight() - into.getPaddingTop() - into.getPaddingBottom() : into.getWidth();
|
||||
}
|
||||
|
||||
public CalibrationModelsMenu setFragment(BedFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerAdapter;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public abstract class ListBedMenu extends BedMenu {
|
||||
protected BedFragment fragment;
|
||||
protected RecyclerView recyclerView;
|
||||
protected SimpleRecyclerAdapter adapter;
|
||||
|
||||
@Override
|
||||
public void onSetBed(BedFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(Context ctx, boolean portrait) {
|
||||
recyclerView = new RecyclerView(ctx);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(ctx, portrait ? RecyclerView.HORIZONTAL : RecyclerView.VERTICAL, false));
|
||||
recyclerView.setItemAnimator(null);
|
||||
recyclerView.setClipToPadding(false);
|
||||
recyclerView.setClipChildren(false);
|
||||
adapter = new SimpleRecyclerAdapter() {
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
RecyclerView.ViewHolder vh = super.onCreateViewHolder(parent, viewType);
|
||||
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) vh.itemView.getLayoutParams();
|
||||
if (!portrait && params != null) {
|
||||
params.rightMargin = ViewUtils.dp(6);
|
||||
params.bottomMargin = 0;
|
||||
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
}
|
||||
return vh;
|
||||
}
|
||||
};
|
||||
adapter.setItems(onCreateItems(portrait));
|
||||
recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
||||
if (parent.getChildViewHolder(view).getAdapterPosition() == adapter.getItemCount() - 1) {
|
||||
if (portrait) {
|
||||
outRect.right = ViewUtils.dp(6);
|
||||
} else {
|
||||
outRect.bottom = ViewUtils.dp(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
recyclerView.setAdapter(adapter);
|
||||
return recyclerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v) {
|
||||
super.onViewCreated(v);
|
||||
Santoku.EVENT_BUS.registerListener(ListBedMenu.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDestroyed() {
|
||||
super.onViewDestroyed();
|
||||
Santoku.EVENT_BUS.unregisterListener(ListBedMenu.this);
|
||||
}
|
||||
|
||||
protected abstract List<SimpleRecyclerItem> onCreateItems(boolean portrait);
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.UnfoldMenu;
|
||||
import com.dark98.santoku.events.FlattenModeResetEvent;
|
||||
import com.dark98.santoku.events.LongClickTranslationEvent;
|
||||
import com.dark98.santoku.events.NeedSnackbarEvent;
|
||||
import com.dark98.santoku.events.ObjectsListChangedEvent;
|
||||
import com.dark98.santoku.events.SelectedObjectChangedEvent;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.recycler.SpaceItem;
|
||||
import com.dark98.santoku.slic3r.Model;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.BeamButton;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
import com.dark98.santoku.view.PositionScrollView;
|
||||
import com.dark98.santoku.view.TextColorImageSpan;
|
||||
|
||||
public class OrientationMenu extends ListBedMenu {
|
||||
private boolean hasSelection() {
|
||||
return fragment.getGlView().getRenderer().getModel() != null && fragment.getGlView().getRenderer().getSelectedObject() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
|
||||
return Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuOrientationArrange, R.drawable.grid_layout_outline_28).onClick(v -> {
|
||||
fragment.getGlView().arrange();
|
||||
fragment.getGlView().queueEvent(() -> {
|
||||
if (fragment.getGlView().getRenderer().invalidateFlattenMode()) {
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
});
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuOrientationArrangeFinished));
|
||||
}).setEnabled(fragment.getGlView().getRenderer().getModel() != null),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(8) : 0, portrait ? 0 : ViewUtils.dp(8)),
|
||||
new BedMenuItem(R.string.MenuOrientationAutoOrient, R.drawable.menu_orientation_auto_28).setEnabled(hasSelection()).onClick(view -> {
|
||||
if (fragment.getGlView().getRenderer().resetFlattenMode()) {
|
||||
fragment.getGlView().requestRender();
|
||||
((BedMenuItem) adapter.getItems().get(3)).isChecked = false;
|
||||
adapter.notifyItemChanged(3);
|
||||
}
|
||||
|
||||
int i = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
fragment.getGlView().getRenderer().getModel().autoOrient(i);
|
||||
fragment.getGlView().getRenderer().invalidateGlModel(i);
|
||||
fragment.getGlView().requestRender();
|
||||
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuOrientationAutoOrientDone, Snackbar.LENGTH_SHORT));
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuOrientationFlatten, R.drawable.menu_orientation_flatten_28).setEnabled(hasSelection()).setCheckable((buttonView, isChecked) -> {
|
||||
fragment.getGlView().getRenderer().setInFlattenMode(isChecked);
|
||||
fragment.getGlView().requestRender();
|
||||
}, false),
|
||||
new BedMenuItem(R.string.MenuOrientationPosition, R.drawable.menu_orientation_position_28).setEnabled(hasSelection()).onClick(v -> {
|
||||
if (fragment.getGlView().getRenderer().resetFlattenMode()) {
|
||||
fragment.getGlView().requestRender();
|
||||
((BedMenuItem) adapter.getItems().get(3)).isChecked = false;
|
||||
adapter.notifyItemChanged(3);
|
||||
}
|
||||
fragment.showUnfoldMenu(new PositionMenu(), v);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuOrientationRotation, R.drawable.menu_orientation_rotation_28).setEnabled(hasSelection()).onClick(v -> {
|
||||
if (fragment.getGlView().getRenderer().resetFlattenMode()) {
|
||||
fragment.getGlView().requestRender();
|
||||
((BedMenuItem) adapter.getItems().get(3)).isChecked = false;
|
||||
adapter.notifyItemChanged(3);
|
||||
}
|
||||
fragment.showUnfoldMenu(new RotationMenu(), v);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onFlattenModeReset(FlattenModeResetEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(3)).isChecked = false;
|
||||
adapter.notifyItemChanged(3);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onObjectsChanged(ObjectsListChangedEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(0)).setEnabled(fragment.getGlView().getRenderer().getModel() != null);
|
||||
adapter.notifyItemChanged(0);
|
||||
|
||||
for (int i = 2; i <= 5; i++) {
|
||||
BedMenuItem item = (BedMenuItem) adapter.getItems().get(i);
|
||||
item.setEnabled(hasSelection());
|
||||
if (item.isCheckable) {
|
||||
item.isChecked = false;
|
||||
}
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onSelectionChanged(SelectedObjectChangedEvent e) {
|
||||
for (int i = 2; i <= 5; i++) {
|
||||
BedMenuItem item = (BedMenuItem) adapter.getItems().get(i);
|
||||
item.setEnabled(hasSelection());
|
||||
if (item.isCheckable) {
|
||||
item.isChecked = false;
|
||||
}
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
}
|
||||
|
||||
public final class PositionMenu extends UnfoldMenu {
|
||||
private PositionScrollView xTrack, yTrack, zTrack;
|
||||
private TextView xTitle, yTitle, zTitle;
|
||||
private Vec3d tempVec = new Vec3d();
|
||||
private int startedScrollObject;
|
||||
|
||||
public void translateVisual(Double x, Double y, Double z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
startedScrollObject = j;
|
||||
|
||||
if (x != null) {
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionXValue, x));
|
||||
}
|
||||
if (y != null) {
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionYValue, y));
|
||||
}
|
||||
if (z != null) {
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionZValue, z));
|
||||
}
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getTranslation(j, tempVec);
|
||||
|
||||
double dx = 0, dy = 0, dz = 0;
|
||||
if (x != null) dx = x - tempVec.x;
|
||||
if (y != null) dy = y - tempVec.y;
|
||||
if (z != null) dz = z - tempVec.z;
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionTranslation(dx, dy, dz);
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
private void translate(Double x, Double y, Double z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
startedScrollObject = -1;
|
||||
|
||||
fragment.getGlView().queueEvent(() -> {
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getTranslation(j, tempVec);
|
||||
|
||||
double dx = 0, dy = 0, dz = 0;
|
||||
if (x != null) {
|
||||
dx = x - tempVec.x;
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionXValue, x));
|
||||
}
|
||||
if (y != null) {
|
||||
dy = y - tempVec.y;
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionYValue, y));
|
||||
}
|
||||
if (z != null) {
|
||||
dz = z - tempVec.z;
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionZValue, z));
|
||||
}
|
||||
|
||||
model.translate(j, dx, dy, dz);
|
||||
fragment.getGlView().getRenderer().setSelectionTranslation(0, 0, 0);
|
||||
fragment.getGlView().getRenderer().invalidateGlModel(j);
|
||||
fragment.getGlView().requestRender();
|
||||
});
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
private CharSequence formatTrackTitle(int res, double value) {
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(Santoku.INSTANCE.getString(res, value));
|
||||
sb.append(" d");
|
||||
int size = ViewUtils.dp(14);
|
||||
Drawable dr = ContextCompat.getDrawable(Santoku.INSTANCE, R.drawable.edit_outline_28);
|
||||
dr.setTint(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.setSpan(new TextColorImageSpan(dr, 0), sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return sb;
|
||||
}
|
||||
|
||||
private void showManualEditor(int title, boolean x, boolean y, boolean z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getTranslation(j, tempVec);
|
||||
|
||||
double current;
|
||||
if (x) {
|
||||
current = tempVec.x;
|
||||
} else if (y) {
|
||||
current = tempVec.y;
|
||||
} else {
|
||||
current = tempVec.z;
|
||||
}
|
||||
|
||||
Context ctx = getView().getContext();
|
||||
FrameLayout fl = new FrameLayout(ctx);
|
||||
EditText text = new EditText(ctx);
|
||||
text.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
|
||||
text.setText(String.format(Locale.ROOT, "%.2f", current));
|
||||
text.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
fl.addView(text, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(title)
|
||||
.setView(fl)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
double value;
|
||||
try {
|
||||
value = Double.parseDouble(text.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
value = current;
|
||||
}
|
||||
Double dx = null, dy = null, dz = null;
|
||||
if (x) xTrack.setCurrentPosition((dx = value).intValue());
|
||||
if (y) yTrack.setCurrentPosition((dy = value).intValue());
|
||||
if (z) zTrack.setCurrentPosition((dz = value).intValue());
|
||||
|
||||
translate(dx, dy, dz);
|
||||
})
|
||||
.show();
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
text.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(text, 0);
|
||||
text.setSelection(text.getText().length());
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, 0);
|
||||
|
||||
xTitle = new TextView(ctx);
|
||||
xTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
xTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
xTitle.setTextColor(ThemesRepo.getColor(R.attr.xTrackColor));
|
||||
xTitle.setGravity(Gravity.CENTER);
|
||||
xTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationPositionX, true, false, false));
|
||||
ll.addView(xTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
xTrack = new PositionScrollView(ctx);
|
||||
xTrack.setActiveColor(R.attr.xTrackColor);
|
||||
xTrack.setProgressListener(integer -> translateVisual(integer.doubleValue(), (double) yTrack.getCurrentPosition(), null));
|
||||
xTrack.setListener(integer -> translate(integer.doubleValue(), (double) yTrack.getCurrentPosition(), null));
|
||||
ll.addView(xTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
yTitle = new TextView(ctx);
|
||||
yTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
yTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
yTitle.setTextColor(ThemesRepo.getColor(R.attr.yTrackColor));
|
||||
yTitle.setGravity(Gravity.CENTER);
|
||||
yTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationPositionY, false, true, false));
|
||||
ll.addView(yTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
yTrack = new PositionScrollView(ctx);
|
||||
yTrack.setActiveColor(R.attr.yTrackColor);
|
||||
yTrack.setProgressListener(integer -> translateVisual((double) xTrack.getCurrentPosition(), integer.doubleValue(), null));
|
||||
yTrack.setListener(integer -> translate((double) xTrack.getCurrentPosition(), integer.doubleValue(), null));
|
||||
ll.addView(yTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
// TODO: Sinking parts are not supported yet, so no reason to show it here
|
||||
// zTitle = new TextView(ctx);
|
||||
// zTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
// zTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
// zTitle.setTextColor(ThemesRepo.getColor(R.attr.zTrackColor));
|
||||
// zTitle.setGravity(Gravity.CENTER);
|
||||
// zTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationPositionZ, false, true, false));
|
||||
// ll.addView(zTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
// bottomMargin = ViewUtils.dp(4);
|
||||
// }});
|
||||
//
|
||||
// zTrack = new PositionScrollView(ctx);
|
||||
// zTrack.setActiveColor(R.attr.zTrackColor);
|
||||
// zTrack.setProgressListener(integer -> translateVisual((double) xTrack.getCurrentPosition(), (double) yTrack.getCurrentPosition(), integer.doubleValue()));
|
||||
// zTrack.setListener(integer -> translate((double) xTrack.getCurrentPosition(), (double) yTrack.getCurrentPosition(), integer.doubleValue()));
|
||||
// ll.addView(zTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
ll.addView(new Space(ctx), new LinearLayout.LayoutParams(0, 0, 1f));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return portrait ? ViewUtils.dp(52) + ViewUtils.dp(80 + 24) * 2 + ViewUtils.dp(12) : (int) (into.getWidth() * 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
setSelectionValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
stopScroll();
|
||||
}
|
||||
|
||||
private void setSelectionValues() {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionTranslation(0, 0, 0);
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getTranslation(j, tempVec);
|
||||
|
||||
xTrack.setCurrentPosition((int) tempVec.x);
|
||||
yTrack.setCurrentPosition((int) tempVec.y);
|
||||
// zTrack.setCurrentPosition((int) tempVec.z);
|
||||
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionXValue, tempVec.x));
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionYValue, tempVec.y));
|
||||
// zTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionZValue, tempVec.z));
|
||||
}
|
||||
|
||||
private void stopScroll() {
|
||||
xTrack.stopScroll();
|
||||
yTrack.stopScroll();
|
||||
// zTrack.stopScroll();
|
||||
|
||||
if (startedScrollObject != -1) {
|
||||
fragment.getGlView().getRenderer().setSelectionTranslation(0, 0, 0);
|
||||
}
|
||||
startedScrollObject = -1;
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onLongClickTranslation(LongClickTranslationEvent e) {
|
||||
if (e.visual) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getTranslation(j, tempVec);
|
||||
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionXValue, tempVec.x + e.x));
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationPositionYValue, tempVec.y + e.y));
|
||||
xTrack.setCurrentPosition((int) (tempVec.x + e.x));
|
||||
yTrack.setCurrentPosition((int) (tempVec.y + e.y));
|
||||
} else {
|
||||
setSelectionValues();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onSelectedObjectChanged(SelectedObjectChangedEvent e) {
|
||||
stopScroll();
|
||||
|
||||
if (fragment.getGlView().getRenderer().getSelectedObject() == -1) {
|
||||
dismiss();
|
||||
} else {
|
||||
setSelectionValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class RotationMenu extends UnfoldMenu {
|
||||
private PositionScrollView xTrack, yTrack, zTrack;
|
||||
private TextView xTitle, yTitle, zTitle;
|
||||
private Vec3d bbMin = new Vec3d(), bbMax = new Vec3d();
|
||||
private int startedScrollObject;
|
||||
|
||||
private void rotateVisual(Double x, Double y, Double z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
startedScrollObject = j;
|
||||
|
||||
if (x != null) {
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationXValue, x));
|
||||
}
|
||||
if (y != null) {
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationYValue, y));
|
||||
}
|
||||
if (z != null) {
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationZValue, z));
|
||||
}
|
||||
|
||||
double dx = 0, dy = 0, dz = 0;
|
||||
if (x != null) dx = x;
|
||||
if (y != null) dy = y;
|
||||
if (z != null) dz = z;
|
||||
|
||||
dx %= 360;
|
||||
dy %= 360;
|
||||
dz %= 360;
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionRotation(dx, dy, dz);
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
private void rotate(Double x, Double y, Double z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
startedScrollObject = -1;
|
||||
|
||||
fragment.getGlView().queueEvent(() -> {
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
|
||||
double dx = 0, dy = 0, dz = 0;
|
||||
if (x != null) {
|
||||
dx = x;
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationXValue, 0));
|
||||
xTrack.setCurrentPosition(0);
|
||||
}
|
||||
if (y != null) {
|
||||
dy = y;
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationYValue, 0));
|
||||
yTrack.setCurrentPosition(0);
|
||||
}
|
||||
if (z != null) {
|
||||
dz = z;
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationZValue, 0));
|
||||
zTrack.setCurrentPosition(0);
|
||||
}
|
||||
|
||||
dx %= 360;
|
||||
dy %= 360;
|
||||
dz %= 360;
|
||||
|
||||
model.rotate(j, Math.toRadians(dx), Math.toRadians(dy), Math.toRadians(dz));
|
||||
model.ensureOnBed(j);
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionRotation(0, 0, 0);
|
||||
fragment.getGlView().getRenderer().invalidateGlModel(j);
|
||||
fragment.getGlView().requestRender();
|
||||
});
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
private CharSequence formatTrackTitle(int res, double value) {
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(Santoku.INSTANCE.getString(res, value));
|
||||
sb.append(" d");
|
||||
int size = ViewUtils.dp(14);
|
||||
Drawable dr = ContextCompat.getDrawable(Santoku.INSTANCE, R.drawable.edit_outline_28);
|
||||
dr.setTint(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.setSpan(new TextColorImageSpan(dr, 0), sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return sb;
|
||||
}
|
||||
|
||||
private void showManualEditor(int title, boolean x, boolean y, boolean z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
double current;
|
||||
if (x) {
|
||||
current = xTrack.getCurrentPosition();
|
||||
} else if (y) {
|
||||
current = yTrack.getCurrentPosition();
|
||||
} else {
|
||||
current = zTrack.getCurrentPosition();
|
||||
}
|
||||
|
||||
Context ctx = getView().getContext();
|
||||
FrameLayout fl = new FrameLayout(ctx);
|
||||
EditText text = new EditText(ctx);
|
||||
text.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
|
||||
text.setText(String.format(Locale.ROOT, "%.2f", current));
|
||||
text.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
fl.addView(text, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(title)
|
||||
.setView(fl)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
double value;
|
||||
try {
|
||||
value = Double.parseDouble(text.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
value = current;
|
||||
}
|
||||
Double dx = null, dy = null, dz = null;
|
||||
if (x) xTrack.setCurrentPosition((dx = value).intValue());
|
||||
if (y) yTrack.setCurrentPosition((dy = value).intValue());
|
||||
if (z) zTrack.setCurrentPosition((dz = value).intValue());
|
||||
|
||||
rotateVisual(dx, dy, dz);
|
||||
})
|
||||
.show();
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
text.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(text, 0);
|
||||
text.setSelection(text.getText().length());
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, 0);
|
||||
|
||||
xTitle = new TextView(ctx);
|
||||
xTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
xTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
xTitle.setTextColor(ThemesRepo.getColor(R.attr.xTrackColor));
|
||||
xTitle.setGravity(Gravity.CENTER);
|
||||
xTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationRotationX, true, false, false));
|
||||
ll.addView(xTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
xTrack = new PositionScrollView(ctx);
|
||||
xTrack.setActiveColor(R.attr.xTrackColor);
|
||||
xTrack.setProgressListener(integer -> rotateVisual(integer.doubleValue(), (double) yTrack.getCurrentPosition(), (double) zTrack.getCurrentPosition()));
|
||||
ll.addView(xTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
yTitle = new TextView(ctx);
|
||||
yTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
yTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
yTitle.setTextColor(ThemesRepo.getColor(R.attr.yTrackColor));
|
||||
yTitle.setGravity(Gravity.CENTER);
|
||||
yTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationRotationY, false, true, false));
|
||||
ll.addView(yTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
yTrack = new PositionScrollView(ctx);
|
||||
yTrack.setActiveColor(R.attr.yTrackColor);
|
||||
yTrack.setProgressListener(integer -> rotateVisual((double) xTrack.getCurrentPosition(), integer.doubleValue(), (double) zTrack.getCurrentPosition()));
|
||||
ll.addView(yTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
zTitle = new TextView(ctx);
|
||||
zTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
zTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
zTitle.setTextColor(ThemesRepo.getColor(R.attr.zTrackColor));
|
||||
zTitle.setGravity(Gravity.CENTER);
|
||||
zTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationRotationZ, false, false, true));
|
||||
ll.addView(zTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
zTrack = new PositionScrollView(ctx);
|
||||
zTrack.setActiveColor(R.attr.zTrackColor);
|
||||
zTrack.setProgressListener(integer -> rotateVisual((double) xTrack.getCurrentPosition(), (double) yTrack.getCurrentPosition(), integer.doubleValue()));
|
||||
ll.addView(zTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
ll.addView(new Space(ctx), new LinearLayout.LayoutParams(0, 0, 1f));
|
||||
|
||||
BeamButton btn = new BeamButton(ctx);
|
||||
btn.setText(R.string.MenuOrientationRotationApply);
|
||||
btn.setOnClickListener(v -> rotate((double) xTrack.getCurrentPosition(), (double) yTrack.getCurrentPosition(), (double) zTrack.getCurrentPosition()));
|
||||
if (portrait) {
|
||||
ll.addView(btn, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
topMargin = bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
}
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(portrait ? 12 : 4), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
|
||||
if (!portrait) {
|
||||
toolbar.addView(btn, new LinearLayout.LayoutParams(0, ViewUtils.dp(42), 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
}
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return portrait ? ViewUtils.dp(52) + ViewUtils.dp(56) + ViewUtils.dp(80 + 24) * 3 + ViewUtils.dp(12) : (int) (into.getWidth() * 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
setSelectionValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
stopScroll();
|
||||
}
|
||||
|
||||
private void setSelectionValues() {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionTranslation(0, 0, 0);
|
||||
|
||||
xTrack.setCurrentPosition(0);
|
||||
yTrack.setCurrentPosition(0);
|
||||
zTrack.setCurrentPosition(0);
|
||||
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationXValue, 0));
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationYValue, 0));
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuOrientationRotationZValue, 0));
|
||||
}
|
||||
|
||||
private void stopScroll() {
|
||||
xTrack.stopScroll();
|
||||
yTrack.stopScroll();
|
||||
zTrack.stopScroll();
|
||||
|
||||
if (startedScrollObject != -1) {
|
||||
fragment.getGlView().getRenderer().setSelectionRotation(0, 0, 0);
|
||||
}
|
||||
startedScrollObject = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionRotation(0, 0, 0);
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onSelectedObjectChanged(SelectedObjectChangedEvent e) {
|
||||
stopScroll();
|
||||
|
||||
if (fragment.getGlView().getRenderer().getSelectedObject() == -1) {
|
||||
dismiss();
|
||||
} else {
|
||||
setSelectionValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import com.google.android.material.checkbox.MaterialCheckBox;
|
||||
import com.loopj.android.http.AsyncHttpClient;
|
||||
import com.loopj.android.http.AsyncHttpResponseHandler;
|
||||
import com.loopj.android.http.RequestParams;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import cz.msebera.android.httpclient.Header;
|
||||
import cz.msebera.android.httpclient.entity.ContentType;
|
||||
import cz.msebera.android.httpclient.message.BasicHeader;
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.MainActivity;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.UnfoldMenu;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.events.NeedDismissSnackbarEvent;
|
||||
import com.dark98.santoku.events.NeedSnackbarEvent;
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.print_host.ElegooLinkClient;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.slic3r.GCodeProcessorResult;
|
||||
import com.dark98.santoku.slic3r.GCodeViewer;
|
||||
import com.dark98.santoku.slic3r.Slic3rLocalization;
|
||||
import com.dark98.santoku.theme.IThemeView;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
import com.dark98.santoku.view.PositionScrollView;
|
||||
import com.dark98.santoku.view.SegmentsView;
|
||||
import com.dark98.santoku.view.SnackbarsLayout;
|
||||
|
||||
public class SliceMenu extends ListBedMenu {
|
||||
private AsyncHttpClient client = new AsyncHttpClient();
|
||||
|
||||
{
|
||||
client.setLoggingEnabled(true);
|
||||
client.setMaxRetriesAndTimeout(0, 10000);
|
||||
}
|
||||
|
||||
private final static List<String> SUPPORTED_SEND = Arrays.asList("octoprint", "elegoolink");
|
||||
private int lastUid;
|
||||
|
||||
@Override
|
||||
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
|
||||
lastUid = Santoku.CONFIG_UID;
|
||||
List<SimpleRecyclerItem> items = new ArrayList<>(Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuSliceInfo, R.drawable.clock_circle_dashed_outline_24).onClick(v -> fragment.showUnfoldMenu(new InfoMenu(), v)),
|
||||
new BedMenuItem(R.string.MenuSliceLayers, R.drawable.square_stack_up_outline_28).onClick(v -> fragment.showUnfoldMenu(new LayersMenu(), v)),
|
||||
new BedMenuItem(R.string.MenuSliceExportToFile, R.drawable.folder_simple_arrow_right_outline_28).onClick(v -> {
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
i.setType("application/x-gcode");
|
||||
i.putExtra(Intent.EXTRA_TITLE, fragment.getGlView().getRenderer().getGcodeResult().getRecommendedName());
|
||||
act.startActivityForResult(i, MainActivity.REQUEST_CODE_EXPORT_GCODE);
|
||||
}
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuSliceShare, R.drawable.share_external_28).onClick(v -> {
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
File f = BedFragment.getTempGCodePath();
|
||||
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
Intent i = new Intent(Intent.ACTION_SEND_MULTIPLE);
|
||||
i.setType("application/x-gcode");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Simple trick for Samsung to display "1 element" instead of "temp.gcode"
|
||||
// It doesn't actually resolve name from provider and uses path-parsing instead, bruh.
|
||||
i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, new ArrayList<>(Collections.singletonList(FileProvider.getUriForFile(act, BuildConfig.APPLICATION_ID + ".provider", f, BedFragment.getTempFileName()))));
|
||||
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
} else {
|
||||
i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f));
|
||||
}
|
||||
act.startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
})
|
||||
));
|
||||
ConfigObject obj = Santoku.CONFIG.findPrinter(Santoku.CONFIG.presets.get("printer"));
|
||||
assertTrue(obj != null);
|
||||
String type = obj.get("host_type");
|
||||
if (type == null) type = "octoprint";
|
||||
String host = obj.get("print_host");
|
||||
String apiKey = obj.get("printhost_apikey");
|
||||
if (SUPPORTED_SEND.contains(type) && !TextUtils.isEmpty(host)) {
|
||||
String finalType = type;
|
||||
ConfigObject finalObj = obj;
|
||||
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinter, R.drawable.send_outline_28).onClick(v -> upload(finalType, host, apiKey, false, finalObj)));
|
||||
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinterAndPrint, R.drawable.send_28).onClick(v -> upload(finalType, host, apiKey, true, finalObj)));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private void upload(String type, String host, String apiKey, boolean print, ConfigObject config) {
|
||||
String name = fragment.getGlView().getRenderer().getGcodeResult().getRecommendedName();
|
||||
switch (type) {
|
||||
default:
|
||||
case "octoprint":
|
||||
if (!host.startsWith("http://")) {
|
||||
host = "http://" + host;
|
||||
}
|
||||
String tag = UUID.randomUUID().toString();
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuSliceSendToPrinterLoading).tag(tag));
|
||||
Header[] headers = TextUtils.isEmpty(apiKey) ? new Header[0] : new Header[] {new BasicHeader("X-Api-Key", apiKey)};
|
||||
RequestParams params = new RequestParams();
|
||||
try {
|
||||
params.put("file", new FileInputStream(BedFragment.getTempGCodePath()), name, ContentType.TEXT_PLAIN.getMimeType());
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
params.put("select", String.valueOf(print));
|
||||
params.put("print", String.valueOf(print));
|
||||
|
||||
client.post(Santoku.INSTANCE, host + "/api/files/local", headers, params, null, new AsyncHttpResponseHandler() {
|
||||
@Override
|
||||
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
|
||||
try {
|
||||
JSONObject obj = new JSONObject(new String(responseBody));
|
||||
if (!obj.has("action") && !obj.has("files")) {
|
||||
throw new JSONException(obj.toString());
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(print ? SnackbarsLayout.Type.INFO : SnackbarsLayout.Type.DONE, print ? R.string.MenuSliceSendToPrinterPrintStarted : R.string.MenuSliceSendToPrinterOK));
|
||||
} catch (JSONException e) {
|
||||
onFailure(statusCode, headers, responseBody, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(tag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(fragment.getContext())
|
||||
.setTitle(R.string.MenuSliceSendToPrinterFailed)
|
||||
.setMessage(error.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show());
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "elegoolink": {
|
||||
if (!host.startsWith("http://") && !host.startsWith("https://")) {
|
||||
host = "http://" + host;
|
||||
}
|
||||
String elegooTag = UUID.randomUUID().toString();
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuSliceSendToPrinterLoading).tag(elegooTag));
|
||||
String finalHost = host;
|
||||
final boolean timelapse = config != null && "1".equals(config.get("elegoolink_timelapse"));
|
||||
final boolean bedLeveling = config != null && "1".equals(config.get("elegoolink_bed_leveling"));
|
||||
int bedTypeVal = 0;
|
||||
if (config != null) {
|
||||
String bedTypeValue = config.get("elegoolink_bed_type");
|
||||
if ("1".equals(bedTypeValue) || "pc".equalsIgnoreCase(bedTypeValue)) {
|
||||
bedTypeVal = 1;
|
||||
}
|
||||
}
|
||||
final int bedType = bedTypeVal;
|
||||
new Thread(() -> {
|
||||
ElegooLinkClient.Result result = ElegooLinkClient.upload(BedFragment.getTempGCodePath(), finalHost, name, print, timelapse, bedLeveling, bedType);
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(elegooTag));
|
||||
if (result.ok) {
|
||||
Santoku.EVENT_BUS.fireEvent(new NeedSnackbarEvent(print ? SnackbarsLayout.Type.INFO : SnackbarsLayout.Type.DONE, print ? R.string.MenuSliceSendToPrinterPrintStarted : R.string.MenuSliceSendToPrinterOK));
|
||||
} else {
|
||||
new BeamAlertDialogBuilder(fragment.getContext())
|
||||
.setTitle(R.string.MenuSliceSendToPrinterFailed)
|
||||
.setMessage(result.error)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}).start();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v) {
|
||||
super.onViewCreated(v);
|
||||
v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull View v) {
|
||||
if (lastUid != Santoku.CONFIG_UID) {
|
||||
adapter.setItems(onCreateItems(v.getWidth() < v.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull View v) {}
|
||||
});
|
||||
}
|
||||
|
||||
private final static class InfoMenu extends UnfoldMenu implements IThemeView {
|
||||
private TextView totalView;
|
||||
private SegmentsView segmentsView;
|
||||
private ExtrusionRoleView[] roleViews = new ExtrusionRoleView[GCodeViewer.EXTRUSION_ROLES_COUNT];
|
||||
private static DecimalFormat format = new DecimalFormat("0.##");
|
||||
|
||||
private GCodeViewer getViewer() {
|
||||
return fragment.getGlView().getRenderer().getViewer();
|
||||
}
|
||||
|
||||
private GCodeProcessorResult getResult() {
|
||||
return fragment.getGlView().getRenderer().getGcodeResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
ll.addView(new Space(ctx), new LinearLayout.LayoutParams(0, 0, 1f));
|
||||
|
||||
for (int i = 0; i < GCodeViewer.EXTRUSION_ROLES_COUNT; i++) {
|
||||
ll.addView(roleViews[i] = new ExtrusionRoleView(ctx));
|
||||
}
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
totalView = new TextView(ctx);
|
||||
totalView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13);
|
||||
totalView.setGravity(Gravity.CENTER);
|
||||
ll.addView(totalView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(18)) {{
|
||||
topMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
segmentsView = new SegmentsView(ctx);
|
||||
ll.addView(segmentsView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(12)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
topMargin = bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
onApplyTheme();
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
GCodeViewer viewer = getViewer();
|
||||
GCodeProcessorResult result = getResult();
|
||||
if (viewer != null) {
|
||||
for (int i = 0; i < GCodeViewer.EXTRUSION_ROLES_COUNT; i++) {
|
||||
boolean visible = viewer.getEstimatedTime(i) != 0;
|
||||
roleViews[i].setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
if (visible) {
|
||||
roleViews[i].bind(viewer, result, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateValues();
|
||||
ViewUtils.postOnMainThread(() -> segmentsView.startAnimation(), 50);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
segmentsView.setNotVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
if (portrait) {
|
||||
GCodeViewer viewer = getViewer();
|
||||
if (viewer != null) {
|
||||
int visibleCount = 0;
|
||||
for (int i = 0; i < GCodeViewer.EXTRUSION_ROLES_COUNT; i++) {
|
||||
if (viewer.getEstimatedTime(i) != 0) {
|
||||
visibleCount++;
|
||||
}
|
||||
}
|
||||
return ViewUtils.dp(42) * visibleCount + ViewUtils.dp(28) + ViewUtils.dp(52) + ViewUtils.dp(18 + 8);
|
||||
}
|
||||
}
|
||||
return super.getRequestedSize(into, portrait);
|
||||
}
|
||||
|
||||
private float getTotalEstimatedTime() {
|
||||
GCodeViewer viewer = getViewer();
|
||||
if (viewer == null) return 0;
|
||||
float total = 0;
|
||||
for (int i = 0; i < GCodeViewer.EXTRUSION_ROLES_COUNT; i++) {
|
||||
if (viewer.isExtrusionRoleVisible(i)) {
|
||||
total += viewer.getEstimatedTime(i);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void updateValues() {
|
||||
GCodeViewer viewer = getViewer();
|
||||
GCodeProcessorResult result = getResult();
|
||||
if (viewer == null) return;
|
||||
|
||||
float[] values = new float[2 + GCodeViewer.EXTRUSION_ROLES_COUNT];
|
||||
double totalWeight = 0;
|
||||
double totalLength = 0;
|
||||
values[0] = 0;
|
||||
values[values.length - 1] = 1;
|
||||
float prev = 0;
|
||||
int lastVisible = 0;
|
||||
float totalTime = getTotalEstimatedTime();
|
||||
for (int i = 0; i < GCodeViewer.EXTRUSION_ROLES_COUNT; i++) {
|
||||
if (viewer.isExtrusionRoleVisible(i)) {
|
||||
float percent = viewer.getEstimatedTime(i) / totalTime;
|
||||
values[i + 1] = prev + percent;
|
||||
lastVisible = i;
|
||||
prev = values[i + 1];
|
||||
totalLength += result.getUsedFilamentMM(i);
|
||||
totalWeight += result.getUsedFilamentG(i);
|
||||
} else {
|
||||
values[i + 1] = prev;
|
||||
}
|
||||
}
|
||||
|
||||
values[lastVisible] = 1;
|
||||
|
||||
segmentsView.setValues(values);
|
||||
totalView.setText(formatComplex(totalWeight, totalLength, totalTime));
|
||||
}
|
||||
|
||||
private static String formatComplex(double weight, double length, float time) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (weight > 0) {
|
||||
sb.append(format.format(weight)).append(" ").append(Santoku.INSTANCE.getString(R.string.MenuSliceInfoWeight)).append(" | ");
|
||||
}
|
||||
sb.append(format.format(length)).append(" ").append(Santoku.INSTANCE.getString(R.string.MenuSliceInfoLength)).append(" | ");
|
||||
sb.append(formatTime(time));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String formatTime(float time) {
|
||||
int secondsTotal = (int) Math.round(Math.ceil(time));
|
||||
int seconds = secondsTotal % 60;
|
||||
int minutes = ((secondsTotal - seconds) / 60) % 60;
|
||||
int hours = ((secondsTotal - seconds) / 60) / 60;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (hours > 0) {
|
||||
sb.append(hours).append(" ").append(Santoku.INSTANCE.getString(R.string.MenuSliceInfoHour));
|
||||
}
|
||||
if (minutes > 0) {
|
||||
if (sb.length() > 0) sb.append(" ");
|
||||
|
||||
sb.append(minutes).append(" ").append(Santoku.INSTANCE.getString(R.string.MenuSliceInfoMinute));
|
||||
}
|
||||
if (seconds > 0 || sb.length() == 0) {
|
||||
if (sb.length() > 0) sb.append(" ");
|
||||
|
||||
sb.append(seconds).append(" ").append(Santoku.INSTANCE.getString(R.string.MenuSliceInfoSecond));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
totalView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
}
|
||||
|
||||
private final class ExtrusionRoleView extends LinearLayout implements IThemeView {
|
||||
private MaterialCheckBox checkBox;
|
||||
private TextView titleView;
|
||||
private TextView timeView;
|
||||
|
||||
private Runnable invalidateGl;
|
||||
|
||||
public ExtrusionRoleView(Context context) {
|
||||
super(context);
|
||||
setOrientation(HORIZONTAL);
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(16), 0);
|
||||
|
||||
checkBox = new MaterialCheckBox(getContext()) {
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
addView(checkBox, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
titleView = new TextView(context);
|
||||
titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
addView(titleView, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
rightMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
timeView = new TextView(context);
|
||||
timeView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12);
|
||||
addView(timeView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(42)));
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
public void bind(GCodeViewer viewer, GCodeProcessorResult result, @GCodeViewer.ExtrusionRole int role) {
|
||||
switch (role) {
|
||||
case GCodeViewer.EXTRUSION_ROLE_NONE:
|
||||
titleView.setText(Slic3rLocalization.getString("Unknown"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_PERIMETER:
|
||||
titleView.setText(Slic3rLocalization.getString("Perimeter"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_EXTERNAL_PERIMETER:
|
||||
titleView.setText(Slic3rLocalization.getString("External perimeter"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_OVERHANG_PERIMETER:
|
||||
titleView.setText(Slic3rLocalization.getString("Overhang perimeter"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_INTERNAL_INFILL:
|
||||
titleView.setText(Slic3rLocalization.getString("Internal infill"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_SOLID_INFILL:
|
||||
titleView.setText(Slic3rLocalization.getString("Solid infill"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_TOP_SOLID_INFILL:
|
||||
titleView.setText(Slic3rLocalization.getString("Top solid infill"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_IRONING:
|
||||
titleView.setText(Slic3rLocalization.getString("Ironing"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_BRIDGE_INFILL:
|
||||
titleView.setText(Slic3rLocalization.getString("Bridge infill"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_GAP_FILL:
|
||||
titleView.setText(Slic3rLocalization.getString("Gap fill"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_SKIRT:
|
||||
titleView.setText(Slic3rLocalization.getString("Skirt/Brim"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_SUPPORT_MATERIAL:
|
||||
titleView.setText(Slic3rLocalization.getString("Support material"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_SUPPORT_MATERIAL_INTERFACE:
|
||||
titleView.setText(Slic3rLocalization.getString("Support material interface"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_WIPE_TOWER:
|
||||
titleView.setText(Slic3rLocalization.getString("Wipe tower"));
|
||||
break;
|
||||
case GCodeViewer.EXTRUSION_ROLE_CUSTOM:
|
||||
titleView.setText(Slic3rLocalization.getString("Custom"));
|
||||
break;
|
||||
}
|
||||
|
||||
timeView.setText(formatComplex(result.getUsedFilamentG(role), result.getUsedFilamentMM(role), viewer.getEstimatedTime(role)));
|
||||
|
||||
checkBox.setChecked(viewer.isExtrusionRoleVisible(role));
|
||||
checkBox.setButtonTintList(ColorStateList.valueOf(SegmentsView.mapColor(role)));
|
||||
setOnClickListener(v -> {
|
||||
if (getTotalEstimatedTime() == viewer.getEstimatedTime(role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewer.toggleExtrusionRoleVisible(role);
|
||||
checkBox.setChecked(!checkBox.isChecked());
|
||||
updateValues();
|
||||
if (invalidateGl != null) ViewUtils.removeCallbacks(invalidateGl);
|
||||
ViewUtils.postOnMainThread(invalidateGl = () -> {
|
||||
Pair<Long, Long> p = viewer.getLayersViewRange();
|
||||
viewer.setLayersViewRange(p.first, p.second);
|
||||
fragment.getGlView().requestRender();
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
titleView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
timeView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 12));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final static class LayersMenu extends UnfoldMenu {
|
||||
private PositionScrollView fromTrack, toTrack;
|
||||
private TextView title;
|
||||
|
||||
private Runnable applyCallback;
|
||||
|
||||
private GCodeViewer getViewer() {
|
||||
return fragment.getGlView().getRenderer().getViewer();
|
||||
}
|
||||
|
||||
private void applyView(int from, int to) {
|
||||
if (applyCallback != null) ViewUtils.removeCallbacks(applyCallback);
|
||||
|
||||
GCodeViewer viewer = getViewer();
|
||||
if (viewer == null) {
|
||||
return;
|
||||
}
|
||||
viewer.setLayersViewRange(from - 1, to - 1);
|
||||
fragment.getGlView().requestRender();
|
||||
|
||||
title.setText(fragment.getContext().getString(R.string.MenuSliceInfoLayers, from, to));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
ll.addView(new Space(ctx), new LinearLayout.LayoutParams(0, 0, 1f));
|
||||
|
||||
title = new TextView(ctx);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
title.setGravity(Gravity.CENTER);
|
||||
ll.addView(title, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
fromTrack = new PositionScrollView(ctx);
|
||||
fromTrack.setProgressListener(integer -> {
|
||||
if (getViewer() == null) return;
|
||||
toTrack.setMinMax(integer, (int) getViewer().getLayersCount());
|
||||
if (toTrack.getCurrentPosition() < integer) {
|
||||
toTrack.setCurrentPosition(integer);
|
||||
}
|
||||
title.setText(fragment.getContext().getString(R.string.MenuSliceInfoLayers, fromTrack.getCurrentPosition(), integer));
|
||||
|
||||
ViewUtils.removeCallbacks(applyCallback);
|
||||
ViewUtils.postOnMainThread(applyCallback = ()-> applyView(integer, toTrack.getCurrentPosition()), 50);
|
||||
});
|
||||
fromTrack.setListener(integer -> applyView(integer, toTrack.getCurrentPosition()));
|
||||
ll.addView(fromTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
toTrack = new PositionScrollView(ctx);
|
||||
toTrack.setProgressListener(integer -> {
|
||||
title.setText(fragment.getContext().getString(R.string.MenuSliceInfoLayers, fromTrack.getCurrentPosition(), integer));
|
||||
|
||||
ViewUtils.removeCallbacks(applyCallback);
|
||||
ViewUtils.postOnMainThread(applyCallback = ()-> applyView(fromTrack.getCurrentPosition(), integer), 50);
|
||||
});
|
||||
toTrack.setListener(integer -> applyView(fromTrack.getCurrentPosition(), integer));
|
||||
ll.addView(toTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return portrait ? ViewUtils.dp(80) * 2 + ViewUtils.dp(24) + ViewUtils.dp(52) + ViewUtils.dp(12) : super.getRequestedSize(into, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
GCodeViewer viewer = getViewer();
|
||||
if (viewer == null) return;
|
||||
long max = viewer.getLayersCount();
|
||||
fromTrack.setMinMax(1, (int) max);
|
||||
toTrack.setMinMax(1, (int) max);
|
||||
|
||||
Pair<Long, Long> range = viewer.getLayersViewRange();
|
||||
// TODO: Support long instead of int in PositionScrollView
|
||||
fromTrack.setCurrentPosition(Math.min(range.first.intValue() + 1, range.second.intValue() + 1));
|
||||
toTrack.setCurrentPosition(Math.max(range.first.intValue() + 1, range.second.intValue() + 1));
|
||||
|
||||
title.setText(fragment.getContext().getString(R.string.MenuSliceInfoLayers, fromTrack.getCurrentPosition(), toTrack.getCurrentPosition()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
fromTrack.stopScroll();
|
||||
toTrack.stopScroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
package com.dark98.santoku.components.bed_menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.UnfoldMenu;
|
||||
import com.dark98.santoku.events.ObjectsListChangedEvent;
|
||||
import com.dark98.santoku.events.SelectedObjectChangedEvent;
|
||||
import com.dark98.santoku.recycler.PreferenceSwitchItem;
|
||||
import com.dark98.santoku.recycler.SimpleRecyclerItem;
|
||||
import com.dark98.santoku.render.GLRenderer;
|
||||
import com.dark98.santoku.slic3r.Model;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.DoubleMatrix;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
import com.dark98.santoku.view.PositionScrollView;
|
||||
import com.dark98.santoku.view.TextColorImageSpan;
|
||||
|
||||
public class TransformMenu extends ListBedMenu {
|
||||
private double[] tempMatrix = new double[16];
|
||||
private double[] tempVecArr = new double[4];
|
||||
|
||||
private boolean hasSelection() {
|
||||
return fragment.getGlView().getRenderer().getModel() != null && fragment.getGlView().getRenderer().getSelectedObject() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
|
||||
return Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuTransformScale, R.drawable.arrow_up_right_corner_outline_24).setEnabled(hasSelection()).onClick(v -> fragment.showUnfoldMenu(new ScaleMenu(), v)),
|
||||
new BedMenuItem(R.string.MenuTransformClone, R.drawable.square_stack_up_outline_28).setEnabled(hasSelection()).onClick(v -> {
|
||||
Context ctx = fragment.getContext();
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
AtomicBoolean autoArrange = new AtomicBoolean(false);
|
||||
|
||||
EditText countInput = new EditText(ctx);
|
||||
countInput.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
countInput.setHint(R.string.MenuTransformCloneCountHint);
|
||||
countInput.setText("1");
|
||||
countInput.setSelection(countInput.getText().length());
|
||||
countInput.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
ll.addView(countInput, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
|
||||
PreferenceSwitchItem.SwitchPreferenceHolderView holderView = new PreferenceSwitchItem.SwitchPreferenceHolderView(ctx);
|
||||
holderView.title.setText(R.string.MenuTransformCloneAutoArrange);
|
||||
holderView.title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
holderView.subtitle.setVisibility(View.GONE);
|
||||
holderView.icon.setVisibility(View.GONE);
|
||||
holderView.matSwitch.setChecked(autoArrange.get());
|
||||
holderView.setOnClickListener(v1 -> {
|
||||
autoArrange.set(!autoArrange.get());
|
||||
holderView.matSwitch.setChecked(autoArrange.get());
|
||||
});
|
||||
ll.addView(holderView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.MenuTransformCloneCountTitle)
|
||||
.setView(ll)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int count = 1;
|
||||
try {
|
||||
count = Integer.parseInt(countInput.getText().toString());
|
||||
} catch (NumberFormatException ignored) {
|
||||
count = 1;
|
||||
}
|
||||
if (count <= 0) return;
|
||||
int clones = count;
|
||||
boolean arrange = autoArrange.get();
|
||||
fragment.getGlView().queueEvent(() -> {
|
||||
GLRenderer renderer = fragment.getGlView().getRenderer();
|
||||
Model model = renderer.getModel();
|
||||
int selected = renderer.getSelectedObject();
|
||||
if (model == null || selected == -1) return;
|
||||
|
||||
for (int i = 0; i < clones; i++) {
|
||||
model.addObject(model, selected);
|
||||
}
|
||||
if (arrange) {
|
||||
renderer.getBed().arrange(model);
|
||||
renderer.resetGlModels();
|
||||
}
|
||||
fragment.getGlView().requestRender();
|
||||
Santoku.EVENT_BUS.fireEvent(new ObjectsListChangedEvent());
|
||||
ViewUtils.postOnMainThread(() -> Toast.makeText(fragment.getContext(), fragment.getContext().getResources().getQuantityString(R.plurals.MenuTransformCloneSuccessCount, clones, clones), Toast.LENGTH_SHORT).show());
|
||||
});
|
||||
fragment.getGlView().requestRender();
|
||||
})
|
||||
.show();
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
countInput.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(countInput, 0);
|
||||
}, 200);
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuTransformMirror, R.drawable.menu_transform_cut_or_mirror_28).setEnabled(hasSelection()).onClick(v -> {
|
||||
Context ctx = fragment.getContext();
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.MenuTransformMirror)
|
||||
.setItems(new CharSequence[] {
|
||||
ctx.getString(R.string.MenuTransformMirrorX),
|
||||
ctx.getString(R.string.MenuTransformMirrorY),
|
||||
ctx.getString(R.string.MenuTransformMirrorZ)
|
||||
}, (dialog, which) -> {
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
Vec3d tempVec = new Vec3d();
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
model.getMirror(j, tempVec);
|
||||
|
||||
double dx = tempVec.x, dy = tempVec.y, dz = tempVec.z;
|
||||
|
||||
switch (which) {
|
||||
case 0:
|
||||
dx = -dx;
|
||||
break;
|
||||
case 1:
|
||||
dy = -dy;
|
||||
break;
|
||||
case 2:
|
||||
dz = -dz;
|
||||
break;
|
||||
}
|
||||
|
||||
model.getScale(j, tempVec);
|
||||
dx *= tempVec.x;
|
||||
dy *= tempVec.y;
|
||||
dz *= tempVec.z;
|
||||
|
||||
model.scale(j, dx, dy, dz);
|
||||
fragment.getGlView().getRenderer().invalidateGlModel(j);
|
||||
fragment.getGlView().requestRender();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onObjectsChanged(ObjectsListChangedEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(0)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(0);
|
||||
|
||||
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(1);
|
||||
|
||||
((BedMenuItem) adapter.getItems().get(2)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(2);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onSelectionChanged(SelectedObjectChangedEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(0)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(0);
|
||||
|
||||
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(1);
|
||||
|
||||
((BedMenuItem) adapter.getItems().get(2)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(2);
|
||||
}
|
||||
|
||||
public final class ScaleMenu extends UnfoldMenu {
|
||||
private PositionScrollView xTrack, yTrack, zTrack;
|
||||
private TextView xTitle, yTitle, zTitle;
|
||||
private Vec3d tempVec = new Vec3d(), tempVec2 = new Vec3d();
|
||||
private int startedScrollObject;
|
||||
private boolean isLinked;
|
||||
|
||||
public void setLinked(boolean linked) {
|
||||
if (isLinked == linked) return;
|
||||
isLinked = linked;
|
||||
|
||||
if (isLinked) {
|
||||
xTrack.addSynced(yTrack);
|
||||
xTrack.addSynced(zTrack);
|
||||
|
||||
yTrack.addSynced(xTrack);
|
||||
yTrack.addSynced(zTrack);
|
||||
|
||||
zTrack.addSynced(xTrack);
|
||||
zTrack.addSynced(yTrack);
|
||||
} else {
|
||||
xTrack.removeSynced(yTrack);
|
||||
xTrack.removeSynced(zTrack);
|
||||
|
||||
yTrack.removeSynced(xTrack);
|
||||
yTrack.removeSynced(zTrack);
|
||||
|
||||
zTrack.removeSynced(xTrack);
|
||||
zTrack.removeSynced(yTrack);
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleVisual(Double x, Double y, Double z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
startedScrollObject = j;
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getScale(j, tempVec);
|
||||
double scaleX = tempVec.x, scaleY = tempVec.y, scaleZ = tempVec.z;
|
||||
model.getBoundingBoxExact(j, tempVec, tempVec2);
|
||||
|
||||
if (x != null) {
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuTransformScaleXValue, x * 100, (tempVec2.x - tempVec.x) / scaleX * x));
|
||||
}
|
||||
if (y != null) {
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuTransformScaleYValue, y * 100, (tempVec2.y - tempVec.y) / scaleY * y));
|
||||
}
|
||||
if (z != null) {
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuTransformScaleZValue, z * 100, (tempVec2.z - tempVec.z) / scaleZ * z));
|
||||
}
|
||||
|
||||
model.getRotation(j, tempVec);
|
||||
DoubleMatrix.setIdentityM(tempMatrix, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, Math.toDegrees(tempVec.x), 1, 0, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, Math.toDegrees(tempVec.y), 0, 1, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, Math.toDegrees(tempVec.z), 0, 0, 1);
|
||||
|
||||
model.getScale(j, tempVec);
|
||||
tempVecArr[0] = tempVec.x;
|
||||
tempVecArr[1] = tempVec.y;
|
||||
tempVecArr[2] = tempVec.z;
|
||||
tempVecArr[3] = 1;
|
||||
DoubleMatrix.multiplyMV(tempVecArr, 0, tempMatrix, 0, tempVecArr, 0);
|
||||
double sx = Math.abs(tempVecArr[0] / tempVecArr[3]);
|
||||
double sy = Math.abs(tempVecArr[1] / tempVecArr[3]);
|
||||
double sz = Math.abs(tempVecArr[2] / tempVecArr[3]);
|
||||
|
||||
double dx = 1, dy = 1, dz = 1;
|
||||
if (x != null) dx = 1 / sx * x;
|
||||
if (y != null) dy = 1 / sy * y;
|
||||
if (z != null) dz = 1 / sz * z;
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionScale(dx, dy, dz);
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
private void scale(Double x, Double y, Double z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
startedScrollObject = -1;
|
||||
|
||||
fragment.getGlView().queueEvent(() -> {
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getScale(j, tempVec);
|
||||
double scaleX = tempVec.x, scaleY = tempVec.y, scaleZ = tempVec.z;
|
||||
model.getBoundingBoxExact(j, tempVec, tempVec2);
|
||||
|
||||
double dx = 1f, dy = 1f, dz = 1f;
|
||||
if (x != null) {
|
||||
dx = x;
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuTransformScaleXValue, x * 100, (tempVec2.x - tempVec.x) / scaleX * x));
|
||||
}
|
||||
if (y != null) {
|
||||
dy = y;
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuTransformScaleYValue, y * 100, (tempVec2.y - tempVec.y) / scaleY * y));
|
||||
}
|
||||
if (z != null) {
|
||||
dz = z;
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuTransformScaleZValue, z * 100, (tempVec2.z - tempVec.z) / scaleZ * z));
|
||||
}
|
||||
|
||||
model.getRotation(j, tempVec);
|
||||
DoubleMatrix.setIdentityM(tempMatrix, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, Math.toDegrees(tempVec.x), 1, 0, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, Math.toDegrees(tempVec.y), 0, 1, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, Math.toDegrees(tempVec.z), 0, 0, 1);
|
||||
tempVecArr[0] = dx;
|
||||
tempVecArr[1] = dy;
|
||||
tempVecArr[2] = dz;
|
||||
tempVecArr[3] = 1;
|
||||
DoubleMatrix.multiplyMV(tempVecArr, 0, tempMatrix, 0, tempVecArr, 0);
|
||||
dx = Math.abs(tempVecArr[0] / tempVecArr[3]);
|
||||
dy = Math.abs(tempVecArr[1] / tempVecArr[3]);
|
||||
dz = Math.abs(tempVecArr[2] / tempVecArr[3]);
|
||||
|
||||
model.getMirror(j, tempVec);
|
||||
dx *= tempVec.x;
|
||||
dy *= tempVec.y;
|
||||
dz *= tempVec.z;
|
||||
|
||||
model.getScale(j, tempVec);
|
||||
model.scale(j, dx, dy, dz);
|
||||
model.ensureOnBed(j);
|
||||
|
||||
fragment.getGlView().getRenderer().invalidateSelectionObject();
|
||||
fragment.getGlView().getRenderer().setSelectionScale(1, 1, 1);
|
||||
fragment.getGlView().getRenderer().invalidateGlModel(j);
|
||||
fragment.getGlView().requestRender();
|
||||
});
|
||||
fragment.getGlView().requestRender();
|
||||
}
|
||||
|
||||
private CharSequence formatTrackTitle(int res, double value, double mm) {
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(Santoku.INSTANCE.getString(res, value, mm));
|
||||
sb.append(" d");
|
||||
int size = ViewUtils.dp(14);
|
||||
Drawable dr = ContextCompat.getDrawable(Santoku.INSTANCE, R.drawable.edit_outline_28);
|
||||
dr.setTint(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.setSpan(new TextColorImageSpan(dr, 0), sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return sb;
|
||||
}
|
||||
|
||||
private void showManualEditor(int title, boolean x, boolean y, boolean z) {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getScale(j, tempVec);
|
||||
|
||||
Context ctx = getView().getContext();
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
AtomicBoolean inputInMM = new AtomicBoolean(Prefs.isScaleInputInMM());
|
||||
|
||||
double cur;
|
||||
if (x) {
|
||||
cur = tempVec.x * 100;
|
||||
} else if (y) {
|
||||
cur = tempVec.y * 100;
|
||||
} else {
|
||||
cur = tempVec.z * 100;
|
||||
}
|
||||
|
||||
model.getScale(j, tempVec);
|
||||
double scaleX = tempVec.x, scaleY = tempVec.y, scaleZ = tempVec.z;
|
||||
model.getBoundingBoxExact(j, tempVec, tempVec2);
|
||||
if (inputInMM.get()) {
|
||||
cur /= 100.0;
|
||||
|
||||
if (x) {
|
||||
cur *= (tempVec2.x - tempVec.x) / scaleX;
|
||||
} else if (y) {
|
||||
cur *= (tempVec2.y - tempVec.y) / scaleY;
|
||||
} else {
|
||||
cur *= (tempVec2.z - tempVec.z) / scaleZ;
|
||||
}
|
||||
}
|
||||
double current = cur;
|
||||
|
||||
EditText text = new EditText(ctx);
|
||||
text.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
text.setText(String.format(Locale.ROOT, "%.2f", current));
|
||||
text.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
|
||||
PreferenceSwitchItem.SwitchPreferenceHolderView holderView = new PreferenceSwitchItem.SwitchPreferenceHolderView(ctx);
|
||||
holderView.title.setText(R.string.MenuTransformScaleMM);
|
||||
holderView.title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
holderView.matSwitch.setChecked(inputInMM.get());
|
||||
holderView.subtitle.setVisibility(View.GONE);
|
||||
holderView.setOnClickListener(v -> {
|
||||
inputInMM.set(!inputInMM.get());
|
||||
|
||||
double value;
|
||||
try {
|
||||
value = Double.parseDouble(text.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
value = current;
|
||||
}
|
||||
|
||||
if (inputInMM.get()) {
|
||||
value /= 100.0;
|
||||
|
||||
if (x) {
|
||||
value *= (tempVec2.x - tempVec.x) / scaleX;
|
||||
} else if (y) {
|
||||
value *= (tempVec2.y - tempVec.y) / scaleY;
|
||||
} else {
|
||||
value *= (tempVec2.z - tempVec.z) / scaleZ;
|
||||
}
|
||||
} else {
|
||||
if (x) {
|
||||
value /= (tempVec2.x - tempVec.x) / scaleX;
|
||||
} else if (y) {
|
||||
value /= (tempVec2.y - tempVec.y) / scaleY;
|
||||
} else {
|
||||
value /= (tempVec2.z - tempVec.z) / scaleZ;
|
||||
}
|
||||
value *= 100.0;
|
||||
}
|
||||
text.setText(String.format(Locale.ROOT, "%.2f", value));
|
||||
holderView.matSwitch.setChecked(inputInMM.get());
|
||||
Prefs.setScaleInputInMM(inputInMM.get());
|
||||
});
|
||||
ll.addView(holderView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(title)
|
||||
.setView(ll)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
double value;
|
||||
try {
|
||||
value = Double.parseDouble(text.getText().toString());
|
||||
|
||||
if (inputInMM.get()) {
|
||||
if (x) {
|
||||
value /= (tempVec2.x - tempVec.x) / scaleX;
|
||||
} else if (y) {
|
||||
value /= (tempVec2.y - tempVec.y) / scaleY;
|
||||
} else {
|
||||
value /= (tempVec2.z - tempVec.z) / scaleZ;
|
||||
}
|
||||
} else {
|
||||
value /= 100.0;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
value = current;
|
||||
}
|
||||
|
||||
double dx = tempVec.x, dy = tempVec.y, dz = tempVec.z;
|
||||
if (x || isLinked) xTrack.setCurrentPosition((int) ((dx = value) * 100));
|
||||
if (y || isLinked) yTrack.setCurrentPosition((int) ((dy = value) * 100));
|
||||
if (z || isLinked) zTrack.setCurrentPosition((int) ((dz = value) * 100));
|
||||
|
||||
scale(dx, dy, dz);
|
||||
})
|
||||
.show();
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
text.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(text, 0);
|
||||
text.setSelection(text.getText().length());
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, 0);
|
||||
|
||||
xTitle = new TextView(ctx);
|
||||
xTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
xTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
xTitle.setTextColor(ThemesRepo.getColor(R.attr.xTrackColor));
|
||||
xTitle.setGravity(Gravity.CENTER);
|
||||
xTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationPositionX, true, false, false));
|
||||
ll.addView(xTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
xTrack = new PositionScrollView(ctx);
|
||||
xTrack.setActiveColor(R.attr.xTrackColor);
|
||||
xTrack.setProgressListener(integer -> scaleVisual(integer.doubleValue() / 100.0, yTrack.getCurrentPosition() / 100.0, zTrack.getCurrentPosition() / 100.0));
|
||||
xTrack.setListener(integer -> scale(integer.doubleValue() / 100.0, yTrack.getCurrentPosition() / 100.0, zTrack.getCurrentPosition() / 100.0));
|
||||
xTrack.setMinMax(1, Integer.MAX_VALUE);
|
||||
ll.addView(xTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
yTitle = new TextView(ctx);
|
||||
yTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
yTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
yTitle.setTextColor(ThemesRepo.getColor(R.attr.yTrackColor));
|
||||
yTitle.setGravity(Gravity.CENTER);
|
||||
yTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationPositionY, false, true, false));
|
||||
ll.addView(yTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
yTrack = new PositionScrollView(ctx);
|
||||
yTrack.setActiveColor(R.attr.yTrackColor);
|
||||
yTrack.setProgressListener(integer -> scaleVisual(xTrack.getCurrentPosition() / 100.0, integer.doubleValue() / 100.0, zTrack.getCurrentPosition() / 100.0));
|
||||
yTrack.setListener(integer -> scale(xTrack.getCurrentPosition() / 100.0, integer.doubleValue() / 100.0, zTrack.getCurrentPosition() / 100.0));
|
||||
yTrack.setMinMax(1, Integer.MAX_VALUE);
|
||||
ll.addView(yTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
zTitle = new TextView(ctx);
|
||||
zTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
zTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
zTitle.setTextColor(ThemesRepo.getColor(R.attr.zTrackColor));
|
||||
zTitle.setGravity(Gravity.CENTER);
|
||||
zTitle.setOnClickListener(v -> showManualEditor(R.string.MenuOrientationPositionZ, false, false, true));
|
||||
ll.addView(zTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(20)) {{
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
zTrack = new PositionScrollView(ctx);
|
||||
zTrack.setActiveColor(R.attr.zTrackColor);
|
||||
zTrack.setProgressListener(integer -> scaleVisual(xTrack.getCurrentPosition() / 100.0, yTrack.getCurrentPosition() / 100.0, integer.doubleValue() / 100.0));
|
||||
zTrack.setListener(integer -> scale(xTrack.getCurrentPosition() / 100.0, yTrack.getCurrentPosition() / 100.0, integer.doubleValue() / 100.0));
|
||||
zTrack.setMinMax(1, Integer.MAX_VALUE);
|
||||
ll.addView(zTrack, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(80)));
|
||||
|
||||
ll.addView(new Space(ctx), new LinearLayout.LayoutParams(0, 0, 1f));
|
||||
|
||||
PreferenceSwitchItem.SwitchPreferenceHolderView holderView = new PreferenceSwitchItem.SwitchPreferenceHolderView(ctx);
|
||||
holderView.title.setText(R.string.MenuTransformScaleLink);
|
||||
holderView.title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
holderView.matSwitch.setChecked(Prefs.isScaleLinked());
|
||||
holderView.subtitle.setVisibility(View.GONE);
|
||||
holderView.setOnClickListener(v -> {
|
||||
boolean check = !Prefs.isScaleLinked();
|
||||
holderView.matSwitch.setChecked(check);
|
||||
Prefs.setScaleLinked(check);
|
||||
setLinked(check);
|
||||
});
|
||||
if (portrait) {
|
||||
ll.addView(holderView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(64)));
|
||||
}
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
if (!portrait) {
|
||||
title.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
((LinearLayout.LayoutParams) title.getLayoutParams()).weight = 0;
|
||||
|
||||
holderView.title.setMaxLines(1);
|
||||
toolbar.addView(holderView, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
}
|
||||
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
|
||||
setLinked(Prefs.isScaleLinked());
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return portrait ? ViewUtils.dp(52) + ViewUtils.dp(64) + ViewUtils.dp(80 + 24) * 3 + ViewUtils.dp(12) : (int) (into.getWidth() * 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
setSelectionValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
stopScroll();
|
||||
}
|
||||
|
||||
private void setSelectionValues() {
|
||||
int j = fragment.getGlView().getRenderer().getSelectedObject();
|
||||
if (j == -1) return;
|
||||
|
||||
fragment.getGlView().getRenderer().setSelectionScale(1, 1, 1);
|
||||
|
||||
Model model = fragment.getGlView().getRenderer().getModel();
|
||||
model.getScale(j, tempVec);
|
||||
double scaleX = tempVec.x, scaleY = tempVec.y, scaleZ = tempVec.z;
|
||||
|
||||
xTrack.setCurrentPosition((int) Math.round(scaleX * 100));
|
||||
yTrack.setCurrentPosition((int) Math.round(scaleY * 100));
|
||||
zTrack.setCurrentPosition((int) Math.round(scaleZ * 100));
|
||||
|
||||
model.getBoundingBoxExact(j, tempVec, tempVec2);
|
||||
xTitle.setText(formatTrackTitle(R.string.MenuTransformScaleXValue, scaleX * 100, (tempVec2.x - tempVec.x)));
|
||||
yTitle.setText(formatTrackTitle(R.string.MenuTransformScaleYValue, scaleY * 100, (tempVec2.y - tempVec.y)));
|
||||
zTitle.setText(formatTrackTitle(R.string.MenuTransformScaleZValue, scaleZ * 100, (tempVec2.z - tempVec.z)));
|
||||
|
||||
xTrack.updateSyncDeltas();
|
||||
yTrack.updateSyncDeltas();
|
||||
zTrack.updateSyncDeltas();
|
||||
}
|
||||
|
||||
private void stopScroll() {
|
||||
xTrack.stopScroll();
|
||||
yTrack.stopScroll();
|
||||
zTrack.stopScroll();
|
||||
|
||||
if (startedScrollObject != -1) {
|
||||
fragment.getGlView().getRenderer().setSelectionScale(1, 1, 1);
|
||||
}
|
||||
startedScrollObject = -1;
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onSelectedObjectChanged(SelectedObjectChangedEvent e) {
|
||||
stopScroll();
|
||||
|
||||
if (fragment.getGlView().getRenderer().getSelectedObject() == -1) {
|
||||
dismiss();
|
||||
} else {
|
||||
setSelectionValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.dark98.santoku.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.fragment.ProfileListFragment;
|
||||
|
||||
/** @noinspection CopyConstructorMissesField*/
|
||||
public class ConfigObject implements ProfileListFragment.ProfileListItem {
|
||||
public final static int PROFILE_LIST_PRINT = 0, PROFILE_LIST_FILAMENT = 1, PROFILE_LIST_PRINTER = 2;
|
||||
|
||||
private String title;
|
||||
public Map<String, String> values = new HashMap<>();
|
||||
|
||||
// Used only in setup
|
||||
public String thumbnailUrl;
|
||||
|
||||
// Type for isSelected()
|
||||
public int profileListType;
|
||||
|
||||
public ConfigObject() {
|
||||
title = null;
|
||||
}
|
||||
|
||||
public ConfigObject(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public ConfigObject(ConfigObject from) {
|
||||
this.title = from.title;
|
||||
this.values.putAll(from.values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: suitable only from "printer" config
|
||||
*/
|
||||
public int getExtruderCount() {
|
||||
return get("nozzle_diameter") != null ? get("nozzle_diameter").replaceAll("[^.]+", "").length() : 1;
|
||||
}
|
||||
|
||||
public boolean has(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
return values.get(key);
|
||||
}
|
||||
|
||||
public void remove(String key) {
|
||||
values.remove(key);
|
||||
}
|
||||
|
||||
public void put(String key, String value) {
|
||||
values.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
switch (profileListType) {
|
||||
case PROFILE_LIST_PRINT:
|
||||
return getTitle().equals(Santoku.CONFIG.presets.get("print"));
|
||||
case PROFILE_LIST_FILAMENT:
|
||||
return getTitle().equals(Santoku.CONFIG.presets.get("filament"));
|
||||
case PROFILE_LIST_PRINTER:
|
||||
return getTitle().equals(Santoku.CONFIG.presets.get("printer"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# generated by Slice Beam ").append(BuildConfig.VERSION_NAME).append("\n\n");
|
||||
for (Map.Entry<String, String> en : values.entrySet()) {
|
||||
sb.append(en.getKey()).append(" = ").append(en.getValue().replace("\n", "\\n")).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static ConfigObject createCustomPrinterProfile() {
|
||||
ConfigObject custom = new ConfigObject(Santoku.INSTANCE.getString(R.string.IntroCustomProfileName));
|
||||
custom.put("printer_technology", "FFF");
|
||||
custom.put("bed_shape", "0x0,200x0,200x200,0x200");
|
||||
custom.put("auto_arrange_bed_clearance", "5");
|
||||
custom.put("auto_arrange_rotate", "1");
|
||||
custom.put("binary_gcode", "0");
|
||||
custom.put("gcode_flavor", "marlin");
|
||||
custom.put("max_print_height", "200");
|
||||
custom.put("min_layer_height", "0.15");
|
||||
custom.put("max_layer_height", "0.30");
|
||||
custom.put("layer_height", "0.2");
|
||||
custom.put("nozzle_diameter", "0.4");
|
||||
custom.put("z_offset", "0");
|
||||
custom.put("retract_length", "0.5");
|
||||
custom.put("retract_speed", "30");
|
||||
custom.put("deretract_speed", "30");
|
||||
custom.put("retract_before_travel", "2");
|
||||
|
||||
custom.put("machine_limits_usage", "time_estimate_only");
|
||||
custom.put("machine_max_acceleration_e", "5000");
|
||||
custom.put("machine_max_acceleration_extruding", "500");
|
||||
custom.put("machine_max_acceleration_retracting", "1000");
|
||||
custom.put("machine_max_acceleration_travel", "500");
|
||||
custom.put("machine_max_acceleration_x", "500");
|
||||
custom.put("machine_max_acceleration_y", "500");
|
||||
custom.put("machine_max_acceleration_z", "100");
|
||||
custom.put("machine_max_feedrate_e", "60");
|
||||
custom.put("machine_max_feedrate_x", "500");
|
||||
custom.put("machine_max_feedrate_y", "500");
|
||||
custom.put("machine_max_feedrate_z", "10");
|
||||
custom.put("machine_max_jerk_e", "5");
|
||||
custom.put("machine_max_jerk_x", "8");
|
||||
custom.put("machine_max_jerk_y", "8");
|
||||
custom.put("machine_max_jerk_z", "0.4");
|
||||
custom.put("machine_min_extruding_rate", "0");
|
||||
custom.put("machine_min_travel_rate", "0");
|
||||
|
||||
custom.put("elegoolink_timelapse", "0");
|
||||
custom.put("elegoolink_bed_leveling", "0");
|
||||
custom.put("elegoolink_bed_type", "pte");
|
||||
|
||||
custom.put("start_gcode", "G90 ; use absolute coordinates\\nM83 ; extruder relative mode\\nM104 S{is_nil(idle_temperature[0]) ? 150 : idle_temperature[0]} ; set temporary nozzle temp to prevent oozing during homing\\nM140 S{first_layer_bed_temperature[0]} ; set final bed temp\\nG4 S30 ; allow partial nozzle warmup\\nG28 ; home all axis\\nG1 Z50 F240\\nG1 X2.0 Y10 F3000\\nM104 S{first_layer_temperature[0]} ; set final nozzle temp\\nM190 S{first_layer_bed_temperature[0]} ; wait for bed temp to stabilize\\nM109 S{first_layer_temperature[0]} ; wait for nozzle temp to stabilize\\nG1 Z0.28 F240\\nG92 E0\\nG1 X2.0 Y140 E10 F1500 ; prime the nozzle\\nG1 X2.3 Y140 F5000\\nG92 E0\\nG1 X2.3 Y10 E10 F1200 ; prime the nozzle\\nG92 E0");
|
||||
custom.put("end_gcode", "{if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+2, max_print_height)} F600 ; Move print head up{endif}\\nG1 X5 Y{print_bed_max[1]*0.85} F{travel_speed*60} ; present print\\n{if max_layer_z < max_print_height-10}G1 Z{z_offset+min(max_layer_z+70, max_print_height-10)} F600 ; Move print head further up{endif}\\n{if max_layer_z < max_print_height*0.6}G1 Z{max_print_height*0.6} F600 ; Move print head further up{endif}\\nM140 S0 ; turn off heatbed\\nM104 S0 ; turn off temperature\\nM107 ; turn off fan\\nM84 X Y E ; disable motors");
|
||||
|
||||
return custom;
|
||||
}
|
||||
|
||||
public static ConfigObject createCustomFilamentProfile() {
|
||||
ConfigObject genericFilament = new ConfigObject(Santoku.INSTANCE.getString(R.string.IntroCustomProfileFilamentName));
|
||||
genericFilament.profileListType = ConfigObject.PROFILE_LIST_FILAMENT;
|
||||
genericFilament.put("first_layer_bed_temperature", "60");
|
||||
genericFilament.put("bed_temperature", "60");
|
||||
genericFilament.put("first_layer_temperature", "210");
|
||||
genericFilament.put("temperature", "210");
|
||||
genericFilament.put("filament_type", "PLA");
|
||||
genericFilament.put("slowdown_below_layer_time", "8");
|
||||
genericFilament.put("cooling", "1");
|
||||
genericFilament.put("fan_always_on", "1");
|
||||
genericFilament.put("fan_below_layer_time", "20");
|
||||
genericFilament.put("idle_temperature", "150");
|
||||
return genericFilament;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class BeamServerDataUpdatedEvent {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudFeaturesUpdatedEvent {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudLoginStateUpdatedEvent {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudModelsRemainingCountUpdatedEvent {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudSyncFinishedEvent {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudUserInfoUpdatedEvent {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class FlattenModeResetEvent {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class LongClickTranslationEvent {
|
||||
public final double x;
|
||||
public final double y;
|
||||
public final boolean visual;
|
||||
|
||||
public LongClickTranslationEvent(double x, double y, boolean visual) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.visual = visual;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class NeedDismissAIGeneratorMenu {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class NeedDismissCalibrationsMenu {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class NeedDismissSnackbarEvent {
|
||||
public final String tag;
|
||||
|
||||
public NeedDismissSnackbarEvent(String tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.view.SnackbarsLayout;
|
||||
|
||||
@Event
|
||||
public class NeedSnackbarEvent {
|
||||
public final CharSequence title;
|
||||
public SnackbarsLayout.Type type = SnackbarsLayout.Type.DONE;
|
||||
public String tag;
|
||||
|
||||
public CharSequence buttonTitle;
|
||||
public View.OnClickListener buttonClick;
|
||||
|
||||
public NeedSnackbarEvent(SnackbarsLayout.Type type, CharSequence title) {
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public NeedSnackbarEvent(CharSequence title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public NeedSnackbarEvent(int title, Object... args) {
|
||||
this.title = Santoku.INSTANCE.getString(title, args);
|
||||
}
|
||||
|
||||
public NeedSnackbarEvent(SnackbarsLayout.Type type, int title, Object... args) {
|
||||
this.type = type;
|
||||
this.title = Santoku.INSTANCE.getString(title, args);
|
||||
}
|
||||
|
||||
public NeedSnackbarEvent tag(String tag) {
|
||||
this.tag = tag;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NeedSnackbarEvent button(int title, View.OnClickListener click) {
|
||||
this.buttonTitle = Santoku.INSTANCE.getString(title);
|
||||
this.buttonClick = click;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class ObjectsListChangedEvent {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class SelectedObjectChangedEvent {}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dark98.santoku.events;
|
||||
|
||||
public class SlicingProgressEvent {
|
||||
public final int progress;
|
||||
public final String message;
|
||||
|
||||
public SlicingProgressEvent(int progress, String message) {
|
||||
this.progress = progress;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
package com.dark98.santoku.fragment;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Process;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.google.android.material.navigation.NavigationBarView;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.SliceProgressBottomSheet;
|
||||
import com.dark98.santoku.components.UnfoldMenu;
|
||||
import com.dark98.santoku.components.bed_menu.BedMenu;
|
||||
import com.dark98.santoku.components.bed_menu.CameraMenu;
|
||||
import com.dark98.santoku.components.bed_menu.FileMenu;
|
||||
import com.dark98.santoku.components.bed_menu.OrientationMenu;
|
||||
import com.dark98.santoku.components.bed_menu.SliceMenu;
|
||||
import com.dark98.santoku.components.bed_menu.TransformMenu;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.events.FlattenModeResetEvent;
|
||||
import com.dark98.santoku.events.NeedDismissSnackbarEvent;
|
||||
import com.dark98.santoku.events.NeedSnackbarEvent;
|
||||
import com.dark98.santoku.events.SlicingProgressEvent;
|
||||
import com.dark98.santoku.navigation.Fragment;
|
||||
import com.dark98.santoku.slic3r.Bed3D;
|
||||
import com.dark98.santoku.slic3r.GCodeProcessorResult;
|
||||
import com.dark98.santoku.slic3r.GCodeThumbnailer;
|
||||
import com.dark98.santoku.slic3r.Model;
|
||||
import com.dark98.santoku.slic3r.Slic3rRuntimeError;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.BedSwipeDownLayout;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
import com.dark98.santoku.view.GLView;
|
||||
import com.dark98.santoku.view.SnackbarsLayout;
|
||||
import com.dark98.santoku.view.ThemeBottomNavigationView;
|
||||
import com.dark98.santoku.view.ThemeRailNavigationView;
|
||||
|
||||
public class BedFragment extends Fragment {
|
||||
private final static boolean DEBUG_VIEWER = false;
|
||||
private final static int MENU_SIZE_DP = 80;
|
||||
|
||||
private FrameLayout overlayLayout;
|
||||
private SnackbarsLayout snackbarsLayout;
|
||||
private GLView glView;
|
||||
private NavigationBarView navigationView;
|
||||
|
||||
private boolean isAnimatingMenu;
|
||||
private boolean isChangingByCode;
|
||||
private int currentMenuSlot;
|
||||
private FrameLayout menuView;
|
||||
private SparseArray<BedMenu> menuMap = new SparseArray<BedMenu>() {
|
||||
@Override
|
||||
public BedMenu get(int key) {
|
||||
BedMenu menu = super.get(key);
|
||||
if (menu == null) {
|
||||
switch (MenuCategory.values()[key]) {
|
||||
default:
|
||||
case FILE:
|
||||
menu = new FileMenu();
|
||||
break;
|
||||
case CAMERA:
|
||||
menu = new CameraMenu();
|
||||
break;
|
||||
case ORIENTATION:
|
||||
menu = new OrientationMenu();
|
||||
break;
|
||||
case TRANSFORM:
|
||||
menu = new TransformMenu();
|
||||
break;
|
||||
case SLICE_AND_EXPORT:
|
||||
menu = new SliceMenu();
|
||||
break;
|
||||
}
|
||||
|
||||
put(key, menu);
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
};
|
||||
|
||||
private View contentView;
|
||||
|
||||
private Model model;
|
||||
private GCodeProcessorResult gCodeResult;
|
||||
private UnfoldMenu currentUnfoldMenu;
|
||||
|
||||
private BedSwipeDownLayout swipeDownLayout;
|
||||
private boolean hasWebError;
|
||||
private WebView panelWebView;
|
||||
private LinearLayout panelWebViewError;
|
||||
private ImageView webViewErrIcon;
|
||||
private TextView webViewErrDescription;
|
||||
private ProgressBar webViewProgressBar;
|
||||
|
||||
private static String tempFileName;
|
||||
private static File tempExportingFile;
|
||||
|
||||
public static String getTempFileName() {
|
||||
return tempFileName;
|
||||
}
|
||||
|
||||
public static File getTempGCodePath() {
|
||||
return tempExportingFile != null ? tempExportingFile : new File(Santoku.INSTANCE.getCacheDir(), "temp.gcode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Santoku.EVENT_BUS.registerListener(this);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onNeedSnackbar(NeedSnackbarEvent e) {
|
||||
SnackbarsLayout.Snackbar s = new SnackbarsLayout.Snackbar(e.type, e.title);
|
||||
if (e.tag != null) {
|
||||
s.tag(e.tag);
|
||||
}
|
||||
if (e.buttonTitle != null) {
|
||||
s.lifetime = 0;
|
||||
s.buttonTitle = e.buttonTitle;
|
||||
s.buttonClick = e.buttonClick;
|
||||
}
|
||||
snackbarsLayout.show(s);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDismissSnackbar(NeedDismissSnackbarEvent e) {
|
||||
snackbarsLayout.dismiss(e.tag);
|
||||
}
|
||||
|
||||
public void showUnfoldMenu(UnfoldMenu menu, View from) {
|
||||
if (currentUnfoldMenu != null) return;
|
||||
|
||||
menu.setOnDismiss(()-> {
|
||||
if (menu.isAttached()) return;
|
||||
currentUnfoldMenu = null;
|
||||
});
|
||||
currentUnfoldMenu = menu;
|
||||
menu.show(from, this);
|
||||
}
|
||||
|
||||
public void loadGCode(File f) {
|
||||
gCodeResult = new GCodeProcessorResult(f);
|
||||
ViewUtils.postOnMainThread(()-> {
|
||||
glView.queueEvent(()->{
|
||||
glView.getRenderer().setGCodeViewer(gCodeResult);
|
||||
glView.requestRender();
|
||||
});
|
||||
|
||||
tempFileName = gCodeResult.getRecommendedName();
|
||||
tempExportingFile = f;
|
||||
|
||||
isChangingByCode = true;
|
||||
navigationView.setSelectedItemId(MenuCategory.SLICE_AND_EXPORT.ordinal());
|
||||
isChangingByCode = false;
|
||||
|
||||
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
|
||||
boolean portrait = dm.widthPixels < dm.heightPixels;
|
||||
selectMenu(getContext(), portrait, MenuCategory.SLICE_AND_EXPORT.ordinal());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (currentUnfoldMenu != null) {
|
||||
currentUnfoldMenu.dismiss();
|
||||
return true;
|
||||
}
|
||||
if (swipeDownLayout.onBackPressed()) {
|
||||
return true;
|
||||
}
|
||||
if (currentMenuSlot != 0) {
|
||||
navigationView.setSelectedItemId(0);
|
||||
return true;
|
||||
}
|
||||
return super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Santoku.EVENT_BUS.unregisterListener(this);
|
||||
|
||||
for (int i = 0; i < menuMap.size(); i++) {
|
||||
menuMap.valueAt(i).onViewDestroyed();
|
||||
}
|
||||
|
||||
if (!(getContext() instanceof Activity && ((Activity) getContext()).isChangingConfigurations())) {
|
||||
if (model != null) {
|
||||
model.release();
|
||||
model = null;
|
||||
}
|
||||
if (gCodeResult != null) {
|
||||
gCodeResult.release();
|
||||
gCodeResult = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
glView.onResume();
|
||||
ConfigObject cfg = Santoku.CONFIG.findPrinter(Santoku.CONFIG.presets.get("printer"));
|
||||
boolean enable = cfg != null && cfg.get("host_type") != null && !TextUtils.isEmpty(cfg.get("print_host")) && panelWebView != null;
|
||||
swipeDownLayout.setEnableTop(enable);
|
||||
if (enable) {
|
||||
String host = cfg.get("print_host");
|
||||
if (host.contains(":")) {
|
||||
try {
|
||||
int port = Integer.parseInt(host.split(":")[1]);
|
||||
if (port >= 7125 && port <= 7200) {
|
||||
host = host.split(":")[0];
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
if (!host.startsWith("http://")) {
|
||||
host = "http://" + host;
|
||||
}
|
||||
webViewProgressBar.animate().alpha(1).setDuration(150).start();
|
||||
panelWebView.setAlpha(0f);
|
||||
hasWebError = false;
|
||||
panelWebView.loadUrl(host);
|
||||
panelWebViewError.animate().alpha(0).setDuration(150).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
panelWebViewError.setVisibility(View.GONE);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
glView.onPause();
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
public View onCreateView(Context ctx) {
|
||||
glView = new GLView(ctx);
|
||||
glView.getRenderer().setModel(model);
|
||||
glView.getRenderer().setGCodeViewer(gCodeResult);
|
||||
overlayLayout = new FrameLayout(ctx) {
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
if (currentUnfoldMenu != null) {
|
||||
currentUnfoldMenu.relayout();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
|
||||
boolean portrait = dm.widthPixels < dm.heightPixels;
|
||||
|
||||
ll.setOrientation(portrait ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
|
||||
navigationView = null;
|
||||
constructMenuView(ctx, portrait);
|
||||
|
||||
if (!portrait) {
|
||||
ll.addView(navigationView = new ThemeRailNavigationView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewUtils.dp(1), ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
ll.addView(menuView, new LinearLayout.LayoutParams(ViewUtils.dp(MENU_SIZE_DP), ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
swipeDownLayout = new BedSwipeDownLayout(ctx);
|
||||
FrameLayout wfl = new FrameLayout(ctx);
|
||||
try {
|
||||
panelWebView = new WebView(ctx);
|
||||
panelWebView.getSettings().setJavaScriptEnabled(true);
|
||||
panelWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||
hasWebError = true;
|
||||
webViewErrDescription.setText(description);
|
||||
panelWebViewError.setVisibility(View.VISIBLE);
|
||||
panelWebViewError.setAlpha(0f);
|
||||
panelWebViewError.animate().alpha(1).setDuration(150).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
panelWebView.setVisibility(View.GONE);
|
||||
}
|
||||
}).start();
|
||||
webViewProgressBar.animate().alpha(0).setDuration(150).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
if (!hasWebError) {
|
||||
panelWebView.animate().alpha(1).setDuration(150).start();
|
||||
webViewProgressBar.animate().alpha(0).setDuration(150).start();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wfl.addView(panelWebView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
panelWebViewError = new LinearLayout(ctx);
|
||||
panelWebViewError.setVisibility(View.GONE);
|
||||
panelWebViewError.setOrientation(LinearLayout.VERTICAL);
|
||||
panelWebViewError.setGravity(Gravity.CENTER);
|
||||
panelWebViewError.setPadding(ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12));
|
||||
webViewErrIcon = new ImageView(ctx);
|
||||
webViewErrIcon.setImageResource(R.drawable.globe_cross_outline_28);
|
||||
panelWebViewError.addView(webViewErrIcon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
|
||||
bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
webViewErrDescription = new TextView(ctx);
|
||||
webViewErrDescription.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
webViewErrDescription.setGravity(Gravity.CENTER);
|
||||
panelWebViewError.addView(webViewErrDescription);
|
||||
wfl.addView(panelWebViewError, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
webViewProgressBar = new ProgressBar(ctx);
|
||||
webViewProgressBar.setAlpha(0f);
|
||||
wfl.addView(webViewProgressBar, new FrameLayout.LayoutParams(ViewUtils.dp(36), ViewUtils.dp(36), Gravity.CENTER));
|
||||
} catch (Exception e) {
|
||||
Log.wtf("BedFragment", "Failed to initialize webview", e);
|
||||
}
|
||||
|
||||
if (portrait) {
|
||||
LinearLayout inner = new LinearLayout(ctx);
|
||||
inner.setOrientation(LinearLayout.VERTICAL);
|
||||
ll = inner;
|
||||
|
||||
inner.addView(glView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
swipeDownLayout.addView(inner);
|
||||
swipeDownLayout.addView(wfl);
|
||||
} else {
|
||||
swipeDownLayout.addView(glView);
|
||||
swipeDownLayout.addView(wfl);
|
||||
ll.addView(swipeDownLayout, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
}
|
||||
|
||||
if (portrait) {
|
||||
ll.addView(menuView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(MENU_SIZE_DP)));
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1)));
|
||||
ll.addView(navigationView = new ThemeBottomNavigationView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
navigationView.setLabelVisibilityMode(NavigationBarView.LABEL_VISIBILITY_LABELED);
|
||||
for (MenuCategory cat : MenuCategory.values()) {
|
||||
navigationView.getMenu().add(0, cat.ordinal(), 0, cat.titleRes).setIcon(cat.iconRes);
|
||||
}
|
||||
navigationView.setSelectedItemId(currentMenuSlot);
|
||||
navigationView.setOnItemSelectedListener(item -> {
|
||||
if (currentMenuSlot == item.getItemId() || isChangingByCode) return true;
|
||||
if (isAnimatingMenu) return false;
|
||||
if (item.getItemId() == MenuCategory.SLICE_AND_EXPORT.ordinal()) {
|
||||
if (glView.getRenderer().getModel() == null && !DEBUG_VIEWER) {
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.SliceFailed)
|
||||
.setMessage(R.string.SliceFailedNoModels)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
tempExportingFile = null;
|
||||
File cfg = Santoku.getCurrentConfigFile();
|
||||
File gcode = getTempGCodePath();
|
||||
|
||||
if (!DEBUG_VIEWER) {
|
||||
new SliceProgressBottomSheet(ctx).show();
|
||||
}
|
||||
new Thread(()->{
|
||||
try {
|
||||
Process.setThreadPriority(-20);
|
||||
|
||||
try {
|
||||
Santoku.genCurrentConfig();
|
||||
} catch (Exception e) {
|
||||
Log.e("BedFragment", "Failed to write config", e);
|
||||
|
||||
ViewUtils.postOnMainThread(()->{
|
||||
Santoku.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, ""));
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.SliceFailed)
|
||||
.setMessage(e.getMessage())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
if (!DEBUG_VIEWER) {
|
||||
gCodeResult = glView.getRenderer().getModel().slice(cfg.getAbsolutePath(), gcode.getAbsolutePath(), (progress, text) -> Santoku.EVENT_BUS.fireEvent(new SlicingProgressEvent(progress, text)));
|
||||
GCodeThumbnailer.addThumbnailsToGcode(gcode, Santoku.buildCurrentConfigObject(), glView);
|
||||
Santoku.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, ""));
|
||||
} else {
|
||||
gCodeResult = new GCodeProcessorResult(gcode);
|
||||
GCodeThumbnailer.addThumbnailsToGcode(gcode, Santoku.buildCurrentConfigObject(), glView);
|
||||
}
|
||||
ViewUtils.postOnMainThread(()-> {
|
||||
glView.queueEvent(()->{
|
||||
glView.getRenderer().setGCodeViewer(gCodeResult);
|
||||
glView.requestRender();
|
||||
});
|
||||
|
||||
tempFileName = gCodeResult.getRecommendedName();
|
||||
tempExportingFile = null;
|
||||
|
||||
isChangingByCode = true;
|
||||
navigationView.setSelectedItemId(item.getItemId());
|
||||
isChangingByCode = false;
|
||||
selectMenu(ctx, portrait, item.getItemId());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("BedFragment", "Slice failed", e);
|
||||
ViewUtils.postOnMainThread(()->{
|
||||
Santoku.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, ""));
|
||||
new BeamAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.SliceFailed)
|
||||
.setMessage(e.getMessage())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
glView.queueEvent(()->{
|
||||
if (gCodeResult != null) {
|
||||
gCodeResult.release();
|
||||
gCodeResult = null;
|
||||
}
|
||||
|
||||
glView.getRenderer().setGCodeViewer(null);
|
||||
glView.requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
selectMenu(ctx, portrait, item.getItemId());
|
||||
return true;
|
||||
});
|
||||
|
||||
if (portrait) {
|
||||
overlayLayout.addView(contentView = swipeDownLayout);
|
||||
} else {
|
||||
overlayLayout.addView(contentView = ll);
|
||||
}
|
||||
overlayLayout.addView(snackbarsLayout = new SnackbarsLayout(ctx), new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) {{
|
||||
if (portrait) {
|
||||
bottomMargin = ViewUtils.dp(80 * 2);
|
||||
} else {
|
||||
leftMargin = ViewUtils.dp(80 * 2);
|
||||
}
|
||||
}});
|
||||
return overlayLayout;
|
||||
}
|
||||
|
||||
public SnackbarsLayout getSnackbarsLayout() {
|
||||
return snackbarsLayout;
|
||||
}
|
||||
|
||||
public FrameLayout getOverlayLayout() {
|
||||
return overlayLayout;
|
||||
}
|
||||
|
||||
private void selectMenu(Context ctx, boolean portrait, int slot) {
|
||||
if (glView.getRenderer().resetFlattenMode()) {
|
||||
glView.requestRender();
|
||||
Santoku.EVENT_BUS.fireEvent(new FlattenModeResetEvent());
|
||||
}
|
||||
isAnimatingMenu = true;
|
||||
|
||||
BedMenu prevMenu = menuMap.get(currentMenuSlot);
|
||||
boolean forward = slot > currentMenuSlot;
|
||||
currentMenuSlot = slot;
|
||||
|
||||
BedMenu currentMenu = menuMap.get(currentMenuSlot);
|
||||
if (currentMenu.getView() == null) {
|
||||
currentMenu.onSetBed(this);
|
||||
currentMenu.onViewCreated(currentMenu.onCreateView(ctx, portrait));
|
||||
}
|
||||
View v = currentMenu.getView();
|
||||
if (v.getParent() != null) {
|
||||
menuView.removeView(v);
|
||||
}
|
||||
menuView.addView(v, 0, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
Runnable next = ()->{
|
||||
if (portrait) {
|
||||
v.setTranslationX(v.getWidth() * (forward ? 1 : -1));
|
||||
} else {
|
||||
v.setTranslationY(v.getHeight() * (forward ? 1 : -1));
|
||||
}
|
||||
v.setAlpha(0f);
|
||||
|
||||
View prevView = prevMenu.getView();
|
||||
|
||||
new SpringAnimation(new FloatValueHolder(0))
|
||||
.setMinimumVisibleChange(1 / 256f)
|
||||
.setSpring(new SpringForce(1f)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener((animation, value, velocity) -> {
|
||||
prevView.setAlpha(1f - value);
|
||||
v.setAlpha(value);
|
||||
|
||||
if (portrait) {
|
||||
prevView.setTranslationX(-v.getWidth() * value * 0.5f * (forward ? 1 : -1));
|
||||
v.setTranslationX(v.getWidth() * (1f - value) * 0.5f * (forward ? 1 : -1));
|
||||
} else {
|
||||
prevView.setTranslationY(-prevView.getHeight() * value * 0.5f * (forward ? 1 : -1));
|
||||
v.setTranslationY(v.getHeight() * (1f - value) * 0.5f * (forward ? 1 : -1));
|
||||
}
|
||||
})
|
||||
.addEndListener((animation, canceled, value, velocity) -> {
|
||||
menuView.removeView(prevMenu.getView());
|
||||
isAnimatingMenu = false;
|
||||
})
|
||||
.start();
|
||||
};
|
||||
|
||||
if (!v.isLaidOut()) {
|
||||
v.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
v.removeOnLayoutChangeListener(this);
|
||||
next.run();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next.run();
|
||||
}
|
||||
}
|
||||
|
||||
public GLView getGlView() {
|
||||
return glView;
|
||||
}
|
||||
|
||||
public void loadModel(File f) throws Slic3rRuntimeError {
|
||||
Model m = new Model(f);
|
||||
if (model != null) {
|
||||
glView.queueEvent(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
if (bed == null) {
|
||||
ViewUtils.postOnMainThread(()-> glView.queueEvent(this));
|
||||
return;
|
||||
}
|
||||
Vec3d center = bed.getVolumeMin().center(bed.getVolumeMax());
|
||||
Vec3d objMin = new Vec3d(), objMax = new Vec3d();
|
||||
Vec3d objTranslate = new Vec3d();
|
||||
for (int i = 0; i < m.getObjectsCount(); i++) {
|
||||
m.getTranslation(i, objTranslate);
|
||||
m.getBoundingBoxExact(i, objMin, objMax);
|
||||
|
||||
m.translate(i, -objTranslate.x + center.x, -objTranslate.y + center.y, -objTranslate.z + (objMax.z - objMin.z) / 2);
|
||||
}
|
||||
|
||||
for (int i = 0; i < m.getObjectsCount(); i++) {
|
||||
model.addObject(m, i);
|
||||
}
|
||||
m.release();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
glView.queueEvent(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Bed3D bed = glView.getRenderer().getBed();
|
||||
if (bed == null) {
|
||||
ViewUtils.postOnMainThread(()-> glView.queueEvent(this));
|
||||
return;
|
||||
}
|
||||
glView.getRenderer().setModel(model = m);
|
||||
|
||||
Vec3d center = bed.getVolumeMin().center(bed.getVolumeMax());
|
||||
Vec3d objMin = new Vec3d(), objMax = new Vec3d();
|
||||
Vec3d objTranslate = new Vec3d();
|
||||
for (int i = 0; i < m.getObjectsCount(); i++) {
|
||||
m.getTranslation(i, objTranslate);
|
||||
m.getBoundingBoxExact(i, objMin, objMax);
|
||||
|
||||
m.translate(i, -objTranslate.x + center.x, -objTranslate.y + center.y, -objTranslate.z + (objMax.z - objMin.z) / 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
glView.requestRender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
super.onApplyTheme();
|
||||
|
||||
if (panelWebView != null) {
|
||||
webViewErrIcon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
webViewErrDescription.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
webViewProgressBar.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
}
|
||||
menuView.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
for (int i = 0; i < MenuCategory.values().length; i++) {
|
||||
if (i != currentMenuSlot) {
|
||||
ThemesRepo.invalidateView(menuMap.get(i).getView());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void constructMenuView(Context ctx, boolean portrait) {
|
||||
menuView = new FrameLayout(ctx);
|
||||
BedMenu currentMenu = menuMap.get(currentMenuSlot);
|
||||
if (currentMenu.getView() == null) {
|
||||
currentMenu.onSetBed(this);
|
||||
currentMenu.onViewCreated(currentMenu.onCreateView(ctx, portrait));
|
||||
}
|
||||
menuView.addView(currentMenu.getView(), new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
public void updateModel() {
|
||||
model = glView.getRenderer().getModel();
|
||||
}
|
||||
|
||||
public enum MenuCategory {
|
||||
FILE(R.string.MenuFile, R.drawable.folder_simple_outline_28),
|
||||
CAMERA(R.string.MenuCamera, R.drawable.camera_outline_28),
|
||||
ORIENTATION(R.string.MenuOrientation, R.drawable.menu_orientation_28),
|
||||
TRANSFORM(R.string.MenuTransform, R.drawable.menu_scale_28),
|
||||
// MODIFIERS(R.string.MenuModifiers, R.drawable.sliders_outline_28),
|
||||
SLICE_AND_EXPORT(R.string.MenuSlice, R.drawable.magic_wand_outline_28);
|
||||
|
||||
final int titleRes;
|
||||
final int iconRes;
|
||||
|
||||
MenuCategory(int titleRes, int iconRes) {
|
||||
this.titleRes = titleRes;
|
||||
this.iconRes = iconRes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package com.dark98.santoku.fragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.recycler.SpaceItem;
|
||||
import com.dark98.santoku.slic3r.PrintConfigDef;
|
||||
import com.dark98.santoku.slic3r.Slic3rLocalization;
|
||||
import com.dark98.santoku.slic3r.Slic3rUtils;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class FilamentConfigFragment extends ProfileListFragment {
|
||||
private List<ProfileListItem> compatItems;
|
||||
private String lastPrinter;
|
||||
private String lastPrint;
|
||||
private int lastUid;
|
||||
private ConfigObject currentConfig;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
onResetConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProfileListItem> getItems(boolean filter) {
|
||||
List<ConfigObject> list = Santoku.CONFIG.filamentConfigs;
|
||||
if (filter) {
|
||||
String printer = Santoku.CONFIG.presets.get("printer");
|
||||
String print = Santoku.CONFIG.presets.get("print");
|
||||
if (Objects.equals(lastPrinter, printer) && Objects.equals(lastPrint, print) && compatItems != null && lastUid == Santoku.CONFIG_UID) {
|
||||
return compatItems;
|
||||
}
|
||||
|
||||
List<ConfigObject> nList = new ArrayList<>(list.size());
|
||||
Slic3rUtils.ConfigChecker checker = new Slic3rUtils.ConfigChecker(Santoku.CONFIG.findPrinter(printer).serialize());
|
||||
if (Santoku.CONFIG.findPrint(print) != null) {
|
||||
Slic3rUtils.ConfigChecker printChecker = new Slic3rUtils.ConfigChecker(Santoku.CONFIG.findPrint(print).serialize());
|
||||
for (ConfigObject obj : list) {
|
||||
if (checker.checkCompatibility(obj.get("compatible_printers_condition")) && printChecker.checkCompatibility(obj.get("compatible_prints_condition"))) {
|
||||
nList.add(obj);
|
||||
}
|
||||
}
|
||||
printChecker.release();
|
||||
}
|
||||
checker.release();
|
||||
lastPrinter = printer;
|
||||
lastPrint = print;
|
||||
lastUid = Santoku.CONFIG_UID;
|
||||
return compatItems = (List) nList;
|
||||
}
|
||||
return (List) list;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<OptionElement> getConfigItems() {
|
||||
PrintConfigDef def = PrintConfigDef.getInstance();
|
||||
return Arrays.asList(
|
||||
new OptionElement(R.drawable.slot_filament_28, Slic3rLocalization.getString("Filament")),
|
||||
new OptionElement(new SubHeader("Filament")),
|
||||
new OptionElement(def.options.get("filament_colour")),
|
||||
new OptionElement(def.options.get("filament_diameter")),
|
||||
new OptionElement(def.options.get("extrusion_multiplier")),
|
||||
new OptionElement(def.options.get("filament_density")),
|
||||
new OptionElement(def.options.get("filament_cost")),
|
||||
new OptionElement(def.options.get("filament_spool_weight")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Temperature")),
|
||||
new OptionElement(def.options.get("idle_temperature")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Nozzle")),
|
||||
new OptionElement(def.options.get("first_layer_temperature")),
|
||||
new OptionElement(def.options.get("temperature")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Bed")),
|
||||
new OptionElement(def.options.get("first_layer_bed_temperature")),
|
||||
new OptionElement(def.options.get("bed_temperature")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Chamber")),
|
||||
new OptionElement(def.options.get("chamber_temperature")),
|
||||
new OptionElement(def.options.get("chamber_minimal_temperature")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.mode_fan_24, Slic3rLocalization.getString("Cooling")),
|
||||
new OptionElement(new SubHeader("Enable")),
|
||||
new OptionElement(def.options.get("fan_always_on")),
|
||||
new OptionElement(def.options.get("cooling")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Fan speed")),
|
||||
new OptionElement(def.options.get("min_fan_speed")),
|
||||
new OptionElement(def.options.get("max_fan_speed")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(def.options.get("bridge_fan_speed")),
|
||||
new OptionElement(def.options.get("disable_fan_first_layers")),
|
||||
new OptionElement(def.options.get("full_fan_speed_layer")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Dynamic fan speeds")),
|
||||
new OptionElement(def.options.get("enable_dynamic_fan_speeds")),
|
||||
new OptionElement(def.options.get("overhang_fan_speed_0")),
|
||||
new OptionElement(def.options.get("overhang_fan_speed_1")),
|
||||
new OptionElement(def.options.get("overhang_fan_speed_2")),
|
||||
new OptionElement(def.options.get("overhang_fan_speed_3")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Cooling thresholds")),
|
||||
new OptionElement(def.options.get("fan_below_layer_time")),
|
||||
new OptionElement(def.options.get("slowdown_below_layer_time")),
|
||||
new OptionElement(def.options.get("min_print_speed")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.settings_outline_28, Slic3rLocalization.getString("Advanced")),
|
||||
new OptionElement(new SubHeader("Filament properties")),
|
||||
new OptionElement(def.options.get("filament_type")),
|
||||
new OptionElement(def.options.get("filament_soluble")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Print speed override")),
|
||||
new OptionElement(def.options.get("filament_max_volumetric_speed")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(def.options.get("filament_infill_max_speed")),
|
||||
new OptionElement(def.options.get("filament_infill_max_crossing_speed")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Shrinkage compensation")),
|
||||
// TODO: Support x_size_compensation/y_size_compensation instead (I'm too lazy to write description for now)
|
||||
// new OptionElement(def.options.get("filament_shrinkage_compensation_x")),
|
||||
// new OptionElement(def.options.get("filament_shrinkage_compensation_y")),
|
||||
new OptionElement(def.options.get("filament_shrinkage_compensation_xy")),
|
||||
new OptionElement(def.options.get("filament_shrinkage_compensation_z")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Wipe tower parameters")),
|
||||
new OptionElement(def.options.get("filament_minimal_purge_on_wipe_tower")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Toolchange parameters with single extruder MM printers")),
|
||||
new OptionElement(def.options.get("filament_loading_speed_start")),
|
||||
new OptionElement(def.options.get("filament_loading_speed")),
|
||||
new OptionElement(def.options.get("filament_unloading_speed_start")),
|
||||
new OptionElement(def.options.get("filament_unloading_speed")),
|
||||
new OptionElement(def.options.get("filament_load_time")),
|
||||
new OptionElement(def.options.get("filament_unload_time")),
|
||||
new OptionElement(def.options.get("filament_toolchange_delay")),
|
||||
new OptionElement(def.options.get("filament_cooling_moves")),
|
||||
new OptionElement(def.options.get("filament_cooling_initial_speed")),
|
||||
new OptionElement(def.options.get("filament_cooling_final_speed")),
|
||||
new OptionElement(def.options.get("filament_stamping_loading_speed")),
|
||||
new OptionElement(def.options.get("filament_stamping_distance")),
|
||||
new OptionElement(def.options.get("filament_purge_multiplier")),
|
||||
new OptionElement(def.options.get("filament_ramming_parameters")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Toolchange parameters with multi extruder MM printers")),
|
||||
new OptionElement(def.options.get("filament_multitool_ramming")),
|
||||
new OptionElement(def.options.get("filament_multitool_ramming_volume")),
|
||||
new OptionElement(def.options.get("filament_multitool_ramming_flow")),
|
||||
|
||||
new OptionElement(R.drawable.settings_outline_28, Slic3rLocalization.getString("Filament Overrides")),
|
||||
new OptionElement(new SubHeader("Travel lift")),
|
||||
new OptionElement(def.options.get("filament_retract_lift")),
|
||||
new OptionElement(def.options.get("filament_travel_ramping_lift")),
|
||||
new OptionElement(def.options.get("filament_travel_max_lift")),
|
||||
new OptionElement(def.options.get("filament_travel_slope")),
|
||||
new OptionElement(def.options.get("filament_travel_lift_before_obstacle")),
|
||||
new OptionElement(def.options.get("filament_retract_lift_above")),
|
||||
new OptionElement(def.options.get("filament_retract_lift_below")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Retraction")),
|
||||
new OptionElement(def.options.get("filament_retract_length")),
|
||||
new OptionElement(def.options.get("filament_retract_speed")),
|
||||
new OptionElement(def.options.get("filament_deretract_speed")),
|
||||
new OptionElement(def.options.get("filament_retract_restart_extra")),
|
||||
new OptionElement(def.options.get("filament_retract_before_travel")),
|
||||
new OptionElement(def.options.get("filament_retract_layer_change")),
|
||||
new OptionElement(def.options.get("filament_wipe")),
|
||||
new OptionElement(def.options.get("filament_retract_before_wipe")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Retraction when tool is disabled")),
|
||||
new OptionElement(def.options.get("filament_retract_length_toolchange")),
|
||||
new OptionElement(def.options.get("filament_retract_restart_extra_toolchange")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.settings_outline_28, Slic3rLocalization.getString("Custom G-code")),
|
||||
new OptionElement(new SubHeader("Start G-code")),
|
||||
new OptionElement(def.options.get("start_filament_gcode")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("End G-code")),
|
||||
new OptionElement(def.options.get("end_filament_gcode")),
|
||||
|
||||
new OptionElement(R.drawable.note_pen_outline_96, Slic3rLocalization.getString("Notes")),
|
||||
new OptionElement(new SubHeader("Notes")),
|
||||
new OptionElement(def.options.get("filament_notes")),
|
||||
|
||||
new OptionElement(R.drawable.wrench_outline_28, Slic3rLocalization.getString("Dependencies")),
|
||||
new OptionElement(new SubHeader("Profile dependencies")),
|
||||
new OptionElement(def.options.get("compatible_printers_condition")),
|
||||
new OptionElement(def.options.get("compatible_prints")),
|
||||
new OptionElement(def.options.get("compatible_prints_condition"))
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cloneCurrentProfile() {
|
||||
ConfigObject obj = new ConfigObject(Santoku.INSTANCE.getString(R.string.SettingsProfileCopy, currentConfig.getTitle()));
|
||||
obj.values.putAll(currentConfig.values);
|
||||
currentConfig = new ConfigObject(obj);
|
||||
|
||||
Santoku.CONFIG.filamentConfigs.add(obj);
|
||||
Santoku.CONFIG.presets.put("filament", obj.getTitle());
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
|
||||
currentConfig = new ConfigObject(obj);
|
||||
dropdownView.setTitle(getCurrentConfig().getTitle());
|
||||
compatItems = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteCurrentProfile() {
|
||||
compatItems = null;
|
||||
Santoku.CONFIG.filamentConfigs.remove(Santoku.CONFIG.findFilament(currentConfig.getTitle()));
|
||||
selectItem(getItems(true).get(0));
|
||||
|
||||
dropdownView.setTitle(getCurrentConfig().getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplyConfig(String title) {
|
||||
compatItems = null;
|
||||
ConfigObject obj = Santoku.CONFIG.findFilament(currentConfig.getTitle());
|
||||
obj.setTitle(title);
|
||||
obj.values.putAll(currentConfig.values);
|
||||
currentConfig.setTitle(title);
|
||||
|
||||
Santoku.CONFIG.presets.put("filament", title);
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
|
||||
dropdownView.setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResetConfig() {
|
||||
currentConfig = new ConfigObject(Santoku.CONFIG.findFilament(Santoku.CONFIG.presets.get("filament")));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigObject getCurrentConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTitle() {
|
||||
return R.string.SlotFilamentConfigTooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void selectItem(ProfileListItem item) {
|
||||
currentConfig = new ConfigObject((ConfigObject) item);
|
||||
Santoku.CONFIG.presets.put("filament", item.getTitle());
|
||||
Santoku.saveConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package com.dark98.santoku.fragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.recycler.SpaceItem;
|
||||
import com.dark98.santoku.slic3r.PrintConfigDef;
|
||||
import com.dark98.santoku.slic3r.Slic3rUtils;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class PrintConfigFragment extends ProfileListFragment {
|
||||
private List<ProfileListItem> compatItems;
|
||||
private String lastPrinter;
|
||||
private int lastUid;
|
||||
|
||||
private ConfigObject currentConfig;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
onResetConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProfileListItem> getItems(boolean filter) {
|
||||
List<ConfigObject> list = Santoku.CONFIG.printConfigs;
|
||||
if (filter) {
|
||||
String printer = Santoku.CONFIG.presets.get("printer");
|
||||
if (Objects.equals(lastPrinter, printer) && compatItems != null && lastUid == Santoku.CONFIG_UID) {
|
||||
return compatItems;
|
||||
}
|
||||
|
||||
List<ConfigObject> nList = new ArrayList<>(list.size());
|
||||
Slic3rUtils.ConfigChecker checker = new Slic3rUtils.ConfigChecker(Santoku.CONFIG.findPrinter(printer).serialize());
|
||||
for (ConfigObject obj : list) {
|
||||
if (checker.checkCompatibility(obj.get("compatible_printers_condition"))) {
|
||||
nList.add(obj);
|
||||
}
|
||||
}
|
||||
checker.release();
|
||||
lastPrinter = printer;
|
||||
lastUid = Santoku.CONFIG_UID;
|
||||
return compatItems = (List) nList;
|
||||
}
|
||||
return (List) list;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<OptionElement> getConfigItems() {
|
||||
PrintConfigDef def = PrintConfigDef.getInstance();
|
||||
return Arrays.asList(
|
||||
new OptionElement(R.drawable.print_layers_28, "Layers and perimeters"),
|
||||
new OptionElement(new SubHeader("Layer height")),
|
||||
new OptionElement(def.options.get("layer_height")),
|
||||
new OptionElement(def.options.get("first_layer_height")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Vertical shells")),
|
||||
new OptionElement(def.options.get("perimeters")),
|
||||
new OptionElement(def.options.get("spiral_vase")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Horizontal shells")),
|
||||
new OptionElement(def.options.get("top_solid_layers")),
|
||||
new OptionElement(def.options.get("bottom_solid_layers")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Minimum shell thickness")),
|
||||
new OptionElement(def.options.get("top_solid_min_thickness")),
|
||||
new OptionElement(def.options.get("bottom_solid_min_thickness")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Quality (slower slicing)")),
|
||||
new OptionElement(def.options.get("extra_perimeters")),
|
||||
new OptionElement(def.options.get("extra_perimeters_on_overhangs")),
|
||||
new OptionElement(def.options.get("avoid_crossing_curled_overhangs")),
|
||||
new OptionElement(def.options.get("avoid_crossing_perimeters")),
|
||||
new OptionElement(def.options.get("avoid_crossing_perimeters_max_detour")),
|
||||
new OptionElement(def.options.get("thin_walls")),
|
||||
new OptionElement(def.options.get("thick_bridges")),
|
||||
new OptionElement(def.options.get("overhangs")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Advanced")),
|
||||
new OptionElement(def.options.get("seam_position")),
|
||||
new OptionElement(def.options.get("staggered_inner_seams")),
|
||||
new OptionElement(def.options.get("external_perimeters_first")),
|
||||
new OptionElement(def.options.get("gap_fill_enabled")),
|
||||
new OptionElement(def.options.get("perimeter_generator")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Fuzzy skin (experimental)")),
|
||||
new OptionElement(def.options.get("fuzzy_skin")),
|
||||
new OptionElement(def.options.get("fuzzy_skin_thickness")),
|
||||
new OptionElement(def.options.get("fuzzy_skin_point_dist")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Only one perimeter")),
|
||||
new OptionElement(def.options.get("top_one_perimeter_type")),
|
||||
new OptionElement(def.options.get("only_one_perimeter_first_layer")),
|
||||
|
||||
new OptionElement(R.drawable.print_infill_28, "Infill"),
|
||||
new OptionElement(new SubHeader("Infill")),
|
||||
new OptionElement(def.options.get("fill_density")),
|
||||
new OptionElement(def.options.get("fill_pattern")),
|
||||
new OptionElement(def.options.get("infill_anchor")),
|
||||
new OptionElement(def.options.get("infill_anchor_max")),
|
||||
new OptionElement(def.options.get("top_fill_pattern")),
|
||||
new OptionElement(def.options.get("bottom_fill_pattern")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Ironing")),
|
||||
new OptionElement(def.options.get("ironing")),
|
||||
new OptionElement(def.options.get("ironing_type")),
|
||||
new OptionElement(def.options.get("ironing_flowrate")),
|
||||
new OptionElement(def.options.get("ironing_spacing")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Reducing printing time")),
|
||||
new OptionElement(def.options.get("infill_every_layers")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Advanced")),
|
||||
new OptionElement(def.options.get("solid_infill_every_layers")),
|
||||
new OptionElement(def.options.get("fill_angle")),
|
||||
new OptionElement(def.options.get("solid_infill_below_area")),
|
||||
new OptionElement(def.options.get("bridge_angle")),
|
||||
new OptionElement(def.options.get("only_retract_when_crossing_perimeters")),
|
||||
new OptionElement(def.options.get("infill_first")),
|
||||
|
||||
new OptionElement(R.drawable.print_skirt_28, "Skirt and brim"),
|
||||
new OptionElement(new SubHeader("Skirt")),
|
||||
new OptionElement(def.options.get("skirts")),
|
||||
new OptionElement(def.options.get("skirt_distance")),
|
||||
new OptionElement(def.options.get("skirt_height")),
|
||||
new OptionElement(def.options.get("draft_shield")),
|
||||
new OptionElement(def.options.get("min_skirt_length")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Brim")),
|
||||
new OptionElement(def.options.get("brim_type")),
|
||||
new OptionElement(def.options.get("brim_width")),
|
||||
new OptionElement(def.options.get("brim_separation")),
|
||||
|
||||
new OptionElement(R.drawable.print_support_28, "Support material"),
|
||||
new OptionElement(new SubHeader("Support material")),
|
||||
new OptionElement(def.options.get("support_material")),
|
||||
new OptionElement(def.options.get("support_material_auto")),
|
||||
new OptionElement(def.options.get("support_material_threshold")),
|
||||
new OptionElement(def.options.get("support_material_enforce_layers")),
|
||||
new OptionElement(def.options.get("raft_first_layer_density")),
|
||||
new OptionElement(def.options.get("raft_first_layer_expansion")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Raft")),
|
||||
new OptionElement(def.options.get("raft_layers")),
|
||||
new OptionElement(def.options.get("raft_contact_distance")),
|
||||
new OptionElement(def.options.get("raft_expansion")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Options for support material and raft")),
|
||||
new OptionElement(def.options.get("support_material_style")),
|
||||
new OptionElement(def.options.get("support_material_contact_distance")),
|
||||
new OptionElement(def.options.get("support_material_bottom_contact_distance")),
|
||||
new OptionElement(def.options.get("support_material_pattern")),
|
||||
new OptionElement(def.options.get("support_material_with_sheath")),
|
||||
new OptionElement(def.options.get("support_material_spacing")),
|
||||
new OptionElement(def.options.get("support_material_angle")),
|
||||
new OptionElement(def.options.get("support_material_closing_radius")),
|
||||
new OptionElement(def.options.get("support_material_interface_layers")),
|
||||
new OptionElement(def.options.get("support_material_bottom_interface_layers")),
|
||||
new OptionElement(def.options.get("support_material_interface_pattern")),
|
||||
new OptionElement(def.options.get("support_material_interface_spacing")),
|
||||
new OptionElement(def.options.get("support_material_interface_contact_loops")),
|
||||
new OptionElement(def.options.get("support_material_buildplate_only")),
|
||||
new OptionElement(def.options.get("support_material_xy_spacing")),
|
||||
new OptionElement(def.options.get("dont_support_bridges")),
|
||||
new OptionElement(def.options.get("support_material_synchronize_layers")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Organic supports")),
|
||||
new OptionElement(def.options.get("support_tree_angle")),
|
||||
new OptionElement(def.options.get("support_tree_angle_slow")),
|
||||
new OptionElement(def.options.get("support_tree_branch_diameter")),
|
||||
new OptionElement(def.options.get("support_tree_branch_diameter_angle")),
|
||||
new OptionElement(def.options.get("support_tree_branch_diameter_double_wall")),
|
||||
new OptionElement(def.options.get("support_tree_tip_diameter")),
|
||||
new OptionElement(def.options.get("support_tree_branch_distance")),
|
||||
new OptionElement(def.options.get("support_tree_top_rate")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.clock_outline_28, "Speed"),
|
||||
new OptionElement(new SubHeader("Speed for print moves")),
|
||||
new OptionElement(def.options.get("perimeter_speed")),
|
||||
new OptionElement(def.options.get("small_perimeter_speed")),
|
||||
new OptionElement(def.options.get("external_perimeter_speed")),
|
||||
new OptionElement(def.options.get("infill_speed")),
|
||||
new OptionElement(def.options.get("solid_infill_speed")),
|
||||
new OptionElement(def.options.get("top_solid_infill_speed")),
|
||||
new OptionElement(def.options.get("support_material_speed")),
|
||||
new OptionElement(def.options.get("support_material_interface_speed")),
|
||||
new OptionElement(def.options.get("bridge_speed")),
|
||||
new OptionElement(def.options.get("gap_fill_speed")),
|
||||
new OptionElement(def.options.get("ironing_speed")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Dynamic overhang speed")),
|
||||
new OptionElement(def.options.get("overhang_speed_0")),
|
||||
new OptionElement(def.options.get("overhang_speed_1")),
|
||||
new OptionElement(def.options.get("overhang_speed_2")),
|
||||
new OptionElement(def.options.get("overhang_speed_3")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Speed for non-print moves")),
|
||||
new OptionElement(def.options.get("travel_speed")),
|
||||
new OptionElement(def.options.get("travel_speed_z")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Modifiers")),
|
||||
new OptionElement(def.options.get("first_layer_speed")),
|
||||
new OptionElement(def.options.get("first_layer_speed_over_raft")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Acceleration control (advanced)")),
|
||||
new OptionElement(def.options.get("external_perimeter_acceleration")),
|
||||
new OptionElement(def.options.get("top_solid_infill_acceleration")),
|
||||
new OptionElement(def.options.get("solid_infill_acceleration")),
|
||||
new OptionElement(def.options.get("infill_acceleration")),
|
||||
new OptionElement(def.options.get("bridge_acceleration")),
|
||||
new OptionElement(def.options.get("first_layer_acceleration")),
|
||||
new OptionElement(def.options.get("first_layer_acceleration_over_raft")),
|
||||
new OptionElement(def.options.get("wipe_tower_acceleration")),
|
||||
new OptionElement(def.options.get("travel_acceleration")),
|
||||
new OptionElement(def.options.get("default_acceleration")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Autospeed (advanced)")),
|
||||
new OptionElement(def.options.get("max_print_speed")),
|
||||
new OptionElement(def.options.get("max_volumetric_speed")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Pressure equalizer (experimental)")),
|
||||
new OptionElement(def.options.get("max_volumetric_extrusion_rate_slope_positive")),
|
||||
new OptionElement(def.options.get("max_volumetric_extrusion_rate_slope_negative")),
|
||||
|
||||
new OptionElement(R.drawable.hashtag_outline_28, "Multiple Extruders"),
|
||||
new OptionElement(new SubHeader("Extruders")),
|
||||
new OptionElement(def.options.get("perimeter_extruder")),
|
||||
new OptionElement(def.options.get("infill_extruder")),
|
||||
new OptionElement(def.options.get("solid_infill_extruder")),
|
||||
new OptionElement(def.options.get("support_material_extruder")),
|
||||
new OptionElement(def.options.get("support_material_interface_extruder")),
|
||||
new OptionElement(def.options.get("wipe_tower_extruder")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Ooze prevention")),
|
||||
new OptionElement(def.options.get("ooze_prevention")),
|
||||
new OptionElement(def.options.get("standby_temperature_delta")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Wipe tower")),
|
||||
new OptionElement(def.options.get("wipe_tower")),
|
||||
new OptionElement(def.options.get("wipe_tower_x")),
|
||||
new OptionElement(def.options.get("wipe_tower_y")),
|
||||
new OptionElement(def.options.get("wipe_tower_width")),
|
||||
new OptionElement(def.options.get("wipe_tower_rotation_angle")),
|
||||
new OptionElement(def.options.get("wipe_tower_brim_width")),
|
||||
new OptionElement(def.options.get("wipe_tower_bridging")),
|
||||
new OptionElement(def.options.get("wipe_tower_cone_angle")),
|
||||
new OptionElement(def.options.get("wipe_tower_extra_spacing")),
|
||||
new OptionElement(def.options.get("wipe_tower_extra_flow")),
|
||||
new OptionElement(def.options.get("wipe_tower_no_sparse_layers")),
|
||||
new OptionElement(def.options.get("single_extruder_multi_material_priming")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Advanced")),
|
||||
new OptionElement(def.options.get("interface_shells")),
|
||||
new OptionElement(def.options.get("mmu_segmented_region_max_width")),
|
||||
new OptionElement(def.options.get("mmu_segmented_region_interlocking_depth")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.settings_outline_28, "Advanced"),
|
||||
new OptionElement(new SubHeader("Extrusion width")),
|
||||
new OptionElement(def.options.get("extrusion_width")),
|
||||
new OptionElement(def.options.get("first_layer_extrusion_width")),
|
||||
new OptionElement(def.options.get("perimeter_extrusion_width")),
|
||||
new OptionElement(def.options.get("external_perimeter_extrusion_width")),
|
||||
new OptionElement(def.options.get("infill_extrusion_width")),
|
||||
new OptionElement(def.options.get("solid_infill_extrusion_width")),
|
||||
new OptionElement(def.options.get("top_infill_extrusion_width")),
|
||||
new OptionElement(def.options.get("support_material_extrusion_width")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Overlap")),
|
||||
new OptionElement(def.options.get("infill_overlap")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Flow")),
|
||||
new OptionElement(def.options.get("bridge_flow_ratio")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Slicing")),
|
||||
new OptionElement(def.options.get("slice_closing_radius")),
|
||||
new OptionElement(def.options.get("slicing_mode")),
|
||||
new OptionElement(def.options.get("resolution")),
|
||||
new OptionElement(def.options.get("gcode_resolution")),
|
||||
new OptionElement(def.options.get("arc_fitting")),
|
||||
// TODO: Support x_size_compensation/y_size_compensation instead (I'm too lazy to write description for now)
|
||||
// new OptionElement(def.options.get("x_size_compensation")),
|
||||
// new OptionElement(def.options.get("y_size_compensation")),
|
||||
new OptionElement(def.options.get("xy_size_compensation")),
|
||||
new OptionElement(def.options.get("elefant_foot_compensation")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Arachne perimeter generator")),
|
||||
new OptionElement(def.options.get("wall_transition_angle")),
|
||||
new OptionElement(def.options.get("wall_transition_filter_deviation")),
|
||||
new OptionElement(def.options.get("wall_transition_length")),
|
||||
new OptionElement(def.options.get("wall_distribution_count")),
|
||||
new OptionElement(def.options.get("min_bead_width")),
|
||||
new OptionElement(def.options.get("min_feature_size")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.door_arrow_right_outline_28, "Output options"),
|
||||
new OptionElement(new SubHeader("Sequential printing")),
|
||||
new OptionElement(def.options.get("complete_objects")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Extruder clearance")),
|
||||
new OptionElement(def.options.get("extruder_clearance_radius")),
|
||||
new OptionElement(def.options.get("extruder_clearance_height")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Output file")),
|
||||
new OptionElement(def.options.get("gcode_comments")),
|
||||
new OptionElement(def.options.get("gcode_label_objects")),
|
||||
new OptionElement(def.options.get("output_filename_format")),
|
||||
|
||||
new OptionElement(R.drawable.note_pen_outline_96, "Notes"),
|
||||
new OptionElement(new SubHeader("Notes")),
|
||||
new OptionElement(def.options.get("notes")),
|
||||
|
||||
new OptionElement(R.drawable.wrench_outline_28, "Dependencies"),
|
||||
new OptionElement(new SubHeader("Profile dependencies")),
|
||||
new OptionElement(def.options.get("compatible_printers")),
|
||||
new OptionElement(def.options.get("compatible_printers_condition"))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cloneCurrentProfile() {
|
||||
ConfigObject obj = new ConfigObject(Santoku.INSTANCE.getString(R.string.SettingsProfileCopy, currentConfig.getTitle()));
|
||||
obj.values.putAll(currentConfig.values);
|
||||
currentConfig = new ConfigObject(obj);
|
||||
|
||||
Santoku.CONFIG.printConfigs.add(obj);
|
||||
Santoku.CONFIG.presets.put("print", obj.getTitle());
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
|
||||
currentConfig = new ConfigObject(obj);
|
||||
dropdownView.setTitle(getCurrentConfig().getTitle());
|
||||
compatItems = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteCurrentProfile() {
|
||||
compatItems = null;
|
||||
Santoku.CONFIG.printConfigs.remove(Santoku.CONFIG.findPrint(currentConfig.getTitle()));
|
||||
selectItem(getItems(true).get(0));
|
||||
|
||||
dropdownView.setTitle(getCurrentConfig().getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplyConfig(String title) {
|
||||
compatItems = null;
|
||||
ConfigObject obj = Santoku.CONFIG.findPrint(currentConfig.getTitle());
|
||||
obj.setTitle(title);
|
||||
obj.values.putAll(currentConfig.values);
|
||||
currentConfig.setTitle(title);
|
||||
|
||||
Santoku.CONFIG.presets.put("print", title);
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
|
||||
dropdownView.setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResetConfig() {
|
||||
ConfigObject print = Santoku.CONFIG.findPrint(Santoku.CONFIG.presets.get("print"));
|
||||
if (print != null) {
|
||||
currentConfig = new ConfigObject(print);
|
||||
} else {
|
||||
currentConfig = new ConfigObject(Santoku.INSTANCE.getString(R.string.IntroCustomProfileName));
|
||||
Santoku.CONFIG.printConfigs.add(new ConfigObject(currentConfig));
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigObject getCurrentConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTitle() {
|
||||
return R.string.SlotPrintConfigTooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void selectItem(ProfileListItem item) {
|
||||
currentConfig = new ConfigObject((ConfigObject) item);
|
||||
Santoku.CONFIG.presets.put("print", item.getTitle());
|
||||
Santoku.saveConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.dark98.santoku.fragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.recycler.SpaceItem;
|
||||
import com.dark98.santoku.slic3r.PrintConfigDef;
|
||||
import com.dark98.santoku.slic3r.ConfigOptionDef;
|
||||
import com.dark98.santoku.slic3r.Slic3rLocalization;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class PrinterConfigFragment extends ProfileListFragment {
|
||||
private ConfigObject currentConfig;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
onResetConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProfileListItem> getItems(boolean filter) {
|
||||
return (List) Santoku.CONFIG.printerConfigs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<OptionElement> getConfigItems() {
|
||||
PrintConfigDef def = PrintConfigDef.getInstance();
|
||||
ArrayList<OptionElement> list = new ArrayList<>(Arrays.asList(
|
||||
new OptionElement(R.drawable.printer_outline_28, "General"),
|
||||
new OptionElement(new SubHeader("Size and coordinates")),
|
||||
new OptionElement(def.options.get("bed_shape")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(def.options.get("max_print_height")),
|
||||
new OptionElement(def.options.get("z_offset")),
|
||||
|
||||
new OptionElement(new SubHeader("Capabilities")),
|
||||
// TODO: Extruders setting
|
||||
new OptionElement(def.options.get("single_extruder_multi_material")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Firmware")),
|
||||
new OptionElement(def.options.get("gcode_flavor")),
|
||||
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")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Advanced")),
|
||||
new OptionElement(def.options.get("use_relative_e_distances")),
|
||||
new OptionElement(def.options.get("use_firmware_retraction")),
|
||||
new OptionElement(def.options.get("use_volumetric_e")),
|
||||
new OptionElement(def.options.get("variable_layer_height")),
|
||||
new OptionElement(def.options.get("prefer_clockwise_movements")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(R.drawable.settings_outline_28, "Custom G-code"),
|
||||
new OptionElement(new SubHeader("Start G-code")),
|
||||
new OptionElement(def.options.get("start_gcode")),
|
||||
new OptionElement(def.options.get("autoemit_temperature_commands")),
|
||||
|
||||
new OptionElement(new SubHeader("End G-code")),
|
||||
new OptionElement(def.options.get("end_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("Before layer change G-code")),
|
||||
new OptionElement(def.options.get("before_layer_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("After layer change G-code")),
|
||||
new OptionElement(def.options.get("layer_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("Tool change G-code")),
|
||||
new OptionElement(def.options.get("toolchange_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("Between objects G-code (for sequential printing)")),
|
||||
new OptionElement(def.options.get("between_objects_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("Color Change G-code")),
|
||||
new OptionElement(def.options.get("color_change_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("Pause Print G-code")),
|
||||
new OptionElement(def.options.get("pause_print_gcode")),
|
||||
|
||||
new OptionElement(new SubHeader("Template Custom G-code")),
|
||||
new OptionElement(def.options.get("template_custom_gcode")),
|
||||
|
||||
new OptionElement(R.drawable.note_pen_outline_96, "Machine limits"),
|
||||
new OptionElement(new SubHeader("General")),
|
||||
new OptionElement(def.options.get("machine_limits_usage")),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Maximum feedrates")),
|
||||
new OptionElement(def.options.get("machine_max_acceleration_x")),
|
||||
new OptionElement(def.options.get("machine_max_acceleration_y")),
|
||||
new OptionElement(def.options.get("machine_max_acceleration_z")),
|
||||
new OptionElement(def.options.get("machine_max_acceleration_e")),
|
||||
new OptionElement(def.options.get("machine_max_acceleration_extruding")),
|
||||
new OptionElement(def.options.get("machine_max_acceleration_retracting")),
|
||||
// TODO: m_supports_travel_acceleration? new OptionElement(def.options.get("machine_max_acceleration_travel")) <= repetier/reprap/marlin
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Jerk limits")),
|
||||
new OptionElement(def.options.get("machine_max_jerk_x")),
|
||||
new OptionElement(def.options.get("machine_max_jerk_y")),
|
||||
new OptionElement(def.options.get("machine_max_jerk_z")),
|
||||
new OptionElement(def.options.get("machine_max_jerk_e"))
|
||||
|
||||
// TODO: m_supports_min_feedrates? <= marlin/marlin legacy
|
||||
));
|
||||
|
||||
int count = currentConfig.getExtruderCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
int j = count == 1 ? -1 : i;
|
||||
list.addAll(Arrays.asList(
|
||||
new OptionElement(R.drawable.hashtag_outline_28, String.format(Slic3rLocalization.getString("Extruder %d"), i + 1)),
|
||||
new OptionElement(new SubHeader("Size")),
|
||||
new OptionElement(def.options.get("nozzle_diameter"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Preview")),
|
||||
new OptionElement(def.options.get("extruder_colour"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Layer height limits")),
|
||||
new OptionElement(def.options.get("min_layer_height"), j),
|
||||
new OptionElement(def.options.get("max_layer_height"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Position (for multi-extruder printers)")),
|
||||
new OptionElement(def.options.get("extruder_offset"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Travel lift")),
|
||||
new OptionElement(def.options.get("retract_lift"), j),
|
||||
new OptionElement(def.options.get("travel_ramping_lift"), j),
|
||||
new OptionElement(def.options.get("travel_max_lift"), j),
|
||||
new OptionElement(def.options.get("travel_slope"), j),
|
||||
new OptionElement(def.options.get("travel_lift_before_obstacle"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Only lift")),
|
||||
new OptionElement(def.options.get("retract_lift_above"), j),
|
||||
new OptionElement(def.options.get("retract_lift_below"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Retraction")),
|
||||
new OptionElement(def.options.get("retract_length"), j),
|
||||
new OptionElement(def.options.get("retract_speed"), j),
|
||||
new OptionElement(def.options.get("deretract_speed"), j),
|
||||
new OptionElement(def.options.get("retract_restart_extra"), j),
|
||||
new OptionElement(def.options.get("retract_before_travel"), j),
|
||||
new OptionElement(def.options.get("retract_layer_change"), j),
|
||||
new OptionElement(def.options.get("wipe"), j),
|
||||
new OptionElement(def.options.get("retract_before_wipe"), j),
|
||||
new OptionElement(new SpaceItem(0, ViewUtils.dp(4))),
|
||||
|
||||
new OptionElement(new SubHeader("Retraction when tool is disabled (advanced settings for multi-extruder setups)")),
|
||||
new OptionElement(def.options.get("retract_length_toolchange"), j),
|
||||
new OptionElement(def.options.get("retract_restart_extra_toolchange"), j)
|
||||
));
|
||||
}
|
||||
list.addAll(Arrays.asList(
|
||||
new OptionElement(R.drawable.settings_outline_28, "Advanced"),
|
||||
new OptionElement(new SubHeader("Auto arrange")),
|
||||
new OptionElement(def.options.get("auto_arrange_bed_clearance")),
|
||||
new OptionElement(def.options.get("auto_arrange_rotate")),
|
||||
|
||||
new OptionElement(R.drawable.note_pen_outline_96, "Notes"),
|
||||
new OptionElement(new SubHeader("Notes")),
|
||||
new OptionElement(def.options.get("printer_notes")),
|
||||
|
||||
new OptionElement(R.drawable.power_socket_outline_28, "Physical Printer"),
|
||||
new OptionElement(new SubHeader("Print Host upload")),
|
||||
new OptionElement(def.options.get("host_type")),
|
||||
new OptionElement(def.options.get("print_host")),
|
||||
new OptionElement(def.options.get("printhost_apikey"))
|
||||
));
|
||||
|
||||
String hostType = null;
|
||||
if (diffObject != null && diffObject.has("host_type")) {
|
||||
hostType = diffObject.get("host_type");
|
||||
}
|
||||
if (hostType == null) {
|
||||
hostType = currentConfig.get("host_type");
|
||||
}
|
||||
if ("elegoolink".equalsIgnoreCase(hostType)) {
|
||||
list.addAll(Arrays.asList(
|
||||
new OptionElement(new SubHeader("ElegooLink")),
|
||||
new OptionElement(def.options.get("elegoolink_timelapse")),
|
||||
new OptionElement(def.options.get("elegoolink_bed_leveling")),
|
||||
new OptionElement(def.options.get("elegoolink_bed_type"))
|
||||
));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cloneCurrentProfile() {
|
||||
ConfigObject obj = new ConfigObject(Santoku.INSTANCE.getString(R.string.SettingsProfileCopy, currentConfig.getTitle()));
|
||||
obj.values.putAll(currentConfig.values);
|
||||
currentConfig = new ConfigObject(obj);
|
||||
|
||||
Santoku.CONFIG.printerConfigs.add(obj);
|
||||
Santoku.CONFIG.presets.put("printer", obj.getTitle());
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
|
||||
currentConfig = new ConfigObject(obj);
|
||||
dropdownView.setTitle(getCurrentConfig().getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteCurrentProfile() {
|
||||
Santoku.CONFIG.printerConfigs.remove(Santoku.CONFIG.findPrinter(currentConfig.getTitle()));
|
||||
selectItem(getItems(true).get(0));
|
||||
dropdownView.setTitle(getCurrentConfig().getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplyConfig(String title) {
|
||||
ConfigObject obj = Santoku.CONFIG.findPrinter(currentConfig.getTitle());
|
||||
obj.setTitle(title);
|
||||
obj.values.putAll(currentConfig.values);
|
||||
currentConfig.setTitle(title);
|
||||
|
||||
Santoku.CONFIG.presets.put("printer", title);
|
||||
Santoku.saveConfig();
|
||||
Santoku.getCurrentConfigFile().delete();
|
||||
|
||||
dropdownView.setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResetConfig() {
|
||||
currentConfig = new ConfigObject(Santoku.CONFIG.findPrinter(Santoku.CONFIG.presets.get("printer")));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigObject getCurrentConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTitle() {
|
||||
return R.string.SlotPrinterConfigTooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void selectItem(ProfileListItem item) {
|
||||
currentConfig = new ConfigObject((ConfigObject) item);
|
||||
Santoku.CONFIG.presets.put("printer", item.getTitle());
|
||||
|
||||
// TODO: Reset print/filament profiles, maybe physical profiles?
|
||||
Santoku.saveConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateConfigField(ConfigOptionDef def, int i, String value) {
|
||||
super.updateConfigField(def, i, value);
|
||||
if ("host_type".equals(def.key)) {
|
||||
onUpdateConfigItems();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateConfigItems() {
|
||||
setConfigItems(getConfigItems());
|
||||
super.onUpdateConfigItems();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,221 @@
|
||||
package com.dark98.santoku.fragment;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.mrudultora.colorpicker.ColorPickerPopUp;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import com.dark98.santoku.BeamServerData;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.SetupActivity;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.components.BeamAlertDialogBuilder;
|
||||
import com.dark98.santoku.components.BeamColorPickerPopUp;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.events.BeamServerDataUpdatedEvent;
|
||||
import com.dark98.santoku.events.CloudUserInfoUpdatedEvent;
|
||||
import com.dark98.santoku.recycler.PreferenceItem;
|
||||
import com.dark98.santoku.theme.BeamTheme;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public class SettingsFragment extends ProfileListFragment {
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v) {
|
||||
super.onViewCreated(v);
|
||||
dropdownView.setVisibility(View.GONE);
|
||||
((View) saveButton.getParent()).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<OptionElement> getConfigItems() {
|
||||
return Arrays.asList(
|
||||
BeamServerData.isCloudAvailable() ? new OptionElement(SPECIAL_TYPE_CLOUD_HEADER).setOnClick(() -> {
|
||||
Activity act = (Activity) getContext();
|
||||
act.startActivity(new Intent(act, SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true));
|
||||
}) : null,
|
||||
new OptionElement(R.drawable.paint_roller_outline_28, getContext().getString(R.string.SettingsInterface)),
|
||||
new OptionElement(new PreferenceItem().setTitle(getContext().getString(R.string.SettingsInterfaceTheme)).setValueProvider(() -> getContext().getString(Prefs.getThemeMode().title)).setOnClickListener(v -> {
|
||||
String[] items = new String[Prefs.ThemeMode.values().length];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = getContext().getString(Prefs.ThemeMode.values()[i].title);
|
||||
}
|
||||
new BeamAlertDialogBuilder(getContext())
|
||||
.setTitle(R.string.SettingsInterfaceTheme)
|
||||
.setSingleChoiceItems(items, Prefs.getThemeMode().ordinal(), (dialog, which) -> {
|
||||
boolean activity = getContext() instanceof Activity;
|
||||
if (activity) {
|
||||
Activity act = (Activity) getContext();
|
||||
ViewGroup decorView = (ViewGroup) act.getWindow().getDecorView();
|
||||
Bitmap bm = Bitmap.createBitmap(decorView.getWidth(), decorView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bm);
|
||||
decorView.draw(c);
|
||||
|
||||
ImageView overlay = new ImageView(act);
|
||||
overlay.setImageBitmap(bm);
|
||||
decorView.addView(overlay);
|
||||
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(1, 0).setDuration(250);
|
||||
anim.addUpdateListener(animation -> overlay.setAlpha((float) animation.getAnimatedValue()));
|
||||
anim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
decorView.removeView(overlay);
|
||||
bm.recycle();
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
}
|
||||
Prefs.setThemeMode(which);
|
||||
if (activity) {
|
||||
ThemesRepo.invalidate((Activity) getContext());
|
||||
}
|
||||
dialog.dismiss();
|
||||
})
|
||||
.show();
|
||||
})),
|
||||
new OptionElement(new PreferenceItem().setTitle(getContext().getString(R.string.SettingsInterfaceColor)).setValueProvider(() -> "#" + String.format("%08X", Prefs.getAccentColor())).setOnClickListener(v -> {
|
||||
new BeamColorPickerPopUp(getContext())
|
||||
.setDialogTitle(getContext().getString(R.string.SettingsInterfaceColor))
|
||||
.setDefaultColor(Prefs.getAccentColor())
|
||||
.setShowAlpha(false)
|
||||
.setOnPickColorListener(new ColorPickerPopUp.OnPickColorListener() {
|
||||
@Override
|
||||
public void onColorPicked(int color) {
|
||||
Prefs.setAccentColor(color);
|
||||
onChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
Prefs.setAccentColor(SetupActivity.AccentColors.DEFAULT.color);
|
||||
onChanged();
|
||||
}
|
||||
|
||||
void onChanged() {
|
||||
BeamTheme.LIGHT.colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
|
||||
BeamTheme.DARK.colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
|
||||
ThemesRepo.invalidate((Activity) getContext());
|
||||
recyclerView.getAdapter().notifyItemChanged(2 - (BeamServerData.isCloudAvailable() ? 0 : 1));
|
||||
}
|
||||
})
|
||||
.setNegativeButtonText(getContext().getString(R.string.SettingsInterfaceColorReset))
|
||||
.show();
|
||||
})),
|
||||
new OptionElement(new PreferenceItem().setTitle(getContext().getString(R.string.SettingsInterfaceResolutionScale)).setSubtitle(getContext().getString(R.string.SettingsInterfaceResolutionScaleDescription)).setValueProvider(() -> (int) (Prefs.getRenderScale() * 100) + "%").setOnClickListener(v -> {
|
||||
float[] variants = {1, 0.9f, 0.8f, 0.7f, 0.6f, 0.5f};
|
||||
String[] items = new String[variants.length];
|
||||
int j = 0;
|
||||
for (int i = 0; i < variants.length; i++) {
|
||||
items[i] = (int) (variants[i] * 100) + "%";
|
||||
if (variants[i] == Prefs.getRenderScale()) {
|
||||
j = i;
|
||||
}
|
||||
}
|
||||
|
||||
new BeamAlertDialogBuilder(getContext())
|
||||
.setTitle(R.string.SettingsInterfaceResolutionScale)
|
||||
.setSingleChoiceItems(items, j, (dialog, which) -> {
|
||||
Prefs.setRenderScale(variants[which]);
|
||||
dialog.dismiss();
|
||||
// I'm too lazy to calculate real position for now
|
||||
recyclerView.getAdapter().notifyItemChanged(4 - (BeamServerData.isCloudAvailable() ? 0 : 1));
|
||||
})
|
||||
.show();
|
||||
})),
|
||||
new OptionElement(R.drawable.info_outline_28, getContext().getString(R.string.SettingsAbout)).setOnClick(() -> {
|
||||
Activity act = (Activity) getContext();
|
||||
act.startActivity(new Intent(act, SetupActivity.class).putExtra(SetupActivity.EXTRA_ABOUT, true));
|
||||
}),
|
||||
new OptionElement(R.drawable.telegram, getContext().getString(R.string.SettingsTelegram)).setColor(R.attr.telegramColor, false).setOnClick(() -> {
|
||||
Activity act = (Activity) getContext();
|
||||
act.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/ytkab0bp_channel")));
|
||||
}),
|
||||
BeamServerData.isBoostyAvailable() ? new OptionElement(R.drawable.boosty, getContext().getString(R.string.SettingsBoosty)).setColor(R.attr.boostyColorTop, true).setOnClick(() -> {
|
||||
Activity act = (Activity) getContext();
|
||||
act.startActivity(new Intent(act, SetupActivity.class).putExtra(SetupActivity.EXTRA_BOOSTY_ONLY, true));
|
||||
}) : null,
|
||||
new OptionElement(R.drawable.k3d_logo_new_14, getContext().getString(R.string.SettingsK3D)).setColor(R.attr.k3dColor, true).setOnClick(() -> {
|
||||
Activity act = (Activity) getContext();
|
||||
act.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/K_3_D")));
|
||||
}),
|
||||
new OptionElement(R.drawable.refresh_outline_28, getContext().getString(R.string.SettingsResetToDefault)).setColor(R.attr.textColorNegative, false).setOnClick(() -> {
|
||||
Context ctx = getContext();
|
||||
if (ctx instanceof Activity) {
|
||||
Activity act = (Activity) ctx;
|
||||
new BeamAlertDialogBuilder(getContext())
|
||||
.setTitle(R.string.SettingsResetToDefaultTitle)
|
||||
.setMessage(R.string.SettingsResetToDefaultDescription)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Santoku.getConfigFile().delete();
|
||||
Santoku.CONFIG = null;
|
||||
Prefs.getPrefs().edit().clear().apply();
|
||||
Prefs.setLastCommit();
|
||||
act.startActivity(new Intent(act, SetupActivity.class));
|
||||
act.finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null).
|
||||
show();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDataUpdated(BeamServerDataUpdatedEvent e) {
|
||||
setConfigItems(getConfigItems());
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onUserInfoUpdated(CloudUserInfoUpdatedEvent e) {
|
||||
if (BeamServerData.isCloudAvailable()) {
|
||||
recyclerView.getAdapter().notifyItemChanged(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cloneCurrentProfile() {}
|
||||
|
||||
@Override
|
||||
protected void deleteCurrentProfile() {}
|
||||
|
||||
@Override
|
||||
protected void onApplyConfig(String title) {}
|
||||
|
||||
@Override
|
||||
protected void onResetConfig() {}
|
||||
|
||||
@Override
|
||||
protected ConfigObject getCurrentConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTitle() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void selectItem(ProfileListItem item) {}
|
||||
|
||||
@Override
|
||||
protected List<ProfileListItem> getItems(boolean filter) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.dark98.santoku.navigation;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.fragment.BedFragment;
|
||||
import com.dark98.santoku.fragment.FilamentConfigFragment;
|
||||
import com.dark98.santoku.fragment.PrintConfigFragment;
|
||||
import com.dark98.santoku.fragment.PrinterConfigFragment;
|
||||
import com.dark98.santoku.fragment.SettingsFragment;
|
||||
|
||||
public abstract class DelegateSlotImpl extends NavigationDelegate {
|
||||
public int getSlotCount() {
|
||||
return 5;
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
public int getSlotIcon(int slot) {
|
||||
switch (slot) {
|
||||
default:
|
||||
case 0:
|
||||
return R.drawable.view_in_ar_24;
|
||||
case 1:
|
||||
return R.drawable.wrench_outline_28;
|
||||
case 2:
|
||||
return R.drawable.slot_filament_28;
|
||||
case 3:
|
||||
return R.drawable.printer_outline_28;
|
||||
case 4:
|
||||
return R.drawable.settings_outline_28;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean needDisplaySlotGear(int slot) {
|
||||
return slot != 0 && slot != 4;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getSlotTitle(int slot) {
|
||||
switch (slot) {
|
||||
default:
|
||||
case 0:
|
||||
return R.string.SlotBed;
|
||||
case 1:
|
||||
return R.string.SlotPrintConfig;
|
||||
case 2:
|
||||
return R.string.SlotFilamentConfig;
|
||||
case 3:
|
||||
return R.string.SlotPrinterConfig;
|
||||
case 4:
|
||||
return R.string.SlotAppSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getSlotTooltip(int slot) {
|
||||
switch (slot) {
|
||||
default:
|
||||
return getSlotTitle(slot);
|
||||
case 1:
|
||||
return R.string.SlotPrintConfigTooltip;
|
||||
case 2:
|
||||
return R.string.SlotFilamentConfigTooltip;
|
||||
case 3:
|
||||
return R.string.SlotPrinterConfigTooltip;
|
||||
case 4:
|
||||
return R.string.SlotAppSettingsTooltip;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment newFragment(int slot) {
|
||||
switch (slot) {
|
||||
default:
|
||||
case 0:
|
||||
return new BedFragment();
|
||||
case 1:
|
||||
return new PrintConfigFragment();
|
||||
case 2:
|
||||
return new FilamentConfigFragment();
|
||||
case 3:
|
||||
return new PrinterConfigFragment();
|
||||
case 4:
|
||||
return new SettingsFragment();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.dark98.santoku.navigation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
|
||||
public abstract class Fragment {
|
||||
private View mView;
|
||||
private Context context;
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
void setContext(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onCreate() {}
|
||||
|
||||
@CallSuper
|
||||
public void onResume() {}
|
||||
|
||||
@CallSuper
|
||||
public void onPause() {}
|
||||
|
||||
@CallSuper
|
||||
public void onDestroy() {
|
||||
mView = null;
|
||||
}
|
||||
|
||||
public void onApplyTheme() {
|
||||
mView.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
}
|
||||
|
||||
public abstract View onCreateView(Context ctx);
|
||||
|
||||
@CallSuper
|
||||
public void onViewCreated(View v) {
|
||||
mView = v;
|
||||
}
|
||||
|
||||
public View getView() {
|
||||
return mView;
|
||||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public SlotChangeCallback onCheckDelayForSlotChange() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public interface SlotChangeCallback {
|
||||
boolean needDelay(Runnable callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.dark98.santoku.navigation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.view.menu.MenuItemImpl;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.navigation.NavigationBarView;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.TextColorImageSpan;
|
||||
import com.dark98.santoku.view.ThemeBottomNavigationView;
|
||||
import com.dark98.santoku.view.ThemeRailNavigationView;
|
||||
|
||||
public class MobileNavigationDelegate extends DelegateSlotImpl {
|
||||
private boolean portrait;
|
||||
private FrameLayout root;
|
||||
private NavigationBarView navigationView;
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
super.onApplyTheme();
|
||||
ThemesRepo.invalidateView(navigationView);
|
||||
root.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public View onCreateView(Context ctx) {
|
||||
FrameLayout fl = new FrameLayout(ctx);
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
|
||||
portrait = dm.widthPixels < dm.heightPixels;
|
||||
ll.setOrientation(portrait ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
|
||||
|
||||
if (portrait) ll.addView(navigationView = new ThemeBottomNavigationView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
container = new FrameLayout(ctx);
|
||||
Fragment fr = getCurrentFragment();
|
||||
if (fr.getView() == null) {
|
||||
View v = fr.onCreateView(ctx);
|
||||
fr.onViewCreated(v);
|
||||
fr.onApplyTheme();
|
||||
|
||||
container.addView(v);
|
||||
} else {
|
||||
container.addView(fr.getView());
|
||||
}
|
||||
ll.addView(container, new LinearLayout.LayoutParams(portrait ? ViewGroup.LayoutParams.MATCH_PARENT : 0, portrait ? 0 : ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
|
||||
if (!portrait) {
|
||||
ll.addView(navigationView = new ThemeRailNavigationView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
navigationView.setLabelVisibilityMode(BottomNavigationView.LABEL_VISIBILITY_LABELED);
|
||||
Menu menu = navigationView.getMenu();
|
||||
for (int i = 0; i < getSlotCount(); i++) {
|
||||
MenuItemImpl item = (MenuItemImpl) menu.add(0, i, 0, getSlotTitle(i)).setIcon(getSlotIcon(i));
|
||||
if (needDisplaySlotGear(i)) {
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf("d ");
|
||||
Drawable dr = ContextCompat.getDrawable(ctx, R.drawable.settings_outline_28);
|
||||
int size = ViewUtils.dp(13);
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.setSpan(new ReplacementSpan() {
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
|
||||
return ViewUtils.dp(2f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {}
|
||||
}, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.setSpan(new TextColorImageSpan(dr, ViewUtils.dp(1.5f)), 0, 1, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append(item.getTitle());
|
||||
item.setTitle(sb);
|
||||
}
|
||||
item.setTooltipText(ctx.getString(getSlotTooltip(i)));
|
||||
}
|
||||
navigationView.setSelectedItemId(currentSlot);
|
||||
NavigationBarView finalNavigationView = navigationView;
|
||||
navigationView.setOnItemSelectedListener(item -> {
|
||||
if (item.getItemId() == currentSlot) return true;
|
||||
switchSlot(item.getItemId(), () -> finalNavigationView.setSelectedItemId(item.getItemId()));
|
||||
return false;
|
||||
});
|
||||
|
||||
fl.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
|
||||
fl.addView(ll);
|
||||
return root = fl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (super.onBackPressed()) {
|
||||
return true;
|
||||
}
|
||||
if (currentSlot != 0) {
|
||||
switchSlot(0, () -> navigationView.setSelectedItemId(0));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameLayout getOverlayView() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSwitchingWithX() {
|
||||
return portrait;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package com.dark98.santoku.navigation;
|
||||
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import java.util.Stack;
|
||||
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
|
||||
public abstract class NavigationDelegate {
|
||||
protected Context context;
|
||||
|
||||
protected SparseArray<Stack<Fragment>> fragmentStack = new SparseArray<>();
|
||||
protected int currentSlot = 0;
|
||||
protected FrameLayout container;
|
||||
|
||||
private SpringAnimation switchAnimation;
|
||||
|
||||
public final void setContext(Context ctx) {
|
||||
context = ctx;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onCreate() {
|
||||
for (int i = 0; i < fragmentStack.size(); i++) {
|
||||
Stack<Fragment> fragments = fragmentStack.get(fragmentStack.keyAt(i));
|
||||
for (Fragment fr : fragments) {
|
||||
fr.setContext(context);
|
||||
fr.onCreate();
|
||||
}
|
||||
}
|
||||
|
||||
if (fragmentStack.get(currentSlot) == null) {
|
||||
Stack<Fragment> stack = new Stack<>();
|
||||
fragmentStack.put(currentSlot, stack);
|
||||
Fragment fr = newFragment(currentSlot);
|
||||
fr.setContext(context);
|
||||
fr.onCreate();
|
||||
stack.push(fr);
|
||||
}
|
||||
}
|
||||
|
||||
public void onApplyTheme() {
|
||||
for (int i = 0; i < fragmentStack.size(); i++) {
|
||||
Stack<Fragment> st = fragmentStack.valueAt(i);
|
||||
assertTrue(st != null);
|
||||
for (Fragment fr : st) {
|
||||
fr.onApplyTheme();
|
||||
if (fr.getView() != null) {
|
||||
ThemesRepo.invalidateView(fr.getView());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onResume() {
|
||||
fragmentStack.get(currentSlot).peek().onResume();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onPause() {
|
||||
fragmentStack.get(currentSlot).peek().onPause();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onDestroy() {
|
||||
for (int i = 0; i < fragmentStack.size(); i++) {
|
||||
Stack<Fragment> fragments = fragmentStack.get(fragmentStack.keyAt(i));
|
||||
for (Fragment fr : fragments) {
|
||||
fr.onDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FrameLayout getContainerView() {
|
||||
return container;
|
||||
}
|
||||
|
||||
public abstract View onCreateView(Context ctx);
|
||||
public abstract Fragment newFragment(int slot);
|
||||
public abstract FrameLayout getOverlayView();
|
||||
|
||||
public boolean isSwitchingWithX() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void switchSlot(int slot, Runnable onInterfaceChange) {
|
||||
Stack<Fragment> stack = fragmentStack.get(slot);
|
||||
if (stack == null) {
|
||||
fragmentStack.put(slot, stack = new Stack<>());
|
||||
Fragment fr = newFragment(slot);
|
||||
fr.setContext(context);
|
||||
fr.onCreate();
|
||||
stack.push(fr);
|
||||
}
|
||||
int wasSlot = currentSlot;
|
||||
currentSlot = slot;
|
||||
|
||||
if (container.getChildCount() > 0) {
|
||||
if (switchAnimation != null) {
|
||||
switchAnimation.cancel();
|
||||
}
|
||||
|
||||
Fragment cur = fragmentStack.get(wasSlot).peek();
|
||||
cur.onPause();
|
||||
|
||||
Runnable next = () -> {
|
||||
onInterfaceChange.run();
|
||||
|
||||
Fragment fr = fragmentStack.get(slot).peek();
|
||||
View wasView = container.getChildAt(0);
|
||||
View newView;
|
||||
if (fr.getView() == null) {
|
||||
newView = fr.onCreateView(context);
|
||||
fr.onViewCreated(newView);
|
||||
fr.onApplyTheme();
|
||||
} else {
|
||||
newView = fr.getView();
|
||||
}
|
||||
container.addView(newView);
|
||||
|
||||
boolean forward = slot > wasSlot;
|
||||
switchAnimation = new SpringAnimation(new FloatValueHolder(0))
|
||||
.setMinimumVisibleChange(1 / 256f)
|
||||
.setSpring(new SpringForce(1f)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener((animation, value, velocity) -> {
|
||||
float fValue = value;
|
||||
if (!forward) {
|
||||
fValue = 1f - fValue;
|
||||
}
|
||||
|
||||
if (isSwitchingWithX()) {
|
||||
wasView.setTranslationX(fValue * -wasView.getWidth() * 0.75f);
|
||||
newView.setTranslationX((1f - value) * (forward ? 1 : -1) * newView.getWidth() * 0.75f);
|
||||
} else {
|
||||
wasView.setTranslationY(fValue * -wasView.getHeight() * 0.75f);
|
||||
newView.setTranslationY((1f - value) * (forward ? 1 : -1) * newView.getHeight() * 0.75f);
|
||||
}
|
||||
wasView.setAlpha(1f - value);
|
||||
newView.setAlpha(value);
|
||||
})
|
||||
.addEndListener((animation, canceled, value, velocity) -> {
|
||||
switchAnimation = null;
|
||||
container.removeView(wasView);
|
||||
fr.onResume();
|
||||
});
|
||||
switchAnimation.start();
|
||||
};
|
||||
|
||||
Fragment.SlotChangeCallback callback = cur.onCheckDelayForSlotChange();
|
||||
if (callback == null || !cur.onCheckDelayForSlotChange().needDelay(next)) {
|
||||
next.run();
|
||||
}
|
||||
} else {
|
||||
Fragment fr = fragmentStack.get(slot).peek();
|
||||
fr.setContext(context);
|
||||
fr.onCreate();
|
||||
View v = fr.onCreateView(context);
|
||||
fr.onViewCreated(v);
|
||||
fr.onApplyTheme();
|
||||
container.addView(v);
|
||||
fr.onResume();
|
||||
currentSlot = slot;
|
||||
|
||||
onInterfaceChange.run();
|
||||
}
|
||||
}
|
||||
|
||||
public Fragment getCurrentFragment() {
|
||||
return fragmentStack.get(currentSlot).peek();
|
||||
}
|
||||
|
||||
public boolean destroyCurrent() {
|
||||
if (fragmentStack.get(currentSlot).size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Fragment fr = fragmentStack.get(currentSlot).peek();
|
||||
fr.onPause();
|
||||
|
||||
Fragment prev = fragmentStack.get(currentSlot).get(fragmentStack.get(currentSlot).size() - 2);
|
||||
|
||||
View wasView = container.getChildAt(0);
|
||||
View newView = prev.getView();
|
||||
|
||||
switchAnimation = new SpringAnimation(new FloatValueHolder(0))
|
||||
.setMinimumVisibleChange(1 / 256f)
|
||||
.setSpring(new SpringForce(1f)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener((animation, value, velocity) -> {
|
||||
float fValue = 1f - value;
|
||||
|
||||
if (isSwitchingWithX()) {
|
||||
wasView.setTranslationX(-fValue * wasView.getWidth() * 0.75f);
|
||||
newView.setTranslationX((1f - fValue) * newView.getWidth() * 0.75f);
|
||||
} else {
|
||||
wasView.setTranslationY(-fValue * wasView.getHeight() * 0.75f);
|
||||
newView.setTranslationY((1f - fValue) * newView.getHeight() * 0.75f);
|
||||
}
|
||||
wasView.setAlpha(1f - value);
|
||||
newView.setAlpha(value);
|
||||
})
|
||||
.addEndListener((animation, canceled, value, velocity) -> {
|
||||
switchAnimation = null;
|
||||
container.removeView(wasView);
|
||||
prev.onResume();
|
||||
fr.onDestroy();
|
||||
});
|
||||
switchAnimation.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void pushFragment(Fragment fragment) {
|
||||
fragment.setContext(context);
|
||||
fragment.onCreate();
|
||||
|
||||
fragmentStack.get(currentSlot).peek().onPause();
|
||||
View wasView = container.getChildAt(0);
|
||||
View newView = fragment.onCreateView(context);
|
||||
fragment.onViewCreated(newView);
|
||||
fragment.onApplyTheme();
|
||||
fragmentStack.get(currentSlot).push(fragment);
|
||||
|
||||
switchAnimation = new SpringAnimation(new FloatValueHolder(0))
|
||||
.setMinimumVisibleChange(1 / 256f)
|
||||
.setSpring(new SpringForce(1f)
|
||||
.setStiffness(1000f)
|
||||
.setDampingRatio(1f))
|
||||
.addUpdateListener((animation, value, velocity) -> {
|
||||
if (isSwitchingWithX()) {
|
||||
wasView.setTranslationX(-value * wasView.getWidth() * 0.75f);
|
||||
newView.setTranslationX((1f - value) * newView.getWidth() * 0.75f);
|
||||
} else {
|
||||
wasView.setTranslationY(-value * wasView.getHeight() * 0.75f);
|
||||
newView.setTranslationY((1f - value) * newView.getHeight() * 0.75f);
|
||||
}
|
||||
wasView.setAlpha(1f - value);
|
||||
newView.setAlpha(value);
|
||||
})
|
||||
.addEndListener((animation, canceled, value, velocity) -> {
|
||||
switchAnimation = null;
|
||||
container.removeView(wasView);
|
||||
fragment.onResume();
|
||||
});
|
||||
switchAnimation.start();
|
||||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
Fragment fr = getCurrentFragment();
|
||||
if (fr != null && fr.onBackPressed()) return true;
|
||||
else if (fragmentStack.get(currentSlot).size() > 1) {
|
||||
return destroyCurrent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
package com.dark98.santoku.print_host;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSink;
|
||||
|
||||
public final class ElegooLinkClient {
|
||||
private static final int MAX_UPLOAD_PACKAGE_LENGTH = 1024 * 1024;
|
||||
private static final MediaType OCTET_STREAM = MediaType.parse("application/octet-stream");
|
||||
|
||||
private ElegooLinkClient() {}
|
||||
|
||||
public static Result upload(File gcode, String host, String uploadName, boolean startPrint, boolean timelapse, boolean bedLeveling, int bedType) {
|
||||
if (gcode == null || !gcode.exists()) {
|
||||
return Result.error("G-code file not found.");
|
||||
}
|
||||
String finalName = (uploadName == null || uploadName.isEmpty()) ? gcode.getName() : uploadName;
|
||||
String baseUrl = normalizeBaseUrl(host);
|
||||
String uploadUrl = baseUrl + "/uploadFile/upload";
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.callTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
String md5;
|
||||
try {
|
||||
md5 = md5File(gcode);
|
||||
} catch (Exception e) {
|
||||
return Result.error("Failed to compute MD5: " + e.getMessage());
|
||||
}
|
||||
long size = gcode.length();
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
int packageCount = (int) ((size + MAX_UPLOAD_PACKAGE_LENGTH - 1) / MAX_UPLOAD_PACKAGE_LENGTH);
|
||||
for (int i = 0; i < packageCount; i++) {
|
||||
long offset = (long) MAX_UPLOAD_PACKAGE_LENGTH * i;
|
||||
long length = Math.min(MAX_UPLOAD_PACKAGE_LENGTH, size - offset);
|
||||
Result partRes = uploadPart(client, uploadUrl, gcode, finalName, md5, uuid, size, offset, length);
|
||||
if (!partRes.ok) {
|
||||
return partRes;
|
||||
}
|
||||
}
|
||||
|
||||
if (!startPrint) {
|
||||
return Result.ok();
|
||||
}
|
||||
return startPrint(client, host, finalName, timelapse, bedLeveling, bedType);
|
||||
}
|
||||
|
||||
private static Result uploadPart(OkHttpClient client, String url, File file, String uploadName, String md5, String uuid, long totalSize, long offset, long length) {
|
||||
RequestBody fileBody = new FileSliceRequestBody(file, offset, length);
|
||||
MultipartBody requestBody = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Check", "1")
|
||||
.addFormDataPart("S-File-MD5", md5)
|
||||
.addFormDataPart("Offset", String.valueOf(offset))
|
||||
.addFormDataPart("Uuid", uuid)
|
||||
.addFormDataPart("TotalSize", String.valueOf(totalSize))
|
||||
.addFormDataPart("File", uploadName, fileBody)
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
if (!response.isSuccessful()) {
|
||||
return Result.error("Upload failed: HTTP " + response.code());
|
||||
}
|
||||
if (!isElegooOk(body)) {
|
||||
return Result.error(parseElegooError(body));
|
||||
}
|
||||
return Result.ok();
|
||||
} catch (IOException e) {
|
||||
return Result.error("Upload failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Result startPrint(OkHttpClient client, String host, String filename, boolean timelapse, boolean bedLeveling, int bedType) {
|
||||
WebSocketSession session = null;
|
||||
String lastError = null;
|
||||
for (String wsUrl : buildWebSocketCandidates(host)) {
|
||||
WebSocketSession attempt = new WebSocketSession(client, wsUrl);
|
||||
if (attempt.awaitOpen(10, TimeUnit.SECONDS)) {
|
||||
session = attempt;
|
||||
break;
|
||||
}
|
||||
lastError = attempt.failureSummary();
|
||||
if (attempt.isTooManyClients()) {
|
||||
return Result.error("Printer reports too many connected clients. Close the ElegooLink web UI and other apps, then try again.");
|
||||
}
|
||||
attempt.close();
|
||||
}
|
||||
if (session == null) {
|
||||
if (lastError != null && !lastError.isEmpty()) {
|
||||
return Result.error("Failed to connect to ElegooLink websocket. " + lastError);
|
||||
}
|
||||
return Result.error("Failed to connect to ElegooLink websocket.");
|
||||
}
|
||||
|
||||
String requestId = UUID.randomUUID().toString().replace("-", "");
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String json = "{"
|
||||
+ "\"Id\":\"\","
|
||||
+ "\"Data\":{"
|
||||
+ "\"Cmd\":128,"
|
||||
+ "\"Data\":{"
|
||||
+ "\"Filename\":\"/local/" + filename + "\","
|
||||
+ "\"StartLayer\":0,"
|
||||
+ "\"Calibration_switch\":" + (bedLeveling ? 1 : 0) + ","
|
||||
+ "\"PrintPlatformType\":" + (bedType != 0 ? 1 : 0) + ","
|
||||
+ "\"Tlp_Switch\":" + (timelapse ? 1 : 0)
|
||||
+ "},"
|
||||
+ "\"RequestID\":\"" + requestId + "\","
|
||||
+ "\"MainboardID\":\"\","
|
||||
+ "\"TimeStamp\":" + timestamp + ","
|
||||
+ "\"From\":1"
|
||||
+ "}"
|
||||
+ "}";
|
||||
|
||||
session.sendText(json);
|
||||
String response = session.receiveText(30, TimeUnit.SECONDS);
|
||||
if (response == null) {
|
||||
session.close();
|
||||
return Result.error("Start print timeout.");
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(response);
|
||||
JSONObject data = root.optJSONObject("Data");
|
||||
if (data == null) {
|
||||
session.close();
|
||||
return Result.error("Invalid response from printer.");
|
||||
}
|
||||
int cmd = data.optInt("Cmd", -1);
|
||||
if (cmd != 128) {
|
||||
session.close();
|
||||
return Result.error("Unexpected response from printer.");
|
||||
}
|
||||
JSONObject ackData = data.optJSONObject("Data");
|
||||
int ack = ackData != null ? ackData.optInt("Ack", -1) : -1;
|
||||
if (ack == 0) {
|
||||
session.close();
|
||||
return Result.ok();
|
||||
}
|
||||
String error = mapAckError(ack);
|
||||
session.close();
|
||||
return Result.error(error);
|
||||
} catch (JSONException e) {
|
||||
session.close();
|
||||
return Result.error("Invalid response from printer.");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isElegooOk(String body) {
|
||||
try {
|
||||
JSONObject root = new JSONObject(body);
|
||||
String code = root.optString("code", "");
|
||||
return "000000".equals(code);
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String parseElegooError(String body) {
|
||||
try {
|
||||
JSONObject root = new JSONObject(body);
|
||||
String code = root.optString("code", "unknown");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ErrorCode: ").append(code);
|
||||
JSONArray messages = root.optJSONArray("messages");
|
||||
if (messages != null) {
|
||||
for (int i = 0; i < messages.length(); i++) {
|
||||
JSONObject msg = messages.optJSONObject(i);
|
||||
if (msg != null) {
|
||||
sb.append("\n").append(msg.optString("field", ""))
|
||||
.append(":").append(msg.optString("message", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (JSONException e) {
|
||||
return "Upload failed.";
|
||||
}
|
||||
}
|
||||
|
||||
private static String mapAckError(int ack) {
|
||||
switch (ack) {
|
||||
case 1:
|
||||
return "The printer is busy.";
|
||||
case 2:
|
||||
return "The file is missing.";
|
||||
case 3:
|
||||
return "MD5 check failed.";
|
||||
case 4:
|
||||
return "File I/O error.";
|
||||
case 5:
|
||||
case 6:
|
||||
return "File format or resolution is invalid.";
|
||||
case 7:
|
||||
return "File does not match the printer.";
|
||||
default:
|
||||
return "Unknown error. Error code: " + ack;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String host) {
|
||||
String value = host.trim();
|
||||
if (!value.contains("://")) {
|
||||
value = "http://" + value;
|
||||
}
|
||||
if (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static List<String> buildWebSocketCandidates(String host) {
|
||||
String h = sanitizeWebSocketHost(host);
|
||||
List<String> urls = new ArrayList<>(1);
|
||||
urls.add(String.format(Locale.US, "ws://%s:3030/websocket", h));
|
||||
return urls;
|
||||
}
|
||||
|
||||
private static String sanitizeWebSocketHost(String host) {
|
||||
String base = normalizeBaseUrl(host);
|
||||
try {
|
||||
URI uri = new URI(base);
|
||||
String h = uri.getHost() != null ? uri.getHost() : uri.getAuthority();
|
||||
if (h != null && h.contains(":")) {
|
||||
h = h.substring(0, h.indexOf(':'));
|
||||
}
|
||||
if (h == null || h.isEmpty()) {
|
||||
h = host;
|
||||
}
|
||||
return wrapIpv6Host(h);
|
||||
} catch (URISyntaxException e) {
|
||||
String h = host;
|
||||
int schemeIdx = h.indexOf("://");
|
||||
if (schemeIdx != -1) {
|
||||
h = h.substring(schemeIdx + 3);
|
||||
}
|
||||
int slashIdx = h.indexOf('/');
|
||||
if (slashIdx != -1) {
|
||||
h = h.substring(0, slashIdx);
|
||||
}
|
||||
int portIdx = h.indexOf(':');
|
||||
if (portIdx != -1) {
|
||||
h = h.substring(0, portIdx);
|
||||
}
|
||||
return wrapIpv6Host(h);
|
||||
}
|
||||
}
|
||||
|
||||
private static String wrapIpv6Host(String host) {
|
||||
if (host == null || host.isEmpty()) {
|
||||
return host;
|
||||
}
|
||||
if (host.indexOf(':') != -1 && !host.startsWith("[") && !host.endsWith("]")) {
|
||||
return "[" + host + "]";
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
private static String md5File(File file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest.digest()) {
|
||||
sb.append(String.format(Locale.US, "%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final class FileSliceRequestBody extends RequestBody {
|
||||
private final File file;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
|
||||
FileSliceRequestBody(File file, long offset, long length) {
|
||||
this.file = file;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return OCTET_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException {
|
||||
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
|
||||
raf.seek(offset);
|
||||
byte[] buffer = new byte[8192];
|
||||
long remaining = length;
|
||||
while (remaining > 0) {
|
||||
int read = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining));
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
sink.write(buffer, 0, read);
|
||||
remaining -= read;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WebSocketSession extends okhttp3.WebSocketListener {
|
||||
private final java.util.concurrent.BlockingQueue<String> messages = new java.util.concurrent.LinkedBlockingQueue<>();
|
||||
private final okhttp3.WebSocket socket;
|
||||
private volatile boolean opened;
|
||||
private volatile String failureBody;
|
||||
private volatile okhttp3.Response failureResponse;
|
||||
private volatile Throwable failure;
|
||||
|
||||
WebSocketSession(OkHttpClient client, String url) {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
socket = client.newWebSocket(request, this);
|
||||
}
|
||||
|
||||
boolean awaitOpen(long timeout, TimeUnit unit) {
|
||||
try {
|
||||
long deadline = System.nanoTime() + unit.toNanos(timeout);
|
||||
while (!opened && System.nanoTime() < deadline) {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
return opened;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void sendText(String message) {
|
||||
socket.send(message);
|
||||
}
|
||||
|
||||
String receiveText(long timeout, TimeUnit unit) {
|
||||
try {
|
||||
return messages.poll(timeout, unit);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
socket.close(1000, "done");
|
||||
}
|
||||
|
||||
String failureSummary() {
|
||||
if (failureResponse != null) {
|
||||
return "HTTP " + failureResponse.code();
|
||||
}
|
||||
if (failure != null) {
|
||||
return failure.getMessage();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
boolean isTooManyClients() {
|
||||
return failureBody != null && failureBody.toLowerCase(Locale.US).contains("too many client");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(okhttp3.WebSocket webSocket, okhttp3.Response response) {
|
||||
opened = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(okhttp3.WebSocket webSocket, String text) {
|
||||
messages.offer(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(okhttp3.WebSocket webSocket, Throwable t, okhttp3.Response response) {
|
||||
failure = t;
|
||||
failureResponse = response;
|
||||
if (response != null && response.body() != null) {
|
||||
try {
|
||||
failureBody = response.body().string();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
public final boolean ok;
|
||||
public final String error;
|
||||
|
||||
private Result(boolean ok, String error) {
|
||||
this.ok = ok;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static Result ok() {
|
||||
return new Result(true, null);
|
||||
}
|
||||
|
||||
public static Result error(String error) {
|
||||
return new Result(false, error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class BigHeaderItem extends SimpleRecyclerItem<TextView> {
|
||||
public String title;
|
||||
|
||||
public BigHeaderItem() {}
|
||||
|
||||
public BigHeaderItem(String t) {
|
||||
title = t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView onCreateView(Context ctx) {
|
||||
TextView tv = new TextView(ctx);
|
||||
tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
tv.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
tv.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
tv.setPadding(ViewUtils.dp(21), ViewUtils.dp(12), ViewUtils.dp(21), ViewUtils.dp(12));
|
||||
tv.setGravity(Gravity.START);
|
||||
tv.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return tv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(TextView view) {
|
||||
view.setText(title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.view.View;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
/**
|
||||
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
|
||||
* animations on remove, add, and move events that happen to the items in
|
||||
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class CubicBezierItemAnimator extends SimpleItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static TimeInterpolator sDefaultInterpolator = ViewUtils.CUBIC_INTERPOLATOR;
|
||||
|
||||
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
|
||||
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
|
||||
|
||||
ArrayList<ArrayList<RecyclerView.ViewHolder>> mAdditionsList = new ArrayList<>();
|
||||
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
|
||||
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
|
||||
|
||||
ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<>();
|
||||
ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<>();
|
||||
ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
|
||||
ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<>();
|
||||
|
||||
private static class MoveInfo {
|
||||
public RecyclerView.ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChangeInfo {
|
||||
public RecyclerView.ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{"
|
||||
+ "oldHolder=" + oldHolder
|
||||
+ ", newHolder=" + newHolder
|
||||
+ ", fromX=" + fromX
|
||||
+ ", fromY=" + fromY
|
||||
+ ", toX=" + toX
|
||||
+ ", toY=" + toY
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
public CubicBezierItemAnimator() {
|
||||
int duration = 180;
|
||||
setAddDuration(duration);
|
||||
setRemoveDuration(duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||
boolean movesPending = !mPendingMoves.isEmpty();
|
||||
boolean changesPending = !mPendingChanges.isEmpty();
|
||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
// nothing to animate
|
||||
return;
|
||||
}
|
||||
// First, remove stuff
|
||||
for (RecyclerView.ViewHolder holder : mPendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
mPendingRemovals.clear();
|
||||
// Next, move stuff
|
||||
if (movesPending) {
|
||||
final ArrayList<MoveInfo> moves = new ArrayList<>();
|
||||
moves.addAll(mPendingMoves);
|
||||
mMovesList.add(moves);
|
||||
mPendingMoves.clear();
|
||||
Runnable mover = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (MoveInfo moveInfo : moves) {
|
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||
moveInfo.toX, moveInfo.toY);
|
||||
}
|
||||
moves.clear();
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
View view = moves.get(0).holder.itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
||||
} else {
|
||||
mover.run();
|
||||
}
|
||||
}
|
||||
// Next, change stuff, to run in parallel with move animations
|
||||
if (changesPending) {
|
||||
final ArrayList<ChangeInfo> changes = new ArrayList<>();
|
||||
changes.addAll(mPendingChanges);
|
||||
mChangesList.add(changes);
|
||||
mPendingChanges.clear();
|
||||
Runnable changer = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (ChangeInfo change : changes) {
|
||||
animateChangeImpl(change);
|
||||
}
|
||||
changes.clear();
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
|
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff
|
||||
if (additionsPending) {
|
||||
final ArrayList<RecyclerView.ViewHolder> additions = new ArrayList<>();
|
||||
additions.addAll(mPendingAdditions);
|
||||
mAdditionsList.add(additions);
|
||||
mPendingAdditions.clear();
|
||||
Runnable adder = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (RecyclerView.ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
View view = additions.get(0).itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimator animation = view.animate();
|
||||
mRemoveAnimations.add(holder);
|
||||
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
|
||||
new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
animation.setListener(null);
|
||||
view.setAlpha(1);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
holder.itemView.setAlpha(0);
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
void animateAddImpl(final RecyclerView.ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimator animation = view.animate();
|
||||
mAddAnimations.add(holder);
|
||||
animation.alpha(1).setDuration(getAddDuration())
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {
|
||||
view.setAlpha(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
animation.setListener(null);
|
||||
dispatchAddFinished(holder);
|
||||
mAddAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += (int) holder.itemView.getTranslationX();
|
||||
fromY += (int) holder.itemView.getTranslationY();
|
||||
resetAnimation(holder);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
if (deltaX != 0) {
|
||||
view.setTranslationX(-deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
view.setTranslationY(-deltaY);
|
||||
}
|
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
view.animate().translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
view.animate().translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
final ViewPropertyAnimator animation = view.animate();
|
||||
mMoveAnimations.add(holder);
|
||||
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {
|
||||
if (deltaX != 0) {
|
||||
view.setTranslationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
view.setTranslationY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
mMoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
if (oldHolder == newHolder) {
|
||||
// Don't know how to run change animations when the same view holder is re-used.
|
||||
// run a move animation to handle position changes.
|
||||
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
||||
}
|
||||
final float prevTranslationX = oldHolder.itemView.getTranslationX();
|
||||
final float prevTranslationY = oldHolder.itemView.getTranslationY();
|
||||
final float prevAlpha = oldHolder.itemView.getAlpha();
|
||||
resetAnimation(oldHolder);
|
||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// recover prev translation state after ending animation
|
||||
oldHolder.itemView.setTranslationX(prevTranslationX);
|
||||
oldHolder.itemView.setTranslationY(prevTranslationY);
|
||||
oldHolder.itemView.setAlpha(prevAlpha);
|
||||
if (newHolder != null) {
|
||||
// carry over translation values
|
||||
resetAnimation(newHolder);
|
||||
newHolder.itemView.setTranslationX(-deltaX);
|
||||
newHolder.itemView.setTranslationY(-deltaY);
|
||||
newHolder.itemView.setAlpha(0);
|
||||
}
|
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder == null ? null : holder.itemView;
|
||||
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
if (view != null) {
|
||||
final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
|
||||
getChangeDuration());
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
oldViewAnim.setListener(null);
|
||||
view.setAlpha(1);
|
||||
view.setTranslationX(0);
|
||||
view.setTranslationY(0);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
if (newView != null) {
|
||||
final ViewPropertyAnimator newViewAnimation = newView.animate();
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
|
||||
.alpha(1).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
newViewAnimation.setListener(null);
|
||||
newView.setAlpha(1);
|
||||
newView.setTranslationX(0);
|
||||
newView.setTranslationY(0);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
mChangeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) {
|
||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||
ChangeInfo changeInfo = infoList.get(i);
|
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||
infoList.remove(changeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||
if (changeInfo.oldHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||
}
|
||||
if (changeInfo.newHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||
}
|
||||
}
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
|
||||
boolean oldItem = false;
|
||||
if (changeInfo.newHolder == item) {
|
||||
changeInfo.newHolder = null;
|
||||
} else if (changeInfo.oldHolder == item) {
|
||||
changeInfo.oldHolder = null;
|
||||
oldItem = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
item.itemView.setAlpha(1);
|
||||
item.itemView.setTranslationX(0);
|
||||
item.itemView.setTranslationY(0);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(RecyclerView.ViewHolder item) {
|
||||
final View view = item.itemView;
|
||||
// this will trigger end callback which should set properties to their target values.
|
||||
view.animate().cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(item);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(mPendingChanges, item);
|
||||
if (mPendingRemovals.remove(item)) {
|
||||
view.setAlpha(1);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (mPendingAdditions.remove(item)) {
|
||||
view.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
endChangeAnimation(changes, item);
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(i);
|
||||
}
|
||||
}
|
||||
for (int i = mMovesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
for (int j = moves.size() - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
if (moveInfo.holder == item) {
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
|
||||
if (additions.remove(item)) {
|
||||
view.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animations should be ended by the cancel above.
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mRemoveAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mAddAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mAddAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mChangeAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mMoveAnimations list");
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
private void resetAnimation(RecyclerView.ViewHolder holder) {
|
||||
holder.itemView.animate().setInterpolator(sDefaultInterpolator);
|
||||
endAnimation(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!mPendingAdditions.isEmpty()
|
||||
|| !mPendingChanges.isEmpty()
|
||||
|| !mPendingMoves.isEmpty()
|
||||
|| !mPendingRemovals.isEmpty()
|
||||
|| !mMoveAnimations.isEmpty()
|
||||
|| !mRemoveAnimations.isEmpty()
|
||||
|| !mAddAnimations.isEmpty()
|
||||
|| !mChangeAnimations.isEmpty()
|
||||
|| !mMovesList.isEmpty()
|
||||
|| !mAdditionsList.isEmpty()
|
||||
|| !mChangesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = mPendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
MoveInfo item = mPendingMoves.get(i);
|
||||
View view = item.holder.itemView;
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(item.holder);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
count = mPendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
RecyclerView.ViewHolder item = mPendingRemovals.get(i);
|
||||
dispatchRemoveFinished(item);
|
||||
mPendingRemovals.remove(i);
|
||||
}
|
||||
count = mPendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
RecyclerView.ViewHolder item = mPendingAdditions.get(i);
|
||||
item.itemView.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
mPendingAdditions.remove(i);
|
||||
}
|
||||
count = mPendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||
}
|
||||
mPendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = mMovesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
count = moves.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
RecyclerView.ViewHolder item = moveInfo.holder;
|
||||
View view = item.itemView;
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(moveInfo.holder);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mAdditionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
|
||||
count = additions.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
RecyclerView.ViewHolder item = additions.get(j);
|
||||
View view = item.itemView;
|
||||
view.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mChangesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
count = changes.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
endChangeAnimationIfNecessary(changes.get(j));
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelAll(mRemoveAnimations);
|
||||
cancelAll(mMoveAnimations);
|
||||
cancelAll(mAddAnimations);
|
||||
cancelAll(mChangeAnimations);
|
||||
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
|
||||
void cancelAll(List<RecyclerView.ViewHolder> viewHolders) {
|
||||
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
||||
viewHolders.get(i).itemView.animate().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
|
||||
* When this is the case:
|
||||
* <ul>
|
||||
* <li>If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both
|
||||
* ViewHolder arguments will be the same instance.
|
||||
* </li>
|
||||
* <li>
|
||||
* If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
|
||||
* then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and
|
||||
* run a move animation instead.
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull List<Object> payloads) {
|
||||
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.DividerView;
|
||||
|
||||
public class DividerItem extends SimpleRecyclerItem<DividerView> {
|
||||
@Override
|
||||
public DividerView onCreateView(Context ctx) {
|
||||
DividerView v = new DividerView(ctx);
|
||||
v.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
topMargin = bottomMargin = ViewUtils.dp(6);
|
||||
}});
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.theme.BeamTheme;
|
||||
import com.dark98.santoku.theme.IThemeView;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.PreferenceHolderView> {
|
||||
private Drawable mIcon;
|
||||
private CharSequence mTitle;
|
||||
private ValueProvider mSubtitle;
|
||||
private View.OnClickListener onClickListener;
|
||||
private View.OnLongClickListener onLongClickListener;
|
||||
private int textColorRes;
|
||||
private boolean noTint;
|
||||
private ValueProvider valueProvider;
|
||||
private float roundRadius;
|
||||
private int mPaddings = ViewUtils.dp(12);
|
||||
private boolean mForceDark;
|
||||
|
||||
public PreferenceItem setTitle(CharSequence title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setSubtitle(CharSequence subtitle) {
|
||||
mSubtitle = ()->subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setPaddings(int paddings) {
|
||||
this.mPaddings = paddings;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setForceDark(boolean mForceDark) {
|
||||
this.mForceDark = mForceDark;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setSubtitleProvider(ValueProvider mSubtitle) {
|
||||
this.mSubtitle = mSubtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setValueProvider(ValueProvider valueProvider) {
|
||||
this.valueProvider = valueProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setValue(String text) {
|
||||
this.valueProvider = () -> text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setIcon(int iconRes) {
|
||||
mIcon = ContextCompat.getDrawable(Santoku.INSTANCE, iconRes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setIcon(Drawable drawable) {
|
||||
mIcon = drawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setNoTint(boolean noTint) {
|
||||
this.noTint = noTint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setRoundRadius(float roundRadius) {
|
||||
this.roundRadius = roundRadius;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setTextColorRes(int textColorRes) {
|
||||
this.textColorRes = textColorRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setOnClickListener(View.OnClickListener onClickListener) {
|
||||
this.onClickListener = onClickListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
|
||||
this.onLongClickListener = onLongClickListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceHolderView onCreateView(Context ctx) {
|
||||
return new PreferenceHolderView(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(PreferenceHolderView view) {
|
||||
view.bind(this);
|
||||
}
|
||||
|
||||
public final static class PreferenceHolderView extends LinearLayout implements IThemeView {
|
||||
private TextView title, subtitle;
|
||||
private ImageView icon;
|
||||
private TextView value;
|
||||
private float radius;
|
||||
|
||||
private PreferenceItem item;
|
||||
|
||||
public PreferenceHolderView(Context context) {
|
||||
super(context);
|
||||
|
||||
setOrientation(HORIZONTAL);
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
|
||||
icon = new AppCompatImageView(context) {
|
||||
private Path path = new Path();
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (radius != 0) {
|
||||
canvas.save();
|
||||
path.rewind();
|
||||
path.addRoundRect(0, 0, getWidth(), getHeight(), radius, radius, Path.Direction.CW);
|
||||
canvas.clipPath(path);
|
||||
}
|
||||
super.draw(canvas);
|
||||
if (radius != 0) {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
icon.setLayoutParams(new LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
|
||||
setMarginStart(ViewUtils.dp(4));
|
||||
setMarginEnd(ViewUtils.dp(8));
|
||||
}});
|
||||
addView(icon);
|
||||
|
||||
LinearLayout innerLayout = new LinearLayout(context);
|
||||
innerLayout.setOrientation(VERTICAL);
|
||||
innerLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
|
||||
title = new TextView(context);
|
||||
title.setEllipsize(TextUtils.TruncateAt.END);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
innerLayout.addView(title);
|
||||
|
||||
subtitle = new TextView(context);
|
||||
subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
|
||||
innerLayout.addView(subtitle);
|
||||
|
||||
addView(innerLayout, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
setMarginStart(ViewUtils.dp(8));
|
||||
setMarginEnd(ViewUtils.dp(8));
|
||||
}});
|
||||
|
||||
value = new TextView(context);
|
||||
value.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
value.setPadding(ViewUtils.dp(8), ViewUtils.dp(6), ViewUtils.dp(8), ViewUtils.dp(6));
|
||||
value.setVisibility(GONE);
|
||||
addView(value, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
setMinimumHeight(ViewUtils.dp(56));
|
||||
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
void bind(PreferenceItem item) {
|
||||
this.item = item;
|
||||
setPadding(item.mPaddings, item.mPaddings, item.mPaddings, item.mPaddings);
|
||||
title.setText(item.mTitle);
|
||||
title.setVisibility(TextUtils.isEmpty(item.mTitle) ? GONE : VISIBLE);
|
||||
|
||||
CharSequence sub = item.mSubtitle != null ? item.mSubtitle.provide() : null;
|
||||
subtitle.setText(sub);
|
||||
subtitle.setVisibility(TextUtils.isEmpty(sub) ? GONE : VISIBLE);
|
||||
|
||||
CharSequence v = item.valueProvider != null ? item.valueProvider.provide() : null;
|
||||
value.setText(v);
|
||||
value.setVisibility(TextUtils.isEmpty(v) ? GONE : VISIBLE);
|
||||
|
||||
if (item.mIcon != null) {
|
||||
icon.setVisibility(VISIBLE);
|
||||
icon.setImageDrawable(item.mIcon);
|
||||
} else {
|
||||
icon.setVisibility(GONE);
|
||||
}
|
||||
if (item.onClickListener != null) {
|
||||
setOnClickListener(item.onClickListener);
|
||||
} else {
|
||||
setClickable(false);
|
||||
}
|
||||
setOnLongClickListener(item.onLongClickListener);
|
||||
|
||||
if (item.textColorRes != 0) {
|
||||
title.setTextColor(ThemesRepo.getColor(item.textColorRes));
|
||||
}
|
||||
|
||||
if (item.textColorRes != 0 || item.mIcon != null) {
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
} else {
|
||||
title.setTypeface(Typeface.DEFAULT);
|
||||
}
|
||||
|
||||
if (item.noTint) {
|
||||
icon.setImageTintList(null);
|
||||
} else {
|
||||
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(item.textColorRes != 0 ? item.textColorRes : android.R.attr.textColorSecondary)));
|
||||
}
|
||||
radius = item.roundRadius;
|
||||
icon.invalidate();
|
||||
|
||||
ViewGroup.LayoutParams params = icon.getLayoutParams();
|
||||
params.width = params.height = radius != 0 ? ViewUtils.dp(42) : ViewUtils.dp(28);
|
||||
if (item.mForceDark) {
|
||||
onApplyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
BeamTheme theme = item != null && item.mForceDark ? BeamTheme.DARK : ThemesRepo.getCurrent();
|
||||
title.setTextColor(theme.colors.get(android.R.attr.textColorPrimary));
|
||||
subtitle.setTextColor(theme.colors.get(android.R.attr.textColorSecondary));
|
||||
value.setTextColor(theme.colors.get(android.R.attr.textColorSecondary));
|
||||
icon.setImageTintList(ColorStateList.valueOf(theme.colors.get(android.R.attr.textColorSecondary)));
|
||||
setBackground(ViewUtils.createRipple(theme.colors.get(android.R.attr.colorControlHighlight), 16));
|
||||
}
|
||||
}
|
||||
|
||||
public interface ValueProvider {
|
||||
CharSequence provide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.theme.IThemeView;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.BeamSwitch;
|
||||
|
||||
public class PreferenceSwitchItem extends SimpleRecyclerItem<PreferenceSwitchItem.SwitchPreferenceHolderView> {
|
||||
private Drawable mIcon;
|
||||
private CharSequence mTitle;
|
||||
private CharSequence mSubtitle;
|
||||
private String mKey;
|
||||
private boolean mDefaultValue;
|
||||
private CompoundButton.OnCheckedChangeListener mChangeListener;
|
||||
private View.OnLongClickListener mLongClickListener;
|
||||
private ValueProvider valueProvider;
|
||||
|
||||
public PreferenceSwitchItem setValueProvider(ValueProvider valueProvider) {
|
||||
this.valueProvider = valueProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setKeyAndDefaultValue(String key, boolean value) {
|
||||
mKey = key;
|
||||
mDefaultValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setChangeListener(CompoundButton.OnCheckedChangeListener listener) {
|
||||
mChangeListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setLongClickListener(View.OnLongClickListener longClickListener) {
|
||||
mLongClickListener = longClickListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setTitle(CharSequence title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setSubtitle(CharSequence subtitle) {
|
||||
mSubtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setIcon(int iconRes) {
|
||||
mIcon = ContextCompat.getDrawable(Santoku.INSTANCE, iconRes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceSwitchItem setIcon(Drawable drawable) {
|
||||
mIcon = drawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SwitchPreferenceHolderView onCreateView(Context ctx) {
|
||||
return new SwitchPreferenceHolderView(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(SwitchPreferenceHolderView view) {
|
||||
view.bind(this);
|
||||
}
|
||||
|
||||
public final static class SwitchPreferenceHolderView extends LinearLayout implements IThemeView {
|
||||
public TextView title, subtitle;
|
||||
public ImageView icon;
|
||||
public BeamSwitch matSwitch;
|
||||
|
||||
public SwitchPreferenceHolderView(Context context) {
|
||||
super(context);
|
||||
|
||||
setOrientation(HORIZONTAL);
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
|
||||
icon = new ImageView(context);
|
||||
icon.setLayoutParams(new LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
|
||||
setMarginStart(ViewUtils.dp(4));
|
||||
setMarginEnd(ViewUtils.dp(8));
|
||||
}});
|
||||
addView(icon);
|
||||
|
||||
LinearLayout innerLayout = new LinearLayout(context);
|
||||
innerLayout.setOrientation(VERTICAL);
|
||||
innerLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
|
||||
title = new TextView(context);
|
||||
title.setEllipsize(TextUtils.TruncateAt.END);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
innerLayout.addView(title);
|
||||
|
||||
subtitle = new TextView(context);
|
||||
subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
|
||||
innerLayout.addView(subtitle);
|
||||
|
||||
addView(innerLayout, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
setMarginStart(ViewUtils.dp(8));
|
||||
setMarginEnd(ViewUtils.dp(8));
|
||||
gravity = Gravity.CENTER_VERTICAL;
|
||||
}});
|
||||
|
||||
addView(matSwitch = new BeamSwitch(context), new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewUtils.dp(32)));
|
||||
|
||||
int pad = ViewUtils.dp(12);
|
||||
setPadding(pad, pad, pad, pad);
|
||||
setMinimumHeight(ViewUtils.dp(52));
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
void bind(PreferenceSwitchItem item) {
|
||||
title.setText(item.mTitle);
|
||||
subtitle.setText(item.mSubtitle);
|
||||
if (TextUtils.isEmpty(item.mSubtitle)) {
|
||||
subtitle.setVisibility(GONE);
|
||||
} else {
|
||||
subtitle.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (item.mIcon != null) {
|
||||
icon.setVisibility(VISIBLE);
|
||||
icon.setImageDrawable(item.mIcon);
|
||||
} else {
|
||||
icon.setVisibility(GONE);
|
||||
}
|
||||
if (item.mKey != null) {
|
||||
matSwitch.setChecked(Prefs.getPrefs().getBoolean(item.mKey, item.mDefaultValue));
|
||||
} else {
|
||||
matSwitch.setChecked(item.valueProvider.provide());
|
||||
}
|
||||
setOnClickListener(v -> {
|
||||
boolean check;
|
||||
if (item.mKey != null) {
|
||||
check = !Prefs.getPrefs().getBoolean(item.mKey, item.mDefaultValue);
|
||||
Prefs.getPrefs().edit().putBoolean(item.mKey, check).apply();
|
||||
matSwitch.setChecked(check);
|
||||
} else {
|
||||
matSwitch.setChecked(check = !matSwitch.isChecked());
|
||||
}
|
||||
|
||||
if (item.mChangeListener != null) {
|
||||
item.mChangeListener.onCheckedChanged(matSwitch, check);
|
||||
}
|
||||
});
|
||||
setOnLongClickListener(item.mLongClickListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
}
|
||||
}
|
||||
|
||||
public interface ValueProvider {
|
||||
boolean provide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SimpleRecyclerAdapter extends RecyclerView.Adapter {
|
||||
private Map<Class<?>, Integer> viewType = new HashMap<>();
|
||||
private Map<Integer, SimpleRecyclerItem> viewCreator = new HashMap<>();
|
||||
private int lastType;
|
||||
|
||||
private List<SimpleRecyclerItem> items = new ArrayList<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new RecyclerView.ViewHolder(viewCreator.get(viewType).onCreateView(parent.getContext())) {};
|
||||
}
|
||||
|
||||
/** @noinspection unchecked*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
items.get(position).onBindView(holder.itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return items.get(position).hashCode();
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
public void setItems(List<SimpleRecyclerItem> items) {
|
||||
this.items = items;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Integer t = viewType.get(items.get(position).getClass());
|
||||
if (t == null) {
|
||||
viewType.put(items.get(position).getClass(), t = lastType++);
|
||||
viewCreator.put(t, items.get(position));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
public List<SimpleRecyclerItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
public abstract class SimpleRecyclerItem<V extends View> {
|
||||
public abstract V onCreateView(Context ctx);
|
||||
public void onBindView(V view) {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Space;
|
||||
|
||||
public class SpaceItem extends SimpleRecyclerItem<Space> {
|
||||
private int x, y;
|
||||
|
||||
public SpaceItem(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Space onCreateView(Context ctx) {
|
||||
return new Space(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(Space view) {
|
||||
view.setMinimumWidth(x);
|
||||
view.setMinimumHeight(y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.dark98.santoku.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class TextHintRecyclerItem extends SimpleRecyclerItem<TextView> {
|
||||
public String title;
|
||||
|
||||
public TextHintRecyclerItem() {}
|
||||
|
||||
public TextHintRecyclerItem(String t) {
|
||||
title = t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView onCreateView(Context ctx) {
|
||||
TextView tv = new TextView(ctx);
|
||||
tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
|
||||
tv.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
tv.setPadding(ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12));
|
||||
tv.setGravity(Gravity.CENTER);
|
||||
tv.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return tv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(TextView view) {
|
||||
view.setText(title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.dark98.santoku.render;
|
||||
|
||||
import androidx.core.math.MathUtils;
|
||||
|
||||
import com.dark98.santoku.utils.DoubleMatrix;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
|
||||
public class Camera {
|
||||
private double[] viewMatrix = new double[16];
|
||||
|
||||
public Vec3d position = new Vec3d(0, 0, 0);
|
||||
public Vec3d origin = new Vec3d(0, 0, 0);
|
||||
public Vec3d up = new Vec3d(0, 0, 1);
|
||||
|
||||
private double[] tempMatrix = new double[16];
|
||||
private float zoom = 1f;
|
||||
|
||||
public Vec3d getDirToBed() {
|
||||
return origin.clone().multiply(1, 1, 0).add(position.clone().negate()).normalize();
|
||||
}
|
||||
|
||||
public Vec3d getDirForward() {
|
||||
return origin.clone().add(position.clone().negate()).normalize();
|
||||
}
|
||||
|
||||
public double[] getViewModelMatrix() {
|
||||
DoubleMatrix.setIdentityM(viewMatrix, 0);
|
||||
DoubleMatrix.setLookAtM(viewMatrix, 0, position.x, position.y, position.z, origin.x, origin.y, origin.z, up.x, up.y, up.z);
|
||||
return viewMatrix;
|
||||
}
|
||||
|
||||
public float getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
public void zoom(float zoom) {
|
||||
this.zoom = MathUtils.clamp(this.zoom + zoom / 25f, 1f, 10f);
|
||||
}
|
||||
|
||||
public void setZoom(float zoom) {
|
||||
this.zoom = zoom;
|
||||
}
|
||||
|
||||
public Vec3d calcScreenMovement(float x, float y) {
|
||||
x /= zoom;
|
||||
y /= zoom;
|
||||
|
||||
Vec3d dir = getDirForward();
|
||||
double yaw = Math.atan2(dir.x, dir.y);
|
||||
double pitch = Math.asin(-dir.z);
|
||||
Vec3d upMod = up.clone();
|
||||
upMod.x = Math.sin(pitch) * Math.sin(yaw);
|
||||
upMod.y = Math.sin(pitch) * Math.cos(yaw);
|
||||
upMod.z = Math.cos(pitch);
|
||||
|
||||
Vec3d right = dir.crossProduct(upMod);
|
||||
|
||||
Vec3d screenY = dir.crossProduct(right);
|
||||
Vec3d screenX = right.clone();
|
||||
|
||||
screenX.multiply(x);
|
||||
screenY.multiply(y);
|
||||
|
||||
return new Vec3d(screenX).add(screenY);
|
||||
}
|
||||
public void move(float x, float y) {
|
||||
Vec3d move = calcScreenMovement(x, y);
|
||||
position.add(move);
|
||||
origin.add(move);
|
||||
}
|
||||
|
||||
public void rotateAround(double rx, double ry) {
|
||||
double[] v = position.clone().add(origin.clone().negate()).asDoubleArray();
|
||||
|
||||
DoubleMatrix.setIdentityM(tempMatrix, 0);
|
||||
|
||||
Vec3d dir = getDirForward();
|
||||
double yaw = Math.atan2(dir.x, dir.y);
|
||||
double pitch = Math.toDegrees(Math.asin(-dir.z));
|
||||
|
||||
double mry = -ry;
|
||||
if (pitch + mry > 90) {
|
||||
mry = 0;
|
||||
} else if (pitch + mry < -90) {
|
||||
mry = 0;
|
||||
}
|
||||
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, -mry * Math.cos(yaw), 1, 0, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, mry * Math.sin(yaw), 0, 1, 0);
|
||||
DoubleMatrix.rotateM(tempMatrix, 0, rx, 0, 0, 1);
|
||||
|
||||
DoubleMatrix.multiplyMV(v, 0, tempMatrix, 0, v, 0);
|
||||
position.set(v[0] / v[3], v[1] / v[3], v[2] / v[3]);
|
||||
position.add(origin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.dark98.santoku.render;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.slic3r.GLModel;
|
||||
import com.dark98.santoku.slic3r.GLShaderProgram;
|
||||
import com.dark98.santoku.slic3r.GLShadersManager;
|
||||
import com.dark98.santoku.slic3r.Slic3rUtils;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.DoubleMatrix;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
|
||||
public class CoordAxes {
|
||||
public Vec3d origin = new Vec3d(0, 0, 0);
|
||||
|
||||
private float stemRadius = 0.5f;
|
||||
private float stemLength = 25.0f;
|
||||
private float tipRadius = 2.5f * stemRadius;
|
||||
private float tipLength = 5.0f;
|
||||
|
||||
private GLModel arrow = new GLModel();
|
||||
|
||||
public void setStemLength(float stemLength) {
|
||||
this.stemLength = stemLength;
|
||||
arrow.reset();
|
||||
}
|
||||
|
||||
private double[] matrix = new double[16];
|
||||
private double[] matrix2 = new double[16];
|
||||
private double[] matrix3 = new double[16];
|
||||
|
||||
private double[] normals = new double[12];
|
||||
|
||||
private void renderAxis(GLShaderProgram shader, double[] viewMatrix, double[] projectionMatrix) {
|
||||
DoubleMatrix.multiplyMM(matrix3, 0, viewMatrix, 0, matrix2, 0);
|
||||
shader.setUniformMatrix4fv("view_model_matrix", matrix3);
|
||||
shader.setUniformMatrix4fv("projection_matrix", projectionMatrix);
|
||||
Slic3rUtils.calcViewNormalMatrix(viewMatrix, matrix2, normals);
|
||||
shader.setUniformMatrix3fv("view_normal_matrix", normals);
|
||||
arrow.render();
|
||||
}
|
||||
|
||||
public void render(GLShadersManager shadersManager, double[] viewMatrix, double[] projectionMatrix, float emissionFactor, float invZoom) {
|
||||
if (!arrow.isInitialized()) {
|
||||
arrow.stilizedArrow(tipRadius, tipLength, stemRadius, stemLength);
|
||||
}
|
||||
|
||||
GLShaderProgram currentShader = shadersManager.getCurrentShader();
|
||||
GLShaderProgram shader = shadersManager.get(GLShadersManager.SHADER_GOURAUD_LIGHT);
|
||||
if (currentShader != null) {
|
||||
currentShader.stopUsing();
|
||||
}
|
||||
|
||||
shader.startUsing();
|
||||
shader.setUniform("emission_factor", emissionFactor);
|
||||
|
||||
DoubleMatrix.setIdentityM(matrix, 0);
|
||||
float scale = Math.min(1, invZoom * 2f);
|
||||
DoubleMatrix.scaleM(matrix, 0, scale, scale, scale);
|
||||
|
||||
arrow.setColor(ThemesRepo.getColor(R.attr.xTrackColor));
|
||||
DoubleMatrix.setIdentityM(matrix2, 0);
|
||||
DoubleMatrix.translateM(matrix2, 0, origin.x, origin.y, origin.z);
|
||||
DoubleMatrix.rotateM(matrix2, 0, 90f, 0, 1, 0);
|
||||
DoubleMatrix.multiplyMM(matrix2, 0, matrix, 0, matrix2, 0);
|
||||
renderAxis(shader, viewMatrix, projectionMatrix);
|
||||
|
||||
arrow.setColor(ThemesRepo.getColor(R.attr.yTrackColor));
|
||||
DoubleMatrix.setIdentityM(matrix2, 0);
|
||||
DoubleMatrix.translateM(matrix2, 0, origin.x, origin.y, origin.z);
|
||||
DoubleMatrix.rotateM(matrix2, 0, -90f, 1, 0, 0);
|
||||
DoubleMatrix.multiplyMM(matrix2, 0, matrix, 0, matrix2, 0);
|
||||
renderAxis(shader, viewMatrix, projectionMatrix);
|
||||
|
||||
arrow.setColor(ThemesRepo.getColor(R.attr.zTrackColor));
|
||||
DoubleMatrix.setIdentityM(matrix2, 0);
|
||||
DoubleMatrix.translateM(matrix2, 0, origin.x, origin.y, origin.z);
|
||||
DoubleMatrix.multiplyMM(matrix2, 0, matrix, 0, matrix2, 0);
|
||||
renderAxis(shader, viewMatrix, projectionMatrix);
|
||||
|
||||
shader.stopUsing();
|
||||
if (currentShader != null) {
|
||||
currentShader.startUsing();
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
arrow.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,864 @@
|
||||
package com.dark98.santoku.render;
|
||||
|
||||
import static android.opengl.GLES30.*;
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.util.Log;
|
||||
|
||||
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;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.events.ObjectsListChangedEvent;
|
||||
import com.dark98.santoku.events.SelectedObjectChangedEvent;
|
||||
import com.dark98.santoku.slic3r.Bed3D;
|
||||
import com.dark98.santoku.slic3r.GCodeProcessorResult;
|
||||
import com.dark98.santoku.slic3r.GCodeViewer;
|
||||
import com.dark98.santoku.slic3r.GLModel;
|
||||
import com.dark98.santoku.slic3r.GLShaderProgram;
|
||||
import com.dark98.santoku.slic3r.GLShadersManager;
|
||||
import com.dark98.santoku.slic3r.Model;
|
||||
import com.dark98.santoku.slic3r.Slic3rUtils;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.DoubleMatrix;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
import com.dark98.santoku.view.GLView;
|
||||
|
||||
public class GLRenderer implements GLSurfaceView.Renderer {
|
||||
private final static float FOV = 60f;
|
||||
private final static float NEAR_PLANE = 10f;
|
||||
private final static float FAR_PLANE = 1000f;
|
||||
|
||||
private Camera camera = new Camera();
|
||||
private double[] projectionMatrix = new double[16];
|
||||
private double[] modelMatrix = new double[16];
|
||||
private double[] normalMatrix = new double[12];
|
||||
private double[] outModelMatrix = new double[16];
|
||||
|
||||
private int viewportWidth, viewportHeight;
|
||||
|
||||
private boolean cameraIsDirty = true;
|
||||
|
||||
// Instance values, should be released
|
||||
private Bed3D bed;
|
||||
private boolean bedVisible = true;
|
||||
private int lastConfigUid;
|
||||
private GLShadersManager shadersManager;
|
||||
private GLModel backgroundModel;
|
||||
private GLModel selectionModel;
|
||||
private List<GLModel> glModels = new ArrayList<>();
|
||||
|
||||
private Model model;
|
||||
|
||||
private GCodeProcessorResult gcodeResult;
|
||||
private GCodeViewer viewer;
|
||||
private boolean isViewerEnabled;
|
||||
|
||||
private int selectedObject = -1;
|
||||
private double selX, selY, selZ;
|
||||
private double selRotX, selRotY, selRotZ;
|
||||
private double selScaleX = 1, selScaleY = 1, selScaleZ = 1;
|
||||
|
||||
private long lastDraw;
|
||||
private GLView glView;
|
||||
private Vec3d translate = new Vec3d();
|
||||
private Vec3d rotate = new Vec3d();
|
||||
private ArrayList<GLModel.HitResult> raycastHits = new ArrayList<>();
|
||||
|
||||
private Vec3d bbMin = new Vec3d(), bbMax = new Vec3d();
|
||||
private boolean isInFlattenMode;
|
||||
private ArrayList<GLModel> flattenPlanes = new ArrayList<>();
|
||||
private static final double TOP_VIEW_MARGIN = 1.1;
|
||||
|
||||
public Camera getCamera() {
|
||||
return camera;
|
||||
}
|
||||
|
||||
public Bed3D getBed() {
|
||||
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;
|
||||
}
|
||||
|
||||
public int getViewportWidth() {
|
||||
return viewportWidth;
|
||||
}
|
||||
|
||||
public int getViewportHeight() {
|
||||
return viewportHeight;
|
||||
}
|
||||
|
||||
public void setGCodeViewer(GCodeProcessorResult result) {
|
||||
this.isViewerEnabled = result != null;
|
||||
this.gcodeResult = result;
|
||||
|
||||
if (!isViewerEnabled && viewer != null) {
|
||||
viewer.release();
|
||||
viewer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public GLRenderer(GLView glView) {
|
||||
this.glView = glView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
if (bed != null) {
|
||||
onDestroy();
|
||||
}
|
||||
onCreate();
|
||||
glViewport(0, 0, viewportWidth = width, viewportHeight = height);
|
||||
updateProjection();
|
||||
}
|
||||
|
||||
public void updateProjection() {
|
||||
if (bed == null || !bed.isValid()) return;
|
||||
float aspectRatio = (float) viewportWidth / viewportHeight;
|
||||
float invZoom = 1f / camera.getZoom();
|
||||
if (Prefs.isOrthoProjectionEnabled()) {
|
||||
Vec3d diff = bed.getVolumeMax().clone().add(bed.getVolumeMin().clone());
|
||||
double scale = (Math.max(diff.x, diff.y) / 2f + 10f) * invZoom;
|
||||
|
||||
float ratioHorizontal = aspectRatio > 1 ? aspectRatio : 1;
|
||||
float ratioVertical = aspectRatio < 1 ? 1f / aspectRatio : 1;
|
||||
DoubleMatrix.orthoM(projectionMatrix, 0, -scale * ratioHorizontal, scale * ratioHorizontal, -scale * ratioVertical, scale * ratioVertical, NEAR_PLANE, FAR_PLANE);
|
||||
} else {
|
||||
DoubleMatrix.perspectiveM(projectionMatrix, 0, FOV * invZoom * (viewportWidth > viewportHeight ? 1 / aspectRatio : 1), aspectRatio, NEAR_PLANE, FAR_PLANE);
|
||||
}
|
||||
}
|
||||
|
||||
public int getSelectedObject() {
|
||||
return selectedObject;
|
||||
}
|
||||
|
||||
public void invalidateGlModel(int i) {
|
||||
if (model == null) return;
|
||||
if (i < glModels.size()) {
|
||||
GLModel glModel = glModels.get(i);
|
||||
glModel.reset();
|
||||
glModel.initFrom(model, i);
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateSelectionObject() {
|
||||
if (selectionModel != null) {
|
||||
selectionModel.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetGlModels() {
|
||||
if (model == null) return;
|
||||
for (int i = 0; i < model.getObjectsCount(); i++) {
|
||||
if (i >= glModels.size()) continue;
|
||||
|
||||
GLModel glModel = glModels.get(i);
|
||||
glModel.reset();
|
||||
glModel.initFrom(model, i);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean invalidateFlattenMode() {
|
||||
if (isInFlattenMode) {
|
||||
setInFlattenMode(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean resetFlattenMode() {
|
||||
if (isInFlattenMode) {
|
||||
setInFlattenMode(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setInFlattenMode(boolean inFlattenMode) {
|
||||
isInFlattenMode = inFlattenMode;
|
||||
|
||||
for (int i = 0, c = flattenPlanes.size(); i < c; i++) {
|
||||
flattenPlanes.get(i).release();
|
||||
}
|
||||
flattenPlanes.clear();
|
||||
|
||||
if (isInFlattenMode) {
|
||||
List<GLModel> planes = model.createFlattenPlanes(selectedObject);
|
||||
flattenPlanes.addAll(planes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
if (backgroundModel == null) return; // Not initialized yet
|
||||
long dt = Math.min(System.currentTimeMillis() - lastDraw, 16);
|
||||
lastDraw = System.currentTimeMillis();
|
||||
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
GLShaderProgram shader = shadersManager.get(GLShadersManager.SHADER_BACKGROUND);
|
||||
shader.startUsing();
|
||||
shader.setUniformColor("top_color", ThemesRepo.getColor(R.attr.backgroundColorTop));
|
||||
shader.setUniformColor("bottom_color", ThemesRepo.getColor(R.attr.backgroundColorBottom));
|
||||
backgroundModel.render();
|
||||
shader.stopUsing();
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
boolean bottom = Prefs.isOrthoProjectionEnabled() ? camera.getDirForward().z > 0 : camera.getDirToBed().z > 0;
|
||||
if (lastConfigUid != Santoku.CONFIG_UID) {
|
||||
configureBed();
|
||||
}
|
||||
if (bed.isValid() && bedVisible) {
|
||||
bed.render(shadersManager, bottom, camera.getViewModelMatrix(), projectionMatrix, 1f / camera.getZoom());
|
||||
}
|
||||
|
||||
if (isViewerEnabled) {
|
||||
if (viewer == null) {
|
||||
viewer = new GCodeViewer();
|
||||
viewer.initGL();
|
||||
viewer.setThemeColors();
|
||||
viewer.load(gcodeResult);
|
||||
}
|
||||
|
||||
viewer.render(camera.getViewModelMatrix(), projectionMatrix);
|
||||
}
|
||||
if (viewer == null && model != null) {
|
||||
shader = shadersManager.get(GLShadersManager.SHADER_GOURAUD_LIGHT);
|
||||
shader.startUsing();
|
||||
int color = ThemesRepo.getColor(android.R.attr.colorAccent);
|
||||
int hoverColor = ThemesRepo.getColor(R.attr.modelHoverColor);
|
||||
|
||||
for (int i = 0; i < model.getObjectsCount(); i++) {
|
||||
boolean left = model.isLeftHanded(i);
|
||||
if (left) {
|
||||
glFrontFace(GL_CW);
|
||||
}
|
||||
|
||||
boolean selected = i == selectedObject;
|
||||
|
||||
shader.setUniform("emission_factor", 0.05f);
|
||||
DoubleMatrix.setIdentityM(modelMatrix, 0);
|
||||
if (selected) {
|
||||
DoubleMatrix.translateM(modelMatrix, 0, selX, selY, selZ);
|
||||
|
||||
model.getTranslation(i, translate);
|
||||
model.getRotation(i, rotate);
|
||||
DoubleMatrix.translateM(modelMatrix, 0, translate.x, translate.y, translate.z);
|
||||
DoubleMatrix.rotateM(modelMatrix, 0, selRotX, 1, 0, 0);
|
||||
DoubleMatrix.rotateM(modelMatrix, 0, selRotY, 0, 1, 0);
|
||||
DoubleMatrix.rotateM(modelMatrix, 0, selRotZ, 0, 0, 1);
|
||||
DoubleMatrix.scaleM(modelMatrix, 0, selScaleX, selScaleY, selScaleZ);
|
||||
DoubleMatrix.translateM(modelMatrix, 0, -translate.x, -translate.y, -translate.z);
|
||||
}
|
||||
DoubleMatrix.multiplyMM(outModelMatrix, 0, camera.getViewModelMatrix(), 0, modelMatrix, 0);
|
||||
|
||||
shader.setUniformMatrix4fv("view_model_matrix", outModelMatrix);
|
||||
shader.setUniformMatrix4fv("projection_matrix", projectionMatrix);
|
||||
|
||||
Slic3rUtils.calcViewNormalMatrix(camera.getViewModelMatrix(), modelMatrix, normalMatrix);
|
||||
shader.setUniformMatrix3fv("view_normal_matrix", normalMatrix);
|
||||
|
||||
shader.setUniform("volume_mirrored", left);
|
||||
|
||||
if (glModels.size() < i + 1) {
|
||||
GLModel glModel = new GLModel();
|
||||
glModel.initFrom(model, i);
|
||||
glModels.add(glModel);
|
||||
}
|
||||
GLModel glModel = glModels.get(i);
|
||||
boolean hovering = glModel.isHovering || selectedObject == i;
|
||||
// FIXME: Render is lagging out with hover progress
|
||||
// if (hovering && glModel.hoverProgress < 1) {
|
||||
// glModel.hoverProgress = Math.min(glModel.hoverProgress + dt / 50f, 1);
|
||||
// glView.queueEvent(() -> glView.requestRender());
|
||||
// } else if (!hovering && glModel.hoverProgress > 0) {
|
||||
// glModel.hoverProgress = Math.max(glModel.hoverProgress - dt / 50f, 0);
|
||||
// glView.queueEvent(() -> glView.requestRender());
|
||||
// }
|
||||
glModel.setColor(ColorUtils.blendARGB(color, hoverColor, hovering ? 1 : 0));
|
||||
glModel.render();
|
||||
|
||||
if (left) {
|
||||
glFrontFace(GL_CCW);
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
shader.stopUsing();
|
||||
|
||||
GLShaderProgram flat = shadersManager.get(GLShadersManager.SHADER_FLAT);
|
||||
glLineWidth(ViewUtils.dp(1.5f));
|
||||
|
||||
flat.startUsing();
|
||||
flat.setUniformMatrix4fv("view_model_matrix", outModelMatrix);
|
||||
flat.setUniformMatrix4fv("projection_matrix", projectionMatrix);
|
||||
|
||||
if (selectionModel == null) {
|
||||
selectionModel = new GLModel();
|
||||
}
|
||||
selectionModel.initBoundingBox(model, i);
|
||||
selectionModel.setColor(hoverColor);
|
||||
selectionModel.render();
|
||||
|
||||
flat.stopUsing();
|
||||
|
||||
shader.startUsing();
|
||||
}
|
||||
|
||||
if (isInFlattenMode) {
|
||||
shader.stopUsing();
|
||||
|
||||
GLShaderProgram flat = shadersManager.get(GLShadersManager.SHADER_FLAT);
|
||||
|
||||
flat.startUsing();
|
||||
glEnable(GL_BLEND);
|
||||
flat.setUniformMatrix4fv("view_model_matrix", outModelMatrix);
|
||||
flat.setUniformMatrix4fv("projection_matrix", projectionMatrix);
|
||||
|
||||
for (GLModel plane : flattenPlanes) {
|
||||
boolean hoveringPlane = plane.isHovering;
|
||||
int clr = ColorUtils.blendARGB(hoverColor, color, hoveringPlane ? 1 : 0);
|
||||
plane.setColor(ColorUtils.setAlphaComponent(clr, (int) (Color.alpha(clr) * 0.75f)));
|
||||
plane.render();
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
flat.stopUsing();
|
||||
|
||||
shader.startUsing();
|
||||
}
|
||||
}
|
||||
shader.stopUsing();
|
||||
}
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
public boolean deleteObject(int i) {
|
||||
if (model == null) return false;
|
||||
assertTrue(i >= 0 && i < model.getObjectsCount());
|
||||
|
||||
model.deleteObject(i);
|
||||
if (glModels.size() > i) {
|
||||
glModels.remove(i).release();
|
||||
}
|
||||
if (i == selectedObject) {
|
||||
selectedObject = -1;
|
||||
selX = selY = selZ = 0;
|
||||
selRotX = selRotY = selRotZ = 0;
|
||||
selScaleX = selScaleY = selScaleZ = 1;
|
||||
Santoku.EVENT_BUS.fireEvent(new SelectedObjectChangedEvent());
|
||||
}
|
||||
|
||||
if (model.getObjectsCount() == 0) {
|
||||
model.release();
|
||||
model = null;
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new ObjectsListChangedEvent());
|
||||
return true;
|
||||
}
|
||||
|
||||
public int raycastObjectIndex(float x, float y) {
|
||||
if (model == null) return -1;
|
||||
double minDistance = Double.MAX_VALUE;
|
||||
int j = -1;
|
||||
for (int i = 0, c = model.getObjectsCount(); i < c; i++) {
|
||||
if (i >= glModels.size()) continue;
|
||||
|
||||
GLModel glModel = glModels.get(i);
|
||||
glModel.getRaycaster().raycast(this, raycastHits, x, y);
|
||||
|
||||
boolean hovered = !raycastHits.isEmpty();
|
||||
if (hovered) {
|
||||
double distance = raycastHits.get(0).position.distance(camera.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
j = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
public boolean onClick(float x, float y) {
|
||||
if (model == null || isViewerEnabled) return false;
|
||||
|
||||
int j = raycastObjectIndex(x, y);
|
||||
|
||||
if (isInFlattenMode && (j == selectedObject || j == -1)) {
|
||||
int minPlane = -1;
|
||||
double minDistancePlane = Double.MAX_VALUE;
|
||||
|
||||
for (int i = 0, c = flattenPlanes.size(); i < c; i++) {
|
||||
GLModel glModel = flattenPlanes.get(i);
|
||||
glModel.getRaycaster().raycast(this, raycastHits, x, y);
|
||||
|
||||
double minDistanceRay = Double.MAX_VALUE;
|
||||
if (!raycastHits.isEmpty()) {
|
||||
for (GLModel.HitResult res : raycastHits) {
|
||||
double distance = res.position.distance(camera.position);
|
||||
if (distance < minDistanceRay) {
|
||||
minDistanceRay = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minDistanceRay < minDistancePlane) {
|
||||
minDistancePlane = minDistanceRay;
|
||||
minPlane = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (minPlane != -1) {
|
||||
GLModel glModel = flattenPlanes.get(minPlane);
|
||||
model.flattenRotate(selectedObject, glModel);
|
||||
model.ensureOnBed(selectedObject);
|
||||
|
||||
invalidateGlModel(selectedObject);
|
||||
for (int k = 0, l = flattenPlanes.size(); k < l; k++) {
|
||||
flattenPlanes.get(k).release();
|
||||
}
|
||||
flattenPlanes.clear();
|
||||
|
||||
selectedObject = -1;
|
||||
Santoku.EVENT_BUS.fireEvent(new SelectedObjectChangedEvent());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean render = j != selectedObject || j != -1;
|
||||
selectedObject = j == selectedObject ? -1 : j;
|
||||
if (render) {
|
||||
if (isInFlattenMode) {
|
||||
setInFlattenMode(false);
|
||||
}
|
||||
if (selectedObject == -1) {
|
||||
selX = selY = selZ = 0;
|
||||
selRotX = selRotY = selRotZ = 0;
|
||||
selScaleX = selScaleY = selScaleZ = 1;
|
||||
}
|
||||
Santoku.EVENT_BUS.fireEvent(new SelectedObjectChangedEvent());
|
||||
}
|
||||
return render;
|
||||
}
|
||||
|
||||
public boolean hover(float x, float y) {
|
||||
if (model == null || isViewerEnabled) return false;
|
||||
|
||||
boolean render = false;
|
||||
double minDistance = Double.MAX_VALUE;
|
||||
GLModel minModel = null;
|
||||
for (int i = 0, c = model.getObjectsCount(); i < c; i++) {
|
||||
if (i >= glModels.size()) continue;
|
||||
|
||||
GLModel glModel = glModels.get(i);
|
||||
glModel.getRaycaster().raycast(this, raycastHits, x, y);
|
||||
|
||||
boolean hovered = !raycastHits.isEmpty();
|
||||
if (hovered) {
|
||||
double distance = raycastHits.get(0).position.distance(camera.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
minModel = glModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0, c = model.getObjectsCount(); i < c; i++) {
|
||||
if (i >= glModels.size()) continue;
|
||||
|
||||
GLModel glModel = glModels.get(i);
|
||||
|
||||
boolean hovered = minModel == glModel;
|
||||
if (glModel.isHovering && !hovered) {
|
||||
glModel.isHovering = false;
|
||||
render = true;
|
||||
} else if (!glModel.isHovering && hovered) {
|
||||
glModel.isHovering = true;
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInFlattenMode) {
|
||||
int minPlane = -1;
|
||||
double minDistancePlane = Double.MAX_VALUE;
|
||||
|
||||
for (int i = 0, c = flattenPlanes.size(); i < c; i++) {
|
||||
GLModel glModel = flattenPlanes.get(i);
|
||||
glModel.getRaycaster().raycast(this, raycastHits, x, y);
|
||||
|
||||
double minDistanceRay = Double.MAX_VALUE;
|
||||
if (!raycastHits.isEmpty()) {
|
||||
for (GLModel.HitResult res : raycastHits) {
|
||||
double distance = res.position.distance(camera.position);
|
||||
if (distance < minDistanceRay) {
|
||||
minDistanceRay = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minDistanceRay < minDistancePlane) {
|
||||
minDistancePlane = minDistanceRay;
|
||||
minPlane = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (minPlane != -1) {
|
||||
for (int i = 0; i < flattenPlanes.size(); i++) {
|
||||
flattenPlanes.get(i).isHovering = i == minPlane;
|
||||
}
|
||||
render = true;
|
||||
} else {
|
||||
for (int i = 0; i < flattenPlanes.size(); i++) {
|
||||
if (flattenPlanes.get(i).isHovering) {
|
||||
flattenPlanes.get(i).isHovering = false;
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return render;
|
||||
}
|
||||
|
||||
public boolean stopHover() {
|
||||
if (model == null) return false;
|
||||
|
||||
boolean render = false;
|
||||
for (int i = 0, c = model.getObjectsCount(); i < c; i++) {
|
||||
if (i >= glModels.size()) continue;
|
||||
|
||||
GLModel glModel = glModels.get(i);
|
||||
if (glModel.isHovering) {
|
||||
glModel.isHovering = false;
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
return render;
|
||||
}
|
||||
|
||||
public void setSelectionRotation(double x, double y, double z) {
|
||||
selRotX = x;
|
||||
selRotY = y;
|
||||
selRotZ = z;
|
||||
}
|
||||
|
||||
public void setSelectionScale(double x, double y, double z) {
|
||||
selScaleX = x;
|
||||
selScaleY = y;
|
||||
selScaleZ = z;
|
||||
}
|
||||
|
||||
public void setSelectionTranslation(double x, double y, double z) {
|
||||
selX = x;
|
||||
selY = y;
|
||||
selZ = z;
|
||||
}
|
||||
|
||||
public void setModel(Model model) {
|
||||
this.model = model;
|
||||
resetGlModels();
|
||||
}
|
||||
|
||||
public Model getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public GCodeProcessorResult getGcodeResult() {
|
||||
return gcodeResult;
|
||||
}
|
||||
|
||||
public GCodeViewer getViewer() {
|
||||
return viewer;
|
||||
}
|
||||
|
||||
private void configureBed() {
|
||||
try {
|
||||
lastConfigUid = Santoku.CONFIG_UID;
|
||||
Santoku.genCurrentConfig();
|
||||
bed.configure(Santoku.getCurrentConfigFile());
|
||||
} catch (Exception e) {
|
||||
Log.e("GLRenderer", "Failed to update config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onCreate() {
|
||||
bed = new Bed3D();
|
||||
configureBed();
|
||||
|
||||
backgroundModel = new GLModel();
|
||||
backgroundModel.initBackgroundTriangles();
|
||||
shadersManager = new GLShadersManager();
|
||||
if (!bed.isValid()) return;
|
||||
|
||||
if (cameraIsDirty) {
|
||||
Vec3d min = bed.getVolumeMin(), max = bed.getVolumeMax();
|
||||
Vec3d center = min.center(max);
|
||||
camera.origin.set(center);
|
||||
camera.origin.z = 0;
|
||||
|
||||
camera.position.x = center.x - center.z * 2;
|
||||
camera.position.y = center.y - center.z * 2;
|
||||
camera.position.z = min.z + Math.sqrt(center.z * center.z * 8);
|
||||
cameraIsDirty = false;
|
||||
}
|
||||
if (isViewerEnabled) {
|
||||
viewer = new GCodeViewer();
|
||||
viewer.initGL();
|
||||
viewer.setThemeColors();
|
||||
viewer.load(gcodeResult);
|
||||
}
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
if (shadersManager != null) {
|
||||
shadersManager.clearShaders();
|
||||
shadersManager = null;
|
||||
}
|
||||
if (backgroundModel != null) {
|
||||
backgroundModel.release();
|
||||
backgroundModel = null;
|
||||
}
|
||||
if (selectionModel != null) {
|
||||
selectionModel.release();
|
||||
selectionModel = null;
|
||||
}
|
||||
if (bed != null) {
|
||||
bed.release();
|
||||
bed = null;
|
||||
}
|
||||
if (viewer != null) {
|
||||
viewer.release();
|
||||
viewer = null;
|
||||
}
|
||||
for (int i = 0; i < glModels.size(); i++) {
|
||||
glModels.get(i).release();
|
||||
}
|
||||
glModels.clear();
|
||||
|
||||
isInFlattenMode = false;
|
||||
for (int i = 0; i < flattenPlanes.size(); i++) {
|
||||
flattenPlanes.get(i).release();
|
||||
}
|
||||
flattenPlanes.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import static android.opengl.GLES30.*;
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.render.CoordAxes;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
import com.dark98.santoku.utils.DoubleMatrix;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
import com.dark98.santoku.utils.ViewUtils;
|
||||
|
||||
public class Bed3D {
|
||||
private final static float GROUND_Z = -0.02f;
|
||||
|
||||
private long pointer;
|
||||
|
||||
private GLModel triangles;
|
||||
private GLModel gridlines;
|
||||
private GLModel contourlines;
|
||||
|
||||
private CoordAxes axes = new CoordAxes();
|
||||
private double[] boundingVolume;
|
||||
private Vec3d min, max;
|
||||
|
||||
private boolean likelyDelta;
|
||||
|
||||
private double[] modelMatrix = new double[16];
|
||||
private double[] outModelMatrix = new double[16];
|
||||
|
||||
public Bed3D() {
|
||||
long[] data = new long[3];
|
||||
pointer = Native.bed_create(data);
|
||||
triangles = new GLModel(data[0]);
|
||||
gridlines = new GLModel(data[1]);
|
||||
contourlines = new GLModel(data[2]);
|
||||
}
|
||||
|
||||
public void configure(File f) {
|
||||
configure(f.getAbsolutePath());
|
||||
}
|
||||
|
||||
private void configure(String path) {
|
||||
Native.bed_configure(pointer, path);
|
||||
Native.bed_init_triangles_mesh(pointer, triangles.pointer);
|
||||
boundingVolume = Native.bed_get_bounding_volume(pointer);
|
||||
|
||||
min = max = null;
|
||||
|
||||
axes.origin.set(0, 0, GROUND_Z);
|
||||
axes.setStemLength(0.1f * Native.bed_get_bounding_volume_max_size(pointer));
|
||||
|
||||
if (isValid()) {
|
||||
Vec3d center = getVolumeMin().center(getVolumeMax());
|
||||
likelyDelta = (center.x == 0 || center.y == 0) && getVolumeMin().x < 0 && getVolumeMin().y < 0;
|
||||
} else {
|
||||
likelyDelta = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void arrange(Model model) {
|
||||
Native.bed_arrange(pointer, model.pointer);
|
||||
}
|
||||
|
||||
public Vec3d getVolumeMin() {
|
||||
if (min == null && boundingVolume != null) min = new Vec3d(boundingVolume[0], boundingVolume[1], boundingVolume[2]);
|
||||
return min;
|
||||
}
|
||||
|
||||
public Vec3d getVolumeMax() {
|
||||
if (max == null && boundingVolume != null) max = new Vec3d(boundingVolume[3], boundingVolume[4], boundingVolume[5]);
|
||||
return max;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return boundingVolume != null;
|
||||
}
|
||||
|
||||
public GLModel.MeshRaycaster getRaycaster() {
|
||||
return triangles.getRaycaster();
|
||||
}
|
||||
|
||||
public void render(GLShadersManager shadersManager, boolean bottom, double[] viewModelMatrix, double[] projectionMatrix, float invZoom) {
|
||||
assertTrue(viewModelMatrix.length == 16);
|
||||
assertTrue(projectionMatrix.length == 16);
|
||||
|
||||
DoubleMatrix.setIdentityM(modelMatrix, 0);
|
||||
DoubleMatrix.multiplyMM(outModelMatrix, 0, viewModelMatrix, 0, modelMatrix, 0);
|
||||
renderDefaultBed(shadersManager, bottom, outModelMatrix, projectionMatrix);
|
||||
axes.render(shadersManager, viewModelMatrix, projectionMatrix, 0.25f, invZoom);
|
||||
}
|
||||
|
||||
private void renderDefaultBed(GLShadersManager shadersManager, boolean bottom, double[] viewModelMatrix, double[] projectionMatrix) {
|
||||
GLShaderProgram shader = shadersManager.get(GLShadersManager.SHADER_FLAT);
|
||||
shader.startUsing();
|
||||
|
||||
shader.setUniformMatrix4fv("view_model_matrix", viewModelMatrix);
|
||||
shader.setUniformMatrix4fv("projection_matrix", projectionMatrix);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
if (!bottom) {
|
||||
glDepthMask(false);
|
||||
triangles.setColor(ThemesRepo.getColor(R.attr.defaultBedColor));
|
||||
triangles.render();
|
||||
glDepthMask(true);
|
||||
}
|
||||
|
||||
glLineWidth(ViewUtils.dp(1));
|
||||
gridlines.setColor(ThemesRepo.getColor(R.attr.bedGridlinesColor));
|
||||
gridlines.render();
|
||||
|
||||
contourlines.setColor(ThemesRepo.getColor(R.attr.bedContourlinesColor));
|
||||
contourlines.render();
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
shader.stopUsing();
|
||||
}
|
||||
|
||||
private void renderTexturedBed(GLShadersManager shadersManager, boolean bottom, float[] viewModelMatrix, float[] projectionMatrix) {
|
||||
GLShaderProgram shader = shadersManager.get(GLShadersManager.SHADER_PRINTBED);
|
||||
shader.startUsing();
|
||||
|
||||
shader.setUniform3f("view_model_matrix", viewModelMatrix);
|
||||
shader.setUniform3f("projection_matrix", projectionMatrix);
|
||||
shader.setUniform("transparent_background", bottom);
|
||||
shader.setUniform("svg_source", false);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
if (bottom) {
|
||||
glDepthMask(false);
|
||||
}
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
if (bottom) {
|
||||
glFrontFace(GL_CW);
|
||||
}
|
||||
|
||||
// TODO: glBindTexture(GL_TEXTURE_2D, tex_id);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
if (bottom) {
|
||||
glFrontFace(GL_CCW);
|
||||
}
|
||||
glDisable(GL_BLEND);
|
||||
if (bottom) {
|
||||
glDepthMask(true);
|
||||
}
|
||||
|
||||
shader.stopUsing();
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (pointer != 0) {
|
||||
Native.bed_release(pointer);
|
||||
axes.release();
|
||||
pointer = 0;
|
||||
|
||||
// triangles.release();
|
||||
// gridlines.release();
|
||||
// contourlines.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public class ConfigOptionDef {
|
||||
public String key;
|
||||
|
||||
// What type? bool, int, string etc.
|
||||
public ConfigOptionType type = ConfigOptionType.NONE;
|
||||
|
||||
// Usually empty.
|
||||
// Special values - "i_enum_open", "f_enum_open" to provide combo box for int or float selection,
|
||||
// "select_open" - to open a selection dialog (currently only a serial port selection).
|
||||
public GUIType guiType;
|
||||
|
||||
// Label of the GUI input field.
|
||||
// In case the GUI input fields are grouped in some views, the label defines a short label of a grouped value,
|
||||
// while full_label contains a label of a stand-alone field.
|
||||
// The full label is shown, when adding an override parameter for an object or a modified object.
|
||||
public String label;
|
||||
public String fullLabel;
|
||||
|
||||
// With which printer technology is this configuration valid?
|
||||
public PrinterTechnology printerTechnology = PrinterTechnology.UNKNOWN;
|
||||
|
||||
// Category of a configuration field, from the GUI perspective.
|
||||
// One of: "Layers and Perimeters", "Infill", "Support material", "Speed", "Extruders", "Advanced", "Extrusion Width"
|
||||
public String category;
|
||||
|
||||
// A tooltip text shown in the GUI.
|
||||
public String tooltip;
|
||||
|
||||
// Text right from the input field, usually a unit of measurement.
|
||||
public String sidetext;
|
||||
|
||||
// True for multiline strings.
|
||||
public boolean multiline;
|
||||
|
||||
// For text input: If true, the GUI text box spans the complete page width.
|
||||
public boolean fullWidth;
|
||||
|
||||
// Not editable. Currently only used for the display of the number of threads.
|
||||
public boolean readonly = false;
|
||||
|
||||
// Height of a multiline GUI text box.
|
||||
public int height = -1;
|
||||
|
||||
// Optional width of an input field.
|
||||
public int width = -1;
|
||||
|
||||
// <min, max> limit of a numeric input.
|
||||
// If not set, the <min, max> is set to <INT_MIN, INT_MAX>
|
||||
// By setting min=0, only nonnegative input is allowed.
|
||||
public float min = Float.MIN_VALUE;
|
||||
public float max = Float.MAX_VALUE;
|
||||
|
||||
public ConfigOptionMode mode = ConfigOptionMode.SIMPLE;
|
||||
|
||||
public String defaultValue;
|
||||
|
||||
public String[] enumLabels;
|
||||
public String[] enumValues;
|
||||
|
||||
public String getLabel() {
|
||||
return TextUtils.isEmpty(label) ? fullLabel : label;
|
||||
}
|
||||
|
||||
public String getFullLabel() {
|
||||
return TextUtils.isEmpty(fullLabel) ? label : fullLabel;
|
||||
}
|
||||
|
||||
ConfigOptionDef() {}
|
||||
|
||||
public enum ConfigOptionType {
|
||||
NONE,
|
||||
// single float
|
||||
FLOAT,
|
||||
// vector of floats
|
||||
FLOATS(true),
|
||||
// single int
|
||||
INT,
|
||||
// vector of ints
|
||||
INTS(true),
|
||||
// single string
|
||||
STRING,
|
||||
// vector of strings
|
||||
STRINGS(true),
|
||||
// percent value. Currently only used for infill.
|
||||
PERCENT,
|
||||
// percents value. Currently used for retract before wipe only.
|
||||
PERCENTS(true),
|
||||
// a fraction or an absolute value
|
||||
FLOAT_OR_PERCENT,
|
||||
// vector of the above
|
||||
FLOATS_OR_PERCENTS(true),
|
||||
// single 2d point (Point2f). Currently not used.
|
||||
POINT,
|
||||
// vector of 2d points (Point2f). Currently used for the definition of the print bed and for the extruder offsets.
|
||||
POINTS(true),
|
||||
POINT3,
|
||||
// single boolean value
|
||||
BOOL,
|
||||
// vector of boolean values
|
||||
BOOLS(true),
|
||||
// a generic enum
|
||||
ENUM,
|
||||
// vector of enum values
|
||||
ENUMS;
|
||||
|
||||
public final boolean list;
|
||||
|
||||
ConfigOptionType() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
ConfigOptionType(boolean list) {
|
||||
this.list = list;
|
||||
}
|
||||
}
|
||||
|
||||
public enum GUIType {
|
||||
UNDEFINED,
|
||||
// Open enums, integer value could be one of the enumerated values or something else.
|
||||
I_ENUM_OPEN,
|
||||
// Open enums, float value could be one of the enumerated values or something else.
|
||||
F_ENUM_OPEN,
|
||||
// Open enums, string value could be one of the enumerated values or something else.
|
||||
SELECT_OPEN,
|
||||
// Color picker, string value.
|
||||
COLOR,
|
||||
// Currently unused.
|
||||
SLIDER,
|
||||
// Static text
|
||||
LEGEND,
|
||||
// Vector value, but edited as a single string.
|
||||
ONE_STRING,
|
||||
// Close parameter, string value could be one of the list values.
|
||||
SELECT_CLOSE,
|
||||
// Password, string vaule is hidden by asterisk.
|
||||
PASSWORD
|
||||
}
|
||||
|
||||
public enum PrinterTechnology {
|
||||
// Fused Filament Fabrication
|
||||
FFF,
|
||||
// Stereolitography
|
||||
SLA,
|
||||
// Unknown, useful for command line processing
|
||||
UNKNOWN,
|
||||
// Any technology, useful for parameters compatible with both ptFFF and ptSLA
|
||||
ANY
|
||||
}
|
||||
|
||||
public enum ConfigOptionMode {
|
||||
SIMPLE,
|
||||
ADVANCED,
|
||||
EXPERT,
|
||||
UNDEFINED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class GCodeProcessorResult {
|
||||
long pointer;
|
||||
|
||||
public GCodeProcessorResult(File f) {
|
||||
pointer = Native.gcoderesult_load_file(f.getAbsolutePath(), f.getName());
|
||||
}
|
||||
|
||||
GCodeProcessorResult(long ptr) {
|
||||
pointer = ptr;
|
||||
}
|
||||
|
||||
public double getUsedFilamentMM(@GCodeViewer.ExtrusionRole int role) {
|
||||
return Native.gcoderesult_get_used_filament_mm(pointer, role);
|
||||
}
|
||||
|
||||
public double getUsedFilamentG(@GCodeViewer.ExtrusionRole int role) {
|
||||
return Native.gcoderesult_get_used_filament_g(pointer, role);
|
||||
}
|
||||
|
||||
public String getRecommendedName() {
|
||||
return Native.gcoderesult_get_recommended_name(pointer);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (pointer != 0) {
|
||||
Native.gcoderesult_release(pointer);
|
||||
pointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package com.dark98.santoku.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 com.dark98.santoku.config.ConfigObject;
|
||||
import com.dark98.santoku.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.theme.ThemesRepo;
|
||||
|
||||
public class GCodeViewer {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(value = {
|
||||
EXTRUSION_ROLE_NONE,
|
||||
EXTRUSION_ROLE_PERIMETER,
|
||||
EXTRUSION_ROLE_EXTERNAL_PERIMETER,
|
||||
EXTRUSION_ROLE_OVERHANG_PERIMETER,
|
||||
EXTRUSION_ROLE_INTERNAL_INFILL,
|
||||
EXTRUSION_ROLE_SOLID_INFILL,
|
||||
EXTRUSION_ROLE_TOP_SOLID_INFILL,
|
||||
EXTRUSION_ROLE_IRONING,
|
||||
EXTRUSION_ROLE_BRIDGE_INFILL,
|
||||
EXTRUSION_ROLE_GAP_FILL,
|
||||
EXTRUSION_ROLE_SKIRT,
|
||||
EXTRUSION_ROLE_SUPPORT_MATERIAL,
|
||||
EXTRUSION_ROLE_SUPPORT_MATERIAL_INTERFACE,
|
||||
EXTRUSION_ROLE_WIPE_TOWER,
|
||||
EXTRUSION_ROLE_CUSTOM
|
||||
})
|
||||
public @interface ExtrusionRole{}
|
||||
|
||||
public final static int EXTRUSION_ROLE_NONE = 0,
|
||||
EXTRUSION_ROLE_PERIMETER = 1,
|
||||
EXTRUSION_ROLE_EXTERNAL_PERIMETER = 2,
|
||||
EXTRUSION_ROLE_OVERHANG_PERIMETER = 3,
|
||||
EXTRUSION_ROLE_INTERNAL_INFILL = 4,
|
||||
EXTRUSION_ROLE_SOLID_INFILL = 5,
|
||||
EXTRUSION_ROLE_TOP_SOLID_INFILL = 6,
|
||||
EXTRUSION_ROLE_IRONING = 7,
|
||||
EXTRUSION_ROLE_BRIDGE_INFILL = 8,
|
||||
EXTRUSION_ROLE_GAP_FILL = 9,
|
||||
EXTRUSION_ROLE_SKIRT = 10,
|
||||
EXTRUSION_ROLE_SUPPORT_MATERIAL = 11,
|
||||
EXTRUSION_ROLE_SUPPORT_MATERIAL_INTERFACE = 12,
|
||||
EXTRUSION_ROLE_WIPE_TOWER = 13,
|
||||
EXTRUSION_ROLE_CUSTOM = 14,
|
||||
|
||||
EXTRUSION_ROLES_COUNT = 15;
|
||||
|
||||
private ThreadLocal<float[]> viewMatrixBuffer = new ThreadLocal<float[]>() {
|
||||
@Override
|
||||
protected float[] initialValue() {
|
||||
return new float[16];
|
||||
}
|
||||
};
|
||||
private ThreadLocal<float[]> projectionMatrixBuffer = new ThreadLocal<float[]>() {
|
||||
@Override
|
||||
protected float[] initialValue() {
|
||||
return new float[16];
|
||||
}
|
||||
};
|
||||
|
||||
private long pointer;
|
||||
|
||||
public GCodeViewer() {
|
||||
pointer = Native.vgcode_create();
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return Native.vgcode_is_initialized(pointer);
|
||||
}
|
||||
|
||||
public void initGL() {
|
||||
Native.vgcode_init(pointer);
|
||||
}
|
||||
|
||||
public void load(GCodeProcessorResult result) {
|
||||
Native.vgcode_load(pointer, result.pointer);
|
||||
}
|
||||
|
||||
public void setLayersViewRange(long min, long max) {
|
||||
Native.vgcode_set_layers_view_range(pointer, min, max);
|
||||
}
|
||||
|
||||
public Pair<Long, Long> getLayersViewRange() {
|
||||
long[] data = Native.vgcode_get_layers_view_range(pointer);
|
||||
return new Pair<>(data[0], data[1] < 0 ? getLayersCount() : data[1]);
|
||||
}
|
||||
|
||||
public long getLayersCount() {
|
||||
return Native.vgcode_get_layers_count(pointer);
|
||||
}
|
||||
|
||||
public float getEstimatedTime() {
|
||||
return Native.vgcode_get_estimated_time(pointer);
|
||||
}
|
||||
|
||||
public float getEstimatedTime(@ExtrusionRole int extrusionRole) {
|
||||
return Native.vgcode_get_estimated_time_role(pointer, extrusionRole);
|
||||
}
|
||||
|
||||
public boolean isExtrusionRoleVisible(@ExtrusionRole int extrusionRole) {
|
||||
return Native.vgcode_is_extrusion_role_visible(pointer, extrusionRole);
|
||||
}
|
||||
|
||||
public void toggleExtrusionRoleVisible(@ExtrusionRole int extrusionRole) {
|
||||
Native.vgcode_toggle_extrusion_role_visibility(pointer, extrusionRole);
|
||||
}
|
||||
|
||||
public void render(double[] viewMatrix, double[] projectionMatrix) {
|
||||
assertTrue(viewMatrix.length == 16);
|
||||
assertTrue(projectionMatrix.length == 16);
|
||||
|
||||
float[] vmFloats = viewMatrixBuffer.get();
|
||||
for (int i = 0; i < viewMatrix.length; i++) {
|
||||
vmFloats[i] = (float) viewMatrix[i];
|
||||
}
|
||||
float[] pmFloats = projectionMatrixBuffer.get();
|
||||
for (int i = 0; i < projectionMatrix.length; i++) {
|
||||
pmFloats[i] = (float) projectionMatrix[i];
|
||||
}
|
||||
Native.vgcode_render(pointer, vmFloats, pmFloats);
|
||||
}
|
||||
|
||||
public void setThemeColors() {
|
||||
setColors(
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerSkirt),
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerExternalPerimeter),
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerSupportMaterial),
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerSupportMaterialInterface),
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerInternalInfill),
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerSolidInfill),
|
||||
ThemesRepo.getColor(R.attr.gcodeViewerWipeTower)
|
||||
);
|
||||
}
|
||||
|
||||
public void setColors(int skirt, int externalPerimeter, int supportMaterial, int supportMaterialInterface, int internalInfill, int solidInfill, int wipeTower) {
|
||||
Native.vgcode_set_colors(pointer, new int[] {
|
||||
Color.red(skirt), Color.green(skirt), Color.blue(skirt),
|
||||
Color.red(externalPerimeter), Color.green(externalPerimeter), Color.blue(externalPerimeter),
|
||||
Color.red(supportMaterial), Color.green(supportMaterial), Color.blue(supportMaterial),
|
||||
Color.red(supportMaterialInterface), Color.green(supportMaterialInterface), Color.blue(supportMaterialInterface),
|
||||
Color.red(internalInfill), Color.green(internalInfill), Color.blue(internalInfill),
|
||||
Color.red(solidInfill), Color.green(solidInfill), Color.blue(solidInfill),
|
||||
Color.red(wipeTower), Color.green(wipeTower), Color.blue(wipeTower)
|
||||
});
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
Native.vgcode_reset(pointer);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (pointer != 0) {
|
||||
Native.vgcode_release(pointer);
|
||||
pointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import com.dark98.santoku.render.Camera;
|
||||
import com.dark98.santoku.render.GLRenderer;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
|
||||
public class GLModel {
|
||||
long pointer;
|
||||
private MeshRaycaster raycaster;
|
||||
|
||||
public float hoverProgress;
|
||||
public boolean isHovering;
|
||||
|
||||
/* package */ GLModel(long ptr) {
|
||||
pointer = ptr;
|
||||
}
|
||||
|
||||
public GLModel() {
|
||||
this(Native.glmodel_create());
|
||||
}
|
||||
|
||||
public GLModel(Model model) {
|
||||
this(Native.glmodel_create());
|
||||
initFrom(model);
|
||||
}
|
||||
|
||||
public void initFrom(Model model) {
|
||||
Native.glmodel_init_from_model(pointer, model.pointer);
|
||||
}
|
||||
|
||||
public void initFrom(Model model, int i) {
|
||||
Native.glmodel_init_from_model_object(pointer, model.pointer, i);
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
Native.glmodel_set_color(pointer, Color.red(color) / (float) 0xFF, Color.green(color) / (float) 0xFF, Color.blue(color) / (float) 0xFF, Color.alpha(color) / (float) 0xFF);
|
||||
}
|
||||
|
||||
public void stilizedArrow(float tipRadius, float tipLength, float stemRadius, float stemLength) {
|
||||
Native.glmodel_stilized_arrow(pointer, tipRadius, tipLength, stemRadius, stemLength);
|
||||
}
|
||||
|
||||
public void initBackgroundTriangles() {
|
||||
Native.glmodel_init_background_triangles(pointer);
|
||||
}
|
||||
|
||||
public void initBoundingBox(Model model, int i) {
|
||||
Native.glmodel_init_bounding_box(pointer, model.pointer, i);
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return Native.glmodel_is_initialized(pointer);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return Native.glmodel_is_empty(pointer);
|
||||
}
|
||||
|
||||
public void render() {
|
||||
Native.glmodel_render(pointer);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
Native.glmodel_reset(pointer);
|
||||
raycaster = null;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (pointer != 0) {
|
||||
Native.glmodel_release(pointer);
|
||||
pointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public MeshRaycaster getRaycaster() {
|
||||
if (raycaster == null) {
|
||||
Native.glmodel_init_raycast_data(pointer);
|
||||
raycaster = new MeshRaycaster();
|
||||
}
|
||||
return raycaster;
|
||||
}
|
||||
|
||||
public final class MeshRaycaster {
|
||||
public void raycast(GLRenderer renderer, ArrayList<HitResult> list, float x, float y) {
|
||||
assertTrue(renderer != null);
|
||||
list.clear();
|
||||
|
||||
Camera camera = renderer.getCamera();
|
||||
Vec3d point = Slic3rUtils.unproject(camera.getViewModelMatrix(), renderer.getProjectionMatrix(), renderer.getViewportWidth(), renderer.getViewportHeight(), x, y);
|
||||
Vec3d direction = camera.getDirForward().clone();
|
||||
if (!Prefs.isOrthoProjectionEnabled()) {
|
||||
direction = point.clone().add(camera.position.clone().negate()).normalize();
|
||||
}
|
||||
double[] v = Native.glmodel_raycast_closest_hit(pointer, point.asDoubleArray(), direction.asDoubleArray());
|
||||
list.ensureCapacity(v.length / 6);
|
||||
for (int i = 0; i < v.length; i += 6) {
|
||||
list.add(new HitResult(
|
||||
new Vec3d(v[i], v[i + 1], v[i + 2]),
|
||||
new Vec3d(v[i + 3], v[i + 4], v[i + 5])
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class HitResult {
|
||||
public final Vec3d position, normal;
|
||||
|
||||
public HitResult(Vec3d position, Vec3d normal) {
|
||||
this.position = position;
|
||||
this.normal = normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Color;
|
||||
import android.opengl.GLES30;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.utils.IOUtils;
|
||||
|
||||
public class GLShaderProgram {
|
||||
long pointer;
|
||||
private static ThreadLocal<FloatBuffer> matrixBuffer = new ThreadLocal<FloatBuffer>() {
|
||||
@Nullable
|
||||
@Override
|
||||
protected FloatBuffer initialValue() {
|
||||
return ByteBuffer.allocateDirect(16 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
|
||||
}
|
||||
};
|
||||
private static ThreadLocal<float[]> float16Buffer = new ThreadLocal<float[]>() {
|
||||
@Override
|
||||
protected float[] initialValue() {
|
||||
return new float[16];
|
||||
}
|
||||
};
|
||||
private static ThreadLocal<float[]> float12Buffer = new ThreadLocal<float[]>() {
|
||||
@Override
|
||||
protected float[] initialValue() {
|
||||
return new float[12];
|
||||
}
|
||||
};
|
||||
|
||||
public GLShaderProgram(String name) {
|
||||
AssetManager assets = Santoku.INSTANCE.getAssets();
|
||||
try {
|
||||
pointer = Native.shader_init_from_texts(name, IOUtils.readString(assets.open("shaders/" + name + ".fs")), IOUtils.readString(assets.open("shaders/" + name + ".vs")));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void startUsing() {
|
||||
Native.shader_start_using(pointer);
|
||||
}
|
||||
|
||||
public void stopUsing() {
|
||||
Native.shader_stop_using(pointer);
|
||||
}
|
||||
|
||||
public int getUniformLocation(String name) {
|
||||
// This function uses native uniform cache. Java one does not
|
||||
return Native.shader_get_uniform_location(pointer, name);
|
||||
}
|
||||
|
||||
public int getAttribLocation(String name) {
|
||||
// Same as getUniformLocation
|
||||
return Native.shader_get_attrib_location(pointer, name);
|
||||
}
|
||||
|
||||
public void setUniform(String name, boolean value) {
|
||||
GLES30.glUniform1i(getUniformLocation(name), value ? 1 : 0);
|
||||
}
|
||||
|
||||
public void setUniform(String name, float value) {
|
||||
GLES30.glUniform1f(getUniformLocation(name), value);
|
||||
}
|
||||
|
||||
public void setUniformMatrix3fv(String name, double[] value) {
|
||||
assertTrue(value.length == 12);
|
||||
|
||||
float[] floats = float12Buffer.get();
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
floats[i] = (float) value[i];
|
||||
}
|
||||
setUniformMatrix3fv(name, floats);
|
||||
}
|
||||
|
||||
public void setUniformMatrix3fv(String name, float[] value) {
|
||||
assertTrue(value.length == 12);
|
||||
|
||||
FloatBuffer buf = matrixBuffer.get();
|
||||
buf.position(0).limit(12);
|
||||
buf.put(value);
|
||||
buf.flip();
|
||||
GLES30.glUniformMatrix3fv(getUniformLocation(name), 1, false, buf);
|
||||
}
|
||||
|
||||
public void setUniformMatrix4fv(String name, double[] value) {
|
||||
assertTrue(value.length == 16);
|
||||
|
||||
float[] floats = float16Buffer.get();
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
floats[i] = (float) value[i];
|
||||
}
|
||||
setUniformMatrix4fv(name, floats);
|
||||
}
|
||||
|
||||
public void setUniformMatrix4fv(String name, float[] value) {
|
||||
assertTrue(value.length == 16);
|
||||
|
||||
FloatBuffer buf = matrixBuffer.get();
|
||||
buf.position(0).limit(16);
|
||||
buf.put(value);
|
||||
buf.flip();
|
||||
GLES30.glUniformMatrix4fv(getUniformLocation(name), 1, false, buf);
|
||||
}
|
||||
|
||||
public void setUniformColor(String name, int color) {
|
||||
setUniform4f(name, (float) Color.red(color) / 0xFF, (float) Color.green(color) / 0xFF, (float) Color.blue(color) / 0xFF, (float) Color.alpha(color) / 0xFF);
|
||||
}
|
||||
|
||||
public void setUniform4f(String name, float... value) {
|
||||
assertTrue(value.length == 4);
|
||||
|
||||
GLES30.glUniform4f(getUniformLocation(name), value[0], value[1], value[2], value[3]);
|
||||
}
|
||||
|
||||
public void setUniform3f(String name, float... value) {
|
||||
assertTrue(value.length == 3);
|
||||
|
||||
GLES30.glUniform3f(getUniformLocation(name), value[0], value[1], value[2]);
|
||||
}
|
||||
|
||||
public void setUniform2f(String name, float... value) {
|
||||
assertTrue(value.length == 2);
|
||||
|
||||
GLES30.glUniform2f(getUniformLocation(name), value[0], value[1]);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return Native.shader_get_id(pointer);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (pointer != 0) {
|
||||
Native.shader_release(pointer);
|
||||
pointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import android.opengl.GLES30;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class GLShadersManager {
|
||||
public final static String
|
||||
SHADER_BACKGROUND = "background",
|
||||
SHADER_DASHED_LINES = "dashed_lines",
|
||||
SHADER_FLAT = "flat",
|
||||
SHADER_FLAT_CLIP = "flat_clip",
|
||||
SHADER_FLAT_TEXTURE = "flat_texture",
|
||||
SHADER_GOURAUD = "gouraud",
|
||||
SHADER_GOURAUD_LIGHT = "gouraud_light",
|
||||
SHADER_GOURAUD_LIGHT_INSTANCED = "gouraud_light_instanced",
|
||||
SHADER_IMGUI = "imgui",
|
||||
SHADER_MM_CONTOUR = "mm_contour",
|
||||
SHADER_MM_GOURAUD = "mm_gouraud",
|
||||
SHADER_PRINTBED = "printbed",
|
||||
SHADER_TOOLPATHS_COG = "toolpaths_cog",
|
||||
SHADER_VARIABLE_LAYER_HEIGHT = "variable_layer_height",
|
||||
SHADER_WIREFRAME = "wireframe",
|
||||
SHADER_BEAM_INTRO = "beam_intro";
|
||||
|
||||
@StringDef(value = {
|
||||
SHADER_BACKGROUND,
|
||||
SHADER_DASHED_LINES,
|
||||
SHADER_FLAT,
|
||||
SHADER_FLAT_CLIP,
|
||||
SHADER_FLAT_TEXTURE,
|
||||
SHADER_GOURAUD,
|
||||
SHADER_GOURAUD_LIGHT,
|
||||
SHADER_GOURAUD_LIGHT_INSTANCED,
|
||||
SHADER_IMGUI,
|
||||
SHADER_MM_CONTOUR,
|
||||
SHADER_MM_GOURAUD,
|
||||
SHADER_GOURAUD,
|
||||
SHADER_PRINTBED,
|
||||
SHADER_TOOLPATHS_COG,
|
||||
SHADER_VARIABLE_LAYER_HEIGHT,
|
||||
SHADER_WIREFRAME,
|
||||
SHADER_BEAM_INTRO
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface ShaderType {}
|
||||
|
||||
private final static List<GLShadersManager> managers = new ArrayList<>();
|
||||
|
||||
private final Map<String, GLShaderProgram> shaders = new HashMap<String, GLShaderProgram>() {
|
||||
@Override
|
||||
public GLShaderProgram get(@Nullable Object key) {
|
||||
GLShaderProgram shader = super.get(key);
|
||||
if (shader == null) put((String) key, shader = new GLShaderProgram((String) key));
|
||||
return shader;
|
||||
}
|
||||
};
|
||||
|
||||
public GLShadersManager() {
|
||||
managers.add(this);
|
||||
}
|
||||
|
||||
public void clearShaders() {
|
||||
for (GLShaderProgram program : shaders.values()) {
|
||||
program.release();
|
||||
}
|
||||
shaders.clear();
|
||||
managers.remove(this);
|
||||
}
|
||||
|
||||
public GLShaderProgram get(@ShaderType String key) {
|
||||
return shaders.get(key);
|
||||
}
|
||||
|
||||
@Keep
|
||||
private static long getCurrentShaderPointer() {
|
||||
GLShaderProgram prog = null;
|
||||
for (GLShadersManager manager : managers) {
|
||||
prog = manager.getCurrentShader();
|
||||
if (prog != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return prog != null ? prog.pointer : 0;
|
||||
}
|
||||
|
||||
public GLShaderProgram getCurrentShader() {
|
||||
int[] idRef = {0};
|
||||
GLES30.glGetIntegerv(GLES30.GL_CURRENT_PROGRAM, idRef, 0);
|
||||
int id = idRef[0];
|
||||
if (id != 0) {
|
||||
for (GLShaderProgram program : shaders.values()) {
|
||||
if (program.getId() == id) {
|
||||
return program;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
|
||||
public class Model {
|
||||
public final String key = UUID.randomUUID().toString();
|
||||
long pointer;
|
||||
|
||||
private double[] boundingExact;
|
||||
private double[] boundingApprox;
|
||||
|
||||
public Model() {
|
||||
this(Native.model_create());
|
||||
}
|
||||
|
||||
public Model(File f) throws Slic3rRuntimeError {
|
||||
this(f.getAbsolutePath());
|
||||
}
|
||||
|
||||
public Model(String path) throws Slic3rRuntimeError {
|
||||
this(Native.model_read_from_file(path, getBaseName(path)));
|
||||
}
|
||||
|
||||
private Model(long ptr) {
|
||||
this.pointer = ptr;
|
||||
}
|
||||
|
||||
private static String getBaseName(String path) {
|
||||
if (path.contains("/")) {
|
||||
path = path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
if (path.contains(".")) {
|
||||
path = path.substring(0, path.lastIndexOf('.'));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public void getBoundingBoxExact(int i, Vec3d min, Vec3d max) {
|
||||
double[] data = Native.model_get_bounding_box_exact(pointer, i);
|
||||
min.set(data[0], data[1], data[2]);
|
||||
max.set(data[3], data[4], data[5]);
|
||||
}
|
||||
|
||||
public void getBoundingBoxApprox(int i, Vec3d min, Vec3d max) {
|
||||
double[] data = Native.model_get_bounding_box_approx(pointer, i);
|
||||
min.set(data[0], data[1], data[2]);
|
||||
max.set(data[3], data[4], data[5]);
|
||||
}
|
||||
|
||||
public Vec3d getBoundingBoxExactMin() {
|
||||
if (boundingExact == null) boundingExact = Native.model_get_bounding_box_exact_global(pointer);
|
||||
return new Vec3d(boundingExact[0], boundingExact[1], boundingExact[2]);
|
||||
}
|
||||
|
||||
public Vec3d getBoundingBoxExactMax() {
|
||||
if (boundingExact == null) boundingExact = Native.model_get_bounding_box_exact_global(pointer);
|
||||
return new Vec3d(boundingExact[3], boundingExact[4], boundingExact[5]);
|
||||
}
|
||||
|
||||
public Vec3d getBoundingBoxApproxMin() {
|
||||
if (boundingApprox == null) boundingApprox = Native.model_get_bounding_box_approx_global(pointer);
|
||||
return new Vec3d(boundingApprox[0], boundingApprox[1], boundingApprox[2]);
|
||||
}
|
||||
|
||||
public Vec3d getBoundingBoxApproxMax() {
|
||||
if (boundingApprox == null) boundingApprox = Native.model_get_bounding_box_approx_global(pointer);
|
||||
return new Vec3d(boundingApprox[3], boundingApprox[4], boundingApprox[5]);
|
||||
}
|
||||
|
||||
public void resetBoundingBox() {
|
||||
boundingExact = null;
|
||||
boundingApprox = null;
|
||||
}
|
||||
|
||||
public void translate(int i, double x, double y, double z) {
|
||||
Native.model_translate(pointer, i, x, y, z);
|
||||
}
|
||||
|
||||
public void translate(double x, double y, double z) {
|
||||
Native.model_translate_global(pointer, x, y, z);
|
||||
resetBoundingBox();
|
||||
}
|
||||
|
||||
public void ensureOnBed(int i) {
|
||||
Native.model_ensure_on_bed(pointer, i);
|
||||
}
|
||||
|
||||
public void scale(int i, double x, double y, double z) {
|
||||
Native.model_scale(pointer, i, x, y, z);
|
||||
}
|
||||
|
||||
public void rotate(int i, double x, double y, double z) {
|
||||
Native.model_rotate(pointer, i, x, y, z);
|
||||
}
|
||||
|
||||
public void flattenRotate(int i, GLModel surface) {
|
||||
Native.model_flatten_rotate(pointer, i, surface.pointer);
|
||||
}
|
||||
|
||||
public int getObjectsCount() {
|
||||
return Native.model_get_objects_count(pointer);
|
||||
}
|
||||
|
||||
public void addObject(Model from, int i) {
|
||||
Native.model_add_object_from_another(pointer, from.pointer, i);
|
||||
}
|
||||
|
||||
public void deleteObject(int i) {
|
||||
Native.model_delete_object(pointer, i);
|
||||
}
|
||||
|
||||
public void getTranslation(int i, Vec3d vec) {
|
||||
double[] tr = Native.model_get_translation(pointer, i);
|
||||
vec.set(tr[0], tr[1], tr[2]);
|
||||
}
|
||||
|
||||
public void getRotation(int i, Vec3d vec) {
|
||||
double[] tr = Native.model_get_rotation(pointer, i);
|
||||
vec.set(tr[0], tr[1], tr[2]);
|
||||
}
|
||||
|
||||
public boolean isLeftHanded(int i) {
|
||||
return Native.model_is_left_handed(pointer, i);
|
||||
}
|
||||
|
||||
public void getScale(int i, Vec3d vec) {
|
||||
double[] tr = Native.model_get_scale(pointer, i);
|
||||
vec.set(tr[0], tr[1], tr[2]);
|
||||
}
|
||||
|
||||
public void getMirror(int i, Vec3d vec) {
|
||||
double[] tr = Native.model_get_mirror(pointer, i);
|
||||
vec.set(tr[0], tr[1], tr[2]);
|
||||
}
|
||||
|
||||
public List<GLModel> createFlattenPlanes(int i) {
|
||||
long[] ptr = Native.model_create_flatten_planes(pointer, i);
|
||||
List<GLModel> list = new ArrayList<>(ptr.length);
|
||||
for (long l : ptr) {
|
||||
list.add(new GLModel(l));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public int getExtruder(int i) {
|
||||
return Native.model_get_extruder(pointer, i);
|
||||
}
|
||||
|
||||
public void setExtruder(int i, int extruder) {
|
||||
Native.model_set_extruder(pointer, i, extruder);
|
||||
}
|
||||
|
||||
public void autoOrient(int i) {
|
||||
Native.model_auto_orient(pointer, i);
|
||||
}
|
||||
|
||||
public boolean isBigObject(int i) {
|
||||
return Native.model_is_big_object(pointer, i);
|
||||
}
|
||||
|
||||
public GCodeProcessorResult slice(String configPath, String gcodePath, SliceListener listener) throws Slic3rRuntimeError {
|
||||
return new GCodeProcessorResult(Native.model_slice(pointer, configPath, gcodePath, listener));
|
||||
}
|
||||
|
||||
public void export3mf(String configPath, String _3mfPath) throws Slic3rRuntimeError {
|
||||
Native.model_export_3mf(pointer, configPath, _3mfPath);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (pointer != 0) {
|
||||
Native.model_release(pointer);
|
||||
pointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
release();
|
||||
}
|
||||
|
||||
public static Model merge(Model... models) {
|
||||
long[] ptrs = new long[models.length];
|
||||
for (int i = 0, modelsSize = models.length; i < modelsSize; i++) {
|
||||
Model m = models[i];
|
||||
ptrs[i] = m.pointer;
|
||||
}
|
||||
return new Model(Native.models_merge(ptrs));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
|
||||
class Native {
|
||||
static {
|
||||
System.loadLibrary("c++_shared");
|
||||
System.loadLibrary("gmp");
|
||||
System.loadLibrary("gmpxx");
|
||||
System.loadLibrary("mpfr");
|
||||
OCCTLoader.load();
|
||||
|
||||
System.loadLibrary("slic3r");
|
||||
|
||||
set_svg_path_prefix(Santoku.INSTANCE.getCacheDir().getAbsolutePath());
|
||||
}
|
||||
|
||||
static native void get_print_config_def(PrintConfigDef def);
|
||||
|
||||
static native void set_svg_path_prefix(String prefix);
|
||||
|
||||
static native long shader_init_from_texts(String name, String fragment, String vertex);
|
||||
static native int shader_get_id(long ptr);
|
||||
static native int shader_get_uniform_location(long ptr, String name);
|
||||
static native int shader_get_attrib_location(long ptr, String name);
|
||||
static native void shader_start_using(long ptr);
|
||||
static native void shader_stop_using(long ptr);
|
||||
static native void shader_release(long ptr);
|
||||
|
||||
static native long glmodel_create();
|
||||
static native void glmodel_init_from_model(long ptr, long model);
|
||||
static native void glmodel_init_from_model_object(long ptr, long model, int i);
|
||||
static native void glmodel_init_raycast_data(long ptr);
|
||||
static native void glmodel_set_color(long ptr, float red, float green, float blue, float alpha);
|
||||
static native void glmodel_render(long ptr);
|
||||
static native void glmodel_stilized_arrow(long ptr, float tipRadius, float tipLength, float stemRadius, float stemLength);
|
||||
static native void glmodel_init_background_triangles(long ptr);
|
||||
static native void glmodel_init_bounding_box(long ptr, long modelPtr, int i);
|
||||
static native boolean glmodel_is_initialized(long ptr);
|
||||
static native boolean glmodel_is_empty(long ptr);
|
||||
static native double[] glmodel_raycast_closest_hit(long ptr, double[] point, double[] direction);
|
||||
static native void glmodel_reset(long ptr);
|
||||
static native void glmodel_release(long ptr);
|
||||
|
||||
static native long bed_create(long[] data);
|
||||
static native int bed_get_bounding_volume_max_size(long ptr);
|
||||
static native double[] bed_get_bounding_volume(long ptr);
|
||||
static native void bed_configure(long ptr, String configPath);
|
||||
static native void bed_init_triangles_mesh(long ptr, long triangles);
|
||||
static native boolean bed_arrange(long ptr, long modelPtr);
|
||||
static native void bed_release(long ptr);
|
||||
|
||||
static native long models_merge(long... ptrs);
|
||||
static native long model_create();
|
||||
static native long model_read_from_file(String path, String baseName) throws Slic3rRuntimeError;
|
||||
static native int model_get_objects_count(long ptr);
|
||||
static native void model_add_object_from_another(long ptr, long from, int i);
|
||||
static native void model_delete_object(long ptr, int i);
|
||||
static native double[] model_get_translation(long ptr, int objectIndex);
|
||||
static native double[] model_get_scale(long ptr, int objectIndex);
|
||||
static native double[] model_get_mirror(long ptr, int objectIndex);
|
||||
static native double[] model_get_rotation(long ptr, int objectIndex);
|
||||
static native double[] model_get_bounding_box_exact(long ptr, int i);
|
||||
static native double[] model_get_bounding_box_approx(long ptr, int i);
|
||||
static native double[] model_get_bounding_box_exact_global(long ptr);
|
||||
static native double[] model_get_bounding_box_approx_global(long ptr);
|
||||
static native boolean model_is_left_handed(long ptr, int i);
|
||||
static native void model_translate(long ptr, int i, double x, double y, double z);
|
||||
static native void model_translate_global(long ptr, double x, double y, double z);
|
||||
static native void model_ensure_on_bed(long ptr, int i);
|
||||
static native void model_scale(long ptr, int i, double x, double y, double z);
|
||||
static native void model_rotate(long ptr, int i, double x, double y, double z);
|
||||
static native void model_flatten_rotate(long ptr, int i, long surfacePtr);
|
||||
static native long[] model_create_flatten_planes(long ptr, int i);
|
||||
static native void model_auto_orient(long ptr, int i);
|
||||
static native boolean model_is_big_object(long ptr, int i);
|
||||
static native int model_get_extruder(long ptr, int i);
|
||||
static native void model_set_extruder(long ptr, int i, int extruder);
|
||||
static native long model_slice(long ptr, String configPath, String path, SliceListener listener) throws Slic3rRuntimeError;
|
||||
static native void model_export_3mf(long ptr, String configPath, String path) throws Slic3rRuntimeError;
|
||||
static native void model_release(long ptr);
|
||||
|
||||
static native long gcoderesult_load_file(String path, String name);
|
||||
static native String gcoderesult_get_recommended_name(long ptr);
|
||||
static native double gcoderesult_get_used_filament_mm(long ptr, int role);
|
||||
static native double gcoderesult_get_used_filament_g(long ptr, int role);
|
||||
static native void gcoderesult_release(long ptr);
|
||||
|
||||
static native long vgcode_create();
|
||||
static native void vgcode_init(long ptr);
|
||||
static native boolean vgcode_is_initialized(long ptr);
|
||||
static native void vgcode_set_colors(long ptr, int[] colors);
|
||||
static native long vgcode_get_layers_count(long ptr);
|
||||
static native void vgcode_load(long ptr, long resultPtr);
|
||||
static native void vgcode_render(long ptr, float[] viewMatrix, float[] projectionMatrix);
|
||||
static native void vgcode_set_layers_view_range(long ptr, long min, long max);
|
||||
static native long[] vgcode_get_layers_view_range(long ptr);
|
||||
static native float vgcode_get_estimated_time(long ptr);
|
||||
static native float vgcode_get_estimated_time_role(long ptr, int role);
|
||||
static native boolean vgcode_is_extrusion_role_visible(long ptr, int role);
|
||||
static native void vgcode_toggle_extrusion_role_visibility(long ptr, int role);
|
||||
static native void vgcode_reset(long ptr);
|
||||
static native void vgcode_release(long ptr);
|
||||
|
||||
static native long utils_config_create(String config);
|
||||
static native boolean utils_config_check_compatibility(long ptr, String condition);
|
||||
static native String utils_config_eval(long ptr, String condition) throws Slic3rRuntimeError;
|
||||
static native void utils_config_release(long ptr);
|
||||
|
||||
static native void utils_calc_view_normal_matrix(double[] viewMatrix, double[] worldMatrix, double[] normalMatrix);
|
||||
static native double[] utils_unproject(double[] viewMatrix, double[] projectionMatrix, int screenWidth, int screenHeight, double x, double y);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
class OCCTLoader {
|
||||
private final static List<String> LIBS = Arrays.asList(
|
||||
"TKDESTEP",
|
||||
"TKXCAF",
|
||||
"TKLCAF",
|
||||
"TKCAF",
|
||||
"TKCDF",
|
||||
"TKV3d",
|
||||
"TKMesh",
|
||||
"TKXMesh",
|
||||
"TKBO",
|
||||
"TKPrim",
|
||||
"TKHLR",
|
||||
"TKShHealing",
|
||||
"TKTopAlgo",
|
||||
"TKGeomAlgo",
|
||||
"TKGeomBase",
|
||||
"TKBRep",
|
||||
"TKG3d",
|
||||
"TKG2d",
|
||||
"TKMath",
|
||||
"TKernel",
|
||||
"TKDE"
|
||||
);
|
||||
|
||||
static void load() {
|
||||
for (String lib : LIBS) {
|
||||
System.loadLibrary(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PrintConfigDef {
|
||||
public static List<String> SKIP_DEFAULT_OPTIONS = Arrays.asList(
|
||||
"tilt_up_initial_speed",
|
||||
"tilt_up_finish_speed",
|
||||
"tilt_down_initial_speed",
|
||||
"tilt_down_finish_speed",
|
||||
"tower_speed",
|
||||
"thumbnails_format"
|
||||
);
|
||||
|
||||
private static PrintConfigDef instance;
|
||||
|
||||
private final static Map<String, Class<?>> clzMap = new HashMap<String, Class<?>>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<?> get(@Nullable Object key) {
|
||||
Class<?> clz = super.get(key);
|
||||
if (clz == null) {
|
||||
try {
|
||||
put((String) key, clz = Class.forName((String) key));
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return clz;
|
||||
}
|
||||
};
|
||||
private final static Map<Pair<Class<?>, String>, Field> fieldMap = new HashMap<Pair<Class<?>, String>, Field>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public Field get(@Nullable Object key) {
|
||||
Field f = super.get(key);
|
||||
if (f == null) {
|
||||
Pair<Class<?>, String> k = (Pair<Class<?>, String>) key;
|
||||
try {
|
||||
f = k.first.getDeclaredField(k.second);
|
||||
f.setAccessible(true);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
};
|
||||
private final static Map<Pair<Class<?>, String>, Object> valueMap = new HashMap<>();
|
||||
|
||||
public Map<String, ConfigOptionDef> options = new HashMap<>();
|
||||
|
||||
@Keep
|
||||
PrintConfigDef() {}
|
||||
|
||||
@Keep
|
||||
static Object resolveEnum(String className, String value) {
|
||||
className = className.replace("/", ".");
|
||||
Class<?> clz = clzMap.get(className);
|
||||
Pair<Class<?>, String> key = new Pair<>(clz, value);
|
||||
Object val = valueMap.get(key);
|
||||
if (val != null) return val;
|
||||
|
||||
Field f = fieldMap.get(key);
|
||||
try {
|
||||
valueMap.put(key, val = f.get(null));
|
||||
return val;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PrintConfigDef getInstance() {
|
||||
if (instance == null) {
|
||||
Native.get_print_config_def(instance = new PrintConfigDef());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Keep
|
||||
void addOption(String key, ConfigOptionDef def) {
|
||||
options.put(key, def);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
|
||||
public class Slic3rConfigWrapper {
|
||||
public final static String BLACKLISTED_SYMBOLS = "<>[]:/\\|?*\"";
|
||||
|
||||
public final static List<String> PRINT_CONFIG_KEYS = Arrays.asList(
|
||||
"layer_height", "first_layer_height", "perimeters", "spiral_vase", "slice_closing_radius", "slicing_mode",
|
||||
"top_solid_layers", "top_solid_min_thickness", "bottom_solid_layers", "bottom_solid_min_thickness",
|
||||
"extra_perimeters", "extra_perimeters_on_overhangs", "avoid_crossing_curled_overhangs", "avoid_crossing_perimeters", "thin_walls", "overhangs",
|
||||
"seam_position","staggered_inner_seams", "external_perimeters_first", "fill_density", "fill_pattern", "top_fill_pattern", "bottom_fill_pattern",
|
||||
"infill_every_layers", /*"infill_only_where_needed",*/ "solid_infill_every_layers", "fill_angle", "bridge_angle",
|
||||
"solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first",
|
||||
"ironing", "ironing_type", "ironing_flowrate", "ironing_speed", "ironing_spacing",
|
||||
"max_print_speed", "max_volumetric_speed", "avoid_crossing_perimeters_max_detour",
|
||||
"fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_dist",
|
||||
"max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative",
|
||||
"perimeter_speed", "small_perimeter_speed", "external_perimeter_speed", "infill_speed", "solid_infill_speed",
|
||||
"enable_dynamic_overhang_speeds", "overhang_speed_0", "overhang_speed_1", "overhang_speed_2", "overhang_speed_3",
|
||||
"top_solid_infill_speed", "support_material_speed", "support_material_xy_spacing", "support_material_interface_speed",
|
||||
"bridge_speed", "gap_fill_speed", "gap_fill_enabled", "travel_speed", "travel_speed_z", "first_layer_speed", "first_layer_speed_over_raft", "perimeter_acceleration", "infill_acceleration",
|
||||
"external_perimeter_acceleration", "top_solid_infill_acceleration", "solid_infill_acceleration", "travel_acceleration", "wipe_tower_acceleration",
|
||||
"bridge_acceleration", "first_layer_acceleration", "first_layer_acceleration_over_raft", "default_acceleration", "skirts", "skirt_distance", "skirt_height", "draft_shield",
|
||||
"min_skirt_length", "brim_width", "brim_separation", "brim_type", "support_material", "support_material_auto", "support_material_threshold", "support_material_enforce_layers",
|
||||
"raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion",
|
||||
"support_material_pattern", "support_material_with_sheath", "support_material_spacing", "support_material_closing_radius", "support_material_style",
|
||||
"support_material_synchronize_layers", "support_material_angle", "support_material_interface_layers", "support_material_bottom_interface_layers",
|
||||
"support_material_interface_pattern", "support_material_interface_spacing", "support_material_interface_contact_loops",
|
||||
"support_material_contact_distance", "support_material_bottom_contact_distance",
|
||||
"support_material_buildplate_only",
|
||||
"support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", "support_tree_branch_diameter_angle", "support_tree_branch_diameter_double_wall",
|
||||
"support_tree_top_rate", "support_tree_branch_distance", "support_tree_tip_diameter",
|
||||
"dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius",
|
||||
"extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "perimeter_extruder",
|
||||
"infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder",
|
||||
"ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width",
|
||||
"perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width",
|
||||
"top_infill_extrusion_width", "support_material_extrusion_width", "infill_overlap", "infill_anchor", "infill_anchor_max", "bridge_flow_ratio",
|
||||
"elefant_foot_compensation", "xy_size_compensation", "resolution", "gcode_resolution", "arc_fitting",
|
||||
"wipe_tower", "wipe_tower_x", "wipe_tower_y",
|
||||
"wipe_tower_width", "wipe_tower_cone_angle", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_bridging", "single_extruder_multi_material_priming", "mmu_segmented_region_max_width",
|
||||
"mmu_segmented_region_interlocking_depth", "wipe_tower_extruder", "wipe_tower_no_sparse_layers", "wipe_tower_extra_flow", "wipe_tower_extra_spacing", "compatible_printers", "compatible_printers_condition", "inherits",
|
||||
"perimeter_generator", "wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle",
|
||||
"wall_distribution_count", "min_feature_size", "min_bead_width",
|
||||
"top_one_perimeter_type", "only_one_perimeter_first_layer"
|
||||
);
|
||||
public final static List<String> FILAMENT_CONFIG_KEYS = Arrays.asList(
|
||||
"filament_colour", "filament_diameter", "filament_type", "filament_soluble", "filament_notes", "filament_max_volumetric_speed", "filament_infill_max_speed", "filament_infill_max_crossing_speed",
|
||||
"extrusion_multiplier", "filament_density", "filament_cost", "filament_spool_weight", "filament_loading_speed", "filament_loading_speed_start", "filament_load_time",
|
||||
"filament_unloading_speed", "filament_unloading_speed_start", "filament_unload_time", "filament_toolchange_delay", "filament_cooling_moves", "filament_stamping_loading_speed", "filament_stamping_distance",
|
||||
"filament_cooling_initial_speed", "filament_purge_multiplier", "filament_cooling_final_speed", "filament_ramming_parameters", "filament_minimal_purge_on_wipe_tower",
|
||||
"filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow",
|
||||
"temperature", "idle_temperature", "first_layer_temperature", "bed_temperature", "first_layer_bed_temperature", "fan_always_on", "cooling", "min_fan_speed",
|
||||
"max_fan_speed", "bridge_fan_speed", "disable_fan_first_layers", "full_fan_speed_layer", "fan_below_layer_time", "slowdown_below_layer_time", "min_print_speed",
|
||||
"start_filament_gcode", "end_filament_gcode", "enable_dynamic_fan_speeds", "chamber_temperature", "chamber_minimal_temperature",
|
||||
"overhang_fan_speed_0", "overhang_fan_speed_1", "overhang_fan_speed_2", "overhang_fan_speed_3",
|
||||
// Retract overrides
|
||||
"filament_retract_length", "filament_retract_lift", "filament_retract_lift_above", "filament_retract_lift_below", "filament_retract_speed", "filament_deretract_speed", "filament_retract_restart_extra", "filament_retract_before_travel",
|
||||
"filament_retract_layer_change", "filament_wipe", "filament_retract_before_wipe", "filament_retract_length_toolchange", "filament_retract_restart_extra_toolchange", "filament_travel_ramping_lift",
|
||||
"filament_travel_slope", "filament_travel_max_lift", "filament_travel_lift_before_obstacle",
|
||||
// Profile compatibility
|
||||
"filament_vendor", "compatible_prints", "compatible_prints_condition", "compatible_printers", "compatible_printers_condition", "inherits",
|
||||
// Shrinkage compensation
|
||||
"filament_shrinkage_compensation_xy", "filament_shrinkage_compensation_z"
|
||||
);
|
||||
public final static List<String> PRINTER_CONFIG_KEYS = Arrays.asList(
|
||||
"printer_technology", "autoemit_temperature_commands",
|
||||
"bed_shape", "auto_arrange_bed_clearance", "auto_arrange_rotate", "bed_custom_texture", "bed_custom_model", "binary_gcode", "z_offset", "gcode_flavor", "use_relative_e_distances",
|
||||
"use_firmware_retraction", "use_volumetric_e", "variable_layer_height", "prefer_clockwise_movements",
|
||||
//FIXME the print host keys are left here just for conversion from the Printer preset to Physical Printer preset.
|
||||
"host_type", "print_host", "printhost_apikey", "printhost_cafile",
|
||||
"single_extruder_multi_material", "start_gcode", "end_gcode", "before_layer_gcode", "layer_gcode", "toolchange_gcode",
|
||||
"color_change_gcode", "pause_print_gcode", "template_custom_gcode",
|
||||
"between_objects_gcode", "printer_vendor", "printer_model", "printer_variant", "printer_notes", "cooling_tube_retraction",
|
||||
"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",
|
||||
"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",
|
||||
"machine_min_extruding_rate", "machine_min_travel_rate",
|
||||
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e",
|
||||
"elegoolink_timelapse", "elegoolink_bed_leveling", "elegoolink_bed_type"
|
||||
);
|
||||
public final static List<String> PHYSICAL_PRINTER_CONFIG_KEYS = Arrays.asList(
|
||||
"preset_name", // temporary option to compatibility with older Slicer
|
||||
"preset_names",
|
||||
"printer_technology",
|
||||
"host_type",
|
||||
"print_host",
|
||||
"printhost_apikey",
|
||||
"printhost_cafile",
|
||||
"printhost_port",
|
||||
"printhost_authorization_type",
|
||||
// HTTP digest authentization (RFC 2617)
|
||||
"printhost_user",
|
||||
"printhost_password",
|
||||
"printhost_ssl_ignore_revoke"
|
||||
);
|
||||
|
||||
private File file;
|
||||
|
||||
public List<ConfigObject> printConfigs = new ArrayList<>();
|
||||
public List<ConfigObject> printerConfigs = new ArrayList<>();
|
||||
public List<ConfigObject> filamentConfigs = new ArrayList<>();
|
||||
public List<ConfigObject> physicalPrintersConfigs = new ArrayList<>();
|
||||
|
||||
public List<ConfigObject> printerModels = new ArrayList<>();
|
||||
public ConfigObject presets;
|
||||
public ConfigObject vendor;
|
||||
|
||||
public Slic3rConfigWrapper() {}
|
||||
|
||||
public Slic3rConfigWrapper(File f) throws IOException {
|
||||
file = f;
|
||||
readFromStream(new FileInputStream(file));
|
||||
}
|
||||
|
||||
public Slic3rConfigWrapper(InputStream in) throws IOException {
|
||||
readFromStream(in);
|
||||
}
|
||||
|
||||
public void importPrint(ConfigObject obj) {
|
||||
importInto(printConfigs, obj);
|
||||
}
|
||||
|
||||
public void importPrinter(ConfigObject obj) {
|
||||
importInto(printerConfigs, obj);
|
||||
}
|
||||
|
||||
public void importFilament(ConfigObject obj) {
|
||||
importInto(filamentConfigs, obj);
|
||||
}
|
||||
|
||||
public void importInto(List<ConfigObject> list, ConfigObject obj) {
|
||||
for (ConfigObject o : list) {
|
||||
if (o.getTitle().equals(obj.getTitle())) {
|
||||
o.values.clear();
|
||||
o.values.putAll(obj.values);
|
||||
return;
|
||||
}
|
||||
}
|
||||
list.add(obj);
|
||||
}
|
||||
|
||||
public ConfigObject findFilament(String key) {
|
||||
for (ConfigObject obj : filamentConfigs) {
|
||||
if (key.equals(obj.getTitle())) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ConfigObject findPrinterVariant(String model, String variant) {
|
||||
for (ConfigObject obj : printerConfigs) {
|
||||
if (model.equals(obj.get("printer_model")) && variant.equals(obj.get("printer_variant"))) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ConfigObject findPrint(String key) {
|
||||
for (ConfigObject obj : printConfigs) {
|
||||
if (key.equals(obj.getTitle())) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ConfigObject findPrinter(String key) {
|
||||
for (ConfigObject obj : printerConfigs) {
|
||||
if (key.equals(obj.getTitle())) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void serializeList(StringBuilder sb, String key, List<ConfigObject> list) {
|
||||
for (ConfigObject cfg : list) {
|
||||
sb.append("[").append(key).append(":").append(cfg.getTitle()).append("]\n");
|
||||
|
||||
for (Map.Entry<String, String> en : cfg.values.entrySet()) {
|
||||
sb.append(en.getKey()).append(" = ").append(en.getValue().replace("\n", "\\n")).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# generated by Slice Beam ").append(BuildConfig.VERSION_NAME).append("\n\n");
|
||||
serializeList(sb, "printer", printerConfigs);
|
||||
serializeList(sb, "print", printConfigs);
|
||||
serializeList(sb, "filament", filamentConfigs);
|
||||
|
||||
if (presets != null) {
|
||||
sb.append("[presets]\n");
|
||||
for (Map.Entry<String, String> en : presets.values.entrySet()) {
|
||||
sb.append(en.getKey()).append(" = ").append(en.getValue().replace("\n", "\\n")).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void readFromStream(InputStream in) throws IOException {
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(in));
|
||||
|
||||
ConfigObject currentPrintConfig = null;
|
||||
ConfigObject currentPrinterConfig = null;
|
||||
ConfigObject currentFilamentConfig = null;
|
||||
ConfigObject currentPhysicalPrinterConfig = null;
|
||||
|
||||
ConfigObject explicitObject = null;
|
||||
|
||||
Map<String, ConfigObject> parentMap = new HashMap<>();
|
||||
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.startsWith("#")) continue;
|
||||
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
if (line.equals("[obsolete_presets]")) {
|
||||
explicitObject = new ConfigObject();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(":") && !line.equals("[presets]") && !line.equals("[vendor]")) {
|
||||
throw new UnsupportedEncodingException(String.format("Failed to decode config category: %s", line));
|
||||
}
|
||||
if (currentPrintConfig != null || currentPrinterConfig != null || currentFilamentConfig != null || currentPhysicalPrinterConfig != null) {
|
||||
throw new UnsupportedEncodingException("Failed to decode config: explicit category in combined profile!");
|
||||
}
|
||||
|
||||
if (line.equals("[presets]")) {
|
||||
explicitObject = presets = new ConfigObject();
|
||||
continue;
|
||||
}
|
||||
if (line.equals("[vendor]")) {
|
||||
explicitObject = vendor = new ConfigObject();
|
||||
continue;
|
||||
}
|
||||
|
||||
line = line.substring(1, line.length() - 1);
|
||||
String[] spl = line.split(":");
|
||||
String key = spl[0];
|
||||
String name = spl[1];
|
||||
|
||||
switch (key) {
|
||||
case "printer_model": {
|
||||
printerModels.add(explicitObject = new ConfigObject(name));
|
||||
break;
|
||||
}
|
||||
case "print": {
|
||||
printConfigs.add(explicitObject = new ConfigObject(name));
|
||||
explicitObject.profileListType = ConfigObject.PROFILE_LIST_PRINT;
|
||||
break;
|
||||
}
|
||||
case "printer": {
|
||||
printerConfigs.add(explicitObject = new ConfigObject(name));
|
||||
explicitObject.profileListType = ConfigObject.PROFILE_LIST_PRINTER;
|
||||
break;
|
||||
}
|
||||
case "physical_printer": {
|
||||
physicalPrintersConfigs.add(explicitObject = new ConfigObject(name));
|
||||
break;
|
||||
}
|
||||
case "filament": {
|
||||
filamentConfigs.add(explicitObject = new ConfigObject(name));
|
||||
explicitObject.profileListType = ConfigObject.PROFILE_LIST_FILAMENT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parentMap.put(name, explicitObject);
|
||||
}
|
||||
|
||||
int i = line.indexOf(" = ");
|
||||
if (i != -1) {
|
||||
String key = line.substring(0, i);
|
||||
String value = line.substring(i + 3).trim().replace("\\n", "\n");
|
||||
|
||||
if (key.equals("ironing_type") && value.equals("no ironing")) {
|
||||
value = "top";
|
||||
}
|
||||
if (key.equals("thumbnails")) {
|
||||
value = value.replaceAll(", \\d+x\\d+/COLPIC", "");
|
||||
}
|
||||
|
||||
if (explicitObject != null) {
|
||||
explicitObject.put(key, value);
|
||||
} else {
|
||||
if (key.equals("printer_settings_id")) {
|
||||
if (currentPrinterConfig == null)
|
||||
currentPrinterConfig = new ConfigObject();
|
||||
currentPrinterConfig.setTitle(value);
|
||||
}
|
||||
if (key.equals("print_settings_id")) {
|
||||
if (currentPrintConfig == null)
|
||||
currentPrintConfig = new ConfigObject();
|
||||
currentPrintConfig.setTitle(value);
|
||||
}
|
||||
if (key.equals("filament_settings_id")) {
|
||||
if (currentFilamentConfig == null)
|
||||
currentFilamentConfig = new ConfigObject();
|
||||
currentFilamentConfig.setTitle(value);
|
||||
}
|
||||
|
||||
if (PRINT_CONFIG_KEYS.contains(key)) {
|
||||
if (currentPrintConfig == null)
|
||||
currentPrintConfig = new ConfigObject();
|
||||
currentPrintConfig.put(key, value);
|
||||
}
|
||||
if (FILAMENT_CONFIG_KEYS.contains(key)) {
|
||||
if (currentFilamentConfig == null)
|
||||
currentFilamentConfig = new ConfigObject();
|
||||
currentFilamentConfig.put(key, value);
|
||||
}
|
||||
if (PRINTER_CONFIG_KEYS.contains(key)) {
|
||||
if (currentPrinterConfig == null)
|
||||
currentPrinterConfig = new ConfigObject();
|
||||
currentPrinterConfig.put(key, value);
|
||||
}
|
||||
if (PHYSICAL_PRINTER_CONFIG_KEYS.contains(key)) {
|
||||
if (currentPhysicalPrinterConfig == null)
|
||||
currentPhysicalPrinterConfig = new ConfigObject();
|
||||
currentPhysicalPrinterConfig.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ConfigObject obj : parentMap.values()) {
|
||||
while (obj.values.containsKey("inherits")) {
|
||||
String value = obj.values.remove("inherits");
|
||||
if (value.isEmpty()) continue;
|
||||
|
||||
if (value.contains(";")) {
|
||||
String[] spl = value.split(";");
|
||||
|
||||
for (String s : spl) {
|
||||
String str = s.trim();
|
||||
Map<String, String> newValues = new HashMap<>();
|
||||
newValues.putAll(parentMap.get(str).values);
|
||||
newValues.putAll(obj.values);
|
||||
obj.values = newValues;
|
||||
}
|
||||
} else {
|
||||
if (parentMap.containsKey(value)) {
|
||||
Map<String, String> newValues = new HashMap<>();
|
||||
newValues.putAll(parentMap.get(value).values);
|
||||
newValues.putAll(obj.values);
|
||||
obj.values = newValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPrintConfig != null) {
|
||||
printConfigs.add(currentPrintConfig);
|
||||
}
|
||||
if (currentPrinterConfig != null) {
|
||||
printerConfigs.add(currentPrinterConfig);
|
||||
}
|
||||
if (currentFilamentConfig != null) {
|
||||
filamentConfigs.add(currentFilamentConfig);
|
||||
}
|
||||
if (currentPhysicalPrinterConfig != null) {
|
||||
physicalPrintersConfigs.add(currentPhysicalPrinterConfig);
|
||||
}
|
||||
|
||||
if (presets == null) {
|
||||
presets = new ConfigObject();
|
||||
if (currentPrintConfig != null) {
|
||||
presets.put("print", currentPrintConfig.getTitle());
|
||||
}
|
||||
if (currentFilamentConfig != null) {
|
||||
presets.put("filament", currentFilamentConfig.getTitle());
|
||||
}
|
||||
if (currentPrinterConfig != null) {
|
||||
presets.put("printer", currentPrinterConfig.getTitle());
|
||||
}
|
||||
if (currentPhysicalPrinterConfig != null) {
|
||||
presets.put("physical_printer", currentPhysicalPrinterConfig.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
r.close();
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
|
||||
public class Slic3rLocalization {
|
||||
private static Map<String, Slic3rLocalization> localesMap = new HashMap<String, Slic3rLocalization>() {
|
||||
@Override
|
||||
public Slic3rLocalization get(@Nullable Object key) {
|
||||
Slic3rLocalization locale = super.get(key);
|
||||
if (locale == null) {
|
||||
try {
|
||||
put((String) key, locale = new Slic3rLocalization((String) key));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
try {
|
||||
put((String) key, locale = new Slic3rLocalization("en"));
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
};
|
||||
|
||||
private Map<String, String> map = new HashMap<>();
|
||||
|
||||
public Slic3rLocalization(String key) throws IOException {
|
||||
InputStream in = Santoku.INSTANCE.getAssets().open("localization/" + key + ".po");
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(in));
|
||||
String line;
|
||||
StringBuilder msgId = null;
|
||||
StringBuilder msgStr = null;
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.startsWith("#")) continue;
|
||||
|
||||
if (line.startsWith("msgid")) {
|
||||
msgId = new StringBuilder(line.substring(7, line.length() - 1));
|
||||
} else if (line.startsWith("msgstr")) {
|
||||
msgStr = new StringBuilder(line.substring(8, line.length() - 1));
|
||||
} else if (line.isEmpty()) {
|
||||
if (!TextUtils.isEmpty(msgId) && !TextUtils.isEmpty(msgStr)) {
|
||||
// This hack allows us to maintain vanilla strings in native code while using our app name at the same time
|
||||
map.put(msgId.toString(), replaceStr(msgStr.toString()));
|
||||
}
|
||||
|
||||
msgId = null;
|
||||
msgStr = null;
|
||||
} else if (line.startsWith("\"") && line.endsWith("\"")) {
|
||||
if (msgStr != null) {
|
||||
msgStr.append(line.substring(1, line.length() - 1));
|
||||
} else if (msgId != null) {
|
||||
msgId.append(line.substring(1, line.length() - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
r.close();
|
||||
in.close();
|
||||
}
|
||||
|
||||
private static String replaceStr(String val) {
|
||||
return val.replace("\\n", "\n").replaceAll("\\\\(.)", "$1").replace("Slic3r", "Slice Beam").replace("PrusaSlicer", "Slice Beam");
|
||||
}
|
||||
|
||||
public static String getString(String key) {
|
||||
return getInstance().get(key);
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
String val = map.get(key);
|
||||
if (TextUtils.isEmpty(val)) {
|
||||
map.put(key, val = replaceStr(key));
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
public static Slic3rLocalization getInstance(String key) {
|
||||
return localesMap.get(key);
|
||||
}
|
||||
|
||||
public static Slic3rLocalization getInstance() {
|
||||
return getInstance(Locale.getDefault().getLanguage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
public class Slic3rRuntimeError extends Exception {
|
||||
public Slic3rRuntimeError() {
|
||||
}
|
||||
|
||||
public Slic3rRuntimeError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public Slic3rRuntimeError(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public Slic3rRuntimeError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
import static com.dark98.santoku.utils.DebugUtils.assertTrue;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.dark98.santoku.utils.Vec3d;
|
||||
|
||||
public class Slic3rUtils {
|
||||
public static void calcViewNormalMatrix(double[] viewMatrix, double[] worldMatrix, double[] normalMatrix) {
|
||||
assertTrue(viewMatrix.length == 16);
|
||||
assertTrue(worldMatrix.length == 16);
|
||||
assertTrue(normalMatrix.length == 12);
|
||||
|
||||
Native.utils_calc_view_normal_matrix(viewMatrix, worldMatrix, normalMatrix);
|
||||
}
|
||||
|
||||
public static Vec3d unproject(double[] viewMatrix, double[] projectionMatrix, int screenWidth, int screenHeight, double x, double y) {
|
||||
assertTrue(viewMatrix.length == 16);
|
||||
assertTrue(projectionMatrix.length == 16);
|
||||
|
||||
double[] v = Native.utils_unproject(viewMatrix, projectionMatrix, screenWidth, screenHeight, x, y);
|
||||
return new Vec3d(v[0], v[1], v[2]);
|
||||
}
|
||||
|
||||
public final static class ConfigChecker {
|
||||
private final long pointer;
|
||||
|
||||
public ConfigChecker(String config) {
|
||||
pointer = Native.utils_config_create(config);
|
||||
}
|
||||
|
||||
public boolean checkCompatibility(String condition) {
|
||||
if (TextUtils.isEmpty(condition)) return true;
|
||||
return Native.utils_config_check_compatibility(pointer, condition);
|
||||
}
|
||||
|
||||
public String eval(String condition) throws Slic3rRuntimeError {
|
||||
return Native.utils_config_eval(pointer, condition);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
Native.utils_config_release(pointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.dark98.santoku.slic3r;
|
||||
|
||||
public interface SliceListener {
|
||||
void onProgress(int progress, String text);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.dark98.santoku.theme;
|
||||
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public class BeamTheme {
|
||||
public final static BeamTheme LIGHT = new BeamTheme() {{
|
||||
nameRes = R.string.SettingsInterfaceThemeLight;
|
||||
colors.put(R.attr.textColorOnAccent, 0xffffffff);
|
||||
colors.put(R.attr.defaultBedColor, 0xff404040);
|
||||
colors.put(R.attr.bedGridlinesColor, 0x99e5e5e5);
|
||||
colors.put(R.attr.bedContourlinesColor, 0x80ffffff);
|
||||
colors.put(R.attr.backgroundColorTop, 0xffc0c0c0);
|
||||
colors.put(R.attr.backgroundColorBottom, 0xff7a7a7a);
|
||||
colors.put(R.attr.dividerColor, 0xffeeeeee);
|
||||
colors.put(R.attr.dividerContrastColor, 0xffcccccc);
|
||||
colors.put(R.attr.dialogBackground, 0xffffffff);
|
||||
colors.put(R.attr.switchThumbUncheckedColor, 0xffeef2f3);
|
||||
colors.put(R.attr.boostyColorTop, 0xfff06e2a);
|
||||
colors.put(R.attr.boostyColorBottom, 0xff884725);
|
||||
colors.put(R.attr.telegramColor, 0xff27a7e7);
|
||||
colors.put(R.attr.k3dColor, 0xff039045);
|
||||
colors.put(R.attr.modelHoverColor, 0xffffffff);
|
||||
colors.put(R.attr.textColorNegative, 0xffff464a);
|
||||
|
||||
colors.put(R.attr.gcodeViewerNone, 0xFFE6B3B3);
|
||||
colors.put(R.attr.gcodeViewerPerimeter, 0xFFFFE64D);
|
||||
colors.put(R.attr.gcodeViewerExternalPerimeter, 0xFFFF7D38);
|
||||
colors.put(R.attr.gcodeViewerOverhangPerimeter, 0xFF1F1FFF);
|
||||
colors.put(R.attr.gcodeViewerInternalInfill, 0xFFB03029);
|
||||
colors.put(R.attr.gcodeViewerSolidInfill, 0xFF9654CC);
|
||||
colors.put(R.attr.gcodeViewerTopSolidInfill, 0xFFF04040);
|
||||
colors.put(R.attr.gcodeViewerIroning, 0xFFFF8C69);
|
||||
colors.put(R.attr.gcodeViewerBridgeInfill, 0xFF4D80BA);
|
||||
colors.put(R.attr.gcodeViewerGapFill, 0xFFFFFFFF);
|
||||
colors.put(R.attr.gcodeViewerSkirt, 0xFF00876E);
|
||||
colors.put(R.attr.gcodeViewerSupportMaterial, 0xFF00FF00);
|
||||
colors.put(R.attr.gcodeViewerSupportMaterialInterface, 0xFF008000);
|
||||
colors.put(R.attr.gcodeViewerWipeTower, 0xFFB3E3AB);
|
||||
colors.put(R.attr.gcodeViewerCustom, 0xFF5ED194);
|
||||
|
||||
colors.put(R.attr.xTrackColor, 0xffbf0000);
|
||||
colors.put(R.attr.yTrackColor, 0xff00bf00);
|
||||
colors.put(R.attr.zTrackColor, 0xff0000bf);
|
||||
|
||||
colors.put(R.attr.snackbarBase, 0xFFEEEEEE);
|
||||
colors.put(R.attr.snackbarDone, 0xFF56AB2F);
|
||||
colors.put(R.attr.snackbarWarning, 0xFFAE660C);
|
||||
colors.put(R.attr.snackbarInfo, 0xFF009DC6);
|
||||
colors.put(R.attr.snackbarError, 0xFFDC100E);
|
||||
|
||||
colors.put(android.R.attr.textColorPrimary, 0xff000000);
|
||||
colors.put(android.R.attr.textColorSecondary, 0x99000000);
|
||||
colors.put(android.R.attr.windowBackground, 0xffffffff);
|
||||
colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
|
||||
colors.put(android.R.attr.colorControlHighlight, 0x21000000);
|
||||
}};
|
||||
public final static BeamTheme DARK = new BeamTheme() {{
|
||||
nameRes = R.string.SettingsInterfaceThemeDark;
|
||||
colors = LIGHT.colors.clone();
|
||||
|
||||
colors.put(R.attr.dividerColor, 0xff333333);
|
||||
colors.put(R.attr.dividerContrastColor, 0xff444444);
|
||||
colors.put(R.attr.dialogBackground, 0xff212121);
|
||||
colors.put(R.attr.switchThumbUncheckedColor, 0xff212121);
|
||||
|
||||
colors.put(R.attr.defaultBedColor, 0xff333333);
|
||||
colors.put(R.attr.bedGridlinesColor, 0x99e5e5e5);
|
||||
colors.put(R.attr.bedContourlinesColor, 0x40ffffff);
|
||||
colors.put(R.attr.backgroundColorTop, 0xff292929);
|
||||
colors.put(R.attr.backgroundColorBottom, 0xff181818);
|
||||
colors.put(R.attr.boostyColorBottom, 0xff884725);
|
||||
|
||||
colors.put(R.attr.xTrackColor, 0xffee0000);
|
||||
colors.put(R.attr.yTrackColor, 0xff00ee00);
|
||||
colors.put(R.attr.zTrackColor, 0xff0000ee);
|
||||
|
||||
colors.put(R.attr.snackbarBase, 0xFF212121);
|
||||
|
||||
colors.put(android.R.attr.textColorPrimary, 0xffffffff);
|
||||
colors.put(android.R.attr.textColorSecondary, 0x99ffffff);
|
||||
colors.put(android.R.attr.windowBackground, 0xff121212);
|
||||
colors.put(android.R.attr.colorControlHighlight, 0x21ffffff);
|
||||
}};
|
||||
|
||||
String name;
|
||||
@StringRes
|
||||
int nameRes;
|
||||
|
||||
public SparseIntArray colors = new SparseIntArray();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.dark98.santoku.theme;
|
||||
|
||||
public interface IThemeView {
|
||||
void onApplyTheme();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.dark98.santoku.theme;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.res.Configuration;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dark98.santoku.MainActivity;
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.utils.Prefs;
|
||||
|
||||
public class ThemesRepo {
|
||||
private static Boolean resolvedSystemMode;
|
||||
|
||||
public static BeamTheme getCurrent() {
|
||||
if (Prefs.getThemeMode() == Prefs.ThemeMode.SYSTEM) {
|
||||
if (resolvedSystemMode == null) {
|
||||
resolvedSystemMode = (Santoku.INSTANCE.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
return resolvedSystemMode ? BeamTheme.DARK : BeamTheme.LIGHT;
|
||||
}
|
||||
return Prefs.getThemeMode() == Prefs.ThemeMode.LIGHT ? BeamTheme.LIGHT : BeamTheme.DARK;
|
||||
}
|
||||
|
||||
public static void resetSystemResolvedTheme() {
|
||||
resolvedSystemMode = null;
|
||||
}
|
||||
|
||||
public static int getColor(int res) {
|
||||
return getCurrent().colors.get(res);
|
||||
}
|
||||
|
||||
public static void invalidate(Activity act) {
|
||||
if (act instanceof MainActivity) {
|
||||
((MainActivity) act).onApplyTheme();
|
||||
} else {
|
||||
invalidateView(act.getWindow().getDecorView());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
public static void invalidateView(View v) {
|
||||
if (v instanceof IThemeView) {
|
||||
((IThemeView) v).onApplyTheme();
|
||||
}
|
||||
if (v instanceof ViewGroup) {
|
||||
ViewGroup vg = (ViewGroup) v;
|
||||
for (int i = 0; i < vg.getChildCount(); i++) {
|
||||
invalidateView(vg.getChildAt(i));
|
||||
}
|
||||
}
|
||||
if (v instanceof RecyclerView) {
|
||||
((RecyclerView) v).getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.dark98.santoku.utils;
|
||||
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
|
||||
public class DebugUtils {
|
||||
public static void assertTrue(boolean value) {
|
||||
throwIfNot(value);
|
||||
}
|
||||
|
||||
public static void assertFalse(boolean value) {
|
||||
throwIfNot(!value);
|
||||
}
|
||||
|
||||
private static void throwIfNot(boolean value) {
|
||||
if (!BuildConfig.DEBUG) return;
|
||||
if (!value) {
|
||||
throw new AssertionError("Assert failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,973 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.dark98.santoku.utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Double alternative to android.opengl.Matrix
|
||||
*
|
||||
* Matrix math utilities. These methods operate on OpenGL ES format
|
||||
* matrices and vectors stored in double arrays.
|
||||
* <p>
|
||||
* Matrices are 4 x 4 column-vector matrices stored in column-major
|
||||
* order:
|
||||
* <pre>
|
||||
* m[offset + 0] m[offset + 4] m[offset + 8] m[offset + 12]
|
||||
* m[offset + 1] m[offset + 5] m[offset + 9] m[offset + 13]
|
||||
* m[offset + 2] m[offset + 6] m[offset + 10] m[offset + 14]
|
||||
* m[offset + 3] m[offset + 7] m[offset + 11] m[offset + 15]</pre>
|
||||
*
|
||||
* Vectors are 4 x 1 column vectors stored in order:
|
||||
* <pre>
|
||||
* v[offset + 0]
|
||||
* v[offset + 1]
|
||||
* v[offset + 2]
|
||||
* v[offset + 3]</pre>
|
||||
*/
|
||||
public class DoubleMatrix {
|
||||
|
||||
/** Temporary memory for operations that need temporary matrix data. */
|
||||
private static final ThreadLocal<double[]> ThreadTmp = new ThreadLocal() {
|
||||
@Override protected double[] initialValue() {
|
||||
return new double[32];
|
||||
}
|
||||
};
|
||||
|
||||
private static boolean overlap(
|
||||
double[] a, int aStart, int aLength, double[] b, int bStart, int bLength) {
|
||||
if (a != b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aStart == bStart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int aEnd = aStart + aLength;
|
||||
int bEnd = bStart + bLength;
|
||||
|
||||
if (aEnd == bEnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aStart < bStart && bStart < aEnd) {
|
||||
return true;
|
||||
}
|
||||
if (aStart < bEnd && bEnd < aEnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bStart < aStart && aStart < bEnd) {
|
||||
return true;
|
||||
}
|
||||
return bStart < aEnd && aEnd < bEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies two 4x4 matrices together and stores the result in a third 4x4
|
||||
* matrix. In matrix notation: result = lhs x rhs. Due to the way
|
||||
* matrix multiplication works, the result matrix will have the same
|
||||
* effect as first multiplying by the rhs matrix, then multiplying by
|
||||
* the lhs matrix. This is the opposite of what you might expect.
|
||||
* <p>
|
||||
* The same double array may be passed for result, lhs, and/or rhs. This
|
||||
* operation is expected to do the correct thing if the result elements
|
||||
* overlap with either of the lhs or rhs elements.
|
||||
*
|
||||
* @param result The double array that holds the result.
|
||||
* @param resultOffset The offset into the result array where the result is
|
||||
* stored.
|
||||
* @param lhs The double array that holds the left-hand-side matrix.
|
||||
* @param lhsOffset The offset into the lhs array where the lhs is stored
|
||||
* @param rhs The double array that holds the right-hand-side matrix.
|
||||
* @param rhsOffset The offset into the rhs array where the rhs is stored.
|
||||
*
|
||||
* @throws IllegalArgumentException under any of the following conditions:
|
||||
* result, lhs, or rhs are null;
|
||||
* resultOffset + 16 > result.length
|
||||
* or lhsOffset + 16 > lhs.length
|
||||
* or rhsOffset + 16 > rhs.length;
|
||||
* resultOffset < 0 or lhsOffset < 0 or rhsOffset < 0
|
||||
*/
|
||||
public static void multiplyMM(double[] result, int resultOffset,
|
||||
double[] lhs, int lhsOffset, double[] rhs, int rhsOffset) {
|
||||
// error checking
|
||||
if (result == null) {
|
||||
throw new IllegalArgumentException("result == null");
|
||||
}
|
||||
if (lhs == null) {
|
||||
throw new IllegalArgumentException("lhs == null");
|
||||
}
|
||||
if (rhs == null) {
|
||||
throw new IllegalArgumentException("rhs == null");
|
||||
}
|
||||
if (resultOffset < 0) {
|
||||
throw new IllegalArgumentException("resultOffset < 0");
|
||||
}
|
||||
if (lhsOffset < 0) {
|
||||
throw new IllegalArgumentException("lhsOffset < 0");
|
||||
}
|
||||
if (rhsOffset < 0) {
|
||||
throw new IllegalArgumentException("rhsOffset < 0");
|
||||
}
|
||||
if (result.length < resultOffset + 16) {
|
||||
throw new IllegalArgumentException("result.length < resultOffset + 16");
|
||||
}
|
||||
if (lhs.length < lhsOffset + 16) {
|
||||
throw new IllegalArgumentException("lhs.length < lhsOffset + 16");
|
||||
}
|
||||
if (rhs.length < rhsOffset + 16) {
|
||||
throw new IllegalArgumentException("rhs.length < rhsOffset + 16");
|
||||
}
|
||||
|
||||
// Check for overlap between rhs and result or lhs and result
|
||||
if ( overlap(result, resultOffset, 16, lhs, lhsOffset, 16)
|
||||
|| overlap(result, resultOffset, 16, rhs, rhsOffset, 16) ) {
|
||||
double[] tmp = ThreadTmp.get();
|
||||
for (int i=0; i<4; i++) {
|
||||
final double rhs_i0 = rhs[4 * i + rhsOffset];
|
||||
double ri0 = lhs[lhsOffset] * rhs_i0;
|
||||
double ri1 = lhs[ 1 + lhsOffset ] * rhs_i0;
|
||||
double ri2 = lhs[ 2 + lhsOffset ] * rhs_i0;
|
||||
double ri3 = lhs[ 3 + lhsOffset ] * rhs_i0;
|
||||
for (int j=1; j<4; j++) {
|
||||
final double rhs_ij = rhs[ 4*i + j + rhsOffset];
|
||||
ri0 += lhs[4 * j + lhsOffset] * rhs_ij;
|
||||
ri1 += lhs[ 4*j + 1 + lhsOffset ] * rhs_ij;
|
||||
ri2 += lhs[ 4*j + 2 + lhsOffset ] * rhs_ij;
|
||||
ri3 += lhs[ 4*j + 3 + lhsOffset ] * rhs_ij;
|
||||
}
|
||||
tmp[4 * i] = ri0;
|
||||
tmp[ 4*i + 1 ] = ri1;
|
||||
tmp[ 4*i + 2 ] = ri2;
|
||||
tmp[ 4*i + 3 ] = ri3;
|
||||
}
|
||||
|
||||
// copy from tmp to result
|
||||
System.arraycopy(tmp, 0, result, 0 + resultOffset, 16);
|
||||
|
||||
} else {
|
||||
for (int i=0; i<4; i++) {
|
||||
final double rhs_i0 = rhs[4 * i + rhsOffset];
|
||||
double ri0 = lhs[lhsOffset] * rhs_i0;
|
||||
double ri1 = lhs[ 1 + lhsOffset ] * rhs_i0;
|
||||
double ri2 = lhs[ 2 + lhsOffset ] * rhs_i0;
|
||||
double ri3 = lhs[ 3 + lhsOffset ] * rhs_i0;
|
||||
for (int j=1; j<4; j++) {
|
||||
final double rhs_ij = rhs[ 4*i + j + rhsOffset];
|
||||
ri0 += lhs[4 * j + lhsOffset] * rhs_ij;
|
||||
ri1 += lhs[ 4*j + 1 + lhsOffset ] * rhs_ij;
|
||||
ri2 += lhs[ 4*j + 2 + lhsOffset ] * rhs_ij;
|
||||
ri3 += lhs[ 4*j + 3 + lhsOffset ] * rhs_ij;
|
||||
}
|
||||
result[4 * i + resultOffset] = ri0;
|
||||
result[ 4*i + 1 + resultOffset ] = ri1;
|
||||
result[ 4*i + 2 + resultOffset ] = ri2;
|
||||
result[ 4*i + 3 + resultOffset ] = ri3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies a 4 element vector by a 4x4 matrix and stores the result in a
|
||||
* 4-element column vector. In matrix notation: result = lhs x rhs
|
||||
* <p>
|
||||
* The same double array may be passed for resultVec, lhsMat, and/or rhsVec.
|
||||
* This operation is expected to do the correct thing if the result elements
|
||||
* overlap with either of the lhs or rhs elements.
|
||||
*
|
||||
* @param resultVec The double array that holds the result vector.
|
||||
* @param resultVecOffset The offset into the result array where the result
|
||||
* vector is stored.
|
||||
* @param lhsMat The double array that holds the left-hand-side matrix.
|
||||
* @param lhsMatOffset The offset into the lhs array where the lhs is stored
|
||||
* @param rhsVec The double array that holds the right-hand-side vector.
|
||||
* @param rhsVecOffset The offset into the rhs vector where the rhs vector
|
||||
* is stored.
|
||||
*
|
||||
* @throws IllegalArgumentException under any of the following conditions:
|
||||
* resultVec, lhsMat, or rhsVec are null;
|
||||
* resultVecOffset + 4 > resultVec.length
|
||||
* or lhsMatOffset + 16 > lhsMat.length
|
||||
* or rhsVecOffset + 4 > rhsVec.length;
|
||||
* resultVecOffset < 0 or lhsMatOffset < 0 or rhsVecOffset < 0
|
||||
*/
|
||||
public static void multiplyMV(double[] resultVec,
|
||||
int resultVecOffset, double[] lhsMat, int lhsMatOffset,
|
||||
double[] rhsVec, int rhsVecOffset) {
|
||||
// error checking
|
||||
if (resultVec == null) {
|
||||
throw new IllegalArgumentException("resultVec == null");
|
||||
}
|
||||
if (lhsMat == null) {
|
||||
throw new IllegalArgumentException("lhsMat == null");
|
||||
}
|
||||
if (rhsVec == null) {
|
||||
throw new IllegalArgumentException("rhsVec == null");
|
||||
}
|
||||
if (resultVecOffset < 0) {
|
||||
throw new IllegalArgumentException("resultVecOffset < 0");
|
||||
}
|
||||
if (lhsMatOffset < 0) {
|
||||
throw new IllegalArgumentException("lhsMatOffset < 0");
|
||||
}
|
||||
if (rhsVecOffset < 0) {
|
||||
throw new IllegalArgumentException("rhsVecOffset < 0");
|
||||
}
|
||||
if (resultVec.length < resultVecOffset + 4) {
|
||||
throw new IllegalArgumentException("resultVec.length < resultVecOffset + 4");
|
||||
}
|
||||
if (lhsMat.length < lhsMatOffset + 16) {
|
||||
throw new IllegalArgumentException("lhsMat.length < lhsMatOffset + 16");
|
||||
}
|
||||
if (rhsVec.length < rhsVecOffset + 4) {
|
||||
throw new IllegalArgumentException("rhsVec.length < rhsVecOffset + 4");
|
||||
}
|
||||
|
||||
double tmp0 = lhsMat[lhsMatOffset] * rhsVec[rhsVecOffset] +
|
||||
lhsMat[4 + lhsMatOffset] * rhsVec[1 + rhsVecOffset] +
|
||||
lhsMat[4 * 2 + lhsMatOffset] * rhsVec[2 + rhsVecOffset] +
|
||||
lhsMat[4 * 3 + lhsMatOffset] * rhsVec[3 + rhsVecOffset];
|
||||
double tmp1 = lhsMat[1 + lhsMatOffset] * rhsVec[rhsVecOffset] +
|
||||
lhsMat[1 + 4 + lhsMatOffset] * rhsVec[1 + rhsVecOffset] +
|
||||
lhsMat[1 + 4 * 2 + lhsMatOffset] * rhsVec[2 + rhsVecOffset] +
|
||||
lhsMat[1 + 4 * 3 + lhsMatOffset] * rhsVec[3 + rhsVecOffset];
|
||||
double tmp2 = lhsMat[2 + lhsMatOffset] * rhsVec[rhsVecOffset] +
|
||||
lhsMat[2 + 4 + lhsMatOffset] * rhsVec[1 + rhsVecOffset] +
|
||||
lhsMat[2 + 4 * 2 + lhsMatOffset] * rhsVec[2 + rhsVecOffset] +
|
||||
lhsMat[2 + 4 * 3 + lhsMatOffset] * rhsVec[3 + rhsVecOffset];
|
||||
double tmp3 = lhsMat[3 + lhsMatOffset] * rhsVec[rhsVecOffset] +
|
||||
lhsMat[3 + 4 + lhsMatOffset] * rhsVec[1 + rhsVecOffset] +
|
||||
lhsMat[3 + 4 * 2 + lhsMatOffset] * rhsVec[2 + rhsVecOffset] +
|
||||
lhsMat[3 + 4 * 3 + lhsMatOffset] * rhsVec[3 + rhsVecOffset];
|
||||
|
||||
resultVec[resultVecOffset] = tmp0;
|
||||
resultVec[ 1 + resultVecOffset ] = tmp1;
|
||||
resultVec[ 2 + resultVecOffset ] = tmp2;
|
||||
resultVec[ 3 + resultVecOffset ] = tmp3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transposes a 4 x 4 matrix.
|
||||
* <p>
|
||||
* mTrans and m must not overlap.
|
||||
*
|
||||
* @param mTrans the array that holds the output transposed matrix
|
||||
* @param mTransOffset an offset into mTrans where the transposed matrix is
|
||||
* stored.
|
||||
* @param m the input array
|
||||
* @param mOffset an offset into m where the input matrix is stored.
|
||||
*/
|
||||
public static void transposeM(double[] mTrans, int mTransOffset, double[] m,
|
||||
int mOffset) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int mBase = i * 4 + mOffset;
|
||||
mTrans[i + mTransOffset] = m[mBase];
|
||||
mTrans[i + 4 + mTransOffset] = m[mBase + 1];
|
||||
mTrans[i + 8 + mTransOffset] = m[mBase + 2];
|
||||
mTrans[i + 12 + mTransOffset] = m[mBase + 3];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts a 4 x 4 matrix.
|
||||
* <p>
|
||||
* mInv and m must not overlap.
|
||||
*
|
||||
* @param mInv the array that holds the output inverted matrix
|
||||
* @param mInvOffset an offset into mInv where the inverted matrix is
|
||||
* stored.
|
||||
* @param m the input array
|
||||
* @param mOffset an offset into m where the input matrix is stored.
|
||||
* @return true if the matrix could be inverted, false if it could not.
|
||||
*/
|
||||
public static boolean invertM(double[] mInv, int mInvOffset, double[] m,
|
||||
int mOffset) {
|
||||
// Invert a 4 x 4 matrix using Cramer's Rule
|
||||
|
||||
// transpose matrix
|
||||
final double src0 = m[mOffset];
|
||||
final double src4 = m[mOffset + 1];
|
||||
final double src8 = m[mOffset + 2];
|
||||
final double src12 = m[mOffset + 3];
|
||||
|
||||
final double src1 = m[mOffset + 4];
|
||||
final double src5 = m[mOffset + 5];
|
||||
final double src9 = m[mOffset + 6];
|
||||
final double src13 = m[mOffset + 7];
|
||||
|
||||
final double src2 = m[mOffset + 8];
|
||||
final double src6 = m[mOffset + 9];
|
||||
final double src10 = m[mOffset + 10];
|
||||
final double src14 = m[mOffset + 11];
|
||||
|
||||
final double src3 = m[mOffset + 12];
|
||||
final double src7 = m[mOffset + 13];
|
||||
final double src11 = m[mOffset + 14];
|
||||
final double src15 = m[mOffset + 15];
|
||||
|
||||
// calculate pairs for first 8 elements (cofactors)
|
||||
final double atmp0 = src10 * src15;
|
||||
final double atmp1 = src11 * src14;
|
||||
final double atmp2 = src9 * src15;
|
||||
final double atmp3 = src11 * src13;
|
||||
final double atmp4 = src9 * src14;
|
||||
final double atmp5 = src10 * src13;
|
||||
final double atmp6 = src8 * src15;
|
||||
final double atmp7 = src11 * src12;
|
||||
final double atmp8 = src8 * src14;
|
||||
final double atmp9 = src10 * src12;
|
||||
final double atmp10 = src8 * src13;
|
||||
final double atmp11 = src9 * src12;
|
||||
|
||||
// calculate first 8 elements (cofactors)
|
||||
final double dst0 = (atmp0 * src5 + atmp3 * src6 + atmp4 * src7)
|
||||
- (atmp1 * src5 + atmp2 * src6 + atmp5 * src7);
|
||||
final double dst1 = (atmp1 * src4 + atmp6 * src6 + atmp9 * src7)
|
||||
- (atmp0 * src4 + atmp7 * src6 + atmp8 * src7);
|
||||
final double dst2 = (atmp2 * src4 + atmp7 * src5 + atmp10 * src7)
|
||||
- (atmp3 * src4 + atmp6 * src5 + atmp11 * src7);
|
||||
final double dst3 = (atmp5 * src4 + atmp8 * src5 + atmp11 * src6)
|
||||
- (atmp4 * src4 + atmp9 * src5 + atmp10 * src6);
|
||||
final double dst4 = (atmp1 * src1 + atmp2 * src2 + atmp5 * src3)
|
||||
- (atmp0 * src1 + atmp3 * src2 + atmp4 * src3);
|
||||
final double dst5 = (atmp0 * src0 + atmp7 * src2 + atmp8 * src3)
|
||||
- (atmp1 * src0 + atmp6 * src2 + atmp9 * src3);
|
||||
final double dst6 = (atmp3 * src0 + atmp6 * src1 + atmp11 * src3)
|
||||
- (atmp2 * src0 + atmp7 * src1 + atmp10 * src3);
|
||||
final double dst7 = (atmp4 * src0 + atmp9 * src1 + atmp10 * src2)
|
||||
- (atmp5 * src0 + atmp8 * src1 + atmp11 * src2);
|
||||
|
||||
// calculate pairs for second 8 elements (cofactors)
|
||||
final double btmp0 = src2 * src7;
|
||||
final double btmp1 = src3 * src6;
|
||||
final double btmp2 = src1 * src7;
|
||||
final double btmp3 = src3 * src5;
|
||||
final double btmp4 = src1 * src6;
|
||||
final double btmp5 = src2 * src5;
|
||||
final double btmp6 = src0 * src7;
|
||||
final double btmp7 = src3 * src4;
|
||||
final double btmp8 = src0 * src6;
|
||||
final double btmp9 = src2 * src4;
|
||||
final double btmp10 = src0 * src5;
|
||||
final double btmp11 = src1 * src4;
|
||||
|
||||
// calculate second 8 elements (cofactors)
|
||||
final double dst8 = (btmp0 * src13 + btmp3 * src14 + btmp4 * src15)
|
||||
- (btmp1 * src13 + btmp2 * src14 + btmp5 * src15);
|
||||
final double dst9 = (btmp1 * src12 + btmp6 * src14 + btmp9 * src15)
|
||||
- (btmp0 * src12 + btmp7 * src14 + btmp8 * src15);
|
||||
final double dst10 = (btmp2 * src12 + btmp7 * src13 + btmp10 * src15)
|
||||
- (btmp3 * src12 + btmp6 * src13 + btmp11 * src15);
|
||||
final double dst11 = (btmp5 * src12 + btmp8 * src13 + btmp11 * src14)
|
||||
- (btmp4 * src12 + btmp9 * src13 + btmp10 * src14);
|
||||
final double dst12 = (btmp2 * src10 + btmp5 * src11 + btmp1 * src9 )
|
||||
- (btmp4 * src11 + btmp0 * src9 + btmp3 * src10);
|
||||
final double dst13 = (btmp8 * src11 + btmp0 * src8 + btmp7 * src10)
|
||||
- (btmp6 * src10 + btmp9 * src11 + btmp1 * src8 );
|
||||
final double dst14 = (btmp6 * src9 + btmp11 * src11 + btmp3 * src8 )
|
||||
- (btmp10 * src11 + btmp2 * src8 + btmp7 * src9 );
|
||||
final double dst15 = (btmp10 * src10 + btmp4 * src8 + btmp9 * src9 )
|
||||
- (btmp8 * src9 + btmp11 * src10 + btmp5 * src8 );
|
||||
|
||||
// calculate determinant
|
||||
final double det =
|
||||
src0 * dst0 + src1 * dst1 + src2 * dst2 + src3 * dst3;
|
||||
|
||||
if (det == 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// calculate matrix inverse
|
||||
final double invdet = 1.0f / det;
|
||||
mInv[ mInvOffset] = dst0 * invdet;
|
||||
mInv[ 1 + mInvOffset] = dst1 * invdet;
|
||||
mInv[ 2 + mInvOffset] = dst2 * invdet;
|
||||
mInv[ 3 + mInvOffset] = dst3 * invdet;
|
||||
|
||||
mInv[ 4 + mInvOffset] = dst4 * invdet;
|
||||
mInv[ 5 + mInvOffset] = dst5 * invdet;
|
||||
mInv[ 6 + mInvOffset] = dst6 * invdet;
|
||||
mInv[ 7 + mInvOffset] = dst7 * invdet;
|
||||
|
||||
mInv[ 8 + mInvOffset] = dst8 * invdet;
|
||||
mInv[ 9 + mInvOffset] = dst9 * invdet;
|
||||
mInv[10 + mInvOffset] = dst10 * invdet;
|
||||
mInv[11 + mInvOffset] = dst11 * invdet;
|
||||
|
||||
mInv[12 + mInvOffset] = dst12 * invdet;
|
||||
mInv[13 + mInvOffset] = dst13 * invdet;
|
||||
mInv[14 + mInvOffset] = dst14 * invdet;
|
||||
mInv[15 + mInvOffset] = dst15 * invdet;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes an orthographic projection matrix.
|
||||
*
|
||||
* @param m returns the result
|
||||
* @param mOffset
|
||||
* @param left
|
||||
* @param right
|
||||
* @param bottom
|
||||
* @param top
|
||||
* @param near
|
||||
* @param far
|
||||
*/
|
||||
public static void orthoM(double[] m, int mOffset,
|
||||
double left, double right, double bottom, double top,
|
||||
double near, double far) {
|
||||
if (left == right) {
|
||||
throw new IllegalArgumentException("left == right");
|
||||
}
|
||||
if (bottom == top) {
|
||||
throw new IllegalArgumentException("bottom == top");
|
||||
}
|
||||
if (near == far) {
|
||||
throw new IllegalArgumentException("near == far");
|
||||
}
|
||||
|
||||
final double r_width = 1.0f / (right - left);
|
||||
final double r_height = 1.0f / (top - bottom);
|
||||
final double r_depth = 1.0f / (far - near);
|
||||
final double x = 2.0f * (r_width);
|
||||
final double y = 2.0f * (r_height);
|
||||
final double z = -2.0f * (r_depth);
|
||||
final double tx = -(right + left) * r_width;
|
||||
final double ty = -(top + bottom) * r_height;
|
||||
final double tz = -(far + near) * r_depth;
|
||||
m[mOffset] = x;
|
||||
m[mOffset + 5] = y;
|
||||
m[mOffset +10] = z;
|
||||
m[mOffset +12] = tx;
|
||||
m[mOffset +13] = ty;
|
||||
m[mOffset +14] = tz;
|
||||
m[mOffset +15] = 1.0f;
|
||||
m[mOffset + 1] = 0.0f;
|
||||
m[mOffset + 2] = 0.0f;
|
||||
m[mOffset + 3] = 0.0f;
|
||||
m[mOffset + 4] = 0.0f;
|
||||
m[mOffset + 6] = 0.0f;
|
||||
m[mOffset + 7] = 0.0f;
|
||||
m[mOffset + 8] = 0.0f;
|
||||
m[mOffset + 9] = 0.0f;
|
||||
m[mOffset + 11] = 0.0f;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a projection matrix in terms of six clip planes.
|
||||
*
|
||||
* @param m the double array that holds the output perspective matrix
|
||||
* @param offset the offset into double array m where the perspective
|
||||
* matrix data is written
|
||||
* @param left
|
||||
* @param right
|
||||
* @param bottom
|
||||
* @param top
|
||||
* @param near
|
||||
* @param far
|
||||
*/
|
||||
public static void frustumM(double[] m, int offset,
|
||||
double left, double right, double bottom, double top,
|
||||
double near, double far) {
|
||||
if (left == right) {
|
||||
throw new IllegalArgumentException("left == right");
|
||||
}
|
||||
if (top == bottom) {
|
||||
throw new IllegalArgumentException("top == bottom");
|
||||
}
|
||||
if (near == far) {
|
||||
throw new IllegalArgumentException("near == far");
|
||||
}
|
||||
if (near <= 0.0f) {
|
||||
throw new IllegalArgumentException("near <= 0.0f");
|
||||
}
|
||||
if (far <= 0.0f) {
|
||||
throw new IllegalArgumentException("far <= 0.0f");
|
||||
}
|
||||
final double r_width = 1.0f / (right - left);
|
||||
final double r_height = 1.0f / (top - bottom);
|
||||
final double r_depth = 1.0f / (near - far);
|
||||
final double x = 2.0f * (near * r_width);
|
||||
final double y = 2.0f * (near * r_height);
|
||||
final double A = (right + left) * r_width;
|
||||
final double B = (top + bottom) * r_height;
|
||||
final double C = (far + near) * r_depth;
|
||||
final double D = 2.0f * (far * near * r_depth);
|
||||
m[offset] = x;
|
||||
m[offset + 5] = y;
|
||||
m[offset + 8] = A;
|
||||
m[offset + 9] = B;
|
||||
m[offset + 10] = C;
|
||||
m[offset + 14] = D;
|
||||
m[offset + 11] = -1.0f;
|
||||
m[offset + 1] = 0.0f;
|
||||
m[offset + 2] = 0.0f;
|
||||
m[offset + 3] = 0.0f;
|
||||
m[offset + 4] = 0.0f;
|
||||
m[offset + 6] = 0.0f;
|
||||
m[offset + 7] = 0.0f;
|
||||
m[offset + 12] = 0.0f;
|
||||
m[offset + 13] = 0.0f;
|
||||
m[offset + 15] = 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a projection matrix in terms of a field of view angle, an
|
||||
* aspect ratio, and z clip planes.
|
||||
*
|
||||
* @param m the double array that holds the perspective matrix
|
||||
* @param offset the offset into double array m where the perspective
|
||||
* matrix data is written
|
||||
* @param fovy field of view in y direction, in degrees
|
||||
* @param aspect width to height aspect ratio of the viewport
|
||||
* @param zNear
|
||||
* @param zFar
|
||||
*/
|
||||
public static void perspectiveM(double[] m, int offset,
|
||||
double fovy, double aspect, double zNear, double zFar) {
|
||||
double f = 1.0f / Math.tan(fovy * (Math.PI / 360.0));
|
||||
double rangeReciprocal = 1.0f / (zNear - zFar);
|
||||
|
||||
m[offset] = f / aspect;
|
||||
m[offset + 1] = 0.0f;
|
||||
m[offset + 2] = 0.0f;
|
||||
m[offset + 3] = 0.0f;
|
||||
|
||||
m[offset + 4] = 0.0f;
|
||||
m[offset + 5] = f;
|
||||
m[offset + 6] = 0.0f;
|
||||
m[offset + 7] = 0.0f;
|
||||
|
||||
m[offset + 8] = 0.0f;
|
||||
m[offset + 9] = 0.0f;
|
||||
m[offset + 10] = (zFar + zNear) * rangeReciprocal;
|
||||
m[offset + 11] = -1.0f;
|
||||
|
||||
m[offset + 12] = 0.0f;
|
||||
m[offset + 13] = 0.0f;
|
||||
m[offset + 14] = 2.0f * zFar * zNear * rangeReciprocal;
|
||||
m[offset + 15] = 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the length of a vector.
|
||||
*
|
||||
* @param x x coordinate of a vector
|
||||
* @param y y coordinate of a vector
|
||||
* @param z z coordinate of a vector
|
||||
* @return the length of a vector
|
||||
*/
|
||||
public static double length(double x, double y, double z) {
|
||||
return Math.sqrt(x * x + y * y + z * z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets matrix m to the identity matrix.
|
||||
*
|
||||
* @param sm returns the result
|
||||
* @param smOffset index into sm where the result matrix starts
|
||||
*/
|
||||
public static void setIdentityM(double[] sm, int smOffset) {
|
||||
for (int i=0 ; i<16 ; i++) {
|
||||
sm[smOffset + i] = 0;
|
||||
}
|
||||
for(int i = 0; i < 16; i += 5) {
|
||||
sm[smOffset + i] = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales matrix m by x, y, and z, putting the result in sm.
|
||||
* <p>
|
||||
* m and sm must not overlap.
|
||||
*
|
||||
* @param sm returns the result
|
||||
* @param smOffset index into sm where the result matrix starts
|
||||
* @param m source matrix
|
||||
* @param mOffset index into m where the source matrix starts
|
||||
* @param x scale factor x
|
||||
* @param y scale factor y
|
||||
* @param z scale factor z
|
||||
*/
|
||||
public static void scaleM(double[] sm, int smOffset,
|
||||
double[] m, int mOffset,
|
||||
double x, double y, double z) {
|
||||
for (int i=0 ; i<4 ; i++) {
|
||||
int smi = smOffset + i;
|
||||
int mi = mOffset + i;
|
||||
sm[ smi] = m[ mi] * x;
|
||||
sm[ 4 + smi] = m[ 4 + mi] * y;
|
||||
sm[ 8 + smi] = m[ 8 + mi] * z;
|
||||
sm[12 + smi] = m[12 + mi];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales matrix m in place by sx, sy, and sz.
|
||||
*
|
||||
* @param m matrix to scale
|
||||
* @param mOffset index into m where the matrix starts
|
||||
* @param x scale factor x
|
||||
* @param y scale factor y
|
||||
* @param z scale factor z
|
||||
*/
|
||||
public static void scaleM(double[] m, int mOffset,
|
||||
double x, double y, double z) {
|
||||
for (int i=0 ; i<4 ; i++) {
|
||||
int mi = mOffset + i;
|
||||
m[ mi] *= x;
|
||||
m[ 4 + mi] *= y;
|
||||
m[ 8 + mi] *= z;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates matrix m by x, y, and z, putting the result in tm.
|
||||
* <p>
|
||||
* m and tm must not overlap.
|
||||
*
|
||||
* @param tm returns the result
|
||||
* @param tmOffset index into sm where the result matrix starts
|
||||
* @param m source matrix
|
||||
* @param mOffset index into m where the source matrix starts
|
||||
* @param x translation factor x
|
||||
* @param y translation factor y
|
||||
* @param z translation factor z
|
||||
*/
|
||||
public static void translateM(double[] tm, int tmOffset,
|
||||
double[] m, int mOffset,
|
||||
double x, double y, double z) {
|
||||
System.arraycopy(m, mOffset + 0, tm, tmOffset + 0, 12);
|
||||
for (int i=0 ; i<4 ; i++) {
|
||||
int tmi = tmOffset + i;
|
||||
int mi = mOffset + i;
|
||||
tm[12 + tmi] = m[mi] * x + m[4 + mi] * y + m[8 + mi] * z +
|
||||
m[12 + mi];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates matrix m by x, y, and z in place.
|
||||
*
|
||||
* @param m matrix
|
||||
* @param mOffset index into m where the matrix starts
|
||||
* @param x translation factor x
|
||||
* @param y translation factor y
|
||||
* @param z translation factor z
|
||||
*/
|
||||
public static void translateM(
|
||||
double[] m, int mOffset,
|
||||
double x, double y, double z) {
|
||||
for (int i=0 ; i<4 ; i++) {
|
||||
int mi = mOffset + i;
|
||||
m[12 + mi] += m[mi] * x + m[4 + mi] * y + m[8 + mi] * z;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates matrix m by angle a (in degrees) around the axis (x, y, z).
|
||||
* <p>
|
||||
* m and rm must not overlap.
|
||||
*
|
||||
* @param rm returns the result
|
||||
* @param rmOffset index into rm where the result matrix starts
|
||||
* @param m source matrix
|
||||
* @param mOffset index into m where the source matrix starts
|
||||
* @param a angle to rotate in degrees
|
||||
* @param x X axis component
|
||||
* @param y Y axis component
|
||||
* @param z Z axis component
|
||||
*/
|
||||
public static void rotateM(double[] rm, int rmOffset,
|
||||
double[] m, int mOffset,
|
||||
double a, double x, double y, double z) {
|
||||
double[] tmp = ThreadTmp.get();
|
||||
setRotateM(tmp, 16, a, x, y, z);
|
||||
multiplyMM(rm, rmOffset, m, mOffset, tmp, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates matrix m in place by angle a (in degrees)
|
||||
* around the axis (x, y, z).
|
||||
*
|
||||
* @param m source matrix
|
||||
* @param mOffset index into m where the matrix starts
|
||||
* @param a angle to rotate in degrees
|
||||
* @param x X axis component
|
||||
* @param y Y axis component
|
||||
* @param z Z axis component
|
||||
*/
|
||||
public static void rotateM(double[] m, int mOffset,
|
||||
double a, double x, double y, double z) {
|
||||
rotateM(m, mOffset, m, mOffset, a, x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a matrix for rotation by angle a (in degrees)
|
||||
* around the axis (x, y, z).
|
||||
* <p>
|
||||
* An optimized path will be used for rotation about a major axis
|
||||
* (e.g. x=1.0f y=0.0f z=0.0f).
|
||||
*
|
||||
* @param rm returns the result
|
||||
* @param rmOffset index into rm where the result matrix starts
|
||||
* @param a angle to rotate in degrees
|
||||
* @param x X axis component
|
||||
* @param y Y axis component
|
||||
* @param z Z axis component
|
||||
*/
|
||||
public static void setRotateM(double[] rm, int rmOffset,
|
||||
double a, double x, double y, double z) {
|
||||
rm[rmOffset + 3] = 0;
|
||||
rm[rmOffset + 7] = 0;
|
||||
rm[rmOffset + 11]= 0;
|
||||
rm[rmOffset + 12]= 0;
|
||||
rm[rmOffset + 13]= 0;
|
||||
rm[rmOffset + 14]= 0;
|
||||
rm[rmOffset + 15]= 1;
|
||||
a *= Math.PI / 180.0f;
|
||||
double s = Math.sin(a);
|
||||
double c = Math.cos(a);
|
||||
if (1.0f == x && 0.0f == y && 0.0f == z) {
|
||||
rm[rmOffset + 5] = c; rm[rmOffset + 10]= c;
|
||||
rm[rmOffset + 6] = s; rm[rmOffset + 9] = -s;
|
||||
rm[rmOffset + 1] = 0; rm[rmOffset + 2] = 0;
|
||||
rm[rmOffset + 4] = 0; rm[rmOffset + 8] = 0;
|
||||
rm[rmOffset] = 1;
|
||||
} else if (0.0f == x && 1.0f == y && 0.0f == z) {
|
||||
rm[rmOffset] = c; rm[rmOffset + 10]= c;
|
||||
rm[rmOffset + 8] = s; rm[rmOffset + 2] = -s;
|
||||
rm[rmOffset + 1] = 0; rm[rmOffset + 4] = 0;
|
||||
rm[rmOffset + 6] = 0; rm[rmOffset + 9] = 0;
|
||||
rm[rmOffset + 5] = 1;
|
||||
} else if (0.0f == x && 0.0f == y && 1.0f == z) {
|
||||
rm[rmOffset] = c; rm[rmOffset + 5] = c;
|
||||
rm[rmOffset + 1] = s; rm[rmOffset + 4] = -s;
|
||||
rm[rmOffset + 2] = 0; rm[rmOffset + 6] = 0;
|
||||
rm[rmOffset + 8] = 0; rm[rmOffset + 9] = 0;
|
||||
rm[rmOffset + 10]= 1;
|
||||
} else {
|
||||
double len = length(x, y, z);
|
||||
if (1.0f != len) {
|
||||
double recipLen = 1.0f / len;
|
||||
x *= recipLen;
|
||||
y *= recipLen;
|
||||
z *= recipLen;
|
||||
}
|
||||
double nc = 1.0f - c;
|
||||
double xy = x * y;
|
||||
double yz = y * z;
|
||||
double zx = z * x;
|
||||
double xs = x * s;
|
||||
double ys = y * s;
|
||||
double zs = z * s;
|
||||
rm[rmOffset] = x*x*nc + c;
|
||||
rm[rmOffset + 4] = xy*nc - zs;
|
||||
rm[rmOffset + 8] = zx*nc + ys;
|
||||
rm[rmOffset + 1] = xy*nc + zs;
|
||||
rm[rmOffset + 5] = y*y*nc + c;
|
||||
rm[rmOffset + 9] = yz*nc - xs;
|
||||
rm[rmOffset + 2] = zx*nc - ys;
|
||||
rm[rmOffset + 6] = yz*nc + xs;
|
||||
rm[rmOffset + 10] = z*z*nc + c;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Euler angles to a rotation matrix.
|
||||
*
|
||||
* @param rm returns the result
|
||||
* @param rmOffset index into rm where the result matrix starts
|
||||
* @param x angle of rotation, in degrees
|
||||
* @param y is broken, do not use
|
||||
* @param z angle of rotation, in degrees
|
||||
*
|
||||
* @deprecated This method is incorrect around the y axis. This method is
|
||||
* deprecated and replaced (below) by setRotateEulerM2 which
|
||||
* behaves correctly
|
||||
*/
|
||||
@Deprecated
|
||||
public static void setRotateEulerM(double[] rm, int rmOffset,
|
||||
double x, double y, double z) {
|
||||
x *= Math.PI / 180.0f;
|
||||
y *= Math.PI / 180.0f;
|
||||
z *= Math.PI / 180.0f;
|
||||
double cx = Math.cos(x);
|
||||
double sx = Math.sin(x);
|
||||
double cy = Math.cos(y);
|
||||
double sy = Math.sin(y);
|
||||
double cz = Math.cos(z);
|
||||
double sz = Math.sin(z);
|
||||
double cxsy = cx * sy;
|
||||
double sxsy = sx * sy;
|
||||
|
||||
rm[rmOffset] = cy * cz;
|
||||
rm[rmOffset + 1] = -cy * sz;
|
||||
rm[rmOffset + 2] = sy;
|
||||
rm[rmOffset + 3] = 0.0f;
|
||||
|
||||
rm[rmOffset + 4] = cxsy * cz + cx * sz;
|
||||
rm[rmOffset + 5] = -cxsy * sz + cx * cz;
|
||||
rm[rmOffset + 6] = -sx * cy;
|
||||
rm[rmOffset + 7] = 0.0f;
|
||||
|
||||
rm[rmOffset + 8] = -sxsy * cz + sx * sz;
|
||||
rm[rmOffset + 9] = sxsy * sz + sx * cz;
|
||||
rm[rmOffset + 10] = cx * cy;
|
||||
rm[rmOffset + 11] = 0.0f;
|
||||
|
||||
rm[rmOffset + 12] = 0.0f;
|
||||
rm[rmOffset + 13] = 0.0f;
|
||||
rm[rmOffset + 14] = 0.0f;
|
||||
rm[rmOffset + 15] = 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Euler angles to a rotation matrix.
|
||||
*
|
||||
* @param rm returns the result
|
||||
* @param rmOffset index into rm where the result matrix starts
|
||||
* @param x angle of rotation, in degrees
|
||||
* @param y angle of rotation, in degrees
|
||||
* @param z angle of rotation, in degrees
|
||||
*
|
||||
* @throws IllegalArgumentException if rm is null;
|
||||
* or if rmOffset + 16 > rm.length;
|
||||
* rmOffset < 0
|
||||
*/
|
||||
public static void setRotateEulerM2(@NonNull double[] rm, int rmOffset,
|
||||
double x, double y, double z) {
|
||||
if (rm == null) {
|
||||
throw new IllegalArgumentException("rm == null");
|
||||
}
|
||||
if (rmOffset < 0) {
|
||||
throw new IllegalArgumentException("rmOffset < 0");
|
||||
}
|
||||
if (rm.length < rmOffset + 16) {
|
||||
throw new IllegalArgumentException("rm.length < rmOffset + 16");
|
||||
}
|
||||
|
||||
x *= Math.PI / 180.0f;
|
||||
y *= Math.PI / 180.0f;
|
||||
z *= Math.PI / 180.0f;
|
||||
double cx = Math.cos(x);
|
||||
double sx = Math.sin(x);
|
||||
double cy = Math.cos(y);
|
||||
double sy = Math.sin(y);
|
||||
double cz = Math.cos(z);
|
||||
double sz = Math.sin(z);
|
||||
double cxsy = cx * sy;
|
||||
double sxsy = sx * sy;
|
||||
|
||||
rm[rmOffset] = cy * cz;
|
||||
rm[rmOffset + 1] = -cy * sz;
|
||||
rm[rmOffset + 2] = sy;
|
||||
rm[rmOffset + 3] = 0.0f;
|
||||
|
||||
rm[rmOffset + 4] = sxsy * cz + cx * sz;
|
||||
rm[rmOffset + 5] = -sxsy * sz + cx * cz;
|
||||
rm[rmOffset + 6] = -sx * cy;
|
||||
rm[rmOffset + 7] = 0.0f;
|
||||
|
||||
rm[rmOffset + 8] = -cxsy * cz + sx * sz;
|
||||
rm[rmOffset + 9] = cxsy * sz + sx * cz;
|
||||
rm[rmOffset + 10] = cx * cy;
|
||||
rm[rmOffset + 11] = 0.0f;
|
||||
|
||||
rm[rmOffset + 12] = 0.0f;
|
||||
rm[rmOffset + 13] = 0.0f;
|
||||
rm[rmOffset + 14] = 0.0f;
|
||||
rm[rmOffset + 15] = 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a viewing transformation in terms of an eye point, a center of
|
||||
* view, and an up vector.
|
||||
*
|
||||
* @param rm returns the result
|
||||
* @param rmOffset index into rm where the result matrix starts
|
||||
* @param eyeX eye point X
|
||||
* @param eyeY eye point Y
|
||||
* @param eyeZ eye point Z
|
||||
* @param centerX center of view X
|
||||
* @param centerY center of view Y
|
||||
* @param centerZ center of view Z
|
||||
* @param upX up vector X
|
||||
* @param upY up vector Y
|
||||
* @param upZ up vector Z
|
||||
*/
|
||||
public static void setLookAtM(double[] rm, int rmOffset,
|
||||
double eyeX, double eyeY, double eyeZ,
|
||||
double centerX, double centerY, double centerZ, double upX, double upY,
|
||||
double upZ) {
|
||||
|
||||
// See the OpenGL GLUT documentation for gluLookAt for a description
|
||||
// of the algorithm. We implement it in a straightforward way:
|
||||
|
||||
double fx = centerX - eyeX;
|
||||
double fy = centerY - eyeY;
|
||||
double fz = centerZ - eyeZ;
|
||||
|
||||
// Normalize f
|
||||
double rlf = 1.0f / DoubleMatrix.length(fx, fy, fz);
|
||||
fx *= rlf;
|
||||
fy *= rlf;
|
||||
fz *= rlf;
|
||||
|
||||
// compute s = f x up (x means "cross product")
|
||||
double sx = fy * upZ - fz * upY;
|
||||
double sy = fz * upX - fx * upZ;
|
||||
double sz = fx * upY - fy * upX;
|
||||
|
||||
// and normalize s
|
||||
double rls = 1.0f / DoubleMatrix.length(sx, sy, sz);
|
||||
sx *= rls;
|
||||
sy *= rls;
|
||||
sz *= rls;
|
||||
|
||||
// compute u = s x f
|
||||
double ux = sy * fz - sz * fy;
|
||||
double uy = sz * fx - sx * fz;
|
||||
double uz = sx * fy - sy * fx;
|
||||
|
||||
rm[rmOffset] = sx;
|
||||
rm[rmOffset + 1] = ux;
|
||||
rm[rmOffset + 2] = -fx;
|
||||
rm[rmOffset + 3] = 0.0f;
|
||||
|
||||
rm[rmOffset + 4] = sy;
|
||||
rm[rmOffset + 5] = uy;
|
||||
rm[rmOffset + 6] = -fy;
|
||||
rm[rmOffset + 7] = 0.0f;
|
||||
|
||||
rm[rmOffset + 8] = sz;
|
||||
rm[rmOffset + 9] = uz;
|
||||
rm[rmOffset + 10] = -fz;
|
||||
rm[rmOffset + 11] = 0.0f;
|
||||
|
||||
rm[rmOffset + 12] = 0.0f;
|
||||
rm[rmOffset + 13] = 0.0f;
|
||||
rm[rmOffset + 14] = 0.0f;
|
||||
rm[rmOffset + 15] = 1.0f;
|
||||
|
||||
translateM(rm, rmOffset, -eyeX, -eyeY, -eyeZ);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package com.dark98.santoku.utils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import com.dark98.santoku.Santoku;
|
||||
import com.dark98.santoku.config.ConfigObject;
|
||||
|
||||
public class IOUtils {
|
||||
public static ExecutorService IO_POOL = Executors.newCachedThreadPool();
|
||||
|
||||
public static String getDisplayName(Uri uri) {
|
||||
ContentResolver resolver = Santoku.INSTANCE.getContentResolver();
|
||||
|
||||
String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
|
||||
Cursor metaCursor = resolver.query(uri, projection, null, null, null);
|
||||
String fileName = null;
|
||||
if (metaCursor != null) {
|
||||
try {
|
||||
if (metaCursor.moveToFirst()) {
|
||||
fileName = metaCursor.getString(0);
|
||||
}
|
||||
} finally {
|
||||
metaCursor.close();
|
||||
}
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
public static String readString(InputStream in) throws IOException {
|
||||
return readString(in, false);
|
||||
}
|
||||
|
||||
public static String readString(InputStream in, boolean close) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[10240]; int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, c);
|
||||
}
|
||||
if (close) {
|
||||
in.close();
|
||||
}
|
||||
return new String(bos.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String configJsonToString(Object obj) throws JSONException {
|
||||
if (obj instanceof JSONArray) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
JSONArray arr = (JSONArray) obj;
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
if (sb.length() != 0) sb.append(",");
|
||||
sb.append(arr.getString(i));
|
||||
}
|
||||
return sb.toString();
|
||||
} else {
|
||||
return obj.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static ConfigObject downloadProfilesRecursively(String vendor, String type, String profile, List<String> supportedKeys) throws IOException, JSONException, MissingProfileException {
|
||||
ConfigObject cfg = new ConfigObject();
|
||||
|
||||
HttpURLConnection con = (HttpURLConnection) new URL(String.format("https://raw.githubusercontent.com/SoftFever/OrcaSlicer/main/resources/profiles/%s/%s/%s.json", vendor, type, profile)).openConnection();
|
||||
if (con.getResponseCode() == 404) {
|
||||
throw new MissingProfileException(profile);
|
||||
}
|
||||
JSONObject obj = new JSONObject(readString(con.getInputStream()));
|
||||
if (!TextUtils.isEmpty(obj.optString("inherits", null))) {
|
||||
ConfigObject o = downloadProfilesRecursively(vendor, type, obj.getString("inherits"), supportedKeys);
|
||||
|
||||
for (Map.Entry<String, String> en : o.values.entrySet()) {
|
||||
if (supportedKeys.contains(en.getKey())) {
|
||||
if (en.getKey().equals("ironing_type") && en.getValue().equals("no ironing")) {
|
||||
cfg.values.put("ironing", "0");
|
||||
cfg.values.put("ironing_type", "top");
|
||||
} else if (!en.getKey().equals("thumbnails")) {
|
||||
cfg.values.put(en.getKey(), en.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Iterator<String> it = obj.keys(); it.hasNext(); ) {
|
||||
String key = it.next();
|
||||
|
||||
if (key.equals("print_settings_id") || key.equals("filament_settings_id") || key.equals("printer_settings_id")) {
|
||||
cfg.setTitle(obj.getString(key));
|
||||
} else if (!key.equals("inherits")) {
|
||||
cfg.put(key, configJsonToString(obj.get(key)));
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
public static ConfigObject configJsonToIni(JSONObject obj, String type, List<String> supportedKeys, List<String> inBundle) throws JSONException, IOException, MissingProfileException {
|
||||
ConfigObject cfg = new ConfigObject();
|
||||
if (!TextUtils.isEmpty(obj.optString("inherits", null))) {
|
||||
String inherit = obj.getString("inherits");
|
||||
|
||||
if (inBundle.contains(inherit)) {
|
||||
// Will do it later then
|
||||
cfg.put("inherits", inherit);
|
||||
} else if (inherit.indexOf(' ') == -1) {
|
||||
throw new MissingProfileException(inherit);
|
||||
} else {
|
||||
String vendor;
|
||||
if (inherit.indexOf(' ') == -1) {
|
||||
throw new MissingProfileException(inherit);
|
||||
}
|
||||
if (inherit.contains("@BBL")) {
|
||||
vendor = "BBL";
|
||||
} else if (type.equals("process")) {
|
||||
int i = inherit.indexOf('@') + 1;
|
||||
int j = inherit.indexOf(' ', i);
|
||||
if (j == -1) j = inherit.length();
|
||||
vendor = inherit.substring(i, j);
|
||||
} else {
|
||||
vendor = inherit.substring(0, inherit.indexOf(' '));
|
||||
}
|
||||
|
||||
if (vendor.equals("Generic") || inherit.startsWith("Bambu Lab")) vendor = "BBL";
|
||||
|
||||
ConfigObject _obj = downloadProfilesRecursively(vendor, type, inherit, supportedKeys);
|
||||
for (Map.Entry<String, String> en : _obj.values.entrySet()) {
|
||||
String key = en.getKey();
|
||||
switch (key) {
|
||||
case "machine_start_gcode":
|
||||
key = "start_gcode";
|
||||
break;
|
||||
case "machine_end_gcode":
|
||||
key = "end_gcode";
|
||||
break;
|
||||
case "printable_area":
|
||||
key = "bed_shape";
|
||||
break;
|
||||
case "printable_height":
|
||||
key = "max_print_height";
|
||||
break;
|
||||
case "layer_change_gcode":
|
||||
key = "layer_gcode";
|
||||
break;
|
||||
case "before_layer_change_gcode":
|
||||
key = "before_layer_gcode";
|
||||
break;
|
||||
case "filament_start_gcode":
|
||||
key = "start_filament_gcode";
|
||||
break;
|
||||
case "filament_end_gcode":
|
||||
key = "end_filament_gcode";
|
||||
break;
|
||||
case "retraction_minimum_level":
|
||||
key = "retract_before_travel";
|
||||
break;
|
||||
case "retraction_length":
|
||||
key = "retract_length";
|
||||
break;
|
||||
case "retraction_speed":
|
||||
key = "retract_speed";
|
||||
break;
|
||||
case "deretraction_speed":
|
||||
key = "deretract_speed";
|
||||
break;
|
||||
case "change_filament_gcode":
|
||||
key = "pause_print_gcode";
|
||||
break;
|
||||
case "nozzle_temperature":
|
||||
key = "temperature";
|
||||
break;
|
||||
case "nozzle_temperature_initial_layer":
|
||||
key = "first_layer_temperature";
|
||||
break;
|
||||
case "filament_flow_ratio":
|
||||
key = "extrusion_multiplier";
|
||||
break;
|
||||
case "chamber_temperatures":
|
||||
key = "chamber_temperature";
|
||||
break;
|
||||
case "fan_max_speed":
|
||||
key = "max_fan_speed";
|
||||
break;
|
||||
case "fan_min_speed":
|
||||
key = "min_fan_speed";
|
||||
break;
|
||||
case "overhang_fan_speed":
|
||||
key = "bridge_fan_speed";
|
||||
break;
|
||||
case "slow_down_layer_time":
|
||||
key = "slowdown_below_layer_time";
|
||||
break;
|
||||
case "slow_down_min_speed":
|
||||
key = "min_print_speed";
|
||||
break;
|
||||
}
|
||||
|
||||
if (key.equals("pressure_advance")) {
|
||||
StringBuilder sb = new StringBuilder("SET_PRESSURE_ADVANCE ADVANCE=").append(en.getValue());
|
||||
if (cfg.values.containsKey("start_filament_gcode")) {
|
||||
sb.append("\n").append(cfg.get("start_filament_gcode"));
|
||||
}
|
||||
cfg.values.put("start_filament_gcode", sb.toString());
|
||||
}
|
||||
|
||||
if (supportedKeys.contains(key)) {
|
||||
if (key.equals("ironing_type") && en.getValue().equals("no ironing")) {
|
||||
cfg.values.put("ironing", "0");
|
||||
cfg.values.put("ironing_type", "top");
|
||||
}
|
||||
if (key.equals("start_filament_gcode") || key.equals("end_filament_gcode") ||
|
||||
key.equals("start_gcode") || key.equals("end_gcode")) {
|
||||
|
||||
String val = en.getValue();
|
||||
if (key.equals("start_filament_gcode")) {
|
||||
if (cfg.values.containsKey("start_filament_gcode")) {
|
||||
val = cfg.get("start_filament_gcode") + "\n" + val;
|
||||
}
|
||||
}
|
||||
|
||||
cfg.values.put(key, val.replaceAll("(\\{|\\[)nozzle_temperature_initial_layer(\\[\\d+]|)(}|])", "$1first_layer_temperature$2$3")
|
||||
.replaceAll("(\\{|\\[)bed_temperature_initial_layer_single(\\[\\d+]|)(}|])", "$1first_layer_bed_temperature$2$3"));
|
||||
} else if (!key.equals("thumbnails")) {
|
||||
cfg.values.put(key, en.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Iterator<String> it = obj.keys(); it.hasNext(); ) {
|
||||
String key = it.next();
|
||||
|
||||
if (key.equals("print_settings_id") || key.equals("filament_settings_id") || key.equals("printer_settings_id")) {
|
||||
String v = obj.getString(key);
|
||||
if (v.startsWith("[\"") && v.endsWith("\"]")) v = v.substring(2, v.length() - 2);
|
||||
cfg.setTitle(v);
|
||||
} else if (!key.equals("inherits") && supportedKeys.contains(key)) {
|
||||
cfg.put(key, configJsonToString(obj.get(key)));
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
public static class MissingProfileException extends Exception {
|
||||
public final String profile;
|
||||
|
||||
public MissingProfileException(String profile) {
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MissingProfileException{" +
|
||||
"profile='" + profile + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.dark98.santoku.utils;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.dark98.santoku.BuildConfig;
|
||||
import com.dark98.santoku.R;
|
||||
import com.dark98.santoku.SetupActivity;
|
||||
|
||||
public class Prefs {
|
||||
public final static int CAMERA_CONTROL_MODE_ROTATE_MOVE = 0,
|
||||
CAMERA_CONTROL_MODE_MOVE_ROTATE = 1,
|
||||
CAMERA_CONTROL_MODE_MOVE_ONLY = 2;
|
||||
|
||||
private static SharedPreferences mPrefs;
|
||||
|
||||
public static void init(Application ctx) {
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
}
|
||||
|
||||
public static SharedPreferences getPrefs() {
|
||||
return mPrefs;
|
||||
}
|
||||
|
||||
public static String getLastCommit() {
|
||||
return mPrefs.getString("last_commit", null);
|
||||
}
|
||||
|
||||
public static void setLastCommit() {
|
||||
mPrefs.edit().putString("last_commit", BuildConfig.COMMIT).apply();
|
||||
}
|
||||
|
||||
public static boolean isScaleInputInMM() {
|
||||
return mPrefs.getBoolean("scale_input_mm", false);
|
||||
}
|
||||
|
||||
public static void setScaleInputInMM(boolean v) {
|
||||
mPrefs.edit().putBoolean("scale_input_mm", v).apply();
|
||||
}
|
||||
|
||||
public static boolean isScaleLinked() {
|
||||
return mPrefs.getBoolean("scale_linked", true);
|
||||
}
|
||||
|
||||
public static void setScaleLinked(boolean v) {
|
||||
mPrefs.edit().putBoolean("scale_linked", v).apply();
|
||||
}
|
||||
|
||||
public static long getLastCheckedInfo() {
|
||||
return mPrefs.getLong("last_checked_info", 0);
|
||||
}
|
||||
|
||||
public static void setLastCheckedInfo() {
|
||||
mPrefs.edit().putLong("last_checked_info", System.currentTimeMillis()).apply();
|
||||
}
|
||||
|
||||
// Only used for displaying Boosty info, nothing more
|
||||
public static boolean isRussianIP() {
|
||||
return mPrefs.getBoolean("russian_ip", false);
|
||||
}
|
||||
|
||||
public static void setRussianIP(boolean v) {
|
||||
mPrefs.edit().putBoolean("russian_ip", v).apply();
|
||||
}
|
||||
|
||||
public static void setBeamServerData(String data) {
|
||||
mPrefs.edit().putString("beam_server_data", data).apply();
|
||||
}
|
||||
|
||||
public static String getBeamServerData() {
|
||||
return mPrefs.getString("beam_server_data", "{}");
|
||||
}
|
||||
|
||||
public static int getCameraControlMode() {
|
||||
return mPrefs.getInt("camera_control_mode", mPrefs.getBoolean("rotation_enabled", true) ? CAMERA_CONTROL_MODE_ROTATE_MOVE : CAMERA_CONTROL_MODE_MOVE_ONLY);
|
||||
}
|
||||
|
||||
public static void setCameraControlMode(int mode) {
|
||||
mPrefs.edit().putInt("camera_control_mode", mode).apply();
|
||||
}
|
||||
|
||||
public static boolean isOrthoProjectionEnabled() {
|
||||
return mPrefs.getBoolean("ortho_projection", true);
|
||||
}
|
||||
|
||||
public static void setOrthoProjectionEnabled(boolean e) {
|
||||
mPrefs.edit().putBoolean("ortho_projection", e).apply();
|
||||
}
|
||||
|
||||
public static float getCameraSensitivity() {
|
||||
return 5f;
|
||||
}
|
||||
|
||||
public static int getAccentColor() {
|
||||
return mPrefs.getInt("accent", SetupActivity.AccentColors.DEFAULT.color);
|
||||
}
|
||||
|
||||
public static void setAccentColor(int color) {
|
||||
mPrefs.edit().putInt("accent", color).apply();
|
||||
}
|
||||
|
||||
public static boolean isVibrationEnabled() {
|
||||
return mPrefs.getBoolean("vibration", true);
|
||||
}
|
||||
|
||||
public static float getRenderScale() {
|
||||
return mPrefs.getFloat("render_scale", 1f);
|
||||
}
|
||||
|
||||
public static void setRenderScale(float s) {
|
||||
mPrefs.edit().putFloat("render_scale", s).apply();
|
||||
}
|
||||
|
||||
private static ThemeMode cachedThemeMode;
|
||||
public static ThemeMode getThemeMode() {
|
||||
if (cachedThemeMode == null) {
|
||||
cachedThemeMode = ThemeMode.values()[mPrefs.getInt("theme_mode", 0)];
|
||||
}
|
||||
return cachedThemeMode;
|
||||
}
|
||||
|
||||
public static void setThemeMode(int i) {
|
||||
mPrefs.edit().putInt("theme_mode", i).apply();
|
||||
cachedThemeMode = null;
|
||||
}
|
||||
|
||||
public static String getCloudAPIToken() {
|
||||
return mPrefs.getString("cloud_api_token", null);
|
||||
}
|
||||
|
||||
public static void setCloudAPIToken(String token) {
|
||||
SharedPreferences.Editor e = mPrefs.edit();
|
||||
if (token == null) {
|
||||
e.remove("cloud_api_token");
|
||||
} else {
|
||||
e.putString("cloud_api_token", token);
|
||||
}
|
||||
e.apply();
|
||||
}
|
||||
|
||||
public static boolean isCloudProfileSyncEnabled() {
|
||||
return mPrefs.getBoolean("cloud_profile_sync", true);
|
||||
}
|
||||
|
||||
public static void setCloudProfileSyncEnabled(boolean en) {
|
||||
mPrefs.edit().putBoolean("cloud_profile_sync", en).apply();
|
||||
}
|
||||
|
||||
public static String getCloudCachedUserInfo() {
|
||||
return mPrefs.getString("cloud_cached_user_info", null);
|
||||
}
|
||||
|
||||
public static void setCloudCachedUserInfo(String info) {
|
||||
SharedPreferences.Editor e = mPrefs.edit();
|
||||
if (info == null) {
|
||||
e.remove("cloud_cached_user_info");
|
||||
} else {
|
||||
e.putString("cloud_cached_user_info", info);
|
||||
}
|
||||
e.apply();
|
||||
}
|
||||
|
||||
public static int getCloudCachedUsedModels() {
|
||||
return mPrefs.getInt("cloud_cached_models_used", 0);
|
||||
}
|
||||
|
||||
public static int getCloudCachedMaxModels() {
|
||||
return mPrefs.getInt("cloud_cached_models_max", 50);
|
||||
}
|
||||
|
||||
public static void setCloudCachedUsedMaxModels(int used, int max) {
|
||||
mPrefs.edit().putInt("cloud_cached_models_used", used).putInt("cloud_cached_models_max", max).apply();
|
||||
}
|
||||
|
||||
public static String getCloudCachedUserFeatures() {
|
||||
return mPrefs.getString("cloud_cached_user_features", null);
|
||||
}
|
||||
|
||||
public static void setCloudCachedUserFeatures(String features) {
|
||||
SharedPreferences.Editor e = mPrefs.edit();
|
||||
if (features == null) {
|
||||
e.remove("cloud_cached_user_features");
|
||||
} else {
|
||||
e.putString("cloud_cached_user_features", features);
|
||||
}
|
||||
e.apply();
|
||||
}
|
||||
|
||||
public static long getCloudLastFeaturesSync() {
|
||||
return mPrefs.getLong("cloud_last_features_sync", 0);
|
||||
}
|
||||
|
||||
public static void setCloudLastFeaturesSync(long ls) {
|
||||
mPrefs.edit().putLong("cloud_last_features_sync", ls).apply();
|
||||
}
|
||||
|
||||
public static long getCloudLastSync() {
|
||||
return mPrefs.getLong("cloud_last_sync", 0);
|
||||
}
|
||||
|
||||
public static void setCloudLastSync(long ls) {
|
||||
mPrefs.edit().putLong("cloud_last_sync", ls).apply();
|
||||
}
|
||||
|
||||
public static long getCloudLocalLastSentModified() {
|
||||
return mPrefs.getLong("cloud_local_last_sent_modified", 0);
|
||||
}
|
||||
|
||||
public static void setCloudLocalLastSentModified(long lm) {
|
||||
mPrefs.edit().putLong("cloud_local_last_sent_modified", lm).apply();
|
||||
}
|
||||
|
||||
public static long getCloudLocalLastModified() {
|
||||
return mPrefs.getLong("cloud_local_last_modified", 0);
|
||||
}
|
||||
|
||||
public static void setCloudLocalLastModified(long lm) {
|
||||
mPrefs.edit().putLong("cloud_local_last_modified", lm).apply();
|
||||
}
|
||||
|
||||
public static long getCloudRemoteLastModified() {
|
||||
return mPrefs.getLong("cloud_remote_last_modified", 0);
|
||||
}
|
||||
|
||||
public static void setCloudRemoteLastModified(long lm) {
|
||||
mPrefs.edit().putLong("cloud_remote_last_modified", lm).apply();
|
||||
}
|
||||
|
||||
public enum ThemeMode {
|
||||
SYSTEM(R.string.SettingsInterfaceThemeSystem),
|
||||
LIGHT(R.string.SettingsInterfaceThemeLight),
|
||||
DARK(R.string.SettingsInterfaceThemeDark);
|
||||
|
||||
public final int title;
|
||||
|
||||
ThemeMode(int title) {
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.dark98.santoku.utils;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class RandomUtils {
|
||||
|
||||
public final static Random RANDOM = new Random();
|
||||
|
||||
public static float randomf(float min, float max) {
|
||||
return min + RANDOM.nextFloat() * (max - min);
|
||||
}
|
||||
|
||||
public static long randoml(long min, long max) {
|
||||
return (long) (min + RANDOM.nextDouble() * (max - min));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.dark98.santoku.utils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class ThreadLocalDoubleArray extends ThreadLocal<double[]> {
|
||||
private final int size;
|
||||
|
||||
public ThreadLocalDoubleArray(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected double[] initialValue() {
|
||||
return new double[size];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user