dsh-drawai 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/lib/client.js +72 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# dsh-drawai
|
|
6
6
|
|
|
7
|
-
[](https://github.com/fourzkw/dsh-drawai) [](LICENSE)
|
|
7
|
+
[](https://github.com/fourzkw/dsh-drawai) [](https://www.npmjs.com/package/dsh-drawai) [](LICENSE)
|
|
8
8
|
|
|
9
9
|
## 功能一览
|
|
10
10
|
|
|
@@ -56,7 +56,7 @@ dsh plugin --profile web add github:fourzkw/dsh-drawai
|
|
|
56
56
|
|
|
57
57
|
| 入口 | 你能得到什么 |
|
|
58
58
|
|---|---|
|
|
59
|
-
| **右侧栏画布** | 打开或新建画布、画节点与连线、就地改字、图层、分区、顺序、多选对齐、查找、微移、多页切换、复制粘贴、导出 SVG / PNG
|
|
59
|
+
| **右侧栏画布** | 打开或新建画布、画节点与连线、就地改字、图层、分区、顺序、多选对齐、查找、微移、多页切换、复制粘贴、导出 SVG / PNG / 透明 PNG(4×,含导出选中) |
|
|
60
60
|
| **`diagram_apply`** | 让模型按 ops 改图:增删改、重接边、复制、排序、分层、导出;支持 `ids` 批量、`as` 别名、`expectRevision` |
|
|
61
61
|
| **`diagram_read`** | 让模型看见图的结构与样式、图层与父级、页面尺寸、你此刻的选区、文件指纹 `revision` |
|
|
62
62
|
| **选中与回退** | 你选中什么模型就知道什么;模型的每一轮改动都能在界面上退回去 |
|
|
@@ -78,7 +78,7 @@ dsh plugin --profile web add github:fourzkw/dsh-drawai
|
|
|
78
78
|
| **图层** | 列出 / 新建 / 显示隐藏 / 锁定 / 重命名 / 删除(合并到邻层)/ 选中移层 / 设为当前层;隐藏与锁定写入文件 |
|
|
79
79
|
| **顺序** | 置顶 / 上移 / 下移 / 置底 —— 写回时**真的改动文件里单元的先后**(否则重新打开就变回去了) |
|
|
80
80
|
| **自定义数据** | 右键「编辑数据」,按 `key=value` 一行一项改单元属性;按 user object 存回文件,drawio 打开不会丢 |
|
|
81
|
-
| **导出** | SVG / PNG(2×);「看一眼画布效果」走 DSH 附件服务,**不在工作区落文件** |
|
|
81
|
+
| **导出** | SVG / PNG(2×) / 透明 PNG(4×);「看一眼画布效果」走 DSH 附件服务,**不在工作区落文件** |
|
|
82
82
|
| **回退** | 「编辑 → 撤销 AI 改动」最多可以连着退 8 步 |
|
|
83
83
|
|
|
84
84
|
## 🤖 两个工具 / The two tools
|
|
@@ -211,7 +211,7 @@ id / layer / parent、选区能上报给模型、写回是逐单元的、画布
|
|
|
211
211
|
- **多页**:可切换编辑当前页(视图菜单);其它页保存时原样保留。AI 可用 `diagram_read` / `diagram_apply` 的 `page` 参数指定页。多页的**新建页**尚未实现。
|
|
212
212
|
- 独立 `edgeLabel` 单元只读;`entityRelation` 等其它 edgeStyle、箭头字形、旋转 / 翻转 / 透明、stencil 形状只保留、不渲染;`.drawio.svg` / `.png` / `.html` 内嵌载体不支持。
|
|
213
213
|
- **图片**:内嵌 SVG(`data:image/svg+xml,…`)与内嵌位图(PNG/JPEG/GIF/WebP data URI)都会画出来;**外链 / 相对路径**图片仍按矩形占位,单元原样保留(工作区磁盘上的图片文件画布直接读不到——要复刻请用 `traceImage`)。
|
|
214
|
-
- 导出只有 SVG
|
|
214
|
+
- 导出只有 SVG、PNG(2×) 与透明 PNG(4×):没有 PDF。
|
|
215
215
|
- 交互细节:对齐辅助线只跟节点比(不与折点、端点对齐);单条选中的连线不能复制。
|
|
216
216
|
|
|
217
217
|
完整的取舍与实现细节在 [docs/design.md](docs/design.md)。
|
package/lib/client.js
CHANGED
|
@@ -7677,6 +7677,7 @@ window.__ModuleLoader__.load({
|
|
|
7677
7677
|
if (junk[i].parentNode !== null) junk[i].parentNode.removeChild(junk[i])
|
|
7678
7678
|
}
|
|
7679
7679
|
const ids = opts !== undefined && opts !== null && Array.isArray(opts.ids) ? opts.ids : null
|
|
7680
|
+
const transparent = opts !== undefined && opts !== null && opts.transparent === true
|
|
7680
7681
|
let boundsDoc = current
|
|
7681
7682
|
if (ids !== null && ids.length > 0) {
|
|
7682
7683
|
const filtered = docFilteredToIds(current, ids)
|
|
@@ -7716,22 +7717,35 @@ window.__ModuleLoader__.load({
|
|
|
7716
7717
|
// 就是严格模式)会因为"用了未声明的前缀"整份丢弃:导出的 SVG/PNG 里那张图凭空消失。
|
|
7717
7718
|
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
|
|
7718
7719
|
clone.removeAttribute('style')
|
|
7719
|
-
//
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7720
|
+
// 视口底:网格纸(url(#drawai-grid-major…))或关网格时的实色纸。它是 svg 下第一个 <rect>。
|
|
7721
|
+
let pageRect = null
|
|
7722
|
+
for (let i = 0; i < clone.childNodes.length; i += 1) {
|
|
7723
|
+
const child = clone.childNodes[i]
|
|
7724
|
+
if (child.nodeType === 1 && String(child.nodeName).toLowerCase() === 'rect') {
|
|
7725
|
+
pageRect = child
|
|
7726
|
+
break
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
if (transparent) {
|
|
7730
|
+
// 透明导出:底纸/网格都要拿掉,否则 alpha 通道会被盖成不透明。
|
|
7731
|
+
if (pageRect !== null && pageRect.parentNode !== null) pageRect.parentNode.removeChild(pageRect)
|
|
7732
|
+
} else {
|
|
7733
|
+
// 网格纸在实时视图里只铺满"当前视口";导出要铺满整图,否则缩放状态下网格会断掉。
|
|
7734
|
+
if (pageRect !== null) {
|
|
7735
|
+
pageRect.setAttribute('x', String(bounds.minX - pad))
|
|
7736
|
+
pageRect.setAttribute('y', String(bounds.minY - pad))
|
|
7737
|
+
pageRect.setAttribute('width', String(w))
|
|
7738
|
+
pageRect.setAttribute('height', String(h))
|
|
7739
|
+
}
|
|
7740
|
+
// 网格图案是半透明的:不加一层底色的话,PNG 出来是透明背景。
|
|
7741
|
+
const paper = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
|
7742
|
+
paper.setAttribute('x', String(bounds.minX - pad))
|
|
7743
|
+
paper.setAttribute('y', String(bounds.minY - pad))
|
|
7744
|
+
paper.setAttribute('width', String(w))
|
|
7745
|
+
paper.setAttribute('height', String(h))
|
|
7746
|
+
paper.setAttribute('fill', mode === 'dark' ? '#1b1b1b' : '#ffffff')
|
|
7747
|
+
clone.insertBefore(paper, clone.firstChild)
|
|
7748
|
+
}
|
|
7735
7749
|
return { node: clone, width: w, height: h }
|
|
7736
7750
|
}
|
|
7737
7751
|
|
|
@@ -7837,12 +7851,16 @@ window.__ModuleLoader__.load({
|
|
|
7837
7851
|
setTimeout(() => URL.revokeObjectURL(url), 2000)
|
|
7838
7852
|
}
|
|
7839
7853
|
|
|
7840
|
-
function downloadPng(node, width, height, filename) {
|
|
7854
|
+
function downloadPng(node, width, height, filename, opts) {
|
|
7841
7855
|
const markup = new XMLSerializer().serializeToString(node)
|
|
7842
7856
|
const url = URL.createObjectURL(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }))
|
|
7843
7857
|
const image = new Image()
|
|
7844
7858
|
image.onload = () => {
|
|
7845
|
-
|
|
7859
|
+
// 默认 2×;透明高质量导出用 4×。过大时压到浏览器常见 canvas 上限,避免整图失败。
|
|
7860
|
+
let scale = opts !== undefined && opts !== null && typeof opts.scale === 'number' && opts.scale > 0 ? opts.scale : 2
|
|
7861
|
+
const transparent = opts !== undefined && opts !== null && opts.transparent === true
|
|
7862
|
+
const maxEdge = 8192
|
|
7863
|
+
while (scale > 1 && (width * scale > maxEdge || height * scale > maxEdge)) scale = scale / 2
|
|
7846
7864
|
const canvas = document.createElement('canvas')
|
|
7847
7865
|
canvas.width = Math.max(1, Math.round(width * scale))
|
|
7848
7866
|
canvas.height = Math.max(1, Math.round(height * scale))
|
|
@@ -7851,8 +7869,10 @@ window.__ModuleLoader__.load({
|
|
|
7851
7869
|
URL.revokeObjectURL(url)
|
|
7852
7870
|
return
|
|
7853
7871
|
}
|
|
7854
|
-
|
|
7855
|
-
|
|
7872
|
+
if (transparent !== true) {
|
|
7873
|
+
context.fillStyle = mode === 'dark' ? '#1b1b1b' : '#ffffff'
|
|
7874
|
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
7875
|
+
}
|
|
7856
7876
|
context.drawImage(image, 0, 0, canvas.width, canvas.height)
|
|
7857
7877
|
URL.revokeObjectURL(url)
|
|
7858
7878
|
canvas.toBlob((blob) => {
|
|
@@ -9274,19 +9294,23 @@ window.__ModuleLoader__.load({
|
|
|
9274
9294
|
// 导出。刻意放在 effect 里跑:按钮先清掉选中,等这一帧渲染完(DOM 里没有手柄和选中框了)
|
|
9275
9295
|
// 再序列化,产物才干净。若在点击回调里直接导,会把手柄一起拍进去。
|
|
9276
9296
|
//
|
|
9277
|
-
// 两种触发形态:菜单里点的是字符串('svg' / 'png')→
|
|
9278
|
-
// (`{format, requestId
|
|
9279
|
-
//
|
|
9297
|
+
// 两种触发形态:菜单里点的是字符串('svg' / 'png')→ 浏览器下载;对象形态
|
|
9298
|
+
// (`{format, requestId?, name?, ids?, transparent?, scale?}`)→ 菜单的选区/透明导出,
|
|
9299
|
+
// 或 AI 的 `{format, requestId, name}`(svg 回执落盘、png 仍走浏览器下载)。
|
|
9280
9300
|
React.useEffect(() => {
|
|
9281
9301
|
if (exportRequest === null) return
|
|
9282
9302
|
const request = exportRequest
|
|
9283
9303
|
setExportRequest(null)
|
|
9284
|
-
const
|
|
9285
|
-
const format =
|
|
9286
|
-
const requestId =
|
|
9287
|
-
const askedName =
|
|
9288
|
-
const exportIds =
|
|
9289
|
-
const
|
|
9304
|
+
const fromObj = typeof request === 'object' && request !== null
|
|
9305
|
+
const format = fromObj ? request.format : request
|
|
9306
|
+
const requestId = fromObj && typeof request.requestId === 'string' ? request.requestId : null
|
|
9307
|
+
const askedName = fromObj && typeof request.name === 'string' && request.name.length > 0 ? request.name : null
|
|
9308
|
+
const exportIds = fromObj && Array.isArray(request.ids) ? request.ids : null
|
|
9309
|
+
const transparent = fromObj && request.transparent === true
|
|
9310
|
+
const scale = fromObj && typeof request.scale === 'number' && request.scale > 0 ? request.scale : 2
|
|
9311
|
+
const built = buildExportSvg(
|
|
9312
|
+
exportIds !== null || transparent ? { ids: exportIds !== null ? exportIds : undefined, transparent: transparent } : undefined,
|
|
9313
|
+
)
|
|
9290
9314
|
if (built === null) {
|
|
9291
9315
|
if (requestId !== null) reportExportResult(requestId, format, { ok: false, error: '画布还没准备好,导出没做成' })
|
|
9292
9316
|
else if (exportIds !== null) setSaveNote('导出选中失败:选区里没有可导出的单元')
|
|
@@ -9297,7 +9321,9 @@ window.__ModuleLoader__.load({
|
|
|
9297
9321
|
const name =
|
|
9298
9322
|
askedName !== null
|
|
9299
9323
|
? askedName
|
|
9300
|
-
: String(source).split(/[\\/]/).pop().replace(/\.drawio$/i, '') +
|
|
9324
|
+
: String(source).split(/[\\/]/).pop().replace(/\.drawio$/i, '') +
|
|
9325
|
+
(exportIds !== null ? '-selection' : '') +
|
|
9326
|
+
(transparent ? '-transparent' : '')
|
|
9301
9327
|
if (format === 'svg') {
|
|
9302
9328
|
const markup = new XMLSerializer().serializeToString(built.node)
|
|
9303
9329
|
// AI 要的是"落成工作区里的一个文件":把文本回执给宿主,由它写在 .drawio 旁边。
|
|
@@ -9309,7 +9335,7 @@ window.__ModuleLoader__.load({
|
|
|
9309
9335
|
downloadBlob(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }), name + '.svg')
|
|
9310
9336
|
return
|
|
9311
9337
|
}
|
|
9312
|
-
downloadPng(built.node, built.width, built.height, name + '.png')
|
|
9338
|
+
downloadPng(built.node, built.width, built.height, name + '.png', { scale: scale, transparent: transparent })
|
|
9313
9339
|
// PNG 走浏览器下载(AI 触发的也一样):宿主写二进制得另开通道,而"点一次导出"就有的东西
|
|
9314
9340
|
// 不值得为它搬 base64。下载成不成浏览器不给回执,所以这里只报"已经交给下载"。
|
|
9315
9341
|
if (requestId !== null) {
|
|
@@ -11254,7 +11280,15 @@ window.__ModuleLoader__.load({
|
|
|
11254
11280
|
setSelectedIds([])
|
|
11255
11281
|
setExportRequest('png')
|
|
11256
11282
|
},
|
|
11257
|
-
{ hint: '
|
|
11283
|
+
{ hint: '位图,白/深色底,适合贴到文档里', disabled: empty },
|
|
11284
|
+
),
|
|
11285
|
+
item(
|
|
11286
|
+
'导出透明 PNG(4×)',
|
|
11287
|
+
() => {
|
|
11288
|
+
setSelectedIds([])
|
|
11289
|
+
setExportRequest({ format: 'png', transparent: true, scale: 4 })
|
|
11290
|
+
},
|
|
11291
|
+
{ hint: '高质量透明背景,适合叠到幻灯片/海报上', disabled: empty },
|
|
11258
11292
|
),
|
|
11259
11293
|
item(
|
|
11260
11294
|
'导出选中为 SVG',
|
|
@@ -11264,7 +11298,12 @@ window.__ModuleLoader__.load({
|
|
|
11264
11298
|
item(
|
|
11265
11299
|
'导出选中为 PNG',
|
|
11266
11300
|
() => setExportRequest({ format: 'png', ids: selectedIds.slice() }),
|
|
11267
|
-
{ hint: '
|
|
11301
|
+
{ hint: '只导出当前选区(白/深色底)', disabled: empty || selectedIds.length === 0 },
|
|
11302
|
+
),
|
|
11303
|
+
item(
|
|
11304
|
+
'导出选中为透明 PNG',
|
|
11305
|
+
() => setExportRequest({ format: 'png', ids: selectedIds.slice(), transparent: true, scale: 4 }),
|
|
11306
|
+
{ hint: '选区透明背景,4× 分辨率', disabled: empty || selectedIds.length === 0 },
|
|
11268
11307
|
),
|
|
11269
11308
|
],
|
|
11270
11309
|
},
|