mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Insert: Lucide icon gallery as stroked shapes
Insert › Icons places seventy Lucide 1.56.0 stroke icons as custom geometry: no fill, round caps, and a line width scaled from the 24-unit design. The gallery searches by name. SVG paths, including arcs, are converted when the icon is inserted so the renderer strokes the outline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,8 @@ fails if a file under `assets/`, `docs/images/` or `docs/brand/` is missing from
|
||||
DeckCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, fonts, themes,
|
||||
templates or other assets**. Its themes, colour schemes, layouts, preset-shape geometry, sample
|
||||
deck and UI icons are original work of the DeckCraft contributors (drawn or generated in code).
|
||||
Icons placed from Insert › Icons are Lucide paths (ISC; Feather-derived icons MIT), not Microsoft
|
||||
artwork. The ribbon and menu icons stay original drawings.
|
||||
|
||||
## In this repository
|
||||
|
||||
@@ -31,6 +33,7 @@ deck and UI icons are original work of the DeckCraft contributors (drawn or gene
|
||||
| `crates/fonts/tests/fixtures/legacy-semibold.ttf` | DeckCraft contributors | a one-glyph Semibold face named like Segoe UI Semibold (its Windows family differs from the typographic one), built by `make_legacy_font.py` (fontTools as the builder only) | CC0-1.0 | Font lookup by Windows family name tests |
|
||||
| `crates/fonts/tests/fixtures/legacy-regular.ttf` | DeckCraft contributors | the same family's one-glyph Regular face, built by `make_legacy_font.py` (fontTools as the builder only) | CC0-1.0 | Font lookup by Windows family name tests |
|
||||
| `crates/render/tests/fixtures/make_colr_font.py` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the COLR test font |
|
||||
| Lucide icon paths in `crates/engine/src/icon_data.rs` (Insert › Icons) | Lucide Contributors; Feather icons by Cole Bemis | https://github.com/lucide-icons/lucide/tree/440a1e1dfb1e712505308da46bead3c28b1f1398/icons (release 1.56.0, 2026-10-13) | ISC; MIT for the Feather-derived icons named in that commit's LICENSE | Stroke icons inserted as shapes |
|
||||
|
||||
## Bundled through dependencies
|
||||
|
||||
|
||||
@@ -19,6 +19,51 @@ list (author, source, license) is in ATTRIBUTION.md.
|
||||
ATTRIBUTION.md (Inter, Carlito, Caladea, Liberation, Montserrat, Lato, Open Sans, Roboto,
|
||||
Merriweather, Playfair Display, Poppins, Nunito Sans, BIZ UD, Shippori Mincho, Noto Sans
|
||||
Arabic) under the SIL Open Font License 1.1; their licence texts ship with release packages.
|
||||
- Lucide icon paths embedded in crates/engine/src/icon_data.rs (Insert › Icons), release 1.56.0,
|
||||
commit 440a1e1dfb1e712505308da46bead3c28b1f1398, https://github.com/lucide-icons/lucide.
|
||||
ISC License, Copyright (c) 2026 Lucide Icons and Contributors. Icons Lucide derives from Feather
|
||||
are MIT, Copyright (c) 2013-present Cole Bemis; which names those are is the list in Lucide's
|
||||
LICENSE at that commit. Among the paths embedded here, the Feather-derived names are arrow-down,
|
||||
arrow-left, arrow-right, arrow-up, at-sign, calendar, check, chevron-right, clipboard, clock,
|
||||
compass, download, info, key, link, lock, minus, moon, move, music, plus, search, target and x.
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2026 Lucide Icons and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
The MIT License (MIT) (for the Feather-derived icons)
|
||||
|
||||
Copyright (c) 2013-present Cole Bemis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Code dependencies
|
||||
|
||||
|
||||
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Lucide icon gallery; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
@@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-14 | Insert › Icons: a gallery of Lucide stroke icons (release 1.56.0) inserted as custom shapes, no fill, round stroke scaled from the 24-unit design |
|
||||
| 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless |
|
||||
| 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline |
|
||||
| 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting |
|
||||
@@ -150,6 +151,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | trivial | Progress log: Lucide icon gallery |
|
||||
| 2026-10-13 | trivial | Progress log: wild text cases |
|
||||
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
|
||||
| 2026-10-12 | trivial | Progress log: lines in scaled groups (#233) |
|
||||
|
||||
@@ -5,7 +5,7 @@ use deckcraft_geom::Xfrm;
|
||||
use deckcraft_model::chart::ChartType;
|
||||
use deckcraft_model::style::PictureFill;
|
||||
use deckcraft_model::text::{AutoFit, RunKind, TextBody};
|
||||
use deckcraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table};
|
||||
use deckcraft_model::{Chart, ColorRef, Fill, Geom, Line, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
@@ -22,6 +22,16 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_slide,
|
||||
insert_shape
|
||||
),
|
||||
cmd!(
|
||||
"insert.icon",
|
||||
"Icons",
|
||||
["Insert", "Illustrations"],
|
||||
None,
|
||||
"{name: star|arrow-right|… (see insert.icons), rect?: [x,y,w,h]}",
|
||||
has_slide,
|
||||
insert_icon
|
||||
),
|
||||
cmd!(query "insert.icons", "Icon Gallery", [], None, "{} → [{name, label, category}]", always, list_icons),
|
||||
cmd!(query "shape.presets", "Shape Gallery", [], None, "{} → [{name, label, category}]", always, presets),
|
||||
cmd!("insert.textBox", "Text Box", ["Insert", "Text"], None, "{rect?: [x,y,w,h], text?: string}", has_slide, text_box),
|
||||
cmd!(
|
||||
@@ -186,6 +196,46 @@ fn presets(_s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
))
|
||||
}
|
||||
|
||||
fn list_icons(_s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
Ok(Value::Array(crate::icons::icons().iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category})).collect()))
|
||||
}
|
||||
|
||||
/// A Lucide stroke icon: custom geometry, no fill, round line scaled from the 24-unit design.
|
||||
fn insert_icon(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("insert.icon", "missing `name`"))?;
|
||||
let icon = crate::icons::find(name).ok_or_else(|| bad("insert.icon", format!("unknown icon `{name}`")))?;
|
||||
let d = crate::icons::path_data(icon.name).ok_or_else(|| bad("insert.icon", format!("could not build icon `{}`", icon.name)))?;
|
||||
let rect = rect_param(p, "rect").unwrap_or_else(|| default_rect(s, 72.0, 72.0));
|
||||
if rect.w < 1.0 || rect.h < 1.0 {
|
||||
return Err(bad("insert.icon", "rect needs a positive width and height"));
|
||||
}
|
||||
let shape = Shape {
|
||||
name: icon.label.to_string(),
|
||||
xfrm: Some(rect),
|
||||
geom: Geom::Custom {
|
||||
paths: vec![deckcraft_model::CustomPath {
|
||||
w: crate::icons::VIEW,
|
||||
h: crate::icons::VIEW,
|
||||
d,
|
||||
fill: deckcraft_geom::preset::FillMode::None,
|
||||
stroke: true,
|
||||
}],
|
||||
},
|
||||
fill: Some(Fill::None),
|
||||
line: Some(Line {
|
||||
fill: Some(Fill::solid(ColorRef::scheme(SchemeSlot::Tx1))),
|
||||
width: Some(crate::icons::stroke_width(icon, rect.w, rect.h)),
|
||||
cap: Some(deckcraft_model::style::LineCap::Round),
|
||||
join: Some(deckcraft_model::style::LineJoin::Round),
|
||||
..Default::default()
|
||||
}),
|
||||
style: Some(ShapeStyle::line(SchemeSlot::Tx1)),
|
||||
..Default::default()
|
||||
};
|
||||
let id = add_shape(s, shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
pub(crate) fn new_text_box(rect: Xfrm, text: &str) -> Shape {
|
||||
let mut body = TextBody::from_text(text);
|
||||
body.body.wrap = Some(true);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Lucide 1.56.0 (commit 440a1e1dfb1e712505308da46bead3c28b1f1398).
|
||||
// Stroke centerlines on a 24×24 viewBox. Independent SVG subpaths are separated by ` | `.
|
||||
// ISC; Feather-derived icons MIT. See NOTICE and ATTRIBUTION.md.
|
||||
static ICONS: &[Icon] = &[
|
||||
Icon { name: "arrow-up", label: "Arrow up", category: "Arrows", stroke: 2.0, d: "m5 12 7-7 7 7 | M12 19V5" },
|
||||
Icon { name: "arrow-down", label: "Arrow down", category: "Arrows", stroke: 2.0, d: "M12 5v14 | m19 12-7 7-7-7" },
|
||||
Icon { name: "arrow-left", label: "Arrow left", category: "Arrows", stroke: 2.0, d: "m12 19-7-7 7-7 | M19 12H5" },
|
||||
Icon { name: "arrow-right", label: "Arrow right", category: "Arrows", stroke: 2.0, d: "M5 12h14 | m12 5 7 7-7 7" },
|
||||
Icon { name: "chevron-right", label: "Chevron", category: "Arrows", stroke: 2.0, d: "m9 18 6-6-6-6" },
|
||||
Icon { name: "undo-2", label: "Undo", category: "Arrows", stroke: 2.0, d: "M9 14 4 9l5-5 | M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" },
|
||||
Icon { name: "refresh-cw", label: "Refresh", category: "Arrows", stroke: 2.0, d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8 | M21 3v5h-5 | M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16 | M8 16H3v5" },
|
||||
Icon { name: "move", label: "Move", category: "Arrows", stroke: 2.0, d: "M12 2v20 | m15 19-3 3-3-3 | m19 9 3 3-3 3 | M2 12h20 | m5 9-3 3 3 3 | m9 5 3-3 3 3" },
|
||||
Icon { name: "check", label: "Check", category: "Marks", stroke: 2.0, d: "M20 6 9 17l-5-5" },
|
||||
Icon { name: "x", label: "X", category: "Marks", stroke: 2.0, d: "M18 6 6 18 | m6 6 12 12" },
|
||||
Icon { name: "plus", label: "Plus", category: "Marks", stroke: 2.0, d: "M5 12h14 | M12 5v14" },
|
||||
Icon { name: "minus", label: "Minus", category: "Marks", stroke: 2.0, d: "M5 12h14" },
|
||||
Icon { name: "star", label: "Star", category: "Marks", stroke: 2.0, d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" },
|
||||
Icon { name: "heart", label: "Heart", category: "Marks", stroke: 2.0, d: "M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" },
|
||||
Icon { name: "flag", label: "Flag", category: "Marks", stroke: 2.0, d: "M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528" },
|
||||
Icon { name: "bookmark", label: "Bookmark", category: "Marks", stroke: 2.0, d: "M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" },
|
||||
Icon { name: "award", label: "Award", category: "Marks", stroke: 2.0, d: "m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526 | M 6 8 A 6 6 0 0 1 12 2 A 6 6 0 0 1 18 8 A 6 6 0 0 1 12 14 A 6 6 0 0 1 6 8 Z" },
|
||||
Icon { name: "thumbs-up", label: "Thumbs up", category: "Marks", stroke: 2.0, d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z | M7 10v12" },
|
||||
Icon { name: "circle-check", label: "Check circle", category: "Marks", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | m16 9-5.5 5.5L8 12" },
|
||||
Icon { name: "triangle-alert", label: "Warning", category: "Marks", stroke: 2.0, d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3 | M12 9v4 | M12 17h.01" },
|
||||
Icon { name: "user", label: "User", category: "People", stroke: 2.0, d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2 | M 8 7 A 4 4 0 0 1 12 3 A 4 4 0 0 1 16 7 A 4 4 0 0 1 12 11 A 4 4 0 0 1 8 7 Z" },
|
||||
Icon { name: "users", label: "Users", category: "People", stroke: 2.0, d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 | M16 3.128a4 4 0 0 1 0 7.744 | M22 21v-2a4 4 0 0 0-3-3.87 | M 5 7 A 4 4 0 0 1 9 3 A 4 4 0 0 1 13 7 A 4 4 0 0 1 9 11 A 4 4 0 0 1 5 7 Z" },
|
||||
Icon { name: "circle-user", label: "Account", category: "People", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | M 9 10 A 3 3 0 0 1 12 7 A 3 3 0 0 1 15 10 A 3 3 0 0 1 12 13 A 3 3 0 0 1 9 10 Z | M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" },
|
||||
Icon { name: "hand", label: "Hand", category: "People", stroke: 2.0, d: "M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2 | M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2 | M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8 | M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" },
|
||||
Icon { name: "calendar", label: "Calendar", category: "Time", stroke: 2.0, d: "M8 2v3 | M16 2v3 | M 5 3 H 19 A 2 2 0 0 1 21 5 V 19 A 2 2 0 0 1 19 21 H 5 A 2 2 0 0 1 3 19 V 5 A 2 2 0 0 1 5 3 Z | M3 9h18" },
|
||||
Icon { name: "clock", label: "Clock", category: "Time", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | M12 6v6l4 2" },
|
||||
Icon { name: "hourglass", label: "Hourglass", category: "Time", stroke: 2.0, d: "M5 22h14 | M5 2h14 | M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22 | M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2" },
|
||||
Icon { name: "timer", label: "Timer", category: "Time", stroke: 2.0, d: "M 10 2 L 14 2 | M 12 14 L 15 11 | M 4 14 A 8 8 0 0 1 12 6 A 8 8 0 0 1 20 14 A 8 8 0 0 1 12 22 A 8 8 0 0 1 4 14 Z" },
|
||||
Icon { name: "mail", label: "Mail", category: "Communication", stroke: 2.0, d: "m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7 | M 4 4 H 20 A 2 2 0 0 1 22 6 V 18 A 2 2 0 0 1 20 20 H 4 A 2 2 0 0 1 2 18 V 6 A 2 2 0 0 1 4 4 Z" },
|
||||
Icon { name: "phone", label: "Phone", category: "Communication", stroke: 2.0, d: "M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384" },
|
||||
Icon { name: "message-circle", label: "Message", category: "Communication", stroke: 2.0, d: "M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" },
|
||||
Icon { name: "bell", label: "Bell", category: "Communication", stroke: 2.0, d: "M10.268 21a2 2 0 0 0 3.464 0 | M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" },
|
||||
Icon { name: "megaphone", label: "Megaphone", category: "Communication", stroke: 2.0, d: "M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z | M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14 | M8 6v8" },
|
||||
Icon { name: "at-sign", label: "Mention", category: "Communication", stroke: 2.0, d: "M 8 12 A 4 4 0 0 1 12 8 A 4 4 0 0 1 16 12 A 4 4 0 0 1 12 16 A 4 4 0 0 1 8 12 Z | M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8" },
|
||||
Icon { name: "chart-column", label: "Column chart", category: "Charts", stroke: 2.0, d: "M3 3v16a2 2 0 0 0 2 2h16 | M18 17V9 | M13 17V5 | M8 17v-3" },
|
||||
Icon { name: "chart-bar", label: "Bar chart", category: "Charts", stroke: 2.0, d: "M3 3v16a2 2 0 0 0 2 2h16 | M7 16h8 | M7 11h12 | M7 6h3" },
|
||||
Icon { name: "chart-pie", label: "Pie chart", category: "Charts", stroke: 2.0, d: "M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z | M21.21 15.89A10 10 0 1 1 8 2.83" },
|
||||
Icon { name: "trending-up", label: "Trend", category: "Charts", stroke: 2.0, d: "M16 7h6v6 | m22 7-8.5 8.5-5-5L2 17" },
|
||||
Icon { name: "target", label: "Target", category: "Charts", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | M 6 12 A 6 6 0 0 1 12 6 A 6 6 0 0 1 18 12 A 6 6 0 0 1 12 18 A 6 6 0 0 1 6 12 Z | M 10 12 A 2 2 0 0 1 12 10 A 2 2 0 0 1 14 12 A 2 2 0 0 1 12 14 A 2 2 0 0 1 10 12 Z" },
|
||||
Icon { name: "lightbulb", label: "Idea", category: "Charts", stroke: 2.0, d: "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5 | M9 18h6 | M10 22h4" },
|
||||
Icon { name: "presentation", label: "Presentation", category: "Charts", stroke: 2.0, d: "M2 3h20 | M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3 | m7 21 5-5 5 5" },
|
||||
Icon { name: "file", label: "File", category: "Files", stroke: 2.0, d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z | M14 2v5a1 1 0 0 0 1 1h5" },
|
||||
Icon { name: "folder", label: "Folder", category: "Files", stroke: 2.0, d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" },
|
||||
Icon { name: "file-text", label: "Document", category: "Files", stroke: 2.0, d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z | M14 2v5a1 1 0 0 0 1 1h5 | M10 9H8 | M16 13H8 | M16 17H8" },
|
||||
Icon { name: "paperclip", label: "Attachment", category: "Files", stroke: 2.0, d: "m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" },
|
||||
Icon { name: "clipboard", label: "Clipboard", category: "Files", stroke: 2.0, d: "M 9 2 H 15 A 1 1 0 0 1 16 3 V 5 A 1 1 0 0 1 15 6 H 9 A 1 1 0 0 1 8 5 V 3 A 1 1 0 0 1 9 2 Z | M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" },
|
||||
Icon { name: "link", label: "Link", category: "Files", stroke: 2.0, d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71 | M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" },
|
||||
Icon { name: "download", label: "Download", category: "Files", stroke: 2.0, d: "M12 15V3 | M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 | m7 10 5 5 5-5" },
|
||||
Icon { name: "share-2", label: "Share", category: "Files", stroke: 2.0, d: "M 15 5 A 3 3 0 0 1 18 2 A 3 3 0 0 1 21 5 A 3 3 0 0 1 18 8 A 3 3 0 0 1 15 5 Z | M 3 12 A 3 3 0 0 1 6 9 A 3 3 0 0 1 9 12 A 3 3 0 0 1 6 15 A 3 3 0 0 1 3 12 Z | M 15 19 A 3 3 0 0 1 18 16 A 3 3 0 0 1 21 19 A 3 3 0 0 1 18 22 A 3 3 0 0 1 15 19 Z | M 8.59 13.51 L 15.42 17.49 | M 15.41 6.51 L 8.59 10.49" },
|
||||
Icon { name: "house", label: "Home", category: "Places", stroke: 2.0, d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8 | M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" },
|
||||
Icon { name: "map-pin", label: "Pin", category: "Places", stroke: 2.0, d: "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0 | M 9 10 A 3 3 0 0 1 12 7 A 3 3 0 0 1 15 10 A 3 3 0 0 1 12 13 A 3 3 0 0 1 9 10 Z" },
|
||||
Icon { name: "globe", label: "Globe", category: "Places", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20 | M2 12h20" },
|
||||
Icon { name: "building", label: "Building", category: "Places", stroke: 2.0, d: "M12 10h.01 | M12 14h.01 | M12 6h.01 | M16 10h.01 | M16 14h.01 | M16 6h.01 | M8 10h.01 | M8 14h.01 | M8 6h.01 | M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3 | M 6 2 H 18 A 2 2 0 0 1 20 4 V 20 A 2 2 0 0 1 18 22 H 6 A 2 2 0 0 1 4 20 V 4 A 2 2 0 0 1 6 2 Z" },
|
||||
Icon { name: "compass", label: "Compass", category: "Places", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" },
|
||||
Icon { name: "briefcase", label: "Briefcase", category: "Places", stroke: 2.0, d: "M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16 | M 4 6 H 20 A 2 2 0 0 1 22 8 V 18 A 2 2 0 0 1 20 20 H 4 A 2 2 0 0 1 2 18 V 8 A 2 2 0 0 1 4 6 Z" },
|
||||
Icon { name: "image", label: "Picture", category: "Media", stroke: 2.0, d: "M 5 3 H 19 A 2 2 0 0 1 21 5 V 19 A 2 2 0 0 1 19 21 H 5 A 2 2 0 0 1 3 19 V 5 A 2 2 0 0 1 5 3 Z | M 7 9 A 2 2 0 0 1 9 7 A 2 2 0 0 1 11 9 A 2 2 0 0 1 9 11 A 2 2 0 0 1 7 9 Z | m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" },
|
||||
Icon { name: "camera", label: "Camera", category: "Media", stroke: 2.0, d: "M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z | M 9 13 A 3 3 0 0 1 12 10 A 3 3 0 0 1 15 13 A 3 3 0 0 1 12 16 A 3 3 0 0 1 9 13 Z" },
|
||||
Icon { name: "play", label: "Play", category: "Media", stroke: 2.0, d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" },
|
||||
Icon { name: "clapperboard", label: "Clapperboard", category: "Media", stroke: 2.0, d: "m12.296 3.464 3.02 3.956 | M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3z | M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z | m6.18 5.276 3.1 3.899" },
|
||||
Icon { name: "music", label: "Music", category: "Media", stroke: 2.0, d: "M9 18V5l12-2v13 | M 3 18 A 3 3 0 0 1 6 15 A 3 3 0 0 1 9 18 A 3 3 0 0 1 6 21 A 3 3 0 0 1 3 18 Z | M 15 16 A 3 3 0 0 1 18 13 A 3 3 0 0 1 21 16 A 3 3 0 0 1 18 19 A 3 3 0 0 1 15 16 Z" },
|
||||
Icon { name: "settings", label: "Settings", category: "Objects", stroke: 2.0, d: "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915 | M 9 12 A 3 3 0 0 1 12 9 A 3 3 0 0 1 15 12 A 3 3 0 0 1 12 15 A 3 3 0 0 1 9 12 Z" },
|
||||
Icon { name: "search", label: "Search", category: "Objects", stroke: 2.0, d: "m21 21-4.34-4.34 | M 3 11 A 8 8 0 0 1 11 3 A 8 8 0 0 1 19 11 A 8 8 0 0 1 11 19 A 8 8 0 0 1 3 11 Z" },
|
||||
Icon { name: "info", label: "Info", category: "Objects", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | M12 16v-4 | M12 8h.01" },
|
||||
Icon { name: "circle-alert", label: "Alert", category: "Objects", stroke: 2.0, d: "M 2 12 A 10 10 0 0 1 12 2 A 10 10 0 0 1 22 12 A 10 10 0 0 1 12 22 A 10 10 0 0 1 2 12 Z | M 12 8 L 12 12 | M 12 16 L 12.01 16" },
|
||||
Icon { name: "lock", label: "Lock", category: "Objects", stroke: 2.0, d: "M 5 11 H 19 A 2 2 0 0 1 21 13 V 20 A 2 2 0 0 1 19 22 H 5 A 2 2 0 0 1 3 20 V 13 A 2 2 0 0 1 5 11 Z | M7 11V7a5 5 0 0 1 10 0v4" },
|
||||
Icon { name: "key", label: "Key", category: "Objects", stroke: 2.0, d: "m2 21 9.6-9.6 | m7.5 15.5 2.3 2.3a1 1 0 0 1 0 1.4l-2.1 2.1a1 1 0 0 1-1.4 0L4 19 | M 10 7.5 A 5.5 5.5 0 0 1 15.5 2 A 5.5 5.5 0 0 1 21 7.5 A 5.5 5.5 0 0 1 15.5 13 A 5.5 5.5 0 0 1 10 7.5 Z" },
|
||||
Icon { name: "zap", label: "Bolt", category: "Objects", stroke: 2.0, d: "M15.914 4a1.5 1.5 0 00-2.474-1.561l-9 9A1.5 1.5 0 005.5 14h4.002a.5.5 0 01.471.666L8.086 20a1.5 1.5 0 002.475 1.56l9-9A1.5 1.5 0 0018.5 10h-3.997a.5.5 0 01-.472-.667z" },
|
||||
Icon { name: "sun", label: "Sun", category: "Objects", stroke: 2.0, d: "M 8 12 A 4 4 0 0 1 12 8 A 4 4 0 0 1 16 12 A 4 4 0 0 1 12 16 A 4 4 0 0 1 8 12 Z | M12 2v2 | M12 20v2 | m4.93 4.93 1.41 1.41 | m17.66 17.66 1.41 1.41 | M2 12h2 | M20 12h2 | m6.34 17.66-1.41 1.41 | m19.07 4.93-1.41 1.41" },
|
||||
Icon { name: "moon", label: "Moon", category: "Objects", stroke: 2.0, d: "M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" },
|
||||
Icon { name: "cloud", label: "Cloud", category: "Objects", stroke: 2.0, d: "M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" },
|
||||
];
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Insert › Icons: Lucide stroke icons as custom geometry.
|
||||
//!
|
||||
//! The paths in `icon_data.rs` are Lucide 1.56.0 centerlines (24×24, fill none). They are converted
|
||||
//! to DeckCraft's `M`/`L`/`C`/`Q`/`Z` syntax when inserted, so the shape is a stroked outline rather
|
||||
//! than a filled silhouette or an SVG picture.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use deckcraft_geom::{BezPath, PathEl};
|
||||
|
||||
/// Design size of every icon, in path units. Lucide's viewBox is `0 0 24 24`.
|
||||
pub const VIEW: f64 = 24.0;
|
||||
|
||||
/// One icon from the Insert gallery.
|
||||
pub struct Icon {
|
||||
pub name: &'static str,
|
||||
pub label: &'static str,
|
||||
pub category: &'static str,
|
||||
/// Design stroke width in viewBox units (Lucide's default is 2).
|
||||
pub stroke: f64,
|
||||
/// SVG path data. Independent subpaths (separate SVG elements) are separated by ` | `.
|
||||
pub d: &'static str,
|
||||
}
|
||||
|
||||
include!("icon_data.rs");
|
||||
|
||||
/// Every icon, in gallery order.
|
||||
pub fn icons() -> &'static [Icon] {
|
||||
ICONS
|
||||
}
|
||||
|
||||
/// `name` is the Lucide id (`arrow-right`, `arrow_right`) or the gallery label (`Arrow right`).
|
||||
pub fn find(name: &str) -> Option<&'static Icon> {
|
||||
let raw = name.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let key = raw.to_ascii_lowercase().replace('_', "-");
|
||||
ICONS.iter().find(|i| i.name == key || i.label.eq_ignore_ascii_case(raw))
|
||||
}
|
||||
|
||||
/// The icon's outline in the 24×24 viewBox.
|
||||
pub fn outline(name: &str) -> Option<&'static BezPath> {
|
||||
let icon = find(name)?;
|
||||
parsed().get(icon.name)
|
||||
}
|
||||
|
||||
/// DeckCraft path syntax for `name`, in the 24×24 viewBox.
|
||||
pub fn path_data(name: &str) -> Option<String> {
|
||||
outline(name).and_then(deck_path)
|
||||
}
|
||||
|
||||
/// Stroke width in points for a shape `w`×`h` points. Lucide's stroke is `stroke` units on a
|
||||
/// 24-unit box, so a 72 pt icon is a 6 pt line. Resizing the shape later does not change it.
|
||||
pub fn stroke_width(icon: &Icon, w: f64, h: f64) -> f64 {
|
||||
let span = w.min(h).abs().min(4_000.0);
|
||||
let design = if icon.stroke.is_finite() && icon.stroke > 0.0 { icon.stroke } else { 2.0 };
|
||||
(span * design / VIEW).clamp(0.25, 400.0)
|
||||
}
|
||||
|
||||
fn parsed() -> &'static HashMap<&'static str, BezPath> {
|
||||
static CACHE: OnceLock<HashMap<&'static str, BezPath>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
let mut m = HashMap::new();
|
||||
for icon in ICONS {
|
||||
if let Some(bp) = bezpath(icon.d) {
|
||||
m.insert(icon.name, bp);
|
||||
}
|
||||
}
|
||||
m
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse SVG path data, one independent subpath at a time (` | ` resets the current point).
|
||||
fn bezpath(d: &str) -> Option<BezPath> {
|
||||
let mut out = BezPath::new();
|
||||
for part in d.split(" | ") {
|
||||
let part = part.trim();
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let bp = BezPath::from_svg(part).ok()?;
|
||||
for el in bp.elements() {
|
||||
out.push(*el);
|
||||
}
|
||||
}
|
||||
if out.elements().is_empty() { None } else { Some(out) }
|
||||
}
|
||||
|
||||
fn deck_path(bp: &BezPath) -> Option<String> {
|
||||
let mut out = String::new();
|
||||
let mut started = false;
|
||||
for el in bp.elements() {
|
||||
match *el {
|
||||
PathEl::MoveTo(p) => {
|
||||
push(&mut out, &format!("M {} {}", num(p.x), num(p.y)));
|
||||
started = true;
|
||||
}
|
||||
PathEl::LineTo(p) if started => push(&mut out, &format!("L {} {}", num(p.x), num(p.y))),
|
||||
PathEl::QuadTo(a, b) if started => {
|
||||
push(&mut out, &format!("Q {} {} {} {}", num(a.x), num(a.y), num(b.x), num(b.y)));
|
||||
}
|
||||
PathEl::CurveTo(a, b, c) if started => {
|
||||
push(&mut out, &format!("C {} {} {} {} {} {}", num(a.x), num(a.y), num(b.x), num(b.y), num(c.x), num(c.y)));
|
||||
}
|
||||
PathEl::ClosePath if started => out.push_str(" Z"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if out.is_empty() { None } else { Some(out) }
|
||||
}
|
||||
|
||||
fn push(out: &mut String, cmd: &str) {
|
||||
if !out.is_empty() {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push_str(cmd);
|
||||
}
|
||||
|
||||
fn num(v: f64) -> String {
|
||||
if !v.is_finite() {
|
||||
return "0".into();
|
||||
}
|
||||
let s = format!("{v:.4}");
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
if s == "-0" { "0".into() } else { s }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use deckcraft_geom::preset::FillMode;
|
||||
use deckcraft_model::{Geom, ShapeId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn every_icon_converts_and_a_second_subpath_keeps_its_own_origin() {
|
||||
assert!(icons().len() >= 60);
|
||||
for icon in icons() {
|
||||
let bp = outline(icon.name).unwrap_or_else(|| panic!("{} did not parse", icon.name));
|
||||
assert!(bp.elements().len() >= 2, "{}", icon.name);
|
||||
let d = path_data(icon.name).unwrap_or_else(|| panic!("{}", icon.name));
|
||||
assert!(d.starts_with('M'), "{d}");
|
||||
let again = deckcraft_render::parse_path(&d);
|
||||
assert!(!again.elements().is_empty(), "{}", icon.name);
|
||||
}
|
||||
// `m19 12…` is a second SVG element, not a continuation of the shaft.
|
||||
let down = path_data("arrow-down").unwrap();
|
||||
assert!(down.contains("M 19 12"), "{down}");
|
||||
// Relative arcs in the star become cubics our renderer understands.
|
||||
assert!(path_data("star").unwrap().contains(" C "), "{}", path_data("star").unwrap());
|
||||
assert!(find("arrow_right").is_some_and(|i| i.name == "arrow-right"));
|
||||
assert!(find("Star").is_some_and(|i| i.name == "star"));
|
||||
assert!(find("nope").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_icon_strokes_the_outline_and_round_trips_through_pptx() {
|
||||
let mut s = Session::with_new();
|
||||
assert!(s.execute("insert.icon", &json!({})).is_err());
|
||||
assert!(s.execute("insert.icon", &json!({"name": "not-an-icon"})).is_err());
|
||||
let listed = s.execute("insert.icons", &json!({})).unwrap();
|
||||
assert!(listed.as_array().unwrap().iter().any(|i| i["name"] == "star" && i["category"] == "Marks"));
|
||||
|
||||
let before = s.doc().unwrap().shapes().len();
|
||||
let id = s.execute("insert.icon", &json!({"name": "star", "rect": [100.0, 80.0, 72.0, 72.0]})).unwrap()["id"].as_u64().unwrap();
|
||||
{
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.shapes().len(), before + 1);
|
||||
let sh = st.shape(ShapeId(id as u32)).unwrap();
|
||||
assert_eq!(sh.name, "Star");
|
||||
assert!(sh.fill.as_ref().is_some_and(|f| f.is_none()));
|
||||
let Geom::Custom { paths } = &sh.geom else { panic!("{:?}", sh.geom) };
|
||||
assert_eq!(paths.len(), 1);
|
||||
assert_eq!(paths[0].fill, FillMode::None);
|
||||
assert!(paths[0].stroke);
|
||||
assert!((paths[0].w - VIEW).abs() < 1e-9 && (paths[0].h - VIEW).abs() < 1e-9);
|
||||
assert!(paths[0].d.contains('M') && paths[0].d.contains('C'));
|
||||
let width = sh.line.as_ref().and_then(|l| l.width).unwrap();
|
||||
assert!((width - 6.0).abs() < 1e-6, "{width}");
|
||||
let img = deckcraft_render::render_slide(&st.doc, st.selection.slide, &deckcraft_render::RenderOpts { scale: 2.0, ..Default::default() });
|
||||
let ink = ink_in(&img, 100.0, 80.0, 72.0, 72.0, 2.0);
|
||||
assert!(ink > 40, "star stroke pixels {ink}");
|
||||
let mid = img.pixel((100.0 + 36.0) as u32 * 2, (80.0 + 36.0) as u32 * 2);
|
||||
let sum = mid[0] as u16 + mid[1] as u16 + mid[2] as u16;
|
||||
assert!(sum > 680, "a stroked star is hollow: {mid:?}");
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shapes().len(), before);
|
||||
|
||||
s.execute("insert.icon", &json!({"name": "arrow-right", "rect": [200.0, 200.0, 96.0, 96.0]})).unwrap();
|
||||
{
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.shapes().iter().find(|s| s.name == "Arrow right").unwrap();
|
||||
let width = sh.line.as_ref().and_then(|l| l.width).unwrap();
|
||||
assert!((width - 8.0).abs() < 1e-6, "{width}");
|
||||
let img = deckcraft_render::render_slide(&st.doc, st.selection.slide, &deckcraft_render::RenderOpts { scale: 2.0, ..Default::default() });
|
||||
assert!(ink_in(&img, 200.0, 200.0, 96.0, 96.0, 2.0) > 40);
|
||||
}
|
||||
|
||||
let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap();
|
||||
let p = deckcraft_pptx::import(&bytes).unwrap();
|
||||
let sh = p.slides[0].shapes.iter().find(|s| s.name == "Arrow right").unwrap();
|
||||
let Geom::Custom { paths } = &sh.geom else { panic!("{:?}", sh.geom) };
|
||||
assert!(paths.iter().any(|path| path.fill == FillMode::None && path.stroke && path.d.contains('M')));
|
||||
let width = sh.line.as_ref().and_then(|l| l.width).unwrap();
|
||||
assert!((width - 8.0).abs() < 0.05, "{width}");
|
||||
}
|
||||
|
||||
fn ink_in(img: &deckcraft_render::Image, x: f64, y: f64, w: f64, h: f64, scale: f64) -> usize {
|
||||
let x0 = (x * scale).floor().max(0.0) as u32;
|
||||
let y0 = (y * scale).floor().max(0.0) as u32;
|
||||
let x1 = ((x + w) * scale).ceil().min(img.width as f64) as u32;
|
||||
let y1 = ((y + h) * scale).ceil().min(img.height as f64) as u32;
|
||||
let mut n = 0;
|
||||
for yy in y0..y1 {
|
||||
for xx in x0..x1 {
|
||||
let p = img.pixel(xx, yy);
|
||||
let sum = p[0] as u16 + p[1] as u16 + p[2] as u16;
|
||||
if p[3] > 200 && sum < 680 {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod cmd;
|
||||
pub mod connect;
|
||||
pub mod fonts;
|
||||
pub mod guard;
|
||||
pub mod icons;
|
||||
pub mod keys;
|
||||
pub mod links;
|
||||
pub mod recovery;
|
||||
|
||||
@@ -1041,8 +1041,8 @@ fn insert(app: &mut SlideApp, ui: &mut Ui) {
|
||||
app.session.set_tool(deckcraft_engine::ToolKind::Shape { preset: p.to_string() });
|
||||
}
|
||||
});
|
||||
let r = big_button(ui, Icon::Icons, "Icons", enabled(app, "shape.insert"));
|
||||
egui::Popup::menu(&r).width(260.0).show(|ui| icon_library(app, ui));
|
||||
let r = big_button(ui, Icon::Icons, "Icons", enabled(app, "insert.icon"));
|
||||
egui::Popup::menu(&r).width(340.0).show(|ui| icon_library(app, ui));
|
||||
big(app, ui, Icon::SmartArt, "SmartArt", "app.dialog", json!({"id": "smartart"}));
|
||||
let r = big_button(ui, Icon::Chart, "Chart", enabled(app, "insert.chart"));
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
@@ -1122,53 +1122,98 @@ pub fn table_grid_picker(ui: &mut Ui) -> Option<(usize, usize)> {
|
||||
if resp.clicked() && hover.0 > 0 { Some(hover) } else { None }
|
||||
}
|
||||
|
||||
/// A small built-in icon library: our own simple pictograms built from preset shapes.
|
||||
/// Lucide stroke icons (Insert › Icons). Search filters the gallery; a click inserts the outline.
|
||||
fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let items: [(&str, &str); 24] = [
|
||||
("star5", "Star"),
|
||||
("heart", "Heart"),
|
||||
("lightningBolt", "Lightning"),
|
||||
("sun", "Sun"),
|
||||
("moon", "Moon"),
|
||||
("cloud", "Cloud"),
|
||||
("smileyFace", "Smile"),
|
||||
("noSmoking", "Not allowed"),
|
||||
("mathPlus", "Plus"),
|
||||
("mathMultiply", "Multiply"),
|
||||
("mathEqual", "Equals"),
|
||||
("chevron", "Chevron"),
|
||||
("rightArrow", "Arrow"),
|
||||
("quadArrow", "Move"),
|
||||
("uturnArrow", "Return"),
|
||||
("flowChartDecision", "Decision"),
|
||||
("flowChartMagneticDisk", "Database"),
|
||||
("flowChartDocument", "Document"),
|
||||
("wedgeRoundRectCallout", "Speech"),
|
||||
("cloudCallout", "Thought"),
|
||||
("teardrop", "Pin"),
|
||||
("donut", "Ring"),
|
||||
("irregularSeal1", "Burst"),
|
||||
("ribbon2", "Banner"),
|
||||
];
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (preset, label) in items {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(36.0, 36.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), t.hover);
|
||||
let (color, hover) = (t.text, t.hover);
|
||||
let id = ui.id().with("icon-query");
|
||||
let mut query = ui.data_mut(|d| d.get_temp::<String>(id).unwrap_or_default());
|
||||
ui.add(egui::TextEdit::singleline(&mut query).hint_text("Search icons").desired_width(ui.available_width()));
|
||||
let q = query.trim().to_lowercase();
|
||||
ui.data_mut(|d| d.insert_temp(id, query));
|
||||
let icons = deckcraft_engine::icons::icons();
|
||||
egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| {
|
||||
if q.is_empty() {
|
||||
let mut cats: Vec<&str> = Vec::new();
|
||||
for icon in icons {
|
||||
if !cats.contains(&icon.category) {
|
||||
cats.push(icon.category);
|
||||
}
|
||||
}
|
||||
paint_preset(ui.painter(), r.shrink(6.0), preset, t.text, t.text);
|
||||
if resp.on_hover_text(label).clicked() {
|
||||
let size = app.session.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE);
|
||||
let _ = app.run("shape.insert", json!({"preset": preset, "rect": [size.width / 2.0 - 48.0, size.height / 2.0 - 48.0, 96, 96]}));
|
||||
let _ = app.run("shape.fill", json!({"color": "tx1"}));
|
||||
let _ = app.run("shape.line", json!({"none": true}));
|
||||
ui.close();
|
||||
for cat in cats {
|
||||
ui.label(egui::RichText::new(cat).small().strong());
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for icon in icons.iter().filter(|i| i.category == cat) {
|
||||
icon_cell(app, ui, icon, color, hover);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let mut n = 0;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for icon in icons {
|
||||
if icon.name.contains(&q) || icon.label.to_lowercase().contains(&q) {
|
||||
icon_cell(app, ui, icon, color, hover);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
if n == 0 {
|
||||
ui.label("No icons match.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn icon_cell(app: &mut SlideApp, ui: &mut Ui, icon: &deckcraft_engine::icons::Icon, color: Color32, hover: Color32) {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(36.0, 36.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), hover);
|
||||
}
|
||||
paint_icon(ui.painter(), r.shrink(6.0), icon, color);
|
||||
if resp.on_hover_text(icon.label).clicked() {
|
||||
let _ = app.run("insert.icon", json!({"name": icon.name}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke a Lucide outline into a gallery cell. The line is the design stroke scaled to the cell.
|
||||
fn paint_icon(p: &egui::Painter, r: Rect, icon: &deckcraft_engine::icons::Icon, color: Color32) {
|
||||
let Some(src) = deckcraft_engine::icons::outline(icon.name) else { return };
|
||||
let mut bp = src.clone();
|
||||
let (w, h) = (r.width().max(1.0) as f64, r.height().max(1.0) as f64);
|
||||
bp.apply_affine(deckcraft_geom::Affine::scale_non_uniform(w / deckcraft_engine::icons::VIEW, h / deckcraft_engine::icons::VIEW));
|
||||
let sw = ((r.width() * icon.stroke as f32) / deckcraft_engine::icons::VIEW as f32).clamp(1.0, 3.5);
|
||||
let mut pts: Vec<egui::Pos2> = vec![];
|
||||
let mut polys: Vec<(Vec<egui::Pos2>, bool)> = vec![];
|
||||
kurbo_flatten(&bp, |el| match el {
|
||||
Fl::Move(x, y) => {
|
||||
if pts.len() > 1 {
|
||||
polys.push((std::mem::take(&mut pts), false));
|
||||
}
|
||||
pts.clear();
|
||||
pts.push(pos2(r.min.x + x as f32, r.min.y + y as f32));
|
||||
}
|
||||
Fl::Line(x, y) => pts.push(pos2(r.min.x + x as f32, r.min.y + y as f32)),
|
||||
Fl::Close => {
|
||||
if pts.len() > 1 {
|
||||
polys.push((std::mem::take(&mut pts), true));
|
||||
}
|
||||
}
|
||||
});
|
||||
if pts.len() > 1 {
|
||||
polys.push((pts, false));
|
||||
}
|
||||
for (poly, closed) in polys {
|
||||
p.add(egui::Shape::Path(egui::epaint::PathShape {
|
||||
points: poly,
|
||||
closed,
|
||||
fill: Color32::TRANSPARENT,
|
||||
stroke: egui::epaint::PathStroke::new(sw, color),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Draw ----------
|
||||
|
||||
fn draw(app: &mut SlideApp, ui: &mut Ui) {
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Where DeckCraft falls short of PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101; previous: minor, core-workflow alpha gate added; stage stays alpha; before that: major, first itemized gap list; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Insert › Icons places a Lucide stroke set; 3-D models and stock content remain; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
Every known shortfall, one entry each, ranked by how much it stops someone who presents with
|
||||
PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in
|
||||
@@ -188,7 +188,7 @@ Zoom (summary/section/slide, 6–10 h), reuse slides (2–4 h), laser pointer (1
|
||||
sounds (1–2 h), artistic effects (6–10 h), remove background (6–10 h; needs a model or a classical
|
||||
matte), compress pictures (2–3 h), insert screenshot (2–3 h), record audio/screen and record slide
|
||||
show (8–12 h), compare presentations (6–10 h), password protection (4–6 h, ECMA-376 agile
|
||||
encryption), ink to shape/text (6–10 h), icons and 3-D models (8–14 h, needs an open icon set).
|
||||
encryption), ink to shape/text (6–10 h), 3-D models and stock content (6–10 h; Insert › Icons places a Lucide stroke set).
|
||||
|
||||
## UI/UX
|
||||
|
||||
@@ -256,6 +256,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Insert › Icons places Lucide 1.56.0 stroke icons as custom shapes. The checklist row stays missing until 3-D models and stock content exist |
|
||||
| 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) |
|
||||
| 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines |
|
||||
| 2026-10-12 | minor | EMF/WMF text, pattern and hatch brushes drawn; legacy OLE and ActiveX previews shown; the SVG gap keeps only EMF clip regions |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DeckCraft vs Microsoft PowerPoint: parity assessment
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready recomputed from written weights, 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; replaces the parity sections of ROADMAP.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (media remainder no longer lists icons: Insert › Icons is a Lucide stroke set; percentages unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?".
|
||||
[ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall;
|
||||
@@ -192,7 +192,7 @@ it is the Features row of the dimension table. Automation and Platforms are scor
|
||||
| Charts | 7 | 65% | ~45% | 12–20 | Data grid editing, chart elements, the `chartex` types (waterfall, funnel, treemap, sunburst, histogram, box) | [parity-checklist.md](parity-checklist.md) |
|
||||
| SmartArt | 4 | 33% | ~25% | 10–16 | Text pane, layout switching, the diagram data model on save | [parity-checklist.md](parity-checklist.md) |
|
||||
| Pictures | 7 | 48% | ~45% | 12–20 | SVG/EMF/WMF, clipboard paste on Windows/Linux (#53), artistic effects, remove background, compress | [file-format-parity.md](file-format-parity.md) |
|
||||
| Media | 4 | 67% | ~60% | 8–14 | WMA/WMV decode, record audio/screen, icons and 3-D models, playback options lost on PPTX round trip (#74) | [file-format-parity.md](file-format-parity.md) |
|
||||
| Media | 4 | 67% | ~60% | 8–14 | WMA/WMV decode, record audio/screen, 3-D models and stock content, playback options lost on PPTX round trip (#74) | [file-format-parity.md](file-format-parity.md) |
|
||||
| Transitions | 4 | 92% | ~80% | 4–8 | Transition sounds, 3-D transitions approximated in 2-D | [animation-parity.md](animation-parity.md) |
|
||||
| Animations | 6 | 79% | ~65% | 10–16 | 64 of PowerPoint's ~190 effects, Animation Pane timeline, preview, rewind lost on save (#77) | [animation-parity.md](animation-parity.md) |
|
||||
| Slide show and presenter view | 8 | 66% | ~60% | 10–16 | Second-display placement, laser pointer, recording, subtitles, ink depth | [slideshow-parity.md](slideshow-parity.md) |
|
||||
@@ -276,6 +276,7 @@ scored done (16 open issues).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Media remainder: Insert › Icons places Lucide stroke icons. 3-D models and stock content remain. Headline percentages unchanged; the checklist row stays missing |
|
||||
| 2026-10-10 | minor | Added the readiness-by-audience table: each number gets its own hours to ~95% (full 340–600, mainstream 150–250, essentials 50–90). The full number stays an additive weighted sum; no change to the percentages |
|
||||
| 2026-10-10 | minor | Added the mainstream practitioner (~53%) and essentials user (~62%) numbers with written weights, discounts and user evidence. Features dimension recomputed from written area weights (66% judgement → 62.4% weighted mean), which moves ready for real work from ~56% to ~55%; stage unchanged |
|
||||
| 2026-10-10 | minor | Stage checked against the core-workflow gate (all six pass); stays alpha |
|
||||
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
# UI parity with PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (File tab / Backstage, #76; previous: off-slide objects in Normal view, #270) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Insert › Icons is a Lucide stroke gallery; ribbon icons stay original drawings; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
@@ -127,8 +127,9 @@ Alt+Shift+arrows in text are handled elsewhere (#78, #93).
|
||||
## Visual fidelity
|
||||
|
||||
Colours, spacing, ribbon card and tab geometry follow the observations in
|
||||
`plan/powerpoint/01-observed-ui.md`; icons are original (~240, drawn in code or Lucide) and must not
|
||||
imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been run since
|
||||
`plan/powerpoint/01-observed-ui.md`; the ribbon and menu icons are original (~240, drawn in code)
|
||||
and must not imitate Microsoft's. Insert › Icons places Lucide stroke icons (ISC, release 1.56.0)
|
||||
on the slide as shapes. No pixel comparison against PowerPoint screenshots has been run since
|
||||
2026-10-07. Cross-app icon consistency is open (#73, #56).
|
||||
|
||||
## Estimates
|
||||
@@ -148,6 +149,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Insert › Icons is a searchable gallery of Lucide stroke icons inserted as shapes. Ribbon chrome stays original drawings |
|
||||
| 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home |
|
||||
| 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) |
|
||||
| 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |
|
||||
|
||||
Reference in New Issue
Block a user