* M8: .fig import: a routed connector takes its line style from the generated record (#1807)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* M8: .fig import: a routed connector keeps its own empty paints and dash pattern
CodeRabbit review on #1813: an explicit empty strokePaints (no stroke) or
dashPattern (solid) on the connector no longer takes the generated record's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M8: an arc's spread shadow gets room for its mitred corners
Rectangles, frames and ellipses grow by exactly their spread, so their
layers only make room for the spread itself; other shapes make room for
the mitre at sharp corners (up to four times the spread). An ellipse
with arc data (a pie slice, or a ring cut short) counted as an ellipse,
but its ends meet its sides in sharp corners: a spread of 25 on a slice
of 0.3 rad cut its shadow off at the layer's edge (up to 255 levels).
Only an ellipse that goes all the way round now grows by exactly its
spread. `figcraft_geom::full_sweep` is what `ellipse` already used to
tell a whole ellipse or ring from an arc, so the two agree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M8: an arc's mitred stroke gets room for its corners too
Review: `outline_reach` counted every ellipse as an outline whose stroke
mitres end on the grown box, the same mistake as the spread. A thin
slice with a 20-unit mitred stroke and a spread of 10 still had its
shadow cut off (255 levels). It now asks `grows_by_exactly_its_spread`,
which picks the same shapes and leaves arcs out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The SVG export blurs a spread drop shadow in a filter whose region is
the silhouette's page bounds grown by the spread's mitre and the blur.
The spread grows the outline in the node's own units, though, and the
transform then scales it: on a star scaled three times, a spread of 10
mitred out to some 97 units while the region only made room for 48, so
SVG viewers cut the shadow's points off. The spread now grows the region
as far across and down as the transform takes a local step of that
length, as the paint bounds already do.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* text: use craft-font-catalog on Unix
On Linux and BSD distributions that do not follow traditional FHS layout
(such as NixOS and Guix), or systems using custom font configurations,
fonts are not located under `/usr/share/fonts` or `/usr/local/share/fonts`.
Instead, font directories are declared in `/etc/fonts/fonts.conf` and its
included configuration files (such as `/etc/fonts/conf.d`), or under
`$XDG_DATA_DIRS/fonts`.
`figcraft-text` already depends on `craft-font-catalog`, which provides
`craft_font_catalog::font_dirs`:
- Preserves all existing directory sources (`/usr/share/fonts`,
`/usr/local/share/fonts`, and user font directories).
- Adds Flatpak host directories (`/run/host/fonts`), `$XDG_DATA_DIRS`,
and fontconfig directory trees (`/etc/fonts/fonts.conf`).
- Deduplicates the combined list via `craft_font_catalog::dedup_dirs`
using canonical paths, ensuring equivalent directories and symlinks
are not redundantly scanned.
Use `craft_font_catalog::font_dirs` in `system_font_dirs()` on Unix, and
retain `unix_user_font_dirs` under `#[cfg(test)]` for its unit tests.
* text: walk each system font folder once
Fontconfig usually lists folders the scan already has (/usr/share/fonts); the list is deduplicated
so their faces aren't indexed twice (CodeRabbit on #1765).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* text: scan the catalog's full Unix font folder list
fontconfig_dirs alone dropped /usr/share/fonts, ~/.fonts and the XDG folders when no fontconfig
configuration exists; font_dirs keeps them, adds what fontconfig names, and deduplicates, as the
PR description intended. The local user-folder helper it replaces is removed with its tests (the
catalog applies the same XDG rules).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/variables_io.rs:~80-105 (css, non-tailwind `name` closure using css_var_of) with crates/engine/src/dev/codegen/css.rs:41 (css_var_name)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/dev/codegen/css.rs:121 (css_decls, the PaintKind::Gradient arm calling css_gradient at :255)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* Escape backslashes and line breaks in CSS font-family export
Found while testing crates/engine/src/dev/codegen/css.rs:223 (text_decls, the font-family put)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Escape quotes and < in CSS font-family export
A double quote in a family name is escaped instead of turned into a single quote (CodeRabbit on
#1752), and < is written as \3C so a name can't end the generated page's <style> block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found while testing crates/ui-egui/src/panels/typography/spacing.rs:166 (parse_line_height, px branch; also :183 and :180 in parse_letter_spacing, px and % branches)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/ui-egui/src/widgets/field_math.rs:168 (stepped, used by widgets.rs:131 scrubbed)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* Fix code export printing inf for very large finite numbers
Found while testing crates/engine/src/dev/codegen.rs:185 (num)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Keep rem values finite past num6's scaling overflow
num6 had the same x1e6 overflow as num: a huge finite length printed 0rem (CodeRabbit on #1757).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M3: library analytics list how each style and variable is used
A file reports how many of its layers use each of a team library's styles and
variables, and Show usage lists them by a Type choice with their layers and
files. Layers in instances count for their component. Also ignores plugins/target.
* M3: the style analytics test waits for the instance report too
Found while testing crates/color/src/lib.rs:247 (css_args, used by the rgb/rgba and hsl/hsla arms of parse_css at :159 and :178)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A widget description can have a line, 100 long and black and 1 thick unless told
otherwise, and an SVG that comes in as locked layers, scaled to fit a size and
within the widget's 256 layers. The Counter example uses both.
Both reports were checked by running them and are already fixed on main. Pins a rounded rectangle or frame stroking only the side asked for in every alignment, and SVG font families and ids leaving out characters XML forbids.
The desktop shell swallows a plain Cmd/Ctrl+V, press and release, and sends an
image paste event; FigCraft ignored it and waited for a release that no longer
comes. The browser's paste event gives eframe only text, so the page now reads
the image from it, under the same limits.
Bound variables with an iOS or Android code syntax are named in generated SwiftUI and Compose in place of their value: paints, stroke weight, sizes, corner radius, gaps, padding, opacity and text. A variable with none for the platform keeps the value.
Choosing Image shows a checkered placeholder and Upload from computer instead of a dialog at once; an image paint shows its asset preview. A file dropped on the open picker replaces the image, keeping the fill mode, crop, position and adjustments (image.replace, one undo step; paths capped as image.place's). The browser's file picker reaches the tab too. Also fixes a race in library_detach_tests.
Found while testing crates/render/src/draw.rs:190-240 (stroke_area, the Inside/Outside ring fast path)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gap 10: a file counts the library instances it places, creates, pastes or duplicates and reports them like its detaches. The server counts what a file's totals grow past their highest in the current week and keeps a year of weeks (actions.json). library.usage {days} and Show usage give insertions and detachments over the past 30 days, 90 days or year. New variants are appended; an older server refuses them and Show usage says so.
A request line can carry timeoutMs beside id and method, capped at 600,000 ms; without it the wait stays 60 s and older apps ignore the field. figcraft-cli mcp --attach PORT --timeout SECONDS sends it with every request and reads the answers that much longer. A timeout's message says how long the request waited.
* M11: a widget opens its plugin's panel (plugin API 5)
Gap 6: a widget callback's ui_show opens the plugin's panel and keeps the module loaded; the panel's callbacks get the widget's id and state and redraw it in the same undo step. Older plugins' ui_show from a widget still opens nothing. The Counter example sets its count from a panel.
* M11: open_panel takes its opener as one value
Gap 14: variable.extendCollection and Extend collection on a collection's row in the Variables editor. Extensions follow edits of the collection they extend at once (normalising keeps them in step whenever variables change), extensions extend again, and a loop of extensions from a hostile file comes apart into plain collections.