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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/lib/client.js +72 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # dsh-drawai
6
6
 
7
- [![GitHub stars](https://img.shields.io/github/stars/fourzkw/dsh-drawai?style=social)](https://github.com/fourzkw/dsh-drawai) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![GitHub stars](https://img.shields.io/github/stars/fourzkw/dsh-drawai?style=social)](https://github.com/fourzkw/dsh-drawai) [![npm downloads](https://img.shields.io/npm/dt/dsh-drawai)](https://www.npmjs.com/package/dsh-drawai) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](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 与 PNG(2×):没有 PDF。
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
- const grid = clone.querySelector('rect[fill^="url(#drawai-grid-major"]')
7721
- if (grid !== null) {
7722
- grid.setAttribute('x', String(bounds.minX - pad))
7723
- grid.setAttribute('y', String(bounds.minY - pad))
7724
- grid.setAttribute('width', String(w))
7725
- grid.setAttribute('height', String(h))
7726
- }
7727
- // 网格图案是半透明的:不加一层底色的话,PNG 出来是透明背景。
7728
- const paper = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
7729
- paper.setAttribute('x', String(bounds.minX - pad))
7730
- paper.setAttribute('y', String(bounds.minY - pad))
7731
- paper.setAttribute('width', String(w))
7732
- paper.setAttribute('height', String(h))
7733
- paper.setAttribute('fill', mode === 'dark' ? '#1b1b1b' : '#ffffff')
7734
- clone.insertBefore(paper, clone.firstChild)
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
- const scale = 2
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
- context.fillStyle = mode === 'dark' ? '#1b1b1b' : '#ffffff'
7855
- context.fillRect(0, 0, canvas.width, canvas.height)
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')→ 浏览器下载;AI 要的是对象
9278
- // (`{format, requestId, name}`)→ svg 把文本回执给宿主落盘、png 仍走浏览器下载。
9279
- // 导出选中:`{format, ids}` —— 不清选区(还要用 ids),从克隆里剔掉未选中的单元。
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 fromAi = typeof request === 'object' && request !== null
9285
- const format = fromAi ? request.format : request
9286
- const requestId = fromAi && typeof request.requestId === 'string' ? request.requestId : null
9287
- const askedName = fromAi && typeof request.name === 'string' && request.name.length > 0 ? request.name : null
9288
- const exportIds = fromAi && Array.isArray(request.ids) ? request.ids : null
9289
- const built = buildExportSvg(exportIds !== null ? { ids: exportIds } : undefined)
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, '') + (exportIds !== null ? '-selection' : '')
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: '位图,适合贴到文档里', disabled: empty },
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: '只导出当前选区', disabled: empty || selectedIds.length === 0 },
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-drawai",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "description": "DSH 右侧栏的可编辑画布 + AI 语义绘图工具(diagram_apply / diagram_read),直接读写原生 .drawio 文件",
6
6
  "license": "MIT",