feat: Implement (a common subset) G-code parsing.
It turns out that G-code is a turing complete programming language. That was a surprise! I've implemented enough of the parser to be able to parse the output of Fusion 360 and Cura, both of which I use on a daily basis. It is not a complete implementation.
This commit is contained in:
parent
fa7eb9e961
commit
ad8f6c44ef
95 changed files with 9832 additions and 273 deletions
13
lib/gcode.ex
13
lib/gcode.ex
|
@ -2,7 +2,18 @@ defmodule Gcode do
|
|||
alias Gcode.{Model.Program, Model.Serialise, Result}
|
||||
|
||||
@moduledoc """
|
||||
Documentation for `Gcode`.
|
||||
Gcode - a library for parsing and serialising G-code.
|
||||
|
||||
If you haven't heard of G-code before, then you probably don't need this
|
||||
library, but if you're working with CNC machines or 3D printers then G-code is
|
||||
the defacto standard for working with these machines. As such it behoves us
|
||||
to have first class support for working with G-code in Elixir.
|
||||
|
||||
You're welcome.
|
||||
|
||||
For functions related to parsing G-code files and commands, see the `Parser`
|
||||
module. For generating your own programs see the contents of `Model`, and for
|
||||
converting programs back into G-code see the `Model.Serialise` protocol.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -2,16 +2,21 @@ defmodule Gcode.Model.Block do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct words: [], comment: none()
|
||||
alias Gcode.Model.{Block, Comment, Skip, Word}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
alias Gcode.Model.{Block, Comment, Expr, Skip, Word}
|
||||
|
||||
defguardp is_pushable(value)
|
||||
when is_struct(value, Block) or is_struct(value, Comment) or is_struct(value, Skip) or
|
||||
is_struct(value, Word) or is_expression(value)
|
||||
|
||||
@moduledoc """
|
||||
A sequence of G-code words.
|
||||
A sequence of G-code words on a single line.
|
||||
"""
|
||||
|
||||
@type t :: %Block{words: [block_contents], comment: Option.t(Comment)}
|
||||
@typedoc "Any error results in this module will return this type"
|
||||
@type block_error :: {:block_error, String.t()}
|
||||
@type block_contents :: Word.t() | Skip.t()
|
||||
@type block_contents :: Word.t() | Skip.t() | Expr.t()
|
||||
|
||||
@doc """
|
||||
Initialise a new empty G-code program.
|
||||
|
@ -37,20 +42,10 @@ defmodule Gcode.Model.Block do
|
|||
...> {:ok, block} = Block.comment(block, comment)
|
||||
...> Result.ok?(block.comment)
|
||||
true
|
||||
|
||||
iex> {:ok, comment} = Comment.init("Jen, in the swing seat, with her night terrors")
|
||||
...> {:ok, block} = Block.init()
|
||||
...> {:ok, block} = Block.comment(block, comment)
|
||||
...> Block.comment(block, comment)
|
||||
{:error, {:block_error, "Block already contains a comment"}}
|
||||
|
||||
"""
|
||||
@spec comment(t, Comment.t()) :: Result.t(t, block_error)
|
||||
def comment(%Block{comment: none()} = block, %Comment{} = comment),
|
||||
do: {:ok, %Block{block | comment: some(comment)}}
|
||||
|
||||
def comment(%Block{comment: some(_)}, _comment),
|
||||
do: {:error, {:block_error, "Block already contains a comment"}}
|
||||
def comment(%Block{} = block, comment),
|
||||
do: ok(%Block{block | comment: some(comment)})
|
||||
|
||||
@doc """
|
||||
Pushes a `Word` onto the word list.
|
||||
|
@ -66,19 +61,26 @@ defmodule Gcode.Model.Block do
|
|||
...> {:ok, block} = Block.push(block, word)
|
||||
...> {:ok, word} = Word.init("N", 100)
|
||||
...> Block.push(block, word)
|
||||
{:ok, %Block{words: [%Word{word: "N", address: 100}, %Word{word: "G", address: 0}]}}
|
||||
{:ok, %Block{words: [%Word{word: "N", address: %Integer{i: 100}}, %Word{word: "G", address: %Integer{i: 0}}]}}
|
||||
"""
|
||||
@spec push(t, block_contents) :: Result.t(t, block_error)
|
||||
def push(%Block{words: words} = block, word)
|
||||
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
|
||||
do: {:ok, %Block{block | words: [word | words]}}
|
||||
def push(%Block{words: words} = block, pushable)
|
||||
when is_pushable(pushable) and is_list(words),
|
||||
do: ok(%Block{block | words: [pushable | words]})
|
||||
|
||||
def push(%Block{words: words}, word)
|
||||
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
|
||||
do:
|
||||
{:error,
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
|
||||
def push(%Block{words: words}, pushable) when is_pushable(pushable),
|
||||
do:
|
||||
error(
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
|
||||
)
|
||||
|
||||
def push(%Block{words: words}, pushable) when is_list(words),
|
||||
do:
|
||||
error(
|
||||
{:block_error,
|
||||
"Expected element to be pushable, but it is not. Received #{inspect(pushable)}"}
|
||||
)
|
||||
|
||||
@doc """
|
||||
An accessor which returns the block's words in the correct order.
|
||||
|
@ -89,14 +91,15 @@ defmodule Gcode.Model.Block do
|
|||
...> {:ok, word} = Word.init("N", 100)
|
||||
...> {:ok, block} = Block.push(block, word)
|
||||
...> Block.words(block)
|
||||
{:ok, [%Word{word: "G", address: 0}, %Word{word: "N", address: 100}]}
|
||||
{:ok, [%Word{word: "G", address: %Integer{i: 0}}, %Word{word: "N", address: %Integer{i: 100}}]}
|
||||
"""
|
||||
@spec words(t) :: Result.t([Word.t()], block_error)
|
||||
def words(%Block{words: words}) when is_list(words), do: {:ok, Enum.reverse(words)}
|
||||
def words(%Block{words: words}) when is_list(words), do: ok(Enum.reverse(words))
|
||||
|
||||
def words(%Block{words: words}),
|
||||
do:
|
||||
{:error,
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
|
||||
error(
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -3,6 +3,12 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Block`, meaning that we can convert
|
||||
blocks into human-readable strings.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@spec describe(Block.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(%Block{words: words, comment: some(comment)}, options) do
|
||||
words = describe_words(words, options)
|
||||
|
|
|
@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Block do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Block`, meaning that blocks can be
|
||||
turned into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Block.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Block{words: words, comment: some(comment)}) do
|
||||
words
|
||||
|
|
|
@ -2,6 +2,11 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Comment do
|
|||
alias Gcode.Model.Comment
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Comment`.
|
||||
"""
|
||||
|
||||
@doc "Stubbornly refuse to describe comments"
|
||||
@spec describe(Comment.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(_comment, _opts \\ []), do: none()
|
||||
end
|
||||
|
|
|
@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Comment`, allowing it to be turned
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Comment{comment: comment}) when is_binary(comment) do
|
||||
comment
|
||||
|
|
15
lib/gcode/model/expr.ex
Normal file
15
lib/gcode/model/expr.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defprotocol Gcode.Model.Expr do
|
||||
use Gcode.Result
|
||||
alias Gcode.Model.Expr
|
||||
|
||||
@moduledoc """
|
||||
A protocol for evaluating expressions.
|
||||
"""
|
||||
|
||||
@type scalar :: number | boolean | String.t()
|
||||
@type expr :: scalar | [scalar]
|
||||
@type result :: Result.t(expr)
|
||||
|
||||
@spec evaluate(Expr.t()) :: result
|
||||
def evaluate(_expr)
|
||||
end
|
36
lib/gcode/model/expr/binary.ex
Normal file
36
lib/gcode/model/expr/binary.ex
Normal file
|
@ -0,0 +1,36 @@
|
|||
defmodule Gcode.Model.Expr.Binary do
|
||||
use Gcode.Option
|
||||
defstruct op: none(), lhs: none(), rhs: none()
|
||||
alias Gcode.Model.{Expr, Expr.Binary}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a binary (or infix) expression in G-code, consisting of two
|
||||
operands (`lhs` and `rhs`) and an operator to apply.
|
||||
"""
|
||||
|
||||
@operators ~w[* / + - == != < <= > >= && || ^]a
|
||||
|
||||
@typedoc "Valid infix operators"
|
||||
@type operator :: :* | :/ | :+ | :- | :== | :!= | :< | :<= | :> | :>= | :&& | :|| | :^
|
||||
|
||||
@type t :: %Binary{op: Option.t(operator), lhs: Option.t(Expr.t()), rhs: Option.t(Expr.t())}
|
||||
|
||||
@doc """
|
||||
Wrap an operator and two expressions in a binary expression.
|
||||
"""
|
||||
@spec init(operator, Expr.t(), Expr.t()) :: Result.t(t)
|
||||
def init(operator, lhs, rhs)
|
||||
when operator in @operators and is_expression(lhs) and is_expression(rhs),
|
||||
do: ok(%Binary{op: some(operator), lhs: some(lhs), rhs: some(rhs)})
|
||||
|
||||
def init(operator, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:expression_error,
|
||||
"Expected an operator and two expressions, but received #{
|
||||
inspect(operator: operator, lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
end
|
177
lib/gcode/model/expr/binary/expr.ex
Normal file
177
lib/gcode/model/expr/binary/expr.ex
Normal file
|
@ -0,0 +1,177 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Binary do
|
||||
alias Gcode.Model.{Expr, Expr.Binary}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Binary`, which will try and perform the
|
||||
infix calculation to the best of it's ability.
|
||||
"""
|
||||
|
||||
@spec evaluate(Binary.t()) :: Expr.result()
|
||||
def evaluate(%Binary{op: some(op), lhs: some(lhs), rhs: some(rhs)}) do
|
||||
with ok(lhs) <- Expr.evaluate(lhs),
|
||||
ok(rhs) <- Expr.evaluate(rhs),
|
||||
do: do_evaluate(op, lhs, rhs)
|
||||
end
|
||||
|
||||
def evaluate(_binary), do: error({:program_error, "Unable to evaluate binary expression"})
|
||||
|
||||
defp do_evaluate(:*, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs * rhs)
|
||||
defp do_evaluate(:*, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs * rhs)
|
||||
|
||||
defp do_evaluate(:*, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a multiplication expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:/, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs / rhs)
|
||||
|
||||
defp do_evaluate(:/, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a division expression must be the floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:+, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs + rhs)
|
||||
defp do_evaluate(:+, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs + rhs)
|
||||
|
||||
defp do_evaluate(:+, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an addition expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:-, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs - rhs)
|
||||
defp do_evaluate(:-, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs - rhs)
|
||||
|
||||
defp do_evaluate(:-, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a subtraction expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:==, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs == rhs)
|
||||
defp do_evaluate(:==, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs == rhs)
|
||||
defp do_evaluate(:==, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs == rhs)
|
||||
|
||||
defp do_evaluate(:==, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an equality expression must be the same type, and either integers, floats or strings. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:!=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs != rhs)
|
||||
defp do_evaluate(:!=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs != rhs)
|
||||
defp do_evaluate(:!=, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs != rhs)
|
||||
|
||||
defp do_evaluate(:!=, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an inequality expression must be the same type, and either integers, floats or strings. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:<, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs < rhs)
|
||||
defp do_evaluate(:<, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs < rhs)
|
||||
|
||||
defp do_evaluate(:<, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an LT expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:<=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs <= rhs)
|
||||
defp do_evaluate(:<=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs <= rhs)
|
||||
|
||||
defp do_evaluate(:<=, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an LTE expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:>, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs > rhs)
|
||||
defp do_evaluate(:>, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs > rhs)
|
||||
|
||||
defp do_evaluate(:>, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an GT expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:>=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs >= rhs)
|
||||
defp do_evaluate(:>=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs >= rhs)
|
||||
|
||||
defp do_evaluate(:>=, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an GTE expression must be the same type, and either integers or floats. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:&&, lhs, rhs) when is_boolean(lhs) and is_boolean(rhs), do: ok(lhs && rhs)
|
||||
|
||||
defp do_evaluate(:&&, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a logical and expression must be booleans. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:||, lhs, rhs) when is_boolean(lhs) and is_boolean(rhs), do: ok(lhs || rhs)
|
||||
|
||||
defp do_evaluate(:||, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a logical or expression must be booleans. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:^, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs <> rhs)
|
||||
|
||||
defp do_evaluate(:^, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a string concatenation must be strings. Received #{
|
||||
inspect(lhs: lhs, rhs: rhs)
|
||||
}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(op, lhs, rhs),
|
||||
do:
|
||||
error({:program_error, "Invalid infix expression. #{inspect(op: op, lhs: lhs, rhs: rhs)}"})
|
||||
end
|
18
lib/gcode/model/expr/binary/serialse.ex
Normal file
18
lib/gcode/model/expr/binary/serialse.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Binary do
|
||||
alias Gcode.Model.{Expr.Binary, Serialise}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Binary`, allowing them to be convered
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Binary.t()) :: Serialise.result()
|
||||
def serialise(%Binary{op: some(op), lhs: some(lhs), rhs: some(rhs)}) do
|
||||
with ok(lhs) <- Serialise.serialise(lhs),
|
||||
ok(rhs) <- Serialise.serialise(rhs) do
|
||||
ok([lhs, to_string(op), rhs])
|
||||
end
|
||||
end
|
||||
end
|
21
lib/gcode/model/expr/boolean.ex
Normal file
21
lib/gcode/model/expr/boolean.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Gcode.Model.Expr.Boolean do
|
||||
defstruct b: false
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a boolean expression in G-code. Can be either `true` or `false`.
|
||||
"""
|
||||
|
||||
@type t :: %Boolean{b: boolean}
|
||||
|
||||
@doc """
|
||||
Initialise a `Boolean` from a boolean value.
|
||||
"""
|
||||
@spec init(boolean) :: Result.t(t)
|
||||
def init(value) when is_boolean(value),
|
||||
do: ok(%Boolean{b: value})
|
||||
|
||||
def init(value),
|
||||
do: error({:expression_error, "Expected a boolean value, instead received #{inspect(value)}"})
|
||||
end
|
13
lib/gcode/model/expr/boolean/expr.ex
Normal file
13
lib/gcode/model/expr/boolean/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Boolean do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Boolean`, which will return either true or
|
||||
false.
|
||||
"""
|
||||
|
||||
@spec evaluate(Boolean.t()) :: Expr.result()
|
||||
def evaluate(%Boolean{b: b}), do: ok(b)
|
||||
end
|
14
lib/gcode/model/expr/boolean/serialise.ex
Normal file
14
lib/gcode/model/expr/boolean/serialise.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Boolean do
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Boolean`, allowing them to be convered
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Boolean.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Boolean{b: true}), do: ok(["true"])
|
||||
def serialise(%Boolean{b: false}), do: ok(["false"])
|
||||
def serialise(_), do: error({:serialise_error, "Invalid boolean"})
|
||||
end
|
31
lib/gcode/model/expr/constant.ex
Normal file
31
lib/gcode/model/expr/constant.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Gcode.Model.Expr.Constant do
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct name: none()
|
||||
alias Gcode.Model.Expr.Constant
|
||||
|
||||
@moduledoc """
|
||||
Represents a number of special constant values defined by some G-code
|
||||
controllers:
|
||||
|
||||
* `iterations` - the number of completed iterations of the innermost loop.
|
||||
* `line` - the current line number in the file being executed.
|
||||
* `null` - the null object.
|
||||
* `pi` - the constant π.
|
||||
* `result` - 0 if the last G-, M- or T-command on this input channel was
|
||||
successful, 1 if it returned a warning, 2 if it returned an error.
|
||||
"""
|
||||
|
||||
@type constant :: :iterations | :line | :null | :pi | :result
|
||||
@type t :: %Constant{name: Option.t(constant)}
|
||||
|
||||
@doc """
|
||||
Initialise a `Constant`.
|
||||
"""
|
||||
def init(name) when name in ~w[iterations line null pi result]a,
|
||||
do: ok(%Constant{name: name})
|
||||
|
||||
def init(name),
|
||||
do:
|
||||
error({:expression_error, "Expected a valid constant name, but received #{inspect(name)}"})
|
||||
end
|
25
lib/gcode/model/expr/constant/expr.ex
Normal file
25
lib/gcode/model/expr/constant/expr.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Constant do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Constant
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Constant`, which will return either true
|
||||
or false.
|
||||
|
||||
Currently only knows how to evaluate the following constants:
|
||||
|
||||
* `pi` evaluates to the result of `:math.pi()`
|
||||
* `null` evaulates to `nil`
|
||||
|
||||
Other constants cannot be evaluated at this time, because they need to
|
||||
understand the machine state.
|
||||
"""
|
||||
|
||||
@spec evaluate(Constant.t()) :: Expr.result()
|
||||
def evaluate(%Constant{name: :pi}), do: ok(:math.pi())
|
||||
def evaluate(%Constant{name: :null}), do: ok(nil)
|
||||
|
||||
def evaluate(%Constant{name: name}),
|
||||
do: error({:program_error, "Unable to evaluate constant `#{name}`"})
|
||||
end
|
12
lib/gcode/model/expr/constant/serialise.ex
Normal file
12
lib/gcode/model/expr/constant/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Constant do
|
||||
alias Gcode.Model.Expr.Constant
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Constant`, allowing them to be
|
||||
convered into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Constant.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Constant{name: name}), do: ok([to_string(name)])
|
||||
end
|
21
lib/gcode/model/expr/float.ex
Normal file
21
lib/gcode/model/expr/float.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Gcode.Model.Expr.Float do
|
||||
defstruct f: 0.0
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a floating-point number expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %Float{f: float}
|
||||
|
||||
@doc """
|
||||
Initialise a `Float` from a floating-point value.
|
||||
"""
|
||||
@spec init(float) :: Result.t(t)
|
||||
def init(value) when is_float(value),
|
||||
do: ok(%Float{f: value})
|
||||
|
||||
def init(value),
|
||||
do: error({:expression_error, "Expected a float value, instead received #{inspect(value)}"})
|
||||
end
|
13
lib/gcode/model/expr/float/expr.ex
Normal file
13
lib/gcode/model/expr/float/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Float do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Float`, which will return evaluate to a
|
||||
float.
|
||||
"""
|
||||
|
||||
@spec evaluate(Float.t()) :: Expr.result()
|
||||
def evaluate(%Float{f: f}), do: ok(f)
|
||||
end
|
13
lib/gcode/model/expr/float/serialise.ex
Normal file
13
lib/gcode/model/expr/float/serialise.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Float do
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Float`, allowing them to be
|
||||
convered into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Float.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Float{f: value}) when is_float(value), do: ok([Elixir.Float.to_string(value)])
|
||||
def serialise(_), do: error({:serialise_error, "Invalid float"})
|
||||
end
|
17
lib/gcode/model/expr/helpers.ex
Normal file
17
lib/gcode/model/expr/helpers.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Gcode.Model.Expr.Helpers do
|
||||
alias Gcode.Model.Expr
|
||||
|
||||
@moduledoc """
|
||||
Helpers for working with expressions.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
A guard which ensures that `value` is an expression struct.
|
||||
"""
|
||||
@spec is_expression(any) :: Macro.t()
|
||||
defguard is_expression(value)
|
||||
when is_struct(value, Expr.Binary) or is_struct(value, Expr.Boolean) or
|
||||
is_struct(value, Expr.Constant) or is_struct(value, Expr.Float) or
|
||||
is_struct(value, Expr.Integer) or is_struct(value, Expr.List) or
|
||||
is_struct(value, Expr.String) or is_struct(value, Expr.Unary)
|
||||
end
|
18
lib/gcode/model/expr/integer.ex
Normal file
18
lib/gcode/model/expr/integer.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Gcode.Model.Expr.Integer do
|
||||
defstruct i: 0
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents an integer number expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %Integer{i: integer}
|
||||
|
||||
@spec init(integer) :: Result.t(t)
|
||||
def init(value) when is_integer(value),
|
||||
do: ok(%Integer{i: value})
|
||||
|
||||
def init(value),
|
||||
do: error({:expression_error, "Expected an integer value, but received #{inspect(value)}"})
|
||||
end
|
13
lib/gcode/model/expr/integer/expr.ex
Normal file
13
lib/gcode/model/expr/integer/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Integer do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Integer`, which will return evaluate to a
|
||||
integer.
|
||||
"""
|
||||
|
||||
@spec evaluate(Integer.t()) :: Expr.result()
|
||||
def evaluate(%Integer{i: i}), do: ok(i)
|
||||
end
|
15
lib/gcode/model/expr/integer/serialise.ex
Normal file
15
lib/gcode/model/expr/integer/serialise.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Integer do
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Integer`, allowing them to be
|
||||
convered into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Integer.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Integer{i: value}) when is_integer(value),
|
||||
do: ok([Elixir.Integer.to_string(value)])
|
||||
|
||||
def serialise(_), do: error({:serialise_error, "Invalid integer"})
|
||||
end
|
28
lib/gcode/model/expr/list.ex
Normal file
28
lib/gcode/model/expr/list.ex
Normal file
|
@ -0,0 +1,28 @@
|
|||
defmodule Gcode.Model.Expr.List do
|
||||
defstruct elements: []
|
||||
alias Gcode.Model.{Expr, Expr.List}
|
||||
use Gcode.Result
|
||||
import Gcode.Model.Expr.Helpers
|
||||
|
||||
@moduledoc """
|
||||
Represents an array expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %List{elements: [Expr.t()]}
|
||||
|
||||
@doc """
|
||||
Initialise a `List` from a boolean value.
|
||||
"""
|
||||
@spec init :: Result.t(t)
|
||||
def init, do: ok(%List{})
|
||||
|
||||
@doc """
|
||||
Push an expressions onto the list.
|
||||
"""
|
||||
@spec push(t, Expr.t()) :: Result.t(t)
|
||||
def push(%List{elements: elements}, expr) when is_expression(expr),
|
||||
do: ok(%List{elements: [expr | elements]})
|
||||
|
||||
def push(%List{}, expr),
|
||||
do: error({:expression_error, "Expected expression, but received #{inspect(expr)}"})
|
||||
end
|
18
lib/gcode/model/expr/list/expr.ex
Normal file
18
lib/gcode/model/expr/list/expr.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.List do
|
||||
alias Gcode.Model.{Expr, Expr.List}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `List`, which will return evaluate to a
|
||||
list.
|
||||
"""
|
||||
|
||||
@spec evaluate(List.t()) :: Expr.result()
|
||||
def evaluate(%List{elements: elements}) do
|
||||
elements =
|
||||
elements
|
||||
|> Enum.map(&Expr.evaluate/1)
|
||||
|
||||
ok(elements)
|
||||
end
|
||||
end
|
12
lib/gcode/model/expr/list/serialise.ex
Normal file
12
lib/gcode/model/expr/list/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.List do
|
||||
alias Gcode.Model.{Expr.List, Serialise}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `List`. Unfortunately it's impossible
|
||||
to serialise a list into G-code.
|
||||
"""
|
||||
|
||||
@spec serialise(List.t()) :: Serialise.result()
|
||||
def serialise(%List{}), do: error({:serialise_error, "Cannot serialise a list"})
|
||||
end
|
40
lib/gcode/model/expr/string.ex
Normal file
40
lib/gcode/model/expr/string.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
defmodule Gcode.Model.Expr.String do
|
||||
alias Gcode.Model.Expr.String
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct value: Option.none()
|
||||
|
||||
@moduledoc """
|
||||
Represents a string expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %String{
|
||||
value: Option.t(String.t())
|
||||
}
|
||||
|
||||
@doc """
|
||||
Initialise a comment.
|
||||
|
||||
## Example
|
||||
|
||||
iex> "Doc, in the carpark, with plutonium"
|
||||
...> |> String.init()
|
||||
{:ok, %String{value: "Doc, in the carpark, with plutonium"}}
|
||||
"""
|
||||
@spec init(Elixir.String.t()) :: Result.t(t)
|
||||
def init(comment) when is_binary(comment) do
|
||||
if Elixir.String.printable?(comment) do
|
||||
ok(%String{value: comment})
|
||||
else
|
||||
error(
|
||||
{:string_error, "String should be a valid UTF-8 string, received #{inspect(comment)}"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def init(comment),
|
||||
do:
|
||||
error(
|
||||
{:string_error, "String should be a valid UTF-8 string, received #{inspect(comment)}"}
|
||||
)
|
||||
end
|
13
lib/gcode/model/expr/string/expr.ex
Normal file
13
lib/gcode/model/expr/string/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.String do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.String
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `String`, which will return evaluate to an
|
||||
Erlang binary.
|
||||
"""
|
||||
|
||||
@spec evaluate(String.t()) :: Expr.result()
|
||||
def evaluate(%String{value: value}), do: ok(value)
|
||||
end
|
12
lib/gcode/model/expr/string/serialise.ex
Normal file
12
lib/gcode/model/expr/string/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.String do
|
||||
alias Gcode.Model.Expr.String
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `String`, allowing them to be converted
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(String.t()) :: Result.t([Elixir.String.t()], {:serialise_error, any})
|
||||
def serialise(%String{value: value}), do: ok([inspect(value)])
|
||||
end
|
41
lib/gcode/model/expr/unary.ex
Normal file
41
lib/gcode/model/expr/unary.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
defmodule Gcode.Model.Expr.Unary do
|
||||
use Gcode.Option
|
||||
defstruct op: none(), expr: none()
|
||||
alias Gcode.Model.{Expr, Expr.Unary}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a unary (or prefix) expression in G-code. A unary consists of a
|
||||
single operand and an operator.
|
||||
"""
|
||||
|
||||
@operators ~w[! + - #]a
|
||||
|
||||
@typedoc "Valid unary operators"
|
||||
@type operator :: :! | :+ | :- | :"#"
|
||||
|
||||
@type t :: %Unary{op: Option.t(atom), expr: Option.t(Expr.t())}
|
||||
|
||||
@doc """
|
||||
Wrap an inner expression and operator in a unary expression.
|
||||
"""
|
||||
@spec init(operator, Expr.t()) :: Result.t(t)
|
||||
def init(operator, expr) when operator in @operators and is_expression(expr),
|
||||
do: ok(%Unary{op: some(operator), expr: some(expr)})
|
||||
|
||||
def init(operator, expr) when operator in @operators,
|
||||
do: error({:expression_error, "Expected expression, but received #{inspect(expr)}"})
|
||||
|
||||
def init(operator, expr) when is_expression(expr),
|
||||
do: error({:expression_error, "Expected unary operator, but received #{inspect(operator)}"})
|
||||
|
||||
def init(operator, expr),
|
||||
do:
|
||||
error(
|
||||
{:expression_error,
|
||||
"Expected unary operator and expression, but received #{
|
||||
inspect(operator: operator, expression: expr)
|
||||
}"}
|
||||
)
|
||||
end
|
78
lib/gcode/model/expr/unary/expr.ex
Normal file
78
lib/gcode/model/expr/unary/expr.ex
Normal file
|
@ -0,0 +1,78 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Unary do
|
||||
alias Gcode.Model.{Expr, Expr.Unary}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Unary`, which will attempt to apply the
|
||||
operator to the operand.
|
||||
"""
|
||||
|
||||
@spec evaluate(Unary.t()) :: Expr.result()
|
||||
def evaluate(%Unary{op: some(:!), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_boolean(result) ->
|
||||
ok(!result)
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaulate to boolean, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(result) ->
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%Unary{op: some(:-), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_number(result) ->
|
||||
ok(0 - result)
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaluate to a number, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(reason) ->
|
||||
error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%Unary{op: some(:+), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_number(result) ->
|
||||
ok(0 + result)
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaluate to a number, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(reason) ->
|
||||
error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%Unary{op: some(:"#"), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_list(result) ->
|
||||
ok(length(result))
|
||||
|
||||
ok(result) when is_binary(result) ->
|
||||
ok(String.length(result))
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaluate to an array or string, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(reason) ->
|
||||
error(reason)
|
||||
end
|
||||
end
|
||||
end
|
21
lib/gcode/model/expr/unary/serialise.ex
Normal file
21
lib/gcode/model/expr/unary/serialise.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Unary do
|
||||
alias Gcode.Model.{Expr.Unary, Serialise}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Unary`, allowing them to be converted
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Unary.t()) :: Serialise.result()
|
||||
def serialise(%Unary{op: some(op), expr: some(inner)}) do
|
||||
case Serialise.serialise(inner) do
|
||||
ok(inner) -> ok([to_string(op) | inner])
|
||||
error(reason) -> error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def serialise(unary),
|
||||
do: error({:serialise_error, "Invalid unary: #{inspect(unary)}"})
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.Program do
|
||||
defstruct elements: []
|
||||
alias Gcode.Model.{Block, Comment, Program, Skip, Tape}
|
||||
alias Gcode.Model.{Block, Comment, Program, Tape}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
|
@ -20,7 +20,7 @@ defmodule Gcode.Model.Program do
|
|||
elements: [element]
|
||||
}
|
||||
|
||||
@type element :: Block.t() | Comment.t() | Skip.t() | Tape.t()
|
||||
@type element :: Block.t() | Comment.t() | Tape.t()
|
||||
@type error :: {:program_error, String.t()}
|
||||
|
||||
@doc """
|
||||
|
@ -43,8 +43,7 @@ defmodule Gcode.Model.Program do
|
|||
@spec push(t, element) :: Result.t(t, error)
|
||||
def push(%Program{elements: elements} = program, element)
|
||||
when is_list(elements) and
|
||||
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Skip) or
|
||||
is_struct(element, Tape)),
|
||||
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Tape)),
|
||||
do: ok(%Program{program | elements: [element | elements]})
|
||||
|
||||
def push(%Program{elements: elements}, _element) when not is_list(elements),
|
||||
|
|
|
@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Program do
|
|||
alias Gcode.Model.{Describe, Program}
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Program`.
|
||||
"""
|
||||
|
||||
@spec describe(Program.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(%Program{elements: elements}, options) do
|
||||
lines =
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
defimpl Enumerable, for: Gcode.Model.Program do
|
||||
alias Gcode.Model.Program
|
||||
@moduledoc false
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Enumerable` protocol for `Program`.
|
||||
"""
|
||||
|
||||
@spec count(Program.t()) :: {:ok, non_neg_integer()} | {:error, module}
|
||||
def count(%Program{elements: elements}), do: {:ok, Enum.count(elements)}
|
||||
|
|
|
@ -2,6 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Program do
|
|||
alias Gcode.{Model.Program, Model.Serialise}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Program`, allowing it to be turned
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Program.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Program{elements: elements}) do
|
||||
with elements <- Enum.reverse(elements),
|
||||
|
|
|
@ -3,9 +3,11 @@ defprotocol Gcode.Model.Serialise do
|
|||
alias Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
A protocol which is used to serialise the model into a string.
|
||||
A protocol which is used to serialise the model into G-code output.
|
||||
"""
|
||||
|
||||
@type result :: Result.t([String.t()], {:serialise_error, String.t()})
|
||||
|
||||
@spec serialise(Serialise.t()) :: Result.t([String.t()])
|
||||
def serialise(serialisable)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Skip do
|
|||
alias Gcode.Model.Skip
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Skip`.
|
||||
"""
|
||||
|
||||
@spec describe(Skip.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(_skip, _opts \\ []), do: none()
|
||||
end
|
||||
|
|
|
@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Skip do
|
|||
alias Gcode.{Model.Skip, Result}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Skip`, allowing it to be turned into
|
||||
G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Skip.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Skip{number: none()}), do: ok(["/"])
|
||||
|
|
|
@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Tape do
|
|||
alias Gcode.Model.Tape
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Tape`.
|
||||
"""
|
||||
|
||||
@spec describe(Tape.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(_tape, _opts \\ []), do: none()
|
||||
end
|
||||
|
|
|
@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Tape do
|
|||
alias Gcode.{Model.Tape, Result}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Tape`, allowing it to be turned into
|
||||
G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Tape.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Tape{leader: none()}), do: ok(["%"])
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Gcode.Model.Word do
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Expr, Word}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
|
@ -11,7 +12,7 @@ defmodule Gcode.Model.Word do
|
|||
|
||||
@type t :: %Word{
|
||||
word: String.t(),
|
||||
address: number
|
||||
address: Expr.t()
|
||||
}
|
||||
|
||||
@doc """
|
||||
|
@ -20,10 +21,10 @@ defmodule Gcode.Model.Word do
|
|||
## Example
|
||||
|
||||
iex> Word.init("G", 0)
|
||||
{:ok, %Word{word: "G", address: 0}}
|
||||
{:ok, %Word{word: "G", address: %Integer{i: 0}}}
|
||||
"""
|
||||
@spec init(String.t(), number) :: Result.t(t)
|
||||
def init(word, address) when is_binary(word) and is_number(address) do
|
||||
@spec init(String.t(), number | Expr.t()) :: Result.t(t)
|
||||
def init(word, address) when is_binary(word) and is_expression(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(%Word{word: word, address: address})
|
||||
else
|
||||
|
@ -31,9 +32,31 @@ defmodule Gcode.Model.Word do
|
|||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_number(address),
|
||||
def init(word, address) when is_binary(word) and is_integer(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(address) = Expr.Integer.init(address)
|
||||
ok(%Word{word: word, address: address})
|
||||
else
|
||||
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
|
||||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_binary(word) and is_float(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(address) = Expr.Float.init(address)
|
||||
ok(%Word{word: word, address: address})
|
||||
else
|
||||
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
|
||||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_expression(address),
|
||||
do: error({:word_error, "Expected word to be a string, received #{inspect(word)}"})
|
||||
|
||||
def init(_word, address),
|
||||
do: error({:word_error, "Expected address to be a number, received #{inspect(address)}"})
|
||||
do:
|
||||
error(
|
||||
{:word_error,
|
||||
"Expected address to be an expression or a number, received #{inspect(address)}"}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Expr, Word}
|
||||
|
||||
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
|
||||
|
||||
@moduledoc """
|
||||
Describes common/conventional words.
|
||||
|
@ -49,283 +52,560 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
|
|||
do: do_describe(word, Enum.into(options, %{}))
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
|
||||
when axis in ~w[A B C] and angle >= 0,
|
||||
do: some("Rotate #{axis} axis counterclockwise to #{angle}º")
|
||||
when axis in ~w[A B C] do
|
||||
case Expr.evaluate(angle) do
|
||||
ok(angle) when is_number(angle) and angle >= 0 ->
|
||||
some("Rotate #{axis} axis counterclockwise to #{angle}º")
|
||||
|
||||
ok(angle) when is_number(angle) and angle < 0 ->
|
||||
some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
|
||||
|
||||
error(_) ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
|
||||
when axis in ~w[A B C] and angle >= 0,
|
||||
do: some("Rotate #{axis} axis counterclockwise by #{angle}º")
|
||||
when axis in ~w[A B C] do
|
||||
case Expr.evaluate(angle) do
|
||||
ok(angle) when is_number(angle) and angle >= 0 ->
|
||||
some("Rotate #{axis} axis counterclockwise by #{angle}º")
|
||||
|
||||
ok(angle) when is_number(angle) and angle < 0 ->
|
||||
some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
|
||||
|
||||
error(_) ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, _)
|
||||
when axis in ~w[A B C] and angle >= 0,
|
||||
do: some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
|
||||
when axis in ~w[A B C] and angle < 0,
|
||||
do: some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
|
||||
when axis in ~w[A B C] do
|
||||
case Expr.evaluate(angle) do
|
||||
ok(angle) when is_number(angle) and angle >= 0 ->
|
||||
some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
|
||||
|
||||
ok(angle) when is_number(angle) and angle < 0 ->
|
||||
some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
|
||||
|
||||
error(_) ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options) do
|
||||
case distance_with_unit(depth, options) do
|
||||
some(depth) ->
|
||||
some("Depth of cut #{depth}")
|
||||
|
||||
_ ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}) do
|
||||
case Expr.evaluate(aperture) do
|
||||
some(aperture) when is_number(aperture) -> some("Aperture #{aperture}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
some(offset) -> some("Left radial offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
some(offset) -> some("Right radial offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
some(offset) -> some("Radial offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
|
||||
when axis in ~w[A B C] and angle < 0,
|
||||
do: some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options) do
|
||||
case feedrate(feedrate, options) do
|
||||
some(feedrate) -> some("Extruder feedrate #{feedrate}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, _)
|
||||
when axis in ~w[A B C] and angle < 0,
|
||||
do: some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options) do
|
||||
case feedrate(feedrate, options) do
|
||||
some(feedrate) -> some("Precision feedrate #{feedrate}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options),
|
||||
do: some("Depth of cut #{distance_with_unit(depth, options)}")
|
||||
defp do_describe(%Word{word: "F", address: feedrate}, options) do
|
||||
case feedrate(feedrate, options) do
|
||||
some(feedrate) -> some("Feedrate #{feedrate}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}),
|
||||
do: some("Aperture #{aperture}")
|
||||
defp do_describe(%Word{word: "G", address: address}, options) do
|
||||
case {Expr.evaluate(address), options} do
|
||||
{ok(0), _} ->
|
||||
some("Rapid move")
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options),
|
||||
do: some("Left radial offset #{distance_with_unit(offset, options)}")
|
||||
{ok(1), _} ->
|
||||
some("Linear move")
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options),
|
||||
do: some("Right radial offset #{distance_with_unit(offset, options)}")
|
||||
{ok(2), _} ->
|
||||
some("Clockwise circular move")
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, options),
|
||||
do: some("Radial offset #{distance_with_unit(offset, options)}")
|
||||
{ok(3), _} ->
|
||||
some("Counterclockwise circular move")
|
||||
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options),
|
||||
do: some("Extruder feedrate #{feedrate(feedrate, options)}")
|
||||
{ok(4), _} ->
|
||||
some("Dwell")
|
||||
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options),
|
||||
do: some("Precision feedrate #{feedrate(feedrate, options)}")
|
||||
{ok(5), _} ->
|
||||
some("High-precision contour control")
|
||||
|
||||
defp do_describe(%Word{word: "F", address: feedrate}, options),
|
||||
do: some("Feedrate #{feedrate(feedrate, options)}")
|
||||
{ok(5.1), _} ->
|
||||
some("AI advanced preview control")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 0}, _), do: some("Rapid move")
|
||||
defp do_describe(%Word{word: "G", address: 1}, _), do: some("Linear move")
|
||||
defp do_describe(%Word{word: "G", address: 2}, _), do: some("Clockwise circular move")
|
||||
defp do_describe(%Word{word: "G", address: 3}, _), do: some("Counterclockwise circular move")
|
||||
defp do_describe(%Word{word: "G", address: 4}, _), do: some("Dwell")
|
||||
defp do_describe(%Word{word: "G", address: 5}, _), do: some("High-precision contour control")
|
||||
defp do_describe(%Word{word: "G", address: 5.1}, _), do: some("AI advanced preview control")
|
||||
defp do_describe(%Word{word: "G", address: 6.1}, _), do: some("NURBS machining")
|
||||
defp do_describe(%Word{word: "G", address: 7}, _), do: some("Imaginary axis designation")
|
||||
defp do_describe(%Word{word: "G", address: 9}, _), do: some("Exact stop check - non-modal")
|
||||
defp do_describe(%Word{word: "G", address: 10}, _), do: some("Programmable data input")
|
||||
defp do_describe(%Word{word: "G", address: 11}, _), do: some("Data write cancel")
|
||||
defp do_describe(%Word{word: "G", address: 17}, _), do: some("XY plane selection")
|
||||
defp do_describe(%Word{word: "G", address: 18}, _), do: some("ZX plane selection")
|
||||
defp do_describe(%Word{word: "G", address: 19}, _), do: some("YZ plane selection")
|
||||
defp do_describe(%Word{word: "G", address: 20}, _), do: some("Unit is inches")
|
||||
defp do_describe(%Word{word: "G", address: 21}, _), do: some("Unit is mm")
|
||||
defp do_describe(%Word{word: "G", address: 28}, _), do: some("Return to home position")
|
||||
{ok(6.1), _} ->
|
||||
some("NURBS machining")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 30}, _),
|
||||
do: some("Return to secondary home position")
|
||||
{ok(7), _} ->
|
||||
some("Imaginary axis designation")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 31}, _), do: some("Feed until skip function")
|
||||
defp do_describe(%Word{word: "G", address: 32}, _), do: some("Single-point threading")
|
||||
defp do_describe(%Word{word: "G", address: 33}, _), do: some("Variable pitch threading")
|
||||
defp do_describe(%Word{word: "G", address: 40}, _), do: some("Tool radius compensation off")
|
||||
defp do_describe(%Word{word: "G", address: 41}, _), do: some("Tool radius compensation left")
|
||||
defp do_describe(%Word{word: "G", address: 42}, _), do: some("Tool radius compensation right")
|
||||
{ok(9), _} ->
|
||||
some("Exact stop check - non-modal")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 43}, _),
|
||||
do: some("Tool height offset compensation negative")
|
||||
{ok(10), _} ->
|
||||
some("Programmable data input")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 44}, _),
|
||||
do: some("Tool height offset compensation positive")
|
||||
{ok(11), _} ->
|
||||
some("Data write cancel")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 45}, _), do: some("Axis offset single increase")
|
||||
defp do_describe(%Word{word: "G", address: 46}, _), do: some("Axis offset single decrease")
|
||||
defp do_describe(%Word{word: "G", address: 47}, _), do: some("Axis offset double increase")
|
||||
defp do_describe(%Word{word: "G", address: 48}, _), do: some("Axis offset double decrease")
|
||||
{ok(17), _} ->
|
||||
some("XY plane selection")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 49}, _),
|
||||
do: some("Tool length offset compensation cancel")
|
||||
{ok(18), _} ->
|
||||
some("ZX plane selection")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 50}, %{operation: :turning}),
|
||||
do: some("Position register")
|
||||
{ok(19), _} ->
|
||||
some("YZ plane selection")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 50}, _), do: some("Scaling function cancel")
|
||||
defp do_describe(%Word{word: "G", address: 52}, _), do: some("Local coordinate system")
|
||||
defp do_describe(%Word{word: "G", address: 53}, _), do: some("Machine coordinate system")
|
||||
{ok(20), _} ->
|
||||
some("Unit is inches")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: address}, _)
|
||||
when address in [54, 55, 56, 57, 58, 59, 54.1],
|
||||
do: some("Work coordinate system")
|
||||
{ok(21), _} ->
|
||||
some("Unit is mm")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 61}, _), do: some("Exact stop check - modal")
|
||||
defp do_describe(%Word{word: "G", address: 62}, _), do: some("Automatic corner override")
|
||||
defp do_describe(%Word{word: "G", address: 64}, _), do: some("Default cutting mode")
|
||||
defp do_describe(%Word{word: "G", address: 68}, _), do: some("Rotate coordinate system")
|
||||
{ok(28), _} ->
|
||||
some("Return to home position")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 69}, _),
|
||||
do: some("Turn off coordinate system rotation")
|
||||
{ok(30), _} ->
|
||||
some("Return to secondary home position")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 70}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for finishing")
|
||||
{ok(31), _} ->
|
||||
some("Feed until skip function")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 71}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
|
||||
{ok(32), _} ->
|
||||
some("Single-point threading")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 72}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
|
||||
{ok(33), _} ->
|
||||
some("Variable pitch threading")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 73}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
|
||||
{ok(40), _} ->
|
||||
some("Tool radius compensation off")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 73}, _), do: some("Peck drilling cycle")
|
||||
{ok(41), _} ->
|
||||
some("Tool radius compensation left")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 74}, %{operation: :turning}),
|
||||
do: some("Peck drilling cycle")
|
||||
{ok(42), _} ->
|
||||
some("Tool radius compensation right")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 74}, _), do: some("Tapping cycle")
|
||||
{ok(43), _} ->
|
||||
some("Tool height offset compensation negative")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 75}, %{operation: :turning}),
|
||||
do: some("Peck grooving cycle")
|
||||
{ok(44), _} ->
|
||||
some("Tool height offset compensation positive")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 76}, %{operation: :turning}),
|
||||
do: some("Threading cycle")
|
||||
{ok(45), _} ->
|
||||
some("Axis offset single increase")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 76}, _), do: some("Fine boring cycle")
|
||||
defp do_describe(%Word{word: "G", address: 80}, _), do: some("Cancel cycle")
|
||||
defp do_describe(%Word{word: "G", address: 81}, _), do: some("Simple drilling cycle")
|
||||
defp do_describe(%Word{word: "G", address: 82}, _), do: some("Drilling cycle with dwell")
|
||||
defp do_describe(%Word{word: "G", address: 83}, _), do: some("Peck drilling cycle")
|
||||
{ok(46), _} ->
|
||||
some("Axis offset single decrease")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 84}, _),
|
||||
do: some("Tapping cycle, righthand thread, M03 spindle direction")
|
||||
{ok(47), _} ->
|
||||
some("Axis offset double increase")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 84.2}, _),
|
||||
do: some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
|
||||
{ok(48), _} ->
|
||||
some("Axis offset double decrease")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 84.3}, _),
|
||||
do: some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
|
||||
{ok(49), _} ->
|
||||
some("Tool length offset compensation cancel")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 85}, _), do: some("Boring cycle, feed in/feed out")
|
||||
{ok(50), %{operation: :turning}} ->
|
||||
some("Position register")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 86}, _),
|
||||
do: some("Boring cycle, feed in/spindle stop/rapid out")
|
||||
{ok(50), _} ->
|
||||
some("Scaling function cancel")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 87}, _), do: some("Boring cycle, backboring")
|
||||
{ok(52), _} ->
|
||||
some("Local coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 88}, _),
|
||||
do: some("Boring cycle, feed in/spindle stop/manual operation")
|
||||
{ok(53), _} ->
|
||||
some("Machine coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 89}, _),
|
||||
do: some("Boring cycle, feed in/dwell/feed out")
|
||||
{ok(address), _} when address in [54, 55, 56, 57, 58, 59, 54.1] ->
|
||||
some("Work coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 90}, _), do: some("Absolute positioning")
|
||||
defp do_describe(%Word{word: "G", address: 91}, _), do: some("Relative positioning")
|
||||
defp do_describe(%Word{word: "G", address: 92}, _), do: some("Position register")
|
||||
defp do_describe(%Word{word: "G", address: 94}, _), do: some("Feedrate per minute")
|
||||
defp do_describe(%Word{word: "G", address: 95}, _), do: some("Feedrate per revolution")
|
||||
defp do_describe(%Word{word: "G", address: 96}, _), do: some("Constant surface speed")
|
||||
defp do_describe(%Word{word: "G", address: 97}, _), do: some("Constant spindle speed")
|
||||
{ok(61), _} ->
|
||||
some("Exact stop check - modal")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 98}, %{operation: :turning}),
|
||||
do: some("Feedrate per minute")
|
||||
{ok(62), _} ->
|
||||
some("Automatic corner override")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 98}, _),
|
||||
do: some("Return to initial Z level in canned cycle")
|
||||
{ok(64), _} ->
|
||||
some("Default cutting mode")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 99}, %{operation: :turning}),
|
||||
do: some("Feedrate per revolution")
|
||||
{ok(68), _} ->
|
||||
some("Rotate coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 99}, _),
|
||||
do: some("Return to R level in canned cycle")
|
||||
{ok(69), _} ->
|
||||
some("Turn off coordinate system rotation")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 100}, _), do: some("Tool length measurement")
|
||||
{ok(70), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for finishing")
|
||||
|
||||
defp do_describe(%Word{word: "H", address: length}, options),
|
||||
do: some("Tool length offset #{distance_with_unit(length, options)}")
|
||||
{ok(71), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
|
||||
|
||||
defp do_describe(%Word{word: "I", address: offset}, options),
|
||||
do: some("X arc center offset #{distance_with_unit(offset, options)}")
|
||||
{ok(72), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
|
||||
|
||||
defp do_describe(%Word{word: "J", address: offset}, options),
|
||||
do: some("Y arc center offset #{distance_with_unit(offset, options)}")
|
||||
{ok(73), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
|
||||
|
||||
defp do_describe(%Word{word: "K", address: offset}, options),
|
||||
do: some("Z arc center offset #{distance_with_unit(offset, options)}")
|
||||
{ok(73), _} ->
|
||||
some("Peck drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "L", address: count}, _), do: some("Loop count #{count}")
|
||||
{ok(74), %{operation: :turning}} ->
|
||||
some("Peck drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 0}, _), do: some("Compulsory stop")
|
||||
defp do_describe(%Word{word: "M", address: 1}, _), do: some("Optional stop")
|
||||
defp do_describe(%Word{word: "M", address: 2}, _), do: some("End of program")
|
||||
defp do_describe(%Word{word: "M", address: 3}, _), do: some("Spindle on clockwise")
|
||||
defp do_describe(%Word{word: "M", address: 4}, _), do: some("Spindle on counterclockwise")
|
||||
defp do_describe(%Word{word: "M", address: 5}, _), do: some("Spindle stop")
|
||||
defp do_describe(%Word{word: "M", address: 6}, _), do: some("Automatic tool change")
|
||||
defp do_describe(%Word{word: "M", address: 7}, _), do: some("Coolant mist")
|
||||
defp do_describe(%Word{word: "M", address: 8}, _), do: some("Coolant flood")
|
||||
defp do_describe(%Word{word: "M", address: 9}, _), do: some("Coolant off")
|
||||
defp do_describe(%Word{word: "M", address: 10}, _), do: some("Pallet clamp on")
|
||||
defp do_describe(%Word{word: "M", address: 11}, _), do: some("Pallet clamp off")
|
||||
{ok(74), _} ->
|
||||
some("Tapping cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 13}, _),
|
||||
do: some("Spindle on clockwise and coolant flood")
|
||||
{ok(75), %{operation: :turning}} ->
|
||||
some("Peck grooving cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 19}, _), do: some("Spindle orientation")
|
||||
{ok(76), %{operation: :turning}} ->
|
||||
some("Threading cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 21}, %{operation: :turning}),
|
||||
do: some("Tailstock forward")
|
||||
{ok(76), _} ->
|
||||
some("Fine boring cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 21}, _), do: some("Mirror X axis")
|
||||
{ok(80), _} ->
|
||||
some("Cancel cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 22}, %{operation: :turning}),
|
||||
do: some("Tailstock backward")
|
||||
{ok(81), _} ->
|
||||
some("Simple drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 22}, _), do: some("Mirror Y axis")
|
||||
{ok(82), _} ->
|
||||
some("Drilling cycle with dwell")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 23}, %{operation: :turning}),
|
||||
do: some("Thread gradual pullout on")
|
||||
{ok(83), _} ->
|
||||
some("Peck drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 23}, _), do: some("Mirror off")
|
||||
{ok(84), _} ->
|
||||
some("Tapping cycle, righthand thread, M03 spindle direction")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 24}, %{operation: :turning}),
|
||||
do: some("Thread gradual pullout off")
|
||||
{ok(84.2), _} ->
|
||||
some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 30}, _), do: some("End of program")
|
||||
{ok(84.3), _} ->
|
||||
some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: gear}, %{operation: :turning})
|
||||
when is_integer(gear) and gear > 40 and gear < 45,
|
||||
do: some("Gear select #{gear - 40}")
|
||||
{ok(85), _} ->
|
||||
some("Boring cycle, feed in/feed out")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 48}, _), do: some("Feedrate override allowed")
|
||||
defp do_describe(%Word{word: "M", address: 49}, _), do: some("Feedrate override not allowed")
|
||||
defp do_describe(%Word{word: "M", address: 52}, _), do: some("Unload tool")
|
||||
defp do_describe(%Word{word: "M", address: 60}, _), do: some("Automatic pallet change")
|
||||
defp do_describe(%Word{word: "M", address: 98}, _), do: some("Subprogram call")
|
||||
defp do_describe(%Word{word: "M", address: 99}, _), do: some("Subprogram end")
|
||||
defp do_describe(%Word{word: "M", address: 100}, _), do: some("Clean nozzle")
|
||||
defp do_describe(%Word{word: "N", address: line}, _), do: some("Line #{line}")
|
||||
defp do_describe(%Word{word: "O", address: name}, _), do: some("Program #{name}")
|
||||
defp do_describe(%Word{word: "P", address: param}, _), do: some("Parameter #{param}")
|
||||
{ok(86), _} ->
|
||||
some("Boring cycle, feed in/spindle stop/rapid out")
|
||||
|
||||
defp do_describe(%Word{word: "Q", address: distance}, options),
|
||||
do: some("Peck increment #{distance_with_unit(distance, options)}")
|
||||
{ok(87), _} ->
|
||||
some("Boring cycle, backboring")
|
||||
|
||||
defp do_describe(%Word{word: "R", address: distance}, options),
|
||||
do: some("Radius #{distance_with_unit(distance, options)}")
|
||||
{ok(88), _} ->
|
||||
some("Boring cycle, feed in/spindle stop/manual operation")
|
||||
|
||||
defp do_describe(%Word{word: "S", address: speed}, _), do: some("Speed #{speed}")
|
||||
defp do_describe(%Word{word: "T", address: tool}, _), do: some("Tool #{tool}")
|
||||
{ok(89), _} ->
|
||||
some("Boring cycle, feed in/dwell/feed out")
|
||||
|
||||
defp do_describe(%Word{word: "X", address: position}, options),
|
||||
do: some("X #{distance_with_unit(position, options)}")
|
||||
{ok(90), _} ->
|
||||
some("Absolute positioning")
|
||||
|
||||
defp do_describe(%Word{word: "Y", address: position}, options),
|
||||
do: some("Y #{distance_with_unit(position, options)}")
|
||||
{ok(91), _} ->
|
||||
some("Relative positioning")
|
||||
|
||||
defp do_describe(%Word{word: "Z", address: position}, options),
|
||||
do: some("Z #{distance_with_unit(position, options)}")
|
||||
{ok(92), _} ->
|
||||
some("Position register")
|
||||
|
||||
{ok(94), _} ->
|
||||
some("Feedrate per minute")
|
||||
|
||||
{ok(95), _} ->
|
||||
some("Feedrate per revolution")
|
||||
|
||||
{ok(96), _} ->
|
||||
some("Constant surface speed")
|
||||
|
||||
{ok(97), _} ->
|
||||
some("Constant spindle speed")
|
||||
|
||||
{ok(98), %{operation: :turning}} ->
|
||||
some("Feedrate per minute")
|
||||
|
||||
{ok(98), _} ->
|
||||
some("Return to initial Z level in canned cycle")
|
||||
|
||||
{ok(99), %{operation: :turning}} ->
|
||||
some("Feedrate per revolution")
|
||||
|
||||
{ok(99), _} ->
|
||||
some("Return to R level in canned cycle")
|
||||
|
||||
{ok(100), _} ->
|
||||
some("Tool length measurement")
|
||||
|
||||
_ ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "H", address: length}, options) do
|
||||
case distance_with_unit(length, options) do
|
||||
ok(length) -> some("Tool length offset #{length}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "I", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
ok(offset) -> some("X arc center offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "J", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
ok(offset) -> some("Y arc center offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "K", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
ok(offset) -> some("Z arc center offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "L", address: count}, _) do
|
||||
case Expr.evaluate(count) do
|
||||
ok(count) -> some("Loop count #{count}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "M", address: address}, options) do
|
||||
case {Expr.evaluate(address), options} do
|
||||
{ok(0), _} ->
|
||||
some("Compulsory stop")
|
||||
|
||||
{ok(1), _} ->
|
||||
some("Optional stop")
|
||||
|
||||
{ok(2), _} ->
|
||||
some("End of program")
|
||||
|
||||
{ok(3), _} ->
|
||||
some("Spindle on clockwise")
|
||||
|
||||
{ok(4), _} ->
|
||||
some("Spindle on counterclockwise")
|
||||
|
||||
{ok(5), _} ->
|
||||
some("Spindle stop")
|
||||
|
||||
{ok(6), _} ->
|
||||
some("Automatic tool change")
|
||||
|
||||
{ok(7), _} ->
|
||||
some("Coolant mist")
|
||||
|
||||
{ok(8), _} ->
|
||||
some("Coolant flood")
|
||||
|
||||
{ok(9), _} ->
|
||||
some("Coolant off")
|
||||
|
||||
{ok(10), _} ->
|
||||
some("Pallet clamp on")
|
||||
|
||||
{ok(11), _} ->
|
||||
some("Pallet clamp off")
|
||||
|
||||
{ok(13), _} ->
|
||||
some("Spindle on clockwise and coolant flood")
|
||||
|
||||
{ok(19), _} ->
|
||||
some("Spindle orientation")
|
||||
|
||||
{ok(21), %{operation: :turning}} ->
|
||||
some("Tailstock forward")
|
||||
|
||||
{ok(21), _} ->
|
||||
some("Mirror X axis")
|
||||
|
||||
{ok(22), %{operation: :turning}} ->
|
||||
some("Tailstock backward")
|
||||
|
||||
{ok(22), _} ->
|
||||
some("Mirror Y axis")
|
||||
|
||||
{ok(23), %{operation: :turning}} ->
|
||||
some("Thread gradual pullout on")
|
||||
|
||||
{ok(23), _} ->
|
||||
some("Mirror off")
|
||||
|
||||
{ok(24), %{operation: :turning}} ->
|
||||
some("Thread gradual pullout off")
|
||||
|
||||
{ok(30), _} ->
|
||||
some("End of program")
|
||||
|
||||
{ok(gear), %{operation: :turning}} when gear > 40 and gear < 45 ->
|
||||
some("Gear select #{gear - 40}")
|
||||
|
||||
{ok(48), _} ->
|
||||
some("Feedrate override allowed")
|
||||
|
||||
{ok(49), _} ->
|
||||
some("Feedrate override not allowed")
|
||||
|
||||
{ok(52), _} ->
|
||||
some("Unload tool")
|
||||
|
||||
{ok(60), _} ->
|
||||
some("Automatic pallet change")
|
||||
|
||||
{ok(98), _} ->
|
||||
some("Subprogram call")
|
||||
|
||||
{ok(99), _} ->
|
||||
some("Subprogram end")
|
||||
|
||||
{ok(100), _} ->
|
||||
some("Clean nozzle")
|
||||
|
||||
_ ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "N", address: line}, _) do
|
||||
case Expr.evaluate(line) do
|
||||
ok(line) -> some("Line #{line}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "O", address: name}, _) do
|
||||
case Expr.evaluate(name) do
|
||||
ok(name) -> some("Program #{name}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "P", address: param}, _) do
|
||||
case Expr.evaluate(param) do
|
||||
ok(param) -> some("Parameter #{param}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "Q", address: distance}, options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
ok(distance) -> some("Peck increment #{distance}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "R", address: distance}, options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
ok(distance) -> some("Radius #{distance}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "S", address: speed}, _) do
|
||||
case Expr.evaluate(speed) do
|
||||
ok(speed) -> some("Speed #{speed}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "T", address: tool}, _) do
|
||||
case Expr.evaluate(tool) do
|
||||
ok(tool) -> some("Tool #{tool}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: distance}, options) when axis in ~w[X Y Z] do
|
||||
case distance_with_unit(distance, options) do
|
||||
ok(distance) -> some("#{axis} #{distance}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{}, _options), do: none()
|
||||
|
||||
defp distance_with_unit(distance, %{units: :mm}), do: "#{distance}mm"
|
||||
defp distance_with_unit(distance, %{units: :inches}), do: "#{distance}\""
|
||||
defp distance_with_unit(distance, _), do: distance
|
||||
defp distance_with_unit(distance, %{units: :mm}) when is_number(distance),
|
||||
do: some("#{distance}mm")
|
||||
|
||||
defp feedrate(distance, %{operation: :turning} = options),
|
||||
do: "#{distance_with_unit(distance, options)}/rev"
|
||||
defp distance_with_unit(distance, %{units: :inches}) when is_number(distance),
|
||||
do: some("#{distance}\"")
|
||||
|
||||
defp feedrate(distance, options), do: "#{distance_with_unit(distance, options)}/min"
|
||||
defp distance_with_unit(distance, _) when is_number(distance),
|
||||
do: some(to_string(distance))
|
||||
|
||||
defp distance_with_unit(distance, options) when is_expression(distance) do
|
||||
case Expr.evaluate(distance) do
|
||||
ok(distance) when is_number(distance) -> distance_with_unit(distance, options)
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp distance_with_unit(_, _), do: none()
|
||||
|
||||
defp feedrate(distance, %{operation: :turning} = options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
some(distance) -> some("#{distance}/rev")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp feedrate(distance, options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
some(distance) -> some("#{distance}/min")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
defimpl Inspect, for: Gcode.Model.Word do
|
||||
alias Gcode.Model.{Describe, Word}
|
||||
alias Gcode.Model.{Describe, Expr, Word}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
import Inspect.Algebra
|
||||
@moduledoc false
|
||||
|
||||
def inspect(%Word{word: letter, address: address} = word, opts) do
|
||||
address =
|
||||
case Expr.evaluate(address) do
|
||||
ok(address) -> address
|
||||
error(reason) -> "Address error: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
case Describe.describe(word) do
|
||||
some(description) ->
|
||||
concat([
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Word, Serialise}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Word{word: word, address: address}) when is_integer(address) do
|
||||
ok(["#{word}#{address}"])
|
||||
end
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Word`, allowing it to be turned into
|
||||
G-code output.
|
||||
"""
|
||||
|
||||
def serialise(%Word{word: word, address: address}) when is_float(address) do
|
||||
ok(["#{word}#{address}"])
|
||||
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Word{word: word, address: address})
|
||||
when is_binary(word) and byte_size(word) == 1 do
|
||||
with ok(address) <- Serialise.serialise(address),
|
||||
do: ok(["#{word}#{address}"])
|
||||
end
|
||||
|
||||
def serialise(_word), do: error({:serialise_error, "invalid word"})
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Gcode.Option do
|
|||
A helper which represents an optional type.
|
||||
"""
|
||||
|
||||
@spec __using__(any) :: Macro.t()
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
alias Gcode.Option
|
||||
|
@ -16,14 +17,17 @@ defmodule Gcode.Option do
|
|||
@type some(t) :: {:ok, t}
|
||||
@type opt_none :: :error
|
||||
|
||||
@doc "Is the value a none?"
|
||||
@spec none?(t(any)) :: boolean
|
||||
def none?(:error), do: true
|
||||
def none?({:ok, _}), do: false
|
||||
|
||||
@doc "Is the value a some?"
|
||||
@spec some?(t(any)) :: boolean
|
||||
def some?(:error), do: false
|
||||
def some?({:ok, _}), do: true
|
||||
|
||||
@doc "Create or match a none"
|
||||
@spec none :: Macro.t()
|
||||
defmacro none do
|
||||
quote do
|
||||
|
@ -31,6 +35,7 @@ defmodule Gcode.Option do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Create or match a some"
|
||||
@spec some(any) :: Macro.t()
|
||||
defmacro some(pattern) do
|
||||
quote do
|
||||
|
@ -38,6 +43,7 @@ defmodule Gcode.Option do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Attempt to unwrap an option. Raises an error if the option is a none"
|
||||
@spec unwrap!(t) :: any | no_return
|
||||
def unwrap!({:ok, result}), do: result
|
||||
def unwrap!(:error), do: raise("Attempt to unwrap a none")
|
||||
|
|
211
lib/gcode/parser.ex
Normal file
211
lib/gcode/parser.ex
Normal file
|
@ -0,0 +1,211 @@
|
|||
defmodule Gcode.Parser do
|
||||
use Gcode.Result
|
||||
alias Gcode.Model.{Block, Comment, Expr, Program, Word}
|
||||
alias Gcode.Parser.{Engine, Error}
|
||||
|
||||
@moduledoc """
|
||||
A parser for G-code programs.
|
||||
|
||||
This parser converts G-code input (in UTF-8 encoding) into representations
|
||||
with the contents of `Gcode.Model`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Attempt to parse a G-code program from a string.
|
||||
"""
|
||||
@spec parse_string(String.t()) :: Result.t(Program.t(), {:parse_error, String.t()})
|
||||
def parse_string(input) do
|
||||
with ok(tokens) <- Engine.parse(input),
|
||||
ok(program) <- hydrate(tokens) do
|
||||
ok(program)
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:parse_error, reason}
|
||||
|
||||
{:error, {message, unexpected, _, {line, _}, col}} ->
|
||||
{:parse_error,
|
||||
"Unexpected #{inspect(unexpected)} at line: #{line}:#{col + 1}. #{message}."}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attempt to parse the G-code program at the given path.
|
||||
"""
|
||||
@spec parse_file(Path.t()) :: Result.t(Program.t(), {:parse_error, String.t()})
|
||||
def parse_file(path) do
|
||||
with ok(input) <- File.read(path),
|
||||
ok(tokens) <- Engine.parse(input),
|
||||
ok(program) <- hydrate(tokens) do
|
||||
ok(program)
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:parse_error, reason}
|
||||
|
||||
{:error, {message, unexpected, _, {line, _}, col}} ->
|
||||
{:parse_error,
|
||||
"Unexpected #{inspect(unexpected)} at line: #{line}:#{col + 1}. #{message}."}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parse and stream the G-code program from a string.
|
||||
|
||||
Note that this function doesn't yield `Program` objects, but blocks, comments,
|
||||
etc.
|
||||
"""
|
||||
@spec stream_string!(String.t()) :: Enumerable.t() | no_return
|
||||
def stream_string!(input) do
|
||||
input
|
||||
|> String.split(~r/\r?\n/)
|
||||
|> Stream.with_index()
|
||||
|> ParallelStream.map(&trim_line/1)
|
||||
|> ParallelStream.reject(&(elem(&1, 0) == ""))
|
||||
|> ParallelStream.map(&parse_line!/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parse and stream the G-code program at the given location.
|
||||
|
||||
Note that this function doesn't yield `Program` objects, but blocks, comments,
|
||||
etc.
|
||||
"""
|
||||
@spec stream_file!(Path.t()) :: Enumerable.t() | no_return
|
||||
def stream_file!(path) do
|
||||
path
|
||||
|> File.stream!()
|
||||
|> Stream.with_index()
|
||||
|> ParallelStream.map(&trim_line/1)
|
||||
|> ParallelStream.reject(&(elem(&1, 0) == ""))
|
||||
|> ParallelStream.map(&parse_line!/1)
|
||||
end
|
||||
|
||||
defp trim_line({input, line_no}), do: {String.trim(input), line_no}
|
||||
|
||||
defp parse_line!({input, line_no}) do
|
||||
with ok(tokens) <- Engine.parse(input),
|
||||
ok(%Program{elements: [element]}) <- hydrate(tokens) do
|
||||
element
|
||||
else
|
||||
ok(%Program{elements: elements}) ->
|
||||
raise Error, "Expected line to result in 1 element, but contained #{length(elements)}"
|
||||
|
||||
ok(%Block{}) ->
|
||||
raise Error, "Expected parser to return a program, but returned a block instead."
|
||||
|
||||
error({:block_error, reason}) ->
|
||||
raise Error, "Block error #{reason}"
|
||||
|
||||
error({:comment_error, reason}) ->
|
||||
raise Error, "Comment error #{reason}"
|
||||
|
||||
error({:expression_error, reason}) ->
|
||||
raise Error, "Expression error #{reason}"
|
||||
|
||||
error({:parse_error, reason}) ->
|
||||
raise Error, "Parse error: #{reason}"
|
||||
|
||||
error({:program_error, reason}) ->
|
||||
raise Error, "Program error: #{reason}"
|
||||
|
||||
error({:string_error, reason}) ->
|
||||
raise Error, "String error: #{reason}"
|
||||
|
||||
error({:word_error, reason}) ->
|
||||
raise Error, "Word error: #{reason}"
|
||||
|
||||
error({message, unexpected, _, _, col}) ->
|
||||
raise Error,
|
||||
"Unexpected #{inspect(unexpected)} at line: #{line_no + 1}:#{col + 1}. #{message}."
|
||||
end
|
||||
end
|
||||
|
||||
defp hydrate(tokens) do
|
||||
with ok(program) <- Program.init(),
|
||||
do: hydrate(tokens, program)
|
||||
end
|
||||
|
||||
defp hydrate([], result), do: ok(result)
|
||||
|
||||
defp hydrate([{:comment, comment} | remaining], %Program{} = program) do
|
||||
with ok(comment) <- Comment.init(List.to_string(comment)),
|
||||
ok(program) <- Program.push(program, comment),
|
||||
do: hydrate(remaining, program)
|
||||
end
|
||||
|
||||
defp hydrate([{:comment, comment} | remaining], %Block{} = block) do
|
||||
with ok(comment) <- Comment.init(List.to_string(comment)),
|
||||
ok(block) <- Block.comment(block, comment),
|
||||
do: hydrate(remaining, block)
|
||||
end
|
||||
|
||||
defp hydrate([{:block, words} | remaining], %Program{} = program) do
|
||||
with ok(block) <- Block.init(),
|
||||
ok(block) <- hydrate(words, block),
|
||||
ok(program) <- Program.push(program, block),
|
||||
do: hydrate(remaining, program)
|
||||
end
|
||||
|
||||
defp hydrate([{:word, contents} | remaining], %Block{} = block) do
|
||||
with ok(command) <- Keyword.fetch(contents, :command),
|
||||
ok(address) <- Keyword.fetch(contents, :address),
|
||||
ok(address) <- expression(address),
|
||||
ok(word) <- Word.init(List.to_string(command), address),
|
||||
ok(block) <- Block.push(block, word),
|
||||
do: hydrate(remaining, block)
|
||||
end
|
||||
|
||||
defp hydrate([{:string, _} = str | remaining], %Block{} = block) do
|
||||
with ok(str) <- expression([str]),
|
||||
ok(block) <- Block.push(block, str),
|
||||
do: hydrate(remaining, block)
|
||||
end
|
||||
|
||||
defp hydrate([{:newline, _} | remaining], %Program{} = program), do: hydrate(remaining, program)
|
||||
defp hydrate([{:newline, _}], %Block{} = block), do: ok(block)
|
||||
|
||||
defp expression([{:-, inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:-, inner)
|
||||
end
|
||||
|
||||
defp expression([{:+, inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:+, inner)
|
||||
end
|
||||
|
||||
defp expression([{:!, inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:!, inner)
|
||||
end
|
||||
|
||||
defp expression([{:"#", inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:"#", inner)
|
||||
end
|
||||
|
||||
defp expression(integer: value) do
|
||||
value =
|
||||
value
|
||||
|> List.to_string()
|
||||
|> String.to_integer()
|
||||
|
||||
Expr.Integer.init(value)
|
||||
end
|
||||
|
||||
defp expression(float: value) do
|
||||
value =
|
||||
value
|
||||
|> List.to_string()
|
||||
|> String.to_float()
|
||||
|
||||
Expr.Float.init(value)
|
||||
end
|
||||
|
||||
defp expression(string: value) do
|
||||
value =
|
||||
value
|
||||
|> List.to_string()
|
||||
|
||||
Expr.String.init(value)
|
||||
end
|
||||
end
|
238
lib/gcode/parser/engine.ex
Normal file
238
lib/gcode/parser/engine.ex
Normal file
|
@ -0,0 +1,238 @@
|
|||
defmodule Gcode.Parser.Engine do
|
||||
import NimbleParsec
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
A parser for G-code programs using Parsec.
|
||||
"""
|
||||
|
||||
defcombinatorp(
|
||||
:whitespace,
|
||||
[
|
||||
string(" "),
|
||||
string("\t")
|
||||
]
|
||||
|> choice()
|
||||
|> repeat()
|
||||
|> ignore()
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:whitespace?,
|
||||
optional(parsec(:whitespace))
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:newline,
|
||||
[string("\r"), string("\n")] |> choice() |> times(min: 1) |> tag(:newline)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:eol,
|
||||
empty()
|
||||
|> parsec(:whitespace?)
|
||||
|> choice([parsec(:newline), eos()])
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:tape,
|
||||
empty()
|
||||
|> ignore(string("%"))
|
||||
|> optional(
|
||||
parsec(:whitespace?)
|
||||
|> repeat(utf8_char([]))
|
||||
)
|
||||
|> tag(:tape)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:braces_comment,
|
||||
empty()
|
||||
|> ignore(
|
||||
string("(")
|
||||
|> parsec(:whitespace?)
|
||||
)
|
||||
|> repeat(
|
||||
lookahead_not(
|
||||
choice([
|
||||
string(")"),
|
||||
parsec(:whitespace)
|
||||
|> string(")")
|
||||
])
|
||||
)
|
||||
|> utf8_char([])
|
||||
)
|
||||
|> tag(:comment)
|
||||
|> ignore(
|
||||
parsec(:whitespace?)
|
||||
|> string(")")
|
||||
)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:semi_comment,
|
||||
empty()
|
||||
|> ignore(
|
||||
string(";")
|
||||
|> parsec(:whitespace?)
|
||||
)
|
||||
|> repeat(
|
||||
lookahead_not(parsec(:eol))
|
||||
|> utf8_char([])
|
||||
)
|
||||
|> tag(:comment)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:comment,
|
||||
choice([
|
||||
parsec(:braces_comment),
|
||||
parsec(:semi_comment)
|
||||
])
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:integer,
|
||||
times(
|
||||
utf8_char([?0..?9]),
|
||||
min: 1
|
||||
)
|
||||
|> tag(:integer)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:float,
|
||||
times(utf8_char([?0..?9]), min: 1)
|
||||
|> utf8_char([?.])
|
||||
|> times(utf8_char([?0..?9]), min: 1)
|
||||
|> tag(:float)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:number,
|
||||
choice([
|
||||
parsec(:float),
|
||||
parsec(:integer)
|
||||
])
|
||||
)
|
||||
|
||||
defcombinator(
|
||||
:constant,
|
||||
choice([
|
||||
choice([
|
||||
string("true"),
|
||||
string("false")
|
||||
])
|
||||
|> tag(:boolean),
|
||||
string("iterations")
|
||||
|> tag(:iterations),
|
||||
string("line")
|
||||
|> tag(:line),
|
||||
string("pi")
|
||||
|> tag(:pi),
|
||||
string("result")
|
||||
|> tag(:result)
|
||||
])
|
||||
|> tag(:constant)
|
||||
)
|
||||
|
||||
defcombinator(
|
||||
:prefix,
|
||||
choice([
|
||||
ignore(string("!") |> parsec(:whitespace?)) |> tag(parsec(:expression), :!),
|
||||
ignore(string("+") |> parsec(:whitespace?)) |> tag(parsec(:expression), :+),
|
||||
ignore(string("-") |> parsec(:whitespace?)) |> tag(parsec(:expression), :-),
|
||||
ignore(string("#") |> parsec(:whitespace?)) |> tag(parsec(:expression), :"#")
|
||||
])
|
||||
)
|
||||
|
||||
defcombinator(
|
||||
:expression,
|
||||
choice([
|
||||
parsec(:prefix),
|
||||
parsec(:number),
|
||||
parsec(:constant)
|
||||
])
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:bare_string,
|
||||
times(
|
||||
lookahead_not(
|
||||
parsec(:whitespace?)
|
||||
|> choice([
|
||||
parsec(:newline),
|
||||
parsec(:comment)
|
||||
])
|
||||
)
|
||||
|> utf8_char([]),
|
||||
min: 1
|
||||
)
|
||||
|> tag(:string)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:quoted_string,
|
||||
ignore(string("\""))
|
||||
|> repeat(
|
||||
lookahead_not(
|
||||
choice([
|
||||
parsec(:newline),
|
||||
string("\"")
|
||||
])
|
||||
)
|
||||
|> utf8_char([])
|
||||
)
|
||||
|> tag(:string)
|
||||
|> ignore(optional(string("\"")))
|
||||
)
|
||||
|
||||
defcombinatorp(:string, choice([parsec(:quoted_string), parsec(:bare_string)]))
|
||||
|
||||
defcombinatorp(
|
||||
:word,
|
||||
empty()
|
||||
|> utf8_char([?A..?Z])
|
||||
|> tag(:command)
|
||||
|> parsec(:whitespace?)
|
||||
|> tag(
|
||||
parsec(:expression),
|
||||
:address
|
||||
)
|
||||
|> tag(:word)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:block,
|
||||
empty()
|
||||
|> times(
|
||||
choice([
|
||||
parsec(:word),
|
||||
parsec(:string)
|
||||
])
|
||||
|> parsec(:whitespace?),
|
||||
min: 1
|
||||
)
|
||||
|> optional(parsec(:comment))
|
||||
|> tag(:block)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:line,
|
||||
choice([
|
||||
parsec(:tape),
|
||||
parsec(:comment),
|
||||
parsec(:block)
|
||||
])
|
||||
)
|
||||
|
||||
defparsecp(:program, times(parsec(:line) |> parsec(:eol), min: 1))
|
||||
|
||||
@spec parse(String.t()) :: Result.t(keyword)
|
||||
def parse(program) do
|
||||
case program(program) do
|
||||
{:ok, tokens, _, _, _, _} -> ok(tokens)
|
||||
{:error, a, b, c, d, e} -> error({a, b, c, d, e})
|
||||
end
|
||||
end
|
||||
end
|
8
lib/gcode/parser/error.ex
Normal file
8
lib/gcode/parser/error.ex
Normal file
|
@ -0,0 +1,8 @@
|
|||
defmodule Gcode.Parser.Error do
|
||||
defexception message: nil
|
||||
|
||||
@moduledoc """
|
||||
Parser's streaming outputs have no way to return a result type, so we are
|
||||
forced to rely on exceptions. These are those exceptions.
|
||||
"""
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
defmodule Gcode.Result do
|
||||
@moduledoc """
|
||||
A helper which represents a result type.
|
||||
|
||||
This is really just a wrapper around Erlang's ok/error tuples.
|
||||
"""
|
||||
|
||||
@type t :: t(any, any)
|
||||
|
@ -9,6 +11,7 @@ defmodule Gcode.Result do
|
|||
@type ok(result) :: {:ok, result}
|
||||
@type error(error) :: {:error, error}
|
||||
|
||||
@spec __using__(any) :: Macro.t()
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
alias Gcode.Result
|
||||
|
@ -17,6 +20,7 @@ defmodule Gcode.Result do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Initialise or match an ok value"
|
||||
@spec ok(any) :: Macro.t()
|
||||
defmacro ok(result) do
|
||||
quote do
|
||||
|
@ -24,6 +28,7 @@ defmodule Gcode.Result do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Initialise or match an error value"
|
||||
@spec error(any) :: Macro.t()
|
||||
defmacro error(error) do
|
||||
quote do
|
||||
|
@ -31,18 +36,22 @@ defmodule Gcode.Result do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Is the result ok?"
|
||||
@spec ok?(t) :: boolean
|
||||
def ok?({:ok, _}), do: true
|
||||
def ok?({:error, _}), do: false
|
||||
|
||||
@doc "Is the result an error?"
|
||||
@spec error?(t) :: boolean
|
||||
def error?({:ok, _}), do: false
|
||||
def error?({:error, _}), do: true
|
||||
|
||||
@doc "Attempt to unwrap a result and return the inner value. Raises an exception if the result contains an error."
|
||||
@spec unwrap!(t) :: any | no_return
|
||||
def unwrap!({:ok, result}), do: result
|
||||
def unwrap!({:error, error}), do: raise(error)
|
||||
|
||||
@doc "Convert a successful result another result."
|
||||
@spec map(t, (any -> t)) :: t
|
||||
def map({:ok, value}, mapper) when is_function(mapper, 1) do
|
||||
case mapper.(value) do
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
defmodule Gcode.Result.Enum do
|
||||
@moduledoc false
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Common enumerableish functions on results.
|
||||
"""
|
||||
|
||||
@type result :: Result.t()
|
||||
@type result(result) :: Result.t(result)
|
||||
@type result(result, error) :: Result.t(result, error)
|
||||
|
@ -13,32 +16,44 @@ defmodule Gcode.Result.Enum do
|
|||
reducer :: (any, any -> result(any))
|
||||
) :: result(any)
|
||||
def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do
|
||||
Enum.reduce_while(elements, {:ok, acc}, fn element, {:ok, acc} ->
|
||||
Enum.reduce_while(elements, ok(acc), fn element, ok(acc) ->
|
||||
case reducer.(element, acc) do
|
||||
{:ok, acc} -> {:cont, {:ok, acc}}
|
||||
{:error, reason} -> {:halt, {:error, reason}}
|
||||
ok(acc) -> {:cont, ok(acc)}
|
||||
error(reason) -> {:halt, error(reason)}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps a collection of results using a mapping function.
|
||||
|
||||
Both the input to the map must be an ok result and the result of each mapping
|
||||
function.
|
||||
"""
|
||||
@spec map(result([any]), mapper :: (any -> result(any))) :: result([any])
|
||||
def map({:ok, enumerable}, mapper) when is_function(mapper, 1) do
|
||||
def map(ok(enumerable), mapper) when is_function(mapper, 1) do
|
||||
reduce_while_ok(enumerable, [], fn element, acc ->
|
||||
case mapper.(element) do
|
||||
{:ok, mapped} -> {:ok, [mapped | acc]}
|
||||
{:error, reason} -> {:error, reason}
|
||||
ok(mapped) -> ok([mapped | acc])
|
||||
error(reason) -> error(reason)
|
||||
end
|
||||
end)
|
||||
|> reverse()
|
||||
end
|
||||
|
||||
def map({:error, reason}, _mapper), do: {:error, reason}
|
||||
def map(error(reason), _mapper), do: error(reason)
|
||||
|
||||
@doc """
|
||||
Reverse the enumerable contents of an ok result.
|
||||
"""
|
||||
@spec reverse(result([any])) :: result([any])
|
||||
def reverse({:ok, enumerable}), do: {:ok, Enum.reverse(enumerable)}
|
||||
def reverse({:error, reason}), do: {:error, reason}
|
||||
def reverse(ok(enumerable)), do: ok(Enum.reverse(enumerable))
|
||||
def reverse(error(reason)), do: error(reason)
|
||||
|
||||
@doc """
|
||||
Join the string contents of an ok result.
|
||||
"""
|
||||
@spec join(result([String.t()]), String.t()) :: result(String.t())
|
||||
def join({:ok, strings}, joiner) when is_binary(joiner), do: {:ok, Enum.join(strings, joiner)}
|
||||
def join({:error, reason}, _joiner), do: {:error, reason}
|
||||
def join(ok(strings), joiner) when is_binary(joiner), do: ok(Enum.join(strings, joiner))
|
||||
def join(error(reason), _joiner), do: error(reason)
|
||||
end
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -44,7 +44,9 @@ defmodule Gcode.MixProject do
|
|||
{:ex_doc, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
|
||||
{:earmark, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
|
||||
{:credo, "~> 1.1", only: ~w[dev test]a, runtime: false},
|
||||
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false}
|
||||
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false},
|
||||
{:nimble_parsec, "~> 1.1"},
|
||||
{:parallel_stream, "~> 1.0"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -11,4 +11,5 @@
|
|||
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
|
||||
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"},
|
||||
}
|
||||
|
|
6723
priv/fixtures/cura_marlin.gcode
Normal file
6723
priv/fixtures/cura_marlin.gcode
Normal file
File diff suppressed because it is too large
Load diff
501
priv/fixtures/fusion_360_milling_grbl.nc
Normal file
501
priv/fixtures/fusion_360_milling_grbl.nc
Normal file
|
@ -0,0 +1,501 @@
|
|||
(1001)
|
||||
(Machine)
|
||||
( vendor: Gennmitsu)
|
||||
( model: 3018 Pro)
|
||||
( description: Gennmitsu 3018 Pro)
|
||||
(T1 D=3.175 CR=0 - ZMIN=-6 - flat end mill)
|
||||
G90 G94
|
||||
G17
|
||||
G21
|
||||
(When using Fusion 360 for Personal Use, the feedrate of )
|
||||
(rapid moves is reduced to match the feedrate of cutting )
|
||||
(moves, which can increase machining time. Unrestricted )
|
||||
(rapid moves are available with a Fusion 360 Subscription. )
|
||||
G28 G91 Z0
|
||||
G90
|
||||
|
||||
(2D Contour1)
|
||||
T1
|
||||
S1000 M3
|
||||
G54
|
||||
G0 X160.042 Y80.893
|
||||
Z5
|
||||
G1 Z2 F120
|
||||
Z1.317 F10
|
||||
G19 G3 Y81.21 Z1 J0.317 K0 F120
|
||||
G1 Y81.527
|
||||
G17 G3 X159.724 Y81.845 I-0.318 J0
|
||||
G1 X21.16 Z0.52
|
||||
G3 X21.16 Y78.155 Z0.5 I0 J-1.845
|
||||
G1 X159.724 Z0.02
|
||||
G3 X159.724 Y81.845 Z0 I0 J1.845
|
||||
G1 X21.16 Z-0.48
|
||||
G3 X21.16 Y78.155 Z-0.5 I0 J-1.845
|
||||
G1 X159.724 Z-0.98
|
||||
G3 X159.724 Y81.845 Z-1 I0 J1.845
|
||||
G1 X21.16 Z-1.48
|
||||
G3 X21.16 Y78.155 Z-1.5 I0 J-1.845
|
||||
G1 X159.724 Z-1.98
|
||||
G3 X159.724 Y81.845 Z-2 I0 J1.845
|
||||
G1 X21.16 Z-2.48
|
||||
G3 X21.16 Y78.155 Z-2.5 I0 J-1.845
|
||||
G1 X159.724 Z-2.98
|
||||
G3 X159.724 Y81.845 Z-3 I0 J1.845
|
||||
G1 X21.16 Z-3.48
|
||||
G3 X21.16 Y78.155 Z-3.5 I0 J-1.845
|
||||
G1 X159.724 Z-3.98
|
||||
G3 X159.724 Y81.845 Z-4 I0 J1.845
|
||||
G1 X21.16 Z-4.48
|
||||
G3 X21.16 Y78.155 Z-4.5 I0 J-1.845
|
||||
G1 X159.724 Z-4.98
|
||||
G3 X159.724 Y81.845 Z-5 I0 J1.845
|
||||
G1 X21.16 Z-5.48
|
||||
G3 X21.16 Y78.155 Z-5.5 I0 J-1.845
|
||||
G1 X159.724 Z-5.98
|
||||
G3 X159.724 Y81.845 Z-6 I0 J1.845
|
||||
G1 X21.16
|
||||
G3 X21.16 Y78.155 I0 J-1.845
|
||||
G1 X159.724
|
||||
G3 X159.724 Y81.845 I0 J1.845
|
||||
G2 X159.486 Y82.004 I0 J0.258
|
||||
G3 X159.248 Y82.162 I-0.238 J-0.099
|
||||
G1 X21.16 F90
|
||||
G3 X21.16 Y77.838 I0 J-2.162
|
||||
G1 X159.724
|
||||
G3 X159.724 Y82.162 I0 J2.162
|
||||
G1 X159.248
|
||||
G3 X158.93 Y81.845 I0 J-0.317 F120
|
||||
G1 Y81.527
|
||||
G19 G2 Y81.21 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X151.956 Y94.683
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G18 G2 X152.274 Z1 I0.318 K0 F120
|
||||
G1 X152.592
|
||||
G17 G3 X152.909 Y95 I0 J0.317
|
||||
X151.064 Y96.845 Z0.989 I-1.845 J0
|
||||
G1 X29.82 Z0.511
|
||||
G3 X29.82 Y93.155 Z0.489 I0 J-1.845
|
||||
G1 X151.064 Z0.011
|
||||
G3 X151.064 Y96.845 Z-0.011 I0 J1.845
|
||||
G1 X29.82 Z-0.489
|
||||
G3 X29.82 Y93.155 Z-0.511 I0 J-1.845
|
||||
G1 X151.064 Z-0.989
|
||||
G3 X151.064 Y96.845 Z-1.011 I0 J1.845
|
||||
G1 X29.82 Z-1.489
|
||||
G3 X29.82 Y93.155 Z-1.511 I0 J-1.845
|
||||
G1 X151.064 Z-1.989
|
||||
G3 X151.064 Y96.845 Z-2.011 I0 J1.845
|
||||
G1 X29.82 Z-2.489
|
||||
G3 X29.82 Y93.155 Z-2.511 I0 J-1.845
|
||||
G1 X151.064 Z-2.989
|
||||
G3 X151.064 Y96.845 Z-3.011 I0 J1.845
|
||||
G1 X29.82 Z-3.489
|
||||
G3 X29.82 Y93.155 Z-3.511 I0 J-1.845
|
||||
G1 X151.064 Z-3.989
|
||||
G3 X151.064 Y96.845 Z-4.011 I0 J1.845
|
||||
G1 X29.82 Z-4.489
|
||||
G3 X29.82 Y93.155 Z-4.511 I0 J-1.845
|
||||
G1 X151.064 Z-4.989
|
||||
G3 X151.064 Y96.845 Z-5.011 I0 J1.845
|
||||
G1 X29.82 Z-5.489
|
||||
G3 X29.82 Y93.155 Z-5.511 I0 J-1.845
|
||||
G1 X151.064 Z-5.989
|
||||
G3 X152.909 Y95 Z-6 I0 J1.845
|
||||
X151.064 Y96.845 I-1.845 J0
|
||||
G1 X29.82
|
||||
G3 X29.82 Y93.155 I0 J-1.845
|
||||
G1 X151.064
|
||||
G3 X152.909 Y95 I0 J1.845
|
||||
G2 X153.055 Y95.229 I0.253 J0
|
||||
G3 X153.174 Y95.472 I-0.092 J0.196
|
||||
X151.064 Y97.162 I-2.11 J-0.472 F90
|
||||
G1 X29.82
|
||||
G3 X29.82 Y92.838 I0 J-2.162
|
||||
G1 X151.064
|
||||
G3 X153.174 Y95.472 I0 J2.162
|
||||
X152.795 Y95.713 I-0.31 J-0.069 F120
|
||||
G1 X152.485 Y95.644
|
||||
X152.416 Y95.628 Z-5.992
|
||||
X152.351 Y95.613 Z-5.969
|
||||
X152.292 Y95.6 Z-5.931
|
||||
X152.243 Y95.589 Z-5.88
|
||||
X152.206 Y95.581 Z-5.82
|
||||
X152.183 Y95.576 Z-5.753
|
||||
X152.175 Y95.574 Z-5.683
|
||||
Z3
|
||||
X142.721 Y110.893
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G3 Y111.21 Z1 J0.317 K0 F120
|
||||
G1 Y111.527
|
||||
G17 G3 X142.404 Y111.845 I-0.317 J0
|
||||
G1 X38.481 Z0.526
|
||||
G3 X38.481 Y108.155 Z0.5 I0 J-1.845
|
||||
G1 X142.404 Z0.026
|
||||
G3 X142.404 Y111.845 Z0 I0 J1.845
|
||||
G1 X38.481 Z-0.474
|
||||
G3 X38.481 Y108.155 Z-0.5 I0 J-1.845
|
||||
G1 X142.404 Z-0.974
|
||||
G3 X142.404 Y111.845 Z-1 I0 J1.845
|
||||
G1 X38.481 Z-1.474
|
||||
G3 X38.481 Y108.155 Z-1.5 I0 J-1.845
|
||||
G1 X142.404 Z-1.974
|
||||
G3 X142.404 Y111.845 Z-2 I0 J1.845
|
||||
G1 X38.481 Z-2.474
|
||||
G3 X38.481 Y108.155 Z-2.5 I0 J-1.845
|
||||
G1 X142.404 Z-2.974
|
||||
G3 X142.404 Y111.845 Z-3 I0 J1.845
|
||||
G1 X38.481 Z-3.474
|
||||
G3 X38.481 Y108.155 Z-3.5 I0 J-1.845
|
||||
G1 X142.404 Z-3.974
|
||||
G3 X142.404 Y111.845 Z-4 I0 J1.845
|
||||
G1 X38.481 Z-4.474
|
||||
G3 X38.481 Y108.155 Z-4.5 I0 J-1.845
|
||||
G1 X142.404 Z-4.974
|
||||
G3 X142.404 Y111.845 Z-5 I0 J1.845
|
||||
G1 X38.481 Z-5.474
|
||||
G3 X38.481 Y108.155 Z-5.5 I0 J-1.845
|
||||
G1 X142.404 Z-5.974
|
||||
G3 X142.404 Y111.845 Z-6 I0 J1.845
|
||||
G1 X38.481
|
||||
G3 X38.481 Y108.155 I0 J-1.845
|
||||
G1 X142.404
|
||||
G3 X142.404 Y111.845 I0 J1.845
|
||||
G2 X142.166 Y112.004 I0 J0.258
|
||||
G3 X141.928 Y112.162 I-0.238 J-0.099
|
||||
G1 X38.481 F90
|
||||
G3 X38.481 Y107.838 I0 J-2.162
|
||||
G1 X142.404
|
||||
G3 X142.404 Y112.162 I0 J2.162
|
||||
G1 X141.928
|
||||
G3 X141.61 Y111.845 I0 J-0.317 F120
|
||||
G1 Y111.527
|
||||
G19 G2 Y111.21 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X46.824 Y124.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G2 Y123.79 Z1 J-0.317 K0 F120
|
||||
G1 Y123.473
|
||||
G17 G3 X47.141 Y123.155 I0.317 J0
|
||||
G1 X133.744 Z0.531
|
||||
G3 X133.744 Y126.845 Z0.5 I0 J1.845
|
||||
G1 X47.141 Z0.031
|
||||
G3 X47.141 Y123.155 Z0 I0 J-1.845
|
||||
G1 X133.744 Z-0.469
|
||||
G3 X133.744 Y126.845 Z-0.5 I0 J1.845
|
||||
G1 X47.141 Z-0.969
|
||||
G3 X47.141 Y123.155 Z-1 I0 J-1.845
|
||||
G1 X133.744 Z-1.469
|
||||
G3 X133.744 Y126.845 Z-1.5 I0 J1.845
|
||||
G1 X47.141 Z-1.969
|
||||
G3 X47.141 Y123.155 Z-2 I0 J-1.845
|
||||
G1 X133.744 Z-2.469
|
||||
G3 X133.744 Y126.845 Z-2.5 I0 J1.845
|
||||
G1 X47.141 Z-2.969
|
||||
G3 X47.141 Y123.155 Z-3 I0 J-1.845
|
||||
G1 X133.744 Z-3.469
|
||||
G3 X133.744 Y126.845 Z-3.5 I0 J1.845
|
||||
G1 X47.141 Z-3.969
|
||||
G3 X47.141 Y123.155 Z-4 I0 J-1.845
|
||||
G1 X133.744 Z-4.469
|
||||
G3 X133.744 Y126.845 Z-4.5 I0 J1.845
|
||||
G1 X47.141 Z-4.969
|
||||
G3 X47.141 Y123.155 Z-5 I0 J-1.845
|
||||
G1 X133.744 Z-5.469
|
||||
G3 X133.744 Y126.845 Z-5.5 I0 J1.845
|
||||
G1 X47.141 Z-5.969
|
||||
G3 X47.141 Y123.155 Z-6 I0 J-1.845
|
||||
G1 X133.744
|
||||
G3 X133.744 Y126.845 I0 J1.845
|
||||
G1 X47.141
|
||||
G3 X47.141 Y123.155 I0 J-1.845
|
||||
G2 X47.379 Y122.996 I0 J-0.258
|
||||
G3 X47.617 Y122.838 I0.238 J0.099
|
||||
G1 X133.744 F90
|
||||
G3 X133.744 Y127.162 I0 J2.162
|
||||
G1 X47.141
|
||||
G3 X47.141 Y122.838 I0 J-2.162
|
||||
G1 X47.617
|
||||
G3 X47.935 Y123.155 I0 J0.317 F120
|
||||
G1 Y123.473
|
||||
G19 G3 Y123.79 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X124.766 Y139.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G2 Y138.79 Z1 J-0.318 K0 F120
|
||||
G1 Y138.473
|
||||
G17 G3 X125.083 Y138.155 I0.317 J0
|
||||
X125.083 Y141.845 Z0.961 I0 J1.845
|
||||
G1 X55.801 Z0.5
|
||||
G3 X55.801 Y138.155 Z0.461 I0 J-1.845
|
||||
G1 X125.083 Z0
|
||||
G3 X125.083 Y141.845 Z-0.039 I0 J1.845
|
||||
G1 X55.801 Z-0.5
|
||||
G3 X55.801 Y138.155 Z-0.539 I0 J-1.845
|
||||
G1 X125.083 Z-1
|
||||
G3 X125.083 Y141.845 Z-1.039 I0 J1.845
|
||||
G1 X55.801 Z-1.5
|
||||
G3 X55.801 Y138.155 Z-1.539 I0 J-1.845
|
||||
G1 X125.083 Z-2
|
||||
G3 X125.083 Y141.845 Z-2.039 I0 J1.845
|
||||
G1 X55.801 Z-2.5
|
||||
G3 X55.801 Y138.155 Z-2.539 I0 J-1.845
|
||||
G1 X125.083 Z-3
|
||||
G3 X125.083 Y141.845 Z-3.039 I0 J1.845
|
||||
G1 X55.801 Z-3.5
|
||||
G3 X55.801 Y138.155 Z-3.539 I0 J-1.845
|
||||
G1 X125.083 Z-4
|
||||
G3 X125.083 Y141.845 Z-4.039 I0 J1.845
|
||||
G1 X55.801 Z-4.5
|
||||
G3 X55.801 Y138.155 Z-4.539 I0 J-1.845
|
||||
G1 X125.083 Z-5
|
||||
G3 X125.083 Y141.845 Z-5.039 I0 J1.845
|
||||
G1 X55.801 Z-5.5
|
||||
G3 X55.801 Y138.155 Z-5.539 I0 J-1.845
|
||||
G1 X125.083 Z-6
|
||||
G3 X125.083 Y141.845 I0 J1.845
|
||||
G1 X55.801
|
||||
G3 X55.801 Y138.155 I0 J-1.845
|
||||
G1 X125.083
|
||||
G2 X125.312 Y138.009 I0 J-0.253
|
||||
G3 X125.555 Y137.89 I0.196 J0.092
|
||||
X125.083 Y142.163 I-0.472 J2.11 F90
|
||||
G1 X55.801
|
||||
G3 X55.801 Y137.837 I0 J-2.163
|
||||
G1 X125.083
|
||||
G3 X125.555 Y137.89 I0 J2.163
|
||||
X125.796 Y138.269 I-0.069 J0.31 F120
|
||||
G1 X125.727 Y138.579
|
||||
X125.711 Y138.648 Z-5.992
|
||||
X125.696 Y138.713 Z-5.969
|
||||
X125.683 Y138.772 Z-5.931
|
||||
X125.672 Y138.821 Z-5.88
|
||||
X125.664 Y138.858 Z-5.82
|
||||
X125.659 Y138.881 Z-5.753
|
||||
X125.657 Y138.889 Z-5.683
|
||||
Z3
|
||||
X29.503 Y64.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G2 Y63.79 Z1 J-0.317 K0 F120
|
||||
G1 Y63.472
|
||||
G17 G3 X29.82 Y63.155 I0.317 J0
|
||||
G1 X151.064 Z0.523
|
||||
G3 X151.064 Y66.845 Z0.5 I0 J1.845
|
||||
G1 X29.82 Z0.023
|
||||
G3 X29.82 Y63.155 Z0 I0 J-1.845
|
||||
G1 X151.064 Z-0.477
|
||||
G3 X151.064 Y66.845 Z-0.5 I0 J1.845
|
||||
G1 X29.82 Z-0.977
|
||||
G3 X29.82 Y63.155 Z-1 I0 J-1.845
|
||||
G1 X151.064 Z-1.477
|
||||
G3 X151.064 Y66.845 Z-1.5 I0 J1.845
|
||||
G1 X29.82 Z-1.977
|
||||
G3 X29.82 Y63.155 Z-2 I0 J-1.845
|
||||
G1 X151.064 Z-2.477
|
||||
G3 X151.064 Y66.845 Z-2.5 I0 J1.845
|
||||
G1 X29.82 Z-2.977
|
||||
G3 X29.82 Y63.155 Z-3 I0 J-1.845
|
||||
G1 X151.064 Z-3.477
|
||||
G3 X151.064 Y66.845 Z-3.5 I0 J1.845
|
||||
G1 X29.82 Z-3.977
|
||||
G3 X29.82 Y63.155 Z-4 I0 J-1.845
|
||||
G1 X151.064 Z-4.477
|
||||
G3 X151.064 Y66.845 Z-4.5 I0 J1.845
|
||||
G1 X29.82 Z-4.977
|
||||
G3 X29.82 Y63.155 Z-5 I0 J-1.845
|
||||
G1 X151.064 Z-5.477
|
||||
G3 X151.064 Y66.845 Z-5.5 I0 J1.845
|
||||
G1 X29.82 Z-5.977
|
||||
G3 X29.82 Y63.155 Z-6 I0 J-1.845
|
||||
G1 X151.064
|
||||
G3 X151.064 Y66.845 I0 J1.845
|
||||
G1 X29.82
|
||||
G3 X29.82 Y63.155 I0 J-1.845
|
||||
G2 X30.059 Y62.996 I0 J-0.258
|
||||
G3 X30.297 Y62.838 I0.238 J0.099
|
||||
G1 X151.064 F90
|
||||
G3 X151.064 Y67.162 I0 J2.162
|
||||
G1 X29.82
|
||||
G3 X29.82 Y62.838 I0 J-2.162
|
||||
G1 X30.297
|
||||
G3 X30.614 Y63.155 I0 J0.317 F120
|
||||
G1 Y63.472
|
||||
G19 G3 Y63.79 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X143.296 Y49.682
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G18 G2 X143.614 Z1 I0.318 K0 F120
|
||||
G1 X143.932
|
||||
G17 G3 X144.249 Y50 I0 J0.318
|
||||
X142.404 Y51.845 Z0.987 I-1.845 J0
|
||||
G1 X38.481 Z0.513
|
||||
G3 X38.481 Y48.155 Z0.487 I0 J-1.845
|
||||
G1 X142.404 Z0.013
|
||||
G3 X142.404 Y51.845 Z-0.013 I0 J1.845
|
||||
G1 X38.481 Z-0.487
|
||||
G3 X38.481 Y48.155 Z-0.513 I0 J-1.845
|
||||
G1 X142.404 Z-0.987
|
||||
G3 X142.404 Y51.845 Z-1.013 I0 J1.845
|
||||
G1 X38.481 Z-1.487
|
||||
G3 X38.481 Y48.155 Z-1.513 I0 J-1.845
|
||||
G1 X142.404 Z-1.987
|
||||
G3 X142.404 Y51.845 Z-2.013 I0 J1.845
|
||||
G1 X38.481 Z-2.487
|
||||
G3 X38.481 Y48.155 Z-2.513 I0 J-1.845
|
||||
G1 X142.404 Z-2.987
|
||||
G3 X142.404 Y51.845 Z-3.013 I0 J1.845
|
||||
G1 X38.481 Z-3.487
|
||||
G3 X38.481 Y48.155 Z-3.513 I0 J-1.845
|
||||
G1 X142.404 Z-3.987
|
||||
G3 X142.404 Y51.845 Z-4.013 I0 J1.845
|
||||
G1 X38.481 Z-4.487
|
||||
G3 X38.481 Y48.155 Z-4.513 I0 J-1.845
|
||||
G1 X142.404 Z-4.987
|
||||
G3 X142.404 Y51.845 Z-5.013 I0 J1.845
|
||||
G1 X38.481 Z-5.487
|
||||
G3 X38.481 Y48.155 Z-5.513 I0 J-1.845
|
||||
G1 X142.404 Z-5.987
|
||||
G3 X144.249 Y50 Z-6 I0 J1.845
|
||||
X142.404 Y51.845 I-1.845 J0
|
||||
G1 X38.481
|
||||
G3 X38.481 Y48.155 I0 J-1.845
|
||||
G1 X142.404
|
||||
G3 X144.249 Y50 I0 J1.845
|
||||
G2 X144.395 Y50.229 I0.253 J0
|
||||
G3 X144.514 Y50.472 I-0.092 J0.196
|
||||
X142.404 Y52.162 I-2.11 J-0.472 F90
|
||||
G1 X38.481
|
||||
G3 X38.481 Y47.838 I0 J-2.162
|
||||
G1 X142.404
|
||||
G3 X144.514 Y50.472 I0 J2.162
|
||||
X144.135 Y50.713 I-0.31 J-0.069 F120
|
||||
G1 X143.825 Y50.644
|
||||
X143.756 Y50.628 Z-5.992
|
||||
X143.691 Y50.613 Z-5.969
|
||||
X143.632 Y50.6 Z-5.931
|
||||
X143.583 Y50.589 Z-5.88
|
||||
X143.546 Y50.581 Z-5.82
|
||||
X143.523 Y50.576 Z-5.753
|
||||
X143.515 Y50.574 Z-5.683
|
||||
Z3
|
||||
X133.426 Y34.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G2 Y33.79 Z1 J-0.317 K0 F120
|
||||
G1 Y33.472
|
||||
G17 G3 X133.744 Y33.155 I0.318 J0
|
||||
X133.744 Y36.845 Z0.969 I0 J1.845
|
||||
G1 X47.141 Z0.5
|
||||
G3 X47.141 Y33.155 Z0.469 I0 J-1.845
|
||||
G1 X133.744 Z0
|
||||
G3 X133.744 Y36.845 Z-0.031 I0 J1.845
|
||||
G1 X47.141 Z-0.5
|
||||
G3 X47.141 Y33.155 Z-0.531 I0 J-1.845
|
||||
G1 X133.744 Z-1
|
||||
G3 X133.744 Y36.845 Z-1.031 I0 J1.845
|
||||
G1 X47.141 Z-1.5
|
||||
G3 X47.141 Y33.155 Z-1.531 I0 J-1.845
|
||||
G1 X133.744 Z-2
|
||||
G3 X133.744 Y36.845 Z-2.031 I0 J1.845
|
||||
G1 X47.141 Z-2.5
|
||||
G3 X47.141 Y33.155 Z-2.531 I0 J-1.845
|
||||
G1 X133.744 Z-3
|
||||
G3 X133.744 Y36.845 Z-3.031 I0 J1.845
|
||||
G1 X47.141 Z-3.5
|
||||
G3 X47.141 Y33.155 Z-3.531 I0 J-1.845
|
||||
G1 X133.744 Z-4
|
||||
G3 X133.744 Y36.845 Z-4.031 I0 J1.845
|
||||
G1 X47.141 Z-4.5
|
||||
G3 X47.141 Y33.155 Z-4.531 I0 J-1.845
|
||||
G1 X133.744 Z-5
|
||||
G3 X133.744 Y36.845 Z-5.031 I0 J1.845
|
||||
G1 X47.141 Z-5.5
|
||||
G3 X47.141 Y33.155 Z-5.531 I0 J-1.845
|
||||
G1 X133.744 Z-6
|
||||
G3 X133.744 Y36.845 I0 J1.845
|
||||
G1 X47.141
|
||||
G3 X47.141 Y33.155 I0 J-1.845
|
||||
G1 X133.744
|
||||
G2 X133.973 Y33.009 I0 J-0.253
|
||||
G3 X134.216 Y32.89 I0.196 J0.092
|
||||
X133.744 Y37.162 I-0.472 J2.11 F90
|
||||
G1 X47.141
|
||||
G3 X47.141 Y32.838 I0 J-2.162
|
||||
G1 X133.744
|
||||
G3 X134.216 Y32.89 I0 J2.162
|
||||
X134.457 Y33.269 I-0.069 J0.31 F120
|
||||
G1 X134.388 Y33.579
|
||||
X134.372 Y33.648 Z-5.992
|
||||
X134.357 Y33.713 Z-5.969
|
||||
X134.344 Y33.772 Z-5.931
|
||||
X134.333 Y33.821 Z-5.88
|
||||
X134.325 Y33.858 Z-5.82
|
||||
X134.32 Y33.881 Z-5.753
|
||||
X134.318 Y33.889 Z-5.683
|
||||
Z3
|
||||
X124.766 Y19.108
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G2 Y18.79 Z1 J-0.317 K0 F120
|
||||
G1 Y18.472
|
||||
G17 G3 X125.083 Y18.155 I0.317 J0
|
||||
X125.083 Y21.845 Z0.961 I0 J1.845
|
||||
G1 X55.801 Z0.5
|
||||
G3 X55.801 Y18.155 Z0.461 I0 J-1.845
|
||||
G1 X125.083 Z0
|
||||
G3 X125.083 Y21.845 Z-0.039 I0 J1.845
|
||||
G1 X55.801 Z-0.5
|
||||
G3 X55.801 Y18.155 Z-0.539 I0 J-1.845
|
||||
G1 X125.083 Z-1
|
||||
G3 X125.083 Y21.845 Z-1.039 I0 J1.845
|
||||
G1 X55.801 Z-1.5
|
||||
G3 X55.801 Y18.155 Z-1.539 I0 J-1.845
|
||||
G1 X125.083 Z-2
|
||||
G3 X125.083 Y21.845 Z-2.039 I0 J1.845
|
||||
G1 X55.801 Z-2.5
|
||||
G3 X55.801 Y18.155 Z-2.539 I0 J-1.845
|
||||
G1 X125.083 Z-3
|
||||
G3 X125.083 Y21.845 Z-3.039 I0 J1.845
|
||||
G1 X55.801 Z-3.5
|
||||
G3 X55.801 Y18.155 Z-3.539 I0 J-1.845
|
||||
G1 X125.083 Z-4
|
||||
G3 X125.083 Y21.845 Z-4.039 I0 J1.845
|
||||
G1 X55.801 Z-4.5
|
||||
G3 X55.801 Y18.155 Z-4.539 I0 J-1.845
|
||||
G1 X125.083 Z-5
|
||||
G3 X125.083 Y21.845 Z-5.039 I0 J1.845
|
||||
G1 X55.801 Z-5.5
|
||||
G3 X55.801 Y18.155 Z-5.539 I0 J-1.845
|
||||
G1 X125.083 Z-6
|
||||
G3 X125.083 Y21.845 I0 J1.845
|
||||
G1 X55.801
|
||||
G3 X55.801 Y18.155 I0 J-1.845
|
||||
G1 X125.083
|
||||
G2 X125.312 Y18.009 I0 J-0.253
|
||||
G3 X125.555 Y17.89 I0.196 J0.092
|
||||
X125.083 Y22.163 I-0.472 J2.11 F90
|
||||
G1 X55.801
|
||||
G3 X55.801 Y17.837 I0 J-2.163
|
||||
G1 X125.083
|
||||
G3 X125.555 Y17.89 I0 J2.163
|
||||
X125.796 Y18.269 I-0.069 J0.31 F120
|
||||
G1 X125.727 Y18.579
|
||||
X125.711 Y18.648 Z-5.992
|
||||
X125.696 Y18.713 Z-5.969
|
||||
X125.683 Y18.772 Z-5.931
|
||||
X125.672 Y18.821 Z-5.88
|
||||
X125.664 Y18.858 Z-5.82
|
||||
X125.659 Y18.881 Z-5.753
|
||||
X125.657 Y18.889 Z-5.683
|
||||
Z5
|
||||
G28 G91 Z0
|
||||
G90
|
||||
G53 G0 X0 Y0
|
||||
M5
|
||||
M30
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.Block.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Block, Serialise, Word}
|
||||
alias Gcode.Model.{Block, Expr, Serialise, Word}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
|
@ -8,9 +8,11 @@ defmodule Gcode.Model.Block.SerialiseTest do
|
|||
assert ok(block) =
|
||||
with(
|
||||
ok(block) <- Block.init(),
|
||||
ok(word) <- Word.init("G", 0),
|
||||
ok(address) <- Expr.Integer.init(0),
|
||||
ok(word) <- Word.init("G", address),
|
||||
ok(block) <- Block.push(block, word),
|
||||
ok(word) <- Word.init("N", 100),
|
||||
ok(address) <- Expr.Integer.init(100),
|
||||
ok(word) <- Word.init("N", address),
|
||||
ok(block) <- Block.push(block, word),
|
||||
do: ok(block)
|
||||
)
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.BlockTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Block, Comment, Word}
|
||||
alias Gcode.Model.{Block, Comment, Expr.Integer, Word}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
doctest Gcode.Model.Block
|
53
test/gcode/model/expr/binary/expr_test.exs
Normal file
53
test/gcode/model/expr/binary/expr_test.exs
Normal file
|
@ -0,0 +1,53 @@
|
|||
defmodule Gcode.Model.Expr.Binary.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Gcode.Result
|
||||
import InfixHelper
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
it_evaluates_to(:*, 2, 3, ok(6))
|
||||
it_evaluates_to(:*, 2.0, 3.0, ok(6.0))
|
||||
it_evaluates_to(:*, 2, 3.0, error({:program_error, _}))
|
||||
it_evaluates_to(:/, 3.0, 2.0, ok(1.5))
|
||||
it_evaluates_to(:/, 2, 3, error({:program_error, _}))
|
||||
it_evaluates_to(:+, 2, 3, ok(5))
|
||||
it_evaluates_to(:+, 2.0, 3.0, ok(5.0))
|
||||
it_evaluates_to(:-, 2, 3, ok(-1))
|
||||
it_evaluates_to(:-, 2.0, 3.0, ok(-1.0))
|
||||
it_evaluates_to(:==, 1, 1, ok(true))
|
||||
it_evaluates_to(:==, 1, 2, ok(false))
|
||||
it_evaluates_to(:==, 1.0, 1.0, ok(true))
|
||||
it_evaluates_to(:==, 1.0, 2.0, ok(false))
|
||||
it_evaluates_to(:==, "a", "a", ok(true))
|
||||
it_evaluates_to(:==, "a", "b", ok(false))
|
||||
it_evaluates_to(:!=, 1, 1, ok(false))
|
||||
it_evaluates_to(:!=, 1, 2, ok(true))
|
||||
it_evaluates_to(:!=, 1.0, 1.0, ok(false))
|
||||
it_evaluates_to(:!=, 1.0, 2.0, ok(true))
|
||||
it_evaluates_to(:!=, "a", "a", ok(false))
|
||||
it_evaluates_to(:!=, "a", "b", ok(true))
|
||||
it_evaluates_to(:<, 1, 2, ok(true))
|
||||
it_evaluates_to(:<, 1, 1, ok(false))
|
||||
it_evaluates_to(:<, 1.0, 2.0, ok(true))
|
||||
it_evaluates_to(:<, 1.0, 1.0, ok(false))
|
||||
it_evaluates_to(:<=, 1, 2, ok(true))
|
||||
it_evaluates_to(:<=, 1, 1, ok(true))
|
||||
it_evaluates_to(:<=, 1.0, 2.0, ok(true))
|
||||
it_evaluates_to(:<=, 1.0, 1.0, ok(true))
|
||||
it_evaluates_to(:>, 1, 2, ok(false))
|
||||
it_evaluates_to(:>, 1, 1, ok(false))
|
||||
it_evaluates_to(:>, 1.0, 2.0, ok(false))
|
||||
it_evaluates_to(:>, 1.0, 1.0, ok(false))
|
||||
it_evaluates_to(:>=, 1, 2, ok(false))
|
||||
it_evaluates_to(:>=, 1, 1, ok(true))
|
||||
it_evaluates_to(:>=, 1.0, 2.0, ok(false))
|
||||
it_evaluates_to(:>=, 1.0, 1.0, ok(true))
|
||||
it_evaluates_to(:&&, true, true, ok(true))
|
||||
it_evaluates_to(:&&, true, false, ok(false))
|
||||
it_evaluates_to(:&&, false, false, ok(false))
|
||||
it_evaluates_to(:||, true, true, ok(true))
|
||||
it_evaluates_to(:||, true, false, ok(true))
|
||||
it_evaluates_to(:||, false, false, ok(false))
|
||||
it_evaluates_to(:^, "a", "b", ok("ab"))
|
||||
end
|
||||
end
|
20
test/gcode/model/expr/binary/serialise_test.exs
Normal file
20
test/gcode/model/expr/binary/serialise_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Gcode.Model.Expr.Binary.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Binary, Expr.Integer, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
for op <- ~w[* / + - == != < <= > >= && || ^]a do
|
||||
quote do
|
||||
test "when the operator is `#{unquote(op)}` it serialises correctly" do
|
||||
ok(lhs) = Integer.init(1)
|
||||
ok(rhs) = Integer.init(2)
|
||||
ok(unary) = Binary.init(unquote(op), lhs, rhs)
|
||||
as_s = to_string(unquote(op))
|
||||
assert ok(["1", as_s, "2"]) = Serialise.serialise(unary)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
17
test/gcode/model/expr/binary_test.exs
Normal file
17
test/gcode/model/expr/binary_test.exs
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Gcode.Model.Expr.BinaryTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.{Binary, Integer}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/3" do
|
||||
test "when the operator and expressions are valid, it is ok" do
|
||||
ok(lhs) = Integer.init(1)
|
||||
ok(rhs) = Integer.init(2)
|
||||
|
||||
assert ok(%Binary{op: some(:-), lhs: some(^lhs), rhs: some(^rhs)}) =
|
||||
Binary.init(:-, lhs, rhs)
|
||||
end
|
||||
end
|
||||
end
|
18
test/gcode/model/expr/boolean/expr_test.exs
Normal file
18
test/gcode/model/expr/boolean/expr_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Gcode.Model.Expr.Boolean.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Boolean}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the value is `true` it evaluates to `true`" do
|
||||
ok(bool) = Boolean.init(true)
|
||||
assert ok(true) = Expr.evaluate(bool)
|
||||
end
|
||||
|
||||
test "when the value is `false` it evaluates to `false`" do
|
||||
ok(bool) = Boolean.init(false)
|
||||
assert ok(false) = Expr.evaluate(bool)
|
||||
end
|
||||
end
|
||||
end
|
18
test/gcode/model/expr/boolean/serialise_test.exs
Normal file
18
test/gcode/model/expr/boolean/serialise_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Gcode.Model.Expr.Boolean.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Boolean, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "when the value is `true` it is serialised to `\"true\"`" do
|
||||
ok(bool) = Boolean.init(true)
|
||||
assert ok(["true"]) = Serialise.serialise(bool)
|
||||
end
|
||||
|
||||
test "when the value is `false` it is serialised to `\"false\"`" do
|
||||
ok(bool) = Boolean.init(false)
|
||||
assert ok(["false"]) = Serialise.serialise(bool)
|
||||
end
|
||||
end
|
||||
end
|
20
test/gcode/model/expr/boolean_test.exs
Normal file
20
test/gcode/model/expr/boolean_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Gcode.Model.Expr.BooleanTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "when the argument is `true` it is ok" do
|
||||
assert ok(%Boolean{}) = Boolean.init(true)
|
||||
end
|
||||
|
||||
test "when the argument is `false` it is ok" do
|
||||
assert ok(%Boolean{}) = Boolean.init(false)
|
||||
end
|
||||
|
||||
test "when passed any other argument, it fails" do
|
||||
assert error({:expression_error, _}) = Boolean.init(nil)
|
||||
end
|
||||
end
|
||||
end
|
29
test/gcode/model/expr/constant/expr_test.exs
Normal file
29
test/gcode/model/expr/constant/expr_test.exs
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule Gcode.Model.Expr.Constant.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Constant}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the constant is `:pi` it returns Pi" do
|
||||
ok(const) = Constant.init(:pi)
|
||||
assert ok(pi) = Expr.evaluate(const)
|
||||
assert_in_delta :math.pi(), pi, 0.1
|
||||
end
|
||||
|
||||
test "when the constant is `line` it returns an error" do
|
||||
ok(const) = Constant.init(:line)
|
||||
assert error(_) = Expr.evaluate(const)
|
||||
end
|
||||
|
||||
test "when the constant is `null` it returns `nil`" do
|
||||
ok(const) = Constant.init(:null)
|
||||
assert ok(nil) = Expr.evaluate(const)
|
||||
end
|
||||
|
||||
test "when the constant is `result` it returns an error" do
|
||||
ok(const) = Constant.init(:result)
|
||||
assert error(_) = Expr.evaluate(const)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/constant/serialise_test.exs
Normal file
13
test/gcode/model/expr/constant/serialise_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Constant.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Constant, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it serialises correctly" do
|
||||
ok(const) = Constant.init(:pi)
|
||||
assert ok(["pi"]) = Serialise.serialise(const)
|
||||
end
|
||||
end
|
||||
end
|
20
test/gcode/model/expr/constant_test.exs
Normal file
20
test/gcode/model/expr/constant_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Gcode.Model.Expr.ConstantTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Constant
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
for name <- ~w[iterations line null pi result]a do
|
||||
quote do
|
||||
test "when the argument is `#{unquote(name)}`, it is ok" do
|
||||
assert ok(%Constant{name: unquote(name)}) = Constant.init(unquote(name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "otherwise, it is an error" do
|
||||
assert error(_) = Constant.init(:wat)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/float/expr_test.exs
Normal file
13
test/gcode/model/expr/float/expr_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Float.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Float}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the value is is a float it evaluates to it" do
|
||||
ok(float) = Float.init(1.23)
|
||||
assert ok(1.23) = Expr.evaluate(float)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/float/serialise_test.exs
Normal file
13
test/gcode/model/expr/float/serialise_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Float.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Float, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it serialises correctly" do
|
||||
ok(float) = Float.init(1.23)
|
||||
assert ok(["1.23"]) = Serialise.serialise(float)
|
||||
end
|
||||
end
|
||||
end
|
16
test/gcode/model/expr/float_test.exs
Normal file
16
test/gcode/model/expr/float_test.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Gcode.Model.Expr.FloatTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "when the value is a float, it is ok" do
|
||||
assert ok(%Float{}) = Float.init(1.23)
|
||||
end
|
||||
|
||||
test "when the value is not a float, it is an error" do
|
||||
assert error({:expression_error, _}) = Float.init(123)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/integer/expr_test.exs
Normal file
13
test/gcode/model/expr/integer/expr_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Integer.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Integer}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the value is is an integer it evaluates to it" do
|
||||
ok(integer) = Integer.init(123)
|
||||
assert ok(123) = Expr.evaluate(integer)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/integer/serialise_test.exs
Normal file
13
test/gcode/model/expr/integer/serialise_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Integer.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Integer, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it serialises correctly" do
|
||||
ok(float) = Integer.init(123)
|
||||
assert ok(["123"]) = Serialise.serialise(float)
|
||||
end
|
||||
end
|
||||
end
|
16
test/gcode/model/expr/integer_test.exs
Normal file
16
test/gcode/model/expr/integer_test.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Gcode.Model.Expr.IntegerTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "when the value is an integer, it is ok" do
|
||||
assert ok(%Integer{}) = Integer.init(123)
|
||||
end
|
||||
|
||||
test "when the value is not an integer, it is an error" do
|
||||
assert error({:expression_error, _}) = Integer.init(1.23)
|
||||
end
|
||||
end
|
||||
end
|
14
test/gcode/model/expr/list/expr.ex
Normal file
14
test/gcode/model/expr/list/expr.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.List do
|
||||
alias Gcode.Model.{Expr, Expr.List}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@spec evaluate(List.t()) :: Expr.result()
|
||||
def evaluate(%List{elements: elements}) do
|
||||
elements =
|
||||
elements
|
||||
|> Enum.map(&Expr.evaluate/1)
|
||||
|
||||
ok(elements)
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/list/serialise.ex
Normal file
13
test/gcode/model/expr/list/serialise.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.List do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.List, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it cannot be serialised" do
|
||||
ok(list) = List.init()
|
||||
assert error(_) = Serialise.serialise(list)
|
||||
end
|
||||
end
|
||||
end
|
25
test/gcode/model/expr/list_test.exs
Normal file
25
test/gcode/model/expr/list_test.exs
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule Gcode.Model.Expr.ListTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.{Integer, List}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "it is ok" do
|
||||
assert ok(%List{}) = List.init()
|
||||
end
|
||||
end
|
||||
|
||||
describe "push/2" do
|
||||
test "when the element is an expression, it is ok" do
|
||||
ok(list) = List.init()
|
||||
ok(expr) = Integer.init(123)
|
||||
assert ok(%List{elements: [^expr]}) = List.push(list, expr)
|
||||
end
|
||||
|
||||
test "otherwise it's an error" do
|
||||
ok(list) = List.init()
|
||||
assert error({:expression_error, _}) = List.push(list, :marty)
|
||||
end
|
||||
end
|
||||
end
|
63
test/gcode/model/expr/unary/expr_test.exs
Normal file
63
test/gcode/model/expr/unary/expr_test.exs
Normal file
|
@ -0,0 +1,63 @@
|
|||
defmodule Gcode.Model.Expr.Unary.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Gcode.Model.{
|
||||
Expr,
|
||||
Expr.Boolean,
|
||||
Expr.Float,
|
||||
Expr.Integer,
|
||||
Expr.List,
|
||||
Expr.String,
|
||||
Expr.Unary
|
||||
}
|
||||
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the operator is `!` and the inner value evaluates to a boolean, it returns it's inverse" do
|
||||
ok(inner) = Boolean.init(true)
|
||||
ok(unary) = Unary.init(:!, inner)
|
||||
assert ok(false) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `!` and the inner value evaluates to a non-boolean, it returns an error" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(unary) = Unary.init(:!, inner)
|
||||
assert error({:program_error, _}) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `+` the inner value is an integer, it evaluates it" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(inner) = Unary.init(:-, inner)
|
||||
ok(unary) = Unary.init(:+, inner)
|
||||
assert ok(-123) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `+` the inner value is an float, it evaluates it" do
|
||||
ok(inner) = Float.init(1.23)
|
||||
ok(unary) = Unary.init(:+, inner)
|
||||
assert ok(1.23) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `#` the inner value evaluates to a list, it returns it's length" do
|
||||
ok(list) = List.init()
|
||||
ok(int) = Integer.init(123)
|
||||
ok(inner) = List.push(list, int)
|
||||
ok(unary) = Unary.init(:"#", inner)
|
||||
assert ok(1) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `#` the inner value evaluates to a string, it returns it's length" do
|
||||
ok(inner) = String.init("Marty McFly")
|
||||
ok(unary) = Unary.init(:"#", inner)
|
||||
assert ok(11) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `#`, otherwise it returns an error" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(unary) = Unary.init(:"#", inner)
|
||||
assert error({:program_error, _}) = Expr.evaluate(unary)
|
||||
end
|
||||
end
|
||||
end
|
19
test/gcode/model/expr/unary/serialise_test.exs
Normal file
19
test/gcode/model/expr/unary/serialise_test.exs
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Gcode.Model.Expr.Unary.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Integer, Expr.Unary, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
for op <- ~w[! + - #]a do
|
||||
quote do
|
||||
test "when the operator is `#{unquote(op)}` it serialises correctly" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(unary) = Unary.init(unquote(op), inner)
|
||||
as_s = to_string(unquote(op))
|
||||
assert ok([as_s, "123"]) = Serialise.serialise(unary)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
27
test/gcode/model/expr/unary_test.exs
Normal file
27
test/gcode/model/expr/unary_test.exs
Normal file
|
@ -0,0 +1,27 @@
|
|||
defmodule Gcode.Model.Expr.UnaryTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.{Integer, Unary}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/2" do
|
||||
test "when the operator is valid and the inner value is an expression, it is ok" do
|
||||
ok(inner) = Integer.init(123)
|
||||
assert ok(%Unary{op: some(:-), expr: some(^inner)}) = Unary.init(:-, inner)
|
||||
end
|
||||
|
||||
test "when the operator is valid and the inner value is not an expresion, it is an error" do
|
||||
assert error({:expression_error, _}) = Unary.init(:-, 1.21)
|
||||
end
|
||||
|
||||
test "when the inner value is an expression but the operator is invalid, it an error" do
|
||||
ok(inner) = Integer.init(123)
|
||||
assert error({:expression_error, _}) = Unary.init(:%, inner)
|
||||
end
|
||||
|
||||
test "when both the operator and inner value are invalid, it is an error" do
|
||||
assert error({:expression_error, _}) = Unary.init(:%, 1.21)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.WordTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Expr.Integer, Word}
|
||||
doctest Gcode.Model.Word
|
||||
@moduledoc false
|
||||
end
|
57
test/gcode/parser/engine_test.exs
Normal file
57
test/gcode/parser/engine_test.exs
Normal file
|
@ -0,0 +1,57 @@
|
|||
defmodule Gcode.Parser.EngineTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Gcode.Result
|
||||
use ParserEngineHelper
|
||||
import FixtureHelper
|
||||
@moduledoc false
|
||||
|
||||
it_parses_into("%", tape: '')
|
||||
it_parses_into("% hello", tape: 'hello')
|
||||
it_parses_into("()", comment: '')
|
||||
it_parses_into("(hello)", comment: 'hello')
|
||||
it_parses_into("( hello )", comment: 'hello')
|
||||
it_parses_into("; hello", comment: 'hello')
|
||||
it_parses_into("G0", block: [word: [command: 'G', address: [integer: '0']]])
|
||||
it_parses_into("G 0", block: [word: [command: 'G', address: [integer: '0']]])
|
||||
it_parses_into("G54.1", block: [word: [command: 'G', address: [float: '54.1']]])
|
||||
it_parses_into("G 54.1", block: [word: [command: 'G', address: [float: '54.1']]])
|
||||
it_parses_into("G-1", block: [word: [command: 'G', address: [{:-, [integer: '1']}]]])
|
||||
it_parses_into("G+1", block: [word: [command: 'G', address: [{:+, [integer: '1']}]]])
|
||||
it_parses_into("G!1", block: [word: [command: 'G', address: [{:!, [integer: '1']}]]])
|
||||
|
||||
it_parses_into("G1 X112.518 Y131.525 E59.51636 (hello)",
|
||||
block: [
|
||||
word: [command: 'G', address: [integer: '1']],
|
||||
word: [command: 'X', address: [float: '112.518']],
|
||||
word: [command: 'Y', address: [float: '131.525']],
|
||||
word: [command: 'E', address: [float: '59.51636']],
|
||||
comment: 'hello'
|
||||
]
|
||||
)
|
||||
|
||||
it_parses_into("M82 ;absolute extrusion mode",
|
||||
block: [word: [command: 'M', address: [integer: '82']], comment: 'absolute extrusion mode']
|
||||
)
|
||||
|
||||
it_parses_into("M117 Hello world",
|
||||
block: [word: [command: 'M', address: [integer: '117']], string: 'Hello world']
|
||||
)
|
||||
|
||||
it_parses_into(read_fixture("fusion_360_milling_grbl.nc"), fn tokens ->
|
||||
lines =
|
||||
tokens
|
||||
|> Enum.reject(&(elem(&1, 0) == :newline))
|
||||
|> Enum.count()
|
||||
|
||||
assert lines == 500
|
||||
end)
|
||||
|
||||
it_parses_into(read_fixture("cura_marlin.gcode"), fn tokens ->
|
||||
lines =
|
||||
tokens
|
||||
|> Enum.reject(&(elem(&1, 0) == :newline))
|
||||
|> Enum.count()
|
||||
|
||||
assert lines == 6723
|
||||
end)
|
||||
end
|
133
test/gcode/parser_test.exs
Normal file
133
test/gcode/parser_test.exs
Normal file
|
@ -0,0 +1,133 @@
|
|||
defmodule Gcode.ParserTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Parser
|
||||
alias Gcode.Model.{Block, Comment, Expr, Program, Word}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
import FixtureHelper
|
||||
@moduledoc false
|
||||
|
||||
describe "parse_string/1" do
|
||||
test "( hello )" do
|
||||
assert ok(program) = Parser.parse_string("( hello )")
|
||||
assert %Program{elements: [%Comment{comment: "hello"}]} = program
|
||||
end
|
||||
|
||||
test "G29" do
|
||||
assert ok(program) = Parser.parse_string("G29")
|
||||
|
||||
assert %Program{
|
||||
elements: [%Block{words: [%Word{word: "G", address: %Expr.Integer{i: 29}}]}]
|
||||
} = program
|
||||
end
|
||||
|
||||
test "G54.1" do
|
||||
assert ok(program) = Parser.parse_string("G54.1")
|
||||
|
||||
assert %Program{
|
||||
elements: [%Block{words: [%Word{word: "G", address: %Expr.Float{f: 54.1}}]}]
|
||||
} = program
|
||||
end
|
||||
|
||||
test "X-12" do
|
||||
assert ok(program) = Parser.parse_string("X-12")
|
||||
|
||||
assert %Program{
|
||||
elements: [
|
||||
%Block{
|
||||
words: [
|
||||
%Word{
|
||||
word: "X",
|
||||
address: %Expr.Unary{
|
||||
op: some(:-),
|
||||
expr: some(%Expr.Integer{i: 12})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} = program
|
||||
end
|
||||
|
||||
test "M117 Marty McFly" do
|
||||
assert ok(program) = Parser.parse_string("M117 Marty McFly")
|
||||
|
||||
assert %Program{
|
||||
elements: [
|
||||
%Block{
|
||||
words: [
|
||||
%Expr.String{value: "Marty McFly"},
|
||||
%Word{word: "M", address: %Expr.Integer{i: 117}}
|
||||
]
|
||||
}
|
||||
]
|
||||
} = program
|
||||
end
|
||||
|
||||
test "it can parse `fusion_360_milling_grbl.nc`" do
|
||||
input = read_fixture("fusion_360_milling_grbl.nc")
|
||||
assert ok(%Program{}) = Parser.parse_string(input)
|
||||
end
|
||||
|
||||
test "it can parse `cura_marlin.gcode`" do
|
||||
input = read_fixture("cura_marlin.gcode")
|
||||
assert ok(%Program{}) = Parser.parse_string(input)
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_file/1" do
|
||||
test "it can parse `fusion_360_milling_grbl.nc`" do
|
||||
assert ok(%Program{elements: elements}) =
|
||||
Parser.parse_file(fixture_path("fusion_360_milling_grbl.nc"))
|
||||
|
||||
assert 500 = length(elements)
|
||||
end
|
||||
|
||||
test "it can parse `cura_marlin.gcode`" do
|
||||
assert ok(%Program{elements: elements}) =
|
||||
Parser.parse_file(fixture_path("cura_marlin.gcode"))
|
||||
|
||||
assert 6723 = length(elements)
|
||||
end
|
||||
end
|
||||
|
||||
describe "stream_string!/1" do
|
||||
test "it can stream `fusion_360_milling_grbl.nc`" do
|
||||
elements =
|
||||
read_fixture("fusion_360_milling_grbl.nc")
|
||||
|> Parser.stream_string!()
|
||||
|> Enum.to_list()
|
||||
|
||||
assert 500 = length(elements)
|
||||
end
|
||||
|
||||
test "it can stream `cura_marlin.gcode`" do
|
||||
elements =
|
||||
read_fixture("cura_marlin.gcode")
|
||||
|> Parser.stream_string!()
|
||||
|> Enum.to_list()
|
||||
|
||||
assert 6723 = length(elements)
|
||||
end
|
||||
end
|
||||
|
||||
describe "stream_file!/1" do
|
||||
test "it can stream `fusion_360_milling_grbl.nc`" do
|
||||
elements =
|
||||
fixture_path("fusion_360_milling_grbl.nc")
|
||||
|> Parser.stream_file!()
|
||||
|> Enum.to_list()
|
||||
|
||||
assert 500 = length(elements)
|
||||
end
|
||||
|
||||
test "it can stream `cura_marlin.gcode`" do
|
||||
elements =
|
||||
fixture_path("cura_marlin.gcode")
|
||||
|> Parser.stream_file!()
|
||||
|> Enum.to_list()
|
||||
|
||||
assert 6723 = length(elements)
|
||||
end
|
||||
end
|
||||
end
|
17
test/support/fixture_helper.ex
Normal file
17
test/support/fixture_helper.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule FixtureHelper do
|
||||
@moduledoc false
|
||||
|
||||
def read_fixture(name) do
|
||||
name
|
||||
|> fixture_path()
|
||||
|> File.read!()
|
||||
end
|
||||
|
||||
def fixture_path(name),
|
||||
do:
|
||||
:gcode
|
||||
|> :code.priv_dir()
|
||||
|> List.to_string()
|
||||
|> Path.join("fixtures")
|
||||
|> Path.join(name)
|
||||
end
|
28
test/support/infix_helper.ex
Normal file
28
test/support/infix_helper.ex
Normal file
|
@ -0,0 +1,28 @@
|
|||
defmodule InfixHelper do
|
||||
alias Gcode.Model.{Expr, Expr.Binary}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@type value :: boolean | integer | float | String.t()
|
||||
|
||||
@spec cast_expression(value) :: Result.t(Expr.t())
|
||||
def cast_expression(value) when is_integer(value), do: Expr.Integer.init(value)
|
||||
def cast_expression(value) when is_float(value), do: Expr.Float.init(value)
|
||||
def cast_expression(value) when is_boolean(value), do: Expr.Boolean.init(value)
|
||||
def cast_expression(value) when is_binary(value), do: Expr.String.init(value)
|
||||
|
||||
@spec it_evaluates_to(atom, value, value, any) :: Macro.t()
|
||||
defmacro it_evaluates_to(op, lhs, rhs, result) do
|
||||
quote do
|
||||
test "when the operator is `#{unquote(op)}` and the lhs is `#{inspect(unquote(lhs))}` and the rhs is `#{
|
||||
inspect(unquote(rhs))
|
||||
}` it is correct" do
|
||||
ok(lhs) = InfixHelper.cast_expression(unquote(lhs))
|
||||
ok(rhs) = InfixHelper.cast_expression(unquote(rhs))
|
||||
ok(bin) = Binary.init(unquote(op), lhs, rhs)
|
||||
|
||||
assert unquote(result) = Expr.evaluate(bin)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
43
test/support/parser_engine_helper.ex
Normal file
43
test/support/parser_engine_helper.ex
Normal file
|
@ -0,0 +1,43 @@
|
|||
defmodule ParserEngineHelper do
|
||||
alias Gcode.Parser.Engine
|
||||
@moduledoc false
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
require ParserEngineHelper
|
||||
import ParserEngineHelper
|
||||
end
|
||||
end
|
||||
|
||||
defmacro it_parses_into(input, {:fn, _, _} = callback) do
|
||||
quote do
|
||||
input_length = byte_size(unquote(input))
|
||||
|
||||
description =
|
||||
if input_length < 60,
|
||||
do: unquote(input),
|
||||
else: "#{input_length} byte input"
|
||||
|
||||
test description do
|
||||
assert ok(tokens) = Engine.parse(unquote(input))
|
||||
unquote(callback).(tokens)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro it_parses_into(input, pattern) do
|
||||
quote do
|
||||
input_length = byte_size(unquote(input))
|
||||
|
||||
description =
|
||||
if input_length < 60,
|
||||
do: unquote(input),
|
||||
else: "#{input_length} byte input"
|
||||
|
||||
test description do
|
||||
assert ok(tokens) = Engine.parse(unquote(input))
|
||||
assert unquote(pattern) = tokens
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue