9 changed files with 1628 additions and 310 deletions
File diff suppressed because it is too large
@ -0,0 +1,408 @@ |
|||||
|
<template> |
||||
|
<div class="fabric-view"> |
||||
|
<el-space :gutter="10" class="toolbar"> |
||||
|
<el-button @click="()=>performanceTest()">性能测试</el-button> |
||||
|
<el-button @click="selectAll">全选</el-button> |
||||
|
<div class="demo-color-block"> |
||||
|
<span class="demonstration">物体数:<el-text type="danger">{{ restate.objects }}</el-text></span> |
||||
|
<span class="demonstration"> 顶点数:<el-text type="danger">{{ restate.vertices }}</el-text></span> |
||||
|
<span class="demonstration"> 三角形数:<el-text type="danger">{{ restate.faces }}</el-text></span> |
||||
|
</div> |
||||
|
</el-space> |
||||
|
<div class="main-content"> |
||||
|
<div class="canvas-container" ref="canvasContainer" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup lang="ts"> |
||||
|
import * as fabric from 'fabric' |
||||
|
import { nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue' |
||||
|
import Theme from '@/core/Theme.ts' |
||||
|
|
||||
|
// DOM refs |
||||
|
const canvasContainer = ref(null) |
||||
|
|
||||
|
const restate = reactive({ |
||||
|
targetColor: '#ff0000', |
||||
|
loadScale: 1, |
||||
|
mode: 'translate', |
||||
|
objects: 0, |
||||
|
vertices: 0, |
||||
|
faces: 0 |
||||
|
}) |
||||
|
|
||||
|
// 状态变量 |
||||
|
const state = { |
||||
|
showAxesHelper: true, |
||||
|
showGridHelper: true, |
||||
|
camera: { |
||||
|
position: { x: 0, y: 5, z: 10 }, |
||||
|
rotation: { x: 0, y: 0, z: 0 } |
||||
|
} |
||||
|
} |
||||
|
onMounted(() => { |
||||
|
nextTick(() => { |
||||
|
initCanvas() |
||||
|
drawOriginMarker() |
||||
|
|
||||
|
drawHelpGrid() |
||||
|
addPanAndZoomHandlers() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
onBeforeUnmount(() => { |
||||
|
disposeFabric() |
||||
|
}) |
||||
|
|
||||
|
let canvas: fabric.Canvas |
||||
|
let resizeObserver: ResizeObserver | null = null |
||||
|
|
||||
|
function initCanvas() { |
||||
|
const container = canvasContainer.value |
||||
|
if (!container) return |
||||
|
|
||||
|
// 创建 fabric.Canvas 实例 |
||||
|
canvas = new fabric.Canvas(null, { |
||||
|
width: container.clientWidth, |
||||
|
height: container.clientHeight, |
||||
|
selection: true, |
||||
|
backgroundColor: Theme.backgroundColor |
||||
|
// preserveObjectStacking: true, |
||||
|
// renderOnAddRemove: false |
||||
|
}) |
||||
|
container.appendChild(canvas.getElement()) |
||||
|
canvas.enableMouseWheel = true |
||||
|
|
||||
|
// 设置视口初始位置 |
||||
|
// canvas.setViewportTransform([1, 0, 0, 1, 0, 0]) |
||||
|
canvas.requestRenderAll() |
||||
|
canvas.zoomToPoint(new fabric.Point(0, 0), 36) |
||||
|
|
||||
|
const viewerDom = canvasContainer.value |
||||
|
if (resizeObserver) { |
||||
|
resizeObserver.unobserve(viewerDom) |
||||
|
} |
||||
|
resizeObserver = new ResizeObserver(handleResize) |
||||
|
resizeObserver.observe(viewerDom) |
||||
|
} |
||||
|
|
||||
|
const handleResize = _.debounce(() => { |
||||
|
const container = canvasContainer.value // 假设这是你的容器元素引用 |
||||
|
if (container && canvas) { |
||||
|
const newWidth = container.clientWidth |
||||
|
const newHeight = container.clientHeight |
||||
|
|
||||
|
// 更新 canvas 尺寸 |
||||
|
canvas.setDimensions({ |
||||
|
width: newWidth, |
||||
|
height: newHeight |
||||
|
}) |
||||
|
canvas.requestRenderAll() |
||||
|
} |
||||
|
}, 200) |
||||
|
|
||||
|
function addPanAndZoomHandlers() { |
||||
|
const lowerCanvas = canvas.getElement() |
||||
|
let isDragging = false |
||||
|
let lastPosX = 0 |
||||
|
let lastPosY = 0 |
||||
|
let dragStartTransform = [] |
||||
|
|
||||
|
// 🔥 阻止默认右键菜单弹出 |
||||
|
lowerCanvas.addEventListener('contextmenu', (e) => e.preventDefault()) |
||||
|
|
||||
|
// ✅ 使用原生事件监听右键按下 |
||||
|
lowerCanvas.addEventListener('mousedown', function(e) { |
||||
|
if (e.button === 2) { // 右键 |
||||
|
isDragging = true |
||||
|
lastPosX = e.clientX |
||||
|
lastPosY = e.clientY |
||||
|
dragStartTransform = [...canvas.viewportTransform] |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
lowerCanvas.addEventListener('mousemove', function(e) { |
||||
|
if (isDragging) { |
||||
|
const deltaX = e.clientX - lastPosX |
||||
|
const deltaY = e.clientY - lastPosY |
||||
|
|
||||
|
// 更新 viewport 平移部分 |
||||
|
canvas.viewportTransform[4] = dragStartTransform[4] + deltaX |
||||
|
canvas.viewportTransform[5] = dragStartTransform[5] + deltaY |
||||
|
|
||||
|
canvas.renderAll() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
lowerCanvas.addEventListener('mouseup', function() { |
||||
|
isDragging = false |
||||
|
}) |
||||
|
|
||||
|
lowerCanvas.addEventListener('wheel', function(e) { |
||||
|
const delta = e.deltaY |
||||
|
let zoom = canvas.getZoom() |
||||
|
zoom *= 0.999 ** delta |
||||
|
if (zoom > 150) zoom = 150 |
||||
|
if (zoom < 5) zoom = 5 |
||||
|
canvas.zoomToPoint({ x: e.offsetX, y: e.offsetY }, zoom) |
||||
|
console.log('Zoom level:', zoom) |
||||
|
e.preventDefault() |
||||
|
e.stopPropagation() |
||||
|
}, { passive: false }) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 绘制辅助网格线 |
||||
|
*/ |
||||
|
function drawHelpGrid() { |
||||
|
const step = 1 |
||||
|
const xcount = 200 |
||||
|
const ycount = 200 |
||||
|
|
||||
|
// 从 -xcount/2 到 xcount/2 画辅助线, 步长为 step |
||||
|
// 从 -ycount/2 到 ycount/2 画辅助线, 步长为 step |
||||
|
// 颜色为 Theme.helpGridLineColor |
||||
|
|
||||
|
const halfWidth = (xcount / 2) * step |
||||
|
const halfHeight = (ycount / 2) * step |
||||
|
|
||||
|
// 垂直线(X轴方向) |
||||
|
for (let x = -halfWidth; x <= halfWidth; x += step) { |
||||
|
const line = new fabric.Line([x, -halfHeight, x, halfHeight], { |
||||
|
stroke: Theme.helpGridLineColor, |
||||
|
selectable: false, |
||||
|
evented: false, |
||||
|
strokeWidth: 0.05, |
||||
|
hasControls: false, |
||||
|
hasBorders: false |
||||
|
}) |
||||
|
canvas.add(line) |
||||
|
} |
||||
|
|
||||
|
// 水平线(Y轴方向) |
||||
|
for (let y = -halfHeight; y <= halfHeight; y += step) { |
||||
|
const line = new fabric.Line([-halfWidth, y, halfWidth, y], { |
||||
|
stroke: Theme.helpGridLineColor, |
||||
|
selectable: false, |
||||
|
evented: false, |
||||
|
strokeWidth: 0.05, |
||||
|
hasControls: false, |
||||
|
hasBorders: false |
||||
|
}) |
||||
|
canvas.add(line) |
||||
|
} |
||||
|
|
||||
|
canvas.renderAll() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
function drawOriginMarker() { |
||||
|
const marker = new fabric.Circle({ |
||||
|
radius: 5, |
||||
|
fill: 'red', |
||||
|
left: 0, |
||||
|
top: 0, |
||||
|
hasControls: false, |
||||
|
hasBorders: false, |
||||
|
lockMovementX: true, |
||||
|
lockMovementY: true, |
||||
|
selectable: false, |
||||
|
excludeFromExport: true, |
||||
|
objectCaching: false |
||||
|
}) |
||||
|
|
||||
|
marker.render = function(ctx) { |
||||
|
ctx.save() |
||||
|
ctx.translate(this.left, this.top) |
||||
|
ctx.beginPath() |
||||
|
ctx.arc(0, 0, 0.5, 0, Math.PI * 2, true) |
||||
|
ctx.fillStyle = 'red' |
||||
|
ctx.fill() |
||||
|
ctx.restore() |
||||
|
} |
||||
|
|
||||
|
canvas.add(marker) |
||||
|
canvas.requestRenderAll() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
function selectAll() { |
||||
|
const objects = canvas.getObjects().filter(obj => obj.selectable !== false) |
||||
|
const group = new fabric.Group(objects, { |
||||
|
originX: 'center', |
||||
|
originY: 'center' |
||||
|
}) |
||||
|
canvas.setActiveObject(group) |
||||
|
canvas.requestRenderAll() |
||||
|
} |
||||
|
|
||||
|
function disposeFabric() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
function performanceTest(count = 10) { |
||||
|
const spacing = 0.3 |
||||
|
const nodes = [] |
||||
|
debugger |
||||
|
|
||||
|
// 创建节点并加入画布 |
||||
|
for (let y = 0; y < count; y++) { |
||||
|
for (let x = 0; x < count; x++) { |
||||
|
const node = new fabric.Rect({ |
||||
|
left: x * spacing, |
||||
|
top: y * spacing, |
||||
|
width: 10, |
||||
|
height: 10, |
||||
|
fill: 'blue', |
||||
|
originX: 'center', |
||||
|
originY: 'center', |
||||
|
hasControls: false, |
||||
|
hasBorders: false |
||||
|
}) |
||||
|
canvas.add(node) |
||||
|
nodes.push(node) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 创建连线 |
||||
|
for (let i = 0; i < nodes.length; i++) { |
||||
|
const node = nodes[i] |
||||
|
const col = i % count |
||||
|
const row = Math.floor(i / count) |
||||
|
|
||||
|
// 连接右侧节点 |
||||
|
if (col < count - 1) { |
||||
|
const rightNode = nodes[i + 1] |
||||
|
const line = new fabric.Line( |
||||
|
[node.left, node.top, rightNode.left, rightNode.top], |
||||
|
{ |
||||
|
stroke: 'black', |
||||
|
strokeWidth: 0.5, |
||||
|
selectable: false |
||||
|
} |
||||
|
) |
||||
|
canvas.add(line) |
||||
|
} |
||||
|
|
||||
|
// 连接下方节点 |
||||
|
if (row < count - 1) { |
||||
|
const bottomNode = nodes[i + count] |
||||
|
const line = new fabric.Line( |
||||
|
[node.left, node.top, bottomNode.left, bottomNode.top], |
||||
|
{ |
||||
|
stroke: 'black', |
||||
|
strokeWidth: 0.5, |
||||
|
selectable: false |
||||
|
} |
||||
|
) |
||||
|
canvas.add(line) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
canvas.requestRenderAll() |
||||
|
} |
||||
|
|
||||
|
function deleteSelected() { |
||||
|
const activeObj = canvas.getActiveObject() |
||||
|
if (!activeObj) return |
||||
|
|
||||
|
if (activeObj.type === 'group') { |
||||
|
const g: fabric.Group = activeObj |
||||
|
g.getObjects().forEach(obj => canvas.remove(obj)) |
||||
|
} else { |
||||
|
canvas.remove(activeObj) |
||||
|
} |
||||
|
canvas.discardActiveObject() |
||||
|
canvas.requestRenderAll() |
||||
|
} |
||||
|
|
||||
|
fabric.Canvas.prototype.rateRule = 1 |
||||
|
|
||||
|
fabric.Canvas.prototype.toPixel = function(value) { |
||||
|
return value * this.rateRule |
||||
|
} |
||||
|
|
||||
|
fabric.Canvas.prototype.toUnit = function(value, unit = 'cm') { |
||||
|
return value / this.rateRule |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
<style scoped lang="less"> |
||||
|
.fabric-view { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
flex-grow: 1; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.canvas-left-toolbar { |
||||
|
position: absolute; |
||||
|
left: 5px; |
||||
|
top: 5px; |
||||
|
width: 32px; |
||||
|
background: #eee; |
||||
|
text-align: center; |
||||
|
|
||||
|
button { |
||||
|
color: #555; |
||||
|
background-color: #ddd; |
||||
|
border: 0; |
||||
|
margin: 0; |
||||
|
padding: 5px 8px; |
||||
|
font-size: 12px; |
||||
|
text-transform: uppercase; |
||||
|
cursor: pointer; |
||||
|
outline: none; |
||||
|
} |
||||
|
|
||||
|
button.selected { |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.toolbar { |
||||
|
padding: 10px; |
||||
|
background: #f5f5f5; |
||||
|
border-bottom: 1px solid #ccc; |
||||
|
} |
||||
|
|
||||
|
.dialog-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100vh; |
||||
|
} |
||||
|
|
||||
|
.main-content { |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.model3d-content { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.canvas-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.model3d-gui-wrap { |
||||
|
border-left: 1px solid #ccc; |
||||
|
background: #111; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.gui-panel { |
||||
|
:deep(.lil-gui.root) { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,6 @@ |
|||||
|
const Theme = { |
||||
|
backgroundColor: '#DDDDDD', |
||||
|
helpGridLineColor: '#F5F5F5' |
||||
|
} |
||||
|
|
||||
|
export default Theme |
||||
@ -0,0 +1,77 @@ |
|||||
|
/** |
||||
|
* 构建AGV性能数据 |
||||
|
* @param rows 行数 |
||||
|
* @param cols 列数 |
||||
|
*/ |
||||
|
export function buildAgvPerformanceData(rows, cols) { |
||||
|
const spacingX = 1.25 // X轴间距
|
||||
|
const spacingZ = 1.25 // Y轴间距
|
||||
|
|
||||
|
// 创建一个二维数组来存储点阵数据
|
||||
|
const data = new Map() |
||||
|
for (let row = 0; row < rows; row++) { |
||||
|
for (let col = 0; col < cols; col++) { |
||||
|
// 计算每个点的坐标
|
||||
|
const node = createAgvNode(row, col) |
||||
|
node.tf[0][0] = row * spacingX |
||||
|
node.tf[0][2] = col * spacingZ |
||||
|
data.set(node.id, node) |
||||
|
|
||||
|
// 与前一个点进行连线
|
||||
|
// if (row > 0 && col > 0) {
|
||||
|
// const preXNode = data.get('wp_' + (row - 1) + '_' + (col))
|
||||
|
// node.dt.center.push(preXNode.id)
|
||||
|
// preXNode.dt.center.push(node.id)
|
||||
|
//
|
||||
|
// const preYNode = data.get('wp_' + (row) + '_' + (col - 1))
|
||||
|
// node.dt.center.push(preYNode.id)
|
||||
|
// preYNode.dt.center.push(node.id)
|
||||
|
//
|
||||
|
// } else if (row > 0) {
|
||||
|
// const preXNode = data.get('wp_' + (row - 1) + '_' + (col))
|
||||
|
// node.dt.center.push(preXNode.id)
|
||||
|
// preXNode.dt.center.push(node.id)
|
||||
|
//
|
||||
|
// } else if (col > 0) {
|
||||
|
// const preYNode = data.get('wp_' + (row) + '_' + (col - 1))
|
||||
|
// node.dt.center.push(preYNode.id)
|
||||
|
// preYNode.dt.center.push(node.id)
|
||||
|
// }
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
data.get('wp_0_0').dt.center.push('wp_0_' + (cols - 1)) |
||||
|
data.get('wp_0_' + (cols - 1)).dt.center.push('wp_0_0') |
||||
|
|
||||
|
data.get('wp_' + (rows - 1) + '_0').dt.center.push('wp_' + (rows - 1) + '_' + (cols - 1)) |
||||
|
data.get('wp_' + (rows - 1) + '_' + (cols - 1)).dt.center.push('wp_' + (rows - 1) + '_0') |
||||
|
|
||||
|
data.get('wp_' + (rows - 1) + '_0').dt.center.push('wp_0_0') |
||||
|
data.get('wp_0_0').dt.center.push('wp_' + (rows - 1) + '_0') |
||||
|
|
||||
|
data.get('wp_' + (rows - 1) + '_' + (cols - 1)).dt.center.push('wp_0_' + (cols - 1)) |
||||
|
data.get('wp_0_' + (cols - 1)).dt.center.push('wp_' + (rows - 1) + '_' + (cols - 1)) |
||||
|
return Array.from(data.values()) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* @returns {ItemJson} |
||||
|
*/ |
||||
|
export function createAgvNode(x, z) { |
||||
|
return { |
||||
|
id: 'wp_' + x + '_' + z, |
||||
|
t: 'measure', |
||||
|
tf: [ |
||||
|
[x, 0.01, z], |
||||
|
[90, 0, 0], |
||||
|
[0.25, 0.25, 0.1] |
||||
|
], |
||||
|
dt: { |
||||
|
in: [], |
||||
|
out: [], |
||||
|
center: [] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
import 'fabric' |
||||
|
|
||||
|
declare module 'fabric' { |
||||
|
interface Canvas { |
||||
|
enableMouseWheel: boolean |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue