Public source code release

This commit is contained in:
YTKAB0BP
2024-11-01 08:28:55 +03:00
parent 20b730b1c8
commit 0b2ba24c7f
6691 changed files with 2325292 additions and 1 deletions
@@ -0,0 +1,93 @@
package ru.ytkab0bp.slicebeam;
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 ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
import ru.ytkab0bp.slicebeam.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, "SliceBeam/%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 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 {
SliceBeam.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);
SliceBeam.EVENT_BUS.fireEvent(new BeamServerDataUpdatedEvent());
}
});
} else {
SliceBeam.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,657 @@
package ru.ytkab0bp.slicebeam;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
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.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.zip.ZipFile;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.ChangeLogBottomSheet;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.navigation.MobileNavigationDelegate;
import ru.ytkab0bp.slicebeam.navigation.NavigationDelegate;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rConfigWrapper;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.IOUtils;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class MainActivity extends AppCompatActivity {
public final static int REQUEST_CODE_OPEN_FILE = 1, REQUEST_CODE_EXPORT_GCODE = 2,
REQUEST_CODE_IMPORT_PROFILES = 3, REQUEST_CODE_EXPORT_PROFILES = 4;
private static MainActivity activeInstance;
public static List<ConfigObject> EXPORTING_PRINTS;
public static List<ConfigObject> EXPORTING_FILAMENTS;
public static List<ConfigObject> EXPORTING_PRINTERS;
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 (SliceBeam.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) && SliceBeam.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);
}
@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_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(SliceBeam.CONFIG.presets.get("print")) != null) {
w.presets.put("print", SliceBeam.CONFIG.presets.get("print"));
}
if (w.findFilament(SliceBeam.CONFIG.presets.get("filament")) != null) {
w.presets.put("filament", SliceBeam.CONFIG.presets.get("filament"));
}
if (w.findPrinter(SliceBeam.CONFIG.presets.get("printer")) != null) {
w.presets.put("printer", SliceBeam.CONFIG.presets.get("printer"));
}
OutputStream out = getContentResolver().openOutputStream(data.getData());
out.write(w.serialize().getBytes(StandardCharsets.UTF_8));
out.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
} else if (requestCode == MainActivity.REQUEST_CODE_IMPORT_PROFILES) {
Uri uri = data.getData();
ContentResolver resolver = 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();
}
}
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")) {
Toast.makeText(MainActivity.this, R.string.OrcaConversionPleaseWait, Toast.LENGTH_SHORT).show();
File f = new File(SliceBeam.getModelCacheDir(), "orca_conv.zip");
new Thread(()->{
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();
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();
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();
loadIniForImport(new ByteArrayInputStream(w.serialize().getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
ViewUtils.postOnMainThread(() -> {
new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileImportProfilesFailed)
.setMessage(e.toString())
.setPositiveButton(android.R.string.ok, null)
.show();
});
}
}).start();
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(resolver.openInputStream(uri));
} catch (FileNotFoundException e) {
new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileImportProfilesFailed)
.setMessage(e.toString())
.setPositiveButton(android.R.string.ok, null)
.show();
}
}
}
}
private void loadIniForImport(InputStream in) {
new Thread(()->{
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;
}
new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileExportProfilesPrints)
.setMultiChoiceItems(prints, enabledPrints, (dialog, which, isChecked) -> enabledPrints[which] = isChecked)
.setPositiveButton(android.R.string.ok, (d1, w1) -> new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileExportProfilesFilaments)
.setMultiChoiceItems(filaments, enabledFilaments, (dialog, which, isChecked) -> enabledFilaments[which] = isChecked)
.setPositiveButton(android.R.string.ok, (d2, w2) -> new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileExportProfilesPrinters)
.setMultiChoiceItems(printers, enabledPrinters, (dialog, which, isChecked) -> enabledPrinters[which] = isChecked)
.setPositiveButton(android.R.string.ok, (d3, w3) -> {
for (int i = 0; i < enabledPrints.length; i++) {
if (enabledPrints[i]) {
SliceBeam.CONFIG.importPrint(w.printConfigs.get(i));
}
}
for (int i = 0; i < enabledFilaments.length; i++) {
if (enabledFilaments[i]) {
SliceBeam.CONFIG.importFilament(w.filamentConfigs.get(i));
}
}
for (int i = 0; i < enabledPrinters.length; i++) {
if (enabledPrinters[i]) {
SliceBeam.CONFIG.importPrinter(w.printerConfigs.get(i));
}
}
SliceBeam.saveConfig();
})
.setNegativeButton(android.R.string.cancel, null)
.show())
.setNegativeButton(android.R.string.cancel, null)
.show())
.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());
}
}).start();
}
private void loadFile(Uri uri) {
if (uri == null) return;
ContentResolver resolver = 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();
}
}
if (fileName == null) {
new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileOpenFileFailed)
.setMessage(R.string.MenuFileOpenFileFailedNullName)
.setPositiveButton(android.R.string.ok, null)
.show();
return;
}
File f = new File(SliceBeam.getModelCacheDir(), fileName);
// TODO: Check if file already exists
new Thread(()->{
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();
ViewUtils.postOnMainThread(() -> {
if (delegate.getCurrentFragment() instanceof BedFragment) {
BedFragment fragment = (BedFragment) delegate.getCurrentFragment();
try {
if (f.getName().endsWith(".gcode")) {
fragment.loadGCode(f);
} else {
fragment.loadModel(f);
SliceBeam.EVENT_BUS.fireEvent(new ObjectsListChangedEvent());
}
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileOpenFileLoaded));
} catch (Slic3rRuntimeError e) {
Log.e("MainActivity", "Failed to load model", e);
f.delete();
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileOpenFileFailed)
.setMessage(e.toString())
.setPositiveButton(android.R.string.ok, null)
.show());
}
}
});
} 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());
}
}).start();
}
@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 ru.ytkab0bp.slicebeam;
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 ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.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);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,151 @@
package ru.ytkab0bp.slicebeam;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Intent;
import android.util.Log;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
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.Map;
import ru.ytkab0bp.eventbus.EventBus;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rConfigWrapper;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.VibrationUtils;
public class SliceBeam extends Application {
public static SliceBeam INSTANCE;
public static EventBus EVENT_BUS = EventBus.newBus("main");
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;
EventBus.registerImpl(this);
Prefs.init(this);
VibrationUtils.init(this);
tryCheckInfo();
PrintConfigDef.getInstance();
try {
getAssets().open("update.json").close();
hasUpdateInfo = true;
} catch (IOException e) {
hasUpdateInfo = false;
}
File cache = SliceBeam.getModelCacheDir();
if (cache.exists()) {
for (File f : cache.listFiles()) {
f.delete();
}
}
File cfgFile = getConfigFile();
getCurrentConfigFile().delete();
if (cfgFile.exists()) {
try {
CONFIG = new Slic3rConfigWrapper(cfgFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
WebView.setWebContentsDebuggingEnabled(true);
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);
});
}
private static void tryCheckInfo() {
try {
SERVER_DATA = new BeamServerData(new JSONObject(Prefs.getBeamServerData()));
} catch (JSONException e) {
throw new RuntimeException(e);
}
if (System.currentTimeMillis() - Prefs.getLastCheckedInfo() >= 86400000L) {
BeamServerData.load();
}
}
public static void saveConfig() {
SliceBeam.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);
}
}
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();
singleObject.values.putAll(SliceBeam.CONFIG.findPrinter(SliceBeam.CONFIG.presets.get("printer")).values);
if (SliceBeam.CONFIG.findPrint(SliceBeam.CONFIG.presets.get("print")) != null) {
singleObject.values.putAll(SliceBeam.CONFIG.findPrint(SliceBeam.CONFIG.presets.get("print")).values);
}
if (SliceBeam.CONFIG.findFilament(SliceBeam.CONFIG.presets.get("filament")) != null) {
singleObject.values.putAll(SliceBeam.CONFIG.findFilament(SliceBeam.CONFIG.presets.get("filament")).values);
}
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();
if (!cfg.exists()) {
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");
}
}
@@ -0,0 +1,176 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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,249 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.BeamServerData;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.BeamButton;
import ru.ytkab0bp.slicebeam.view.BoostySubsView;
public class ChangeLogBottomSheet extends BottomSheetDialog {
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);
BoostySubsView subsView = new BoostySubsView(context);
if (SliceBeam.SERVER_DATA != null) {
List<String> list = new ArrayList<>(SliceBeam.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);
SliceBeam.EVENT_BUS.registerListener(this);
setOnDismissListener(dialog -> SliceBeam.EVENT_BUS.unregisterListener(this));
}
@EventHandler(runOnMainThread = true)
public void onDataUpdated(BeamServerDataUpdatedEvent e) {
pager.getAdapter().notifyDataSetChanged();
}
@Override
public void show() {
super.show();
getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
@@ -0,0 +1,133 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.events.SlicingProgressEvent;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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();
SliceBeam.EVENT_BUS.registerListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
SliceBeam.EVENT_BUS.unregisterListener(this);
}
}
@@ -0,0 +1,248 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.MirrorView;
public abstract class UnfoldMenu {
protected BedFragment fragment;
private boolean isVisible;
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;
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 : 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 onDetachedFromWindow() {
super.onDetachedFromWindow();
if (isVisible) {
onDestroy();
isVisible = false;
}
}
};
rootView.addView(mirror);
rootView.addView(innerView = onCreateView(ctx, portrait));
innerView.setAlpha(0f);
rootView.setTranslationX(fromTranslationX);
rootView.setTranslationY(fromTranslationY);
onCreate();
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();
}
});
spring.start();
}
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 void dismiss() {
dismiss(false);
}
public void dismiss(boolean alphaOnly) {
if (!isVisible) return;
this.isVisible = false;
onDestroy();
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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.events.NeedDismissCalibrationsMenu;
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.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(), "SliceBeam");
WebSettings settings = webView.getSettings();
settings.setUserAgentString(String.format(Locale.ROOT, "SliceBeam/%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.getWidth();
}
private final class Bridge {
@JavascriptInterface
public void beginDownload(String filename) {
cacheFile = new File(SliceBeam.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);
SliceBeam.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 ru.ytkab0bp.slicebeam.components.bed_menu;
import android.content.Context;
import android.view.View;
import androidx.annotation.CallSuper;
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.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,189 @@
package ru.ytkab0bp.slicebeam.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.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.graphics.ColorUtils;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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 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 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 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 float checkedProgress;
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);
}});
setWillNotDraw(false);
onApplyTheme();
}
@Override
public void draw(@NonNull Canvas canvas) {
int rad = ViewUtils.dp(16);
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), rad, rad, bgPaint);
if (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);
}
@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) {
title.setMaxLines(item.isSingleLine ? 1 : 2);
title.setText(item.titleRes);
icon.setImageResource(item.iconRes);
checkedProgress = item.isCheckable && item.isChecked ? 1 : 0;
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));
}
}
}
@@ -0,0 +1,171 @@
package ru.ytkab0bp.slicebeam.components.bed_menu;
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 ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
import ru.ytkab0bp.slicebeam.render.Camera;
import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.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.MenuCameraEnableRotation, R.drawable.sync_outline_28).setCheckable((buttonView, isChecked) -> Prefs.setRotationEnabled(isChecked), Prefs.isRotationEnabled()),
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,362 @@
package ru.ytkab0bp.slicebeam.components.bed_menu;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
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.graphics.ColorUtils;
import androidx.recyclerview.widget.RecyclerView;
import java.io.ByteArrayOutputStream;
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 ru.ytkab0bp.slicebeam.MainActivity;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.components.WebViewMenu;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.NeedDismissCalibrationsMenu;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
import ru.ytkab0bp.slicebeam.events.SelectedObjectChangedEvent;
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.FadeRecyclerView;
public class FileMenu extends ListBedMenu {
private final static List<String> K3D_SUPPORTED_LANGUAGES = Arrays.asList("en", "ru");
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 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.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)),
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[SliceBeam.CONFIG.printConfigs.size()];
boolean[] enabledPrints = new boolean[prints.length];
for (int i = 0; i < prints.length; i++) {
prints[i] = SliceBeam.CONFIG.printConfigs.get(i).getTitle();
enabledPrints[i] = true;
}
CharSequence[] filaments = new CharSequence[SliceBeam.CONFIG.filamentConfigs.size()];
boolean[] enabledFilaments = new boolean[filaments.length];
for (int i = 0; i < filaments.length; i++) {
filaments[i] = SliceBeam.CONFIG.filamentConfigs.get(i).getTitle();
enabledFilaments[i] = true;
}
CharSequence[] printers = new CharSequence[SliceBeam.CONFIG.printerConfigs.size()];
boolean[] enabledPrinters = new boolean[printers.length];
for (int i = 0; i < printers.length; i++) {
printers[i] = SliceBeam.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(SliceBeam.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(SliceBeam.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(SliceBeam.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();
})
);
}
@EventHandler(runOnMainThread = true)
public void onObjectsChanged(ObjectsListChangedEvent e) {
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
adapter.notifyItemChanged(1);
}
@EventHandler(runOnMainThread = true)
public void onSelectionChanged(SelectedObjectChangedEvent e) {
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
adapter.notifyItemChanged(1);
}
public final class CalibrationsMenu extends UnfoldMenu {
public int getRequestedSize(FrameLayout into, boolean portrait) {
return (int) (portrait ? into.getHeight() * 0.3f : into.getWidth() * 0.6f);
}
private String loadJSLoader(String key) {
try {
InputStream in = SliceBeam.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 = SliceBeam.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);
}
})
));
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();
SliceBeam.EVENT_BUS.registerListener(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
SliceBeam.EVENT_BUS.unregisterListener(this);
}
}
}
@@ -0,0 +1,80 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.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);
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);
SliceBeam.EVENT_BUS.registerListener(ListBedMenu.this);
}
@Override
public void onViewDestroyed() {
super.onViewDestroyed();
SliceBeam.EVENT_BUS.unregisterListener(ListBedMenu.this);
}
protected abstract List<SimpleRecyclerItem> onCreateItems(boolean portrait);
}
@@ -0,0 +1,654 @@
package ru.ytkab0bp.slicebeam.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 java.util.Arrays;
import java.util.List;
import java.util.Locale;
import ru.ytkab0bp.eventbus.EventHandler;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
import ru.ytkab0bp.slicebeam.events.SelectedObjectChangedEvent;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
import ru.ytkab0bp.slicebeam.slic3r.Model;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.BeamButton;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.PositionScrollView;
import ru.ytkab0bp.slicebeam.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();
SliceBeam.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.MenuOrientationPosition, R.drawable.menu_orientation_position_28).setEnabled(hasSelection()).onClick(v -> fragment.showUnfoldMenu(new PositionMenu(), v)),
new BedMenuItem(R.string.MenuOrientationRotation, R.drawable.menu_orientation_rotation_28).setEnabled(hasSelection()).onClick(v -> fragment.showUnfoldMenu(new RotationMenu(), v))
);
}
@EventHandler(runOnMainThread = true)
public void onObjectsChanged(ObjectsListChangedEvent e) {
((BedMenuItem) adapter.getItems().get(0)).setEnabled(fragment.getGlView().getRenderer().getModel() != null);
adapter.notifyItemChanged(0);
((BedMenuItem) adapter.getItems().get(2)).setEnabled(hasSelection());
adapter.notifyItemChanged(2);
((BedMenuItem) adapter.getItems().get(3)).setEnabled(hasSelection());
adapter.notifyItemChanged(3);
}
@EventHandler(runOnMainThread = true)
public void onSelectionChanged(SelectedObjectChangedEvent e) {
((BedMenuItem) adapter.getItems().get(2)).setEnabled(hasSelection());
adapter.notifyItemChanged(2);
((BedMenuItem) adapter.getItems().get(3)).setEnabled(hasSelection());
adapter.notifyItemChanged(3);
}
public final class PositionMenu extends UnfoldMenu {
private PositionScrollView xTrack, yTrack, zTrack;
private TextView xTitle, yTitle, zTitle;
private Vec3d tempVec = new Vec3d();
private int startedScrollObject;
private 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(SliceBeam.INSTANCE.getString(res, value));
sb.append(" d");
int size = ViewUtils.dp(14);
Drawable dr = ContextCompat.getDrawable(SliceBeam.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();
SliceBeam.EVENT_BUS.registerListener(this);
setSelectionValues();
}
@Override
protected void onDestroy() {
super.onDestroy();
SliceBeam.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 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.getBoundingBoxExact(j, bbMin, bbMax);
model.translate(j, 0, 0, -bbMin.z);
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(SliceBeam.INSTANCE.getString(res, value));
sb.append(" d");
int size = ViewUtils.dp(14);
Drawable dr = ContextCompat.getDrawable(SliceBeam.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();
SliceBeam.EVENT_BUS.registerListener(this);
setSelectionValues();
}
@Override
protected void onDestroy() {
super.onDestroy();
SliceBeam.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,293 @@
package ru.ytkab0bp.slicebeam.components.bed_menu;
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
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.Space;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.core.util.Pair;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.entity.ContentType;
import cz.msebera.android.httpclient.message.BasicHeader;
import ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.MainActivity;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.slic3r.GCodeViewer;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.PositionScrollView;
public class SliceMenu extends ListBedMenu {
private AsyncHttpClient client = new AsyncHttpClient();
{
client.setLoggingEnabled(true);
client.setMaxRetriesAndTimeout(0, 5000);
}
private final static List<String> SUPPORTED_SEND = Collections.singletonList("octoprint");
private int lastUid;
@Override
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
lastUid = SliceBeam.CONFIG_UID;
List<SimpleRecyclerItem> items = new ArrayList<>(Arrays.asList(
new BedMenuItem(R.string.MenuSliceInfo, R.drawable.square_stack_up_outline_28).onClick(v -> fragment.showUnfoldMenu(new PrintInfoMenu(), 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 = SliceBeam.CONFIG.findPrinter(SliceBeam.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;
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinter, R.drawable.send_outline_28).onClick(v -> upload(finalType, host, apiKey, false)));
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinterAndPrint, R.drawable.send_28).onClick(v -> upload(finalType, host, apiKey, true)));
}
return items;
}
private void upload(String type, String host, String apiKey, boolean print) {
String name = fragment.getGlView().getRenderer().getGcodeResult().getRecommendedName();
switch (type) {
default:
case "octoprint":
if (!host.startsWith("http://")) {
host = "http://" + host;
}
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuSliceSendToPrinterStarted));
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(SliceBeam.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());
}
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(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) {
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(fragment.getContext())
.setTitle(R.string.MenuSliceSendToPrinterFailed)
.setMessage(error.toString())
.setPositiveButton(android.R.string.ok, null)
.show());
}
});
break;
}
}
@Override
public void onViewCreated(View v) {
super.onViewCreated(v);
v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(@NonNull View v) {
if (lastUid != SliceBeam.CONFIG_UID) {
adapter.setItems(onCreateItems(v.getWidth() < v.getHeight()));
}
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) {}
});
}
private final static class PrintInfoMenu extends UnfoldMenu {
private PositionScrollView fromTrack, toTrack;
private TextView title;
private GCodeViewer getViewer() {
return fragment.getGlView().getRenderer().getViewer();
}
private void applyView(int from, int to) {
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);
}
applyView(integer, toTrack.getCurrentPosition());
});
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 -> {
// TODO: apply only visual?
applyView(fromTrack.getCurrentPosition(), integer);
});
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();
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();
if (getViewer() != null) {
applyView(1, (int) getViewer().getLayersCount());
}
}
}
}
@@ -0,0 +1,488 @@
package ru.ytkab0bp.slicebeam.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 java.util.Arrays;
import java.util.List;
import java.util.Locale;
import ru.ytkab0bp.eventbus.EventHandler;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
import ru.ytkab0bp.slicebeam.events.SelectedObjectChangedEvent;
import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.slic3r.Model;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.DoubleMatrix;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.PositionScrollView;
import ru.ytkab0bp.slicebeam.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.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);
}
@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);
}
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;
if (x != null) {
xTitle.setText(formatTrackTitle(R.string.MenuTransformScaleXValue, x * 100));
}
if (y != null) {
yTitle.setText(formatTrackTitle(R.string.MenuTransformScaleYValue, y * 100));
}
if (z != null) {
zTitle.setText(formatTrackTitle(R.string.MenuTransformScaleZValue, z * 100));
}
Model model = fragment.getGlView().getRenderer().getModel();
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();
double dx = 1f, dy = 1f, dz = 1f;
if (x != null) {
dx = x;
xTitle.setText(formatTrackTitle(R.string.MenuTransformScaleXValue, x * 100));
}
if (y != null) {
dy = y;
yTitle.setText(formatTrackTitle(R.string.MenuTransformScaleYValue, y * 100));
}
if (z != null) {
dz = z;
zTitle.setText(formatTrackTitle(R.string.MenuTransformScaleZValue, z * 100));
}
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.getBoundingBoxExact(j, tempVec, tempVec2);
model.translate(j, 0, 0, -tempVec.z);
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) {
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(SliceBeam.INSTANCE.getString(res, value));
sb.append(" d");
int size = ViewUtils.dp(14);
Drawable dr = ContextCompat.getDrawable(SliceBeam.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);
double current;
if (x) {
current = tempVec.x * 100;
} else if (y) {
current = tempVec.y * 100;
} else {
current = tempVec.z * 100;
}
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);
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()) / 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();
SliceBeam.EVENT_BUS.registerListener(this);
setSelectionValues();
}
@Override
protected void onDestroy() {
super.onDestroy();
SliceBeam.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);
xTrack.setCurrentPosition((int) Math.round(tempVec.x * 100));
yTrack.setCurrentPosition((int) Math.round(tempVec.y * 100));
zTrack.setCurrentPosition((int) Math.round(tempVec.z * 100));
xTitle.setText(formatTrackTitle(R.string.MenuTransformScaleXValue, tempVec.x * 100));
yTitle.setText(formatTrackTitle(R.string.MenuTransformScaleYValue, tempVec.y * 100));
zTitle.setText(formatTrackTitle(R.string.MenuTransformScaleZValue, tempVec.z * 100));
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,133 @@
package ru.ytkab0bp.slicebeam.config;
import java.util.HashMap;
import java.util.Map;
import ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.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);
}
public String get(String key) {
return values.get(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(SliceBeam.CONFIG.presets.get("print"));
case PROFILE_LIST_FILAMENT:
return getTitle().equals(SliceBeam.CONFIG.presets.get("filament"));
case PROFILE_LIST_PRINTER:
return getTitle().equals(SliceBeam.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(SliceBeam.INSTANCE.getString(R.string.IntroCustomProfileName));
custom.put("printer_technology", "FFF");
custom.put("bed_shape", "0x0,200x0,200x200,0x200");
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("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(SliceBeam.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 ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class BeamServerDataUpdatedEvent {}
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class NeedDismissCalibrationsMenu {}
@@ -0,0 +1,17 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
import ru.ytkab0bp.slicebeam.SliceBeam;
@Event
public class NeedSnackbarEvent {
public final CharSequence title;
public NeedSnackbarEvent(CharSequence title) {
this.title = title;
}
public NeedSnackbarEvent(int title, Object... args) {
this.title = SliceBeam.INSTANCE.getString(title, args);
}
}
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class ObjectsListChangedEvent {}
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class SelectedObjectChangedEvent {}
@@ -0,0 +1,11 @@
package ru.ytkab0bp.slicebeam.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,504 @@
package ru.ytkab0bp.slicebeam.fragment;
import android.app.Activity;
import android.content.Context;
import android.os.Process;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.google.android.material.navigation.NavigationBarView;
import com.google.android.material.snackbar.Snackbar;
import java.io.File;
import ru.ytkab0bp.eventbus.EventHandler;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.SliceProgressBottomSheet;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.components.bed_menu.BedMenu;
import ru.ytkab0bp.slicebeam.components.bed_menu.CameraMenu;
import ru.ytkab0bp.slicebeam.components.bed_menu.FileMenu;
import ru.ytkab0bp.slicebeam.components.bed_menu.OrientationMenu;
import ru.ytkab0bp.slicebeam.components.bed_menu.SliceMenu;
import ru.ytkab0bp.slicebeam.components.bed_menu.TransformMenu;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.SlicingProgressEvent;
import ru.ytkab0bp.slicebeam.navigation.Fragment;
import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
import ru.ytkab0bp.slicebeam.slic3r.GCodeProcessorResult;
import ru.ytkab0bp.slicebeam.slic3r.Model;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.GLView;
import ru.ytkab0bp.slicebeam.view.ThemeBottomNavigationView;
import ru.ytkab0bp.slicebeam.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 CoordinatorLayout 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 static String tempFileName;
private static File tempExportingFile;
public static String getTempFileName() {
return tempFileName;
}
public static File getTempGCodePath() {
return tempExportingFile != null ? tempExportingFile : new File(SliceBeam.INSTANCE.getCacheDir(), "temp.gcode");
}
@Override
public void onCreate() {
super.onCreate();
SliceBeam.EVENT_BUS.registerListener(this);
}
@EventHandler(runOnMainThread = true)
public void onNeedSnackbar(NeedSnackbarEvent e) {
Snackbar.make(snackbarsLayout, e.title, Snackbar.LENGTH_SHORT).show();
}
public void showUnfoldMenu(UnfoldMenu menu, View from) {
if (currentUnfoldMenu != null) return;
menu.setOnDismiss(()-> 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 (currentMenuSlot != 0) {
navigationView.setSelectedItemId(0);
return true;
}
return super.onBackPressed();
}
@Override
public void onDestroy() {
super.onDestroy();
SliceBeam.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();
}
@Override
public void onPause() {
super.onPause();
glView.onPause();
}
@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));
}
ll.addView(glView, new LinearLayout.LayoutParams(portrait ? ViewGroup.LayoutParams.MATCH_PARENT : 0, portrait ? 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 = SliceBeam.getCurrentConfigFile();
File gcode = getTempGCodePath();
if (!DEBUG_VIEWER) {
new SliceProgressBottomSheet(ctx).show();
}
new Thread(()->{
try {
Process.setThreadPriority(-20);
try {
SliceBeam.genCurrentConfig();
} catch (Exception e) {
Log.e("BedFragment", "Failed to write config", e);
ViewUtils.postOnMainThread(()->{
SliceBeam.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) -> SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(progress, text)));
SliceBeam.EVENT_BUS.fireEvent(new SlicingProgressEvent(100, ""));
} else {
gCodeResult = new GCodeProcessorResult(gcode);
}
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(()->{
SliceBeam.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;
});
overlayLayout.addView(contentView = ll);
overlayLayout.addView(snackbarsLayout = new CoordinatorLayout(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 CoordinatorLayout getSnackbarsLayout() {
return snackbarsLayout;
}
public FrameLayout getOverlayLayout() {
return overlayLayout;
}
private void selectMenu(Context ctx, boolean portrait, int slot) {
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.getRenderer().setModel(model = m);
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);
}
}
});
}
glView.requestRender();
}
@Override
public void onApplyTheme() {
super.onApplyTheme();
menuView.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
}
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,278 @@
package ru.ytkab0bp.slicebeam.fragment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rUtils;
import ru.ytkab0bp.slicebeam.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 = SliceBeam.CONFIG.filamentConfigs;
if (filter) {
String printer = SliceBeam.CONFIG.presets.get("printer");
String print = SliceBeam.CONFIG.presets.get("print");
if (Objects.equals(lastPrinter, printer) && Objects.equals(lastPrint, print) && compatItems != null && lastUid == SliceBeam.CONFIG_UID) {
return compatItems;
}
List<ConfigObject> nList = new ArrayList<>(list.size());
Slic3rUtils.ConfigChecker checker = new Slic3rUtils.ConfigChecker(SliceBeam.CONFIG.findPrinter(printer).serialize());
Slic3rUtils.ConfigChecker printChecker = new Slic3rUtils.ConfigChecker(SliceBeam.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 = SliceBeam.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(SliceBeam.INSTANCE.getString(R.string.SettingsProfileCopy, currentConfig.getTitle()));
obj.values.putAll(currentConfig.values);
currentConfig = new ConfigObject(obj);
SliceBeam.CONFIG.filamentConfigs.add(obj);
SliceBeam.CONFIG.presets.put("filament", obj.getTitle());
SliceBeam.saveConfig();
SliceBeam.getCurrentConfigFile().delete();
currentConfig = new ConfigObject(obj);
dropdownView.setTitle(getCurrentConfig().getTitle());
compatItems = null;
}
@Override
protected void deleteCurrentProfile() {
compatItems = null;
SliceBeam.CONFIG.filamentConfigs.remove(SliceBeam.CONFIG.findFilament(currentConfig.getTitle()));
selectItem(getItems(true).get(0));
dropdownView.setTitle(getCurrentConfig().getTitle());
}
@Override
protected void onApplyConfig(String title) {
compatItems = null;
ConfigObject obj = SliceBeam.CONFIG.findFilament(currentConfig.getTitle());
obj.setTitle(title);
obj.values.putAll(currentConfig.values);
currentConfig.setTitle(title);
SliceBeam.CONFIG.presets.put("filament", title);
SliceBeam.saveConfig();
SliceBeam.getCurrentConfigFile().delete();
dropdownView.setTitle(title);
}
@Override
protected void onResetConfig() {
currentConfig = new ConfigObject(SliceBeam.CONFIG.findFilament(SliceBeam.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);
SliceBeam.CONFIG.presets.put("filament", item.getTitle());
SliceBeam.saveConfig();
}
}
@@ -0,0 +1,424 @@
package ru.ytkab0bp.slicebeam.fragment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rUtils;
import ru.ytkab0bp.slicebeam.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 = SliceBeam.CONFIG.printConfigs;
if (filter) {
String printer = SliceBeam.CONFIG.presets.get("printer");
if (Objects.equals(lastPrinter, printer) && compatItems != null && lastUid == SliceBeam.CONFIG_UID) {
return compatItems;
}
List<ConfigObject> nList = new ArrayList<>(list.size());
Slic3rUtils.ConfigChecker checker = new Slic3rUtils.ConfigChecker(SliceBeam.CONFIG.findPrinter(printer).serialize());
for (ConfigObject obj : list) {
if (checker.checkCompatibility(obj.get("compatible_printers_condition"))) {
nList.add(obj);
}
}
checker.release();
lastPrinter = printer;
lastUid = SliceBeam.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(SliceBeam.INSTANCE.getString(R.string.SettingsProfileCopy, currentConfig.getTitle()));
obj.values.putAll(currentConfig.values);
currentConfig = new ConfigObject(obj);
SliceBeam.CONFIG.printConfigs.add(obj);
SliceBeam.CONFIG.presets.put("print", obj.getTitle());
SliceBeam.saveConfig();
SliceBeam.getCurrentConfigFile().delete();
currentConfig = new ConfigObject(obj);
dropdownView.setTitle(getCurrentConfig().getTitle());
compatItems = null;
}
@Override
protected void deleteCurrentProfile() {
compatItems = null;
SliceBeam.CONFIG.printConfigs.remove(SliceBeam.CONFIG.findPrint(currentConfig.getTitle()));
selectItem(getItems(true).get(0));
dropdownView.setTitle(getCurrentConfig().getTitle());
}
@Override
protected void onApplyConfig(String title) {
compatItems = null;
ConfigObject obj = SliceBeam.CONFIG.findPrint(currentConfig.getTitle());
obj.setTitle(title);
obj.values.putAll(currentConfig.values);
currentConfig.setTitle(title);
SliceBeam.CONFIG.presets.put("print", title);
SliceBeam.saveConfig();
SliceBeam.getCurrentConfigFile().delete();
dropdownView.setTitle(title);
}
@Override
protected void onResetConfig() {
ConfigObject print = SliceBeam.CONFIG.findPrint(SliceBeam.CONFIG.presets.get("print"));
if (print != null) {
currentConfig = new ConfigObject(print);
} else {
currentConfig = new ConfigObject(SliceBeam.INSTANCE.getString(R.string.IntroCustomProfileName));
SliceBeam.CONFIG.printConfigs.add(new ConfigObject(currentConfig));
SliceBeam.saveConfig();
SliceBeam.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);
SliceBeam.CONFIG.presets.put("print", item.getTitle());
SliceBeam.saveConfig();
}
}
@@ -0,0 +1,241 @@
package ru.ytkab0bp.slicebeam.fragment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
import ru.ytkab0bp.slicebeam.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) SliceBeam.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")),
// TODO: Thumbnails are not working *yet*
// 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.get("nozzle_diameter") != null ? currentConfig.get("nozzle_diameter").replaceAll("[^.]+", "").length() : 1;
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.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"))
));
return list;
}
@Override
protected void cloneCurrentProfile() {
ConfigObject obj = new ConfigObject(SliceBeam.INSTANCE.getString(R.string.SettingsProfileCopy, currentConfig.getTitle()));
obj.values.putAll(currentConfig.values);
currentConfig = new ConfigObject(obj);
SliceBeam.CONFIG.printerConfigs.add(obj);
SliceBeam.CONFIG.presets.put("printer", obj.getTitle());
SliceBeam.saveConfig();
SliceBeam.getCurrentConfigFile().delete();
currentConfig = new ConfigObject(obj);
dropdownView.setTitle(getCurrentConfig().getTitle());
}
@Override
protected void deleteCurrentProfile() {
SliceBeam.CONFIG.printerConfigs.remove(SliceBeam.CONFIG.findPrinter(currentConfig.getTitle()));
selectItem(getItems(true).get(0));
dropdownView.setTitle(getCurrentConfig().getTitle());
}
@Override
protected void onApplyConfig(String title) {
ConfigObject obj = SliceBeam.CONFIG.findPrinter(currentConfig.getTitle());
obj.setTitle(title);
obj.values.putAll(currentConfig.values);
currentConfig.setTitle(title);
SliceBeam.CONFIG.presets.put("printer", title);
SliceBeam.saveConfig();
SliceBeam.getCurrentConfigFile().delete();
dropdownView.setTitle(title);
}
@Override
protected void onResetConfig() {
currentConfig = new ConfigObject(SliceBeam.CONFIG.findPrinter(SliceBeam.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);
SliceBeam.CONFIG.presets.put("printer", item.getTitle());
// TODO: Reset print/filament profiles, maybe physical profiles?
SliceBeam.saveConfig();
}
}
@@ -0,0 +1,892 @@
package ru.ytkab0bp.slicebeam.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
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.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.graphics.ColorUtils;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import com.mrudultora.colorpicker.ColorPickerPopUp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.BeamColorPickerPopUp;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.navigation.Fragment;
import ru.ytkab0bp.slicebeam.recycler.CubicBezierItemAnimator;
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rConfigWrapper;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.BeamButton;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.FadeRecyclerView;
import ru.ytkab0bp.slicebeam.view.ProfileDropdownView;
public abstract class ProfileListFragment extends Fragment {
private final static Object ROTATION_PAYLOAD = new Object();
protected ProfileDropdownView dropdownView;
protected FadeRecyclerView recyclerView;
protected ImageView resetButton;
protected BeamButton saveButton;
protected boolean changedConfig;
private List<OptionWrapper> currentList = Collections.emptyList();
private SparseArray<List<OptionWrapper>> categoryElements = new SparseArray<>();
private SparseBooleanArray unfolded = new SparseBooleanArray();
@Override
public View onCreateView(Context ctx) {
FrameLayout containerLayout = new FrameLayout(ctx);
LinearLayout ll = new LinearLayout(ctx);
ll.setOrientation(LinearLayout.VERTICAL);
dropdownView = new ProfileDropdownView(ctx);
ProfileListItem selectedItem = getSelectedItem();
dropdownView.setTitle(selectedItem != null ? selectedItem.getTitle() : null);
dropdownView.setOnClickListener(v -> {
List<ProfileListItem> items = getItems(true);
String[] titles = new String[items.size()];
int selected = -1;
for (int i = 0; i < items.size(); i++) {
ProfileListItem item = items.get(i);
titles[i] = item.getTitle();
if (item.isSelected()) {
selected = i;
}
}
AlertDialog.Builder builder = new BeamAlertDialogBuilder(getContext())
.setTitle(getTitle())
.setSingleChoiceItems(titles, selected, (dialog, which) -> {
dropdownView.setTitle(items.get(which).getTitle());
selectItem(items.get(which));
onUpdateConfigItems();
dialog.dismiss();
});
if (items.size() > 1) {
builder.setNegativeButton(R.string.SettingsDeleteProfile, (dialog, which) -> {
deleteCurrentProfile();
onUpdateConfigItems();
});
}
builder.setPositiveButton(R.string.SettingsCloneProfile, (dialog, which) -> {
cloneCurrentProfile();
onUpdateConfigItems();
});
builder.show();
});
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
boolean portrait = dm.widthPixels < dm.heightPixels;
ll.addView(dropdownView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{
topMargin = portrait ? 0 : ViewUtils.dp(12);
leftMargin = rightMargin = ViewUtils.dp(12);
bottomMargin = ViewUtils.dp(8);
}});
recyclerView = new FadeRecyclerView(ctx) {
private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
{
onApplyTheme();
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
int startI = -1;
boolean drawn = false;
for (int i = 0; i < getChildCount(); i++) {
View ch = getChildAt(i);
int pos = getChildViewHolder(ch).getAdapterPosition();
if (pos == -1 || ch.getAlpha() < 1) continue;
boolean top = currentList.get(pos).title != null;
boolean bottom = pos == getAdapter().getItemCount() - 1 || currentList.get(pos + 1).title != null;
if (top && startI != -1) {
c.drawRoundRect(0, getChildAt(startI).getTop() + getChildAt(startI).getTranslationY(), getWidth(), ch.getTop() + ch.getTranslationY() - ViewUtils.dp(8), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
drawn = true;
startI = -1;
} else if (bottom) {
if (!top && startI == -1) {
c.drawRoundRect(0, -ViewUtils.dp(32), getWidth(), ch.getBottom() + ch.getTranslationY(), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
drawn = true;
}
}
if (top) {
int color = ch.getTag() != null ? ThemesRepo.getColor((Integer) ch.getTag()) : 0;
if (color != 0) {
bgPaint.setColor(ColorUtils.setAlphaComponent(color, 0x22));
} else {
bgPaint.setColor(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10));
}
startI = i;
}
if (startI != -1 && bottom && pos == getAdapter().getItemCount() - 1) {
View s = getChildAt(startI);
c.drawRoundRect(0, s.getTop() + s.getTranslationY(), getWidth(), ch.getBottom() + ch.getTranslationY(), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
drawn = true;
startI = -1;
}
}
if (startI != -1) {
View ch = getChildAt(startI);
View last = getChildAt(getChildCount() - 1);
boolean bottom = getChildViewHolder(last).getAdapterPosition() == getAdapter().getItemCount() - 1;
c.drawRoundRect(0, ch.getTop() + ch.getTranslationY(), getWidth(), bottom ? ViewUtils.lerp(ch.getBottom(), last.getBottom(), last.getAlpha()) : getHeight() + ViewUtils.dp(32), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
drawn = true;
}
if (!drawn) {
c.drawRoundRect(0, -ViewUtils.dp(32), getWidth(), getHeight() + ViewUtils.dp(32), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
}
// TODO: Determine when user folds/unfolds category, animate only there
invalidate();
}
@Override
public void onApplyTheme() {
super.onApplyTheme();
if (bgPaint != null) {
bgPaint.setColor(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10));
}
}
};
recyclerView.setItemAnimator(new CubicBezierItemAnimator());
recyclerView.setAdapter(new RecyclerView.Adapter() {
private final static int TYPE_TITLE = 0, TYPE_SIMPLE = 1;
private Map<Class<?>, Integer> viewType = new HashMap<>();
private Map<Integer, SimpleRecyclerItem> viewCreator = new HashMap<>();
private int lastType = TYPE_SIMPLE;
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v;
switch (viewType) {
default: {
v = viewCreator.get(viewType).onCreateView(ctx);
break;
}
case TYPE_TITLE:
v = new CategoryHolderView(ctx);
break;
}
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, @NonNull List payloads) {
if (payloads.contains(ROTATION_PAYLOAD)) {
CategoryHolderView holderView = (CategoryHolderView) holder.itemView;
OptionWrapper w = currentList.get(position);
new SpringAnimation(holderView.dropdown, DynamicAnimation.ROTATION)
.setSpring(new SpringForce(unfolded.get(w.categoryIndex) ? 180 : 0)
.setStiffness(1000f)
.setDampingRatio(1f))
.start();
return;
}
super.onBindViewHolder(holder, position, payloads);
}
@SuppressLint("RecyclerView")
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) holder.itemView.getLayoutParams();
boolean top = position == 0 || currentList.get(position).title != null;
params.topMargin = top ? ViewUtils.dp(8) : 0;
params.bottomMargin = position == getItemCount() - 1 ? ViewUtils.dp(8) : 0;
int type = getItemViewType(position);
switch (type) {
default: {
OptionElement el = currentList.get(position).optionEl;
el.boundIndex = position;
el.simpleItem.onBindView(holder.itemView);
break;
}
case TYPE_TITLE: {
OptionWrapper w = currentList.get(position);
CategoryHolderView holderView = (CategoryHolderView) holder.itemView;
holderView.icon.setImageResource(w.icon);
holderView.title.setText(Slic3rLocalization.getString(w.title));
holderView.dropdown.setRotation(unfolded.get(w.categoryIndex) ? 180 : 0);
holderView.dropdown.setVisibility(w.onClick != null ? View.GONE : View.VISIBLE);
holderView.setTag(w.color);
holderView.icon.setTag(w.noTint ? true : null);
holderView.onApplyTheme();
holderView.setOnClickListener(v -> {
if (w.onClick != null) {
w.onClick.run();
return;
}
boolean unfold = !unfolded.get(w.categoryIndex, false);
unfolded.put(w.categoryIndex, unfold);
notifyItemChanged(holder.getAdapterPosition(), ROTATION_PAYLOAD);
int i = holder.getAdapterPosition() + 1;
List<OptionWrapper> l = categoryElements.get(w.categoryIndex);
if (l != null) {
if (unfold) {
currentList.addAll(i, l);
notifyItemRangeInserted(i, l.size());
recyclerView.invalidate();
} else {
currentList.removeAll(l);
notifyItemRangeRemoved(i, l.size());
recyclerView.invalidate();
}
}
});
break;
}
}
}
@Override
public int getItemViewType(int position) {
OptionWrapper w = currentList.get(position);
if (w.title != null) return TYPE_TITLE;
if (w.optionEl.simpleItem != null) {
SimpleRecyclerItem it = w.optionEl.simpleItem;
Integer t = viewType.get(it.getClass());
if (t == null) {
viewType.put(it.getClass(), t = lastType++);
viewCreator.put(t, it);
}
return t;
}
return -1;
}
@Override
public int getItemCount() {
return currentList.size();
}
});
setConfigItems(getConfigItems());
ll.addView(recyclerView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
LinearLayout bottomLL = new LinearLayout(ctx);
bottomLL.setOrientation(LinearLayout.HORIZONTAL);
bottomLL.setGravity(Gravity.CENTER_VERTICAL);
saveButton = new BeamButton(ctx);
saveButton.setText(R.string.SettingsSave);
saveButton.setPadding(ViewUtils.dp(21), ViewUtils.dp(12), ViewUtils.dp(21), ViewUtils.dp(12));
saveButton.setOnClickListener(v -> {
FrameLayout fl = new FrameLayout(ctx);
EditText text = new EditText(ctx);
text.setText(getCurrentConfig().getTitle());
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);
}});
AlertDialog dialog = new BeamAlertDialogBuilder(ctx)
.setTitle(R.string.SettingsSaveTitle)
// TODO: Draw settings delta
.setView(fl)
.setPositiveButton(android.R.string.ok, (d, which) -> {
onApplyConfig(text.getText().toString());
resetButton.animate().alpha(0.6f).start();
resetButton.setClickable(false);
onUpdateConfigItems();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
text.addTextChangedListener(new TextWatcher() {
char[] chars = Slic3rConfigWrapper.BLACKLISTED_SYMBOLS.toCharArray();
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
String str = s.toString();
boolean valid = true;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
for (char aChar : chars) {
if (ch == aChar) {
valid = false;
break;
}
}
if (!valid) break;
}
text.getBackground().setTintList(ColorStateList.valueOf(ThemesRepo.getColor(valid ? android.R.attr.textColorPrimary : R.attr.textColorNegative)));
View btn = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
btn.setAlpha(valid ? 1f : 0.6f);
btn.setClickable(valid);
}
});
// I don't think we need keyboard here every time
// ViewUtils.postOnMainThread(() -> {
// text.requestFocus();
// InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
// imm.showSoftInput(text, 0);
// text.setSelection(text.getText().length());
// }, 500);
});
bottomLL.addView(saveButton, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
resetButton = new ImageView(ctx);
resetButton.setImageResource(R.drawable.refresh_outline_28);
resetButton.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorPrimary)));
resetButton.setScaleX(-1f);
resetButton.setAlpha(0.6f);
resetButton.setOnClickListener(v -> new BeamAlertDialogBuilder(ctx)
.setTitle(R.string.SettingsResetProfileTitle)
.setMessage(R.string.SettingsResetProfileDescription)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
onResetConfig();
resetButton.animate().alpha(0.6f).start();
resetButton.setClickable(false);
onUpdateConfigItems();
})
.setNegativeButton(android.R.string.cancel, null)
.show());
resetButton.setClickable(false);
bottomLL.addView(resetButton, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
leftMargin = ViewUtils.dp(12);
rightMargin = ViewUtils.dp(4);
}});
ll.addView(bottomLL, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
leftMargin = topMargin = rightMargin = bottomMargin = ViewUtils.dp(12);
bottomMargin += portrait ? 0 : ViewUtils.dp(6);
}});
containerLayout.addView(ll);
return containerLayout;
}
@Override
public void onCreate() {
super.onCreate();
SliceBeam.EVENT_BUS.registerListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
SliceBeam.EVENT_BUS.unregisterListener(this);
}
@SuppressLint("NotifyDataSetChanged")
protected void setConfigItems(List<OptionElement> items) {
List<OptionWrapper> list = new ArrayList<>();
int j = 0;
for (int i = 0; i < items.size(); i++) {
OptionElement el = items.get(i);
if (el == null) continue;
OptionWrapper w = el.title != null ? new OptionWrapper(el.icon, el.title, el.onClick, el.color, el.noTint) : new OptionWrapper(el);
if (el.title != null) {
w.categoryIndex = j;
categoryElements.put(j, new ArrayList<>());
j++;
list.add(w);
} else {
categoryElements.get(j - 1).add(w);
}
}
currentList = list;
recyclerView.getAdapter().notifyDataSetChanged();
}
@Override
public void onResume() {
super.onResume();
ProfileListItem selectedItem = getSelectedItem();
dropdownView.setTitle(selectedItem != null ? selectedItem.getTitle() : null);
}
protected ProfileListItem getSelectedItem() {
for (ProfileListItem item : getItems(false)) {
if (item.isSelected()) {
return item;
}
}
return null;
}
private String opt(ConfigOptionDef def, int i) {
String v = getCurrentConfig().get(def.key);
if (i != -1) {
try {
v = v.split(",")[i];
} catch (ArrayIndexOutOfBoundsException e) {
Log.w("ProfileListFragment", "Failed to parse mm option", e);
}
}
return v != null ? v : (Objects.equals("host_type", def.key) ? "octoprint" : def.defaultValue);
}
protected void updateConfigField(ConfigOptionDef def, int i, String value) {
if (i != -1) {
String[] vals = opt(def, -1).split(",");
vals[i] = value;
value = TextUtils.join(",", vals);
}
if (!Objects.equals(opt(def, i), value)) {
changedConfig = true;
resetButton.animate().alpha(1).start();
resetButton.setClickable(true);
}
getCurrentConfig().put(def.key, value);
}
@SuppressLint("NotifyDataSetChanged")
protected void onUpdateConfigItems() {
recyclerView.getAdapter().notifyDataSetChanged();
}
protected abstract void cloneCurrentProfile();
protected abstract void deleteCurrentProfile();
protected abstract void onApplyConfig(String title);
protected abstract void onResetConfig();
protected abstract ConfigObject getCurrentConfig();
protected abstract int getTitle();
protected abstract void selectItem(ProfileListItem item);
protected abstract List<ProfileListItem> getItems(boolean filter);
protected abstract List<OptionElement> getConfigItems();
public interface ProfileListItem {
String getTitle();
boolean isSelected();
}
public final class OptionElement {
public int icon;
public String title;
public int color;
public boolean noTint;
public SimpleRecyclerItem simpleItem;
private Runnable onClick;
private int boundIndex;
public OptionElement(ConfigOptionDef def) {
this(def, -1);
}
public OptionElement(ConfigOptionDef def, int eIndex) {
if (def.type != ConfigOptionDef.ConfigOptionType.BOOL && def.type != ConfigOptionDef.ConfigOptionType.BOOLS) {
simpleItem = new PreferenceItem().setTitle(Slic3rLocalization.getString(def.getLabel())).setOnClickListener(v -> {
if (def.guiType == ConfigOptionDef.GUIType.COLOR) {
int defClr;
try {
defClr = (Color.parseColor(opt(def, eIndex)) & 0xFFFFFF) + 0xFF000000;
} catch (Exception ignored) {
defClr = Prefs.getAccentColor();
}
new BeamColorPickerPopUp(getContext())
.setDialogTitle(Slic3rLocalization.getString(def.getFullLabel()))
.setDefaultColor(defClr)
.setShowAlpha(false)
.setOnPickColorListener(new ColorPickerPopUp.OnPickColorListener() {
@Override
public void onColorPicked(int color) {
int clr = color & 0xFFFFFF;
updateConfigField(def, eIndex, String.format("#%06X", clr));
recyclerView.getAdapter().notifyItemChanged(boundIndex);
}
@Override
public void onCancel() {}
})
.show();
return;
}
AtomicReference<AlertDialog> ref = new AtomicReference<>();
AlertDialog.Builder builder = new BeamAlertDialogBuilder(getContext())
.setTitle(Slic3rLocalization.getString(def.getFullLabel()));
if (def.type == ConfigOptionDef.ConfigOptionType.ENUM) {
String[] labels;
String[] values;
if (Objects.equals("host_type", def.key)) {
labels = new String[]{"OctoPrint"};
values = new String[]{"octoprint"};
} else {
labels = new String[def.enumLabels.length];
values = def.enumValues;
for (int i = 0; i < def.enumLabels.length; i++) {
labels[i] = Slic3rLocalization.getString(def.enumLabels[i]);
}
}
builder.setSingleChoiceItems(labels, Arrays.asList(values).indexOf(opt(def, eIndex)), (dialog, which) -> {
updateConfigField(def, eIndex, values[which]);
// TODO: Update only value
recyclerView.getAdapter().notifyItemChanged(boundIndex);
dialog.dismiss();
});
} else {
String msg = Slic3rLocalization.getString(def.tooltip);
Context ctx = getContext();
LinearLayout ll = new LinearLayout(ctx);
ll.setOrientation(LinearLayout.VERTICAL);
if (!TextUtils.isEmpty(msg)) {
ScrollView scrollView = new ScrollView(ctx);
TextView subtitle = new TextView(ctx);
subtitle.setTextAppearance(ctx, com.google.android.material.R.style.MaterialAlertDialog_Material3_Body_Text);
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
subtitle.setText(msg);
subtitle.setPadding(ViewUtils.dp(24), ViewUtils.dp(12), ViewUtils.dp(24), ViewUtils.dp(12));
scrollView.addView(subtitle, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
ll.addView(scrollView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
}
EditText text = new EditText(ctx);
text.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
if (def.type == ConfigOptionDef.ConfigOptionType.FLOAT) {
text.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
if (def.min < 0) {
text.setInputType(text.getInputType() | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
} else if (def.type == ConfigOptionDef.ConfigOptionType.INT) {
text.setInputType(InputType.TYPE_CLASS_NUMBER);
if (def.min < 0) {
text.setInputType(text.getInputType() | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
} else {
text.setInputType(InputType.TYPE_CLASS_TEXT);
}
if (def.multiline) {
text.setInputType(text.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
text.setMaxLines(def.height);
}
text.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
private boolean validateFloat(String msg) {
if (msg.isEmpty()) return false;
try {
float v = Float.parseFloat(msg);
return v >= def.min && v <= def.max;
} catch (NumberFormatException e) {
return false;
}
}
@Override
public void afterTextChanged(Editable s) {
String msg = s.toString();
boolean valid;
if (def.type == ConfigOptionDef.ConfigOptionType.FLOAT_OR_PERCENT) {
valid = msg.endsWith("%") ? validateFloat(msg.substring(0, msg.length() - 1).trim()) : validateFloat(msg.trim());
} else if (def.type == ConfigOptionDef.ConfigOptionType.PERCENT) {
valid = msg.endsWith("%") && validateFloat(msg.substring(0, msg.length() - 1).trim());
} else if (def.type == ConfigOptionDef.ConfigOptionType.FLOAT || def.type == ConfigOptionDef.ConfigOptionType.INT) {
valid = validateFloat(msg.trim());
} else if (def.type == ConfigOptionDef.ConfigOptionType.FLOATS || def.type == ConfigOptionDef.ConfigOptionType.INTS) {
String[] vals = msg.split(",");
valid = true;
for (String val : vals) {
if (!validateFloat(val.trim())) {
valid = false;
break;
}
}
} else {
valid = true;
}
text.getBackground().setTintList(ColorStateList.valueOf(ThemesRepo.getColor(valid ? android.R.attr.textColorPrimary : R.attr.textColorNegative)));
if (ref.get() != null) {
View btn = ref.get().getButton(AlertDialog.BUTTON_POSITIVE);
btn.setAlpha(valid ? 1f : 0.6f);
btn.setClickable(valid);
}
}
});
text.setText(opt(def, eIndex));
ll.addView(text, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
leftMargin = rightMargin = ViewUtils.dp(21);
}});
builder.setView(ll).setPositiveButton(android.R.string.ok, (dialog, which) -> {
String str = text.getText().toString();
updateConfigField(def, eIndex, str);
// TODO: Update only value
recyclerView.getAdapter().notifyItemChanged(boundIndex);
}).setNegativeButton(android.R.string.cancel, null);
ViewUtils.postOnMainThread(() -> {
text.requestFocus();
InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(text, 0);
text.setSelection(text.getText().length());
}, 500);
}
ref.set(builder.show());
});
if (def.type == ConfigOptionDef.ConfigOptionType.STRING || def.type == ConfigOptionDef.ConfigOptionType.STRINGS) {
((PreferenceItem) simpleItem).setSubtitleProvider(() -> opt(def, eIndex).trim());
if (def.key.endsWith("_gcode")) {
((PreferenceItem) simpleItem).setTitle(null);
}
} else {
((PreferenceItem) simpleItem).setValueProvider(() -> def.type == ConfigOptionDef.ConfigOptionType.ENUM ? Slic3rLocalization.getString(def.enumLabels[Arrays.asList(def.enumValues).indexOf(opt(def, eIndex))]) : opt(def, eIndex));
}
}
switch (def.type) {
case BOOL:
case BOOLS:
simpleItem = new PreferenceSwitchItem().setTitle(Slic3rLocalization.getString(def.label))
.setValueProvider(() -> "1".equals(opt(def, eIndex)))
.setChangeListener((buttonView, isChecked) -> updateConfigField(def, eIndex, String.valueOf(isChecked ? 1 : 0)));
break;
}
}
public OptionElement(int icon, String title) {
this.icon = icon;
this.title = title;
}
public OptionElement(SimpleRecyclerItem item) {
simpleItem = item;
}
public OptionElement setOnClick(Runnable onClick) {
this.onClick = onClick;
return this;
}
public OptionElement setColor(int color, boolean noTint) {
this.color = color;
this.noTint = noTint;
return this;
}
}
public final static class SubHeader extends SimpleRecyclerItem<SubHeader.SubHeaderHolderView> {
public final String title;
public SubHeader(String title) {
this.title = title;
}
@Override
public SubHeaderHolderView onCreateView(Context ctx) {
return new SubHeaderHolderView(ctx);
}
@Override
public void onBindView(SubHeaderHolderView view) {
view.bind(this);
}
private final static class SubHeaderHolderView extends LinearLayout {
TextView title;
SubHeaderHolderView(Context context) {
super(context);
setOrientation(VERTICAL);
addView(new DividerView(context, R.attr.dividerContrastColor), new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
title = new TextView(context);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
title.setPadding(ViewUtils.dp(20), ViewUtils.dp(12), ViewUtils.dp(20), 0);
addView(title);
}
void bind(SubHeader h) {
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
title.setText(Slic3rLocalization.getString(h.title));
}
}
}
public final static class SubHint extends SimpleRecyclerItem<SubHint.SubHintHolderView> {
public final PreferenceItem.ValueProvider provider;
public SubHint(PreferenceItem.ValueProvider title) {
this.provider = title;
}
@Override
public SubHintHolderView onCreateView(Context ctx) {
return new SubHintHolderView(ctx);
}
@Override
public void onBindView(SubHintHolderView view) {
view.bind(this);
}
private final static class SubHintHolderView extends LinearLayout {
TextView title;
SubHintHolderView(Context context) {
super(context);
setOrientation(VERTICAL);
addView(new DividerView(context, R.attr.dividerContrastColor), new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
title = new TextView(context);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
title.setPadding(ViewUtils.dp(20), ViewUtils.dp(6), ViewUtils.dp(20), ViewUtils.dp(12));
addView(title);
}
void bind(SubHint h) {
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
title.setText(h.provider.provide());
}
}
}
private final static class OptionWrapper extends SimpleRecyclerItem<View> {
int icon;
String title;
Runnable onClick;
int color;
OptionElement optionEl;
boolean noTint;
int categoryIndex;
OptionWrapper(int icon, String t, Runnable onClick, int color, boolean noTint) {
this.icon = icon;
this.title = t;
this.onClick = onClick;
this.color = color;
this.noTint = noTint;
}
OptionWrapper(OptionElement el) {
optionEl = el;
}
@Override
public View onCreateView(Context ctx) {
FrameLayout v = new FrameLayout(ctx);
v.setBackgroundColor(Color.GREEN);
v.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(32)) {{
bottomMargin = ViewUtils.dp(16);
}});
return v;
}
}
private final static class CategoryHolderView extends LinearLayout implements IThemeView {
private ImageView icon;
private TextView title;
private ImageView dropdown;
public CategoryHolderView(Context context) {
super(context);
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
setPadding(ViewUtils.dp(21), ViewUtils.dp(16), ViewUtils.dp(21), ViewUtils.dp(16));
icon = new ImageView(context);
addView(icon, new LayoutParams(ViewUtils.dp(26), ViewUtils.dp(26)));
title = new TextView(context);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
addView(title, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
leftMargin = ViewUtils.dp(12);
}});
dropdown = new ImageView(context);
dropdown.setImageResource(R.drawable.dropdown_24);
addView(dropdown, new LayoutParams(ViewUtils.dp(24), ViewUtils.dp(24)));
onApplyTheme();
}
@Override
public void onApplyTheme() {
int color = getTag() != null ? ThemesRepo.getColor((Integer) getTag()) : 0;
if (icon.getTag() != null) {
icon.setImageTintList(null);
} else {
icon.setImageTintList(ColorStateList.valueOf(color != 0 ? color : ThemesRepo.getColor(android.R.attr.textColorSecondary)));
}
title.setTextColor(color != 0 ? color : ThemesRepo.getColor(android.R.attr.textColorPrimary));
dropdown.setImageTintList(ColorStateList.valueOf(color != 0 ? color : ThemesRepo.getColor(android.R.attr.textColorPrimary)));
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 32));
}
}
}
@@ -0,0 +1,209 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.BeamServerData;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SetupActivity;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.BeamColorPickerPopUp;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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(
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(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(3);
})
.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(Intent.ACTION_VIEW, Uri.parse("https://boosty.to/ytkab0bp")));
}) : 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) -> {
SliceBeam.getConfigFile().delete();
SliceBeam.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());
}
@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 ru.ytkab0bp.slicebeam.navigation;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
import ru.ytkab0bp.slicebeam.fragment.FilamentConfigFragment;
import ru.ytkab0bp.slicebeam.fragment.PrintConfigFragment;
import ru.ytkab0bp.slicebeam.fragment.PrinterConfigFragment;
import ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.navigation;
import android.content.Context;
import android.view.View;
import androidx.annotation.CallSuper;
import ru.ytkab0bp.slicebeam.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,119 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.TextColorImageSpan;
import ru.ytkab0bp.slicebeam.view.ThemeBottomNavigationView;
import ru.ytkab0bp.slicebeam.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 FrameLayout getOverlayView() {
return root;
}
@Override
public boolean isSwitchingWithX() {
return portrait;
}
}
@@ -0,0 +1,270 @@
package ru.ytkab0bp.slicebeam.navigation;
import static ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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,39 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.recycler;
import android.content.Context;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.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,196 @@
package ru.ytkab0bp.slicebeam.recycler;
import android.content.Context;
import android.content.res.ColorStateList;
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.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.PreferenceHolderView> {
private Drawable mIcon;
private CharSequence mTitle;
private ValueProvider mSubtitle;
private View.OnClickListener onClickListener;
private int textColorRes;
private boolean noTint;
private ValueProvider valueProvider;
public PreferenceItem setTitle(CharSequence title) {
mTitle = title;
return this;
}
public PreferenceItem setSubtitle(CharSequence subtitle) {
mSubtitle = ()->subtitle;
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(SliceBeam.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 setTextColorRes(int textColorRes) {
this.textColorRes = textColorRes;
return this;
}
public PreferenceItem setOnClickListener(View.OnClickListener onClickListener) {
this.onClickListener = onClickListener;
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;
public PreferenceHolderView(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));
}});
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));
int pad = ViewUtils.dp(12);
setPadding(pad, pad, pad, pad);
setMinimumHeight(ViewUtils.dp(56));
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
onApplyTheme();
}
void bind(PreferenceItem item) {
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);
}
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)));
}
}
@Override
public void onApplyTheme() {
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
value.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 {
CharSequence provide();
}
}
@@ -0,0 +1,171 @@
package ru.ytkab0bp.slicebeam.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.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.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 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 setTitle(CharSequence title) {
mTitle = title;
return this;
}
public PreferenceSwitchItem setSubtitle(CharSequence subtitle) {
mSubtitle = subtitle;
return this;
}
public PreferenceSwitchItem setIcon(int iconRes) {
mIcon = ContextCompat.getDrawable(SliceBeam.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)) {{
setMarginEnd(ViewUtils.dp(16));
gravity = Gravity.CENTER_VERTICAL;
}});
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);
}
});
}
@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.colorAccent)));
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
}
}
public interface ValueProvider {
boolean provide();
}
}
@@ -0,0 +1,62 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.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,94 @@
package ru.ytkab0bp.slicebeam.render;
import androidx.core.math.MathUtils;
import ru.ytkab0bp.slicebeam.utils.DoubleMatrix;
import ru.ytkab0bp.slicebeam.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, 5f);
}
public void setZoom(float zoom) {
this.zoom = zoom;
}
public void move(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);
Vec3d move = new Vec3d(screenX).add(screenY);
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 ru.ytkab0bp.slicebeam.render;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.slic3r.GLModel;
import ru.ytkab0bp.slicebeam.slic3r.GLShaderProgram;
import ru.ytkab0bp.slicebeam.slic3r.GLShadersManager;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rUtils;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.DoubleMatrix;
import ru.ytkab0bp.slicebeam.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(double[] viewMatrix, double[] projectionMatrix, float emissionFactor, float invZoom) {
if (!arrow.isInitialized()) {
arrow.stilizedArrow(tipRadius, tipLength, stemRadius, stemLength);
}
GLShaderProgram currentShader = GLShadersManager.getCurrentShader();
GLShaderProgram shader = GLShadersManager.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,503 @@
package ru.ytkab0bp.slicebeam.render;
import static android.opengl.GLES30.*;
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
import android.opengl.GLSurfaceView;
import android.util.Log;
import androidx.core.graphics.ColorUtils;
import java.util.ArrayList;
import java.util.List;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
import ru.ytkab0bp.slicebeam.events.SelectedObjectChangedEvent;
import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
import ru.ytkab0bp.slicebeam.slic3r.GCodeProcessorResult;
import ru.ytkab0bp.slicebeam.slic3r.GCodeViewer;
import ru.ytkab0bp.slicebeam.slic3r.GLModel;
import ru.ytkab0bp.slicebeam.slic3r.GLShaderProgram;
import ru.ytkab0bp.slicebeam.slic3r.GLShadersManager;
import ru.ytkab0bp.slicebeam.slic3r.Model;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rUtils;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.DoubleMatrix;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.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 int lastConfigUid;
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<>();
public Camera getCamera() {
return camera;
}
public Bed3D getBed() {
return bed;
}
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);
}
}
@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 = GLShadersManager.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 != SliceBeam.CONFIG_UID) {
configureBed();
}
if (bed.isValid()) {
bed.render(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 = GLShadersManager.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 dash = GLShadersManager.get(GLShadersManager.SHADER_FLAT);
glLineWidth(ViewUtils.dp(1.5f));
dash.startUsing();
dash.setUniformMatrix4fv("view_model_matrix", outModelMatrix);
dash.setUniformMatrix4fv("projection_matrix", projectionMatrix);
if (selectionModel == null) {
selectionModel = new GLModel();
}
selectionModel.initBoundingBox(model, i);
selectionModel.setColor(hoverColor);
selectionModel.render();
dash.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;
SliceBeam.EVENT_BUS.fireEvent(new SelectedObjectChangedEvent());
}
if (model.getObjectsCount() == 0) {
model.release();
model = null;
}
SliceBeam.EVENT_BUS.fireEvent(new ObjectsListChangedEvent());
return true;
}
public boolean onClick(float x, float y) {
if (model == null || isViewerEnabled) return false;
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;
}
}
}
boolean render = j != selectedObject || j != -1;
selectedObject = j == selectedObject ? -1 : j;
if (render) {
if (selectedObject == -1) {
selX = selY = selZ = 0;
selRotX = selRotY = selRotZ = 0;
selScaleX = selScaleY = selScaleZ = 1;
}
SliceBeam.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;
}
}
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 = SliceBeam.CONFIG_UID;
SliceBeam.genCurrentConfig();
bed.configure(SliceBeam.getCurrentConfigFile());
} catch (Exception e) {
Log.e("GLRenderer", "Failed to update config", e);
}
}
private void onCreate() {
bed = new Bed3D();
configureBed();
backgroundModel = new GLModel();
backgroundModel.initBackgroundTriangles();
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() {
GLShadersManager.clearShaders();
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();
}
}
@@ -0,0 +1,166 @@
package ru.ytkab0bp.slicebeam.slic3r;
import static android.opengl.GLES30.*;
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
import java.io.File;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.render.CoordAxes;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.DoubleMatrix;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
import ru.ytkab0bp.slicebeam.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);
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 void render(boolean bottom, double[] viewModelMatrix, double[] projectionMatrix, float invZoom) {
assertTrue(viewModelMatrix.length == 16);
assertTrue(projectionMatrix.length == 16);
DoubleMatrix.setIdentityM(modelMatrix, 0);
if (!likelyDelta) {
DoubleMatrix.translateM(modelMatrix, 0, -getVolumeMin().x * 2, -getVolumeMin().y * 2, -getVolumeMin().z);
}
DoubleMatrix.multiplyMM(outModelMatrix, 0, viewModelMatrix, 0, modelMatrix, 0);
renderDefaultBed(bottom, outModelMatrix, projectionMatrix);
axes.render(viewModelMatrix, projectionMatrix, 0.25f, invZoom);
}
private void renderDefaultBed(boolean bottom, double[] viewModelMatrix, double[] projectionMatrix) {
GLShaderProgram shader = GLShadersManager.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(boolean bottom, float[] viewModelMatrix, float[] projectionMatrix) {
GLShaderProgram shader = GLShadersManager.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() {
Native.bed_release(pointer);
axes.release();
// triangles.release();
// gridlines.release();
// contourlines.release();
}
}
@@ -0,0 +1,163 @@
package ru.ytkab0bp.slicebeam.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,23 @@
package ru.ytkab0bp.slicebeam.slic3r;
import java.io.File;
public class GCodeProcessorResult {
final long pointer;
public GCodeProcessorResult(File f) {
pointer = Native.gcoderesult_load_file(f.getAbsolutePath(), f.getName());
}
GCodeProcessorResult(long ptr) {
pointer = ptr;
}
public String getRecommendedName() {
return Native.gcoderesult_get_recommended_name(pointer);
}
public void release() {
Native.gcoderesult_release(pointer);
}
}
@@ -0,0 +1,103 @@
package ru.ytkab0bp.slicebeam.slic3r;
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
import android.graphics.Color;
import androidx.core.util.Pair;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
public class GCodeViewer {
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 final 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 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() {
Native.vgcode_release(pointer);
}
}
@@ -0,0 +1,119 @@
package ru.ytkab0bp.slicebeam.slic3r;
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
import android.graphics.Color;
import java.util.ArrayList;
import java.util.List;
import ru.ytkab0bp.slicebeam.render.Camera;
import ru.ytkab0bp.slicebeam.render.GLRenderer;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
public class GLModel {
private 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() {
Native.glmodel_release(pointer);
}
public MeshRaycaster getRaycaster() {
if (raycaster == null) {
Native.glmodel_init_raycast_data(pointer);
raycaster = new MeshRaycaster();
}
return raycaster;
}
public final class MeshRaycaster {
public List<HitResult> 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])
));
}
return list;
}
}
public static class HitResult {
public final Vec3d position, normal;
public HitResult(Vec3d position, Vec3d normal) {
this.position = position;
this.normal = normal;
}
}
}
@@ -0,0 +1,145 @@
package ru.ytkab0bp.slicebeam.slic3r;
import static ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.utils.IOUtils;
public class GLShaderProgram {
final 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 = SliceBeam.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() {
Native.shader_release(pointer);
}
}
@@ -0,0 +1,94 @@
package ru.ytkab0bp.slicebeam.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.HashMap;
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 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 static void clearShaders() {
for (GLShaderProgram program : shaders.values()) {
program.release();
}
shaders.clear();
}
public static GLShaderProgram get(@ShaderType String key) {
return shaders.get(key);
}
@Keep
private static long getCurrentShaderPointer() {
GLShaderProgram prog = getCurrentShader();
return prog != null ? prog.pointer : 0;
}
public static 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,147 @@
package ru.ytkab0bp.slicebeam.slic3r;
import java.io.File;
import java.util.UUID;
import ru.ytkab0bp.slicebeam.utils.Vec3d;
public class Model {
public final String key = UUID.randomUUID().toString();
final 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 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 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 GCodeProcessorResult slice(String configPath, String gcodePath, SliceListener listener) throws Slic3rRuntimeError {
return new GCodeProcessorResult(Native.model_slice(pointer, configPath, gcodePath, listener));
}
public void release() {
Native.model_release(pointer);
}
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,97 @@
package ru.ytkab0bp.slicebeam.slic3r;
import ru.ytkab0bp.slicebeam.SliceBeam;
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(SliceBeam.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 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_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 long model_slice(long ptr, String configPath, String path, SliceListener listener) 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 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 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 ru.ytkab0bp.slicebeam.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,91 @@
package ru.ytkab0bp.slicebeam.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"
);
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,410 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.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", "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", "thumbnails_format",
"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"
);
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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.SliceBeam;
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 = SliceBeam.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 ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.slic3r;
import static ru.ytkab0bp.slicebeam.utils.DebugUtils.assertTrue;
import android.text.TextUtils;
import ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.slic3r;
public interface SliceListener {
void onProgress(int progress, String text);
}
@@ -0,0 +1,79 @@
package ru.ytkab0bp.slicebeam.theme;
import android.util.SparseIntArray;
import androidx.annotation.StringRes;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.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, 0xfffce2d4);
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.gcodeViewerSkirt, 0x7FFF7F);
colors.put(R.attr.gcodeViewerExternalPerimeter, 0xFFFF00);
colors.put(R.attr.gcodeViewerSupportMaterial, 0x7FFF7F);
colors.put(R.attr.gcodeViewerSupportMaterialInterface, 0x7FFF7F);
colors.put(R.attr.gcodeViewerInternalInfill, 0xFF7F7F);
colors.put(R.attr.gcodeViewerSolidInfill, 0xFF7F7F);
colors.put(R.attr.gcodeViewerWipeTower, 0xF7FF7F);
colors.put(R.attr.xTrackColor, 0xffbf0000);
colors.put(R.attr.yTrackColor, 0xff00bf00);
colors.put(R.attr.zTrackColor, 0xff0000bf);
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(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 ru.ytkab0bp.slicebeam.theme;
public interface IThemeView {
void onApplyTheme();
}
@@ -0,0 +1,59 @@
package ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.MainActivity;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.utils.Prefs;
public class ThemesRepo {
private static Boolean resolvedSystemMode;
public static BeamTheme getCurrent() {
if (Prefs.getThemeMode() == Prefs.ThemeMode.SYSTEM) {
if (resolvedSystemMode == null) {
resolvedSystemMode = (SliceBeam.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 ru.ytkab0bp.slicebeam.utils;
import ru.ytkab0bp.slicebeam.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 ru.ytkab0bp.slicebeam.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,157 @@
package ru.ytkab0bp.slicebeam.utils;
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.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
public class IOUtils {
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 {
ConfigObject cfg = new ConfigObject();
URLConnection con = new URL(String.format("https://raw.githubusercontent.com/SoftFever/OrcaSlicer/main/resources/profiles/%s/%s/%s.json", vendor, type, profile)).openConnection();
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;
vendor = inherit.substring(i, inherit.indexOf(' ', i));
} 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()) {
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");
} if (en.getKey().equals("start_filament_gcode") || en.getKey().equals("end_filament_gcode") ||
en.getKey().equals("start_gcode") || en.getKey().equals("end_gcode")) {
cfg.values.put(en.getKey(), en.getValue().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 (!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")) {
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,127 @@
package ru.ytkab0bp.slicebeam.utils;
import android.app.Application;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SetupActivity;
public class Prefs {
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 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 boolean isRotationEnabled() {
return mPrefs.getBoolean("rotation_enabled", true);
}
public static void setRotationEnabled(boolean e) {
mPrefs.edit().putBoolean("rotation_enabled", e).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 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,17 @@
package ru.ytkab0bp.slicebeam.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];
}
}
@@ -0,0 +1,118 @@
package ru.ytkab0bp.slicebeam.utils;
public class Vec3d {
public double x, y, z;
public Vec3d() {}
public Vec3d(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public Vec3d(Vec3d from) {
this.x = from.x;
this.y = from.y;
this.z = from.z;
}
public Vec3d center(Vec3d with) {
return clone().add(with.clone().add(clone().negate()).multiply(0.5));
}
public Vec3d set(Vec3d vec) {
this.x = vec.x;
this.y = vec.y;
this.z = vec.z;
return this;
}
public Vec3d set(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
public Vec3d add(Vec3d vec) {
this.x += vec.x;
this.y += vec.y;
this.z += vec.z;
return this;
}
public Vec3d add(double x, double y, double z) {
this.x += x;
this.y += y;
this.z += z;
return this;
}
public Vec3d multiply(Vec3d vec) {
this.x *= vec.x;
this.y *= vec.y;
this.z *= vec.z;
return this;
}
public Vec3d multiply(double x, double y, double z) {
this.x *= x;
this.y *= y;
this.z *= z;
return this;
}
public Vec3d multiply(double m) {
this.x *= m;
this.y *= m;
this.z *= m;
return this;
}
public double magnitude() {
return Math.sqrt(x * x + y * y + z * z);
}
public double distance(Vec3d with) {
double dx = x - with.x, dy = with.y - y, dz = with.z - z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
public Vec3d normalize() {
double d = magnitude();
if (d > 0) multiply(1 / d);
return this;
}
public Vec3d negate() {
return multiply(-1);
}
public Vec3d divide(Vec3d vec) {
this.x /= vec.x;
this.y /= vec.y;
this.z /= vec.z;
return this;
}
public Vec3d crossProduct(Vec3d with) {
return new Vec3d(
y * with.z - z * with.y,
z * with.x - x * with.z,
x * with.y - y * with.x
);
}
public Vec3d clone() {
return new Vec3d(this);
}
public float[] asArray() {
return new float[]{(float) x, (float) y, (float) z, 1};
}
public double[] asDoubleArray() {
return new double[]{x, y, z, 1};
}
}
@@ -0,0 +1,17 @@
package ru.ytkab0bp.slicebeam.utils;
import android.content.Context;
import android.os.Build;
import android.os.Vibrator;
public class VibrationUtils {
private static Vibrator vibrator;
public static void init(Context ctx) {
vibrator = (Vibrator) ctx.getSystemService(Context.VIBRATOR_SERVICE);
}
public static boolean hasAmplitudeControl() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && vibrator.hasAmplitudeControl();
}
}
@@ -0,0 +1,78 @@
package ru.ytkab0bp.slicebeam.utils;
import android.animation.TimeInterpolator;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Handler;
import android.os.Looper;
import android.util.TypedValue;
import android.view.animation.PathInterpolator;
import java.util.HashMap;
import java.util.Map;
import ru.ytkab0bp.slicebeam.SliceBeam;
public class ViewUtils {
public final static TimeInterpolator CUBIC_INTERPOLATOR = new PathInterpolator(0.25f, 0, 0.25f, 1f);
public final static String ROBOTO_MEDIUM = "roboto_medium";
private static Handler uiHandler = new Handler(Looper.getMainLooper());
private static Map<String, Typeface> typefaceCache = new HashMap<>();
public static void postOnMainThread(Runnable runnable) {
uiHandler.post(runnable);
}
public static void postOnMainThread(Runnable runnable, long delay) {
uiHandler.postDelayed(runnable, delay);
}
public static void removeCallbacks(Runnable runnable) {
uiHandler.removeCallbacks(runnable);
}
public static Typeface getTypeface(String key) {
Typeface typeface = typefaceCache.get(key);
if (typeface == null) {
typefaceCache.put(key, typeface = Typeface.createFromAsset(SliceBeam.INSTANCE.getAssets(), "font/" + key + ".ttf"));
}
return typeface;
}
public static float lerp(float a, float b, float progress) {
return a + (b - a) * progress;
}
public static double lerpd(double a, double b, double c, float progress) {
return lerpd(lerpd(a, b, Math.min(progress, 0.5f) / 0.5f), c, (Math.max(progress, 0.5f) - 0.5f) / 0.5f);
}
public static double lerpd(double a, double b, float progress) {
return a + (b - a) * progress;
}
public static RippleDrawable createRipple(int color, float radiusDp) {
return createRipple(color, 0, radiusDp);
}
public static RippleDrawable createRipple(int color, int fillColor, float radiusDp) {
if (radiusDp == -1) {
return new RippleDrawable(ColorStateList.valueOf(color), null, null);
}
GradientDrawable mask = new GradientDrawable();
mask.setColor(Color.BLACK);
mask.setCornerRadius(dp(radiusDp));
return new RippleDrawable(ColorStateList.valueOf(color), fillColor != 0 ? new GradientDrawable() {{
setColor(fillColor);
setCornerRadius(dp(radiusDp));
}} : null, mask);
}
public static int dp(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, SliceBeam.INSTANCE.getResources().getDisplayMetrics());
}
}
@@ -0,0 +1,43 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.util.TypedValue;
import android.view.Gravity;
import androidx.appcompat.widget.AppCompatTextView;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class BeamButton extends AppCompatTextView implements IThemeView {
private int colorRes = android.R.attr.colorAccent;
private int color;
public BeamButton(Context context) {
super(context);
setGravity(Gravity.CENTER);
setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
setPadding(ViewUtils.dp(21), 0, ViewUtils.dp(21), 0);
onApplyTheme();
}
public void setColor(int color) {
this.color = color;
this.colorRes = 0;
onApplyTheme();
}
public void setColorRes(int colorRes) {
this.colorRes = colorRes;
onApplyTheme();
}
@Override
public void onApplyTheme() {
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), colorRes != 0 ? ThemesRepo.getColor(colorRes) : color, 16));
setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
}
}
@@ -0,0 +1,42 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.content.res.ColorStateList;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import com.google.android.material.materialswitch.MaterialSwitch;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
public class BeamSwitch extends MaterialSwitch implements IThemeView {
public BeamSwitch(@NonNull Context context) {
super(context);
onApplyTheme();
}
@Override
public void onApplyTheme() {
setTrackTintList(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.switchThumbUncheckedColor)
}));
}
@Override
public boolean isLaidOut() {
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return false;
}
}
@@ -0,0 +1,108 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.View;
import androidx.core.math.MathUtils;
import java.util.ArrayList;
import java.util.List;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class BoostySubsView extends View {
private TextPaint paint = new TextPaint();
private List<String> strings = new ArrayList<>();
private SparseArray<CharSequence> ellipsizedStrings = new SparseArray<>();
private int index;
private float progress;
private long lastUpdated;
private int firstHeight;
private Rect rect = new Rect();
public BoostySubsView(Context context) {
super(context);
paint.setTextSize(ViewUtils.dp(20));
paint.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
updateColors();
}
public void setStrings(List<String> strings) {
this.strings = strings;
ellipsizedStrings.clear();
index = 0;
progress = 0;
if (!strings.isEmpty()) {
String str = strings.get(index);
paint.getTextBounds(str, 0, str.length(), rect);
firstHeight = rect.height();
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
long dt = Math.min(16, System.currentTimeMillis() - lastUpdated);
lastUpdated = System.currentTimeMillis();
if (!strings.isEmpty()) {
float tY = (ViewUtils.dp(24) + firstHeight) * progress;
canvas.save();
canvas.translate(0, -tY);
float halfHeight = getHeight() / 2f;
int y = 0;
int i = index;
while (y <= getHeight() + tY) {
int j = i;
while (j < 0) j += strings.size();
while (j >= strings.size()) j -= strings.size();
CharSequence str = ellipsizedStrings.get(j);
if (str == null) {
ellipsizedStrings.set(j, str = TextUtils.ellipsize(strings.get(j), paint, getWidth() - getPaddingLeft() - getPaddingRight(), TextUtils.TruncateAt.END));
}
paint.getTextBounds(str.toString(), 0, str.length(), rect);
float highlight = (1f - Math.abs((y - tY - firstHeight / 2f - halfHeight) / halfHeight));
highlight = MathUtils.clamp(highlight, 0, 1);
paint.setAlpha((int) (0xFF * highlight));
float x = (getWidth() - rect.width()) / 2f;
canvas.drawText(str, 0, str.length(), x, y, paint);
y += rect.height() + ViewUtils.dp(24);
i++;
}
canvas.restore();
progress += dt / 2000f;
if (progress > 1) {
progress -= 1f;
index++;
index %= strings.size();
String str = strings.get(index);
paint.getTextBounds(str, 0, str.length(), rect);
firstHeight = rect.height();
}
invalidate();
}
}
public void updateColors() {
paint.setColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
invalidate();
}
}
@@ -0,0 +1,41 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.View;
import androidx.annotation.NonNull;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
public class DividerView extends View implements IThemeView {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int colorRes;
public DividerView(Context context) {
this(context, R.attr.dividerColor);
}
public DividerView(Context context, int colorRes) {
super(context);
this.colorRes = colorRes;
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
onApplyTheme();
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
canvas.drawPaint(paint);
}
@Override
public void onApplyTheme() {
paint.setColor(ThemesRepo.getColor(colorRes));
}
}
@@ -0,0 +1,92 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.math.MathUtils;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class FadeRecyclerView extends RecyclerView implements IThemeView {
private final static int HEIGHT_DP = 32;
private Paint topPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint bottomPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float topProgress, bottomProgress;
private float overlayAlpha = 1f;
public FadeRecyclerView(@NonNull Context context) {
super(context);
LinearLayoutManager llm = new LinearLayoutManager(context);
setLayoutManager(llm);
setWillNotDraw(false);
addOnScrollListener(new OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
topProgress = 1f;
if (llm.findFirstVisibleItemPosition() == 0) {
View ch = llm.getChildAt(0);
int size = Math.min(ch.getHeight(), ViewUtils.dp(HEIGHT_DP) / 2);
topProgress = MathUtils.clamp(-ch.getTop() / (float) size, 0, 1);
}
bottomProgress = 1f;
if (llm.findLastVisibleItemPosition() == recyclerView.getAdapter().getItemCount() - 1) {
View ch = llm.getChildAt(llm.getChildCount() - 1);
int size = Math.min(ch.getHeight(), ViewUtils.dp(HEIGHT_DP) / 2);
bottomProgress = MathUtils.clamp((ch.getBottom() - getHeight()) / (float) size, 0, 1);
}
invalidate();
}
});
onApplyTheme();
}
public void setOverlayAlpha(float overlayAlpha) {
this.overlayAlpha = overlayAlpha;
invalidate();
}
@Override
public void draw(Canvas c) {
super.draw(c);
if (topProgress > 0) {
topPaint.setAlpha((int) (topProgress * overlayAlpha * 0xFF));
c.drawRect(0, 0, getWidth(), ViewUtils.dp(HEIGHT_DP), topPaint);
}
if (bottomProgress > 0) {
bottomPaint.setAlpha((int) (bottomProgress * overlayAlpha * 0xFF));
c.drawRect(0, getHeight() - ViewUtils.dp(HEIGHT_DP), getWidth(), getHeight(), bottomPaint);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
invalidateShaders();
}
private void invalidateShaders() {
if (getWidth() == 0 || getHeight() == 0) return;
topPaint.setShader(new LinearGradient(getWidth() / 2f, 0, getWidth() / 2f, ViewUtils.dp(HEIGHT_DP), ThemesRepo.getColor(android.R.attr.windowBackground), 0, Shader.TileMode.CLAMP));
bottomPaint.setShader(new LinearGradient(getWidth() / 2f, getHeight() - ViewUtils.dp(HEIGHT_DP), getWidth() / 2f, getHeight(), 0, ThemesRepo.getColor(android.R.attr.windowBackground), Shader.TileMode.CLAMP));
invalidate();
}
@Override
public void onApplyTheme() {
invalidateShaders();
}
}
@@ -0,0 +1,411 @@
package ru.ytkab0bp.slicebeam.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Region;
import android.graphics.Typeface;
import android.opengl.GLES30;
import android.opengl.GLException;
import android.opengl.GLSurfaceView;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.ViewConfiguration;
import java.nio.IntBuffer;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.render.GLRenderer;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class GLView extends GLSurfaceView implements IThemeView {
private GLRenderer renderer;
private float lastX, lastY;
private float lastLength;
private int touchSlop;
private boolean fromTwoPointers;
private boolean isRotating;
private boolean isMoving;
private boolean isScaling;
private long lastActionTime = System.currentTimeMillis();
private Path path = new Path();
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint xferPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private TextPaint invalidBedText = new TextPaint(Paint.ANTI_ALIAS_FLAG);
private StaticLayout invalidBedDescriptionLayout;
private float lastScale;
private long lastDraw;
private float invalidOffset;
public GLView(Context context) {
super(context);
xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
setEGLContextClientVersion(3);
renderer = new GLRenderer(this);
setRenderer(renderer);
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
setWillNotDraw(false);
}
public void arrange() {
if (renderer.getModel() == null) return;
queueEvent(() -> {
renderer.getBed().arrange(renderer.getModel());
renderer.resetGlModels();
requestRender();
});
}
public GLRenderer getRenderer() {
return renderer;
}
public void drawOverlay(Canvas canvas, boolean toBitmap) {
long dt = Math.min(System.currentTimeMillis() - lastDraw, 16);
lastDraw = System.currentTimeMillis();
int rad = ViewUtils.dp(16);
float offsetX = getTranslationX(), offsetY = -getTranslationY();
if (toBitmap) {
paint.setColor(ThemesRepo.getColor(android.R.attr.windowBackground));
canvas.drawRect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY, paint);
canvas.drawRoundRect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY, rad, rad, xferPaint);
} else {
path.rewind();
path.addRoundRect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY, rad, rad, Path.Direction.CW);
canvas.save();
canvas.clipPath(path, Region.Op.DIFFERENCE);
canvas.drawColor(ThemesRepo.getColor(android.R.attr.windowBackground));
canvas.restore();
if (getRenderer().getBed() != null && !getRenderer().getBed().isValid()) {
invalidOffset += dt / 10000f;
paint.setColor(ThemesRepo.getColor(android.R.attr.windowBackground));
int size = ViewUtils.dp(200);
canvas.drawRect(0, (getHeight() - size) / 2f, getWidth(), (getHeight() + size) / 2f, paint);
double angle = Math.toRadians(60);
int stableWidth = ViewUtils.dp(16);
int lineHeight = ViewUtils.dp(16);
int lineWidth = ViewUtils.dp(16 + (float) (32 * Math.sin(angle)));
Path linePath = new Path();
linePath.moveTo(0, 0);
linePath.lineTo(stableWidth, 0);
linePath.lineTo(lineWidth, lineHeight);
linePath.lineTo(lineWidth - stableWidth, lineHeight);
linePath.lineTo(0, 0);
linePath.close();
paint.setColor(ThemesRepo.getColor(android.R.attr.colorAccent));
int x = (int) (-lineWidth - invalidOffset * getWidth());
while (x < getWidth()) {
canvas.save();
canvas.translate(x, (getHeight() - size) / 2f);
canvas.drawPath(linePath, paint);
canvas.translate(stableWidth, 0);
int alpha = paint.getAlpha();
paint.setAlpha((int) (alpha * 0.5f));
canvas.drawPath(linePath, paint);
canvas.restore();
paint.setAlpha(alpha);
x += stableWidth * 2;
}
x = (int) (getWidth() + lineWidth + invalidOffset * getWidth());
while (x >= -lineWidth) {
canvas.save();
canvas.translate(x, (getHeight() + size) / 2f - lineHeight);
canvas.drawPath(linePath, paint);
canvas.translate(-stableWidth, 0);
int alpha = paint.getAlpha();
paint.setAlpha((int) (alpha * 0.5f));
canvas.drawPath(linePath, paint);
canvas.restore();
paint.setAlpha(alpha);
x -= stableWidth * 2;
}
if (invalidBedDescriptionLayout == null) {
invalidBedText.setTextSize(ViewUtils.dp(16));
invalidBedText.setColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
invalidBedText.setTypeface(Typeface.DEFAULT);
invalidBedDescriptionLayout = new StaticLayout(getContext().getString(R.string.BedConfigurationErrorDesc), invalidBedText, getWidth() - ViewUtils.dp(32), Layout.Alignment.ALIGN_CENTER, 1, 0, false);
}
int realTextSize = ViewUtils.dp(22);
int padding = ViewUtils.dp(12);
int totalHeight = realTextSize + invalidBedDescriptionLayout.getHeight();
invalidBedText.setTextSize(ViewUtils.dp(18));
invalidBedText.setColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
invalidBedText.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
String errString = getContext().getString(R.string.BedConfigurationError);
canvas.drawText(errString, 0, errString.length(), (getWidth() - invalidBedText.measureText(errString)) / 2f, getHeight() / 2f - totalHeight / 2f + realTextSize - padding / 2f, invalidBedText);
invalidBedText.setTextSize(ViewUtils.dp(16));
invalidBedText.setColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
invalidBedText.setTypeface(Typeface.DEFAULT);
canvas.save();
canvas.translate((getWidth() - invalidBedDescriptionLayout.getWidth()) / 2f, getHeight() / 2f + totalHeight / 2f - invalidBedDescriptionLayout.getHeight() + padding / 2f);
invalidBedDescriptionLayout.draw(canvas);
canvas.restore();
invalidate();
}
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
drawOverlay(canvas, false);
}
public Bitmap snapshotBitmap() {
int w = getWidth(), h = getHeight();
int[] bitmapBuffer = new int[w * h];
int[] bitmapSource = new int[w * h];
IntBuffer intBuffer = IntBuffer.wrap(bitmapBuffer);
intBuffer.position(0);
try {
GLES30.glReadPixels(0, 0, w, h, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, intBuffer);
int offset1, offset2;
for (int i = 0; i < h; i++) {
offset1 = i * w;
offset2 = (h - i - 1) * w;
for (int j = 0; j < w; j++) {
int texturePixel = bitmapBuffer[offset1 + j];
int blue = (texturePixel >> 16) & 0xff;
int red = (texturePixel << 16) & 0x00ff0000;
int pixel = (texturePixel & 0xff00ff00) | red | blue;
bitmapSource[offset2 + j] = pixel;
}
}
} catch (GLException e) {
throw new RuntimeException(e);
}
return Bitmap.createBitmap(bitmapSource, w, h, Bitmap.Config.ARGB_8888);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
private void calcStartFocus(MotionEvent e) {
lastX = (e.getX(0) + e.getX(1)) / 2f;
lastY = (e.getY(0) + e.getY(1)) / 2f;
float x = e.getX(0) - e.getX(1), y = e.getY(0) - e.getY(1);
lastLength = (float) Math.sqrt(x * x + y * y);
}
@Override
public boolean onHoverEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT ? renderer.stopHover() : renderer.hover(event.getX() * Prefs.getRenderScale(), event.getY() * Prefs.getRenderScale())) {
queueEvent(this::requestRender);
}
return super.onHoverEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent e) {
long deltaMs = System.currentTimeMillis() - lastActionTime;
lastActionTime = System.currentTimeMillis();
int action = e.getActionMasked();
if (e.getPointerCount() > 2) {
return true;
}
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
if (e.getPointerCount() == 2) {
calcStartFocus(e);
fromTwoPointers = true;
} else {
lastX = e.getX();
lastY = e.getY();
}
return true;
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_CANCEL) {
if (fromTwoPointers) {
if (e.getPointerCount() == 1) {
fromTwoPointers = false;
isScaling = false;
isMoving = false;
lastActionTime = 0;
}
return true;
}
if (e.getPointerCount() == 1) {
if (!isRotating && action != MotionEvent.ACTION_CANCEL) {
if (renderer.onClick(e.getX() * Prefs.getRenderScale(), e.getY() * Prefs.getRenderScale())) {
requestRender();
}
}
lastX = e.getX(0);
lastY = e.getY(0);
isRotating = false;
}
// TODO: Rotate with inertia
return true;
}
if (action == MotionEvent.ACTION_MOVE) {
if (e.getPointerCount() == 2) {
float x = (e.getX(0) + e.getX(1)) / 2f;
float y = (e.getY(0) + e.getY(1)) / 2f;
float lenX = e.getX(0) - e.getX(1), lenY = e.getY(0) - e.getY(1);
float len = (float) Math.sqrt(lenX * lenX + lenY * lenY);
float distanceX = lastX - x, distanceY = lastY - y;
if (deltaMs > 128) {
isScaling = false;
isMoving = false;
}
boolean startingGesture = false;
if (!isScaling && !isMoving) {
if (Math.abs(distanceX) < touchSlop && Math.abs(distanceY) < touchSlop && Math.abs(len - lastLength) > touchSlop * 1.5f) {
isScaling = true;
startingGesture = true;
} else if (Math.sqrt(distanceX * distanceX + distanceY * distanceY) >= touchSlop) {
isMoving = true;
startingGesture = true;
}
}
if (isScaling) {
float delta = len - lastLength;
lastLength = len;
if (!startingGesture) {
renderer.getCamera().zoom(delta / touchSlop * Prefs.getCameraSensitivity());
renderer.updateProjection();
requestRender();
}
lastX = x;
lastY = y;
} else if (isMoving) {
if (!startingGesture) {
renderer.getCamera().move(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity());
requestRender();
}
lastX = x;
lastY = y;
}
} else if (!fromTwoPointers) {
float distanceX = lastX - e.getX(), distanceY = lastY - e.getY();
boolean startingGesture = false;
if (!isRotating) {
if (Math.sqrt(distanceX * distanceX + distanceY * distanceY) >= touchSlop) {
isRotating = true;
startingGesture = true;
}
}
if (isRotating) {
if (!startingGesture) {
if (Prefs.isRotationEnabled()) {
renderer.getCamera().rotateAround(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity());
} else {
renderer.getCamera().move(distanceX / touchSlop * Prefs.getCameraSensitivity(), distanceY / touchSlop * Prefs.getCameraSensitivity());
}
requestRender();
}
lastX = e.getX();
lastY = e.getY();
}
}
}
return true;
}
private void applyScale() {
int w = getWidth(), h = getHeight();
float realScale = Math.round(w * (lastScale = Prefs.getRenderScale())) / (float) w;
getHolder().setFixedSize((int) (w * realScale), (int) (h * realScale));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != 0 && h != 0) {
applyScale();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (getWidth() > 0 && getHeight() > 0 && lastScale != Prefs.getRenderScale()) {
applyScale();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(holder);
requestRender();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
super.surfaceChanged(holder, format, w, h);
requestRender();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
renderer.onDestroy();
}
@Override
public void onApplyTheme() {
requestRender();
}
}
@@ -0,0 +1,60 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class MiniColorView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint outlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint outlinePaintSecondary = new Paint(Paint.ANTI_ALIAS_FLAG);
private float selectionProgress;
public MiniColorView(Context context) {
super(context);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setStrokeWidth(ViewUtils.dp(2f));
outlinePaintSecondary.setStyle(Paint.Style.STROKE);
outlinePaintSecondary.setStrokeWidth(ViewUtils.dp(2f));
outlinePaintSecondary.setColor(ThemesRepo.getColor(R.attr.dividerColor));
}
public void setSelectionProgress(float selectionProgress) {
this.selectionProgress = selectionProgress;
invalidate();
}
public void setColor(int color) {
paint.setColor(color);
outlinePaint.setColor(color);
outlinePaintSecondary.setColor(ColorUtils.calculateLuminance(color) > 0.9f ? ThemesRepo.getColor(R.attr.dividerColor) : Color.TRANSPARENT);
selectionProgress = Prefs.getAccentColor() == color ? 1f : 0f;
invalidate();
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
int size = Math.min(getWidth(), getHeight());
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, size / 2f - outlinePaintSecondary.getStrokeWidth() / 2f, outlinePaintSecondary);
float w = (outlinePaint.getStrokeWidth() + outlinePaintSecondary.getStrokeWidth()) / 2f;
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, size / 2f - w, outlinePaint);
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, size / 2f - ViewUtils.lerp(0, w * 3f, selectionProgress), paint);
}
}
@@ -0,0 +1,29 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.graphics.Canvas;
import android.view.View;
import androidx.annotation.NonNull;
public class MirrorView extends View {
private View mirrored;
public MirrorView(Context context) {
super(context);
}
public void setMirroredView(View mirrored) {
this.mirrored = mirrored;
invalidate();
}
public View getMirroredView() {
return mirrored;
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
mirrored.draw(canvas);
}
}
@@ -0,0 +1,440 @@
package ru.ytkab0bp.slicebeam.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.text.TextPaint;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import androidx.core.math.MathUtils;
import androidx.core.util.Consumer;
import androidx.core.view.ViewCompat;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.VibrationUtils;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class PositionScrollView extends View implements IThemeView {
private final static int STEP = 1;
private final static int MARK_STEP = 1;
private final static int TOP_MARGIN_DP = ViewUtils.dp(6);
private final static int BOTTOM_MARGIN_DP = ViewUtils.dp(14);
private final static int STEP_DP = ViewUtils.dp(10);
private final static int GRADIENT_DP = ViewUtils.dp(32);
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
private int currentPosition = 0;
private float progress;
private Consumer<Integer> listener;
private Consumer<Integer> progressListener;
private GradientDrawable leftDrawable = new GradientDrawable(), rightDrawable = new GradientDrawable();
private GestureDetector gestureDetector;
private int lastX;
private Scroller gestureScroller;
private SpringAnimation stepAnimation;
private float wasProgress;
private int activeColor = android.R.attr.colorAccent;
private int inactiveColor = android.R.attr.textColorSecondary;
private boolean notClick;
private int min = Integer.MIN_VALUE, max = Integer.MAX_VALUE;
private List<PositionScrollView> synchronizedScrolls = new ArrayList<>();
private Map<PositionScrollView, Integer> syncDeltas = new HashMap<>();
public PositionScrollView(Context context) {
super(context);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(ViewUtils.dp(2.5f));
textPaint.setTextSize(ViewUtils.dp(14));
gestureScroller = new Scroller(context);
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
private boolean gotOffset;
@Override
public boolean onDown(@NonNull MotionEvent e) {
if (stepAnimation != null) {
stepAnimation.cancel();
}
gotOffset = false;
if (!gestureScroller.isFinished()) {
notClick = true;
}
gestureScroller.forceFinished(true);
getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
@Override
public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
if (stepAnimation != null) {
stepAnimation.cancel();
}
if (!gotOffset) {
gotOffset = true;
} else {
scrollDx(distanceX);
}
return true;
}
@Override
public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
gestureScroller.fling(lastX = 0, 0, (int) (-velocityX * 2f), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
ViewCompat.postInvalidateOnAnimation(PositionScrollView.this);
return true;
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
return !notClick && callOnClick();
}
});
onApplyTheme();
}
public void addSynced(PositionScrollView scroll) {
syncDeltas.put(scroll, scroll.getCurrentPosition() - currentPosition);
synchronizedScrolls.add(scroll);
}
public void removeSynced(PositionScrollView scroll) {
syncDeltas.remove(scroll);
synchronizedScrolls.remove(scroll);
}
public void updateSyncDeltas() {
for (PositionScrollView scroll : synchronizedScrolls) {
syncDeltas.put(scroll, scroll.getCurrentPosition() - currentPosition);
}
}
public void setActiveColor(int activeColor) {
this.activeColor = activeColor;
invalidate();
}
public void setInactiveColor(int inactiveColor) {
this.inactiveColor = inactiveColor;
invalidate();
}
public void setCurrentPosition(int currentPosition) {
setCurrentPosition(currentPosition, false);
}
private void onSetProgress(float progress) {
this.progress = progress;
for (PositionScrollView s : synchronizedScrolls) {
if (currentPosition + syncDeltas.get(s) <= s.min && syncDeltas.get(s) < 0) {
s.progress = 0;
} else {
s.progress = progress;
}
s.invalidate();
}
}
private void onSetCurrentPosition(int currentPosition) {
this.currentPosition = currentPosition;
for (PositionScrollView s : synchronizedScrolls) {
s.currentPosition = Math.max(currentPosition + syncDeltas.get(s), s.min);
s.invalidate();
}
}
public void setCurrentPosition(int currentPosition, boolean notify) {
onSetCurrentPosition(currentPosition);
onSetProgress(0);
if (notify && listener != null) {
listener.accept(currentPosition);
}
invalidate();
}
public void stopScroll() {
gestureScroller.forceFinished(true);
computeScroll();
}
private boolean scrollDx(float dx) {
if (currentPosition == min && dx < 0 || currentPosition == max && dx > 0) {
invalidate();
return false;
}
float pr = -dx / ViewUtils.dp(STEP_DP);
onSetProgress(progress + pr);
if (progress > 0f && currentPosition == min || progress < 0f && currentPosition == max) {
onSetProgress(0f);
}
while (progress >= 1f) {
onSetProgress(progress - 1);
if (currentPosition > min) {
onSetCurrentPosition(currentPosition - STEP);
notifyNewCurrent();
}
}
while (progress <= -1f) {
onSetProgress(progress + 1);
if (currentPosition < max) {
onSetCurrentPosition(currentPosition + STEP);
notifyNewCurrent();
}
}
invalidate();
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (gestureScroller.computeScrollOffset()) {
int dx = gestureScroller.getCurrX() - lastX;
lastX = gestureScroller.getCurrX();
if (!scrollDx(dx)) {
gestureScroller.forceFinished(true);
lastX = 0;
if (stepAnimation != null) {
stepAnimation.cancel();
}
wasProgress = 0;
stepAnimation = new SpringAnimation(new FloatValueHolder(progress))
.setMinimumVisibleChange(1 / 500f)
.setSpring(new SpringForce(0f)
.setStiffness(400f)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
.setStartVelocity(MathUtils.clamp(-(gestureScroller.getCurrVelocity() / 1500) / ViewUtils.dp(STEP_DP), -6f, 6f))
.addUpdateListener((animation, value, velocity) -> {
onSetProgress(value);
invalidate();
})
.addEndListener((animation, canceled, value, velocity) -> {
if (progress >= 1f) {
onSetProgress(progress - 1);
onSetCurrentPosition(currentPosition - STEP);
notifyNewCurrent();
} else if (progress <= -1f) {
onSetProgress(progress + 1);
onSetCurrentPosition(currentPosition + STEP);
notifyNewCurrent();
}
})
.addEndListener((animation, canceled, value, velocity) -> {
stepAnimation = null;
if (listener != null) {
listener.accept(currentPosition);
}
});
stepAnimation.start();
}
} else if (lastX != 0) {
onScrollFinished();
}
}
private void onScrollFinished() {
lastX = 0;
notClick = false;
if (stepAnimation != null) {
stepAnimation.cancel();
}
stepAnimation = new SpringAnimation(new FloatValueHolder(wasProgress = progress))
.setMinimumVisibleChange(1 / 500f)
.setSpring(new SpringForce(progress > 0.75f ? 1f : progress < -0.75f ? -1f : 0f)
.setStiffness(400f)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
.addUpdateListener((animation, value, velocity) -> {
onSetProgress(value);
invalidate();
})
.addEndListener((animation, canceled, value, velocity) -> {
if (progress >= 1f) {
onSetProgress(progress - 1);
onSetCurrentPosition(currentPosition - STEP);
notifyNewCurrent();
} else if (progress <= -1f) {
onSetProgress(progress + 1);
onSetCurrentPosition(currentPosition + STEP);
notifyNewCurrent();
}
})
.addEndListener((animation, canceled, value, velocity) -> {
stepAnimation = null;
if (listener != null) {
listener.accept(currentPosition);
}
});
stepAnimation.start();
}
public int getCurrentPosition() {
return currentPosition;
}
public void notifyNewCurrent() {
if (progressListener != null) {
progressListener.accept(currentPosition);
}
for (PositionScrollView s : synchronizedScrolls) {
if (s.progressListener != null) {
s.progressListener.accept(Math.max(currentPosition + syncDeltas.get(s), s.min));
}
}
vibrateHaptic();
invalidate();
}
private void vibrateHaptic() {
if (Prefs.isVibrationEnabled() && VibrationUtils.hasAmplitudeControl() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
}
}
public void setProgressListener(Consumer<Integer> progressListener) {
this.progressListener = progressListener;
}
public void setListener(Consumer<Integer> listener) {
this.listener = listener;
}
public void setMinMax(int min, int max) {
this.min = min;
this.max = max;
invalidate();
}
private int resolveInactiveColor() {
int clr = ThemesRepo.getColor(inactiveColor);
return ColorUtils.setAlphaComponent(clr, (int) (Color.alpha(clr) * 0.6f));
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
float progressClamped = stepAnimation != null && wasProgress == 0f ? 0f : MathUtils.clamp(progress, -1f, 1f);
float transformProgress = Math.abs(progressClamped);
paint.setColor(ColorUtils.blendARGB(ThemesRepo.getColor(activeColor), resolveInactiveColor(), transformProgress));
float cX = getWidth() / 2f + progress * ViewUtils.dp(STEP_DP);
canvas.drawLine(cX, ViewUtils.lerp(paint.getStrokeWidth(), ViewUtils.dp(TOP_MARGIN_DP), transformProgress), cX, getHeight() - ViewUtils.dp(BOTTOM_MARGIN_DP), paint);
drawTextIfNeeded(canvas, cX, currentPosition, transformProgress);
float x = cX;
int i = currentPosition;
while (x > getPaddingLeft() && i > min) {
x -= ViewUtils.dp(STEP_DP);
i -= STEP;
float pr = i == currentPosition - STEP ? Math.max(0, progressClamped) : 0;
float top = ViewUtils.lerp(ViewUtils.dp(TOP_MARGIN_DP), 0, pr);
paint.setColor(ColorUtils.blendARGB(resolveInactiveColor(), ThemesRepo.getColor(activeColor), pr));
canvas.drawLine(x, top, x, getHeight() - ViewUtils.dp(BOTTOM_MARGIN_DP), paint);
drawTextIfNeeded(canvas, x, i, 1f - pr);
}
x = cX;
i = currentPosition;
while (x < getWidth() - getPaddingRight() && i < max) {
x += ViewUtils.dp(STEP_DP);
i += STEP;
float pr = i == currentPosition + STEP ? -Math.min(0, progressClamped) : 0;
float top = ViewUtils.lerp(ViewUtils.dp(TOP_MARGIN_DP), 0, pr);
paint.setColor(ColorUtils.blendARGB(resolveInactiveColor(), ThemesRepo.getColor(activeColor), pr));
canvas.drawLine(x, top, x, getHeight() - ViewUtils.dp(BOTTOM_MARGIN_DP), paint);
drawTextIfNeeded(canvas, x, i, 1f - pr);
}
paint.setColor(ThemesRepo.getColor(android.R.attr.windowBackground));
canvas.drawRect(0, 0, getPaddingLeft(), getHeight(), paint);
canvas.drawRect(getWidth() - getPaddingRight(), 0, getWidth(), getHeight(), paint);
leftDrawable.setBounds(getPaddingLeft(), 0, ViewUtils.dp(GRADIENT_DP), getHeight());
leftDrawable.draw(canvas);
rightDrawable.setBounds(getWidth() - ViewUtils.dp(GRADIENT_DP), 0, getWidth() - getPaddingRight(), getHeight());
rightDrawable.draw(canvas);
}
private void drawTextIfNeeded(Canvas canvas, float x, int i, float alpha) {
if (i % MARK_STEP == 0 || i == min) {
String str = String.valueOf(i);
float width = textPaint.measureText(str);
int wasAlpha = textPaint.getAlpha();
textPaint.setAlpha((int) (alpha * wasAlpha));
canvas.save();
float sc = 0.85f + alpha * 0.15f;
canvas.scale(sc, sc, x - width / 2f, getHeight() - ViewUtils.dp(BOTTOM_MARGIN_DP));
canvas.drawText(str, x - width / 2f, getHeight() - ViewUtils.dp(BOTTOM_MARGIN_DP) / 2f, textPaint);
canvas.restore();
textPaint.setAlpha(wasAlpha);
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean detector = gestureDetector.onTouchEvent(event);
if ((event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) && gestureScroller.isFinished()) {
onScrollFinished();
}
return detector;
}
@Override
public void onApplyTheme() {
textPaint.setColor(resolveInactiveColor());
leftDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
leftDrawable.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
leftDrawable.setColors(new int[] {ThemesRepo.getColor(android.R.attr.windowBackground), Color.TRANSPARENT});
rightDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
rightDrawable.setOrientation(GradientDrawable.Orientation.RIGHT_LEFT);
rightDrawable.setColors(new int[] {ThemesRepo.getColor(android.R.attr.windowBackground), Color.TRANSPARENT});
}
}
@@ -0,0 +1,109 @@
package ru.ytkab0bp.slicebeam.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class ProfileDropdownView extends View implements IThemeView {
private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
private Drawable dropdown;
private String title;
private CharSequence titleEllipsized;
private StaticLayout layout;
private float progress;
public ProfileDropdownView(Context context) {
super(context);
textPaint.setTextSize(ViewUtils.dp(16));
textPaint.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
dropdown = ContextCompat.getDrawable(context, R.drawable.dropdown_24);
setWillNotDraw(false);
onApplyTheme();
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
titleEllipsized = null;
layout = null;
requestLayout();
invalidate();
}
public void setProgress(float progress) {
this.progress = progress;
invalidateColor();
invalidate();
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (title != null) {
titleEllipsized = TextUtils.ellipsize(title, textPaint, MeasureSpec.getSize(widthMeasureSpec) - ViewUtils.dp(21) * 2 - ViewUtils.dp(24), TextUtils.TruncateAt.END);
layout = new StaticLayout(titleEllipsized, textPaint, Math.round(textPaint.measureText(titleEllipsized, 0, titleEllipsized.length())), Layout.Alignment.ALIGN_NORMAL, 0f, 0f, false);
} else {
titleEllipsized = null;
layout = null;
}
}
@Override
public void draw(@NonNull Canvas canvas) {
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), ViewUtils.dp(16), ViewUtils.dp(16), bgPaint);
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), ViewUtils.dp(16), ViewUtils.dp(16), paint);
if (titleEllipsized != null) {
canvas.save();
canvas.translate(ViewUtils.dp(21), (getHeight() - layout.getHeight()) / 2f);
layout.draw(canvas);
canvas.restore();
dropdown.setBounds(0, 0, ViewUtils.dp(24), ViewUtils.dp(24));
canvas.save();
canvas.translate(getWidth() - dropdown.getBounds().width() - ViewUtils.dp(12), (getHeight() - dropdown.getBounds().height()) / 2f);
dropdown.setAlpha((int) ((1f - progress) * 0xFF));
dropdown.draw(canvas);
canvas.restore();
}
super.draw(canvas);
}
private void invalidateColor() {
textPaint.setColor(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorPrimary), 0, progress));
dropdown.setTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorPrimary)));
}
@Override
public void onApplyTheme() {
bgPaint.setColor(ThemesRepo.getColor(android.R.attr.windowBackground));
paint.setColor(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10));
invalidateColor();
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
invalidate();
}
}
@@ -0,0 +1,26 @@
package ru.ytkab0bp.slicebeam.view;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.style.ImageSpan;
import androidx.annotation.NonNull;
public class TextColorImageSpan extends ImageSpan {
private float offsetY;
public TextColorImageSpan(@NonNull Drawable drawable, float offset) {
super(drawable, ALIGN_BASELINE);
this.offsetY = offset;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
getDrawable().setTint(paint.getColor());
canvas.save();
canvas.translate(0, offsetY);
super.draw(canvas, text, start, end, x, top, y, bottom, paint);
canvas.restore();
}
}
@@ -0,0 +1,47 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.content.res.ColorStateList;
import androidx.annotation.NonNull;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
public class ThemeBottomNavigationView extends BottomNavigationView implements IThemeView {
public ThemeBottomNavigationView(@NonNull Context context) {
super(context);
setItemTextAppearanceInactive(R.style.Theme_SliceBeam_NavigationTextFix);
setItemTextAppearanceActive(R.style.Theme_SliceBeam_NavigationTextFixActive);
onApplyTheme();
}
@Override
public void onApplyTheme() {
setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
setItemActiveIndicatorColor(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorAccent)));
setItemTextColor(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(android.R.attr.textColorSecondary)
}));
setItemIconTintList(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(R.attr.textColorOnAccent),
ThemesRepo.getColor(android.R.attr.textColorSecondary)
}));
setItemRippleColor(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorControlHighlight)));
}
@Override
public int getMaxItemCount() {
return 6;
}
}
@@ -0,0 +1,42 @@
package ru.ytkab0bp.slicebeam.view;
import android.content.Context;
import android.content.res.ColorStateList;
import android.view.Gravity;
import androidx.annotation.NonNull;
import com.google.android.material.navigationrail.NavigationRailView;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
public class ThemeRailNavigationView extends NavigationRailView implements IThemeView {
public ThemeRailNavigationView(@NonNull Context context) {
super(context);
setMenuGravity(Gravity.CENTER);
onApplyTheme();
}
@Override
public void onApplyTheme() {
setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
setItemActiveIndicatorColor(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorAccent)));
setItemTextColor(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(android.R.attr.textColorSecondary)
}));
setItemIconTintList(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(R.attr.textColorOnAccent),
ThemesRepo.getColor(android.R.attr.textColorSecondary)
}));
setItemRippleColor(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorControlHighlight)));
}
}