Skip to content

Commit a0ab3d9

Browse files
committed
feat: Panzoom support for preview
This closes #5
1 parent a176960 commit a0ab3d9

10 files changed

+127
-59
lines changed

bun.lockb

1.71 KB
Binary file not shown.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
},
2929
"dependencies": {
3030
"@aklinker1/check": "^1.3.1",
31+
"panzoom": "^9.4.3",
3132
"standard-version": "^9.5.0",
3233
"vue-tsc": "^2.0.10"
3334
}

web/components/CutlistPreview.vue

+40-27
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
11
<script lang="ts" setup>
22
const { data, isLoading, error } = useBoardLayoutsQuery();
3+
4+
const container = ref<HTMLDivElement>();
5+
const { scale, resetZoom, zoomIn, zoomOut } = usePanZoom(container);
36
</script>
47

58
<template>
6-
<div class="flex flex-col dots-bg">
7-
<p v-if="error">{{ error }}</p>
8-
<template v-else-if="data">
9-
<p
10-
v-if="data.layouts.length === 0"
11-
class="m-auto bg-gray-900 shadow-2xl shadow-gray-900 p-4"
12-
>
13-
No board layouts found
14-
</p>
15-
<LayoutList v-else :layouts="data.layouts" />
16-
</template>
9+
<div>
10+
<!-- Cutlist Preview -->
11+
<div class="absolute inset-0 overflow-none">
12+
<p v-if="error" class="m-auto">{{ error }}</p>
13+
14+
<template v-else-if="data">
15+
<p
16+
v-if="data.layouts.length === 0"
17+
class="m-auto bg-gray-900 shadow-2xl shadow-gray-900 p-4"
18+
>
19+
No board layouts found
20+
</p>
21+
<div v-else ref="container">
22+
<div class="flex flex-col">
23+
<LayoutList :layouts="data.layouts" />
24+
</div>
25+
</div>
26+
</template>
1727

18-
<div v-else-if="isLoading" class="m-auto flex items-center space-x-4">
19-
<USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" />
20-
<div class="space-y-2">
21-
<USkeleton class="h-4 w-[250px]" />
22-
<USkeleton class="h-4 w-[200px]" />
28+
<div v-else-if="isLoading" class="m-auto flex items-center space-x-4">
29+
<USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" />
30+
<div class="space-y-2">
31+
<USkeleton class="h-4 w-[250px]" />
32+
<USkeleton class="h-4 w-[200px]" />
33+
</div>
2334
</div>
2435
</div>
36+
37+
<!-- Controlls -->
38+
<div class="absolute bottom-4 right-4 flex gap-4 print:hidden z-10">
39+
<ScaleController
40+
v-if="scale != null"
41+
class="bg-white rounded shadow-2xl"
42+
:scale="scale"
43+
@reset-zoom="resetZoom"
44+
@zoom-in="zoomIn"
45+
@zoom-out="zoomOut"
46+
/>
47+
<FitController class="bg-white rounded shadow-2xl" />
48+
</div>
2549
</div>
2650
</template>
27-
28-
<style scoped>
29-
.dots-bg {
30-
background-size: 40px 40px;
31-
background-image: radial-gradient(
32-
circle,
33-
rgba(255, 255, 255, 20%) 2px,
34-
rgba(255, 255, 255, 0) 1px
35-
);
36-
}
37-
</style>

web/components/PartListItem.vue

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const height = usePx(() => props.placement.lengthM);
1414
const left = usePx(() => props.placement.leftM);
1515
const bottom = usePx(() => props.placement.bottomM);
1616
17-
const getPx = useGetPx();
1817
const fontSize = usePx(() =>
1918
Math.min(
2019
props.placement.widthM / 2,
@@ -44,6 +43,12 @@ const showPartNumbers = useShowPartNumbers();
4443
{{ placement.partNumber }}
4544
</p>
4645
</UPlaceholder>
47-
<PartDetailsTooltip v-if="isHovered" :part="placement" />
46+
<Teleport to="body">
47+
<PartDetailsTooltip
48+
v-if="isHovered"
49+
:part="placement"
50+
class="pointer-events-none"
51+
/>
52+
</Teleport>
4853
</div>
4954
</template>

web/components/ScaleController.vue

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
<script lang="ts" setup>
2-
const scale = useScale();
3-
const { zoomIn, zoomOut, resetZoom } = useScaleControls();
2+
const props = defineProps<{
3+
scale: number;
4+
}>();
45
5-
const percent = computed(() => `${Math.round(scale.value * 100)}%`);
6+
const emit = defineEmits<{
7+
zoomOut: [];
8+
zoomIn: [];
9+
resetZoom: [];
10+
}>();
11+
12+
const percent = computed(() => `${Math.round(props.scale * 100)}%`);
613
</script>
714

815
<template>
@@ -13,23 +20,24 @@ const percent = computed(() => `${Math.round(scale.value * 100)}%`);
1320
size="lg"
1421
color="black"
1522
icon="i-heroicons-minus"
16-
@click="zoomOut"
23+
@click="emit('zoomOut')"
1724
/>
1825
<UButton
1926
:title="`${percent} - Click to reset to 100%`"
2027
class="w-20 justify-center"
2128
size="lg"
2229
color="black"
23-
@click="resetZoom"
30+
@click="emit('resetZoom')"
2431
>
2532
{{ percent }}
2633
</UButton>
2734
<UButton
35+
title="Zoom in"
2836
square
2937
size="lg"
3038
color="black"
3139
icon="i-heroicons-plus"
32-
@click="zoomIn"
40+
@click="emit('zoomIn')"
3341
/>
3442
</div>
3543
</template>

web/composables/useGetPx.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export default function () {
2-
const scale = useScale();
3-
return (value: number) => `${value * 500 * scale.value}px`;
2+
return (value: number) => `${value * 500}px`;
43
}

web/composables/usePanZoom.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { PanZoom } from 'panzoom';
2+
import panzoom from 'panzoom';
3+
4+
export default function (container: Ref<HTMLElement | undefined>) {
5+
let instance = ref<PanZoom>();
6+
const scale = ref<number>();
7+
8+
whenever(container, (container) => {
9+
instance.value = panzoom(container, {
10+
autocenter: true,
11+
minZoom: 0.2,
12+
maxZoom: 10,
13+
});
14+
scale.value = instance.value.getTransform().scale;
15+
instance.value.on('zoom', () => {
16+
scale.value = instance.value?.getTransform().scale;
17+
});
18+
});
19+
whenever(
20+
() => !container.value,
21+
() => {
22+
instance.value?.dispose();
23+
},
24+
);
25+
onUnmounted(() => {
26+
instance.value?.dispose();
27+
});
28+
29+
const setZoom = (cb: (scale: number) => number, x?: number, y?: number) => {
30+
if (instance.value == null) return;
31+
const current = instance.value.getTransform();
32+
const currentScale = current.scale;
33+
const newScale = cb(current.scale);
34+
instance.value?.smoothZoom(
35+
x ?? current.x,
36+
y ?? current.y,
37+
newScale / currentScale,
38+
);
39+
};
40+
const zoomBy = (increment: number) => setZoom((scale) => scale + increment);
41+
42+
return {
43+
scale,
44+
zoomIn: () => zoomBy(0.1),
45+
zoomOut: () => zoomBy(-0.1),
46+
resetZoom: () => {
47+
setZoom(() => 1);
48+
instance.value?.smoothMoveTo(0, 0);
49+
},
50+
};
51+
}

web/composables/useScale.ts

-1
This file was deleted.

web/composables/useScaleControls.ts

-8
This file was deleted.

web/pages/index.vue

+13-13
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@ const isExpanded = useIsExpanded();
77
<ClientOnly>
88
<MainSidebar
99
v-if="!isExpanded"
10-
class="bg-gray-50 dark:bg-gray-800 shrink-0 print:hidden min-w-[28rem]"
10+
class="bg-gray-50 dark:bg-gray-800 shrink-0 print:hidden min-w-[28rem] relative z-10"
1111
/>
1212
</ClientOnly>
1313

1414
<ClientOnly>
15-
<div class="flex-1 relative">
16-
<!-- Cutlist Preview -->
17-
<div class="absolute inset-0 overflow-auto">
18-
<CutlistPreview class="min-h-full min-w-full dots-bg w-max" />
19-
</div>
20-
21-
<!-- Controlls -->
22-
<div class="absolute bottom-4 right-4 flex gap-4 print:hidden">
23-
<ScaleController class="bg-white rounded shadow-2xl" />
24-
<FitController class="bg-white rounded shadow-2xl" />
25-
</div>
26-
</div>
15+
<CutlistPreview class="flex-1 relative z-0 dots-bg" />
2716
</ClientOnly>
2817
</div>
2918
</template>
19+
20+
<style scoped>
21+
.dots-bg {
22+
background-size: 40px 40px;
23+
background-image: radial-gradient(
24+
circle,
25+
rgba(255, 255, 255, 20%) 2px,
26+
rgba(255, 255, 255, 0) 1px
27+
);
28+
}
29+
</style>

0 commit comments

Comments
 (0)