Skip to content

Commit be8916b

Browse files
authored
feat: Add visual crop preview widget for ImageCrop node - widget ImageCrop (#7825)
## Summary Another implementation for image crop node, alternative for #7014 As discussed with @christian-byrne and @DrJKL we could have single widget - IMAGECROP with 4 ints and UI preview. However, this solution requires changing the definition of image crop node in BE (sent [here](Comfy-Org/ComfyUI#11594)), which will break the exsiting workflow, also it would not allow connect separate int node as input, I am not sure it is a good idea. So I keep two PRs openned for references ## Screenshots https://github.com/user-attachments/assets/fde6938c-4395-48f6-ac05-6282c5eb8157 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7825-feat-Add-visual-crop-preview-widget-for-ImageCrop-node-widget-ImageCrop-2dc6d73d3650812bb8a2cdff4615032b) by [Unito](https://www.unito.io)
1 parent de2e37e commit be8916b

File tree

17 files changed

+932
-63
lines changed

17 files changed

+932
-63
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<template>
2+
<div class="grid grid-cols-[auto_1fr] gap-x-2 gap-y-1">
3+
<label class="content-center text-xs text-node-component-slot-text">
4+
{{ $t('boundingBox.x') }}
5+
</label>
6+
<input
7+
v-model.number="x"
8+
type="number"
9+
:min="0"
10+
step="1"
11+
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
12+
/>
13+
<label class="content-center text-xs text-node-component-slot-text">
14+
{{ $t('boundingBox.y') }}
15+
</label>
16+
<input
17+
v-model.number="y"
18+
type="number"
19+
:min="0"
20+
step="1"
21+
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
22+
/>
23+
<label class="content-center text-xs text-node-component-slot-text">
24+
{{ $t('boundingBox.width') }}
25+
</label>
26+
<input
27+
v-model.number="width"
28+
type="number"
29+
:min="1"
30+
step="1"
31+
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
32+
/>
33+
<label class="content-center text-xs text-node-component-slot-text">
34+
{{ $t('boundingBox.height') }}
35+
</label>
36+
<input
37+
v-model.number="height"
38+
type="number"
39+
:min="1"
40+
step="1"
41+
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
42+
/>
43+
</div>
44+
</template>
45+
46+
<script setup lang="ts">
47+
import { computed } from 'vue'
48+
49+
import type { Bounds } from '@/renderer/core/layout/types'
50+
51+
const modelValue = defineModel<Bounds>({
52+
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
53+
})
54+
55+
const x = computed({
56+
get: () => modelValue.value.x,
57+
set: (x) => {
58+
modelValue.value = { ...modelValue.value, x }
59+
}
60+
})
61+
62+
const y = computed({
63+
get: () => modelValue.value.y,
64+
set: (y) => {
65+
modelValue.value = { ...modelValue.value, y }
66+
}
67+
})
68+
69+
const width = computed({
70+
get: () => modelValue.value.width,
71+
set: (width) => {
72+
modelValue.value = { ...modelValue.value, width }
73+
}
74+
})
75+
76+
const height = computed({
77+
get: () => modelValue.value.height,
78+
set: (height) => {
79+
modelValue.value = { ...modelValue.value, height }
80+
}
81+
})
82+
</script>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<template>
2+
<div
3+
class="widget-expands relative flex h-full w-full flex-col gap-1"
4+
@pointerdown.stop
5+
@pointermove.stop
6+
@pointerup.stop
7+
>
8+
<!-- Image preview container -->
9+
<div
10+
ref="containerEl"
11+
class="relative min-h-0 flex-1 overflow-hidden rounded-[5px] bg-node-component-surface"
12+
>
13+
<div v-if="isLoading" class="flex size-full items-center justify-center">
14+
<span class="text-sm">{{ $t('imageCrop.loading') }}</span>
15+
</div>
16+
17+
<div
18+
v-else-if="!imageUrl"
19+
class="flex size-full flex-col items-center justify-center text-center"
20+
>
21+
<i class="mb-2 icon-[lucide--image] h-12 w-12" />
22+
<p class="text-sm">{{ $t('imageCrop.noInputImage') }}</p>
23+
</div>
24+
25+
<img
26+
v-else
27+
ref="imageEl"
28+
:src="imageUrl"
29+
:alt="$t('imageCrop.cropPreviewAlt')"
30+
draggable="false"
31+
class="block size-full object-contain select-none brightness-50"
32+
@load="handleImageLoad"
33+
@error="handleImageError"
34+
@dragstart.prevent
35+
/>
36+
37+
<div
38+
v-if="imageUrl && !isLoading"
39+
class="absolute box-content cursor-move overflow-hidden border-2 border-white"
40+
:style="cropBoxStyle"
41+
@pointerdown="handleDragStart"
42+
@pointermove="handleDragMove"
43+
@pointerup="handleDragEnd"
44+
>
45+
<div class="pointer-events-none size-full" :style="cropImageStyle" />
46+
</div>
47+
48+
<div
49+
v-for="handle in resizeHandles"
50+
v-show="imageUrl && !isLoading"
51+
:key="handle.direction"
52+
:class="['absolute', handle.class]"
53+
:style="handle.style"
54+
@pointerdown="(e) => handleResizeStart(e, handle.direction)"
55+
@pointermove="handleResizeMove"
56+
@pointerup="handleResizeEnd"
57+
/>
58+
</div>
59+
60+
<WidgetBoundingBox v-model="modelValue" class="shrink-0" />
61+
</div>
62+
</template>
63+
64+
<script setup lang="ts">
65+
import { useTemplateRef } from 'vue'
66+
67+
import WidgetBoundingBox from '@/components/boundingbox/WidgetBoundingBox.vue'
68+
import { useImageCrop } from '@/composables/useImageCrop'
69+
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
70+
import type { Bounds } from '@/renderer/core/layout/types'
71+
72+
const props = defineProps<{
73+
nodeId: NodeId
74+
}>()
75+
76+
const modelValue = defineModel<Bounds>({
77+
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
78+
})
79+
80+
const imageEl = useTemplateRef<HTMLImageElement>('imageEl')
81+
const containerEl = useTemplateRef<HTMLDivElement>('containerEl')
82+
83+
const {
84+
imageUrl,
85+
isLoading,
86+
87+
cropBoxStyle,
88+
cropImageStyle,
89+
resizeHandles,
90+
91+
handleImageLoad,
92+
handleImageError,
93+
handleDragStart,
94+
handleDragMove,
95+
handleDragEnd,
96+
handleResizeStart,
97+
handleResizeMove,
98+
handleResizeEnd
99+
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
100+
</script>

0 commit comments

Comments
 (0)