init
This commit is contained in:
commit
c6863f2bfc
34 files changed
+6909
No files matched your search
@@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
PhCircle,
|
||||
PhImage,
|
||||
PhPlus,
|
||||
PhSquare,
|
||||
PhTextT,
|
||||
PhUploadSimple,
|
||||
} from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { isSvgFile, loadImage, parseSvgDimensions, readAsDataUrl } from '@/lib/fileLoader'
|
||||
import type { Asset } from '@/types/scene'
|
||||
|
||||
const store = useSceneStore()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const uploadError = ref<string | null>(null)
|
||||
|
||||
function addSampleRect() {
|
||||
const asset = store.addAsset({
|
||||
kind: 'rect',
|
||||
name: 'Rect',
|
||||
width: 160,
|
||||
height: 100,
|
||||
fill: '#6ea8ff',
|
||||
})
|
||||
store.addLayer(asset.id)
|
||||
}
|
||||
|
||||
function addSampleCircle() {
|
||||
const asset = store.addAsset({
|
||||
kind: 'circle',
|
||||
name: 'Circle',
|
||||
radius: 60,
|
||||
fill: '#ff7a59',
|
||||
})
|
||||
store.addLayer(asset.id)
|
||||
}
|
||||
|
||||
async function handleFileSelected(event: Event) {
|
||||
uploadError.value = null
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files ?? [])
|
||||
try {
|
||||
for (const file of files) await importFile(file)
|
||||
} catch (err) {
|
||||
uploadError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function importFile(file: File) {
|
||||
const dataUrl = await readAsDataUrl(file)
|
||||
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
if (isSvgFile(file)) {
|
||||
const text = await file.text()
|
||||
const dims = parseSvgDimensions(text)
|
||||
if (dims) {
|
||||
width = dims.width
|
||||
height = dims.height
|
||||
}
|
||||
}
|
||||
|
||||
if (!width || !height) {
|
||||
const img = await loadImage(dataUrl)
|
||||
width = img.naturalWidth || 300
|
||||
height = img.naturalHeight || 300
|
||||
}
|
||||
|
||||
const asset = store.addAsset({
|
||||
kind: 'image',
|
||||
name: file.name,
|
||||
src: dataUrl,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
store.addLayer(asset.id)
|
||||
}
|
||||
|
||||
function triggerUpload() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const assetIconMap: Record<Asset['kind'], typeof PhSquare> = {
|
||||
rect: PhSquare,
|
||||
circle: PhCircle,
|
||||
image: PhImage,
|
||||
text: PhTextT,
|
||||
}
|
||||
|
||||
const assetsWithUsage = computed(() =>
|
||||
store.scene.assets.map((a) => ({
|
||||
asset: a,
|
||||
inUse: store.scene.layers.some((l) => l.assetId === a.id),
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel">
|
||||
<h3>Assets</h3>
|
||||
|
||||
<button class="upload-btn" @click="triggerUpload">
|
||||
<PhUploadSimple :size="16" />
|
||||
<span>SVG / Bild hinzufügen</span>
|
||||
</button>
|
||||
|
||||
<div class="actions">
|
||||
<button class="shape-btn" @click="addSampleRect">
|
||||
<PhSquare :size="14" />
|
||||
Rect
|
||||
</button>
|
||||
<button class="shape-btn" @click="addSampleCircle">
|
||||
<PhCircle :size="14" />
|
||||
Circle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/svg+xml,image/png,image/jpeg,image/webp,image/gif"
|
||||
multiple
|
||||
style="display: none"
|
||||
@change="handleFileSelected"
|
||||
/>
|
||||
<p v-if="uploadError" class="error">{{ uploadError }}</p>
|
||||
|
||||
<h4>Bibliothek</h4>
|
||||
<ul class="assets">
|
||||
<li v-for="item in assetsWithUsage" :key="item.asset.id" :class="{ unused: !item.inUse }">
|
||||
<component :is="assetIconMap[item.asset.kind]" :size="14" class="asset-icon" />
|
||||
<span class="name" :title="item.asset.name">{{ item.asset.name }}</span>
|
||||
<button
|
||||
v-if="!item.inUse"
|
||||
class="add-layer"
|
||||
title="Als Layer einfügen"
|
||||
@click="store.addLayer(item.asset.id)"
|
||||
>
|
||||
<PhPlus :size="12" weight="bold" />
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="!store.scene.assets.length" class="empty">keine Assets</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 6px 0 4px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--accent-soft);
|
||||
border: 1px dashed var(--accent);
|
||||
color: var(--accent);
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-btn:hover:not(:disabled) {
|
||||
background: var(--accent-soft-strong);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.shape-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-size: 11px;
|
||||
margin: 2px 0 0;
|
||||
padding: 6px 8px;
|
||||
background: var(--danger-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.assets {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.assets li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--panel-alt);
|
||||
font-size: 12px;
|
||||
transition: background 140ms var(--ease);
|
||||
}
|
||||
|
||||
.assets li:hover {
|
||||
background: var(--panel-hover);
|
||||
}
|
||||
|
||||
.assets li.unused {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
color: var(--text-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.add-layer {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.add-layer:hover:not(:disabled) {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.empty {
|
||||
background: transparent !important;
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user