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 --- fetchTaskCodegenerateUploadFilegetGcode
  • beam-studio-core/src/web/helpers/api/svg-laser-parser.ts --- uploadToSvgeditorAPIgetTaskCode
  • fluxghost/api/svgeditor_toolpath.py --- cmd_svgeditor_uploadcmd_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.tssvg-laser-parser.tsdivideSVG / uploadPlainSVG

4.2 路径命令怎么进后端

fluxsvg 是 CairoSVG 的分支。每个节点同时画到两个 context:

  • Cairo context:渲染、填充栅格
  • Beamify bcontext:激光刀路

path.pyd 逐命令翻译,曲线原样交给 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

空心矩形不经过 dshapes.py 直接 bcontext.rectangle(x, y, w, h)(圆角拆成直线 + 曲线)。对刀路来说,它和「四条边 + Z」的 path 是同一类闭合轮廓。

变换、裁剪、虚线也同步进 Beamify:beamify.Matrixclipset_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-shadingconvertShapeToBitmap 只用于喷墨打印层,激光填充不在浏览器里栅格化。


6. 位图通道:像素变成功率

导入的 PNG/JPG 是另一路:本来就是栅格,前端先做成「后端该雕成什么样」的 PNG,后端再扫。

6.1 前端预处理

<image> 带着元数据:

属性 作用
origImage 未处理原图,重算用
xlink:href 当前显示 / 导出的 PNG
data-shading true 渐变;false 二值
data-threshold 1--255,渐变默认 254,二值默认 128
data-fullcolor 喷墨全彩,不做灰度

灰度在 grayscale.ts0.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_mmBitmapFactory 再:

  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 fetchTaskCodegenerateUploadFilegetGcode 导出编排:转文字、刷图、上传、要任务码
helpers/api/svg-laser-parser.ts uploadToSvgeditorAPIgetTaskCodedivideSVGgcodeToFcode WebSocket 协议封装
app/actions/beambox/font-funcs.ts tempConvertTextToPathAmoungSvgcontentconvertTextToPath 文字 → 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 setElemsFillsetElemsUnfillcalcPathClosed 填充开关
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_uploadcmd_gocmd_g2f 收场景、调生成、回传
fluxsvg/__init__.py parsedividedivide_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_toellipse_arcsortget_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 svgeditor2laserbitmap2laser 较早的写码实现(公开仓库可见)

前端仓库里没有路径偏置、扫描线生成或 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 把「画布上的图」变成「机器上的烧痕」的核心技术路径。

相关推荐
公爵爱学习2 小时前
无人机定点飞行-介绍
开发语言·图像处理·python·学习·线性回归·无人机
maybeyaluokenai3 小时前
unity build in渲染管线概述
unity·图形渲染
Evand J19 小时前
【MATLAB例程,图像滤波5】 反谐波均值滑动窗口滤波(CHMF)图像降噪与质量评价,附代码下载链接
图像处理·算法·计算机视觉·matlab·均值算法·滑动窗口滤波·均值滑动
DolitD21 小时前
实时云渲染多应用并发:CELL容器技术突破3D隔离瓶颈
云原生·容器·实时互动·图形渲染·数据可视化
PhotonixBay1 天前
3D共聚焦显微镜与探针式粗糙度仪:谁更适合亚表面损伤检测?
图像处理·人工智能·测试工具·3d
AndrewHZ1 天前
图像处理入门026 | 分段线性变换:灰度窗与阶梯变换——CT 窗宽窗位的数学本质
图像处理·ct影像·分段线性变换·灰度窗·阶梯变换·密度分层·中间调拉伸
Ro Jace1 天前
基于深度学习的图像处理方法
图像处理·人工智能·深度学习
AndrewHZ2 天前
图像处理入门025 | 非线性灰度变换:伽马校正与 Log 变换——暗图救星的两条曲线
图像处理·伽马校正·log变换·非线性点运算·自动伽马·lut加速·暗图增强
AndrewHZ2 天前
图像处理入门024 | 线性灰度变换:亮度与对比度调整
图像处理·性能基准·线性灰度变换·亮度 β·对比度 α·uint8溢出·hsv/lab