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 小玩具
用法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 `<scene>`
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>
| <obj>
`<scene>` ::= `<scene>` "." `<op>` |`<obj>`
<obj> ::= <obj_ty>
| <obj_ty> "(" <args> ")"
`<obj>` ::= `<obj_ty>` | `<obj_ty>` "(" <args> ")"
<op> ::= <op_ty>
| <op_ty> "(" <args> ")"
`<op>` ::= `<op_ty>` | `<op_ty>` "(" `<args>` ")"
<args> ::= <args> "," <arg>
| <arg>
`<args>` ::= `<args>` "," `<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_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()
@ -12,8 +14,9 @@ async def _(args: Message = CommandArg()):
img = await render.render(cmd, (512, 512))
buffer = io.BytesIO()
# img.show()
# img.save("/mnt/d/output.png", format="GIF")
img.save(buffer, format="PNG")
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())
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)
"""
import pathlib
from dataclasses import dataclass
import re
import numpy as np
import regex as re
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:
@ -29,7 +25,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,8 +42,10 @@ class CommandChainParser:
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]+(?=\(|\.|$)"
ARG_PATTERN = CommandChainParser.CHAIN_PATTERN
TRIM_PATTERN = r"^\s*\,\s*"
def __init__(self, _command: str) -> None:
self.command = _command
@ -59,90 +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 "" 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()
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
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(id, args)
raise StopIteration

View File

@ -5,23 +5,31 @@ 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
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()
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)
try:
ctx = moderngl.create_context(standalone=True)
except:
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']
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)
@ -32,4 +40,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)
return Image.frombytes('RGBA', fbo.size, fbo.read(components=4), 'raw', 'RGBA', 0, -1)

View File

@ -1,7 +1,9 @@
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
import re
OBJECT_ENTRIES = {}
@ -21,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)
@ -37,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)
@ -72,18 +77,27 @@ 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.to_vec4(inv)} * vec4(p, 1.0)).xyz"
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
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
@ -93,23 +107,22 @@ class Cube(Object):
super().__init__()
self.size = _size
def parse_args(self, args: list[str]):
def parse_args(self, args: 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]}))"
@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]):
def parse_args(self, args: 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})"
@ -120,17 +133,16 @@ class Cylinder(Object):
self.radius = _radius
self.height = _height
def parse_args(self, args: list[str]):
def parse_args(self, args: str):
param = ArgParser.as_vec2(args)
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})"
)
@make_obj("torus")
class Torus(Object):
def __init__(self, _r1: float = 1.0, _r2: float = 0.4) -> None:
@ -143,7 +155,7 @@ 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}))"
@ -151,24 +163,116 @@ class Torus(Object):
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_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")
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)

View File

@ -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:
@ -44,7 +44,13 @@ 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")
@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
// compatibility
#define float4x4 mat4x4
#define float4 vec4
#define float3 vec3
#define float2 vec2
const float EPS = 0.001;
const int MAX_ITER = 128;
const float INF = 1e10;
uniform vec2 u_resolution;
out vec4 fragColor;
@ -41,10 +36,24 @@ 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 *= 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 = 100000000.0;
qry.value = INF;
qry.obj_id = -1;
<SDF_BLOCK>
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 regex as re
from konabot.plugins.marchtoy.texture import COLORS
class Formatter:
@staticmethod
def float4(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]])
@ -27,14 +18,28 @@ TODO: 除零出现 nan 情况的单独处理
"""
class ArgParser:
@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:
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
# raise Exception(f"cannot parse {args}")
return default
@staticmethod
def as_vec2(
@ -65,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(
@ -91,4 +96,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

131
poetry.lock generated
View File

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

View File

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

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