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

View File

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

View File

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

View File

@ -1368,3 +1368,49 @@ export const handleGetSvgDataExtraContent = ({
footerHeight 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
}
}