激光雕刻软件要解决的核心问题很具体:画布上的路径、矩形、文字和图片,最终必须变成机器能执行的一串运动指令。Beam Studio 没有在浏览器里做刀路计算,而是把作品整理成带图层参数的 SVG,交给本地后端展平曲线、扫描像素、再写成 Flux 方言的 G-code / Fcode。
这篇文章顺着数据走一遍:图元怎么存、怎么导出、空心轮廓怎么走刀、填充怎么扫、位图怎么变成功率变化、曲线靠什么变成折线。读完应能建立对整条技术方案的完整图景。
1. 先看全貌
beam-studio-web 是 Web 壳,编辑与导出逻辑在 beam-studio-core。真正的几何离散和扫描发生在三个后端库里:
| 组件 | 职责 |
|---|---|
| fluxghost | WebSocket 编排:收 SVG、调解析、回传任务文件 |
| fluxsvg | 解析 SVG,拆描边 / 填充 / 位图,把路径喂给 Beamify |
| Beamify | 把贝塞尔、圆弧展成折线,输出点列 |
| fluxclient | 把点列和扫描行写成 G-code 或 Fcode |
#mermaid-svg-WbEvPJgVn5APVL6E{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WbEvPJgVn5APVL6E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WbEvPJgVn5APVL6E .error-icon{fill:#552222;}#mermaid-svg-WbEvPJgVn5APVL6E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WbEvPJgVn5APVL6E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WbEvPJgVn5APVL6E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WbEvPJgVn5APVL6E .marker.cross{stroke:#333333;}#mermaid-svg-WbEvPJgVn5APVL6E svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WbEvPJgVn5APVL6E p{margin:0;}#mermaid-svg-WbEvPJgVn5APVL6E .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WbEvPJgVn5APVL6E .cluster-label text{fill:#333;}#mermaid-svg-WbEvPJgVn5APVL6E .cluster-label span{color:#333;}#mermaid-svg-WbEvPJgVn5APVL6E .cluster-label span p{background-color:transparent;}#mermaid-svg-WbEvPJgVn5APVL6E .label text,#mermaid-svg-WbEvPJgVn5APVL6E span{fill:#333;color:#333;}#mermaid-svg-WbEvPJgVn5APVL6E .node rect,#mermaid-svg-WbEvPJgVn5APVL6E .node circle,#mermaid-svg-WbEvPJgVn5APVL6E .node ellipse,#mermaid-svg-WbEvPJgVn5APVL6E .node polygon,#mermaid-svg-WbEvPJgVn5APVL6E .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WbEvPJgVn5APVL6E .rough-node .label text,#mermaid-svg-WbEvPJgVn5APVL6E .node .label text,#mermaid-svg-WbEvPJgVn5APVL6E .image-shape .label,#mermaid-svg-WbEvPJgVn5APVL6E .icon-shape .label{text-anchor:middle;}#mermaid-svg-WbEvPJgVn5APVL6E .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WbEvPJgVn5APVL6E .rough-node .label,#mermaid-svg-WbEvPJgVn5APVL6E .node .label,#mermaid-svg-WbEvPJgVn5APVL6E .image-shape .label,#mermaid-svg-WbEvPJgVn5APVL6E .icon-shape .label{text-align:center;}#mermaid-svg-WbEvPJgVn5APVL6E .node.clickable{cursor:pointer;}#mermaid-svg-WbEvPJgVn5APVL6E .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WbEvPJgVn5APVL6E .arrowheadPath{fill:#333333;}#mermaid-svg-WbEvPJgVn5APVL6E .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WbEvPJgVn5APVL6E .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WbEvPJgVn5APVL6E .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WbEvPJgVn5APVL6E .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WbEvPJgVn5APVL6E .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WbEvPJgVn5APVL6E .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WbEvPJgVn5APVL6E .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WbEvPJgVn5APVL6E .cluster text{fill:#333;}#mermaid-svg-WbEvPJgVn5APVL6E .cluster span{color:#333;}#mermaid-svg-WbEvPJgVn5APVL6E div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WbEvPJgVn5APVL6E .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WbEvPJgVn5APVL6E rect.text{fill:none;stroke-width:0;}#mermaid-svg-WbEvPJgVn5APVL6E .icon-shape,#mermaid-svg-WbEvPJgVn5APVL6E .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WbEvPJgVn5APVL6E .icon-shape p,#mermaid-svg-WbEvPJgVn5APVL6E .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WbEvPJgVn5APVL6E .icon-shape .label rect,#mermaid-svg-WbEvPJgVn5APVL6E .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WbEvPJgVn5APVL6E .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WbEvPJgVn5APVL6E .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WbEvPJgVn5APVL6E :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 产物
fluxghost · svgeditor-laser-parser
Beam Studio 前端
WebSocket svgeditor_upload
默认
-gc
画布 SVG / BVG
图层 · 速度 · 功率
文字 → 路径
位图:灰度 / 阈值 / 全分辨率
打包 thumbnail + SVG
SvgeditorImage 解析图层
fluxsvg 拆通道
Beamify 曲线 → 折线
填充 / 位图 → 扫描线
svgeditor2taskcode
Fcode .fc
下发机器
G-code
路径预览
默认下发 Fcode (.fc,机器二进制)。加 -gc 才出可读 G-code,主要用于路径预览。两条输出共用同一套刀路,只是 writer 不同。
2. 画布上的数据长什么样
作品的权威模型就是 SVG。图层是 <g class="layer">,激光参数写在 data 属性上:
xml
<g class="layer"
data-speed="5"
data-strength="60"
data-repeat="1"
data-height="-3">
<title>Cutting</title>
<rect fill="none" fill-opacity="0" stroke="#FE0000" .../>
</g>
| 属性 | 含义 |
|---|---|
data-speed |
速度,mm/s |
data-strength |
功率,0--100% |
data-repeat |
重复次数 |
data-height / data-zstep |
对焦高度 / 分层步进 |
data-engrave_dpi |
位图分辨率档位(写在根节点) |
坐标系:10 dpmm,1 个 SVG 单位 = 0.1 mm。Beamo 工作区 300×210 mm,对应 3000×2100 SVG 单位。原点在工作区左下,预览解析 G-code 时 Y 会取反。
图元分两类,后面走完全不同的通道:
| 空心矢量 | 填充矢量 | 位图 | |
|---|---|---|---|
| 典型元素 | <path> <rect> <line>,fill="none" |
同上,但 fill-opacity="1" |
<image> |
| 后端通道 | Beamify 折线走刀 | Cairo 涂实 → 当位图扫 | 像素扫描 + PWM |
判断填充的规则在前端写死:
text
isFilled = fill-opacity ≠ 0 且 fill ≠ "none"
白填充(#FFF / #FFFFFF)也视为空心
矩形默认就是空心:fill="none"、fill-opacity="0"。右侧「设定填充」只做两件事------写上 fill 颜色,并把 fill-opacity 设为 1。开口 path 和 line 不能开填充。
3. 导出主流程:从「开始」到任务文件
用户点 Go / 导出 / 路径预览,都进 fetchTaskCode()。前端不算刀路,只保证后端拿到一份干净、可解析的场景。
fluxclient toolpath fluxsvg + Beamify fluxghost 前端 fluxclient toolpath fluxsvg + Beamify fluxghost 前端 #mermaid-svg-dxRHIRAtNwl0bHtU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dxRHIRAtNwl0bHtU .error-icon{fill:#552222;}#mermaid-svg-dxRHIRAtNwl0bHtU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dxRHIRAtNwl0bHtU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dxRHIRAtNwl0bHtU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dxRHIRAtNwl0bHtU .marker.cross{stroke:#333333;}#mermaid-svg-dxRHIRAtNwl0bHtU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dxRHIRAtNwl0bHtU p{margin:0;}#mermaid-svg-dxRHIRAtNwl0bHtU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dxRHIRAtNwl0bHtU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dxRHIRAtNwl0bHtU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dxRHIRAtNwl0bHtU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dxRHIRAtNwl0bHtU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dxRHIRAtNwl0bHtU .sequenceNumber{fill:white;}#mermaid-svg-dxRHIRAtNwl0bHtU #sequencenumber{fill:#333;}#mermaid-svg-dxRHIRAtNwl0bHtU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dxRHIRAtNwl0bHtU .messageText{fill:#333;stroke:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dxRHIRAtNwl0bHtU .labelText,#mermaid-svg-dxRHIRAtNwl0bHtU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .loopText,#mermaid-svg-dxRHIRAtNwl0bHtU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dxRHIRAtNwl0bHtU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dxRHIRAtNwl0bHtU .noteText,#mermaid-svg-dxRHIRAtNwl0bHtU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dxRHIRAtNwl0bHtU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dxRHIRAtNwl0bHtU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dxRHIRAtNwl0bHtU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dxRHIRAtNwl0bHtU .actorPopupMenu{position:absolute;}#mermaid-svg-dxRHIRAtNwl0bHtU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dxRHIRAtNwl0bHtU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dxRHIRAtNwl0bHtU .actor-man circle,#mermaid-svg-dxRHIRAtNwl0bHtU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dxRHIRAtNwl0bHtU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 文字转路径 位图刷成全分辨率 PNG svgeditor_upload thumbnailsvg 解析图层、拆描边/填充/图像 折线点列 + 扫描用栅格 go -f -gc 机型 DPI ... svgeditor2taskcode(writer, factory) Fcode 或 G-code 二进制流
对应代码:
FontFuncs.tempConvertTextToPathAmoungSvgcontent()
用 fontkit 抽 glyph 轮廓;失败再走 ghost 的uploadPlainTextSVG+divideSVG。到后端时文字已经是<path>。updateImagesResolution(true)
用origImage按当前 shading / threshold 重算全分辨率 PNG,写回xlink:href。generateUploadFile()
svgCanvas.getSvgString(),和缩略图拼成[thumbnail][svg]。svgeditorParser.uploadToSvgeditorAPI()
WebSocket 发svgeditor_upload,带上机型和 DPI。svgeditorParser.getTaskCode()
发go ... -f,要 G-code 再加-gc。
核心文件:
beam-studio-core/src/web/app/actions/beambox/export-funcs.ts---fetchTaskCode、generateUploadFile、getGcodebeam-studio-core/src/web/helpers/api/svg-laser-parser.ts---uploadToSvgeditorAPI、getTaskCodefluxghost/api/svgeditor_toolpath.py---cmd_svgeditor_upload、cmd_go
cmd_go 里按 -gc 选 writer,然后调用同一套生成函数:
python
if output_fcode:
writer = FCodeV1MemoryWriter(...) # 或 V2
else:
writer = GCodeMemoryWriter()
svgeditor2taskcode(writer, factory, travel_speed=7500, ...)
4. 矢量通道:沿着几何走
空心 path、空心矩形、开口线,都走这一路:激光跟着轮廓走,内部不烧。
4.1 导入时先拆一层
外部 SVG 进画布时,前端会先让 ghost 拆文件(导出时不再拆,但同一套解析器):
text
uploadPlainSVG → divide_svg / divide_svg_by_layer
fluxsvg 吐出:
strokes:描边路径colors:填充色块bitmap:嵌入图 / 渐变抽成的 PNG
前端再把这些 blob 读回画布。填充和描边从一开始就被分开,后面可以分别走「沿线」或「扫描」。
核心文件:importSvg.ts、svg-laser-parser.ts 的 divideSVG / uploadPlainSVG。
4.2 路径命令怎么进后端
fluxsvg 是 CairoSVG 的分支。每个节点同时画到两个 context:
- Cairo
context:渲染、填充栅格 - Beamify
bcontext:激光刀路
path.py 按 d 逐命令翻译,曲线原样交给 Beamify,前端不先折线化:
| SVG 命令 | Beamify |
|---|---|
M / L / H / V |
move_to / line_to |
C / S |
curve_to / rel_curve_to |
Q / T |
fluxsvg 升成三次贝塞尔,再 curve_to |
A |
ellipse_arc |
Z |
close_path |
空心矩形不经过 d,shapes.py 直接 bcontext.rectangle(x, y, w, h)(圆角拆成直线 + 曲线)。对刀路来说,它和「四条边 + Z」的 path 是同一类闭合轮廓。
变换、裁剪、虚线也同步进 Beamify:beamify.Matrix、clip、set_dash。
4.3 曲线如何变成折线:Beamify
这是矢量方案里最关键的一步。
机器指令没有 G2/G3,Flux 方言的矢量移动只有直线 G1。所以贝塞尔和圆弧必须离散成折线。这件事由 Beamify 做,不在浏览器里。
text
SVG d="M 0,0 C 10,20 40,20 50,0 A 20,20 0 0 1 90,0"
│
▼
fluxsvg 解析(不展平)
│ curve_to / ellipse_arc
▼
beamify.Context 先存曲线
│ sort()
│ get_array()
▼
[(x0,y0), (x1,y1), (x2,y2), ...] 折线点列
│ 坐标 ÷ 10 → mm
▼
G1V 一段接一段
对外入口:
python
# fluxsvg/__init__.py
def parse(...):
return SURFACES['SVG'].convert(...).get_array()
convert() 走完 SVG 树后:
python
instance.bcontext.sort() # 重排路径,减少空移
return instance.bcontext # 再 get_array() 取出点
二次曲线的升阶在 fluxsvg,不在 Beamify:
python
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(...)
surface.bcontext.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
set_compensation_length(loop_compensation) 让闭合圈在接缝处多走一点,避免首尾接不上。hide_path() 则丢掉当前路径(无描边,或粗描边改走填充通道)。
Beamify 仓库目前不公开,细分是固定步长还是按曲率自适应,只能从接口推断:进的是曲线命令,出的是点列。早期 fluxclient 还有 Cython 的 get_all_points(svg),作用类似,当前主路径是 fluxsvg → Beamify。
4.4 折线写成 G-code
svgeditor2taskcode(或公开版 SvgeditorFactory._gen_svg_walk_path)沿点列吐移动:
text
图层功率 = data-strength / 100
速度 = data-speed(mm/s,写成 F 时 × 60)
for 每个子路径:
G1S 空移到起点 # 激光关
开光,功率 = 图层功率
for 后续每个点:
G1V X.. Y.. F(速度) # 沿折线走
关光
功率整段恒定,没有按像素变化。空心矢量速度大于 20 mm/s 时,前端会警告------GoButton.tsx 把「空心」和「高速切割」绑在一起。
5. 填充模式:封闭面变成实心图再扫
「填充」不是沿轮廓画平行线 hatch。闭合图形开了 Infill 之后,内部被 Cairo 涂实成一张位图,再按 DPI 一行一行扫。
5.1 前端只改属性
text
setElemsFill → fill = 描边色, fill-opacity = 1
setElemsUnfill → fill = none, fill-opacity = 0
InFillBlock.tsx 是开关。path 必须先通过 calcPathClosed(起点终点重合),否则没有填充按钮。
5.2 后端拆成两条通道
fluxsvg.divide_path_and_fill() 把同一份 SVG 拆开:
text
path_context / bcontext → 描边 → 矢量折线
fill_context → fill_preserve() → 实心 PNG
bitmap_context → <image> / 渐变
判定:
text
fill 为 none / #fff / opacity=0 → 不填充,只走矢量
fill 有效且 alpha > 0 → fill_available,涂实内部
描边和填充可以同时存在:
| 情况 | 矢量轮廓 | 填充扫描 |
|---|---|---|
| 空心 + 有描边 | 有 | 无 |
| 填充 + 细描边(width ≤ 1) | 仍有,边再走一圈 | 有 |
| 填充 + 无描边 | 无 | 有 |
| 填充 + 粗描边(width > 1) | 关掉,粗边并进栅格 | 有 |
UI 点 Infill 会保留 stroke。所以常见结果是:内部扫描 + 外框再沿轮廓走一圈。
5.3 填充矩形的扫描长什么样
对 <rect x y width height>:
shapes.rect()建矩形几何fill_context.fill_preserve()把内部涂实- 导出 PNG,按雕刻 DPI 对齐工作区
- 按行扫描,和实心黑图一样
text
for y in bbox.top .. bbox.bottom: # 行距 ≈ 1/DPI mm
奇数行左→右,偶数行右→左
G1S 到该行第一个实心像素
G1V 扫到该行最后一个实心像素 # 功率 = 图层 data-strength
关光,换行
一个 50×30 mm、medium(10 px/mm)的填充矩形,大约 300 条扫描线,不是 4 条边。时间随面积 × DPI 涨,不随周长涨。
闭合 path 开填充时算法相同:Cairo 按 fill-rule(nonzero / evenodd)算内部,自交和孔洞会镂空,得到一张「实心剪影」,再扫描。
填充不走 前端的 grayscale.ts,也不读 data-shading。convertShapeToBitmap 只用于喷墨打印层,激光填充不在浏览器里栅格化。
6. 位图通道:像素变成功率
导入的 PNG/JPG 是另一路:本来就是栅格,前端先做成「后端该雕成什么样」的 PNG,后端再扫。
6.1 前端预处理
<image> 带着元数据:
| 属性 | 作用 |
|---|---|
origImage |
未处理原图,重算用 |
xlink:href |
当前显示 / 导出的 PNG |
data-shading |
true 渐变;false 二值 |
data-threshold |
1--255,渐变默认 254,二值默认 128 |
data-fullcolor |
喷墨全彩,不做灰度 |
灰度在 grayscale.ts:0.299R + 0.587G + 0.114B,透明当白。
text
渐变 shading:保留灰阶,高于 threshold 的像素变白
二值 threshold:低于阈值全黑,高于全白
导出前 updateImagesResolution(true) 用原图 + 当前参数重算全分辨率,后端吃的是这份图,不是原图。
DPI 在上传时传给 ghost:
| 档位 | 标志 | 约等于 |
|---|---|---|
| low | -ldpi |
100 DPI / 5 px/mm |
| medium | -mdpi |
250 DPI / 10 px/mm |
| high | -hdpi |
500 DPI / 20 px/mm |
| ultra | -udpi |
1000 DPI / 40 px/mm |
6.2 后端扫描
BitmapImage 读位置、尺寸、shading、threshold,缩放到 pixel_per_mm。BitmapFactory 再:
- 把图画到和工作区同尺寸的 raster
- 渐变:Floyd--Steinberg 抖动,连续灰阶打成接近二值的点阵
- 二值:按 threshold 直接黑白
- 从上到下、左右交替扫
pwm = (255 - gray) / 255 × 图层功率
白像素跳过,只在功率变化处落 G1,不是每个像素一条指令。行与行之间有约 5 mm 加速缓冲区,避免一开光就变速。
Fast Gradient(-fg)打开时,扫描行收成 F16 字节流(F16 1 H/M/L 表示 0.05 / 0.1 / 0.2 mm 分辨率),语义仍是按行扫,只是编码更紧。
7. G-code 长什么样
Flux 不是标准 GRBL。预览解析器 tmpParseGcode.js 认的是这套:
| 指令 | 含义 |
|---|---|
G1S / G1S0 |
空移,激光关(预览当 G0) |
G1V / G1V0 |
雕刻 / 切割,激光开(预览当 G1) |
G1 X.. Y.. F.. |
位置 + 进给(mm/min) |
S / U |
功率 |
T |
操作类型 |
Z / A |
对焦 / 旋转轴 |
F16 ... |
Fast Gradient 位图块 |
空移默认约 7500 mm/min。Y 在解析时取反,对齐左下原点。
下发机器用 Fcode;G-code 主要用于预览,也可经 g2f 再转回 Fcode。
8. 三条通道放在一张图里
#mermaid-svg-sP2e5gCCv2KRDBsi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sP2e5gCCv2KRDBsi .error-icon{fill:#552222;}#mermaid-svg-sP2e5gCCv2KRDBsi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sP2e5gCCv2KRDBsi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sP2e5gCCv2KRDBsi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sP2e5gCCv2KRDBsi .marker.cross{stroke:#333333;}#mermaid-svg-sP2e5gCCv2KRDBsi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sP2e5gCCv2KRDBsi p{margin:0;}#mermaid-svg-sP2e5gCCv2KRDBsi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi .cluster-label text{fill:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi .cluster-label span{color:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi .cluster-label span p{background-color:transparent;}#mermaid-svg-sP2e5gCCv2KRDBsi .label text,#mermaid-svg-sP2e5gCCv2KRDBsi span{fill:#333;color:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi .node rect,#mermaid-svg-sP2e5gCCv2KRDBsi .node circle,#mermaid-svg-sP2e5gCCv2KRDBsi .node ellipse,#mermaid-svg-sP2e5gCCv2KRDBsi .node polygon,#mermaid-svg-sP2e5gCCv2KRDBsi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sP2e5gCCv2KRDBsi .rough-node .label text,#mermaid-svg-sP2e5gCCv2KRDBsi .node .label text,#mermaid-svg-sP2e5gCCv2KRDBsi .image-shape .label,#mermaid-svg-sP2e5gCCv2KRDBsi .icon-shape .label{text-anchor:middle;}#mermaid-svg-sP2e5gCCv2KRDBsi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sP2e5gCCv2KRDBsi .rough-node .label,#mermaid-svg-sP2e5gCCv2KRDBsi .node .label,#mermaid-svg-sP2e5gCCv2KRDBsi .image-shape .label,#mermaid-svg-sP2e5gCCv2KRDBsi .icon-shape .label{text-align:center;}#mermaid-svg-sP2e5gCCv2KRDBsi .node.clickable{cursor:pointer;}#mermaid-svg-sP2e5gCCv2KRDBsi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sP2e5gCCv2KRDBsi .arrowheadPath{fill:#333333;}#mermaid-svg-sP2e5gCCv2KRDBsi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sP2e5gCCv2KRDBsi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sP2e5gCCv2KRDBsi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sP2e5gCCv2KRDBsi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sP2e5gCCv2KRDBsi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sP2e5gCCv2KRDBsi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sP2e5gCCv2KRDBsi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sP2e5gCCv2KRDBsi .cluster text{fill:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi .cluster span{color:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sP2e5gCCv2KRDBsi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sP2e5gCCv2KRDBsi rect.text{fill:none;stroke-width:0;}#mermaid-svg-sP2e5gCCv2KRDBsi .icon-shape,#mermaid-svg-sP2e5gCCv2KRDBsi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sP2e5gCCv2KRDBsi .icon-shape p,#mermaid-svg-sP2e5gCCv2KRDBsi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sP2e5gCCv2KRDBsi .icon-shape .label rect,#mermaid-svg-sP2e5gCCv2KRDBsi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sP2e5gCCv2KRDBsi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sP2e5gCCv2KRDBsi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sP2e5gCCv2KRDBsi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 空心 path / rect / line
闭合且 fill-opacity=1
若仍有细描边
image
导出 SVG
g.layer + 图元
图元类型?
fluxsvg → Beamify
曲线展成折线 get_array
沿点列 G1V
功率 = 图层恒定
Cairo fill_preserve
实心 PNG
按 DPI 扫描线
前端已灰度 / 阈值的 PNG
缩放到 pixel_per_mm
可选 Floyd-Steinberg
pwm = 灰阶 × 图层功率
G1V 扫行 / F16
GCodeMemoryWriter
或 FCodeMemoryWriter
同一图层里可以同时有空心路径、填充矩形和图片。后端按图层读 data-speed / data-strength,先处理该层里的扫描对象,再走矢量轮廓(或相反,取决于 factory 的 walk 顺序),data-repeat 整层重复。
9. 核心文件与方法
前端(beam-studio-core)
| 文件 | 关键方法 | 做什么 |
|---|---|---|
app/actions/beambox/export-funcs.ts |
fetchTaskCode、generateUploadFile、getGcode |
导出编排:转文字、刷图、上传、要任务码 |
helpers/api/svg-laser-parser.ts |
uploadToSvgeditorAPI、getTaskCode、divideSVG、gcodeToFcode |
WebSocket 协议封装 |
app/actions/beambox/font-funcs.ts |
tempConvertTextToPathAmoungSvgcontent、convertTextToPath |
文字 → path |
helpers/image-data.ts |
imageData() |
读图、缩放、EXIF、调灰度 |
helpers/grayscale.ts |
grayScale() |
灰阶 / 阈值 |
helpers/image/updateImagesResolution.ts |
updateImagesResolution(true) |
导出前全分辨率刷新 |
app/svgedit/operations/import/importSvg.ts |
importSvg |
导入时 divide 拆层 |
app/svgedit/operations/import/readBitmapFile.ts |
readBitmapFile |
位图进画布 |
app/svgedit/svgcanvas.ts |
setElemsFill、setElemsUnfill、calcPathClosed |
填充开关 |
app/views/.../InFillBlock.tsx |
onClick |
填充 UI |
app/components/.../GoButton.tsx |
空心 + 高速检查 | 矢量速度警告 |
app/views/beambox/tmpParseGcode.js |
parseGcode |
预览:G1S/G1V → 轨迹 |
后端
| 文件 | 关键方法 | 做什么 |
|---|---|---|
fluxghost/api/svgeditor_toolpath.py |
cmd_svgeditor_upload、cmd_go、cmd_g2f |
收场景、调生成、回传 |
fluxsvg/__init__.py |
parse、divide、divide_path_and_fill |
SVG 解析入口 |
fluxsvg/surface.py |
draw、填充/描边分流、bcontext.stroke |
三通道绘制 |
fluxsvg/path.py |
path() |
d → Cairo + Beamify |
fluxsvg/shapes.py |
rect / ellipse / polygon |
基本形 → Beamify |
Beamify context |
curve_to、ellipse_arc、sort、get_array |
曲线展平为折线 |
fluxclient/.../svgeditor_factory.py |
SvgeditorImage、_gen_svg_walk_path、_gen_bitmap_walk_path |
图层参数、点列 walk、扫描 walk |
fluxclient/.../toolpath.py |
svgeditor2taskcode |
walk 流 → G/Fcode |
fluxclient/.../laser.py |
svgeditor2laser、bitmap2laser |
较早的写码实现(公开仓库可见) |
前端仓库里没有路径偏置、扫描线生成或 PWM 计算。浏览器保证:图层参数正确、文字已是路径、位图已按模式处理好。刀路在 Python / C++ 后端。
10. 方案可以收成这样
矢量空心 :解析 d → Beamify 把 C/Q/A 展成折线 → 沿点列 G1V,功率等于图层。这是切割和刻线。
矢量填充 :闭合 + fill-opacity=1 → Cairo 涂实内部 → 当位图按 DPI 扫。不是 hatch。有细描边时,外框额外走一圈矢量。
位图:前端灰度/阈值 → 后端缩放到 DPI → 可选抖动 → 扫描线,功率随像素变。渐变用灰阶(或抖动后的点);二值只雕黑区。
三条路在 svgeditor2taskcode 汇合,按图层速度/功率/重复写成 Flux 的 G1S/G1V(或 F16),再包成 Fcode 下发。
理解这条链路,就理解了 Beam Studio 把「画布上的图」变成「机器上的烧痕」的核心技术路径。