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>
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>
A symbol text on a folio with its width on "Auto" kept every line of a
multi-line text on the left, whatever its alignment: a QTextDocument
with no text width has no width to centre or right-align lines in. It
only worked once the user gave the text a width wide enough.
With the width on Auto and a centre or right alignment, give the
document the width of its longest line. The size of the text does not
change, only where the shorter lines sit. The fit is redone when the
text, the alignment, the width or the font changes; on a font change
the text's anchor is kept, as the font change itself does.
textWidth() now returns the width the user set (-1 for Auto), the value
that is saved, rather than the document's, as IndependentTextItem
already does. Readers (properties panel, resize-handle undo, script
API, project database) see -1 for Auto as before.
Checked: every folio of the 25 example projects renders identically to
master (136 PNGs); a centred and a right-aligned two-line text now
render aligned; changing their font size leaves the same box and anchor
as master, and without the font-change refit the lines wrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
itemChange() removed the corner handles when the text left the scene
but did not bring them back when it returned still selected, which Qt
allows (undoing a scripted delete does it) without a selection change.
Refresh on every scene change. The two handle helpers are protected:
nothing outside the class calls them.
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>
Found by review of #1354:
- Terminal names (shown on the folio) read backwards on a mirrored
symbol: Terminal::paint() now mirrors them back about the centre of
their box, in the frame they are drawn in.
- "Reload the drawings of the symbols" left mirrored symbols with the old
drawing: reloadPicture() refreshes the mirrored pictures too.
- Turning a group of texts on a mirrored symbol left the group's
readable mirror about the centre of its old box until the next layout:
ElementTextItemGroup::setRotation() reapplies it, as texts already do.
- The mirrored build of a symbol's drawing leaked the texts of its
primitives, which only the drawing without mirror keeps.
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>
A selected free text gets the corner handles of the previous commit:
dragging one sets the width the text wraps to, keeping the opposite
corner in place; double-clicking a handle goes back to the automatic
width. One undo step per drag, on the folio's undo stack. The handles
are hidden while the text is typed in.
DiagramTextItem shows the handles for a subclass that asks for them
(textResizeHandlesWanted()); only free texts do for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- setTextWidth() returns without a signal when the width does not
change, which is also the case on every load of a text without one.
- Say why IndependentTextItem::textWidth()/setTextWidth() hide the
non-virtual QGraphicsTextItem functions, and what that means for code
holding a base pointer.
- The fixture no longer carries a /tmp path from the machine it was
made on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A free text (IndependentTextItem) gets a textWidth property: -1 is the
automatic width (one line, or lines broken by hand, as today); a
positive width makes the text wrap between words. Its top-left corner
stays in place. The property is what a spin box and resize handles will
set in follow-up changes.
File format: the width is saved as text_width on <input>, only when one
is set, so a project without widths is saved byte-identical
(tst_resaveunchanged). An older QElectroTech ignores the attribute and
shows the text on one line, with no load error.
DiagramTextItem::setHtml() no longer widens a centred or right-aligned
rich text to idealWidth() + 40 when the text has a width of its own;
with the automatic width it still does.
Test: tst_freetextwidth (--resave keeps the width only where set,
--export-svg draws the text wrapped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- replaceImage(), mirror() and setTransparentColor() no longer add a
dummy QUndoCommand child: the imageSource child already keeps
QPropertyUndoCommand::mergeWith() from merging two of them.
- The explanation of the position and pivot maths moves from crop() to
applyCrop(), where that code now lives; the stale older doc block of
crop() goes.
- applyCrop() returns false when nothing was cropped, and
qet.cropImage() passes that on. tst_imagecropundo checks it for the
current crop, an empty rectangle and one outside the picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stacking added in #1287 tested the crosses' bounding rects, so
crosses whose frames or empty margins touched were raised although no
text was hidden (the list display case found in review). The texts each
cross really draws are now recorded while its geometry is built, and a
cross is raised only when one of its texts would be drawn over a text of
another one.
A new "Empiler les références dont les textes se chevauchent" checkbox
in the cross reference properties, per type, turns the stacking off;
the crosses of that type are then only centred, as before #1287. It is
saved in the project (xref attribute "stackoverlapping") and in the
settings, and is on when absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cropping, keying out a colour, mirroring and replacing a picture set
its crop rectangle, transparent colours and original directly and put
only the displayed pixmap into the undo command. After Ctrl+Z the
picture looked right, but a save still wrote the undone crop and
colours, and the picture came back cropped once reopened; a later
crop or colour dialog also started from the undone values. The three
are now one property, imageSource, changed in the same undo step as
the pixmap.
The crop itself moves out of the dialog into applyCrop(), so that it
can be applied without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: in list mode `drawAsContacts()` adds a margin (`adjust(-30, -4, 4, 4)`) to `boundingRect()`, and the horizontal test in `stackAtBottom()` uses that rect. Two lists overlapping only inside the empty margin (1.4 units in my log) count as overlapping. `gap` only changes how far the raised ones move, not which ones.
Fix: test the content only in list mode (`rect.adjust(30, 0, -4, 0)`), with the two margins as constants shared with `drawAsContacts()`.
Result: all references stay at the bottom (980 -> 980), none raised by mistake, also when moving an element sideways.
Checked: [to complete: same-column coils in list mode still stack, cross mode unchanged]
A text group kept at the bottom of the page, on a master element,
connects to its project's XRefPropertiesChanged with a lambda that has
no context object. The connection belongs to the project, so it
outlives the group: once the group is deleted (paste a coil, undo,
then make any other edit), changing a cross-reference setting in the
project properties calls QTimer::singleShot() on the freed group and
QElectroTech crashes.
Pass the group as the context of that connection and of the
linkedElementChanged one beside it, so Qt removes both when the group
goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A conductor draws a junction dot at each of its bends that lies on
another conductor. It only looked at the conductors on its own two
terminals, so a bend lying on a conductor of the same potential that
shares no terminal with it got no dot. Dragging the horizontal parts of
a chain of conductors onto one line, as in the report, does exactly that.
Look at every conductor of the potential on the folio instead
(relatedPotentialConductors(false)). The folio scene uses NoIndex, so a
scene lookup at each bend scans every item and made export of the
366-conductor Polonez example 60% slower; walking the potential costs
nothing measurable there (2.08 s vs 2.11 s) or on a 153-conductor
potential (0.97 s both).
The test for one other conductor moves into bendMakesJunction(). It
also stops a point being appended once per matching segment: master drew
the dot above K2 in the fixture ten times.
Across the 23 examples (133 folios) exported to SVG, 6 dots are added,
all at T-joints that had none, and none removed.
tst_junctiondots exports fixtures/junction_dot_shared_potential.qet and
checks for both dots; it fails on master with the dot above K3 missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With "snap to bottom", each master's cross was centred under it at the
bottom of the folio on its own, so two coils in one column drew their
crosses on the same spot and the references printed over each other.
The crosses of a folio are now placed together: when two would overlap,
the one of the higher element goes above the other, so a column of
coils gets its crosses in the same order. Crosses that do not overlap
stay where they were. Moving an element sideways now re-places them
too, since it can move a cross into or out of another's column.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schemas now get saved with a UUID and do not have to be looked up according to the form. They can be renamed and it gets reflected in existing elements. The GUI for
editing them is now much easier, e.g. forms can be reshuffled. A formula can be applied. Many options now also for conductors and sheets.
Discussion #1158: a real terminal takes one or two wires (four with
double ferrules), a folio report is a virtual point that carries one, but
QElectroTech lets any number of wires be connected anywhere.
Project properties > General gets a "Conducteurs par borne" group:
- the most wires a terminal may take (0, the default, is no limit);
- "one wire per folio report".
When set, a new wire that would go past the limit is refused: the
terminal shows red while dragging, as for any refused link, and a tooltip
says why on release. Auto-connect on placing, moving or multi-pasting a
symbol skips full terminals, and qet.addConductor() refuses with its own
message. Wires already drawn, opening, pasting and undo are never
refused.
Settings > General gets a master switch, on by default, that turns every
rule of this feature off in every project; the project group is greyed
with a note while it is off.
The rules live in wiringrules.cpp. The project setting is saved as
<wiring_rules .../> only when a rule is on, so a project that never used
it saves exactly as before; an older version drops it, which means "off".
tst_wiringrules covers the rules, the save round trip and, through the
real binary, that a wire past the limit is refused unless the master
switch is off (checked to fail with the limit check removed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
A submenu under View (Affichage > Afficher) with one tick per kind:
symbol texts, wire texts, free texts, shapes, pictures, tables and
cross-references. Unticking one hides it on every folio of every open
project; the status bar says how many kinds are hidden. Session only,
not saved in the project.
Each item is tagged with its kind by its constructor (ShownKinds::tag),
so an item created while its kind is hidden starts hidden whatever
created it. A tag rather than type(): the cross-reference under a
contact's label is a plain QGraphicsTextItem.
Items that already hide themselves (wire text switched off, one text per
potential, a cross-reference snapped elsewhere) now go through
ShownKinds::setVisible(): the kind state can only veto a show, and
apply() re-shows only what it hid itself (hidden_key), never what an
item hid. Conductor::updateTextVisibility() replaces the same visibility
rule pasted in diagrampropertiesdialog.cpp and qetscriptapi.cpp.
Hidden items are not selectable (Qt), so Select All, copy and delete
skip them. Print, PDF and image export render the scene and leave them
out; the DXF export walks items itself and skips them through
ShownKinds::isHidden(). CrossRefItem::linkedChanged() keeps following
its slaves while hidden only by kind, so its label is right when shown.
Known limitation: QGraphicsScene::itemsBoundingRect() still counts hidden
items, so Zoom to fit and borderless image export keep their space.
Verified: tst_shownkinds (12 cases, two mutation checks) and ctest 55/55;
GUI hide/show of symbol texts, wire texts (incl. one text per potential),
free texts, shapes and cross-references returns pixel-identical folios;
with nothing hidden, --export-dxf of the 24 examples is byte-identical to
master (133 files) and --export-pdf renders identically (133 pages).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
- Rename the `near` lambda in Conductor::setPathPoints() to `isNear`:
`near` and `far` are empty macros in the Windows headers (minwindef.h).
- Reword the log when setPathPoints() refuses a route. "does not join the
two terminals" named only one of its reasons; the router always returns
the two terminals and at least one exit point, so a refusal means the
route is not a run of horizontal and vertical segments between them.
- README: running along another wire is penalised, not forbidden, so two
wires can end up drawn on top of each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new conductor gets the default two or three straight segments, which
run through whatever symbol or wire lies between its terminals; scripts
then fix it segment by segment with moveConductorSegment().
ConductorRouter finds an orthogonal path on the folio grid that leaves
and enters each terminal in its own direction, keeps clear of every
element's rectangle, stays inside the border, and charges for bends and
for running along or crossing other wires. qet.routeConductor() and
qet.routeConductorBetween() apply it through Conductor::setPathPoints(),
which pushes the same ChangeConductorCommand a handle drag does, so the
path is saved, survives a reload, and one undo restores the default.
Where no route exists the wire keeps its path and the call says so.
qet-mcp: add_conductor takes "route": "avoid", and a route_conductor op
reroutes an existing conductor (by terminal or by uuid). The router
methods are required only by an edit that routes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With "one text per potential" on, the number is drawn on the
potential's longest conductor. When two conductors tie for longest,
longestConductorInPotential() kept whichever it met first, iterating a
QSet<Conductor *> in pointer order -- which changes from run to run. The
same file, exported twice, could put the number on either conductor:
same numbers, different PDF.
A tie is now broken by where the conductors' ends are on the folio, then
by uuid. Not by uuid first: a file with no conductor uuids gets fresh
ones on every load, and a project generated again by a script gets new
ones each time, while the drawing is the same.
tst_potentialtextcarrier exports a fixture with such a tie eight times;
before the fix it failed in each of three tries.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A contact (slave) not linked to a coil has no element to take its label
from: DynamicElementTextItem::elementUseForInfo() returns null. Since
8985babfe (#978) the composite-text branch of updateLabel() called
element->actualLabel() without checking, so a text built from %{label}
on such a contact crashed QElectroTech while the project was loading
(QETProject::refresh -> Diagram::refreshContents). The project could not
be opened again.
Check the element as the ElementInfo branch above already does; with no
element, %{label} shows empty, as it did before 8985babfe.
Test: tst_resaveunchanged opens a blank project holding one unlinked
contact with a %{label} text. It fails without the fix (the --resave
crashes) and passes with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The middle handle of a half arc sits exactly on the middle of the top
or bottom edge of the ellipse -- a resize handle in Size mode, a skew
handle in RotateSkew mode -- and, drawn last, it covered that handle
(arummler, discussion #1203). It is now shown in Size mode only, where
the resize handle under it is hidden instead: the middle handle already
changes that height, with the ends kept in place. RotateSkew mode shows
its skew handle again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The diagram toolbar gets an "Ajouter un arc" button next to the
ellipse. Click one end, then click the other end at the height the arc
should reach: the result is a half arc bulging up or down from the line
between the two clicks (Shift: a true half circle). It is not filled,
since a fill would close it into a half disc.
A selected half arc shows one more handle, in the middle of the curve.
Dragging it makes the arc deeper or flatter while both ends stay put;
dragging it across the line between the ends turns the arc over. The
handle is hidden whenever the arc is not a half arc on a horizontal or
vertical diameter, because only then is "keep the ends, move the
middle" one well-defined change.
Nothing new is stored: an arc stays an Ellipse with a start and end
angle, exactly as the existing arc handles of an ellipse save it, so
files read by older versions are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where two wires cross without being connected, a project can now draw a
small arc (a hop) on one of them, instead of the user inserting a jump
symbol and splitting the wire. It is a project setting, off by default:
Project properties > Général > "Croisements de conducteurs", with no
hops, hops on horizontal wires, or hops on vertical wires. Choosing the
orientation rather than following drawing order keeps a project
consistent.
Only the drawing changes: no element is added and no wire is split. A
wire that ends or bends on another one is a junction and never hops,
and a crossing too close to the end of a segment for the arc to fit is
drawn plainly. PDF, SVG and image export and printing show the hops,
since they paint through Conductor::paint(); DXF export, which writes
the segments itself, does not.
The setting is saved as <wire_crossings hop="..."/> under the project
root, next to <usage>, and only when hops are on, so a project that
never used them saves exactly as before.
The geometry is in wirehops.cpp, free of any graphics item, and tested
on its own. Conductor::paintedPath() feeds it the conductors of the
folio from a snapshot of their scene points, rebuilt only when a
conductor changes shape, moves, or enters or leaves a folio; each hop
path is cached the same way. Asking the scene for the conductors in a
rect instead strokes every candidate's shape, which made a 366-wire
folio render 0.6 s slower; with the snapshot the difference is within
measurement noise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text properties showed and edited the top-left corner of a text,
whatever its alignment, and changing the alignment did not move the
text. A right-aligned label next to a symbol therefore needed a
different X for every text length to keep the same gap (#1155).
Position X/Y is now the anchor point: the point of the text chosen by
the alignment (right edge for "right", middle for "centre"...). Typing a
position puts that point there, and changing the alignment moves the
text so that the new anchor point is on the same X/Y. It is the same
point that already stays fixed when the text changes.
Applies to the element text properties on a folio and to the text
field properties in the symbol editor. The saved x/y is still the
top-left corner, so projects and symbols load and save unchanged and
look the same; only the numbers shown for texts that are not top-left
aligned change.
The geometry is in textanchor.h, shared by DiagramTextItem and
PartDynamicTextField, and tested in tst_textanchor.
Not changed: static texts of the symbol editor (PartText), whose
position is tied to the font baseline; texts inside a text group, which
have no position or alignment of their own; rotation still pivots on
the top-left corner (or the centre), not on the anchor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>