Fixed position of links

This commit is contained in:
thepaperpilot 2022-03-22 22:55:48 -05:00
parent 5ce8195779
commit da6a47b1da
4 changed files with 81 additions and 76 deletions

View file

@ -1,6 +1,5 @@
<template> <template>
<slot /> <slot />
<div ref="resizeListener" class="resize-listener" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -10,25 +9,12 @@ import {
NodesInjectionKey, NodesInjectionKey,
FeatureNode FeatureNode
} from "game/layers"; } from "game/layers";
import { nextTick, onMounted, provide, ref } from "vue"; import { nextTick, provide, ref } from "vue";
const observer = new MutationObserver(updateNodes);
const resizeObserver = new ResizeObserver(updateNodes);
const nodes = ref<Record<string, FeatureNode | undefined>>({}); const nodes = ref<Record<string, FeatureNode | undefined>>({});
defineExpose({ nodes }); defineExpose({ nodes });
const resizeListener = ref<Element | null>(null);
onMounted(() => {
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
const resListener = resizeListener.value;
if (resListener != null) {
resizeObserver.observe(resListener);
}
});
const observerOptions = { const observerOptions = {
attributes: true, attributes: true,
childList: true, childList: true,
@ -36,54 +22,22 @@ const observerOptions = {
}; };
provide(RegisterNodeInjectionKey, (id, element) => { provide(RegisterNodeInjectionKey, (id, element) => {
nodes.value[id] = { element }; const observer = new MutationObserver(() => updateNode(id));
observer.observe(element, observerOptions); observer.observe(element, observerOptions);
nextTick(() => { nodes.value[id] = { element, observer };
if (resizeListener.value != null) { nextTick(() => updateNode(id));
updateNode(id);
}
});
}); });
provide(UnregisterNodeInjectionKey, id => { provide(UnregisterNodeInjectionKey, id => {
nodes.value[id]?.observer?.disconnect();
nodes.value[id] = undefined; nodes.value[id] = undefined;
}); });
provide(NodesInjectionKey, nodes); provide(NodesInjectionKey, nodes);
let isDirty = true;
let boundingRect = resizeListener.value?.getBoundingClientRect();
function updateNodes() {
if (resizeListener.value != null && isDirty) {
isDirty = false;
nextTick(() => {
boundingRect = resizeListener.value?.getBoundingClientRect();
Object.keys(nodes.value).forEach(id => updateNode(id));
isDirty = true
});
}
}
function updateNode(id: string) { function updateNode(id: string) {
const node = nodes.value[id]; const node = nodes.value[id];
if (!node || boundingRect == null) { if (node == null) {
return; return;
} }
const nodeRect = node.element.getBoundingClientRect(); node.rect = node.element.getBoundingClientRect();
node.y = nodeRect.x + nodeRect.width / 2 - boundingRect.x;
node.x = nodeRect.y + nodeRect.height / 2 - boundingRect.y;
node.rect = nodeRect;
} }
</script> </script>
<style scoped>
svg,
.resize-listener {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -10;
pointer-events: none;
}
</style>

View file

@ -13,32 +13,47 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link } from "features/links/links"; import { Link } from "features/links/links";
import { FeatureNode } from "game/layers"; import { FeatureNode } from "game/layers";
import { computed, toRefs, unref } from "vue"; import { computed, toRefs } from "vue";
const _props = defineProps<{ const _props = defineProps<{
link: Link; link: Link;
startNode: FeatureNode; startNode: FeatureNode;
endNode: FeatureNode; endNode: FeatureNode;
boundingRect: DOMRect | undefined;
}>(); }>();
const props = toRefs(_props); const props = toRefs(_props);
const startPosition = computed(() => { const startPosition = computed(() => {
const position = { x: props.startNode.value.x || 0, y: props.startNode.value.y || 0 }; const rect = props.startNode.value.rect;
const boundingRect = props.boundingRect.value;
const position =
rect && boundingRect
? {
x: rect.x + rect.width / 2 - boundingRect.x,
y: rect.y + rect.height / 2 - boundingRect.y
}
: { x: 0, y: 0 };
if (props.link.value.offsetStart) { if (props.link.value.offsetStart) {
position.x += unref(props.link.value.offsetStart).x; position.x += props.link.value.offsetStart.x;
position.y += unref(props.link.value.offsetStart).y; position.y += props.link.value.offsetStart.y;
} }
return position; return position;
}); });
const endPosition = computed(() => { const endPosition = computed(() => {
const position = { x: props.endNode.value.x || 0, y: props.endNode.value.y || 0 }; const rect = props.endNode.value.rect;
const boundingRect = props.boundingRect.value;
const position =
rect && boundingRect
? {
x: rect.x + rect.width / 2 - boundingRect.x,
y: rect.y + rect.height / 2 - boundingRect.y
}
: { x: 0, y: 0 };
if (props.link.value.offsetEnd) { if (props.link.value.offsetEnd) {
position.x += unref(props.link.value.offsetEnd).x; position.x += props.link.value.offsetEnd.x;
position.y += unref(props.link.value.offsetEnd).y; position.y += props.link.value.offsetEnd.y;
} }
return position; return position;
}); });
</script> </script>
<style scoped></style>

View file

@ -4,34 +4,71 @@
v-for="(link, index) in validLinks" v-for="(link, index) in validLinks"
:key="index" :key="index"
:link="link" :link="link"
:boundingRect="boundingRect"
:startNode="nodes[link.startNode.id]!" :startNode="nodes[link.startNode.id]!"
:endNode="nodes[link.endNode.id]!" :endNode="nodes[link.endNode.id]!"
/> />
</svg> </svg>
<div ref="resizeListener" class="resize-listener" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Link } from "features/links/links"; import { Link } from "features/links/links";
import { NodesInjectionKey } from "game/layers"; import { NodesInjectionKey } from "game/layers";
import { computed, inject, toRef } from "vue"; import { computed, inject, nextTick, onMounted, ref, toRef } from "vue";
import LinkVue from "./Link.vue"; import LinkVue from "./Link.vue";
const _props = defineProps<{ links?: Link[] }>(); const _props = defineProps<{ links?: Link[] }>();
const links = toRef(_props, "links"); const links = toRef(_props, "links");
const resizeObserver = new ResizeObserver(updateNodes);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nodes = inject(NodesInjectionKey)!; const nodes = inject(NodesInjectionKey)!;
const validLinks = computed( const resizeListener = ref<Element | null>(null);
() =>
links.value?.filter(link => { onMounted(() => {
const n = nodes.value; // ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
return ( const resListener = resizeListener.value;
n[link.startNode.id]?.x != undefined && if (resListener != null) {
n[link.startNode.id]?.y != undefined && resizeObserver.observe(resListener);
n[link.endNode.id]?.x != undefined && }
n[link.endNode.id]?.y != undefined });
let isDirty = true;
let boundingRect = ref(resizeListener.value?.getBoundingClientRect());
function updateNodes() {
if (resizeListener.value != null && isDirty) {
isDirty = false;
nextTick(() => {
boundingRect.value = resizeListener.value?.getBoundingClientRect();
Object.values(nodes.value).forEach(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
node => (node!.rect = node?.element.getBoundingClientRect())
); );
}) ?? [] isDirty = true;
); });
}
}
const validLinks = computed(() => {
const n = nodes.value;
return (
links.value?.filter(link => n[link.startNode.id]?.rect && n[link.startNode.id]?.rect) ?? []
);
});
</script> </script>
<style scoped>
.resize-listener,
svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -10;
pointer-events: none;
}
</style>

View file

@ -22,9 +22,8 @@ import { persistent, PersistentRef } from "./persistence";
import player from "./player"; import player from "./player";
export interface FeatureNode { export interface FeatureNode {
x?: number;
y?: number;
rect?: DOMRect; rect?: DOMRect;
observer?: MutationObserver;
element: HTMLElement; element: HTMLElement;
} }