Merge branch 'vue-matrix-display' into 'v3'

A matrix display component with copy button that copys as code.

See merge request openflexure/openflexure-microscope-server!436
This commit is contained in:
Julian Stirling 2025-11-10 13:50:39 +00:00
commit c89a2d24c2
3 changed files with 192 additions and 9 deletions

View file

@ -1,6 +1,6 @@
<template>
<div id="CSMSettings" class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
<div class="uk-width-xlarge">
<div class="uk-width-large">
<h3>Camera to Stage Mapping</h3>
<p>
Camera-stage mapping allows the stage to move relative to the camera view. This enables

View file

@ -20,28 +20,43 @@
v-show="showExtraSettings"
type="button"
class="uk-button uk-button-default uk-width-1-1"
:disabled="!csmMatrix"
:class="{ 'uk-disabled': !csmMatrix }"
@click="getCalibrationData()"
>
Download Calibration Data
</button>
<div v-if="csmMatrix != 'undefined'" style="margin: 10px">
<div v-if="csmMatrix" style="margin: 10px">
<details>
<summary>Calibration Details</summary>
<strong>CSM calculated for images with a resolution of {{ csmResolution }}</strong>
<ul>
<li>
Current CSM Matrix: <tt>{{ csmMatrix }}</tt>
<strong>Current CSM Matrix:</strong>
<br />
<matrixDisplay :matrix="csmMatrix" />
</li>
<li>
CSM calculated for images with a resolution of:
<br />
<matrixDisplay :matrix="csmResolution" :bracket-height="1.5" />
</li>
<li>Pixels per motor step: {{ csmRatio }}</li>
<li>Full field of view: {{ csmFOV }} motor steps</li>
<li>
Full field of view in motor steps:
<br />
<matrixDisplay :matrix="csmFOV" :bracket-height="1.5" />
</li>
</ul>
</details>
</div>
<p v-else><strong>No Calibration Available</strong></p>
</div>
</template>
<script>
import ActionButton from "../../../labThingsComponents/actionButton.vue";
import ActionButton from "@/components/labThingsComponents/actionButton.vue";
import matrixDisplay from "@/components/ui/matrixDisplay.vue";
// Export main app
export default {
@ -49,6 +64,7 @@ export default {
components: {
ActionButton,
matrixDisplay,
},
props: {
@ -61,9 +77,10 @@ export default {
data() {
return {
csmMatrix: "undefined",
csmResolution: "undefined",
csmRatio: "undefined",
csmMatrix: undefined,
csmResolution: undefined,
csmRatio: undefined,
csmFOV: undefined,
};
},

View file

@ -0,0 +1,166 @@
<template>
<div class="matrix-container uk-inline">
<div class="matrix-wrapper">
<div class="matrix-bracket" :style="bracketSize">(</div>
<div class="matrix-grid">
<div v-for="(row, i) in always2DMatrix" :key="i" class="matrix-row">
<span v-for="(cell, j) in row" :key="j" class="matrix-cell">
{{ cell }}
</span>
</div>
</div>
<div class="matrix-bracket" :style="bracketSize">)</div>
</div>
<button
class="copy-button"
:class="{ success: copied }"
:title="'Copy as Python list'"
@click="copyMatrix"
>
<span class="material-symbols-outlined">{{ copySymbol }}</span>
</button>
</div>
</template>
<script>
export default {
name: "MatrixDisplay",
props: {
matrix: {
type: Array,
required: true,
},
bracketHeight: {
type: Number,
default: 3,
},
},
data() {
return {
copied: false,
};
},
computed: {
always2DMatrix() {
if (!this.matrix || this.matrix.length === 0) return [];
if (!Array.isArray(this.matrix[0])) {
return [this.matrix];
}
return this.matrix;
},
copySymbol() {
return this.copied ? "check" : "content_copy";
},
bracketSize() {
return { fontSize: `${this.bracketHeight}rem`, lineHeight: "1" };
},
},
methods: {
copyMatrix() {
const pythonFormat = JSON.stringify(this.matrix)
.replaceAll("],", "], ")
.replaceAll(",", ", ");
this.copyText(pythonFormat);
},
/**
* Cannot use modern text copy as we don't have https.
*
* This uses the deprecated execCommand as navigator.clipboard.writeText() is not
* available.
*/
copyText(text) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
this.copied = true;
setTimeout(() => (this.copied = false), 1000);
} catch (err) {
console.error("Copy failed:", err);
} finally {
document.body.removeChild(textarea);
}
},
},
};
</script>
<style scoped lang="less">
@import "../../assets/less/theme.less";
.matrix-container {
display: inline-block;
padding: 8px 12px;
border-radius: 6px;
position: relative;
}
.matrix-wrapper {
display: flex;
align-items: center;
}
.matrix-bracket {
display: flex;
align-items: center;
padding: 0 0.25rem;
}
.matrix-grid {
display: inline-flex;
flex-direction: column;
align-items: center;
}
.matrix-row {
display: flex;
}
.matrix-cell {
padding: 2px 6px;
min-width: 24px;
text-align: center;
}
.copy-button {
position: absolute;
top: -5px;
right: -20px;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
color: @global-muted-color;
transition: color 0.2s ease;
}
.copy-button:hover {
color: @global-color;
}
.hook-inverse() {
.copy-button:hover {
color: @inverse-global-color;
}
}
.copy-button.success {
color: #4caf50;
&:hover {
color: #4caf50;
}
}
</style>