Add support for Rust embedded graphics#233
Conversation
|
@yappiverse theres no reason to include both circles and ellipses. If platform has ellipses then we shouldn't show circle as a separate tool. If RX == RY then the code preview must render it as Circle, otherwise its Ellipse |
@yappiverse the code looks broken when Layer titles enabled
|
sbrin
left a comment
There was a problem hiding this comment.
Great addition! The platform structure is clean, the per-layer function approach reads well in Rust, and the test coverage is solid. Two production bugs need fixing before merge — the color one will cause every black element to render white. Details inline.
| if (typeof props.color === 'string') { | ||
| props.color = this.packColor(props.color); | ||
| } |
There was a problem hiding this comment.
🔴 Bug: packColor is called twice — every black element renders white in production.
processLayerModifiers already calls this.packColor(layer.color) via the base class color modifier (see platform.ts:144). This block runs again on the already-packed string:
// After processLayerModifiers:
// props.color = 'BinaryColor::Off' (correct)
if (typeof props.color === 'string') {
props.color = this.packColor('BinaryColor::Off');
// 'BinaryColor::Off' !== '#000000' → returns 'BinaryColor::On' ❌
}
The tests don't catch this because layersMock uses defaultFeatures (no hasIndexedColors), so the base class color modifier never fires in tests. In production with EmbeddedGraphicsPlatform (which sets hasIndexedColors: true), the double-pack always fires.
Fix: Remove this block. processLayerModifiers already handles color for platforms with hasIndexedColors: true. If you need the explicit call for layers without a color modifier, add color to the overrides map passed to processLayerModifiers instead:
this.processLayerModifiers(layer, props, {
x: ...,
y: ...,
color: (l, p) => { p.color = this.packColor(l.color); },
});
// ← no explicit packColor call hereAlso: add a test that constructs a black rectangle using EmbeddedGraphicsPlatform.features and asserts the output contains BinaryColor::Off.
| each layer in layers | ||
| | !{pad}!{layer.functionName}(display)?; |
There was a problem hiding this comment.
🔴 Bug: wrap=false generates invalid Rust — function-call statements at module scope.
When wrap=false and preview_window=false, hasDrawWrapper=false and no enclosing fn draw_xxx() is generated. But this each loop still runs, emitting:
draw_box_veqtjp8jf9ln6isyfz(display)?;...at module scope, outside any function. The ? operator is only valid inside a function returning Result, and bare statement expressions at module level are illegal in Rust. This will always produce a compile error.
The test at embedded-graphics.test.ts:585 checks that these call strings ARE present but doesn't validate that the output is legal Rust.
Fix: Gate the dispatch loop on hasDrawWrapper:
if hasDrawWrapper
each layer in layers
| !{pad}!{layer.functionName}(display)?;
|With wrap=false, users call each per-layer function directly — the dispatch block is only useful inside a wrapper.
| const nameRegexp = new RegExp(`${name}_?\\d*`); | ||
| const countWithSameName = bitmapNames.filter((currentName) => nameRegexp.test(currentName)).length; |
There was a problem hiding this comment.
bitmapNames stores entries uppercase: IMAGE_LOGO_BITS. But nameRegexp uses lowercase name from toRustIdentifier:
const nameRegexp = new RegExp(`${name}_?\\d*`); // e.g. /logo_?\d*/
bitmapNames.filter((n) => nameRegexp.test(n)) // tests against "IMAGE_LOGO_BITS"
// /logo_?\d*/.test("IMAGE_LOGO_BITS") === false (case-sensitive)
// countWithSameName is always 0Result: two different paint layers both named "logo" both get varName = IMAGE_LOGO_BITS → duplicate static IMAGE_LOGO_BITS: &[u8] declarations → error[E0428]: the name 'IMAGE_LOGO_BITS' is defined multiple times.
Fix:
const countWithSameName = bitmapNames.filter((n) =>
nameRegexp.test(n.toLowerCase())
).length;| return normalized || fallback; | ||
| } | ||
|
|
||
| private getImports(layers: any[]): TEmbeddedGraphicsImports { |
There was a problem hiding this comment.
getImports doesn't respect include_images=false — emits a stale import.
When include_images=false, the static IMAGE_*_BITS declarations are suppressed (correctly), but getImports still returns image: true if there are paint/icon layers. The template emits:
use embedded_graphics::image::{Image, ImageRaw};...even though Image::new() calls reference variables that were never declared. With -D warnings (standard in embedded Rust CI), unused imports are errors.
Fix: Pass templateSettings into getImports and short-circuit:
private getImports(layers: any[], settings: typeof templateSettings): TEmbeddedGraphicsImports {
// ...
case 'paint':
case 'icon':
if (settings.include_images) image = true;
break;| | @!{layer.uid}; Text::with_baseline( | ||
| | !{textContent}, | ||
| | Point::new(!{layer.x}, !{layer.y}), | ||
| | MonoTextStyle::new(&FONT_6X10, !{layer.color}), |
There was a problem hiding this comment.
ℹ️ Font is hardcoded as FONT_6X10 — layer.font is ignored.
Whatever font the user selected in the editor doesn't affect the generated output. Every text layer always uses FONT_6X10 regardless.
If this is a known limitation for now, that's fine — but it should either be noted in a comment in the template or surfaced as a UI warning so users know their font selection won't carry through to the generated code.
| | .into_styled(!{style}) | ||
| | .draw(display)?; | ||
| when 'string' | ||
| - var textContent = layer.isTextVariable ? layer.text : `"${layer.text}"` |
There was a problem hiding this comment.
ℹ️ Text content isn't escaped for backslashes.
The text modifier in platform.ts escapes " → \" but not \. If a user types path\to as text content, the generated Rust string literal becomes "path\to". In Rust, \t is a tab and \p is an invalid escape sequence (compile error).
Fix in platform.ts text modifier (or here before the template renders textContent):
props.text = props.text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');| when 'paint' | ||
| | @!{layer.uid}; Image::new( | ||
| | &ImageRaw::<BinaryColor>::new(!{layer.imageName}, !{layer.w}), | ||
| | Point::new(!{layer.x}, !{layer.y}), | ||
| | ) | ||
| | .draw(display)?; | ||
| when 'icon' | ||
| | @!{layer.uid}; Image::new( | ||
| | &ImageRaw::<BinaryColor>::new(!{layer.imageName}, !{layer.w}), | ||
| | Point::new(!{layer.x}, !{layer.y}), | ||
| | ) | ||
| | .draw(display)?; |
There was a problem hiding this comment.
ℹ️ paint and icon cases are byte-for-byte identical — any future change will need to be made in two places.
Pug supports fall-through with multiple when labels:
when 'paint'
when 'icon'
| @!{layer.uid}; Image::new(
| &ImageRaw::<BinaryColor>::new(!{layer.imageName}, !{layer.w}),
| Point::new(!{layer.x}, !{layer.y}),
| )
| .draw(display)?;
I ended up wrapping each layer into separate function
I also add this button to wrap the code with simulator so user can run the generated code without actually pushing it to their microcontroller