This commit is contained in:
X1627315083
2025-06-27 13:45:03 +08:00
9 changed files with 337 additions and 173 deletions

View File

@@ -34,11 +34,11 @@
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
<div class="section-actions" v-if="category === '材质'">
<!-- <div class="section-actions" v-if="category === '纹理'">
<button class="action-btn" @click="showLibrary = !showLibrary">
<i class="icon-library">📚</i> 材质库
</button>
</div>
</div> -->
</div>
<!-- 针对每个属性根据其类型渲染合适的控件 -->
@@ -163,7 +163,7 @@
</template>
<!-- 文件选择器(用于材质) -->
<template v-else-if="prop.type === 'file'">
<!-- <template v-else-if="prop.type === 'file'">
<div class="file-property">
<div class="file-header">
<span>{{ prop.name }}</span>
@@ -191,7 +191,7 @@
</button>
</div>
</div>
</template>
</template> -->
<!-- 材质网格选择器 -->
<template v-else-if="prop.type === 'texture-grid'">
@@ -200,6 +200,7 @@
<span>{{ prop.name }}</span>
</div>
<div class="texture-grid">
<!-- 预设纹理 -->
<div
v-for="texture in prop.options"
:key="texture.value"
@@ -212,8 +213,17 @@
:alt="texture.label"
class="texture-thumbnail"
/>
<span class="texture-label">{{ texture.label }}</span>
<!-- <span class="texture-label">{{ texture.label }}</span> -->
</div>
</div>
<div class="uploaded-textures-section">
<div class="uploaded-textures-divider">
<span>{{ $t("上传的纹理") }}</span>
</div>
</div>
<div class="texture-grid">
<!-- 上传的纹理缓存区域 -->
<!-- 自定义纹理上传按钮 -->
<div
class="texture-item upload-item"
@@ -222,8 +232,30 @@
<div class="upload-icon">
<span>+</span>
</div>
<span class="texture-label">上传纹理</span>
<span class="texture-label">{{ $t("上传纹理") }}</span>
</div>
<div
v-for="textureId in brushStore.state.uploadedTextures"
:key="textureId"
class="texture-item upload-item"
:class="{ active: prop.value === textureId }"
@click="handleTextureSelect(textureId)"
>
<img
:src="getUploadedTexturePreview(textureId)"
:alt="getUploadedTextureName(textureId)"
class="texture-thumbnail"
/>
<!-- 删除按钮 -->
<div
class="texture-remove-btn"
@click.stop="removeUploadedTexture(textureId)"
:title="$t('删除纹理')"
>
<span>×</span>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="textureFileInput"
@@ -261,7 +293,7 @@
</template>
<!-- 材质库弹窗 -->
<div
<!-- <div
v-if="showLibrary"
class="texture-library-overlay"
@click.self="showLibrary = false"
@@ -312,7 +344,7 @@
</button>
</div>
</div>
</div>
</div> -->
<!-- 笔刷预设 -->
<div class="brush-section">
@@ -373,6 +405,9 @@ const toolManager = inject("toolManager");
// 命令管理器
const commandManager = inject("commandManager");
// 注入纹理预设管理器
const texturePresetManager = inject("texturePresetManager");
// 计算属性:按分类获取当前笔刷可配置属性
const propertiesByCategory = computed(() => {
return BrushStore.getPropertiesByCategory();
@@ -460,7 +495,11 @@ function handleTextureSelect(textureId) {
// 触发纹理上传文件选择
function triggerTextureUpload() {
handleFileSelect("texturePath");
if (textureFileInput.value.length) {
textureFileInput.value[0].click();
return;
}
textureFileInput.value.click();
}
// 处理纹理文件上传
@@ -482,8 +521,7 @@ async function handleTextureUpload(event) {
return;
}
// 获取纹理预设管理器和笔刷管理器
const texturePresetManager = toolManager?.texturePresetManager;
// 获取笔刷管理器
const brushManager = toolManager?.brushManager;
if (!texturePresetManager || !brushManager) {
@@ -501,7 +539,14 @@ async function handleTextureUpload(event) {
brushManager: brushManager,
});
await commandManager.execute(command);
const result = await commandManager.execute(command);
// 上传成功后将纹理ID添加到缓存列表
if (result && result.textureId) {
brushStore.cacheUploadedTexture(result.textureId);
// 自动选择新上传的纹理
handleTextureSelect(result.textureId);
}
// 清空文件输入,允许重复上传同一文件
event.target.value = "";
@@ -511,6 +556,42 @@ async function handleTextureUpload(event) {
}
}
// 获取上传纹理的预览图
function getUploadedTexturePreview(textureId) {
const texture = texturePresetManager?.getTextureById?.(textureId);
return texture?.preview || texture?.path || "/placeholder-texture.png";
}
// 获取上传纹理的名称
function getUploadedTextureName(textureId) {
const texture = texturePresetManager?.getTextureById?.(textureId);
return texture?.name || "未知纹理";
}
// 删除上传的纹理
function removeUploadedTexture(textureId) {
try {
// 从缓存中移除
brushStore.removeCachedTexture(textureId);
// 从纹理预设管理器中删除
if (texturePresetManager?.removeCustomTexture) {
texturePresetManager.removeCustomTexture(textureId);
}
// 如果当前选中的是被删除的纹理,清空选择
const currentProperty = propertiesByCategory.value?.["纹理"]?.find(
(p) => p.id === "textureSelector"
);
if (currentProperty?.value === textureId) {
handlePropertyChange("textureSelector", null);
}
} catch (error) {
console.error("删除纹理失败:", error);
alert(`删除失败: ${error.message}`);
}
}
// 格式化属性值显示
function formatPropertyValue(prop) {
if (prop.type === "slider") {
@@ -560,30 +641,6 @@ function setBrushTypeWithCommand(type) {
commandManager.execute(command, { nonUndoable: true }); // 不需要撤销
}
// 处理文件选择
function handleFileSelect(propId) {
// 创建一个文件输入元素
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
// 更新属性值
handlePropertyChange(propId, e.target.result);
};
reader.readAsDataURL(file);
}
};
input.click();
}
// 获取笔刷预览样式
function getBrushPreviewStyle(brush) {
const baseStyle = {
@@ -1360,7 +1417,7 @@ const brushStore = BrushStore;
.texture-thumbnail {
width: 100%;
height: 45px;
height: 100%;
object-fit: cover;
border-radius: 4px;
margin-bottom: 8px;
@@ -1611,10 +1668,97 @@ const brushStore = BrushStore;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 移除内部动画只保留整体动画 */
.brush-type-grid,
.property-list,
.presets-container {
animation: none;
/* 上传纹理缓存区域样式 */
.uploaded-textures-section {
width: 100%;
margin-top: 15px;
}
.uploaded-textures-divider {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 0 4px;
}
.uploaded-textures-divider::before,
.uploaded-textures-divider::after {
content: "";
flex: 1;
height: 1px;
background: linear-gradient(
to right,
transparent,
rgba(0, 0, 0, 0.08),
transparent
);
}
.uploaded-textures-divider span {
padding: 0 12px;
font-size: 13px;
color: #666;
background-color: #fff;
white-space: nowrap;
font-weight: 500;
}
.uploaded-textures-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.uploaded-texture-item {
position: relative;
border: 1px solid rgba(108, 117, 125, 0.2);
background-color: rgba(248, 249, 250, 0.8);
}
.uploaded-texture-item:hover {
border-color: rgba(66, 133, 244, 0.3);
background-color: rgba(66, 133, 244, 0.05);
}
.uploaded-texture-item.active {
background-color: rgba(66, 133, 244, 0.15);
border-color: rgba(66, 133, 244, 0.4);
}
.texture-remove-btn {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
background-color: rgba(244, 67, 54, 0.9);
border: 1px solid #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.texture-remove-btn span {
color: #fff;
font-size: 12px;
font-weight: bold;
line-height: 1;
}
.uploaded-texture-item:hover .texture-remove-btn {
opacity: 1;
transform: scale(1.1);
}
.texture-remove-btn:hover {
background-color: rgba(244, 67, 54, 1);
transform: scale(1.2) !important;
}
</style>

View File

@@ -6,12 +6,10 @@
:class="{ 'is-active': visible }"
>
<div class="text-editor-panel-header">
<button class="header-btn import-btn">导入字体</button>
<div class="header-btn import-btn">编辑文本样式</div>
<div class="header-actions">
<button class="header-btn cancel-btn" @click="close">取消</button>
<button class="header-btn confirm-btn" @click="confirmEdit">
完成
</button>
<div class="header-btn cancel-btn" @click="close">取消</div>
<div class="header-btn confirm-btn" @click="confirmEdit">完成</div>
</div>
</div>
@@ -76,22 +74,6 @@
</div>
</div>
<div class="param-item">
<div class="param-label">字符间距</div>
<div class="param-control">
<input
type="range"
v-model.number="textSpacing"
min="-50"
max="100"
step="0.1"
@input="updateTextSpacing"
class="slider-control"
/>
<div class="param-value">{{ textSpacing.toFixed(1) }}%</div>
</div>
</div>
<div class="param-item">
<div class="param-label">行距</div>
<div class="param-control">
@@ -108,7 +90,7 @@
</div>
</div>
<div class="param-item">
<!-- <div class="param-item">
<div class="param-label">基线</div>
<div class="param-control">
<input
@@ -121,7 +103,7 @@
/>
<div class="param-value">{{ baseline }}pt</div>
</div>
</div>
</div> -->
<div class="param-item">
<div class="param-label">不透明度</div>
@@ -145,71 +127,76 @@
<div class="column-header">字体属性</div>
<div class="text-alignment">
<button
<div
class="align-btn"
:class="{ active: textAlign === 'left' }"
@click="setTextAlign('left')"
>
<div class="align-icon align-left"></div>
</button>
<button
<svg-icon name="CFleft" size="20" />
</div>
<div
class="align-btn"
:class="{ active: textAlign === 'center' }"
@click="setTextAlign('center')"
>
<div class="align-icon align-center"></div>
</button>
<button
<svg-icon name="CFcenter" size="20" />
</div>
<div
class="align-btn"
:class="{ active: textAlign === 'right' }"
@click="setTextAlign('right')"
>
<div class="align-icon align-right"></div>
</button>
<button
<svg-icon name="CFright" size="20" />
</div>
<div
class="align-btn"
:class="{ active: textAlign === 'justify' }"
@click="setTextAlign('justify')"
>
<div class="align-icon align-justify"></div>
</button>
<svg-icon name="CFjustify" size="26" />
</div>
</div>
<div class="text-styles">
<button
<div
class="style-btn"
:class="{ active: underline }"
@click="toggleStyle('underline', !underline)"
>
<div class="style-icon underline-icon">U</div>
</button>
<button
</div>
<div
class="style-btn"
:class="{ active: overline }"
@click="toggleStyle('overline', !overline)"
>
<div class="style-icon overline-icon">O</div>
</button>
<button
</div>
<div
class="style-btn"
:class="{ active: linethrough }"
@click="toggleStyle('linethrough', !linethrough)"
>
<div class="style-icon linethrough-icon">S</div>
</button>
<button class="style-btn color-btn" @click="openColorPicker('text')">
<div
class="style-icon color-icon"
:style="{ backgroundColor: textColor }"
></div>
</button>
</div>
</div>
<!-- 添加字体色控制区域 -->
<div class="background-controls">
<div class="bg-header">字体色</div>
<div class="bg-options">
<div class="style-btn color-btn" @click="openColorPicker('text')">
<div
class="style-icon color-icon"
:style="{ backgroundColor: textColor }"
></div>
</div>
</div>
</div>
<!-- 添加背景色控制区域 -->
<div class="background-controls">
<div class="bg-header">背景色</div>
<div class="bg-options">
<button
<div
class="style-btn color-btn"
@click="openColorPicker('background')"
>
@@ -228,14 +215,14 @@
backgroundPosition: hasTransparentBg ? '0 0, 4px 4px' : '0 0',
}"
></div>
</button>
<button
</div>
<div
class="transparent-btn"
:class="{ active: hasTransparentBg }"
@click="setTransparentBackground"
>
透明
</button>
</div>
</div>
</div>
</div>
@@ -248,9 +235,7 @@
<span>{{
colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色"
}}</span>
<button class="close-color-picker" @click="closeColorPicker">
×
</button>
<div class="close-color-picker" @click="closeColorPicker">×</div>
</div>
<div class="color-picker-content">
<input
@@ -268,9 +253,9 @@
@click="selectPresetColor(color)"
></div>
</div>
<button class="confirm-color-btn" @click="confirmColorSelection">
<div class="confirm-color-btn" @click="confirmColorSelection">
确定
</button>
</div>
</div>
</div>
</div>
@@ -991,6 +976,7 @@ export default {
font-size: 14px;
margin-bottom: 10px;
color: #333;
text-align: left;
}
.bg-options {

View File

@@ -18,6 +18,7 @@ import CanvasConfig from "./config/canvasConfig.js";
import { LiquifyManager } from "./managers/liquify/LiquifyManager";
import { SelectionManager } from "./managers/selection/SelectionManager";
import { RedGreenModeManager } from "./managers/RedGreenModeManager";
import texturePresetManager from "./managers/brushes/TexturePresetManager";
// import { MinimapManager } from "./managers/minimap/MinimapManager";
@@ -271,6 +272,7 @@ onMounted(async () => {
provide("keyboardManager", keyboardManager); // 提供给子组件使用
provide("activeTool", activeTool); // 提供给子组件使用
provide("liquifyManager", () => liquifyManager); // 提供液化管理器
provide("texturePresetManager", texturePresetManager); // 提供纹理预设管理器
provide("layers", layers); // 提供图层数据
// 初始化网格设置
// toggleGridVisibility(gridEnabled.value);

View File

@@ -478,80 +478,80 @@ export class TextureBrush extends BaseBrush {
order: 100,
hidden: allTextures.length === 0,
},
{
id: "textureRepeat",
name: "纹理重复模式",
type: "select",
defaultValue: this.textureRepeat,
options: [
{ value: "repeat", label: "双向重复" },
{ value: "repeat-x", label: "水平重复" },
{ value: "repeat-y", label: "垂直重复" },
{ value: "no-repeat", label: "不重复" },
],
description: "设置纹理的重复模式",
category: "纹理设置",
order: 110,
},
{
id: "textureScale",
name: "纹理缩放",
type: "slider",
defaultValue: this.textureScale,
min: 0.1,
max: 5,
step: 0.1,
description: "调整纹理的缩放比例",
category: "纹理设置",
order: 120,
},
{
id: "textureAngle",
name: "纹理旋转",
type: "slider",
defaultValue: this.textureAngle,
min: 0,
max: 360,
step: 5,
description: "调整纹理的旋转角度",
category: "纹理设置",
order: 130,
},
{
id: "textureOpacity",
name: "纹理透明度",
type: "slider",
defaultValue: this.textureOpacity,
min: 0,
max: 1,
step: 0.05,
description: "调整纹理的透明度",
category: "纹理设置",
order: 140,
},
{
id: "uploadTexture",
name: "上传纹理",
type: "button",
action: "uploadTexture",
description: "上传自定义纹理",
category: "纹理设置",
order: 150,
},
{
id: "texturePreview",
name: "纹理预览",
type: "preview",
description: "当前纹理预览",
category: "纹理设置",
order: 160,
getValue: () => {
const currentTexture = this.getCurrentTexture();
return currentTexture
? texturePresetManager.getTexturePreviewUrl(currentTexture)
: null;
},
},
// {
// id: "textureRepeat",
// name: "纹理重复模式",
// type: "select",
// defaultValue: this.textureRepeat,
// options: [
// { value: "repeat", label: "双向重复" },
// { value: "repeat-x", label: "水平重复" },
// { value: "repeat-y", label: "垂直重复" },
// { value: "no-repeat", label: "不重复" },
// ],
// description: "设置纹理的重复模式",
// category: "纹理设置",
// order: 110,
// },
// {
// id: "textureScale",
// name: "纹理缩放",
// type: "slider",
// defaultValue: this.textureScale,
// min: 0.1,
// max: 5,
// step: 0.1,
// description: "调整纹理的缩放比例",
// category: "纹理设置",
// order: 120,
// },
// {
// id: "textureAngle",
// name: "纹理旋转",
// type: "slider",
// defaultValue: this.textureAngle,
// min: 0,
// max: 360,
// step: 5,
// description: "调整纹理的旋转角度",
// category: "纹理设置",
// order: 130,
// },
// {
// id: "textureOpacity",
// name: "纹理透明度",
// type: "slider",
// defaultValue: this.textureOpacity,
// min: 0,
// max: 1,
// step: 0.05,
// description: "调整纹理的透明度",
// category: "纹理设置",
// order: 140,
// },
// {
// id: "uploadTexture",
// name: "上传纹理",
// type: "file",
// action: "uploadTexture",
// description: "上传自定义纹理",
// category: "纹理设置",
// order: 150,
// },
// {
// id: "texturePreview",
// name: "纹理预览",
// type: "preview",
// description: "当前纹理预览",
// category: "纹理设置",
// order: 160,
// getValue: () => {
// const currentTexture = this.getCurrentTexture();
// return currentTexture
// ? texturePresetManager.getTexturePreviewUrl(currentTexture)
// : null;
// },
// },
];
// 合并并返回所有属性

View File

@@ -72,6 +72,9 @@ const state = reactive({
},
],
// 上传的纹理缓存列表
uploadedTextures: [],
// 最近使用的颜色
recentColors: ["#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff"],
@@ -559,6 +562,35 @@ const actions = {
}
});
},
/**
* 清空上传的纹理缓存
*/
clearUploadedTextures() {
state.uploadedTextures = [];
},
/**
* 添加纹理到缓存
* @param {String} textureId 材质ID
*/
cacheUploadedTexture(textureId) {
const texture = texturePresetManager.getTextureById(textureId);
if (texture && !state.uploadedTextures.includes(textureId)) {
state.uploadedTextures.push(textureId);
}
},
/**
* 从缓存中移除纹理
* @param {String} textureId 材质ID
*/
removeCachedTexture(textureId) {
const index = state.uploadedTextures.indexOf(textureId);
if (index !== -1) {
state.uploadedTextures.splice(index, 1);
}
},
};
// 暴露给组件使用的Store对象