Last active
October 24, 2025 13:58
-
-
Save NachiaVivias/c100a57da91c147e094dab862970278e to your computer and use it in GitHub Desktop.
Procon36 brief visualizer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # include <Siv3D.hpp> // Siv3D v0.6.15 | |
| // #define ForWeb | |
| static inline String SampleProblemJson = UR"({"problem":{"field":{"size":4,"entities":[[6,3,4,0],[1,5,3,5],[2,7,0,6],[1,2,7,4]]}}})"; | |
| static inline String SampleSolutionJson = UR"({"ops": [{"n": 2,"x": 0,"y": 0},{"n": 3,"x": 1,"y": 1},{"n": 2,"x": 2,"y": 1},{"n": 2,"x": 2,"y": 2},{"n": 2,"x": 2,"y": 2}]})"; | |
| struct VerifyResult { | |
| bool isOk; | |
| String msg; | |
| }; | |
| struct Operation { | |
| int64 y; | |
| int64 x; | |
| int64 r; | |
| static Operation FromJson(JSON json) { | |
| Operation res; | |
| res.x = json[U"x"].get<int32>(); | |
| res.y = json[U"y"].get<int32>(); | |
| res.r = json[U"n"].get<int32>(); | |
| return res; | |
| } | |
| JSON toJson() { | |
| JSON res; | |
| res[U"x"] = x; | |
| res[U"y"] = y; | |
| res[U"n"] = r; | |
| return res; | |
| } | |
| bool inRange(int64 a, int64 b) { | |
| return x <= a && a < x + r && y <= b && b < y + r; | |
| } | |
| }; | |
| struct Board { | |
| Grid<int32> grid; | |
| auto width() const { return grid.width(); } | |
| auto height() const { return grid.height(); } | |
| Size size() const { return Size(width(), height()); } | |
| Board appliedOperation(const Operation& op) { | |
| Board res = *this; | |
| for (auto [i, j] : step(Size{ op.r, op.r })) res.grid[Point(op.x + i, op.y + j)] = grid[Point(op.x + j, op.y + (op.r - 1 - i))]; | |
| return res; | |
| } | |
| Array<Operation> allCandidateOperations() const { | |
| Array<Operation> res; | |
| for (int32 r = 2; r <= width() && r <= height(); r++) { | |
| for (int32 x = 0; x + r <= width(); x++) { | |
| for (int32 y = 0; y + r <= height(); y++) { | |
| res.push_back({ y,x,r }); | |
| } | |
| } | |
| } | |
| return res; | |
| } | |
| int32 numAdjacentPairs() const { | |
| int32 ans = 0; | |
| for (auto p : step(Size(width(), height() - 1))) if (grid[p] == grid[p + Point{ 0, 1 }]) ans += 1; | |
| for (auto p : step(Size(width() - 1, height()))) if (grid[p] == grid[p + Point{ 1, 0 }]) ans += 1; | |
| return ans; | |
| } | |
| int32 sumManhattanDist() const { | |
| Array<std::array<Point, 2>> entityCenterPos(size().area() / 2); | |
| for (auto p : step(size())) { | |
| entityCenterPos[grid[p]][0] = p; | |
| std::swap(entityCenterPos[grid[p]][0], entityCenterPos[grid[p]][1]); | |
| } | |
| int32 ans = 0; | |
| for (auto [u, v] : entityCenterPos) { | |
| Point d = u - v; | |
| ans += std::abs(d.x) + std::abs(d.y) - 1; | |
| } | |
| return ans; | |
| } | |
| }; | |
| struct TextReadWrap { | |
| struct BadFormatError { String msg; }; | |
| TextReader cin; | |
| int32 r = 0; | |
| TextReadWrap(String fname) : cin(fname) {} | |
| void badFormat(String msg) { throw BadFormatError{ U"Problem format error ({}) on line {}"_fmt(msg, r) }; } | |
| String readLine() { r++; return cin.readLine().value_or_eval([&]() -> String { badFormat(U"line read failed"); }); } | |
| Array<int64> readLineSplitInt(int32 num, int64 low, int64 high) { | |
| auto sp = readLine().split(U' '); | |
| if ((int32)sp.size() < num) badFormat(U"integer token missing"); | |
| size_t i = 0; | |
| return sp.map([&](String f) -> int64 { | |
| auto k = ParseIntOpt<int64>(f).value_or_eval([&]() -> int64 { badFormat(U"integer parse error"); }); | |
| if (k < low || high < k) badFormat(U"value range is invalid #{}"_fmt(i)); | |
| i++; | |
| return k; }); | |
| } | |
| }; | |
| struct Problem { | |
| Board board; | |
| auto width() const { return board.width(); } | |
| auto height() const { return board.height(); } | |
| int32 numEntity() const { return int32(width() * height() / 2); } | |
| static Problem LoadFromTextFormat(String fname) { | |
| TextReadWrap cin{ fname }; | |
| Problem res; | |
| int32 n = (int32)cin.readLineSplitInt(1, 2, 1000)[0], m = n * n / 2; | |
| res.board.grid.assign(n, n, -1); | |
| Array<int32> cnt(m); | |
| for (int32 i = 0; i < n; i++) { | |
| auto g = cin.readLineSplitInt(n, 0, m - 1); | |
| for (int32 j : step((int32)n)) { | |
| res.board.grid[Point{ j, i }] = (int32)g[j]; | |
| } | |
| } | |
| return res; | |
| } | |
| static Problem FromJson(JSON json) { | |
| Problem res; | |
| json = json[U"problem"][U"field"]; | |
| int32 n = json[U"size"].get<int32>(); | |
| auto g = json[U"entities"]; | |
| res.board.grid.resize({ n, n }); | |
| for (auto p : step(Size(n, n))) res.board.grid[p] = g[p.y][p.x].get<int32>(); | |
| return res; | |
| } | |
| VerifyResult verify(bool strict = false) { | |
| if (strict && width() != height()) return { false, U"width != height" }; | |
| auto w = width(); | |
| auto h = height(); | |
| if (w < 4) return { false, U"width < 4" }; | |
| if (h < 4) return { false, U"height < 4" }; | |
| if (w * h % 2 != 0) return { false, U"(number of entity) % 2 != 0" }; | |
| Array<int32> occurrence(numEntity()); | |
| for (auto p : step(Size(w, h))) { | |
| auto v = board.grid[p]; | |
| if (!(0 <= v && v < numEntity())) return { false, U"entity id out of range" }; | |
| occurrence[v]++; | |
| } | |
| for (auto a : step(numEntity())) if (occurrence[a] != 2) return { false, U"(entity {} occurrence) != 2"_fmt(a) }; | |
| return { true, U"" }; | |
| } | |
| }; | |
| struct Solution { | |
| Array<Operation> ops; | |
| int32 operationCount() const { return (int32)ops.size(); } | |
| Operation get(size_t i) const { return ops[i]; } | |
| static Solution LoadFromTextFormat(String fname) { | |
| TextReadWrap cin{ fname }; | |
| Solution res; | |
| int32 n = (int32)(cin.readLineSplitInt(1, 2, 100000)[0]); | |
| res.ops = step(n).map([&](auto) -> Operation { | |
| auto g = cin.readLineSplitInt(n, 0, 1000); | |
| return Operation{ g[0], g[1], g[2] }; | |
| }); | |
| return res; | |
| } | |
| static Solution FromJson(JSON json) { | |
| Solution res; | |
| for (auto op : json[U"ops"].arrayView()) res.ops.push_back(Operation::FromJson(op)); | |
| return res; | |
| } | |
| JSON toJson() const { | |
| Array<JSON> opsJson = ops.map([&](Operation op) -> JSON { return op.toJson(); }); | |
| JSON res; | |
| res[U"ops"] = opsJson; | |
| return res; | |
| } | |
| VerifyResult verify(const Problem& problem) { | |
| int32 x_max = (int32)problem.width(); | |
| int32 y_max = (int32)problem.height(); | |
| int32 i = 0; | |
| for (auto& op : ops) { | |
| if (op.r < 1) return { false, U"op.r < 0 at ops[{}]"_fmt(i) }; | |
| if (op.r >= x_max) return { false, U"op.r >= width at ops[{}]"_fmt(i) }; | |
| if (op.r >= y_max) return { false, U"op.r >= height at ops[{}]"_fmt(i) }; | |
| if (op.x < 0) return { false, U"rotation area range error(0) at ops[{}]"_fmt(i) }; | |
| if (op.x + op.r - 1 >= x_max) return { false, U"rotation area range error(1) at ops[{}]"_fmt(i) }; | |
| if (op.y < 0) return { false, U"rotation area range error(2) at ops[{}]"_fmt(i) }; | |
| if (op.y + op.r - 1 >= y_max) return { false, U"rotation area range error(3) at ops[{}]"_fmt(i) }; | |
| i++; | |
| } | |
| return { true, U"" }; | |
| } | |
| }; | |
| struct SolutionScore { | |
| bool isValid; | |
| int32 numPair; | |
| int32 numOperation; | |
| }; | |
| struct SolutionProgress { | |
| Problem task; | |
| Solution sol; | |
| Array<Board> allBoard; | |
| void setSolution( | |
| Problem _task, | |
| Solution _sol | |
| ) { | |
| task = std::move(_task); | |
| sol = std::move(_sol); | |
| allBoard.resize(sol.operationCount() + 1); | |
| allBoard[0] = task.board; | |
| for (size_t i = 0; i < sol.operationCount(); i++) { | |
| allBoard[i + 1] = allBoard[i].appliedOperation(sol.get(i)); | |
| } | |
| } | |
| int32 sceneCount() const { | |
| return sol.operationCount() + 1; | |
| } | |
| }; | |
| Array<ColorF> GetColorSet(int32 ty) { | |
| Array<ColorF> colors; | |
| if (ty == 0) { | |
| colors.push_back(Palette::Red); | |
| colors.push_back(Palette::Orange); | |
| colors.push_back(Palette::Blue); | |
| colors.push_back(Palette::Cyan); | |
| } | |
| if (ty == 1) { | |
| colors.push_back(Palette::Darkred); | |
| colors.push_back(Palette::Darkgoldenrod); | |
| colors.push_back(Palette::Darkblue); | |
| colors.push_back(Palette::Darkcyan); | |
| } | |
| return colors; | |
| } | |
| SolutionProgress MakeSolutionProgress(Problem problem, Solution solution) { | |
| SolutionProgress res; | |
| res.task = problem; | |
| res.sol = solution; | |
| res.allBoard = {}; | |
| res.allBoard.push_back(res.task.board); | |
| for (auto& op : res.sol.ops) { | |
| res.allBoard.push_back(res.allBoard.back().appliedOperation(op)); | |
| } | |
| return res; | |
| } | |
| ColorF RandomEntityColor() { | |
| return ColorF(Random(), Random(), Random(), 1.0); | |
| } | |
| String PROBLEM_PATH = U"problem.json"; | |
| String SOLUTION_PATH = U"solution.json"; | |
| String SOLUTION_DIRECTORY = U"sols"; | |
| bool ReloadFromJson(Problem& problem, Solution& solution, JSON problemJson, JSON solutionJson) { | |
| Problem problem_tmp; | |
| Solution solution_tmp; | |
| try { | |
| problem_tmp = Problem::FromJson(problemJson); | |
| solution_tmp = Solution::FromJson(solutionJson); | |
| } | |
| catch (...) { | |
| Console << U"a file is not correctly saved"; | |
| return false; | |
| } | |
| auto pverify = problem_tmp.verify(); | |
| if (!pverify.isOk) { | |
| Console << pverify.msg; | |
| return false; | |
| } | |
| auto sverify = solution_tmp.verify(problem_tmp); | |
| if (!sverify.isOk) { | |
| Console << sverify.msg; | |
| return false; | |
| } | |
| std::swap(problem_tmp, problem); | |
| std::swap(solution_tmp, solution); | |
| return true; | |
| } | |
| bool ReloadFromJsonString(Problem& problem, Solution& solution, String problemJsonString, String solutionJsonString) { | |
| JSON jsonProblem; | |
| JSON jsonSolution; | |
| try { | |
| jsonProblem = JSON::Parse(problemJsonString); | |
| jsonSolution = JSON::Parse(solutionJsonString); | |
| return ReloadFromJson(problem, solution, jsonProblem, jsonSolution); | |
| } | |
| catch (...) { | |
| Console << U"json load error"; | |
| return false; | |
| } | |
| } | |
| bool ReloadFromFile(Problem& problem, Solution& solution) { | |
| try { | |
| const JSON jsonProblem = JSON::Load(PROBLEM_PATH); | |
| const JSON jsonSolution = JSON::Load(SOLUTION_PATH); | |
| return ReloadFromJson(problem, solution, jsonProblem, jsonSolution); | |
| } | |
| catch (...) { | |
| Console << U"a file is not correctly saved"; | |
| return false; | |
| } | |
| } | |
| void VisualizerMain() | |
| { | |
| Problem problem; | |
| Solution solution; | |
| SolutionProgress digest; | |
| Grid<Vec2> animOffset(problem.width(), problem.height(), Vec2()); | |
| Grid<int32> animHighlight(problem.width(), problem.height(), 0); | |
| int32 id = 0; | |
| double animFrame = 0.0; | |
| double animSpeed = 0.0; | |
| bool animating = false; | |
| double sliderVal = 0.0; | |
| bool bad_load = true; | |
| double auto_anim_speed = 0.0; | |
| double uiAreaBorderY = 50.0; | |
| RectF boardDisplayRect; | |
| Array<String> uiPhaseId = { U"Board", U"Graph", U"File" }; | |
| int32 uiPhase = 0; | |
| // phase 0 : board | |
| int32 maxOpSize = 1; | |
| Array<int32> opCountBySize; | |
| Array<int32> opCountBySizeAll; | |
| Grid<double> visualHeat; | |
| const double heatDuration = 0.5; | |
| Array<ColorF> entityColor; | |
| // phase 1 : statistic graph | |
| RectF graph1Rect = RectF(50.0, uiAreaBorderY + 20.0, 550.0, 350.0).movedBy(Vec2(0.5, 0.5)); | |
| Array<double> matchGraphData; | |
| Array<double> distGraphData; | |
| // phase 2 : file import | |
| #ifdef ForWeb | |
| TextEditState problemFileTextArea; | |
| TextEditState solutionFileTextArea; | |
| #else | |
| TextAreaEditState problemFileTextArea; | |
| TextAreaEditState solutionFileTextArea; | |
| #endif | |
| RectF problemFileAreaRect = RectF(50.0, uiAreaBorderY + 20.0, 550.0, 200.0); | |
| RectF solutionFileAreaRect = RectF(50.0, uiAreaBorderY + 230.0, 550.0, 200.0); | |
| int32 jsonLoadStatus = 0; | |
| bool flagMarkPairs = true; | |
| bool flagColorByDistance = true; | |
| bool flagConnectPairs = false; | |
| bool flagEntityId = false; | |
| bool flagHeat = true; | |
| bool switchUI = true; | |
| auto UpdateTurnId = [&](int32 newId) { | |
| if (newId < id) { | |
| for (auto& h : visualHeat) h = 0.0; | |
| } | |
| id = newId; | |
| opCountBySize.assign(maxOpSize, 0); | |
| for (int32 i = 0; i < id; i++) { | |
| opCountBySize[solution.ops[i].r - 1] += 1; | |
| } | |
| if (id != int32(digest.sol.operationCount())) { | |
| auto op = digest.sol.ops[id]; | |
| } | |
| else { | |
| auto_anim_speed = 0.0; | |
| animSpeed = 0.0; | |
| animating = false; | |
| } | |
| animFrame = 0.0; | |
| sliderVal = (double)id / digest.sol.operationCount(); | |
| }; | |
| auto when_reloaded = [&]() -> void { | |
| digest = MakeSolutionProgress(problem, solution); | |
| auto board_size = problem.board.size(); | |
| id = 0; | |
| animFrame = 0.0; | |
| animSpeed = 0.0; | |
| animating = false; | |
| boardDisplayRect = RectF(50.0, uiAreaBorderY + 5.0, 500.0, 500.0); | |
| maxOpSize = (int32)std::min(board_size.x, board_size.y); | |
| opCountBySize.assign(maxOpSize, 0); | |
| opCountBySizeAll = opCountBySize; | |
| for (auto& op : solution.ops) opCountBySizeAll[op.r - 1] += 1; | |
| visualHeat.resize(board_size); | |
| entityColor.resize(board_size.area() / 2); | |
| for (auto& c : entityColor) c = RandomEntityColor(); | |
| matchGraphData = digest.allBoard.map([&](const Board& b) -> double { return (double)b.numAdjacentPairs() / digest.task.numEntity(); }); | |
| double maxDist = 0.0; | |
| { | |
| auto [n, m] = board_size; | |
| for (int32 i : step(n)) for (int32 j : step(m)) maxDist += std::abs(m - 1 - j * 2); | |
| for (int32 i : step(n)) for (int32 j : step(m)) maxDist += std::abs(n - 1 - i * 2); | |
| maxDist /= 2.0; | |
| } | |
| maxDist -= problem.numEntity(); | |
| distGraphData = digest.allBoard.map([&](const Board& b) -> double { return (double)b.sumManhattanDist() / maxDist; }); | |
| UpdateTurnId(0); | |
| }; | |
| auto reload = [&]() -> void { | |
| bad_load = !ReloadFromFile(problem, solution); | |
| if (!bad_load) when_reloaded(); | |
| }; | |
| #ifdef ForWeb | |
| Font font{ 30 }; | |
| Font boldFont{ 30 }; | |
| #else | |
| Font font{ 30 }; | |
| Font boldFont{ 30, Typeface::Bold }; | |
| #endif | |
| //reload(); | |
| ReloadFromJsonString(problem, solution, SampleProblemJson, SampleSolutionJson); | |
| when_reloaded(); | |
| bad_load = false; | |
| while (System::Update()) | |
| { | |
| // control R,L arrow keys | |
| { | |
| if (KeyRight.down()) { | |
| if (animating) { | |
| animSpeed = 0.0; animFrame = 0.0; | |
| animating = false; | |
| if (id + 1 < int32(digest.sceneCount())) { | |
| UpdateTurnId(id + 1); | |
| } | |
| } | |
| if (id + 1 < int32(digest.sceneCount())) { | |
| animSpeed = 0.04; | |
| animating = true; | |
| } | |
| } | |
| if (KeyLeft.down()) { | |
| if (animating) { | |
| animSpeed = 0.0; animFrame = 0.0; | |
| animating = false; | |
| } | |
| else { | |
| if (id - 1 >= 0) UpdateTurnId(id - 1); | |
| animating = false; | |
| } | |
| } | |
| } | |
| // control number keys | |
| { | |
| int32 input_number_key = 0; | |
| if (Key1.down()) input_number_key = 1; | |
| if (Key2.down()) input_number_key = 2; | |
| if (Key3.down()) input_number_key = 3; | |
| if (Key4.down()) input_number_key = 4; | |
| if (Key5.down()) input_number_key = 5; | |
| if (Key6.down()) input_number_key = 6; | |
| if (Key7.down()) input_number_key = 7; | |
| if (Key8.down()) input_number_key = 8; | |
| if (Key9.down()) input_number_key = 9; | |
| input_number_key *= 2; | |
| if (input_number_key != 0 && KeyShift.pressed()) input_number_key -= 1; | |
| if (input_number_key != 0) { | |
| if (input_number_key <= 2) { | |
| auto_anim_speed = 0.0; | |
| } | |
| else { | |
| auto_anim_speed = (double)(digest.sceneCount() - 1) * 0.5 / 60.0 / Pow(1.5, 18 - input_number_key); | |
| auto_anim_speed = Max(auto_anim_speed, 0.005); | |
| animSpeed = auto_anim_speed; | |
| animating = true; | |
| } | |
| } | |
| } | |
| // animation step | |
| int32 prevTurnStep = animFrame <= 0.0 ? id : id + 1; | |
| { | |
| if (animSpeed >= 1.0) { | |
| animFrame = 0.0; | |
| UpdateTurnId(Clamp<int32>(id + (int32)Round(animSpeed), 0, digest.sceneCount() - 1)); | |
| animSpeed = auto_anim_speed; | |
| animating = auto_anim_speed != 0.0; | |
| } | |
| else { | |
| animFrame += animSpeed; | |
| if (animFrame > 1.0) { | |
| animSpeed = auto_anim_speed; | |
| animFrame = 0.0; | |
| animating = auto_anim_speed != 0.0; | |
| UpdateTurnId(id + 1); | |
| } | |
| } | |
| } | |
| int32 newTurnStep = animFrame <= 0.0 ? id : id + 1; | |
| // tick visual heat | |
| { | |
| double delta = Scene::DeltaTime(); | |
| for (auto& h : visualHeat) { | |
| h -= delta; | |
| if (h < 0.0) h = 0.0; | |
| } | |
| // heaten | |
| for (auto i = prevTurnStep; i < newTurnStep; i++) { | |
| auto op = digest.sol.ops[i]; | |
| for (auto x = op.x; x < op.x + op.r; x++) { | |
| for (auto y = op.y; y < op.y + op.r; y++) { | |
| visualHeat[Point(x, y)] = heatDuration; | |
| } | |
| } | |
| } | |
| } | |
| if (uiPhase == 0) { | |
| boardDisplayRect.drawFrame(); | |
| // Array<ColorF> colors = GetColorSet(1); | |
| Array<ColorF> colors = GetColorSet(0); | |
| Array<ColorF> colorsHighlighted = GetColorSet(0); | |
| // draw | |
| { | |
| // bool anim_enabled = animSpeed <= 0.2 && animating; | |
| auto viewport = ScopedViewport2D(boardDisplayRect.asRect()); | |
| RectF drawArea{ Vec2::Zero(), boardDisplayRect.size }; | |
| double massDisplayWidth = std::max(drawArea.w / digest.task.width(), drawArea.h / digest.task.height()) * 0.95; | |
| Vec2 gridAreaSize = Vec2(digest.task.width(), digest.task.height()) * massDisplayWidth; | |
| RectF gridArea{ drawArea.center() - gridAreaSize / 2.0, gridAreaSize }; | |
| Vec2 displayMassSize{ massDisplayWidth, massDisplayWidth }; | |
| double textSize = massDisplayWidth * 0.45; | |
| Optional<Operation> animatingOp = none; | |
| if (0.0 < animFrame) animatingOp = digest.sol.ops[id]; | |
| Vec2 opCenterInDisplay = Vec2::Zero(); | |
| if (animatingOp.has_value()) { | |
| Vec2 opCenter = Vec2(animatingOp->x + animatingOp->r * 0.5, animatingOp->y + animatingOp->r * 0.5); | |
| opCenterInDisplay = gridArea.tl() + displayMassSize * opCenter; | |
| } | |
| // calc pair mark | |
| Array<int32> pairMarks(digest.task.numEntity(), 0); | |
| { | |
| auto& grid = digest.allBoard[id].grid; | |
| int32 req = 0; | |
| req += 1; | |
| for (const auto& [x, y] : step(digest.task.board.size())) { | |
| if (0 < x && grid[Point(x - 1, y)] == grid[Point(x, y)]) { | |
| pairMarks[grid[Point(x, y)]] += 1; | |
| } | |
| if (0 < y && grid[Point(x, y - 1)] == grid[Point(x, y)]) { | |
| pairMarks[grid[Point(x, y)]] += 1; | |
| } | |
| } | |
| if (animFrame > 0.0) { | |
| auto& nxgrid = digest.allBoard[id + 1].grid; | |
| req += 1; | |
| for (const auto& [x, y] : step(digest.task.board.size())) { | |
| if (0 < x && nxgrid[Point(x - 1, y)] == nxgrid[Point(x, y)]) { | |
| pairMarks[nxgrid[Point(x, y)]] += 1; | |
| } | |
| if (0 < y && nxgrid[Point(x, y - 1)] == nxgrid[Point(x, y)]) { | |
| pairMarks[nxgrid[Point(x, y)]] += 1; | |
| } | |
| } | |
| } | |
| for (auto& a : pairMarks) { | |
| a = (a == req ? 0 : -1); | |
| } | |
| } | |
| Array<std::array<Vec2, 2>> entityCenterPos(problem.numEntity()); | |
| Grid<Polygon> massAfterRotate(problem.board.size()); | |
| // calc mass position | |
| for (auto p : step(problem.board.size())) { | |
| bool massRotating = animatingOp.has_value() && animatingOp->inRange(p.x, p.y); | |
| RectF rect = RectF(gridArea.tl() + displayMassSize * Vec2(p), displayMassSize); | |
| Polygon beforeRotate = rect.asPolygon(); | |
| Polygon afterRotate = beforeRotate; | |
| if (massRotating) afterRotate.rotateAt(opCenterInDisplay, 0.5 * Math::Pi * animFrame); | |
| auto center = afterRotate.centroid(); | |
| auto entityId = digest.allBoard[id].grid[p]; | |
| entityCenterPos[entityId][0] = center; | |
| std::swap(entityCenterPos[entityId][0], entityCenterPos[entityId][1]); | |
| massAfterRotate[p] = afterRotate; | |
| } | |
| double minDistMass = 1.0; | |
| double maxDistMass = gridAreaSize.length(); | |
| // mass background | |
| for (int rotated_priority : step(2)) { | |
| for (auto p : step(problem.board.size())) { | |
| bool massRotating = animatingOp.has_value() && animatingOp->inRange(p.x, p.y); | |
| if ((massRotating && rotated_priority == 0) || (!massRotating && rotated_priority == 1)) continue; | |
| Polygon afterRotate = massAfterRotate[p]; | |
| auto center = afterRotate.centroid(); | |
| auto entityId = digest.allBoard[id].grid[p]; | |
| ColorF entityPaintColor = entityColor[entityId].withAlpha(0.25); | |
| if (flagColorByDistance) { | |
| double distEntity = entityCenterPos[entityId][0].distanceFrom(entityCenterPos[entityId][1]); | |
| double distanceScore = 0.0; | |
| if (distEntity > massDisplayWidth * (1.0 + 1.0e-4)) { | |
| distanceScore = Min(1.0, 0.2 + 0.8 * Clamp((distEntity - massDisplayWidth) / (maxDistMass - minDistMass), 0.0, 1.0)); | |
| } | |
| entityPaintColor = ColorF(Palette::Black).lerp(Palette::Yellow, distanceScore * 0.8); | |
| } | |
| afterRotate.draw(entityPaintColor); | |
| } | |
| } | |
| // entity id number | |
| if (flagEntityId) { | |
| for (auto p : step(problem.board.size())) { | |
| auto entityId = digest.allBoard[id].grid[p]; | |
| Polygon afterRotate = massAfterRotate[p]; | |
| auto center = afterRotate.centroid(); | |
| boldFont(ToString(entityId)).drawAt(textSize, center, Palette::White); | |
| } | |
| } | |
| // draw pair mark | |
| if (flagMarkPairs) { | |
| for (auto i : step(problem.numEntity())) { | |
| if (pairMarks[i] >= 0) { | |
| Vec2 p0 = entityCenterPos[i][0]; | |
| Vec2 p1 = entityCenterPos[i][1]; | |
| double markWidth = massDisplayWidth * 0.25; | |
| Vec2 normal = Line(p0, p1).normal() * -markWidth; | |
| Polygon mark = Polygon::Correct(Array<Vec2>({ p0 - normal, p0 + normal, p1 + normal, p1 - normal }))[0]; | |
| mark.append(Circle(p1, markWidth).asPolygon()); | |
| mark.append(Circle(p0, markWidth).asPolygon()); | |
| mark.draw(ColorF(Palette::Limegreen).withAlpha(0.3)); | |
| } | |
| } | |
| } | |
| // pairs segment | |
| if (flagConnectPairs) { | |
| for (auto i : step(problem.numEntity())) { | |
| Line(entityCenterPos[i][0], entityCenterPos[i][1]).draw(2.0, entityColor[i]); | |
| } | |
| } | |
| // heat | |
| if (flagHeat) { | |
| for (auto [x, y] : step(Size(problem.width(), problem.height()))) { | |
| RectF rect = RectF(gridArea.tl() + displayMassSize * Vec2(x, y), displayMassSize); | |
| rect.draw(Palette::Red.withAlpha((int32)(255 * 0.8 * visualHeat[Point(x, y)]))); | |
| } | |
| } | |
| } | |
| // drawText | |
| { | |
| double ypos = uiAreaBorderY + 3.0; | |
| double xpos = 570.0; | |
| double maxHist = Max(1.0, (double)*std::max_element(opCountBySizeAll.begin(), opCountBySizeAll.end())); | |
| for (int32 i = 2; i <= maxOpSize; i++) { | |
| double margin = 1.0; | |
| double h = 20.0; | |
| double hist_x = xpos + 65.0; | |
| RectF rg = { Vec2{ hist_x, ypos + margin }, Vec2{ 100.0, h - margin * 2 } }; | |
| RectF hist1 = rg; | |
| RectF hist2 = rg; | |
| hist1.w *= opCountBySizeAll[i - 1] / maxHist; | |
| hist2.w *= opCountBySize[i - 1] / maxHist; | |
| hist1.draw(Palette::White.withAlpha(64)); | |
| hist2.draw(Palette::Orange); | |
| font(U"{}:"_fmt(i, i)).draw(h - margin * 2, Arg::rightCenter(Vec2(xpos + 24.0, ypos + h * 0.5))); | |
| font(U"{}"_fmt(opCountBySize[i - 1])).draw(18.0, Arg::rightCenter(Vec2(xpos + 60.0, ypos + h * 0.5))); | |
| ypos += h; | |
| } | |
| } | |
| } | |
| else if (uiPhase == 1) { | |
| graph1Rect.drawFrame(0.0, 2.0, Palette::White.withAlpha(0), Palette::White); | |
| const int32 wn = matchGraphData.size(); | |
| int32 memori = 100; | |
| if (wn <= 500) memori = 50; | |
| if (wn <= 200) memori = 20; | |
| if (wn <= 100) memori = 10; | |
| Array<Vec2> curve(wn), curve2(wn); | |
| Array<double> relX = step(wn).asArray().map([&](auto i) { return (double)i / (curve.size() - 1); }); | |
| for (auto i : step(wn)) { | |
| curve[i] = graph1Rect.getRelativePoint(relX[i], 1.0 - matchGraphData[i]); | |
| } | |
| for (auto i : step(wn)) { | |
| curve2[i] = graph1Rect.getRelativePoint(relX[i], 1.0 - distGraphData[i]); | |
| } | |
| for (int32 i = 0; i < curve.size(); i += memori) { | |
| font(ToString(i)).draw(16.0, graph1Rect.getRelativePoint(relX[i], 1.0) + Vec2(2.0, 0.0)); | |
| } | |
| for (int32 i = 10; i <= 100; i+=10) { | |
| font(ToString(i) + U"%").draw(16.0, Arg::leftCenter(graph1Rect.getRelativePoint(1.0, 1.0 - i / 100.0) + Vec2(5.0, 0.0))); | |
| } | |
| for (auto i : step(curve.size())) if (1 <= i) Line(curve2[i], curve2[i - 1]).draw(3.0, Palette::Yellow.withAlpha(96)); | |
| for (auto i : step(curve.size())) if (1 <= i) Line(curve[i], curve[i - 1]).draw(3.0, Palette::Lime.withAlpha(96)); | |
| for (auto i : step(curve.size())) curve[i].asCircle(2.0).draw(Palette::Lime); | |
| for (int32 i = memori; i < curve.size(); i += memori) { | |
| Line(graph1Rect.getRelativePoint(relX[i], 0.0), graph1Rect.getRelativePoint(relX[i], 1.0)).draw(); | |
| } | |
| for (int32 i = 1; i < 10; i += 1) { | |
| Line(graph1Rect.getRelativePoint(0.0, i * 0.1), graph1Rect.getRelativePoint(1.0, i * 0.1)).draw(); | |
| } | |
| Line(graph1Rect.getRelativePoint(relX[id], 0.0), graph1Rect.getRelativePoint(relX[id], 1.0)).draw(Palette::Yellow); | |
| } | |
| else if (uiPhase == 2) { | |
| font(U"Problem file").draw(problemFileAreaRect.tl()); | |
| font(U"Solution file").draw(solutionFileAreaRect.tl()); | |
| if (SimpleGUI::Button(U"clear", problemFileAreaRect.tl() + Vec2(220.0, 0.0))) { | |
| problemFileTextArea.clear(); | |
| } | |
| if (SimpleGUI::Button(U"clear", solutionFileAreaRect.tl() + Vec2(220.0, 0.0))) { | |
| solutionFileTextArea.clear(); | |
| } | |
| #ifdef ForWeb | |
| SimpleGUI::TextBox(problemFileTextArea, problemFileAreaRect.tl() + Vec2(0.0, 40.0), problemFileAreaRect.size.x, 1000000); | |
| SimpleGUI::TextBox(solutionFileTextArea, solutionFileAreaRect.tl() + Vec2(0.0, 40.0), solutionFileAreaRect.size.x, 1000000); | |
| #else | |
| SimpleGUI::TextArea(problemFileTextArea, problemFileAreaRect.tl() + Vec2(0.0, 40.0), problemFileAreaRect.size - Vec2(0.0, 40.0), 1000000); | |
| SimpleGUI::TextArea(solutionFileTextArea, solutionFileAreaRect.tl() + Vec2(0.0, 40.0), solutionFileAreaRect.size - Vec2(0.0, 40.0), 1000000); | |
| #endif | |
| if (SimpleGUI::Button(U"load", solutionFileAreaRect.bl() + Vec2(0.0, 20.0))) { | |
| if (!ReloadFromJsonString(problem, solution, problemFileTextArea.text, solutionFileTextArea.text)) { | |
| jsonLoadStatus = -1; | |
| } | |
| else { | |
| jsonLoadStatus = 1; | |
| when_reloaded(); | |
| } | |
| } | |
| if (jsonLoadStatus == 1) { | |
| font(U"Success").draw(solutionFileAreaRect.bl() + Vec2(100.0, 20.0), Palette::Limegreen); | |
| } | |
| else if (jsonLoadStatus == -1) { | |
| font(U"Failed").draw(solutionFileAreaRect.bl() + Vec2(100.0, 20.0), Palette::Orange); | |
| } | |
| } | |
| // top | |
| { | |
| double xpos = 10.0; | |
| double yline = 10.0; | |
| //if (SimpleGUI::Button(U"load", Vec2(xpos, yline), 80.0)) { reload(); } xpos += 100.0; | |
| if (bad_load) { | |
| font(U"FAILED TO LOAD").draw(Vec2(xpos, yline), Palette::Orange); xpos += 250.0; | |
| } | |
| else { | |
| String turnText = U"{} / {}"_fmt(id, solution.operationCount()); | |
| RectF reg = font(turnText).region(Arg::leftCenter(Vec2(xpos, (yline + uiAreaBorderY) / 2))); | |
| font(turnText).draw(Arg::leftCenter(Vec2(xpos, (yline + uiAreaBorderY) / 2))); | |
| font(U"turns").draw(20.0, Arg::bottomLeft(reg.br() + Vec2(5.0, 0.0))); | |
| xpos += 250.0; | |
| } | |
| int32 numAdjacent = digest.allBoard[id].numAdjacentPairs(); | |
| double prop = (double)numAdjacent / digest.task.numEntity(); | |
| RectF gaugeOutRect = RectF::FromPoints(Vec2(xpos, yline), Vec2(boardDisplayRect.tr().x, uiAreaBorderY)); | |
| RectF gaugeRect = RectF::FromPoints(gaugeOutRect.getRelativePoint(0.0, 0.1), gaugeOutRect.getRelativePoint(1.0, 0.9)); | |
| RectF gaugeActiveRect = RectF(gaugeRect.pos, gaugeRect.size * Vec2(prop, 1.0)); | |
| gaugeRect.stretched(2.0).draw(Palette::Gray); | |
| gaugeRect.draw(Palette::Black); | |
| gaugeActiveRect.draw(Palette::Darkgreen); | |
| font(U"{} / {} Pairs"_fmt(numAdjacent, digest.task.numEntity())).draw(20.0, Arg::rightCenter(gaugeRect.rightCenter() - Vec2(10.0, 0.0))); xpos += 250.0; | |
| Line(Vec2(0.0, uiAreaBorderY), Vec2(1000.0, uiAreaBorderY)).movedBy(Vec2(0.0, 0.5)).draw(Palette::White); | |
| } | |
| // UI controller | |
| if (switchUI) { | |
| RectF UIControlArea = RectF::FromPoints(Vec2(560.0, 10.0), Vec2(790.0, 50.0)); | |
| double xpos = UIControlArea.x; | |
| double margin = 6.0; | |
| for (auto i : step(uiPhaseId.size())) { | |
| double wid = font(uiPhaseId[i]).region(18.0).w; | |
| RectF usearea = RectF(Vec2(xpos, UIControlArea.y), Vec2(wid + margin * 2.0, UIControlArea.h)); | |
| RectF hitbox = usearea.stretched(-2.0); | |
| if (i == uiPhase) { | |
| hitbox.draw(Palette::Yellow.withAlpha(128)); | |
| } | |
| font(uiPhaseId[i]).drawAt(18.0, usearea.center()); | |
| xpos += usearea.w; | |
| if (hitbox.leftClicked()) uiPhase = int32(i); | |
| } | |
| } | |
| // Slider | |
| { | |
| if (SimpleGUI::Slider(sliderVal, Vec2(0.0, 560.0), 600.0)) { | |
| int32 nextTurnId = (int32)Round(sliderVal * solution.operationCount()); | |
| UpdateTurnId(nextTurnId); | |
| } | |
| } | |
| // check boxes | |
| if (switchUI && uiPhase == 0) { | |
| double cb_max_width = 0.0; | |
| auto txSize = font(U"[Space] : show/hide").region(15.0).size; | |
| cb_max_width = Max(cb_max_width, txSize.x); | |
| auto cbSize_1 = SimpleGUI::CheckBoxRegion(U"Connect Pairs", Vec2::Zero()).size; | |
| cb_max_width = Max(cb_max_width, cbSize_1.x); | |
| auto cbSize_2 = SimpleGUI::CheckBoxRegion(U"Mark Pairs", Vec2::Zero()).size; | |
| cb_max_width = Max(cb_max_width, cbSize_2.x); | |
| auto cbSize_3 = SimpleGUI::CheckBoxRegion(U"Distance Color", Vec2::Zero()).size; | |
| cb_max_width = Max(cb_max_width, cbSize_3.x); | |
| auto cbSize_4 = SimpleGUI::CheckBoxRegion(U"Entity ID", Vec2::Zero()).size; | |
| cb_max_width = Max(cb_max_width, cbSize_4.x); | |
| auto cbSize_5 = SimpleGUI::CheckBoxRegion(U"Heat", Vec2::Zero()).size; | |
| cb_max_width = Max(cb_max_width, cbSize_5.x); | |
| Vec2 cb_pos = Scene::Rect().br(); | |
| cb_pos.x -= cb_max_width; | |
| font(U"[Space] : show/hide").draw(15.0, Arg::bottomLeft(cb_pos)); | |
| cb_pos.y -= txSize.y; | |
| cb_pos.y -= cbSize_1.y; | |
| SimpleGUI::CheckBox(flagConnectPairs, U"Connect Pairs", cb_pos); | |
| cb_pos.y -= cbSize_2.y; | |
| SimpleGUI::CheckBox(flagMarkPairs, U"Mark Pairs", cb_pos); | |
| cb_pos.y -= cbSize_3.y; | |
| SimpleGUI::CheckBox(flagColorByDistance, U"Distance Color", cb_pos); | |
| cb_pos.y -= cbSize_4.y; | |
| SimpleGUI::CheckBox(flagEntityId, U"Entity ID", cb_pos); | |
| cb_pos.y -= cbSize_5.y; | |
| SimpleGUI::CheckBox(flagHeat, U"Heat", cb_pos); | |
| } | |
| if (KeySpace.down()) { | |
| switchUI = !switchUI; | |
| } | |
| } | |
| } | |
| void Main() { | |
| VisualizerMain(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment