Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions src/components/maskeditor/MaskEditorContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class="maskEditor-dialog-root flex h-full w-full flex-col"
@contextmenu.prevent
@dragstart="handleDragStart"
@keydown.stop
>
<div
id="maskEditorCanvasContainer"
Expand Down
100 changes: 100 additions & 0 deletions src/components/maskeditor/dialog/TopBarHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,72 @@
</svg>
</button>

<div class="h-5 w-px bg-[var(--p-form-field-border-color)]" />

<button
:class="iconButtonClass"
:title="t('maskEditor.rotateLeft')"
@click="onRotateLeft"
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-[var(--input-text)]"
>
<path
d="M7.5,2.5c2.76,0,5,2.24,5,5s-2.24,5-5,5c-2.21,0-4.09-1.44-4.75-3.43-.08-.24.05-.5.29-.58.24-.08.5.05.58.29.54,1.63,2.08,2.81,3.88,2.81,2.26,0,4.09-1.83,4.09-4.09s-1.83-4.09-4.09-4.09c-1.13,0-2.15.46-2.89,1.2l1.39,1.39c.13.13.16.32.08.48-.08.16-.24.26-.42.26h-3.18c-.28,0-.5-.22-.5-.5v-3.18c0-.18.1-.34.26-.42.16-.08.35-.05.48.08l1.05,1.05c.95-.95,2.26-1.54,3.71-1.54Z"
/>
</svg>
</button>

<button
:class="iconButtonClass"
:title="t('maskEditor.rotateRight')"
@click="onRotateRight"
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-[var(--input-text)]"
>
<path
d="M7.5,2.5c-2.76,0-5,2.24-5,5s2.24,5,5,5c2.21,0,4.09-1.44,4.75-3.43.08-.24-.05-.5-.29-.58-.24-.08-.5.05-.58.29-.54,1.63-2.08,2.81-3.88,2.81-2.26,0-4.09-1.83-4.09-4.09s1.83-4.09,4.09-4.09c1.13,0,2.15.46,2.89,1.2l-1.39,1.39c-.13.13-.16.32-.08.48.08.16.24.26.42.26h3.18c.28,0,.5-.22.5-.5v-3.18c0-.18-.1-.34-.26-.42-.16-.08-.35-.05-.48.08l-1.05,1.05c-.95-.95-2.26-1.54-3.71-1.54Z"
/>
</svg>
</button>

<button
:class="iconButtonClass"
:title="t('maskEditor.mirrorHorizontal')"
@click="onMirrorHorizontal"
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-[var(--input-text)]"
>
<path
d="M7.5,1.5c-.28,0-.5.22-.5.5v11c0,.28.22.5.5.5s.5-.22.5-.5v-11c0-.28-.22-.5-.5-.5Z"
/>
<path d="M3.5,4.5l-2,3,2,3v-6ZM11.5,4.5v6l2-3-2-3Z" />
</svg>
</button>

<button
:class="iconButtonClass"
:title="t('maskEditor.mirrorVertical')"
@click="onMirrorVertical"
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-[var(--input-text)]"
>
<path
d="M2,7.5c0-.28.22-.5.5-.5h11c.28,0,.5.22.5.5s-.22.5-.5.5h-11c-.28,0-.5-.22-.5-.5Z"
/>
<path d="M4.5,4.5l3-2,3,2h-6ZM4.5,10.5h6l-3,2-3-2Z" />
</svg>
</button>

<div class="h-5 w-px bg-[var(--p-form-field-border-color)]" />

<button :class="textButtonClass" @click="onInvert">
{{ t('maskEditor.invert') }}
</button>
Expand Down Expand Up @@ -63,6 +129,7 @@ import { ref } from 'vue'

import Button from '@/components/ui/button/Button.vue'
import { useCanvasTools } from '@/composables/maskeditor/useCanvasTools'
import { useCanvasTransform } from '@/composables/maskeditor/useCanvasTransform'
import { useMaskEditorSaver } from '@/composables/maskeditor/useMaskEditorSaver'
import { t } from '@/i18n'
import { useDialogStore } from '@/stores/dialogStore'
Expand All @@ -71,6 +138,7 @@ import { useMaskEditorStore } from '@/stores/maskEditorStore'
const store = useMaskEditorStore()
const dialogStore = useDialogStore()
const canvasTools = useCanvasTools()
const canvasTransform = useCanvasTransform()
const saver = useMaskEditorSaver()

const saveButtonText = ref(t('g.save'))
Expand All @@ -90,6 +158,38 @@ const onRedo = () => {
store.canvasHistory.redo()
}

const onRotateLeft = async () => {
try {
await canvasTransform.rotateAllLayers(false)
} catch (error) {
console.error('[TopBarHeader] Rotate left failed:', error)
}
}

const onRotateRight = async () => {
try {
await canvasTransform.rotateAllLayers(true)
} catch (error) {
console.error('[TopBarHeader] Rotate right failed:', error)
}
}

const onMirrorHorizontal = async () => {
try {
await canvasTransform.mirrorAllLayers(true)
} catch (error) {
console.error('[TopBarHeader] Mirror horizontal failed:', error)
}
}

const onMirrorVertical = async () => {
try {
await canvasTransform.mirrorAllLayers(false)
} catch (error) {
console.error('[TopBarHeader] Mirror vertical failed:', error)
}
}

const onInvert = () => {
canvasTools.invertMask()
}
Expand Down
123 changes: 123 additions & 0 deletions src/composables/maskeditor/useBrushDrawing.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="@webgpu/types" />
import { ref, watch, nextTick, onUnmounted } from 'vue'
import QuickLRU from '@alloc/quick-lru'
import { debounce } from 'es-toolkit/compat'
Expand Down Expand Up @@ -233,6 +234,128 @@ export function useBrushDrawing(initialSettings?: {
}
)

const isRecreatingTextures = ref(false)

watch(
() => store.gpuTexturesNeedRecreation,
async (needsRecreation) => {
if (
!needsRecreation ||
!device ||
!store.maskCanvas ||
isRecreatingTextures.value
)
return

isRecreatingTextures.value = true

const width = store.gpuTextureWidth
const height = store.gpuTextureHeight

try {
// Destroy old textures
if (maskTexture) {
maskTexture.destroy()
maskTexture = null
}
if (rgbTexture) {
rgbTexture.destroy()
rgbTexture = null
}

// Create new textures with updated dimensions
maskTexture = device.createTexture({
size: [width, height],
format: 'rgba8unorm',
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.COPY_SRC
})

rgbTexture = device.createTexture({
size: [width, height],
format: 'rgba8unorm',
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.COPY_SRC
})

// Upload pending data if available
if (store.pendingGPUMaskData && store.pendingGPURgbData) {
device.queue.writeTexture(
{ texture: maskTexture },
store.pendingGPUMaskData,
{ bytesPerRow: width * 4 },
{ width, height }
)

device.queue.writeTexture(
{ texture: rgbTexture },
store.pendingGPURgbData,
{ bytesPerRow: width * 4 },
{ width, height }
)
} else {
// Fallback: read from canvas
await updateGPUFromCanvas()
}

// Update preview canvas if it exists
if (previewCanvas && renderer) {
previewCanvas.width = width
previewCanvas.height = height
}

// Recreate readback buffers with new size
const bufferSize = width * height * 4
if (currentBufferSize !== bufferSize) {
readbackStorageMask?.destroy()
readbackStorageRgb?.destroy()
readbackStagingMask?.destroy()
readbackStagingRgb?.destroy()

readbackStorageMask = device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
})
readbackStorageRgb = device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
})
readbackStagingMask = device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
})
readbackStagingRgb = device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
})

currentBufferSize = bufferSize
}
} catch (error) {
console.error(
'[useBrushDrawing] Failed to recreate GPU textures:',
error
)
} finally {
// Clear the recreation flag and pending data
store.gpuTexturesNeedRecreation = false
store.gpuTextureWidth = 0
store.gpuTextureHeight = 0
store.pendingGPUMaskData = null
store.pendingGPURgbData = null
isRecreatingTextures.value = false
}
}
)

// Cleanup GPU resources on unmount
onUnmounted(() => {
if (renderer) {
Expand Down
Loading
Loading