Files
konabot/konabot/plugins/marchtoy/command.py
alcoholicgirl b4f167e5f6 regex fix
2026-04-27 02:07:01 +08:00

149 lines
5.2 KiB
Python

"""
raymarch toy
usage: march <scene1> <scene2> ... <sceneN>
<scene> ::= <scene> "." <command>
| <command>
<command> ::= <id>
| <id> "(" <args> ")"
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
args: list[str]
class CommandChainParser:
CHAIN_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?(\.[a-zA-Z]+(\([^(]*\))?)*"
def __init__(self, _command_chain: str) -> None:
self.command_chain = _command_chain
def __iter__(self):
return self
def __next__(self):
if query := re.match(CommandChainParser.CHAIN_PATTERN, self.command_chain):
cmd_chain = query[0]
self.command_chain = self.command_chain[len(cmd_chain) + 1 :]
return cmd_chain
raise StopIteration
class CommandParser:
CMD_PATTERN = r"^[a-zA-Z]+(\([^(]*\))?"
ID_PATTERN = r"^[a-zA-Z]+(?=\(|\.|$)"
def __init__(self, _command: str) -> None:
self.command = _command
def __iter__(self):
return self
def __next__(self):
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]
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