diff --git a/src/components/gameMaster/mapEditor/partials/ListPanel.vue b/src/components/gameMaster/mapEditor/partials/ListPanel.vue deleted file mode 100644 index 9b2a99a..0000000 --- a/src/components/gameMaster/mapEditor/partials/ListPanel.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> - <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="isOpen"> - <div class="flex flex-col gap-2.5 p-2.5"> - <div class="relative flex"> - <img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" /> - <label class="mb-1.5 font-titles hidden" for="search">Search</label> - <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" /> - </div> - <div class="flex"> - <select class="input-field w-full" name="lists" v-model="lists"> - <option value="tile">Tiles</option> - <option value="map_object">Objects</option> - </select> - </div> - </div> - <div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5"> - <TileList v-if="mapEditor.drawMode.value === 'tile'" /> - <ObjectList v-if="mapEditor.drawMode.value === 'map_object'" /> - </div> - <div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500"> - <span>Tags:</span> - <div class="flex grow items-center flex-wrap gap-1.5 overflow-auto"> - <span class="m-auto">No tags selected</span> - </div> - </div> - </div> -</template> - -<script setup lang="ts"> -import type { Tile } from '@/application/types' -import ObjectList from '@/components/gameMaster/mapEditor/partials/lists/MapObjectList.vue' -import TileList from '@/components/gameMaster/mapEditor/partials/lists/TileList.vue' -import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { TileStorage } from '@/storage/storages' -import { liveQuery } from 'dexie' -import { onMounted, onUnmounted, ref, watch } from 'vue' - -const mapEditor = useMapEditorComposable() - -const isOpen = ref(false) -const tileStorage = new TileStorage() -const searchQuery = ref('') -const tiles = ref<Tile[]>([]) -const lists = ref<string>(mapEditor.drawMode.value) - -defineExpose({ - open: () => (isOpen.value = true), - close: () => (isOpen.value = false), - toggle: () => (isOpen.value = !isOpen.value) -}) - -watch(lists, (list) => { - mapEditor.setDrawMode(list) -}) - -watch(mapEditor.drawMode, (list) => { - lists.value = list -}) - -let subscription: any = null - -onMounted(() => { - subscription = liveQuery(() => tileStorage.liveQuery()).subscribe({ - next: (result) => { - tiles.value = result - }, - error: (error) => { - console.error('Failed to fetch tiles:', error) - } - }) -}) - -onUnmounted(() => { - if (subscription) { - subscription.unsubscribe() - } -}) -</script> diff --git a/src/components/gameMaster/mapEditor/partials/MapObjectList.vue b/src/components/gameMaster/mapEditor/partials/MapObjectList.vue new file mode 100644 index 0000000..619ca5c --- /dev/null +++ b/src/components/gameMaster/mapEditor/partials/MapObjectList.vue @@ -0,0 +1,107 @@ +<template> + <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800" v-if="isOpen"> + <div class="flex flex-col gap-2.5 p-2.5"> + <div class="relative flex"> + <img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" /> + <label class="mb-1.5 font-titles hidden" for="search">Search</label> + <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" /> + </div> + <div class="flex"> + <select class="input-field w-full" name="lists"> + <option value="tile">Tiles</option> + <option value="map_object">Objects</option> + </select> + </div> + </div> + <div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5"> + <div class="h-full overflow-auto"> + <div class="flex justify-between flex-wrap gap-2.5 items-center"> + <div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block"> + <img + class="border-2 border-solid rounded max-w-full" + :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" + alt="Object" + @click="mapEditor.setSelectedMapObject(mapObject)" + :class="{ + 'cursor-pointer transition-all duration-300': true, + 'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id, + 'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id + }" + /> + </div> + </div> + </div> + </div> + <div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500"> + <span>Tags:</span> + <div class="flex grow items-center flex-wrap gap-1.5 overflow-auto"> + <span class="m-auto">No tags selected</span> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import config from '@/application/config' +import type { MapObject } from '@/application/types' +import Modal from '@/components/utilities/Modal.vue' +import { useMapEditorComposable } from '@/composables/useMapEditorComposable' +import { MapObjectStorage } from '@/storage/storages' +import { liveQuery } from 'dexie' +import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue' + +const isOpen = ref(false) +const mapObjectStorage = new MapObjectStorage() +const isModalOpen = ref(false) +const mapEditor = useMapEditorComposable() +const searchQuery = ref('') +const selectedTags = ref<string[]>([]) +const mapObjectList = ref<MapObject[]>([]) + +defineExpose({ + open: () => (isOpen.value = true), + close: () => (isOpen.value = false), + toggle: () => (isOpen.value = !isOpen.value) +}) + +const uniqueTags = computed(() => { + const allTags = mapObjectList.value.flatMap((obj) => obj.tags || []) + return Array.from(new Set(allTags)) +}) + +const filteredMapObjects = computed(() => { + return mapObjectList.value.filter((object) => { + const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase()) + const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag))) + return matchesSearch && matchesTags + }) +}) + +const toggleTag = (tag: string) => { + if (selectedTags.value.includes(tag)) { + selectedTags.value = selectedTags.value.filter((t) => t !== tag) + } else { + selectedTags.value.push(tag) + } +} + +let subscription: any = null + +onMounted(() => { + isModalOpen.value = true + subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({ + next: (result) => { + mapObjectList.value = result + }, + error: (error) => { + console.error('Failed to fetch tiles:', error) + } + }) +}) + +onUnmounted(() => { + if (subscription) { + subscription.unsubscribe() + } +}) +</script> diff --git a/src/components/gameMaster/mapEditor/partials/TileList.vue b/src/components/gameMaster/mapEditor/partials/TileList.vue new file mode 100644 index 0000000..322a318 --- /dev/null +++ b/src/components/gameMaster/mapEditor/partials/TileList.vue @@ -0,0 +1,173 @@ +<template> + <div class="absolute border-0 border-l-2 border-solid border-gray-500 w-1/4 min-w-80 flex flex-col top-0 right-0 z-10 h-dvh bg-gray-800"> + <div class="flex flex-col gap-2.5 p-2.5"> + <div class="relative flex"> + <img src="/assets/icons/mapEditor/search.svg" class="w-4 h-4 py-0.5 absolute top-1/2 -translate-y-1/2 left-2.5" alt="Search icon" /> + <label class="mb-1.5 font-titles hidden" for="search">Search</label> + <input @mousedown.stop class="!pl-7 input-field w-full" type="text" name="search" placeholder="Search" v-model="searchQuery" /> + </div> + <div class="flex"> + <select class="input-field w-full" name="lists"> + <option value="tile">Tiles</option> + <option value="map_object">Objects</option> + </select> + </div> + </div> + <div class="h-full overflow-auto relative border-0 border-t border-solid border-gray-500 p-2.5"> + <div class="h-full" v-if="!selectedGroup"> + <div class="grid grid-cols-4 gap-2 justify-items-center"> + <div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> + <img + class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300" + :src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`" + :alt="group.parent.name" + @click="openGroup(group)" + @load="() => tileProcessor.processTile(group.parent)" + :class="{ + 'border-cyan shadow-lg': isActiveTile(group.parent), + 'border-transparent hover:border-gray-300': !isActiveTile(group.parent) + }" + /> + <span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span> + <span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs"> + {{ group.children.length + 1 }} + </span> + </div> + </div> + </div> + <div v-else class="h-full overflow-auto"> + <div class="p-4"> + <button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button> + <h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4> + <div class="grid grid-cols-4 gap-2 justify-items-center"> + <div class="flex flex-col items-center justify-center"> + <img + class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300" + :src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`" + :alt="selectedGroup.parent.name" + @click="selectTile(selectedGroup.parent.id)" + :class="{ + 'border-cyan shadow-lg': isActiveTile(selectedGroup.parent), + 'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent) + }" + /> + <span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span> + </div> + <div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center"> + <img + class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300" + :src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`" + :alt="childTile.name" + @click="selectTile(childTile.id)" + :class="{ + 'border-cyan shadow-lg': isActiveTile(childTile), + 'border-transparent hover:border-gray-300': !isActiveTile(childTile) + }" + /> + <span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span> + </div> + </div> + </div> + </div> + </div> + <div class="flex flex-col h-40 gap-2.5 p-3.5 border-t border-0 border-solid border-gray-500"> + <span>Tags:</span> + <div class="flex grow items-center flex-wrap gap-1.5 overflow-auto"> + <span class="m-auto">No tags selected</span> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import config from '@/application/config' +import type { Tile } from '@/application/types' +import { useMapEditorComposable } from '@/composables/useMapEditorComposable' +import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable' +import { TileStorage } from '@/storage/storages' +import { computed, onMounted, onUnmounted, ref } from 'vue' + +const tileStorage = new TileStorage() +const mapEditor = useMapEditorComposable() +const tileProcessor = useTileProcessingComposable() +const searchQuery = ref('') +const selectedTags = ref<string[]>([]) +const tileCategories = ref<Map<string, string>>(new Map()) +const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null) +const tiles = ref<Tile[]>([]) + +defineExpose({ + open: () => (isOpen.value = true), + close: () => (isOpen.value = false), + toggle: () => (isOpen.value = !isOpen.value) +}) + +const uniqueTags = computed(() => { + const allTags = tiles.value.flatMap((tile) => tile.tags || []) + return Array.from(new Set(allTags)) +}) + +const groupedTiles = computed(() => { + const groups: { parent: Tile; children: Tile[] }[] = [] + const filteredTiles = tiles.value.filter((tile) => { + const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase()) + const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag))) + return matchesSearch && matchesTags + }) + + filteredTiles.forEach((tile) => { + const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile)) + if (parentGroup && parentGroup.parent.id !== tile.id) { + parentGroup.children.push(tile) + } else { + groups.push({ parent: tile, children: [] }) + } + }) + + return groups +}) + +const toggleTag = (tag: string) => { + if (selectedTags.value.includes(tag)) { + selectedTags.value = selectedTags.value.filter((t) => t !== tag) + } else { + selectedTags.value.push(tag) + } +} + +function getTileCategory(tile: Tile): string { + return tileCategories.value.get(tile.id) || '' +} + +function openGroup(group: { parent: Tile; children: Tile[] }) { + selectedGroup.value = group +} + +function closeGroup() { + selectedGroup.value = null +} + +function selectTile(tile: string) { + mapEditor.setSelectedTile(tile) +} + +function isActiveTile(tile: Tile): boolean { + return mapEditor.selectedTile.value === tile.id +} + +onMounted(async () => { + tiles.value = await tileStorage.getAll() + const initialBatchSize = 20 + const initialTiles = tiles.value.slice(0, initialBatchSize) + initialTiles.forEach((tile) => tileProcessor.processTile(tile)) + + // Process remaining tiles in background + setTimeout(() => { + tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile)) + }, 1000) +}) + +onUnmounted(() => { + tileProcessor.cleanup() +}) +</script> diff --git a/src/components/gameMaster/mapEditor/partials/Toolbar.vue b/src/components/gameMaster/mapEditor/partials/Toolbar.vue index 35587ef..e1661b2 100644 --- a/src/components/gameMaster/mapEditor/partials/Toolbar.vue +++ b/src/components/gameMaster/mapEditor/partials/Toolbar.vue @@ -1,5 +1,5 @@ <template> - <div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open' : listOpen }"> + <div class="flex justify-between p-5 w-[calc(100%_-_40px)] fixed bottom-0 left-0 z-20" :class="{ 'list-open': listOpen }"> <div class="toolbar rounded flex bg-gray solid border-solid border-2 border-gray-500 text-gray-300 p-1.5 px-3 h-10"> <div ref="toolbar" class="tools flex gap-2.5" v-if="mapEditor.currentMap.value"> <button class="flex justify-center items-center min-w-10 p-0 relative" :class="{ 'border-0 border-b-[3px] border-solid border-cyan gap-2.5': mapEditor.tool.value === 'move' }" @click="handleClick('move')"> @@ -104,7 +104,7 @@ import { onBeforeUnmount, onMounted, ref } from 'vue' const mapEditor = useMapEditorComposable() -const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor', 'open-lists', 'close-lists']) +const emit = defineEmits(['save', 'clear', 'open-maps', 'open-settings', 'close-editor']) // States const toolbar = ref(null) @@ -116,15 +116,6 @@ const listOpen = ref(false) // drawMode function setDrawMode(value: string) { - if (mapEditor.tool.value === 'paint' || mapEditor.tool.value === 'pencil' || mapEditor.tool.value === 'eraser') { - listOpen.value = false - emit('close-lists') - if (value === 'tile' || value === 'map_object') { - listOpen.value = true - emit('open-lists') - } - } - mapEditor.setDrawMode(value) selectPencilOpen.value = false selectEraserOpen.value = false @@ -154,15 +145,10 @@ function handleClick(tool: string) { if (tool === 'mapEditorSettings') { isMapEditorSettingsModalOpen.value = true listOpen.value = false - emit('close-lists') - } - if (tool === 'settings') { + } else if (tool === 'settings') { listOpen.value = false - emit('open-settings') - emit('close-lists') } else if (tool === 'move') { listOpen.value = false - emit('close-lists') mapEditor.setTool(tool) } else { mapEditor.setTool(tool) diff --git a/src/components/gameMaster/mapEditor/partials/lists/MapObjectList.vue b/src/components/gameMaster/mapEditor/partials/lists/MapObjectList.vue deleted file mode 100644 index 0b785aa..0000000 --- a/src/components/gameMaster/mapEditor/partials/lists/MapObjectList.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> - <div class="h-full overflow-auto"> - <div class="flex justify-between flex-wrap gap-2.5 items-center"> - <div v-for="(mapObject, index) in filteredMapObjects" :key="index" class="max-w-1/4 inline-block"> - <img - class="border-2 border-solid rounded max-w-full" - :src="`${config.server_endpoint}/textures/map_objects/${mapObject.id}.png`" - alt="Object" - @click="mapEditor.setSelectedMapObject(mapObject)" - :class="{ - 'cursor-pointer transition-all duration-300': true, - 'border-cyan shadow-lg': mapEditor.selectedMapObject.value?.id === mapObject.id, - 'border-transparent hover:border-gray-300': mapEditor.selectedMapObject.value?.id !== mapObject.id - }" - /> - </div> - </div> - </div> -</template> - -<script setup lang="ts"> -import config from '@/application/config' -import type { MapObject } from '@/application/types' -import Modal from '@/components/utilities/Modal.vue' -import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { MapObjectStorage } from '@/storage/storages' -import { liveQuery } from 'dexie' -import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue' - -const isOpen = ref(false) -const mapObjectStorage = new MapObjectStorage() -const isModalOpen = ref(false) -const mapEditor = useMapEditorComposable() -const searchQuery = ref('') -const selectedTags = ref<string[]>([]) -const mapObjectList = ref<MapObject[]>([]) - -defineExpose({ - open: () => (isOpen.value = true), - close: () => (isOpen.value = false), - toggle: () => (isOpen.value = !isOpen.value) -}) - -const uniqueTags = computed(() => { - const allTags = mapObjectList.value.flatMap((obj) => obj.tags || []) - return Array.from(new Set(allTags)) -}) - -const filteredMapObjects = computed(() => { - return mapObjectList.value.filter((object) => { - const matchesSearch = !searchQuery.value || object.name.toLowerCase().includes(searchQuery.value.toLowerCase()) - const matchesTags = selectedTags.value.length === 0 || (object.tags && selectedTags.value.some((tag) => object.tags.includes(tag))) - return matchesSearch && matchesTags - }) -}) - -const toggleTag = (tag: string) => { - if (selectedTags.value.includes(tag)) { - selectedTags.value = selectedTags.value.filter((t) => t !== tag) - } else { - selectedTags.value.push(tag) - } -} - -let subscription: any = null - -onMounted(() => { - isModalOpen.value = true - subscription = liveQuery(() => mapObjectStorage.liveQuery()).subscribe({ - next: (result) => { - mapObjectList.value = result - }, - error: (error) => { - console.error('Failed to fetch tiles:', error) - } - }) -}) - -onUnmounted(() => { - if (subscription) { - subscription.unsubscribe() - } -}) -</script> diff --git a/src/components/gameMaster/mapEditor/partials/lists/TileList.vue b/src/components/gameMaster/mapEditor/partials/lists/TileList.vue deleted file mode 100644 index f7f24b1..0000000 --- a/src/components/gameMaster/mapEditor/partials/lists/TileList.vue +++ /dev/null @@ -1,151 +0,0 @@ -<template> - <div class="h-full" v-if="!selectedGroup"> - <div class="grid grid-cols-4 gap-2 justify-items-center"> - <div v-for="group in groupedTiles" :key="group.parent.id" class="flex flex-col items-center justify-center relative"> - <img - class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300" - :src="`${config.server_endpoint}/textures/tiles/${group.parent.id}.png`" - :alt="group.parent.name" - @click="openGroup(group)" - @load="() => tileProcessor.processTile(group.parent)" - :class="{ - 'border-cyan shadow-lg': isActiveTile(group.parent), - 'border-transparent hover:border-gray-300': !isActiveTile(group.parent) - }" - /> - <span class="text-xs mt-1">{{ getTileCategory(group.parent) }}</span> - <span v-if="group.children.length > 0" class="absolute top-0 right-0 bg-cyan text-white rounded-full w-5 h-5 flex items-center justify-center text-xs"> - {{ group.children.length + 1 }} - </span> - </div> - </div> - </div> - <div v-else class="h-full overflow-auto"> - <div class="p-4"> - <button @click="closeGroup" class="btn-cyan mb-4">Back to All Tiles</button> - <h4 class="text-lg mb-4">{{ selectedGroup.parent.name }} Group</h4> - <div class="grid grid-cols-4 gap-2 justify-items-center"> - <div class="flex flex-col items-center justify-center"> - <img - class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300" - :src="`${config.server_endpoint}/textures/tiles/${selectedGroup.parent.id}.png`" - :alt="selectedGroup.parent.name" - @click="selectTile(selectedGroup.parent.id)" - :class="{ - 'border-cyan shadow-lg': isActiveTile(selectedGroup.parent), - 'border-transparent hover:border-gray-300': !isActiveTile(selectedGroup.parent) - }" - /> - <span class="text-xs mt-1">{{ getTileCategory(selectedGroup.parent) }}</span> - </div> - <div v-for="childTile in selectedGroup.children" :key="childTile.id" class="flex flex-col items-center justify-center"> - <img - class="max-w-full max-h-full border-2 border-solid rounded cursor-pointer transition-all duration-300" - :src="`${config.server_endpoint}/textures/tiles/${childTile.id}.png`" - :alt="childTile.name" - @click="selectTile(childTile.id)" - :class="{ - 'border-cyan shadow-lg': isActiveTile(childTile), - 'border-transparent hover:border-gray-300': !isActiveTile(childTile) - }" - /> - <span class="text-xs mt-1">{{ getTileCategory(childTile) }}</span> - </div> - </div> - </div> - </div> -</template> - -<script setup lang="ts"> -import config from '@/application/config' -import type { Tile } from '@/application/types' -import { useMapEditorComposable } from '@/composables/useMapEditorComposable' -import { useTileProcessingComposable } from '@/composables/useTileProcessingComposable' -import { TileStorage } from '@/storage/storages' -import { computed, onMounted, onUnmounted, ref } from 'vue' - -const isOpen = ref(false) -const tileStorage = new TileStorage() -const mapEditor = useMapEditorComposable() -const tileProcessor = useTileProcessingComposable() -const searchQuery = ref('') -const selectedTags = ref<string[]>([]) -const tileCategories = ref<Map<string, string>>(new Map()) -const selectedGroup = ref<{ parent: Tile; children: Tile[] } | null>(null) -const tiles = ref<Tile[]>([]) - -defineExpose({ - open: () => (isOpen.value = true), - close: () => (isOpen.value = false), - toggle: () => (isOpen.value = !isOpen.value) -}) - -const uniqueTags = computed(() => { - const allTags = tiles.value.flatMap((tile) => tile.tags || []) - return Array.from(new Set(allTags)) -}) - -const groupedTiles = computed(() => { - const groups: { parent: Tile; children: Tile[] }[] = [] - const filteredTiles = tiles.value.filter((tile) => { - const matchesSearch = !searchQuery.value || tile.name.toLowerCase().includes(searchQuery.value.toLowerCase()) - const matchesTags = selectedTags.value.length === 0 || (tile.tags && selectedTags.value.some((tag) => tile.tags.includes(tag))) - return matchesSearch && matchesTags - }) - - filteredTiles.forEach((tile) => { - const parentGroup = groups.find((group) => tileProcessor.areTilesRelated(group.parent, tile)) - if (parentGroup && parentGroup.parent.id !== tile.id) { - parentGroup.children.push(tile) - } else { - groups.push({ parent: tile, children: [] }) - } - }) - - return groups -}) - -const toggleTag = (tag: string) => { - if (selectedTags.value.includes(tag)) { - selectedTags.value = selectedTags.value.filter((t) => t !== tag) - } else { - selectedTags.value.push(tag) - } -} - -function getTileCategory(tile: Tile): string { - return tileCategories.value.get(tile.id) || '' -} - -function openGroup(group: { parent: Tile; children: Tile[] }) { - selectedGroup.value = group -} - -function closeGroup() { - selectedGroup.value = null -} - -function selectTile(tile: string) { - mapEditor.setSelectedTile(tile) -} - -function isActiveTile(tile: Tile): boolean { - return mapEditor.selectedTile.value === tile.id -} - -onMounted(async () => { - tiles.value = await tileStorage.getAll() - const initialBatchSize = 20 - const initialTiles = tiles.value.slice(0, initialBatchSize) - initialTiles.forEach((tile) => tileProcessor.processTile(tile)) - - // Process remaining tiles in background - setTimeout(() => { - tiles.value.slice(initialBatchSize).forEach((tile) => tileProcessor.processTile(tile)) - }, 1000) -}) - -onUnmounted(() => { - tileProcessor.cleanup() -}) -</script> diff --git a/src/components/screens/MapEditor.vue b/src/components/screens/MapEditor.vue index c72202f..c559fee 100644 --- a/src/components/screens/MapEditor.vue +++ b/src/components/screens/MapEditor.vue @@ -5,9 +5,10 @@ <div v-if="!isLoaded" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-3xl font-ui">Loading...</div> <div v-else> <Map v-if="mapEditor.currentMap.value" :key="mapEditor.currentMap.value?.id" /> - <Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @close-editor="mapEditor.toggleActive" @close-lists="list?.close" @open-lists="list?.open" /> + <Toolbar ref="toolbar" @save="save" @clear="clear" @open-maps="mapModal?.open" @open-settings="mapSettingsModal?.open" @close-editor="mapEditor.toggleActive" /> <MapList ref="mapModal" @open-create-map="mapSettingsModal?.open" /> - <ListPanel ref="list" /> + <TileList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'tile'" /> + <MapObjectList v-if="mapEditor.tool.value === 'pencil' && mapEditor.drawMode.value === 'map_object'" /> <MapSettings ref="mapSettingsModal" /> <TeleportModal ref="teleportModal" /> </div> @@ -21,10 +22,11 @@ import config from '@/application/config' import 'phaser' import type { Map as MapT } from '@/application/types' import Map from '@/components/gameMaster/mapEditor/Map.vue' -import ListPanel from '@/components/gameMaster/mapEditor/partials/ListPanel.vue' import MapList from '@/components/gameMaster/mapEditor/partials/MapList.vue' +import MapObjectList from '@/components/gameMaster/mapEditor/partials/MapObjectList.vue' import MapSettings from '@/components/gameMaster/mapEditor/partials/MapSettings.vue' import TeleportModal from '@/components/gameMaster/mapEditor/partials/TeleportModal.vue' +import TileList from '@/components/gameMaster/mapEditor/partials/TileList.vue' import Toolbar from '@/components/gameMaster/mapEditor/partials/Toolbar.vue' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' import { loadAllTileTextures } from '@/services/mapService' @@ -38,7 +40,6 @@ const mapEditor = useMapEditorComposable() const gameStore = useGameStore() const mapModal = useTemplateRef('mapModal') -const list = useTemplateRef('list') const mapSettingsModal = useTemplateRef('mapSettingsModal') const isLoaded = ref(false) @@ -93,7 +94,7 @@ function save() { pvp: currentMap.pvp, mapEffects: currentMap.mapEffects, mapEventTiles: currentMap.mapEventTiles, - placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, depth, isRotated, positionX, positionY }) => ({ id, mapObject, depth, isRotated, positionX, positionY })) ?? [] + placedMapObjects: currentMap.placedMapObjects.map(({ id, mapObject, isRotated, positionX, positionY }) => ({ id, mapObject, isRotated, positionX, positionY })) ?? [] } gameStore.connection?.emit('gm:map:update', data, (response: MapT) => { diff --git a/src/composables/useTileProcessingComposable.ts b/src/composables/useTileProcessingComposable.ts index 3b3d0b0..d67dd5f 100644 --- a/src/composables/useTileProcessingComposable.ts +++ b/src/composables/useTileProcessingComposable.ts @@ -4,26 +4,32 @@ import type { TileAnalysisResult, TileWorkerMessage } from '@/types/tileTypes' import { ref } from 'vue' // Constants for image processing -const DOWNSCALE_WIDTH = 32 -const DOWNSCALE_HEIGHT = 16 +const DOWNSCALE_WIDTH = 16 +const DOWNSCALE_HEIGHT = 8 const COLOR_SIMILARITY_THRESHOLD = 30 const EDGE_SIMILARITY_THRESHOLD = 20 -const BATCH_SIZE = 4 +const BATCH_SIZE = 8 export function useTileProcessingComposable() { const tileAnalysisCache = ref<Map<string, { color: { r: number; g: number; b: number }; edge: number; namePrefix: string }>>(new Map()) const processingQueue = ref<Tile[]>([]) let isProcessing = false - const worker = new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' }) - worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => { - const { tileId, color, edge, namePrefix } = e.data - tileAnalysisCache.value.set(tileId, { color, edge, namePrefix }) - isProcessing = false - processBatch() - } + const NUM_WORKERS = 4 + const workers = Array.from({ length: NUM_WORKERS }, () => new Worker(new URL('@/workers/tileAnalyzerWorker.ts', import.meta.url), { type: 'module' })) + let currentWorker = 0 - async function processTileAsync(tile: Tile): Promise<void> { + // Modify worker message handling + workers.forEach((worker) => { + worker.onmessage = (e: MessageEvent<TileAnalysisResult>) => { + const { tileId, color, edge, namePrefix } = e.data + tileAnalysisCache.value.set(tileId, { color, edge, namePrefix }) + isProcessing = false + processBatch() + } + }) + + async function processTileAsync(tile: Tile, worker: Worker): Promise<void> { if (tileAnalysisCache.value.has(tile.id)) return return new Promise((resolve) => { @@ -60,7 +66,12 @@ export function useTileProcessingComposable() { isProcessing = true const batch = processingQueue.value.splice(0, BATCH_SIZE) - Promise.all(batch.map((tile) => processTileAsync(tile))).then(() => { + Promise.all( + batch.map((tile) => { + currentWorker = (currentWorker + 1) % NUM_WORKERS + return processTileAsync(tile, workers[currentWorker]) + }) + ).then(() => { isProcessing = false if (processingQueue.value.length > 0) { setTimeout(processBatch, 0) @@ -87,7 +98,7 @@ export function useTileProcessingComposable() { } function cleanup() { - worker.terminate() + workers.forEach((worker) => worker.terminate()) } return { diff --git a/src/workers/tileAnalyzerWorker.ts b/src/workers/tileAnalyzerWorker.ts index 752e7a4..4ead142 100644 --- a/src/workers/tileAnalyzerWorker.ts +++ b/src/workers/tileAnalyzerWorker.ts @@ -12,7 +12,6 @@ function analyzeTile(imageData: ImageData, tileId: string, tileName: string): Ti const { r, g, b } = getDominantColorFast(imageData) const edge = getEdgeComplexityFast(imageData) const namePrefix = tileName.split('_')[0] - return { tileId, color: { r, g, b }, @@ -53,16 +52,14 @@ function getEdgeComplexityFast(imageData: ImageData) { const height = imageData.height let edgePixels = 0 - for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE) { - for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE) { + // Only check every other row/column + for (let y = 0; y < height; y += PIXEL_SAMPLE_RATE * 2) { + for (let x = 0; x < width; x += PIXEL_SAMPLE_RATE * 2) { const i = (y * width + x) * 4 - if ( - data[i + 3] > 0 && - (x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i - width * 4 * PIXEL_SAMPLE_RATE + 3] === 0 || data[i + width * 4 * PIXEL_SAMPLE_RATE + 3] === 0) - ) { + if (data[i + 3] > 0 && (x === 0 || y === 0 || x >= width - PIXEL_SAMPLE_RATE || y >= height - PIXEL_SAMPLE_RATE || data[i - 4 * PIXEL_SAMPLE_RATE + 3] === 0)) { edgePixels++ } } } - return edgePixels * PIXEL_SAMPLE_RATE + return edgePixels * PIXEL_SAMPLE_RATE * 2 }