Source code for pybosl2.turtle._fluent

# Copyright (c) 2026, pinkfish
#
# Licensed under the BSD 2-Clause License. See the LICENSE file in the project
# root for the full license text.
# SPDX-License-Identifier: BSD-2-Clause

"""The turtle command language as methods, so a path reads as a sequence of moves.

``TurtleCommand(TurtleCommandType.MOVE, size=40)`` is an argument bag describing one step; SPEC
P-8 says the object should own the operation instead, and P-1 says the common case should be the
short one. :class:`TurtleCommands` gives both turtles a method per command --
``turtle.move(40).arc_left(radius=8)`` -- built from one table so the 2-D and 3-D turtles cannot
drift apart. The command objects still work, and are still what the methods build underneath.

Commands a 2-D turtle cannot honour (the z-axis moves and rotations) raise the same errors they
always did, from the same place: these methods add no validation of their own.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from pybosl2.exceptions import Bosl2ValueError
from pybosl2.turtle.commands import TurtleCommand, TurtleCommandType

if TYPE_CHECKING:
    from collections.abc import Sequence

    from typing_extensions import Self

#: method name -> (command, the field its first positional argument fills, what it does).
_COMMANDS: dict[str, tuple[TurtleCommandType, str, str]] = {
    "move": (TurtleCommandType.MOVE, "size", "Move forward by *size* (default: the current step length)"),
    "x_move": (TurtleCommandType.XMOVE, "size", "Move along +X by *size*, leaving the heading alone"),
    "y_move": (TurtleCommandType.YMOVE, "size", "Move along +Y by *size*, leaving the heading alone"),
    "z_move": (TurtleCommandType.ZMOVE, "size", "Move along +Z by *size*, leaving the heading alone (3-D only)"),
    "until_x": (TurtleCommandType.UNTILX, "size", "Move along the heading until X reaches *size*"),
    "until_y": (TurtleCommandType.UNTILY, "size", "Move along the heading until Y reaches *size*"),
    "until_z": (TurtleCommandType.UNTILZ, "size", "Move along the heading until Z reaches *size* (3-D only)"),
    "jump": (TurtleCommandType.JUMP, "size", "Jump to an absolute position without drawing"),
    "x_jump": (TurtleCommandType.XJUMP, "size", "Jump to an absolute X, keeping the other coordinates"),
    "y_jump": (TurtleCommandType.YJUMP, "size", "Jump to an absolute Y, keeping the other coordinates"),
    "z_jump": (TurtleCommandType.ZJUMP, "size", "Jump to an absolute Z, keeping the other coordinates (3-D only)"),
    "left": (TurtleCommandType.LEFT, "angle", "Turn left by *angle* degrees (default: the current turn angle)"),
    "right": (TurtleCommandType.RIGHT, "angle", "Turn right by *angle* degrees (default: the current turn angle)"),
    "up": (TurtleCommandType.UP, "angle", "Pitch up by *angle* degrees (3-D only)"),
    "down": (TurtleCommandType.DOWN, "angle", "Pitch down by *angle* degrees (3-D only)"),
    "roll": (TurtleCommandType.ROLL, "angle", "Roll about the heading by *angle* degrees (3-D only)"),
    "set_direction": (TurtleCommandType.SETDIR, "size", "Point the turtle along a direction vector"),
    "set_angle": (TurtleCommandType.ANGLE, "angle", "Set the default turn angle for later turns"),
    "set_length": (TurtleCommandType.LENGTH, "size", "Set the default step length for later moves"),
    "add_length": (TurtleCommandType.ADDLENGTH, "size", "Add *size* to the default step length"),
    "scale_length": (TurtleCommandType.SCALE, "size", "Multiply the default step length by *size*"),
    "set_arc_steps": (TurtleCommandType.ARCSTEPS, "steps", "Set the segment count for later arcs (0 = automatic)"),
    "arc_left": (TurtleCommandType.ARCLEFT, "radius", "Arc left with the given *radius*"),
    "arc_right": (TurtleCommandType.ARCRIGHT, "radius", "Arc right with the given *radius*"),
    "arc_up": (TurtleCommandType.ARCUP, "radius", "Arc upward with the given *radius* (3-D only)"),
    "arc_down": (TurtleCommandType.ARCDOWN, "radius", "Arc downward with the given *radius* (3-D only)"),
    "arc_left_to": (TurtleCommandType.ARCLEFTTO, "radius", "Arc left until the heading reaches an absolute *angle*"),
    "arc_right_to": (
        TurtleCommandType.ARCRIGHTTO,
        "radius",
        "Arc right until the heading reaches an absolute *angle*",
    ),
}


[docs] class TurtleCommands: """A method per turtle command, each running it and returning the turtle (SPEC P-8). Mixed into :class:`~pybosl2.turtle.turtle2d.Turtle2D` and :class:`~pybosl2.turtle.turtle3d.Turtle3D`; every method builds a :class:`~pybosl2.turtle.commands.TurtleCommand` and hands it to that turtle's ``run()``, so the two spellings execute exactly the same code: .. code-block:: python Turtle2D().move(40).arc_left(radius=8) # methods turtle2d([TurtleCommand(TurtleCommandType.MOVE, size=40)]) # command objects Examples: .. pythonscad-example:: from pybosl2.turtle import Turtle2D path = Turtle2D().set_length(40).set_arc_steps(24) for _ in range(4): path.move().arc_left(radius=8) path.points().stroke(width=3, closed=True).linear_extrude(height=4).show() """
[docs] def run(self, commands: Sequence[TurtleCommand], repeat: int = 1) -> Self: """Execute *commands*; provided by the concrete turtle. Args: commands: The commands to run. repeat: How many times to repeat them. """ raise NotImplementedError
[docs] def command(self, command: TurtleCommand) -> Self: """Run one :class:`~pybosl2.turtle.commands.TurtleCommand`. The escape hatch for the commands without a method of their own -- the compound arcs, and anything built programmatically. Args: command: The command to run. Returns: This turtle, so calls chain. """ return self.run([command])
def _make(name: str, cmd_type: TurtleCommandType, field: str, summary: str) -> Any: def method(self: TurtleCommands, value: Any = None, **kwargs: Any) -> Any: if value is not None: if field in kwargs: raise Bosl2ValueError(f"{name}(): give {field} once -- positionally or by name, not both.") kwargs[field] = value return self.run([TurtleCommand(cmd_type, **kwargs)]) method.__name__ = name method.__qualname__ = f"TurtleCommands.{name}" method.__doc__ = f"""{summary}. Runs ``TurtleCommandType.{cmd_type.name}``. Args: value: The command's *{field}*; ``None`` uses the turtle's current default. **kwargs: Any other :class:`~pybosl2.turtle.commands.TurtleCommand` field, e.g. ``angle=`` on an arc. Returns: This turtle, so calls chain. """ return method for _name, (_cmd, _field, _summary) in _COMMANDS.items(): setattr(TurtleCommands, _name, _make(_name, _cmd, _field, _summary)) __all__ = ["TurtleCommands"]