From bcc27ad1822b16dd7e5601b00515450929108126 Mon Sep 17 00:00:00 2001 From: Josh Yelon Date: Tue, 12 Jul 2005 13:04:17 +0000 Subject: [PATCH] New Max Egg Importer --- pandatool/src/maxeggimport/maxEggImport.cxx | 471 ++++++++++++++++++++ pandatool/src/maxeggimport/maxEggImport.def | 7 + pandatool/src/maxeggimport/maxImportRes.h | 21 + pandatool/src/maxeggimport/maxImportRes.obj | Bin 0 -> 1332 bytes pandatool/src/maxeggimport/maxImportRes.rc | 122 +++++ 5 files changed, 621 insertions(+) create mode 100755 pandatool/src/maxeggimport/maxEggImport.cxx create mode 100755 pandatool/src/maxeggimport/maxEggImport.def create mode 100755 pandatool/src/maxeggimport/maxImportRes.h create mode 100755 pandatool/src/maxeggimport/maxImportRes.obj create mode 100755 pandatool/src/maxeggimport/maxImportRes.rc diff --git a/pandatool/src/maxeggimport/maxEggImport.cxx b/pandatool/src/maxeggimport/maxEggImport.cxx new file mode 100755 index 0000000000..788c8d4dc0 --- /dev/null +++ b/pandatool/src/maxeggimport/maxEggImport.cxx @@ -0,0 +1,471 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// maxEggImport.cxx - Egg Importer for 3D Studio Max. +// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "Max.h" +#include "maxImportRes.h" +#include "istdplug.h" +#include "stdmat.h" +#include "decomp.h" +#include "shape.h" +#include "splshape.h" +#include "dummy.h" + +#include "eggData.h" +#include "eggVertexPool.h" +#include "eggVertex.h" +#include "eggPolygon.h" +#include "eggPrimitive.h" +#include "eggGroupNode.h" +#include "eggPolysetMaker.h" +#include "eggBin.h" + +#include + +static FILE *lgfile; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// The MaxEggImporter class +// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class MaxEggMesh; + +class MaxEggImporter : public SceneImport +{ +public: + MaxEggImporter(); + ~MaxEggImporter(); + int ExtCount(); // Number of extensions supported + const TCHAR * Ext(int n); // Extension #n (i.e. "EGG") + const TCHAR * LongDesc(); // Long ASCII description (i.e. "Egg Importer") + const TCHAR * ShortDesc(); // Short ASCII description (i.e. "Egg") + const TCHAR * AuthorName(); // ASCII Author name + const TCHAR * CopyrightMessage();// ASCII Copyright message + const TCHAR * OtherMessage1(); // Other message #1 + const TCHAR * OtherMessage2(); // Other message #2 + unsigned int Version(); // Version number * 100 (i.e. v3.01 = 301) + void ShowAbout(HWND hWnd); // Show DLL's "About..." box + int DoImport(const TCHAR *name,ImpInterface *ei,Interface *i, BOOL suppressPrompts); + MaxEggMesh *GetMesh(EggVertexPool *pool); + void TraverseEggData(EggData *data); + void TraverseEggNode(EggNode *node, EggGroup *context); + +public: + Interface *_ip; + ImpInterface *_impip; + static BOOL _merge; + static BOOL _importmodel; + static BOOL _importanim; + + typedef pmap MeshTable; + typedef second_of_pair_iterator MeshIterator; + MeshTable _mesh_tab; +}; + + +BOOL MaxEggImporter::_merge = TRUE; +BOOL MaxEggImporter::_importmodel = TRUE; +BOOL MaxEggImporter::_importanim = TRUE; + +MaxEggImporter::MaxEggImporter() +{ +} + +MaxEggImporter::~MaxEggImporter() +{ +} + +int MaxEggImporter::ExtCount() +{ + // Number of different extensions handled by this importer. + return 1; +} + +const TCHAR * MaxEggImporter::Ext(int n) +{ + // Fetch the extensions handled by this importer. + switch(n) { + case 0: return _T("egg"); + default: return _T(""); + } +} + +const TCHAR * MaxEggImporter::LongDesc() +{ + return _T("Panda3D Egg Importer"); +} + +const TCHAR * MaxEggImporter::ShortDesc() +{ + return _T("Panda3D Egg"); +} + +const TCHAR * MaxEggImporter::AuthorName() +{ + return _T("Joshua Yelon"); +} + +const TCHAR * MaxEggImporter::CopyrightMessage() +{ + return _T("Copyight (c) 2005 Josh Yelon"); +} + +const TCHAR * MaxEggImporter::OtherMessage1() +{ + return _T(""); +} + +const TCHAR * MaxEggImporter::OtherMessage2() +{ + return _T(""); +} + +unsigned int MaxEggImporter::Version() +{ + return 100; +} + +static BOOL CALLBACK AboutBoxDlgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) { + case WM_INITDIALOG: + CenterWindow(hWnd, GetParent(hWnd)); + break; + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: + EndDialog(hWnd, 1); + break; + } + break; + default: + return FALSE; + } + return TRUE; +} + +void MaxEggImporter::ShowAbout(HWND hWnd) +{ + DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, AboutBoxDlgProc, 0); +} + +static BOOL CALLBACK ImportDlgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + MaxEggImporter *imp = (MaxEggImporter*)GetWindowLong(hWnd,GWL_USERDATA); + switch (msg) { + case WM_INITDIALOG: + imp = (MaxEggImporter*)lParam; + SetWindowLong(hWnd,GWL_USERDATA,lParam); + CenterWindow(hWnd, GetParent(hWnd)); + CheckDlgButton(hWnd, IDC_MERGE, imp->_merge); + CheckDlgButton(hWnd, IDC_IMPORTMODEL, imp->_importmodel); + CheckDlgButton(hWnd, IDC_IMPORTANIM, imp->_importanim); + break; + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: + imp->_merge = IsDlgButtonChecked(hWnd, IDC_MERGE); + imp->_importmodel = IsDlgButtonChecked(hWnd, IDC_IMPORTMODEL); + imp->_importanim = IsDlgButtonChecked(hWnd, IDC_IMPORTANIM); + EndDialog(hWnd, 1); + break; + case IDCANCEL: + EndDialog(hWnd, 0); + break; + } + break; + default: + return FALSE; + } + return TRUE; +} + +int MaxEggImporter::DoImport(const TCHAR *name,ImpInterface *ii,Interface *i, BOOL suppressPrompts) +{ + // Grab the interface pointer. + _ip = i; + _impip = ii; + + // Prompt the user with our dialogbox. + if (!DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_IMPORT_DLG), + _ip->GetMAXHWnd(), ImportDlgProc, (LPARAM)this)) { + return 1; + } + + // Read in the egg file. + EggData data; + Filename datafn = Filename::from_os_specific(name); + MessageBox(NULL, datafn.c_str(), "Panda3D Egg Importer", MB_OK); + if (!data.read(datafn)) { + MessageBox(NULL, "Cannot read Egg file", "Panda3D Egg Importer", MB_OK); + return 1; + } + + // Do all the good stuff. + TraverseEggData(&data); + MessageBox(NULL, "Import Complete", "Panda3D Egg Importer", MB_OK); + return 1; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// MaxEggMesh +// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class MaxEggMesh +{ +public: + + EggVertexPool *_pool; + TriObject *_obj; + Mesh *_mesh; + INode *_node; + int _vert_count; + int _tvert_count; + int _cvert_count; + int _face_count; + + typedef pair VertexPos; + typedef pair VertexContext; + phash_map _vert_tab; + phash_map _tvert_tab; + phash_map _cvert_tab; + + int GetVert(Vertexd pos, Normald norm, EggGroup *context); + int GetTVert(TexCoordd uv); + int GetCVert(Colorf col); + int AddFace(int v0, int v1, int v2, int tv0, int tv1, int tv2, int cv0, int cv1, int cv2); + void LogData(void); +}; + +void MaxEggMesh::LogData(void) +{ + fprintf(lgfile,"Mesh %08x faces: %d\n",_mesh,_mesh->numFaces); + for (int i=0; i<_mesh->numFaces; i++) { + fprintf(lgfile," -- %d %d %d\n",_mesh->tvFace[i].t[0],_mesh->tvFace[i].t[1],_mesh->tvFace[i].t[2]); + } +} + +int MaxEggMesh::GetVert(Vertexd pos, Normald norm, EggGroup *context) +{ + VertexContext key = VertexContext(VertexPos(pos,norm),context); + if (_vert_tab.count(key)) + return _vert_tab[key]; + if (_vert_count == _mesh->numVerts) { + int nsize = _vert_count*2 + 100; + _mesh->setNumVerts(nsize, _vert_count?TRUE:FALSE); + } + int idx = _vert_count++; + _mesh->setVert(idx, pos.get_x(), pos.get_y(), pos.get_z()); + _vert_tab[key] = idx; + return idx; +} + +int MaxEggMesh::GetTVert(TexCoordd uv) +{ + if (_tvert_tab.count(uv)) + return _tvert_tab[uv]; + if (_tvert_count == _mesh->numTVerts) { + int nsize = _tvert_count*2 + 100; + _mesh->setNumTVerts(nsize, _tvert_count?TRUE:FALSE); + } + int idx = _tvert_count++; + _mesh->setTVert(idx, uv.get_x(), uv.get_y(), 0.0); + _tvert_tab[uv] = idx; + return idx; +} + +int MaxEggMesh::GetCVert(Colorf col) +{ + if (_cvert_tab.count(col)) + return _cvert_tab[col]; + if (_cvert_count == _mesh->numCVerts) { + int nsize = _cvert_count*2 + 100; + _mesh->setNumVertCol(nsize, _cvert_count?TRUE:FALSE); + } + int idx = _cvert_count++; + _mesh->vertCol[idx] = Point3(col.get_x(), col.get_y(), col.get_z()); + _cvert_tab[col] = idx; + return idx; +} + +MaxEggMesh *MaxEggImporter::GetMesh(EggVertexPool *pool) +{ + MaxEggMesh *result = _mesh_tab[pool]; + if (result == 0) { + string name = pool->get_name(); + int nsize = name.size(); + if ((nsize > 6) && (name.rfind(".verts")==(nsize-6))) + name.resize(nsize-6); + result = new MaxEggMesh; + result->_pool = pool; + result->_obj = CreateNewTriObject(); + result->_mesh = &result->_obj->GetMesh(); + result->_mesh->setMapSupport(0, TRUE); + result->_node = _ip->CreateObjectNode(result->_obj); + result->_vert_count = 0; + result->_tvert_count = 0; + result->_cvert_count = 0; + result->_face_count = 0; + // result->_node->SetName(name.c_str()); + _mesh_tab[pool] = result; + } + return result; +} + +int MaxEggMesh::AddFace(int v0, int v1, int v2, int tv0, int tv1, int tv2, int cv0, int cv1, int cv2) +{ + static int dump = 0; + if (_face_count == _mesh->numFaces) { + int nsize = _face_count*2 + 100; + BOOL keep = _mesh->numFaces ? TRUE:FALSE; + _mesh->setNumFaces(nsize, keep); + _mesh->setNumTVFaces(nsize, keep, _face_count); + _mesh->setNumVCFaces(nsize, keep, _face_count); + } + int idx = _face_count++; + _mesh->faces[idx].setVerts(v0,v1,v2); + _mesh->faces[idx].smGroup = 1; + _mesh->faces[idx].flags = EDGE_ALL | HAS_TVERTS; + _mesh->tvFace[idx].setTVerts(tv0,tv1,tv2); + _mesh->vcFace[idx].setTVerts(cv0,cv1,cv2); + return idx; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// TraverseEggData +// +// We have an EggData in memory, and now we're going to copy that +// over into the max scene graph. +// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void MaxEggImporter::TraverseEggNode(EggNode *node, EggGroup *context) +{ + vector vertIndices; + vector tvertIndices; + vector cvertIndices; + + if (node->is_of_type(EggPolygon::get_class_type())) { + EggPolygon *poly = DCAST(EggPolygon, node); + + EggPolygon::const_iterator ci; + MaxEggMesh *mesh = GetMesh(poly->get_pool()); + vertIndices.clear(); + tvertIndices.clear(); + cvertIndices.clear(); + for (ci = poly->begin(); ci != poly->end(); ++ci) { + EggVertex *vtx = (*ci); + EggVertexPool *pool = poly->get_pool(); + vertIndices.push_back(mesh->GetVert(vtx->get_pos3(), vtx->get_normal(), context)); + tvertIndices.push_back(mesh->GetTVert(vtx->get_uv())); + cvertIndices.push_back(mesh->GetCVert(vtx->get_color())); + } + for (int i=1; iAddFace(vertIndices[0], vertIndices[i], vertIndices[i+1], + tvertIndices[0], tvertIndices[i], tvertIndices[i+1], + cvertIndices[0], cvertIndices[i], cvertIndices[i+1]); + } else if (node->is_of_type(EggGroupNode::get_class_type())) { + EggGroupNode *group = DCAST(EggGroupNode, node); + if (node->is_of_type(EggGroup::get_class_type())) { + EggGroup *group = DCAST(EggGroup, node); + if (group->is_joint()) context = group; + } + EggGroupNode::const_iterator ci; + for (ci = group->begin(); ci != group->end(); ++ci) { + TraverseEggNode(*ci, context); + } + } +} + +void MaxEggImporter::TraverseEggData(EggData *data) +{ + lgfile = fopen("MaxEggImporter.log","w"); + TraverseEggNode(data, NULL); + MeshIterator ci; + for (ci = _mesh_tab.begin(); ci != _mesh_tab.end(); ++ci) { + MaxEggMesh *mesh = (*ci); + mesh->_mesh->setNumVerts(mesh->_vert_count, TRUE); + mesh->_mesh->setNumTVerts(mesh->_tvert_count, TRUE); + mesh->_mesh->setNumVertCol(mesh->_cvert_count, TRUE); + mesh->_mesh->setNumFaces(mesh->_face_count, TRUE); + mesh->_mesh->setNumTVFaces(mesh->_face_count, TRUE, mesh->_face_count); + mesh->_mesh->setNumVCFaces(mesh->_face_count, TRUE, mesh->_face_count); + mesh->_mesh->InvalidateTopologyCache(); + mesh->_mesh->InvalidateGeomCache(); + mesh->_mesh->buildNormals(); + } + if (lgfile) fclose(lgfile); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Plugin Initialization +// +// The following code enables Max to load this DLL, get a list +// of the classes defined in this DLL, and provides a means for +// Max to create instances of those classes. +// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +HINSTANCE hInstance; + +BOOL WINAPI DllMain(HINSTANCE hinstDLL,ULONG fdwReason,LPVOID lpvReserved) +{ + static int controlsInit = FALSE; + hInstance = hinstDLL; + + if (!controlsInit) { + controlsInit = TRUE; + InitCustomControls(hInstance); + InitCommonControls(); + } + + return (TRUE); +} + +#define PANDAEGGIMP_CLASS_ID1 0x377193ab +#define PANDAEGGIMP_CLASS_ID2 0x897afe12 + +class MaxEggImporterClassDesc: public ClassDesc +{ +public: + int IsPublic() {return 1;} + void *Create(BOOL loading = FALSE) {return new MaxEggImporter;} + const TCHAR *ClassName() {return _T("MaxEggImporter");} + SClass_ID SuperClassID() {return SCENE_IMPORT_CLASS_ID;} + Class_ID ClassID() {return Class_ID(PANDAEGGIMP_CLASS_ID1,PANDAEGGIMP_CLASS_ID2);} + const TCHAR *Category() {return _T("Chrutilities");} +}; + +static MaxEggImporterClassDesc MaxEggImporterDesc; + +__declspec( dllexport ) const TCHAR* LibDescription() +{ + return _T("Panda3D Egg Importer"); +} + +__declspec( dllexport ) int LibNumberClasses() +{ + return 1; +} + +__declspec( dllexport ) ClassDesc* LibClassDesc(int i) +{ + switch(i) { + case 0: return &MaxEggImporterDesc; + default: return 0; + } +} + +__declspec( dllexport ) ULONG LibVersion() +{ + return VERSION_3DSMAX; +} + diff --git a/pandatool/src/maxeggimport/maxEggImport.def b/pandatool/src/maxeggimport/maxEggImport.def new file mode 100755 index 0000000000..40cf3482cd --- /dev/null +++ b/pandatool/src/maxeggimport/maxEggImport.def @@ -0,0 +1,7 @@ +EXPORTS + LibDescription @1 + LibNumberClasses @2 + LibClassDesc @3 + LibVersion @4 +SECTIONS + .data READ WRITE diff --git a/pandatool/src/maxeggimport/maxImportRes.h b/pandatool/src/maxeggimport/maxImportRes.h new file mode 100755 index 0000000000..9619536508 --- /dev/null +++ b/pandatool/src/maxeggimport/maxImportRes.h @@ -0,0 +1,21 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by maxImportRes.rc +// +#define IDD_PANEL 101 +#define IDD_ABOUTBOX 102 +#define IDD_IMPORT_DLG 103 +#define IDC_MERGE 1002 +#define IDC_IMPORTMODEL 1003 +#define IDC_IMPORTANIM 1004 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 105 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1020 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/pandatool/src/maxeggimport/maxImportRes.obj b/pandatool/src/maxeggimport/maxImportRes.obj new file mode 100755 index 0000000000000000000000000000000000000000..ff31dd0c37eb637bdde4f3bcd05a48161518aaa1 GIT binary patch literal 1332 zcmb7EO=}ZT6g@9vO`3uYSg0$RMHUuxNUKPfX=25qwIL}k#e7WS6lW&POlSp>;L3u3 z#9!dXMRDcIwfGY>|Dm4y-b|g;BK3y6k8{s^@7#OeyeG=S@{ezgH&=i%v?*uc_9mlh z$4r689DK+oXqlW64io!Ud(Cb>+i!25=}wvK6c*Ut z0H%L185J;OyGeY+zV(-1VUc-xDbzVL_)&^IJQvI7++5a{_%A}(5?hgKPER?|c7m@{ z$rJDiP2F#yhY%Aau&Jnv00Xvn@qqJfjQF#$10Q3uBC=ez<_usL9eO)@Rjfzi^>mba zbKNMYRs(mifos$Ps+nR>S2@oqw2IXF5moxd-d$QvnWbq`LqzQ^^?Gb=YIE_5zmRB| zkp!vsgiMb!pMJq?>~qv-)&p{dKf-IG&D3w=6{Y9%cvZ{)f~UH_Oz!?aZ^zXBue?!J z>limRN9roMm0SdjNaD|%MeZ_IaKJr(@?Hy9vJ^35$=D(Dk}wYv`-EteWm5Ddslqag=aRsX&}x$g$C~Mt&K;QZ4ygCiM$>6EBY8zFs%G zaNQP9^H|;P+1c6A^?vA&9d9sOdQQEfp+0oGiBqd7QVf!Riv+clyhihue}^RJmXepq id{r4U{{~SeIr0k4TeV%&CaC5>BkT7zAMt2|i0?NB#JQsY literal 0 HcmV?d00001 diff --git a/pandatool/src/maxeggimport/maxImportRes.rc b/pandatool/src/maxeggimport/maxImportRes.rc new file mode 100755 index 0000000000..c88a7bc71a --- /dev/null +++ b/pandatool/src/maxeggimport/maxImportRes.rc @@ -0,0 +1,122 @@ +// Microsoft Visual C++ generated resource script. +// +#include "maxImportRes.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_ABOUTBOX, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 176 + TOPMARGIN, 7 + BOTTOMMARGIN, 60 + END + + IDD_IMPORT_DLG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 187 + TOPMARGIN, 7 + BOTTOMMARGIN, 71 + END +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "maxImportRes.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""afxres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ABOUTBOX DIALOGEX 0, 0, 183, 67 +STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "About Panda3D Egg Importer" +FONT 8, "MS Sans Serif", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,66,45,50,14 + CTEXT "Panda3D Egg Importer\n\nCarnegie Mellon\nEntertainment Technology Center", + IDC_STATIC,7,7,169,36 +END + +IDD_IMPORT_DLG DIALOGEX 0, 0, 194, 78 +STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Panda3D Egg Import" +FONT 8, "MS Sans Serif", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,137,10,50,14 + PUSHBUTTON "Cancel",IDCANCEL,137,30,50,14 + CONTROL "Merge with Current Scene",IDC_MERGE,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,15,20,106,10 + GROUPBOX "Input Options",IDC_STATIC,5,7,126,64 + CONTROL "Import Model",IDC_IMPORTMODEL,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,15,41,73,10 + CONTROL "Import Animation",IDC_IMPORTANIM,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,15,54,73,10 +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED +