Fix flip/mirror bugs: preserve on morph, keep pin text upright, flip non-rotatable ports#412
Merged
Merged
Conversation
The `.rotatable` metadata flag / `isRotatable()` getter was a misnomer: when it is false the element still rotates and flips -- it just keeps its icon upright and repositions its ports around the centre instead of transforming the graphic. Rename the constraint/metadata field and the getter to `rotatesGraphic` so the name matches what it controls. Pure mechanical rename across the metadata, getter, all call sites (incl. the MCP handler, whose guard behaviour is unchanged here) and the affected element tests; no behaviour change.
MorphCommand::transferConnections() copied only a subset of element properties (rotation/label/color/frequency/trigger), so morphing a flipped element silently lost its mirror state -- and the audio file, volume and clock delay were dropped too. Carry the flip flags over unconditionally (flip is a base property of every element) and copy audio/volume/delay guarded by the existing hasAudio()/hasVolume()/hasDelay() capability checks, matching the surrounding pattern. The state round-trips through undo automatically. Covered by testMorphPreservesFlip and testMorphPreservesVolume (a Buzzer->AudioBox morph carrying a non-default volume).
…ning their ports Flipping a pushbutton -- or any non-rotatable input/output element (switch, LED, VCC/GND, clock, buzzer, 7/14/16-seg displays) -- did nothing: FlipCommand::redo() applied the mirror only when rotatesGraphic(), and these declare rotatesGraphic=false, so only their position was mirrored (a no-op for a lone element). Rotation already repositioned their ports. Make flip behave the same way: mirror the PORTS to the opposite side while keeping the graphic upright. A new GraphicElement::applyPortOrientation() applies the combined Rotate(centre, angle) then Flip about the centre to each port; it is recomputed from the flags each call (involutive) and reproduces the previous rotation when not flipped. rotatePorts() (now argument-less), the base updatePortsProperties() and the load path all route through it, and the FlipCommand gate is removed. Display7/14/16 override updatePortsProperties without chaining to the base, so they re-apply the orientation explicitly. The graphic is never mirrored, so a 7-seg display never renders reversed. This also reconciles the MCP with the GUI: the context menu rotates these elements fine, but the rotate_element handler rejected them and setElementProperties ignored a rotation request. Drop those guards so the MCP repositions the ports exactly as the GUI does, and fix the now-stale negative-case test. Covered by testFlipNonRotatablePorts, testRotateNonRotatablePorts and testFlipRotateNonRotatablePort.
…tating Latches and flip-flops bake their pin letters (D, E, Q, S, R...) into the SVG as <text>. The item-level flip (scale(-1)) and rotation rotated those glyphs with the body, so they read backwards or sideways. When an element is flipped or rotated, derive its pixmap from a variant rendered out of the same SVG with each <text> node counter-oriented about its own centre (Rotate(-angle) after the flip) via QSvgRenderer. The item transform then mirrors/rotates the whole pixmap: the body moves but the per-text counter cancels the glyph transform, so the letters stay upright. Counter-transforming about each text's own centre composes cleanly with any parent transform. Unflipped, unrotated and text-free SVGs go through the plain base pixmap unchanged (byte-identical), and variants are cached per (path, angle, flip). Two robustness measures are folded in: the counter-orientation is gated on rotatesGraphic() -- a non-rotatable element keeps its icon upright, so its text must never be corrected -- and the cache key canonicalizes the angle to [0,360) so equivalent rotations share one entry. Covered by testFlipTextPixmapVariant, testRotateTextPixmapVariant and testFlipRotateTextPixmapVariant.
…zoomed Element appearances were rasterised once into a fixed ~64x64 QPixmap at load time, and paint() blitted that bitmap, so zooming scaled the raster up and blurred every SVG element. Render the SVG as vectors instead: each element holds one parsed QSvgRenderer (rebuilt only when its appearance or flip/rotate state changes, via applyPixmapOrientation) and paint() calls QSvgRenderer::render() at the painter's current resolution. Qt's existing DeviceCoordinateCache re-renders per zoom level, so the body stays sharp at any magnification. Raster (PNG/JPG) appearances fall back to drawPixmap; the rasterised m_pixmap is kept for sizing, palette thumbnails and the flip/rotate test probe. Pin labels stay upright under rotation/flip via the same orientSvgTextNodes correction, now also fed to the renderer (gated on rotatesGraphic()).
…to Sans everywhere The inverted outputs (Q/P/C with overline) on latches & flip-flops drew the overline as a separate hand-drawn <path> grouped with the letter, so on rotation the bar desynced from its letter. Collapse each into a single <text> whose letter carries a combining overline (U+0305); the bar is now part of the glyph and rides with the letter through the counter-orientation, staying upright and attached at any angle. font-family 'Droid Sans' -> 'Noto Sans' throughout. Bundle Noto Sans (the maintained successor of Droid Sans, which the SVGs already named but was never shipped -- the bar was hand-drawn precisely because Droid Sans has no combining overline). Register it in the Application constructor so it loads on desktop and in the test runner before any element pixmap is built. Crucially, ship it on EVERY platform: Fonts.qrc previously compiled only into the WASM build, so on desktop the glyph silently fell back to a system font. Split the resources -- Fonts.qrc now carries only NotoSans-Regular and is compiled everywhere, while the large per-script CJK/etc. fonts move to a new WASM-only ScriptFonts.qrc. NotoSans-Regular.ttf is OFL, covered by the existing App/Resources/Fonts/OFL.txt.
Remove dead Inkscape "Glow" <defs> filter blocks and leftover empty <text> placeholder nodes from the memory-element SVGs, and re-center each pin letter on its column using Noto Sans 18px advances (x = columnCenter - advance/2; baselines normalized to y=23/52). Left inputs, mid preset/clear, and right outputs each share a calibrated column centre, applied uniformly across the D/SR latches and D/SR/JK/T flip-flops in both Dark and Light themes so the labels line up consistently. The clock triangle and overline glyphs are unchanged.
…r-identical) Ran SVGO 4 with a conservative allow-list config (no geometry/path/viewBox/style/color plugins) over all 191 resource SVGs, removing editor-only bloat: XML comments, <sodipodi:namedview>, <metadata>/RDF, every inkscape:/sodipodi: attribute (including inkscape:export-filename, which leaked the original author's local /home/davi/... paths in 121 files), unreferenced <defs> filters/gradients, unused ids, and now-unused xmlns declarations. Most files shrink dramatically (e.g. counter_a.svg 109 -> 6 lines). The pass is render-identical: QSvgRenderer ignores all the removed content, and path/ coordinate data, styles and used gradients/filters are left byte-for-byte intact; verified by a QSvgRenderer pixel-compare of all 191 SVGs (0 differences at 256px). The 16 memory SVGs additionally had a stray, unbound sodipodi:role="line" removed. dolphin_icon.svg's embedded PNG is kept as clean single-line base64 (SVGO 4 line-wraps and can corrupt whitespace inside base64 data URIs). svgo.config.mjs is added so the cleanup is reproducible, and documents that hazard.
…a path The dolphin in dolphin_icon.svg was a 64x64 embedded PNG (raster -- blurry when zoomed, and the last embedded base64 in the repo). Traced its flat #181638 silhouette with potrace into a single vector <path> (96% IoU vs the original mask), replacing the <image>. The icon is now fully vector -- blue-circle rects/gradient + dolphin path -- crisp at any zoom, with no base64.
…rrectly Qt's QSvgRenderer ignores SVG clip-path entirely, so three locale flags rendered wrong in-app while looking correct in browsers. Bake each clip into explicit path geometry (QPainterPath boolean ops) and drop the clip-path attributes + <clipPath> defs: - brasil.svg: the white "Ordem e Progresso" band (clipped to the blue disc) overflowed the flag, and the letter G in "PROGRESSO" (an O-ring carved by a clip) was malformed. Band = disc intersect outer band-circle minus inner band-circle (4 arcs); G = O-ring with the arm notch (~5 commands). - english.svg: the red St Patrick saltire (full diagonals clipped to the counterchanged halves) drew as a symmetric red X; baked as stroke(diagonals,4) intersect the 4 counterchange triangles. - slovak.svg: the blue hills (clipped to the shield) overflowed; baked as hills intersect shield (compact polygon; the shield path #s is kept in <defs> for its two <use> refs). Verified by rendering each asset through QSvgRenderer offscreen and pixel-matching the baked geometry against the QPainterPath intersection ground truth (edge anti-aliasing only).
d6e3c4f to
cbaafa4
Compare
…y zoom) ICs and TruthTables composed their whole body into a fixed-resolution QPixmap in generatePixmap() and blitted it in an overridden paint(), so the body -- the DIP package rect, the rasterised mascot/table logo, the shadow and (IC) the pin-1 notch -- blurred when zoomed, bypassing the vector rendering the other SVG elements gained. Draw the body straight onto the painter every paint() instead: the rounded-rects and the notch are vector primitives, and the logo renders through a shared lazily-built QSvgRenderer rather than a pre-rasterised QPixmap. generatePixmap() now only produces a transparent, correctly-sized m_pixmap so pixmapCenter()/boundingRect() are unchanged; the ports and boundingRect() are untouched, and the existing DeviceCoordinateCache caches the crisp result per zoom level.
Re-ran the update_translations target after the history rework and the IC/TruthTable change: only the <location line="N"> references shift to track the new source line numbers (548 strings, 0 new or removed). No translatable text or translations change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Flip/mirror correctness for circuit elements — plus the vector-rendering (including ICs and truth tables), font, and SVG-asset work that grew out of fixing the baked-in pin labels. Organised as a focused, self-verifying commit sequence.
1. Rename
rotatable→rotatesGraphic(no behaviour change)The
.rotatable/isRotatable()name was misleading: arotatable = falseelement still rotates and flips — it just keeps its icon upright and repositions its ports instead of transforming the graphic. Renamed the constraint/metadata field and the getter torotatesGraphicso the name matches what it controls. Pure mechanical rename across the metadata, getter, every call site (incl. the MCP handler) and the affected element tests.2. Preserve flip / audio / volume / delay when morphing
MorphCommand::transferConnections()copied only a subset of element properties, so morphing a flipped element silently un-flipped it — and the audio file, volume and clock delay were dropped too. Flip is now carried over unconditionally (a base property of every element); audio/volume/delay are copied guarded by the existinghasAudio()/hasVolume()/hasDelay()checks, and the state round-trips through undo.3. Flip & rotate fixed-graphic inputs/outputs by repositioning their ports (+ reconcile MCP)
Flipping a non-rotatable input/output — pushbutton, switch, LED, VCC/GND, clock, buzzer, 7/14/16-seg display — was a no-op:
FlipCommand::redo()mirrored only elements that rotate their graphic. Flip now mirrors the ports to the opposite side while keeping the graphic upright, exactly how rotation already repositions them. A newGraphicElement::applyPortOrientation()applies the combined rotation + flip to each port (involutive; reproduces the previous rotation exactly when not flipped), and the old gate is removed. A pushbutton's output port moves to the left edge on horizontal flip; a display's pin columns swap; wires follow. The graphic is never mirrored, so a 7-seg display never renders reversed.This also reconciles the MCP with the GUI: the context menu rotates these elements fine, but the
rotate_elementhandler rejected them andsetElementPropertiesignored the request. Those guards are dropped so the MCP repositions ports exactly as the GUI does; the stale negative-case test is fixed.4. Keep baked-in SVG pin text upright when flipping and rotating
Latches/flip-flops bake their pin letters (
D,E,Q,S,R…) into the SVG as<text>, so the item flip (scale(-1)) and rotation rendered them backwards/sideways. When flipped or rotated, the displayed pixmap is derived from a variant rendered out of the same SVG with each<text>counter-oriented (Rotate(-angle)after the flip) about its own centre: the body moves to the mirrored/rotated side but the letters stay upright. No asset edits; unflipped/unrotated/text-free elements are byte-identical. Two robustness measures are folded in — the correction is gated onrotatesGraphic()(a non-rotatable element's text is never corrected) and the variant cache key canonicalizes the angle to[0,360)so equivalent rotations share one entry.5. Crisp vector element rendering
Element SVGs were rasterized once into a fixed ~64×64
QPixmapand scaled by the view, blurring when zoomed in.GraphicElement::paint()now renders the SVG as vectors viaQSvgRenderer(one parsed renderer per element, rebuilt only on appearance/flip/rotate change; Qt's existingDeviceCoordinateCachere-renders per zoom level), so every SVG element stays sharp at any magnification. Raster (PNG/JPG) appearances fall back todrawPixmap; pin labels stay upright via the same<text>counter-orientation as §4.6. Inverted outputs as real glyphs + Noto Sans bundled on every platform
The inverted-output overlines (
Q̄/P̄/C̄) were hand-drawn<path>bars that desynced from their letter under rotation; replaced with a single combining-overline glyph (U+0305) that rides with the letter through the counter-orientation. Noto Sans — the maintained successor of Droid Sans, which the SVGs already named but was never shipped — is bundled and registered inApplication's constructor, and now ships on every platform:Fonts.qrcwas previously compiled only into the WASM build, so on desktop the glyph silently fell back to a system font. Resources are split: desktopFonts.qrc= NotoSans-Regular; WASM-onlyScriptFonts.qrc= the large CJK/script fonts.7. Re-center memory SVG pin labels
Removed dead Inkscape "Glow"
<defs>filter blocks and leftover empty<text>placeholders from the memory-element SVGs, and re-centered each pin letter on its column (Noto Sans advances; baselines normalized to y=23/52) across the D/SR latches and D/SR/JK/T flip-flops in both Dark and Light themes. The clock triangle and overline glyphs are unchanged.8. Repo-wide SVG cruft scrub (render-identical)
Stripped Inkscape editor cruft from all 191 SVGs via a conservative SVGO allow-list (no geometry/path/style plugins): XML comments,
<sodipodi:namedview>,<metadata>/RDF, everyinkscape:/sodipodi:attribute (includinginkscape:export-filename, which leaked the original author's local/home/…paths in 121 files), unreferenced<defs>, and unused ids/namespaces. Most files shrink dramatically (e.g. a counter segment 109 → 6 lines); ~−20k lines net. The dolphin's embedded PNG is kept as clean single-line base64 (SVGO 4 line-wraps and can corrupt whitespace inside base64 data URIs).svgo.config.mjsmakes the cleanup reproducible and documents that hazard.9. Vectorize
dolphin_iconTraced
dolphin_icon.svg's 64×64 embedded PNG — the last embedded raster in the repo — into a single vector<path>(potrace, 96% IoU vs the original mask), replacing the<image>. The toolbar icon is now fully vector, crisp at any zoom, with no base64.10. Locale flags render correctly in Qt (clip-paths baked to geometry)
Qt's
QSvgRendererdoes not supportclip-path(confirmed against Qt's own docs — the lone gap;filter/feGaussianBlur,mask,pattern,symbol,markerare all parsed since 6.7). Three locale flags rendered correctly in browsers but wrong in-app. Each clip is baked into explicitQPainterPath-boolean geometry, dropping theclip-pathattributes and<clipPath>defs while staying pixel-identical in spec-compliant renderers:#sis kept in<defs>for its two<use>refs).11. Crisp IC and TruthTable bodies
§5 missed ICs and truth tables: each composed its whole body into a fixed-resolution raster
QPixmap(the DIP package rect, a rasterised mascot/table logo, the shadow and — for IC — the pin-1 notch) ingeneratePixmap()and blitted it in an overriddenpaint(), so the body blurred when zoomed. Both now draw the body directly on the painter every paint — the rounded-rects and the notch are vector primitives, and the logo renders through a shared lazily-builtQSvgRendererinstead of a pre-rasterised pixmap.generatePixmap()keepsm_pixmaponly as a transparent, correctly-sized placeholder (sopixmapCenter()/boundingRect()and the ports are unchanged), and the existingDeviceCoordinateCachecaches the crisp result per zoom level.12. Refresh translation source locations
Mechanical
update_translationspass after the rework: only the<location line="N">references across the 39.tsfiles shift to track the new source line numbers (548 strings, 0 new or removed). No translatable text or translations change.Tests & verification
testMorphPreservesFlip/testMorphPreservesVolume— flip, and a non-default volume, survive morph + undo (Buzzer↔AudioBox).testFlipTextPixmapVariant/testRotateTextPixmapVariant/testFlipRotateTextPixmapVariant— a flipped / rotated / both latch gets a text-corrected variant distinct from base, rotate-only and flip-only; round-trips; no-op for a text-free gate.testFlipNonRotatablePorts/testRotateNonRotatablePorts/testFlipRotateNonRotatablePort— a pushbutton's (andDisplay7's) port mirrors/rotates to the opposite side while the item transform stays identity, and round-trips on undo.QSvgRendererpixel-compare of all 191 SVGs (pristine vs cleaned) shows 0 differences at 256px. (The same compare caught — and this PR avoids — a base64 character SVGO had mangled in the dolphin's PNG.)QSvgRenderer(before: band/saltire/hills overflow or malformed; after: correct) and pixel-matching the baked geometry against theQPainterPathintersection ground truth (edge anti-aliasing only).ctest --preset debug, 176/176.