Feat:支持导出某个节点为图片

This commit is contained in:
街角小林 2024-04-16 19:02:35 +08:00
parent 6878d92ebe
commit e072dcb170
4 changed files with 120 additions and 14 deletions

View File

@ -16,10 +16,10 @@ import {
import { SVG } from '@svgdotjs/svg.js'
import {
simpleDeepClone,
getType,
getObjectChangedProps,
isUndef,
handleGetSvgDataExtraContent
handleGetSvgDataExtraContent,
getNodeTreeBoundingRect
} from './src/utils'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
@ -410,7 +410,8 @@ class MindMap {
paddingY = 0,
ignoreWatermark = false,
addContentToHeader,
addContentToFooter
addContentToFooter,
node
} = {}) {
const { cssTextList, header, headerHeight, footer, footerHeight } =
handleGetSvgDataExtraContent({
@ -428,6 +429,11 @@ class MindMap {
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 需要裁减的区域
let clipData = null
if (node) {
clipData = getNodeTreeBoundingRect(node, rect.x, rect.y, paddingX, paddingY)
}
// 内边距
const fixHeight = 0
rect.width += paddingX * 2
@ -507,6 +513,7 @@ class MindMap {
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
clipData,
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height // 思维导图图形的宽高比

View File

@ -565,6 +565,12 @@ class Node {
this.renderer.emitNodeActiveEvent(this)
}
// 取消激活该节点
deactivate() {
this.mindMap.renderer.removeNodeFromActiveList(this)
this.mindMap.renderer.emitNodeActiveEvent()
}
// 更新节点
update() {
if (!this.group) {

View File

@ -47,15 +47,26 @@ class Export {
}
// 获取svg数据
async getSvgData() {
let { exportPaddingX, exportPaddingY, errorHandler, resetCss, addContentToHeader, addContentToFooter } =
this.mindMap.opt
let { svg, svgHTML } = this.mindMap.getSvgData({
async getSvgData(node) {
let {
exportPaddingX,
exportPaddingY,
errorHandler,
resetCss,
addContentToHeader,
addContentToFooter
} = this.mindMap.opt
let { svg, svgHTML, clipData } = this.mindMap.getSvgData({
paddingX: exportPaddingX,
paddingY: exportPaddingY,
addContentToHeader,
addContentToFooter
addContentToFooter,
node
})
if (clipData) {
clipData.paddingX = exportPaddingX
clipData.paddingY = exportPaddingY
}
// svg的image标签把图片的url转换成data:url类型否则导出会丢失图片
const task1 = this.createTransformImgTaskList(
svg,
@ -90,12 +101,13 @@ class Export {
}
return {
node: svg,
str: svgHTML
str: svgHTML,
clipData
}
}
// svg转png
svgToPng(svgSrc, transparent) {
svgToPng(svgSrc, transparent, clipData = null) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@ -109,6 +121,15 @@ class Export {
)
let imgWidth = img.width
let imgHeight = img.height
// 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦
let paddingX = 0
let paddingY = 0
if (clipData) {
paddingX = clipData.paddingX
paddingY = clipData.paddingY
imgWidth = clipData.width + paddingX * 2
imgHeight = clipData.height + paddingY * 2
}
// 检查是否超出canvas支持的像素上限
const maxSize = 16384 / dpr
const maxArea = maxSize * maxSize
@ -135,7 +156,22 @@ class Export {
await this.drawBackgroundToCanvas(ctx, imgWidth, imgHeight)
}
// 图片绘制到canvas里
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
// 如果有裁减数据,那么需要进行裁减
if (clipData) {
ctx.drawImage(
img,
clipData.left,
clipData.top,
clipData.width,
clipData.height,
paddingX,
paddingY,
clipData.width,
clipData.height
)
} else {
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
}
resolve(canvas.toDataURL())
} catch (error) {
reject(error)
@ -219,13 +255,24 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false) {
const { str } = await this.getSvgData()
async png(name, transparent = false, node = null) {
this.handleNodeExport(node)
const { str, clipData } = await this.getSvgData(node)
const svgUrl = await this.fixSvgStrAndToBlob(str)
const res = await this.svgToPng(svgUrl, transparent)
const res = await this.svgToPng(svgUrl, transparent, clipData)
return res
}
// 导出指定节点,如果该节点是激活状态,那么取消激活和隐藏展开收起按钮
handleNodeExport(node) {
if (node && node.getData('isActive')) {
node.deactivate()
if (!this.mindMap.opt.alwaysShowExpandBtn && node.getData('expand')) {
node.removeExpandBtn()
}
}
}
// 导出为pdf
async pdf(name, transparent = false) {
if (!this.mindMap.doExportPDF) {

View File

@ -1368,3 +1368,49 @@ export const handleGetSvgDataExtraContent = ({
footerHeight
}
}
// 获取指定节点的包围框信息
export const getNodeTreeBoundingRect = (node, x, y, paddingX, paddingY) => {
let minX = Infinity
let maxX = -Infinity
let minY = Infinity
let maxY = -Infinity
const walk = root => {
const { x, y, width, height } = root.group.findOne('.smm-node-shape').rbox()
if (x < minX) {
minX = x
}
if (x + width > maxX) {
maxX = x + width
}
if (y < minY) {
minY = y
}
if (y + height > maxY) {
maxY = y + height
}
if (root._generalizationList.length > 0) {
root._generalizationList.forEach(item => {
walk(item.generalizationNode)
})
}
if (root.children) {
root.children.forEach(item => {
walk(item)
})
}
}
walk(node)
minX = minX - x + paddingX
minY = minY - y + paddingY
maxX = maxX - x + paddingX
maxY = maxY - y + paddingY
return {
left: minX,
top: minY,
width: maxX - minX,
height: maxY - minY
}
}