2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 液化实时更新器
|
|
|
|
|
|
* 负责高效地更新液化效果到画布上,避免频繁创建fabric对象导致的性能问题
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class LiquifyRealTimeUpdater {
|
|
|
|
|
|
constructor(canvas, options = {}) {
|
|
|
|
|
|
this.canvas = canvas;
|
|
|
|
|
|
this.targetObject = null;
|
|
|
|
|
|
this.isUpdating = false;
|
|
|
|
|
|
this.updateQueue = [];
|
|
|
|
|
|
this.lastUpdateTime = 0;
|
|
|
|
|
|
this.currImage = options.currImage || { value: null };
|
|
|
|
|
|
|
|
|
|
|
|
// 配置选项
|
|
|
|
|
|
this.config = {
|
|
|
|
|
|
throttleTime: options.throttleTime || 16, // 60fps
|
|
|
|
|
|
maxQueueSize: options.maxQueueSize || 5,
|
|
|
|
|
|
useDirectUpdate: options.useDirectUpdate !== false, // 默认启用直接更新
|
|
|
|
|
|
imageQuality: options.imageQuality || 1.0, // 图像质量 (0.1-1.0)
|
|
|
|
|
|
skipRenderDuringDrag: options.skipRenderDuringDrag || false, // 拖拽时跳过渲染
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 临时canvas用于快速渲染
|
|
|
|
|
|
this.tempCanvas = document.createElement("canvas");
|
|
|
|
|
|
this.tempCtx = this.tempCanvas.getContext("2d");
|
|
|
|
|
|
|
|
|
|
|
|
// 高质量canvas用于最终输出
|
|
|
|
|
|
this.highQualityCanvas = document.createElement("canvas");
|
|
|
|
|
|
this.highQualityCtx = this.highQualityCanvas.getContext("2d");
|
|
|
|
|
|
|
|
|
|
|
|
// 当前缓存的图像数据
|
|
|
|
|
|
this.cachedDataURL = null;
|
|
|
|
|
|
this.pendingImageData = null;
|
|
|
|
|
|
this.renderingScheduled = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 优化Canvas画布渲染设置
|
|
|
|
|
|
this.canvas.renderOnAddRemove = false; // 禁用自动渲染
|
|
|
|
|
|
this.canvas.skipOffscreen = true; // 跳过离屏元素渲染
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置目标对象
|
|
|
|
|
|
* @param {Object} fabricObject fabric图像对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
setTargetObject(fabricObject) {
|
|
|
|
|
|
this.targetObject = fabricObject;
|
|
|
|
|
|
if (fabricObject && fabricObject._element) {
|
|
|
|
|
|
// 设置临时canvas尺寸
|
|
|
|
|
|
this.tempCanvas.width = fabricObject.width;
|
|
|
|
|
|
this.tempCanvas.height = fabricObject.height;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置高质量canvas尺寸
|
|
|
|
|
|
this.highQualityCanvas.width = fabricObject.width;
|
|
|
|
|
|
this.highQualityCanvas.height = fabricObject.height;
|
|
|
|
|
|
|
|
|
|
|
|
// 配置高质量渲染上下文
|
|
|
|
|
|
this.highQualityCtx.imageSmoothingEnabled = true;
|
|
|
|
|
|
this.highQualityCtx.imageSmoothingQuality = "high";
|
|
|
|
|
|
|
|
|
|
|
|
// 配置临时canvas上下文(快速渲染)
|
|
|
|
|
|
this.tempCtx.imageSmoothingEnabled = false; // 快速模式关闭平滑
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 实时更新图像数据到画布
|
|
|
|
|
|
* @param {ImageData} imageData 新的图像数据
|
|
|
|
|
|
* @param {Boolean} isDrawing 是否正在绘制(拖拽过程中)
|
|
|
|
|
|
* @returns {Promise} 更新完成的Promise
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateImage(imageData, isDrawing = false) {
|
|
|
|
|
|
if (!this.targetObject || !imageData) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 节流控制
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if (now - this.lastUpdateTime < this.config.throttleTime && isDrawing) {
|
|
|
|
|
|
// 在绘制过程中进行节流,缓存最新的图像数据
|
|
|
|
|
|
this.pendingImageData = imageData;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.lastUpdateTime = now;
|
|
|
|
|
|
|
|
|
|
|
|
if (isDrawing && this.config.useDirectUpdate) {
|
|
|
|
|
|
// 拖拽过程中使用快速更新(降低质量以提高性能)
|
|
|
|
|
|
this._fastUpdate(imageData);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 拖拽结束后使用完整更新(最高质量)
|
|
|
|
|
|
await this._fullUpdate(imageData);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 智能图像质量更新
|
|
|
|
|
|
* 根据图像尺寸和设备性能动态调整质量
|
|
|
|
|
|
* @param {ImageData} imageData 图像数据
|
|
|
|
|
|
* @param {Boolean} isDrawing 是否正在绘制
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_getOptimalQuality(imageData, isDrawing) {
|
|
|
|
|
|
const pixelCount = imageData.width * imageData.height;
|
|
|
|
|
|
|
|
|
|
|
|
if (isDrawing) {
|
|
|
|
|
|
// 拖拽时根据图像大小调整质量
|
|
|
|
|
|
if (pixelCount > 1000000) {
|
|
|
|
|
|
// 大于1M像素
|
|
|
|
|
|
return 0.7;
|
|
|
|
|
|
} else if (pixelCount > 500000) {
|
|
|
|
|
|
// 大于500K像素
|
|
|
|
|
|
return 0.8;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 拖拽结束时始终使用最高质量
|
|
|
|
|
|
return 1.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 快速更新 - 直接修改现有对象的图像源
|
|
|
|
|
|
* @param {ImageData} imageData 图像数据
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_fastUpdate(imageData) {
|
|
|
|
|
|
if (!this.targetObject || !this.targetObject._element) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 将ImageData渲染到临时canvas(快速模式)
|
|
|
|
|
|
this.tempCtx.putImageData(imageData, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取智能质量设置
|
|
|
|
|
|
const quality = this._getOptimalQuality(imageData, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 直接更新fabric对象的图像源(使用PNG格式保持质量)
|
|
|
|
|
|
const targetElement = this.targetObject._element;
|
|
|
|
|
|
|
|
|
|
|
|
// 方案1: 直接设置src属性(最高性能)
|
|
|
|
|
|
const dataURL = this.tempCanvas.toDataURL("image/png", quality);
|
|
|
|
|
|
|
|
|
|
|
|
if (targetElement.src !== dataURL) {
|
|
|
|
|
|
targetElement.src = dataURL;
|
|
|
|
|
|
|
|
|
|
|
|
// 关键优化:直接设置fabric对象为脏状态,但不立即渲染
|
|
|
|
|
|
// this.targetObject.dirty = false; // 标记为不需要立即渲染
|
|
|
|
|
|
// this.canvas.renderOnAddRemove = true; // 恢复自动渲染
|
|
|
|
|
|
// this.renderingScheduled = false; // 重置渲染调度状态
|
|
|
|
|
|
this?.scheduleRender?.(); // 调度一次渲染
|
|
|
|
|
|
// 使用requestAnimationFrame进行批量渲染优化
|
|
|
|
|
|
// if (!this.renderingScheduled && !this.config.skipRenderDuringDrag) {
|
|
|
|
|
|
// this.renderingScheduled = true;
|
|
|
|
|
|
// requestIdleCallback(() => {
|
|
|
|
|
|
// this.canvas.renderAll();
|
|
|
|
|
|
// this.renderingScheduled = false;
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
|
|
|
|
|
} else {
|
2025-07-14 01:00:23 +08:00
|
|
|
|
console.warn("=================快速更新液化效果时,图像数据未变化,跳过更新");
|
2025-06-18 11:05:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("快速更新液化效果失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getImageData(imageData) {
|
|
|
|
|
|
// 使用高质量canvas进行最终渲染
|
|
|
|
|
|
this.highQualityCtx.putImageData(imageData, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 生成高质量DataURL(PNG格式,最大质量)
|
|
|
|
|
|
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
|
|
|
|
|
return dataURL;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 完整更新 - 创建新的fabric对象
|
|
|
|
|
|
* @param {ImageData} imageData 图像数据
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _fullUpdate(imageData) {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// 临时禁用画布自动渲染
|
|
|
|
|
|
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 使用高质量canvas进行最终渲染
|
|
|
|
|
|
this.highQualityCtx.putImageData(imageData, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 生成高质量DataURL(PNG格式,最大质量)
|
|
|
|
|
|
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果DataURL没有变化,跳过更新
|
|
|
|
|
|
if (this.cachedDataURL === dataURL) {
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.cachedDataURL = dataURL;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的fabric图像对象,保持最高质量
|
|
|
|
|
|
fabric.Image.fromURL(
|
|
|
|
|
|
dataURL,
|
|
|
|
|
|
(newImg) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!this.targetObject) {
|
|
|
|
|
|
console.warn("目标对象为空,跳过更新");
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存原对象信息用于智能查找
|
|
|
|
|
|
const originalObjId = this.targetObject.id;
|
|
|
|
|
|
const originalObjLayerId = this.targetObject.layerId;
|
|
|
|
|
|
|
|
|
|
|
|
// 保留原对象的所有变换属性
|
|
|
|
|
|
const originalObj = this.targetObject;
|
|
|
|
|
|
newImg.set({
|
|
|
|
|
|
left: originalObj.left,
|
|
|
|
|
|
top: originalObj.top,
|
|
|
|
|
|
scaleX: originalObj.scaleX,
|
|
|
|
|
|
scaleY: originalObj.scaleY,
|
|
|
|
|
|
angle: originalObj.angle,
|
|
|
|
|
|
flipX: originalObj.flipX,
|
|
|
|
|
|
flipY: originalObj.flipY,
|
|
|
|
|
|
opacity: originalObj.opacity,
|
|
|
|
|
|
originX: originalObj.originX,
|
|
|
|
|
|
originY: originalObj.originY,
|
|
|
|
|
|
id: originalObj.id,
|
|
|
|
|
|
name: originalObj.name,
|
|
|
|
|
|
layerId: originalObj.layerId,
|
|
|
|
|
|
selected: false,
|
|
|
|
|
|
evented: originalObj.evented,
|
|
|
|
|
|
});
|
|
|
|
|
|
this.canvas.renderOnAddRemove = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 智能查找和替换canvas上的对象
|
|
|
|
|
|
const allObjects = this.canvas.getObjects();
|
|
|
|
|
|
let targetIndex = allObjects.indexOf(originalObj);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果直接查找失败,尝试通过ID查找
|
|
|
|
|
|
if (targetIndex === -1 && originalObjId) {
|
2025-07-14 01:00:23 +08:00
|
|
|
|
targetIndex = allObjects.findIndex((obj) => obj.id === originalObjId);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
if (targetIndex !== -1) {
|
|
|
|
|
|
console.log(`通过ID找到目标对象: ${originalObjId}`);
|
|
|
|
|
|
// 更新目标对象引用
|
|
|
|
|
|
this.targetObject = allObjects[targetIndex];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果通过ID查找仍然失败,尝试通过图层ID查找
|
|
|
|
|
|
if (targetIndex === -1 && originalObjLayerId) {
|
2025-07-14 01:00:23 +08:00
|
|
|
|
targetIndex = allObjects.findIndex((obj) => obj.layerId === originalObjLayerId);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
if (targetIndex !== -1) {
|
|
|
|
|
|
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
|
|
|
|
|
|
// 更新目标对象引用
|
|
|
|
|
|
this.targetObject = allObjects[targetIndex];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (targetIndex !== -1) {
|
|
|
|
|
|
// 找到目标对象,执行替换
|
|
|
|
|
|
this.canvas.remove(this.targetObject);
|
|
|
|
|
|
this.canvas.insertAt(newImg, targetIndex);
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复自动渲染设置
|
|
|
|
|
|
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新目标对象引用
|
|
|
|
|
|
this.targetObject = newImg;
|
|
|
|
|
|
|
|
|
|
|
|
// 一次性重新渲染画布
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 液化对象更新成功,位置: ${targetIndex}`);
|
|
|
|
|
|
resolve(newImg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
|
2025-07-14 01:00:23 +08:00
|
|
|
|
console.warn("在画布中找不到目标对象,可能已被其他操作移除或替换");
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 恢复自动渲染设置
|
|
|
|
|
|
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试添加新对象到画布末尾
|
|
|
|
|
|
this.canvas.add(newImg);
|
|
|
|
|
|
this.targetObject = newImg;
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔄 已将新对象添加到画布末尾");
|
|
|
|
|
|
resolve(newImg);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 恢复自动渲染设置
|
|
|
|
|
|
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
|
|
|
|
|
console.error("更新fabric对象时出错:", error);
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ crossOrigin: "anonymous" }
|
|
|
|
|
|
); // 确保跨域支持
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("完整更新过程出错:", error);
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理待处理的图像数据
|
|
|
|
|
|
* 在拖拽结束后调用,处理可能积压的更新
|
|
|
|
|
|
*/
|
|
|
|
|
|
async processPendingUpdates() {
|
|
|
|
|
|
if (this.pendingImageData && !this.isUpdating) {
|
|
|
|
|
|
this.isUpdating = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this._fullUpdate(this.pendingImageData);
|
|
|
|
|
|
this.pendingImageData = null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("处理待处理更新失败:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.isUpdating = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前目标对象
|
|
|
|
|
|
* @returns {Object} 当前的fabric对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
getTargetObject() {
|
|
|
|
|
|
return this.targetObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 强制进行完整更新
|
|
|
|
|
|
* @param {ImageData} imageData 图像数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
async forceFullUpdate(imageData) {
|
|
|
|
|
|
return this._fullUpdate(imageData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启用拖拽模式 - 暂停渲染以提高性能
|
|
|
|
|
|
*/
|
|
|
|
|
|
enableDragMode() {
|
|
|
|
|
|
this.config.skipRenderDuringDrag = true;
|
|
|
|
|
|
this.canvas.renderOnAddRemove = false;
|
|
|
|
|
|
console.log("🚀 启用拖拽优化模式");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 禁用拖拽模式 - 恢复正常渲染
|
|
|
|
|
|
*/
|
|
|
|
|
|
disableDragMode() {
|
|
|
|
|
|
this.config.skipRenderDuringDrag = false;
|
|
|
|
|
|
this.canvas.renderOnAddRemove = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 执行一次完整渲染
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
console.log("✅ 恢复正常渲染模式");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置图像质量
|
|
|
|
|
|
* @param {Number} quality 质量值 (0.1-1.0)
|
|
|
|
|
|
*/
|
|
|
|
|
|
setImageQuality(quality) {
|
|
|
|
|
|
this.config.imageQuality = Math.max(0.1, Math.min(1.0, quality));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 优化的批量渲染方法
|
|
|
|
|
|
*/
|
|
|
|
|
|
scheduleRender() {
|
|
|
|
|
|
if (!this.renderingScheduled) {
|
|
|
|
|
|
this.renderingScheduled = true;
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
this.renderingScheduled = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// /**
|
|
|
|
|
|
// * 清理资源
|
|
|
|
|
|
// */
|
|
|
|
|
|
// dispose() {
|
|
|
|
|
|
// this.targetObject = null;
|
|
|
|
|
|
// this.cachedDataURL = null;
|
|
|
|
|
|
// this.pendingImageData = null;
|
|
|
|
|
|
// this.updateQueue.length = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// // 清理临时canvas
|
|
|
|
|
|
// if (this.tempCanvas) {
|
|
|
|
|
|
// this.tempCanvas.width = 0;
|
|
|
|
|
|
// this.tempCanvas.height = 0;
|
|
|
|
|
|
// this.tempCanvas = null;
|
|
|
|
|
|
// this.tempCtx = null;
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清理资源
|
|
|
|
|
|
*/
|
|
|
|
|
|
dispose() {
|
|
|
|
|
|
// 恢复canvas设置
|
|
|
|
|
|
this.canvas.renderOnAddRemove = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 清理缓存
|
|
|
|
|
|
this.cachedDataURL = null;
|
|
|
|
|
|
this.pendingImageData = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 清理canvas
|
|
|
|
|
|
if (this.tempCanvas) {
|
|
|
|
|
|
this.tempCanvas.width = 0;
|
|
|
|
|
|
this.tempCanvas.height = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.highQualityCanvas) {
|
|
|
|
|
|
this.highQualityCanvas.width = 0;
|
|
|
|
|
|
this.highQualityCanvas.height = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🧹 液化实时更新器资源已清理");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|