You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

682 lines
18KB

  1. #include "app/RackWidget.hpp"
  2. #include "widget/TransparentWidget.hpp"
  3. #include "app/RackRail.hpp"
  4. #include "app/Scene.hpp"
  5. #include "app/ModuleBrowser.hpp"
  6. #include "settings.hpp"
  7. #include "plugin.hpp"
  8. #include "engine/Engine.hpp"
  9. #include "app.hpp"
  10. #include "asset.hpp"
  11. #include "patch.hpp"
  12. #include "osdialog.h"
  13. #include <map>
  14. #include <algorithm>
  15. #include <queue>
  16. namespace rack {
  17. namespace app {
  18. static ModuleWidget *moduleFromJson(json_t *moduleJ) {
  19. // Get slugs
  20. json_t *pluginSlugJ = json_object_get(moduleJ, "plugin");
  21. if (!pluginSlugJ)
  22. return NULL;
  23. json_t *modelSlugJ = json_object_get(moduleJ, "model");
  24. if (!modelSlugJ)
  25. return NULL;
  26. std::string pluginSlug = json_string_value(pluginSlugJ);
  27. std::string modelSlug = json_string_value(modelSlugJ);
  28. // Get Model
  29. plugin::Model *model = plugin::getModel(pluginSlug, modelSlug);
  30. if (!model)
  31. return NULL;
  32. // Create ModuleWidget
  33. ModuleWidget *moduleWidget = model->createModuleWidget();
  34. assert(moduleWidget);
  35. moduleWidget->fromJson(moduleJ);
  36. return moduleWidget;
  37. }
  38. struct ModuleContainer : widget::Widget {
  39. void draw(const DrawArgs &args) override {
  40. // Draw shadows behind each ModuleWidget first, so the shadow doesn't overlap the front of other ModuleWidgets.
  41. for (widget::Widget *child : children) {
  42. ModuleWidget *w = dynamic_cast<ModuleWidget*>(child);
  43. assert(w);
  44. nvgSave(args.vg);
  45. nvgTranslate(args.vg, child->box.pos.x, child->box.pos.y);
  46. w->drawShadow(args);
  47. nvgRestore(args.vg);
  48. }
  49. Widget::draw(args);
  50. }
  51. };
  52. struct CableContainer : widget::TransparentWidget {
  53. void draw(const DrawArgs &args) override {
  54. // Draw cable plugs
  55. for (widget::Widget *w : children) {
  56. CableWidget *cw = dynamic_cast<CableWidget*>(w);
  57. assert(cw);
  58. cw->drawPlugs(args);
  59. }
  60. Widget::draw(args);
  61. }
  62. };
  63. RackWidget::RackWidget() {
  64. railFb = new widget::FramebufferWidget;
  65. railFb->box.size = math::Vec();
  66. railFb->oversample = 1.0;
  67. {
  68. RackRail *rail = new RackRail;
  69. rail->box.size = math::Vec();
  70. railFb->addChild(rail);
  71. }
  72. addChild(railFb);
  73. moduleContainer = new ModuleContainer;
  74. addChild(moduleContainer);
  75. cableContainer = new CableContainer;
  76. addChild(cableContainer);
  77. }
  78. RackWidget::~RackWidget() {
  79. clear();
  80. }
  81. void RackWidget::step() {
  82. Widget::step();
  83. }
  84. void RackWidget::draw(const DrawArgs &args) {
  85. // Resize and reposition the RackRail to align on the grid.
  86. math::Rect railBox;
  87. railBox.pos = args.clipBox.pos.div(BUS_BOARD_GRID_SIZE).floor().mult(BUS_BOARD_GRID_SIZE);
  88. railBox.size = args.clipBox.size.div(BUS_BOARD_GRID_SIZE).ceil().plus(math::Vec(1, 1)).mult(BUS_BOARD_GRID_SIZE);
  89. if (!railFb->box.size.isEqual(railBox.size)) {
  90. railFb->dirty = true;
  91. }
  92. railFb->box = railBox;
  93. RackRail *rail = railFb->getFirstDescendantOfType<RackRail>();
  94. rail->box.size = railFb->box.size;
  95. Widget::draw(args);
  96. }
  97. void RackWidget::onHover(const event::Hover &e) {
  98. // Set before calling children's onHover()
  99. mousePos = e.pos;
  100. OpaqueWidget::onHover(e);
  101. }
  102. void RackWidget::onHoverKey(const event::HoverKey &e) {
  103. OpaqueWidget::onHoverKey(e);
  104. if (e.isConsumed())
  105. return;
  106. if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
  107. switch (e.key) {
  108. case GLFW_KEY_V: {
  109. if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
  110. pastePresetClipboardAction();
  111. e.consume(this);
  112. }
  113. } break;
  114. }
  115. }
  116. }
  117. void RackWidget::onDragHover(const event::DragHover &e) {
  118. // Set before calling children's onDragHover()
  119. mousePos = e.pos;
  120. OpaqueWidget::onDragHover(e);
  121. }
  122. void RackWidget::onButton(const event::Button &e) {
  123. Widget::onButton(e);
  124. e.stopPropagating();
  125. if (e.isConsumed())
  126. return;
  127. if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) {
  128. APP->scene->moduleBrowser->show();
  129. e.consume(this);
  130. }
  131. }
  132. void RackWidget::clear() {
  133. // This isn't required because removing all ModuleWidgets should remove all cables, but do it just in case.
  134. clearCables();
  135. // Remove ModuleWidgets
  136. std::list<widget::Widget*> widgets = moduleContainer->children;
  137. for (widget::Widget *w : widgets) {
  138. ModuleWidget *moduleWidget = dynamic_cast<ModuleWidget*>(w);
  139. assert(moduleWidget);
  140. removeModule(moduleWidget);
  141. delete moduleWidget;
  142. }
  143. }
  144. json_t *RackWidget::toJson() {
  145. // root
  146. json_t *rootJ = json_object();
  147. // Get module offset so modules are aligned to (0, 0) when the patch is loaded.
  148. math::Vec moduleOffset = math::Vec(INFINITY, INFINITY);
  149. for (widget::Widget *w : moduleContainer->children) {
  150. moduleOffset = moduleOffset.min(w->box.pos);
  151. }
  152. if (moduleContainer->children.empty()) {
  153. moduleOffset = RACK_OFFSET;
  154. }
  155. // modules
  156. json_t *modulesJ = json_array();
  157. for (widget::Widget *w : moduleContainer->children) {
  158. ModuleWidget *moduleWidget = dynamic_cast<ModuleWidget*>(w);
  159. assert(moduleWidget);
  160. // module
  161. json_t *moduleJ = moduleWidget->toJson();
  162. {
  163. // id
  164. json_object_set_new(moduleJ, "id", json_integer(moduleWidget->module->id));
  165. // pos
  166. math::Vec pos = moduleWidget->box.pos.minus(moduleOffset);
  167. pos = pos.div(RACK_GRID_SIZE).round();
  168. json_t *posJ = json_pack("[i, i]", (int) pos.x, (int) pos.y);
  169. json_object_set_new(moduleJ, "pos", posJ);
  170. }
  171. json_array_append_new(modulesJ, moduleJ);
  172. }
  173. json_object_set_new(rootJ, "modules", modulesJ);
  174. // cables
  175. json_t *cablesJ = json_array();
  176. for (widget::Widget *w : cableContainer->children) {
  177. CableWidget *cw = dynamic_cast<CableWidget*>(w);
  178. assert(cw);
  179. // Only serialize complete cables
  180. if (!cw->isComplete())
  181. continue;
  182. json_t *cableJ = cw->toJson();
  183. {
  184. // id
  185. json_object_set_new(rootJ, "id", json_integer(cw->cable->id));
  186. }
  187. json_array_append_new(cablesJ, cableJ);
  188. }
  189. json_object_set_new(rootJ, "cables", cablesJ);
  190. return rootJ;
  191. }
  192. void RackWidget::fromJson(json_t *rootJ) {
  193. // modules
  194. json_t *modulesJ = json_object_get(rootJ, "modules");
  195. if (!modulesJ)
  196. return;
  197. size_t moduleIndex;
  198. json_t *moduleJ;
  199. json_array_foreach(modulesJ, moduleIndex, moduleJ) {
  200. ModuleWidget *moduleWidget = moduleFromJson(moduleJ);
  201. if (moduleWidget) {
  202. // Before 1.0, the module ID was the index in the "modules" array
  203. if (APP->patch->isLegacy(2)) {
  204. moduleWidget->module->id = moduleIndex;
  205. }
  206. // id
  207. json_t *idJ = json_object_get(moduleJ, "id");
  208. if (idJ)
  209. moduleWidget->module->id = json_integer_value(idJ);
  210. // pos
  211. json_t *posJ = json_object_get(moduleJ, "pos");
  212. double x, y;
  213. json_unpack(posJ, "[F, F]", &x, &y);
  214. math::Vec pos = math::Vec(x, y);
  215. if (APP->patch->isLegacy(1)) {
  216. // Before 0.6, positions were in pixel units
  217. moduleWidget->box.pos = pos;
  218. }
  219. else {
  220. moduleWidget->box.pos = pos.mult(RACK_GRID_SIZE);
  221. }
  222. moduleWidget->box.pos = moduleWidget->box.pos.plus(RACK_OFFSET);
  223. addModule(moduleWidget);
  224. }
  225. else {
  226. json_t *pluginSlugJ = json_object_get(moduleJ, "plugin");
  227. json_t *modelSlugJ = json_object_get(moduleJ, "model");
  228. std::string pluginSlug = json_string_value(pluginSlugJ);
  229. std::string modelSlug = json_string_value(modelSlugJ);
  230. APP->patch->warningLog += string::f("Could not find module \"%s\" of plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str());
  231. }
  232. }
  233. // cables
  234. json_t *cablesJ = json_object_get(rootJ, "cables");
  235. // Before 1.0, cables were called wires
  236. if (!cablesJ)
  237. cablesJ = json_object_get(rootJ, "wires");
  238. assert(cablesJ);
  239. size_t cableIndex;
  240. json_t *cableJ;
  241. json_array_foreach(cablesJ, cableIndex, cableJ) {
  242. // Create a unserialize cable
  243. CableWidget *cw = new CableWidget;
  244. cw->fromJson(cableJ);
  245. if (!cw->isComplete()) {
  246. delete cw;
  247. continue;
  248. }
  249. // Before 1.0, cables IDs were not used, so just use the index of the "cables" array.
  250. if (APP->patch->isLegacy(2)) {
  251. cw->cable->id = cableIndex;
  252. }
  253. // id
  254. json_t *idJ = json_object_get(cableJ, "id");
  255. if (idJ)
  256. cw->cable->id = json_integer_value(idJ);
  257. addCable(cw);
  258. }
  259. }
  260. void RackWidget::pastePresetClipboardAction() {
  261. const char *moduleJson = glfwGetClipboardString(APP->window->win);
  262. if (!moduleJson) {
  263. WARN("Could not get text from clipboard.");
  264. return;
  265. }
  266. json_error_t error;
  267. json_t *moduleJ = json_loads(moduleJson, 0, &error);
  268. if (moduleJ) {
  269. ModuleWidget *mw = moduleFromJson(moduleJ);
  270. json_decref(moduleJ);
  271. addModuleAtMouse(mw);
  272. // history::ModuleAdd
  273. history::ModuleAdd *h = new history::ModuleAdd;
  274. h->setModule(mw);
  275. APP->history->push(h);
  276. }
  277. else {
  278. WARN("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
  279. }
  280. }
  281. static void RackWidget_updateAdjacent(RackWidget *that) {
  282. for (widget::Widget *w : that->moduleContainer->children) {
  283. math::Vec pLeft = w->box.pos.div(RACK_GRID_SIZE).round();
  284. math::Vec pRight = w->box.getTopRight().div(RACK_GRID_SIZE).round();
  285. ModuleWidget *mwLeft = NULL;
  286. ModuleWidget *mwRight = NULL;
  287. // Find adjacent modules
  288. for (widget::Widget *w2 : that->moduleContainer->children) {
  289. if (w2 == w)
  290. continue;
  291. math::Vec p2Left = w2->box.pos.div(RACK_GRID_SIZE).round();
  292. math::Vec p2Right = w2->box.getTopRight().div(RACK_GRID_SIZE).round();
  293. // Check if this is a left module
  294. if (p2Right.isEqual(pLeft)) {
  295. mwLeft = dynamic_cast<ModuleWidget*>(w2);
  296. }
  297. // Check if this is a right module
  298. if (p2Left.isEqual(pRight)) {
  299. mwRight = dynamic_cast<ModuleWidget*>(w2);
  300. }
  301. }
  302. ModuleWidget *mw = dynamic_cast<ModuleWidget*>(w);
  303. mw->module->rightModuleId = mwRight ? mwRight->module->id : -1;
  304. mw->module->leftModuleId = mwLeft ? mwLeft->module->id : -1;
  305. }
  306. }
  307. void RackWidget::addModule(ModuleWidget *m) {
  308. // Add module to ModuleContainer
  309. assert(m);
  310. // Module must be 3U high and at least 1HP wide
  311. assert(m->box.size.x >= RACK_GRID_WIDTH);
  312. assert(m->box.size.y == RACK_GRID_HEIGHT);
  313. moduleContainer->addChild(m);
  314. if (m->module) {
  315. // Add module to Engine
  316. APP->engine->addModule(m->module);
  317. }
  318. RackWidget_updateAdjacent(this);
  319. }
  320. void RackWidget::addModuleAtMouse(ModuleWidget *mw) {
  321. assert(mw);
  322. // Move module nearest to the mouse position
  323. math::Vec pos = mousePos.minus(mw->box.size.div(2));
  324. setModulePosNearest(mw, pos);
  325. addModule(mw);
  326. }
  327. void RackWidget::removeModule(ModuleWidget *m) {
  328. // Unset touchedParamWidget
  329. if (touchedParam) {
  330. ModuleWidget *touchedModule = touchedParam->getAncestorOfType<ModuleWidget>();
  331. if (touchedModule == m)
  332. touchedParam = NULL;
  333. }
  334. // Disconnect cables
  335. m->disconnect();
  336. if (m->module) {
  337. // Remove module from Engine
  338. APP->engine->removeModule(m->module);
  339. }
  340. // Remove module from ModuleContainer
  341. moduleContainer->removeChild(m);
  342. }
  343. bool RackWidget::requestModulePos(ModuleWidget *mw, math::Vec pos) {
  344. // Check intersection with other modules
  345. math::Rect mwBox = math::Rect(pos, mw->box.size);
  346. for (widget::Widget *w2 : moduleContainer->children) {
  347. // Don't intersect with self
  348. if (mw == w2)
  349. continue;
  350. // Don't intersect with invisible modules
  351. if (!w2->visible)
  352. continue;
  353. // Check intersection
  354. if (mwBox.isIntersecting(w2->box)) {
  355. return false;
  356. }
  357. }
  358. // Accept requested position
  359. mw->box = mwBox;
  360. RackWidget_updateAdjacent(this);
  361. return true;
  362. }
  363. void RackWidget::setModulePosNearest(ModuleWidget *mw, math::Vec pos) {
  364. // Dijkstra's algorithm to generate a sorted list of Vecs closest to `pos`.
  365. // Comparison of distance of Vecs to `pos`
  366. auto cmpNearest = [&](const math::Vec &a, const math::Vec &b) {
  367. return a.minus(pos).square() > b.minus(pos).square();
  368. };
  369. // Comparison of dictionary order of Vecs
  370. auto cmp = [&](const math::Vec &a, const math::Vec &b) {
  371. if (a.x != b.x)
  372. return a.x < b.x;
  373. return a.y < b.y;
  374. };
  375. // Priority queue sorted by distance from `pos`
  376. std::priority_queue<math::Vec, std::vector<math::Vec>, decltype(cmpNearest)> queue(cmpNearest);
  377. // Set of already-tested Vecs
  378. std::set<math::Vec, decltype(cmp)> visited(cmp);
  379. // Seed priority queue with closest Vec
  380. math::Vec closestPos = pos.div(RACK_GRID_SIZE).round().mult(RACK_GRID_SIZE);
  381. queue.push(closestPos);
  382. while (!queue.empty()) {
  383. math::Vec testPos = queue.top();
  384. // Check testPos
  385. if (requestModulePos(mw, testPos))
  386. return;
  387. // Move testPos to visited set
  388. queue.pop();
  389. visited.insert(testPos);
  390. // Add adjacent Vecs
  391. static const std::vector<math::Vec> deltas = {
  392. math::Vec(-1, 0).mult(RACK_GRID_SIZE),
  393. math::Vec(1, 0).mult(RACK_GRID_SIZE),
  394. math::Vec(0, -1).mult(RACK_GRID_SIZE),
  395. math::Vec(0, 1).mult(RACK_GRID_SIZE),
  396. };
  397. for (math::Vec delta : deltas) {
  398. math::Vec newPos = testPos.plus(delta);
  399. if (visited.find(newPos) == visited.end()) {
  400. queue.push(newPos);
  401. }
  402. }
  403. }
  404. // We failed to find a box. This shouldn't happen on an infinite rack.
  405. assert(0);
  406. }
  407. void RackWidget::setModulePosForce(ModuleWidget *mw, math::Vec pos) {
  408. mw->box.pos = pos.div(RACK_GRID_SIZE).round().mult(RACK_GRID_SIZE);
  409. // Comparison of center X coordinates
  410. auto cmp = [&](const widget::Widget *a, const widget::Widget *b) {
  411. return a->box.pos.x + a->box.size.x / 2 < b->box.pos.x + b->box.size.x / 2;
  412. };
  413. // Collect modules to the left and right of `mw`
  414. std::set<widget::Widget*, decltype(cmp)> leftModules(cmp);
  415. std::set<widget::Widget*, decltype(cmp)> rightModules(cmp);
  416. for (widget::Widget *w2 : moduleContainer->children) {
  417. if (w2 == mw)
  418. continue;
  419. // Modules must be on the same row as `mw`
  420. if (w2->box.pos.y != mw->box.pos.y)
  421. continue;
  422. if (cmp(w2, mw))
  423. leftModules.insert(w2);
  424. else
  425. rightModules.insert(w2);
  426. }
  427. // Shove left modules
  428. float xLimit = mw->box.pos.x;
  429. for (auto it = leftModules.rbegin(); it != leftModules.rend(); it++) {
  430. widget::Widget *w = *it;
  431. float x = xLimit - w->box.size.x;
  432. x = std::round(x / RACK_GRID_WIDTH) * RACK_GRID_WIDTH;
  433. if (w->box.pos.x < x)
  434. break;
  435. w->box.pos.x = x;
  436. xLimit = x;
  437. }
  438. // Shove right modules
  439. xLimit = mw->box.pos.x + mw->box.size.x;
  440. for (auto it = rightModules.begin(); it != rightModules.end(); it++) {
  441. widget::Widget *w = *it;
  442. float x = xLimit;
  443. x = std::round(x / RACK_GRID_WIDTH) * RACK_GRID_WIDTH;
  444. if (w->box.pos.x > x)
  445. break;
  446. w->box.pos.x = x;
  447. xLimit = x + w->box.size.x;
  448. }
  449. }
  450. ModuleWidget *RackWidget::getModule(int moduleId) {
  451. for (widget::Widget *w : moduleContainer->children) {
  452. ModuleWidget *mw = dynamic_cast<ModuleWidget*>(w);
  453. assert(mw);
  454. if (mw->module->id == moduleId)
  455. return mw;
  456. }
  457. return NULL;
  458. }
  459. bool RackWidget::isEmpty() {
  460. return moduleContainer->children.empty();
  461. }
  462. void RackWidget::updateModuleDragPositions() {
  463. moduleDragPositions.clear();
  464. for (widget::Widget *w : moduleContainer->children) {
  465. ModuleWidget *mw = dynamic_cast<ModuleWidget*>(w);
  466. assert(mw);
  467. moduleDragPositions[mw->module->id] = mw->box.pos;
  468. }
  469. }
  470. history::ComplexAction *RackWidget::getModuleDragAction() {
  471. history::ComplexAction *h = new history::ComplexAction;
  472. for (widget::Widget *w : moduleContainer->children) {
  473. ModuleWidget *mw = dynamic_cast<ModuleWidget*>(w);
  474. assert(mw);
  475. math::Vec pos = moduleDragPositions.at(mw->module->id);
  476. if (!pos.isEqual(mw->box.pos)) {
  477. history::ModuleMove *mmh = new history::ModuleMove;
  478. mmh->moduleId = mw->module->id;
  479. mmh->oldPos = pos;
  480. mmh->newPos = mw->box.pos;
  481. h->push(mmh);
  482. }
  483. }
  484. return h;
  485. }
  486. void RackWidget::clearCables() {
  487. for (widget::Widget *w : cableContainer->children) {
  488. CableWidget *cw = dynamic_cast<CableWidget*>(w);
  489. assert(cw);
  490. if (!cw->isComplete())
  491. continue;
  492. APP->engine->removeCable(cw->cable);
  493. }
  494. incompleteCable = NULL;
  495. cableContainer->clearChildren();
  496. }
  497. void RackWidget::clearCablesAction() {
  498. // Add CableRemove for every cable to a ComplexAction
  499. history::ComplexAction *complexAction = new history::ComplexAction;
  500. complexAction->name = "clear cables";
  501. for (widget::Widget *w : cableContainer->children) {
  502. CableWidget *cw = dynamic_cast<CableWidget*>(w);
  503. assert(cw);
  504. if (!cw->isComplete())
  505. continue;
  506. // history::CableRemove
  507. history::CableRemove *h = new history::CableRemove;
  508. h->setCable(cw);
  509. complexAction->push(h);
  510. }
  511. APP->history->push(complexAction);
  512. clearCables();
  513. }
  514. void RackWidget::clearCablesOnPort(PortWidget *port) {
  515. for (CableWidget *cw : getCablesOnPort(port)) {
  516. // Check if cable is connected to port
  517. if (cw == incompleteCable) {
  518. incompleteCable = NULL;
  519. cableContainer->removeChild(cw);
  520. }
  521. else {
  522. removeCable(cw);
  523. }
  524. delete cw;
  525. }
  526. }
  527. void RackWidget::addCable(CableWidget *w) {
  528. assert(w->isComplete());
  529. APP->engine->addCable(w->cable);
  530. cableContainer->addChild(w);
  531. }
  532. void RackWidget::removeCable(CableWidget *w) {
  533. assert(w->isComplete());
  534. APP->engine->removeCable(w->cable);
  535. cableContainer->removeChild(w);
  536. }
  537. void RackWidget::setIncompleteCable(CableWidget *w) {
  538. if (incompleteCable) {
  539. cableContainer->removeChild(incompleteCable);
  540. delete incompleteCable;
  541. incompleteCable = NULL;
  542. }
  543. if (w) {
  544. cableContainer->addChild(w);
  545. incompleteCable = w;
  546. }
  547. }
  548. CableWidget *RackWidget::releaseIncompleteCable() {
  549. CableWidget *cw = incompleteCable;
  550. cableContainer->removeChild(incompleteCable);
  551. incompleteCable = NULL;
  552. return cw;
  553. }
  554. CableWidget *RackWidget::getTopCable(PortWidget *port) {
  555. for (auto it = cableContainer->children.rbegin(); it != cableContainer->children.rend(); it++) {
  556. CableWidget *cw = dynamic_cast<CableWidget*>(*it);
  557. assert(cw);
  558. // Ignore incomplete cables
  559. if (!cw->isComplete())
  560. continue;
  561. if (cw->inputPort == port || cw->outputPort == port)
  562. return cw;
  563. }
  564. return NULL;
  565. }
  566. CableWidget *RackWidget::getCable(int cableId) {
  567. for (widget::Widget *w : cableContainer->children) {
  568. CableWidget *cw = dynamic_cast<CableWidget*>(w);
  569. assert(cw);
  570. if (cw->cable->id == cableId)
  571. return cw;
  572. }
  573. return NULL;
  574. }
  575. std::list<CableWidget*> RackWidget::getCablesOnPort(PortWidget *port) {
  576. assert(port);
  577. std::list<CableWidget*> cables;
  578. for (widget::Widget *w : cableContainer->children) {
  579. CableWidget *cw = dynamic_cast<CableWidget*>(w);
  580. assert(cw);
  581. if (cw->inputPort == port || cw->outputPort == port) {
  582. cables.push_back(cw);
  583. }
  584. }
  585. return cables;
  586. }
  587. } // namespace app
  588. } // namespace rack