fix(back-end): shift model
This commit is contained in:
@@ -18,10 +18,13 @@ import java.util.HashMap;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class QuoteController {
|
public class QuoteController {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(QuoteController.class.getName());
|
||||||
|
|
||||||
private final SlicerService slicerService;
|
private final SlicerService slicerService;
|
||||||
private final StlService stlService;
|
private final StlService stlService;
|
||||||
private final QuoteCalculator quoteCalculator;
|
private final QuoteCalculator quoteCalculator;
|
||||||
@@ -113,6 +116,7 @@ public class QuoteController {
|
|||||||
|
|
||||||
// Save uploaded file temporarily
|
// Save uploaded file temporarily
|
||||||
Path tempInput = Files.createTempFile("upload_", "_" + file.getOriginalFilename());
|
Path tempInput = Files.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||||
|
com.printcalculator.model.StlShiftResult shift = null;
|
||||||
try {
|
try {
|
||||||
file.transferTo(tempInput.toFile());
|
file.transferTo(tempInput.toFile());
|
||||||
|
|
||||||
@@ -123,9 +127,23 @@ public class QuoteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate model size against machine volume
|
// Validate model size against machine volume
|
||||||
validateModelSize(tempInput.toFile(), machine);
|
StlBounds bounds = validateModelSize(tempInput.toFile(), machine);
|
||||||
|
|
||||||
PrintStats stats = slicerService.slice(tempInput.toFile(), slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
// Auto-center if needed
|
||||||
|
shift = stlService.shiftToFitIfNeeded(
|
||||||
|
tempInput.toFile(),
|
||||||
|
bounds,
|
||||||
|
machine.getBuildVolumeXMm(),
|
||||||
|
machine.getBuildVolumeYMm(),
|
||||||
|
machine.getBuildVolumeZMm()
|
||||||
|
);
|
||||||
|
java.io.File sliceInput = shift.shifted() ? shift.shiftedPath().toFile() : tempInput.toFile();
|
||||||
|
if (shift.shifted()) {
|
||||||
|
logger.info(String.format("Auto-centered STL by offset (mm): x=%.3f y=%.3f z=%.3f",
|
||||||
|
shift.offsetX(), shift.offsetY(), shift.offsetZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintStats stats = slicerService.slice(sliceInput, slicerMachineProfile, filament, process, machineOverrides, processOverrides);
|
||||||
|
|
||||||
// Calculate Quote (Pass machine display name for pricing lookup)
|
// Calculate Quote (Pass machine display name for pricing lookup)
|
||||||
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filament);
|
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filament);
|
||||||
@@ -133,10 +151,15 @@ public class QuoteController {
|
|||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
} finally {
|
} finally {
|
||||||
Files.deleteIfExists(tempInput);
|
Files.deleteIfExists(tempInput);
|
||||||
|
if (shift != null && shift.shifted()) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(shift.shiftedPath());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateModelSize(java.io.File stlFile, PrinterMachine machine) throws IOException {
|
private StlBounds validateModelSize(java.io.File stlFile, PrinterMachine machine) throws IOException {
|
||||||
StlBounds bounds = stlService.readBounds(stlFile);
|
StlBounds bounds = stlService.readBounds(stlFile);
|
||||||
double x = bounds.sizeX();
|
double x = bounds.sizeX();
|
||||||
double y = bounds.sizeY();
|
double y = bounds.sizeY();
|
||||||
@@ -146,6 +169,13 @@ public class QuoteController {
|
|||||||
int by = machine.getBuildVolumeYMm();
|
int by = machine.getBuildVolumeYMm();
|
||||||
int bz = machine.getBuildVolumeZMm();
|
int bz = machine.getBuildVolumeZMm();
|
||||||
|
|
||||||
|
logger.info(String.format(
|
||||||
|
"STL bounds (mm): min(%.3f,%.3f,%.3f) max(%.3f,%.3f,%.3f) size(%.3f,%.3f,%.3f) bed(%d,%d,%d)",
|
||||||
|
bounds.minX(), bounds.minY(), bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY(), bounds.maxZ(),
|
||||||
|
x, y, z, bx, by, bz
|
||||||
|
));
|
||||||
|
|
||||||
double eps = 0.01;
|
double eps = 0.01;
|
||||||
boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps)
|
boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps)
|
||||||
|| (y <= bx + eps && x <= by + eps && z <= bz + eps);
|
|| (y <= bx + eps && x <= by + eps && z <= bz + eps);
|
||||||
@@ -153,5 +183,6 @@ public class QuoteController {
|
|||||||
if (!fits) {
|
if (!fits) {
|
||||||
throw new ModelTooLargeException(x, y, z, bx, by, bz);
|
throw new ModelTooLargeException(x, y, z, bx, by, bz);
|
||||||
}
|
}
|
||||||
|
return bounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,15 @@ import java.util.Map;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.UrlResource;
|
import org.springframework.core.io.UrlResource;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/quote-sessions")
|
@RequestMapping("/api/quote-sessions")
|
||||||
|
|
||||||
public class QuoteSessionController {
|
public class QuoteSessionController {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(QuoteSessionController.class.getName());
|
||||||
|
|
||||||
private final QuoteSessionRepository sessionRepo;
|
private final QuoteSessionRepository sessionRepo;
|
||||||
private final QuoteLineItemRepository lineItemRepo;
|
private final QuoteLineItemRepository lineItemRepo;
|
||||||
private final SlicerService slicerService;
|
private final SlicerService slicerService;
|
||||||
@@ -125,6 +128,7 @@ public class QuoteSessionController {
|
|||||||
// Resolve absolute path for slicing and storage usage
|
// Resolve absolute path for slicing and storage usage
|
||||||
Path persistentPath = storageService.loadAsResource(relativePath).getFile().toPath();
|
Path persistentPath = storageService.loadAsResource(relativePath).getFile().toPath();
|
||||||
|
|
||||||
|
com.printcalculator.model.StlShiftResult shift = null;
|
||||||
try {
|
try {
|
||||||
// Apply Basic/Advanced Logic
|
// Apply Basic/Advanced Logic
|
||||||
applyPrintSettings(settings);
|
applyPrintSettings(settings);
|
||||||
@@ -137,6 +141,20 @@ public class QuoteSessionController {
|
|||||||
// 2. Validate model size against machine volume
|
// 2. Validate model size against machine volume
|
||||||
StlBounds bounds = validateModelSize(persistentPath.toFile(), machine);
|
StlBounds bounds = validateModelSize(persistentPath.toFile(), machine);
|
||||||
|
|
||||||
|
// 2b. Auto-center if needed (keeps the stored STL unchanged)
|
||||||
|
shift = stlService.shiftToFitIfNeeded(
|
||||||
|
persistentPath.toFile(),
|
||||||
|
bounds,
|
||||||
|
machine.getBuildVolumeXMm(),
|
||||||
|
machine.getBuildVolumeYMm(),
|
||||||
|
machine.getBuildVolumeZMm()
|
||||||
|
);
|
||||||
|
java.io.File sliceInput = shift.shifted() ? shift.shiftedPath().toFile() : persistentPath.toFile();
|
||||||
|
if (shift.shifted()) {
|
||||||
|
logger.info(String.format("Auto-centered STL by offset (mm): x=%.3f y=%.3f z=%.3f",
|
||||||
|
shift.offsetX(), shift.offsetY(), shift.offsetZ()));
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Pick Profiles
|
// 3. Pick Profiles
|
||||||
String machineProfile = machine.getSlicerMachineProfile();
|
String machineProfile = machine.getSlicerMachineProfile();
|
||||||
if (machineProfile == null || machineProfile.isBlank()) {
|
if (machineProfile == null || machineProfile.isBlank()) {
|
||||||
@@ -205,7 +223,7 @@ public class QuoteSessionController {
|
|||||||
|
|
||||||
// 4. Slice (Use persistent path)
|
// 4. Slice (Use persistent path)
|
||||||
PrintStats stats = slicerService.slice(
|
PrintStats stats = slicerService.slice(
|
||||||
persistentPath.toFile(),
|
sliceInput,
|
||||||
machineProfile,
|
machineProfile,
|
||||||
filamentProfile,
|
filamentProfile,
|
||||||
processProfile,
|
processProfile,
|
||||||
@@ -252,6 +270,12 @@ public class QuoteSessionController {
|
|||||||
storageService.delete(Paths.get("quotes", session.getId().toString(), storedFilename));
|
storageService.delete(Paths.get("quotes", session.getId().toString(), storedFilename));
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (shift != null && shift.shifted()) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(shift.shiftedPath());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +289,13 @@ public class QuoteSessionController {
|
|||||||
int by = machine.getBuildVolumeYMm();
|
int by = machine.getBuildVolumeYMm();
|
||||||
int bz = machine.getBuildVolumeZMm();
|
int bz = machine.getBuildVolumeZMm();
|
||||||
|
|
||||||
|
logger.info(String.format(
|
||||||
|
"STL bounds (mm): min(%.3f,%.3f,%.3f) max(%.3f,%.3f,%.3f) size(%.3f,%.3f,%.3f) bed(%d,%d,%d)",
|
||||||
|
bounds.minX(), bounds.minY(), bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY(), bounds.maxZ(),
|
||||||
|
x, y, z, bx, by, bz
|
||||||
|
));
|
||||||
|
|
||||||
double eps = 0.01;
|
double eps = 0.01;
|
||||||
boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps)
|
boolean fits = (x <= bx + eps && y <= by + eps && z <= bz + eps)
|
||||||
|| (y <= bx + eps && x <= by + eps && z <= bz + eps);
|
|| (y <= bx + eps && x <= by + eps && z <= bz + eps);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.printcalculator.model;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public record StlShiftResult(Path shiftedPath,
|
||||||
|
double offsetX,
|
||||||
|
double offsetY,
|
||||||
|
double offsetZ,
|
||||||
|
boolean shifted) {
|
||||||
|
}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
package com.printcalculator.service;
|
package com.printcalculator.service;
|
||||||
|
|
||||||
import com.printcalculator.model.StlBounds;
|
import com.printcalculator.model.StlBounds;
|
||||||
|
import com.printcalculator.model.StlShiftResult;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class StlService {
|
public class StlService {
|
||||||
@@ -21,6 +27,30 @@ public class StlService {
|
|||||||
return readAsciiBounds(stlFile);
|
return readAsciiBounds(stlFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StlShiftResult shiftToFitIfNeeded(File stlFile, StlBounds bounds,
|
||||||
|
int bedX, int bedY, int bedZ) throws IOException {
|
||||||
|
double sizeX = bounds.sizeX();
|
||||||
|
double sizeY = bounds.sizeY();
|
||||||
|
double sizeZ = bounds.sizeZ();
|
||||||
|
|
||||||
|
double targetMinX = (bedX - sizeX) / 2.0;
|
||||||
|
double targetMinY = (bedY - sizeY) / 2.0;
|
||||||
|
double targetMinZ = 0.0;
|
||||||
|
|
||||||
|
double offsetX = targetMinX - bounds.minX();
|
||||||
|
double offsetY = targetMinY - bounds.minY();
|
||||||
|
double offsetZ = targetMinZ - bounds.minZ();
|
||||||
|
|
||||||
|
boolean needsShift = Math.abs(offsetX) > 1e-6 || Math.abs(offsetY) > 1e-6 || Math.abs(offsetZ) > 1e-6;
|
||||||
|
if (!needsShift) {
|
||||||
|
return new StlShiftResult(null, offsetX, offsetY, offsetZ, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path shiftedPath = Files.createTempFile("stl_shifted_", ".stl");
|
||||||
|
writeShifted(stlFile, shiftedPath.toFile(), offsetX, offsetY, offsetZ);
|
||||||
|
return new StlShiftResult(shiftedPath, offsetX, offsetY, offsetZ, true);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isBinaryStl(File stlFile, long size) throws IOException {
|
private boolean isBinaryStl(File stlFile, long size) throws IOException {
|
||||||
try (RandomAccessFile raf = new RandomAccessFile(stlFile, "r")) {
|
try (RandomAccessFile raf = new RandomAccessFile(stlFile, "r")) {
|
||||||
raf.seek(80);
|
raf.seek(80);
|
||||||
@@ -71,6 +101,82 @@ public class StlService {
|
|||||||
return acc.toBounds();
|
return acc.toBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void writeShifted(File input, File output, double offsetX, double offsetY, double offsetZ) throws IOException {
|
||||||
|
long size = input.length();
|
||||||
|
if (size >= 84 && isBinaryStl(input, size)) {
|
||||||
|
writeShiftedBinary(input, output, offsetX, offsetY, offsetZ);
|
||||||
|
} else {
|
||||||
|
writeShiftedAscii(input, output, offsetX, offsetY, offsetZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeShiftedAscii(File input, File output, double offsetX, double offsetY, double offsetZ) throws IOException {
|
||||||
|
try (BufferedReader reader = Files.newBufferedReader(input.toPath(), StandardCharsets.US_ASCII);
|
||||||
|
BufferedWriter writer = Files.newBufferedWriter(output.toPath(), StandardCharsets.US_ASCII)) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith("vertex")) {
|
||||||
|
writer.write(line);
|
||||||
|
writer.newLine();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] parts = trimmed.split("\\s+");
|
||||||
|
if (parts.length < 4) {
|
||||||
|
writer.write(line);
|
||||||
|
writer.newLine();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double x = Double.parseDouble(parts[1]) + offsetX;
|
||||||
|
double y = Double.parseDouble(parts[2]) + offsetY;
|
||||||
|
double z = Double.parseDouble(parts[3]) + offsetZ;
|
||||||
|
int idx = line.indexOf("vertex");
|
||||||
|
String indent = idx > 0 ? line.substring(0, idx) : "";
|
||||||
|
writer.write(indent + String.format(Locale.US, "vertex %.6f %.6f %.6f", x, y, z));
|
||||||
|
writer.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeShiftedBinary(File input, File output, double offsetX, double offsetY, double offsetZ) throws IOException {
|
||||||
|
try (RandomAccessFile raf = new RandomAccessFile(input, "r");
|
||||||
|
OutputStream out = new FileOutputStream(output)) {
|
||||||
|
byte[] header = new byte[80];
|
||||||
|
raf.readFully(header);
|
||||||
|
out.write(header);
|
||||||
|
|
||||||
|
long triangleCount = readLEUInt32(raf);
|
||||||
|
writeLEUInt32(out, triangleCount);
|
||||||
|
|
||||||
|
for (long i = 0; i < triangleCount; i++) {
|
||||||
|
// normal
|
||||||
|
writeLEFloat(out, readLEFloat(raf));
|
||||||
|
writeLEFloat(out, readLEFloat(raf));
|
||||||
|
writeLEFloat(out, readLEFloat(raf));
|
||||||
|
|
||||||
|
// vertices
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetX));
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetY));
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetZ));
|
||||||
|
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetX));
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetY));
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetZ));
|
||||||
|
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetX));
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetY));
|
||||||
|
writeLEFloat(out, (float) (readLEFloat(raf) + offsetZ));
|
||||||
|
|
||||||
|
// attribute byte count
|
||||||
|
int b1 = raf.read();
|
||||||
|
int b2 = raf.read();
|
||||||
|
if ((b1 | b2) < 0) throw new IOException("Unexpected EOF while reading STL");
|
||||||
|
out.write(b1);
|
||||||
|
out.write(b2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long readLEUInt32(RandomAccessFile raf) throws IOException {
|
private long readLEUInt32(RandomAccessFile raf) throws IOException {
|
||||||
int b1 = raf.read();
|
int b1 = raf.read();
|
||||||
int b2 = raf.read();
|
int b2 = raf.read();
|
||||||
@@ -99,6 +205,21 @@ public class StlService {
|
|||||||
return Float.intBitsToFloat(readLEInt(raf));
|
return Float.intBitsToFloat(readLEInt(raf));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void writeLEUInt32(OutputStream out, long value) throws IOException {
|
||||||
|
out.write((int) (value & 0xFF));
|
||||||
|
out.write((int) ((value >> 8) & 0xFF));
|
||||||
|
out.write((int) ((value >> 16) & 0xFF));
|
||||||
|
out.write((int) ((value >> 24) & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLEFloat(OutputStream out, float value) throws IOException {
|
||||||
|
int bits = Float.floatToIntBits(value);
|
||||||
|
out.write(bits & 0xFF);
|
||||||
|
out.write((bits >> 8) & 0xFF);
|
||||||
|
out.write((bits >> 16) & 0xFF);
|
||||||
|
out.write((bits >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
private static class BoundsAccumulator {
|
private static class BoundsAccumulator {
|
||||||
private boolean hasPoint = false;
|
private boolean hasPoint = false;
|
||||||
private double minX;
|
private double minX;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.printcalculator.model.PrintStats;
|
|||||||
import com.printcalculator.model.QuoteResult;
|
import com.printcalculator.model.QuoteResult;
|
||||||
import com.printcalculator.entity.PrinterMachine;
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
import com.printcalculator.model.StlBounds;
|
import com.printcalculator.model.StlBounds;
|
||||||
|
import com.printcalculator.model.StlShiftResult;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -33,6 +34,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -109,6 +111,8 @@ public class ManualSessionPersistenceTest {
|
|||||||
new QuoteResult(10.0, "CHF", new PrintStats(100, "1m", 10.0, 100), 0.0)
|
new QuoteResult(10.0, "CHF", new PrintStats(100, "1m", 10.0, 100), 0.0)
|
||||||
);
|
);
|
||||||
when(stlService.readBounds(any())).thenReturn(new StlBounds(0, 0, 0, 10, 10, 10));
|
when(stlService.readBounds(any())).thenReturn(new StlBounds(0, 0, 0, 10, 10, 10));
|
||||||
|
when(stlService.shiftToFitIfNeeded(any(), any(), anyInt(), anyInt(), anyInt()))
|
||||||
|
.thenReturn(new StlShiftResult(null, 0, 0, 0, false));
|
||||||
when(storageService.loadAsResource(any())).thenReturn(new org.springframework.core.io.ByteArrayResource("dummy".getBytes()){
|
when(storageService.loadAsResource(any())).thenReturn(new org.springframework.core.io.ByteArrayResource("dummy".getBytes()){
|
||||||
@Override
|
@Override
|
||||||
public File getFile() { return new File("dummy"); }
|
public File getFile() { return new File("dummy"); }
|
||||||
|
|||||||
Reference in New Issue
Block a user