1
0
mirror of https://github.com/robonen/eulerian-cycle.git synced 2026-03-20 02:44:47 +00:00

Added pop-ups, guide, fixed selection when dragging and duplicating links

This commit is contained in:
2021-12-15 05:13:04 +07:00
parent 0dc400d78c
commit 098d020105
10 changed files with 289 additions and 228 deletions

View File

@@ -4,4 +4,6 @@
<style>
@import url("~@/assets/css/formular.css");
@import "~@/assets/css/creation.css";
@import "~@/assets/css/graph.css";
</style>

View File

@@ -13,7 +13,7 @@
background-color: var(--body-color);
}
body>#app {
body > #app {
margin: 0;
font-family: "Formular";
height: 100%;
@@ -308,7 +308,7 @@ header {
width: 20px;
height: 20px;
opacity: 0.6;
transition: .2s;
transition: 0.2s;
}
.menu-icon:last-child {
@@ -394,7 +394,8 @@ p {
font-weight: 500;
}
.menu-prompt, .prompt {
.menu-prompt,
.prompt {
left: 100%;
position: absolute;
z-index: 6;
@@ -410,7 +411,7 @@ p {
border-radius: 4px;
opacity: 0;
box-shadow: 0px 1px 5px 1px 0px 1px 5px 1px rgb(56 58 63 / 35%);
transition: .2s;
transition: 0.2s;
display: block;
width: -webkit-max-content;
width: -moz-max-content;
@@ -457,10 +458,11 @@ p {
transform: translate(-50%, 0);
}
.menu-icon:hover .menu-prompt, .control-button:hover .prompt {
.menu-icon:hover .menu-prompt,
.control-button:hover .prompt {
opacity: 1;
visibility: visible;
transition-delay: .5s;
transition-delay: 0.5s;
}
.error {
@@ -474,6 +476,7 @@ p {
position: absolute;
z-index: 5;
box-shadow: 0px 1px 5px 1px rgb(56 58 63 / 15%);
cursor: pointer;
}
.popup-el.popup-button-cont {
@@ -505,3 +508,22 @@ p {
color: gray;
font-size: 14px;
}
.fade-enter-active {
animation: fade-in 0.5s;
}
.fade-leave-active {
animation: fade-in 0.5s reverse;
}
@keyframes fade-in {
from {
transform: translateY(-40px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View File

@@ -4,6 +4,7 @@
ref="renderer"
@dblclick="createNode"
@click.left.stop="unselectAllNodes"
@contextmenu.prevent
>
<g>
<component
@@ -66,6 +67,9 @@ export default {
// Const
const RADIUS = 25;
// Vars
let draggableNode = false;
// Reactive
const renderer = ref(null);
@@ -104,6 +108,7 @@ export default {
// }
// );
// Methods
const loopPosition = (coords) => {
const node = nodes.value[coords];
@@ -117,7 +122,6 @@ export default {
},${y}`;
};
// Methods
const hasntIntersections = (node) => {
return nodes.value.every((current) => {
return (
@@ -127,13 +131,13 @@ export default {
});
};
// Nodes
const activateNodes = (ids) =>
ids.forEach((e) => (nodes.value[e].selected = true));
const deactivateNodes = (ids) =>
ids.forEach((e) => (nodes.value[e].selected = false));
// Nodes
const createNode = ({ offsetX, offsetY }) => {
if (nodes.value.length >= 99) return;
@@ -167,6 +171,11 @@ export default {
};
const selectNode = (id) => {
if (draggableNode) {
draggableNode = false;
return;
}
if (linker.sourceEmpty()) {
linker.setSource(id);
activateNodes([id]);
@@ -207,19 +216,36 @@ export default {
const nodeMove = ({ offsetX, offsetY }) => {
const d = drag.value;
draggableNode = true;
nodes.value[d.id].x = offsetX - d.offsetX;
nodes.value[d.id].y = offsetY - d.offsetY;
};
// Links
linker.onLink((source, target) => {
let duplicateLink = null;
links.value.forEach((e, idx) => {
if (
(e.source === source && e.target === target) ||
(e.source === target && e.target === source)
)
duplicateLink = idx;
});
if (duplicateLink !== null) {
links.value.splice(duplicateLink, 1);
return;
}
links.value.push({
selected: false,
source,
target,
});
euler.loadLinks([...links.value]);
euler.loadLinks(Object.values(links.value));
if (euler.check()) emit("isEuler", euler.find());
else emit("isEuler", []);
@@ -254,7 +280,8 @@ svg {
}
circle,
line {
line,
path {
cursor: pointer;
}

67
src/components/Guide.vue Normal file
View File

@@ -0,0 +1,67 @@
<template>
<popup
leftBtnText="Назад"
:rightBtnText="currentStep + 1 === steps.length ? 'Завершить' : 'Далее'"
@left="stepDown"
@right="stepUp"
@close="close"
>
<template v-slot:title>
{{ steps[currentStep].name }}
<span class="version">{{ currentStep + 1 }} / {{ steps.length }}</span>
</template>
<template v-slot:content>
{{ steps[currentStep].content }}
</template>
</popup>
</template>
<script>
import { ref } from "vue";
import Popup from "./Popup";
export default {
name: "Guide",
components: {
Popup,
},
props: {
steps: {
type: Array,
required: true,
},
},
setup(props, { emit }) {
// Reactive
const currentStep = ref(0);
// Methods
const stepDown = () => {
if (currentStep.value <= 0) return;
currentStep.value -= 1;
};
const stepUp = () => {
if (currentStep.value + 1 >= props.steps.length) {
close();
return;
}
currentStep.value += 1;
};
const close = () => {
currentStep.value = 0;
emit("close");
};
return {
currentStep,
stepDown,
stepUp,
close,
};
},
};
</script>

View File

@@ -1,24 +1,37 @@
<template>
<div class="adjacency_matrix-cont">
<div class="adjacency_matrix">
<div class="adjacency_matrix-menu">
<div class="header header-matrix">Матрица смежности</div>
<div class="menu-icon close-icon"></div>
</div>
<div class="matrix-cont">
<div class="value-rows-matrix-cont">
<div class="value-row-matrix" v-for="ni in size" :key="ni">
{{ ni }}
</div>
<div class="value-row-matrix">0</div>
<div class="value-row-matrix">1</div>
<div class="value-row-matrix">2</div>
</div>
<div class="body-matrix">
<div class="matrix">
<div class="matrix-column" v-for="nj in size" :key="nj">
<div class="value-matrix-column">{{ nj }}</div>
<div
class="matrix-cell"
v-for="ni in size"
:key="ni"
:class="{
'active-cell': isActive(ni - 1, nj - 1),
'auto-matrix-cell': isAbove(ni - 1, nj - 1),
}"
@click="change(ni - 1, nj - 1)"
></div>
<div class="matrix-column">
<div class="value-matrix-column">0</div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
</div>
<div class="matrix-column">
<div class="value-matrix-column">1</div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
</div>
<div class="matrix-column">
<div class="value-matrix-column">2</div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
</div>
</div>
</div>
</div>
</div>
@@ -26,67 +39,7 @@
</template>
<script>
// active-cell - one with background
// matrix-cell - default
// auto-matrix-cell - without background
// active-auto-cell - one
const newArray = (size) =>
Array(size)
.fill(0)
.map(() => Array(size).fill(0));
export default {
name: "Matrix",
props: {
size: {
type: Number,
required: true,
},
},
data() {
return {
matrix: newArray(this.size),
};
},
watch: {
size(val) {
this.matrix = newArray(val);
},
},
methods: {
isActive(i, j) {
return this.matrix[i][j] === 1;
},
isAbove(i, j) {
return i > j;
},
change(i, j) {
const val = this.matrix[i][j] === 1 ? 0 : 1;
this.matrix[i][j] = val;
if (i != j) this.matrix[j][i] = val;
this.checkEuler();
},
checkEuler() {
const isValid = this.matrix.reduce(
(res, current) => {
const relations = current.reduce((sum, i) => sum + i);
res.sum += relations;
res.even &= !(relations % 2);
return res;
},
{ sum: 0, even: 1 }
);
const result = isValid.sum && isValid.even;
this.$emit("valid", result);
},
},
};
</script>

58
src/components/Popup.vue Normal file
View File

@@ -0,0 +1,58 @@
<template>
<div class="popup-cont">
<div class="popup">
<div class="popup-el popup-header">
<div class="header">
<slot name="title"></slot>
</div>
<div class="menu-icon close-icon" @click="closeBtn"></div>
</div>
<div class="popup-el popup-text">
<slot name="content"></slot>
</div>
<!-- <div class="popup-el popup-text gray">by Robonen</div> -->
<div class="popup-el popup-button-cont">
<div v-if="leftBtnText" class="popup-button" @click="leftBtn">
{{ leftBtnText }}
</div>
<div v-if="rightBtnText" class="main-popup-button" @click="rightBtn">
{{ rightBtnText }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "View",
props: {
leftBtnText: {
type: String,
},
rightBtnText: {
type: String,
},
},
setup(_, { emit }) {
// Methods
const closeBtn = () => {
emit("close");
};
const leftBtn = () => {
emit("left");
};
const rightBtn = () => {
emit("right");
};
return {
closeBtn,
leftBtn,
rightBtn,
};
},
};
</script>

View File

@@ -6,15 +6,16 @@ const app = createApp(App).use(router);
app.directive("click-outside", {
beforeMount(el, binding) {
el.clickOutsideEvent = function (event) {
if (!(el === event.target || el.contains(event.target))) {
const ourClickEventHandler = (event) => {
if (!el.contains(event.target) && el !== event.target) {
binding.value(event, el);
}
};
document.body.addEventListener("click", el.clickOutsideEvent);
el.__vueClickEventHandler__ = ourClickEventHandler;
document.addEventListener("click", ourClickEventHandler);
},
unmounted(el) {
document.body.removeEventListener("click", el.clickOutsideEvent);
document.removeEventListener("click", el.__vueClickEventHandler__);
},
});

View File

@@ -1,64 +0,0 @@
<template>
<div class="wrapper">
<div class="input-cont">
<div class="text-input-cont">размер матрицы:</div>
<div class="number">
<button class="number-minus" type="button" @click="sub">-</button>
<input type="number" v-model="size" />
<button class="number-plus" type="button" @click="add">+</button>
</div>
</div>
<matrix
:size="size"
@valid="isValid = $event"
@save="matrix = $event"
></matrix>
<router-link
to="/view"
class="creation-button"
:class="{ 'disabled-button': !isValid }"
>
СОЗДАТЬ ГРАФ
</router-link>
</div>
</template>
<script>
import Matrix from "../components/Matrix.vue";
export default {
name: "Home",
components: {
Matrix,
},
mounted() {
document.addEventListener("keydown", (evt) => {
if (evt.keyCode === 38) this.add();
if (evt.keyCode === 40) this.sub();
});
},
data() {
return {
size: 5,
isValid: false,
};
},
methods: {
add() {
if (this.size < 30) this.size++;
},
sub() {
if (this.size > 1) this.size--;
},
},
watch: {
size(val) {
this.size = Math.min(Math.max(1, val), 30);
},
},
};
</script>
<style>
@import "~@/assets/css/creation.css";
</style>

View File

@@ -1,78 +1,38 @@
<template>
<!--<div class="adjacency_matrix-cont">
<div class="adjacency_matrix">
<div class="adjacency_matrix-menu">
<div class="header header-matrix">Матрица смежности</div>
<div class="menu-icon close-icon"></div>
</div>
<div class="matrix-cont">
<div class="value-rows-matrix-cont">
<div class="value-row-matrix">0</div>
<div class="value-row-matrix">1</div>
<div class="value-row-matrix">2</div>
</div>
<div class="body-matrix">
<div class="matrix">
<div class="matrix-column">
<div class="value-matrix-column">0</div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
</div>
<div class="matrix-column">
<div class="value-matrix-column">1</div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
</div>
<div class="matrix-column">
<div class="value-matrix-column">2</div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
<div class="auto-matrix-cell"></div>
</div>
</div>
</div>
</div>
</div>
</div>-->
<!--<div class="popup-cont">
<div class="popup">
<div class="popup-el popup-header">
<div class="header">Эйлеров граф <span class="version">v0.1</span></div>
<div class="menu-icon close-icon"></div>
</div>
<div class="popup-el popup-text">
<guide v-show="showGuide" :steps="guide" @close="showGuide = false"></guide>
<popup v-show="showInfo" @close="showInfo = false">
<template v-slot:title>
Циклы в эйлером графе
<span class="version">v0.2</span>
</template>
<template v-slot:content>
<p>
Граф как математический объект есть совокупность двух множеств
множества самих объектов, называемого множеством вершин, и множества
их парных связей, называемого множеством рёбер.
<b>Эйлеров цикл</b>
&mdash; путь, проходящий по всем ребрам графа, и при этом только по
одному разу.
</p>
<p>Элемент множества рёбер есть пара элементов множества вершин.</p>
</div>
<div class="popup-el popup-text gray">by Robonen</div>
<div class="popup-el popup-button-cont">
<div class="popup-button">Уволиться</div>
<div class="main-popup-button">Поставить 10 баллов</div>
</div>
</div>
</div>-->
</template>
</popup>
<div class="addition-cont menu">
<div class="menu-cont">
<div class="menu-icon matrix-icon">
<div class="menu-prompt">Матрица смежности</div>
</div>
<div class="menu-icon help-icon">
<div class="menu-icon help-icon" @click="showGuide = true">
<div class="menu-prompt">Обучение управлению</div>
</div>
<div class="menu-icon info-icon">
<div class="menu-icon info-icon" @click="showInfo = true">
<div class="menu-prompt">О программе</div>
</div>
</div>
</div>
<div class="wrapper">
<header>
<!--<div class="error">Эйлерова цикла в этом графе нет</div>-->
<transition name="fade">
<div v-if="errorText" class="error" @click="errorText = ''">
{{ errorText }}
</div>
</transition>
<div class="header-step-cont inaccessible">
<div class="header-step-text">
<div class="header-vertex">
@@ -142,23 +102,54 @@
</template>
<script>
import { computed, ref } from "vue";
import Graph from "../components/Graph.vue";
// Frontend не выдержит ещё одних правок. Тут и так сейчас много говна
import { computed, onMounted, ref } from "vue";
import Graph from "../components/Graph";
import Guide from "../components/Guide";
import Popup from "../components/Popup";
export default {
name: "View",
components: {
Graph,
Guide,
Popup,
},
setup() {
// Const
let timer = null;
const guide = [
{
name: "Создание вершин",
content:
"Для создания новой вершины необходимо дважды нажать левую кнопку мыши",
video: "/video/create.mp4",
},
{
name: "Связывание вершин",
content:
"Чтобы связать вершины, необходимо кликнуть левой кнопкой мыши по вершине, которую необходимо связать. Далее выбираются вершины, с которыми необходимо связать",
video: "/video/linking.mp4",
},
];
// Reactive
const steps = ref([]);
const currentStep = ref(0);
const currentStepData = ref({});
const played = ref(false);
const showInfo = ref(false);
const showGuide = ref(false);
const errorText = ref("");
// Mounted
onMounted(() => {
const key = "first_start";
if (JSON.parse(localStorage.getItem(key)) !== true) {
localStorage.setItem(key, true);
showGuide.value = true;
}
});
// Computed
const stepExists = computed(() => {
@@ -170,6 +161,10 @@ export default {
});
// Methods
const log = () => {
showInfo.value = true;
};
const getSteps = (data) => {
steps.value = data;
currentStep.value = 0;
@@ -215,12 +210,12 @@ export default {
prevStep,
play,
stop,
log,
showInfo,
showGuide,
guide,
errorText,
};
},
};
</script>
<style>
@import "~@/assets/css/creation.css";
@import "~@/assets/css/graph.css";
</style>