zobrazenie pozicii cisel na mieste kliknutia,

pridane toleracnie 0 pre presny pixel a 1 pre okolie pixela
This commit is contained in:
2026-08-01 19:59:14 +02:00
parent d753697840
commit 47a4c1195f
8 changed files with 219 additions and 51 deletions

View File

@ -1,4 +1,4 @@
import { computed, markRaw, reactive, ref, shallowRef } from 'vue'
import { computed, markRaw, reactive, ref, shallowRef, watch } from 'vue'
import { defineStore } from 'pinia'
import type {
ColorGroup,
@ -7,10 +7,16 @@ import type {
ProjectFile,
ToastMessage,
} from '@/types/mosaic'
import { floodFillRegion } from '@/utils/floodFill'
import { floodFillRegion, getFloodFillReferenceColor } from '@/utils/floodFill'
import { groupRegionsByColor, refreshGroupColors, sortGroupsByLightness } from '@/utils/colorGroups'
import { rgbToLab } from '@/utils/colors'
import { buildOccupancy, createRegionMask } from '@/utils/regionMask'
import { colorDistance, rgbToLab } from '@/utils/colors'
import {
buildOccupancy,
createRegionMask,
maskContains,
subtractRegionMask,
summarizeRegionMask,
} from '@/utils/regionMask'
import { createProjectFile, isProjectFile } from '@/utils/projectExport'
import { clearLastProject, loadLastProject, saveLastProject } from '@/utils/projectStorage'
@ -26,6 +32,7 @@ const DEFAULT_SETTINGS: MosaicSettings = {
connectivity: 4,
exportOverlay: false,
otherGroupsMode: 'dim',
useUserClickPositionForLabels: false,
}
const cloneRegions = (regions: MosaicRegion[]): MosaicRegion[] => regions.map((region) => markRaw({
@ -34,6 +41,7 @@ const cloneRegions = (regions: MosaicRegion[]): MosaicRegion[] => regions.map((r
labColor: { ...region.labColor },
boundingBox: { ...region.boundingBox },
labelPosition: { ...region.labelPosition },
userClickPosition: region.userClickPosition ? { ...region.userClickPosition } : undefined,
// Masks are immutable; sharing their typed arrays keeps the 30-step history compact.
mask: region.mask,
}))
@ -170,21 +178,29 @@ export const useMosaicStore = defineStore('mosaic', () => {
function addRegionAt(x: number, y: number): void {
if (!imageData.value || isAnalyzing.value) return
const occupancy = buildOccupancy(imageWidth.value, imageHeight.value, regions.value.map((region) => region.mask))
if (occupancy[y * imageWidth.value + x]) {
const floodTolerance = settings.floodTolerance
const minimumRegionPixelCount = floodTolerance <= 1 ? 1 : 12
const referenceColor = getFloodFillReferenceColor(imageData.value, x, y, floodTolerance)
const existingRegion = regions.value.find((region) => maskContains(region.mask, x, y))
if (existingRegion && colorDistance(existingRegion.color, referenceColor) <= floodTolerance) {
notify('Táto plocha už bola označená.', 'warning')
return
}
const occupancy = buildOccupancy(
imageWidth.value,
imageHeight.value,
regions.value.filter((region) => region.id !== existingRegion?.id).map((region) => region.mask),
)
isAnalyzing.value = true
window.setTimeout(() => {
try {
const result = floodFillRegion(imageData.value as ImageData, x, y, {
tolerance: settings.floodTolerance,
tolerance: floodTolerance,
darkBoundaryThreshold: settings.darkBoundaryThreshold,
connectivity: settings.connectivity,
occupancy,
})
if (!result || result.pixelCount < 12) {
if (!result || result.pixelCount < minimumRegionPixelCount) {
notify('Nájdená plocha je príliš malá. Skúste iný bod alebo vyššiu toleranciu.', 'warning')
return
}
@ -200,10 +216,27 @@ export const useMosaicStore = defineStore('mosaic', () => {
pixelCount: result.pixelCount,
boundingBox: result.boundingBox,
labelPosition: result.labelPosition,
userClickPosition: { x, y },
groupId: '',
mask: result.mask,
})
regions.value = [...regions.value, region]
const nextRegions = regions.value.flatMap((item) => {
if (item.id !== existingRegion?.id) return [item]
const remainingMask = subtractRegionMask(item.mask, result.mask)
const summary = remainingMask ? summarizeRegionMask(remainingMask) : null
if (!remainingMask || !summary || summary.pixelCount < minimumRegionPixelCount) return []
return [markRaw({
...item,
mask: remainingMask,
pixelCount: summary.pixelCount,
boundingBox: summary.boundingBox,
labelPosition: summary.labelPosition,
userClickPosition: item.userClickPosition && maskContains(remainingMask, item.userClickPosition.x, item.userClickPosition.y)
? item.userClickPosition
: undefined,
})]
})
regions.value = [...nextRegions, region]
regroup(false)
selectedRegionId.value = region.id
selectedGroupId.value = regions.value.find((item) => item.id === region.id)?.groupId ?? null
@ -308,12 +341,15 @@ export const useMosaicStore = defineStore('mosaic', () => {
}
async function applyProject(project: ProjectFile, blob: Blob | null = null): Promise<void> {
Object.assign(settings, project.settings)
Object.assign(settings, DEFAULT_SETTINGS, project.settings, {
useUserClickPositionForLabels: project.settings.useUserClickPositionForLabels ?? false,
})
imageName.value = project.imageName
imageWidth.value = project.dimensions.width
imageHeight.value = project.dimensions.height
regions.value = project.regions.map((region) => markRaw({
...region,
userClickPosition: region.userClickPosition ? { ...region.userClickPosition } : undefined,
mask: createRegionMask(region.mask.width, region.mask.height, region.mask.runs),
}))
groups.value = cloneGroups(project.groups)
@ -373,6 +409,8 @@ export const useMosaicStore = defineStore('mosaic', () => {
await clearLastProject().catch(() => undefined)
}
watch(settings, scheduleAutosave)
return {
imageCanvas, imageData, imageBlob, imageName, imageWidth, imageHeight, regions, groups,
selectedGroupId, selectedRegionId, settings, toasts, isAnalyzing, hasRecovery, recoveryTimestamp,