diff --git a/src/assets/icons/dc/add.svg b/src/assets/icons/dc/add.svg
new file mode 100644
index 0000000..3e0c3ba
--- /dev/null
+++ b/src/assets/icons/dc/add.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/dc/remove.svg b/src/assets/icons/dc/remove.svg
new file mode 100644
index 0000000..581dab1
--- /dev/null
+++ b/src/assets/icons/dc/remove.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Canvas/DepthCanvas/components/ai-selectbox-panel.vue b/src/components/Canvas/DepthCanvas/components/ai-selectbox-panel.vue
new file mode 100644
index 0000000..7979ed3
--- /dev/null
+++ b/src/components/Canvas/DepthCanvas/components/ai-selectbox-panel.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+ Add
+
+
+
+ Remove
+
+
+
+
+
+
+
+
diff --git a/src/components/Canvas/DepthCanvas/depth-canvas.vue b/src/components/Canvas/DepthCanvas/depth-canvas.vue
index 66e569c..60e2e52 100644
--- a/src/components/Canvas/DepthCanvas/depth-canvas.vue
+++ b/src/components/Canvas/DepthCanvas/depth-canvas.vue
@@ -6,8 +6,10 @@
- emit('workbench', v)" />
+
+ emit('workbench', v)" />
+
{
+ fabric.Image.fromURL(url, (img) => {
+ resolve(img);
+ }, { crossOrigin: "anonymous" });// 防止污染
+ });
+ }
+ rgba = { r: 0, g: 255, b: 0, a: 200 };
+ selectionStyle = {
+ stroke: "rgba(255, 77, 71, 1)",
+ strokeWidth: 1.5,
+ strokeDashArray: [4, 4],
+ fill: "rgba(255, 186, 186, 0.5)",
+ strokeUniform: true, // 保持描边宽度不随缩放改变
+ // strokeLineCap: "round",// 折线端点样式
+ // strokeLineJoin: "bevel", // 折线连接样式
+ // selectable: false,
+ // evented: false,
+ excludeFromExport: true,
+ hoverCursor: "default",
+ moveCursor: "default",
+ };
+ async createSelectbox() {
+ const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
+ const image = await this.loadImageToObject(url)
+ const canvas = getObjectAlphaToCanvas(image, null, 0, this.rgba);
+ const fobject = this.canvasManager.canvas.clipPath
+ // const top = fobject.top - fobject.height * scaleY / 2;
+ // const left = fobject.left - fobject.width * scaleX / 2;
+ const scaleY = fobject.scaleY
+ const scaleX = fobject.scaleX
+ const top = fobject.top
+ const left = fobject.left
+ const arr = traceImageContour(canvas);
+ let minX = fobject.width;
+ let minY = fobject.height;
+ const str = arr.map((v) => {
+ if (v.x < minX) minX = v.x;
+ if (v.y < minY) minY = v.y;
+ return `${v.x} ${v.y}`
+ }).join(" L ");
+ const path = new fabric.Path(`M ${str} z`);
+ path.set({
+ left: left + minX * scaleX,
+ top: top + minY * scaleY,
+ scaleX: scaleX,
+ scaleY: scaleY,
+ ...this.selectionStyle,
+ });
+ const rect1 = new fabric.Rect({
+ left: 0,
+ top: 0,
+ width: 100,
+ height: 100,
+ fill: '#f00',
+ info: {
+ id: createId("rect"),
+ name: '矩形图层',
+ }
+ })
+ const rect2 = new fabric.Rect({
+ left: 200,
+ top: 200,
+ width: 100,
+ height: 100,
+ fill: '#ff0',
+ info: {
+ id: createId("rect"),
+ name: '矩形图层',
+ }
+ })
+ this.layerManager.createGroupLayer({
+ child: [rect1, rect2],
+ })
+ // this.canvasManager.canvas.add(path)
+ // this.canvasManager.canvas.renderAll()
+
+ }
+
+
+
+
+
+
+
dispose() { }
}
+
+
+/**
+ * 获取对象黑白通道画布
+ * @param {fabric.Object} object - 要处理的 fabric 对象
+ * @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明
+ * @param {number} diff - 差值,默认 25
+ * @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
+ * @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData
+ * @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
+*/
+export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
+ const image = object.getElement();
+ if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
+ console.warn("对象不是图片");
+ return null;
+ }
+ const { width, height } = image;
+ if (!width || !height) {
+ console.warn("对象没有元素");
+ return null;
+ }
+ const canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0, width, height);
+ const data = ctx.getImageData(0, 0, width, height);
+ for (let i = 0; i < data.data.length; i += 4) {
+ const r = data.data[i + 0];
+ const g = data.data[i + 1];
+ const b = data.data[i + 2];
+ const a = data.data[i + 3];
+ const revR = revData?.data[i + 0] || 0;
+ const revG = revData?.data[i + 1] || 0;
+ const revB = revData?.data[i + 2] || 0;
+ const revA = revData?.data[i + 3] || 0;
+ let isHave = false;
+ if (r || g || b || a) {
+ if (revR > diff || revG > diff || revB > diff || revA > diff) {
+ isHave = false;
+ } else {
+ isHave = true;
+ }
+ }
+ if (isMerge && (revR || revG || revB || revA)) isHave = true;
+ if (isHave) {
+ data.data[i + 0] = rgba.r;
+ data.data[i + 1] = rgba.g;
+ data.data[i + 2] = rgba.b;
+ data.data[i + 3] = rgba.a;
+ } else {
+ data.data[i + 0] = 0;
+ data.data[i + 1] = 0;
+ data.data[i + 2] = 0;
+ data.data[i + 3] = 0;
+ }
+ }
+ ctx.clearRect(0, 0, width, height);
+ ctx.putImageData(data, 0, 0);
+ return canvas;
+}
+
+
+/**
+ * 图片边界跟踪算法(透明底)
+ * @param {HTMLCanvasElement} canvas - canvas元素
+ * @param {Number} scale - 缩放比例
+ * @returns {Array} 边界点数组 [{x, y}, ...]
+*/
+export function traceImageContour(canvas) {
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+ const width = canvas.width;
+ const height = canvas.height;
+
+ // 查找起始点(第一个不透明像素)
+ let startX = -1;
+ let startY = -1;
+
+ outer: for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const index = (y * width + x) * 4;
+ if (data[index + 3] > 0) {
+ startX = x;
+ startY = y;
+ break outer;
+ }
+ }
+ }
+
+ if (startX === -1) return []; // 没有不透明像素
+
+ // Moore-Neighbor边界跟踪算法
+ const contour = [];
+ const visited = new Set();
+ const directions = [
+ [-1, 0],
+ [-1, -1],
+ [0, -1],
+ [1, -1],
+ [1, 0],
+ [1, 1],
+ [0, 1],
+ [-1, 1],
+ ];
+
+ let currentX = startX;
+ let currentY = startY;
+ let backtrackDir = 4; // 起始方向:右
+
+ do {
+ const pointKey = `${currentX},${currentY}`;
+ if (!visited.has(pointKey)) {
+ contour.push({ x: currentX, y: currentY });
+ visited.add(pointKey);
+ }
+
+ // 从右方向开始顺时针查找
+ let found = false;
+ for (let i = 0; i < 8; i++) {
+ const dir = (backtrackDir + i) % 8;
+ const dx = directions[dir][0];
+ const dy = directions[dir][1];
+ const checkX = currentX + dx;
+ const checkY = currentY + dy;
+
+ if (
+ checkX >= 0 &&
+ checkX < width &&
+ checkY >= 0 &&
+ checkY < height
+ ) {
+ const index = (checkY * width + checkX) * 4;
+ if (data[index + 3] > 0) {
+ currentX = checkX;
+ currentY = checkY;
+ backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found) break;
+ } while (
+ !(currentX === startX && currentY === startY) &&
+ visited.size < width * height
+ );
+
+ return contour;
+}
\ No newline at end of file
diff --git a/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts b/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts
index c958adf..4a89cad 100644
--- a/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts
+++ b/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts
@@ -29,6 +29,17 @@ fabric.Object.prototype.toObject = function () {
return object
}
+fabric.Image.fromURL = (function (originalFromURL) {
+ return function (url, callback, imgOptions) {
+ // 为所有图片请求添加 crossOrigin
+ const options = {
+ crossOrigin: 'anonymous', // 关键设置
+ ...imgOptions
+ };
+ return originalFromURL.call(this, url, callback, options);
+ };
+})(fabric.Image.fromURL);
+
interface CanvasInitOptions {
canvasRef: any
canvasViewWidth?: number
diff --git a/src/components/Canvas/DepthCanvas/manager/LayerManager.ts b/src/components/Canvas/DepthCanvas/manager/LayerManager.ts
index 91a81e9..cbf24a4 100644
--- a/src/components/Canvas/DepthCanvas/manager/LayerManager.ts
+++ b/src/components/Canvas/DepthCanvas/manager/LayerManager.ts
@@ -151,6 +151,37 @@ export class LayerManager {
if (isActive) this.setActiveID(emptyObject.info.id, false)
return emptyObject
}
+ /** 创建组图层 */
+ createGroupLayer(options?: any, isRecord = true, isActive = false) {
+ const child = options?.child || []
+ delete options.child
+ const groupObject = new fabric.Group(child, {
+ subTargetCheck: true, // 关键:检测子对象
+ interactive: true, // 启用交互
+ hasControls: true,
+ hasBorders: true,
+
+ // // 子对象样式
+ // cornerColor: 'blue',
+ // cornerSize: 8,
+ // borderColor: 'green',
+
+ // // 允许子对象独立变换
+ // lockScalingX: false,
+ // lockScalingY: false,
+ // lockRotation: false,
+ info: {
+ id: createId("group"),
+ name: '组图层',
+ ...(options?.info || {}),
+ }
+ })
+ // this.setLayerPosition(groupObject)
+ this.canvasManager.add(groupObject, isRecord)
+ if (isActive) this.setActiveID(groupObject.info.id, false)
+ return groupObject
+ }
+
/** 创建文本图层 */
async createTextLayer(text: string, options?: any) {
const textObject = new fabric.IText(text, {
diff --git a/src/views/home/test.vue b/src/views/home/test.vue
index 2f910ec..7f4f9d7 100644
--- a/src/views/home/test.vue
+++ b/src/views/home/test.vue
@@ -10,7 +10,7 @@
import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
- const url = ''
+ const url = 'https://www.minio-api.aida.com.hk/fida-test/furniture/sketches/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260320%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260320T020948Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=7dc192bac887bce7b02c99d7037c08d9d684310f00add9b0e63b74b36ee63d37'
const openCanvas = () => {
myEvent.emit('openFlowCanvas', { url })
}