Beam Studio:矢量图与位图如何变成 G-code

激光雕刻软件要解决的核心问题很具体:画布上的路径、矩形、文字和图片,最终必须变成机器能执行的一串运动指令。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 二进制流

对应代码:

  1. FontFuncs.tempConvertTextToPathAmoungSvgcontent()
    用 fontkit 抽 glyph 轮廓;失败再走 ghost 的 uploadPlainTextSVG + divideSVG。到后端时文字已经是 <path>。
  2. updateImagesResolution(true)
    用 origImage 按当前 shading / threshold 重算全分辨率 PNG,写回 xlink:href。
  3. generateUploadFile()
    svgCanvas.getSvgString(),和缩略图拼成 [thumbnail][svg]。
  4. svgeditorParser.uploadToSvgeditorAPI()
    WebSocket 发 svgeditor_upload,带上机型和 DPI。
  5. svgeditorParser.getTaskCode()
    发 go ... -f,要 G-code 再加 -gc。

核心文件:

  • beam-studio-core/src/web/app/actions/beambox/export-funcs.ts --- fetchTaskCode、generateUploadFile、getGcode
  • beam-studio-core/src/web/helpers/api/svg-laser-parser.ts --- uploadToSvgeditorAPI、getTaskCode
  • fluxghost/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>:

  1. shapes.rect() 建矩形几何
  2. fill_context.fill_preserve() 把内部涂实
  3. 导出 PNG,按雕刻 DPI 对齐工作区
  4. 按行扫描,和实心黑图一样
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 再:

  1. 把图画到和工作区同尺寸的 raster
  2. 渐变:Floyd--Steinberg 抖动,连续灰阶打成接近二值的点阵
  3. 二值:按 threshold 直接黑白
  4. 从上到下、左右交替扫
  5. 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 把「画布上的图」变成「机器上的烧痕」的核心技术路径。

相关推荐
Zentceh20 小时前
PixelClean 这类 AI 降噪算法是怎么工作的?从 RAW 域到 GAN 色彩重建的技术拆解
图像处理·人工智能·科技·算法·计算机视觉·车载系统·智能硬件
ss.li1 天前
rppg阅读笔记 2026.10.15
图像处理·python·计算机视觉
DolitD1 天前
水利数字孪生走向深水区,点量云流实时云渲染落地实践
安全·云原生·云计算·图形渲染
波力海苔夹心脆6752 天前
VisionPro 与 VisionMaster 对比(上):软件定位、开发环境与核心视觉工具对照
图像处理·算法·计算机视觉·c#·视觉检测·个人开发·vp9
Martina_03212 天前
一张地图怎么生成可编辑3D地形?用6步检查高度图、道路约束与Mesh导出
图像处理·人工智能·平面·3d·ai·自然语言处理·游戏策划
遇码2 天前
macOS 触摸板捏合在 GPUI 里无响应:vendor 打补丁,把手势合成为 cmd+滚轮事件
rust·图形渲染·策略模式·桌面应用·gpui
小虎牙^O^2 天前
棋盘格隐写:基于奇偶相位的双图隐藏与还原
图像处理·python·安全
u0111026752 天前
JPG、PNG、WebP 怎么选?按透明度、体积和使用场景做判断
前端·javascript·vue.js·图像处理·ai作画
lank_M3 天前
WebCodecs解码竖拍视频变横的?四个浏览器实测
图像处理·人工智能·计算机视觉·音视频
陈嘿萌3 天前
学术速递|2026年8月 arXiv 图像融合论文汇总(08.01–08.31)29 篇最新成果
图像处理·人工智能·深度学习·计算机视觉·图像融合·学术研究与理论基础