Skip to content

Commit ded5d18

Browse files
committed
Improved grapher
1 parent 04fe265 commit ded5d18

File tree

7 files changed

+246
-246
lines changed

7 files changed

+246
-246
lines changed

src/main/java/uk/co/ryanharrison/mathengine/gui/MainFrame.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public void keyTyped(KeyEvent e) {
136136
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
137137
setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
138138

139-
input.requestFocus();
139+
SwingUtilities.invokeLater(input::grabFocus);
140140
}
141141

142142
private void evaluateAndDisplay(String expression) {
@@ -159,8 +159,7 @@ private void evaluateAndDisplay(String expression) {
159159
private void appendToOutput(String text, SimpleAttributeSet style) {
160160
try {
161161
output.getDocument().insertString(output.getDocument().getLength(), text, style);
162-
} catch (BadLocationException e) {
163-
e.printStackTrace();
162+
} catch (BadLocationException ignore) {
164163
}
165164
}
166165
}

src/main/java/uk/co/ryanharrison/mathengine/plotting/FunctionListPanel.java

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,10 @@ private void editSelectedFunction() {
244244
gbc.insets = new Insets(5, 5, 5, 5);
245245
gbc.fill = GridBagConstraints.HORIZONTAL;
246246

247-
JTextField nameField = new JTextField(currentFunc.getName(), 15);
247+
JTextField nameField = new JTextField(currentFunc.name(), 15);
248248
JTextField equationField = new JTextField(currentFunc.getEquation(), 15);
249249
JButton colorButton = new JButton("Change Color");
250-
final Color[] selectedColor = {currentFunc.getColor()};
250+
final Color[] selectedColor = {currentFunc.color()};
251251

252252
colorButton.setBackground(selectedColor[0]);
253253
colorButton.setOpaque(true);
@@ -294,10 +294,10 @@ private void editSelectedFunction() {
294294
Function func = new Function(equation);
295295
PlottedFunction newFunc = PlottedFunction.builder()
296296
.function(func)
297-
.name(name.isEmpty() ? currentFunc.getName() : name)
297+
.name(name.isEmpty() ? currentFunc.name() : name)
298298
.color(selectedColor[0])
299-
.strokeWidth(currentFunc.getStrokeWidth())
300-
.visible(currentFunc.isVisible())
299+
.strokeWidth(currentFunc.strokeWidth())
300+
.visible(currentFunc.visible())
301301
.build();
302302

303303
updateFunction(index, newFunc);
@@ -322,7 +322,7 @@ private void removeSelectedFunction() {
322322

323323
int confirm = JOptionPane.showConfirmDialog(
324324
this,
325-
"Remove function: " + listModel.get(index).function.getName() + "?",
325+
"Remove function: " + listModel.get(index).function.name() + "?",
326326
"Confirm Remove",
327327
JOptionPane.YES_NO_OPTION
328328
);
@@ -336,7 +336,7 @@ private void removeSelectedFunction() {
336336
private void toggleFunctionVisibility(int index) {
337337
if (index >= 0 && index < listModel.size()) {
338338
FunctionListItem item = listModel.get(index);
339-
PlottedFunction newFunc = item.function.withVisible(!item.function.isVisible());
339+
PlottedFunction newFunc = item.function.withVisible(!item.function.visible());
340340
listModel.set(index, new FunctionListItem(newFunc, item.index));
341341
functionList.repaint();
342342
fireFunctionUpdated(index, newFunc);
@@ -384,14 +384,7 @@ default void onFunctionRemoved(int index) {
384384
/**
385385
* Internal wrapper for list items.
386386
*/
387-
private static class FunctionListItem {
388-
final PlottedFunction function;
389-
final int index;
390-
391-
FunctionListItem(PlottedFunction function, int index) {
392-
this.function = function;
393-
this.index = index;
394-
}
387+
private record FunctionListItem(PlottedFunction function, int index) {
395388
}
396389

397390
/**
@@ -438,9 +431,9 @@ public Component getListCellRendererComponent(
438431

439432
PlottedFunction func = item.function;
440433

441-
visibilityCheckbox.setSelected(func.isVisible());
442-
colorLabel.setBackground(func.getColor());
443-
nameLabel.setText(func.getName());
434+
visibilityCheckbox.setSelected(func.visible());
435+
colorLabel.setBackground(func.color());
436+
nameLabel.setText(func.name());
444437
equationLabel.setText(func.getEquation());
445438

446439
if (isSelected) {

src/main/java/uk/co/ryanharrison/mathengine/plotting/FunctionRenderer.java

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package uk.co.ryanharrison.mathengine.plotting;
22

33
import java.awt.*;
4-
import java.awt.geom.Line2D;
54
import java.awt.geom.Path2D;
65
import java.awt.geom.Point2D;
76
import java.util.ArrayList;
@@ -42,9 +41,6 @@ public final class FunctionRenderer {
4241
// Minimum horizontal distance between sample points (in pixels)
4342
private static final double MIN_SAMPLE_DISTANCE = 0.5;
4443

45-
// Maximum horizontal distance between sample points (in pixels)
46-
private static final double MAX_SAMPLE_DISTANCE = 2.0;
47-
4844
/**
4945
* Renders a function onto the given graphics context.
5046
*
@@ -53,14 +49,14 @@ public final class FunctionRenderer {
5349
* @param coords the coordinate system for transformations
5450
*/
5551
public void render(Graphics2D g, PlottedFunction function, GraphCoordinateSystem coords) {
56-
if (!function.isVisible()) {
52+
if (!function.visible()) {
5753
return;
5854
}
5955

60-
g.setColor(function.getColor());
56+
g.setColor(function.color());
6157
// Use high-quality stroke with round caps and joins for smooth curves
6258
g.setStroke(new BasicStroke(
63-
function.getStrokeWidth(),
59+
function.strokeWidth(),
6460
BasicStroke.CAP_ROUND,
6561
BasicStroke.JOIN_ROUND,
6662
10.0f, // Miter limit
@@ -95,7 +91,6 @@ private List<Point2D.Double> sampleFunction(PlottedFunction function, GraphCoord
9591
// Determine sampling density based on zoom level
9692
// More samples per pixel at higher zoom for smooth curves
9793
int baseSamples = (int) (screenWidth / MIN_SAMPLE_DISTANCE);
98-
int maxSamples = Math.min(baseSamples * 2, 4000); // Cap for performance
9994

10095
// Initial uniform sampling
10196
double dx = (maxX - minX) / baseSamples;
@@ -201,7 +196,7 @@ private void renderSegment(Graphics2D g, List<Point2D.Double> segment) {
201196
}
202197

203198
Path2D.Double path = new Path2D.Double();
204-
Point2D.Double first = segment.get(0);
199+
Point2D.Double first = segment.getFirst();
205200
path.moveTo(first.x, first.y);
206201

207202
for (int i = 1; i < segment.size(); i++) {
@@ -211,65 +206,4 @@ private void renderSegment(Graphics2D g, List<Point2D.Double> segment) {
211206

212207
g.draw(path);
213208
}
214-
215-
/**
216-
* Renders a simple point-to-point version of the function (legacy mode).
217-
* <p>
218-
* This is a faster but lower-quality rendering method that doesn't
219-
* handle discontinuities as well. Useful for real-time preview during
220-
* dragging operations.
221-
* </p>
222-
*
223-
* @param g the graphics context
224-
* @param function the function to render
225-
* @param coords the coordinate system
226-
*/
227-
public void renderSimple(Graphics2D g, PlottedFunction function, GraphCoordinateSystem coords) {
228-
if (!function.isVisible()) {
229-
return;
230-
}
231-
232-
g.setColor(function.getColor());
233-
g.setStroke(new BasicStroke(function.getStrokeWidth()));
234-
235-
double minX = coords.getMinVisibleX();
236-
double width = coords.getWidth();
237-
double dx = (coords.getMaxVisibleX() - minX) / width;
238-
239-
double clippingThreshold = calculateClippingThreshold(coords);
240-
241-
double prevX = minX;
242-
double prevY = function.evaluateAt(prevX);
243-
if (Math.abs(prevY) > clippingThreshold) {
244-
prevY = Math.signum(prevY) * clippingThreshold;
245-
}
246-
247-
for (int i = 1; i <= width; i++) {
248-
double x = minX + i * dx;
249-
double y = function.evaluateAt(x);
250-
251-
if (Double.isNaN(y)) {
252-
prevX = x;
253-
prevY = y;
254-
continue;
255-
}
256-
257-
if (Math.abs(y) > clippingThreshold) {
258-
y = Math.signum(y) * clippingThreshold;
259-
}
260-
261-
if (!Double.isNaN(prevY)) {
262-
Point2D.Double p1 = coords.toScreen(prevX, prevY);
263-
Point2D.Double p2 = coords.toScreen(x, y);
264-
265-
// Simple discontinuity check
266-
if (Math.abs(p2.y - p1.y) < coords.getHeight() * DISCONTINUITY_THRESHOLD) {
267-
g.draw(new Line2D.Double(p1.x, p1.y, p2.x, p2.y));
268-
}
269-
}
270-
271-
prevX = x;
272-
prevY = y;
273-
}
274-
}
275209
}

src/main/java/uk/co/ryanharrison/mathengine/plotting/GraphCoordinateSystem.java

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package uk.co.ryanharrison.mathengine.plotting;
22

33
import java.awt.geom.Point2D;
4+
import java.util.ArrayList;
5+
import java.util.List;
46

57
/**
68
* Manages coordinate transformations between screen space and Cartesian graph space.
@@ -35,15 +37,43 @@
3537
*/
3638
public final class GraphCoordinateSystem {
3739
// Predefined zoom levels - graph units per 80 pixels
38-
private static final double[] UNITS = {
39-
0.00005, 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005,
40-
0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0,
41-
20.0, 50.0, 100.0, 200.0, 500.0, 1000.0
42-
};
40+
// Much finer increments for smooth, Google Maps-like zooming
41+
private static final double[] UNITS = generateZoomLevels();
4342

4443
// Fixed pixel distance for unit scaling
4544
private static final double PIXELS_PER_UNIT_STEP = 80.0;
4645

46+
/**
47+
* Generates fine-grained zoom levels using 1.15x increments.
48+
* This creates smooth zoom transitions like Google Maps.
49+
* Includes exact "nice" values like 0.1, 1, 10, 100 for clean defaults.
50+
*/
51+
private static double[] generateZoomLevels() {
52+
List<Double> levels = new ArrayList<>();
53+
54+
// Generate smooth levels from 0.00001 to 10000
55+
double current = 0.00001;
56+
while (current <= 10000.0) {
57+
levels.add(current);
58+
current *= 1.15; // ~15% increase per level for smooth transitions
59+
}
60+
61+
// Add exact "nice" values if not already present
62+
double[] niceValues = {0.00001, 0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0, 10000.0};
63+
for (double nice : niceValues) {
64+
if (!levels.contains(nice)) {
65+
levels.add(nice);
66+
}
67+
}
68+
69+
// Sort and remove duplicates
70+
return levels.stream()
71+
.distinct()
72+
.sorted()
73+
.mapToDouble(Double::doubleValue)
74+
.toArray();
75+
}
76+
4777
private final double width;
4878
private final double height;
4979

@@ -57,6 +87,7 @@ public final class GraphCoordinateSystem {
5787
// Derived value: pixels per graph unit at current zoom
5888
private double scale;
5989

90+
6091
/**
6192
* Creates a coordinate system for a viewport of the given dimensions.
6293
*
@@ -76,10 +107,33 @@ public GraphCoordinateSystem(double width, double height) {
76107
this.height = height;
77108
this.originX = 0.0;
78109
this.originY = 0.0;
79-
this.zoomLevel = 13; // Default zoom showing ~1 unit per 80 pixels
110+
this.zoomLevel = findDefaultZoomLevel(); // Default zoom showing ~1 unit per 80 pixels
80111
updateScale();
81112
}
82113

114+
/**
115+
* Finds the zoom level for exactly 1.0 units per 80 pixels.
116+
*/
117+
private static int findDefaultZoomLevel() {
118+
// Find exact match for 1.0 first
119+
for (int i = 0; i < UNITS.length; i++) {
120+
if (Math.abs(UNITS[i] - 1.0) < 0.001) {
121+
return i;
122+
}
123+
}
124+
// If not found, find closest to 1.0
125+
int bestIndex = 0;
126+
double bestDiff = Double.MAX_VALUE;
127+
for (int i = 0; i < UNITS.length; i++) {
128+
double diff = Math.abs(UNITS[i] - 1.0);
129+
if (diff < bestDiff) {
130+
bestDiff = diff;
131+
bestIndex = i;
132+
}
133+
}
134+
return bestIndex;
135+
}
136+
83137
/**
84138
* Updates the viewport dimensions.
85139
* <p>
@@ -233,7 +287,7 @@ public boolean zoomToward(double screenX, double screenY, boolean zoomIn) {
233287
public void resetView() {
234288
originX = 0.0;
235289
originY = 0.0;
236-
zoomLevel = 13;
290+
zoomLevel = findDefaultZoomLevel();
237291
updateScale();
238292
}
239293

@@ -256,10 +310,11 @@ public void setOrigin(double x, double y) {
256310
*/
257311
public void setZoom(int zoomLevel) {
258312
if (zoomLevel < 0 || zoomLevel >= UNITS.length) {
259-
throw new IllegalArgumentException(
260-
"Zoom level must be between 0 and " + (UNITS.length - 1) + ", got: " + zoomLevel);
313+
// Clamp instead of throwing to handle edge cases gracefully
314+
this.zoomLevel = Math.max(0, Math.min(UNITS.length - 1, zoomLevel));
315+
} else {
316+
this.zoomLevel = zoomLevel;
261317
}
262-
this.zoomLevel = zoomLevel;
263318
updateScale();
264319
}
265320

0 commit comments

Comments
 (0)