Commit Graph

37 Commits

Author SHA1 Message Date
bflatastic 7227e295ea M3.60: gradient swatches and stops linked to global and spot swatches
Applying a gradient swatch records it as the gradient's swatch (the Swatches panel highlights
it and the Gradient panel's swatch menu marks it). A stop given a global or spot swatch (and a
tint) in paint.editGradient / a gradient paint's stops, dropped from the Swatches panel or
picked in the stop popover stays linked: editing the swatch recolours it, in art and in
gradient swatches, deleting it unlinks it. Spot stops separate on their plate, and a gradient
whose stops are all tints of one spot ink (or paper white) exports to PDF as a Separation
shading; gradients mixing models are written in one colour space (the PDF writer requires it;
mixed models used to abort the export).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:29:19 +07:00
bflatastic b2ececc7b4 M3.59: tints of global and spot colours, tint swatches and Global colour balance
A colour linked to a global or spot swatch keeps a tint (Paint::Solid.tint, 1 by default and
left out of files at 100 %): paint.setFill/setStroke {swatch, tint} apply the swatch at that
percentage, swatch edits recolour every linked colour at its own tint, and Color::tinted
resolves it (CMYK and Gray inks scale, RGB mixes toward white). swatch.new saves a tint as a
tint swatch ("Ink 40%") linked to its base. Spot tints separate as that percentage of their
plate (Separations Preview, PDF Separation value) instead of an ink-ratio guess. Adjust Color
Balance's Global mode (tint) shifts the tints of linked colours. The Color panel shows Tint
mode (the swatch chip, its name and a T slider) for linked colours; the Swatches panel
highlights the tint swatch in use. Gradient stops get the same link and tint fields (the
swatch walks cover them), so GradientStop is no longer Copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:29:17 +07:00
bflatastic 323b178ea7 M3.40: stroke on type — the Stroke panel edits the characters' stroke
With type selected, stroke.set (without an item) gives every run the
weight, cap, join, miter limit and dashes instead of adding an object
stroke; while the Type tool edits text, the Stroke panel styles the
selected characters through text.setRangeStyle `strokeOptions`, and the
panel reads the first run (or the selection's style).

CharStyle gains stroke_cap, stroke_join, stroke_miter_limit and
stroke_dash (defaulted, skipped when default). Character strokes and
object strokes on type now draw through the shared stroke geometry on
the canvas, in PDF and in SVG (live text writes cap/join/miter/dash and
tspans reset what the first run sets; object strokes are drawn on the
glyph outlines), so every stroke option, opacity and blend applies.
Outlines, the Eyedropper and graphic styles keep the options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:09:55 +07:00
bflatastic 8a837fd4b9 M3.68: groups, layers and type have their own fills, strokes and effects, with Contents and Characters rows
A group's or layer's own fills and strokes paint every member's geometry, below or above the
members as the new contents slot (Appearance.contents_index) says; its geometry effects reshape
the members as one piece and its raster effects apply to the composite (one combined shadow),
respecting knockout groups. Type's own fills and strokes paint under or over its characters.
Render, SVG/PDF (via bake) and Expand Appearance evaluate them alike.

Commands: appearance.* / effect.* take target: "object"|"contents" and layer ids;
appearance.moveItem moves the Contents slot (from: "contents") or an item across it
(contents); appearance.targetContents selects a group's members. Graphic styles applied to a
group now style the group itself. The Appearance panel shows Contents/Characters rows that
drag, and double-click to target the members or edit the characters' paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:59:34 +07:00
bflatastic 45ff33e144 M3.75: layer clipping masks from the Layers panel button
layer.clippingMask.toggle {id?} -> {clip}: the top object of the current
layer (or of the one selected group, or of `id`) becomes the clipping path,
unpainted and moved to the bottom so art added later is clipped too; pressing
the button again releases the mask and leaves the path unpainted. One undo
step. The Layers panel button runs it and underlines clipping-path names.

Model: NodeKind::Layer.clip (skipped when false). Clipped layers render, hit
test, bound, and export to SVG and PDF like clip groups (Node::clips /
set_clips); the clip-path helpers are shared with Make/Release Clipping Mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:27:31 +07:00
bflatastic 54427d7a92 M3.74: blending inside a group reaches the art below unless the group isolates it
On screen, a group (or object) with opacity, a blend mode, an opacity mask or
knockout whose content blends is drawn offscreen over a copy of its backdrop
and mixed back in by its opacity, mask and clip (a group blend mode then
applies to the backdrop-free result, as PDF defines it). Isolate Blending keeps
the content starting from transparency. Clip groups no longer isolate their
children (blending content is clipped per drawing), and non-isolated knockout
groups composite their elements against the art below the group. Groups
without blending inside still render as plain layers.

In PDF, such groups are written as non-isolated transparency groups (a
constant-alpha soft mask instead of the writer's isolated opacity group),
knockout groups included.

doc: Node::blends_through / children_blend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:27:31 +07:00
bflatastic a1a5c79b03 M3.73: blend modes follow the reference formulas on screen, in SVG and in PDF
color::blend gains blend_rgb (the 16 blend functions, with the 0.3/0.59/0.11
luminance of the non-separable modes), composite (source-over with blending)
and the shared opacity-mask luminance weights. Exported SVG masks take
luminance from sRGB values (color-interpolation="sRGB"), as the canvas does.

Tests: a per-mode table on the canvas (object, fill and group blend modes,
within 1/255; Exclusion 2/255 in the rasteriser's 8-bit pipeline), the same
table through SVG export rendered by resvg, clip and invert masks against the
canvas, and every mode's PDF name read back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:27:31 +07:00
bflatastic 3981acdb06 Merge wp/C3: dash alignment and variable-width dashes (M3.38, M3.39)
# Conflicts:
#	crates/pdf/src/lib.rs
#	crates/ui-egui/src/panels/mod.rs
2026-10-04 23:48:47 +07:00
bflatastic 43d7ac420d Merge wp/D2
# Conflicts:
#	crates/pdf/src/export.rs
#	crates/render/src/fx.rs
#	crates/render/src/lib.rs
#	crates/svg/src/export.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/dialogs/mod.rs
#	crates/ui-egui/src/panels/appearance.rs
#	docs/control-protocol.md
#	docs/mcp.md
2026-10-04 23:46:38 +07:00
bflatastic 42ad4540b7 M3.38: dashes fitted to corners and path ends, a flag of their own that keeps the offset
With alignDashes every run between corners and path ends holds a whole number of
periods, scaled to fit, with a dash (or dot) centred on each corner and on both
ends of an open path. It lives in the shared dash generator, so the canvas, SVG
and PDF export (as outlines) and Outline Stroke all draw it.

stroke.set: alignDashes and dashOffset edit the current pattern on their own, and
a new dash array keeps the offset and alignment it replaces. The Stroke panel
turns dashes on fitted to corners (or as last turned off), and its Exact/Fit
buttons send only the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:37:58 +07:00
bflatastic 0f1565aed2 M3.43: effects render on type, placed images, symbol instances, blends, envelopes, meshes and repeats; a fill or stroke's own raster effects render and export as SVG filters on that element
Geometry effects reach objects that aren't paths through their outlines (vectorcraft_effects::
reshape: glyph outlines, evaluated live art, the symbol's art, an image clipped by its reshaped
frame), applied to the whole art as one piece; the renderer caches that art per object and
paints raster effects around it in the same filter layers paths use (shared Renderer::raster_fx,
also used per fill/stroke). Exports bake the reshaped art; SVG writes per-item filters; PDF
warns about raster effects on strokes as well as fills and objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:15:13 +07:00
bflatastic abd77d58b0 Merge wp/F2: knockout groups, knockout shape, page groups (M3.46-M3.48)
# Conflicts:
#	crates/pdf/src/export.rs
#	crates/svg/src/export.rs
2026-10-04 23:15:10 +07:00
bflatastic 039c0cbef7 M3.48: Page Isolated Blending and Page Knockout Group
New Document.page_isolate / page_knockout, set by
transparency.togglePageIsolatedBlending / togglePageKnockoutGroup {value?}
(one undo step each, saved with the document, reported by
transparency.info) and the Transparency panel menu's check items. The page
renders as an isolated group (top-level blend modes no longer blend with
what lies under the page) and/or a knockout group whose elements are the
layers (neutral layers pass their objects through). PDF and SVG wrap the
page content in an isolated group and write page knockout as soft masks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:10:06 +07:00
bflatastic f363cdb4ee M3.36: SVG and PDF export draw arrowheads, width profiles, brushes and aligned strokes as the canvas does
Both writers now ask the shared stroke geometry how to write each stroke
(effects::stroke::for_writer): a plain stroke where that is exact, else the
canvas's filled outlines (arrowheads, width profiles, dashes fitted to corners,
dots with round or projecting caps), grouped under the stroke opacity when the
line and its heads overlap. Brushed strokes are written as their brush art.
Inside/outside alignment only applies to closed paths, so open paths export as
centred strokes. The PDF outside-stroke clip frame and the SVG outside mask now
cover the whole stroke reach (miter spikes, projecting caps). SVG stroke
gradients use the same paint box as the canvas. The svg crate docs no longer
claim arrowheads, profiles, brushes and effects are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:07:33 +07:00
bflatastic c95098d15a M3.47: opacity and opacity mask can define an object's knockout shape
New Node.knockout_shape (transparency.set / object.setProps knockoutShape,
reported by transparency.info). In a knockout group such an object knocks
out only as much as its opacity and mask let through (a 50% object knocks
out 50%); otherwise its full coverage knocks out. The renderer, PDF and
SVG masks honour it, and the panel's checkbox is now live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:07:01 +07:00
bflatastic 6e0af835c6 M3.46: three-state knockout groups (on, neutral, off) render, export and edit
Knockout Group is now Knockout {Neutral, Off, On} (old files' true loads as
On, false as Neutral). In a knockout group each child erases what the
children below it drew where it paints, then adds itself, so it composites
against the group's backdrop; neutral plain groups pass their children
through to the enclosing knockout group. transparency.set / object.setProps
take knockout "on"|"off"|"neutral"|bool; transparency.info reports the
state. PDF and SVG write knockout groups as nested soft masks with the same
look (PDF warns); the Transparency panel has a tri-state checkbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:04:20 +07:00
bflatastic 96f1b6852a Merge wp/F1
# Conflicts:
#	crates/doc/src/node.rs
#	crates/engine/src/cmd/appearance.rs
#	crates/engine/src/cmd/paint.rs
#	crates/ui-egui/src/chrome.rs
#	crates/ui-egui/src/panels/transparency.rs
2026-10-04 22:07:49 +07:00
bflatastic 41f2af932c Merge wp/C1
# Conflicts:
#	crates/doc/src/appearance.rs
#	crates/effects/src/stroke/width.rs
#	crates/engine/src/lib.rs
#	crates/pdf/src/export.rs
#	crates/render/src/lib.rs
2026-10-04 21:47:58 +07:00
bflatastic efc0ac31d9 Merge wp/B1
# Conflicts:
#	crates/engine/src/cmd/swatch.rs
#	crates/ui-egui/src/dialogs/mod.rs
#	crates/ui-egui/src/panels/mod.rs
#	docs/control-protocol.md
2026-10-04 21:47:07 +07:00
bflatastic 1ae0c22c26 Merge wp/A1 2026-10-04 21:46:02 +07:00
bflatastic 0af6734b70 Merge M3/M4 wave 0: neutral labels and brands gate, per-area paint modules, one engine loader/encoder (M3.7, M3.8, M4.10-M4.13)
# Conflicts:
#	crates/engine/src/cmd/file.rs
#	crates/mcp/src/headless.rs
#	crates/mcp/src/tools.rs
#	crates/ui-egui/src/io.rs
2026-10-04 21:46:01 +07:00
bflatastic 4f685be57a M3.26: clipping sets with any clip shape
Clip groups clip by compound paths (with their holes), even-odd paths, text
outlines and groups (the union of their members) on screen, in raster
export, in SVG and in PDF alike, through one shared region:
Node::clip_outline (the model walk) and effects::clip_outline (text
outlined, several shapes united), cached per clipping path in the
renderer. Nothing to clip by hides the clipped art everywhere, and clicks
hit a clip group only inside its clipping region. Make Clipping Mask takes
a compound path or text object on top (it loses its paint) and Release
gives it back unchanged in kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:23:57 +07:00
bflatastic 5ae7bfb8ca M3.20: arrowheads: hollow outlines, tip/extend alignment, one opacity for line and head
Hollow heads draw as rings (the open arrow as a chevron) and closed paths get
no heads. The stroke stops under each head, so it never shows through a hollow
head or past the tip; stroke.set {arrowAlign: "extend"|"tip"} chooses whether
the tip extends past the end point (default) or sits on it with the stroke
shortened. Line and heads share one paint box (a gradient runs on into the
head) and take the stroke and object opacity once, as one layer on the canvas
and one group in PDF. StrokeLayer.arrow_align is a new defaulted field, so
older files load unchanged. The Stroke panel's Align buttons get two generated
icons and are disabled when the stroke has no head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:20:40 +07:00
bflatastic 09d7aaa7f4 M3.18: shared stroke-geometry module for arrowheads, width profiles and the profile preset catalogue
Arrowheads, width-profile outlines and dashed line outlines now come from
vectorcraft_effects::stroke (stroke_pieces, line_outline, width_outline), used by
the canvas and PDF export alike; the renderer caches profile outlines too. Width
profiles are a doc-level catalogue (WidthProfile::PRESETS, preset, preset_id,
id_of) shared by stroke.set and the Stroke panel; stroke.set rejects unknown
profile ids and its params doc lists the arrowhead names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:16:17 +07:00
bflatastic aa1b592935 M3.16: the Gradient tool follows the active proxy and type objects — the tool and annotator edit the fill or the stroke by the focused proxy (ToolContext.fill_active); paint.setGradientGeom takes stroke? (default: the active proxy) and index? (one appearance item), sets type objects' vectors on their runs in text space and keeps the aspect the document shows; paint.setFill/setStroke vectors on type move into text space; aspect edits place run gradients on the layout bounds; run strokes (and type appearance strokes) fit the stroke-inflated box on screen and in PDF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:16:12 +07:00
storybored379 c20b7deec5 Stroke dashes: an invalid dash pattern draws a solid stroke instead of hanging the renderer
A dash pattern with a negative value (for example [-5, 3], from `stroke.set` or a PDF `d`
operator) was handed to the dasher whenever any value was positive. When the values sum to zero
or less the dasher never advances, so rendering the document hung forever (canvas, screenshot,
PNG/JPEG export, `vectorcraft-cli convert`).

PDF (32000-1 §8.4.3.6) and SVG (`stroke-dasharray`) both define such a pattern as invalid and the
stroke as solid. `Dash::is_dashed` now says whether a pattern draws dashes (finite, non-negative,
not all zero), and the renderer, the width-profile outline, the SVG and PDF exporters and the PDF
importer use it, so an invalid pattern is drawn, written and read back as a solid stroke everywhere
(the PDF exporter used to clamp negatives to 0, writing zero-length dashes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:25:17 -07:00
bflatastic 00a0874d4d M3.9: swatches in colour groups are first-class — Document Color Mode converts them, they can be spot colours with their own plate and PDF Separation, and Document Info counts them
Document::swatches_iter/_mut, swatch_mut, swatch_group_of, remove_swatch and a copy-on-write
walk over linked solid paints (map_solid_paints) cover every colour group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:15:10 +07:00
bflatastic 5d9b97c3bd M4.10: neutral file, preference and MCP wording; vendor-name CI gate
Edit -> "PDF Presets…"; the clipboard preferences read "Legacy vector
clipboard (no transparency)" / "Legacy vector clipboard" (keys copyAicb and
aicbMode unchanged); the second keyboard shortcut set is "Classic Defaults",
and the old set name is still accepted from saved UI preferences, imported
sets and shortcuts.preset. The MCP server instructions, the apply_effect and
create_graph tool descriptions, the text.createInPath params doc,
docs/mcp.md, the ui-egui crate description and the PDF compatibility docs
no longer name another vendor.

New `cargo xtask brands` (run by `cargo xtask ci`) fails on vendor product
or company names in Rust string literals (labels, params docs, menus,
panels, MCP definitions; comments and test code skipped), Cargo.toml
descriptions and text under docs/ and packaging/; a required legacy alias
is marked `brand-ok`. xtask shares one git file listing between the asset
and brand checks. docs/development.md describes the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:47:28 +07:00
Brandon Thomas 09fc2eedd9 M4.3: live effects survive SVG, PDF and clipboard export (vectorcraft_effects::bake_document evaluates object, per-fill/stroke and Pathfinder effects on a copy; SVG raster effects become filters: drop shadow, outer/inner glow, Gaussian blur, feather)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:39:20 -04:00
Brandon Thomas 4878f1c5dd Rename DrawCraft → VectorCraft: crates (vectorcraft-*), apps, binaries, types, env vars, docs, examples (.vectorcraft); old .drawcraft files and preferences still load. Fix: text bounds exact after opening a file; selection no longer marks a document modified (dirty = content differs from the saved document). Screenshots and renders regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:30:09 -04:00
Brandon Thomas 5e15994012 M3: opacity masks — model (linked transform), luminance mask rendering, transparency.* commands, SVG <mask> export/import, PDF luminosity soft masks, Transparency panel controls
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:30:38 -04:00
Brandon Thomas acb2566d89 Distortion tools (Width, Liquify, Puppet Warp, Perspective Grid), Patterns + Repeat, colour management (CMS, proofing, separations), Preferences/Shortcuts/Workspaces; original brand mark; fix filter panic in thumbnails; README demo scenes
- New original DrawCraft mark (gradient + pen nib) replaces the two-letter badge
- render: thumbnails and pattern tiles use a single-threaded context (vello's default is
  multithreaded and panics on filters); per-item raster effects detected
- Type tool state no longer leaks across documents
- ui.set {status} for clean screenshots
- README: new hero, feature shots, Made in DrawCraft gallery; examples/neon-drive, ribbons,
  feature-sheet; ASSETS.md updated; heading renamed to Crafting Apps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:44:39 -04:00
Brandon Thomas c9f38685b4 wip 2026-09-30 11:37:31 -04:00
Brandon Thomas 6dd0d2dcff continued work; wip 2026-09-30 11:24:38 -04:00
Brandon Thomas f8136f1e35 Live Blends/Envelopes/Gradient Mesh, Shape Builder, Live Paint, Image Trace (drawcraft-trace), Recolor Artwork, Export for Screens, Actions panel, prefs persistence, real layer thumbnails; menus bound
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:02:16 -04:00
Brandon Thomas 6d21d36d16 M2/M8/M9/M4.4: drawing + transform tools, live effects, PDF export/import, file/batch commands; UI restyled to measured Illustrator metrics (Medium Dark, categorized toolbar, hint bar, task bar, Layers)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:55:49 -04:00
Brandon Thomas bfee80a22c M0: workspace skeleton — geom, color, doc, render, text, pathops, svg, tools, engine
Engine-first Cargo workspace for DrawCraft (clean-room Illustrator):
- geom: anchor-based path model, shapes, hit testing, snapping
- color: colours, paints, gradients, swatches, blend modes, harmonies
- doc: persistent Arc document tree, appearance stacks, text model, selection
- render: vello_cpu renderer (appearance, gradients, clip groups, blend modes)
- text: font DB, harfrust shaping, point/area/path layout
- pathops: curve booleans (linesweeper), Pathfinder, offset, simplify
- svg: import (usvg) and export
- tools: selection/direct/pen/shape tools emitting commands
- engine: Session, history with interactions, ~110 commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 05:53:29 -04:00