From b7f90b0c9e3c73f335a51a9ca7121e4ed0a74558 Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 14:57:32 +0800 Subject: [PATCH 01/12] =?UTF-8?q?manual=20=E8=A1=A5=E5=85=85=EF=BC=8C?= =?UTF-8?q?=E5=B0=9D=E8=AF=95=E8=BF=81=E7=A7=BB=20cpu=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/march.txt | 18 ++-- konabot/plugins/marchtoy/__init__.py | 1 + konabot/plugins/marchtoy/command.py | 6 +- konabot/plugins/marchtoy/cpu_render.py | 109 +++++++++++++++++++++ konabot/plugins/marchtoy/gl_render.py | 5 +- konabot/plugins/marchtoy/obj.py | 63 +++++++++--- konabot/plugins/marchtoy/shaders/frag.glsl | 8 -- konabot/plugins/marchtoy/skia_render.py | 9 +- konabot/plugins/marchtoy/utilities.py | 2 +- tests/test_marchtoy_transform.py | 17 ---- 10 files changed, 179 insertions(+), 59 deletions(-) create mode 100644 konabot/plugins/marchtoy/cpu_render.py delete mode 100644 tests/test_marchtoy_transform.py diff --git a/konabot/docs/user/march.txt b/konabot/docs/user/march.txt index 2f23961..b2b3dae 100644 --- a/konabot/docs/user/march.txt +++ b/konabot/docs/user/march.txt @@ -1,22 +1,18 @@ # 指令介绍 简易 Raymarch 小玩具 -用法:march +用法:march `` 例:march sphere(1).color(red) box(0.5, 2.0, 0.5).pos(0, 0, 0) cam(0.5).pos(-5).lookat(0) # 主要语法 - ::= "." - | +`` ::= `` "." `` |`` - ::= - | "(" ")" +`` ::= `` | `` "(" ")" - ::= - | "(" ")" +`` ::= `` | `` "(" `` ")" - ::= "," - | +`` ::= `` "," `` | `` -其中 obj_ty、op_ty 分别为物体类型(如 cube、sphere、torus 等)与变换类型(如 pos、rot)。 +其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。 # 特殊说明 - 不包含 scale,因为本工具基于 SDF 渲染,非正交的变换会破坏 SDF 的性质。 \ No newline at end of file +`` 不包含 scale。非正交的变换会破坏 SDF 的性质。 \ No newline at end of file diff --git a/konabot/plugins/marchtoy/__init__.py b/konabot/plugins/marchtoy/__init__.py index 7e48840..68acf25 100644 --- a/konabot/plugins/marchtoy/__init__.py +++ b/konabot/plugins/marchtoy/__init__.py @@ -3,6 +3,7 @@ from nonebot.adapters import Message from nonebot_plugin_alconna import UniMessage from nonebot.params import CommandArg import konabot.plugins.marchtoy.gl_render as render +# import konabot.plugins.marchtoy.cpu_render as render import io cmd_marchtoy = on_command("march") @cmd_marchtoy.handle() diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index 88e8aa9..005e54b 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -19,9 +19,9 @@ import re import numpy as np from dataclasses import dataclass from typing import Optional + from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES from konabot.plugins.marchtoy.op import OPERATION_ENTRIES - @dataclass class Command: id: str @@ -103,7 +103,7 @@ class Scene: raise Exception(f"{op_id} is not a valid operation.") try: - sdf_block = obj_instance.sdf_block() + sdf_block = obj_instance.sdf_block_glsl() self.canvas_objs.append((obj_instance, sdf_block)) except: if type(obj_instance) == Camera: @@ -127,7 +127,7 @@ class Scene: sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" color = obj.texture.color color_block += ( - f"if(obj_id == {index}) return float4(" + f"if(obj_id == {index}) return vec4(" f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n" ) index += 1 diff --git a/konabot/plugins/marchtoy/cpu_render.py b/konabot/plugins/marchtoy/cpu_render.py new file mode 100644 index 0000000..4836d46 --- /dev/null +++ b/konabot/plugins/marchtoy/cpu_render.py @@ -0,0 +1,109 @@ +from typing import Optional + +import numpy as np +from PIL import Image + +from konabot.plugins.marchtoy.command import Scene +from konabot.plugins.marchtoy.obj import Object, Transform + + +class SDQuery: + def __init__(self, _value: float = np.inf, _obj: Optional[Object] = None): + self.value: float = _value + self.obj: Optional[Object] = _obj + + +class Rasterizer: + EPS = 0.001 + MAX_ITER = 128 + MAX_DIST = 128.0 + + def __init__(self, _scene: Scene, _res: tuple[int, int]): + self.scene = _scene + self.width, self.height = _res + self.fb_rgba = np.zeros((self.height, self.width, 4), dtype=np.float32) + + def sdf(self, p: np.ndarray) -> SDQuery: + qry = SDQuery() + for obj, _ in self.scene.canvas_objs: + sd = obj.sdf(p) + if sd < qry.value: + qry.value = sd + qry.obj = obj + return qry + + def nrm(self, p: np.ndarray) -> np.ndarray: + dx = np.array((self.EPS, 0.0, 0.0)) + dy = np.array((0.0, self.EPS, 0.0)) + dz = np.array((0.0, 0.0, self.EPS)) + grad = np.array( + [ + self.sdf(p + dx).value - self.sdf(p - dx).value, + self.sdf(p + dy).value - self.sdf(p - dy).value, + self.sdf(p + dz).value - self.sdf(p - dz).value, + ], + dtype=np.float32, + ) + return Transform.normalize(grad) + + def color(self, p: np.ndarray, obj: Object) -> np.ndarray: + normal = self.nrm(p) + light_dir = Transform.normalize(np.array((0.5, 0.8, -0.6), dtype=np.float32)) + light = 0.2 + 0.8 * max(float(np.dot(normal, light_dir)), 0.0) + base = np.array(obj.texture.color, dtype=np.float32) + return np.array((base[0] * light, base[1] * light, base[2] * light, base[3])) + + def march(self, origin: np.ndarray, direction: np.ndarray) -> np.ndarray: + p = origin.copy() + travel = 0.0 + + for _ in range(self.MAX_ITER): + qry = self.sdf(p) + if not np.isfinite(qry.value): + break + if qry.value < self.EPS and qry.obj is not None: + return self.color(p, qry.obj) + if qry.value > self.MAX_DIST or travel > self.MAX_DIST: + break + p += direction * qry.value + travel += qry.value + + return np.zeros(4, dtype=np.float32) + + def camera_basis(self) -> tuple[np.ndarray, np.ndarray]: + cam = self.scene.camera + cam_pos = cam.transform.t[0:3, 3] + cam_z = Transform.normalize( + (cam.transform.t @ np.array((0.0, 0.0, -1.0, 0.0), dtype=np.float32))[:3] + ) + world_up = ( + np.array((0.0, 0.0, 1.0), dtype=np.float32) + if abs(cam_z[1]) > 0.999 + else np.array((0.0, 1.0, 0.0), dtype=np.float32) + ) + cam_x = Transform.normalize(np.cross(cam_z, world_up)) + cam_y = Transform.normalize(np.cross(cam_x, cam_z)) + view = np.column_stack((cam_x, cam_y, cam_z)) + return cam_pos, view + + def rasterize(self) -> np.ndarray: + cam_pos, view = self.camera_basis() + resolution = np.array((self.width, self.height), dtype=np.float32) + aspect = np.array((self.width / self.height, 1.0), dtype=np.float32) + cam_focus = float(self.scene.camera.focus) + + for y, x in np.ndindex(self.height, self.width): + frag_coord = np.array((x + 0.5, y + 0.5), dtype=np.float32) + uv = 2.0 * (frag_coord / resolution - 0.5) * aspect + ray_local = Transform.normalize(np.array((uv[0], uv[1], cam_focus))) + ray_world = Transform.normalize(view @ ray_local) + self.fb_rgba[y, x] = self.march(cam_pos, ray_world) + + return self.fb_rgba + + +async def render(command: str, res: tuple[int, int]): + rasterizer = Rasterizer(Scene(command), res) + fb_output = rasterizer.rasterize() + rgba = np.clip(fb_output * 255.0, 0.0, 255.0).astype(np.uint8) + return Image.fromarray(rgba, mode="RGBA") diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index 202ea2a..33553b4 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -12,7 +12,7 @@ async def render(command: str, res: tuple[int, int]): PATH = pathlib.Path(__file__).parent / "shaders" with (PATH / "vert.glsl").open(encoding='utf-8') as f: vs = f.read() - ctx = moderngl.create_context(standalone=True) + ctx = moderngl.create_context(standalone=True, backend='egl') ctx.gc_mode = "auto" try: program = ctx.program( @@ -32,4 +32,5 @@ async def render(command: str, res: tuple[int, int]): fbo.use() fbo.clear(0.0, 0.0, 0.0, 0.0) vao.render(moderngl.TRIANGLES) - return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1) \ No newline at end of file + return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1) + diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 58512d5..366d383 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -72,7 +72,7 @@ class Transform: def p_expr(self) -> str: inv = np.linalg.inv(self.t) # + 1e-5 * np.identity(4, dtype=np.float32)) - return f"({Formatter.float4(inv)} * vec4(p, 1.0)).xyz" + return f"({Formatter.vec4(inv)} * vec4(p, 1.0)).xyz" class Object: @@ -83,9 +83,20 @@ class Object: def parse_args(self, args: list[str]): raise NotImplementedError - def sdf_block(self) -> str: + def get_transformed(self, p: np.ndarray) -> np.ndarray: + if p.shape != (3,): + raise Exception(f"{p} is not a vec3") + p = p.copy() + inv = np.linalg.inv(self.transform.t) + + return (inv @ np.array((*p, 1)))[:3] + + def sdf_block_glsl(self) -> str: raise NotImplementedError + def sdf(self, _p: np.ndarray) -> float: + return NotImplementedError + @make_obj("cube", "box") class Cube(Object): @@ -96,9 +107,14 @@ class Cube(Object): def parse_args(self, args: list[str]): self.size = ArgParser.as_vec3(args) - def sdf_block(self) -> str: + def sdf_block_glsl(self) -> str: return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))" + def sdf(self, _p): + p = self.get_transformed(_p) + p = np.abs(p) - self.size + return np.linalg.norm(np.maximum(p, 0.0)) + np.minimum(np.max(p), 0.0) + @make_obj("sphere", "ball") class Sphere(Object): @@ -109,9 +125,13 @@ class Sphere(Object): def parse_args(self, args: list[str]): self.radius = ArgParser.as_float(args) - def sdf_block(self) -> str: + def sdf_block_glsl(self) -> str: return f"sdSphere({self.transform.p_expr()}, {self.radius})" + def sdf(self, _p): + p = self.get_transformed(_p) + return np.linalg.norm(p) - self.radius + @make_obj("cylinder", "cyl") class Cylinder(Object): @@ -125,11 +145,20 @@ class Cylinder(Object): self.radius = param[0] self.height = param[1] - def sdf_block(self) -> str: + def sdf_block_glsl(self) -> str: return ( f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})" ) + def sdf(self, _p): + p = self.get_transformed(_p) + d = np.abs(np.array([np.linalg.norm(p[[0, 2]]), p[1]])) - np.array( + [self.radius, self.height] + ) + return np.minimum(np.maximum(d[0], d[1]), 0.0) + np.linalg.norm( + np.maximum(d, 0.0) + ) + @make_obj("torus") class Torus(Object): @@ -143,24 +172,34 @@ class Torus(Object): self.r1 = param[0] self.r2 = param[1] - def sdf_block(self) -> str: + def sdf_block_glsl(self) -> str: return f"sdTorus({self.transform.p_expr()}, vec2({self.r1}, {self.r2}))" + def sdf(self, _p): + p = self.get_transformed(_p) + q = np.array([np.linalg.norm(p[[0, 2]]) - self.r1, p[1]]) + return np.linalg.norm(q) - self.r2 + @make_obj("capsule", "pill") class Capsule(Object): def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None: super().__init__() - self._h = _h - self._r = _r + self.h = _h + self.r = _r def parse_args(self, args: list[str]): param = ArgParser.as_vec2(args) - self._h = param[0] - self._r = param[1] + self.h = param[0] + self.r = param[1] - def sdf_block(self) -> str: - return f"sdVerticalCapsule({self.transform.p_expr()}, {self._h}, {self._r})" + def sdf_block_glsl(self) -> str: + return f"sdVerticalCapsule({self.transform.p_expr()}, {self.h}, {self.r})" + + def sdf(self, _p): + p = self.get_transformed(_p) + p[1] -= np.clip(p[1], 0.0, self.h) + return np.linalg.norm(p) - self.r @make_obj("camera", "cam") diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index 34a12ac..a7b7fab 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -1,16 +1,8 @@ #version 330 -// compatibility -#define float4x4 mat4x4 -#define float4 vec4 -#define float3 vec3 -#define float2 vec2 - const float EPS = 0.001; const int MAX_ITER = 128; - uniform vec2 u_resolution; out vec4 fragColor; - struct sdQuery { float value; int obj_id; diff --git a/konabot/plugins/marchtoy/skia_render.py b/konabot/plugins/marchtoy/skia_render.py index f04ef40..793ea53 100644 --- a/konabot/plugins/marchtoy/skia_render.py +++ b/konabot/plugins/marchtoy/skia_render.py @@ -1,4 +1,3 @@ -raise DeprecationWarning from command import Scene import skia import struct @@ -8,12 +7,12 @@ import numpy as np # 暂时先照抄小帕的了,之后有空再单独封装一下 -async def render(cmd: str, width: int, height: int): - scene = Scene(cmd) +async def render(instruction: str, res: tuple[int, int]): + scene = Scene(instruction) + width, height = res surface = skia.Surface(width, height) sksl_code = scene.compile() - - runtime_effect = skia.RuntimeEffect.MakeForShader(scene.compile()) + runtime_effect = skia.RuntimeEffect.MakeForShader(sksl_code) if runtime_effect is None: raise Exception("cannot compile sksl shader") diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index 7ff1b7e..34d18f7 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -3,7 +3,7 @@ from konabot.plugins.marchtoy.texture import COLORS class Formatter: @staticmethod - def float4(m: np.ndarray) -> str: + def float4x4(m: np.ndarray) -> str: if m.shape != (4, 4): m = np.identity(4) v_0 = ", ".join([str(x) for x in m[:, 0]]) diff --git a/tests/test_marchtoy_transform.py b/tests/test_marchtoy_transform.py deleted file mode 100644 index add962f..0000000 --- a/tests/test_marchtoy_transform.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys -from pathlib import Path - - -PLUGIN_DIR = Path(__file__).resolve().parents[1] / "konabot" / "plugins" / "marchtoy" -if str(PLUGIN_DIR) not in sys.path: - sys.path.insert(0, str(PLUGIN_DIR)) - -from obj import Transform - - -def test_translate_expression_puts_offset_in_matrix_column(): - transform = Transform().translate(1.0, 2.0, 3.0) - - expr = transform.p_expr() - - assert "float4(-1.0, -2.0, -3.0, 1.0)" in expr From 88f1f45b9490cf3e9629c9f372ab16b214893b18 Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 15:28:13 +0800 Subject: [PATCH 02/12] =?UTF-8?q?manual=20=E8=A1=A5=E5=85=85=EF=BC=9B=20cp?= =?UTF-8?q?u=20=E5=86=99=E6=B3=95=E9=80=9F=E5=BA=A6=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E6=8E=A5=E5=8F=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/march.txt | 18 ++++ konabot/plugins/marchtoy/cpu_render.py | 109 ------------------------- 2 files changed, 18 insertions(+), 109 deletions(-) delete mode 100644 konabot/plugins/marchtoy/cpu_render.py diff --git a/konabot/docs/user/march.txt b/konabot/docs/user/march.txt index b2b3dae..67b50eb 100644 --- a/konabot/docs/user/march.txt +++ b/konabot/docs/user/march.txt @@ -14,5 +14,23 @@ 其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。 +# 支持的物体 +目前支持的物体有(不包含 alias): +`cube`:可选参数长宽高 +`sphere`:可选参数半径 +`torus`:可选参数半径与粗细 +`cylinder`:可选参数半径与高度 +`capsule`:可选参数高度与半径 + +特殊物体: +`camera`:可选参数焦距 + +# 支持的变换 +目前支持的变换有 +`pos` +`rot` +`color` +`lookat` + # 特殊说明 `` 不包含 scale。非正交的变换会破坏 SDF 的性质。 \ No newline at end of file diff --git a/konabot/plugins/marchtoy/cpu_render.py b/konabot/plugins/marchtoy/cpu_render.py deleted file mode 100644 index 4836d46..0000000 --- a/konabot/plugins/marchtoy/cpu_render.py +++ /dev/null @@ -1,109 +0,0 @@ -from typing import Optional - -import numpy as np -from PIL import Image - -from konabot.plugins.marchtoy.command import Scene -from konabot.plugins.marchtoy.obj import Object, Transform - - -class SDQuery: - def __init__(self, _value: float = np.inf, _obj: Optional[Object] = None): - self.value: float = _value - self.obj: Optional[Object] = _obj - - -class Rasterizer: - EPS = 0.001 - MAX_ITER = 128 - MAX_DIST = 128.0 - - def __init__(self, _scene: Scene, _res: tuple[int, int]): - self.scene = _scene - self.width, self.height = _res - self.fb_rgba = np.zeros((self.height, self.width, 4), dtype=np.float32) - - def sdf(self, p: np.ndarray) -> SDQuery: - qry = SDQuery() - for obj, _ in self.scene.canvas_objs: - sd = obj.sdf(p) - if sd < qry.value: - qry.value = sd - qry.obj = obj - return qry - - def nrm(self, p: np.ndarray) -> np.ndarray: - dx = np.array((self.EPS, 0.0, 0.0)) - dy = np.array((0.0, self.EPS, 0.0)) - dz = np.array((0.0, 0.0, self.EPS)) - grad = np.array( - [ - self.sdf(p + dx).value - self.sdf(p - dx).value, - self.sdf(p + dy).value - self.sdf(p - dy).value, - self.sdf(p + dz).value - self.sdf(p - dz).value, - ], - dtype=np.float32, - ) - return Transform.normalize(grad) - - def color(self, p: np.ndarray, obj: Object) -> np.ndarray: - normal = self.nrm(p) - light_dir = Transform.normalize(np.array((0.5, 0.8, -0.6), dtype=np.float32)) - light = 0.2 + 0.8 * max(float(np.dot(normal, light_dir)), 0.0) - base = np.array(obj.texture.color, dtype=np.float32) - return np.array((base[0] * light, base[1] * light, base[2] * light, base[3])) - - def march(self, origin: np.ndarray, direction: np.ndarray) -> np.ndarray: - p = origin.copy() - travel = 0.0 - - for _ in range(self.MAX_ITER): - qry = self.sdf(p) - if not np.isfinite(qry.value): - break - if qry.value < self.EPS and qry.obj is not None: - return self.color(p, qry.obj) - if qry.value > self.MAX_DIST or travel > self.MAX_DIST: - break - p += direction * qry.value - travel += qry.value - - return np.zeros(4, dtype=np.float32) - - def camera_basis(self) -> tuple[np.ndarray, np.ndarray]: - cam = self.scene.camera - cam_pos = cam.transform.t[0:3, 3] - cam_z = Transform.normalize( - (cam.transform.t @ np.array((0.0, 0.0, -1.0, 0.0), dtype=np.float32))[:3] - ) - world_up = ( - np.array((0.0, 0.0, 1.0), dtype=np.float32) - if abs(cam_z[1]) > 0.999 - else np.array((0.0, 1.0, 0.0), dtype=np.float32) - ) - cam_x = Transform.normalize(np.cross(cam_z, world_up)) - cam_y = Transform.normalize(np.cross(cam_x, cam_z)) - view = np.column_stack((cam_x, cam_y, cam_z)) - return cam_pos, view - - def rasterize(self) -> np.ndarray: - cam_pos, view = self.camera_basis() - resolution = np.array((self.width, self.height), dtype=np.float32) - aspect = np.array((self.width / self.height, 1.0), dtype=np.float32) - cam_focus = float(self.scene.camera.focus) - - for y, x in np.ndindex(self.height, self.width): - frag_coord = np.array((x + 0.5, y + 0.5), dtype=np.float32) - uv = 2.0 * (frag_coord / resolution - 0.5) * aspect - ray_local = Transform.normalize(np.array((uv[0], uv[1], cam_focus))) - ray_world = Transform.normalize(view @ ray_local) - self.fb_rgba[y, x] = self.march(cam_pos, ray_world) - - return self.fb_rgba - - -async def render(command: str, res: tuple[int, int]): - rasterizer = Rasterizer(Scene(command), res) - fb_output = rasterizer.rasterize() - rgba = np.clip(fb_output * 255.0, 0.0, 255.0).astype(np.uint8) - return Image.fromarray(rgba, mode="RGBA") From d80d8d91c2a196628de3b05d5d4ff59f8981cb5f Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 18:11:32 +0800 Subject: [PATCH 03/12] regex fix, more primitives --- konabot/plugins/marchtoy/__init__.py | 2 +- konabot/plugins/marchtoy/command.py | 96 ++-------------------- konabot/plugins/marchtoy/gl_render.py | 2 +- konabot/plugins/marchtoy/obj.py | 78 ++++++++---------- konabot/plugins/marchtoy/scene.py | 88 ++++++++++++++++++++ konabot/plugins/marchtoy/shaders/frag.glsl | 7 +- konabot/plugins/marchtoy/utilities.py | 11 +++ 7 files changed, 145 insertions(+), 139 deletions(-) create mode 100644 konabot/plugins/marchtoy/scene.py diff --git a/konabot/plugins/marchtoy/__init__.py b/konabot/plugins/marchtoy/__init__.py index 68acf25..38026af 100644 --- a/konabot/plugins/marchtoy/__init__.py +++ b/konabot/plugins/marchtoy/__init__.py @@ -17,4 +17,4 @@ async def _(args: Message = CommandArg()): buffer.seek(0) await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export()) except Exception as e: - await cmd_marchtoy.send(await UniMessage.text(f"发生了错误: {e}").export()) \ No newline at end of file + await cmd_marchtoy.send(await UniMessage.text(f"cannot render: {e}").export()) \ No newline at end of file diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index 005e54b..eb6ac6a 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -13,15 +13,9 @@ example: march cube.pos(0, 0, 0).color(red) cam(1.0).pos(1, 1, 1).lookat(0, 0, 0) """ -import pathlib from dataclasses import dataclass import re -import numpy as np from dataclasses import dataclass -from typing import Optional - -from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES -from konabot.plugins.marchtoy.op import OPERATION_ENTRIES @dataclass class Command: id: str @@ -29,7 +23,7 @@ class Command: class CommandChainParser: - CHAIN_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?(\.[a-zA-Z]+(\([^(]*\))?)*" + CHAIN_PATTERN = r"^(([a-zA-Z0-9.]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*)" def __init__(self, _command_chain: str) -> None: self.command_chain = _command_chain @@ -46,7 +40,7 @@ class CommandChainParser: class CommandParser: - CMD_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?" + CMD_PATTERN = r"^(([a-zA-Z0-9.]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?))" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)" def __init__(self, _command: str) -> None: @@ -61,88 +55,8 @@ class CommandParser: if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd): cmd_id = cmd_id_qry[0] self.command = self.command[len(cmd) + 1 :] - cmd_args = cmd[len(cmd_id) + 1 : -1].replace(" ", "").split(",") - while "" in cmd_args: - cmd_args.remove("") + cmd_args = cmd[len(cmd_id) + 1 : -1]#.replace(" ", "").split(",") + # while "" in cmd_args: + # cmd_args.remove("") return Command(cmd_id, cmd_args) raise StopIteration - -class Scene: - def __init__(self, _instruction: str) -> None: - self.canvas_objs: list[tuple[Object, str]] = [] - self.camera: Camera = Camera() - for raw_cmd in CommandChainParser(_instruction): - cmd_queue = CommandParser(raw_cmd) - cmd_obj = next(cmd_queue) - obj_id, obj_args = cmd_obj.id, cmd_obj.args - obj_instance: Optional[Object] = None - if obj_id in OBJECT_ENTRIES: - obj_cls = OBJECT_ENTRIES[obj_id] - if not issubclass(obj_cls, Object): - raise Exception(f"{obj_id} is not a subclass of Object.") - obj_instance = obj_cls() - try: - if len(obj_args) != 0: - obj_instance.parse_args(obj_args) - except Exception as e: - raise Exception( - f"object {obj_id} failed to parse args passed in: {obj_args}" - ) from e - else: - raise Exception(f"{obj_id} is not a valid object type.") - - if obj_instance != None: - for cmd in cmd_queue: - op_id, op_args = cmd.id, cmd.args - if op_id in OPERATION_ENTRIES: - op_func = OPERATION_ENTRIES[op_id] - if not callable(op_func): - raise Exception(f"{op_id} is not a valid operation.") - op_func(obj_instance, op_args) - else: - raise Exception(f"{op_id} is not a valid operation.") - - try: - sdf_block = obj_instance.sdf_block_glsl() - self.canvas_objs.append((obj_instance, sdf_block)) - except: - if type(obj_instance) == Camera: - self.camera = obj_instance - - def __str__(self) -> str: - return ", ".join([str(type(obj)) for obj in self.canvas_objs]) - - def compile(self, fs_src: str = "frag.glsl") -> str: - PATH = pathlib.Path(__file__).parent / "shaders" / fs_src - with PATH.open(encoding="utf-8") as f: - content = f.read() - sdf_block: str = "" - color_block: str = "" - - index = 0 - for canvas_item in self.canvas_objs: - obj, sdf_expr = canvas_item - sdf_block += f"float sd{index} = {sdf_expr};" - sdf_block += f"if(sd{index} < qry.value)" - sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" - color = obj.texture.color - color_block += ( - f"if(obj_id == {index}) return vec4(" - f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n" - ) - index += 1 - - content = content.replace("", sdf_block) - content = content.replace("", color_block) - - cam_pos = self.camera.transform.t[0:3, 3] - cam_focus = self.camera.focus - cam_dir = self.camera.transform.t @ np.array((0.0, 0.0, -1.0, 0.0)) - content = content.replace( - "", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}" - ) - content = content.replace("", str(cam_focus)) - content = content.replace( - "", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}" - ) - return content diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index 33553b4..7a22b92 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -5,7 +5,7 @@ import pathlib import moderngl import numpy as np from PIL import Image -from konabot.plugins.marchtoy.command import Scene +from konabot.plugins.marchtoy.scene import Scene async def render(command: str, res: tuple[int, int]): fs = Scene(command).compile() diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 366d383..364c1dd 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -1,6 +1,8 @@ import numpy as np from konabot.plugins.marchtoy.texture import Texture from konabot.plugins.marchtoy.utilities import ArgParser, Formatter +from nonebot import logger +from typing import Optional OBJECT_ENTRIES = {} @@ -80,7 +82,7 @@ class Object: self.transform: Transform = Transform() self.texture: Texture = Texture() - def parse_args(self, args: list[str]): + def parse_args(self, args: str): raise NotImplementedError def get_transformed(self, p: np.ndarray) -> np.ndarray: @@ -94,9 +96,6 @@ class Object: def sdf_block_glsl(self) -> str: raise NotImplementedError - def sdf(self, _p: np.ndarray) -> float: - return NotImplementedError - @make_obj("cube", "box") class Cube(Object): @@ -104,34 +103,24 @@ class Cube(Object): super().__init__() self.size = _size - def parse_args(self, args: list[str]): - self.size = ArgParser.as_vec3(args) + def parse_args(self, args: str): + self.size = ArgParser.as_vec3(ArgParser.to_params(args)) def sdf_block_glsl(self) -> str: return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))" - def sdf(self, _p): - p = self.get_transformed(_p) - p = np.abs(p) - self.size - return np.linalg.norm(np.maximum(p, 0.0)) + np.minimum(np.max(p), 0.0) - - @make_obj("sphere", "ball") class Sphere(Object): def __init__(self, _radius: float = 1.0) -> None: super().__init__() self.radius = _radius - def parse_args(self, args: list[str]): - self.radius = ArgParser.as_float(args) + def parse_args(self, args: str): + self.radius = ArgParser.as_float(ArgParser.to_params(args)) def sdf_block_glsl(self) -> str: return f"sdSphere({self.transform.p_expr()}, {self.radius})" - def sdf(self, _p): - p = self.get_transformed(_p) - return np.linalg.norm(p) - self.radius - @make_obj("cylinder", "cyl") class Cylinder(Object): @@ -140,8 +129,8 @@ class Cylinder(Object): self.radius = _radius self.height = _height - def parse_args(self, args: list[str]): - param = ArgParser.as_vec2(args) + def parse_args(self, args: str): + param = ArgParser.as_vec2(ArgParser.to_params(args)) self.radius = param[0] self.height = param[1] @@ -150,16 +139,6 @@ class Cylinder(Object): f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})" ) - def sdf(self, _p): - p = self.get_transformed(_p) - d = np.abs(np.array([np.linalg.norm(p[[0, 2]]), p[1]])) - np.array( - [self.radius, self.height] - ) - return np.minimum(np.maximum(d[0], d[1]), 0.0) + np.linalg.norm( - np.maximum(d, 0.0) - ) - - @make_obj("torus") class Torus(Object): def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None: @@ -167,19 +146,14 @@ class Torus(Object): self.r1 = _r1 self.r2 = _r2 - def parse_args(self, args: list[str]): - param = ArgParser.as_vec2(args) + def parse_args(self, args: str): + param = ArgParser.as_vec2(ArgParser.to_params(args)) self.r1 = param[0] self.r2 = param[1] def sdf_block_glsl(self) -> str: return f"sdTorus({self.transform.p_expr()}, vec2({self.r1}, {self.r2}))" - def sdf(self, _p): - p = self.get_transformed(_p) - q = np.array([np.linalg.norm(p[[0, 2]]) - self.r1, p[1]]) - return np.linalg.norm(q) - self.r2 - @make_obj("capsule", "pill") class Capsule(Object): @@ -188,19 +162,32 @@ class Capsule(Object): self.h = _h self.r = _r - def parse_args(self, args: list[str]): - param = ArgParser.as_vec2(args) + def parse_args(self, args: str): + param = ArgParser.as_vec2(ArgParser.to_params(args)) self.h = param[0] self.r = param[1] def sdf_block_glsl(self) -> str: return f"sdVerticalCapsule({self.transform.p_expr()}, {self.h}, {self.r})" - def sdf(self, _p): - p = self.get_transformed(_p) - p[1] -= np.clip(p[1], 0.0, self.h) - return np.linalg.norm(p) - self.r +@make_obj("smoothed", "mix") +class Smoothed(Object): + def __init__(self, _a = None, _b = None, _k: float = 0.25): + super().__init__() + self.a_index: int = _a + self.b: Optional[Object] = _b + self.k: float = _k + + def parse_args(self, args: str): + # from konabot.plugins.marchtoy.command import CommandChainParser + try: + raise Exception + except Exception as e: + raise Exception(f"cannot build smoothed object over {args}: {e}") + + def sdf_block_glsl(self): + return f"smin({self.a.sdf_block_glsl()}, {self.b.sdf_block_glsl()}, {self.k})" @make_obj("camera", "cam") class Camera(Object): @@ -209,5 +196,6 @@ class Camera(Object): self.focus = _focus self.transform.translate(5.0, 5.0, 5.0).lookat(0.0, 0.0, 0.0) - def parse_args(self, args: list[str]): - self.focus = ArgParser.as_float(args) + def parse_args(self, args: str): + self.focus = ArgParser.as_float(ArgParser.to_params(args)) + diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py new file mode 100644 index 0000000..289d88c --- /dev/null +++ b/konabot/plugins/marchtoy/scene.py @@ -0,0 +1,88 @@ +from typing import Optional +import pathlib +import numpy as np +from nonebot import logger + +class Scene: + def __init__(self, _instruction: str) -> None: + from konabot.plugins.marchtoy.command import CommandChainParser, CommandParser + from konabot.plugins.marchtoy.op import OPERATION_ENTRIES + from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES + logger.info(f"building scene: {_instruction}") + self.canvas_objs: list[tuple[Object, str]] = [] + self.camera: Camera = Camera() + for raw_cmd in CommandChainParser(_instruction): + cmd_queue = CommandParser(raw_cmd) + cmd_obj = next(cmd_queue) + obj_id, obj_args = cmd_obj.id, cmd_obj.args + obj_instance: Optional[Object] = None + if obj_id in OBJECT_ENTRIES: + obj_cls = OBJECT_ENTRIES[obj_id] + if not issubclass(obj_cls, Object): + raise Exception(f"{obj_id} is not a subclass of Object.") + obj_instance = obj_cls() + try: + if len(obj_args) != 0: + obj_instance.parse_args(obj_args) + except Exception as e: + raise Exception( + f"object {obj_id} failed to parse args passed in: {obj_args}.\n{e}" + ) from e + else: + raise Exception(f"{obj_id} is not a valid object type.") + + if obj_instance != None: + for cmd in cmd_queue: + op_id, op_args = cmd.id, cmd.args + if op_id in OPERATION_ENTRIES: + op_func = OPERATION_ENTRIES[op_id] + if not callable(op_func): + raise Exception(f"{op_id} is not a valid operation.") + op_func(obj_instance, op_args) + else: + raise Exception(f"{op_id} is not a valid operation.") + + try: + sdf_block = obj_instance.sdf_block_glsl() + self.canvas_objs.append((obj_instance, sdf_block)) + except: + if type(obj_instance) == Camera: + self.camera = obj_instance + + def __str__(self) -> str: + return ", ".join([str(type(obj)) for obj in self.canvas_objs]) + + def compile(self, fs_src: str = "frag.glsl") -> str: + PATH = pathlib.Path(__file__).parent / "shaders" / fs_src + with PATH.open(encoding="utf-8") as f: + content = f.read() + sdf_block: str = "" + color_block: str = "" + + index = 0 + for canvas_item in self.canvas_objs: + obj, sdf_expr = canvas_item + sdf_block += f"float sd{index} = {sdf_expr};" + sdf_block += f"if(sd{index} < qry.value)" + sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" + color = obj.texture.color + color_block += ( + f"if(obj_id == {index}) return vec4(" + f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n" + ) + index += 1 + + content = content.replace("", sdf_block) + content = content.replace("", color_block) + + cam_pos = self.camera.transform.t[0:3, 3] + cam_focus = self.camera.focus + cam_dir = self.camera.transform.t @ np.array((0.0, 0.0, -1.0, 0.0)) + content = content.replace( + "", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}" + ) + content = content.replace("", str(cam_focus)) + content = content.replace( + "", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}" + ) + return content diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index a7b7fab..8cc4068 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -33,7 +33,12 @@ float sdTorus( vec3 p, vec2 t ) vec2 q = vec2(length(p.xz)-t.x,p.y); return length(q)-t.y; } - +float smin( float a, float b, float k ) +{ + k *= 1.0; + float r = exp2(-a/k) + exp2(-b/k); + return -k*log2(r); +} sdQuery sd(vec3 p) { sdQuery qry; qry.value = 100000000.0; diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index 34d18f7..dcadc52 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -26,6 +26,17 @@ class Formatter: TODO: 除零出现 nan 情况的单独处理 """ class ArgParser: + @staticmethod + def to_params(args: str, delim: str = ',') -> list[str]: + _params = args.replace(" ", "").split(delim) + params: list[str] = [] + # while 还是太过于令人生畏了 + for param in _params: + if param == "": + continue + params.append(param) + return paramss + @staticmethod def as_float(args: list[str], default: float = 0.0) -> float: try: From cf52ea683b8407dfa8d45bacd016fd738d64bf86 Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 19:46:10 +0800 Subject: [PATCH 04/12] regex fix --- konabot/plugins/marchtoy/command.py | 6 +- konabot/plugins/marchtoy/gl_render.py | 5 +- konabot/plugins/marchtoy/obj.py | 31 +++-- konabot/plugins/marchtoy/scene.py | 7 +- konabot/plugins/marchtoy/shaders/frag.glsl | 2 +- konabot/plugins/marchtoy/skia_render.py | 41 ------- konabot/plugins/marchtoy/utilities.py | 22 +--- poetry.lock | 131 ++++++++++++++++++++- pyproject.toml | 1 + 9 files changed, 174 insertions(+), 72 deletions(-) delete mode 100644 konabot/plugins/marchtoy/skia_render.py diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index eb6ac6a..6ac5bce 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -14,7 +14,7 @@ example: """ from dataclasses import dataclass -import re +import regex as re from dataclasses import dataclass @dataclass class Command: @@ -23,7 +23,7 @@ class Command: class CommandChainParser: - CHAIN_PATTERN = r"^(([a-zA-Z0-9.]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*)" + CHAIN_PATTERN = r"^(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*)" def __init__(self, _command_chain: str) -> None: self.command_chain = _command_chain @@ -40,7 +40,7 @@ class CommandChainParser: class CommandParser: - CMD_PATTERN = r"^(([a-zA-Z0-9.]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?))" + CMD_PATTERN = r"^(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?))" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)" def __init__(self, _command: str) -> None: diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index 7a22b92..04cd91d 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -6,13 +6,16 @@ import moderngl import numpy as np from PIL import Image from konabot.plugins.marchtoy.scene import Scene +from nonebot import logger + async def render(command: str, res: tuple[int, int]): fs = Scene(command).compile() + # logger.warning(fs) PATH = pathlib.Path(__file__).parent / "shaders" with (PATH / "vert.glsl").open(encoding='utf-8') as f: vs = f.read() - ctx = moderngl.create_context(standalone=True, backend='egl') + ctx = moderngl.create_context(standalone=True) ctx.gc_mode = "auto" try: program = ctx.program( diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 364c1dd..b689a6c 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -3,7 +3,7 @@ from konabot.plugins.marchtoy.texture import Texture from konabot.plugins.marchtoy.utilities import ArgParser, Formatter from nonebot import logger from typing import Optional - +import re OBJECT_ENTRIES = {} @@ -74,7 +74,7 @@ class Transform: def p_expr(self) -> str: inv = np.linalg.inv(self.t) # + 1e-5 * np.identity(4, dtype=np.float32)) - return f"({Formatter.vec4(inv)} * vec4(p, 1.0)).xyz" + return f"({Formatter.to_vec4(inv)} * vec4(p, 1.0)).xyz" class Object: @@ -172,22 +172,37 @@ class Capsule(Object): @make_obj("smoothed", "mix") class Smoothed(Object): - def __init__(self, _a = None, _b = None, _k: float = 0.25): + def __init__(self, _k: float = 0.25): super().__init__() - self.a_index: int = _a - self.b: Optional[Object] = _b + self.block_a: str = "INF" + self.block_b: str = "INF" self.k: float = _k def parse_args(self, args: str): - # from konabot.plugins.marchtoy.command import CommandChainParser + from konabot.plugins.marchtoy.scene import Scene + from konabot.plugins.marchtoy.command import CommandChainParser try: - raise Exception + args = args.replace(" ", "") + cmd_chain_a = next(CommandChainParser(args)) + args = args[len(cmd_chain_a):] + if trim := re.match(r"^[\s]*\,[\s]*", args): + args = args[len(trim[0]):] + cmd_chain_b = next(CommandChainParser(args)) + args = args[len(cmd_chain_b):] + if trim := re.match(r"^[\s]*\,[\s]*", args): + args = args[len(trim[0]):] + scene_a = Scene(cmd_chain_a) + scene_b = Scene(cmd_chain_b) + self.block_a = scene_a.canvas_objs[0][1] + self.block_b = scene_b.canvas_objs[0][1] + # logger.warning(args) + # self.k = ArgParser.as_float(args) except Exception as e: raise Exception(f"cannot build smoothed object over {args}: {e}") def sdf_block_glsl(self): - return f"smin({self.a.sdf_block_glsl()}, {self.b.sdf_block_glsl()}, {self.k})" + return f"smin({self.block_a}, {self.block_b}, {self.k})" @make_obj("camera", "cam") class Camera(Object): diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py index 289d88c..fe488b5 100644 --- a/konabot/plugins/marchtoy/scene.py +++ b/konabot/plugins/marchtoy/scene.py @@ -15,6 +15,7 @@ class Scene: cmd_queue = CommandParser(raw_cmd) cmd_obj = next(cmd_queue) obj_id, obj_args = cmd_obj.id, cmd_obj.args + logger.info(f"parsing object: {obj_id} with args {obj_args}") obj_instance: Optional[Object] = None if obj_id in OBJECT_ENTRIES: obj_cls = OBJECT_ENTRIES[obj_id] @@ -30,10 +31,11 @@ class Scene: ) from e else: raise Exception(f"{obj_id} is not a valid object type.") - + logger.info(f"parsed object {obj_id}({obj_args})") if obj_instance != None: for cmd in cmd_queue: op_id, op_args = cmd.id, cmd.args + logger.info(f"parsing operation {op_id} with args {op_args}") if op_id in OPERATION_ENTRIES: op_func = OPERATION_ENTRIES[op_id] if not callable(op_func): @@ -41,11 +43,14 @@ class Scene: op_func(obj_instance, op_args) else: raise Exception(f"{op_id} is not a valid operation.") + logger.info(f"parsed operation {op_id}({op_args})") try: sdf_block = obj_instance.sdf_block_glsl() self.canvas_objs.append((obj_instance, sdf_block)) + logger.info(f"parsed sdf {sdf_block}") except: + logger.info(f"parsed camera") if type(obj_instance) == Camera: self.camera = obj_instance diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index 8cc4068..10af5d7 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -1,6 +1,6 @@ #version 330 const float EPS = 0.001; -const int MAX_ITER = 128; +const int MAX_ITER = 64; uniform vec2 u_resolution; out vec4 fragColor; struct sdQuery { diff --git a/konabot/plugins/marchtoy/skia_render.py b/konabot/plugins/marchtoy/skia_render.py deleted file mode 100644 index 793ea53..0000000 --- a/konabot/plugins/marchtoy/skia_render.py +++ /dev/null @@ -1,41 +0,0 @@ -from command import Scene -import skia -import struct -from PIL import Image -import numpy as np - -# 暂时先照抄小帕的了,之后有空再单独封装一下 - - -async def render(instruction: str, res: tuple[int, int]): - scene = Scene(instruction) - width, height = res - surface = skia.Surface(width, height) - sksl_code = scene.compile() - runtime_effect = skia.RuntimeEffect.MakeForShader(sksl_code) - if runtime_effect is None: - raise Exception("cannot compile sksl shader") - - uv_uniform = struct.pack("ff", float(width), float(height)) - uniform_data = skia.Data.MakeWithCopy(uv_uniform) - shader = runtime_effect.makeShader(uniform_data, None, 0) - canvas = surface.getCanvas() - canvas.clear(skia.Color(0, 0, 0, 0)) - paint = skia.Paint() - paint.setShader(shader) - canvas.drawRect(skia.Rect.MakeWH(width, height), paint) - image = surface.makeImageSnapshot() - target_info = skia.ImageInfo.Make( - image.width(), - image.height(), - skia.ColorType.kBGRA_8888_ColorType, - skia.AlphaType.kPremul_AlphaType, - ) - pixel_data = bytearray( - image.width() * image.height() * 4 - ) # 4 bytes per pixel (BGRA) - success = image.readPixels(target_info, pixel_data, target_info.minRowBytes()) - img_array = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4)) - rgb_array = img_array[:, :, [2, 1, 0]] - pil_img = Image.fromarray(rgb_array) - return pil_img diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index dcadc52..a3103e2 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -1,19 +1,10 @@ import numpy as np +import regex as re from konabot.plugins.marchtoy.texture import COLORS class Formatter: @staticmethod - def float4x4(m: np.ndarray) -> str: - if m.shape != (4, 4): - m = np.identity(4) - v_0 = ", ".join([str(x) for x in m[:, 0]]) - v_1 = ", ".join([str(x) for x in m[:, 1]]) - v_2 = ", ".join([str(x) for x in m[:, 2]]) - v_3 = ", ".join([str(x) for x in m[:, 3]]) - return f"float4x4(float4({v_0}), float4({v_1}), float4({v_2}), float4({v_3}))" - - @staticmethod - def vec4(m: np.ndarray) -> str: + def to_vec4(m: np.ndarray) -> str: if m.shape != (4, 4): m = np.identity(4) v_0 = ", ".join([str(x) for x in m[:, 0]]) @@ -30,13 +21,12 @@ class ArgParser: def to_params(args: str, delim: str = ',') -> list[str]: _params = args.replace(" ", "").split(delim) params: list[str] = [] - # while 还是太过于令人生畏了 + # 还是避免 while 为好 for param in _params: - if param == "": - continue + if param != "": params.append(param) - return paramss - + return params + @staticmethod def as_float(args: list[str], default: float = 0.0) -> float: try: diff --git a/poetry.lock b/poetry.lock index 0160626..6d5e811 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4400,6 +4400,135 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "mirrors" +[[package]] +name = "regex" +version = "2026.4.4" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f"}, + {file = "regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f"}, + {file = "regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8"}, + {file = "regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9"}, + {file = "regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e"}, + {file = "regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b"}, + {file = "regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5"}, + {file = "regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f"}, + {file = "regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d"}, + {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c"}, + {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760"}, + {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9"}, + {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7"}, + {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22"}, + {file = "regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59"}, + {file = "regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee"}, + {file = "regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98"}, + {file = "regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6"}, + {file = "regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87"}, + {file = "regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8"}, + {file = "regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada"}, + {file = "regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d"}, + {file = "regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87"}, + {file = "regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4"}, + {file = "regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86"}, + {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59"}, + {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453"}, + {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80"}, + {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b"}, + {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f"}, + {file = "regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351"}, + {file = "regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735"}, + {file = "regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54"}, + {file = "regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52"}, + {file = "regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb"}, + {file = "regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76"}, + {file = "regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be"}, + {file = "regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1"}, + {file = "regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13"}, + {file = "regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9"}, + {file = "regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d"}, + {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3"}, + {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0"}, + {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043"}, + {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244"}, + {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73"}, + {file = "regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f"}, + {file = "regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b"}, + {file = "regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983"}, + {file = "regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943"}, + {file = "regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031"}, + {file = "regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7"}, + {file = "regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17"}, + {file = "regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17"}, + {file = "regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae"}, + {file = "regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e"}, + {file = "regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d"}, + {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27"}, + {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf"}, + {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0"}, + {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa"}, + {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b"}, + {file = "regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62"}, + {file = "regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81"}, + {file = "regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427"}, + {file = "regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c"}, + {file = "regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141"}, + {file = "regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717"}, + {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07"}, + {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca"}, + {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520"}, + {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883"}, + {file = "regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b"}, + {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1"}, + {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b"}, + {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff"}, + {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb"}, + {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4"}, + {file = "regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa"}, + {file = "regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0"}, + {file = "regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe"}, + {file = "regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7"}, + {file = "regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752"}, + {file = "regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951"}, + {file = "regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f"}, + {file = "regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8"}, + {file = "regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4"}, + {file = "regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9"}, + {file = "regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83"}, + {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb"}, + {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465"}, + {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4"}, + {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566"}, + {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95"}, + {file = "regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8"}, + {file = "regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4"}, + {file = "regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f"}, + {file = "regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3"}, + {file = "regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e"}, + {file = "regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6"}, + {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359"}, + {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a"}, + {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55"}, + {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99"}, + {file = "regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790"}, + {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc"}, + {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f"}, + {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863"}, + {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a"}, + {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81"}, + {file = "regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74"}, + {file = "regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45"}, + {file = "regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d"}, + {file = "regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "requests" version = "2.33.1" @@ -5525,4 +5654,4 @@ reference = "mirrors" [metadata] lock-version = "2.1" python-versions = ">=3.12,<4.0" -content-hash = "ce664db91fb8047d071f8d47890cd053412a961e594db41953345e87bc34c64a" +content-hash = "91e6623a2ea57cf1189d99cfc085abb3e5fe46f7fd1d68390292ab8d5f0fa304" diff --git a/pyproject.toml b/pyproject.toml index e70d258..05b5766 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "pytest-mock (>=3.15.1,<4.0.0)", "skia-python (>=144.0.post2,<145.0)", "moderngl (>=5.12.0,<6.0.0)", + "regex (>=2026.4.4,<2027.0.0)", ] [tool.poetry] From 733114b941ff9353f92848b4f72b11a89324273b Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 21:18:05 +0800 Subject: [PATCH 05/12] regex fix --- konabot/plugins/marchtoy/__init__.py | 6 ++-- konabot/plugins/marchtoy/command.py | 23 ++++++++---- konabot/plugins/marchtoy/gl_render.py | 14 ++++---- konabot/plugins/marchtoy/obj.py | 42 +++++++++------------- konabot/plugins/marchtoy/op.py | 2 +- konabot/plugins/marchtoy/scene.py | 13 ++++--- konabot/plugins/marchtoy/shaders/frag.glsl | 2 ++ konabot/plugins/marchtoy/utilities.py | 4 +-- 8 files changed, 58 insertions(+), 48 deletions(-) diff --git a/konabot/plugins/marchtoy/__init__.py b/konabot/plugins/marchtoy/__init__.py index 38026af..f2e5fb0 100644 --- a/konabot/plugins/marchtoy/__init__.py +++ b/konabot/plugins/marchtoy/__init__.py @@ -2,6 +2,7 @@ from nonebot import on_command from nonebot.adapters import Message from nonebot_plugin_alconna import UniMessage from nonebot.params import CommandArg + import konabot.plugins.marchtoy.gl_render as render # import konabot.plugins.marchtoy.cpu_render as render import io @@ -10,10 +11,11 @@ cmd_marchtoy = on_command("march") async def _(args: Message = CommandArg()): if cmd := args.extract_plain_text(): try: - img = await render.render(cmd, (512, 512)) + img = await render.render(cmd, (256, 256)) buffer = io.BytesIO() # img.show() - img.save(buffer, format="PNG") + # img.save("/mnt/d/output.png", format="GIF") + img.save(buffer, format="GIF") buffer.seek(0) await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export()) except Exception as e: diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index 6ac5bce..9243299 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -16,6 +16,8 @@ example: from dataclasses import dataclass import regex as re from dataclasses import dataclass + + @dataclass class Command: id: str @@ -40,8 +42,10 @@ class CommandChainParser: class CommandParser: - CMD_PATTERN = r"^(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?))" + CMD_PATTERN = r"^[a-zA-Z]+(?:\(([0-9.]+|(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*))(\s*\,\s*(?1))*\))?" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)" + ARG_PATTERN = CommandChainParser.CHAIN_PATTERN + TRIM_PATTERN = r"^\s*\,\s*" def __init__(self, _command: str) -> None: self.command = _command @@ -53,10 +57,17 @@ class CommandParser: if query := re.match(CommandParser.CMD_PATTERN, self.command): cmd = query[0] if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd): - cmd_id = cmd_id_qry[0] + id = cmd_id_qry[0] + cmd_args = cmd[len(id) + 1 : -1] # .replace(" ", "").split(",") + args: list[str] = [] self.command = self.command[len(cmd) + 1 :] - cmd_args = cmd[len(cmd_id) + 1 : -1]#.replace(" ", "").split(",") + while cmd_arg_qry := re.match(CommandParser.ARG_PATTERN, cmd_args): + arg = cmd_arg_qry[0] + args.append(arg) + cmd_args = cmd_args[len(arg) :] + if trim_qry := re.match(CommandParser.TRIM_PATTERN, cmd_args): + cmd_args = cmd_args[len(trim_qry[0]) :] # while "" in cmd_args: - # cmd_args.remove("") - return Command(cmd_id, cmd_args) - raise StopIteration + # cmd_args.remove("") + return Command(id, args) + raise StopIteration \ No newline at end of file diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index 04cd91d..68ad2d5 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -8,23 +8,23 @@ from PIL import Image from konabot.plugins.marchtoy.scene import Scene from nonebot import logger +PATH = pathlib.Path(__file__).parent / "shaders" +with (PATH / "vert.glsl").open(encoding='utf-8') as f: + VS_SRC = f.read() async def render(command: str, res: tuple[int, int]): fs = Scene(command).compile() - # logger.warning(fs) - PATH = pathlib.Path(__file__).parent / "shaders" - with (PATH / "vert.glsl").open(encoding='utf-8') as f: - vs = f.read() - ctx = moderngl.create_context(standalone=True) + + ctx = moderngl.create_context(standalone=True, backend="egl") ctx.gc_mode = "auto" try: program = ctx.program( - vertex_shader=vs, + vertex_shader=VS_SRC, fragment_shader=fs ) except Exception as e: raise Exception(f"cannot compile glsl: {e}") from e - uniform = program['u_resolution'] + uniform = program["u_resolution"] uniform.write(np.array(res, dtype=np.float32)) vertices = np.array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0], dtype=np.float32) indices = np.array([0, 1, 2, 1, 2, 3], dtype=np.int32) diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index b689a6c..e50bcad 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -82,7 +82,7 @@ class Object: self.transform: Transform = Transform() self.texture: Texture = Texture() - def parse_args(self, args: str): + def parse_args(self, args: list[str]): raise NotImplementedError def get_transformed(self, p: np.ndarray) -> np.ndarray: @@ -104,7 +104,7 @@ class Cube(Object): self.size = _size def parse_args(self, args: str): - self.size = ArgParser.as_vec3(ArgParser.to_params(args)) + self.size = ArgParser.as_vec3(args) def sdf_block_glsl(self) -> str: return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))" @@ -116,7 +116,7 @@ class Sphere(Object): self.radius = _radius def parse_args(self, args: str): - self.radius = ArgParser.as_float(ArgParser.to_params(args)) + self.radius = ArgParser.as_float(args) def sdf_block_glsl(self) -> str: return f"sdSphere({self.transform.p_expr()}, {self.radius})" @@ -130,7 +130,7 @@ class Cylinder(Object): self.height = _height def parse_args(self, args: str): - param = ArgParser.as_vec2(ArgParser.to_params(args)) + param = ArgParser.as_vec2(args) self.radius = param[0] self.height = param[1] @@ -146,8 +146,8 @@ class Torus(Object): self.r1 = _r1 self.r2 = _r2 - def parse_args(self, args: str): - param = ArgParser.as_vec2(ArgParser.to_params(args)) + def parse_args(self, args: list[str]): + param = ArgParser.as_vec2(args) self.r1 = param[0] self.r2 = param[1] @@ -162,8 +162,8 @@ class Capsule(Object): self.h = _h self.r = _r - def parse_args(self, args: str): - param = ArgParser.as_vec2(ArgParser.to_params(args)) + def parse_args(self, args: list[str]): + param = ArgParser.as_vec2(args) self.h = param[0] self.r = param[1] @@ -178,25 +178,17 @@ class Smoothed(Object): self.block_b: str = "INF" self.k: float = _k - def parse_args(self, args: str): + def parse_args(self, args: list[str]): from konabot.plugins.marchtoy.scene import Scene - from konabot.plugins.marchtoy.command import CommandChainParser try: - args = args.replace(" ", "") - cmd_chain_a = next(CommandChainParser(args)) - args = args[len(cmd_chain_a):] - if trim := re.match(r"^[\s]*\,[\s]*", args): - args = args[len(trim[0]):] - cmd_chain_b = next(CommandChainParser(args)) - args = args[len(cmd_chain_b):] - if trim := re.match(r"^[\s]*\,[\s]*", args): - args = args[len(trim[0]):] - scene_a = Scene(cmd_chain_a) - scene_b = Scene(cmd_chain_b) + if not len(args) >= 2: + raise Exception("expecting at least 2 args") + scene_a = Scene(args[0]) + scene_b = Scene(args[1]) self.block_a = scene_a.canvas_objs[0][1] self.block_b = scene_b.canvas_objs[0][1] - # logger.warning(args) - # self.k = ArgParser.as_float(args) + if len(args) > 2: + self.k = ArgParser.as_float(args[2], 0.25) except Exception as e: raise Exception(f"cannot build smoothed object over {args}: {e}") @@ -211,6 +203,6 @@ class Camera(Object): self.focus = _focus self.transform.translate(5.0, 5.0, 5.0).lookat(0.0, 0.0, 0.0) - def parse_args(self, args: str): - self.focus = ArgParser.as_float(ArgParser.to_params(args)) + def parse_args(self, args: list[str]): + self.focus = ArgParser.as_float(args) diff --git a/konabot/plugins/marchtoy/op.py b/konabot/plugins/marchtoy/op.py index a896e42..2d82c1e 100644 --- a/konabot/plugins/marchtoy/op.py +++ b/konabot/plugins/marchtoy/op.py @@ -44,7 +44,7 @@ def color(obj: Object, args: list[str]): col = ArgParser.as_vec4(args) obj.texture.color = (col[0], col[1], col[2], col[3]) else: - raise Exception("unknown color") + raise Exception("invalid argument number") except: raise Exception("unknown color") diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py index fe488b5..9954a9d 100644 --- a/konabot/plugins/marchtoy/scene.py +++ b/konabot/plugins/marchtoy/scene.py @@ -3,6 +3,10 @@ import pathlib import numpy as np from nonebot import logger +PATH = pathlib.Path(__file__).parent / "shaders" / "frag.glsl" +with PATH.open(encoding="utf-8") as f: + FS_SRC = f.read() + class Scene: def __init__(self, _instruction: str) -> None: from konabot.plugins.marchtoy.command import CommandChainParser, CommandParser @@ -57,10 +61,8 @@ class Scene: def __str__(self) -> str: return ", ".join([str(type(obj)) for obj in self.canvas_objs]) - def compile(self, fs_src: str = "frag.glsl") -> str: - PATH = pathlib.Path(__file__).parent / "shaders" / fs_src - with PATH.open(encoding="utf-8") as f: - content = f.read() + def compile(self) -> str: + sdf_block: str = "" color_block: str = "" @@ -76,7 +78,8 @@ class Scene: f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n" ) index += 1 - + + content = FS_SRC content = content.replace("", sdf_block) content = content.replace("", color_block) diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index 10af5d7..b006fff 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -1,8 +1,10 @@ #version 330 const float EPS = 0.001; const int MAX_ITER = 64; + uniform vec2 u_resolution; out vec4 fragColor; + struct sdQuery { float value; int obj_id; diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index a3103e2..4491654 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -34,8 +34,8 @@ class ArgParser: x = float(args[0]) return x except: - raise Exception(f"cannot parse {args}") - return default + # raise Exception(f"cannot parse {args}") + return default @staticmethod def as_vec2( From 250eaaf59c25c17f7b07303aeed7ca3119552c3b Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 21:41:16 +0800 Subject: [PATCH 06/12] bool and smooth --- konabot/plugins/marchtoy/__init__.py | 4 +- konabot/plugins/marchtoy/gl_render.py | 5 ++- konabot/plugins/marchtoy/obj.py | 51 ++++++++++++++++++++-- konabot/plugins/marchtoy/op.py | 16 ++++--- konabot/plugins/marchtoy/scene.py | 4 +- konabot/plugins/marchtoy/shaders/frag.glsl | 2 +- 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/konabot/plugins/marchtoy/__init__.py b/konabot/plugins/marchtoy/__init__.py index f2e5fb0..5d254c6 100644 --- a/konabot/plugins/marchtoy/__init__.py +++ b/konabot/plugins/marchtoy/__init__.py @@ -11,11 +11,11 @@ cmd_marchtoy = on_command("march") async def _(args: Message = CommandArg()): if cmd := args.extract_plain_text(): try: - img = await render.render(cmd, (256, 256)) + img = await render.render(cmd, (512, 512)) buffer = io.BytesIO() # img.show() # img.save("/mnt/d/output.png", format="GIF") - img.save(buffer, format="GIF") + img.save(buffer, format="PNG") buffer.seek(0) await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export()) except Exception as e: diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index 68ad2d5..e54468f 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -24,7 +24,10 @@ async def render(command: str, res: tuple[int, int]): ) except Exception as e: raise Exception(f"cannot compile glsl: {e}") from e - uniform = program["u_resolution"] + try: + uniform = program["u_resolution"] + except Exception as e: + raise Exception("无法获取 uniform,可能相机位于物体内部?") uniform.write(np.array(res, dtype=np.float32)) vertices = np.array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0], dtype=np.float32) indices = np.array([0, 1, 2, 1, 2, 3], dtype=np.int32) diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index e50bcad..32d023f 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -81,6 +81,7 @@ class Object: def __init__(self) -> None: self.transform: Transform = Transform() self.texture: Texture = Texture() + self.round_corner: float = 0.0 def parse_args(self, args: list[str]): raise NotImplementedError @@ -172,7 +173,7 @@ class Capsule(Object): @make_obj("smoothed", "mix") class Smoothed(Object): - def __init__(self, _k: float = 0.25): + def __init__(self, _k: float = 12.00): super().__init__() self.block_a: str = "INF" self.block_b: str = "INF" @@ -196,12 +197,56 @@ class Smoothed(Object): def sdf_block_glsl(self): return f"smin({self.block_a}, {self.block_b}, {self.k})" +@make_obj("intersect", "bool") +class BoolIntersect(Object): + def __init__(self): + super().__init__() + + def parse_args(self, args: list[str]): + from konabot.plugins.marchtoy.scene import Scene + try: + if not len(args) >= 2: + raise Exception("expecting at least 2 args") + scene_a = Scene(args[0]) + scene_b = Scene(args[1]) + self.block_a = scene_a.canvas_objs[0][1] + self.block_b = scene_b.canvas_objs[0][1] + if len(args) > 2: + self.k = ArgParser.as_float(args[2], 0.25) + except Exception as e: + raise Exception(f"cannot build bool object over {args}: {e}") + + def sdf_block_glsl(self): + return f"max({self.block_a}, {self.block_b})" + +@make_obj("substract", "minus") +class BoolSubstract(Object): + def __init__(self): + super().__init__() + + def parse_args(self, args: list[str]): + from konabot.plugins.marchtoy.scene import Scene + try: + if not len(args) >= 2: + raise Exception("expecting at least 2 args") + scene_a = Scene(args[0]) + scene_b = Scene(args[1]) + self.block_a = scene_a.canvas_objs[0][1] + self.block_b = scene_b.canvas_objs[0][1] + if len(args) > 2: + self.k = ArgParser.as_float(args[2], 0.25) + except Exception as e: + raise Exception(f"cannot build bool object over {args}: {e}") + + def sdf_block_glsl(self): + return f"max({self.block_a}, -{self.block_b})" + @make_obj("camera", "cam") class Camera(Object): - def __init__(self, _focus: float = 1.0) -> None: + def __init__(self, _focus: float = 2.0) -> None: super().__init__() self.focus = _focus - self.transform.translate(5.0, 5.0, 5.0).lookat(0.0, 0.0, 0.0) + self.transform.translate(8.0, 8.0, 8.0).lookat(0.0, 0.0, 0.0) def parse_args(self, args: list[str]): self.focus = ArgParser.as_float(args) diff --git a/konabot/plugins/marchtoy/op.py b/konabot/plugins/marchtoy/op.py index 2d82c1e..c9cb7b6 100644 --- a/konabot/plugins/marchtoy/op.py +++ b/konabot/plugins/marchtoy/op.py @@ -3,7 +3,7 @@ from konabot.plugins.marchtoy.utilities import ArgParser OPERATION_ENTRIES = {} -def make_operation(*name: str): +def make_op(*name: str): def decorator(op): # OPERATION_ENTRIES[name] = op for alias in [*name]: @@ -13,25 +13,25 @@ def make_operation(*name: str): return decorator -@make_operation("pos", "translate", "position", "p") +@make_op("pos", "translate", "position", "p") def translate(obj: Object, args: list[str]): pos = ArgParser.as_vec3(args) obj.transform.translate(pos[0], pos[1], pos[2]) -@make_operation("rot", "rotate", "r") +@make_op("rot", "rotate", "r") def rotate(obj: Object, args: list[str]): pos = ArgParser.as_vec3(args) obj.transform.rotate(pos[0], pos[1], pos[2]) -@make_operation("lookat", "look", "l") +@make_op("lookat", "look", "l") def lookat(obj: Object, args: list[str]): pos = ArgParser.as_vec3(args) obj.transform.lookat(pos[0], pos[1], pos[2]) -@make_operation("color", "col", "texture") +@make_op("color", "col", "texture") def color(obj: Object, args: list[str]): try: if len(args) == 1: @@ -48,3 +48,9 @@ def color(obj: Object, args: list[str]): except: raise Exception("unknown color") + +@make_op("rounded", "round_corner", "corner") +def rounded(obj: Object, args: list[str]): + if len(args) >= 1: + obj.round_corner = ArgParser.as_float(args[0]) + \ No newline at end of file diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py index 9954a9d..2162872 100644 --- a/konabot/plugins/marchtoy/scene.py +++ b/konabot/plugins/marchtoy/scene.py @@ -71,7 +71,9 @@ class Scene: obj, sdf_expr = canvas_item sdf_block += f"float sd{index} = {sdf_expr};" sdf_block += f"if(sd{index} < qry.value)" - sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" + # 1e-8 最好换成某个 epsilon + round_corner = f"- {obj.round_corner}" if obj.round_corner > 1e-8 else "" + sdf_block += "{" + f"qry.value = sd{index} {round_corner}; qry.obj_id = {index}; " + "}\n" color = obj.texture.color color_block += ( f"if(obj_id == {index}) return vec4(" diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index b006fff..550d768 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -1,6 +1,6 @@ #version 330 const float EPS = 0.001; -const int MAX_ITER = 64; +const int MAX_ITER = 128; uniform vec2 u_resolution; out vec4 fragColor; From 8997c430c9f47a720c7c5957023fc9bbe6b8a4c0 Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 21:47:25 +0800 Subject: [PATCH 07/12] =?UTF-8?q?manual=20=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/march.txt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/konabot/docs/user/march.txt b/konabot/docs/user/march.txt index 67b50eb..d4c0ef0 100644 --- a/konabot/docs/user/march.txt +++ b/konabot/docs/user/march.txt @@ -1,7 +1,7 @@ # 指令介绍 简易 Raymarch 小玩具 用法:march `` -例:march sphere(1).color(red) box(0.5, 2.0, 0.5).pos(0, 0, 0) cam(0.5).pos(-5).lookat(0) +例:march sphere(1).color(red) box(0.5, 2.0, 0.5).pos(0, 0, 0) cam(2.5).pos(-5).lookat(0) # 主要语法 `` ::= `` "." `` |`` @@ -23,14 +23,18 @@ `capsule`:可选参数高度与半径 特殊物体: -`camera`:可选参数焦距 +`mix`:混合两个物体 +`bool`:两个物体相交 +`minus`:两个物体相减 +`camera`:相机,可选参数焦距 # 支持的变换 目前支持的变换有 -`pos` -`rot` -`color` -`lookat` +`pos`:位移 +`rot`:旋转(欧拉角 xyz) +`color`:基础色 +`lookat`:朝向 +`rounded`:圆角 # 特殊说明 `` 不包含 scale。非正交的变换会破坏 SDF 的性质。 \ No newline at end of file From 129870709b4867973f0c752cf5d77628ab56be6e Mon Sep 17 00:00:00 2001 From: bk_office <2680813175@qq.com> Date: Mon, 27 Apr 2026 21:56:05 +0800 Subject: [PATCH 08/12] rad2deg --- konabot/docs/user/march.txt | 2 +- konabot/plugins/marchtoy/obj.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/konabot/docs/user/march.txt b/konabot/docs/user/march.txt index d4c0ef0..e9a8a82 100644 --- a/konabot/docs/user/march.txt +++ b/konabot/docs/user/march.txt @@ -1,7 +1,7 @@ # 指令介绍 简易 Raymarch 小玩具 用法:march `` -例:march sphere(1).color(red) box(0.5, 2.0, 0.5).pos(0, 0, 0) cam(2.5).pos(-5).lookat(0) +例:march torus(1.0, 0.2).color(1.0, 0.2, 0.2) torus(1.0, 0.2).rot(90, 0, 0).color(0.2, 0.2, 1.0) camera(4.0).pos(4, 0, 0).lookat(0) # 主要语法 `` ::= `` "." `` |`` diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 32d023f..e70a279 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -39,6 +39,9 @@ class Transform: return self def rotate(self, x: float, y: float, z: float): + x = x / 180 * np.pi + y = y / 180 * np.pi + z = z / 180 * np.pi cx, sx = np.cos(x), np.sin(x) cy, sy = np.cos(y), np.sin(y) cz, sz = np.cos(z), np.sin(z) From 3175817b63d591e7d5c2eb182c5ee91b8bcfada5 Mon Sep 17 00:00:00 2001 From: alcoholicgirl <2680813175@qq.com> Date: Mon, 27 Apr 2026 22:49:19 +0800 Subject: [PATCH 09/12] bool addition, few fixes --- konabot/plugins/marchtoy/gl_render.py | 6 ++++-- konabot/plugins/marchtoy/obj.py | 21 +++++++++++++++++++++ konabot/plugins/marchtoy/scene.py | 7 +++---- konabot/plugins/marchtoy/shaders/frag.glsl | 3 ++- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/konabot/plugins/marchtoy/gl_render.py b/konabot/plugins/marchtoy/gl_render.py index e54468f..784c9e6 100644 --- a/konabot/plugins/marchtoy/gl_render.py +++ b/konabot/plugins/marchtoy/gl_render.py @@ -14,8 +14,10 @@ with (PATH / "vert.glsl").open(encoding='utf-8') as f: async def render(command: str, res: tuple[int, int]): fs = Scene(command).compile() - - ctx = moderngl.create_context(standalone=True, backend="egl") + try: + ctx = moderngl.create_context(standalone=True) + except: + ctx = moderngl.create_context(standalone=True, backend="egl") ctx.gc_mode = "auto" try: program = ctx.program( diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index e70a279..019bb3a 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -244,6 +244,27 @@ class BoolSubstract(Object): def sdf_block_glsl(self): return f"max({self.block_a}, -{self.block_b})" +class BoolAddition(Object): + def __init__(self): + super().__init__() + + def parse_args(self, args: list[str]): + from konabot.plugins.marchtoy.scene import Scene + try: + if not len(args) >= 2: + raise Exception("expecting at least 2 args") + scene_a = Scene(args[0]) + scene_b = Scene(args[1]) + self.block_a = scene_a.canvas_objs[0][1] + self.block_b = scene_b.canvas_objs[0][1] + except Exception as e: + raise Exception(f"cannot build bool object over {args}: {e}") + + def sdf_block_glsl(self): + return f"min({self.block_a}, {self.block_b})" + + + @make_obj("camera", "cam") class Camera(Object): def __init__(self, _focus: float = 2.0) -> None: diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py index 2162872..9a0029c 100644 --- a/konabot/plugins/marchtoy/scene.py +++ b/konabot/plugins/marchtoy/scene.py @@ -69,11 +69,10 @@ class Scene: index = 0 for canvas_item in self.canvas_objs: obj, sdf_expr = canvas_item - sdf_block += f"float sd{index} = {sdf_expr};" - sdf_block += f"if(sd{index} < qry.value)" - # 1e-8 最好换成某个 epsilon round_corner = f"- {obj.round_corner}" if obj.round_corner > 1e-8 else "" - sdf_block += "{" + f"qry.value = sd{index} {round_corner}; qry.obj_id = {index}; " + "}\n" + sdf_block += f"float sd{index} = {sdf_expr}{round_corner};" + sdf_block += f"if(sd{index} < qry.value)" + sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" color = obj.texture.color color_block += ( f"if(obj_id == {index}) return vec4(" diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index 550d768..3aca0f7 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -1,6 +1,7 @@ #version 330 const float EPS = 0.001; const int MAX_ITER = 128; +const float INF = 1e10; uniform vec2 u_resolution; out vec4 fragColor; @@ -43,7 +44,7 @@ float smin( float a, float b, float k ) } sdQuery sd(vec3 p) { sdQuery qry; - qry.value = 100000000.0; + qry.value = INF; qry.obj_id = -1; return qry; From 0afbbd2fdfc1a8245f0a5853483bc19472db4068 Mon Sep 17 00:00:00 2001 From: alcoholicgirl <2680813175@qq.com> Date: Mon, 27 Apr 2026 23:08:25 +0800 Subject: [PATCH 10/12] bug fixes --- konabot/plugins/marchtoy/obj.py | 3 ++- konabot/plugins/marchtoy/op.py | 2 +- konabot/plugins/marchtoy/scene.py | 13 +++++++------ konabot/plugins/marchtoy/shaders/frag.glsl | 15 ++++++++++++--- konabot/plugins/marchtoy/utilities.py | 2 +- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 019bb3a..36cf50a 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -23,7 +23,7 @@ class Transform: @staticmethod def normalize(p: np.ndarray) -> np.ndarray: - return p / (np.sqrt(np.dot(p, p)) + 1e-8) + return p / (np.linalg.norm(p) + 1e-8) def translate(self, x: float, y: float, z: float): mat = np.identity(4, dtype=np.float32) @@ -244,6 +244,7 @@ class BoolSubstract(Object): def sdf_block_glsl(self): return f"max({self.block_a}, -{self.block_b})" +@make_obj("add", "addition") class BoolAddition(Object): def __init__(self): super().__init__() diff --git a/konabot/plugins/marchtoy/op.py b/konabot/plugins/marchtoy/op.py index c9cb7b6..8bde49c 100644 --- a/konabot/plugins/marchtoy/op.py +++ b/konabot/plugins/marchtoy/op.py @@ -52,5 +52,5 @@ def color(obj: Object, args: list[str]): @make_op("rounded", "round_corner", "corner") def rounded(obj: Object, args: list[str]): if len(args) >= 1: - obj.round_corner = ArgParser.as_float(args[0]) + obj.round_corner = ArgParser.as_float(args) \ No newline at end of file diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py index 9a0029c..dff3184 100644 --- a/konabot/plugins/marchtoy/scene.py +++ b/konabot/plugins/marchtoy/scene.py @@ -19,7 +19,7 @@ class Scene: cmd_queue = CommandParser(raw_cmd) cmd_obj = next(cmd_queue) obj_id, obj_args = cmd_obj.id, cmd_obj.args - logger.info(f"parsing object: {obj_id} with args {obj_args}") + # logger.info(f"parsing object: {obj_id} with args {obj_args}") obj_instance: Optional[Object] = None if obj_id in OBJECT_ENTRIES: obj_cls = OBJECT_ENTRIES[obj_id] @@ -35,11 +35,11 @@ class Scene: ) from e else: raise Exception(f"{obj_id} is not a valid object type.") - logger.info(f"parsed object {obj_id}({obj_args})") + # logger.info(f"parsed object {obj_id}({obj_args})") if obj_instance != None: for cmd in cmd_queue: op_id, op_args = cmd.id, cmd.args - logger.info(f"parsing operation {op_id} with args {op_args}") + # logger.info(f"parsing operation {op_id} with args {op_args}") if op_id in OPERATION_ENTRIES: op_func = OPERATION_ENTRIES[op_id] if not callable(op_func): @@ -47,14 +47,14 @@ class Scene: op_func(obj_instance, op_args) else: raise Exception(f"{op_id} is not a valid operation.") - logger.info(f"parsed operation {op_id}({op_args})") + # logger.info(f"parsed operation {op_id}({op_args})") try: sdf_block = obj_instance.sdf_block_glsl() self.canvas_objs.append((obj_instance, sdf_block)) - logger.info(f"parsed sdf {sdf_block}") + # logger.info(f"parsed sdf {sdf_block}") except: - logger.info(f"parsed camera") + # logger.info(f"parsed camera") if type(obj_instance) == Camera: self.camera = obj_instance @@ -70,6 +70,7 @@ class Scene: for canvas_item in self.canvas_objs: obj, sdf_expr = canvas_item round_corner = f"- {obj.round_corner}" if obj.round_corner > 1e-8 else "" + logger.info(round_corner) sdf_block += f"float sd{index} = {sdf_expr}{round_corner};" sdf_block += f"if(sd{index} < qry.value)" sdf_block += "{" + f"qry.value = sd{index}; qry.obj_id = {index}; " + "}\n" diff --git a/konabot/plugins/marchtoy/shaders/frag.glsl b/konabot/plugins/marchtoy/shaders/frag.glsl index 3aca0f7..951c29e 100644 --- a/konabot/plugins/marchtoy/shaders/frag.glsl +++ b/konabot/plugins/marchtoy/shaders/frag.glsl @@ -36,12 +36,21 @@ float sdTorus( vec3 p, vec2 t ) vec2 q = vec2(length(p.xz)-t.x,p.y); return length(q)-t.y; } +// float smin( float a, float b, float k ) +// { +// k *= 1.0; +// float r = exp2(-a/k) + exp2(-b/k); +// return -k*log2(r); +// } + float smin( float a, float b, float k ) { - k *= 1.0; - float r = exp2(-a/k) + exp2(-b/k); - return -k*log2(r); + k *= 2.0; + float x = (b-a)/k; + float g = 0.5*(x+sqrt(x*x+1.0)); + return b - k * g; } + sdQuery sd(vec3 p) { sdQuery qry; qry.value = INF; diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index 4491654..ed478d5 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -92,4 +92,4 @@ class ArgParser: def as_literal_color(args: list[str], default=np.array((1.0, 1.0, 1.0, 1.0))): if len(args) == 1 and args[0] in COLORS: return np.array(COLORS[args[0]]) - return default + return default \ No newline at end of file From 7ba30350066da009f52f6edce47746db07b42427 Mon Sep 17 00:00:00 2001 From: alcoholicgirl <2680813175@qq.com> Date: Mon, 27 Apr 2026 23:19:59 +0800 Subject: [PATCH 11/12] robustness + regex fix --- konabot/plugins/marchtoy/command.py | 2 +- konabot/plugins/marchtoy/utilities.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index 9243299..3e7fbdd 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -42,7 +42,7 @@ class CommandChainParser: class CommandParser: - CMD_PATTERN = r"^[a-zA-Z]+(?:\(([0-9.]+|(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*))(\s*\,\s*(?1))*\))?" + CMD_PATTERN = r"^[a-zA-Z]+(?:\(([0-9.\-+]+|(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*))(\s*\,\s*(?1))*\))?" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)" ARG_PATTERN = CommandChainParser.CHAIN_PATTERN TRIM_PATTERN = r"^\s*\,\s*" diff --git a/konabot/plugins/marchtoy/utilities.py b/konabot/plugins/marchtoy/utilities.py index ed478d5..c7d5a4d 100644 --- a/konabot/plugins/marchtoy/utilities.py +++ b/konabot/plugins/marchtoy/utilities.py @@ -19,6 +19,7 @@ TODO: 除零出现 nan 情况的单独处理 class ArgParser: @staticmethod def to_params(args: str, delim: str = ',') -> list[str]: + raise DeprecationWarning _params = args.replace(" ", "").split(delim) params: list[str] = [] # 还是避免 while 为好 @@ -28,11 +29,14 @@ class ArgParser: return params @staticmethod - def as_float(args: list[str], default: float = 0.0) -> float: + def as_float(args: list[str] | str, default: float = 0.0) -> float: try: - if len(args) >= 1: + if isinstance(args, list) and len(args) >= 1: x = float(args[0]) return x + elif isinstance(args, str): + x = float(args) + return x except: # raise Exception(f"cannot parse {args}") return default @@ -66,9 +70,9 @@ class ArgParser: y = float(args[1]) z = float(args[2]) return np.array((x, y, z)) + return default except: raise Exception(f"cannot parse {args}") - return default @staticmethod def as_vec4( From 8f1e0b11a09d80259142ef75c9cb355b94cab0ec Mon Sep 17 00:00:00 2001 From: alcoholicgirl <2680813175@qq.com> Date: Mon, 27 Apr 2026 23:31:38 +0800 Subject: [PATCH 12/12] regex fix, camera default fix --- konabot/plugins/marchtoy/command.py | 2 +- konabot/plugins/marchtoy/obj.py | 2 +- konabot/plugins/marchtoy/scene.py | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/konabot/plugins/marchtoy/command.py b/konabot/plugins/marchtoy/command.py index 3e7fbdd..a201219 100644 --- a/konabot/plugins/marchtoy/command.py +++ b/konabot/plugins/marchtoy/command.py @@ -25,7 +25,7 @@ class Command: class CommandChainParser: - CHAIN_PATTERN = r"^(([a-zA-Z0-9]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*)" + CHAIN_PATTERN = r"^(([a-zA-Z0-9\-+]+(?:\(([^()]*|(?1)+)(\s*\,\s*(?1))*\))?)(\.(?1))*)" def __init__(self, _command_chain: str) -> None: self.command_chain = _command_chain diff --git a/konabot/plugins/marchtoy/obj.py b/konabot/plugins/marchtoy/obj.py index 36cf50a..a27831f 100644 --- a/konabot/plugins/marchtoy/obj.py +++ b/konabot/plugins/marchtoy/obj.py @@ -271,7 +271,7 @@ class Camera(Object): def __init__(self, _focus: float = 2.0) -> None: super().__init__() self.focus = _focus - self.transform.translate(8.0, 8.0, 8.0).lookat(0.0, 0.0, 0.0) + # self.transform.translate(8.0, 8.0, 8.0).lookat(0.0, 0.0, 0.0) def parse_args(self, args: list[str]): self.focus = ArgParser.as_float(args) diff --git a/konabot/plugins/marchtoy/scene.py b/konabot/plugins/marchtoy/scene.py index dff3184..d6a3308 100644 --- a/konabot/plugins/marchtoy/scene.py +++ b/konabot/plugins/marchtoy/scene.py @@ -14,12 +14,14 @@ class Scene: from konabot.plugins.marchtoy.obj import Object, Camera, OBJECT_ENTRIES logger.info(f"building scene: {_instruction}") self.canvas_objs: list[tuple[Object, str]] = [] - self.camera: Camera = Camera() + self.camera: Camera = Camera(1.0 / np.tan(np.deg2rad(30.0))) + self.camera.transform.translate(8.0, 8.0, 8.0).lookat(0.0, 0.0, 0.0) + for raw_cmd in CommandChainParser(_instruction): cmd_queue = CommandParser(raw_cmd) cmd_obj = next(cmd_queue) obj_id, obj_args = cmd_obj.id, cmd_obj.args - # logger.info(f"parsing object: {obj_id} with args {obj_args}") + logger.info(f"parsing object: {obj_id} with args {obj_args}") obj_instance: Optional[Object] = None if obj_id in OBJECT_ENTRIES: obj_cls = OBJECT_ENTRIES[obj_id] @@ -35,11 +37,11 @@ class Scene: ) from e else: raise Exception(f"{obj_id} is not a valid object type.") - # logger.info(f"parsed object {obj_id}({obj_args})") + logger.info(f"parsed object {obj_id}({obj_args})") if obj_instance != None: for cmd in cmd_queue: op_id, op_args = cmd.id, cmd.args - # logger.info(f"parsing operation {op_id} with args {op_args}") + logger.info(f"parsing operation {op_id} with args {op_args}") if op_id in OPERATION_ENTRIES: op_func = OPERATION_ENTRIES[op_id] if not callable(op_func): @@ -47,12 +49,12 @@ class Scene: op_func(obj_instance, op_args) else: raise Exception(f"{op_id} is not a valid operation.") - # logger.info(f"parsed operation {op_id}({op_args})") + logger.info(f"parsed operation {op_id}({op_args})") try: sdf_block = obj_instance.sdf_block_glsl() self.canvas_objs.append((obj_instance, sdf_block)) - # logger.info(f"parsed sdf {sdf_block}") + logger.info(f"parsed sdf {sdf_block}") except: # logger.info(f"parsed camera") if type(obj_instance) == Camera: