Skip to content

Add support for Rust embedded graphics#233

Open
yappiverse wants to merge 5 commits into
sbrin:developfrom
yappiverse:rust-embedded-graphics-support
Open

Add support for Rust embedded graphics#233
yappiverse wants to merge 5 commits into
sbrin:developfrom
yappiverse:rust-embedded-graphics-support

Conversation

@yappiverse
Copy link
Copy Markdown
Contributor

I ended up wrapping each layer into separate function

image

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

image image

@yappiverse yappiverse marked this pull request as draft April 22, 2026 01:48
@yappiverse yappiverse changed the base branch from main to develop April 22, 2026 01:48
@yappiverse yappiverse marked this pull request as ready for review April 22, 2026 01:48
@sbrin
Copy link
Copy Markdown
Owner

sbrin commented Apr 22, 2026

@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

@sbrin
Copy link
Copy Markdown
Owner

sbrin commented Apr 22, 2026

Screenshot 2026-04-22 at 10 49 23 @yappiverse the code looks broken when Layer titles enabled

Copy link
Copy Markdown
Owner

@sbrin sbrin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +104 to +106
if (typeof props.color === 'string') {
props.color = this.packColor(props.color);
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 here

Also: add a test that constructs a black rectangle using EmbeddedGraphicsPlatform.features and asserts the output contains BinaryColor::Off.

Comment on lines +127 to +128
each layer in layers
| !{pad}!{layer.functionName}(display)?;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment on lines +74 to +75
const nameRegexp = new RegExp(`${name}_?\\d*`);
const countWithSameName = bitmapNames.filter((currentName) => nameRegexp.test(currentName)).length;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: dedup regex never matches — two images with the same name generate duplicate static declarations (Rust compile error).

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 0

Result: 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 {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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}),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Font is hardcoded as FONT_6X10layer.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}"`
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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, '\\"');

Comment on lines +101 to +112
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)?;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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)?;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants