├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── config
└── app.settings
├── pom.xml
└── src
└── main
├── java
├── dev
│ └── sgora
│ │ └── mesheditor
│ │ ├── Main.java
│ │ ├── MeshEditor.java
│ │ ├── model
│ │ ├── MouseConfig.java
│ │ ├── NamespaceMap.java
│ │ ├── config
│ │ │ ├── JsonConfig.java
│ │ │ └── JsonFileConfig.java
│ │ ├── geom
│ │ │ ├── Line.java
│ │ │ ├── Mesh.java
│ │ │ ├── Point.java
│ │ │ ├── PointRegion.java
│ │ │ └── polygons
│ │ │ │ ├── Polygon.java
│ │ │ │ ├── Rectangle.java
│ │ │ │ └── Triangle.java
│ │ ├── observables
│ │ │ ├── BindableProperty.java
│ │ │ └── SettableList.java
│ │ ├── paint
│ │ │ └── SerializableColor.java
│ │ └── project
│ │ │ ├── CanvasData.java
│ │ │ ├── CanvasUI.java
│ │ │ ├── LoadState.java
│ │ │ ├── MeshLayer.java
│ │ │ ├── ModelModule.java
│ │ │ ├── PropertyContainer.java
│ │ │ └── VisualProperties.java
│ │ ├── services
│ │ ├── config
│ │ │ ├── ConfigModule.java
│ │ │ ├── JsonAppConfigManager.java
│ │ │ ├── JsonAppConfigReader.java
│ │ │ ├── JsonConfigReader.java
│ │ │ ├── JsonLangConfigReader.java
│ │ │ ├── LangConfigReader.java
│ │ │ ├── annotation
│ │ │ │ ├── AppConfig.java
│ │ │ │ └── AppSettings.java
│ │ │ └── interfaces
│ │ │ │ ├── AppConfigManager.java
│ │ │ │ ├── AppConfigReader.java
│ │ │ │ └── AppConfigWriter.java
│ │ ├── drawing
│ │ │ ├── ColorUtils.java
│ │ │ ├── DrawingModule.java
│ │ │ ├── ImageBox.java
│ │ │ └── MeshBox.java
│ │ ├── files
│ │ │ ├── ConfigModelMapper.java
│ │ │ ├── FileIOModule.java
│ │ │ ├── FileUtils.java
│ │ │ ├── ProjectFileUtils.java
│ │ │ ├── ProjectIOException.java
│ │ │ └── workspace
│ │ │ │ ├── AppRecentProjectManager.java
│ │ │ │ ├── FileChooserAction.java
│ │ │ │ ├── FileType.java
│ │ │ │ ├── WorkspaceActionExecutor.java
│ │ │ │ ├── WorkspaceActionFacade.java
│ │ │ │ ├── WorkspaceActionModule.java
│ │ │ │ └── interfaces
│ │ │ │ ├── RecentProjectManager.java
│ │ │ │ └── WorkspaceAction.java
│ │ ├── history
│ │ │ ├── ActionHistoryModule.java
│ │ │ ├── ActionHistoryService.java
│ │ │ ├── CommandActionHistoryService.java
│ │ │ └── actions
│ │ │ │ ├── UserAction.java
│ │ │ │ ├── node
│ │ │ │ ├── AddNodeAction.java
│ │ │ │ ├── MoveNodeAction.java
│ │ │ │ ├── NodeModifiedAction.java
│ │ │ │ └── RemoveNodeAction.java
│ │ │ │ └── property
│ │ │ │ ├── CheckBoxChangeAction.java
│ │ │ │ ├── PropertyChangeAction.java
│ │ │ │ └── SliderChangeAction.java
│ │ ├── input
│ │ │ ├── CanvasAction.java
│ │ │ ├── CanvasActionFacade.java
│ │ │ ├── InputModule.java
│ │ │ └── MouseListener.java
│ │ ├── mesh
│ │ │ ├── generation
│ │ │ │ ├── FlipBasedTriangulationService.java
│ │ │ │ ├── FlippingUtils.java
│ │ │ │ ├── MeshGenerationModule.java
│ │ │ │ ├── NodeUtils.java
│ │ │ │ ├── TriangleUtils.java
│ │ │ │ ├── TriangulationService.java
│ │ │ │ └── VoronoiDiagramService.java
│ │ │ └── rendering
│ │ │ │ ├── CanvasMeshRenderer.java
│ │ │ │ ├── CanvasRenderer.java
│ │ │ │ ├── JFreeSvgMeshRenderer.java
│ │ │ │ ├── MeshRenderer.java
│ │ │ │ ├── MeshRenderingModule.java
│ │ │ │ └── SvgMeshRenderer.java
│ │ └── ui
│ │ │ ├── PropertyTreeCellFactory.java
│ │ │ ├── UIModule.java
│ │ │ └── UiDialogUtils.java
│ │ ├── ui
│ │ ├── CopyableLabel.java
│ │ ├── canvas
│ │ │ ├── ImageCanvas.java
│ │ │ └── ResizableCanvas.java
│ │ └── properties
│ │ │ ├── PropertyItemType.java
│ │ │ ├── PropertyTreeItem.java
│ │ │ └── TextKeyProvider.java
│ │ └── view
│ │ ├── AboutWindow.java
│ │ ├── CanvasView.java
│ │ ├── MenuView.java
│ │ ├── PropertiesView.java
│ │ ├── ViewType.java
│ │ ├── WindowModule.java
│ │ ├── WindowView.java
│ │ ├── annotation
│ │ ├── MainWindowRoot.java
│ │ └── MainWindowStage.java
│ │ └── sub
│ │ ├── SubView.java
│ │ └── SubViewFactory.java
└── module-info.java
└── resources
├── app.config
├── fxml
├── AboutWindow.fxml
├── CanvasView.fxml
├── MenuView.fxml
├── PropertiesView.fxml
└── WindowView.fxml
├── i18n
├── en.lang
└── pl.lang
├── logo.png
└── styles
└── dark.css
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | target/
3 | out/
4 | *.iml
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk:
3 | - openjdk12
4 | git:
5 | depth: false
6 | cache:
7 | directories:
8 | - '$HOME/.m2/repository'
9 | - '$HOME/.sonar/cache'
10 | script:
11 | - mvn clean compile package sonar:sonar
12 | addons:
13 | sonarcloud:
14 | organization: "stasgora"
15 | token:
16 | secure: nZzAwnJjmFy1kf22Pi2oAWugmGj0Y3IWvaSrZrcbcp/k2NrI4hGbmTHDMG28I6URp4m15DKwFQOeFbzg107iJ+vYPpUy25pLmO8snOh4tj2WdjaSYySAjf4IpoD4kZJEW9a7h7gIYFwGNoU+PEYgimunvh6Qc+o9wEgrAmJB7RUptiPCO7ibbBI0xVNABH5GGY5GA+wWy40ad7BNk/IUPU9M+p5liBkNHB0MOXjcIR/PDoSFj3FxfVfAMCn9AoID3UYma+F44CNbDBGxcsl2UdnWjmP1Helbn8311p8gOclw3ADCJTDuWzRVj19zklPEPxO0CjmFHgNzbOlhfleBnVWkWOaFWGEA/8mWB7P93/Zt0FNfYZUDZiro3Zn5zL/4cjkMO7DxO0oXpKx6o/zDgURx8Lzfhtgd0LcN/RY3OYhz7+0wDqvnlUevzmTgNAkPX/lXHb0zMGshUvrQSLET1N2bkSjmr1FNXWJYyWHmtm5Zb54wjNPVJIAT+Jxck/elUVUYNy+SbJTva81MAmaDdFo44LknjbUsFzDaG6IxM7N9Tid76TIENmRKLJPi0REoRKCvXnRiw8FyP1kEqMjmH/GVaKVrr1rgn8BYqscbNxJvIIrE1sgOGbbiopzgV1/9r90NmRYkmAgNo+/pUkrWmimDwteR5nDAw0tBVOTx00c=
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Stanisław Góra
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/stasgora/mesh-editor)
2 | [](https://github.com/stasgora/mesh-editor/blob/master/LICENSE)
3 | 
4 |
5 | # Mesh Editor
6 | Desktop application created to turn raster images into low poly vector art
7 |
8 | 
9 |
10 | ## Getting started
11 | ### Build & Run - Maven
12 | - Clone project and run in root directory:
13 |
14 | ```mvn clean compile package exec:java```
15 |
16 | ### Latest builds
17 | [Releases page](https://github.com/stasgora/mesh-editor/releases)
18 |
19 | ## Features:
20 | - Dynamic & Kinetic Delaunay Triangulation & Voronoi Diagram
21 | - Project Saving & Loading
22 | - Export to SVG
23 | - Layers properties view with disabling & blending for easy editing
24 | - Mesh colors dynamically sampled from image
25 | - UI localization files
26 |
27 | ### Gallery:
28 | 
29 |
30 | 
31 |
32 | 
33 |
34 | ### Author
35 | Stanisław Góra
36 |
37 | ### License
38 | [MIT License](http://www.opensource.org/licenses/mit-license.php)
39 |
--------------------------------------------------------------------------------
/config/app.settings:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "imageBox": {"zoom": {
4 | "dir": -1,
5 | "speed": 0.0025
6 | }},
7 | "language": "en"
8 | },
9 | "last": {
10 | "windowPlacement": {
11 | "size": {
12 | "w": 1200,
13 | "h": 800
14 | },
15 | "position": {
16 | "x": 360,
17 | "y": 100
18 | }
19 | },
20 | "projects": [],
21 | "chosenDir": {}
22 | }
23 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | dev.sgora
8 | mesh-editor
9 | 0.9.1-SNAPSHOT
10 |
11 | Mesh Editor
12 | JavaFX editor created to turn raster images into low poly vector art
13 | https://github.com/stasgora/mesh-editor
14 |
15 |
16 | MIT License
17 | http://www.opensource.org/licenses/mit-license.php
18 |
19 |
20 |
21 |
22 | Stanisław Góra
23 | contact@sgora.dev
24 |
25 | Project Owner
26 | Developer
27 |
28 |
29 |
30 |
31 |
32 | UTF-8
33 | target
34 |
35 | 4.2.2
36 | 12.0.2
37 | 1.0.3
38 | 20190722
39 | 3.4
40 |
41 |
42 |
43 |
44 |
45 | org.apache.maven.plugins
46 | maven-compiler-plugin
47 | 3.8.1
48 |
49 | 12
50 | 12
51 | ${project.build.sourceEncoding}
52 |
53 |
54 |
55 | org.apache.maven.plugins
56 | maven-jar-plugin
57 | 3.1.2
58 |
59 |
60 |
61 | true
62 | dev.sgora.mesheditor.Main
63 |
64 |
65 |
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-assembly-plugin
70 | 3.1.1
71 |
72 |
73 |
74 | dev.sgora.mesheditor.Main
75 |
76 |
77 |
78 | jar-with-dependencies
79 |
80 |
81 |
82 |
83 | make-assembly
84 | package
85 |
86 | single
87 |
88 |
89 |
90 |
91 |
92 | org.codehaus.mojo
93 | exec-maven-plugin
94 | 1.6.0
95 |
96 |
97 |
98 | java
99 |
100 |
101 |
102 |
103 | dev.sgora.mesheditor.Main
104 |
105 |
106 |
107 |
108 | org.sonarsource.scanner.maven
109 | sonar-maven-plugin
110 | 3.6.1.1688
111 |
112 |
113 |
114 |
115 | ${basedir}/src/main/resources
116 |
117 | **/*
118 |
119 |
120 |
121 | ${basedir}/config
122 | ${project.build.directory}/config
123 |
124 |
125 |
126 |
127 |
128 |
129 | io.github.stasgora
130 | observetree
131 | ${observetree.version}
132 |
133 |
134 |
135 | org.json
136 | json
137 | ${json-lib.version}
138 |
139 |
140 | org.jfree
141 | jfreesvg
142 | ${jfreesvg.version}
143 |
144 |
145 |
146 | org.openjfx
147 | javafx-controls
148 | ${javafx.version}
149 |
150 |
151 | org.openjfx
152 | javafx-fxml
153 | ${javafx.version}
154 |
155 |
156 |
157 | com.google.inject
158 | guice
159 | ${guice.version}
160 |
161 |
162 | com.google.inject.extensions
163 | guice-assistedinject
164 | ${guice.version}
165 |
166 |
167 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/Main.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor;
2 |
3 | /**
4 | * A proxy class to avoid error when launching a build. See https://github.com/javafxports/openjdk-jfx/issues/236 for details.
5 | */
6 | public class Main {
7 |
8 | public static void main(String[] args) {
9 | MeshEditor.main(args);
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/MeshEditor.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor;
2 |
3 | import com.google.inject.Guice;
4 | import com.google.inject.Injector;
5 | import dev.sgora.mesheditor.services.files.FileIOModule;
6 | import dev.sgora.mesheditor.services.files.workspace.WorkspaceActionModule;
7 | import dev.sgora.mesheditor.services.history.ActionHistoryModule;
8 | import dev.sgora.mesheditor.services.mesh.generation.MeshGenerationModule;
9 | import dev.sgora.mesheditor.services.mesh.rendering.MeshRenderingModule;
10 | import dev.sgora.mesheditor.services.ui.UIModule;
11 | import dev.sgora.mesheditor.view.ViewType;
12 | import dev.sgora.mesheditor.view.WindowModule;
13 | import dev.sgora.mesheditor.view.WindowView;
14 | import javafx.application.Application;
15 | import javafx.fxml.FXMLLoader;
16 | import javafx.scene.Parent;
17 | import javafx.scene.image.Image;
18 | import javafx.stage.Stage;
19 | import dev.sgora.mesheditor.model.project.ModelModule;
20 | import dev.sgora.mesheditor.services.config.ConfigModule;
21 | import dev.sgora.mesheditor.services.config.LangConfigReader;
22 | import dev.sgora.mesheditor.services.drawing.DrawingModule;
23 | import dev.sgora.mesheditor.services.files.workspace.interfaces.WorkspaceAction;
24 | import dev.sgora.mesheditor.services.input.InputModule;
25 | import dev.sgora.mesheditor.view.sub.SubViewFactory;
26 |
27 | public class MeshEditor extends Application {
28 | private WindowView windowView;
29 | private Injector injector;
30 |
31 | @Override
32 | public void start(Stage stage) throws Exception {
33 | FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/WindowView.fxml"));
34 | Parent root = loader.load();
35 | windowView = loader.getController();
36 |
37 | injector = Guice.createInjector(new WindowModule(root, stage, loader.getNamespace()), new ConfigModule(),
38 | new ModelModule(), new MeshGenerationModule(), new MeshRenderingModule(), new DrawingModule(), new InputModule(),
39 | new ActionHistoryModule(), new FileIOModule(), new WorkspaceActionModule(), new UIModule());
40 | initViews();
41 |
42 | stage.getIcons().add(new Image(MeshEditor.class.getResourceAsStream("/logo.png")));
43 | stage.requestFocus();
44 | stage.show();
45 | }
46 |
47 | private void initViews() {
48 | injector.injectMembers(windowView);
49 | windowView.setWorkspaceAction(injector.getInstance(WorkspaceAction.class));
50 |
51 | SubViewFactory subViewFactory = injector.getInstance(SubViewFactory.class);
52 | subViewFactory.buildPropertiesView(windowView.getPropertiesViewRoot(), ViewType.PROPERTIES_VIEW);
53 | subViewFactory.buildMenuView(windowView.getMenuViewRoot(), ViewType.MENU_VIEW);
54 | subViewFactory.buildCanvasView(windowView.getCanvasViewRoot(), ViewType.CANVAS_VIEW);
55 |
56 | injector.getInstance(LangConfigReader.class).onSetMainLanguage(); //TODO move once we have language settings
57 | }
58 |
59 | public static void main(String[] args) {
60 | launch(args);
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/MouseConfig.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model;
2 |
3 | import javafx.scene.Cursor;
4 | import javafx.scene.input.MouseButton;
5 |
6 | public class MouseConfig {
7 |
8 | public final MouseButton placeNodeButton = MouseButton.PRIMARY;
9 | public final MouseButton removeNodeButton = MouseButton.SECONDARY;
10 | public final MouseButton moveNodeButton = MouseButton.PRIMARY;
11 | public final MouseButton dragImageButton = MouseButton.MIDDLE;
12 |
13 | public final Cursor defaultCanvasCursor = Cursor.CROSSHAIR;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/NamespaceMap.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model;
2 |
3 | import javafx.collections.ObservableMap;
4 |
5 | import java.util.HashMap;
6 |
7 | public class NamespaceMap extends HashMap> { }
8 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/config/JsonConfig.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.config;
2 |
3 | import org.json.JSONObject;
4 |
5 | public class JsonConfig {
6 |
7 | public final String name;
8 | public final JSONObject config;
9 |
10 | public JsonConfig(String name, JSONObject config) {
11 | this.name = name;
12 | this.config = config;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/config/JsonFileConfig.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.config;
2 |
3 | import org.json.JSONObject;
4 |
5 | import java.io.File;
6 |
7 | public class JsonFileConfig extends JsonConfig {
8 | public final File configFile;
9 |
10 | public JsonFileConfig(File configFile, JSONObject config) {
11 | super(configFile.getName(), config);
12 | this.configFile = configFile;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/Line.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom;
2 |
3 | import java.util.logging.Logger;
4 |
5 | public class Line {
6 | private static final Logger LOGGER = Logger.getLogger(Line.class.getName());
7 |
8 | public final double a;
9 | public final double b;
10 | public final double c;
11 |
12 | public Line(double a, double b, double c) {
13 | this.a = a;
14 | this.b = b;
15 | this.c = c;
16 | }
17 |
18 | public Point intersectWith(Line line) {
19 | double delta = a * line.b - line.a * b;
20 | if (delta == 0) {
21 | LOGGER.warning("Lines are parallel");
22 | return null;
23 | }
24 | return new Point((b * line.c - line.b * c) / delta, (line.a * c - a * line.c) / delta);
25 | }
26 |
27 | public static Line bisectionOf(Point a, Point b) {
28 | return new Line(2 * (a.getX() - b.getX()), 2 * (a.getY() - b.getY()), b.getX() * b.getX() + b.getY() * b.getY() - a.getX() * a.getX() - a.getY() * a.getY());
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/Mesh.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom;
2 |
3 | import io.github.stasgora.observetree.Observable;
4 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
5 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
6 |
7 | import java.io.IOException;
8 | import java.io.ObjectInputStream;
9 | import java.io.ObjectOutputStream;
10 | import java.io.Serializable;
11 | import java.util.ArrayList;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.logging.Logger;
15 | import java.util.stream.Collectors;
16 |
17 | public class Mesh extends Observable implements Serializable {
18 |
19 | private static final Logger LOGGER = Logger.getLogger(Mesh.class.getName());
20 | private static final long serialVersionUID = 1L;
21 |
22 | private List nodeRegions = new ArrayList<>();
23 | private List triangles = new ArrayList<>();
24 |
25 | private List boundingNodes;
26 |
27 | public Mesh(Point[] boundingNodes) {
28 | if (boundingNodes.length != 3) {
29 | LOGGER.warning("Mesh bounding nodes number wrong");
30 | }
31 | this.boundingNodes = List.of(boundingNodes);
32 | addTriangle(new Triangle(boundingNodes[0], boundingNodes[1], boundingNodes[2]));
33 | }
34 |
35 | public void addNode(Point node) {
36 | nodeRegions.add(new PointRegion(node));
37 | addSubObservable(node);
38 | onValueChanged();
39 | }
40 |
41 | public Polygon getPointRegion(Point node) {
42 | for (PointRegion pointRegion : nodeRegions) {
43 | if (pointRegion.node == node)
44 | return pointRegion.region;
45 | }
46 | return null;
47 | }
48 |
49 | public void removeNode(Point node) {
50 | for (int i = 0; i < nodeRegions.size(); i++) {
51 | if (nodeRegions.get(i).node == node) {
52 | nodeRegions.remove(i);
53 | break;
54 | }
55 | }
56 | onValueChanged();
57 | }
58 |
59 | public List getNodes() {
60 | return nodeRegions.stream().map(pointRegion -> pointRegion.node).collect(Collectors.toUnmodifiableList());
61 | }
62 |
63 | public List getNodeRegions() {
64 | return Collections.unmodifiableList(nodeRegions);
65 | }
66 |
67 | public void addTriangle(Triangle triangle) {
68 | triangles.add(triangle);
69 | onValueChanged();
70 | }
71 |
72 | public void removeTriangle(Triangle triangle) {
73 | triangles.remove(triangle);
74 | onValueChanged();
75 | }
76 |
77 | public List getBoundingNodes() {
78 | return boundingNodes;
79 | }
80 |
81 | public Triangle getTriangle(int index) {
82 | return triangles.get(index);
83 | }
84 |
85 | public List getTriangles() {
86 | return Collections.unmodifiableList(triangles);
87 | }
88 |
89 | private void writeObject(ObjectOutputStream out) throws IOException {
90 | for (int i = 0; i < triangles.size(); i++) {
91 | triangles.get(i).setTriangleId(i);
92 | }
93 | out.defaultWriteObject();
94 | }
95 |
96 | private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
97 | in.defaultReadObject();
98 | nodeRegions.forEach(pointRegion -> addSubObservable(pointRegion.node));
99 | triangles.forEach(this::assignTriangleNeighbours);
100 | }
101 |
102 | private void assignTriangleNeighbours(Triangle triangle) {
103 | triangle.setTriangles(new Triangle[3]);
104 | for (int i = 0; i < 3; i++) {
105 | if (triangle.triangleIds[i] >= 0) {
106 | triangle.getTriangles()[i] = triangles.get(triangle.triangleIds[i]);
107 | }
108 | }
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/Point.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom;
2 |
3 | import io.github.stasgora.observetree.Observable;
4 |
5 | import java.io.Serializable;
6 | import java.util.Objects;
7 |
8 | public class Point extends Observable implements Serializable {
9 | private double x;
10 | private double y;
11 |
12 | private static final double ROUNDING_MULT = 100;
13 | private static final long serialVersionUID = 1L;
14 |
15 | public Point(double x, double y) {
16 | this.x = x;
17 | this.y = y;
18 | }
19 |
20 | public Point(Point point) {
21 | this(point.x, point.y);
22 | }
23 |
24 | public Point() {
25 | this(0, 0);
26 | }
27 |
28 | public Point set(Point point) {
29 | return set(point.x, point.y);
30 | }
31 |
32 | public Point set(double x, double y) {
33 | setValues(x, y);
34 | return this;
35 | }
36 |
37 | public Point abs() {
38 | setValues(Math.abs(x), Math.abs(y));
39 | return this;
40 | }
41 |
42 | public Point clamp(Point min, Point max) {
43 | setValues(Math.max(min.x, Math.min(max.x, x)), Math.max(min.y, Math.min(max.y, y)));
44 | return this;
45 | }
46 |
47 | public Point clamp(Point max) {
48 | return clamp(new Point(), max);
49 | }
50 |
51 | public Point multiplyByScalar(double amount) {
52 | setValues(x * amount, y * amount);
53 | return this;
54 | }
55 |
56 | public Point multiply(Point point) {
57 | setValues(x * point.x, y * point.y);
58 | return this;
59 | }
60 |
61 | public Point divide(Point point) {
62 | setValues(x / point.x, y / point.y);
63 | return this;
64 | }
65 |
66 | public Point divideByScalar(double amount) {
67 | setValues(x / amount, y / amount);
68 | return this;
69 | }
70 |
71 | public Point subtract(Point point) {
72 | setValues(x - point.x, y - point.y);
73 | return this;
74 | }
75 |
76 | public Point add(Point point) {
77 | setValues(x + point.x, y + point.y);
78 | return this;
79 | }
80 |
81 | public boolean isBetween(Point min, Point max) {
82 | return x >= min.x && x < max.x && y >= min.y && y < max.y;
83 | }
84 |
85 | private void setValues(double x, double y) {
86 | boolean changed = this.x != x || this.y != y;
87 | this.x = x;
88 | this.y = y;
89 | if (changed) {
90 | onValueChanged();
91 | }
92 | }
93 |
94 | public double getX() {
95 | return x;
96 | }
97 |
98 | public double getY() {
99 | return y;
100 | }
101 |
102 | @Override
103 | public String toString() {
104 | return "(" + Math.round(x * ROUNDING_MULT) / ROUNDING_MULT + ", " + Math.round(y * ROUNDING_MULT) / ROUNDING_MULT + ')';
105 | }
106 |
107 | @Override
108 | public boolean equals(Object o) {
109 | if (this == o) return true;
110 | if (o == null || getClass() != o.getClass()) return false;
111 | Point point = (Point) o;
112 | return Double.compare(point.x, x) == 0 && Double.compare(point.y, y) == 0;
113 | }
114 |
115 | @Override
116 | public int hashCode() {
117 | return Objects.hash(x, y);
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/PointRegion.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom;
2 |
3 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
4 |
5 | import java.io.Serializable;
6 |
7 | public class PointRegion implements Serializable {
8 | public final Point node;
9 | public final Polygon region = new Polygon();
10 |
11 | public PointRegion(Point node) {
12 | this.node = node;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/polygons/Polygon.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom.polygons;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 |
5 | import java.io.Serializable;
6 | import java.util.Arrays;
7 |
8 | public class Polygon implements Serializable {
9 | protected Point[] nodes = new Point[0];
10 |
11 | protected static final long serialVersionUID = 1L;
12 |
13 | public Polygon() {
14 | }
15 |
16 | public Polygon(Point[] nodes) {
17 | this.nodes = nodes;
18 | }
19 |
20 | public int[] xCoords() {
21 | return Arrays.stream(nodes).map(point -> (int) Math.round(point.getX())).mapToInt(Integer::intValue).toArray();
22 | }
23 |
24 | public int[] yCoords() {
25 | return Arrays.stream(nodes).map(point -> (int) Math.round(point.getY())).mapToInt(Integer::intValue).toArray();
26 | }
27 |
28 | @Override
29 | public String toString() {
30 | return Arrays.toString(nodes);
31 | }
32 |
33 | public Point[] getNodes() {
34 | return nodes;
35 | }
36 |
37 | public void setNodes(Point[] nodes) {
38 | this.nodes = nodes;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/polygons/Rectangle.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom.polygons;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 | import io.github.stasgora.observetree.Observable;
5 |
6 | import java.io.Serializable;
7 |
8 | public class Rectangle extends Observable implements Serializable {
9 | private Point position;
10 | private Point size;
11 |
12 | private static final long serialVersionUID = 1L;
13 |
14 | public Rectangle() {
15 | position = new Point();
16 | addSubObservable(position);
17 | size = new Point();
18 | addSubObservable(size);
19 | }
20 |
21 | public boolean contains(Point point) {
22 | return point.getX() >= position.getX() && point.getX() <= position.getX() + size.getX() && point.getY() >= position.getY() && point.getY() <= position.getY() + size.getY();
23 | }
24 |
25 | public Point getPosition() {
26 | return position;
27 | }
28 |
29 | public void setPosition(Point position) {
30 | this.position = position;
31 | }
32 |
33 | public Point getSize() {
34 | return size;
35 | }
36 |
37 | public void setSize(Point size) {
38 | this.size = size;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/geom/polygons/Triangle.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.geom.polygons;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 | import dev.sgora.mesheditor.model.geom.Line;
5 |
6 | import java.io.IOException;
7 | import java.io.ObjectInputStream;
8 | import java.io.ObjectOutputStream;
9 | import java.util.logging.Logger;
10 |
11 | public class Triangle extends Polygon {
12 | private static final Logger LOGGER = Logger.getLogger(Triangle.class.getName());
13 |
14 | private transient Triangle[] triangles = new Triangle[3];
15 |
16 | private transient int triangleId;
17 | public final int[] triangleIds = new int[3];
18 |
19 | public Triangle(Point[] nodes) {
20 | super(nodes);
21 | if (nodes.length != 3)
22 | LOGGER.warning(() -> String.format("Triangle initialized with %d points", nodes.length));
23 | }
24 |
25 | public Triangle(Point a, Point b, Point c) {
26 | super(new Point[]{a, b, c});
27 | }
28 |
29 | public Point circumcenter() {
30 | return Line.bisectionOf(nodes[0], nodes[1]).intersectWith(Line.bisectionOf(nodes[1], nodes[2]));
31 | }
32 |
33 | private void writeObject(ObjectOutputStream out) throws IOException {
34 | for (int i = 0; i < 3; i++) {
35 | triangleIds[i] = triangles[i] != null ? triangles[i].triangleId : -1;
36 | }
37 | out.defaultWriteObject();
38 | }
39 |
40 | private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
41 | in.defaultReadObject();
42 | }
43 |
44 | public Triangle[] getTriangles() {
45 | return triangles;
46 | }
47 |
48 | public void setTriangles(Triangle[] triangles) {
49 | this.triangles = triangles;
50 | }
51 |
52 | public int getTriangleId() {
53 | return triangleId;
54 | }
55 |
56 | public void setTriangleId(int triangleId) {
57 | this.triangleId = triangleId;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/observables/BindableProperty.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.observables;
2 |
3 | import io.github.stasgora.observetree.SettableProperty;
4 | import io.github.stasgora.observetree.listener.ChangeListener;
5 | import javafx.beans.value.ObservableValue;
6 | import javafx.beans.value.WritableValue;
7 |
8 | import java.util.function.Function;
9 |
10 | public class BindableProperty extends SettableProperty {
11 |
12 | public BindableProperty() {
13 | }
14 |
15 | public BindableProperty(T modelValue) {
16 | super(modelValue);
17 | }
18 |
19 | public & ObservableValue> void bindWithFxObservable(O observable) {
20 | bindWithFxObservable(observable, val -> val, val -> val);
21 | }
22 |
23 | public & ObservableValue> void bindWithFxObservable(O observable, Function toFxObservable, Function fromFxObservable) {
24 | observable.addListener((observableValue, oldVal, newVal) -> setAndNotify(fromFxObservable.apply(newVal)));
25 | ChangeListener setFxObservable = () -> observable.setValue(toFxObservable.apply(modelValue));
26 | addListener(setFxObservable);
27 | if (modelValue != null)
28 | setFxObservable.call();
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/observables/SettableList.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.observables;
2 |
3 | import io.github.stasgora.observetree.Observable;
4 |
5 | import java.io.Serializable;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.function.UnaryOperator;
9 |
10 | public class SettableList extends Observable implements Serializable {
11 | private List modelList = new ArrayList<>();
12 |
13 | private static final long serialVersionUID = 1L;
14 |
15 | public SettableList() { }
16 |
17 | public SettableList(List modelList) {
18 | this.modelList.addAll(modelList);
19 | }
20 |
21 | public List get() {
22 | return new ArrayList<>(modelList);
23 | }
24 |
25 | public void set(List modelList) {
26 | if(this.modelList.equals(modelList))
27 | return;
28 | this.modelList = modelList;
29 | onValueChanged();
30 | }
31 |
32 | public void setAndNotify(List modelList) {
33 | set(modelList);
34 | notifyListeners();
35 | }
36 |
37 | public void modify(UnaryOperator> operator) {
38 | set(operator.apply(modelList));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/paint/SerializableColor.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.paint;
2 |
3 | import javafx.scene.paint.Color;
4 |
5 | import java.io.Serializable;
6 |
7 | public class SerializableColor implements Serializable {
8 | private double red;
9 | private double green;
10 | private double blue;
11 | private double alpha;
12 |
13 | private static final long serialVersionUID = 1L;
14 |
15 | public SerializableColor() {
16 | this(1, 1, 1, 1);
17 | }
18 |
19 | public SerializableColor(Color color) {
20 | this.red = color.getRed();
21 | this.green = color.getGreen();
22 | this.blue = color.getBlue();
23 | this.alpha = color.getOpacity();
24 | }
25 |
26 | public SerializableColor(double red, double green, double blue, double alpha) {
27 | this.red = red;
28 | this.green = green;
29 | this.blue = blue;
30 | this.alpha = alpha;
31 | }
32 |
33 | public Color toFXColor() {
34 | return new Color(red, green, blue, alpha);
35 | }
36 |
37 | public java.awt.Color toAwtColor() {
38 | return new java.awt.Color((float) red, (float) green, (float) blue, (float) alpha);
39 | }
40 |
41 | public SerializableColor averageWith(SerializableColor color) {
42 | red = (red + color.red) / 2;
43 | green = (green + color.green) / 2;
44 | blue = (blue + color.blue) / 2;
45 | alpha = (alpha + color.alpha) / 2;
46 | return this;
47 | }
48 |
49 | public double getRed() {
50 | return red;
51 | }
52 |
53 | public SerializableColor setRed(double red) {
54 | this.red = red;
55 | return this;
56 | }
57 |
58 | public double getGreen() {
59 | return green;
60 | }
61 |
62 | public SerializableColor setGreen(double green) {
63 | this.green = green;
64 | return this;
65 | }
66 |
67 | public double getBlue() {
68 | return blue;
69 | }
70 |
71 | public SerializableColor setBlue(double blue) {
72 | this.blue = blue;
73 | return this;
74 | }
75 |
76 | public double getAlpha() {
77 | return alpha;
78 | }
79 |
80 | public SerializableColor setAlpha(double alpha) {
81 | this.alpha = alpha;
82 | return this;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/CanvasData.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import com.google.inject.Singleton;
4 | import io.github.stasgora.observetree.Observable;
5 | import io.github.stasgora.observetree.SettableObservable;
6 | import io.github.stasgora.observetree.SettableProperty;
7 | import javafx.scene.image.Image;
8 | import dev.sgora.mesheditor.model.geom.Mesh;
9 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
10 |
11 | @Singleton
12 | public class CanvasData extends Observable {
13 | public final SettableObservable mesh = new SettableObservable<>();
14 | public final Rectangle imageBox = new Rectangle();
15 |
16 | public final SettableProperty baseImage = new SettableProperty<>();
17 | private byte[] rawImageFile;
18 |
19 | CanvasData() {
20 | addSubObservable(mesh);
21 | addSubObservable(baseImage);
22 | addSubObservable(imageBox);
23 | }
24 |
25 | public byte[] getRawImageFile() {
26 | return rawImageFile;
27 | }
28 |
29 | public void setRawImageFile(byte[] rawImageFile) {
30 | this.rawImageFile = rawImageFile;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/CanvasUI.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.MouseConfig;
6 | import dev.sgora.mesheditor.model.geom.Point;
7 | import dev.sgora.mesheditor.view.annotation.MainWindowStage;
8 | import io.github.stasgora.observetree.Observable;
9 | import javafx.beans.property.ObjectProperty;
10 | import javafx.scene.Cursor;
11 | import javafx.stage.Stage;
12 |
13 | @Singleton
14 | public class CanvasUI extends Observable {
15 |
16 | public final Point canvasViewSize = new Point();
17 | public final ObjectProperty canvasMouseCursor;
18 | public final MouseConfig mouseConfig = new MouseConfig();
19 |
20 | @Inject
21 | CanvasUI(@MainWindowStage Stage window) {
22 | this.canvasMouseCursor = window.getScene().cursorProperty();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/LoadState.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import com.google.inject.Singleton;
4 | import dev.sgora.mesheditor.model.observables.SettableList;
5 | import io.github.stasgora.observetree.Observable;
6 | import io.github.stasgora.observetree.SettableProperty;
7 |
8 | import java.io.File;
9 |
10 | @Singleton
11 | public class LoadState extends Observable {
12 |
13 | public final SettableProperty stateSaved = new SettableProperty<>(true);
14 | public final SettableProperty file = new SettableProperty<>();
15 | public final SettableProperty loaded = new SettableProperty<>(false);
16 | public final SettableList recentProjects = new SettableList<>();
17 |
18 | LoadState() {
19 | addSubObservable(stateSaved);
20 | addSubObservable(file);
21 | addSubObservable(loaded);
22 | addSubObservable(recentProjects);
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/MeshLayer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import dev.sgora.mesheditor.model.observables.BindableProperty;
4 |
5 | public class MeshLayer extends PropertyContainer {
6 | public final BindableProperty layerVisible = new BindableProperty<>();
7 | public final BindableProperty layerTransparency = new BindableProperty<>();
8 |
9 | public final BindableProperty polygonsVisible = new BindableProperty<>();
10 |
11 | public final BindableProperty nodesVisible = new BindableProperty<>();
12 | public final BindableProperty nodeRadius = new BindableProperty<>();
13 |
14 | public final BindableProperty edgesVisible = new BindableProperty<>();
15 | public final BindableProperty edgeThickness = new BindableProperty<>();
16 |
17 | MeshLayer() {
18 | scan();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/ModelModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class ModelModule extends AbstractModule { }
6 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/PropertyContainer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import io.github.stasgora.observetree.Observable;
4 | import io.github.stasgora.observetree.SettableProperty;
5 |
6 | import java.io.IOException;
7 | import java.io.ObjectInputStream;
8 | import java.io.ObjectOutputStream;
9 | import java.util.Arrays;
10 | import java.util.List;
11 | import java.util.function.Consumer;
12 | import java.util.logging.Level;
13 | import java.util.logging.Logger;
14 | import java.util.stream.Collectors;
15 |
16 | public class PropertyContainer extends Observable {
17 | private final Logger logger = Logger.getLogger(getClass().getName());
18 |
19 | private List properties;
20 |
21 | public void scan() {
22 | this.properties = scanFields();
23 | properties.forEach(this::addSubObservable);
24 | }
25 |
26 | private List scanFields() {
27 | return Arrays.stream(getClass().getFields()).filter(field -> SettableProperty.class.isAssignableFrom(field.getType())).map(field -> {
28 | try {
29 | return (SettableProperty) field.get(this);
30 | } catch (IllegalAccessException e) {
31 | logger.log(Level.SEVERE, "Resolving property field " + field.getName() + " failed", e);
32 | }
33 | return null;
34 | }).collect(Collectors.toList());
35 | }
36 |
37 | public void writeProperties(ObjectOutputStream out) throws IOException, ClassNotFoundException {
38 | iterateProperties((property, stream) -> stream.writeObject(property.get()), out);
39 | }
40 |
41 | public void readProperties(ObjectInputStream in) throws IOException, ClassNotFoundException {
42 | iterateProperties((property, stream) -> property.set(stream.readObject()), in);
43 | notifyListeners();
44 | }
45 |
46 | public void saveDefaultValues() {
47 | iterateProperties(SettableProperty::saveAsDefaultValue);
48 | }
49 |
50 | public void restoreDefaultValues() {
51 | iterateProperties(SettableProperty::resetToDefaultValue);
52 | }
53 |
54 | private void iterateProperties(StreamAction propertyAction, T param) throws IOException, ClassNotFoundException {
55 | for (SettableProperty property : properties) {
56 | if (property.get() instanceof PropertyContainer)
57 | ((PropertyContainer) property.get()).iterateProperties(propertyAction, param);
58 | else
59 | propertyAction.execute(property, param);
60 | }
61 | }
62 |
63 | private void iterateProperties(Consumer propertyAction) {
64 | for (SettableProperty property : properties) {
65 | if (property.get() instanceof PropertyContainer)
66 | ((PropertyContainer) property.get()).iterateProperties(propertyAction);
67 | else
68 | propertyAction.accept(property);
69 | }
70 | }
71 |
72 | @FunctionalInterface
73 | private interface StreamAction {
74 | void execute(SettableProperty property, T stream) throws IOException, ClassNotFoundException;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/model/project/VisualProperties.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.model.project;
2 |
3 | import com.google.inject.Singleton;
4 | import dev.sgora.mesheditor.model.observables.BindableProperty;
5 | import io.github.stasgora.observetree.SettableObservable;
6 |
7 | @Singleton
8 | public class VisualProperties extends PropertyContainer {
9 | public final BindableProperty meshVisible = new BindableProperty<>();
10 | public final BindableProperty meshTransparency = new BindableProperty<>();
11 |
12 | public final BindableProperty imageVisible = new BindableProperty<>();
13 | public final BindableProperty imageTransparency = new BindableProperty<>();
14 |
15 | public final SettableObservable triangulationLayer = new SettableObservable<>(new MeshLayer());
16 | public final SettableObservable voronoiDiagramLayer = new SettableObservable<>(new MeshLayer());
17 |
18 | VisualProperties() {
19 | scan();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/ConfigModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config;
2 |
3 | import com.google.inject.AbstractModule;
4 | import com.google.inject.Provides;
5 | import com.google.inject.Singleton;
6 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
7 | import dev.sgora.mesheditor.services.config.annotation.AppSettings;
8 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigManager;
9 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
10 |
11 | public class ConfigModule extends AbstractModule {
12 |
13 | @Override
14 | protected void configure() {
15 | bind(LangConfigReader.class).to(JsonLangConfigReader.class);
16 | }
17 |
18 | @AppConfig
19 | @Provides @Singleton
20 | AppConfigReader appConfig() {
21 | return JsonAppConfigReader.forResource("/app.config");
22 | }
23 |
24 | @AppSettings
25 | @Provides @Singleton
26 | AppConfigManager appSettings() {
27 | return JsonAppConfigManager.forFile("config/app.settings");
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/JsonAppConfigManager.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config;
2 |
3 | import dev.sgora.mesheditor.model.config.JsonConfig;
4 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigManager;
5 | import org.json.JSONArray;
6 | import org.json.JSONObject;
7 | import dev.sgora.mesheditor.model.config.JsonFileConfig;
8 |
9 | import java.io.*;
10 | import java.util.List;
11 | import java.util.logging.Level;
12 | import java.util.logging.Logger;
13 |
14 | public class JsonAppConfigManager extends JsonAppConfigReader implements AppConfigManager {
15 | protected final Logger logger = Logger.getLogger(JsonAppConfigManager.class.getName());
16 |
17 | public JsonAppConfigManager(JsonConfig config) {
18 | super(config);
19 | }
20 |
21 | @Override
22 | public void setDouble(String keyPath, double value) {
23 | setValue(config, keyPath, value);
24 | }
25 |
26 | @Override
27 | public void setString(String keyPath, String value) {
28 | setValue(config, keyPath, value);
29 | }
30 |
31 | @Override
32 | public void setInt(String keyPath, int value) {
33 | setValue(config, keyPath, value);
34 | }
35 |
36 | @Override
37 | public void setBool(String keyPath, boolean value) {
38 | setValue(config, keyPath, value);
39 | }
40 |
41 | @Override
42 | public void setStringList(String keyPath, List list) {
43 | setValue(config, keyPath, new JSONArray(list));
44 | }
45 |
46 | @Override
47 | public void setJsonObject(String keyPath, JSONObject object) {
48 | setValue(config, keyPath, object);
49 | }
50 |
51 | private void saveConfig() {
52 | if(!(config instanceof JsonFileConfig))
53 | return;
54 | try(FileWriter writer = new FileWriter(((JsonFileConfig) config).configFile)) {
55 | writer.write(config.config.toString(4).replaceAll(" {4}", "\t"));
56 | } catch (IOException e) {
57 | logger.log(Level.SEVERE, "Saving config failed", e);
58 | }
59 | }
60 |
61 | protected void setValue(JsonConfig config, String keyPath, T value) {
62 | JSONObject parent = getParent(config, keyPath);
63 | String lastKey = getLastKey(keyPath);
64 | parent.put(lastKey, value);
65 | saveConfig();
66 | }
67 |
68 | static JsonAppConfigManager forFile(String fileName) {
69 | try (InputStream input = new FileInputStream(new File(fileName))) {
70 | return new JsonAppConfigManager(createJsonConfig(input, fileName, true));
71 | } catch (IOException e) {
72 | LOGGER.log(Level.SEVERE, "Failed creating Config manager for resource '" + fileName + "'", e);
73 | }
74 | return null;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/JsonAppConfigReader.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config;
2 |
3 | import dev.sgora.mesheditor.model.config.JsonConfig;
4 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
5 | import org.json.JSONArray;
6 | import org.json.JSONObject;
7 |
8 | import java.util.List;
9 | import java.util.logging.Logger;
10 |
11 | class JsonAppConfigReader extends JsonConfigReader implements AppConfigReader {
12 | protected static final Logger LOGGER = Logger.getLogger(JsonAppConfigReader.class.getName());
13 |
14 | protected JsonConfig config;
15 |
16 | protected JsonAppConfigReader(JsonConfig config) {
17 | this.config = config;
18 | }
19 |
20 | @Override
21 | public double getDouble(String keyPath) {
22 | return this.getValue(config, keyPath, JSONObject::optDouble);
23 | }
24 |
25 | @Override
26 | public String getString(String keyPath) {
27 | return this.getValue(config, keyPath, JSONObject::optString);
28 | }
29 |
30 | @Override
31 | public int getInt(String keyPath) {
32 | return this.getValue(config, keyPath, JSONObject::optInt);
33 | }
34 |
35 | @Override
36 | public boolean getBool(String keyPath) {
37 | return this.getValue(config, keyPath, JSONObject::optBoolean);
38 | }
39 |
40 | @Override
41 | public List getStringList(String keyPath) {
42 | return this.getList(config, keyPath, JSONArray::getString);
43 | }
44 |
45 | @Override
46 | public JSONObject getJsonObject(String keyPath) {
47 | return getJsonObject(config, keyPath);
48 | }
49 |
50 | static JsonAppConfigReader forResource(String fileName) {
51 | return new JsonAppConfigReader(loadJsonConfig(fileName));
52 | }
53 |
54 | @Override
55 | public boolean containsPath(String keyPath) {
56 | return containsPath(config, keyPath);
57 | }
58 |
59 | @Override
60 | public AppConfigReader opt() {
61 | disableLoggingForRead = true;
62 | return this;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/JsonConfigReader.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config;
2 |
3 | import dev.sgora.mesheditor.model.config.JsonConfig;
4 | import dev.sgora.mesheditor.model.config.JsonFileConfig;
5 | import org.json.JSONArray;
6 | import org.json.JSONException;
7 | import org.json.JSONObject;
8 | import org.json.JSONTokener;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.util.ArrayList;
14 | import java.util.Arrays;
15 | import java.util.Collections;
16 | import java.util.List;
17 | import java.util.function.BiFunction;
18 | import java.util.logging.Level;
19 | import java.util.logging.Logger;
20 |
21 | public abstract class JsonConfigReader {
22 | private static final Logger LOGGER = Logger.getLogger(JsonConfigReader.class.getName());
23 |
24 | protected boolean disableLoggingForRead;
25 |
26 | protected T getValue(JsonConfig config, String keyPath, BiFunction getValue) {
27 | JSONObject parent = getParent(config, keyPath);
28 | String lastKey = getLastKey(keyPath);
29 | if (!disableLoggingForRead && !parent.has(lastKey))
30 | logInvalidKey(config.name, lastKey, keyPath);
31 | disableLoggingForRead = false;
32 | return getValue.apply(parent, lastKey);
33 | }
34 |
35 | protected List getList(JsonConfig config, String keyPath, BiFunction getValue) {
36 | JSONObject parent = getParent(config, keyPath);
37 | String lastKey = getLastKey(keyPath);
38 | if (!disableLoggingForRead && !parent.has(lastKey))
39 | logInvalidKey(config.name, lastKey, keyPath);
40 | disableLoggingForRead = false;
41 | JSONArray jsonArray = parent.optJSONArray(lastKey);
42 | if(jsonArray == null)
43 | return Collections.emptyList();
44 | List list = new ArrayList<>(jsonArray.length());
45 | for (int i = 0; i < jsonArray.length(); i++) {
46 | list.add(getValue.apply(jsonArray, i));
47 | }
48 | return list;
49 | }
50 |
51 | protected boolean containsPath(JsonConfig config, String keyPath) {
52 | JSONObject object = config.config;
53 | String[] keyChain = getKeyChain(keyPath);
54 | for (int i = 0; i < keyChain.length; i++) {
55 | if (!object.has(keyChain[i])) {
56 | return false;
57 | }
58 | if (i < keyChain.length - 1) {
59 | object = object.getJSONObject(keyChain[i]);
60 | }
61 | }
62 | return true;
63 | }
64 |
65 | protected String getLastKey(String keyPath) {
66 | String[] path = getKeyChain(keyPath);
67 | return path.length > 0 ? path[path.length - 1] : keyPath;
68 | }
69 |
70 | private String[] getKeyChain(String keyPath) {
71 | return keyPath.split("\\.");
72 | }
73 |
74 | protected JSONObject getParent(JsonConfig config, String keyPath) {
75 | List keys = Arrays.asList(getKeyChain(keyPath));
76 | if (keys.size() <= 1) {
77 | return config.config;
78 | }
79 | return getJsonObject(config, String.join(".", keys.subList(0, keys.size() - 1)));
80 | }
81 |
82 | JSONObject getJsonObject(JsonConfig config, String keyPath) {
83 | String[] keys = getKeyChain(keyPath);
84 | JSONObject parent = config.config;
85 |
86 | for (String key : keys) {
87 | try {
88 | parent = parent.getJSONObject(key);
89 | } catch (JSONException e) {
90 | logInvalidKey(config.name, key, String.join(".", keys));
91 | }
92 | }
93 | return parent;
94 | }
95 |
96 | protected void logInvalidKey(String configName, String key, String keyPath) {
97 | LOGGER.log(Level.SEVERE, () -> String.format("Failed reading '%s' config property '%s' from path '%s'", configName, key, keyPath));
98 | }
99 |
100 | protected static JsonConfig createJsonConfig(InputStream inputStream, String fileName, boolean isFile) {
101 | JSONObject config = new JSONObject(new JSONTokener(inputStream));
102 | if(isFile)
103 | return new JsonFileConfig(new File(fileName), config);
104 | return new JsonConfig(fileName, config);
105 | }
106 |
107 | protected static JsonConfig loadJsonConfig(String fileName) {
108 | try (InputStream input = JsonAppConfigReader.class.getResourceAsStream(fileName)) {
109 | return createJsonConfig(input, fileName, false);
110 | } catch (IOException e) {
111 | LOGGER.log(Level.SEVERE, "Failed creating Config Reader for resource '" + fileName + "'", e);
112 | }
113 | return null;
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/JsonLangConfigReader.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.NamespaceMap;
6 | import dev.sgora.mesheditor.model.config.JsonConfig;
7 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
8 | import dev.sgora.mesheditor.services.config.annotation.AppSettings;
9 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigManager;
10 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
11 | import org.json.JSONArray;
12 | import org.json.JSONObject;
13 |
14 | import java.util.ArrayList;
15 | import java.util.Collections;
16 | import java.util.List;
17 | import java.util.logging.Level;
18 | import java.util.logging.Logger;
19 |
20 | @Singleton
21 | class JsonLangConfigReader extends JsonConfigReader implements LangConfigReader {
22 |
23 | private static final Logger LOGGER = Logger.getLogger(JsonLangConfigReader.class.getName());
24 |
25 | private final AppConfigReader appConfig;
26 | private final AppConfigReader appSettings;
27 | private final NamespaceMap namespaceMap;
28 |
29 | private List configList = new ArrayList<>();
30 |
31 | private static final String FXML_TREE_PREFIX = "fxml";
32 |
33 | @Inject
34 | JsonLangConfigReader(@AppConfig AppConfigReader appConfig, @AppSettings AppConfigManager appSettings, NamespaceMap namespaceMap) {
35 | this.appConfig = appConfig;
36 | this.appSettings = appSettings;
37 | this.namespaceMap = namespaceMap;
38 |
39 | configList.add(loadJsonConfig(getLangFileName(appConfig.getString("default.language"))));
40 | }
41 |
42 | @Override
43 | public String getText(String keyPath) {
44 | for (JsonConfig config : configList) {
45 | if (containsPath(config, keyPath)) {
46 | return getValue(config, keyPath, JSONObject::optString);
47 | }
48 | }
49 | logMissingKey(keyPath);
50 | return "";
51 | }
52 |
53 | @Override
54 | public List getMultipartText(String keyPath) {
55 | for (JsonConfig config : configList) {
56 | if (containsPath(config, keyPath)) {
57 | return getList(config, keyPath, JSONArray::optString);
58 | }
59 | }
60 | logMissingKey(keyPath);
61 | return Collections.emptyList();
62 | }
63 |
64 | @Override
65 | public void onSetMainLanguage() {
66 | populateLanguageList();
67 | populateFXMLNamespace();
68 | }
69 |
70 | @Override
71 | public boolean containsPath(String keyPath) {
72 | for (JsonConfig config : configList) {
73 | if (containsPath(config, keyPath)) {
74 | return true;
75 | }
76 | }
77 | return false;
78 | }
79 |
80 | private void populateLanguageList() {
81 | configList.subList(0, configList.size() - 1).clear();
82 | String defLang = appConfig.getString("default.language");
83 | String mainLang = appSettings.getString("settings.language");
84 | if (mainLang.equals(defLang)) {
85 | return;
86 | }
87 | if (mainLang.contains("_")) {
88 | String generalizedLang = mainLang.split("_")[0];
89 | if (!generalizedLang.equals(defLang)) {
90 | configList.add(0, loadJsonConfig(getLangFileName(generalizedLang)));
91 | }
92 | }
93 | configList.add(0, loadJsonConfig(getLangFileName(mainLang)));
94 | }
95 |
96 | private void populateFXMLNamespace() {
97 | //iterate over default language property tree
98 | JSONObject root = configList.get(configList.size() - 1).config.getJSONObject(FXML_TREE_PREFIX);
99 | scanChildren("", root);
100 | }
101 |
102 | private void scanChildren(String keyPath, JSONObject current) {
103 | for (String key : current.keySet()) {
104 | Object child = current.get(key);
105 | String childKey = keyPath.isEmpty() ? key : keyPath + "." + key;
106 | if (child instanceof JSONObject) {
107 | scanChildren(childKey, (JSONObject) child);
108 | } else {
109 | String viewName = keyPath.split("\\.")[0];
110 | namespaceMap.get(viewName).put(childKey.replace('.', '_'), getText(FXML_TREE_PREFIX + "." + childKey));
111 | }
112 | }
113 | }
114 |
115 | private void logMissingKey(String keyPath) {
116 | String languages = String.join(" -> ", configList.stream().map(item -> item.name).toArray(String[]::new));
117 | LOGGER.log(Level.SEVERE, () -> String.format("Failed reading language property (%s) from path '%s'", languages, keyPath));
118 | }
119 |
120 | private String getLangFileName(String lang) {
121 | return "/i18n/" + lang + '.' + appConfig.getString("extension.localization");
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/LangConfigReader.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config;
2 |
3 | import java.util.List;
4 |
5 | public interface LangConfigReader {
6 |
7 | String getText(String keyPath);
8 |
9 | List getMultipartText(String keyPath);
10 |
11 | void onSetMainLanguage();
12 |
13 | boolean containsPath(String keyPath);
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/annotation/AppConfig.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config.annotation;
2 |
3 | import com.google.inject.BindingAnnotation;
4 |
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.Target;
7 |
8 | import static java.lang.annotation.ElementType.FIELD;
9 | import static java.lang.annotation.ElementType.METHOD;
10 | import static java.lang.annotation.ElementType.PARAMETER;
11 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
12 |
13 | @BindingAnnotation
14 | @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
15 | public @interface AppConfig { }
16 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/annotation/AppSettings.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config.annotation;
2 | import com.google.inject.BindingAnnotation;
3 |
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.Target;
6 |
7 | import static java.lang.annotation.ElementType.FIELD;
8 | import static java.lang.annotation.ElementType.METHOD;
9 | import static java.lang.annotation.ElementType.PARAMETER;
10 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
11 |
12 | @BindingAnnotation
13 | @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
14 | public @interface AppSettings { }
15 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/interfaces/AppConfigManager.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config.interfaces;
2 |
3 | public interface AppConfigManager extends AppConfigReader, AppConfigWriter { }
4 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/interfaces/AppConfigReader.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config.interfaces;
2 |
3 | import org.json.JSONObject;
4 |
5 | import java.util.List;
6 |
7 | public interface AppConfigReader {
8 |
9 | double getDouble(String keyPath);
10 |
11 | String getString(String keyPath);
12 |
13 | int getInt(String keyPath);
14 |
15 | boolean getBool(String keyPath);
16 |
17 | List getStringList(String keyPath);
18 |
19 | JSONObject getJsonObject(String keyPath);
20 |
21 | boolean containsPath(String keyPath);
22 |
23 | AppConfigReader opt();
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/config/interfaces/AppConfigWriter.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.config.interfaces;
2 |
3 | import org.json.JSONObject;
4 |
5 | import java.util.List;
6 |
7 | public interface AppConfigWriter {
8 |
9 | void setDouble(String keyPath, double value);
10 |
11 | void setString(String keyPath, String value);
12 |
13 | void setInt(String keyPath, int value);
14 |
15 | void setBool(String keyPath, boolean value);
16 |
17 | void setStringList(String keyPath, List list);
18 |
19 | void setJsonObject(String keyPath, JSONObject object);
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/drawing/ColorUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.drawing;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import io.github.stasgora.observetree.SettableProperty;
6 | import javafx.scene.image.Image;
7 | import javafx.scene.paint.Color;
8 | import dev.sgora.mesheditor.model.geom.Point;
9 | import dev.sgora.mesheditor.model.paint.SerializableColor;
10 | import dev.sgora.mesheditor.model.project.CanvasData;
11 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
12 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
13 | import dev.sgora.mesheditor.services.mesh.generation.NodeUtils;
14 |
15 | import java.util.ArrayList;
16 | import java.util.List;
17 | import java.util.stream.Collectors;
18 | import java.util.stream.IntStream;
19 |
20 | @Singleton
21 | public class ColorUtils {
22 |
23 | private final NodeUtils nodeUtils;
24 | private final SettableProperty baseImage;
25 | private final AppConfigReader appConfig;
26 |
27 | private static final Color OUTSIDE_IMAGE_COLOR = Color.WHITE;
28 |
29 | @Inject
30 | ColorUtils(NodeUtils nodeUtils, CanvasData canvasData, @AppConfig AppConfigReader appConfig) {
31 | this.nodeUtils = nodeUtils;
32 | this.baseImage = canvasData.baseImage;
33 | this.appConfig = appConfig;
34 | }
35 |
36 | public SerializableColor getPolygonColor(Point[] polygon) {
37 | return getAverageNodeColor(getPolygonSamplePoints(polygon));
38 | }
39 |
40 | public SerializableColor getEdgeColor(Point first, Point second) {
41 | int subdivisions = appConfig.getInt("meshBox.edgeColorSamples");
42 | Point vector = new Point(second).subtract(first);
43 | List nodes = new ArrayList<>();
44 | for (int i = 0; i <= subdivisions; i++) {
45 | nodes.add(new Point(first).add(new Point(vector).multiplyByScalar(i / (double) subdivisions)));
46 | }
47 | return getAverageNodeColor(nodes);
48 | }
49 |
50 | public SerializableColor getNodeColor(Point node) {
51 | node = nodeUtils.proportionalToPixelPos(node);
52 | Point pixelImgSize = new Point(baseImage.get().getWidth(), baseImage.get().getHeight());
53 | Color color = node.isBetween(new Point(), pixelImgSize) ? baseImage.get().getPixelReader().getColor((int) node.getX(), (int) node.getY()) : OUTSIDE_IMAGE_COLOR;
54 | return new SerializableColor(color);
55 | }
56 |
57 | private SerializableColor getAverageNodeColor(List points) {
58 | List colors = points.stream().map(this::getNodeColor).collect(Collectors.toList());
59 | double r = colors.stream().mapToDouble(color -> color.getRed()).average().orElse(255);
60 | double g = colors.stream().mapToDouble(color -> color.getGreen()).average().orElse(255);
61 | double b = colors.stream().mapToDouble(color -> color.getBlue()).average().orElse(255);
62 | return new SerializableColor(r, g, b, 1);
63 | }
64 |
65 | private List getPolygonSamplePoints(Point[] vertices) {
66 | List points = new ArrayList<>();
67 | int subdivisions = appConfig.getInt("meshBox.edgeColorSamples");
68 | for (int i = 0; i < vertices.length; i++) {
69 | points.add(new Point(vertices[i]));
70 | points.addAll(subdivideSegment(vertices[(i + 1) % vertices.length], vertices[i], subdivisions));
71 | }
72 | int margin = subdivisions + 2;
73 | for (int i = points.size() - margin; i >= margin; i--)
74 | points.addAll(subdivideSegment(points.get(0), points.get(i), subdivisions));
75 | return points;
76 | }
77 |
78 | private List subdivideSegment(Point a, Point b, int subdivisions) {
79 | Point baseVector = new Point(b).subtract(a);
80 | return IntStream.rangeClosed(1, subdivisions).mapToObj(i -> new Point(baseVector).multiplyByScalar(i / (double) (subdivisions + 1)).add(a)).collect(Collectors.toList());
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/drawing/DrawingModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.drawing;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class DrawingModule extends AbstractModule {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/drawing/ImageBox.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.drawing;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import io.github.stasgora.observetree.SettableProperty;
6 | import javafx.scene.Cursor;
7 | import javafx.scene.image.Image;
8 | import javafx.scene.input.MouseButton;
9 | import dev.sgora.mesheditor.model.geom.Point;
10 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
11 | import dev.sgora.mesheditor.model.project.CanvasData;
12 | import dev.sgora.mesheditor.model.project.CanvasUI;
13 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
14 | import dev.sgora.mesheditor.services.config.annotation.AppSettings;
15 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigManager;
16 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
17 | import dev.sgora.mesheditor.services.input.MouseListener;
18 |
19 | @Singleton
20 | public class ImageBox implements MouseListener {
21 |
22 | private Point lastCanvasSize;
23 | private double zoom = 1;
24 |
25 | private final Point canvasViewSize;
26 | private final CanvasData canvasData;
27 | private final AppConfigReader appConfig;
28 | private final AppConfigReader appSettings;
29 | private CanvasUI canvasUI;
30 |
31 | @Inject
32 | ImageBox(CanvasData canvasData, CanvasUI canvasUI, @AppConfig AppConfigReader appConfig, @AppSettings AppConfigManager appSettings) {
33 | this.canvasData = canvasData;
34 | this.appConfig = appConfig;
35 | this.appSettings = appSettings;
36 | this.canvasUI = canvasUI;
37 | canvasViewSize = canvasUI.canvasViewSize;
38 | }
39 |
40 | public void onResizeCanvas() {
41 | if (canvasData.baseImage.get() != null) {
42 | return;
43 | }
44 | if (lastCanvasSize == null) {
45 | lastCanvasSize = new Point(canvasViewSize);
46 | return;
47 | }
48 | Point sizeDiff = new Point(canvasViewSize).subtract(lastCanvasSize);
49 | canvasData.imageBox.getPosition().add(sizeDiff.divideByScalar(2));
50 | lastCanvasSize.set(canvasViewSize);
51 | }
52 |
53 | public void calcImageBox() {
54 | if (canvasData.baseImage.get() == null) {
55 | return;
56 | }
57 | SettableProperty baseImage = canvasData.baseImage;
58 | double imgRatio = baseImage.get().getWidth() / baseImage.get().getHeight();
59 |
60 | Point canvasSize = canvasViewSize;
61 | double defBorder = appConfig.getDouble("imageBox.defaultBorder");
62 | if (imgRatio > canvasSize.getX() / canvasSize.getY()) {
63 | double imgWidth = canvasSize.getX() * (1 - defBorder);
64 | double imgHeight = imgWidth / imgRatio;
65 | canvasData.imageBox.getPosition().set(canvasSize.getX() * defBorder * 0.5, (canvasSize.getY() - imgHeight) / 2);
66 | canvasData.imageBox.getSize().set(imgWidth, imgHeight);
67 | } else {
68 | double imgHeight = canvasSize.getY() * (1 - defBorder);
69 | double imgWidth = imgRatio * imgHeight;
70 | canvasData.imageBox.getPosition().set((canvasSize.getX() - imgWidth) / 2, canvasSize.getY() * defBorder * 0.5);
71 | canvasData.imageBox.getSize().set(imgWidth, imgHeight);
72 | }
73 | canvasData.imageBox.notifyListeners();
74 | }
75 |
76 | public boolean onZoom(double amount, Point mousePos) {
77 | double minZoom = appConfig.getDouble("imageBox.zoom.min");
78 | double maxZoom = appConfig.getDouble("imageBox.zoom.max");
79 |
80 | Point baseImageSize = new Point(canvasData.baseImage.get().getWidth(), canvasData.baseImage.get().getHeight());
81 | double zoomFactor = 1 - amount * appSettings.getInt("settings.imageBox.zoom.dir") * appSettings.getDouble("settings.imageBox.zoom.speed");
82 | zoom = Math.max(minZoom, Math.min(maxZoom, zoom * zoomFactor));
83 |
84 | double moveFactor = 1 - baseImageSize.getX() * zoom / canvasData.imageBox.getSize().getX();
85 | Point zoomPos = new Point(mousePos).subtract(canvasData.imageBox.getPosition()).multiplyByScalar(moveFactor);
86 |
87 | canvasData.imageBox.getPosition().add(zoomPos);
88 | canvasData.imageBox.getSize().set(new Point(baseImageSize).multiplyByScalar(zoom));
89 | canvasData.notifyListeners();
90 | return true;
91 | }
92 |
93 | @Override
94 | public boolean onDragStart(Point mousePos, MouseButton button) {
95 | if (button == canvasUI.mouseConfig.dragImageButton) {
96 | canvasUI.canvasMouseCursor.setValue(Cursor.CLOSED_HAND);
97 | return true;
98 | }
99 | return false;
100 | }
101 |
102 | @Override
103 | public void onMouseDrag(Point dragAmount, Point mousePos, MouseButton button) {
104 | if (button != canvasUI.mouseConfig.dragImageButton) {
105 | return;
106 | }
107 | Rectangle imageBox = this.canvasData.imageBox;
108 | imageBox.getPosition().add(dragAmount).clamp(new Point(imageBox.getSize()).multiplyByScalar(-1), canvasViewSize);
109 | imageBox.notifyListeners();
110 | }
111 |
112 | @Override
113 | public void onDragEnd(Point mousePos, MouseButton button) {
114 | canvasUI.canvasMouseCursor.setValue(mousePos.isBetween(new Point(), canvasViewSize) ? canvasUI.mouseConfig.defaultCanvasCursor : Cursor.DEFAULT);
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/drawing/MeshBox.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.drawing;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.services.history.actions.node.AddNodeAction;
6 | import dev.sgora.mesheditor.services.history.actions.node.MoveNodeAction;
7 | import dev.sgora.mesheditor.services.history.actions.node.RemoveNodeAction;
8 | import io.github.stasgora.observetree.SettableObservable;
9 | import javafx.scene.Cursor;
10 | import javafx.scene.input.MouseButton;
11 | import dev.sgora.mesheditor.model.geom.Mesh;
12 | import dev.sgora.mesheditor.model.geom.Point;
13 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
14 | import dev.sgora.mesheditor.model.project.CanvasData;
15 | import dev.sgora.mesheditor.model.project.CanvasUI;
16 | import dev.sgora.mesheditor.services.history.ActionHistoryService;
17 | import dev.sgora.mesheditor.services.input.MouseListener;
18 | import dev.sgora.mesheditor.services.mesh.generation.NodeUtils;
19 | import dev.sgora.mesheditor.services.mesh.generation.TriangulationService;
20 |
21 | @Singleton
22 | public class MeshBox implements MouseListener {
23 |
24 | private Point draggedNode;
25 | private Point draggedNodeStartPosition;
26 |
27 | private final SettableObservable mesh;
28 | private CanvasUI canvasUI;
29 | private final TriangulationService triangulationService;
30 | private final NodeUtils nodeUtils;
31 | private final ActionHistoryService actionHistoryService;
32 |
33 | @Inject
34 | MeshBox(CanvasData canvasData, CanvasUI canvasUI, TriangulationService triangulationService, NodeUtils nodeUtils, ActionHistoryService actionHistoryService) {
35 | this.mesh = canvasData.mesh;
36 | this.canvasUI = canvasUI;
37 | this.triangulationService = triangulationService;
38 | this.nodeUtils = nodeUtils;
39 | this.actionHistoryService = actionHistoryService;
40 | }
41 |
42 | private Point clampCanvasSpaceNodePos(Point node) {
43 | Rectangle box = nodeUtils.getCanvasSpaceNodeBoundingBox();
44 | return node.clamp(box.getPosition(), new Point(box.getPosition()).add(box.getSize()));
45 | }
46 |
47 | public void onMouseMove(Point mousePos) {
48 | Point proportionalPos = nodeUtils.canvasToProportionalPos(mousePos);
49 | draggedNode = triangulationService.findNodeByLocation(proportionalPos);
50 | canvasUI.canvasMouseCursor.setValue(draggedNode != null ? Cursor.HAND : canvasUI.mouseConfig.defaultCanvasCursor);
51 | }
52 |
53 | @Override
54 | public boolean onDragStart(Point mousePos, MouseButton mouseButton) {
55 | Point proportionalPos = nodeUtils.canvasToProportionalPos(mousePos);
56 | if (mouseButton == canvasUI.mouseConfig.removeNodeButton && triangulationService.removeNode(proportionalPos)) {
57 | actionHistoryService.registerAction(new RemoveNodeAction(proportionalPos.getX(), proportionalPos.getY()));
58 | return true;
59 | }
60 | if (mouseButton == canvasUI.mouseConfig.moveNodeButton && draggedNode != null) {
61 | canvasUI.canvasMouseCursor.setValue(Cursor.CLOSED_HAND);
62 | draggedNodeStartPosition = new Point(draggedNode);
63 | return true;
64 | }
65 | return mouseButton == canvasUI.mouseConfig.placeNodeButton;
66 | }
67 |
68 | @Override
69 | public void onMouseDrag(Point dragAmount, Point mousePos, MouseButton button) {
70 | dragPoint(mousePos, button, false);
71 | }
72 |
73 | @Override
74 | public void onDragEnd(Point mousePos, MouseButton mouseButton) {
75 | dragPoint(mousePos, mouseButton, true);
76 | Point point = null;
77 | if (draggedNode == null && mouseButton == canvasUI.mouseConfig.placeNodeButton && nodeUtils.getCanvasSpaceNodeBoundingBox().contains(mousePos)) {
78 | point = nodeUtils.canvasToProportionalPos(mousePos);
79 | if (triangulationService.addNode(point))
80 | actionHistoryService.registerAction(new AddNodeAction(point.getX(), point.getY()));
81 | }
82 | draggedNode = point;
83 | canvasUI.canvasMouseCursor.setValue(mousePos.isBetween(new Point(), canvasUI.canvasViewSize) ? Cursor.HAND : Cursor.DEFAULT);
84 | }
85 |
86 | private void dragPoint(Point mousePos, MouseButton button, boolean dragFinished) {
87 | if (draggedNode == null || button != canvasUI.mouseConfig.moveNodeButton) {
88 | return;
89 | }
90 | Point targetPos = nodeUtils.canvasToProportionalPos(clampCanvasSpaceNodePos(mousePos.clamp(canvasUI.canvasViewSize)));
91 | Point newPos = triangulationService.moveNode(draggedNode, targetPos);
92 | if (dragFinished)
93 | actionHistoryService.registerAction(new MoveNodeAction(newPos, draggedNodeStartPosition));
94 | mesh.get().notifyListeners();
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/ConfigModelMapper.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import io.github.stasgora.observetree.Observable;
6 | import io.github.stasgora.observetree.SettableProperty;
7 | import org.json.JSONObject;
8 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
9 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
10 |
11 | import java.lang.reflect.Field;
12 | import java.lang.reflect.InvocationTargetException;
13 | import java.lang.reflect.Method;
14 | import java.lang.reflect.ParameterizedType;
15 | import java.util.logging.Level;
16 | import java.util.logging.Logger;
17 |
18 | @Singleton
19 | public class ConfigModelMapper {
20 | private static final Logger LOGGER = Logger.getLogger(ConfigModelMapper.class.getName());
21 |
22 | private AppConfigReader appConfig;
23 |
24 | @Inject
25 | ConfigModelMapper(@AppConfig AppConfigReader appConfig) {
26 | this.appConfig = appConfig;
27 | }
28 |
29 | public void map(Object model, String configPath) {
30 | JSONObject configPathRoot = appConfig.getJsonObject(configPath);
31 | for (String propertyKey : configPathRoot.keySet()) {
32 | Field modelField = getModelField(model, propertyKey);
33 | if (modelField == null)
34 | continue;
35 | Class modelFieldType = modelField.getType();
36 | boolean isFieldSettable = SettableProperty.class.isAssignableFrom(modelFieldType);
37 | Object modelFieldValue = getModelFieldValue(model, modelField);
38 | if (modelFieldValue == null)
39 | continue;
40 | SettableProperty modelSettableField = null;
41 | if (isFieldSettable) {
42 | modelSettableField = (SettableProperty) modelFieldValue;
43 | modelFieldValue = modelSettableField.get();
44 | modelFieldType = (Class) ((ParameterizedType) modelField.getGenericType()).getActualTypeArguments()[0];
45 | }
46 | Object configFieldValue = configPathRoot.get(propertyKey);
47 | if (configFieldValue instanceof JSONObject) {
48 | map(modelFieldValue, configPath + "." + propertyKey);
49 | if (isFieldSettable)
50 | callSettableOnValueChanged(modelSettableField);
51 | } else {
52 | Object fieldValue = getPrimitiveConfigFieldValue(configFieldValue, modelFieldType);
53 | if (isFieldSettable)
54 | modelSettableField.set(fieldValue);
55 | else
56 | setModelField(modelField, model, fieldValue);
57 | }
58 | }
59 | }
60 |
61 | private Object getPrimitiveConfigFieldValue(Object configFieldValue, Class modelFieldType) {
62 | if (modelFieldType.equals(Double.class) && configFieldValue.getClass().equals(Integer.class)) {
63 | return ((Integer) configFieldValue).doubleValue();
64 | }
65 | if (modelFieldType.isEnum() && configFieldValue.getClass().equals(String.class)) {
66 | return Enum.valueOf(modelFieldType, (String) configFieldValue);
67 | }
68 | return configFieldValue;
69 | }
70 |
71 | private void callSettableOnValueChanged(SettableProperty object) {
72 | try {
73 | Method method = Observable.class.getDeclaredMethod("onValueChanged");
74 | method.setAccessible(true);
75 | method.invoke(object);
76 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
77 | LOGGER.log(Level.WARNING, "Calling settable onValueChanged failed ", e);
78 | }
79 | }
80 |
81 | private Field getModelField(Object modelObject, String fieldName) {
82 | try {
83 | return modelObject.getClass().getDeclaredField(fieldName);
84 | } catch (NoSuchFieldException e) {
85 | LOGGER.log(Level.WARNING, "Unmappable config field " + fieldName, e);
86 | return null;
87 | }
88 | }
89 |
90 | private void setModelField(Field modelField, Object model, Object fieldValue) {
91 | try {
92 | modelField.set(model, fieldValue);
93 | } catch (IllegalAccessException e) {
94 | LOGGER.log(Level.WARNING, "Setting model field value failed", e);
95 | }
96 | }
97 |
98 | private Object getModelFieldValue(Object model, Field modelField) {
99 | Object modelFieldValue;
100 | try {
101 | modelFieldValue = modelField.get(model);
102 | } catch (IllegalAccessException e) {
103 | LOGGER.log(Level.SEVERE, "Getting model field failed", e);
104 | return null;
105 | }
106 | return modelFieldValue;
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/FileIOModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class FileIOModule extends AbstractModule {
6 | @Override
7 | protected void configure() {
8 | bind(FileUtils.class).to(ProjectFileUtils.class);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/FileUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 |
6 | public interface FileUtils {
7 |
8 | void save(File location) throws ProjectIOException;
9 |
10 | void load(File location) throws ProjectIOException;
11 |
12 | void loadImage(FileInputStream fileStream) throws ProjectIOException;
13 |
14 | byte[] readFileIntoMemory(FileInputStream fileStream) throws ProjectIOException;
15 |
16 | File getProjectFileWithExtension(File projectFile);
17 |
18 | File getFileWithExtension(File file, String extension);
19 |
20 | String getProjectFileName(File projectFile);
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/ProjectFileUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import javafx.scene.image.Image;
6 | import dev.sgora.mesheditor.model.geom.Mesh;
7 | import dev.sgora.mesheditor.model.project.CanvasData;
8 | import dev.sgora.mesheditor.model.project.VisualProperties;
9 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
10 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
11 |
12 | import java.io.*;
13 |
14 | @Singleton
15 | class ProjectFileUtils implements FileUtils {
16 |
17 | private final CanvasData canvasData;
18 | private final AppConfigReader appConfig;
19 | private final VisualProperties visualProperties;
20 |
21 | @Inject
22 | ProjectFileUtils(CanvasData canvasData, @AppConfig AppConfigReader appConfig, VisualProperties visualProperties) {
23 | this.canvasData = canvasData;
24 | this.appConfig = appConfig;
25 | this.visualProperties = visualProperties;
26 | }
27 |
28 | @Override
29 | public void save(File location) throws ProjectIOException {
30 | try {
31 | location.createNewFile();
32 | try (FileOutputStream fileStream = new FileOutputStream(location, false);
33 | ObjectOutputStream objectStream = new ObjectOutputStream(fileStream)) {
34 | objectStream.writeObject(canvasData.mesh.get());
35 | visualProperties.writeProperties(objectStream);
36 | fileStream.write(canvasData.getRawImageFile());
37 | }
38 | } catch (IOException | ClassNotFoundException e) {
39 | throw new ProjectIOException(e);
40 | }
41 | }
42 |
43 | @Override
44 | public void load(File location) throws ProjectIOException {
45 | try (FileInputStream fileStream = new FileInputStream(location);
46 | ObjectInputStream objectStream = new ObjectInputStream(fileStream)) {
47 | canvasData.mesh.set((Mesh) objectStream.readObject());
48 | visualProperties.readProperties(objectStream);
49 | loadImage(fileStream);
50 | } catch (IOException | ClassNotFoundException e) {
51 | throw new ProjectIOException(e);
52 | }
53 | }
54 |
55 | @Override
56 | public void loadImage(FileInputStream fileStream) throws ProjectIOException {
57 | canvasData.setRawImageFile(readFileIntoMemory(fileStream));
58 | try (ByteArrayInputStream imageStream = new ByteArrayInputStream(canvasData.getRawImageFile())) {
59 | canvasData.baseImage.set(new Image(imageStream));
60 | } catch (IOException e) {
61 | throw new ProjectIOException(e);
62 | }
63 | }
64 |
65 | @Override
66 | public byte[] readFileIntoMemory(FileInputStream fileStream) throws ProjectIOException {
67 | try (ByteArrayOutputStream imageStream = new ByteArrayOutputStream()) {
68 | byte[] buffer = new byte[4096];
69 | int read;
70 | while ((read = fileStream.read(buffer)) != -1) {
71 | imageStream.write(buffer, 0, read);
72 | }
73 | return imageStream.toByteArray();
74 | } catch (IOException e) {
75 | throw new ProjectIOException(e);
76 | }
77 | }
78 |
79 | @Override
80 | public File getProjectFileWithExtension(File projectFile) {
81 | return getFileWithExtension(projectFile, appConfig.getString("extension.project"));
82 | }
83 |
84 | @Override
85 | public File getFileWithExtension(File file, String extension) {
86 | String projectExtension = "." + extension;
87 | if (!file.getName().endsWith(projectExtension)) {
88 | return new File(file.getPath() + projectExtension);
89 | }
90 | return file;
91 | }
92 |
93 | @Override
94 | public String getProjectFileName(File projectFile) {
95 | String name = projectFile.getName();
96 | return name.substring(0, name.length() - appConfig.getString("extension.project").length() - 1);
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/ProjectIOException.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files;
2 |
3 | import java.io.IOException;
4 |
5 | public class ProjectIOException extends IOException {
6 |
7 | public ProjectIOException(Throwable cause) {
8 | super(cause);
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/AppRecentProjectManager.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.project.LoadState;
6 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
7 | import dev.sgora.mesheditor.services.config.annotation.AppSettings;
8 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigManager;
9 | import dev.sgora.mesheditor.services.files.workspace.interfaces.RecentProjectManager;
10 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
11 |
12 | import java.io.File;
13 | import java.util.List;
14 | import java.util.stream.Collectors;
15 |
16 | @Singleton
17 | public class AppRecentProjectManager implements RecentProjectManager {
18 | private static final String RECENT_PROJECTS_KEY = "last.projects";
19 |
20 | private AppConfigManager appSettings;
21 | private AppConfigReader appConfig;
22 | private LoadState loadState;
23 |
24 | @Inject
25 | AppRecentProjectManager(@AppSettings AppConfigManager appSettings, @AppConfig AppConfigReader appConfig, LoadState loadState) {
26 | this.appSettings = appSettings;
27 | this.appConfig = appConfig;
28 | this.loadState = loadState;
29 |
30 | loadRecentProjects();
31 | }
32 |
33 | @Override
34 | public void addRecentProject(File location) {
35 | List projects = loadState.recentProjects.get();
36 | location = location.getAbsoluteFile();
37 | projects.remove(location);
38 | projects.add(0, location);
39 | int maxSize = appConfig.getInt("app.recentProjectCount");
40 | if(projects.size() > maxSize)
41 | projects = projects.subList(0, maxSize);
42 | loadState.recentProjects.setAndNotify(projects);
43 | appSettings.setStringList(RECENT_PROJECTS_KEY, projects.stream().map(File::getAbsolutePath).collect(Collectors.toList()));
44 | }
45 |
46 | private void loadRecentProjects() {
47 | List projects = appSettings.getStringList(RECENT_PROJECTS_KEY);
48 | loadState.recentProjects.set(projects.stream().map(File::new).collect(Collectors.toList()));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/FileChooserAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace;
2 |
3 | public enum FileChooserAction {
4 | OPEN_DIALOG("open"),
5 | SAVE_DIALOG("save");
6 |
7 | public final String langKey;
8 |
9 | FileChooserAction(String langKey) {
10 | this.langKey = langKey;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/FileType.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace;
2 |
3 | public enum FileType {
4 | PROJECT("project"),
5 | IMAGE("image"),
6 | EXPORT("export");
7 |
8 | public final String key;
9 |
10 | FileType(String key) {
11 | this.key = key;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/WorkspaceActionExecutor.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.project.CanvasData;
6 | import dev.sgora.mesheditor.model.project.LoadState;
7 | import dev.sgora.mesheditor.model.project.VisualProperties;
8 | import dev.sgora.mesheditor.services.drawing.ImageBox;
9 | import dev.sgora.mesheditor.services.mesh.generation.TriangulationService;
10 | import dev.sgora.mesheditor.services.mesh.rendering.SvgMeshRenderer;
11 | import dev.sgora.mesheditor.services.files.FileUtils;
12 | import dev.sgora.mesheditor.services.files.ProjectIOException;
13 |
14 | import java.io.File;
15 | import java.io.FileInputStream;
16 | import java.io.FileOutputStream;
17 | import java.io.IOException;
18 | import java.nio.charset.StandardCharsets;
19 |
20 | @Singleton
21 | public class WorkspaceActionExecutor {
22 | private final FileUtils fileUtils;
23 | private final LoadState loadState;
24 | private final CanvasData canvasData;
25 | private final SvgMeshRenderer svgMeshRenderer;
26 | private final ImageBox imageBox;
27 | private final TriangulationService triangulationService;
28 | private final VisualProperties visualProperties;
29 |
30 | @Inject
31 | WorkspaceActionExecutor(FileUtils fileUtils, LoadState loadState, CanvasData canvasData, SvgMeshRenderer svgMeshRenderer,
32 | ImageBox imageBox, TriangulationService triangulationService, VisualProperties visualProperties) {
33 | this.fileUtils = fileUtils;
34 | this.loadState = loadState;
35 | this.canvasData = canvasData;
36 | this.svgMeshRenderer = svgMeshRenderer;
37 | this.imageBox = imageBox;
38 | this.triangulationService = triangulationService;
39 | this.visualProperties = visualProperties;
40 | }
41 |
42 | void openProject(File location) throws ProjectIOException {
43 | LoadState state = loadState;
44 | fileUtils.load(location);
45 | state.loaded.set(true);
46 | state.file.set(location);
47 | state.stateSaved.set(true);
48 |
49 | notifyListeners();
50 | }
51 |
52 | File saveProject(File location) throws ProjectIOException {
53 | location = fileUtils.getProjectFileWithExtension(location);
54 | fileUtils.save(location);
55 | loadState.file.set(location);
56 | loadState.stateSaved.set(true);
57 |
58 | loadState.notifyListeners();
59 | return location;
60 | }
61 |
62 | void createNewProject(File location) throws ProjectIOException {
63 | try (FileInputStream fileStream = new FileInputStream(location)) {
64 | fileUtils.loadImage(fileStream);
65 | createProjectModel();
66 |
67 | loadState.loaded.set(true);
68 | loadState.file.set(null);
69 | loadState.stateSaved.set(false);
70 |
71 | notifyListeners();
72 | } catch (IOException e) {
73 | throw new ProjectIOException(e);
74 | }
75 | }
76 |
77 | void exportProjectAsSvg(File location) throws ProjectIOException {
78 | try (FileOutputStream fileStream = new FileOutputStream(fileUtils.getFileWithExtension(location, "svg"))) {
79 | fileStream.write(svgMeshRenderer.renderSvg().getBytes(StandardCharsets.UTF_8));
80 | } catch (IOException e) {
81 | throw new ProjectIOException(e);
82 | }
83 | }
84 |
85 | void closeProject() {
86 | canvasData.mesh.set(null);
87 | canvasData.baseImage.set(null);
88 | canvasData.setRawImageFile(null);
89 |
90 | loadState.loaded.set(false);
91 | loadState.file.set(null);
92 | loadState.stateSaved.set(true);
93 |
94 | loadState.notifyListeners();
95 | canvasData.notifyListeners();
96 | }
97 |
98 | private void notifyListeners() {
99 | loadState.notifyListeners();
100 | canvasData.notifyListeners();
101 | visualProperties.notifyListeners();
102 | }
103 |
104 | private void createProjectModel() {
105 | imageBox.calcImageBox();
106 | triangulationService.createNewMesh();
107 | visualProperties.restoreDefaultValues();
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/WorkspaceActionFacade.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.project.CanvasUI;
6 | import dev.sgora.mesheditor.model.project.LoadState;
7 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
8 | import dev.sgora.mesheditor.services.config.annotation.AppSettings;
9 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigManager;
10 | import dev.sgora.mesheditor.services.files.workspace.interfaces.RecentProjectManager;
11 | import dev.sgora.mesheditor.services.ui.UiDialogUtils;
12 | import javafx.application.Platform;
13 | import javafx.beans.property.ObjectProperty;
14 | import javafx.scene.Cursor;
15 | import javafx.scene.control.ButtonType;
16 | import javafx.stage.FileChooser;
17 | import javafx.stage.WindowEvent;
18 | import dev.sgora.mesheditor.services.config.LangConfigReader;
19 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
20 | import dev.sgora.mesheditor.services.files.FileUtils;
21 | import dev.sgora.mesheditor.services.files.ProjectIOException;
22 | import dev.sgora.mesheditor.services.files.workspace.interfaces.WorkspaceAction;
23 |
24 | import java.io.File;
25 | import java.util.List;
26 | import java.util.Optional;
27 | import java.util.logging.Level;
28 | import java.util.logging.Logger;
29 |
30 | @Singleton
31 | class WorkspaceActionFacade implements WorkspaceAction {
32 | private final Logger logger = Logger.getLogger(getClass().getName());
33 |
34 | private final WorkspaceActionExecutor workspaceActionExecutor;
35 | private final LangConfigReader appLang;
36 | private final UiDialogUtils dialogUtils;
37 | private final FileUtils fileUtils;
38 | private final AppConfigReader appConfig;
39 | private final AppConfigManager appSettings;
40 | private final LoadState loadState;
41 | private final ObjectProperty mouseCursor;
42 | private final RecentProjectManager recentProjectManager;
43 |
44 | @Inject
45 | WorkspaceActionFacade(WorkspaceActionExecutor workspaceActionExecutor, LangConfigReader appLang, UiDialogUtils dialogUtils,
46 | FileUtils fileUtils, @AppConfig AppConfigReader appConfig, @AppSettings AppConfigManager appSettings,
47 | LoadState loadState, CanvasUI canvasUI, RecentProjectManager recentProjectManager) {
48 | this.workspaceActionExecutor = workspaceActionExecutor;
49 | this.appLang = appLang;
50 | this.dialogUtils = dialogUtils;
51 | this.fileUtils = fileUtils;
52 | this.appConfig = appConfig;
53 | this.appSettings = appSettings;
54 | this.loadState = loadState;
55 | this.mouseCursor = canvasUI.canvasMouseCursor;
56 | this.recentProjectManager = recentProjectManager;
57 | }
58 |
59 | @Override
60 | public String getProjectName() {
61 | String projectName;
62 | if (loadState.file.get() == null)
63 | projectName = loadState.loaded.get() ? appLang.getText("defaultProjectName") : null;
64 | else
65 | projectName = fileUtils.getProjectFileName(loadState.file.get());
66 | return projectName;
67 | }
68 |
69 | @Override
70 | public void onNewProject() {
71 | String title = appLang.getText("action.project.create");
72 | if (showConfirmDialog() && !confirmWorkspaceAction(title)) {
73 | return;
74 | }
75 | String[] imageTypes = appConfig.getStringList("supported.imageTypes").stream().map(item -> "*." + item).toArray(String[]::new);
76 | FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(appLang.getText("dialog.fileChooser.extension.image"), imageTypes);
77 | File location = dialogUtils.showFileChooser(FileChooserAction.OPEN_DIALOG, appLang.getText("action.project.new"), getLastChosenDir(FileType.IMAGE), filter);
78 | if (location != null) {
79 | try {
80 | workspaceActionExecutor.createNewProject(location);
81 | setLastChosenDir(FileType.IMAGE, location);
82 | } catch (ProjectIOException e) {
83 | logger.log(Level.SEVERE, "Failed creating new project at '" + location.getAbsolutePath() + "'", e);
84 | showErrorDialog(title);
85 | }
86 | }
87 | }
88 |
89 | @Override
90 | public void onOpenProject() {
91 | String title = appLang.getText("action.project.open");
92 | if (showConfirmDialog() && !confirmWorkspaceAction(title)) {
93 | return;
94 | }
95 | File location = showProjectFileChooser(FileChooserAction.OPEN_DIALOG);
96 | if (location != null) {
97 | openProject(location, title);
98 | setLastChosenDir(FileType.PROJECT, location);
99 | }
100 | }
101 |
102 | @Override
103 | public void onOpenRecentProject(File project) {
104 | openProject(project, appLang.getText("action.project.open"));
105 | }
106 |
107 | @Override
108 | public void onCloseProject() {
109 | if (!showConfirmDialog() || confirmWorkspaceAction(appLang.getText("action.project.close"))) {
110 | workspaceActionExecutor.closeProject();
111 | mouseCursor.setValue(Cursor.DEFAULT);
112 | }
113 | }
114 |
115 | @Override
116 | public void onSaveProject() {
117 | saveProject(false);
118 | }
119 |
120 | @Override
121 | public void onSaveProjectAs() {
122 | saveProject(true);
123 | }
124 |
125 | @Override
126 | public void onExportProject() {
127 | String title = appLang.getText("action.project.export");
128 | FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(appLang.getText("dialog.fileChooser.extension.svg"), "*.svg");
129 | File location = dialogUtils.showFileChooser(FileChooserAction.SAVE_DIALOG, title, getLastChosenDir(FileType.EXPORT), filter);
130 | if (location != null) {
131 | try {
132 | workspaceActionExecutor.exportProjectAsSvg(location);
133 | setLastChosenDir(FileType.EXPORT, location);
134 | } catch (ProjectIOException e) {
135 | logger.log(Level.SEVERE, "Failed exporting project at '" + location.getAbsolutePath() + "'", e);
136 | showErrorDialog(title);
137 | }
138 | }
139 | }
140 |
141 | @Override
142 | public void onWindowCloseRequest(WindowEvent event) {
143 | if (showConfirmDialog() && !confirmWorkspaceAction(appLang.getText("action.quit"))) {
144 | event.consume();
145 | }
146 | }
147 |
148 | @Override
149 | public void onExitApp() {
150 | if (!showConfirmDialog() || confirmWorkspaceAction(appLang.getText("action.quit"))) {
151 | Platform.exit();
152 | }
153 | }
154 |
155 | private File getLastChosenDir(FileType type) {
156 | return new File(appSettings.opt().getString("last.chosenDir." + type.key));
157 | }
158 |
159 | private void setLastChosenDir(FileType type, File file) {
160 | appSettings.setString("last.chosenDir." + type.key, file.getParent());
161 | }
162 |
163 | private void openProject(File location, String errorTitle) {
164 | try {
165 | workspaceActionExecutor.openProject(location);
166 | recentProjectManager.addRecentProject(location);
167 | } catch (ProjectIOException e) {
168 | logger.log(Level.SEVERE, "Failed loading project from '" + location.getAbsolutePath() + "'", e);
169 | showErrorDialog(errorTitle);
170 | }
171 | }
172 |
173 | private boolean showConfirmDialog() {
174 | return appConfig.getBool("flags.showConfirmDialogs");
175 | }
176 |
177 | private File showProjectFileChooser(FileChooserAction action) {
178 | String projectExtension = appConfig.getString("extension.project");
179 | String extensionTitle = appLang.getText("dialog.fileChooser.extension.project");
180 | FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(extensionTitle, "*." + projectExtension);
181 | return dialogUtils.showFileChooser(action, appLang.getText("action.project." + action.langKey), getLastChosenDir(FileType.PROJECT), filter);
182 | }
183 |
184 | private void saveProject(boolean asNew) {
185 | File location;
186 | boolean savingAsNew = asNew || loadState.file.get() == null;
187 | if (savingAsNew) {
188 | location = showProjectFileChooser(FileChooserAction.SAVE_DIALOG);
189 | if (location == null)
190 | return;
191 | else
192 | setLastChosenDir(FileType.PROJECT, location);
193 | } else {
194 | location = loadState.file.get();
195 | }
196 | try {
197 | location = workspaceActionExecutor.saveProject(location);
198 | if(savingAsNew)
199 | recentProjectManager.addRecentProject(location);
200 | } catch (ProjectIOException e) {
201 | logger.log(Level.SEVERE, "Failed saving project to '" + location.getAbsolutePath() + "'", e);
202 | showErrorDialog(appLang.getText("action.project.save"));
203 | }
204 | }
205 |
206 | private boolean confirmWorkspaceAction(String title) {
207 | if (loadState.stateSaved.get()) {
208 | return true;
209 | }
210 | ButtonType saveButton = new ButtonType(appLang.getText("action.save"));
211 | ButtonType discardButton = new ButtonType(appLang.getText("action.discard"));
212 | ButtonType cancelButton = new ButtonType(appLang.getText("action.cancel"));
213 | List headerParts = appLang.getMultipartText("dialog.header.warning.modified");
214 | String headerText = headerParts.get(0) + getProjectName() + headerParts.get(1);
215 | String contentText = appLang.getText("dialog.content.warning.modified");
216 | ButtonType[] buttonTypes = {saveButton, discardButton, cancelButton};
217 | Optional response = dialogUtils.showWarningDialog(title, headerText, contentText, buttonTypes);
218 | if (!response.isPresent() || response.get() == cancelButton) {
219 | return false;
220 | }
221 | if (response.get() == saveButton) {
222 | onSaveProject();
223 | }
224 | return true;
225 | }
226 |
227 | private void showErrorDialog(String titleText) {
228 | dialogUtils.showErrorDialog(titleText, appLang.getText("dialog.header.error.workspaceAction"), appLang.getText("dialog.content.error.workspaceAction"));
229 | }
230 |
231 | }
232 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/WorkspaceActionModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace;
2 |
3 | import com.google.inject.AbstractModule;
4 | import dev.sgora.mesheditor.services.files.workspace.interfaces.RecentProjectManager;
5 | import dev.sgora.mesheditor.services.files.workspace.interfaces.WorkspaceAction;
6 |
7 | public class WorkspaceActionModule extends AbstractModule {
8 | @Override
9 | protected void configure() {
10 | bind(WorkspaceAction.class).to(WorkspaceActionFacade.class);
11 | bind(RecentProjectManager.class).to(AppRecentProjectManager.class);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/interfaces/RecentProjectManager.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace.interfaces;
2 |
3 | import java.io.File;
4 |
5 | public interface RecentProjectManager {
6 | void addRecentProject(File location);
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/files/workspace/interfaces/WorkspaceAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.files.workspace.interfaces;
2 |
3 | import javafx.stage.WindowEvent;
4 |
5 | import java.io.File;
6 |
7 | public interface WorkspaceAction {
8 | String getProjectName();
9 |
10 | void onNewProject();
11 |
12 | void onOpenProject();
13 |
14 | void onOpenRecentProject(File project);
15 |
16 | void onCloseProject();
17 |
18 | void onSaveProject();
19 |
20 | void onSaveProjectAs();
21 |
22 | void onExportProject();
23 |
24 | void onWindowCloseRequest(WindowEvent event);
25 |
26 | void onExitApp();
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/ActionHistoryModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class ActionHistoryModule extends AbstractModule {
6 | @Override
7 | protected void configure() {
8 | bind(ActionHistoryService.class).to(CommandActionHistoryService.class);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/ActionHistoryService.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history;
2 |
3 | import dev.sgora.mesheditor.services.history.actions.UserAction;
4 |
5 | public interface ActionHistoryService {
6 | void undo();
7 |
8 | void redo();
9 |
10 | void registerAction(UserAction action);
11 |
12 | void clearActionHistory();
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/CommandActionHistoryService.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.project.LoadState;
6 | import dev.sgora.mesheditor.services.history.actions.UserAction;
7 | import dev.sgora.mesheditor.services.history.actions.node.NodeModifiedAction;
8 | import dev.sgora.mesheditor.services.mesh.generation.TriangulationService;
9 |
10 | import java.util.ArrayDeque;
11 | import java.util.Deque;
12 |
13 | @Singleton
14 | class CommandActionHistoryService implements ActionHistoryService {
15 | private final Deque undoActionStack = new ArrayDeque<>();
16 | private final Deque redoActionStack = new ArrayDeque<>();
17 |
18 | @Inject
19 | CommandActionHistoryService(LoadState loadState, TriangulationService triangulationService) {
20 | loadState.loaded.addListener(() -> {
21 | if (!loadState.loaded.get())
22 | clearActionHistory();
23 | });
24 | NodeModifiedAction.setNodeMethodReferences(triangulationService::addNode, triangulationService::removeNode);
25 | }
26 |
27 | @Override
28 | public void undo() {
29 | if (undoActionStack.isEmpty())
30 | return;
31 | undoActionStack.peek().unExecute();
32 | redoActionStack.push(undoActionStack.pop());
33 | }
34 |
35 | @Override
36 | public void redo() {
37 | if (redoActionStack.isEmpty())
38 | return;
39 | redoActionStack.peek().execute();
40 | undoActionStack.push(redoActionStack.pop());
41 | }
42 |
43 | @Override
44 | public void registerAction(UserAction action) {
45 | undoActionStack.push(action);
46 | redoActionStack.clear();
47 | }
48 |
49 | @Override
50 | public void clearActionHistory() {
51 | undoActionStack.clear();
52 | redoActionStack.clear();
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/UserAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions;
2 |
3 | public interface UserAction {
4 | void execute();
5 |
6 | void unExecute();
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/node/AddNodeAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.node;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 |
5 | public class AddNodeAction extends NodeModifiedAction {
6 |
7 | public AddNodeAction(double x, double y) {
8 | super(x, y);
9 | }
10 |
11 | @Override
12 | public void execute() {
13 | addNode.accept(new Point(x, y));
14 | }
15 |
16 | @Override
17 | public void unExecute() {
18 | removeNode.accept(new Point(x, y));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/node/MoveNodeAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.node;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 |
5 | public class MoveNodeAction extends NodeModifiedAction {
6 | private Point point;
7 | private double oldX;
8 | private double oldY;
9 |
10 | public MoveNodeAction(Point movedPoint, Point oldPoint) {
11 | super(movedPoint.getX(), movedPoint.getY());
12 | this.point = movedPoint;
13 | this.oldX = oldPoint.getX();
14 | this.oldY = oldPoint.getY();
15 | }
16 |
17 | @Override
18 | public void execute() {
19 | switchNodes(x, y);
20 | }
21 |
22 | @Override
23 | public void unExecute() {
24 | switchNodes(oldX, oldY);
25 | }
26 |
27 | private void switchNodes(double x, double y) {
28 | removeNode.accept(point);
29 | point = new Point(x, y);
30 | addNode.accept(point);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/node/NodeModifiedAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.node;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 | import dev.sgora.mesheditor.services.history.actions.UserAction;
5 |
6 | import java.util.function.Consumer;
7 |
8 | public abstract class NodeModifiedAction implements UserAction {
9 | protected double x;
10 | protected double y;
11 |
12 | protected static Consumer addNode;
13 | protected static Consumer removeNode;
14 |
15 | public NodeModifiedAction(double x, double y) {
16 | this.x = x;
17 | this.y = y;
18 | }
19 |
20 | public static void setNodeMethodReferences(Consumer addPoint, Consumer removePoint) {
21 | NodeModifiedAction.addNode = addPoint;
22 | NodeModifiedAction.removeNode = removePoint;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/node/RemoveNodeAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.node;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 |
5 | public class RemoveNodeAction extends NodeModifiedAction {
6 |
7 | public RemoveNodeAction(double x, double y) {
8 | super(x, y);
9 | }
10 |
11 | @Override
12 | public void execute() {
13 | removeNode.accept(new Point(x, y));
14 | }
15 |
16 | @Override
17 | public void unExecute() {
18 | addNode.accept(new Point(x, y));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/property/CheckBoxChangeAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.property;
2 |
3 | import java.util.function.Consumer;
4 |
5 | public class CheckBoxChangeAction extends PropertyChangeAction {
6 |
7 | public CheckBoxChangeAction(Boolean newValue, Consumer setValue) {
8 | super(newValue, !newValue, setValue);
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/property/PropertyChangeAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.property;
2 |
3 | import dev.sgora.mesheditor.services.history.actions.UserAction;
4 |
5 | import java.util.function.Consumer;
6 |
7 | public class PropertyChangeAction implements UserAction {
8 | private V newValue;
9 | private V oldValue;
10 |
11 | private Consumer setValue;
12 |
13 | public PropertyChangeAction(V newValue, V oldValue, Consumer setValue) {
14 | this.newValue = newValue;
15 | this.oldValue = oldValue;
16 | this.setValue = setValue;
17 | }
18 |
19 | @Override
20 | public void execute() {
21 | setValue.accept(newValue);
22 | }
23 |
24 | @Override
25 | public void unExecute() {
26 | setValue.accept(oldValue);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/history/actions/property/SliderChangeAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.history.actions.property;
2 |
3 | import java.util.function.Consumer;
4 |
5 | public class SliderChangeAction extends PropertyChangeAction {
6 |
7 | public SliderChangeAction(Double newValue, Double oldValue, Consumer setValue) {
8 | super(newValue, oldValue, setValue);
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/input/CanvasAction.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.input;
2 |
3 | import javafx.scene.input.MouseEvent;
4 | import javafx.scene.input.ScrollEvent;
5 |
6 | public interface CanvasAction {
7 | void onMousePress(MouseEvent event);
8 |
9 | void onMouseDrag(MouseEvent event);
10 |
11 | void onMouseRelease(MouseEvent event);
12 |
13 | void onScroll(ScrollEvent event);
14 |
15 | void onMouseEnter(MouseEvent event);
16 |
17 | void onMouseExit(MouseEvent event);
18 |
19 | void onMouseMove(MouseEvent event);
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/input/CanvasActionFacade.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.input;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Point;
6 | import dev.sgora.mesheditor.model.project.LoadState;
7 | import dev.sgora.mesheditor.services.drawing.ImageBox;
8 | import dev.sgora.mesheditor.services.drawing.MeshBox;
9 | import javafx.scene.Cursor;
10 | import javafx.scene.input.MouseEvent;
11 | import javafx.scene.input.ScrollEvent;
12 | import dev.sgora.mesheditor.model.project.CanvasUI;
13 |
14 | @Singleton
15 | class CanvasActionFacade implements CanvasAction {
16 |
17 | private final MouseListener[] eventConsumersQueue;
18 | private final LoadState loadState;
19 | private final CanvasUI canvasUI;
20 | private final ImageBox imageBox;
21 | private final MeshBox meshBox;
22 |
23 | private Point lastMouseDragPoint;
24 | private MouseListener activeConsumer;
25 |
26 | @Inject
27 | CanvasActionFacade(LoadState loadState, ImageBox imageBox, MeshBox meshBox, CanvasUI canvasUI) {
28 | this.imageBox = imageBox;
29 | this.meshBox = meshBox;
30 | this.loadState = loadState;
31 | this.canvasUI = canvasUI;
32 | eventConsumersQueue = new MouseListener[]{meshBox, imageBox};
33 | }
34 |
35 | @Override
36 | public void onMousePress(MouseEvent event) {
37 | Point mousePos = new Point(event.getX(), event.getY());
38 | if (loadState.loaded.get()) {
39 | for (MouseListener consumer : eventConsumersQueue) {
40 | if (consumer.onDragStart(mousePos, event.getButton())) {
41 | activeConsumer = consumer;
42 | break;
43 | }
44 | }
45 | }
46 | lastMouseDragPoint = mousePos;
47 | }
48 |
49 | @Override
50 | public void onMouseDrag(MouseEvent event) {
51 | Point mousePos = new Point(event.getX(), event.getY());
52 | Point dragAmount = new Point(mousePos).subtract(lastMouseDragPoint);
53 | if (loadState.loaded.get() && activeConsumer != null) {
54 | activeConsumer.onMouseDrag(new Point(dragAmount), mousePos, event.getButton());
55 | }
56 | lastMouseDragPoint.set(mousePos);
57 | }
58 |
59 | @Override
60 | public void onMouseRelease(MouseEvent event) {
61 | lastMouseDragPoint = null;
62 | Point mousePos = new Point(event.getX(), event.getY());
63 | if (loadState.loaded.get() && activeConsumer != null) {
64 | activeConsumer.onDragEnd(new Point(mousePos), event.getButton());
65 | }
66 | }
67 |
68 | @Override
69 | public void onScroll(ScrollEvent event) {
70 | if (loadState.loaded.get()) {
71 | imageBox.onZoom(event.getDeltaY(), new Point(event.getX(), event.getY()));
72 | }
73 | }
74 |
75 | @Override
76 | public void onMouseEnter(MouseEvent event) {
77 | if (loadState.loaded.get() && lastMouseDragPoint == null) {
78 | canvasUI.canvasMouseCursor.setValue(canvasUI.mouseConfig.defaultCanvasCursor);
79 | }
80 | }
81 |
82 | @Override
83 | public void onMouseExit(MouseEvent event) {
84 | if (loadState.loaded.get() && lastMouseDragPoint == null) {
85 | canvasUI.canvasMouseCursor.setValue(Cursor.DEFAULT);
86 | }
87 | }
88 |
89 | @Override
90 | public void onMouseMove(MouseEvent event) {
91 | Point mousePos = new Point(event.getX(), event.getY());
92 | if (loadState.loaded.get()) {
93 | meshBox.onMouseMove(mousePos);
94 | }
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/input/InputModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.input;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class InputModule extends AbstractModule {
6 | @Override
7 | protected void configure() {
8 | bind(CanvasAction.class).to(CanvasActionFacade.class);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/input/MouseListener.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.input;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 | import javafx.scene.input.MouseButton;
5 |
6 | public interface MouseListener {
7 |
8 | boolean onDragStart(Point mousePos, MouseButton button);
9 |
10 | void onMouseDrag(Point dragAmount, Point mousePos, MouseButton button);
11 |
12 | void onDragEnd(Point mousePos, MouseButton button);
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/FlipBasedTriangulationService.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Mesh;
6 | import dev.sgora.mesheditor.model.geom.Point;
7 | import dev.sgora.mesheditor.model.project.CanvasData;
8 | import io.github.stasgora.observetree.SettableObservable;
9 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
10 |
11 | import java.util.*;
12 |
13 | @Singleton
14 | class FlipBasedTriangulationService implements TriangulationService {
15 |
16 | private SettableObservable mesh;
17 | private NodeUtils nodeUtils;
18 | private TriangleUtils triangleUtils;
19 | private FlippingUtils flippingUtils;
20 | private VoronoiDiagramService voronoiDiagramService;
21 |
22 | @Inject
23 | FlipBasedTriangulationService(CanvasData canvasData, NodeUtils nodeUtils, TriangleUtils triangleUtils,
24 | FlippingUtils flippingUtils, VoronoiDiagramService voronoiDiagramService) {
25 | this.mesh = canvasData.mesh;
26 | this.nodeUtils = nodeUtils;
27 | this.triangleUtils = triangleUtils;
28 | this.flippingUtils = flippingUtils;
29 | this.voronoiDiagramService = voronoiDiagramService;
30 | }
31 |
32 | @Override
33 | public void createNewMesh() {
34 | mesh.set(new Mesh(nodeUtils.getBoundingNodes()));
35 | }
36 |
37 | @Override
38 | public boolean addNode(Point location) {
39 | Triangle triangle = triangleUtils.findTriangleByLocation(location);
40 | if (nodeUtils.getClosestNode(location, triangle) != null) {
41 | return false;
42 | }
43 | mesh.get().removeTriangle(triangle);
44 | Triangle[] newTriangles = new Triangle[3];
45 | for (int i = 0; i < 3; i++) {
46 | newTriangles[i] = new Triangle(triangle.getNodes()[i], triangle.getNodes()[(i + 1) % 3], location);
47 | triangleUtils.bindTrianglesBothWays(newTriangles[i], 0, triangle.getTriangles()[i], triangle);
48 | }
49 | Deque trianglesToCheck = new ArrayDeque<>();
50 | for (int i = 0; i < 3; i++) {
51 | newTriangles[i].getTriangles()[1] = newTriangles[(i + 1) % 3];
52 | newTriangles[i].getTriangles()[2] = newTriangles[(i + 2) % 3];
53 | mesh.get().addTriangle(newTriangles[i]);
54 | trianglesToCheck.push(newTriangles[i]);
55 | }
56 | List changedTriangles = flippingUtils.flipInvalidTriangles(trianglesToCheck);
57 | changedTriangles.addAll(Arrays.asList(newTriangles));
58 | mesh.get().addNode(location);
59 |
60 | voronoiDiagramService.generateDiagram(triangleUtils.getTrianglePointSet(changedTriangles));
61 | mesh.notifyListeners();
62 | return true;
63 | }
64 |
65 | @Override
66 | public Point findNodeByLocation(Point location) {
67 | Triangle triangle = triangleUtils.findTriangleByLocation(location);
68 | return nodeUtils.getClosestNode(location, triangle);
69 | }
70 |
71 | @Override
72 | public boolean removeNode(Point location) {
73 | Triangle triangle = triangleUtils.findTriangleByLocation(location);
74 | Point node = nodeUtils.getClosestNode(location, triangle);
75 | if (node == null) {
76 | return false;
77 | }
78 | List points = new ArrayList<>();
79 | List triangles = new ArrayList<>();
80 | nodeUtils.getNodeNeighbours(node, triangle, points, triangles);
81 | List neighbourPoints = new ArrayList<>(points);
82 | retriangulateNodeHole(node, points, triangles);
83 | mesh.get().removeNode(node);
84 |
85 | voronoiDiagramService.generateDiagram(neighbourPoints);
86 | mesh.get().notifyListeners();
87 | return true;
88 | }
89 |
90 | @Override
91 | public Point moveNode(Point node, Point position) {
92 | Triangle triangle = triangleUtils.findTriangleByLocation(node);
93 | List points = new ArrayList<>();
94 | List triangles = new ArrayList<>();
95 | nodeUtils.getNodeNeighbours(node, triangle, points, triangles);
96 |
97 | node.set(position);
98 | Deque trianglesToCheck = new ArrayDeque<>(triangles);
99 | flippingUtils.flipInvalidTriangles(trianglesToCheck);
100 |
101 | points.add(node);
102 | voronoiDiagramService.generateDiagram(points);
103 | mesh.get().notifyListeners();
104 | return node;
105 | }
106 |
107 | private void retriangulateNodeHole(Point node, List nodes, List triangles) {
108 | Point[] currentNodes = new Point[3];
109 | currentNodes[0] = nodes.get(0);
110 | while (nodes.size() > 3) {
111 | int currentId = nodes.indexOf(currentNodes[0]);
112 | currentNodes[1] = nodes.get((currentId + 1) % nodes.size());
113 | currentNodes[2] = nodes.get((currentId + 2) % nodes.size());
114 | double earTest = triangleUtils.dMatrixDet(currentNodes[2], currentNodes[1], currentNodes[0]);
115 | double enclosingTest = triangleUtils.dMatrixDet(currentNodes[0], node, currentNodes[2]);
116 | if (earTest >= 0 && enclosingTest >= 0 && checkTriangleAgainstNodes(nodes, currentNodes, currentId)) {
117 | flippingUtils.flipTrianglesFromRing(node, nodes, triangles, currentId);
118 | } else {
119 | currentNodes[0] = nodes.get((currentId + 1) % nodes.size());
120 | }
121 | }
122 | triangleUtils.mergeTrianglesIntoOne(nodes, triangles);
123 | }
124 |
125 | private boolean checkTriangleAgainstNodes(List nodes, Point[] currentNodes, int currentId) {
126 | for (int i = 0; i < nodes.size() - 3; i++) {
127 | int index = (currentId + 3 + i) % nodes.size();
128 | double circumcircleTest = triangleUtils.hMatrixDet(currentNodes[2], currentNodes[1], currentNodes[0], nodes.get(index));
129 | if (circumcircleTest > 0) {
130 | return false;
131 | }
132 | }
133 | return true;
134 | }
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/FlippingUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Mesh;
6 | import dev.sgora.mesheditor.model.geom.Point;
7 | import dev.sgora.mesheditor.model.project.CanvasData;
8 | import io.github.stasgora.observetree.SettableObservable;
9 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
10 |
11 | import java.util.*;
12 |
13 | @Singleton
14 | public class FlippingUtils {
15 |
16 | private final SettableObservable mesh;
17 | private final TriangleUtils triangleUtils;
18 |
19 | @Inject
20 | FlippingUtils(CanvasData canvasData, TriangleUtils triangleUtils) {
21 | this.mesh = canvasData.mesh;
22 | this.triangleUtils = triangleUtils;
23 | }
24 |
25 | List flipInvalidTriangles(Deque remaining) {
26 | List changedTriangles = new ArrayList<>();
27 | while (!remaining.isEmpty()) {
28 | Triangle current = remaining.pop();
29 | if (!mesh.get().getTriangles().contains(current)) {
30 | continue;
31 | }
32 | for (Triangle neighbour : current.getTriangles()) {
33 | if (neighbour == null) {
34 | continue;
35 | }
36 | Point otherNode = triangleUtils.getSeparateNode(neighbour, current);
37 | if (isPointInsideCircumcircle(otherNode, current)) {
38 | Triangle[] created = flipTriangles(current, neighbour);
39 | changedTriangles.addAll(Arrays.asList(created));
40 | remaining.addAll(Arrays.asList(created));
41 | }
42 | }
43 | }
44 | return changedTriangles;
45 | }
46 |
47 | void flipTrianglesFromRing(Point node, List nodes, List triangles, int currentId) {
48 | int nextId = (currentId + 1) % nodes.size();
49 | Triangle[] currentTriangles = new Triangle[]{triangles.get(currentId), triangles.get(nextId)};
50 | Triangle[] newTriangles = flipTriangles(currentTriangles[0], currentTriangles[1]);
51 | Triangle newNeighbour = Arrays.asList(newTriangles[0].getNodes()).contains(node) ? newTriangles[0] : newTriangles[1];
52 | nodes.remove(nextId);
53 | triangles.add(currentId, newNeighbour);
54 | triangles.remove(currentTriangles[0]);
55 | triangles.remove(currentTriangles[1]);
56 | }
57 |
58 | private Triangle[] flipTriangles(Triangle a, Triangle b) {
59 | int aNodeIndex = triangleUtils.getSeparateNodeIndex(a, b);
60 | int bNodeIndex = triangleUtils.getSeparateNodeIndex(b, a);
61 | mesh.get().removeTriangle(a);
62 | mesh.get().removeTriangle(b);
63 | Triangle[] added = new Triangle[2];
64 | added[0] = new Triangle(a.getNodes()[aNodeIndex], a.getNodes()[(aNodeIndex + 1) % 3], b.getNodes()[bNodeIndex]);
65 | added[1] = new Triangle(b.getNodes()[bNodeIndex], b.getNodes()[(bNodeIndex + 1) % 3], a.getNodes()[aNodeIndex]);
66 | triangleUtils.bindTrianglesBothWays(added[0], 0, a.getTriangles()[aNodeIndex], a);
67 | triangleUtils.bindTrianglesBothWays(added[0], 1, b.getTriangles()[(bNodeIndex + 2) % 3], b);
68 | triangleUtils.bindTrianglesBothWays(added[1], 0, b.getTriangles()[bNodeIndex], b);
69 | triangleUtils.bindTrianglesBothWays(added[1], 1, a.getTriangles()[(aNodeIndex + 2) % 3], a);
70 | added[0].getTriangles()[2] = added[1];
71 | added[1].getTriangles()[2] = added[0];
72 | mesh.get().addTriangle(added[0]);
73 | mesh.get().addTriangle(added[1]);
74 | return added;
75 | }
76 |
77 | private boolean isPointInsideCircumcircle(Point node, Triangle triangle) {
78 | return triangleUtils.hMatrixDet(triangle.getNodes()[2], triangle.getNodes()[1], triangle.getNodes()[0], node) > 0;
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/MeshGenerationModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class MeshGenerationModule extends AbstractModule {
6 | @Override
7 | protected void configure() {
8 | bind(TriangulationService.class).to(FlipBasedTriangulationService.class);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/NodeUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Point;
6 | import dev.sgora.mesheditor.model.project.CanvasData;
7 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
8 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
9 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
10 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
11 |
12 | import java.util.Arrays;
13 | import java.util.List;
14 | import java.util.logging.Logger;
15 |
16 | @Singleton
17 | public class NodeUtils {
18 |
19 | private static final Logger LOGGER = Logger.getLogger(NodeUtils.class.getName());
20 |
21 | private final AppConfigReader appConfig;
22 | private final CanvasData canvasData;
23 |
24 | private final double relativeSpaceFactor;
25 |
26 | @Inject
27 | NodeUtils(@AppConfig AppConfigReader appConfig, CanvasData canvasData) {
28 | this.appConfig = appConfig;
29 | this.canvasData = canvasData;
30 | relativeSpaceFactor = appConfig.getDouble("meshBox.proportionalSpaceFactor");
31 | }
32 |
33 | public Triangle findNodeTriangle(Point node) {
34 | return canvasData.mesh.get().getTriangles().stream().filter(triangle -> Arrays.stream(triangle.getNodes()).anyMatch(vertex -> vertex == node)).findFirst().orElse(null);
35 | }
36 |
37 | Point getClosestNode(Point location, Triangle triangle) {
38 | double nodeBoxRadius = appConfig.getDouble("meshBox.nodeBoxRadius") / (canvasData.imageBox.getSize().getX() / relativeSpaceFactor);
39 | for (Point node : triangle.getNodes()) {
40 | Point dist = new Point(node).subtract(location).abs();
41 | if (dist.getX() <= nodeBoxRadius && dist.getY() <= nodeBoxRadius) {
42 | return node;
43 | }
44 | }
45 | return null;
46 | }
47 |
48 | public void getNodeNeighbours(Point node, Triangle firstTriangle, List outPoints, List outTriangles) {
49 | Triangle currentTriangle = firstTriangle;
50 | do {
51 | int nodeIndex = Arrays.asList(currentTriangle.getNodes()).indexOf(node);
52 | if (nodeIndex == -1) {
53 | LOGGER.warning(String.format("triangle %s does not contain given node %s", currentTriangle, node));
54 | }
55 | nodeIndex = (nodeIndex + 2) % 3;
56 | if (outPoints != null)
57 | outPoints.add(currentTriangle.getNodes()[nodeIndex]);
58 | currentTriangle = currentTriangle.getTriangles()[nodeIndex];
59 | if (outTriangles != null)
60 | outTriangles.add(currentTriangle);
61 | } while (currentTriangle != firstTriangle);
62 | }
63 |
64 | public Point proportionalToCanvasPos(Point node) {
65 | Rectangle imageBox = canvasData.imageBox;
66 | return new Point(node).multiplyByScalar(imageBox.getSize().getX() / relativeSpaceFactor).add(imageBox.getPosition());
67 | }
68 |
69 | public Point canvasToProportionalPos(Point node) {
70 | Rectangle imageBox = canvasData.imageBox;
71 | return new Point(node).subtract(imageBox.getPosition()).divideByScalar(imageBox.getSize().getX() / relativeSpaceFactor);
72 | }
73 |
74 | public Point canvasToProportionalSize(Point node) {
75 | return new Point(node).multiplyByScalar(proportionalScaleFactor());
76 | }
77 |
78 | public double proportionalScaleFactor() {
79 | Rectangle imageBox = canvasData.imageBox;
80 | return relativeSpaceFactor / imageBox.getSize().getX();
81 | }
82 |
83 | public Point proportionalToPixelPos(Point node) {
84 | return new Point(node).multiplyByScalar(canvasData.baseImage.get().getWidth() / relativeSpaceFactor);
85 | }
86 |
87 | public Point getProportionalMarginSize() {
88 | double spaceAroundImage = appConfig.getDouble("meshBox.spaceAroundImage");
89 | return canvasToProportionalSize(new Point(canvasData.imageBox.getSize()).multiplyByScalar(spaceAroundImage));
90 | }
91 |
92 | public Rectangle getCanvasSpaceNodeBoundingBox() {
93 | Rectangle imageBox = canvasData.imageBox;
94 | double spaceAroundImage = appConfig.getDouble("meshBox.spaceAroundImage");
95 | Rectangle area = new Rectangle();
96 | Point boxSize = imageBox.getSize();
97 | area.setPosition(new Point(imageBox.getPosition()).subtract(new Point(boxSize).multiplyByScalar(spaceAroundImage)));
98 | area.setSize(new Point(boxSize).multiplyByScalar(spaceAroundImage * 2 + 1));
99 | return area;
100 | }
101 |
102 | Point[] getBoundingNodes() {
103 | Rectangle boundingBox = getProportionalNodeBoundingBox();
104 | double majorLength = boundingBox.getSize().getX() + boundingBox.getSize().getY();
105 | return new Point[]{
106 | new Point(boundingBox.getPosition().getX() + boundingBox.getSize().getX() / 2, -majorLength),
107 | new Point(boundingBox.getPosition().getX() - majorLength, boundingBox.getSize().getY()),
108 | new Point(boundingBox.getPosition().getX() + boundingBox.getSize().getX() + majorLength, boundingBox.getSize().getY())
109 | };
110 | }
111 |
112 | public Rectangle getProportionalNodeBoundingBox() {
113 | Rectangle canvasSpaceBox = getCanvasSpaceNodeBoundingBox();
114 | canvasSpaceBox.setPosition(canvasToProportionalPos(canvasSpaceBox.getPosition()));
115 | canvasSpaceBox.setSize(canvasToProportionalSize(canvasSpaceBox.getSize()));
116 | return canvasSpaceBox;
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/TriangleUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Mesh;
6 | import dev.sgora.mesheditor.model.geom.Point;
7 | import dev.sgora.mesheditor.model.project.CanvasData;
8 | import io.github.stasgora.observetree.SettableObservable;
9 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
10 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
11 |
12 | import java.util.Arrays;
13 | import java.util.List;
14 | import java.util.Set;
15 | import java.util.logging.Logger;
16 | import java.util.stream.Collectors;
17 | import java.util.stream.Stream;
18 |
19 | @Singleton
20 | public class TriangleUtils {
21 |
22 | private static final Logger LOGGER = Logger.getLogger(TriangleUtils.class.getName());
23 |
24 | private final SettableObservable mesh;
25 |
26 | @Inject
27 | TriangleUtils(CanvasData canvasData) {
28 | this.mesh = canvasData.mesh;
29 | }
30 |
31 | void mergeTrianglesIntoOne(List nodes, List triangles) {
32 | Triangle newTriangle = new Triangle(nodes.toArray(new Point[3]));
33 | for (int i = 0; i < 3; i++) {
34 | Triangle triangle = triangles.get(i);
35 | int triNeighbourId = Arrays.asList(triangle.getNodes()).indexOf(nodes.get(i));
36 | bindTrianglesBothWays(newTriangle, i, triangle.getTriangles()[triNeighbourId], triangle);
37 | }
38 | triangles.forEach(mesh.get()::removeTriangle);
39 | mesh.get().addTriangle(newTriangle);
40 | }
41 |
42 | Triangle findTriangleByLocation(Point location) {
43 | Triangle current = mesh.get().getTriangle(0);
44 | Triangle next = null;
45 | do {
46 | for (int i = 0; i < 3; i++) {
47 | next = getCloserTriangle(location, current, i);
48 | if (next != null) {
49 | current = next;
50 | break;
51 | }
52 | }
53 | } while (next != null);
54 | return current;
55 | }
56 |
57 | private Triangle getCloserTriangle(Point node, Triangle current, int nodeIndex) {
58 | double det = dMatrixDet(current.getNodes()[nodeIndex], node, current.getNodes()[(nodeIndex + 1) % 3]);
59 | return det + 1e-5 < 0 ? current.getTriangles()[nodeIndex] : null;
60 | }
61 |
62 | public List getValidVoronoiRegions() {
63 | Stream boundingTriangleStream = mesh.get().getTriangles().stream().filter(triangle -> Arrays.stream(triangle.getNodes()).anyMatch(mesh.get().getBoundingNodes()::contains));
64 | Set boundingNodes = getTrianglePointSet(boundingTriangleStream.collect(Collectors.toList()));
65 | return mesh.get().getNodeRegions().stream().filter(region -> !boundingNodes.contains(region.node)).map(pointRegion -> pointRegion.region).collect(Collectors.toList());
66 | }
67 |
68 | public Set getTrianglePointSet(List triangles) {
69 | return triangles.stream().flatMap(triangle -> Arrays.stream(triangle.getNodes())).collect(Collectors.toSet());
70 | }
71 |
72 | public List getValidTriangles() {
73 | return mesh.get().getTriangles().stream().filter(this::isTriangleValid).collect(Collectors.toList());
74 | }
75 |
76 | private boolean isTriangleValid(Triangle triangle) {
77 | List boundingNodes = mesh.get().getBoundingNodes();
78 | return Arrays.stream(triangle.getNodes()).noneMatch(boundingNodes::contains);
79 | }
80 |
81 | Point getSeparateNode(Triangle from, Triangle with) {
82 | return from.getNodes()[getSeparateNodeIndex(from, with)];
83 | }
84 |
85 | int getSeparateNodeIndex(Triangle from, Triangle with) {
86 | List nodes = Arrays.asList(with.getNodes());
87 | for (int i = 0; i < from.getNodes().length; i++) {
88 | if (!nodes.contains(from.getNodes()[i])) {
89 | return i;
90 | }
91 | }
92 | LOGGER.warning("No separate node found");
93 | return -1;
94 | }
95 |
96 | void bindTrianglesBothWays(Triangle a, int aIndex, Triangle b, Triangle bIndex) {
97 | a.getTriangles()[aIndex] = b;
98 | if (b == null) {
99 | return;
100 | }
101 | int index = Arrays.asList(b.getTriangles()).indexOf(bIndex);
102 | if (index == -1) {
103 | LOGGER.warning(() -> String.format("Triangle %s is not an neighbour of %s", bIndex, b));
104 | return;
105 | }
106 | b.getTriangles()[index] = a;
107 | }
108 |
109 | double dMatrixDet(Point a, Point b, Point c) {
110 | return a.getX() * b.getY() + a.getY() * c.getX() + b.getX() * c.getY() - a.getY() * b.getX() - b.getY() * c.getX() - c.getY() * a.getX();
111 | }
112 |
113 | double hMatrixDet(Point a, Point b, Point c, Point d) {
114 | return a.getX() * a.getX() * b.getX() * c.getY() - a.getX() * a.getX() * b.getX() * d.getY() + a.getX() * a.getX() * (-b.getY()) * c.getX() + a.getX() * a.getX() * b.getY() * d.getX() + a.getX() * a.getX() * c.getX() * d.getY() - a.getX() * a.getX() * c.getY() * d.getX()
115 | - a.getX() * b.getX() * b.getX() * c.getY() + a.getX() * b.getX() * b.getX() * d.getY() - a.getX() * b.getY() * b.getY() * c.getY() + a.getX() * b.getY() * b.getY() * d.getY() + a.getX() * b.getY() * c.getX() * c.getX() + a.getX() * b.getY() * c.getY() * c.getY()
116 | - a.getX() * b.getY() * d.getX() * d.getX() - a.getX() * b.getY() * d.getY() * d.getY() - a.getX() * c.getX() * c.getX() * d.getY() - a.getX() * c.getY() * c.getY() * d.getY() + a.getX() * c.getY() * d.getX() * d.getX() + a.getX() * c.getY() * d.getY() * d.getY()
117 | + a.getY() * a.getY() * b.getX() * c.getY() - a.getY() * a.getY() * b.getX() * d.getY() - a.getY() * a.getY() * b.getY() * c.getX() + a.getY() * a.getY() * b.getY() * d.getX() + a.getY() * a.getY() * c.getX() * d.getY() - a.getY() * a.getY() * c.getY() * d.getX()
118 | + a.getY() * b.getX() * b.getX() * c.getX() - a.getY() * b.getX() * b.getX() * d.getX() - a.getY() * b.getX() * c.getX() * c.getX() - a.getY() * b.getX() * c.getY() * c.getY() + a.getY() * b.getX() * d.getX() * d.getX() + a.getY() * b.getX() * d.getY() * d.getY()
119 | + a.getY() * b.getY() * b.getY() * c.getX() - a.getY() * b.getY() * b.getY() * d.getX() + a.getY() * c.getX() * c.getX() * d.getX() - a.getY() * c.getX() * d.getX() * d.getX() - a.getY() * c.getX() * d.getY() * d.getY() + a.getY() * c.getY() * c.getY() * d.getX()
120 | - b.getX() * b.getX() * c.getX() * d.getY() + b.getX() * b.getX() * c.getY() * d.getX() + b.getX() * c.getX() * c.getX() * d.getY() + b.getX() * c.getY() * c.getY() * d.getY() - b.getX() * c.getY() * d.getX() * d.getX() - b.getX() * c.getY() * d.getY() * d.getY()
121 | - b.getY() * b.getY() * c.getX() * d.getY() + b.getY() * b.getY() * c.getY() * d.getX() - b.getY() * c.getX() * c.getX() * d.getX() + b.getY() * c.getX() * d.getX() * d.getX() + b.getY() * c.getX() * d.getY() * d.getY() - b.getY() * c.getY() * c.getY() * d.getX();
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/TriangulationService.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 |
5 | public interface TriangulationService {
6 |
7 | void createNewMesh();
8 |
9 | boolean addNode(Point location);
10 |
11 | boolean removeNode(Point location);
12 |
13 | Point moveNode(Point node, Point position);
14 |
15 | Point findNodeByLocation(Point location);
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/generation/VoronoiDiagramService.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.generation;
2 |
3 | import com.google.inject.Inject;
4 | import dev.sgora.mesheditor.model.geom.Mesh;
5 | import dev.sgora.mesheditor.model.geom.Point;
6 | import dev.sgora.mesheditor.model.project.CanvasData;
7 | import io.github.stasgora.observetree.SettableObservable;
8 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
9 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
10 |
11 | import java.util.*;
12 | import java.util.logging.Logger;
13 |
14 | public class VoronoiDiagramService {
15 | private final Logger logger = Logger.getLogger(getClass().getName());
16 |
17 | private final SettableObservable mesh;
18 | private final NodeUtils nodeUtils;
19 |
20 | @Inject
21 | VoronoiDiagramService(CanvasData canvasData, NodeUtils nodeUtils) {
22 | this.mesh = canvasData.mesh;
23 | this.nodeUtils = nodeUtils;
24 | }
25 |
26 | public void generateDiagram(Collection nodes) {
27 | Map triangleCircumcenterMap = new HashMap<>();
28 | for (Point node : nodes) {
29 | Polygon pointRegion = mesh.get().getPointRegion(node);
30 | if (pointRegion == null) {
31 | if (!mesh.get().getBoundingNodes().contains(node))
32 | logger.warning("Mesh does not contain given node");
33 | continue;
34 | }
35 | List triangles = new ArrayList<>();
36 | nodeUtils.getNodeNeighbours(node, nodeUtils.findNodeTriangle(node), null, triangles);
37 | List vertices = new ArrayList<>();
38 | for (Triangle triangle : triangles) {
39 | if (!triangleCircumcenterMap.containsKey(triangle)) {
40 | vertices.add(triangle.circumcenter());
41 | triangleCircumcenterMap.put(triangle, vertices.get(vertices.size() - 1));
42 | } else {
43 | vertices.add(triangleCircumcenterMap.get(triangle));
44 | }
45 | }
46 | pointRegion.setNodes(vertices.toArray(Point[]::new));
47 | }
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/rendering/CanvasMeshRenderer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.rendering;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Point;
6 | import dev.sgora.mesheditor.model.paint.SerializableColor;
7 | import dev.sgora.mesheditor.model.project.VisualProperties;
8 | import dev.sgora.mesheditor.services.drawing.ColorUtils;
9 | import javafx.scene.canvas.GraphicsContext;
10 | import javafx.scene.paint.Color;
11 | import javafx.scene.shape.StrokeLineCap;
12 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
13 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
14 | import dev.sgora.mesheditor.services.mesh.generation.NodeUtils;
15 | import dev.sgora.mesheditor.services.mesh.generation.TriangleUtils;
16 |
17 | import java.util.Arrays;
18 |
19 | @Singleton
20 | class CanvasMeshRenderer extends MeshRenderer implements CanvasRenderer {
21 | private GraphicsContext context;
22 | private final NodeUtils nodeUtils;
23 |
24 | @Inject
25 | CanvasMeshRenderer(TriangleUtils triangleUtils, NodeUtils nodeUtils, ColorUtils colorUtils, VisualProperties visualProperties) {
26 | super(colorUtils, triangleUtils, visualProperties);
27 | this.nodeUtils = nodeUtils;
28 | }
29 |
30 | @Override
31 | public void setContext(GraphicsContext context) {
32 | this.context = context;
33 | }
34 |
35 | @Override
36 | protected void drawEdge(Point from, Point to, SerializableColor color) {
37 | context.setStroke(color.toFXColor());
38 | createPath(new Point[]{from, to});
39 | context.stroke();
40 | }
41 |
42 | @Override
43 | protected void drawPoint(Point point, double radius, SerializableColor color) {
44 | context.setFill(color.toFXColor());
45 | radius /= nodeUtils.proportionalScaleFactor();
46 | point = nodeUtils.proportionalToCanvasPos(point);
47 | context.fillOval(point.getX() - radius / 2d, point.getY() - radius / 2d, radius, radius);
48 | }
49 |
50 | @Override
51 | protected void drawPolygon(Polygon polygon, SerializableColor color) {
52 | context.setFill(color.toFXColor());
53 | createPath(polygon.getNodes());
54 | context.fill();
55 | }
56 |
57 | @Override
58 | protected void setUpEdgeDrawing(double thickness) {
59 | context.setLineCap(StrokeLineCap.ROUND);
60 | thickness /= nodeUtils.proportionalScaleFactor();
61 | context.setLineWidth(thickness);
62 | }
63 |
64 | @Override
65 | public void drawBoundingBox(Rectangle boundingBox) {
66 | context.setStroke(Color.gray(0.4));
67 | context.setLineDashes(10, 15);
68 | context.strokeRect(boundingBox.getPosition().getX(), boundingBox.getPosition().getY(), boundingBox.getSize().getX(), boundingBox.getSize().getY());
69 | context.setLineDashes(0);
70 | }
71 |
72 | private void createPath(Point[] vertices) {
73 | vertices = Arrays.stream(vertices).map(nodeUtils::proportionalToCanvasPos).toArray(Point[]::new);
74 | context.beginPath();
75 | context.moveTo(vertices[0].getX(), vertices[0].getY());
76 | for (int i = 1; i < vertices.length; i++) {
77 | context.lineTo(vertices[i].getX(), vertices[i].getY());
78 | }
79 | context.closePath();
80 | context.fill();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/rendering/CanvasRenderer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.rendering;
2 |
3 | import javafx.scene.canvas.GraphicsContext;
4 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
5 |
6 | public interface CanvasRenderer {
7 | void render();
8 | void drawBoundingBox(Rectangle boundingBox);
9 | void setContext(GraphicsContext context);
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/rendering/JFreeSvgMeshRenderer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.rendering;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.model.geom.Point;
6 | import dev.sgora.mesheditor.model.paint.SerializableColor;
7 | import dev.sgora.mesheditor.model.project.VisualProperties;
8 | import dev.sgora.mesheditor.services.drawing.ColorUtils;
9 | import org.jfree.graphics2d.svg.SVGGraphics2D;
10 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
11 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
12 | import dev.sgora.mesheditor.services.mesh.generation.NodeUtils;
13 | import dev.sgora.mesheditor.services.mesh.generation.TriangleUtils;
14 |
15 | import java.awt.*;
16 |
17 | @Singleton
18 | class JFreeSvgMeshRenderer extends MeshRenderer implements SvgMeshRenderer {
19 | private final NodeUtils nodeUtils;
20 | private SVGGraphics2D graphics;
21 |
22 | @Inject
23 | JFreeSvgMeshRenderer(TriangleUtils triangleUtils, NodeUtils nodeUtils, ColorUtils colorUtils, VisualProperties visualProperties) {
24 | super(colorUtils, triangleUtils, visualProperties);
25 | this.nodeUtils = nodeUtils;
26 | }
27 |
28 | public String renderSvg() {
29 | render();
30 | return graphics.getSVGDocument();
31 | }
32 |
33 | @Override
34 | protected void prepareRendering() {
35 | Rectangle boundingBox = nodeUtils.getProportionalNodeBoundingBox();
36 | graphics = new SVGGraphics2D((int) boundingBox.getSize().getX(), (int) boundingBox.getSize().getY());
37 | graphics.setBackground(new Color(1f, 1f, 1f, 0f));
38 | dev.sgora.mesheditor.model.geom.Point marginSize = nodeUtils.getProportionalMarginSize();
39 | graphics.translate(marginSize.getX(), marginSize.getY());
40 | }
41 |
42 | @Override
43 | protected void drawEdge(dev.sgora.mesheditor.model.geom.Point from, dev.sgora.mesheditor.model.geom.Point to, SerializableColor color) {
44 | graphics.setColor(color.toAwtColor());
45 | graphics.drawLine((int) from.getX(), (int) from.getY(), (int) to.getX(), (int) to.getY());
46 | }
47 |
48 | @Override
49 | protected void drawPoint(Point point, double radius, SerializableColor color) {
50 | graphics.setColor(color.toAwtColor());
51 | graphics.fillOval((int) (point.getX() - radius / 2), (int) (point.getY() - radius / 2), (int) radius, (int) radius);
52 | }
53 |
54 | @Override
55 | protected void drawPolygon(Polygon polygon, SerializableColor color) {
56 | graphics.setColor(color.toAwtColor());
57 | graphics.fillPolygon(polygon.xCoords(), polygon.yCoords(), polygon.getNodes().length);
58 | }
59 |
60 | @Override
61 | protected void setUpEdgeDrawing(double thickness) {
62 | graphics.setStroke(new BasicStroke((float) thickness));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/rendering/MeshRenderer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.rendering;
2 |
3 | import dev.sgora.mesheditor.model.geom.Point;
4 | import dev.sgora.mesheditor.model.paint.SerializableColor;
5 | import dev.sgora.mesheditor.model.project.VisualProperties;
6 | import dev.sgora.mesheditor.services.drawing.ColorUtils;
7 | import dev.sgora.mesheditor.model.geom.polygons.Polygon;
8 | import dev.sgora.mesheditor.model.geom.polygons.Triangle;
9 | import dev.sgora.mesheditor.model.project.MeshLayer;
10 | import dev.sgora.mesheditor.services.mesh.generation.TriangleUtils;
11 |
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | public abstract class MeshRenderer {
16 | private final ColorUtils colorUtils;
17 | private final TriangleUtils triangleUtils;
18 | private VisualProperties visualProperties;
19 |
20 | protected MeshRenderer(ColorUtils colorUtils, TriangleUtils triangleUtils, VisualProperties visualProperties) {
21 | this.colorUtils = colorUtils;
22 | this.triangleUtils = triangleUtils;
23 | this.visualProperties = visualProperties;
24 | }
25 |
26 | public void render() {
27 | if (!visualProperties.meshVisible.get())
28 | return;
29 | prepareRendering();
30 |
31 | List regions = triangleUtils.getValidVoronoiRegions();
32 | List triangles = triangleUtils.getValidTriangles();
33 | MeshLayer voronoiDiagramLayer = visualProperties.voronoiDiagramLayer.get();
34 | MeshLayer triangulationLayer = visualProperties.triangulationLayer.get();
35 |
36 | if (voronoiDiagramLayer.layerVisible.get())
37 | drawPolygons(regions, voronoiDiagramLayer);
38 | if (triangulationLayer.layerVisible.get())
39 | drawPolygons(triangles, triangulationLayer);
40 | if (voronoiDiagramLayer.layerVisible.get())
41 | drawEdges(regions, voronoiDiagramLayer);
42 | if (triangulationLayer.layerVisible.get())
43 | drawEdges(triangles, triangulationLayer);
44 | if (voronoiDiagramLayer.layerVisible.get())
45 | drawNodes(regions, voronoiDiagramLayer);
46 | if (triangulationLayer.layerVisible.get())
47 | drawNodes(triangles, triangulationLayer);
48 | }
49 |
50 | protected abstract void drawEdge(Point from, Point to, SerializableColor color);
51 |
52 | protected abstract void drawPoint(Point point, double radius, SerializableColor color);
53 |
54 | protected abstract void drawPolygon(Polygon polygon, SerializableColor color);
55 |
56 | protected abstract void setUpEdgeDrawing(double thickness);
57 |
58 | protected void prepareRendering() {
59 | }
60 |
61 | private void drawNodes(List extends Polygon> polygons, MeshLayer layer) {
62 | if (!layer.nodesVisible.get())
63 | return;
64 | List drawnPoints = new ArrayList<>();
65 | double transparency = visualProperties.meshTransparency.get() * layer.layerTransparency.get();
66 | for (Polygon polygon : polygons) {
67 | for (Point vertex : polygon.getNodes()) {
68 | if (drawnPoints.contains(vertex))
69 | continue;
70 | drawPoint(vertex, layer.nodeRadius.get(), colorUtils.getNodeColor(vertex).setAlpha(transparency));
71 | drawnPoints.add(vertex);
72 | }
73 | }
74 | }
75 |
76 | private void drawPolygons(List extends Polygon> polygons, MeshLayer layer) {
77 | if (!layer.polygonsVisible.get())
78 | return;
79 | double transparency = visualProperties.meshTransparency.get() * layer.layerTransparency.get();
80 | polygons.forEach(polygon -> drawPolygon(polygon, colorUtils.getPolygonColor(polygon.getNodes()).setAlpha(transparency)));
81 | }
82 |
83 | private void drawEdges(List extends Polygon> polygons, MeshLayer layer) {
84 | if (!layer.edgesVisible.get())
85 | return;
86 | setUpEdgeDrawing(layer.edgeThickness.get());
87 | double transparency = visualProperties.meshTransparency.get() * layer.layerTransparency.get();
88 | for (Polygon polygon : polygons) { // FIXME drawing twice most lines
89 | Point[] nodes = polygon.getNodes();
90 | for (int i = 0; i < nodes.length; i++) {
91 | int nextIndex = (i + 1) % nodes.length;
92 | drawEdge(nodes[i], nodes[nextIndex], colorUtils.getEdgeColor(nodes[i], nodes[nextIndex]).setAlpha(transparency));
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/rendering/MeshRenderingModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.rendering;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class MeshRenderingModule extends AbstractModule {
6 | @Override
7 | protected void configure() {
8 | bind(SvgMeshRenderer.class).to(JFreeSvgMeshRenderer.class);
9 | bind(CanvasRenderer.class).to(CanvasMeshRenderer.class);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/mesh/rendering/SvgMeshRenderer.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.mesh.rendering;
2 |
3 | public interface SvgMeshRenderer {
4 | String renderSvg();
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/ui/PropertyTreeCellFactory.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.ui;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import javafx.collections.FXCollections;
6 | import javafx.geometry.Pos;
7 | import javafx.scene.control.*;
8 | import javafx.scene.layout.HBox;
9 | import javafx.util.Callback;
10 | import javafx.util.StringConverter;
11 | import dev.sgora.mesheditor.ui.properties.TextKeyProvider;
12 | import dev.sgora.mesheditor.model.observables.BindableProperty;
13 | import dev.sgora.mesheditor.model.project.MeshLayer;
14 | import dev.sgora.mesheditor.model.project.VisualProperties;
15 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
16 | import dev.sgora.mesheditor.services.config.LangConfigReader;
17 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
18 | import dev.sgora.mesheditor.services.history.ActionHistoryService;
19 | import dev.sgora.mesheditor.services.history.actions.property.CheckBoxChangeAction;
20 | import dev.sgora.mesheditor.services.history.actions.property.PropertyChangeAction;
21 | import dev.sgora.mesheditor.ui.properties.PropertyItemType;
22 | import dev.sgora.mesheditor.ui.properties.PropertyTreeItem;
23 |
24 | import java.util.Arrays;
25 | import java.util.HashMap;
26 | import java.util.Map;
27 | import java.util.function.Function;
28 |
29 | @Singleton
30 | public class PropertyTreeCellFactory implements Callback, TreeCell> {
31 | private static final String PROPERTY_STYLE_CLASS = "property-item";
32 |
33 | private LangConfigReader appLang;
34 | private AppConfigReader appConfig;
35 | private VisualProperties visualProperties;
36 | private ActionHistoryService actionHistoryService;
37 |
38 | private Map> propertyTypeToVisibleValue;
39 | private Map> propertyTypeToSliderValue;
40 | private Map> propertyTypeToComboBoxValue;
41 |
42 | @Inject
43 | PropertyTreeCellFactory(LangConfigReader appLang, @AppConfig AppConfigReader appConfig, VisualProperties visualProperties, ActionHistoryService actionHistoryService) {
44 | this.appLang = appLang;
45 | this.appConfig = appConfig;
46 | this.visualProperties = visualProperties;
47 | this.actionHistoryService = actionHistoryService;
48 |
49 | initPropertyMaps();
50 | }
51 |
52 | private void initPropertyMaps() {
53 | propertyTypeToVisibleValue = Map.of(
54 | PropertyItemType.IMAGE, item -> visualProperties.imageVisible,
55 | PropertyItemType.MESH, item -> visualProperties.meshVisible,
56 | PropertyItemType.TRIANGULATION, item -> visualProperties.triangulationLayer.get().layerVisible,
57 | PropertyItemType.VORONOI_DIAGRAM, item -> visualProperties.voronoiDiagramLayer.get().layerVisible,
58 | PropertyItemType.POLYGONS, item -> getPropertyLayer(item).polygonsVisible,
59 | PropertyItemType.NODES, item -> getPropertyLayer(item).nodesVisible,
60 | PropertyItemType.EDGES, item -> getPropertyLayer(item).edgesVisible
61 | );
62 | propertyTypeToSliderValue = Map.of(
63 | PropertyItemType.IMAGE, item -> visualProperties.imageTransparency,
64 | PropertyItemType.MESH, item -> visualProperties.meshTransparency,
65 | PropertyItemType.TRIANGULATION, item -> visualProperties.triangulationLayer.get().layerTransparency,
66 | PropertyItemType.VORONOI_DIAGRAM, item -> visualProperties.voronoiDiagramLayer.get().layerTransparency,
67 | PropertyItemType.NODES, item -> getPropertyLayer(item).nodeRadius,
68 | PropertyItemType.EDGES, item -> getPropertyLayer(item).edgeThickness
69 | );
70 | propertyTypeToComboBoxValue = new HashMap<>();
71 | }
72 |
73 | private MeshLayer getPropertyLayer(PropertyTreeItem item) {
74 | return ((PropertyTreeItem) item.getParent()).getItemType() == PropertyItemType.TRIANGULATION ?
75 | visualProperties.triangulationLayer.get() : visualProperties.voronoiDiagramLayer.get();
76 | }
77 |
78 | @Override
79 | public TreeCell call(TreeView param) {
80 | return new TreeCell<>() {
81 | @Override
82 | public void updateItem(String value, boolean empty) {
83 | super.updateItem(value, empty);
84 | getStyleClass().add(PROPERTY_STYLE_CLASS);
85 | if (empty) {
86 | setGraphic(null);
87 | setText(null);
88 | return;
89 | }
90 | String itemLabelText = value;
91 | if (getTreeItem() instanceof PropertyTreeItem)
92 | itemLabelText = appLang.getText(((PropertyTreeItem) getTreeItem()).getItemType().getTextKey());
93 |
94 | HBox body = new HBox(new Label(itemLabelText));
95 | setText(null);
96 | setGraphic(body);
97 |
98 | body.setSpacing(5);
99 | body.setAlignment(Pos.CENTER_LEFT);
100 |
101 | if (!(getTreeItem() instanceof PropertyTreeItem))
102 | return;
103 |
104 | PropertyTreeItem treeItem = (PropertyTreeItem) getTreeItem();
105 | if (treeItem.isHasCheckBox())
106 | addCheckBox(treeItem, body);
107 | if (treeItem.isHasSlider())
108 | addSlider(treeItem, body);
109 | }
110 |
111 | private void addCheckBox(PropertyTreeItem treeItem, HBox body) {
112 | CheckBox checkBox = new CheckBox();
113 | checkBox.setOnAction(event -> actionHistoryService.registerAction(new CheckBoxChangeAction(checkBox.isSelected(), checkBox::setSelected)));
114 | checkBox.setTooltip(new Tooltip(appLang.getText("fxml.properties.tooltips.visibility")));
115 | propertyTypeToVisibleValue.get(treeItem.getItemType()).apply(treeItem).bindWithFxObservable(checkBox.selectedProperty());
116 | body.getChildren().add(0, checkBox);
117 | }
118 |
119 | private void addSlider(PropertyTreeItem treeItem, HBox body) {
120 | double minValue = getSliderConfigValue(treeItem.getItemType().getMinValueKey(), 0);
121 | Slider slider = new Slider(minValue, getSliderConfigValue(treeItem.getItemType().getMaxValueKey(), 1), minValue);
122 | slider.setTooltip(new Tooltip(appLang.getText(treeItem.getItemType().getSliderKey())));
123 | propertyTypeToSliderValue.get(treeItem.getItemType()).apply(treeItem).bindWithFxObservable(slider.valueProperty());
124 |
125 | slider.valueChangingProperty().addListener((observable, oldChanging, changing) -> {
126 | if (changing)
127 | treeItem.setSliderChangeStartValue(slider.getValue());
128 | else
129 | actionHistoryService.registerAction(new PropertyChangeAction<>(slider.getValue(), treeItem.getSliderChangeStartValue(), slider::setValue));
130 | });
131 | body.getChildren().add(slider);
132 | }
133 |
134 | private & TextKeyProvider> void addComboBox(Class enumType, PropertyTreeItem treeItem, HBox body) {
135 | ComboBox comboBox = new ComboBox<>();
136 | comboBox.setItems(FXCollections.observableArrayList(enumType.getEnumConstants()));
137 | comboBox.setConverter(new StringConverter<>() {
138 | @Override
139 | public String toString(T meshType) {
140 | return appLang.getText(meshType.getTextKey());
141 | }
142 |
143 | @Override
144 | public T fromString(String s) {
145 | return Arrays.stream(enumType.getEnumConstants()).filter(type -> type.name().equals(s)).findFirst().orElse(null);
146 | }
147 | });
148 | propertyTypeToComboBoxValue.get(treeItem.getItemType()).apply(treeItem).bindWithFxObservable(comboBox.valueProperty());
149 |
150 | body.getChildren().add(comboBox);
151 | }
152 |
153 | private double getSliderConfigValue(String key, double defaultValue) {
154 | return key != null ? appConfig.getDouble(key) : defaultValue;
155 | }
156 | };
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/ui/UIModule.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.ui;
2 |
3 | import com.google.inject.AbstractModule;
4 |
5 | public class UIModule extends AbstractModule { }
6 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/services/ui/UiDialogUtils.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.services.ui;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import javafx.event.EventHandler;
6 | import javafx.scene.control.Alert;
7 | import javafx.scene.control.Button;
8 | import javafx.scene.control.ButtonType;
9 | import javafx.scene.control.DialogPane;
10 | import javafx.scene.input.KeyCode;
11 | import javafx.scene.input.KeyEvent;
12 | import javafx.stage.FileChooser;
13 | import javafx.stage.Stage;
14 | import dev.sgora.mesheditor.services.config.LangConfigReader;
15 | import dev.sgora.mesheditor.services.files.workspace.FileChooserAction;
16 | import dev.sgora.mesheditor.view.annotation.MainWindowStage;
17 |
18 | import java.io.File;
19 | import java.util.Optional;
20 |
21 | @Singleton
22 | public class UiDialogUtils {
23 |
24 | private Stage window;
25 | private LangConfigReader appLang;
26 |
27 | private EventHandler pressOnEnter = event -> {
28 | if (KeyCode.ENTER.equals(event.getCode()) && event.getTarget() instanceof Button) {
29 | ((Button) event.getTarget()).fire();
30 | }
31 | };
32 |
33 | @Inject
34 | UiDialogUtils(@MainWindowStage Stage window, LangConfigReader appLang) {
35 | this.window = window;
36 | this.appLang = appLang;
37 | }
38 |
39 | public File showFileChooser(FileChooserAction action, String title, File initialDirectory, FileChooser.ExtensionFilter extensionFilter) {
40 | FileChooser projectFileChooser = new FileChooser();
41 | projectFileChooser.setTitle(title);
42 | if(initialDirectory != null && initialDirectory.isDirectory())
43 | projectFileChooser.setInitialDirectory(initialDirectory);
44 | projectFileChooser.getExtensionFilters().addAll(extensionFilter, getDefaultFilter());
45 | if (action == FileChooserAction.SAVE_DIALOG) {
46 | return projectFileChooser.showSaveDialog(window);
47 | } else if (action == FileChooserAction.OPEN_DIALOG) {
48 | return projectFileChooser.showOpenDialog(window);
49 | }
50 | return null;
51 | }
52 |
53 | public Optional showWarningDialog(String title, String header, String content, ButtonType[] buttons) {
54 | return showDialog(Alert.AlertType.WARNING, title, header, content, buttons);
55 | }
56 |
57 | public Optional showErrorDialog(String title, String header, String content) {
58 | return showDialog(Alert.AlertType.ERROR, title, header, content, null);
59 | }
60 |
61 | private Optional showDialog(Alert.AlertType type, String title, String header, String content, ButtonType[] buttons) {
62 | Alert dialog;
63 | if (buttons != null) {
64 | dialog = new Alert(type, content, buttons);
65 | } else {
66 | dialog = new Alert(type, content);
67 | }
68 | DialogPane dialogPane = dialog.getDialogPane();
69 | dialogPane.getButtonTypes().stream().map(dialogPane::lookupButton).forEach(button -> button.addEventHandler(KeyEvent.KEY_PRESSED, pressOnEnter));
70 | dialog.setTitle(title);
71 | dialog.setHeaderText(header);
72 | return dialog.showAndWait();
73 | }
74 |
75 | private FileChooser.ExtensionFilter getDefaultFilter() {
76 | String extensionTitle = appLang.getText("dialog.fileChooser.extension.all");
77 | return new FileChooser.ExtensionFilter(extensionTitle, "*.*");
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/ui/CopyableLabel.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.ui;
2 |
3 | import javafx.scene.control.Label;
4 |
5 | public class CopyableLabel extends Label {
6 |
7 | public CopyableLabel() {
8 | }
9 |
10 | public CopyableLabel(CopyableLabel label) {
11 | setFont(label.getFont());
12 | setTextFill(label.getTextFill());
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/ui/canvas/ImageCanvas.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.ui.canvas;
2 |
3 | import dev.sgora.mesheditor.model.project.CanvasData;
4 | import io.github.stasgora.observetree.SettableProperty;
5 | import dev.sgora.mesheditor.model.geom.polygons.Rectangle;
6 |
7 | public class ImageCanvas extends ResizableCanvas {
8 |
9 | private CanvasData canvasData;
10 | private SettableProperty imageTransparency;
11 |
12 | public void init(CanvasData canvasData, SettableProperty imageTransparency) {
13 | this.canvasData = canvasData;
14 | this.imageTransparency = imageTransparency;
15 | }
16 |
17 | public void draw() {
18 | if (!isVisible()) {
19 | return;
20 | }
21 | double alpha = imageTransparency.get();
22 | context.setGlobalAlpha(alpha);
23 | Rectangle imageBox = canvasData.imageBox;
24 | context.drawImage(canvasData.baseImage.get(), imageBox.getPosition().getX(), imageBox.getPosition().getY(), imageBox.getSize().getX(), imageBox.getSize().getY());
25 | context.setGlobalAlpha(1);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/ui/canvas/ResizableCanvas.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.ui.canvas;
2 |
3 | import javafx.scene.canvas.Canvas;
4 | import javafx.scene.canvas.GraphicsContext;
5 |
6 | public class ResizableCanvas extends Canvas {
7 |
8 | protected GraphicsContext context = getGraphicsContext2D();
9 |
10 | @Override
11 | public boolean isResizable() {
12 | return true;
13 | }
14 |
15 | public void clear() {
16 | context.clearRect(0, 0, getWidth(), getHeight());
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/ui/properties/PropertyItemType.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.ui.properties;
2 |
3 | public enum PropertyItemType implements TextKeyProvider {
4 |
5 | IMAGE("image", "transparency"),
6 | MESH("mesh.title", "transparency"),
7 | TRIANGULATION("mesh.layer.triangulation", "transparency"),
8 | VORONOI_DIAGRAM("mesh.layer.voronoiDiagram", "transparency"),
9 |
10 | POLYGONS("mesh.polygons"),
11 | EDGES("mesh.edges", "edgeThickness", "meshBox.edgeThickness"),
12 | NODES("mesh.nodes", "nodeRadius", "meshBox.nodeRadius");
13 |
14 | private static final String KEY_PREFIX = "fxml.properties.";
15 | private static final String TEXT_KEY_PREFIX = KEY_PREFIX + "tree.";
16 | private static final String SLIDER_KEY_PREFIX = KEY_PREFIX + "tooltips.";
17 |
18 | private final String textKey;
19 | private String tooltipKey;
20 | private String valueKey;
21 |
22 | PropertyItemType(String textKey, String tooltipKey) {
23 | this.textKey = textKey;
24 | this.tooltipKey = tooltipKey;
25 | }
26 |
27 | PropertyItemType(String textKey, String tooltipKey, String valueKey) {
28 | this.textKey = textKey;
29 | this.tooltipKey = tooltipKey;
30 | this.valueKey = valueKey;
31 | }
32 |
33 | PropertyItemType(String textKey) {
34 | this.textKey = textKey;
35 | }
36 |
37 | @Override
38 | public String getTextKey() {
39 | return TEXT_KEY_PREFIX + textKey;
40 | }
41 |
42 | public String getSliderKey() {
43 | return SLIDER_KEY_PREFIX + tooltipKey;
44 | }
45 |
46 | public String getMinValueKey() {
47 | if (valueKey == null) return null;
48 | return valueKey + ".min";
49 | }
50 |
51 | public String getMaxValueKey() {
52 | if (valueKey == null) return null;
53 | return valueKey + ".max";
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/ui/properties/PropertyTreeItem.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.ui.properties;
2 |
3 | import javafx.scene.control.TreeItem;
4 |
5 | public class PropertyTreeItem extends TreeItem {
6 | private PropertyItemType itemType;
7 | private boolean hasCheckBox = true;
8 | private boolean hasSlider;
9 | private boolean hasComboBox;
10 |
11 | private double sliderChangeStartValue;
12 |
13 | public PropertyItemType getItemType() {
14 | return itemType;
15 | }
16 |
17 | public void setItemType(PropertyItemType itemType) {
18 | this.itemType = itemType;
19 | }
20 |
21 | public boolean isHasCheckBox() {
22 | return hasCheckBox;
23 | }
24 |
25 | public void setHasCheckBox(boolean hasCheckBox) {
26 | this.hasCheckBox = hasCheckBox;
27 | }
28 |
29 | public boolean isHasSlider() {
30 | return hasSlider;
31 | }
32 |
33 | public void setHasSlider(boolean hasSlider) {
34 | this.hasSlider = hasSlider;
35 | }
36 |
37 | public boolean isHasComboBox() {
38 | return hasComboBox;
39 | }
40 |
41 | public void setHasComboBox(boolean hasComboBox) {
42 | this.hasComboBox = hasComboBox;
43 | }
44 |
45 | public double getSliderChangeStartValue() {
46 | return sliderChangeStartValue;
47 | }
48 |
49 | public void setSliderChangeStartValue(double sliderChangeStartValue) {
50 | this.sliderChangeStartValue = sliderChangeStartValue;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/ui/properties/TextKeyProvider.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.ui.properties;
2 |
3 | public interface TextKeyProvider {
4 | public String getTextKey();
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/view/AboutWindow.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.view;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.Singleton;
5 | import dev.sgora.mesheditor.MeshEditor;
6 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
7 | import dev.sgora.mesheditor.view.annotation.MainWindowStage;
8 | import javafx.fxml.FXML;
9 | import javafx.fxml.FXMLLoader;
10 | import javafx.scene.image.Image;
11 | import javafx.scene.image.ImageView;
12 | import javafx.stage.Popup;
13 | import javafx.stage.Stage;
14 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
15 |
16 | import java.io.IOException;
17 | import java.util.logging.Level;
18 | import java.util.logging.Logger;
19 |
20 | @Singleton
21 | public class AboutWindow {
22 | private final Logger logger = Logger.getLogger(getClass().getName());
23 |
24 | private final Popup aboutWindowPopup = new Popup();
25 | private final Stage mainWindow;
26 | @FXML
27 | private ImageView logo;
28 |
29 | @Inject
30 | AboutWindow(@MainWindowStage Stage mainWindow, @AppConfig AppConfigReader appConfig) {
31 | this.mainWindow = mainWindow;
32 |
33 | FXMLLoader loader = new FXMLLoader(MeshEditor.class.getResource("/fxml/AboutWindow.fxml"));
34 | loader.setController(this);
35 | loader.getNamespace().put("app_version", appConfig.getString("app.version"));
36 | loader.getNamespace().put("app_name", appConfig.getString("app.name"));
37 | try {
38 | aboutWindowPopup.getContent().add(loader.load());
39 | } catch (IOException e) {
40 | logger.log(Level.SEVERE, "Loading about page failed", e);
41 | }
42 | mainWindow.focusedProperty().addListener(((observable, oldValue, newValue) -> aboutWindowPopup.hide()));
43 | aboutWindowPopup.setAutoHide(true);
44 |
45 | logo.setImage(new Image(MeshEditor.class.getResourceAsStream("/logo.png")));
46 | }
47 |
48 | public void show() {
49 | aboutWindowPopup.show(mainWindow);
50 | aboutWindowPopup.centerOnScreen();
51 | aboutWindowPopup.requestFocus();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/view/CanvasView.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.view;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.assistedinject.Assisted;
5 | import dev.sgora.mesheditor.model.geom.Point;
6 | import dev.sgora.mesheditor.model.project.CanvasData;
7 | import dev.sgora.mesheditor.model.project.LoadState;
8 | import dev.sgora.mesheditor.model.project.VisualProperties;
9 | import dev.sgora.mesheditor.services.drawing.ImageBox;
10 | import io.github.stasgora.observetree.enums.ListenerPriority;
11 | import javafx.beans.value.ObservableValue;
12 | import javafx.fxml.FXML;
13 | import javafx.scene.layout.Region;
14 | import dev.sgora.mesheditor.model.NamespaceMap;
15 | import dev.sgora.mesheditor.model.project.CanvasUI;
16 | import dev.sgora.mesheditor.services.input.CanvasAction;
17 | import dev.sgora.mesheditor.services.mesh.generation.NodeUtils;
18 | import dev.sgora.mesheditor.services.mesh.rendering.CanvasRenderer;
19 | import dev.sgora.mesheditor.ui.canvas.ImageCanvas;
20 | import dev.sgora.mesheditor.ui.canvas.ResizableCanvas;
21 | import dev.sgora.mesheditor.view.sub.SubView;
22 |
23 | public class CanvasView extends SubView {
24 | @FXML
25 | private ImageCanvas imageCanvas;
26 | @FXML
27 | private ResizableCanvas meshCanvas;
28 |
29 | private final VisualProperties visualProperties;
30 | private Point canvasViewSize;
31 |
32 | private final CanvasData canvasData;
33 | private ImageBox imageBox;
34 | private NodeUtils nodeUtils;
35 | private final CanvasAction canvasAction;
36 | private final LoadState loadState;
37 | private final CanvasRenderer canvasMeshRenderer;
38 |
39 | @Inject
40 | CanvasView(@Assisted Region root, @Assisted ViewType viewType, NamespaceMap viewNamespaces, VisualProperties visualProperties, CanvasUI canvasUI,
41 | CanvasData canvasData, ImageBox imageBox, NodeUtils nodeUtils, CanvasAction canvasAction, LoadState loadState, CanvasRenderer canvasMeshRenderer) {
42 | super(root, viewType, viewNamespaces);
43 |
44 | this.visualProperties = visualProperties;
45 | this.canvasViewSize = canvasUI.canvasViewSize;
46 | this.canvasData = canvasData;
47 | this.imageBox = imageBox;
48 | this.nodeUtils = nodeUtils;
49 | this.canvasAction = canvasAction;
50 | this.loadState = loadState;
51 | this.canvasMeshRenderer = canvasMeshRenderer;
52 |
53 | imageCanvas.init(canvasData, visualProperties.imageTransparency);
54 | canvasMeshRenderer.setContext(meshCanvas.getGraphicsContext2D());
55 | init();
56 | }
57 |
58 | @Override
59 | protected void init() {
60 | setListeners();
61 | setMouseHandlers();
62 | }
63 |
64 | private void setListeners() {
65 | root.widthProperty().addListener(this::paneSizeChanged);
66 | root.heightProperty().addListener(this::paneSizeChanged);
67 |
68 | canvasViewSize.addListener(() -> {
69 | imageCanvas.setWidth(canvasViewSize.getX());
70 | imageCanvas.setHeight(canvasViewSize.getY());
71 | meshCanvas.setWidth(canvasViewSize.getX());
72 | meshCanvas.setHeight(canvasViewSize.getY());
73 | });
74 | canvasViewSize.addListener(() -> imageBox.onResizeCanvas());
75 | canvasData.mesh.addStaticListener(() -> loadState.stateSaved.setAndNotify(false));
76 |
77 | canvasViewSize.addListener(this::drawBothLayers);
78 | canvasData.addListener(this::drawBothLayers);
79 | visualProperties.addListener(this::drawMesh);
80 |
81 | visualProperties.addListener(this::drawBothLayers);
82 | loadState.loaded.addListener(() -> {
83 | if (!loadState.loaded.get()) return;
84 | imageBox.calcImageBox();
85 |
86 | }, ListenerPriority.HIGH);
87 | }
88 |
89 | private void setMouseHandlers() {
90 | root.setOnScroll(canvasAction::onScroll);
91 | root.setOnMouseMoved(canvasAction::onMouseMove);
92 |
93 | root.setOnMousePressed(canvasAction::onMousePress);
94 | root.setOnMouseDragged(canvasAction::onMouseDrag);
95 |
96 | root.setOnMouseReleased(canvasAction::onMouseRelease);
97 | root.setOnMouseEntered(canvasAction::onMouseEnter);
98 | root.setOnMouseExited(canvasAction::onMouseExit);
99 | }
100 |
101 | private void paneSizeChanged(ObservableValue extends Number> observable, Number oldVal, Number newVal) {
102 | canvasViewSize.set(new Point(root.getWidth(), root.getHeight()));
103 | canvasViewSize.notifyListeners();
104 | }
105 |
106 | private void drawMesh() {
107 | meshCanvas.clear();
108 | if (loadState.loaded.get() && visualProperties.meshVisible.get()) {
109 | canvasMeshRenderer.render();
110 | canvasMeshRenderer.drawBoundingBox(nodeUtils.getCanvasSpaceNodeBoundingBox());
111 | }
112 | }
113 |
114 | private void drawImage() {
115 | imageCanvas.clear();
116 | if (loadState.loaded.get() && visualProperties.imageVisible.get()) {
117 | imageCanvas.draw();
118 | }
119 | }
120 |
121 | private void drawBothLayers() {
122 | drawImage();
123 | drawMesh();
124 | }
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/dev/sgora/mesheditor/view/MenuView.java:
--------------------------------------------------------------------------------
1 | package dev.sgora.mesheditor.view;
2 |
3 | import com.google.inject.Inject;
4 | import com.google.inject.assistedinject.Assisted;
5 | import dev.sgora.mesheditor.model.project.LoadState;
6 | import javafx.collections.ObservableList;
7 | import javafx.fxml.FXML;
8 | import javafx.scene.control.Menu;
9 | import javafx.scene.control.MenuItem;
10 | import javafx.scene.layout.Region;
11 | import javafx.stage.Stage;
12 | import dev.sgora.mesheditor.model.NamespaceMap;
13 | import dev.sgora.mesheditor.services.config.interfaces.AppConfigReader;
14 | import dev.sgora.mesheditor.services.config.annotation.AppConfig;
15 | import dev.sgora.mesheditor.services.files.FileUtils;
16 | import dev.sgora.mesheditor.services.files.workspace.interfaces.WorkspaceAction;
17 | import dev.sgora.mesheditor.services.history.ActionHistoryService;
18 | import dev.sgora.mesheditor.view.annotation.MainWindowStage;
19 | import dev.sgora.mesheditor.view.sub.SubView;
20 |
21 | import java.io.File;
22 | import java.nio.file.Paths;
23 | import java.util.List;
24 | import java.util.stream.Collectors;
25 |
26 | public class MenuView extends SubView {
27 | @FXML
28 | private MenuItem newProjectMenuItem;
29 | @FXML
30 | private MenuItem openProjectMenuItem;
31 | @FXML
32 | private Menu openRecentMenu;
33 | @FXML
34 | private MenuItem closeProjectMenuItem;
35 | @FXML
36 | private MenuItem saveProjectMenuItem;
37 | @FXML
38 | private MenuItem saveProjectAsMenuItem;
39 | @FXML
40 | private MenuItem exportProjectMenuItem;
41 | @FXML
42 | private MenuItem exitAppMenuItem;
43 |
44 | @FXML
45 | private MenuItem undoMenuItem;
46 | @FXML
47 | private MenuItem redoMenuItem;
48 |
49 | @FXML
50 | private MenuItem aboutMenuItem;
51 |
52 | @FXML
53 | private MenuItem reloadStylesMenuItem;
54 |
55 | private final Stage stage;
56 | private final AppConfigReader appConfig;
57 | private WorkspaceAction workspaceAction;
58 | private final ActionHistoryService actionHistoryService;
59 | private final AboutWindow aboutWindow;
60 | private final FileUtils fileUtils;
61 | private final LoadState loadState;
62 |
63 | private static final String MENU_FILE_ITEM_DISABLED = "menu_file_item_disabled";
64 | private static final String DEBUG_MENU_VISIBLE = "debug_menu_visible";
65 |
66 | @Inject
67 | MenuView(@Assisted Region root, @Assisted ViewType viewType, NamespaceMap viewNamespaces, @MainWindowStage Stage stage, @AppConfig AppConfigReader appConfig,
68 | WorkspaceAction workspaceAction, LoadState loadState, ActionHistoryService actionHistoryService, AboutWindow aboutWindow, FileUtils fileUtils) {
69 | super(root, viewType, viewNamespaces);
70 | this.stage = stage;
71 | this.appConfig = appConfig;
72 | this.workspaceAction = workspaceAction;
73 | this.loadState = loadState;
74 | this.actionHistoryService = actionHistoryService;
75 | this.aboutWindow = aboutWindow;
76 | this.fileUtils = fileUtils;
77 | init();
78 | }
79 |
80 | @Override
81 | protected void init() {
82 | bindMenuItems();
83 |
84 | namespace.put(MENU_FILE_ITEM_DISABLED, true);
85 | namespace.put(DEBUG_MENU_VISIBLE, appConfig.getBool("app.debugMode"));
86 | loadState.loaded.addListener(() -> namespace.put(MENU_FILE_ITEM_DISABLED, !((boolean) namespace.get(MENU_FILE_ITEM_DISABLED))));
87 |
88 | loadState.recentProjects.addListener(this::onRecentProjectsChanged);
89 | onRecentProjectsChanged();
90 | }
91 |
92 | private void onRecentProjectsChanged() {
93 | ObservableList