Skip to content

Commit

Permalink
Merge pull request #36 from Arzte/download-backpack
Browse files Browse the repository at this point in the history
Add backpack downloading Inital attempt
  • Loading branch information
ltouroumov authored Sep 24, 2024
2 parents aa2acd0 + 5bf6f8d commit 2104c0c
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 58 deletions.
15 changes: 15 additions & 0 deletions .yarn/patches/dom-to-svg-npm-0.12.2-dfe442df49.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/lib/inline.js b/lib/inline.js
index 10970092d2efa9c5c120b4843b17ebc359fe1360..66ce98d611f0556fa644141296750e48a39c1371 100644
--- a/lib/inline.js
+++ b/lib/inline.js
@@ -18,7 +18,9 @@ export async function inlineResources(element) {
(async () => {
var _a;
if (isSVGImageElement(element)) {
- const blob = await withTimeout(10000, `Timeout fetching ${element.href.baseVal}`, () => fetchResource(element.href.baseVal));
+ const elementHref = element.getAttribute('href') || element.getAttribute('xlink:href');
+ assert(elementHref, '<image> element must have href or xlink:href attribute');
+ const blob = await withTimeout(10000, `Timeout fetching ${elementHref}`, () => fetchResource(elementHref));
if (blob.type === 'image/svg+xml') {
// If the image is an SVG, inline it into the output SVG.
// Some tools (e.g. Figma) do not support nested SVG.
4 changes: 2 additions & 2 deletions components/viewer/ViewMenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
<div class="d-flex gap-1">
<button
class="btn btn-light btn-lg i-solar-magnifer-outline"
@click="toggleSearch(true)"
@click="toggleSearch()"
/>
<button
class="btn btn-light btn-lg i-solar-backpack-outline"
@click="toggleBackpack(true)"
@click="toggleBackpack()"
/>
</div>
</div>
Expand Down
17 changes: 16 additions & 1 deletion components/viewer/ViewProjectObj.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
<img
v-if="obj.image"
class="obj-image"
loading="lazy"
:decoding="alwaysEnable ? `sync` : `auto`"
:loading="alwaysEnable ? `eager` : `lazy`"
:src="obj.image"
:href="objImageIsURL ? obj.image : ''"
:alt="obj.title"
/>
</div>
Expand Down Expand Up @@ -129,6 +131,10 @@ const objTemplateClass = computed(() => {
return 'obj-template-top';
});
const objImageIsURL = computed(() => {
return R.match(/^https?:\/\//, $props.obj.image);
});
const store = useProjectStore();
const { selectedIds, selected } = useProjectRefs();
Expand All @@ -144,6 +150,15 @@ const isEnabled = computed<boolean>(() => {
return condition.value(selectedIds.value);
}
});
const alwaysEnable = computed<boolean>(() => {
switch ($props.viewObject) {
case ViewContext.BackpackEnabled:
case ViewContext.BackpackDisabled:
return true;
default:
return false;
}
});
const canToggle = computed<boolean>(() => {
return (
isEnabled.value &&
Expand Down
16 changes: 9 additions & 7 deletions components/viewer/ViewScoreStatus.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<template>
<div class="d-flex gap-2" :class="{ 'flex-column': vertical }">
<div class="d-flex gap-2" :class="{ 'flex-column': $props.vertical }">
<span
v-for="{ score, value } in activeScores"
:key="score.id"
class="d-flex score flex-row gap-2"
>
<span
v-if="score.beforeText"
:class="short ? ['d-none', 'd-sm-block'] : []"
:class="$props.short ? ['d-none', 'd-sm-block'] : []"
>
{{ score.beforeText }}
</span>
<span>{{ -value }}</span>
<span v-if="score.afterText">{{ score.afterText }}</span>
<span class="score-text">{{ -value }}</span>
<span v-if="score.afterText" class="score-text">{{
score.afterText
}}</span>
</span>
</div>
</template>
Expand All @@ -24,7 +26,7 @@ import { computed } from 'vue';
import type { PointType } from '~/composables/project';
import { useProjectRefs } from '~/composables/store/project';
const { vertical, short = false } = defineProps<{
const $props = defineProps<{
vertical?: boolean;
short?: boolean;
}>();
Expand All @@ -45,7 +47,7 @@ const activeScores = computed<{ score: PointType; value: number }[]>(() => {
});
</script>
<style scoped lang="scss">
.score {
// padding-right: 25px;
.backpackRender .score-text {
font-weight: normal;
}
</style>
218 changes: 171 additions & 47 deletions components/viewer/modal/BackpackModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,75 @@
</template>
<template #default>
<div class="pack-content flex-grow-1 bg-dark">
<div class="pack-info-container">
<div class="pack-scores">
<ViewScoreStatus vertical />
<button
type="button"
class="btn btn-primary mb-3"
@click="backpackToImage"
>
Download backpack as Image
</button>
<div
ref="backpackRef"
class="backpack-container"
:class="{ backpackRender: isLoading }"
>
<div v-if="isLoading" class="project-title">
{{ project?.projectName }}
</div>
<div class="d-flex flex-column pack-selection-controls">
<div class="form-check form-switch">
<input
id="packRowDisabledSwitch"
v-model="lockBackpackObjects"
class="form-check-input"
type="checkbox"
role="switch"
/>
<label class="form-check-label" for="packRowDisabledSwitch">
Lock Objects in the Backpack
</label>
<div class="pack-info-container">
<div class="pack-scores">
<ViewScoreStatus :vertical="!isLoading" />
</div>
<div class="form-check form-switch">
<input
id="hideDisabledAddons"
v-model="hideDisabledAddons"
class="form-check-input"
type="checkbox"
role="switch"
/>
<label class="form-check-label" for="hideDisabledAddons">
Hide Disabled Addons
</label>
<div
v-show="!isLoading"
class="flex-column pack-selection-controls"
>
<div class="form-check form-switch">
<input
id="packRowDisabledSwitch"
v-model="lockBackpackObjects"
class="form-check-input"
type="checkbox"
role="switch"
/>
<label class="form-check-label" for="packRowDisabledSwitch">
Lock Objects in the Backpack
</label>
</div>
<div class="form-check form-switch">
<input
id="hideDisabledAddons"
v-model="hideDisabledAddons"
class="form-check-input"
type="checkbox"
role="switch"
/>
<label class="form-check-label" for="hideDisabledAddons">
Hide Disabled Addons
</label>
</div>
</div>
</div>
</div>
<div
v-for="{ packRow, choices } in packRows"
:key="packRow.id"
class="pack-row"
>
<div class="pack-row-title">
{{ packRow.title }}
</div>
<div class="container-fluid p-0">
<div class="row g-2">
<ViewProjectObj
v-for="{ obj, row } in choices"
:key="obj.id"
:obj="obj"
:row="row"
:width="packRow.objectWidth"
:view-object="objectMode"
:hide-disabled-addons="hideDisabledAddons"
/>
<div
v-for="{ packRow, choices } in packRows"
:key="packRow.id"
class="pack-row"
>
<div class="pack-row-title">
{{ packRow.title }}
</div>
<div class="container-fluid p-0">
<div class="row g-2">
<ViewProjectObj
v-for="{ obj, row } in choices"
:key="obj.id"
:obj="obj"
:row="row"
:width="packRow.objectWidth"
:view-object="objectMode"
:hide-disabled-addons="hideDisabledAddons"
/>
</div>
</div>
</div>
</div>
Expand All @@ -68,8 +87,10 @@
</template>

<script setup lang="ts">
import { elementToSVG, inlineResources } from 'dom-to-svg';
import * as R from 'ramda';
import { computed } from 'vue';
import { useToast } from 'vue-toastification';

import ModalDialog from '~/components/utils/ModalDialog.vue';
import ExportCode from '~/components/viewer/utils/ExportCode.vue';
Expand All @@ -79,7 +100,7 @@ import { useProjectRefs, useProjectStore } from '~/composables/store/project';
import { useViewerRefs, useViewerStore } from '~/composables/store/viewer';
import { ViewContext } from '~/composables/viewer';

const { getObject, getObjectRow, getRow } = useProjectStore();
const { getObject, getObjectRow, getRow, project } = useProjectStore();
const { selected, backpack } = useProjectRefs();
const { toggleBackpack } = useViewerStore();
const { isBackpackVisible } = useViewerRefs();
Expand Down Expand Up @@ -114,6 +135,84 @@ const objectMode = computed(() => {
if (lockBackpackObjects.value) return ViewContext.BackpackDisabled;
else return ViewContext.BackpackEnabled;
});

const backpackRef = ref<HTMLDivElement>();
const isLoading = ref(false);
const backpackToImage = async () => {
if (backpackRef.value && packRows.value.length >= 1) {
isLoading.value = true;
const $toast = useToast();
const toastGenerateImage = $toast.info('Generating image...', {
timeout: false,
});
// Wait for the next tick to ensure DOM is updated before getting the element.
await nextTick();

// Set background color for svg to project background color if it exists
const currentBackground = backpackRef.value.style.backgroundColor;
backpackRef.value.style.backgroundColor =
project?.data.styling.backgroundColor ?? currentBackground;
// Convert backpack to SVG
const svgDocument = elementToSVG(backpackRef.value);
// Inline external resources (fonts, images, etc) as data: URIs
await inlineResources(svgDocument.documentElement);
// Restore background color
backpackRef.value.style.backgroundColor = currentBackground;
// Get SVG string
const svgString = new XMLSerializer().serializeToString(svgDocument);
// Create a Blob from the SVG string
const svg = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
// Create a downloadable link for img src
const svgUrl = URL.createObjectURL(svg);
const img = new Image();
// set the image src to the URL of the Blob
img.src = svgUrl;
// Wait until the image has loaded
await img.decode();
// Create a canvas to draw the image to
const canvas = document.createElement('canvas');
// Set canvas dimensions to match the image
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d')!;
// Draw the image to the canvas
ctx.drawImage(img, 0, 0);
// Get the image data as a PNG string
const url = canvas.toDataURL('image/png');
// Remove the canvas
canvas.remove();

isLoading.value = false;
$toast.dismiss(toastGenerateImage);

// Ensure the URL is valid before trying to download it
if (!url.startsWith('data:image/png')) {
$toast.error('Failed to generate backpack image.');
console.log(url);
} else {
$toast.success('Backpack image generated');
// Create a element to download the image
const element = document.createElement('a');
// Set the download link href and download attribute
element.href = url;
element.download = `backpack-${new Date().toLocaleString()}.png`;

// Click the link to download the image
await nextTick(() => {
element.click();
});
// Remove the element once downloaded
element.remove();
}

// Clean up the URL after download
URL.revokeObjectURL(url);
} else if (packRows.value.length === 0) {
alert(
'No objects selected to create a backpack image, select at least one object to create a image.',
);
}
};
</script>

<style scoped lang="scss">
Expand All @@ -137,6 +236,7 @@ const objectMode = computed(() => {
}

.pack-selection-controls {
display: flex;
position: absolute;
right: 0;
padding-right: 0.5rem;
Expand All @@ -161,6 +261,30 @@ const objectMode = computed(() => {
}
}

.backpackRender {
width: 1280px !important;
}
.backpackRender .pack-info-container {
position: unset;
align-items: center;
justify-content: center;
}
.backpackRender .pack-scores {
align-self: center;
font-size: 1.5rem;
font-weight: bold;
}
.backpackRender .score-text {
font-weight: normal;
}

.project-title {
font-weight: bold;
font-size: 2.5rem;
margin-bottom: 1rem;
text-align: center;
}

.pack-import-export {
display: flex;
flex-direction: row;
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@vueuse/core": "^11.0.3",
"@vueuse/nuxt": "^11.0.3",
"bootstrap": "5.3.*",
"dom-to-svg": "^0.12.2",
"handlebars": "^4.7.8",
"perfect-debounce": "^1.0.0",
"pinia": "^2.2.2",
Expand All @@ -53,5 +54,8 @@
"packageManager": "[email protected]",
"engines": {
"node": "^20.16.3"
},
"resolutions": {
"dom-to-svg@^0.12.2": "patch:dom-to-svg@npm%3A0.12.2#./.yarn/patches/dom-to-svg-npm-0.12.2-dfe442df49.patch"
}
}
Loading

0 comments on commit 2104c0c

Please sign in to comment.