defmodule Mix.Tasks.Diogramos.AndroidFixtures do
@moduledoc """
Regenerates the golden fixtures the Android `ConnectorGeometry` test
asserts against.
mix diogramos.android_fixtures
`ConnectorGeometry` exists twice — here in Elixir for the web, and again
in Kotlin for the phone — because connector routing has to happen
locally to stay smooth under a finger. Two implementations of the same
maths drift silently: nothing errors, diagrams just render subtly wrong
on one of them.
So the Elixir module is the authority, and this task dumps its output
for a spread of documents into a JSON file the Kotlin unit test reads.
Change the routing rules on either side and that test fails until both
agree. Re-run this task whenever the Elixir behaviour changes on
purpose, and commit the result.
"""
use Mix.Task
alias Diogramos.Diagrams.ConnectorGeometry
@shortdoc "Regenerates android/app/src/test/resources/connector_geometry.json"
@output "android/app/src/test/resources/connector_geometry.json"
@impl Mix.Task
def run(_args) do
documents = documents()
cases = Enum.flat_map(documents, &cases_for/1)
# Documents are referenced by name rather than inlined per case: the
# matrix is a few hundred cases over a handful of documents, and
# inlining makes the file ten times bigger for no added coverage.
payload = %{
"generated_by" => "mix diogramos.android_fixtures",
"documents" => Map.new(documents),
"cases" => cases
}
File.mkdir_p!(Path.dirname(@output))
File.write!(@output, Jason.encode!(payload, pretty: true) <> "\n")
Mix.shell().info("wrote #{length(cases)} cases to #{@output}")
end
## Documents ------------------------------------------------------------
defp documents do
[
{"rects-apart", doc_rects()},
{"circle-and-rect", doc_circle()},
{"fractional", doc_fractional()},
{"overlapping", doc_overlapping()}
]
end
defp doc_rects do
elements(%{
"a" => %{"id" => "a", "type" => "rect", "x" => 0, "y" => 0, "w" => 100, "h" => 60},
"b" => %{"id" => "b", "type" => "rect", "x" => 300, "y" => 200, "w" => 120, "h" => 80}
})
end
defp doc_circle do
elements(%{
"a" => %{"id" => "a", "type" => "circle", "cx" => 50, "cy" => 50, "r" => 40},
"b" => %{"id" => "b", "type" => "rounded", "x" => -250, "y" => -120, "w" => 90, "h" => 50}
})
end
# Exercises the number formatter: halves, thirds and values that round
# at the second decimal all reach the path string.
defp doc_fractional do
elements(%{
"a" => %{"id" => "a", "type" => "text", "x" => 1.5, "y" => 2.25, "w" => 33, "h" => 17},
"b" => %{"id" => "b", "type" => "rect", "x" => 101.333, "y" => 0.5, "w" => 45.5, "h" => 21}
})
end
defp doc_overlapping do
elements(%{
"a" => %{"id" => "a", "type" => "rect", "x" => 0, "y" => 0, "w" => 100, "h" => 100},
"b" => %{"id" => "b", "type" => "rect", "x" => 20, "y" => 20, "w" => 100, "h" => 100}
})
end
defp elements(map), do: %{"elements" => map, "order" => Map.keys(map), "connectors" => %{}}
## Connector matrix -----------------------------------------------------
@routings ~w(straight orthogonal curve)
@anchors ~w(auto n e s w)
defp cases_for({doc_name, document}) do
anchor_pairs =
for from <- @anchors, to <- @anchors, do: {%{"anchor" => from}, %{"anchor" => to}}
point_pairs = [
{%{"anchor" => "point", "x" => 12.5, "y" => -3}, %{"anchor" => "e"}},
{%{"anchor" => "e"}, %{"anchor" => "point", "x" => 400, "y" => 333.75}}
]
base =
for routing <- @routings,
{from_anchor, to_anchor} <- anchor_pairs ++ point_pairs do
build_case(doc_name, document, routing, from_anchor, to_anchor, %{})
end
# Styling doesn't change the path, but it does change everything else
# in the payload, and the marker/dash defaults are easy to get wrong.
styled =
for overrides <- [
%{"start_gap" => 0, "end_gap" => 0},
%{"start_gap" => 12, "end_gap" => 3.5},
%{"dash" => "dotted"},
%{"dash" => "dashed"},
%{"dash" => "dash-dot"},
%{"stroke_width" => 0.5},
%{"marker_start" => "diamond", "marker_end" => "none"},
%{"marker_start" => "circle", "marker_end" => "filled-arrow"},
# Every key absent: the defaults are part of the contract.
:bare
] do
build_case(
doc_name,
document,
"straight",
%{"anchor" => "e"},
%{"anchor" => "w"},
overrides
)
end
Enum.reject(base ++ styled, &is_nil/1)
end
defp build_case(doc_name, document, routing, from_anchor, to_anchor, overrides) do
connector =
case overrides do
:bare ->
%{
"id" => "k",
"from" => Map.put(from_anchor, "element", "a"),
"to" => Map.put(to_anchor, "element", "b")
}
map ->
Map.merge(
%{
"id" => "k",
"from" => Map.put(from_anchor, "element", "a"),
"to" => Map.put(to_anchor, "element", "b"),
"routing" => routing,
"dash" => "solid",
"marker_start" => "none",
"marker_end" => "arrow",
"start_gap" => 6,
"end_gap" => 6,
"stroke_width" => 2,
"color" => "base-content"
},
map
)
end
case ConnectorGeometry.render(connector, document) do
nil ->
nil
rendered ->
%{
"name" => "#{doc_name}/#{routing}/#{from_anchor["anchor"]}->#{to_anchor["anchor"]}",
"document" => doc_name,
"connector" => connector,
"expected" => %{
"d" => rendered.d,
"stroke_width" => rendered.stroke_width,
"dash_array" => rendered.dash_array,
"marker_start" => rendered.marker_start,
"marker_end" => rendered.marker_end
}
}
end
end
end
neiam
/diogramos
diogramos
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
6.0 KiB · text
History
99f6896