artcraft 这名字听起来挺“缝合”的,一边是艺术,一边是手艺。但我做这个开源项目的初衷,恰恰就是想把这两样硬生生拼在一起——用代码生成那些我自己手画不出来的图案,再把它们变成真能拿在手里的木杯垫、亚克力挂件、皮革吊牌,甚至一组刺绣线稿。artcraft 本质上是一套连接“生成艺术”和“实体工艺”的个人工具链,目前核心语言是 Python,最终产物以 SVG 矢量文件为主,可以被激光切割机、CNC 雕刻机、刻字机这类常见制作设备直接消费。如果你平时会写点代码,又喜欢折腾手工,这篇文章应该能给你一套可以马上复制的工作流;就算你对编程不熟,也能顺着思路理解“数字设计到实体制作”到底在哪一步容易卡壳。
1. 内容整体设计与思路拆解
1.1 为什么偏偏是 art + craft
做手工的人不一定写代码,写代码的人不一定做手工,这话看似废话,但真正想打通两者的人少得可怜。我身边玩木工的朋友要设计一个花纹,通常是在纸上手绘,然后扫描、描图、导入设备,改一次尺寸就得重画;而程序员朋友能一行代码生成一万种分形,可他们很少会把文件导出成能切割的路径。
后来我发现,这两类人其实都在处理同一个东西:形状。手工是材料上的形状,代码是逻辑上的形状。如果能让代码直接输出工艺设备能读懂的矢量文件,那创意就不受“画功”限制,反而能批量落地。
我最早动这个念头,是想做一套十二生肖镂空灯罩。手绘十二个轮廓画到半夜就放弃了,但用参数化脚本,每个生肖只需要一个基础轮廓,再用不同的重复纹理填充,就能快速生成整套方案。artcraft 这个名字就是那时候定下来的:艺术创意的“art”,手工工艺的“craft”,中间不搞什么复杂概念,就是把两者之间的路修通。
1.2 选型:从 SVG 切入,而不是直接造轮子
项目启动前,我在“用什么中间格式”上纠结了很长时间。试过直接用位图 PNG,后来发现这条路有硬伤。激光切割机和 CNC 需要的是连续的线段路径,而位图放大后会产生锯齿,大部分设备软件做矢量化时会额外塞进一堆转折点,结果就是切割速度变慢,切出来的边缘还可能毛糙。所以,我需要一个天然带“图形结构”的格式,而不是一堆像素点。
SVG 是那个被我反复挑中又没被换掉的答案。它本身就是文本文件,可以用 Python 直接生成和修改,所有软件都能打开预览,而且激光、雕刻、刻字这类工具几乎都支持 SVG 导入。更关键的是,SVG 里可以清晰地表达图元(圆、矩形、贝塞尔曲线)和图层,这让“哪些线是切割、哪些线是雕刻”有了可编程区分的空间。
为什么不直接用三维软件?不是不行,而是对大多数手工爱好者来说,学习成本太高。而且用参数化建模软件折腾一个平面图案,经常是杀鸡用牛刀。SVG 的抽象层级刚好卡在“足够描述图案”和“足够容易编程”之间,学习曲线友好得多。
编程语言的选型也顺理成章用了 Python。一是生态齐全,svgwrite负责生成 SVG,shapely负责处理几何关系,svgpathtools可以读取和改写路径;二是可以直接写批量脚本,一次跑出几十个方案,这是纯手工做不到的效率。
1.3 架构:三层设计,一次生成多样输出
artcraft 的整个架构被我拆成了三层,每层只干一件事,这样即使某个环节写得很烂,也不至于影响整个流程。
- 生成层:根据参数和算法生成 SVG 文件,比如随机流场、分形树、波纹图案。
- 处理层:对 SVG 里的路径做优化,比如合并重叠区域、去除孤立点、闭合缝隙、调整图层颜色。
- 输出层:把处理好的 SVG 按目标设备导出成 DXF、PDF,或者直接交给切割控制软件。
目录结构上,我会按功能模块分开放,看起来大概这样:
artcraft/ ├── generators/ # 图案生成算法 │ ├── ripple.py │ ├── fractal.py │ └── tile.py ├── processors/ # 路径处理和优化 │ ├── merge_paths.py │ └── cleanup.py ├── exporters/ # 输出格式转换 │ ├── to_dxf.py │ └── to_pdf.py ├── recipes/ # 每件作品的配置文件 │ ├── coaster_wood.json │ └── wall_hanging.json └── main.py这里我特别想强调recipes/这个目录。每一件作品都对应一个 JSON 配置,里面写着尺寸、图案类型、材料、处理参数。比如“木杯垫”的配置就是:边长 100mm、图案 ripple、材料 wood_3mm。这样做的最大好处是,每件作品都是一条可复制的流水线,而不是一个孤零零的脚本。你拿到一份配置,改几个数字,就能得到新作品。
2. 核心细节解析与实操要点
2.1 生成算法:一个能跑的 SVG 生成器
artcraft 的灵魂,其实是那些“看起来复杂,代码却很短”的生成算法。我第一个做出来的效果是“波形涟漪”,就是一圈一圈像水面扩散的同心圆环,但圆心会在网格上轻微偏移,形成一种不稳定但又规律的波纹。这种图案手绘很麻烦,但代码写起来很直观。
下面是一个简化版本的生成脚本,你可以存下来直接跑:
import svgwrite W, H = 300, 300 # 画布尺寸,单位目前是 SVG 内部单位,后续再换算成 mm RADIUS = 15 # 基准半径 RING_STEP = 12 # 每圈半径增量 X_VAR = 3 # 圆心 x 方向偏移范围 Y_VAR = 3 # 圆心 y 方向偏移范围 # 初始化画布 dwg = svgwrite.Drawing('ripple.svg', size=(W, H)) # 在网格上生成圆环 for y in range(20, H, 30): for x in range(20, W, 30): # 给圆心加一点随机偏移 cx = x + (hash((x, y)) % (X_VAR * 2) - X_VAR) cy = y + (hash((x * 7, y * 7)) % (Y_VAR * 2) - Y_VAR) # 每个位置画三层同心圆 for r in range(RADIUS, RADIUS + 3 * RING_STEP, RING_STEP): dwg.add(dwg.circle(center=(cx, cy), r=r, stroke='black', fill='none')) dwg.save()这段代码的逻辑就是遍历一个网格,在每个格点画几个同心圆,圆心的位置基于“坐标哈希”产生一个看起来随机、但同一个坐标永远固定的偏移。用这种方式,不需要引入随机种子也能得到稳定的输出。
这里有个新手容易忽略的点:SVG 的坐标系是 Y 轴向下,也就是屏幕坐标体系。如果你要在纸上设计,可能会觉得“数学”里 Y 轴应该向上,但在 SVG 里不需要反转,图案生成时按照屏幕坐标思考反而更顺手。如果最后导入设备发现图案上下镜像了,那是导入设置的问题,后文会专门说。
2.2 从“好看的图”变成“能切割的路径”
生成出来的 SVG 第一眼很漂亮,但直接拿去切割往往会出问题,因为“屏幕显示”和“设备路径”是两套逻辑。屏幕上的线宽、颜色都只是视觉表现,而切割设备真正看的是路径数据和图层。换句话说,你在 SVG 里看到一条细线和一条粗线,在激光切割机看来可能没有太大区别,除非你为它们分配了不同的“动作”。
所以我在 processors 里做了两件关键的事。
第一件事是路径合并。比如两个方形的装饰块靠在一起,边缘重叠了一部分,如果不做合并,激光会在重叠区域走两次,这会导致重复灼烧、材料碳化,甚至切穿。用shapely做布尔并集,再取外轮廓,问题就解决了:
from shapely.geometry import Polygon from shapely.ops import unary_union # 假设这两个矩形对应的坐标 poly1 = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]) poly2 = Polygon([(8, 0), (18, 0), (18, 10), (8, 10)]) merged = unary_union([poly1, poly2]) print(merged.wkt) # 输出一个合并后的多边形坐标第二件事是简化路径。设备处理 SVG 时,点越多,运算越慢,文件越大。圆滑的曲线本来可以用贝塞尔曲线表示,但如果某些环节把它转成了折线,就需要做抽稀。shapely里有现成的simplify方法,容差我一般取0.05mm,太小了没效果,太大了会让原本圆润的图案明显多边形化。
还有一点:SVG 里用stroke-width来控制线宽,但很多切割软件会忽略它,更常见的是按图层颜色来区分“切割”和“雕刻”。所以在输出层,我会规规矩矩地把“切割线”设为纯红色(#ff0000),“雕刻线”设为纯黑色(#000000),再在 JSON 配置里注明这些图层的动作。这样进设备软件后,按颜色就能快速分配参数。
2.3 材料和设备对输出格式的影响
同样是 SVG,激光切割、CNC 雕刻、刻字机需要的东西不太一样。激光切割看重的是闭合轮廓和线颜色;CNC 雕刻看重的是刀具半径补偿和加工深度,一个开放的轮廓可能被当成铣削路径;刻字机则需要图形的轮廓线连续,不然刀刃会走一半断掉。
我习惯按材料整理一份初始参数表,每次拿到新材料都会先做一个小方块测试,而不是直接套用表里的数字。下面是几个常见材料在入门级激光设备上的参考值,仅作思路启发:
| 材料 | 厚度 | 切割速度 | 功率 | 备注 |
|---|---|---|---|---|
| 椴木板 | 3mm | 20mm/s | 60% | 需要测试焦距,切深大约在材料厚度 80% |
| 亚克力 | 3mm | 15mm/s | 45% | 切太快容易发黄,切太慢容易局部融化 |
| 纸卡 | 0.5mm | 50mm/s | 20% | 测试结构时尽量用纸卡,便宜又快捷 |
| 皮革 | 2mm | 30mm/s | 35% | 注意通风,气味很大 |
这个表最大的价值不是让你照抄,而是给你一个起点。每台设备的光路、镜片损耗、环境温度都会影响实际效果,所以“切割前必测试”是我在这个项目里养成的最硬核习惯。
3. 实操过程与核心环节实现
3.1 环境准备
如果你从零开始搭这套环境,我建议直接用 Python 3.10 以上版本。接着用 pip 安装三个核心库就行:
pip install svgwrite shapely svgpathtools这三个库的分工很明确:svgwrite负责“写 SVG”,shapely负责“算几何”,svgpathtools负责“读和改路径”。如果后面要输出 DXF,再补一个ezdxf库,但前期用 SVGs 就能跑通全流程。
我一直强调环境要“干净、可复现”。每个作品建一个虚拟环境,requirements.txt固定版本,这样过了半年再来跑当年的项目,不会因为库更新导致路径计算崩掉。
3.2 做一个木杯垫的完整流程
我们直接做一件具体的东西:六边形“涟漪”杯垫,边长定在 100mm,用 3mm 椴木板激光切割。
第一步,写 recipe 文件:
{ "name": "coaster_ripple_hex", "shape": "hexagon", "size_mm": 100, "material": "basswood_3mm", "pattern": "ripple", "layers": { "cut": "#ff0000", "engrave": "#000000" } }第二步,写生成脚本。核心思路是:先算六边形的外接圆半径,再在六边形内部生成波纹路径。代码大致是这样:
import math import svgwrite SIZE_MM = 100 CENTER = SIZE_MM / 2 R = SIZE_MM * 0.5 / math.cos(math.radians(30)) # 六边形外接圆半径 dwg = svgwrite.Drawing('coaster_ripple_hex.svg', size=(f'{SIZE_MM}mm', f'{SIZE_MM}mm'), viewBox=f'0 0 {SIZE_MM} {SIZE_MM}') # 六边形外轮廓,作为切割层 points = [] for i in range(6): angle = math.radians(60 * i - 30) x = CENTER + R * math.cos(angle) y = CENTER + R * math.sin(angle) points.append((x, y)) hex_poly = dwg.polygon(points, stroke='red', fill='none', stroke_width=0.1) dwg.add(hex_poly) # 内部波纹,作为雕刻层 for ring in range(4): rr = R * 0.55 - ring * 8 if rr < 2: break path = [] for step in range(360): angle = math.radians(step) x = CENTER + rr * math.cos(angle) + 2 * math.sin(angle * 3) y = CENTER + rr * math.sin(angle) path.append((x, y)) polyline = dwg.polyline(path, stroke='black', fill='none', stroke_width=0.2) dwg.add(polyline) dwg.save()第三步,在浏览器里打开生成的 SVG 检查。这里要特别注意两点:一是外轮廓是否完整,二是波纹线是否有跑到六边形外面的情况。代码中波纹半径乘了0.55,就是故意留出安全边距,避免雕刻线碰到切割线导致崩边。
第四步,导入你常用的激光切割控制软件。按之前说的,红色图层设置成“切割”,功率速度参考椴木板参数;黑色图层设置成“雕刻”,速度可以适当放慢,让凹痕更清晰。
这里我踩过最大的坑是单位换算。SVG 是一种很灵活的格式,我可以同时设置size和viewBox。如果只设置size而不设置viewBox,有些软件会以屏幕像素为单位识别,结果 100mm 的东西被当成 100px,实际尺寸只有 2.6mm。后来我强制自己在size里带上mm单位,并且设定与毫米一致的viewBox,才避免尺寸漂移。
第五步,正式切割之前先用废料测试。我会切一个 30mm 见方的小块,先看功率和速度是否匹配,再开整件作品。椴木板第一次切割时,如果边缘发黑严重,我会把速度提高 10%,或者把功率调低一点。
3.3 批量变体:一次跑出二十个不同版本
artcraft 真正让我觉得“值了”的时刻,是批量生成。就在做杯垫那段时间,朋友说想要一套不同波纹密度的杯垫。手工做的话,我得重新画一个又一个图形,但写脚本时,只需要循环修改ring的数量和偏移幅度:
for i, density in enumerate([2, 4, 6, 8]): # 生成不同版本 generate_coaster(density=density, filename=f'coaster_v{i}.svg')这样一次能得到一套渐变纹理的杯垫。每一个文件对应一个 JSON 配置,切割时我可以按文件名顺序排布在同一块板材上,共用同一组切割参数,效率一下子提上去了。
我还习惯随手生成一个“参数记录.txt”,把每个文件的密度、圆环数、材料、日期写进去。因为后期想要复刻某一次最有质感的切割效果时,如果没有记录,你是完全想不起来当时用的哪组参数。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
做这个项目半年,我整理了下面这些最常见的问题,如果你刚好卡在某个环节,可以对照着查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 电脑上正常,设备不识别文件 | SVG 里有文字、复杂形状或不受支持的指令 | 全部转为路径,文字转曲,统一成标准路径指令 |
| 切割尺寸偏小或偏大 | size和viewBox不一致,或单位当成了像素 | 生成时强制改用 mm 单位,并设置对应的viewBox |
| 激光切了一圈但图案掉不下来 | 轮廓线没有闭合,或者闭合路径中有微小缺口 | 用shapely做buffer(0)修复缺口 |
| 切割边缘烧焦严重 | 功率太高或速度太慢 | 先降低功率 10%,速度不变,做小样测试 |
| 同一路径重复切割,有明显凹槽 | 多个几何图形重叠未合并 | 用布尔并集unary_union合并 |
| 设备打开文件后特别卡 | 路径点太多,文件体积过大 | 用simplify抽稀,或减少贝塞尔曲线转换点数 |
| 图案上下/左右镜像 | SVG 坐标系与设备坐标系方向不同 | 在软件里翻转导入,或在生成时反转 Y 坐标 |
这个表里,我最想强调“轮廓线闭合”的问题。很多生成算法画出来的线条首尾之间只差零点几毫米,肉眼看不见,但激光走到缺口时就会熄火或者不回程,导致整片零件废掉。现在我的生成器里默认会调用一次shapely的闭合修复,用极小的容差把断点接上。
4.2 几个文档外的小坑
第一,中文文件名。一开始我情怀上头,给文件命名“杯垫_波浪.svg”,结果激光软件怎么都读不出来,而英文名文件秒开。后来查了下,很多设备固件的中文字符支持并不完整,一旦读不出并不会报错,而是直接不显示图形。现在我的项目里所有导出文件名统一用拼音或数字:beidian_v1.svg、coaster_ripple_v1.svg,配合 JSON 里的中英文备注,既保命又清晰。
第二,图层颜色别用杂色。切割软件按颜色分配功率时,如果一条路径的颜色因为抗锯齿变成了粉色或者浅红,可能被识别成别的图层,导致本该切割的路径变成了雕刻。所以我在处理层会把所有颜色强制归一化,只保留纯红、纯黑、纯蓝几种预设值。
第三,试切一定要用“和最终材料相同厚度”的废料。用纸板测试和用 3mm 木板测试,焦距和速度差很多。纸板只能验证图案结构对不对,不能验证真实切割参数。
4.3 用纸板做全流程预演
我强烈建议每个新作品先做一次“纸板预演”。方法特别简单:把生成的 SVG 导入软件,用 0.5mm 厚纸板切割一遍,得到样品后,用剪刀和小刀手工修整一下,看看结构是否完整。这一步能暴露 90% 的逻辑问题,比如拼接口不对、纹理卡位不合理、图案边界太靠近切割线。
纸板预演的成本几乎可以忽略,但价值极高。它能让你在浪费木材、亚克力这些相对贵的材料之前,先跟自己的设计“握手”。我后来甚至会在纸板上做多个变量对比,把不同参数的图案打成一个测试卡,一次切完,放上桌面对照着看,瞬间就能选出最好看的那版。
做完这些,我最大的感受是:工具链越自动化,越要保留手工感。artcraft 虽然能把图案批量生成,但每次实际制作时,我仍然要亲手调整参数、喷漆、打磨。这个项目不是为了替代手工,而是把重复劳动交给算法,把手感留给人。最后再分享一个小技巧:生成 SVG 后至少隔一夜再看一遍,第二天的眼睛总能发现昨天没注意的重叠线和方向问题。