Replies: 4 comments
-
I also wrote some tests because parsing stuff in typst is not trivial #let testCreateCommandEntry() = {
let tests = (
(("L", " 10 20"), (command: "L", params: (10, 20))),
(("Z", ""), (command: "Z", params: ())),
((none, ""), none)
)
for test in tests {
let input = test.at(0)
let expected = test.at(1)
let result = createCommandEntry(input.at(0), input.at(1))
if result != expected {
return false // Test failed
}
}
true // All tests passed
}
#testCreateCommandEntry()
#let testIsCommandChar() = {
let tests = (
("M", true),
("m", true),
("1", false),
("#", false),
(" ", false)
)
for (input, expected) in tests {
let result = isCommandChar(input)
if result != expected {
return (input,result,expected,false) // Test failed
}
}
true // All tests passed
}
#let testSplitParameters() = {
let tests = (
("10,20,30", (10, 20, 30)),
("10 20 30", (10, 20, 30)),
("10, 20 30", (10, 20, 30)),
("10,, 20, ,30", (10, 20, 30)),
(" , ", ())
)
for (input, expected) in tests {
let result = splitParameters(input)
if result != expected {
return false // Test failed
}
}
true // All tests passed
}
#let testParseCommand() = {
let tests = (
("M10,20", (command: "M", params: (10, 20))),
("l15 25", (command: "l", params: (15, 25))),
("C30,40 50,60 70,80", (command: "C", params: (30, 40, 50, 60, 70, 80))),
("Z", (command: "Z", params: ()))
)
for (input, expected) in tests {
let result = parseCommand(input)
if result != expected {
return false // Test failed
}
}
true // All tests passed
}
#testParseCommand()
#testIsCommandChar()
#testSplitParameters()
#let testParseSVGPath() = {
let tests = (
("M 0 0 L 10 10", ((command: "M", params: (0, 0)), (command: "L", params: (10, 10)))),
("C 10 10, 20 20, 30 30", ((command: "C", params: (10, 10, 20, 20, 30, 30)),)),
("M 5 5 L 15 15 Z", ((command: "M", params: (5, 5)), (command: "L", params: (15, 15)), (command: "Z", params: ()))),
("Z", ((command: "Z", params: ()),)),
("", ()),
("A 30 50 0 0,1 100 100", ((command: "A", params: (30, 50, 0, 0, 1, 100, 100)),)),
("M 10 10 L 20 20 L 30 30 Z", ((command: "M", params: (10, 10)), (command: "L", params: (20, 20)), (command: "L", params: (30, 30)), (command: "Z", params: ()))),
("M -10 -10 C -20 -20, -30 -30, -40 -40", ((command: "M", params: (-10, -10)), (command: "C", params: (-20, -20, -30, -30, -40, -40)))),
("M 100 100 l 10 10", ((command: "M", params: (100, 100)), (command: "l", params: (10, 10)))),
("L 50 50", ((command: "L", params: (50, 50)),))
)
for (input, expected) in tests {
let result = parseSVGPath(input)
if result != expected {
return (input, result, expected, false) // Test failed
}
}
true // All tests passed
}
#testParseSVGPath()
#let testExtractPathCommands() = {
let tests = (
("M 0 0 L 10 10", ("L", " 10 10", (("command": "M", "params": (0, 0)),))),
("C 10 10, 20 20, 30 30", ("C", " 10 10, 20 20, 30 30", ())),
("M 5 5 L 15 15 Z", ("Z", "", (("command": "M", "params": (5, 5)), ("command": "L", "params": (15, 15)),))),
("Z", ("Z", "", ())),
("", (none, "", ()))
)
for (input, expected) in tests {
let (command, params, result) = extractPathCommands((), input)
if (command, params, result) != expected {
return false // Test failed
}
}
true // All tests passed
}
#testExtractPathCommands() |
Beta Was this translation helpful? Give feedback.
-
and then all we need is to somehow implement these functions #let processSVGPathCommands(parsedCommands) = {
let currentPosition = (x: 0, y: 0)
for commandEntry in parsedCommands {
let command = commandEntry.command
let params = commandEntry.params
// Convert lowercase command to uppercase and adjust parameters if necessary
if lower(command) == command {
(command, params) = convertToAbsolute(command, params, currentPosition)
}
let action = (
("M", () => moveTo(params, currentPosition)),
("L", () => lineTo(params, currentPosition)),
("H", () => horizontalLineTo(params, currentPosition)),
("V", () => verticalLineTo(params, currentPosition)),
("C", () => curveTo(params, currentPosition)),
("S", () => smoothCurveTo(params, currentPosition)),
("Q", () => quadraticBezierCurveTo(params, currentPosition)),
("T", () => smoothQuadraticBezierCurveTo(params, currentPosition)),
("A", () => arcTo(params, currentPosition)),
("Z", () => closePath(currentPosition)),
// Lowercase commands have been converted to uppercase
// Add other specific SVG commands here if needed
).find(pair => pair.at(0) == upper(command)).at(1)
// Execute the matched function
currentPosition = action()
}
}
#let convertToAbsolute(command, params, currentPosition) = {
let absoluteCommand = upper(command)
let absoluteParams = // Logic to convert params to absolute based on currentPosition
return (absoluteCommand, absoluteParams)
}
# Function prototypes (for illustration purposes)
#let moveTo(params, currentPosition) = // Implementation...
#let lineTo(params, currentPosition) = // Implementation...
#let horizontalLineTo(params, currentPosition) = // Implementation...
#let verticalLineTo(params, currentPosition) = // Implementation...
#let curveTo(params, currentPosition) = // Implementation...
#let smoothCurveTo(params, currentPosition) = // Implementation...
#let quadraticBezierCurveTo(params, currentPosition) = // Implementation...
#let smoothQuadraticBezierCurveTo(params, currentPosition) = // Implementation...
#let arcTo(params, currentPosition) = // Implementation...
#let closePath(currentPosition) = // Implementation... but if we do, it will be a good day |
Beta Was this translation helpful? Give feedback.
-
I think that would be a good thing to put into its own package? I do not see this in CeTZ itself, at least not as of now. |
Beta Was this translation helpful? Give feedback.
-
#let parseRotate(input) = {
let numbers = input.matches(regex("-?\\d+(\\.\\d+)?")).map(s => float(s.text))
if numbers.len() == 1 {
// Simple rotation around the origin
return cetz.matrix.transform-rotate-z(numbers.at(0)*1deg/1rad)
} else if numbers.len() == 3 {
// Rotation around a specific point (cx, cy)
let angle = numbers.at(0)*1deg/1rad
let cx = numbers.at(1)
let cy = numbers.at(2)
let translateToOrigin = cetz.matrix.transform-translate(-cx, -cy, 0)
let rotate = cetz.matrix.transform-rotate-z(angle)
let translateBack = cetz.matrix.transform-translate(cx, cy, 0)
// Combine the transformations
return cetz.matrix.mul-mat(translateBack, cetz.matrix.mul-mat(rotate, translateToOrigin))
} else {
// Invalid input
return none
}
}
#let parseTranslate(input) = {
let numbers = input.matches(regex("-?\\d+(\\.\\d+)?")).map(s => float(s.text))
if numbers.len() >= 1 {
let tx = numbers.at(0)
let ty = if numbers.len() > 1 { numbers.at(1) } else { 0 }
return cetz.matrix.transform-translate(tx, ty, 0)
} else {
return none
}
}
// Return 4x4 x-shear matrix
#let transform-shear-x(factor) = {
((1, factor, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1))
}
#let transform-shear-x-wrapper(angle) = {
if angle == 90deg {
// Special case: Shear angle is 90 degrees
((1, 1, 0, 0),
(0, 0, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1))
} else if angle == -90deg {
// Special case: Shear angle is -90 degrees
((1, -1, 0, 0),
(0, 0, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1))
} else {
// General case: Calculate scale from tangent of angle
let scale = calc.tan(angle)
transform-shear-x(scale)
}
}
// Return 4x4 y-shear matrix
#let transform-shear-y(factor) = {
((1, 0, 0, 0),
(factor, 1, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1))
}
#let transform-shear-y-wrapper(angle) = {
if angle == 90deg {
// Special case: Shear angle is 90 degrees
((0, 0, 0, 0),
(1, 1, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1))
} else if angle == -90deg {
// Special case: Shear angle is -90 degrees
((0, 0, 0, 0),
(-1, 1, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1))
} else {
// General case: Calculate scale from tangent of angle
let scale = tan(angle)
transform-shear-y(scale)
}
}
#let parseSkewX(input) = {
let numbers = input.matches(regex("-?\\d+(\\.\\d+)?")).map(s => float(s.text))
if numbers.len() == 1 {
let angle = numbers.at(0)*1deg
return transform-shear-x-wrapper(angle)
} else {
return none
}
}
#let parseSkewY(input) = {
let numbers = input.matches(regex("-?\\d+(\\.\\d+)?")).map(s => float(s.text))
if numbers.len() == 1 {
let angle = numbers.at(0)*1deg/1rad
// Assuming a similar function for skewY in cetz.matrix
return transform-shear-y(angle)
} else {
return none
}
}
#let parseScale(input) = {
let numbers = input.matches(regex("-?\\d+(\\.\\d+)?")).map(s => float(s.text))
if numbers.len() >= 1 {
let sx = numbers.at(0)
let sy = if numbers.len() > 1 { numbers.at(1) } else { sx }
return cetz.matrix.transform-scale((x: sx, y: sy))
} else {
return none
}
}
#let reduceList(list, initial, func) = {
let result = initial
for item in list {
result = func(result, item)
}
return result
}
#let parseTransformations(input) = {
// Regular expression to match transformation commands
let regexPattern = regex("(rotate|translate|skewX|skewY|scale)\\([^)]+\\)")
// Find all matches of transformation commands
let matches = input.matches(regexPattern)
// Extract and parse each command
let matrices = matches.map(match => {
let command = match.text // Extract the complete command including parameters
if command.starts-with("rotate") {
parseRotate(command)
} else if command.starts-with("translate") {
parseTranslate(command)
} else if command.starts-with("skewX") {
parseSkewX(command)
} else if command.starts-with("skewY") {
parseSkewY(command)
} else if command.starts-with("scale") {
parseScale(command)
} else {
none
}
}).filter(matrix => matrix != none)
// Combine the matrices
reduceList(matrices, cetz.matrix.ident(m: 4, n: 4),cetz.matrix.mul-mat)
}
//how to use
parseTransformations("rotate(-10 50 100) translate(-36 45.5) skewX(30) scale(1 0.5)") So parseTransformation can read a svg transform and convert it into an affine matrix |
Beta Was this translation helpful? Give feedback.
-
Hi, I wrote a very simple svg path d parser, maybe it could be used to automatically draw from a svg d path? Just an idea
Beta Was this translation helpful? Give feedback.
All reactions