/* * JaamSim Discrete Event Simulation * Copyright (C) 2013 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.DisplayModels; import java.io.File; import java.io.FileOutputStream; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import javax.swing.JFileChooser; import javax.swing.filechooser.FileNameExtensionFilter; import com.jaamsim.Graphics.DisplayEntity; import com.jaamsim.MeshFiles.BlockWriter; import com.jaamsim.MeshFiles.DataBlock; import com.jaamsim.MeshFiles.MeshData; import com.jaamsim.basicsim.Entity; import com.jaamsim.collada.ColParser; import com.jaamsim.controllers.RenderManager; import com.jaamsim.input.ActionListInput; import com.jaamsim.input.FileInput; import com.jaamsim.input.InputErrorException; import com.jaamsim.input.Keyword; import com.jaamsim.input.Output; import com.jaamsim.input.OutputHandle; import com.jaamsim.math.AABB; import com.jaamsim.math.Transform; import com.jaamsim.math.Vec3d; import com.jaamsim.math.Vec4d; import com.jaamsim.render.Action; import com.jaamsim.render.DisplayModelBinding; import com.jaamsim.render.MeshDataCache; import com.jaamsim.render.MeshProtoKey; import com.jaamsim.render.MeshProxy; import com.jaamsim.render.RenderProxy; import com.jaamsim.render.RenderUtils; import com.jaamsim.render.VisibilityInfo; import com.jaamsim.ui.ContextMenu; import com.jaamsim.ui.ContextMenuItem; import com.jaamsim.ui.GUIFrame; import com.jaamsim.ui.LogBox; public class ColladaModel extends DisplayModel { @Keyword(description = "The file containing the 3d object to show, valid formats are: " + "DAE, OBJ, JSM, and JSB, or a compressed version of any of these files in ZIP format.", exampleList = {"..\\graphics\\ship.dae", "..\\graphics\\ship.dae.zip" }) private final FileInput colladaFile; @Keyword(description = "A list of active actions and the entity output that drives them", exampleList = { "{ ContentAction Contents } { BoomAngleAction BoomAngle }" }) private final ActionListInput actions; private static HashMap<URI, MeshProtoKey> _cachedKeys = new HashMap<>(); private static final String[] validFileExtensions; private static final String[] validFileDescriptions; static { validFileExtensions = new String[5]; validFileDescriptions = new String[5]; validFileExtensions[0] = "ZIP"; validFileExtensions[1] = "DAE"; validFileExtensions[2] = "OBJ"; validFileExtensions[3] = "JSM"; validFileExtensions[4] = "JSB"; validFileDescriptions[0] = "Zipped 3D Files (*.zip)"; validFileDescriptions[1] = "COLLADA Files (*.dae)"; validFileDescriptions[2] = "Wavefront Files (*.obj)"; validFileDescriptions[3] = "JaamSim 3D Files (*.jsm)"; validFileDescriptions[4] = "JaamSim 3D Binary Files (*.jsb)"; } { colladaFile = new FileInput( "ColladaFile", "Key Inputs", null ); colladaFile.setFileType("3D"); colladaFile.setValidFileExtensions(validFileExtensions); colladaFile.setValidFileDescriptions(validFileDescriptions); this.addInput( colladaFile); actions = new ActionListInput("Actions", "Key Inputs", new ArrayList<Action.Binding>()); this.addInput(actions); } public ColladaModel() {} @Override public DisplayModelBinding getBinding(Entity ent) { return new Binding(ent, this); } @Override public boolean canDisplayEntity(Entity ent) { return ent instanceof DisplayEntity; } public URI getColladaFile() { return colladaFile.getValue(); } /** * Compares the specified file extension to the list of valid extensions. * * @param str - the file extension to be tested. * @return - TRUE if the extension is valid. */ public static boolean isValidExtension(String str) { for (String ext : validFileExtensions) { if (str.equalsIgnoreCase(ext)) return true; } return false; } /** * Returns a file name extension filter for each of the supported file types. * * @return an array of file name extension filters. */ public static FileNameExtensionFilter[] getFileNameExtensionFilters() { return FileInput.getFileNameExtensionFilters("3D", validFileExtensions, validFileDescriptions); } public static MeshProtoKey getCachedMeshKey(URI shapeURI) { MeshProtoKey meshKey = _cachedKeys.get(shapeURI); if (meshKey == null) { // This has not been cached yet meshKey = RenderUtils.FileNameToMeshProtoKey(shapeURI); assert(meshKey != null); _cachedKeys.put(shapeURI, meshKey); } return _cachedKeys.get(shapeURI); } private class Binding extends DisplayModelBinding { private ArrayList<RenderProxy> cachedProxies; private final DisplayEntity dispEnt; private Transform transCache; private Vec3d scaleCache; private URI colCache; private ArrayList<Action.Queue> actionsCache; private VisibilityInfo viCache; public Binding(Entity ent, DisplayModel dm) { super(ent, dm); dispEnt = (DisplayEntity)observee; } private void updateCache(double simTime) { // Gather some inputs Transform trans; Vec3d scale; long pickingID; if (dispEnt == null) { trans = Transform.ident; scale = DisplayModel.ONES; pickingID = 0; } else { trans = dispEnt.getGlobalTrans(); scale = dispEnt.getSize(); scale.mul3(getModelScale()); pickingID = dispEnt.getEntityNumber(); } URI filename = colladaFile.getValue(); ArrayList<Action.Queue> aqList = new ArrayList<>(); for (Action.Binding b : actions.getValue()) { Action.Queue aq = new Action.Queue(); aq.name = b.actionName; OutputHandle handle = dispEnt.getOutputHandle(b.outputName); aq.time = 0; if (handle != null) { try { aq.time = handle.getValueAsDouble(simTime, 0); } catch (Throwable e) { LogBox.logException(e); } } aqList.add(aq); } VisibilityInfo vi = getVisibilityInfo(); boolean dirty = false; dirty = dirty || !compare(transCache, trans); dirty = dirty || dirty_vec3d(scaleCache, scale); dirty = dirty || !compare(colCache, filename); dirty = dirty || !compare(actionsCache, aqList); dirty = dirty || !compare(viCache, vi); transCache = trans; scaleCache = scale; colCache = filename; actionsCache = aqList; viCache = vi; if (cachedProxies != null && !dirty) { // Nothing changed registerCacheHit("ColladaModel"); return; } registerCacheMiss("ColladaModel"); cachedProxies = new ArrayList<>(); MeshProtoKey meshKey = getCachedMeshKey(filename); AABB bounds = RenderManager.inst().getMeshBounds(meshKey, true); if (bounds == null || bounds.isEmpty()) { // This mesh has not been loaded yet, try again next time cachedProxies = null; // Invalidate the cache return; } // Tweak the transform and scale to adjust for the bounds of the // loaded model Vec3d boundsRad = new Vec3d(bounds.radius); if (boundsRad.z == 0) { boundsRad.z = 1; } Vec4d fixedScale = new Vec4d(0.5 * scale.x / boundsRad.x, 0.5 * scale.y / boundsRad.y, 0.5 * scale.z / boundsRad.z, 1.0d); Vec3d offset = new Vec3d(bounds.center); offset.scale3(-1.0d); offset.mul3(fixedScale); Transform fixedTrans = new Transform(trans); fixedTrans.merge(fixedTrans, new Transform(offset)); cachedProxies.add(new MeshProxy(meshKey, fixedTrans, fixedScale, aqList, vi, pickingID)); } @Override public void collectProxies(double simTime, ArrayList<RenderProxy> out) { if (dispEnt == null || !dispEnt.getShow()) { return; } updateCache(simTime); if (cachedProxies != null) out.addAll(cachedProxies); } } private MeshData getMeshData() { MeshProtoKey key = _cachedKeys.get(colladaFile.getValue()); if (key == null) return null; return MeshDataCache.getMeshData(key); } @Output(name = "Vertices") public int getNumVerticesOutput(double simTime) { MeshData data = getMeshData(); if (data == null) return 0; return data.getNumVertices(); } @Output(name = "Triangles") public int getNumTrianglesOutput(double simTime) { MeshData data = getMeshData(); if (data == null) return 0; return data.getNumTriangles(); } @Output(name = "VertexShareRatio") public double getVertexShareRatioOutput(double simTime) { MeshData data = getMeshData(); if (data == null) return 0; double numTriangles = data.getNumTriangles(); double numVertices = data.getNumVertices(); return numTriangles / (numVertices/3); } @Output(name = "NumSubInstances") public int getNumSubInstancesOutput(double simTime) { MeshData data = getMeshData(); if (data == null) return 0; return data.getNumSubInstances(); } @Output(name = "NumSubMeshes") public int getNumSubMeshesOutput(double simTime) { MeshData data = getMeshData(); if (data == null) return 0; return data.getNumSubMeshes(); } @Override public void validate() { super.validate(); if (!RenderManager.isGood()) return; // Check that any actions listed in the action list exist in the specified collada file MeshProtoKey meshKey = getCachedMeshKey(colladaFile.getValue()); ArrayList<Action.Description> actionDescs = RenderManager.inst().getMeshActions(meshKey, true); ArrayList<Action.Binding> bindings = actions.getValue(); for (Action.Binding b : bindings) { boolean found = false; for (Action.Description desc : actionDescs) { if (b.actionName.equals(desc.name)) { found = true; } } if (!found) { throw new InputErrorException("Input to the Action keyword refers to an action " + "named '%s' that is not specified by the ColladaFile input.", b.actionName); } } } @Output (name = "Actions") public String getActionsOutput(double simTime) { MeshProtoKey meshKey = getCachedMeshKey(colladaFile.getValue()); ArrayList<Action.Description> actionDescs = RenderManager.inst().getMeshActions(meshKey, true); StringBuilder ret = new StringBuilder(); for (Action.Description desc : actionDescs) { ret.append(desc.name + " "); } return ret.toString(); } public void exportBinaryMesh(String outputName) { MeshProtoKey meshKey = getCachedMeshKey(colladaFile.getValue()); try { ColParser.setKeepData(true); MeshData data = ColParser.parse(meshKey.getURI()); DataBlock block = data.getDataAsBlock(); File outFile = new File(outputName); FileOutputStream outStream = new FileOutputStream(outFile); BlockWriter.writeBlock(outStream, block); LogBox.formatRenderLog("Successfully exported: %s\n", outputName); } catch (Exception ex) { LogBox.formatRenderLog("Could not export model. Error: %s\n", ex.getMessage()); LogBox.renderLogException(ex); } } static { ContextMenu.addCustomMenuHandler(new ExportColladaModelHandler()); } private static class ExportColladaModelHandler implements ContextMenuItem { @Override public String getMenuText() { return "Export 3D Binary File (*.jsb)"; } @Override public boolean supportsEntity(Entity ent) { if (ent instanceof ColladaModel) return true; return false; } @Override public void performAction(Entity ent, int x, int y) { ColladaModel model = (ColladaModel)ent; // Create a file chooser File colFile = new File(model.getColladaFile()); final JFileChooser chooser = new JFileChooser(colFile); // Set the file extension filters chooser.setAcceptAllFileFilterUsed(true); FileNameExtensionFilter jsbFilter = new FileNameExtensionFilter("JaamSim 3D Binary Files (*.jsb)", "JSB"); chooser.addChoosableFileFilter(jsbFilter); chooser.setFileFilter(jsbFilter); // Set the default name for the binary file String defName = colFile.getName().concat(".jsb"); chooser.setSelectedFile(new File(defName)); // Show the file chooser and wait for selection int returnVal = chooser.showDialog(null, "Export"); // Create the selected graphics files if (returnVal == JFileChooser.APPROVE_OPTION) { File file = chooser.getSelectedFile(); String filePath = file.getPath(); // Add the file extension ".jsb" if needed filePath = filePath.trim(); if (filePath.indexOf('.') == -1) filePath = filePath.concat(".jsb"); // Confirm overwrite if file already exists File temp = new File(filePath); if (temp.exists()) { boolean confirmed = GUIFrame.showSaveAsDialog(file.getName()); if (!confirmed) { return; } } // Export the JSB file model.exportBinaryMesh(temp.getPath()); } } } }