Pictures dropped together were cascaded 20 px apart from the drop
point; once scaled to fit the folio they lay almost on top of each
other. They are now laid out side by side in a roughly square grid
over the folio's drawing area, inside the same 20 % margin, each
shrunk into its cell with its proportions kept. A single dropped
picture still lands on the drop point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dropped picture too large for the folio is now scaled to leave 20 %
of the drawing area free on every side, instead of being fitted to
the visible part of the view, and every dropped picture is kept inside
the frame -- a scaled-down one also off that margin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picture files (png, jpg, bmp, svg) dragged from the file manager onto a
folio are added there: the first centred on the drop point, the others
cascaded from it, and one undo step removes them all. A picture larger
than the visible part of the folio is scaled down to fit it. Files that
cannot be used are listed once after the others have been placed, and a
drop holding only other files (a .qet project) still reaches the main
window, which opens it.
The checks a picture must pass before it is embedded in the project
move into ImageDrop::load and are now shared by the drop, the add image
dialog and the script API: a regular file of at most 10 MB, and at most
64 megapixels, read from the header before any pixel is allocated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated by tools/qet-en-source/regenerate.sh, not edited by hand. The
tree is the fresh conversion; this commit only joins it to the PR's
history so the branch is never force-pushed.
The folio grid steps, diagrameditor/Xgrid and Ygrid, are read from the
settings in six places with a plain toInt() and used as they come. The
preferences page cannot store anything below 1, but a hand-edited or
damaged settings file can hold 0, a negative number or text:
- Diagram::snapToGrid() divides by the step, so 0 is a SIGFPE on the
first click that places a symbol;
- Diagram::drawBackground() runs "while (g_x % xGrid)" and then loops
"gx += xGrid", so 0 crashes every repaint and a negative step never
ends;
- the paste, duplicate and align paths divide by it or multiply with it.
Add foliogrid.h, a header-only helper: FolioGrid::step() reads a
settings entry and returns the built-in step (Diagram::xGrid, 10) when
the entry is missing, not a number, below 1 or above what an int
holds (QVariant::toInt() wraps such a value around). Every reader of
the two keys goes through it; the settings page, which only writes the
spin box values, is unchanged.
No file-format change, and no change for any step the preferences page
can produce.
Tests: tst_foliogrid covers the helper: 1, 10, "7", int max are kept;
missing, 0, -5, "ten", "", "nan", 99999999999 (as text and as a
number) and int max + 1 fall back; 7.9 rounds to 8 as before; and the
two settings keys through a QSettings scope of the test's own. The
helper is new, so the test cannot fail on master; the six readers are
the replacements in the diff.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Two position comparisons handed to std::sort were not strict weak
orderings, which std::sort requires; with the wrong kind of comparator
the sort is undefined behaviour (libstdc++ can read past the range,
MSVC debug builds assert "invalid comparator").
- comparPos(), used when renumbering the elements of a project, ended
with "<=" on x and y, so two elements at the same position (pasted at
the origin, placed by a script, stacked symbols) were each "before"
the other.
- The terminal numbering dialog compared positions with a 1 px
tolerance ("within 1 px counts as aligned, then compare the other
axis"), which is not transitive: terminals at x = 2, 1.1 and 0.2 give
a < b, b < c and c < a.
Move the comparisons into positionorder.h, a header-only helper:
xThenY()/yThenX() with "<", and roundedXThenY()/roundedYThenX(), which
round the positions to whole pixels first so that items a fraction of a
pixel apart still count as aligned, as the tolerance meant to, while
staying transitive. comparPos() keeps its folio and row-letter stages
and calls xThenY() for the last one.
No file-format change. Elements at distinct positions sort exactly as
before; the terminal order changes only for terminals less than a pixel
apart that straddle a half-pixel boundary.
Tests: tst_positionorder checks each order on a grid of awkward
positions by brute force (irreflexive, asymmetric, transitive, with a
transitive equivalence), the three-terminal cycle, twenty items at one
position, and that std::sort leaves the list sorted and intact. The
helper is new, so the test cannot fail on master; the call sites are
the two replacements in the diff.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
The interface text is now English at the source, so the untranslated
checkbox and tooltip read in English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AssignVariables::assignSequence() replaced the sequential-number
placeholders from 1 upwards with a plain QString::replace(). "%sequ_1"
is also the start of "%sequ_10", so with ten or more sequences a
formula's %sequ_10 became the first value followed by a "0". The same
held for every family (%sequf_, %seqt_, %seqtf_, %seqh_, %seqhf_,
%seqw_, %seqa_).
Run the loop from the highest number down to 1, so the longer
placeholder is gone before the shorter one is looked for.
No file-format change. Labels with fewer than ten sequences come out
exactly as before.
Tests: tst_tensequentialnumbers runs the binary's --export-bom on a
fixture whose element has the formula %sequ_10-%sequ_1 and the unit
values A to J, and expects the label "J-A". On master the label is
"A0-A".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Before the <sequentialNumbers> element, an element's or conductor's
sequential numbers were saved as the attributes sequ_1, sequf_1,
seqt_1, seqtf_1, seqh_1 and seqhf_1. The readers check for one of them
to take the old route, and all three lists had the same slip: sequf_1
twice, seqhf_1 never (Element::fromXml, Conductor::fromXml, and
readSequence() in the project database). A file whose only old
sequence was the hundred-folio one took the new route, found no
<sequentialNumbers>, and lost it; the database built its labels from
an empty sequence instead of refusing the fast path as it does for the
other five attributes.
Name seqhf_1 in the three lists.
No file-format change: nothing is written differently, and a file with
any of the other five attributes loads exactly as before.
Tests: tst_legacysequentialattributes runs the binary's --resave on a
fixture whose element and conductor carry only seqhf_1 (and a second
pair carrying sequ_1 as a control) and reads the <sequentialNumbers>
written back. The two seqhf_1 cases fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
BorderProperties::toXml() writes colsize and rowsize with "%1", so a
project whose default border has a width such as 60.5 saves it as
"60.5". fromXml() read both back with toInt(), which is 0 for a
decimal, and the folio clamps 0 to its 5 px minimum. New folios of such
a project got 5 px columns and rows. The folio's own reader,
BorderTitleBlock::borderFromXml(), already uses toDouble().
Read the two sizes with toDouble(&ok) and keep the previous value when
the attribute is missing, not a number, or not finite.
No file-format change: toXml() is untouched, and whole-number sizes,
which every example project has, load exactly as before.
Tests: tst_borderpropertiesxml compiles borderproperties.cpp alone and
reads 50, 60.5, 61.3 and 61.7 back, round-trips 60.5/80.25 through
toXml(), and leaves the value alone for a missing, text, nan or inf
attribute. Seven cases fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
ConductorProperties::toSettings() writes the conductor width with
QString::number(cond_size), e.g. "1.4", and fromSettings() read it back
with toInt(), which is 0 for any value that is not a whole number. A
default width of 1.4 set in the configuration became 0 on the next
start, so every new conductor was drawn with a pen of width 0.
Read it with toDouble(), and fall back to 1 when the stored value is
not a positive finite number (a hand-edited or truncated settings
file), as the other fallbacks in fromSettings() do.
No file-format change: this is the settings file only; the project
file's condsize attribute was already read with toDouble().
Tests: tst_conductorsizesetting compiles conductorproperties.cpp alone
and round-trips 2, 1.4, 0.4, 61.3 and 61.7 through toSettings() and
fromSettings() in a QSettings scope of its own; text, 0, -1, nan, inf
and a missing value give 1. Nine of the cases fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Diagram::toXml() writes freezeNewElement and freezeNewConductor as
"true"/"false", but Diagram::fromXml() read them with toInt(), which is
0 for both words. So "freeze new elements" and "freeze new conductors"
were off again on every folio after a project was saved and reopened,
since the day the flags were added.
Compare the attribute with "true" instead. The project-level flags in
QETProject already do this.
No file-format change: the attributes are written exactly as before,
and a file without them still loads with both flags off.
Tests: tst_foliofreezeflags runs the binary's --resave on a fixture
with one folio per combination (both, elements only, none, attributes
missing) and reads the saved attributes back. The two frozen folios
fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Before #1310, undoing a crop left the crop rectangle in place, so a
project saved afterwards has a <crop> but shows the uncropped picture.
Since #1368 the picture shown is computed from the original and the
crop, so the first mirror or colour key of such a picture applied the
old crop again, and undoing that edit showed the cropped picture.
The picture shown is always exactly the size of the crop. fromXml() now
drops a crop that does not match it: the crop becomes the whole
original, and when the shown picture is not the original's size either,
the shown picture becomes the original. Valid files load and save as
before.
New tst_imagestalecrop: a valid crop is kept; a stale one is dropped
for a shown picture of the original's size and of another size, where a
later crop cuts the picture that was shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Scale element... scales the whole element about its hotspot by a
factor picked from x0.5, x1.5, x2, x2.5, x3, x4. Only the factors that
leave every terminal on the 10 px folio grid are offered, so wires to a
scaled element stay straight. An element whose terminals are off the grid
today is offered only the factors that bring them on to it; if none does,
the dialog says so and OK is disabled.
Drawn parts are scaled with their existing handleUserTransformation(), as
the resize handles do. Texts, dynamic text fields and terminals are set
directly so that font sizes (whole points, 4 pt minimum), terminal name
offsets and line end sizes scale too, which the resize handles do not do.
One undo step restores the element.
Unlike "Import an element to resize", this needs no external program and
works on the element being edited.
Grew out of issue #1338.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The crop rectangle came back as the string "x,y,width,height", which a
script had to split and parse. elementGeometry() returns a map; the
crop now does the same, and an empty map for no such image.
Suggested in the review of #1310.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A picture remembers whether its pivot was placed by hand: a hand-placed
pivot is saved and kept through a resize, a default one is not saved
and recentres after a resize. Two actions changed that flag outside
their undo step:
- applyCrop() marked the pivot as default after moving it to the centre
of the kept region. After Ctrl+Z the pivot was back where the user
had put it, but marked as default, so the next save dropped it.
- dragging the pivot handle marked it as hand-placed. After Ctrl+Z the
pivot was back at the centre but still marked as hand-placed, so a
later resize left it at the handle's anchor corner.
pivotIsCustom becomes a property, and both actions put it into their
undo command next to rawPivot. tst_imagecropundo checks that a project
saved after undoing a crop keeps its hand-placed pivot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in master's DXF blocks (#1339) through #1354. The upright texts
now live in drawSymbol() too, and a turned symbol whose texts stay
horizontal is drawn in full like a mirrored one: an INSERT would turn
its texts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with the DXF blocks export (#1350) and the picture factory
leak fix. The mirror now lives in drawSymbol(); a mirrored symbol is
drawn in full rather than as an INSERT of its block, since a negative
INSERT scale would mirror its texts too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Configuration > Personnaliser... (also at the bottom of a toolbar's
right-click menu) opens one window with a tab for each page that sets
how the user works: Barres d'outils, Contenu des barres, Barre de
raccourcis, Raccourcis and Gestes de la souris, like SolidWorks'
Tools > Customize. The tabs are the same pages the configuration
dialog shows, which keeps them. OK applies every tab, Cancel none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The right-drag ring showed the shortcut bar's commands for the selection.
A new Preferences page, "Gestes de la souris", lets the user pick the
ring's commands per selection context, one per direction, and choose 4 or
8 directions. Commands are dropped from the list onto a slot of a ring
editor, or placed with a button; Delete empties a slot.
A context the user never edits keeps following the shortcut bar, and 8
directions stays the default, so nothing changes for anyone who does not
open the page. A picked list is positional: a direction left empty, or
holding a command this build lacks, stays empty instead of shifting the
others. Stored like ShortcutBarSettings: diagrameditor/gestures/<context>
id lists in QSettings, defaults not stored.
tst_gesturesettings covers the defaults, positional lists, the 4/8
setting, the overlay's sectors and empty slots, and the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Configure QElectroTech > Toolbar contents: pick a toolbar, drag or add
commands from the list on the left, remove them with Delete, reorder,
add separators, and add, rename or delete toolbars of one's own.
Reset to defaults gives a toolbar its original contents back.
Each toolbar is stored as a list of command ids in QSettings
(diagrameditor/toolbars/<objectName>), the same pattern the shortcut
bar uses. A toolbar the user never changed stores nothing and is built
exactly as before; an id no command carries any more is skipped. The
four buttons that are widgets rather than commands (handle size, text
grid, background colour, conductor colour) get ids of their own and
can each be on one toolbar at a time. Stored scripts can go on any
toolbar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The displayed pixmap and imageSource (original, crop rectangle,
transparent colours) were two undo values that had to change together,
although the pixmap follows from the source. An action that updated
one and not the other would bring back the bug fixed in #1310.
setImageSource() now recomputes the displayed pixmap, and crop, colour
key, mirror and replace push one undo step on imageSource alone. A
plain QUndoCommand holds the property change, so two identical actions
in a row stay two steps. Loading a project still shows the saved pixmap
as before.
tst_imagecropundo also checks the size of the saved picture after undo
and after redo.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The skip applied wherever setPermissions() failed, which on Linux and
macOS would hide a real failure. Only Windows cannot take read
permission away; elsewhere the call must succeed as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A project can now keep the texts drawn in its symbols, and the names of
their terminals, horizontal when a symbol is turned: Project properties >
General, "Garder horizontaux les textes des symboles pivotés". The box of
each text turns with the symbol; the text does not, and reads as it does
in the symbol itself.
It is a project setting, saved as <symbol_texts upright="true"/> and
only when on. A new project starts with it on; a project saved without
it (every existing one) reads with it off, so it looks and saves exactly
as before, and looks the same on every computer. The MCP server's new
projects start with it on too.
It builds on the mirror of #1354, which already redraws the texts of a
mirrored symbol readable: what the symbol does to its texts is now its
mirrors and, with the setting on, its turn (Element::symbolTextsTransform()).
ElementPictureFactory caches one drawing per such transform, terminal
names undo it the same way, and the DXF export places the texts alike.
The fields of a symbol (label, comment...) already keep their angle with
"Garder la rotation visuelle" and are left as they are.
Known limit: two texts stacked in a symbol end up side by side when it is
turned, and can overlap when kept horizontal; the setting can be turned
off for such a project.
Test: tst_uprightsymboltexts turns the symbols of a folio and checks the
angle of each motor's "M" in the DXF, with the setting off and on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > "Miroir horizontal" (M) and "Miroir vertical" (F), also in the
folio's right-click menu, mirror the selected symbols in place. The keys
are the ones the element editor uses for the same two actions.
An element keeps two mirrors about its own axes, applied before its
rotation, and saves them as mirror="horizontal|vertical|both" on its
<element> (written only when set, so other projects save byte for byte
as before). On a symbol turned by 90 or 270 degrees, a mirror of the
folio is the other mirror of the symbol itself, so the rotation never
changes: a label kept upright does not swing round, and "Pivoter" still
turns a mirrored symbol clockwise.
- Terminals face the mirrored way (Terminal::orientation()), so wires
follow.
- The symbol stays where it was: its centre is kept, on the grid, since
the hotspot is often a corner.
- Texts read normally. The element's texts, text groups and cross
reference are mirrored a second time about the centre of their own box
(Element::keepReadable()), and ElementPictureFactory draws the texts
of the symbol itself the same way, in a cached picture per mirror.
Groups and cross references held at the bottom of the folio stay
centred under their element.
- DXF export mirrors the symbol's lines, arcs and texts.
- Scripting: qet.mirrorElement(folio, uuid, vertical) and
qet.elementMirror(folio, uuid); live mode may run both menu commands;
the MCP server gets a mirror_element op and qet_diff reports mirrors.
Not done: the parts of a PLC table drawn at run time
(Element::drawPlcTable()) are not kept readable on a mirrored PLC; the
project database has no column for it, as it has none for the rotation.
Test: tst_scriptmirror mirrors a symbol through --run, checks every
terminal's side and facing, the round trip, undo, a save and reload,
and a symbol turned by 90 degrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With symbols as blocks, every non-empty information field of a symbol
(label, manufacturer, reference, supplier, quantity...) is written as a
hidden attribute of its INSERT, tagged with the field's name. A CAD
program can then list the parts from the drawing, as with AutoCAD's
attribute extraction; hidden, they change nothing on screen, in any
reader. A field already written as a visible attribute (with
--dxf-attributes) is not repeated. The label formula is left out: it is
how the label is made, not part data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With symbols as blocks and the new "DXF : textes des symboles en
attributs" option (--dxf-attributes on the command line), a symbol's own
texts (its label, function, comment...) are attributes of its INSERT
instead of loose texts, so a CAD program moves them with the symbol and
can list or edit them. Each is written exactly where, and as, the text
was. A text showing an information field is tagged with its name
(LABEL, FUNCTION...), a typed text TEXT1, TEXT2...; a text's second line
gets _2. The block defines each tag once, placed where the first symbol
has it.
Its own option, off by default, because LibreCAD (2.2.1.5) does not
show attributes: the labels would vanish there. AutoCAD shows them.
The text geometry moves into textLines(), shared with the text loop,
so the attributes and the plain texts cannot drift apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With "DXF : symboles en blocs" ticked in the export dialog, or
--dxf-blocks on the command line, each symbol definition a folio uses is
written once as a DXF block and every placed symbol as an INSERT of it.
A CAD program then selects, counts and replaces a symbol as one object.
The block is drawn by the same code as a symbol drawn in full, as if
the symbol sat unturned at the folio's origin, which is DXF (0,
sheetHeight): that is the block's base point, so the INSERT carries the
symbol's position and quarter-turn and nothing else. Blocks have to come
before any entity, so the export collects them first and dxfBegin()
writes them in its BLOCKS section. Entities in a block keep their own
layers. Off by default; with it off the file is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>