diff --git a/src/main/java/com/marginallyclever/convenience/helpers/MatrixHelper.java b/src/main/java/com/marginallyclever/convenience/helpers/MatrixHelper.java index 4a2365534..5bb0e9c43 100644 --- a/src/main/java/com/marginallyclever/convenience/helpers/MatrixHelper.java +++ b/src/main/java/com/marginallyclever/convenience/helpers/MatrixHelper.java @@ -803,20 +803,20 @@ public static double[][] createMatrix(int rows, int cols) { public static double [] matrix4dToArray(Matrix4d m) { return new double[] { m.m00, - m.m10, - m.m20, - m.m30, m.m01, - m.m11, - m.m21, - m.m31, m.m02, - m.m12, - m.m22, - m.m32, m.m03, + m.m10, + m.m11, + m.m12, m.m13, + m.m20, + m.m21, + m.m22, m.m23, + m.m30, + m.m31, + m.m32, m.m33, }; } diff --git a/src/main/java/com/marginallyclever/ro3/Factory.java b/src/main/java/com/marginallyclever/ro3/Factory.java index f9bb52ebe..6c5c94dc4 100644 --- a/src/main/java/com/marginallyclever/ro3/Factory.java +++ b/src/main/java/com/marginallyclever/ro3/Factory.java @@ -31,6 +31,11 @@ public Category(String name,Supplier supplier) { this.supplier = supplier; } + public Category(Supplier supplier) { + this.name = supplier.get().getClass().getSimpleName(); + this.supplier = supplier; + } + public void add(Category c) { children.add(c); } @@ -51,10 +56,11 @@ public Supplier getSupplierFor(String path) { List> toCheck = new ArrayList<>(root.children); while(!toCheck.isEmpty()) { Category current = toCheck.remove(0); + toCheck.addAll(current.children); + if(current.name.equals(path)) { return current.supplier; } - toCheck.addAll(current.children); } return null; diff --git a/src/main/java/com/marginallyclever/ro3/RO3Frame.java b/src/main/java/com/marginallyclever/ro3/RO3Frame.java index 306194a1d..56b639b54 100644 --- a/src/main/java/com/marginallyclever/ro3/RO3Frame.java +++ b/src/main/java/com/marginallyclever/ro3/RO3Frame.java @@ -7,9 +7,7 @@ import ModernDocking.ext.ui.DockingUI; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; -import com.marginallyclever.ro3.actions.LoadScene; -import com.marginallyclever.ro3.actions.NewScene; -import com.marginallyclever.ro3.actions.SaveScene; +import com.marginallyclever.ro3.actions.*; import com.marginallyclever.ro3.logpanel.LogPanel; import com.marginallyclever.ro3.node.NodeDetailView; import com.marginallyclever.ro3.node.nodetreeview.NodeTreeView; @@ -143,8 +141,12 @@ public void actionPerformed(java.awt.event.ActionEvent e) { private JMenu buildFileMenu() { JMenu menuFile = new JMenu("File"); menuFile.add(new JMenuItem(new NewScene())); + menuFile.add(new JSeparator()); menuFile.add(new JMenuItem(new LoadScene())); menuFile.add(new JMenuItem(new SaveScene())); + menuFile.add(new JSeparator()); + menuFile.add(new JMenuItem(new ImportScene())); + menuFile.add(new JMenuItem(new ExportScene())); // TODO load recent scene menuFile.add(new JSeparator()); JMenuItem quit = new JMenuItem(new AbstractAction("Quit") { diff --git a/src/main/java/com/marginallyclever/ro3/Registry.java b/src/main/java/com/marginallyclever/ro3/Registry.java index bf4f835d0..1b796b935 100644 --- a/src/main/java/com/marginallyclever/ro3/Registry.java +++ b/src/main/java/com/marginallyclever/ro3/Registry.java @@ -25,14 +25,14 @@ public class Registry { public static EventListenerList listeners = new EventListenerList(); public static void start() { - Factory.Category nodule = new Factory.Category<>("Node", null); + Factory.Category nodule = new Factory.Category<>( "Node", null ); nodeFactory.getRoot().add(nodule); - Factory.Category pose = new Factory.Category<>("Pose", Pose::new); - pose.add(new Factory.Category<>("MeshInstance", MeshInstance::new )); - pose.add(new Factory.Category<>("Camera", Camera::new )); + Factory.Category pose = new Factory.Category<>( Pose::new ); + pose.add(new Factory.Category<>( MeshInstance::new )); + pose.add(new Factory.Category<>( Camera::new )); nodule.add(pose); - nodule.add(new Factory.Category<>("Material", Material::new )); - nodule.add(new Factory.Category<>("DH Parameter", DHParameter::new )); + nodule.add(new Factory.Category<>( Material::new )); + nodule.add(new Factory.Category<>( DHParameter::new )); cameras.add(new Camera("Camera 1")); @@ -51,6 +51,10 @@ public static void removeSceneChangeListener(SceneChangeListener listener) { } public static void setScene(Node newScene) { + for (SceneChangeListener listener : listeners.getListeners(SceneChangeListener.class)) { + listener.beforeSceneChange(newScene); + } + scene = newScene; for (SceneChangeListener listener : listeners.getListeners(SceneChangeListener.class)) { diff --git a/src/main/java/com/marginallyclever/ro3/actions/ExportScene.java b/src/main/java/com/marginallyclever/ro3/actions/ExportScene.java new file mode 100644 index 000000000..7b631f836 --- /dev/null +++ b/src/main/java/com/marginallyclever/ro3/actions/ExportScene.java @@ -0,0 +1,58 @@ +package com.marginallyclever.ro3.actions; + +import com.marginallyclever.robotoverlord.RobotOverlord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.File; + +/** + * Export the scene to a file for sharing on another computer. This is not the same as saving the scene. + * Exporting the scene will save the scene and all the assets it uses to a single file. + */ +public class ExportScene extends AbstractAction { + private static final Logger logger = LoggerFactory.getLogger(SaveScene.class); + private static final JFileChooser chooser = new JFileChooser(); + + public ExportScene() { + super("Export Scene"); + } + + /** + * Invoked when an action occurs. + * + * @param e the event to be processed + */ + @Override + public void actionPerformed(ActionEvent e) { + chooser.setFileFilter(RobotOverlord.FILE_FILTER); + Component source = (Component) e.getSource(); + JFrame parentFrame = (JFrame)SwingUtilities.getWindowAncestor(source); + + if (chooser.showSaveDialog(parentFrame) == JFileChooser.APPROVE_OPTION) { + // check before overwriting. + File selectedFile = chooser.getSelectedFile(); + if (selectedFile.exists()) { + int response = JOptionPane.showConfirmDialog(null, + "Do you want to replace the existing file?", + "Confirm Overwrite", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (response == JOptionPane.NO_OPTION) { + return; + } + } + + String absolutePath = chooser.getSelectedFile().getAbsolutePath(); + exportScene(absolutePath); + } + } + + private void exportScene(String absolutePath) { + logger.info("Export scene to {}", absolutePath); + logger.error("Not implemented yet."); + } +} diff --git a/src/main/java/com/marginallyclever/ro3/actions/ImportScene.java b/src/main/java/com/marginallyclever/ro3/actions/ImportScene.java index a3b4d6565..bd2cafec1 100644 --- a/src/main/java/com/marginallyclever/ro3/actions/ImportScene.java +++ b/src/main/java/com/marginallyclever/ro3/actions/ImportScene.java @@ -4,6 +4,7 @@ import com.marginallyclever.ro3.Registry; import com.marginallyclever.ro3.node.Node; import com.marginallyclever.robotoverlord.RobotOverlord; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,13 +13,15 @@ import java.awt.event.ActionEvent; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; /** * Load a Scene into the existing Scene. */ public class ImportScene extends AbstractAction { - private static final JFileChooser chooser = new JFileChooser(); private static final Logger logger = LoggerFactory.getLogger(ImportScene.class); + private static final JFileChooser chooser = new JFileChooser(); public ImportScene() { super("Import Scene"); @@ -43,16 +46,14 @@ public void actionPerformed(ActionEvent e) { private void loadIntoScene(File selectedFile) { logger.info("Import scene from {}",selectedFile.getAbsolutePath()); - // Create an ObjectMapper instance - ObjectMapper mapper = new ObjectMapper(); - try { - // Read the JSON file and convert it into a Node object - Node loaded = mapper.readValue(selectedFile, Node.class); + String content = new String(Files.readAllBytes(Paths.get(selectedFile.getAbsolutePath()))); + JSONObject json = new JSONObject(content); + Node loaded = new Node("Scene"); + loaded.fromJSON(json); - // option 1: add the loaded scene to the current scene. + // Add the loaded scene to the current scene. Registry.getScene().addChild(loaded); - // TODO option 2: copy the loaded scene into the currently selected Nodes of the NodeTreeView? } catch (IOException e) { logger.error("Error loading scene from JSON", e); } diff --git a/src/main/java/com/marginallyclever/ro3/actions/LoadScene.java b/src/main/java/com/marginallyclever/ro3/actions/LoadScene.java index f40be4992..e320451a0 100644 --- a/src/main/java/com/marginallyclever/ro3/actions/LoadScene.java +++ b/src/main/java/com/marginallyclever/ro3/actions/LoadScene.java @@ -18,8 +18,8 @@ import java.nio.file.Paths; public class LoadScene extends AbstractAction { - private static final JFileChooser chooser = new JFileChooser(); private static final Logger logger = LoggerFactory.getLogger(LoadScene.class); + private static final JFileChooser chooser = new JFileChooser(); public LoadScene() { super("Load Scene"); diff --git a/src/main/java/com/marginallyclever/ro3/actions/SaveScene.java b/src/main/java/com/marginallyclever/ro3/actions/SaveScene.java index 7e34a447c..d96ad7182 100644 --- a/src/main/java/com/marginallyclever/ro3/actions/SaveScene.java +++ b/src/main/java/com/marginallyclever/ro3/actions/SaveScene.java @@ -15,8 +15,8 @@ import java.io.IOException; public class SaveScene extends AbstractAction { - private static final JFileChooser chooser = new JFileChooser(); private static final Logger logger = LoggerFactory.getLogger(SaveScene.class); + private static final JFileChooser chooser = new JFileChooser(); public SaveScene() { super("Save Scene"); diff --git a/src/main/java/com/marginallyclever/ro3/node/Node.java b/src/main/java/com/marginallyclever/ro3/node/Node.java index 4fb7a09bd..1c49b2acf 100644 --- a/src/main/java/com/marginallyclever/ro3/node/Node.java +++ b/src/main/java/com/marginallyclever/ro3/node/Node.java @@ -62,10 +62,6 @@ private void fireAttachEvent(Node child) { for(NodeAttachListener listener : listeners.getListeners(NodeAttachListener.class)) { listener.nodeAttached(child); } - - for(Node grandchild : child.children) { - fireAttachEvent(grandchild); - } } private void fireDetachEvent(Node child) { diff --git a/src/main/java/com/marginallyclever/ro3/node/nodes/DHParameter.java b/src/main/java/com/marginallyclever/ro3/node/nodes/DHParameter.java index 283693fec..d0d8f268d 100644 --- a/src/main/java/com/marginallyclever/ro3/node/nodes/DHParameter.java +++ b/src/main/java/com/marginallyclever/ro3/node/nodes/DHParameter.java @@ -2,6 +2,7 @@ import com.marginallyclever.ro3.node.Node; import com.marginallyclever.robotoverlord.swing.CollapsiblePanel; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +15,7 @@ public class DHParameter extends Node { private static final Logger logger = LoggerFactory.getLogger(DHParameter.class); - double d=0,r=0,alpha=0,theta=0; + private transient double d=0,r=0,alpha=0,theta=0; public DHParameter() { super("DH Parameter"); @@ -120,4 +121,14 @@ public void getComponents(List list) { super.getComponents(list); } + + @Override + public JSONObject toJSON() { + return super.toJSON(); + } + + @Override + public void fromJSON(JSONObject from) { + super.fromJSON(from); + } } diff --git a/src/main/java/com/marginallyclever/ro3/node/nodes/Material.java b/src/main/java/com/marginallyclever/ro3/node/nodes/Material.java index d177c5337..4f3b33a89 100644 --- a/src/main/java/com/marginallyclever/ro3/node/nodes/Material.java +++ b/src/main/java/com/marginallyclever/ro3/node/nodes/Material.java @@ -5,6 +5,7 @@ import com.marginallyclever.ro3.texture.TextureFactoryPanel; import com.marginallyclever.ro3.texture.TextureWithMetadata; import com.marginallyclever.robotoverlord.swing.CollapsiblePanel; +import org.json.JSONObject; import javax.swing.*; import java.awt.*; @@ -72,4 +73,18 @@ private void setTextureButtonLabel(JButton button) { public TextureWithMetadata getTexture() { return texture; } + + @Override + public JSONObject toJSON() { + JSONObject json = super.toJSON(); + json.put("texture",texture.getSource()); + return json; + } + + @Override + public void fromJSON(JSONObject from) { + super.fromJSON(from); + String textureSource = from.getString("texture"); + texture = Registry.textureFactory.load(textureSource); + } } diff --git a/src/main/java/com/marginallyclever/ro3/node/nodes/MeshInstance.java b/src/main/java/com/marginallyclever/ro3/node/nodes/MeshInstance.java index b3c38ff00..24899ae99 100644 --- a/src/main/java/com/marginallyclever/ro3/node/nodes/MeshInstance.java +++ b/src/main/java/com/marginallyclever/ro3/node/nodes/MeshInstance.java @@ -3,6 +3,8 @@ import com.marginallyclever.robotoverlord.swing.CollapsiblePanel; import com.marginallyclever.robotoverlord.systems.render.mesh.Mesh; import com.marginallyclever.robotoverlord.systems.render.mesh.MeshSmoother; +import com.marginallyclever.robotoverlord.systems.render.mesh.load.MeshFactory; +import org.json.JSONObject; import javax.swing.*; import java.awt.*; @@ -62,4 +64,21 @@ private void setMeshButtonLabel(JButton button) { public Mesh getMesh() { return mesh; } + + @Override + public JSONObject toJSON() { + JSONObject json = super.toJSON(); + if(mesh!=null) { + json.put("mesh", mesh.getSourceName()); + } + return json; + } + + @Override + public void fromJSON(JSONObject from) { + super.fromJSON(from); + if(from.has("mesh")) { + mesh = MeshFactory.load(from.getString("mesh")); + } + } } diff --git a/src/main/java/com/marginallyclever/ro3/node/nodes/Pose.java b/src/main/java/com/marginallyclever/ro3/node/nodes/Pose.java index 42e8ba0cd..83c66358b 100644 --- a/src/main/java/com/marginallyclever/ro3/node/nodes/Pose.java +++ b/src/main/java/com/marginallyclever/ro3/node/nodes/Pose.java @@ -3,6 +3,8 @@ import com.marginallyclever.convenience.helpers.MatrixHelper; import com.marginallyclever.ro3.node.Node; import com.marginallyclever.robotoverlord.swing.CollapsiblePanel; +import org.json.JSONArray; +import org.json.JSONObject; import javax.swing.*; import javax.swing.text.NumberFormatter; @@ -142,4 +144,25 @@ public void setPosition(Vector3d p) { local.m13 = p.y; local.m23 = p.z; } + + @Override + public JSONObject toJSON() { + JSONObject json = super.toJSON(); + + double[] localArray = MatrixHelper.matrix4dToArray(local); + json.put("local", new JSONArray(localArray)); + return json; + } + + @Override + public void fromJSON(JSONObject from) { + super.fromJSON(from); + JSONArray localArray = from.getJSONArray("local"); + double[] localData = new double[16]; + for (int i = 0; i < 16; i++) { + localData[i] = localArray.getDouble(i); + } + + local.set(localData); + } } diff --git a/src/main/java/com/marginallyclever/ro3/node/nodetreeview/NodeTreeView.java b/src/main/java/com/marginallyclever/ro3/node/nodetreeview/NodeTreeView.java index fdef9be82..324c7f736 100644 --- a/src/main/java/com/marginallyclever/ro3/node/nodetreeview/NodeTreeView.java +++ b/src/main/java/com/marginallyclever/ro3/node/nodetreeview/NodeTreeView.java @@ -45,9 +45,7 @@ public NodeTreeView(String tabText) { buildMenuBar(); - JScrollPane scroll = new JScrollPane(); - scroll.setViewportView(tree); - add(scroll, BorderLayout.CENTER); + add(tree, BorderLayout.CENTER); add(menuBar, BorderLayout.NORTH); } @@ -255,6 +253,7 @@ public void beforeSceneChange(Node oldScene) { public void afterSceneChange(Node newScene) { logger.debug("afterSceneChange"); listenTo(newScene); + treeModel.removeAllChildren(); treeModel.setUserObject(newScene); ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(treeModel.getRoot()); scanTree(newScene); diff --git a/src/main/java/com/marginallyclever/robotoverlord/systems/render/mesh/load/MeshFactory.java b/src/main/java/com/marginallyclever/robotoverlord/systems/render/mesh/load/MeshFactory.java index 7014daa8c..695efde12 100644 --- a/src/main/java/com/marginallyclever/robotoverlord/systems/render/mesh/load/MeshFactory.java +++ b/src/main/java/com/marginallyclever/robotoverlord/systems/render/mesh/load/MeshFactory.java @@ -38,7 +38,7 @@ public class MeshFactory { * @return an instance of Mesh. It may contain nothing. */ public static Mesh load(String filename) { - if(filename == null || filename.trim().length()==0) return null; + if(filename == null || filename.trim().isEmpty()) return null; Mesh mesh = getMeshFromPool(filename); if(mesh!=null) return mesh;