dsh-codex-subscription 2.3.0 → 2.4.0
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.en.md +30 -41
- package/README.md +38 -36
- package/lib/client.js +809 -7
- package/lib/index.js +39 -11
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -16,13 +16,19 @@ No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay
|
|
|
16
16
|
[](LICENSE)
|
|
17
17
|
[](https://github.com/WSL043/dsh-codex-subscription/stargazers)
|
|
18
18
|
|
|
19
|
-
[What's new](#whats-new-in-
|
|
19
|
+
[What's new](#whats-new-in-240) · [Features](#feature-overview) · [Install](#install) · [Daily use](#inside-the-plugin) · [Examples](#from-sketch-to-image) · [User guide](docs/GUIDE.en.md) · [Update and uninstall](#update-and-uninstall)
|
|
20
20
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<p align="center"><img src="docs/assets/product-account-demo-en.png" width="1100" alt="Codex subscription account and quota interface with model choices, quota windows and alerts; demonstration data"></p>
|
|
24
24
|
|
|
25
|
-
## What's new in 2.
|
|
25
|
+
## What's new in 2.4.0
|
|
26
|
+
|
|
27
|
+
- **Stronger agent drawing (Beta).** A new SVG drawing command lets the model draw a whole layer in the notation it knows best; the result is still editable, undoable and exportable native strokes. Approximations such as gradients or clamped points are reported, and unsupported elements are refused by name.
|
|
28
|
+
- **New showcase.** GPT-6.1-Sol at Xhigh and GPT-6-Astra at Low compared side by side on a mechanical cutaway and a blue-green landscape, run on the official desktop app; see [From sketch to image](#from-sketch-to-image).
|
|
29
|
+
- **Desktop app fixes.** On the official desktop app the Codex subtask component now detects the host version and installs; support diagnostics report the real DSH version; a failed image request says whether it timed out or the connection dropped.
|
|
30
|
+
|
|
31
|
+
### 2.3.0
|
|
26
32
|
|
|
27
33
|
- **The official desktop app works.** The official DeepSeek Harness desktop app (0.2.0-rc.2) passed a hands-on acceptance run: plugin install, sign-in, model list, hiding models, a chat with a tool call, and image generation. See [Install](#install).
|
|
28
34
|
- **Models come only from your account.** The model list is read entirely from the account catalog; before sign-in or when the read fails no model is shown, and there is no built-in fallback. Under Models & runtime you can show or hide each model in the composer.
|
|
@@ -79,7 +85,7 @@ Subscription access, model controls, quota management, and image creation in one
|
|
|
79
85
|
| **Smoothing and navigation** | Smooth complete strokes after release; pan, zoom, undo, and redo | Shortcuts can be customized or disabled |
|
|
80
86
|
| **Multiple drafts** | Save and reopen up to 20 local drafts | Stored in the current browser |
|
|
81
87
|
| **Import and export** | Export PNG, layered PSD, or native drafts that preserve editable strokes | PSD import supports a limited set of ordinary pixel layers |
|
|
82
|
-
| **Agent drawing** | Ask the agent to draw with `@sketch`, follow progress, and stop it | Beta; off by default, requires the canvas |
|
|
88
|
+
| **Agent drawing** | Ask the agent to draw with `@sketch`, follow progress, and stop it; the agent can draw whole stages in SVG, imported as editable native strokes | Beta; off by default, requires the canvas |
|
|
83
89
|
| **Result preview for the model** | Return a canvas image after drawing for subsequent review | Beta; off by default, adds image input usage |
|
|
84
90
|
|
|
85
91
|
### Advanced capabilities and maintenance
|
|
@@ -113,7 +119,7 @@ This plugin supports the latest DeepSeek Harness release recorded in its package
|
|
|
113
119
|
2. Paste this package name, with its version, into the **Package name or address** field:
|
|
114
120
|
|
|
115
121
|
```text
|
|
116
|
-
dsh-codex-subscription@2.
|
|
122
|
+
dsh-codex-subscription@2.4.0
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
3. Click **Install** and wait for completion. Follow the page instructions; save your work before restarting if requested.
|
|
@@ -183,59 +189,42 @@ Image generation, editing, and the sketch canvas are **Beta**. The canvas and ag
|
|
|
183
189
|
Draw by hand with the canvas button, or ask the agent with `@sketch` and your drawing request.
|
|
184
190
|
|
|
185
191
|
<table>
|
|
186
|
-
<tr><th width="50%">
|
|
187
|
-
<tr><td><img src="docs/assets/sketch-
|
|
192
|
+
<tr><th width="50%">Native sketch drawn by the agent</th><th width="50%">Image generated with the sketch as the only reference</th></tr>
|
|
193
|
+
<tr><td><img src="docs/assets/sketch-lighthouse-sketch.webp" alt="Lighthouse sketch drawn by GPT-6.1-Sol" width="480"></td><td><img src="docs/assets/sketch-lighthouse-result.webp" alt="Watercolor lighthouse poster generated from the sketch" width="480"></td></tr>
|
|
188
194
|
</table>
|
|
189
195
|
|
|
190
|
-
|
|
196
|
+
**GPT-6.1-Sol · Xhigh** on the official desktop app 0.2.0-rc.2: one `@sketch` message produced 6 layers and 229 editable native strokes, then called GPT Image 2 (quality: high) once with the sketch as the only reference. About 13 minutes end to end.
|
|
197
|
+
|
|
198
|
+
The prompt (sent in Chinese): draw "Lighthouse and Homecoming" on a 4:3 canvas — a red-and-white lighthouse on a rocky cape casting a fan of light, an orange-sailed fishing boat heading home with a V-shaped wake, a half-risen sun, three cloud bands, three gulls and surf around the rocks, in six named layers; then turn the sketch into a warm watercolor travel poster with no text, keeping the composition.
|
|
191
199
|
|
|
192
200
|
<details>
|
|
193
|
-
<summary>Advanced examples ·
|
|
201
|
+
<summary>Advanced examples · Mechanical cutaway and blue-green landscape (Sol Xhigh vs Astra Low)</summary>
|
|
194
202
|
|
|
195
|
-
**
|
|
203
|
+
Both subjects were chosen to expose quality differences: a mechanical cutaway tests structure and detail (meshing gears, pendulum, chains, tiny figures on a staircase), and a Song-dynasty blue-green landscape tests composition and taste (the "three distances", empty space, texture strokes and mineral color). The same prompt went to **GPT-6.1-Sol · Xhigh** and **GPT-6-Astra · Low**, one run each, on the official desktop app, with GPT Image 2 (quality: high).
|
|
196
204
|
|
|
197
|
-
**
|
|
205
|
+
**Clock Tower Cutaway**
|
|
198
206
|
|
|
199
207
|
<table>
|
|
200
|
-
<tr><th width="
|
|
201
|
-
<tr><td><img src="docs/assets/sketch-
|
|
208
|
+
<tr><th></th><th width="45%">Native sketch</th><th width="45%">Generated image</th></tr>
|
|
209
|
+
<tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-clocktower-sol-sketch.webp" alt="Clock tower sketch by Sol" width="400"></td><td><img src="docs/assets/sketch-clocktower-sol-result.webp" alt="Clock tower illustration from Sol's sketch" width="400"></td></tr>
|
|
210
|
+
<tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-clocktower-astra-sketch.webp" alt="Clock tower sketch by Astra" width="400"></td><td><img src="docs/assets/sketch-clocktower-astra-result.webp" alt="Clock tower illustration from Astra's sketch" width="400"></td></tr>
|
|
202
211
|
</table>
|
|
203
212
|
|
|
204
|
-
|
|
205
|
-
<summary>Show the reproduction prompt and original image request</summary>
|
|
206
|
-
|
|
207
|
-
**Sketch reproduction prompt (reconstructed from the artwork, not the original conversation)**
|
|
213
|
+
Sol first computed tooth counts (48, 38, 27, 46, 36, 24, 34, 30, 22) and pitch circles for the nine gears with a script, then placed them in mesh; drawing took about an hour. Its first image request failed; it asked, was allowed one retry, and succeeded. Astra Low finished sketch and image in about 5 minutes, with a simpler structure and gears that are not individually calibrated.
|
|
208
214
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
```text
|
|
212
|
-
@sketch 用 4:3 横版画板绘制《画布背面有人》:中央偏上是一处撕开的纸洞,洞内是深蓝星空和一位拿颜料桶的小画师;蓝色颜料从桶中流出,形成 S 形河流,流向下方城市。左侧城市保持未上色线稿,右侧城市被暖色点亮,加入纸船与飞鸟。按纸面、洞内世界、颜料河流、城市、画师和细节分层绘制,保留原生可编辑笔画。
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
**Actual image-generation prompt (original Chinese)**
|
|
216
|
-
|
|
217
|
-
```text
|
|
218
|
-
请基于本条附加草图实际调用订阅图片工具一次,生成成品插画。quality=low,模型使用当前默认,不切换型号,不额外生成。主题《画布背面有人》:保留4:3横构图、中央偏上的撕纸洞口、洞内拿颜料桶的小画师、流出成为S形河流的蓝色颜料、下方左侧未上色城市与右侧被点亮城市、纸船飞鸟。精修为惊艳的立体纸艺与精细手绘结合的编辑插画,纸张纤维、真实撕边及柔和投影,深靛蓝洞内星月,丰富青蓝颜料层次和流动质感,赭橙画师与暖色建筑,微小清晰的叙事细节。不重构为风景,不添加文字水印。必须使用本条参考图片编辑,不能仅凭文字生成。生成后简短说明完成即可。
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
</details>
|
|
222
|
-
|
|
223
|
-
Original example released in: [Beta v2.1.0-beta.2](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.2)
|
|
224
|
-
|
|
225
|
-
**Mona Lisa: Astra sketch → GPT image generation**
|
|
226
|
-
|
|
227
|
-
Example version: [2.1.0-beta.5](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.5)
|
|
228
|
-
|
|
229
|
-
Actual results supplied by the user from another computer: Astra draws on a portrait canvas, then GPT image generation turns the sketch into an oil painting.
|
|
215
|
+
**A Thousand Green Peaks**
|
|
230
216
|
|
|
231
217
|
<table>
|
|
232
|
-
<tr><th width="
|
|
233
|
-
<tr><td><img src="docs/assets/sketch-
|
|
218
|
+
<tr><th></th><th width="45%">Native sketch</th><th width="45%">Generated image</th></tr>
|
|
219
|
+
<tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-shanshui-sol-sketch.webp" alt="Landscape sketch by Sol" width="400"></td><td><img src="docs/assets/sketch-shanshui-sol-result.webp" alt="Blue-green landscape from Sol's sketch" width="400"></td></tr>
|
|
220
|
+
<tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-shanshui-astra-sketch.webp" alt="Landscape sketch by Astra" width="400"></td><td><img src="docs/assets/sketch-shanshui-astra-result.webp" alt="Blue-green landscape from Astra's sketch" width="400"></td></tr>
|
|
234
221
|
</table>
|
|
235
222
|
|
|
236
|
-
|
|
223
|
+
Sol Xhigh took about 28 minutes and Astra Low about 5. Both kept the main peak, waterfall, plank path and river in place; Sol's receding ranges and mist bands are more complete.
|
|
224
|
+
|
|
225
|
+
The full Chinese prompts for both subjects are in the [Chinese README](README.md#从草图到作品).
|
|
237
226
|
|
|
238
|
-
|
|
227
|
+
Each example is a single run, not a measure of consistent quality. Sol Xhigh is much slower; Astra or a lower reasoning level is enough for everyday sketches.
|
|
239
228
|
|
|
240
229
|
</details>
|
|
241
230
|
|
package/README.md
CHANGED
|
@@ -16,13 +16,19 @@
|
|
|
16
16
|
[](LICENSE)
|
|
17
17
|
[](https://github.com/WSL043/dsh-codex-subscription/stargazers)
|
|
18
18
|
|
|
19
|
-
[更新亮点](#
|
|
19
|
+
[更新亮点](#240-更新亮点) · [功能](#功能一览) · [安装](#安装) · [日常使用](#界面一览) · [作品案例](#从草图到作品) · [使用指南](docs/GUIDE.zh-CN.md) · [更新与卸载](#更新与卸载)
|
|
20
20
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<p align="center"><img src="docs/assets/product-account-demo.png" width="1100" alt="Codex 订阅账号与额度界面:模型选择、双周期额度与提醒;使用演示数据"></p>
|
|
24
24
|
|
|
25
|
-
## 2.
|
|
25
|
+
## 2.4.0 更新亮点
|
|
26
|
+
|
|
27
|
+
- **Agent 绘图更强(Beta)**:新增 SVG 绘图命令,模型可用最擅长的 SVG 一次画完一整层,导入后仍是可编辑、可撤销、可导出的原生笔画;渐变、越界等近似处理会明确回报,不支持的元素会点名拒绝。
|
|
28
|
+
- **全新作品案例**:在官方桌面版上用 GPT-6.1-Sol · Xhigh 与 GPT-6-Astra · Low 实测,机械剖面与青绿山水两道题并排对照,见[从草图到作品](#从草图到作品)。
|
|
29
|
+
- **桌面版修复**:官方桌面版上 Codex 子任务组件现在能正确识别宿主版本并安装;支持诊断会报告真实的 DSH 版本;图片请求网络失败时会说明是超时还是连接中断。
|
|
30
|
+
|
|
31
|
+
### 2.3.0
|
|
26
32
|
|
|
27
33
|
- **官方桌面端可用**:DeepSeek Harness 官方桌面版(0.2.0-rc.2)已完成实机验收——安装插件、登录、模型列表、隐藏模型、带工具调用的对话、图片生成均通过;安装方式见[安装](#安装)。
|
|
28
34
|
- **模型只来自你的账号**:模型列表完全读取账号目录,登录前或读取失败时不显示任何模型,也没有内置兜底;“模型与运行”里可逐个显示 / 隐藏输入框中的模型。
|
|
@@ -79,7 +85,7 @@
|
|
|
79
85
|
| **笔画平滑与导航** | 抬笔后平滑整条路径;拖动、缩放、撤销重做 | 快捷键可自定义和关闭 |
|
|
80
86
|
| **多份草稿** | 保存并重新编辑最多 20 份本地草稿 | 保存在当前浏览器 |
|
|
81
87
|
| **导入与导出** | 导出 PNG、分层 PSD,或交换保留原生笔画的草稿文件 | PSD 导入仅支持有限的普通像素图层 |
|
|
82
|
-
| **Agent 绘图** | 用 `@sketch` 让 Agent
|
|
88
|
+
| **Agent 绘图** | 用 `@sketch` 让 Agent 绘制,查看进度并随时停止;Agent 可用 SVG 整段作画,导入为可编辑原生笔画 | Beta;默认关闭,需同时开启画板 |
|
|
83
89
|
| **绘图结果回看** | 完成后向模型返回画布预览,帮助后续检查 | Beta;默认关闭,会增加图片输入用量 |
|
|
84
90
|
|
|
85
91
|
### 进阶能力与维护
|
|
@@ -113,7 +119,7 @@
|
|
|
113
119
|
2. 在 **包名或地址** 输入框中粘贴下面的包名(已带版本号):
|
|
114
120
|
|
|
115
121
|
```text
|
|
116
|
-
dsh-codex-subscription@2.
|
|
122
|
+
dsh-codex-subscription@2.4.0
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
3. 点击 **安装**,等待安装完成;按页面提示操作,需要重启时先保存工作。
|
|
@@ -183,59 +189,55 @@ dsh --profile headless "只回复:ok"
|
|
|
183
189
|
手动画图时点击画板按钮;让 Agent 绘图时发送 `@sketch` 加绘图要求。
|
|
184
190
|
|
|
185
191
|
<table>
|
|
186
|
-
<tr><th width="50%"
|
|
187
|
-
<tr><td><img src="docs/assets/sketch-
|
|
192
|
+
<tr><th width="50%">Agent 绘制的原生草图</th><th width="50%">以草图为唯一参考的生图结果</th></tr>
|
|
193
|
+
<tr><td><img src="docs/assets/sketch-lighthouse-sketch.webp" alt="GPT-6.1-Sol 绘制的灯塔与归航草图" width="480"></td><td><img src="docs/assets/sketch-lighthouse-result.webp" alt="根据草图生成的灯塔水彩海报" width="480"></td></tr>
|
|
188
194
|
</table>
|
|
189
195
|
|
|
190
|
-
|
|
196
|
+
**GPT-6.1-Sol · Xhigh**,官方桌面版 0.2.0-rc.2 实测:一条 `@sketch` 消息完成 6 个图层、229 笔可编辑原生笔画,再以这张草图为唯一参考调用一次 GPT Image 2(质量:高),全程约 13 分钟。
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
@sketch 用 4:3 横版画板绘制《灯塔与归航》:左侧礁石岬角上立一座红白条纹灯塔,塔顶灯室射出扇形光束;右侧海面上一艘挂橙色小帆的渔船正驶回港湾,船后拖出 V 形水痕;地平线上是半个初升的太阳,天空有三条横向云带和三只海鸥,前景礁石间有一圈白色浪花。按天空、太阳与云、海面、礁石与灯塔、渔船、海鸥与浪花分层绘制,保留原生可编辑笔画。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持构图不变,做成温暖的水彩旅行海报,晨光金橙配靛青海面,有纸纹和柔和晕染,不添加文字。
|
|
200
|
+
```
|
|
191
201
|
|
|
192
202
|
<details>
|
|
193
|
-
<summary>进阶展示 ·
|
|
203
|
+
<summary>进阶展示 · 机械剖面与青绿山水(Sol Xhigh 与 Astra Low 对照)</summary>
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
两道题都选了容易暴露差距的题材:机械剖面考结构与细节(齿轮咬合、摆锤、链条、楼梯上的人物),青绿山水考构图与审美(三远法、留白、皴法与设色)。同一条提示词分别交给 **GPT-6.1-Sol · Xhigh** 和 **GPT-6-Astra · Low**,各跑一次,均在官方桌面版上完成,生图统一为 GPT Image 2(质量:高)。
|
|
196
206
|
|
|
197
|
-
|
|
207
|
+
**《钟塔剖面图》**
|
|
198
208
|
|
|
199
209
|
<table>
|
|
200
|
-
<tr><th width="
|
|
201
|
-
<tr><td><img src="docs/assets/sketch-
|
|
210
|
+
<tr><th></th><th width="45%">原生草图</th><th width="45%">生图结果</th></tr>
|
|
211
|
+
<tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-clocktower-sol-sketch.webp" alt="Sol 绘制的钟塔剖面草图" width="400"></td><td><img src="docs/assets/sketch-clocktower-sol-result.webp" alt="Sol 草图生成的钟塔剖面插画" width="400"></td></tr>
|
|
212
|
+
<tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-clocktower-astra-sketch.webp" alt="Astra 绘制的钟塔剖面草图" width="400"></td><td><img src="docs/assets/sketch-clocktower-astra-result.webp" alt="Astra 草图生成的钟塔剖面插画" width="400"></td></tr>
|
|
202
213
|
</table>
|
|
203
214
|
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
Sol 先用脚本算出 9 个齿轮的齿数(48、38、27、46、36、24、34、30、22)与节圆,再按咬合位置排布,绘图约 1 小时;第一次生图请求失败,按它的询问同意重试一次后成功。Astra Low 约 5 分钟完成全部草图与生图,结构更简化,齿轮没有逐一校准。
|
|
216
|
+
|
|
217
|
+
**《千峰翠色》**
|
|
218
|
+
|
|
219
|
+
<table>
|
|
220
|
+
<tr><th></th><th width="45%">原生草图</th><th width="45%">生图结果</th></tr>
|
|
221
|
+
<tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-shanshui-sol-sketch.webp" alt="Sol 绘制的青绿山水草图" width="400"></td><td><img src="docs/assets/sketch-shanshui-sol-result.webp" alt="Sol 草图生成的青绿山水" width="400"></td></tr>
|
|
222
|
+
<tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-shanshui-astra-sketch.webp" alt="Astra 绘制的青绿山水草图" width="400"></td><td><img src="docs/assets/sketch-shanshui-astra-result.webp" alt="Astra 草图生成的青绿山水" width="400"></td></tr>
|
|
223
|
+
</table>
|
|
206
224
|
|
|
207
|
-
|
|
225
|
+
Sol Xhigh 约 28 分钟,Astra Low 约 5 分钟。两者都保留了主峰、瀑布、栈道与江面的位置;Sol 的远山层次和雾带留白更完整。
|
|
208
226
|
|
|
209
|
-
|
|
227
|
+
<details>
|
|
228
|
+
<summary>展开两道题的完整提示词</summary>
|
|
210
229
|
|
|
211
230
|
```text
|
|
212
|
-
@sketch 用 4
|
|
231
|
+
@sketch 用 3:4 竖版画板绘制《钟塔剖面图》:蒸汽朋克风格的钟塔纵向剖面技术插图。最上方是圆形大钟面(十二个刻度、两根指针)和一口铜钟;钟面下一层是齿轮机房:至少 9 个大小不同、齿数正确、彼此咬合的齿轮(相邻齿轮转向相反,齿廓要画出锯齿),另有一个锚形擒纵机构;中段是一根贯穿的长摆杆,末端为铜制圆盘摆锤,旁边两条挂着配重铁块的链条;塔壁一侧是沿墙盘旋而上的螺旋楼梯,梯上有三个微小的工匠人物;底部是石砌地基与拱门。剖面用厚实的暗色砖石墙体框出,内部露出黄铜与铁质结构,加入铆钉、管道,以及从小窗透进来的光柱。按背景砖墙、结构框架、齿轮组、摆锤与链条、楼梯与人物、光影与细节分层绘制。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持每个部件的位置、比例与咬合关系不变,做成精密的蒸汽朋克剖面图插画——黄铜与旧钢的金属质感、细致的机械倒影和铆钉、羊皮纸底色配墨线渲染、暖色灯光,不添加文字。
|
|
213
232
|
```
|
|
214
233
|
|
|
215
|
-
**实际生图提示词**
|
|
216
|
-
|
|
217
234
|
```text
|
|
218
|
-
|
|
235
|
+
@sketch 用 3:4 竖版画板绘制《千峰翠色》:宋代青绿山水的构图,严格运用「三远法」。高远:右侧一座主峰拔地而起,占画面上半部,山体用层叠的块面表现,山腰有横向的云雾留白;深远:主峰后方还有两重淡青色远山,越远越淡,之间隔着大片留白的雾带;平远:下方是舒展的江面,一叶渔舟和两只飞鸟。左侧一挂瀑布从中段峭壁倾泻而下,落入山涧,涧边有三株松树,松针成簇;山腰有一座小亭和一条曲折栈道,栈道上有两个极小的行者;前景是坡石与芦苇,用勾勒加披麻皴式的笔触表现。色彩层次为石青、石绿、赭石,远山用淡青。按远山与天空、雾带留白、主峰、瀑布与松树、亭台栈道与行者、江面舟鸟与前景分层绘制。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持山体、瀑布、栈道、江面的位置和三远法的层次不变,做成宋代青绿山水绢本设色——石青石绿矿物颜料的沉着厚重、细腻的皴擦与勾勒、绢面纹理与微微泛黄的古意、云雾留白,不添加文字或印章。
|
|
219
236
|
```
|
|
220
237
|
|
|
221
238
|
</details>
|
|
222
239
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
**《蒙娜丽莎》:Astra 草图 → GPT 生图**
|
|
226
|
-
|
|
227
|
-
案例版本:[2.1.0-beta.5](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.5)
|
|
228
|
-
|
|
229
|
-
用户在另一台电脑上的实际效果:先让 Astra 在竖版画板上绘制,再通过 GPT 生图转成油画。
|
|
230
|
-
|
|
231
|
-
<table>
|
|
232
|
-
<tr><th width="50%">Astra 原生草图</th><th width="50%">GPT 生图:油画效果</th></tr>
|
|
233
|
-
<tr><td><img src="docs/assets/sketch-mona-lisa-source.png" alt="Astra 绘制的蒙娜丽莎草图" width="480"></td><td><img src="docs/assets/sketch-mona-lisa-result.png" alt="草图转换后的蒙娜丽莎油画" width="480"></td></tr>
|
|
234
|
-
</table>
|
|
235
|
-
|
|
236
|
-
草图提示词:`@sketch 用竖版画板画一幅《蒙娜丽莎》`
|
|
237
|
-
|
|
238
|
-
生图提示词:`帮我变成油画`
|
|
240
|
+
每个例子只跑一次,不代表稳定水平;Sol Xhigh 耗时明显更长,日常草图用 Astra 或较低推理档即可。
|
|
239
241
|
|
|
240
242
|
</details>
|
|
241
243
|
|
package/lib/client.js
CHANGED
|
@@ -2321,6 +2321,796 @@ window.__ModuleLoader__.load({
|
|
|
2321
2321
|
}))
|
|
2322
2322
|
};
|
|
2323
2323
|
}
|
|
2324
|
+
//#endregion
|
|
2325
|
+
//#region src/sketch-svg.js
|
|
2326
|
+
const MAX_SVG_CHARACTERS = 2e5;
|
|
2327
|
+
const MAX_BEZIER_SEGMENTS = 64;
|
|
2328
|
+
const FLATTEN_STEPS = 10;
|
|
2329
|
+
const NAMED_COLORS = {
|
|
2330
|
+
black: "#000000",
|
|
2331
|
+
white: "#ffffff",
|
|
2332
|
+
red: "#ff0000",
|
|
2333
|
+
green: "#008000",
|
|
2334
|
+
blue: "#0000ff",
|
|
2335
|
+
yellow: "#ffff00",
|
|
2336
|
+
orange: "#ffa500",
|
|
2337
|
+
purple: "#800080",
|
|
2338
|
+
pink: "#ffc0cb",
|
|
2339
|
+
brown: "#a52a2a",
|
|
2340
|
+
gray: "#808080",
|
|
2341
|
+
grey: "#808080",
|
|
2342
|
+
silver: "#c0c0c0",
|
|
2343
|
+
gold: "#ffd700",
|
|
2344
|
+
navy: "#000080",
|
|
2345
|
+
teal: "#008080",
|
|
2346
|
+
maroon: "#800000",
|
|
2347
|
+
olive: "#808000",
|
|
2348
|
+
lime: "#00ff00",
|
|
2349
|
+
aqua: "#00ffff",
|
|
2350
|
+
cyan: "#00ffff",
|
|
2351
|
+
magenta: "#ff00ff",
|
|
2352
|
+
fuchsia: "#ff00ff",
|
|
2353
|
+
ivory: "#fffff0",
|
|
2354
|
+
beige: "#f5f5dc",
|
|
2355
|
+
tan: "#d2b48c",
|
|
2356
|
+
khaki: "#f0e68c",
|
|
2357
|
+
coral: "#ff7f50",
|
|
2358
|
+
salmon: "#fa8072",
|
|
2359
|
+
crimson: "#dc143c",
|
|
2360
|
+
indigo: "#4b0082",
|
|
2361
|
+
violet: "#ee82ee",
|
|
2362
|
+
turquoise: "#40e0d0",
|
|
2363
|
+
skyblue: "#87ceeb",
|
|
2364
|
+
darkgray: "#a9a9a9",
|
|
2365
|
+
darkgrey: "#a9a9a9",
|
|
2366
|
+
lightgray: "#d3d3d3",
|
|
2367
|
+
lightgrey: "#d3d3d3",
|
|
2368
|
+
darkred: "#8b0000",
|
|
2369
|
+
darkgreen: "#006400",
|
|
2370
|
+
darkblue: "#00008b",
|
|
2371
|
+
transparent: "none"
|
|
2372
|
+
};
|
|
2373
|
+
const UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
2374
|
+
"text",
|
|
2375
|
+
"tspan",
|
|
2376
|
+
"image",
|
|
2377
|
+
"foreignObject",
|
|
2378
|
+
"filter",
|
|
2379
|
+
"clipPath",
|
|
2380
|
+
"mask",
|
|
2381
|
+
"pattern",
|
|
2382
|
+
"style",
|
|
2383
|
+
"script",
|
|
2384
|
+
"switch",
|
|
2385
|
+
"marker",
|
|
2386
|
+
"symbol"
|
|
2387
|
+
]);
|
|
2388
|
+
const IGNORED = /* @__PURE__ */ new Set([
|
|
2389
|
+
"title",
|
|
2390
|
+
"desc",
|
|
2391
|
+
"metadata",
|
|
2392
|
+
"defs",
|
|
2393
|
+
"linearGradient",
|
|
2394
|
+
"radialGradient",
|
|
2395
|
+
"stop",
|
|
2396
|
+
"namedview",
|
|
2397
|
+
"sodipodi:namedview"
|
|
2398
|
+
]);
|
|
2399
|
+
const decode = (value) => value.split(""").join("\"").split("'").join("'").split("<").join("<").split(">").join(">").split("&").join("&");
|
|
2400
|
+
function parseXml(text) {
|
|
2401
|
+
const tag = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!\[CDATA\[[\s\S]*?\]\]>|<!DOCTYPE[^>]*>|<(\/?)([A-Za-z_][\w:.-]*)((?:\s+[^\s=/>]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/g;
|
|
2402
|
+
const attribute = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
2403
|
+
const root = {
|
|
2404
|
+
name: "#root",
|
|
2405
|
+
attrs: {},
|
|
2406
|
+
children: []
|
|
2407
|
+
};
|
|
2408
|
+
const stack = [root];
|
|
2409
|
+
for (let match = tag.exec(text); match; match = tag.exec(text)) {
|
|
2410
|
+
if (match[2] === void 0) continue;
|
|
2411
|
+
if (match[1] === "/") {
|
|
2412
|
+
if (stack.length > 1 && stack.at(-1).name === match[2]) stack.pop();
|
|
2413
|
+
else throw Error(`SVG is malformed: unexpected </${match[2]}>`);
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
const attrs = {};
|
|
2417
|
+
for (let item = attribute.exec(match[3]); item; item = attribute.exec(match[3])) attrs[item[1]] = decode(item[2] ?? item[3]);
|
|
2418
|
+
const node = {
|
|
2419
|
+
name: match[2],
|
|
2420
|
+
attrs,
|
|
2421
|
+
children: []
|
|
2422
|
+
};
|
|
2423
|
+
stack.at(-1).children.push(node);
|
|
2424
|
+
if (match[4] !== "/") stack.push(node);
|
|
2425
|
+
}
|
|
2426
|
+
if (stack.length !== 1) throw Error(`SVG is malformed: <${stack.at(-1).name}> is not closed`);
|
|
2427
|
+
return root;
|
|
2428
|
+
}
|
|
2429
|
+
const IDENTITY = [
|
|
2430
|
+
1,
|
|
2431
|
+
0,
|
|
2432
|
+
0,
|
|
2433
|
+
1,
|
|
2434
|
+
0,
|
|
2435
|
+
0
|
|
2436
|
+
];
|
|
2437
|
+
const multiply = (m, n) => [
|
|
2438
|
+
m[0] * n[0] + m[2] * n[1],
|
|
2439
|
+
m[1] * n[0] + m[3] * n[1],
|
|
2440
|
+
m[0] * n[2] + m[2] * n[3],
|
|
2441
|
+
m[1] * n[2] + m[3] * n[3],
|
|
2442
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
2443
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
2444
|
+
];
|
|
2445
|
+
const apply$1 = (m, x, y) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
|
|
2446
|
+
const NUMBER = /[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g;
|
|
2447
|
+
const numbers = (text) => (String(text ?? "").match(NUMBER) ?? []).map(Number);
|
|
2448
|
+
function parseTransform(text) {
|
|
2449
|
+
let matrix = IDENTITY;
|
|
2450
|
+
const source = String(text ?? "");
|
|
2451
|
+
const item = /(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)/g;
|
|
2452
|
+
if (source.replace(item, "").replace(/[\s,]/g, "")) throw Error(`Unsupported SVG transform: ${source}`);
|
|
2453
|
+
for (let match = item.exec(source); match; match = item.exec(source)) {
|
|
2454
|
+
const v = numbers(match[2]);
|
|
2455
|
+
let next;
|
|
2456
|
+
if (match[1] === "matrix" && v.length === 6) next = v;
|
|
2457
|
+
else if (match[1] === "translate" && v.length >= 1) next = [
|
|
2458
|
+
1,
|
|
2459
|
+
0,
|
|
2460
|
+
0,
|
|
2461
|
+
1,
|
|
2462
|
+
v[0],
|
|
2463
|
+
v[1] ?? 0
|
|
2464
|
+
];
|
|
2465
|
+
else if (match[1] === "scale" && v.length >= 1) next = [
|
|
2466
|
+
v[0],
|
|
2467
|
+
0,
|
|
2468
|
+
0,
|
|
2469
|
+
v[1] ?? v[0],
|
|
2470
|
+
0,
|
|
2471
|
+
0
|
|
2472
|
+
];
|
|
2473
|
+
else if (match[1] === "rotate" && v.length >= 1) {
|
|
2474
|
+
const a = v[0] * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);
|
|
2475
|
+
const turn = [
|
|
2476
|
+
c,
|
|
2477
|
+
s,
|
|
2478
|
+
-s,
|
|
2479
|
+
c,
|
|
2480
|
+
0,
|
|
2481
|
+
0
|
|
2482
|
+
];
|
|
2483
|
+
next = v.length >= 3 ? multiply(multiply([
|
|
2484
|
+
1,
|
|
2485
|
+
0,
|
|
2486
|
+
0,
|
|
2487
|
+
1,
|
|
2488
|
+
v[1],
|
|
2489
|
+
v[2]
|
|
2490
|
+
], turn), [
|
|
2491
|
+
1,
|
|
2492
|
+
0,
|
|
2493
|
+
0,
|
|
2494
|
+
1,
|
|
2495
|
+
-v[1],
|
|
2496
|
+
-v[2]
|
|
2497
|
+
]) : turn;
|
|
2498
|
+
} else if (match[1] === "skewX" && v.length === 1) next = [
|
|
2499
|
+
1,
|
|
2500
|
+
0,
|
|
2501
|
+
Math.tan(v[0] * Math.PI / 180),
|
|
2502
|
+
1,
|
|
2503
|
+
0,
|
|
2504
|
+
0
|
|
2505
|
+
];
|
|
2506
|
+
else if (match[1] === "skewY" && v.length === 1) next = [
|
|
2507
|
+
1,
|
|
2508
|
+
Math.tan(v[0] * Math.PI / 180),
|
|
2509
|
+
0,
|
|
2510
|
+
1,
|
|
2511
|
+
0,
|
|
2512
|
+
0
|
|
2513
|
+
];
|
|
2514
|
+
else throw Error(`Unsupported SVG transform: ${match[0]}`);
|
|
2515
|
+
matrix = multiply(matrix, next);
|
|
2516
|
+
}
|
|
2517
|
+
return matrix;
|
|
2518
|
+
}
|
|
2519
|
+
function parseColor(value) {
|
|
2520
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
2521
|
+
if (!text || text === "none") return "none";
|
|
2522
|
+
if (text === "currentcolor") return "#000000";
|
|
2523
|
+
if (Object.hasOwn(NAMED_COLORS, text)) return NAMED_COLORS[text];
|
|
2524
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(text);
|
|
2525
|
+
if (hex) return hex[1].length === 3 ? `#${[...hex[1]].map((ch) => ch + ch).join("")}` : text;
|
|
2526
|
+
const rgb = /^rgba?\(([^)]*)\)$/.exec(text);
|
|
2527
|
+
if (rgb) {
|
|
2528
|
+
const parts = rgb[1].split(/[\s,/]+/).filter(Boolean);
|
|
2529
|
+
const channel = (part) => Math.max(0, Math.min(255, Math.round(part.endsWith("%") ? parseFloat(part) * 2.55 : parseFloat(part))));
|
|
2530
|
+
if (parts.length >= 3) return `#${parts.slice(0, 3).map((part) => channel(part).toString(16).padStart(2, "0")).join("")}`;
|
|
2531
|
+
}
|
|
2532
|
+
throw Error(`Unsupported SVG color: ${value}. Use #rrggbb, #rgb, rgb(), a basic color name, none, or a gradient id.`);
|
|
2533
|
+
}
|
|
2534
|
+
function styleOf(node, parent) {
|
|
2535
|
+
const own = { ...node.attrs };
|
|
2536
|
+
if (own.style) for (const rule of own.style.split(";")) {
|
|
2537
|
+
const at = rule.indexOf(":");
|
|
2538
|
+
if (at > 0) own[rule.slice(0, at).trim()] = rule.slice(at + 1).trim();
|
|
2539
|
+
}
|
|
2540
|
+
const pick = (name, fallback) => own[name] === void 0 || own[name] === "inherit" ? fallback : own[name];
|
|
2541
|
+
const number = (name, fallback) => {
|
|
2542
|
+
const value = pick(name, void 0);
|
|
2543
|
+
return value === void 0 ? fallback : Number.parseFloat(value);
|
|
2544
|
+
};
|
|
2545
|
+
return {
|
|
2546
|
+
fill: pick("fill", parent.fill),
|
|
2547
|
+
stroke: pick("stroke", parent.stroke),
|
|
2548
|
+
strokeWidth: number("stroke-width", parent.strokeWidth),
|
|
2549
|
+
fillOpacity: number("fill-opacity", parent.fillOpacity),
|
|
2550
|
+
strokeOpacity: number("stroke-opacity", parent.strokeOpacity),
|
|
2551
|
+
opacity: parent.opacity * number("opacity", 1),
|
|
2552
|
+
display: own.display === "none" || own.visibility === "hidden" ? "none" : "inline"
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
function arcToCubics(x1, y1, rx, ry, rotation, large, sweep, x2, y2) {
|
|
2556
|
+
if (x1 === x2 && y1 === y2) return [];
|
|
2557
|
+
rx = Math.abs(rx);
|
|
2558
|
+
ry = Math.abs(ry);
|
|
2559
|
+
if (!rx || !ry) return [[
|
|
2560
|
+
x1,
|
|
2561
|
+
y1,
|
|
2562
|
+
x2,
|
|
2563
|
+
y2,
|
|
2564
|
+
x2,
|
|
2565
|
+
y2
|
|
2566
|
+
]];
|
|
2567
|
+
const phi = rotation * Math.PI / 180, cos = Math.cos(phi), sin = Math.sin(phi);
|
|
2568
|
+
const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
|
|
2569
|
+
const x1p = cos * dx + sin * dy, y1p = -sin * dx + cos * dy;
|
|
2570
|
+
const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
2571
|
+
if (lambda > 1) {
|
|
2572
|
+
rx *= Math.sqrt(lambda);
|
|
2573
|
+
ry *= Math.sqrt(lambda);
|
|
2574
|
+
}
|
|
2575
|
+
const sign = large === sweep ? -1 : 1;
|
|
2576
|
+
const numerator = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
2577
|
+
const denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
2578
|
+
const factor = sign * Math.sqrt(Math.max(0, numerator / denominator));
|
|
2579
|
+
const cxp = factor * rx * y1p / ry, cyp = -factor * ry * x1p / rx;
|
|
2580
|
+
const cx = cos * cxp - sin * cyp + (x1 + x2) / 2, cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
|
|
2581
|
+
const angle = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
2582
|
+
const theta = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
2583
|
+
let delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
2584
|
+
if (!sweep && delta > 0) delta -= 2 * Math.PI;
|
|
2585
|
+
if (sweep && delta < 0) delta += 2 * Math.PI;
|
|
2586
|
+
const count = Math.max(1, Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9));
|
|
2587
|
+
const step = delta / count, k = 4 / 3 * Math.tan(step / 4);
|
|
2588
|
+
const out = [];
|
|
2589
|
+
for (let index = 0; index < count; index++) {
|
|
2590
|
+
const a1 = theta + index * step, a2 = a1 + step;
|
|
2591
|
+
const point = (a, scale) => {
|
|
2592
|
+
const px = rx * Math.cos(a), py = ry * Math.sin(a);
|
|
2593
|
+
const tx = -rx * Math.sin(a) * scale, ty = ry * Math.cos(a) * scale;
|
|
2594
|
+
return [
|
|
2595
|
+
cos * px - sin * py + cx,
|
|
2596
|
+
sin * px + cos * py + cy,
|
|
2597
|
+
cos * tx - sin * ty,
|
|
2598
|
+
sin * tx + cos * ty
|
|
2599
|
+
];
|
|
2600
|
+
};
|
|
2601
|
+
const p1 = point(a1, k), p2 = point(a2, k);
|
|
2602
|
+
out.push([
|
|
2603
|
+
p1[0] + p1[2],
|
|
2604
|
+
p1[1] + p1[3],
|
|
2605
|
+
p2[0] - p2[2],
|
|
2606
|
+
p2[1] - p2[3],
|
|
2607
|
+
p2[0],
|
|
2608
|
+
p2[1]
|
|
2609
|
+
]);
|
|
2610
|
+
}
|
|
2611
|
+
return out;
|
|
2612
|
+
}
|
|
2613
|
+
function parsePathData(data) {
|
|
2614
|
+
const source = String(data ?? "");
|
|
2615
|
+
const subpaths = [];
|
|
2616
|
+
let index = 0, command = "", current, x = 0, y = 0, startX = 0, startY = 0, lastControl, lastCommand = "";
|
|
2617
|
+
const skip = () => {
|
|
2618
|
+
while (index < source.length && /[\s,]/.test(source[index])) index++;
|
|
2619
|
+
};
|
|
2620
|
+
const readNumber = () => {
|
|
2621
|
+
skip();
|
|
2622
|
+
NUMBER.lastIndex = index;
|
|
2623
|
+
const match = NUMBER.exec(source);
|
|
2624
|
+
if (!match || match.index !== index) throw Error(`Malformed SVG path data near "${source.slice(index, index + 12)}"`);
|
|
2625
|
+
index = NUMBER.lastIndex;
|
|
2626
|
+
return Number(match[0]);
|
|
2627
|
+
};
|
|
2628
|
+
const readFlag = () => {
|
|
2629
|
+
skip();
|
|
2630
|
+
const ch = source[index];
|
|
2631
|
+
if (ch !== "0" && ch !== "1") throw Error("Malformed SVG arc flag in path data");
|
|
2632
|
+
index++;
|
|
2633
|
+
return ch === "1";
|
|
2634
|
+
};
|
|
2635
|
+
const begin = (px, py) => {
|
|
2636
|
+
current = {
|
|
2637
|
+
start: [px, py],
|
|
2638
|
+
curves: [],
|
|
2639
|
+
closed: false,
|
|
2640
|
+
straight: true
|
|
2641
|
+
};
|
|
2642
|
+
subpaths.push(current);
|
|
2643
|
+
};
|
|
2644
|
+
const line = (px, py) => {
|
|
2645
|
+
current.curves.push([
|
|
2646
|
+
x,
|
|
2647
|
+
y,
|
|
2648
|
+
px,
|
|
2649
|
+
py,
|
|
2650
|
+
px,
|
|
2651
|
+
py
|
|
2652
|
+
]);
|
|
2653
|
+
x = px;
|
|
2654
|
+
y = py;
|
|
2655
|
+
};
|
|
2656
|
+
const cubic = (c1x, c1y, c2x, c2y, px, py) => {
|
|
2657
|
+
current.curves.push([
|
|
2658
|
+
c1x,
|
|
2659
|
+
c1y,
|
|
2660
|
+
c2x,
|
|
2661
|
+
c2y,
|
|
2662
|
+
px,
|
|
2663
|
+
py
|
|
2664
|
+
]);
|
|
2665
|
+
current.straight = false;
|
|
2666
|
+
lastControl = [c2x, c2y];
|
|
2667
|
+
x = px;
|
|
2668
|
+
y = py;
|
|
2669
|
+
};
|
|
2670
|
+
for (;;) {
|
|
2671
|
+
skip();
|
|
2672
|
+
if (index >= source.length) break;
|
|
2673
|
+
if (/[MmLlHhVvCcSsQqTtAaZz]/.test(source[index])) command = source[index++];
|
|
2674
|
+
else if (!command) throw Error("SVG path data must start with a command");
|
|
2675
|
+
else if (command === "M") command = "L";
|
|
2676
|
+
else if (command === "m") command = "l";
|
|
2677
|
+
else if (command === "Z" || command === "z") throw Error("Unexpected number after Z in SVG path data");
|
|
2678
|
+
const relative = command === command.toLowerCase();
|
|
2679
|
+
const upper = command.toUpperCase();
|
|
2680
|
+
const ox = relative ? x : 0, oy = relative ? y : 0;
|
|
2681
|
+
if (upper === "M") {
|
|
2682
|
+
const px = readNumber() + ox, py = readNumber() + oy;
|
|
2683
|
+
x = startX = px;
|
|
2684
|
+
y = startY = py;
|
|
2685
|
+
begin(px, py);
|
|
2686
|
+
lastControl = void 0;
|
|
2687
|
+
} else if (upper === "Z") {
|
|
2688
|
+
if (current && !current.closed) {
|
|
2689
|
+
if (x !== startX || y !== startY) line(startX, startY);
|
|
2690
|
+
current.closed = true;
|
|
2691
|
+
}
|
|
2692
|
+
x = startX;
|
|
2693
|
+
y = startY;
|
|
2694
|
+
lastControl = void 0;
|
|
2695
|
+
current = void 0;
|
|
2696
|
+
} else {
|
|
2697
|
+
if (!current) {
|
|
2698
|
+
begin(x, y);
|
|
2699
|
+
startX = x;
|
|
2700
|
+
startY = y;
|
|
2701
|
+
}
|
|
2702
|
+
if (upper === "L") line(readNumber() + ox, readNumber() + oy);
|
|
2703
|
+
else if (upper === "H") line(readNumber() + ox, y);
|
|
2704
|
+
else if (upper === "V") line(x, readNumber() + oy);
|
|
2705
|
+
else if (upper === "C") cubic(readNumber() + ox, readNumber() + oy, readNumber() + ox, readNumber() + oy, readNumber() + ox, readNumber() + oy);
|
|
2706
|
+
else if (upper === "S") {
|
|
2707
|
+
const [rx, ry] = lastControl && lastCommand && "CScs".includes(lastCommand) ? [2 * x - lastControl[0], 2 * y - lastControl[1]] : [x, y];
|
|
2708
|
+
cubic(rx, ry, readNumber() + ox, readNumber() + oy, readNumber() + ox, readNumber() + oy);
|
|
2709
|
+
} else if (upper === "Q" || upper === "T") {
|
|
2710
|
+
let qx, qy;
|
|
2711
|
+
if (upper === "Q") {
|
|
2712
|
+
qx = readNumber() + ox;
|
|
2713
|
+
qy = readNumber() + oy;
|
|
2714
|
+
} else [qx, qy] = lastControl && lastCommand && "QTqt".includes(lastCommand) ? [2 * x - lastControl[0], 2 * y - lastControl[1]] : [x, y];
|
|
2715
|
+
const px = readNumber() + ox, py = readNumber() + oy;
|
|
2716
|
+
const from = [x, y];
|
|
2717
|
+
cubic(from[0] + 2 / 3 * (qx - from[0]), from[1] + 2 / 3 * (qy - from[1]), px + 2 / 3 * (qx - px), py + 2 / 3 * (qy - py), px, py);
|
|
2718
|
+
lastControl = [qx, qy];
|
|
2719
|
+
} else if (upper === "A") {
|
|
2720
|
+
const rx = readNumber(), ry = readNumber(), rotation = readNumber(), large = readFlag(), sweep = readFlag();
|
|
2721
|
+
const px = readNumber() + ox, py = readNumber() + oy;
|
|
2722
|
+
const pieces = arcToCubics(x, y, rx, ry, rotation, large, sweep, px, py);
|
|
2723
|
+
for (const piece of pieces) cubic(...piece);
|
|
2724
|
+
if (!pieces.length) {
|
|
2725
|
+
x = px;
|
|
2726
|
+
y = py;
|
|
2727
|
+
}
|
|
2728
|
+
lastControl = void 0;
|
|
2729
|
+
} else throw Error(`Unsupported SVG path command: ${command}`);
|
|
2730
|
+
}
|
|
2731
|
+
lastCommand = command;
|
|
2732
|
+
}
|
|
2733
|
+
return subpaths.filter((subpath) => subpath.curves.length > 0);
|
|
2734
|
+
}
|
|
2735
|
+
const ellipsePath = (cx, cy, rx, ry) => {
|
|
2736
|
+
const k = .5522847498;
|
|
2737
|
+
return {
|
|
2738
|
+
start: [cx + rx, cy],
|
|
2739
|
+
closed: true,
|
|
2740
|
+
straight: false,
|
|
2741
|
+
curves: [
|
|
2742
|
+
[
|
|
2743
|
+
cx + rx,
|
|
2744
|
+
cy + ry * k,
|
|
2745
|
+
cx + rx * k,
|
|
2746
|
+
cy + ry,
|
|
2747
|
+
cx,
|
|
2748
|
+
cy + ry
|
|
2749
|
+
],
|
|
2750
|
+
[
|
|
2751
|
+
cx - rx * k,
|
|
2752
|
+
cy + ry,
|
|
2753
|
+
cx - rx,
|
|
2754
|
+
cy + ry * k,
|
|
2755
|
+
cx - rx,
|
|
2756
|
+
cy
|
|
2757
|
+
],
|
|
2758
|
+
[
|
|
2759
|
+
cx - rx,
|
|
2760
|
+
cy - ry * k,
|
|
2761
|
+
cx - rx * k,
|
|
2762
|
+
cy - ry,
|
|
2763
|
+
cx,
|
|
2764
|
+
cy - ry
|
|
2765
|
+
],
|
|
2766
|
+
[
|
|
2767
|
+
cx + rx * k,
|
|
2768
|
+
cy - ry,
|
|
2769
|
+
cx + rx,
|
|
2770
|
+
cy - ry * k,
|
|
2771
|
+
cx + rx,
|
|
2772
|
+
cy
|
|
2773
|
+
]
|
|
2774
|
+
]
|
|
2775
|
+
};
|
|
2776
|
+
};
|
|
2777
|
+
const polylinePath = (points, closed) => {
|
|
2778
|
+
const path = {
|
|
2779
|
+
start: points[0],
|
|
2780
|
+
curves: [],
|
|
2781
|
+
closed,
|
|
2782
|
+
straight: true
|
|
2783
|
+
};
|
|
2784
|
+
let [x, y] = points[0];
|
|
2785
|
+
for (const [px, py] of points.slice(1)) {
|
|
2786
|
+
path.curves.push([
|
|
2787
|
+
x,
|
|
2788
|
+
y,
|
|
2789
|
+
px,
|
|
2790
|
+
py,
|
|
2791
|
+
px,
|
|
2792
|
+
py
|
|
2793
|
+
]);
|
|
2794
|
+
x = px;
|
|
2795
|
+
y = py;
|
|
2796
|
+
}
|
|
2797
|
+
if (closed && (x !== points[0][0] || y !== points[0][1])) path.curves.push([
|
|
2798
|
+
x,
|
|
2799
|
+
y,
|
|
2800
|
+
points[0][0],
|
|
2801
|
+
points[0][1],
|
|
2802
|
+
points[0][0],
|
|
2803
|
+
points[0][1]
|
|
2804
|
+
]);
|
|
2805
|
+
return path;
|
|
2806
|
+
};
|
|
2807
|
+
const roundedRectPath = (x, y, w, h, rx, ry) => {
|
|
2808
|
+
rx = Math.min(rx, w / 2);
|
|
2809
|
+
ry = Math.min(ry, h / 2);
|
|
2810
|
+
const k = .5522847498;
|
|
2811
|
+
const curves = [];
|
|
2812
|
+
let cx = x + rx, cy = y;
|
|
2813
|
+
const go = (...piece) => {
|
|
2814
|
+
curves.push(piece);
|
|
2815
|
+
cx = piece[4];
|
|
2816
|
+
cy = piece[5];
|
|
2817
|
+
};
|
|
2818
|
+
go(cx, cy, x + w - rx, y, x + w - rx, y);
|
|
2819
|
+
go(x + w - rx + rx * k, y, x + w, y + ry - ry * k, x + w, y + ry);
|
|
2820
|
+
go(x + w, y + ry, x + w, y + h - ry, x + w, y + h - ry);
|
|
2821
|
+
go(x + w, y + h - ry + ry * k, x + w - rx + rx * k, y + h, x + w - rx, y + h);
|
|
2822
|
+
go(x + w - rx, y + h, x + rx, y + h, x + rx, y + h);
|
|
2823
|
+
go(x + rx - rx * k, y + h, x, y + h - ry + ry * k, x, y + h - ry);
|
|
2824
|
+
go(x, y + h - ry, x, y + ry, x, y + ry);
|
|
2825
|
+
go(x, y + ry - ry * k, x + rx - rx * k, y, x + rx, y);
|
|
2826
|
+
return {
|
|
2827
|
+
start: [x + rx, y],
|
|
2828
|
+
curves,
|
|
2829
|
+
closed: true,
|
|
2830
|
+
straight: false
|
|
2831
|
+
};
|
|
2832
|
+
};
|
|
2833
|
+
function svgToSketchCommands(text, { width, height, layer, existingIds = [] }) {
|
|
2834
|
+
if (typeof text !== "string" || !text.trim()) throw Error("svg must be a non-empty SVG document");
|
|
2835
|
+
if (text.length > 2e5) throw Error(`svg is too large: ${text.length} characters, limit ${MAX_SVG_CHARACTERS}. Split it across several svg commands.`);
|
|
2836
|
+
const root = parseXml(text).children.find((node) => node.name === "svg");
|
|
2837
|
+
if (!root) throw Error("svg must contain an <svg> element");
|
|
2838
|
+
const box = numbers(root.attrs.viewBox);
|
|
2839
|
+
const viewWidth = box.length === 4 ? box[2] : Number.parseFloat(root.attrs.width);
|
|
2840
|
+
const viewHeight = box.length === 4 ? box[3] : Number.parseFloat(root.attrs.height);
|
|
2841
|
+
const originX = box.length === 4 ? box[0] : 0, originY = box.length === 4 ? box[1] : 0;
|
|
2842
|
+
if (!(viewWidth > 0 && viewHeight > 0)) throw Error("<svg> needs a viewBox=\"0 0 W H\" (or width and height)");
|
|
2843
|
+
if (Math.abs(viewWidth / viewHeight - width / height) > .01 * (width / height)) throw Error(`SVG viewBox ${viewWidth}x${viewHeight} does not match the ${width}x${height} sketch ratio. Use viewBox="0 0 ${width} ${height}".`);
|
|
2844
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2845
|
+
const index = (node) => {
|
|
2846
|
+
if (node.attrs.id) byId.set(node.attrs.id, node);
|
|
2847
|
+
node.children.forEach(index);
|
|
2848
|
+
};
|
|
2849
|
+
index(root);
|
|
2850
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
2851
|
+
const used = new Set(existingIds);
|
|
2852
|
+
let counter = 0, clamped = 0;
|
|
2853
|
+
const nextId = (suffix) => {
|
|
2854
|
+
for (;;) {
|
|
2855
|
+
counter++;
|
|
2856
|
+
const id = `svg-${counter}${suffix}`;
|
|
2857
|
+
if (!used.has(id)) {
|
|
2858
|
+
used.add(id);
|
|
2859
|
+
return id;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
const scale = width / viewWidth;
|
|
2864
|
+
const commands = [];
|
|
2865
|
+
const clamp = (value) => {
|
|
2866
|
+
if (value < 0 || value > 1) clamped++;
|
|
2867
|
+
return Math.max(0, Math.min(1, value));
|
|
2868
|
+
};
|
|
2869
|
+
const norm = (matrix, px, py) => {
|
|
2870
|
+
const [tx, ty] = apply$1(matrix, px, py);
|
|
2871
|
+
return {
|
|
2872
|
+
x: clamp((tx - originX) / viewWidth),
|
|
2873
|
+
y: clamp((ty - originY) / viewHeight)
|
|
2874
|
+
};
|
|
2875
|
+
};
|
|
2876
|
+
const gradientColor = (reference) => {
|
|
2877
|
+
const target = byId.get(/^url\(\s*['"]?#([^)'"]+)['"]?\s*\)/.exec(reference)?.[1] ?? "");
|
|
2878
|
+
if (!target || !/Gradient$/.test(target.name)) throw Error(`SVG paint ${reference} does not reference a gradient defined in the same SVG`);
|
|
2879
|
+
const stops = target.children.filter((child) => child.name === "stop");
|
|
2880
|
+
if (!stops.length) throw Error(`Gradient ${target.attrs.id} has no stops`);
|
|
2881
|
+
const channels = stops.map((stop) => parseColor(stop.attrs["stop-color"] ?? /stop-color\s*:\s*([^;]+)/.exec(stop.attrs.style ?? "")?.[1] ?? "#000000")).filter((color) => color !== "none").map((color) => [
|
|
2882
|
+
1,
|
|
2883
|
+
3,
|
|
2884
|
+
5
|
|
2885
|
+
].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16)));
|
|
2886
|
+
const mean = [
|
|
2887
|
+
0,
|
|
2888
|
+
1,
|
|
2889
|
+
2
|
|
2890
|
+
].map((channel) => Math.round(channels.reduce((sum, item) => sum + item[channel], 0) / channels.length));
|
|
2891
|
+
warnings.add(`Gradient ${target.attrs.id} was flattened to its average color; stack several bands for a real gradient.`);
|
|
2892
|
+
return `#${mean.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
2893
|
+
};
|
|
2894
|
+
const paint = (value) => String(value).startsWith("url(") ? gradientColor(value) : parseColor(value);
|
|
2895
|
+
const emit = (subpath, matrix, style, id) => {
|
|
2896
|
+
[subpath.start, ...subpath.curves.flatMap((curve) => [
|
|
2897
|
+
[curve[0], curve[1]],
|
|
2898
|
+
[curve[2], curve[3]],
|
|
2899
|
+
[curve[4], curve[5]]
|
|
2900
|
+
])];
|
|
2901
|
+
const fill = style.fill === "none" ? "none" : paint(style.fill);
|
|
2902
|
+
const stroke = style.stroke === "none" ? "none" : paint(style.stroke);
|
|
2903
|
+
const common = layer === void 0 ? {} : { layer };
|
|
2904
|
+
const strokeWidth = Math.max(1, Math.min(256, (style.strokeWidth || 1) * scale * Math.hypot(matrix[0], matrix[1])));
|
|
2905
|
+
const segments = subpath.curves.length;
|
|
2906
|
+
const closed = subpath.closed;
|
|
2907
|
+
const straightPoints = [subpath.start, ...subpath.curves.map((curve) => [curve[4], curve[5]])];
|
|
2908
|
+
const build = (color, opacity, filled, suffix) => {
|
|
2909
|
+
if (subpath.straight && straightPoints.length >= 2) {
|
|
2910
|
+
const vertices = closed ? straightPoints.slice(0, -1) : straightPoints;
|
|
2911
|
+
if (filled || closed) {
|
|
2912
|
+
if (vertices.length >= 3) return [{
|
|
2913
|
+
op: "stroke",
|
|
2914
|
+
...common,
|
|
2915
|
+
id: nextId(suffix),
|
|
2916
|
+
shape: "polygon",
|
|
2917
|
+
color,
|
|
2918
|
+
opacity,
|
|
2919
|
+
fill: filled,
|
|
2920
|
+
width: strokeWidth,
|
|
2921
|
+
points: vertices.map(([px, py]) => norm(matrix, px, py))
|
|
2922
|
+
}];
|
|
2923
|
+
}
|
|
2924
|
+
if (vertices.length === 2 && !filled) return [{
|
|
2925
|
+
op: "stroke",
|
|
2926
|
+
...common,
|
|
2927
|
+
id: nextId(suffix),
|
|
2928
|
+
shape: "line",
|
|
2929
|
+
color,
|
|
2930
|
+
opacity,
|
|
2931
|
+
fill: false,
|
|
2932
|
+
width: strokeWidth,
|
|
2933
|
+
points: vertices.map(([px, py]) => norm(matrix, px, py))
|
|
2934
|
+
}];
|
|
2935
|
+
if (!filled) {
|
|
2936
|
+
const pieces = [];
|
|
2937
|
+
for (let from = 0; from < vertices.length - 1; from += 1900) pieces.push({
|
|
2938
|
+
op: "stroke",
|
|
2939
|
+
...common,
|
|
2940
|
+
id: nextId(suffix),
|
|
2941
|
+
shape: "pen",
|
|
2942
|
+
color,
|
|
2943
|
+
opacity,
|
|
2944
|
+
fill: false,
|
|
2945
|
+
width: strokeWidth,
|
|
2946
|
+
points: vertices.slice(from, from + 1901).map(([px, py]) => norm(matrix, px, py))
|
|
2947
|
+
});
|
|
2948
|
+
return pieces;
|
|
2949
|
+
}
|
|
2950
|
+
return [];
|
|
2951
|
+
}
|
|
2952
|
+
if (filled && segments > MAX_BEZIER_SEGMENTS) {
|
|
2953
|
+
warnings.add(`A filled path with ${segments} curve segments was flattened to a polygon (more than ${MAX_BEZIER_SEGMENTS} segments cannot stay a Bezier).`);
|
|
2954
|
+
const flat = [subpath.start];
|
|
2955
|
+
let [cx, cy] = subpath.start;
|
|
2956
|
+
for (const [a, b, c1, d, e, f] of subpath.curves) {
|
|
2957
|
+
for (let step = 1; step <= FLATTEN_STEPS; step++) {
|
|
2958
|
+
const t = step / FLATTEN_STEPS, u = 1 - t;
|
|
2959
|
+
flat.push([u * u * u * cx + 3 * u * u * t * a + 3 * u * t * t * c1 + t * t * t * e, u * u * u * cy + 3 * u * u * t * b + 3 * u * t * t * d + t * t * t * f]);
|
|
2960
|
+
}
|
|
2961
|
+
cx = e;
|
|
2962
|
+
cy = f;
|
|
2963
|
+
}
|
|
2964
|
+
if (flat.length > 2e3) throw Error("A filled path is too complex to import; simplify it or split it into several paths.");
|
|
2965
|
+
return [{
|
|
2966
|
+
op: "stroke",
|
|
2967
|
+
...common,
|
|
2968
|
+
id: nextId(suffix),
|
|
2969
|
+
shape: "polygon",
|
|
2970
|
+
color,
|
|
2971
|
+
opacity,
|
|
2972
|
+
fill: true,
|
|
2973
|
+
width: strokeWidth,
|
|
2974
|
+
points: flat.map(([px, py]) => norm(matrix, px, py))
|
|
2975
|
+
}];
|
|
2976
|
+
}
|
|
2977
|
+
const pieces = [];
|
|
2978
|
+
for (let from = 0; from < segments; from += MAX_BEZIER_SEGMENTS) {
|
|
2979
|
+
const chunk = subpath.curves.slice(from, from + MAX_BEZIER_SEGMENTS);
|
|
2980
|
+
const chunkPoints = [from === 0 ? subpath.start : [subpath.curves[from - 1][4], subpath.curves[from - 1][5]], ...chunk.flatMap((curve) => [
|
|
2981
|
+
[curve[0], curve[1]],
|
|
2982
|
+
[curve[2], curve[3]],
|
|
2983
|
+
[curve[4], curve[5]]
|
|
2984
|
+
])];
|
|
2985
|
+
pieces.push({
|
|
2986
|
+
op: "stroke",
|
|
2987
|
+
...common,
|
|
2988
|
+
id: nextId(suffix),
|
|
2989
|
+
shape: "bezier",
|
|
2990
|
+
color,
|
|
2991
|
+
opacity,
|
|
2992
|
+
fill: filled,
|
|
2993
|
+
width: strokeWidth,
|
|
2994
|
+
points: chunkPoints.map(([px, py]) => norm(matrix, px, py))
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
return pieces;
|
|
2998
|
+
};
|
|
2999
|
+
if (fill !== "none" && style.fillOpacity * style.opacity > 0) commands.push(...build(fill, style.fillOpacity * style.opacity, true, ""));
|
|
3000
|
+
if (stroke !== "none" && style.strokeOpacity * style.opacity > 0 && style.strokeWidth > 0) commands.push(...build(stroke, style.strokeOpacity * style.opacity, false, "-line"));
|
|
3001
|
+
};
|
|
3002
|
+
const walk = (node, matrix, parentStyle, depth) => {
|
|
3003
|
+
if (depth > 24) throw Error("SVG is nested too deeply");
|
|
3004
|
+
if (UNSUPPORTED.has(node.name)) throw Error(`Unsupported SVG element <${node.name}>. Draw with path, rect, circle, ellipse, line, polyline, polygon, g and use; add text with a native text command.`);
|
|
3005
|
+
if (IGNORED.has(node.name)) return;
|
|
3006
|
+
for (const name of [
|
|
3007
|
+
"clip-path",
|
|
3008
|
+
"mask",
|
|
3009
|
+
"filter"
|
|
3010
|
+
]) if (node.attrs[name] && node.attrs[name] !== "none") throw Error(`Unsupported SVG attribute ${name}. Bake it into the shapes instead.`);
|
|
3011
|
+
const style = styleOf(node, parentStyle);
|
|
3012
|
+
if (style.display === "none") return;
|
|
3013
|
+
const local = multiply(matrix, parseTransform(node.attrs.transform));
|
|
3014
|
+
const n = (name) => Number.parseFloat(node.attrs[name] ?? 0);
|
|
3015
|
+
const finish = (subpaths) => subpaths.forEach((subpath) => emit(subpath, local, style, node.attrs.id));
|
|
3016
|
+
switch (node.name) {
|
|
3017
|
+
case "svg":
|
|
3018
|
+
case "g":
|
|
3019
|
+
case "a":
|
|
3020
|
+
for (const child of node.children) walk(child, local, style, depth + 1);
|
|
3021
|
+
return;
|
|
3022
|
+
case "use": {
|
|
3023
|
+
const href = node.attrs.href ?? node.attrs["xlink:href"];
|
|
3024
|
+
const target = href?.startsWith("#") ? byId.get(href.slice(1)) : void 0;
|
|
3025
|
+
if (!target) throw Error(`<use> must reference an element in the same SVG (${href ?? "missing href"})`);
|
|
3026
|
+
walk(target, multiply(local, [
|
|
3027
|
+
1,
|
|
3028
|
+
0,
|
|
3029
|
+
0,
|
|
3030
|
+
1,
|
|
3031
|
+
n("x"),
|
|
3032
|
+
n("y")
|
|
3033
|
+
]), { ...style }, depth + 1);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
case "path":
|
|
3037
|
+
finish(parsePathData(node.attrs.d));
|
|
3038
|
+
return;
|
|
3039
|
+
case "rect": {
|
|
3040
|
+
const w = n("width"), h = n("height");
|
|
3041
|
+
if (!(w > 0 && h > 0)) return;
|
|
3042
|
+
const rx = node.attrs.rx !== void 0 ? n("rx") : node.attrs.ry !== void 0 ? n("ry") : 0;
|
|
3043
|
+
const ry = node.attrs.ry !== void 0 ? n("ry") : rx;
|
|
3044
|
+
finish([rx > 0 || ry > 0 ? roundedRectPath(n("x"), n("y"), w, h, rx, ry) : polylinePath([
|
|
3045
|
+
[n("x"), n("y")],
|
|
3046
|
+
[n("x") + w, n("y")],
|
|
3047
|
+
[n("x") + w, n("y") + h],
|
|
3048
|
+
[n("x"), n("y") + h]
|
|
3049
|
+
], true)]);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
case "circle":
|
|
3053
|
+
if (n("r") > 0) finish([ellipsePath(n("cx"), n("cy"), n("r"), n("r"))]);
|
|
3054
|
+
return;
|
|
3055
|
+
case "ellipse":
|
|
3056
|
+
if (n("rx") > 0 && n("ry") > 0) finish([ellipsePath(n("cx"), n("cy"), n("rx"), n("ry"))]);
|
|
3057
|
+
return;
|
|
3058
|
+
case "line":
|
|
3059
|
+
finish([polylinePath([[n("x1"), n("y1")], [n("x2"), n("y2")]], false)]);
|
|
3060
|
+
return;
|
|
3061
|
+
case "polyline":
|
|
3062
|
+
case "polygon": {
|
|
3063
|
+
const values = numbers(node.attrs.points);
|
|
3064
|
+
if (values.length < 4 || values.length % 2) throw Error(`<${node.name}> needs an even number of coordinates, at least two points`);
|
|
3065
|
+
const pts = [];
|
|
3066
|
+
for (let at = 0; at < values.length; at += 2) pts.push([values[at], values[at + 1]]);
|
|
3067
|
+
finish([polylinePath(pts, node.name === "polygon")]);
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
default: throw Error(`Unsupported SVG element <${node.name}>`);
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
walk(root, IDENTITY, {
|
|
3074
|
+
fill: "#000000",
|
|
3075
|
+
stroke: "none",
|
|
3076
|
+
strokeWidth: 1,
|
|
3077
|
+
fillOpacity: 1,
|
|
3078
|
+
strokeOpacity: 1,
|
|
3079
|
+
opacity: 1
|
|
3080
|
+
}, 0);
|
|
3081
|
+
if (!commands.length) throw Error("The SVG contained no drawable shapes");
|
|
3082
|
+
return {
|
|
3083
|
+
commands,
|
|
3084
|
+
warnings: [...warnings],
|
|
3085
|
+
clamped
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
function expandSvgCommands(commands, doc, report) {
|
|
3089
|
+
if (!commands.some((command) => command?.op === "svg")) return commands;
|
|
3090
|
+
const width = doc.width ?? 1024, height = doc.height ?? 1024;
|
|
3091
|
+
const ids = new Set(doc.layers.flatMap((item) => item.strokes.map((stroke) => stroke.id).filter(Boolean)));
|
|
3092
|
+
const out = [];
|
|
3093
|
+
let clamped = 0;
|
|
3094
|
+
for (const command of commands) {
|
|
3095
|
+
if (command?.op !== "svg") {
|
|
3096
|
+
out.push(command);
|
|
3097
|
+
if (command?.id) ids.add(command.id);
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
const result = svgToSketchCommands(command.svg, {
|
|
3101
|
+
width,
|
|
3102
|
+
height,
|
|
3103
|
+
layer: command.layer,
|
|
3104
|
+
existingIds: ids
|
|
3105
|
+
});
|
|
3106
|
+
result.commands.forEach((item) => ids.add(item.id));
|
|
3107
|
+
out.push(...result.commands);
|
|
3108
|
+
clamped += result.clamped;
|
|
3109
|
+
if (report) report.warnings = [.../* @__PURE__ */ new Set([...report.warnings ?? [], ...result.warnings])];
|
|
3110
|
+
}
|
|
3111
|
+
if (report && clamped) report.clampedPoints = (report.clampedPoints ?? 0) + clamped;
|
|
3112
|
+
return out;
|
|
3113
|
+
}
|
|
2324
3114
|
const SKETCH_COMMAND_HELP = {
|
|
2325
3115
|
coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
|
|
2326
3116
|
shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
|
|
@@ -2330,19 +3120,22 @@ window.__ModuleLoader__.load({
|
|
|
2330
3120
|
layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
|
|
2331
3121
|
curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
|
|
2332
3122
|
object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
|
|
2333
|
-
resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
|
|
3123
|
+
resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}",
|
|
3124
|
+
svg: "{op:\"svg\",svg:\"<svg viewBox=\\\"0 0 1024 768\\\">...</svg>\",layer:1}. Fastest way to draw: send a whole scene as SVG (path with M L H V C S Q T A Z, rect, circle, ellipse, line, polyline, polygon, g, use, transform, fill/stroke/opacity, style=\"...\"); it becomes native editable strokes. The viewBox must match the canvas size (use the width and height from inspect; 4:3 is 1024x768). Gradients are flattened to their average color, so build glows and skies from stacked bands; text, image, filter, clipPath, mask, pattern and CSS classes are refused. Coordinates outside the canvas are clamped. One svg command may hold up to 200000 characters."
|
|
2334
3125
|
},
|
|
2335
3126
|
limits: {
|
|
2336
3127
|
strokes: MAX_SKETCH_STROKES,
|
|
2337
3128
|
pointsPerStroke: MAX_STROKE_POINTS,
|
|
2338
3129
|
pointsTotal: 2e5,
|
|
2339
|
-
commandsPerBatch: 256
|
|
3130
|
+
commandsPerBatch: 256,
|
|
3131
|
+
svgCharacters: MAX_SVG_CHARACTERS
|
|
2340
3132
|
}
|
|
2341
3133
|
};
|
|
2342
3134
|
const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
2343
|
-
function applySketchCommands(source, commands) {
|
|
3135
|
+
function applySketchCommands(source, commands, report) {
|
|
2344
3136
|
if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
|
|
2345
3137
|
let doc = identifyObjects(source);
|
|
3138
|
+
commands = expandSvgCommands(commands, doc, report);
|
|
2346
3139
|
for (let command of commands) {
|
|
2347
3140
|
if (!command || typeof command !== "object") throw Error("Invalid command");
|
|
2348
3141
|
if (command.op === "stroke") command = expandSketchPreset(command);
|
|
@@ -2544,11 +3337,12 @@ window.__ModuleLoader__.load({
|
|
|
2544
3337
|
}
|
|
2545
3338
|
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
2546
3339
|
let changedObjects;
|
|
3340
|
+
const report = {};
|
|
2547
3341
|
if (request.action === "apply") {
|
|
2548
3342
|
const before = adapter.document();
|
|
2549
3343
|
let next;
|
|
2550
3344
|
try {
|
|
2551
|
-
next = applySketchCommands(before, request.commands);
|
|
3345
|
+
next = applySketchCommands(before, request.commands, report);
|
|
2552
3346
|
} catch (cause) {
|
|
2553
3347
|
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
2554
3348
|
error.code = "SKETCH_INVALID_BATCH";
|
|
@@ -2574,7 +3368,9 @@ window.__ModuleLoader__.load({
|
|
|
2574
3368
|
...changedObjects ? {
|
|
2575
3369
|
changedObjects: changedObjects.slice(0, 100),
|
|
2576
3370
|
changedObjectCount: changedObjects.length
|
|
2577
|
-
} : {}
|
|
3371
|
+
} : {},
|
|
3372
|
+
...report?.warnings?.length ? { warnings: report.warnings } : {},
|
|
3373
|
+
...report?.clampedPoints ? { clampedPoints: report.clampedPoints } : {}
|
|
2578
3374
|
};
|
|
2579
3375
|
completed.set(key, {
|
|
2580
3376
|
fingerprint,
|
|
@@ -5847,6 +6643,10 @@ window.__ModuleLoader__.load({
|
|
|
5847
6643
|
.codexSubscriptionRecover .codexSubscriptionError{flex:1}
|
|
5848
6644
|
.codexSubscriptionRecover button{flex:0 0 auto}
|
|
5849
6645
|
.codexSubscriptionDiagnostics{color:var(--dsw-alias-label-secondary)}
|
|
6646
|
+
.codexSubscriptionDiagnostics ul{margin:0;padding-left:18px;font-size:13px;line-height:20px}
|
|
6647
|
+
.codexSubscriptionDiagnostics li+li{margin-top:6px}
|
|
6648
|
+
.codexSubscriptionDiagnostics li strong{font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
6649
|
+
.codexSubscriptionDiagnostics [role=status]>p{margin:0;font-size:13px}
|
|
5850
6650
|
.codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}
|
|
5851
6651
|
.codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}
|
|
5852
6652
|
.codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}
|
|
@@ -7676,7 +8476,7 @@ window.__ModuleLoader__.load({
|
|
|
7676
8476
|
}
|
|
7677
8477
|
//#endregion
|
|
7678
8478
|
//#region src/version.js
|
|
7679
|
-
const PACKAGE_VERSION = "2.
|
|
8479
|
+
const PACKAGE_VERSION = "2.4.0";
|
|
7680
8480
|
//#endregion
|
|
7681
8481
|
//#region src/client-recovery.js
|
|
7682
8482
|
async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
|
|
@@ -9955,7 +10755,9 @@ window.__ModuleLoader__.load({
|
|
|
9955
10755
|
return {
|
|
9956
10756
|
format: "compact-1",
|
|
9957
10757
|
plugin: report.version,
|
|
9958
|
-
dsh: report.
|
|
10758
|
+
dsh: report.runtime?.dsh,
|
|
10759
|
+
...report.client?.version && report.client.version !== report.version ? { pluginClient: report.client.version } : {},
|
|
10760
|
+
platform: report.runtime ? `${report.runtime.platform}-${report.runtime.arch}` : void 0,
|
|
9959
10761
|
generatedAt: report.generatedAt,
|
|
9960
10762
|
account: report.account?.status,
|
|
9961
10763
|
catalog: report.catalog ? {
|
package/lib/index.js
CHANGED
|
@@ -1205,13 +1205,18 @@ const SUPPORTED_RUNTIME_VERSIONS = Object.freeze([
|
|
|
1205
1205
|
]);
|
|
1206
1206
|
const require = createRequire(import.meta.url);
|
|
1207
1207
|
const execute = promisify(execFile);
|
|
1208
|
+
/** The DSH release this plugin is loaded into, read from the host's own LLM package. */
|
|
1209
|
+
function hostDshVersion(resolve = require.resolve, entry = process.argv[1]) {
|
|
1210
|
+
const attempts = [() => resolve("@deepseek-ai/dsh-llm/package.json")];
|
|
1211
|
+
if (typeof entry === "string" && entry) attempts.push(() => require.resolve("@deepseek-ai/dsh-llm/package.json", { paths: [dirname(entry)] }));
|
|
1212
|
+
for (const attempt of attempts) try {
|
|
1213
|
+
const { version } = JSON.parse(readFileSync(attempt(), "utf8"));
|
|
1214
|
+
if (typeof version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version)) return version;
|
|
1215
|
+
} catch {}
|
|
1216
|
+
}
|
|
1208
1217
|
function matchingSubagentRuntimeVersion(resolve = require.resolve) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return SUPPORTED_RUNTIME_VERSIONS.includes(host.version) ? host.version : void 0;
|
|
1212
|
-
} catch {
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1218
|
+
const version = hostDshVersion(resolve, resolve === require.resolve ? process.argv[1] : "");
|
|
1219
|
+
return SUPPORTED_RUNTIME_VERSIONS.includes(version) ? version : void 0;
|
|
1215
1220
|
}
|
|
1216
1221
|
function inspectSubagentRuntime(resolve = require.resolve) {
|
|
1217
1222
|
try {
|
|
@@ -1799,7 +1804,8 @@ const sketchCommandArray = {
|
|
|
1799
1804
|
"stroke",
|
|
1800
1805
|
"object",
|
|
1801
1806
|
"layer",
|
|
1802
|
-
"resize"
|
|
1807
|
+
"resize",
|
|
1808
|
+
"svg"
|
|
1803
1809
|
]
|
|
1804
1810
|
},
|
|
1805
1811
|
id: {
|
|
@@ -1863,6 +1869,10 @@ const sketchCommandArray = {
|
|
|
1863
1869
|
"clear"
|
|
1864
1870
|
]
|
|
1865
1871
|
},
|
|
1872
|
+
svg: {
|
|
1873
|
+
type: "string",
|
|
1874
|
+
description: "op svg only: a complete <svg viewBox=\"0 0 W H\"> document (W and H equal to the canvas width and height from inspect). Imported as native editable strokes; the fastest way to draw a whole scene. Optional layer sets the target layer."
|
|
1875
|
+
},
|
|
1866
1876
|
value: string,
|
|
1867
1877
|
ratio: {
|
|
1868
1878
|
type: "string",
|
|
@@ -1888,7 +1898,7 @@ const sketchCommandArray = {
|
|
|
1888
1898
|
function createSketchAgentTool(bridge, attachments) {
|
|
1889
1899
|
return defineTool({
|
|
1890
1900
|
name: "codex_sketch",
|
|
1891
|
-
description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
|
|
1901
|
+
description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Use stroke commands for small edits and native text. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
|
|
1892
1902
|
parameters: {
|
|
1893
1903
|
action: {
|
|
1894
1904
|
type: "string",
|
|
@@ -3058,7 +3068,7 @@ async function readSubscriptionCredentials(getAuth, readCredential, signal) {
|
|
|
3058
3068
|
}
|
|
3059
3069
|
//#endregion
|
|
3060
3070
|
//#region src/version.js
|
|
3061
|
-
const PACKAGE_VERSION = "2.
|
|
3071
|
+
const PACKAGE_VERSION = "2.4.0";
|
|
3062
3072
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
3063
3073
|
//#endregion
|
|
3064
3074
|
//#region src/model-catalog.js
|
|
@@ -3551,6 +3561,23 @@ const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
|
|
|
3551
3561
|
const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
|
|
3552
3562
|
const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3553
3563
|
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
3564
|
+
const NETWORK_REASONS = {
|
|
3565
|
+
UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
|
|
3566
|
+
UND_ERR_BODY_TIMEOUT: "the image response stalled",
|
|
3567
|
+
UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
|
|
3568
|
+
UND_ERR_SOCKET: "the connection closed unexpectedly",
|
|
3569
|
+
ECONNRESET: "the connection was reset",
|
|
3570
|
+
ETIMEDOUT: "the connection timed out",
|
|
3571
|
+
ENOTFOUND: "DNS lookup failed",
|
|
3572
|
+
ECONNREFUSED: "the connection was refused",
|
|
3573
|
+
EAI_AGAIN: "DNS lookup failed"
|
|
3574
|
+
};
|
|
3575
|
+
function requestFailureReason(error) {
|
|
3576
|
+
const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
|
|
3577
|
+
if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
|
|
3578
|
+
if (error?.name === "TimeoutError") return ": the image request timed out";
|
|
3579
|
+
return "";
|
|
3580
|
+
}
|
|
3554
3581
|
function normalizeImageOptions(args) {
|
|
3555
3582
|
const quality = nonEmpty(args?.quality) ?? "auto";
|
|
3556
3583
|
const background = nonEmpty(args?.background) ?? "auto";
|
|
@@ -3955,7 +3982,7 @@ function createCodexImageTool(options) {
|
|
|
3955
3982
|
});
|
|
3956
3983
|
} catch (error) {
|
|
3957
3984
|
if (exec.signal.aborted) throw exec.signal.reason;
|
|
3958
|
-
throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed`, { cause: error });
|
|
3985
|
+
throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
|
|
3959
3986
|
}
|
|
3960
3987
|
if (!response.ok) {
|
|
3961
3988
|
if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
|
|
@@ -4642,7 +4669,8 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
4642
4669
|
runtime: {
|
|
4643
4670
|
node: process.version,
|
|
4644
4671
|
platform: process.platform,
|
|
4645
|
-
arch: process.arch
|
|
4672
|
+
arch: process.arch,
|
|
4673
|
+
...hostDshVersion() ? { dsh: hostDshVersion() } : {}
|
|
4646
4674
|
},
|
|
4647
4675
|
account,
|
|
4648
4676
|
login,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|