Merge remote-tracking branch 'origin/master' into fix/database-lock

This commit is contained in:
2026-04-27 23:49:51 +08:00
13 changed files with 476 additions and 226 deletions

View File

@ -1,22 +1,40 @@
# 指令介绍 # 指令介绍
简易 Raymarch 小玩具 简易 Raymarch 小玩具
用法march <scene> 用法march `<scene>`
march sphere(1).color(red) box(0.5, 2.0, 0.5).pos(0, 0, 0) cam(0.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)
# 主要语法 # 主要语法
<scene> ::= <scene> "." <op> `<scene>` ::= `<scene>` "." `<op>` |`<obj>`
| <obj>
<obj> ::= <obj_ty> `<obj>` ::= `<obj_ty>` | `<obj_ty>` "(" <args> ")"
| <obj_ty> "(" <args> ")"
<op> ::= <op_ty> `<op>` ::= `<op_ty>` | `<op_ty>` "(" `<args>` ")"
| <op_ty> "(" <args> ")"
<args> ::= <args> "," <arg> `<args>` ::= `<args>` "," `<arg>` | `<arg>`
| <arg>
其中 obj_tyop_ty 分别为物体类型(如 cubespheretorus 等)与变换类型(如 posrot 其中 `obj_ty`、`op_ty` 分别为物体类型(如 `cube`、`sphere`、`torus` 等)与变换类型(如 `pos`、`rot`)。
# 支持的物体
目前支持的物体有(不包含 alias
`cube`:可选参数长宽高
`sphere`:可选参数半径
`torus`:可选参数半径与粗细
`cylinder`:可选参数半径与高度
`capsule`:可选参数高度与半径
特殊物体:
`mix`:混合两个物体
`bool`:两个物体相交
`minus`:两个物体相减
`camera`:相机,可选参数焦距
# 支持的变换
目前支持的变换有
`pos`:位移
`rot`:旋转(欧拉角 xyz
`color`:基础色
`lookat`:朝向
`rounded`:圆角
# 特殊说明 # 特殊说明
<op_ty> 不包含 scale,因为本工具基于 SDF 渲染,非正交的变换会破坏 SDF 的性质。 `<op_ty>` 不包含 scale非正交的变换会破坏 SDF 的性质。

View File

@ -2,7 +2,9 @@ from nonebot import on_command
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
from nonebot.params import CommandArg from nonebot.params import CommandArg
import konabot.plugins.marchtoy.gl_render as render import konabot.plugins.marchtoy.gl_render as render
# import konabot.plugins.marchtoy.cpu_render as render
import io import io
cmd_marchtoy = on_command("march") cmd_marchtoy = on_command("march")
@cmd_marchtoy.handle() @cmd_marchtoy.handle()
@ -12,8 +14,9 @@ async def _(args: Message = CommandArg()):
img = await render.render(cmd, (512, 512)) img = await render.render(cmd, (512, 512))
buffer = io.BytesIO() buffer = io.BytesIO()
# img.show() # img.show()
# img.save("/mnt/d/output.png", format="GIF")
img.save(buffer, format="PNG") img.save(buffer, format="PNG")
buffer.seek(0) buffer.seek(0)
await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export()) await cmd_marchtoy.send(await UniMessage().image(raw=buffer).export())
except Exception as e: except Exception as e:
await cmd_marchtoy.send(await UniMessage.text(f"发生了错误: {e}").export()) await cmd_marchtoy.send(await UniMessage.text(f"cannot render: {e}").export())

View File

@ -13,14 +13,10 @@ example:
march cube.pos(0, 0, 0).color(red) cam(1.0).pos(1, 1, 1).lookat(0, 0, 0) 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 from dataclasses import dataclass
import re import regex as re
import numpy as np
from dataclasses import dataclass 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 @dataclass
class Command: class Command:
@ -29,7 +25,7 @@ class Command:
class CommandChainParser: 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: def __init__(self, _command_chain: str) -> None:
self.command_chain = _command_chain self.command_chain = _command_chain
@ -46,8 +42,10 @@ class CommandChainParser:
class CommandParser: class CommandParser:
CMD_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?" 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]+(?=\(|\.|$)" ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)"
ARG_PATTERN = CommandChainParser.CHAIN_PATTERN
TRIM_PATTERN = r"^\s*\,\s*"
def __init__(self, _command: str) -> None: def __init__(self, _command: str) -> None:
self.command = _command self.command = _command
@ -59,90 +57,17 @@ class CommandParser:
if query := re.match(CommandParser.CMD_PATTERN, self.command): if query := re.match(CommandParser.CMD_PATTERN, self.command):
cmd = query[0] cmd = query[0]
if cmd_id_qry := re.match(CommandParser.ID_PATTERN, cmd): 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 :] 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):
while "" in cmd_args: arg = cmd_arg_qry[0]
cmd_args.remove("") args.append(arg)
return Command(cmd_id, cmd_args) cmd_args = cmd_args[len(arg) :]
raise StopIteration if trim_qry := re.match(CommandParser.TRIM_PATTERN, cmd_args):
cmd_args = cmd_args[len(trim_qry[0]) :]
class Scene: # while "" in cmd_args:
def __init__(self, _instruction: str) -> None: # cmd_args.remove("")
self.canvas_objs: list[tuple[Object, str]] = [] return Command(id, args)
self.camera: Camera = Camera() raise StopIteration
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()
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 float4("
f"{color[0]}, {color[1]}, {color[2]}, {color[3]});\n"
)
index += 1
content = content.replace("<SDF_BLOCK>", sdf_block)
content = content.replace("<COLOR_BLOCK>", 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(
"<CAM_POS>", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}"
)
content = content.replace("<CAM_FOCUS>", str(cam_focus))
content = content.replace(
"<CAM_DIR>", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}"
)
return content

View File

@ -5,23 +5,31 @@ import pathlib
import moderngl import moderngl
import numpy as np import numpy as np
from PIL import Image from PIL import Image
from konabot.plugins.marchtoy.command import Scene 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]): async def render(command: str, res: tuple[int, int]):
fs = Scene(command).compile() fs = Scene(command).compile()
PATH = pathlib.Path(__file__).parent / "shaders" try:
with (PATH / "vert.glsl").open(encoding='utf-8') as f: ctx = moderngl.create_context(standalone=True)
vs = f.read() except:
ctx = moderngl.create_context(standalone=True) ctx = moderngl.create_context(standalone=True, backend="egl")
ctx.gc_mode = "auto" ctx.gc_mode = "auto"
try: try:
program = ctx.program( program = ctx.program(
vertex_shader=vs, vertex_shader=VS_SRC,
fragment_shader=fs fragment_shader=fs
) )
except Exception as e: except Exception as e:
raise Exception(f"cannot compile glsl: {e}") from 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)) 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) 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) indices = np.array([0, 1, 2, 1, 2, 3], dtype=np.int32)
@ -32,4 +40,5 @@ async def render(command: str, res: tuple[int, int]):
fbo.use() fbo.use()
fbo.clear(0.0, 0.0, 0.0, 0.0) fbo.clear(0.0, 0.0, 0.0, 0.0)
vao.render(moderngl.TRIANGLES) vao.render(moderngl.TRIANGLES)
return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1) return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1)

View File

@ -1,7 +1,9 @@
import numpy as np import numpy as np
from konabot.plugins.marchtoy.texture import Texture from konabot.plugins.marchtoy.texture import Texture
from konabot.plugins.marchtoy.utilities import ArgParser, Formatter from konabot.plugins.marchtoy.utilities import ArgParser, Formatter
from nonebot import logger
from typing import Optional
import re
OBJECT_ENTRIES = {} OBJECT_ENTRIES = {}
@ -21,7 +23,7 @@ class Transform:
@staticmethod @staticmethod
def normalize(p: np.ndarray) -> np.ndarray: 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): def translate(self, x: float, y: float, z: float):
mat = np.identity(4, dtype=np.float32) mat = np.identity(4, dtype=np.float32)
@ -37,6 +39,9 @@ class Transform:
return self return self
def rotate(self, x: float, y: float, z: float): 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) cx, sx = np.cos(x), np.sin(x)
cy, sy = np.cos(y), np.sin(y) cy, sy = np.cos(y), np.sin(y)
cz, sz = np.cos(z), np.sin(z) cz, sz = np.cos(z), np.sin(z)
@ -72,18 +77,27 @@ class Transform:
def p_expr(self) -> str: def p_expr(self) -> str:
inv = np.linalg.inv(self.t) # + 1e-5 * np.identity(4, dtype=np.float32)) 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.to_vec4(inv)} * vec4(p, 1.0)).xyz"
class Object: class Object:
def __init__(self) -> None: def __init__(self) -> None:
self.transform: Transform = Transform() self.transform: Transform = Transform()
self.texture: Texture = Texture() self.texture: Texture = Texture()
self.round_corner: float = 0.0
def parse_args(self, args: list[str]): def parse_args(self, args: list[str]):
raise NotImplementedError 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 raise NotImplementedError
@ -93,23 +107,22 @@ class Cube(Object):
super().__init__() super().__init__()
self.size = _size self.size = _size
def parse_args(self, args: list[str]): def parse_args(self, args: str):
self.size = ArgParser.as_vec3(args) 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]}))" return f"sdCube({self.transform.p_expr()}, vec3({self.size[0]}, {self.size[1]}, {self.size[2]}))"
@make_obj("sphere", "ball") @make_obj("sphere", "ball")
class Sphere(Object): class Sphere(Object):
def __init__(self, _radius: float = 1.0) -> None: def __init__(self, _radius: float = 1.0) -> None:
super().__init__() super().__init__()
self.radius = _radius self.radius = _radius
def parse_args(self, args: list[str]): def parse_args(self, args: str):
self.radius = ArgParser.as_float(args) 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})" return f"sdSphere({self.transform.p_expr()}, {self.radius})"
@ -120,17 +133,16 @@ class Cylinder(Object):
self.radius = _radius self.radius = _radius
self.height = _height self.height = _height
def parse_args(self, args: list[str]): def parse_args(self, args: str):
param = ArgParser.as_vec2(args) param = ArgParser.as_vec2(args)
self.radius = param[0] self.radius = param[0]
self.height = param[1] self.height = param[1]
def sdf_block(self) -> str: def sdf_block_glsl(self) -> str:
return ( return (
f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})" f"sdCappedCylinder({self.transform.p_expr()}, {self.radius}, {self.height})"
) )
@make_obj("torus") @make_obj("torus")
class Torus(Object): class Torus(Object):
def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None: def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None:
@ -143,7 +155,7 @@ class Torus(Object):
self.r1 = param[0] self.r1 = param[0]
self.r2 = param[1] 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}))" return f"sdTorus({self.transform.p_expr()}, vec2({self.r1}, {self.r2}))"
@ -151,24 +163,116 @@ class Torus(Object):
class Capsule(Object): class Capsule(Object):
def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None: def __init__(self, _h: float = 1.0, _r: float = 0.25) -> None:
super().__init__() super().__init__()
self._h = _h self.h = _h
self._r = _r self.r = _r
def parse_args(self, args: list[str]): def parse_args(self, args: list[str]):
param = ArgParser.as_vec2(args) param = ArgParser.as_vec2(args)
self._h = param[0] self.h = param[0]
self._r = param[1] self.r = param[1]
def sdf_block_glsl(self) -> str:
return f"sdVerticalCapsule({self.transform.p_expr()}, {self.h}, {self.r})"
@make_obj("smoothed", "mix")
class Smoothed(Object):
def __init__(self, _k: float = 12.00):
super().__init__()
self.block_a: str = "INF"
self.block_b: str = "INF"
self.k: float = _k
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 smoothed object over {args}: {e}")
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("add", "addition")
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})"
def sdf_block(self) -> str:
return f"sdVerticalCapsule({self.transform.p_expr()}, {self._h}, {self._r})"
@make_obj("camera", "cam") @make_obj("camera", "cam")
class Camera(Object): class Camera(Object):
def __init__(self, _focus: float = 1.0) -> None: def __init__(self, _focus: float = 2.0) -> None:
super().__init__() super().__init__()
self.focus = _focus 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]): def parse_args(self, args: list[str]):
self.focus = ArgParser.as_float(args) self.focus = ArgParser.as_float(args)

View File

@ -3,7 +3,7 @@ from konabot.plugins.marchtoy.utilities import ArgParser
OPERATION_ENTRIES = {} OPERATION_ENTRIES = {}
def make_operation(*name: str): def make_op(*name: str):
def decorator(op): def decorator(op):
# OPERATION_ENTRIES[name] = op # OPERATION_ENTRIES[name] = op
for alias in [*name]: for alias in [*name]:
@ -13,25 +13,25 @@ def make_operation(*name: str):
return decorator return decorator
@make_operation("pos", "translate", "position", "p") @make_op("pos", "translate", "position", "p")
def translate(obj: Object, args: list[str]): def translate(obj: Object, args: list[str]):
pos = ArgParser.as_vec3(args) pos = ArgParser.as_vec3(args)
obj.transform.translate(pos[0], pos[1], pos[2]) 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]): def rotate(obj: Object, args: list[str]):
pos = ArgParser.as_vec3(args) pos = ArgParser.as_vec3(args)
obj.transform.rotate(pos[0], pos[1], pos[2]) 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]): def lookat(obj: Object, args: list[str]):
pos = ArgParser.as_vec3(args) pos = ArgParser.as_vec3(args)
obj.transform.lookat(pos[0], pos[1], pos[2]) 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]): def color(obj: Object, args: list[str]):
try: try:
if len(args) == 1: if len(args) == 1:
@ -44,7 +44,13 @@ def color(obj: Object, args: list[str]):
col = ArgParser.as_vec4(args) col = ArgParser.as_vec4(args)
obj.texture.color = (col[0], col[1], col[2], col[3]) obj.texture.color = (col[0], col[1], col[2], col[3])
else: else:
raise Exception("unknown color") raise Exception("invalid argument number")
except: except:
raise Exception("unknown color") 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)

View File

@ -0,0 +1,100 @@
from typing import Optional
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
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(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}")
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.")
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):
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.")
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
def __str__(self) -> str:
return ", ".join([str(type(obj)) for obj in self.canvas_objs])
def compile(self) -> str:
sdf_block: str = ""
color_block: str = ""
index = 0
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"
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 = FS_SRC
content = content.replace("<SDF_BLOCK>", sdf_block)
content = content.replace("<COLOR_BLOCK>", 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(
"<CAM_POS>", f"{cam_pos[0]}, {cam_pos[1]}, {cam_pos[2]}"
)
content = content.replace("<CAM_FOCUS>", str(cam_focus))
content = content.replace(
"<CAM_DIR>", f"{cam_dir[0]}, {cam_dir[1]}, {cam_dir[2]}"
)
return content

View File

@ -1,12 +1,7 @@
#version 330 #version 330
// compatibility
#define float4x4 mat4x4
#define float4 vec4
#define float3 vec3
#define float2 vec2
const float EPS = 0.001; const float EPS = 0.001;
const int MAX_ITER = 128; const int MAX_ITER = 128;
const float INF = 1e10;
uniform vec2 u_resolution; uniform vec2 u_resolution;
out vec4 fragColor; out vec4 fragColor;
@ -41,10 +36,24 @@ float sdTorus( vec3 p, vec2 t )
vec2 q = vec2(length(p.xz)-t.x,p.y); vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.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 *= 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 sd(vec3 p) {
sdQuery qry; sdQuery qry;
qry.value = 100000000.0; qry.value = INF;
qry.obj_id = -1; qry.obj_id = -1;
<SDF_BLOCK> <SDF_BLOCK>
return qry; return qry;

View File

@ -1,42 +0,0 @@
raise DeprecationWarning
from command import Scene
import skia
import struct
from PIL import Image
import numpy as np
# 暂时先照抄小帕的了,之后有空再单独封装一下
async def render(cmd: str, width: int, height: int):
scene = Scene(cmd)
surface = skia.Surface(width, height)
sksl_code = scene.compile()
runtime_effect = skia.RuntimeEffect.MakeForShader(scene.compile())
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

View File

@ -1,19 +1,10 @@
import numpy as np import numpy as np
import regex as re
from konabot.plugins.marchtoy.texture import COLORS from konabot.plugins.marchtoy.texture import COLORS
class Formatter: class Formatter:
@staticmethod @staticmethod
def float4(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]])
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:
if m.shape != (4, 4): if m.shape != (4, 4):
m = np.identity(4) m = np.identity(4)
v_0 = ", ".join([str(x) for x in m[:, 0]]) v_0 = ", ".join([str(x) for x in m[:, 0]])
@ -27,14 +18,28 @@ TODO: 除零出现 nan 情况的单独处理
""" """
class ArgParser: class ArgParser:
@staticmethod @staticmethod
def as_float(args: list[str], default: float = 0.0) -> float: def to_params(args: str, delim: str = ',') -> list[str]:
raise DeprecationWarning
_params = args.replace(" ", "").split(delim)
params: list[str] = []
# 还是避免 while 为好
for param in _params:
if param != "":
params.append(param)
return params
@staticmethod
def as_float(args: list[str] | str, default: float = 0.0) -> float:
try: try:
if len(args) >= 1: if isinstance(args, list) and len(args) >= 1:
x = float(args[0]) x = float(args[0])
return x return x
elif isinstance(args, str):
x = float(args)
return x
except: except:
raise Exception(f"cannot parse {args}") # raise Exception(f"cannot parse {args}")
return default return default
@staticmethod @staticmethod
def as_vec2( def as_vec2(
@ -65,9 +70,9 @@ class ArgParser:
y = float(args[1]) y = float(args[1])
z = float(args[2]) z = float(args[2])
return np.array((x, y, z)) return np.array((x, y, z))
return default
except: except:
raise Exception(f"cannot parse {args}") raise Exception(f"cannot parse {args}")
return default
@staticmethod @staticmethod
def as_vec4( def as_vec4(
@ -91,4 +96,4 @@ class ArgParser:
def as_literal_color(args: list[str], default=np.array((1.0, 1.0, 1.0, 1.0))): 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: if len(args) == 1 and args[0] in COLORS:
return np.array(COLORS[args[0]]) return np.array(COLORS[args[0]])
return default return default

131
poetry.lock generated
View File

@ -4400,6 +4400,135 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors" 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]] [[package]]
name = "requests" name = "requests"
version = "2.33.1" version = "2.33.1"
@ -5525,4 +5654,4 @@ reference = "mirrors"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12,<4.0" python-versions = ">=3.12,<4.0"
content-hash = "ce664db91fb8047d071f8d47890cd053412a961e594db41953345e87bc34c64a" content-hash = "91e6623a2ea57cf1189d99cfc085abb3e5fe46f7fd1d68390292ab8d5f0fa304"

View File

@ -40,6 +40,7 @@ dependencies = [
"pytest-mock (>=3.15.1,<4.0.0)", "pytest-mock (>=3.15.1,<4.0.0)",
"skia-python (>=144.0.post2,<145.0)", "skia-python (>=144.0.post2,<145.0)",
"moderngl (>=5.12.0,<6.0.0)", "moderngl (>=5.12.0,<6.0.0)",
"regex (>=2026.4.4,<2027.0.0)",
] ]
[tool.poetry] [tool.poetry]

View File

@ -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