feat(ui): add box selection feature

- Selection: add addAll() method for bulk shape addition
- Controller: box selection with mouse drag on empty space
- ShapeDraftman: drawSelectionBox() for rubber-band rendering
- ShapesView: currentSelectionBox field and setter
This commit is contained in:
2026-03-26 23:54:46 +01:00
parent 3a6f98455a
commit b0e3428696
4 changed files with 128 additions and 22 deletions

View File

@@ -9,6 +9,7 @@ import ovh.gasser.newshapes.util.Streamable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -38,6 +39,15 @@ public class Selection implements Streamable<Shape> {
notifyListeners();
}
public void addAll(Collection<Shape> shapes) {
if (shapes == null) {
return;
}
for (Shape shape : shapes) {
add(shape);
}
}
public boolean isEmpty() {
return selectedShapes.isEmpty();
}

View File

@@ -32,6 +32,9 @@ public class Controller {
private ResizeHandle activeHandle;
private Point resizeOrigin;
private boolean resizeMode;
private Point selectionBoxStart;
private Point selectionBoxEnd;
private boolean boxSelecting;
Controller(ShapesView view, SCollection model) {
this.view = view;
@@ -54,6 +57,11 @@ public class Controller {
resizing = false;
activeHandle = null;
resizeOrigin = null;
boxSelecting = false;
selectionBoxStart = null;
selectionBoxEnd = null;
view.setCurrentSelectionBox(null);
view.repaint();
}
};
this.view.addMouseMotionListener(adapter);
@@ -67,10 +75,24 @@ public class Controller {
}
private void handleMouseDragged(MouseEvent evt) {
if (boxSelecting) {
selectionBoxEnd = evt.getPoint();
Rectangle box = createSelectionBox(selectionBoxStart, selectionBoxEnd);
view.setCurrentSelectionBox(box);
updateSelectionFromBox(box);
view.repaint();
return;
}
boxSelecting = false;
if (resizeMode && resizing && activeHandle != null) {
logger.debug("RESIZING with handle {} at ({}, {})", activeHandle, evt.getX(), evt.getY());
int dx = evt.getX() - resizeOrigin.x;
int dy = evt.getY() - resizeOrigin.y;
logger.debug("About to resize {} shapes, dx={}, dy={}", selection.getSelectedShapes().size(), dx, dy);
for (Shape shape : selection) {
logger.debug("Calling resize on shape: {}", shape);
shape.resize(activeHandle, dx, dy);
}
resizeOrigin = evt.getPoint();
@@ -85,8 +107,12 @@ public class Controller {
} else {
resizing = false;
activeHandle = null;
if (lastMousePos == null) {
lastMousePos = evt.getPoint();
}
int dx = evt.getX() - lastMousePos.x;
int dy = evt.getY() - lastMousePos.y;
logger.debug("MOVING shapes with delta ({}, {})", dx, dy);
for (Shape shape : selection) {
shape.translate(dx, dy);
}
@@ -104,9 +130,11 @@ public class Controller {
resizing = false;
activeHandle = null;
resizeOrigin = null;
boxSelecting = false; // Clear box selection mode when clicking
if (resizeMode && !selection.isEmpty()) {
ResizeHandle handle = getHandleAt(evt.getPoint());
logger.debug("In resize mode, checking handle at ({}, {}): {}", evt.getX(), evt.getY(), handle);
if (handle != null) {
resizing = true;
activeHandle = handle;
@@ -122,18 +150,19 @@ public class Controller {
if (!evt.isShiftDown()) {
resetSelection();
}
lastMousePos = evt.getPoint();
selection.add(s);
logger.debug("Selecting {}", s);
if (!selection.getSelectedShapes().contains(s)) {
lastMousePos = evt.getPoint();
selection.add(s);
logger.debug("Selecting {}", s);
}
},
this::resetSelection
() -> {
// Clicked on empty space - start box selection
resetSelection();
selectionBoxStart = evt.getPoint();
boxSelecting = true;
}
);
view.repaint();
}
<<<<<<< HEAD
public void enterTextMode() {
addingText = true;
}
private void placeTextAt(Point point) {
@@ -153,20 +182,56 @@ public class Controller {
}
private ResizeHandle getHandleAt(Point point) {
final int handleSize = 5;
final int handleSize = 10;
for (Shape shape : selection) {
Rectangle bounds = shape.getBounds();
int cx = bounds.x + bounds.width / 2;
int cy = bounds.y + bounds.height / 2;
if (point.x < cx && point.y < cy) return ResizeHandle.NW;
if (point.x > cx && point.y < cy) return ResizeHandle.NE;
if (point.x < cx && point.y > cy) return ResizeHandle.SW;
if (point.x > cx && point.y > cy) return ResizeHandle.SE;
if (point.y < cy) return ResizeHandle.N;
if (point.y > cy) return ResizeHandle.S;
if (point.x < cx) return ResizeHandle.W;
if (point.x > cx) return ResizeHandle.E;
// Always-present SE corner handle
if (point.x >= bounds.x + bounds.width && point.x <= bounds.x + bounds.width + handleSize &&
point.y >= bounds.y + bounds.height && point.y <= bounds.y + bounds.height + handleSize) {
return ResizeHandle.SE;
}
// Resize handles are only active in resize mode
if (resizeMode) {
// NW corner
if (point.x >= bounds.x - handleSize && point.x <= bounds.x &&
point.y >= bounds.y - handleSize && point.y <= bounds.y) {
return ResizeHandle.NW;
}
// NE corner
if (point.x >= bounds.x + bounds.width && point.x <= bounds.x + bounds.width + handleSize &&
point.y >= bounds.y - handleSize && point.y <= bounds.y) {
return ResizeHandle.NE;
}
// SW corner
if (point.x >= bounds.x - handleSize && point.x <= bounds.x &&
point.y >= bounds.y + bounds.height && point.y <= bounds.y + bounds.height + handleSize) {
return ResizeHandle.SW;
}
// N edge
int edgeX = bounds.x + bounds.width / 2 - handleSize / 2;
if (point.x >= edgeX && point.x <= edgeX + handleSize &&
point.y >= bounds.y - handleSize && point.y <= bounds.y) {
return ResizeHandle.N;
}
// S edge
if (point.x >= edgeX && point.x <= edgeX + handleSize &&
point.y >= bounds.y + bounds.height && point.y <= bounds.y + bounds.height + handleSize) {
return ResizeHandle.S;
}
// W edge
int edgeY = bounds.y + bounds.height / 2 - handleSize / 2;
if (point.x >= bounds.x - handleSize && point.x <= bounds.x &&
point.y >= edgeY && point.y <= edgeY + handleSize) {
return ResizeHandle.W;
}
// E edge
if (point.x >= bounds.x + bounds.width && point.x <= bounds.x + bounds.width + handleSize &&
point.y >= edgeY && point.y <= edgeY + handleSize) {
return ResizeHandle.E;
}
}
}
return null;
}
@@ -253,4 +318,19 @@ public class Controller {
.filter(s -> s.getBounds().contains(evt.getPoint()))
.findFirst();
}
private Rectangle createSelectionBox(Point start, Point end) {
int x = Math.min(start.x, end.x);
int y = Math.min(start.y, end.y);
int width = Math.abs(end.x - start.x);
int height = Math.abs(end.y - start.y);
return new Rectangle(x, y, width, height);
}
private void updateSelectionFromBox(Rectangle box) {
resetSelection();
model.stream()
.filter(s -> s.getBounds().intersects(box))
.forEach(selection::add);
}
}

View File

@@ -135,7 +135,7 @@ public class ShapeDraftman implements ShapeVisitor {
if ((selAttrs != null) && (selAttrs.selected)){
Rectangle bounds = s.getBounds();
this.g2d.setColor(Color.RED);
int handleSize = 5;
int handleSize = 10;
this.g2d.drawRect(bounds.x - handleSize, bounds.y - handleSize, handleSize, handleSize);
this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, handleSize, handleSize);
if (resizeMode) {
@@ -149,4 +149,14 @@ public class ShapeDraftman implements ShapeVisitor {
}
}
public void drawSelectionBox(Rectangle box) {
if (box == null) return;
g2d.setXORMode(Color.WHITE);
g2d.setColor(Color.BLUE);
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 1, new float[]{4, 4}, 0));
g2d.drawRect(box.x, box.y, box.width, box.height);
g2d.setPaintMode();
}
}

View File

@@ -13,6 +13,7 @@ public class ShapesView extends JPanel {
private final Controller controller;
private ShapeVisitor draftman;
private boolean resizeMode;
private Rectangle currentSelectionBox;
public ShapesView(SCollection model) {
this.model = model;
@@ -25,6 +26,7 @@ public class ShapesView extends JPanel {
this.draftman = new ShapeDraftman(g);
((ShapeDraftman) this.draftman).setResizeMode(resizeMode);
model.accept(draftman);
((ShapeDraftman) this.draftman).drawSelectionBox(currentSelectionBox);
}
public Controller getController() {
@@ -38,4 +40,8 @@ public class ShapesView extends JPanel {
public void setResizeMode(boolean resizeMode) {
this.resizeMode = resizeMode;
}
public void setCurrentSelectionBox(Rectangle box) {
this.currentSelectionBox = box;
}
}