Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 238 additions & 1 deletion frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
"axios": "^1.7.2",
"echarts": "^5.6.0",
"javascript-color-gradient": "^2.5.0",
"jspdf": "^4.2.0",
"jszip": "^3.10.1",
"lorem-ipsum": "^2.0.8",
"papaparse": "^5.5.3",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.4.1",
"plotly.js-dist": "^3.1.0",
Expand All @@ -39,6 +41,7 @@
"@quasar/quasar-app-extension-qmarkdown": "^2.0.5",
"@types/javascript-color-gradient": "^2.4.2",
"@types/node": "^20.5.9",
"@types/papaparse": "^5.5.2",
"@types/plotly.js": "^3.0.3",
"@types/three": "^0.179.0",
"@vue/eslint-config-prettier": "^10.1.0",
Expand Down
46 changes: 37 additions & 9 deletions frontend/src/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,46 @@

<div class="title-group">
<h1 class="title q-ml-sm">{{ t("title") }}</h1>
<div class="subtitle" v-if="isHome">Regrouping {{ totalWalls.unwrapOrNull() ?? 0 }} walls across {{ numberOfSources.unwrapOrNull() ?? 0 }} sources</div>
<div class="subtitle" v-if="isHome">
A curated dataset of {{ totalWalls.unwrapOrNull() ?? 0 }} irregular stone masonry walls.
</div>
</div>

<q-space />

<div class="side-toolbar">
<q-btn icon="format_quote" class="icons" flat round :title="t('citation')" @click="showCitation = true"></q-btn>
<q-btn icon="mail" class="icons" flat round :title="t('contact')" @click="showContact = true"></q-btn>
<q-btn icon="cloud_upload" class="icons" flat round :title="t('upload')" to="/contribute"></q-btn>
<q-btn icon="handshake" class="icons" flat round :title="t('acknowledgements')"
@click="showAcknowledgements = true"></q-btn>
<q-btn
icon="format_quote"
class="icons"
flat
round
:title="t('citation')"
@click="showCitation = true"
/>
<q-btn
icon="mail"
class="icons"
flat
round
:title="t('contact')"
@click="showContact = true"
/>
<q-btn
icon="cloud_upload"
class="icons"
flat
round
:title="t('upload')"
to="/contribute"
/>
<q-btn
icon="handshake"
class="icons"
flat
round
:title="t('acknowledgements')"
@click="showAcknowledgements = true"
/>
</div>
</div>

Expand All @@ -36,10 +65,10 @@
<simple-dialog v-model="showAcknowledgements" :title="t('acknowledgements')">
<div>
<p>
Thanks to <a href="https://www.epfl.ch/schools/enac/about/data-at-enac/enac-it4research/">ENAC-IT4R</a> for developing the web-based interfaces, visualization features and search capabilities.
Thanks to <a href="https://www.epfl.ch/schools/enac/about/data-at-enac/enac-it4research/">ENAC-IT4R</a> for developing the web-based interfaces, visualization features and search capabilities.
</p>
<p>
This work was financed by <a href="https://www.snf.ch/fr">Swiss National Science Foundation (SNSF)</a> grant as part of the ETH Domain’s ORD program.
This work was financed by <a href="https://www.snf.ch/fr">Swiss National Science Foundation (SNSF)</a> grant as part of the ETH Domain’s ORD program.
</p>
</div>
</simple-dialog>
Expand All @@ -65,7 +94,6 @@ const showAcknowledgements = ref(false);

const propertiesStore = usePropertiesStore()
const totalWalls = useAsyncResultRef(propertiesStore.getColumnValues("Wall ID").chain(values => Result.ok(values.length)));
const numberOfSources = useAsyncResultRef(propertiesStore.getColumnValues("Reference").chain(values => Result.ok(new Set(values).size)));

</script>

Expand Down
3 changes: 0 additions & 3 deletions frontend/src/components/AppToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,10 @@
/>
</a>
</q-toolbar>

<app-header/>
</template>

<script setup lang="ts">
import { useQuasar } from 'quasar';
import AppHeader from 'src/components/AppHeader.vue';

interface Props {
hasDrawer?: boolean;
Expand Down
53 changes: 39 additions & 14 deletions frontend/src/components/LmtCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ const inputLine = defineModel<LineComputeInputLineCoords>('inputLine', {

defineExpose({
redrawCanvas,
imageDataURL,
cleanImageDataURL,
});

watch(() => props.uploadedImage, () => {
initCanvas();
});
watch([() => props.traces, inputLine.value], () => {
redrawCanvas();
void redrawCanvas();
});

const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');
Expand All @@ -45,7 +47,7 @@ function initCanvas() {
}

ctx.value = canvasRef.value?.getContext('2d') || null;
redrawCanvas();
await redrawCanvas();
};
img.src = URL.createObjectURL(props.uploadedImage);
};
Expand Down Expand Up @@ -90,19 +92,26 @@ function onCanvasMouseUp() {
isDrawing.value = false;
};

function redrawCanvas() {
if (!canvasRef.value || !ctx.value || !props.uploadedImage) return;
function redrawCanvas(withLines: boolean = true) {
return new Promise<void>((resolve) => {
if (!canvasRef.value || !ctx.value || !props.uploadedImage) return;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redrawCanvas() returns a Promise but if canvasRef/ctx/uploadedImage is missing, the function returns without calling resolve(), leaving callers (e.g., cleanImageDataURL) awaiting a Promise that never settles. Resolve immediately in the early-exit path (or avoid wrapping in a Promise when prerequisites are missing).

Suggested change
if (!canvasRef.value || !ctx.value || !props.uploadedImage) return;
if (!canvasRef.value || !ctx.value || !props.uploadedImage) {
resolve();
return;
}

Copilot uses AI. Check for mistakes.

const img = new Image();
img.onload = () => {
if (ctx.value) {
ctx.value.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
ctx.value.drawImage(img, 0, 0);
drawLine();
drawResults();
}
};
img.src = URL.createObjectURL(props.uploadedImage);
const img = new Image();
img.onload = () => {
if (ctx.value) {
ctx.value.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
ctx.value.drawImage(img, 0, 0);

if (withLines) {
drawLine();
}
drawResults();
}

resolve();
};
img.src = URL.createObjectURL(props.uploadedImage);
Comment on lines +100 to +113
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.createObjectURL(props.uploadedImage) is called on every redrawCanvas() (and also initCanvas()) without revoking the created URL, which will leak object URLs over time—especially now that cleanImageDataURL() triggers extra redraws. Store the created URL in a local variable and call URL.revokeObjectURL() after img.onload (or img.onerror).

Suggested change
img.onload = () => {
if (ctx.value) {
ctx.value.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
ctx.value.drawImage(img, 0, 0);
if (withLines) {
drawLine();
}
drawResults();
}
resolve();
};
img.src = URL.createObjectURL(props.uploadedImage);
const objectUrl = URL.createObjectURL(props.uploadedImage);
img.onload = () => {
try {
if (ctx.value) {
ctx.value.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
ctx.value.drawImage(img, 0, 0);
if (withLines) {
drawLine();
}
drawResults();
}
} finally {
URL.revokeObjectURL(objectUrl);
resolve();
}
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
resolve();
};
img.src = objectUrl;

Copilot uses AI. Check for mistakes.
});
};

function drawLine() {
Expand Down Expand Up @@ -149,6 +158,22 @@ function drawResults() {
}
};

function imageDataURL() {
if (!canvasRef.value) return null;
const dataUrl = canvasRef.value.toDataURL('image/png');
return dataUrl;
}

async function cleanImageDataURL() {
if (!canvasRef.value) return null;

// Redraw canvas without lines to get a clean image
await redrawCanvas(false);
const dataUrl = canvasRef.value.toDataURL('image/png');
await redrawCanvas(true); // Redraw with lines again
return dataUrl;
}

</script>

<style scoped>
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/components/WelcomeDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<q-dialog v-model="showDataProtectionNotice">
<q-card>
<q-card-section>
<div class="text-h6">Masonry Microstructure Database</div>
</q-card-section>

<q-card-section>
<p>
Welcome to this web-based platform providing open access to realistic 3D microstructures of irregular stone masonry walls.
</p>
<p>
Explore, visualize, and download curated masonry microstructure data and associated geometric descriptors.
</p>
<p>
Welcome aboard!
</p>
</q-card-section>

<q-card-actions>
<q-toggle v-model="dontShowAgain" label="Do not show again" />
</q-card-actions>

<q-card-actions align="right">
<q-btn flat label="Close" color="primary" v-close-popup @click="onClose()" />
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script setup lang="ts">

const settings = useSettingsStore();

const showDataProtectionNotice = ref(!!settings.settings?.intro_shown);
const dontShowAgain = ref(false);

function onClose() {
if (dontShowAgain.value) {
settings.saveSettings({ intro_shown: false })
Comment on lines +35 to +40
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showDataProtectionNotice is initialized from settings.settings?.intro_shown, but the setting name reads like a one-time flag while the current behavior is “show unless the user disables it”. Consider renaming the setting (e.g., intro_enabled/show_intro_dialog) or inverting the logic so intro_shown means “has been shown once” (set to true on first display/close) to avoid confusion and future regressions.

Suggested change
const showDataProtectionNotice = ref(!!settings.settings?.intro_shown);
const dontShowAgain = ref(false);
function onClose() {
if (dontShowAgain.value) {
settings.saveSettings({ intro_shown: false })
const showDataProtectionNotice = ref(!settings.settings?.intro_shown);
const dontShowAgain = ref(false);
function onClose() {
if (dontShowAgain.value) {
settings.saveSettings({ intro_shown: true })

Copilot uses AI. Check for mistakes.
}
}

</script>
4 changes: 4 additions & 0 deletions frontend/src/layouts/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
</left-drawer>

<q-page-container>
<app-header />
<welcome-dialog />
<router-view />
</q-page-container>
</q-layout>
Expand All @@ -17,7 +19,9 @@
<script setup lang="ts">
import { useQuasar } from 'quasar';
import AppToolbar from 'src/components/AppToolbar.vue';
import AppHeader from 'src/components/AppHeader.vue';
import LeftDrawer from 'src/components/LeftDrawer.vue';
import WelcomeDialog from 'src/components/WelcomeDialog.vue';

const $q = useQuasar();
const route = useRoute();
Expand Down
Loading
Loading