Using a library's character or paragraph style, and scrubbing a
numeric field, closed their undo step with Session::end_undo_group,
which closes the open undo group of every document. An undo group open
in another document, such as one begun with edit.undoGroup.begin for
an MCP command_batch with atomic, was closed too, and that document's
next edits became undo steps of their own. When the style failed to
apply, or Escape ended the scrub, the other document's group was
canceled as well and the edits made in it were undone. Inside an undo
group already open in the same document, a scrub closed that group,
Escape undid the edits made in it before the drag, and a style that
failed to apply left the style it had added.
Session::begin_undo_scope and end_undo_scope make the edits in between
one undo step of the active document: the scope opens an undo group
there, or joins the one already open. Ending a scope closes only the
group it opened, in its own document, even when another document
became active in the meantime, and a canceled scope undoes only its
own edits, also inside a group that was already open. library.use and
the scrub use the scope, so Escape inside an open group also takes
back a scrub of the Transform panel's Shear field. edit.undoGroup.end
still closes the open group of every document.
The reference app's graphs help describes adding totals to a column
design: type in the design holds a percent sign followed by two digits.
VectorCraft drew that type as typed, stretched with the design.
Type in a column design that holds a % followed by two digits now shows
the value of each column or stacked segment in place of the % and the
two digits. The first digit is the least number of places before the
decimal point, with zeros added in front (0 for as many as the value
needs), and the second is the number of places after it. Values are
rounded half away from zero. The type keeps its own size and moves to
where the design's copy puts the point it is set from. A repeating
design shows it once per column, where a whole copy ending at the value
end puts it, and Chop Design does not cut it. A sliding design moves it
with the part of the design on its side of the guide. Type inside a
group that clips or has its own opacity, blend mode, appearance or
opacity mask, or inside a blend, compound shape, envelope, repeat or
intertwine, stays in the design and shows the value in each copy. Type
inside a symbol stays as typed. The legend swatch leaves the totals
out.
Docs: docs/mcp.md, docs/target-app-parity.md.
object.objectsOnPath.attach took a path inside a compound path, blend,
envelope, repeat or Intertwine group, or a clipping path, as the path
and put it into the new Objects on Path group, so it was no longer a
part of its container. As objects, a blend's key path, a repeat's
source art, an envelope's content and a clipping path were put into
the new group the same way.
The Objects on Path tool highlighted a blend's key path or a clipping
path and attached the selection to it.
objects_on_path::held now finds the container that holds an object.
The command refuses such a path or object with a reason, and the tool
does not highlight it or attach to it. A member of a compound path
given as an object still attaches as its whole compound path. The four
new messages have rows in every complete-message catalog.
Docs: docs/mcp.md (Objects on Path).
Object › Live Paint › Merge built the merged group anew and copied
only the bottom Live Paint group's name, opacity and blend mode, so
its lock, visibility, appearance, opacity mask, graphic style link and
other properties were lost. Make with a Live Paint group selected, and
a Live Paint Bucket click on a selected path beside a selected Live
Paint group, made a new group with a new id instead, and every face
took the fill of its paths again.
Merge now starts from the bottom Live Paint group, as Gap Options
does, and replaces only its faces, edges and paths and its Gap
Options. Make, and livePaint.fill with ids when the point is outside
that group's faces, add the other objects to the bottom Live Paint
group among them as Merge does.
Docs: docs/mcp.md, docs/target-app-parity.md.
An edge hidden on its own (for example with Object > Hide) while its
Live Paint group stays visible could still be highlighted, selected or
stroked, and a hidden edge nearer the pointer kept a visible one from
being picked.
lp_part_at now takes the nearest visible edge (visible_edge_near), inside
and outside the faces, for the hover, the Live Paint Selection click and
the Bucket's Shift-click. The Bucket's Shift-click sends nothing where no
edge is highlighted, and livePaint.strokeEdge also skips hidden edges, so
the Bucket strokes the edge it highlights.
Distribute Objects and Distribute Spacing space the selected ruler
guides across the axis (vertical ones for the horizontal options,
horizontal ones for the vertical options) together with the selected
objects, as items with no size. Guides along the axis stay where they
are. Two or more guides selected without objects are enough, and the
Align panel's Distribute buttons are enabled for them. With Align to
Artboard, the outermost items go to the artboard's edges.
A key guide (select.key {guide}) is one of those items. With a spacing,
it stays put and the other items are spaced from it, as from a key
object with no size; a key guide along the axis leaves everything where
it is. The Align panel sends the Distribute Spacing value with both
rows of Distribute buttons while a guide is the key, and enables the
value for it.
brush.new and brush.options refused a Hue Shift colorization given
without a key color ("invalid brush: missing field `key`"), and only
Brush Options picked a default. The reference app's Brushes page says
that by default the key color is the most prominent color in the art.
A Hue Shift colorization without a key now takes the solid color that
the art's fills and strokes use most, the first one found on a tie, or
black when the art has no solid color, as Brush Options already does.
Brush definitions read from a document get the same default.
brush.options replaced each field its params named, so an edit that
gave only the colorization method or only an art brush's start guide
was refused because the key or the end guide was missing. It now
merges colorization and an art brush's scale field by field.
Docs: docs/mcp.md, docs/target-app-parity.md.
Document Info had no Brushes category, and document.info refused
category "brushes". The reference app's Document Info panel menu lists
Brushes after Graphic Styles.
Document Info now has a Brushes category after Graphic Styles. For the
whole document it lists the Brushes panel's brushes. With Selection
Only it lists the brushes that the selected objects' strokes use, each
once. document.info gives the names as brushNames, the panel's flyout
offers the category, and the text report that Document Info › Save…
and File › Package write has a BRUSHES section.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app's View menu has Show Live Paint Gaps, which
highlights the gaps in the selected Live Paint group based on the gap
options set for that group. VectorCraft had no such item.
View > Show Live Paint Gaps (view.livePaintGaps) turns the highlight
on or off, and the item reads Hide Live Paint Gaps while it is on.
While it is on and edges show, the canvas draws the gaps of each
selected Live Paint group up to its Paint Stops At size, the gaps that
livePaint.info lists. It draws them as the Gap Options preview does,
as lines in the group's Gap Preview Color. The setting works as View >
Hide Text Threads does: one setting covers every document, it is saved
with the interface state, and Hide Edges hides the gaps. It is off by
default.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app's Brushes panel menu has Select All Unused, which
selects the brushes that are not used in the document. VectorCraft's
Brushes panel menu had no such item, and no command listed the unused
brushes.
brush.unused (Select All Unused) is a query that lists, in library
order, the brushes no stroke paints with. Strokes in the art and its
opacity masks, in symbols, graph designs, pattern tiles and graphic
styles count, and so do strokes in the art of a brush that is used.
The art of a brush that is not used does not count, so a brush whose
own art is the only art that paints with it is listed, and so is a
brush used only in the art of an unused brush. The Brushes panel
menu's Select All Unused selects these brushes, and Delete Brush and
Duplicate Brush then act on them.
Docs: docs/mcp.md, docs/target-app-parity.md.
Select > Object > Bristle Brush Strokes selected the objects whose
brush name contains "bristle". It missed objects with a bristle brush
of any other name, and it selected objects whose brush is of another
type with "Bristle" in its name.
select.object.bristleBrushStrokes now looks up the brush of each
object's topmost stroke in the document's brush library and selects the
object when the brush is a bristle brush, whatever its name.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app adds a new fill with Ctrl+/ on Windows and Command+/
on macOS, and it adds a new stroke with Ctrl+Alt+/ on Windows and
Option+Command+/ on macOS. VectorCraft had Add New Fill and Add New
Stroke on the Appearance panel, with no default key for either.
appearance.addFill now has the default Cmd+/ and appearance.addStroke
has Cmd+Alt+/, which are Ctrl+/ and Ctrl+Alt+/ on Windows and Linux.
Like the panel's buttons and menu items, each key runs its command with
no params and adds the item to every selected object. Keyboard Shortcuts
lists both under Window › Appearance with their keys, and press_key
runs them for agents.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
In a clip group or a clipping layer the bottom child is the clipping path.
Object › Arrange moved the selected children with no regard for that slot:
Send to Back (and Send Backward from just above it) put the art under the
clipping path, so the art became the clipping path, lost its role as art,
and the group drew nothing. Bring Forward / to Front on the clipping path
itself would likewise have made art the clip. Layers-panel moves already
keep the clipping path at the bottom (layer.rs, `index.max(1)`); arrange
didn't.
- object.rs `arrange`: in a parent that clips, the clipping path is left
where it is and the art is arranged within the slots above it (Back goes
to just above the clipping path, Backward stops there).
- tests: Back, Backward, several objects, and Forward/Front on the
clipping path in a clip group (with a render check); Send to Back in a
clipping layer.
craft-docpath follows a stored path only on this computer or on the
share the document is on, but its check is lexical and cannot see
mapped drives. fileio::document_path therefore followed a link stored
as Y:\img\logo.png, where Y: is mapped to \\server\other, from any
document: opening the document, links.check, Link Info and the
linked-file watch read or stamped the file on that server.
On Windows, document_path now reads the final path of the root folder
of a link's drive (std::fs::canonicalize). Windows opens that root
folder on the drive's server to answer, so the check still contacts
the server once per lookup; no file on it is read, stamped or watched.
When the final path names a share, the link is checked against the
document's path on its share, and a link on another share is refused
as on another computer, as a link stored with that share's own path
is. Links on this computer's drives and on the document's share are
followed as before, and a link on the document's own drive is not
looked up. A document opened as \\?\Z:\... is looked up on its share
as Z:\... is. Other systems are unchanged.
Docs: docs/mcp.md, docs/target-app-parity.md.
The desktop app reads the User Defined swatch and graphic style
libraries while it creates its window: set_user_dir runs
Libraries::rescan, which read and parsed every file in the folder
outside the panic guard that commands have. A parser bug that panicked
on one file stopped the app before its window appeared, at every
launch while the file stayed in the folder.
rescan now reads and parses each file inside guard::catch_panic and
skips a file whose reading panics, as it skips one that can't be read.
The other libraries are listed.
A live-effect plug-in was given only its object's geometry. An effect
that draws as wide as the stroke it sits on, or tells its fills apart,
had to ask for them again as parameters, which then went stale when
the paint changed.
A live effect's input object now holds the object's fills and strokes
too, read only and in the form a filter gets them (one encoder,
objects::paint_lists, for both), and _context.item says where the
effect sits: "object", or {"fill": i} / {"stroke": i} for an effect in
a fill's or a stroke's own effects. effects::GeomContext carries the
object's appearance and the item, and the plug-in cache keys on the
paints, so a change of paint runs the plug-in again. Hosts from before
this give geometry only; nothing in the output changes.
Expand worked a fill's geometry effects out in the object's context, so
a plug-in effect there would be told "object" and a gradient fill was
expanded into a shape the canvas doesn't draw: fill_shape now gives them
the fill's context, as drawing does (expand.rs).
Siblings named with a declared prefix, the other half of such context,
are left out: the effected geometry is worked out from the object alone
wherever it is drawn, measured, hit-tested or expanded.
wat::when_given is a plug-in whose output depends on whether its input
or parameters hold a text. tests_plugins.rs uses it to check that an
effect sees the stroke's width and the fill, runs again for a thinner
stroke, and on a second stroke is told so; effect.rs checks the item
counting; a gradient fill under such an effect is drawn and expanded
into the same triangle. CRAFT_PLUGIN_PAINTS_WASM=<folder> saves a test
effect to try in the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
A plug-in could filter objects or reshape them as a live effect, but
nothing it made could leave the app: pen plotters, cutting machines and
laser cutters read formats VectorCraft doesn't write. An exporter is a
third kind of plug-in ("kind": "exporter", with the file "extensions"
it writes), in the same sandbox and with the same ABI v1 exports.
- It adds a write-only format, plugin.<id>. document.formats lists it
with its plug-in id, and document.export writes it by that id, or by
a path ending in one of its extensions that no built-in format has;
"params" carries its parameters, checked against its manifest as a
filter's are.
- File › Export › Export As lists it after the built-in formats, with
Use Artboards greyed out, since the plug-in writes the whole
document. One with parameters asks for them in a dialog generated
from its schema (pluginExport) once the file is picked.
- The plug-in is given the native file's JSON (format::json_part:
images of 8 KiB or more without their data) and _context {mode:
"export", extension, name}. It returns {"dataBase64", "warnings"?}
or {"error"}. The host writes the file, and nothing when the plug-in
declines or returns bad data.
- An export gets 10 G instructions plus 4,000 per byte and 300 s.
Background Export's Cancel stops it between two slices of that
budget (Error::Cancelled), and a cancelled live-effect run is not
cached as a failure.
- plugin.run and Object › Plug-ins say where an exporter is instead of
calling it a live effect.
engine/tests/plugin_export.rs reads back what an exporter is handed
with wat::echo_exporter, an exporter whose file is its input or its
parameters (its base64 encoder is written in WAT). The document it is
given is document.save's JSON, without the large image's data, and its
parameters arrive defaulted, clamped and with the file's name and
extension. It also covers listing and removal, built-in extensions
winning, selectedOnly, declines, bad base64, missing data, warnings,
and Cancel stopping a plug-in that never returns. It is a test binary
of its own: installed plug-ins are process-wide, and the format-list
unit tests check exact lists. dialogs/tests_plugin_export.rs drives
Export As headlessly; plugins/tests/abi.rs covers the budget, the
parameters and the echo exporter; format/tests/native_v4.rs covers
json_part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
With Include SVG Code on, Copy offered the SVG markup as plain text with every placed image
embedded as base64: a 13 MB photo made 17 MB of text. A paste reads the text first (eframe on
every paste key; on Linux and macOS it is the one format written, and holds_ours reads it again),
so each paste moved it through the system clipboard on the UI thread, and on GNOME the app
stopped responding.
The SVG markup now goes out as text only while the images it embeds total at most 1 MiB; above
that it is offered as image/svg+xml and PNG. Type keeps its text, and vector art keeps its SVG
text at any size (only embedded image bytes count).
Windows 11, Ryzen 5 2600X, 13 MB PNG, real Ctrl+C: the longest UI stall fell from 823-1021 ms
to 133 ms. Not run on Linux.
A lone object selected with ruler guides can be the key (the Selection
tool's click on it), and Align to the key moves the other selected
guides to its edge or centre: vertical guides to left, centre or right,
horizontal ones to top, centre or bottom. Parallel guides line up on a
key guide. Aligning to the selection or the artboard leaves guides alone.
A click with the Selection tool on a guide selected with objects makes
it the key (select.key {guide}), drawn thicker; a click on it again
cancels it. Align to Key Object is on with one object and a selected
guide and makes the guide the key. object.align lines the objects (or
selected anchors) up on the guide's line and leaves the guide where it
is: a vertical guide sets x only, a horizontal one y. The selection
keeps key_guide beside key, never both; it goes with the guide or the
objects, and follows the guide when earlier guides are deleted.
Distribute Spacing doesn't space from a key guide.
The list fills the panel down to its bottom bar and scrolls, also while a drag nears its top or
bottom edge (in the dock and in a flyout, as the Layers panel does).
Dragging in the list:
- an item (colour, text style or graphic) dropped on another of its kind lands just before or
after it, by the pointer's half, an accent line showing where, and joins that item's group;
- dropped elsewhere on a group (its header too, even collapsed) it goes into that group, and on
the ungrouped items out of its group, the section outlined; with every item grouped, a
"Drop here to take it out of its group" strip shows while one is dragged;
- a group dragged by its header goes before or after another group.
Colours and graphics still drag onto art and the canvas as before.
Engine: library.moveItem takes `before` / `after` (an item of the same kind) to place the item
in its kind's order, which the panel shows; the new library.moveGroup {group, to} moves a group.
Text styles drag as PanelDrag::LibraryItem, groups as PanelDrag::LibraryGroup, and a library
colour's Paint drag names its library item, so the panel tells it from a swatch dragged in.
Tests: reordering and moving in the engine, the commands in the library fuzz test, and panel
tests that drop styles and group headers and check the list scrolls and scrolls while dragging.
Strings translated in the 23 catalogs; docs/mcp.md and the parity row updated.
Move the Preserve Editing check out of save.rs into svg.rs, where it
reads the options through `plan` as the encoder does; SaveJob::encode
still puts the warning first. Review follow-up on #2994.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A document with more than one artboard saved as SVG/SVGZ without
Preserve Editing Capabilities is written as one artboard and reopens
with one, so the others were lost without a word. The save now says so
(first, since the status bar shows only the first warning) and names the
two ways to keep them all.
Refs storytold/vectorcraft#2091
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CMYK JPEG came out with JFIF's APP0 segment at its head and four components
in the frame:
APP0/JFIF APP14/Adobe APP2[ICC_PROFILE 1/4 … 4/4] SOF0: 4 components
JFIF describes one- and three-component images, greyscale and YCbCr, so an APP0
segment in front of a four-component frame says two different things about the
file's colours: a reader that trusts JFIF decodes nonsense, and one that checks
refuses the file. Adobe's own CMYK JPEGs carry the APP14 marker the encoder
already writes, and no JFIF. `jpeg-encoder` writes JFIF unconditionally, so the
segment is replaced here.
The resolution JFIF carried moves to an EXIF APP1 segment -- `XResolution`,
`YResolution` and `ResolutionUnit` -- which is where `cmd::fileio::ppi` already
looks when a file has no JFIF, so nothing is lost; with no resolution asked for
the segment is simply dropped. RGB and greyscale files keep JFIF, which is what
it is for.
Found while looking at #3084, whose default RGB exports are valid here; this is
the file that is not.
Join can set the surviving junction point to a corner or smooth anchor via `to`, matching Illustrator's join choice without a combined Average+Join command.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mark object.isolate id as optional so runs_without_params includes it in the shortcut editor; engine already isolates the first selected object when id is omitted.
Co-authored-by: Cursor <cursoragent@cursor.com>
When copying a loaded .acb into the user library, use the parsed book
title instead of a macOS sandbox temporary name (storytold/vectorcraft#2996).
Co-authored-by: Cursor <cursoragent@cursor.com>
Honor the requested text mode in the single-page importer, as Open does, instead of forcing outlines for Place and PDF clipboard import.
Add synthetic regressions for bytes, unlinked paths, selected pages and explicit outlines. Update format parity and milestone evidence without changing scores.
With the Anchor Point tool, a drag from an anchor pulls out smooth handles.
Shift held at the press now moves the anchor instead, its handles keeping
their offsets; Shift kept down during the drag keeps the move at 45° steps
(the constrain angle), released it frees it. A Shift-click that doesn't
move leaves the anchor as it was. A selected blend's spine points follow
the same rule (object.blend.spine.moveAnchor).
The move goes through a new engine command, path.moveAnchor
{id, subpath?, anchor, x, y}, since path.moveAnchors only acts on the
direct-selected anchors and the Anchor Point tool keeps no anchor selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.pdfInfo read thumbnail with as_u64, so a value such as -1,
1.5, "1" or true gave a reply without a thumbnail and no error. This
happened for a PDF and for a PostScript file.
A thumbnail that is present and not null must now be a whole number
from 1 to the page count. Any other value is the existing "thumbnail:
no page {n} (the PDF has {count})" error, or "(the file has 1)" for
PostScript, with the value as given. An absent or null thumbnail still
gives no thumbnail.
Found while testing crates/engine/src/cmd/path.rs:632 (without_anchors, closed-path branch uses keep instead of open_run)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
VectorCraft had no Objects on Path. A click with the Objects on Path
tool attaches the selected objects to the path clicked. The tool is in
Edit Toolbar's Modify section and the Blend tool's flyout, and Object ›
Objects on Path › Attach and the context menu with two or more objects
selected choose it. The objects and the path go into an Objects on
Path group with the path at its bottom.
- object.objectsOnPath.attach orders the objects column by column from
left to right and top to bottom in each column. The first one sits
on the path's first anchor, the last one sits on the end of an open
path, and the rest are spread evenly by length; a closed path takes
them all the way round. Each object's attachment point (Pivot, one
of nine, default the center of its bounds without strokes) sits on
the path, and the object is turned to the path there. An object that
was rotated by an angle other than a multiple of 90° is first turned
to the nearest multiple of 90°. Rotate tilts every object by that
many degrees counter-clockwise. Symbol instances are measured by
their art. Guides, compound paths, type, paths without length and
graphs are refused with a reason.
- After every edit the objects go back on the path: editing the path
moves them, an object moved on its own goes back, and objects moved
into or out of the group in the Layers panel join the end of the
path or leave it. Scaling, shearing or reflecting the whole group
keeps each object's scale, shear or reflection, and the objects are
spread evenly along the transformed path again. Undo takes the path
and the objects back in one step, and a copied group stays live on
its own path.
- object.objectsOnPath.options changes Pivot and Rotate, and
object.objectsOnPath.expand leaves a plain group. Both act on the
groups that `ids` names, or on the groups of the selection when
`ids` is not given.
- The native file keeps the group live; SVG, PDF and the other exports
write plain art. The tool highlights the path under the pointer, has
a cursor entry, and Escape during a press cancels the click.
Part of #2545
Docs: docs/mcp.md, docs/target-app-parity.md.
Choosing a data set in the Variables panel, stepping through data sets
with the panel's arrows, and running `dataset.select`, `dataset.next`
and `dataset.prev` applied nothing and showed no message when no object
was bound to a variable that the data set has a value for. This
happens, for example, after Import Data Sets… defines new variables
without binding them.
The app now shows "nothing in data set A was applied: no object is
bound to a variable it has a value for" in the status bar, and the three
commands add it to the reply as `warning`. A data set with a value for
a bound variable gives no warning even when every object bound to it is
locked; `skipped` counts those objects. What applying a data set does
is unchanged.
Docs: docs/mcp.md, docs/target-app-parity.md.
PNG, PDF, EPS, legacy `.ai`, packaged zips and saved `.vectorcraft` files
record the wall-clock time they were made in, so the same commands run twice
gave different bytes although the pixels matched (#2880):
PDF /CreationDate and /ModDate D:20261018155816 vs D:20261018155817
PNG the `Creation Time` tEXt entry
.vectorcraft one byte of the stored date
SVG carries no time, and already repeated
`metadata::output_time` is the time a file records: `SOURCE_DATE_EPOCH` when it
holds a non-negative whole number of seconds, else the clock, as the
reproducible-builds specification defines it. A date a command is given
(`file.new {created}`, a save's `modified`) still wins over the variable, and a
blank, negative or non-numeric value is ignored so that an export is never
refused over the environment.
`metadata::now_unix` keeps the real clock and now has exactly two callers, both
of which must not be pinnable from the environment: the recovery heartbeat, and
the seed PDF encryption draws on.
A .ai saved without PDF compatibility has only a placeholder page in
its PDF part. document.open reads such a file from its editing data,
but with `pages` given it failed with "the file holds only a
placeholder page", even for "1" on a file of one page.
When `pages` names every page of the file, document.open now reads
the editing data as it does without `pages`.
Docs: docs/mcp.md.
A PDF or PDF-compatible .ai saved with Preserve Editing carries the
native document as an embedded file, with a hash of its pages at the
end of the file's /Desc. The import read /Desc with from_utf8_lossy and
looked for "hash ". /Desc is a PDF text string, which may be written as
UTF-16BE with a byte order mark (ISO 32000-1 7.9.2.2). Written that
way it holds no UTF-8 "hash ", so the import found no hash, took the
editing data as intact and reopened the native document with no
warning, even after another app had changed the pages.
/Desc is now read with text_string, the importer's decoder for text
strings, so the hash is found in UTF-16BE, UTF-8 and PDFDocEncoding.
Changed pages open as artwork with the "changed in another app"
warning. The embedded file's /UF and /F names are decoded the same
way, so editing data named only in UTF-16BE is found.
A /Desc without a hash is still trusted, as before.