Development Strip Editor Objects - ShadobaDev/Platemaker-qt GitHub Wiki
Bubbles and their tails, what every placed object shares, placement and creation, presets, and imported artwork.
-
One object, two tools. A bubble is an
ObjectRecord(widgets/objectrecord/): shape, box, tails, text, font, colours, line style. The Text tool is the same object withshape == None. Nine silhouettes (shapeOrder()), plusNone; each brings the rectangle its text may occupy (textSafeArea()), which is the half that takes the thought β a path is a few lines, knowing where words fit inside it is what stops them crossing a stroke. That rectangle is inset by the stroke and byTextProperties::padding(default 8 px), which may be negative so the lettering can burst out of an impact balloon. It is the lettering's property β how the words sit β so it is edited in TEXT, but only a balloon uses it: a text object (None) gets its whole box, carries the value unused, and is not offered the row. -
The lettering is never wrapped.
layOutText()lays it out withQTextOption::NoWrap: the lines are the author's line breaks, and the box width only places each line by its alignment. The layout therefore does not depend on the box.TextProperties::lineSpacing(percent, default 100) sets a proportional line height; at 100 the block format is left alone, because a proportional 100 is not the same arithmetic as Qt's default single height. -
Fitting is geometry. Because the layout does not depend on the box,
Painter::textBlockSize()measures the lettering once, and everything else is a comparison againstPainter::textArea():fits()(half a pixel of slack, so whole-pixel rounding is not an overflow),textOverflows(), andfitBoxFor()β the smallest box on both axes, a binary search per axis repeated until both settle (a thought balloon's lobes follow the shorter side, so the axes are not independent), floored at the grips' 40 px. Fit to text isfittedBox(), the two together.fits()andfitBoxFor()touch no font, so they are unit-tested for every shape without aQGuiApplication. Fix padding is the other way round β the box stays, the padding gives:paddingToFit()binary-searches the largest padding not above the current one at whichfits()holds, so it only ever lowers it, and asksfits()rather than solving the shape formulas so it cannot disagree with the red outline. - Overflow is shown, not prevented. A selected object whose lettering does not fit has its text area outlined in red by the selection chrome, measured when the object reports a change rather than on every repaint. The lettering is still clipped to the text area.
-
Tails are a list. Each is a tip (in balloon coordinates, and free to fall outside the balloon), a
base width and a bend. The base is found by casting a ray from the balloon's centre and binary-searching
for where it crosses the silhouette with
QPainterPath::contains()β shape-agnostic, so every shape grew a working tail for free and a tail may leave any edge.Painter::bounds()therefore computes what the record actually covers;boxis the balloon alone. -
Each tail is an object of its own. It shares nothing with its siblings: its width and bend are its
own, and Add tail only seeds a new tail's starting values from the last one. It is selected in the
object tree or by pressing its handle on the strip, which leaves the bubble selected there β its handles
are drawn only on a selected bubble β but hides its box and grips, which stop resizing while a tail is
the subject; the selected tail's handle is drawn hollow. OBJECT STATE then shows only Tail (width, bend) and
Delete tail, which removes that tail and selects its bubble. A press anywhere else on the bubble selects
the bubble again.
-
A tail is addressed by position,
ObjectController::selectedTail(), not by an id: undo restores whole states and holds no tail by number. A feed that changes how many tails the bubble has cannot say which one went, so the selection moves up to the bubble. - With Ctrl, a tail joins a selection instead of replacing it (Selection).
-
A tail is addressed by position,
-
Every placed thing is a
StripEdit::Object. OneQGraphicsObjectper overlay, and everything an author does to one is the same whatever kind it is: select, move, drag a grip, mute, delete, reorder. That all lives on the base class, once β the drag state machine (beginDrag()/dragTo()/endDrag()) and the "report only a settled drag" rule the undo stack depends on. A kind supplies only what it draws, how big that is, and any extra handle it offers.-
The selection chrome is one item above every object (
SelectionChrome): the dashed box, eight grips (corners and edges, GIMP's model) and every tail handle of whatever is selected. Drawn by each object it sat at that object's place in the stack, so an object above hid its grips and took presses meant for them; drawn above everything, in scene units at a fixed screen size, it does neither, and a picture's item scale no longer changes its grips' size. Its shape is the grips alone, so other presses fall through; a press on a grip hands the drag to the object (Object::beginDrag()). It follows the objects throughObject::chromeChanged()and the zoom throughObjectController::refreshChrome(). -
The resize maths is a pure function,
resizedRect()(objects/resize.*): the dragged corner or edge moves, the opposite one is the anchor; Shift keeps the aspect (a corner follows the axis pulled further), Ctrl anchors the centre. Sizes snap to one item unit before anchoring, so the anchored side never drifts, and a box below 40 px is refused rather than clamped. An edge grip answers along the whole edge and is drawn as a square at its middle; it is not offered on artwork (keepsAspect()) nor on an edge shorter on screen than three grips. -
BubbleObjectβ painted from the authoring model byPainter::paintPaths(), so typing updates the strip with no file round-trip; the file is written from the same paths at the same scale, so the preview is the render. A styled bubble shows the library's own rasterisation at rest (Where a bubble lives). Its tails are the object's handles. -
ArtworkObjectβ imported artwork, drawn from its own picture (an SVG throughQSvgRenderer, below), with no parameters to edit. It keeps aspect on a corner drag, because the record stores one width fraction and a distorted one is not expressible. -
What is clickable is the box, not the bounding rectangle.
Object::shape()is the box, plus the tail tips that are drawn (grabbable at any time); the grips are the chrome's. A tip inside its own balloon draws no tail (Painter::tailsDrawn()), so it is not in the shape β a press there moves the balloon β and is reachable only through the chrome, once the balloon is selected. Qt's default would beboundingRect(), which here is the drawn extent β balloon βͺ glyphs βͺ every tail tip, padded for grips β a rectangle largely made of empty page whenever a tail is long, and one that lets the upper of two overlapping bubbles swallow presses meant for the lower. A tail's shaft is left out deliberately: it is a thin sliver far from anything anyone aims at. -
The split is a correctness measure, not tidiness. One item class switching on whether a pixmap
had been handed to it would force every caller to re-derive the same distinction from the model β
and a caller that gets it wrong persists a default bubble over imported artwork, or makes
Duplicate write a blank balloon. With two types none of that is expressible. Three model-side asks remain and all three are legitimate: choosing which object to
build, naming a row before the scene has a layout, and comparing records in
Project::applyOverlays().
-
The selection chrome is one item above every object (
-
Placement is resolution-independent. Every coordinate and the artwork's width are stored as
fractions of the render's target width, so re-profiling a chapter moves and resizes every object
proportionally and nothing has to remember what the numbers used to mean.
StripLayout::targetWidth()is that unit on the GUI side β the strip's width, which is the target width because the page domain scales every page to it β andStripLayout::pixels()/ the write-back inonOverlayGeometryCommitted()are the only two places a placement crosses between the two forms. -
Placement is page-anchored. An overlay stores
anchorInputUidplus an offset from that page's top; the editor resolves it against the layout it just built, exactly asProcessingPipeline::run()does. Dragging across a page boundary silently re-anchors. An overlay whose page is not in the strip is unanchored: drawn faint, not selectable on the strip (there is nothing to select it on, so it is picked in the object stack), never re-homed by itself and never deleted. Re-anchor to βΈ on its menu puts it on a page (Inputs and anchoring). -
Creation is the library's. The editor emits
recordCreated();Project::createOverlay()writes the SVG and callsProjectItem::addOverlay(), which mints the uid, hashes the file and dedups identical content. Every other edit arrives as the complete new state onoverlaysCommitted().
-
Which preset an object looks like is computed, never stored.
PresetStore::matching()restyles the object by each preset in turn and keeps the one that changes nothing;lookLabel()names it, or says Custom. A remembered "this came from Shout" would be a field that goes stale on the next edit and has no answer at all for a selection of several β this one self-heals if the artist edits back to an exact match, and answers Mixed when a selection disagrees.-
Only the groups a preset fills are compared β shape, fill & outline, line style, text style. The
box, the tails and the lettering are the object's own and
applied()copies them across, and the style seed belongs to no group: comparing whole records would make two balloons that match in every visible way report as different looks. - It is shown where it answers something: OBJECT STATE says what the selection is (Mixed when they disagree, nothing at all for a tail), and the OBJECT STACK names the preset on a row only when it matches one β Custom on every hand-made balloon would be a column of chips reporting that there is nothing to report.
- TOOL OPTIONS wears no chip. Every property a preset covers is on screen a few points below the picker, so a chip there would report what the controls already say, and beside the picker it read as a second control answering the same question β the fault this panel was rearranged to fix.
-
The picker offers; it never claims. The combo in TOOL OPTIONS shows a prompt and no current entry, because
a drop-down showing an entry means this is what is selected and a preset is a one-shot fill that
stops describing anything the moment a control moves. Save⦠and Delete act on the look the
controls currently are β
matching()again β rather than on a picker's selection.
-
Only the groups a preset fills are compared β shape, fill & outline, line style, text style. The
box, the tails and the lettering are the object's own and
-
A preset is a bubble with nothing said in it.
BubblePreset(widgets/stripeditor/presetstore.hpp) is a name plus anObjectRecordwhosetext,box,tailsandstyleSeedare meaningless β applying one copies the look over the selection and copies the content straight back, so restyling never touches the lettering. With nothing selected it restylesprototype()instead, which is what the next placement is built from. Serialised by takingObjectRecord::toJson()and removing those four keys: the reader's defaults fill them back in, and a styling field added toObjectRecordis carried without being enumerated anywhere. Built-ins are code and are not deletable; the artist's own live in the application config (QSettings, keybubblePresets) because restyling follows the artist, not the chapter. A pack is the same array in a file, marked withplatemakerBubblePresets, imported by replacing same-named presets rather than accumulating them.
-
Both axes have a name, and one place each. Which silhouette an object wears is a property;
whether it has one is
ObjectRecord::hasSilhouette(), the one structural question β it decides which property groups the object carries at all. Whether we author the object isObjectController::isParametric(): true for something whose drawing we generate from a record, false for imported artwork. Written out by hand at each call site instead, either test is a place to get it wrong β and the wrong answer is quiet:ObjectRecord::Map::value()returns a default balloon for a uid it does not hold, so a panel bound that way offers a shape, a fill and a line style for a photograph and swallows every edit.-
carriesGroup(record, group)is the one answer to "which sections apply". The lettering is the group every kind carries; shape, fill, line style and tails all need a silhouette to sit on;TailItembelongs to a tail, which is not a record. OBJECT STATE writes it out for a single subject and for a set, and the object menu measures a selection by it β one answer, so a group added to it reaches every surface. A pure function of a record and header-only, which is what lets it be a test rather than a comment. What each surface does with the answer stays that surface's: OBJECT STATE shows a set the union, the menu offers only the intersection, and a set is never offered Shape or Tails because giving several objects one shape is a conversion. -
One write path, and pairing is stated rather than inferred.
applyRecords(uids, records, β¦)writes whichever kind each object is, so no path can take only a balloon and drop a picture without saying so. Records are never matched to the selection by position β that refuses whenever the two lengths disagree, which makes a mixed selection uneditable. OBJECT STATE's own signals carry records and no uids, so the objects it was bound to are remembered inm_panelSubjectsat the moment of binding: a selection that has changed since cannot make the right records land on the wrong objects. - A set of several binds whatever kinds it holds. The panel sorts records into roles β skin and line style to the objects with a silhouette, the lettering to all of them β and writes each group back only to the objects it was bound from, so the union is what shows and nothing is written to an object that has no place for it.
-
The objects hold the state; the controller holds no second copy. A map kept beside the objects
would have to be written at every edit, and a write path that updated only one would leave the other
describing something that is not on screen. What the controller keeps is a seed: the records the last feed brought, read only to build an
object that does not exist yet and to answer for a uid that has none. Every other read goes through
recordFor(), and what goes out onoverlaysCommitted()iscurrentRecords(), derived from the objects at the moment it is asked for. A record reaches an object in exactly two places β its constructor, and the adoption loop insetSource(), which is the one direction a record travels from outside. -
Every object carries an authoring record, and
Object::record()is how anything asks for it. A balloon's is everything about it; a picture's says which file it is and what words are over it, on the base class β so no caller has to prove which subclass it holds before reading a record. What is still cast is what only one kind has: a picture's pixmap, a balloon's library rasterisation. -
The object is the authority on what a picture is.
ArtworkObjectkeeps its record describing the file it loaded, at that file's own pixels (describePicture()), so asking any object for a record can never hand back the default speech balloon thatObjectRecord::Map::value()returns for a uid it does not hold. A record placed before pictures had one, or one whose size was guessed at import, is repaired the moment the object adopts it. The box matters becauseSvg::write()refuses an empty one, so an unrepaired picture silently cannot be lettered; the size itself is read by the one loader that asks an SVG rather than the image plugin, and carried on the import signal because the far end sees only the copy it just wrote. -
A row names a picture by its lettering, or by its file (
ArtworkObject::label()), so no two pictures' rows read the same. -
One OBJECT STATE for every kind of object. A picture needs no panel of its own: deciding
which sections apply (
carriesGroup()) already hides the ones it lacks. So a picture is bound like any other object: its file names it, its lettering is the same section a balloon's is, and its one extra question is a row. Its size is a percentage of its own pixels, where 100% is one image pixel per strip pixel β a percentage rather than a width because the artist's question is how much bigger than I drew it, and because the stored form is a fraction of the page, so the same percentage survives a re-profile. Like blend it belongs to the overlay rather than to the drawing, so it is not a property group and arrives through its own setter. Moving the spin box previews; releasing it is the one history step. Fit to text is absent for a picture, whose box is the picture's and not its words'. - The object's menu offers artwork only what applies to it. Apply preset βΈ, Save as presetβ¦, Apply from tool options βΈ and the two colour entries are absent β not greyed β unless every selected object can take them: greying says not now, and these never apply to a picture somebody else drew. The intersection rather than the union, the same rule Convert to βΈ follows and for the same reason β offering Fill with primary colour over a balloon and a picture would be offering to do it to both and quietly doing it to one. In their place artwork has two of its own, Original size (100%) and Fit to strip width, which a balloon does not get: a balloon has no size of its own, since its drawing is generated at whatever size it is given.
-
-
Placing artwork is a tool, not a menu entry. The Artwork tool's options (TOOL OPTIONS) hold one picture β chosen once, previewed, and remembered in
QSettings. There are two ways to put it down, and they differ in the one thing artwork cares about:- Dragged out of TOOL OPTIONS' preview it lands at its own size, centred where it was dropped β past the strip's edge if that is what it is, which for a sound effect is the point rather than a mistake to correct. The payload is a file URL, so a picture dragged in from a file manager lands the same way, under any tool.
- Dragged on the strip with the tool armed, it is fitted to the box drawn.
Arming the tool with nothing chosen asks for a file there and then, before any drag, so a dialog never interrupts one. A tool, because the object menu is not where anyone looks to add something; Import artwork⦠stays on that menu for the case with no tool armed.
-
An imported SVG that carries a recipe comes home as a balloon. Our own files hold the parameters
they were drawn from in the
pm:namespace, andSvg::read()reads them back β so a bubble sent by a collaborator, or copied out of another chapter, arrives re-typable instead of being filed as a picture of itself. Matched on the namespace URI, never the prefix, which in XML is only a local shorthand. A drawing with no recipe stays artwork, which is the honest answer: someone else's paths are not something our silhouettes can express. -
The namespace is
https://github.com/ShadobaDev/Platemaker-qt/ns/artifact/1β an identifier, not an address. XML compares a namespace name as a string and never fetches it, so it neither has to resolve nor may ever change once files carry it. It names the repository because that is a name the project controls, and says artifact because the record describes pictures as well as balloons. It is compared whole and exactly (QXmlStreamAttributes::hasAttribute(ns, name)), and exactly one namespace is accepted β no alias for any earlier spelling. A file that does not match is not an error: it has no recipe, so it is placed as a picture and still renders. -
One version number. The URI's trailing
/1changes only when a file becomes unreadable to an older build β an old reader should then fail to recognise it at all, which a different namespace achieves by itself. The file carries no version attribute of its own: nothing would read one until a released reader meets a newer file. The first compatible revision that has to be told apart adds one, and a file without it is this format.
-
There is one record type and three kinds of object.
ObjectRecord::artworknames an imported picture β a file name inside the workspace'soverlays/β and its presence is the kind: empty and the drawing is ours (a balloon whenshapeis notNone, lettering when it is), non-empty and the drawing is somebody else's. A second map keyed by the same uid would be a second channel carrying the same object, and the two would drift; a record that says what it is keeps one.isArtwork()makeshasSilhouette()andhasTail()answer no whatever else the record holds, so our geometry cannot be applied to a picture by accident.-
A picture can be lettered. The same
Textgroup a balloon carries is bound to it β one editor, one group, one answer β and laid out across the whole picture, whichtextSafeArea()already gave to any object with no silhouette of ours. Hand-drawn balloons and sound effects are typeset without ceasing to be somebody's drawing. -
What the render composites is a wrapper. A lettered picture's overlay points at a generated
ovl-<sha>.svgthat embeds the picture as a data URI and draws the lettering over it; an unlettered one points straight at the imported file, so the common case writes nothing. Embedded rather than referenced because the library hands the renderer a buffer with no base path (vips_svgload_bufferβ librsvg), so a relativehrefhas nothing to resolve against β and a wrapper that cannot draw its picture is never written, because it would render as lettering floating over nothing. -
The item draws the picture, not the file the library renders. They are the same thing until
the picture is lettered; after that the overlay points at the wrapper, and an item built from
that would draw the words twice β once baked into the wrapper and once itself.
pictureFor()resolves the record'sartworkbeside the overlay's own file, so the preview is built from what the artist imported and the lettering stays the editor's to draw. -
A vector picture stays vector on the canvas.
ArtworkObjectkeeps aQSvgRendererfor an SVG source and draws through it, so zooming in sharpens the sprite instead of magnifying pixels chosen at import. The rasterised copy is kept for the row's icon and for the one question a picture has to answer about itself β how big it is. A raster source draws as it always did. -
The picture itself is never overwritten.
Svg::writeFile()ignores its reuse path for an artwork record: that path may be the imported file, and the imported file is the one thing in the workspace we did not make.
-
A picture can be lettered. The same
-
Imported artwork is the same overlay with fewer attributes. Import artwork⦠copies a file into
overlays/asart-<sha16>.<ext>and registers it with a picture record βartworknaming the file,shapeNone,boxthe picture's own pixels β so it becomes anArtworkObject: placed, moved, re-anchored, muted, resized, lettered and rendered like any bubble, but its drawing is not ours to re-type. Duplicating one goes back through the import channel rather than through creation, and the library dedups the identical bytes onto the file already there.
See also: Strip editor Β· Property groups Β· Workspace folder ownership