This commit is contained in:
commit
40cfdf9bc6
12 changed files with 1293 additions and 0 deletions
23
.github/workflows/test.yml
vendored
Normal file
23
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: erlef/setup-beam@v1
|
||||||
|
with:
|
||||||
|
otp-version: "27.1.2"
|
||||||
|
gleam-version: "1.11.1"
|
||||||
|
rebar3-version: "3"
|
||||||
|
# elixir-version: "1"
|
||||||
|
- run: gleam deps download
|
||||||
|
- run: gleam test
|
||||||
|
- run: gleam format --check src test
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
/build
|
||||||
|
erl_crash.dump
|
||||||
|
**/*.svg
|
9
LICENSE.md
Normal file
9
LICENSE.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2025 Lily Rose
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# substrate
|
||||||
|
|
||||||
|
[](https://hex.pm/packages/substrate)
|
||||||
|
[](https://hexdocs.pm/substrate/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add substrate@1
|
||||||
|
```
|
||||||
|
```gleam
|
||||||
|
import substrate
|
||||||
|
|
||||||
|
pub fn main() -> Nil {
|
||||||
|
// TODO: An example of the project in use
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Further documentation can be found at <https://hexdocs.pm/substrate>.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam run # Run the project
|
||||||
|
gleam test # Run the tests
|
||||||
|
```
|
14
gleam.toml
Normal file
14
gleam.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name = "substrate"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Procedurally generate SVG wallpapers from Kicad footprints"
|
||||||
|
licences = ["MIT"]
|
||||||
|
repository = { type = "forgejo", host = "git.7cs.dev", user = "lily", repo = "substrate" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
||||||
|
simplifile = ">= 2.3.0 and < 3.0.0"
|
||||||
|
lustre = ">= 5.2.1 and < 6.0.0"
|
||||||
|
kicad_sexpr = ">= 1.0.1 and < 2.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = ">= 1.0.0 and < 2.0.0"
|
22
manifest.toml
Normal file
22
manifest.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
|
||||||
|
{ name = "gleam_erlang", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F91CE62A2D011FA13341F3723DB7DB118541AAA5FE7311BD2716D018F01EF9E3" },
|
||||||
|
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
|
||||||
|
{ name = "gleam_otp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7020E652D18F9ABAC9C877270B14160519FA0856EE80126231C505D719AD68DA" },
|
||||||
|
{ name = "gleam_stdlib", version = "0.62.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "DC8872BC0B8550F6E22F0F698CFE7F1E4BDA7312FDEB40D6C3F44C5B706C8310" },
|
||||||
|
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
|
||||||
|
{ name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
|
||||||
|
{ name = "kicad_sexpr", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "kicad_sexpr", source = "hex", outer_checksum = "8ACA8737C6B3517BAA9E8A8278C5B88379AE6EFED28C74F30FB9E7A19622CCF9" },
|
||||||
|
{ name = "lustre", version = "5.2.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "DCD121F8E6B7E179B27D9A8AEB6C828D8380E26DF2E16D078511EDAD1CA9F2A7" },
|
||||||
|
{ name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
|
||||||
|
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||||
|
kicad_sexpr = { version = ">= 1.0.1 and < 2.0.0" }
|
||||||
|
lustre = { version = ">= 5.2.1 and < 6.0.0" }
|
||||||
|
simplifile = { version = ">= 2.3.0 and < 3.0.0" }
|
666
src/substrate.gleam
Normal file
666
src/substrate.gleam
Normal file
|
@ -0,0 +1,666 @@
|
||||||
|
import gleam/float
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option.{None, Some}
|
||||||
|
import gleam/order
|
||||||
|
import kicad_sexpr
|
||||||
|
import kicad_sexpr/token.{type Footprint, type Pad}
|
||||||
|
import lustre/attribute.{style} as _attribute
|
||||||
|
import lustre/element.{type Element, fragment, to_readable_string} as _element
|
||||||
|
import lustre/element/html.{svg}
|
||||||
|
import simplifile
|
||||||
|
import svg_attribute as attribute
|
||||||
|
import svg_element as element
|
||||||
|
import svg_path as path
|
||||||
|
import svg_transform as transform
|
||||||
|
|
||||||
|
pub fn get_footprint(
|
||||||
|
library_location: String,
|
||||||
|
library: String,
|
||||||
|
name: String,
|
||||||
|
) -> Footprint {
|
||||||
|
let path =
|
||||||
|
library_location
|
||||||
|
<> "/footprints/"
|
||||||
|
<> library
|
||||||
|
<> ".pretty/"
|
||||||
|
<> name
|
||||||
|
<> ".kicad_mod"
|
||||||
|
let assert Ok(data) = path |> simplifile.read_bits
|
||||||
|
let assert Ok(footprint_file) = kicad_sexpr.parse_footprint_file(data)
|
||||||
|
footprint_file.footprint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_local_footprint(library: String, name: String) -> Footprint {
|
||||||
|
get_footprint("/home/lily/.local/share/kicad/9.0", library, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_local_footprints(
|
||||||
|
library: String,
|
||||||
|
names: List(String),
|
||||||
|
) -> List(Footprint) {
|
||||||
|
names |> list.map(get_local_footprint(library, _))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_global_footprint(library: String, name: String) -> Footprint {
|
||||||
|
get_footprint("/usr/share/kicad", library, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_global_footprints(
|
||||||
|
library: String,
|
||||||
|
names: List(String),
|
||||||
|
) -> List(Footprint) {
|
||||||
|
names |> list.map(get_global_footprint(library, _))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> Nil {
|
||||||
|
// simplifile.get_files("/usr/share/kicad/footprints/")
|
||||||
|
// |> result.unwrap([])
|
||||||
|
// |> list.filter_map(fn(file_name) {
|
||||||
|
// use data <- result.try(
|
||||||
|
// simplifile.read_bits(file_name) |> result.replace_error(Nil),
|
||||||
|
// )
|
||||||
|
// use footprint_file <- result.try(
|
||||||
|
// kicad_sexpr.parse_footprint_file(data) |> result.replace_error(Nil),
|
||||||
|
// )
|
||||||
|
// case
|
||||||
|
// footprint_file.footprint.pads
|
||||||
|
// |> list.any(fn(pad) {
|
||||||
|
// case pad.chamfer {
|
||||||
|
// option.Some([_, ..]) -> True
|
||||||
|
// _ -> False
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// {
|
||||||
|
// True -> Ok(file_name)
|
||||||
|
// False -> Error(Nil)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// |> string.join("\n")
|
||||||
|
// |> io.println
|
||||||
|
// let capacitors =
|
||||||
|
// get_global_footprints("Capacitor_SMD", [
|
||||||
|
// "C_0603_1608Metric", "C_0805_2012Metric", "C_1210_3225Metric",
|
||||||
|
// ])
|
||||||
|
// let resistors =
|
||||||
|
// get_global_footprints("Resistor_SMD", [
|
||||||
|
// "R_0603_1608Metric", "R_0805_2012Metric", "R_1210_3225Metric",
|
||||||
|
// ])
|
||||||
|
// let sots =
|
||||||
|
// get_global_footprints("Package_TO_SOT_SMD", [
|
||||||
|
// "SOT-23", "SOT-23-5", "SOT-23-6", "SOT-23-8",
|
||||||
|
// ])
|
||||||
|
// let tos =
|
||||||
|
// get_global_footprints("Package_TO_SOT_SMD", ["TO-252-2", "TO-252-3_TabPin2"])
|
||||||
|
// let ssops =
|
||||||
|
// get_global_footprints("Package_SO", [
|
||||||
|
// "SSOP-20_4.4x6.5mm_P0.65mm", "SSOP-8_3.9x5.05mm_P1.27mm",
|
||||||
|
// "SSOP-10-1EP_3.9x4.9mm_P1mm_EP2.1x3.3mm",
|
||||||
|
// ])
|
||||||
|
// let tqfps =
|
||||||
|
// get_global_footprints("Package_QFP", [
|
||||||
|
// "TQFP-32_7x7mm_P0.8mm", "TQFP-44_10x10mm_P0.8mm", "TQFP-64_10x10mm_P0.5mm",
|
||||||
|
// ])
|
||||||
|
// let dfns =
|
||||||
|
// get_global_footprints("Package_DFN_QFN", [
|
||||||
|
// "DFN-6-1EP_3x2mm_P0.5mm_EP1.65x1.35mm", "DFN-8_2x2mm_P0.5mm",
|
||||||
|
// "DFN-14-1EP_3x4mm_P0.5mm_EP1.7x3.3mm",
|
||||||
|
// ])
|
||||||
|
// let qfns =
|
||||||
|
// get_global_footprints("Package_DFN_QFN", [
|
||||||
|
// "QFN-12-1EP_3x3mm_P0.51mm_EP1.45x1.45mm",
|
||||||
|
// "QFN-16-1EP_4x4mm_P0.65mm_EP2.5x2.5mm",
|
||||||
|
// "QFN-24-1EP_4x4mm_P0.5mm_EP2.65x2.65mm",
|
||||||
|
// "QFN-32-1EP_5x5mm_P0.5mm_EP3.6x3.6mm",
|
||||||
|
// ])
|
||||||
|
let test_footprint = get_local_footprint("Testing", "Testing1")
|
||||||
|
let svg = create_svg(test_footprint)
|
||||||
|
let assert Ok(_) = simplifile.write("out.svg", svg)
|
||||||
|
Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const bg_col = "#001021"
|
||||||
|
|
||||||
|
fn create_svg(footprint: Footprint) -> String {
|
||||||
|
let fill = None
|
||||||
|
let stroke = None
|
||||||
|
let stroke_width = None
|
||||||
|
let transform = None
|
||||||
|
svg([attribute.view_box(-4.0, -4.0, 8.0, 8.0), style("background", bg_col)], [
|
||||||
|
element.group(
|
||||||
|
fill:,
|
||||||
|
stroke: Some("#BBBBBB"),
|
||||||
|
stroke_width: Some(0.01),
|
||||||
|
transform:,
|
||||||
|
children: [
|
||||||
|
element.line(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x1: -4.0,
|
||||||
|
y1: 0.0,
|
||||||
|
x2: 4.0,
|
||||||
|
y2: 0.0,
|
||||||
|
),
|
||||||
|
element.line(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x1: 0.0,
|
||||||
|
y1: -4.0,
|
||||||
|
x2: 0.0,
|
||||||
|
y2: 4.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
element.group(
|
||||||
|
fill:,
|
||||||
|
stroke: Some("#FF00FF"),
|
||||||
|
stroke_width: Some(0.01),
|
||||||
|
transform:,
|
||||||
|
children: [
|
||||||
|
element.line(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x1: -0.05,
|
||||||
|
y1: 0.0,
|
||||||
|
x2: 0.05,
|
||||||
|
y2: 0.0,
|
||||||
|
),
|
||||||
|
element.line(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x1: 0.0,
|
||||||
|
y1: -0.05,
|
||||||
|
x2: 0.0,
|
||||||
|
y2: 0.05,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
element.group(
|
||||||
|
fill: Some("#CE3431"),
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
children: footprint.pads
|
||||||
|
|> list.filter(fn(pad) {
|
||||||
|
pad.layers |> list.contains(token.Layer("F.Cu"))
|
||||||
|
|| pad.layers |> list.contains(token.Layer("*.Cu"))
|
||||||
|
})
|
||||||
|
|> list.map(pad_to_element),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|> to_readable_string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_to_element(pad: Pad) -> Element(a) {
|
||||||
|
let token.PositionIdentifier(x:, y:, angle:, ..) = pad.position
|
||||||
|
let token.Size(width: w, height: h) = pad.size
|
||||||
|
let nw = -1.0 *. w
|
||||||
|
let nh = -1.0 *. h
|
||||||
|
let hw = w /. 2.0
|
||||||
|
let hh = h /. 2.0
|
||||||
|
let fill = None
|
||||||
|
let stroke = None
|
||||||
|
let stroke_width = None
|
||||||
|
let transform =
|
||||||
|
angle
|
||||||
|
|> option.map(fn(angle) {
|
||||||
|
transform.Transform(
|
||||||
|
translate: None,
|
||||||
|
rotate: Some(transform.RotateAround(angle: -1.0 *. angle, x:, y:)),
|
||||||
|
scale: None,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
let #(ox, oy) = case pad.drill {
|
||||||
|
Some(token.PadDrillDefinition(offset: Some(token.XY(x: dx, y: dy)), ..)) -> #(
|
||||||
|
x +. dx,
|
||||||
|
y +. dy,
|
||||||
|
)
|
||||||
|
_ -> #(pad.position.x, pad.position.y)
|
||||||
|
}
|
||||||
|
let elem = case pad.shape {
|
||||||
|
token.CirclePadShape ->
|
||||||
|
element.circle(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
cx: ox,
|
||||||
|
cy: oy,
|
||||||
|
r: hw,
|
||||||
|
)
|
||||||
|
token.OvalPadShape ->
|
||||||
|
case float.compare(w, h) {
|
||||||
|
order.Eq ->
|
||||||
|
element.circle(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
cx: ox,
|
||||||
|
cy: oy,
|
||||||
|
r: hw,
|
||||||
|
)
|
||||||
|
order.Lt ->
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path: [
|
||||||
|
path.MoveAbsolute(x: ox +. hw, y: oy +. hw -. hh),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hw,
|
||||||
|
ry: hw,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: nw,
|
||||||
|
dy: 0.0,
|
||||||
|
),
|
||||||
|
path.VerticalLineRelative(h -. w),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hw,
|
||||||
|
ry: hw,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: w,
|
||||||
|
dy: 0.0,
|
||||||
|
),
|
||||||
|
path.ClosePath,
|
||||||
|
])
|
||||||
|
order.Gt ->
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path: [
|
||||||
|
path.MoveAbsolute(x: ox +. hh -. hw, y: oy -. hh),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hh,
|
||||||
|
ry: hh,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: 0.0,
|
||||||
|
dy: h,
|
||||||
|
),
|
||||||
|
path.HorizontalLineRelative(w -. h),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hh,
|
||||||
|
ry: hh,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: 0.0,
|
||||||
|
dy: nh,
|
||||||
|
),
|
||||||
|
path.ClosePath,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
token.RectanglePadShape | token.RoundRectPadShape -> {
|
||||||
|
let rounding = case pad.shape {
|
||||||
|
token.RoundRectPadShape -> pad.roundrect_rratio
|
||||||
|
_ -> None
|
||||||
|
}
|
||||||
|
let rounding = case rounding {
|
||||||
|
Some(value) if value <=. 0.0 -> None
|
||||||
|
Some(value) if value >. 5.0 -> Some(5.0)
|
||||||
|
Some(value) -> Some(value)
|
||||||
|
None -> None
|
||||||
|
}
|
||||||
|
let chamfering = case pad.chamfer_ratio {
|
||||||
|
Some(value) if value <=. 0.0 -> None
|
||||||
|
Some(value) if value >. 5.0 -> Some(5.0)
|
||||||
|
Some(value) -> Some(value)
|
||||||
|
None -> None
|
||||||
|
}
|
||||||
|
case rounding, pad.chamfer, chamfering {
|
||||||
|
None, Some([]), _ | None, None, _ | None, _, None ->
|
||||||
|
element.centered_rect(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x: ox,
|
||||||
|
y: oy,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
)
|
||||||
|
Some(rounding), Some([]), _
|
||||||
|
| Some(rounding), None, _
|
||||||
|
| Some(rounding), _, None
|
||||||
|
-> {
|
||||||
|
let radius = rounding *. float.min(w, h)
|
||||||
|
element.centered_rounded_rect(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x: ox,
|
||||||
|
y: oy,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None, Some(corners), Some(chamfering) -> {
|
||||||
|
let #(tl, tr, bl, br) = chamfered_corners(corners)
|
||||||
|
let o = chamfering *. float.min(w, h)
|
||||||
|
let no = -1.0 *. o
|
||||||
|
let path = [path.ClosePath]
|
||||||
|
let bro = case br {
|
||||||
|
True -> o
|
||||||
|
False -> 0.0
|
||||||
|
}
|
||||||
|
let path = case tr {
|
||||||
|
True -> [
|
||||||
|
path.VerticalLineRelative(dy: nh +. bro +. o),
|
||||||
|
path.LineRelative(dx: no, dy: no),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [path.VerticalLineRelative(dy: nh +. bro), ..path]
|
||||||
|
}
|
||||||
|
let blo = case bl {
|
||||||
|
True -> o
|
||||||
|
False -> 0.0
|
||||||
|
}
|
||||||
|
let path = case br {
|
||||||
|
True -> [
|
||||||
|
path.HorizontalLineRelative(dx: w -. blo -. o),
|
||||||
|
path.LineRelative(dx: o, dy: no),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [path.HorizontalLineRelative(dx: w -. blo), ..path]
|
||||||
|
}
|
||||||
|
let tlo = case tl {
|
||||||
|
True -> o
|
||||||
|
False -> 0.0
|
||||||
|
}
|
||||||
|
let path = case bl {
|
||||||
|
True -> [
|
||||||
|
path.VerticalLineRelative(dy: h -. tlo -. o),
|
||||||
|
path.LineRelative(dx: o, dy: o),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [path.VerticalLineRelative(dy: h -. tlo), ..path]
|
||||||
|
}
|
||||||
|
let path = case tl {
|
||||||
|
True -> [
|
||||||
|
path.MoveAbsolute(x: ox -. hw +. o, y: oy -. hh),
|
||||||
|
path.LineRelative(dx: no, dy: o),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [path.MoveAbsolute(x: ox -. hw, y: oy -. hh), ..path]
|
||||||
|
}
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path:)
|
||||||
|
}
|
||||||
|
Some(rounding), Some(corners), Some(chamfering) -> {
|
||||||
|
let #(tl, tr, bl, br) = chamfered_corners(corners)
|
||||||
|
let o = chamfering *. float.min(w, h)
|
||||||
|
let no = -1.0 *. o
|
||||||
|
let r = rounding *. float.min(w, h)
|
||||||
|
let nr = -1.0 *. r
|
||||||
|
let path = [path.ClosePath]
|
||||||
|
let bro = case br {
|
||||||
|
True -> o
|
||||||
|
False -> r
|
||||||
|
}
|
||||||
|
let path = case tr {
|
||||||
|
True -> [
|
||||||
|
path.VerticalLineRelative(dy: nh +. bro +. o),
|
||||||
|
path.LineRelative(dx: no, dy: no),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [
|
||||||
|
path.VerticalLineRelative(dy: nh +. bro +. r),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: r,
|
||||||
|
ry: r,
|
||||||
|
angle: 90.0,
|
||||||
|
large: True,
|
||||||
|
clockwise: True,
|
||||||
|
dx: nr,
|
||||||
|
dy: nr,
|
||||||
|
),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
}
|
||||||
|
let blo = case bl {
|
||||||
|
True -> o
|
||||||
|
False -> r
|
||||||
|
}
|
||||||
|
let path = case br {
|
||||||
|
True -> [
|
||||||
|
path.HorizontalLineRelative(dx: w -. blo -. o),
|
||||||
|
path.LineRelative(dx: o, dy: no),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [
|
||||||
|
path.HorizontalLineRelative(dx: w -. blo -. r),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: r,
|
||||||
|
ry: r,
|
||||||
|
angle: 90.0,
|
||||||
|
large: True,
|
||||||
|
clockwise: True,
|
||||||
|
dx: r,
|
||||||
|
dy: nr,
|
||||||
|
),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
}
|
||||||
|
let tlo = case tl {
|
||||||
|
True -> o
|
||||||
|
False -> r
|
||||||
|
}
|
||||||
|
let path = case bl {
|
||||||
|
True -> [
|
||||||
|
path.VerticalLineRelative(dy: h -. tlo -. o),
|
||||||
|
path.LineRelative(dx: o, dy: o),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [
|
||||||
|
path.VerticalLineRelative(dy: h -. tlo -. r),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: r,
|
||||||
|
ry: r,
|
||||||
|
angle: 90.0,
|
||||||
|
large: True,
|
||||||
|
clockwise: True,
|
||||||
|
dx: r,
|
||||||
|
dy: r,
|
||||||
|
),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
}
|
||||||
|
let path = case tl {
|
||||||
|
True -> [
|
||||||
|
path.MoveAbsolute(x: ox -. hw +. o, y: oy -. hh),
|
||||||
|
path.LineRelative(dx: no, dy: o),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
False -> [
|
||||||
|
path.MoveAbsolute(x: ox -. hw +. r, y: oy -. hh),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: r,
|
||||||
|
ry: r,
|
||||||
|
angle: 90.0,
|
||||||
|
large: True,
|
||||||
|
clockwise: True,
|
||||||
|
dx: nr,
|
||||||
|
dy: r,
|
||||||
|
),
|
||||||
|
..path
|
||||||
|
]
|
||||||
|
}
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path:)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token.TrapezoidPadShape ->
|
||||||
|
case pad.rect_delta {
|
||||||
|
Some(token.XY(x: 0.0, y: o)) -> {
|
||||||
|
let no = -1.0 *. o
|
||||||
|
let ho = o /. 2.0
|
||||||
|
let path = [
|
||||||
|
path.MoveAbsolute(x: ox -. hw +. ho, y: oy -. hh),
|
||||||
|
path.LineRelative(dx: no, dy: h),
|
||||||
|
path.HorizontalLineRelative(dx: w +. o),
|
||||||
|
path.LineRelative(dx: no, dy: nh),
|
||||||
|
path.ClosePath,
|
||||||
|
]
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path:)
|
||||||
|
}
|
||||||
|
Some(token.XY(x: o, y: 0.0)) -> {
|
||||||
|
let no = -1.0 *. o
|
||||||
|
let ho = o /. 2.0
|
||||||
|
let path = [
|
||||||
|
path.MoveAbsolute(x: ox -. hw, y: oy +. hh +. ho),
|
||||||
|
path.LineRelative(dx: w, dy: no),
|
||||||
|
path.VerticalLineRelative(dy: nh +. o),
|
||||||
|
path.LineRelative(dx: nw, dy: no),
|
||||||
|
path.ClosePath,
|
||||||
|
]
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path:)
|
||||||
|
}
|
||||||
|
_ ->
|
||||||
|
element.centered_rect(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x: ox,
|
||||||
|
y: oy,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
token.CustomPadShape ->
|
||||||
|
case pad.custom_options {
|
||||||
|
Some(token.CustomPadOptions(anchor: token.CircleAnchorPadShape, ..)) ->
|
||||||
|
element.circle(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
cx: ox,
|
||||||
|
cy: oy,
|
||||||
|
r: hw,
|
||||||
|
)
|
||||||
|
Some(token.CustomPadOptions(anchor: token.RectangleAnchorPadShape, ..))
|
||||||
|
| None ->
|
||||||
|
element.centered_rect(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x: ox,
|
||||||
|
y: oy,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fill = Some(bg_col)
|
||||||
|
let stroke = Some("#E7B629")
|
||||||
|
let stroke_width = Some(0.03333)
|
||||||
|
case pad.drill {
|
||||||
|
Some(token.PadDrillDefinition(False, Some(d), ..))
|
||||||
|
| Some(token.PadDrillDefinition(True, Some(d), None, ..)) ->
|
||||||
|
fragment([
|
||||||
|
elem,
|
||||||
|
element.circle(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
cx: x,
|
||||||
|
cy: y,
|
||||||
|
r: d /. 2.0,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
Some(token.PadDrillDefinition(True, Some(w), Some(h), ..)) -> {
|
||||||
|
let hw = w /. 2.0
|
||||||
|
let hh = h /. 2.0
|
||||||
|
let nw = -1.0 *. w
|
||||||
|
let nh = -1.0 *. h
|
||||||
|
fragment([
|
||||||
|
elem,
|
||||||
|
case float.compare(w, h) {
|
||||||
|
order.Eq ->
|
||||||
|
element.circle(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
cx: x,
|
||||||
|
cy: y,
|
||||||
|
r: hw,
|
||||||
|
)
|
||||||
|
order.Lt ->
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path: [
|
||||||
|
path.MoveAbsolute(x: x +. hw, y: y +. hw -. hh),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hw,
|
||||||
|
ry: hw,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: nw,
|
||||||
|
dy: 0.0,
|
||||||
|
),
|
||||||
|
path.VerticalLineRelative(h -. w),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hw,
|
||||||
|
ry: hw,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: w,
|
||||||
|
dy: 0.0,
|
||||||
|
),
|
||||||
|
path.ClosePath,
|
||||||
|
])
|
||||||
|
order.Gt ->
|
||||||
|
element.path(fill:, stroke:, stroke_width:, transform:, path: [
|
||||||
|
path.MoveAbsolute(x: x +. hh -. hw, y: y -. hh),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hh,
|
||||||
|
ry: hh,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: 0.0,
|
||||||
|
dy: h,
|
||||||
|
),
|
||||||
|
path.HorizontalLineRelative(w -. h),
|
||||||
|
path.ArcCurveRelative(
|
||||||
|
rx: hh,
|
||||||
|
ry: hh,
|
||||||
|
angle: 180.0,
|
||||||
|
large: False,
|
||||||
|
clockwise: True,
|
||||||
|
dx: 0.0,
|
||||||
|
dy: nh,
|
||||||
|
),
|
||||||
|
path.ClosePath,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
_ -> elem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chamfered_corners(corners: List(token.Corner)) {
|
||||||
|
corners
|
||||||
|
|> list.fold(#(False, False, False, False), fn(corners, corner) {
|
||||||
|
let #(top_left, top_right, bottom_left, bottom_right) = corners
|
||||||
|
case corner {
|
||||||
|
token.TopLeft -> #(True, top_right, bottom_left, bottom_right)
|
||||||
|
token.TopRight -> #(top_left, True, bottom_left, bottom_right)
|
||||||
|
token.BottomLeft -> #(top_left, top_right, True, bottom_right)
|
||||||
|
token.BottomRight -> #(top_left, top_right, bottom_left, True)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
104
src/svg_attribute.gleam
Normal file
104
src/svg_attribute.gleam
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import gleam/float
|
||||||
|
import gleam/list
|
||||||
|
import gleam/string
|
||||||
|
import lustre/attribute.{type Attribute, attribute}
|
||||||
|
import svg_path.{type Path}
|
||||||
|
import svg_transform.{type Transform}
|
||||||
|
|
||||||
|
pub fn view_box(
|
||||||
|
min_x: Float,
|
||||||
|
min_y: Float,
|
||||||
|
width: Float,
|
||||||
|
height: Float,
|
||||||
|
) -> Attribute(a) {
|
||||||
|
attribute(
|
||||||
|
"viewBox",
|
||||||
|
[min_x, min_y, width, height]
|
||||||
|
|> list.map(float.to_string)
|
||||||
|
|> string.join(" "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn float_attribute(name: String, value: Float) -> Attribute(a) {
|
||||||
|
attribute(name, value |> float.to_precision(6) |> float.to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill(value: String) -> Attribute(a) {
|
||||||
|
attribute("fill", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stroke(value: String) -> Attribute(a) {
|
||||||
|
attribute("stroke", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stroke_width(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("stroke-width", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("width", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("height", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("x", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("y", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dx(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("dx", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dy(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("dy", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x1(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("x1", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y1(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("y1", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x2(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("x2", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y2(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("y2", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rx(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("rx", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ry(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("ry", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cx(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("cx", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cy(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("cy", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn r(value: Float) -> Attribute(a) {
|
||||||
|
float_attribute("r", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn d(value: Path) -> Attribute(a) {
|
||||||
|
attribute("d", svg_path.to_string(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform(value: Transform) -> Attribute(a) {
|
||||||
|
attribute("transform", svg_transform.to_string(value))
|
||||||
|
}
|
221
src/svg_element.gleam
Normal file
221
src/svg_element.gleam
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import gleam/option.{type Option, None, Some}
|
||||||
|
import lustre/attribute.{type Attribute}
|
||||||
|
import lustre/element.{type Element, element}
|
||||||
|
import svg_attribute
|
||||||
|
import svg_path.{type Path}
|
||||||
|
import svg_transform.{type Transform}
|
||||||
|
|
||||||
|
fn styles(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
) -> List(Attribute(a)) {
|
||||||
|
let attrs = []
|
||||||
|
let attrs = case transform {
|
||||||
|
Some(value) -> [svg_attribute.transform(value), ..attrs]
|
||||||
|
None -> attrs
|
||||||
|
}
|
||||||
|
let attrs = case stroke_width {
|
||||||
|
Some(value) -> [svg_attribute.stroke_width(value), ..attrs]
|
||||||
|
None -> attrs
|
||||||
|
}
|
||||||
|
let attrs = case stroke {
|
||||||
|
Some(value) -> [svg_attribute.stroke(value), ..attrs]
|
||||||
|
None -> attrs
|
||||||
|
}
|
||||||
|
let attrs = case fill {
|
||||||
|
Some(value) -> [svg_attribute.fill(value), ..attrs]
|
||||||
|
None -> attrs
|
||||||
|
}
|
||||||
|
attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
children children: List(Element(a)),
|
||||||
|
) -> Element(a) {
|
||||||
|
element("g", styles(fill:, stroke:, stroke_width:, transform:), children)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
x1 x1: Float,
|
||||||
|
y1 y1: Float,
|
||||||
|
x2 x2: Float,
|
||||||
|
y2 y2: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
element(
|
||||||
|
"line",
|
||||||
|
[
|
||||||
|
svg_attribute.x1(x1),
|
||||||
|
svg_attribute.y1(y1),
|
||||||
|
svg_attribute.x2(x2),
|
||||||
|
svg_attribute.y2(y2),
|
||||||
|
..styles(fill:, stroke:, stroke_width:, transform:)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn circle(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
cx cx: Float,
|
||||||
|
cy cy: Float,
|
||||||
|
r r: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
element(
|
||||||
|
"circle",
|
||||||
|
[
|
||||||
|
svg_attribute.cx(cx),
|
||||||
|
svg_attribute.cy(cy),
|
||||||
|
svg_attribute.r(r),
|
||||||
|
..styles(fill:, stroke:, stroke_width:, transform:)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ellipse(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
cx cx: Float,
|
||||||
|
cy cy: Float,
|
||||||
|
rx rx: Float,
|
||||||
|
ry ry: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
element(
|
||||||
|
"ellipse",
|
||||||
|
[
|
||||||
|
svg_attribute.cx(cx),
|
||||||
|
svg_attribute.cy(cy),
|
||||||
|
svg_attribute.rx(rx),
|
||||||
|
svg_attribute.ry(ry),
|
||||||
|
..styles(fill:, stroke:, stroke_width:, transform:)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rect(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
x x: Float,
|
||||||
|
y y: Float,
|
||||||
|
width width: Float,
|
||||||
|
height height: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
element(
|
||||||
|
"rect",
|
||||||
|
[
|
||||||
|
svg_attribute.x(x),
|
||||||
|
svg_attribute.y(y),
|
||||||
|
svg_attribute.width(width),
|
||||||
|
svg_attribute.height(height),
|
||||||
|
..styles(fill:, stroke:, stroke_width:, transform:)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rounded_rect(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
x x: Float,
|
||||||
|
y y: Float,
|
||||||
|
rx rx: Float,
|
||||||
|
ry ry: Float,
|
||||||
|
width width: Float,
|
||||||
|
height height: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
element(
|
||||||
|
"rect",
|
||||||
|
[
|
||||||
|
svg_attribute.x(x),
|
||||||
|
svg_attribute.y(y),
|
||||||
|
svg_attribute.rx(rx),
|
||||||
|
svg_attribute.ry(ry),
|
||||||
|
svg_attribute.width(width),
|
||||||
|
svg_attribute.height(height),
|
||||||
|
..styles(fill:, stroke:, stroke_width:, transform:)
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn centered_rect(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
x x: Float,
|
||||||
|
y y: Float,
|
||||||
|
width width: Float,
|
||||||
|
height height: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
rect(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x: x -. width /. 2.0,
|
||||||
|
y: y -. height /. 2.0,
|
||||||
|
width:,
|
||||||
|
height:,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn centered_rounded_rect(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
x x: Float,
|
||||||
|
y y: Float,
|
||||||
|
rx rx: Float,
|
||||||
|
ry ry: Float,
|
||||||
|
width width: Float,
|
||||||
|
height height: Float,
|
||||||
|
) -> Element(a) {
|
||||||
|
rounded_rect(
|
||||||
|
fill:,
|
||||||
|
stroke:,
|
||||||
|
stroke_width:,
|
||||||
|
transform:,
|
||||||
|
x: x -. width /. 2.0,
|
||||||
|
y: y -. height /. 2.0,
|
||||||
|
rx:,
|
||||||
|
ry:,
|
||||||
|
width:,
|
||||||
|
height:,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(
|
||||||
|
fill fill: Option(String),
|
||||||
|
stroke stroke: Option(String),
|
||||||
|
stroke_width stroke_width: Option(Float),
|
||||||
|
transform transform: Option(Transform),
|
||||||
|
path path: Path,
|
||||||
|
) -> Element(a) {
|
||||||
|
element(
|
||||||
|
"path",
|
||||||
|
[svg_attribute.d(path), ..styles(fill:, stroke:, stroke_width:, transform:)],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
}
|
138
src/svg_path.gleam
Normal file
138
src/svg_path.gleam
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import gleam/bool
|
||||||
|
import gleam/float
|
||||||
|
import gleam/list
|
||||||
|
import gleam/string
|
||||||
|
|
||||||
|
pub type PathCommand {
|
||||||
|
MoveAbsolute(x: Float, y: Float)
|
||||||
|
MoveRelative(dx: Float, dy: Float)
|
||||||
|
LineAbsolute(x: Float, y: Float)
|
||||||
|
LineRelative(dx: Float, dy: Float)
|
||||||
|
HorizontalLineAbsolute(x: Float)
|
||||||
|
HorizontalLineRelative(dx: Float)
|
||||||
|
VerticalLineAbsolute(y: Float)
|
||||||
|
VerticalLineRelative(dy: Float)
|
||||||
|
CubicBezierAbsolute(
|
||||||
|
x1: Float,
|
||||||
|
y1: Float,
|
||||||
|
x2: Float,
|
||||||
|
y2: Float,
|
||||||
|
x: Float,
|
||||||
|
y: Float,
|
||||||
|
)
|
||||||
|
CubicBezierRelative(
|
||||||
|
dx1: Float,
|
||||||
|
dy1: Float,
|
||||||
|
dx2: Float,
|
||||||
|
dy2: Float,
|
||||||
|
dx: Float,
|
||||||
|
dy: Float,
|
||||||
|
)
|
||||||
|
SmoothCubicBezierAbsolute(x2: Float, y2: Float, x: Float, y: Float)
|
||||||
|
SmoothCubicBezierRelative(dx2: Float, dy2: Float, dx: Float, dy: Float)
|
||||||
|
QuadraticBezierAbsolute(x1: Float, y1: Float, x: Float, y: Float)
|
||||||
|
QuadraticBezierRelative(dx1: Float, dy1: Float, dx: Float, dy: Float)
|
||||||
|
SmoothQuadraticBezierAbsolute(x: Float, y: Float)
|
||||||
|
SmoothQuadraticBezierRelative(dx: Float, dy: Float)
|
||||||
|
ArcCurveAbsolute(
|
||||||
|
rx: Float,
|
||||||
|
ry: Float,
|
||||||
|
angle: Float,
|
||||||
|
large: Bool,
|
||||||
|
clockwise: Bool,
|
||||||
|
x: Float,
|
||||||
|
y: Float,
|
||||||
|
)
|
||||||
|
ArcCurveRelative(
|
||||||
|
rx: Float,
|
||||||
|
ry: Float,
|
||||||
|
angle: Float,
|
||||||
|
large: Bool,
|
||||||
|
clockwise: Bool,
|
||||||
|
dx: Float,
|
||||||
|
dy: Float,
|
||||||
|
)
|
||||||
|
ClosePath
|
||||||
|
}
|
||||||
|
|
||||||
|
fn float_to_string(value: Float) -> String {
|
||||||
|
value |> float.to_precision(6) |> float.to_string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_str(id: String, values: List(Float)) -> String {
|
||||||
|
id <> " " <> values |> list.map(float_to_string) |> string.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool_str(value: Bool) -> String {
|
||||||
|
case value {
|
||||||
|
True -> "0"
|
||||||
|
False -> "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_to_string(command: PathCommand) -> String {
|
||||||
|
case command {
|
||||||
|
MoveAbsolute(x:, y:) -> mk_str("M", [x, y])
|
||||||
|
MoveRelative(dx:, dy:) -> mk_str("m", [dx, dy])
|
||||||
|
LineAbsolute(x:, y:) -> mk_str("L", [x, y])
|
||||||
|
LineRelative(dx:, dy:) -> mk_str("l", [dx, dy])
|
||||||
|
HorizontalLineAbsolute(x:) -> mk_str("H", [x])
|
||||||
|
HorizontalLineRelative(dx:) -> mk_str("h", [dx])
|
||||||
|
VerticalLineAbsolute(y:) -> mk_str("V", [y])
|
||||||
|
VerticalLineRelative(dy:) -> mk_str("v", [dy])
|
||||||
|
CubicBezierAbsolute(x1:, y1:, x2:, y2:, x:, y:) ->
|
||||||
|
mk_str("C", [x1, y1, x2, y2, x, y])
|
||||||
|
CubicBezierRelative(dx1:, dy1:, dx2:, dy2:, dx:, dy:) ->
|
||||||
|
mk_str("c", [dx1, dy1, dx2, dy2, dx, dy])
|
||||||
|
SmoothCubicBezierAbsolute(x2:, y2:, x:, y:) -> mk_str("S", [x2, y2, x, y])
|
||||||
|
SmoothCubicBezierRelative(dx2:, dy2:, dx:, dy:) ->
|
||||||
|
mk_str("s", [dx2, dy2, dx, dy])
|
||||||
|
QuadraticBezierAbsolute(x1:, y1:, x:, y:) -> mk_str("Q", [x1, y1, x, y])
|
||||||
|
QuadraticBezierRelative(dx1:, dy1:, dx:, dy:) ->
|
||||||
|
mk_str("q", [dx1, dy1, dx, dy])
|
||||||
|
SmoothQuadraticBezierAbsolute(x:, y:) -> mk_str("T", [x, y])
|
||||||
|
SmoothQuadraticBezierRelative(dx:, dy:) -> mk_str("t", [dx, dy])
|
||||||
|
ArcCurveAbsolute(rx:, ry:, angle:, large:, clockwise:, x:, y:) ->
|
||||||
|
mk_str("A", [rx, ry, angle])
|
||||||
|
<> " "
|
||||||
|
<> bool_str(large)
|
||||||
|
<> " "
|
||||||
|
<> bool_str(clockwise)
|
||||||
|
<> " "
|
||||||
|
<> float_to_string(x)
|
||||||
|
<> " "
|
||||||
|
<> float_to_string(y)
|
||||||
|
ArcCurveRelative(rx:, ry:, angle:, large:, clockwise:, dx:, dy:) ->
|
||||||
|
mk_str("a", [rx, ry, angle])
|
||||||
|
<> " "
|
||||||
|
<> bool_str(large)
|
||||||
|
<> " "
|
||||||
|
<> bool_str(clockwise)
|
||||||
|
<> " "
|
||||||
|
<> float_to_string(dx)
|
||||||
|
<> " "
|
||||||
|
<> float_to_string(dy)
|
||||||
|
ClosePath -> "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Path =
|
||||||
|
List(PathCommand)
|
||||||
|
|
||||||
|
fn not_redundant(command: PathCommand) {
|
||||||
|
case command {
|
||||||
|
MoveRelative(dx: 0.0, dy: 0.0) -> False
|
||||||
|
LineRelative(dx: 0.0, dy: 0.0) -> False
|
||||||
|
HorizontalLineRelative(dx: 0.0) -> False
|
||||||
|
VerticalLineRelative(dy: 0.0) -> False
|
||||||
|
_ -> True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(path: Path) -> String {
|
||||||
|
use <- bool.guard(path == [], " ")
|
||||||
|
path
|
||||||
|
|> list.filter(not_redundant)
|
||||||
|
|> list.map(command_to_string)
|
||||||
|
|> string.join(" ")
|
||||||
|
}
|
62
src/svg_transform.gleam
Normal file
62
src/svg_transform.gleam
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import gleam/float
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option.{type Option, None, Some}
|
||||||
|
import gleam/string
|
||||||
|
|
||||||
|
pub type Translate {
|
||||||
|
Translate(x: Float, y: Float)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Rotate {
|
||||||
|
Rotate(angle: Float)
|
||||||
|
RotateAround(angle: Float, x: Float, y: Float)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Scale {
|
||||||
|
ScaleProportional(scale: Float)
|
||||||
|
ScaleStretched(scale_x: Float, scale_y: Float)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Transform {
|
||||||
|
Transform(
|
||||||
|
translate: Option(Translate),
|
||||||
|
rotate: Option(Rotate),
|
||||||
|
scale: Option(Scale),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn float_to_string(value: Float) -> String {
|
||||||
|
value |> float.to_precision(6) |> float.to_string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_str(name: String, values: List(Float)) -> String {
|
||||||
|
name
|
||||||
|
<> "("
|
||||||
|
<> { values |> list.map(float_to_string) |> string.join(" ") }
|
||||||
|
<> ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(transform: Transform) -> String {
|
||||||
|
let parts = []
|
||||||
|
let parts = case transform.scale {
|
||||||
|
Some(ScaleProportional(scale:)) -> [mk_str("scale", [scale]), ..parts]
|
||||||
|
Some(ScaleStretched(scale_x:, scale_y:)) -> [
|
||||||
|
mk_str("scale", [scale_x, scale_y]),
|
||||||
|
..parts
|
||||||
|
]
|
||||||
|
None -> parts
|
||||||
|
}
|
||||||
|
let parts = case transform.rotate {
|
||||||
|
Some(Rotate(angle:)) -> [mk_str("rotate", [angle]), ..parts]
|
||||||
|
Some(RotateAround(angle:, x:, y:)) -> [
|
||||||
|
mk_str("rotate", [angle, x, y]),
|
||||||
|
..parts
|
||||||
|
]
|
||||||
|
None -> parts
|
||||||
|
}
|
||||||
|
let parts = case transform.translate {
|
||||||
|
Some(Translate(x:, y:)) -> [mk_str("translate", [x, y]), ..parts]
|
||||||
|
None -> parts
|
||||||
|
}
|
||||||
|
parts |> string.join(" ")
|
||||||
|
}
|
5
test/substrate_test.gleam
Normal file
5
test/substrate_test.gleam
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import gleeunit
|
||||||
|
|
||||||
|
pub fn main() -> Nil {
|
||||||
|
gleeunit.main()
|
||||||
|
}
|
Loading…
Reference in a new issue