合并画布代码

This commit is contained in:
X1627315083
2025-06-18 11:05:23 +08:00
parent 903c0ebdf5
commit 9c7fae36eb
118 changed files with 23633 additions and 8201 deletions

View File

@@ -1,338 +1,350 @@
<template>
<div class="brush-panel">
<div class="brush-panel-content">
<!-- 笔刷类型选择 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷类型</span>
</div>
<div class="brush-type-grid">
<div
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<div class="brush-panel" @click.stop="">
<div class="brush-panel-wrapper">
<div class="brush-panel-content">
<!-- 笔刷类型选择 -->
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
<div class="section-actions" v-if="category === '材质'">
<button class="action-btn" @click="showLibrary = !showLibrary">
<i class="icon-library">📚</i> 材质库
</button>
<span>笔刷类型</span>
</div>
<div class="brush-type-grid">
<div
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
<div class="slider-header">
<span>{{ prop.name }}</span>
<span class="slider-value">
{{ formatPropertyValue(prop) }}
</span>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
<div class="section-actions" v-if="category === '材质'">
<button class="action-btn" @click="showLibrary = !showLibrary">
<i class="icon-library">📚</i> 材质库
</button>
</div>
</div>
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
<div class="slider-header">
<span>{{ prop.name }}</span>
<span class="slider-value">
{{ formatPropertyValue(prop) }}
</span>
</div>
<div class="slider-container">
<input
type="range"
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
class="property-slider"
/>
</div>
<!-- 预设值按钮如果定义了预设 -->
<div v-if="prop.presets" class="property-presets">
<button
v-for="preset in prop.presets"
:key="preset"
@click="handlePropertyChange(prop.id, preset)"
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
>
{{ preset }}
</button>
</div>
</div>
<div class="slider-container">
<input
type="range"
</template>
<!-- 颜色选择器 -->
<template v-else-if="prop.type === 'color'">
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
</div>
<div class="color-row">
<input
type="color"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state
.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@click="handlePropertyChange(prop.id, color)"
></div>
</div>
</div>
</div>
</template>
<!-- 复选框 -->
<template v-else-if="prop.type === 'checkbox'">
<div class="checkbox-property">
<span>{{ prop.name }}</span>
<div class="toggle-switch">
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
</div>
</div>
</template>
<!-- 选择器 -->
<template v-else-if="prop.type === 'select'">
<div class="select-property">
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
class="property-slider"
/>
</div>
<!-- 预设值按钮如果定义了预设 -->
<div v-if="prop.presets" class="property-presets">
<button
v-for="preset in prop.presets"
:key="preset"
@click="handlePropertyChange(prop.id, preset)"
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
class="property-select"
>
{{ preset }}
</button>
<option
v-for="option in prop.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</div>
</template>
</template>
<!-- 颜色选择器 -->
<template v-else-if="prop.type === 'color'">
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<!-- 文件选择器(用于材质) -->
<template v-else-if="prop.type === 'file'">
<div class="file-property">
<div class="file-header">
<span>{{ prop.name }}</span>
</div>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
class="file-preview"
@click="handleFileSelect(prop.id)"
>
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
<div v-else class="no-file">点击上传材质图片</div>
</div>
<div class="file-actions">
<button
class="select-file-btn"
@click="handleFileSelect(prop.id)"
>
上传图片
</button>
<button
class="clear-file-btn"
@click="handlePropertyChange(prop.id, '')"
v-if="prop.value"
>
清除
</button>
</div>
</div>
<div class="color-row">
</template>
<!-- 材质网格选择器 -->
<template v-else-if="prop.type === 'texture-grid'">
<div class="texture-grid-property">
<div class="texture-grid-header">
<span>{{ prop.name }}</span>
</div>
<div class="texture-grid">
<div
v-for="texture in prop.options"
:key="texture.value"
class="texture-item"
:class="{ active: prop.value === texture.value }"
@click="handleTextureSelect(texture.value)"
>
<img
:src="texture.preview || texture.value"
:alt="texture.label"
class="texture-thumbnail"
/>
<span class="texture-label">{{ texture.label }}</span>
</div>
<!-- 自定义纹理上传按钮 -->
<div
class="texture-item upload-item"
@click="triggerTextureUpload"
>
<div class="upload-icon">
<span>+</span>
</div>
<span class="texture-label">上传纹理</span>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="textureFileInput"
type="file"
accept="image/*"
@change="handleTextureUpload"
style="display: none"
/>
</div>
</div>
</template>
<!-- 其他类型的属性 -->
<template v-else>
<div class="generic-property">
<span>{{ prop.name }}</span>
<input
type="color"
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@click="handlePropertyChange(prop.id, color)"
></div>
</div>
</div>
</div>
</template>
<!-- 复选框 -->
<template v-else-if="prop.type === 'checkbox'">
<div class="checkbox-property">
<span>{{ prop.name }}</span>
<div class="toggle-switch">
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
</div>
</div>
</template>
<!-- 选择器 -->
<template v-else-if="prop.type === 'select'">
<div class="select-property">
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="property-select"
>
<option
v-for="option in prop.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</template>
<!-- 文件选择器用于材质 -->
<template v-else-if="prop.type === 'file'">
<div class="file-property">
<div class="file-header">
<span>{{ prop.name }}</span>
</div>
<div class="file-preview" @click="handleFileSelect(prop.id)">
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
<div v-else class="no-file">点击上传材质图片</div>
</div>
<div class="file-actions">
<button
class="select-file-btn"
@click="handleFileSelect(prop.id)"
>
上传图片
</button>
<button
class="clear-file-btn"
@click="handlePropertyChange(prop.id, '')"
v-if="prop.value"
>
清除
</button>
</div>
</div>
</template>
<!-- 材质网格选择器 -->
<template v-else-if="prop.type === 'texture-grid'">
<div class="texture-grid-property">
<div class="texture-grid-header">
<span>{{ prop.name }}</span>
</div>
<div class="texture-grid">
<div
v-for="texture in prop.options"
:key="texture.value"
class="texture-item"
:class="{ active: prop.value === texture.value }"
@click="handleTextureSelect(texture.value)"
>
<img
:src="texture.preview || texture.value"
:alt="texture.label"
class="texture-thumbnail"
/>
<span class="texture-label">{{ texture.label }}</span>
</div>
<!-- 自定义纹理上传按钮 -->
<div class="texture-item upload-item" @click="triggerTextureUpload">
<div class="upload-icon">
<span>+</span>
</div>
<span class="texture-label">上传纹理</span>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="textureFileInput"
type="file"
accept="image/*"
@change="handleTextureUpload"
style="display: none;"
class="property-input"
/>
</div>
</div>
</template>
</template>
<!-- 其他类型的属性 -->
<template v-else>
<div class="generic-property">
<span>{{ prop.name }}</span>
<input
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="property-input"
/>
<!-- 属性描述提示 -->
<div v-if="prop.description" class="property-description">
{{ prop.description }}
</div>
</template>
<!-- 属性描述提示 -->
<div v-if="prop.description" class="property-description">
{{ prop.description }}
</div>
</div>
</div>
</div>
</template>
</template>
<!-- 材质库弹窗 -->
<div
v-if="showLibrary"
class="texture-library-overlay"
@click.self="showLibrary = false"
>
<div class="texture-library-modal">
<div class="modal-header">
<h3>材质库</h3>
<button class="close-btn" @click="showLibrary = false">
&times;
</button>
</div>
<div class="modal-content">
<div class="texture-categories">
<button
v-for="(category, index) in textureCategories"
:key="index"
:class="[
'category-btn',
{ active: selectedCategory === category },
]"
@click="selectedCategory = category"
>
{{ category }}
<!-- 材质库弹窗 -->
<div
v-if="showLibrary"
class="texture-library-overlay"
@click.self="showLibrary = false"
>
<div class="texture-library-modal">
<div class="modal-header">
<h3>材质库</h3>
<button class="close-btn" @click="showLibrary = false">
&times;
</button>
</div>
<div class="texture-list">
<div
v-for="(texture, index) in filteredTextures"
:key="index"
class="library-texture-item"
@click="selectLibraryTexture(texture.path)"
>
<img
:src="texture.thumbnail"
:alt="texture.name"
class="texture-thumbnail"
/>
<span class="texture-name">{{ texture.name }}</span>
<div class="modal-content">
<div class="texture-categories">
<button
v-for="(category, index) in textureCategories"
:key="index"
:class="[
'category-btn',
{ active: selectedCategory === category },
]"
@click="selectedCategory = category"
>
{{ category }}
</button>
</div>
<div class="texture-list">
<div
v-for="(texture, index) in filteredTextures"
:key="index"
class="library-texture-item"
@click="selectLibraryTexture(texture.path)"
>
<img
:src="texture.thumbnail"
:alt="texture.name"
class="texture-thumbnail"
/>
<span class="texture-name">{{ texture.name }}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="upload-btn"
@click="handleFileSelect('texturePath')"
>
上传新材质
</button>
</div>
</div>
<div class="modal-footer">
<button class="upload-btn" @click="handleFileSelect('texturePath')">
上传新材质
</div>
<!-- 笔刷预设 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<i class="save-icon">+</i>
</button>
</div>
</div>
</div>
<!-- 笔刷预设 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<i class="save-icon">+</i>
</button>
</div>
<div class="presets-container">
<div
v-for="(preset, index) in brushStore.state.presets"
:key="index"
class="preset-item"
@click="applyPresetWithCommand(index)"
>
<div class="presets-container">
<div
class="preset-color"
:style="{
backgroundColor: preset.color,
width: preset.size + 'px',
height: preset.size + 'px',
opacity: preset.opacity,
}"
></div>
<span class="preset-name">{{ preset.name }}</span>
v-for="(preset, index) in brushStore.state.presets"
:key="index"
class="preset-item"
@click="applyPresetWithCommand(index)"
>
<div
class="preset-color"
:style="{
backgroundColor: preset.color,
width: preset.size + 'px',
height: preset.size + 'px',
opacity: preset.opacity,
}"
></div>
<span class="preset-name">{{ preset.name }}</span>
</div>
</div>
</div>
</div>
@@ -448,10 +460,7 @@ function handleTextureSelect(textureId) {
// 触发纹理上传文件选择
function triggerTextureUpload() {
const fileInput = textureFileInput.value;
if (fileInput) {
fileInput.click();
}
handleFileSelect("texturePath");
}
// 处理纹理文件上传
@@ -461,15 +470,15 @@ async function handleTextureUpload(event) {
try {
// 验证文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
if (!file.type.startsWith("image/")) {
alert("请选择图片文件");
return;
}
// 验证文件大小 (限制为 5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
alert('文件大小不能超过 5MB');
alert("文件大小不能超过 5MB");
return;
}
@@ -478,8 +487,8 @@ async function handleTextureUpload(event) {
const brushManager = toolManager?.brushManager;
if (!texturePresetManager || !brushManager) {
console.error('缺少必要的管理器实例');
alert('系统错误:无法上传纹理');
console.error("缺少必要的管理器实例");
alert("系统错误:无法上传纹理");
return;
}
@@ -493,12 +502,11 @@ async function handleTextureUpload(event) {
});
await commandManager.execute(command);
// 清空文件输入,允许重复上传同一文件
event.target.value = '';
event.target.value = "";
} catch (error) {
console.error('纹理上传失败:', error);
console.error("纹理上传失败:", error);
alert(`上传失败: ${error.message}`);
}
}
@@ -673,29 +681,44 @@ onMounted(() => {
const brushStore = BrushStore;
</script>
<style scoped>
<style scoped lang="less">
.brush-panel {
position: absolute;
right: 20px;
top: 62px;
padding: 0;
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
width: 100%;
overflow-y: auto;
max-height: 85vh;
width: 30%;
/* overflow-y: auto; */
min-width: 280px;
border-left: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
position: relative;
animation: panelFadeIn 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
backdrop-filter: blur(2px); /* 添加模糊效果 */
-webkit-backdrop-filter: blur(2px);
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
z-index: 1000; /* 确保面板在最上层 */
.brush-panel-wrapper {
overflow-y: auto;
height: 100%;
max-height: 85vh; /* 限制最大高度 */
/*优化ios上的滚动效果*/
-webkit-overflow-scrolling: touch;
.brush-panel-content {
display: flex;
flex-direction: column;
margin: 0;
}
}
}
/* 添加指向整个面板的倒三角 */
.brush-panel::before {
content: "";
position: absolute;
top: -10px;
left: 20px;
top: -9px;
right: 58px;
width: 0;
height: 0;
border-left: 10px solid transparent;
@@ -716,12 +739,6 @@ const brushStore = BrushStore;
}
}
.brush-panel-content {
display: flex;
flex-direction: column;
margin: 0;
}
.brush-section {
padding: 0;
background-color: transparent;
@@ -746,9 +763,10 @@ const brushStore = BrushStore;
user-select: none;
position: relative;
transition: background-color 0.2s ease;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
}
.section-header::after {
/* .section-header::after {
content: "";
position: absolute;
right: 16px;
@@ -757,18 +775,18 @@ const brushStore = BrushStore;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #999; /* 更柔和的颜色 */
border-top: 6px solid #999;
transform: translateY(-50%);
transition: transform 0.25s ease;
}
} */
.section-header:hover {
/* .section-header:hover {
background-color: rgba(248, 249, 250, 1);
}
.section-header:hover::after {
border-top-color: #666;
}
} */
.section-actions {
display: flex;
@@ -1114,6 +1132,12 @@ const brushStore = BrushStore;
height: 34px;
}
@media screen and (max-width: 1024px) {
.property-select {
width: 120px;
}
}
.property-select:focus {
border-color: #4285f4;
outline: none;