mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge main into fix/tool-image-trace-envelope-symbol-type-and
This commit is contained in:
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Tech Circuit brush library, G5; previously: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (G5: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); G5 estimate unchanged (3–13 h); previously: Tech Circuit brush library, G5; previously: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
|
||||
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
|
||||
@@ -225,6 +225,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-15 | minor | Graphic Styles: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); G5 estimate unchanged (3–13 h). |
|
||||
| 2026-11-14 | minor | Tech Circuit adds three original procedural brushes (Circuit Trace, Bus Lines, Terminal Nodes); ten built-in brush libraries. G5 remaining estimate 3–13 h (estimated); parity scores and headline totals unchanged. |
|
||||
| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. |
|
||||
| 2026-11-10 | minor | Tidy-up, no content change: the unheaded change bullets at the top of the file and a revision row inside the headline-numbers table move to this table under their commit dates. Scores and estimates unchanged. |
|
||||
|
||||
@@ -162,6 +162,7 @@ pub const STYLE_LIBRARIES: &[BuiltinStyleLibrary] = &[
|
||||
BuiltinStyleLibrary { id: "blends-transparency", name: "Blends and Transparency", make: blends_transparency },
|
||||
BuiltinStyleLibrary { id: "game-hud", name: "Game HUD", make: game_hud },
|
||||
BuiltinStyleLibrary { id: "status-effects", name: "Status Effects", make: status_effects },
|
||||
BuiltinStyleLibrary { id: "neon-glow", name: "Neon & Glow", make: neon_glow },
|
||||
];
|
||||
|
||||
/// Built-in library `id`, made once.
|
||||
@@ -619,6 +620,44 @@ fn status_effects() -> Vec<GraphicStyle> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Neon & Glow: original multi-layer procedural styles with neon tubes, laser beams, and glowing halos.
|
||||
fn neon_glow() -> Vec<GraphicStyle> {
|
||||
vec![
|
||||
style(
|
||||
"Cyan Neon",
|
||||
vec![S(StrokeLayer { opacity: 0.7, ..round("#00e5ff", 6.0) }), S(round("#ffffff", 1.5))],
|
||||
vec![fx("stylize.dropShadow", json!({"mode": "screen", "opacity": 85.0, "x": 0.0, "y": 0.0, "blur": 8.0, "color": "#00e5ff"}))],
|
||||
),
|
||||
GraphicStyle {
|
||||
blend: BlendMode::Screen,
|
||||
..style(
|
||||
"Magenta Laser",
|
||||
vec![S(StrokeLayer { blend: BlendMode::Screen, ..round("#ff007f", 5.0) }), S(round("#ffffff", 1.5))],
|
||||
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#ff007f"}))],
|
||||
)
|
||||
},
|
||||
style(
|
||||
"Amber Tube",
|
||||
vec![S(StrokeLayer { opacity: 0.8, ..round("#ff8c00", 6.0) }), S(round("#ffea00", 1.5))],
|
||||
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#ff8c00"}))],
|
||||
),
|
||||
style(
|
||||
"Electric Lime",
|
||||
vec![S(StrokeLayer { opacity: 0.75, ..round("#39ff14", 6.0) }), S(round("#ffffff", 1.5))],
|
||||
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#39ff14"}))],
|
||||
),
|
||||
style(
|
||||
"Pulse Violet",
|
||||
vec![S(StrokeLayer { opacity: 0.75, ..round("#7b2cbf", 5.0) }), S(round("#e0aaff", 1.5))],
|
||||
vec![
|
||||
fx("stylize.innerGlow", json!({"mode": "screen", "opacity": 75.0, "blur": 6.0, "color": "#c77dff", "source": "edge"})),
|
||||
fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#7b2cbf"})),
|
||||
],
|
||||
),
|
||||
style("Cobalt Phosphor", vec![S(round("#0047ab", 6.0)), S(round("#ffffff", 1.5))], vec![fx("blur.gaussian", json!({"radius": 2.0}))]),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -750,4 +789,36 @@ mod tests {
|
||||
let Paint::Gradient(gp) = g.appearance.fill_paint() else { panic!("a gradient") };
|
||||
assert!(gp.swatch.is_none() && gp.gradient.stops.iter().all(|s| s.swatch.is_none() && s.tint == 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_and_glow_styles_are_defined_with_multi_strokes_and_effects() {
|
||||
let lib = builtin_style_library("neon-glow").expect("neon-glow library exists");
|
||||
assert_eq!(lib.name, "Neon & Glow");
|
||||
assert_eq!(lib.len(), 6);
|
||||
let names: Vec<&str> = lib.styles.iter().map(|g| g.name.as_str()).collect();
|
||||
assert_eq!(names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]);
|
||||
for g in &lib.styles {
|
||||
let strokes = g.appearance.items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count();
|
||||
assert!(strokes >= 2, "{} has {} strokes, expected >= 2", g.name, strokes);
|
||||
assert!(!g.appearance.effects.is_empty(), "{} has effects", g.name);
|
||||
}
|
||||
let cyan = lib.style("Cyan Neon").unwrap();
|
||||
assert_eq!(cyan.appearance.effects[0].id, "stylize.dropShadow");
|
||||
let magenta = lib.style("Magenta Laser").unwrap();
|
||||
assert_eq!(magenta.blend, BlendMode::Screen);
|
||||
let pulse = lib.style("Pulse Violet").unwrap();
|
||||
let pulse_fx: Vec<&str> = pulse.appearance.effects.iter().map(|e| e.id.as_str()).collect();
|
||||
assert_eq!(pulse_fx, ["stylize.innerGlow", "stylize.outerGlow"]);
|
||||
let cobalt = lib.style("Cobalt Phosphor").unwrap();
|
||||
assert_eq!(cobalt.appearance.effects[0].id, "blur.gaussian");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_glow_round_trips_through_vcstyles() {
|
||||
let lib = builtin_style_library("neon-glow").unwrap();
|
||||
let text = write(&lib);
|
||||
assert!(sniff(&text));
|
||||
let read_back = read(&text, "Fallback").unwrap();
|
||||
assert_eq!(*lib, read_back);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ use vectorcraft_geom::{Affine, FillRule, PathData, Rect, Vec2};
|
||||
|
||||
use crate::distort::{transform_move, transform_offsets};
|
||||
use crate::group::{has_own_paint, paints};
|
||||
use crate::reshape::{as_art, clipping_text_outline, dressed, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves};
|
||||
use crate::reshape::{
|
||||
as_art, clipping_text_outline, dressed, geometry_effects, glyph_outlines, outline_nested_text, paint_type_clip, reshape_leaves,
|
||||
};
|
||||
use crate::{
|
||||
GeomContext, adjust_in_document, apply_geometry_carrying, crop_marks_art, evaluate_container, has_adjustment, has_crop_marks, has_geometry,
|
||||
has_instance_row_raster, has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, reshape,
|
||||
@@ -689,6 +691,9 @@ fn moved_type_group(n: &Node) -> Option<Node> {
|
||||
/// them. `None` when no type stays type.
|
||||
fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
|
||||
fn keep(d: &Document, n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut bool) {
|
||||
if !clipping {
|
||||
paint_type_clip(Some(d), n);
|
||||
}
|
||||
let clips = n.clips();
|
||||
let Some(ch) = n.children_mut() else { return };
|
||||
for (i, c) in ch.iter_mut().enumerate() {
|
||||
|
||||
@@ -182,29 +182,29 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
REVOLVE,
|
||||
"Revolve…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{angle: degrees (0..360, 360), offset: pt (0..100000, 0), edge: left|right, rotationX: degrees (0), rotationY: degrees (0), rotationZ: degrees (0), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), segments: integer (8..128, 64), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), cap: bool (true, close circular ends of open profiles; partial-sweep cut sides stay open), expandVisibleOnly: bool (true, trim covered opaque solid surfaces only on Expand Appearance; surfaces with graphics keep every face), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the swept side, see threeD.mapGraphic: symbol: name of a document symbol, face: front (the side), x: pt (0; the offset of the graphic's center from the middle of the side, around the axis: the angle times the profile's distance from the axis at that point), y: pt (0; along the profile, toward its lower end), scale: % (1..10000, 100), rotation: degrees (-360..360, 0; counterclockwise); wrapped around the side at its own size, so it reaches farther around where the profile comes nearer the axis, shaded with it and drawn only where the side's outside faces the viewer; a full turn cuts it at the back; nothing on the caps)}. Shaded surfaces use smooth vector gradients automatically; intersecting profiles use approximate painter visibility.",
|
||||
json!({"angle":360.0,"offset":0.0,"edge":"left","rotationX":0.0,"rotationY":0.0,"rotationZ":0.0,"perspectiveAngle":0.0,"segments":64,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"cap":true,"expandVisibleOnly":true,"graphics":[]}),
|
||||
"{angle: degrees (0..360, 360), offset: pt (0..100000, 0), edge: left|right, rotationX: degrees (0), rotationY: degrees (0), rotationZ: degrees (0), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), segments: integer (8..128, 64), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), cap: bool (true, close circular ends of open profiles; partial-sweep cut sides stay open), expandVisibleOnly: bool (true, trim covered opaque solid surfaces only on Expand Appearance; surfaces with graphics keep every face), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the swept side, see threeD.mapGraphic: symbol: name of a document symbol, face: front (the side), x: pt (0; the offset of the graphic's center from the middle of the side, around the axis: the angle times the profile's distance from the axis at that point), y: pt (0; along the profile, toward its lower end), scale: % (1..10000, 100), rotation: degrees (-360..360, 0; counterclockwise); wrapped around the side at its own size, so it reaches farther around where the profile comes nearer the axis, shaded with it and drawn only where the side's outside faces the viewer; a full turn cuts it at the back; nothing on the caps), render: object ({mode: shaded}; mode: shaded|wireframe, wireframe draws the edges of the faces the effect draws, each once, stroked 0.5 pt black with round joins, with no fill, shadow or graphics, whatever the opacity of the fills, strokes and group members, while the effect stays live; the stroke is VectorCraft's own)}. Shaded surfaces use smooth vector gradients automatically; intersecting profiles use approximate painter visibility.",
|
||||
json!({"angle":360.0,"offset":0.0,"edge":"left","rotationX":0.0,"rotationY":0.0,"rotationZ":0.0,"perspectiveAngle":0.0,"segments":64,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"cap":true,"expandVisibleOnly":true,"graphics":[],"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
EXTRUDE,
|
||||
"Extrude & Bevel…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{depth: pt (0..2000, 50), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelWidth: pt (0..1000, 4), bevelHeight: pt (0..1000, 4), bevelRepeat: integer (1..32, 1), bevelSpace: % (0..90, 0; flat between repeats), bevelInside: bool (false: added around the shape; true: carved into it), bevelBothSides: bool (false), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the caps, see threeD.mapGraphic: symbol: name of a document symbol, face: front|back (front; the back reads left to right as seen from behind), x: pt (0; the offset of the graphic's center from the center of the object's bounds, in the face's own plane), y: pt (0), scale: % (1..10000, 100; of the symbol's own size), rotation: degrees (-360..360, 0; counterclockwise); drawn turned, foreshortened and shaded with the cap and clipped to it, on a solid cap only)}. The front face is the art, extruded backwards about the centre of its bounds; painted with the top fill (else the top stroke).",
|
||||
json!({"depth":50.0,"twist":0.0,"taper":100.0,"cap":true,"bevel":"none","bevelWidth":4.0,"bevelHeight":4.0,"bevelRepeat":1,"bevelSpace":0.0,"bevelInside":false,"bevelBothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[]}),
|
||||
"{depth: pt (0..2000, 50), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelWidth: pt (0..1000, 4), bevelHeight: pt (0..1000, 4), bevelRepeat: integer (1..32, 1), bevelSpace: % (0..90, 0; flat between repeats), bevelInside: bool (false: added around the shape; true: carved into it), bevelBothSides: bool (false), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the caps, see threeD.mapGraphic: symbol: name of a document symbol, face: front|back (front; the back reads left to right as seen from behind), x: pt (0; the offset of the graphic's center from the center of the object's bounds, in the face's own plane), y: pt (0), scale: % (1..10000, 100; of the symbol's own size), rotation: degrees (-360..360, 0; counterclockwise); drawn turned, foreshortened and shaded with the cap and clipped to it, on a solid cap only), render: object ({mode: shaded}; mode: shaded|wireframe, as Revolve's)}. The front face is the art, extruded backwards about the centre of its bounds; painted with the top fill (else the top stroke).",
|
||||
json!({"depth":50.0,"twist":0.0,"taper":100.0,"cap":true,"bevel":"none","bevelWidth":4.0,"bevelHeight":4.0,"bevelRepeat":1,"bevelSpace":0.0,"bevelInside":false,"bevelBothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[],"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
INFLATE,
|
||||
"Inflate…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{depth: pt (0..2000, 0; flat depth between front and back), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: no back, a shell open behind), volume: % (0..100, 50; 50 rounds the edge like a half sphere), bothSides: bool (false: flat back), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across)}. Painted with the top fill (else the top stroke).",
|
||||
json!({"depth":0.0,"twist":0.0,"taper":100.0,"cap":true,"volume":50.0,"bothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0}),
|
||||
"{depth: pt (0..2000, 0; flat depth between front and back), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: no back, a shell open behind), volume: % (0..100, 50; 50 rounds the edge like a half sphere), bothSides: bool (false: flat back), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), render: object ({mode: shaded}; mode: shaded|wireframe, as Revolve's)}. Painted with the top fill (else the top stroke).",
|
||||
json!({"depth":0.0,"twist":0.0,"taper":100.0,"cap":true,"volume":50.0,"bothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
ROTATE,
|
||||
"Rotate…",
|
||||
&["Effect", "3D and Materials"],
|
||||
"{rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the plane, as Extrude & Bevel's, with face front only)}. Turns the flat art in 3D about the centre of its bounds; every fill and stroke keeps its paint, shaded as one plane.",
|
||||
json!({"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[]}),
|
||||
"{rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspectiveAngle: degrees (0..160, 0; 0 is none, wider brings the eye nearer), perspective: % (0..100; the earlier form, read as the perspectiveAngle that shows the same view), shade: bool (true), lightColor: #rrggbb (white, #ffffff; legacy directional light color), lights: null or array of 1..10 objects (color: #rrggbb, intensity: % 0..100, rotation: degrees -180..180, height: degrees 0..90, softness: % 0..100, behind: bool; overrides the single-light fields; ambient is shared), lightRotation: degrees (-180..180, 125.26; round the object on the page, 0 on its right, counterclockwise), lightHeight: degrees (0..90, 30; 0 beside the object, 90 straight in front), lightBehind: bool (false; true lights it from behind), lightIntensity: % (0..100, 80), lightSoftness: % (0..100, 0; how far round the object the light reaches), ambient: % (0..200, 25; Ambient Light), lightAzimuth: degrees (the earlier form: with lightElevation, read as the lightRotation, lightHeight and lightBehind of the same light), lightElevation: degrees (the earlier form, with lightAzimuth), shadow: bool (false; the first light casts a hard-edged shadow, black at 50% times the art's opacity), shadowPosition: behind|below (behind; behind: on a plane square to the view, below: on the floor under the object, turned with it), shadowDistance: % (0..100, 0; how far the plane is from the object, as a share of its largest size), shadowBounds: % (10..400, 100; how far the plane reaches: at 100 it is four times the object's largest size across), graphics: array of 0..64 objects ([]; the 3D and Materials panel's Graphics on the plane, as Extrude & Bevel's, with face front only), render: object ({mode: shaded}; mode: shaded|wireframe, as Revolve's)}. Turns the flat art in 3D about the centre of its bounds; every fill and stroke keeps its paint, shaded as one plane.",
|
||||
json!({"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspectiveAngle":0.0,"shade":true,"lightColor":"#ffffff","lights":null,"lightRotation":125.26438968275465,"lightHeight":30.0,"lightBehind":false,"lightIntensity":80.0,"lightSoftness":0.0,"ambient":25.0,"shadow":false,"shadowPosition":"behind","shadowDistance":0.0,"shadowBounds":100.0,"graphics":[],"render":{"mode":"shaded"}}),
|
||||
),
|
||||
g(
|
||||
CLASSIC_EXTRUDE,
|
||||
|
||||
@@ -91,12 +91,13 @@ pub use revolve::{
|
||||
CLASSIC_REVOLVE, REVOLVE, has_revolve, is_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in,
|
||||
};
|
||||
pub use solids::{
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, GRAPHICS, GraphicFrame, INFLATE, MAX_GRAPHICS, MappedGraphic, ROTATE, ROTATE_SURFACES,
|
||||
ROTATION_PRESETS, SHADING_COLORS, SOLIDS, SolidMesh, THREE_D_TYPES, classic_extrude_options, classic_shading, extrude_options, graphic_bounds,
|
||||
graphic_frame, graphic_symbols, has_solid, inflate_options, is_classic_three_d, is_three_d, mapped_graphics, missing_symbol,
|
||||
names_graphic_symbol, rename_graphic_symbol, rename_graphic_symbol_in, solid_art, solid_art_in, takes_graphics, three_d_meshes,
|
||||
three_d_meshes_in, three_d_meshes_of, three_d_meshes_of_in, three_d_params_as, three_d_wireframe_in, validate_solid, validate_solid_in,
|
||||
validate_three_d, validate_three_d_at, validate_three_d_at_in, validate_three_d_in, validate_three_d_params, view_options, with_graphic,
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, GRAPHICS, GraphicFrame, INFLATE, MAX_GRAPHICS, MappedGraphic, RENDER, RENDER_MODES, ROTATE,
|
||||
ROTATE_SURFACES, ROTATION_PRESETS, SHADING_COLORS, SOLIDS, SolidMesh, THREE_D_TYPES, classic_extrude_options, classic_shading, extrude_options,
|
||||
graphic_bounds, graphic_frame, graphic_symbols, has_solid, inflate_options, is_classic_three_d, is_three_d, mapped_graphics, missing_symbol,
|
||||
names_graphic_symbol, rename_graphic_symbol, rename_graphic_symbol_in, renders_wireframe, solid_art, solid_art_in, takes_graphics,
|
||||
three_d_meshes, three_d_meshes_in, three_d_meshes_of, three_d_meshes_of_in, three_d_params_as, three_d_wireframe_in, validate_solid,
|
||||
validate_solid_in, validate_three_d, validate_three_d_at, validate_three_d_at_in, validate_three_d_in, validate_three_d_params, view_options,
|
||||
with_graphic,
|
||||
};
|
||||
pub use solids::{
|
||||
GRAPHIC_MAX_OFFSET, GRAPHIC_SCALE_RANGE, GRAPHICS_NOT_TAKEN, appearance_graphic_symbols, graphic_frame_at, graphics_reshaped_after,
|
||||
|
||||
@@ -746,9 +746,13 @@ pub(crate) fn clipping_text_outline(t: &Node) -> Option<Node> {
|
||||
|
||||
/// Replace type anywhere inside `n` by its outlines, as art that keeps the type's look
|
||||
/// ([`as_art`]). Type in a clip group's clipping path (all of `n` when `clipping` is set) becomes
|
||||
/// its outlines with its visibility and effects ([`clipping_text_outline`]). The swatches and
|
||||
/// patterns that live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
/// its outlines with its visibility and effects ([`clipping_text_outline`]), and its fills and
|
||||
/// strokes become art of their own ([`paint_type_clip`]). The swatches and patterns that
|
||||
/// live-effect plug-ins painting their pieces name are looked up in `doc`.
|
||||
pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping: bool) {
|
||||
if !clipping {
|
||||
paint_type_clip(doc, n);
|
||||
}
|
||||
let clips = n.clips();
|
||||
let Some(ch) = n.children_mut() else { return };
|
||||
for (i, c) in ch.iter_mut().enumerate() {
|
||||
@@ -766,6 +770,36 @@ pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping
|
||||
}
|
||||
}
|
||||
|
||||
/// When `n` is a clip group or clipping layer whose clipping path is type that paints
|
||||
/// ([`Node::clip_paint`]), make it the art the canvas draws for it: the type's outlines clip
|
||||
/// ([`clipping_text_outline`]), its fills as art ([`as_art`]) paint behind the clipped art inside
|
||||
/// the clip, and its strokes as art paint over it, outside the clip, in a group that `n` becomes
|
||||
/// (keeping its id, name and transparency) with the clip group inside. The other members stay as
|
||||
/// they are. Hidden type paints nothing.
|
||||
pub(crate) fn paint_type_clip(doc: Option<&Document>, n: &mut Node) {
|
||||
let Some((first, rest)) = n.children().and_then(|ch| ch.split_first()) else { return };
|
||||
if !n.clips() || !matches!(first.kind, NodeKind::Text(_)) || !first.visible {
|
||||
return;
|
||||
}
|
||||
let paint = first.clip_paint();
|
||||
if paint.fill.is_none() && paint.stroke.is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(clip) = clipping_text_outline(first) else { return };
|
||||
let art = |part: Option<Node>| part.and_then(|p| as_art(doc, &p, None)).map(Arc::new);
|
||||
let members: Vec<Arc<Node>> = std::iter::once(Arc::new(clip)).chain(art(paint.fill)).chain(rest.iter().cloned()).collect();
|
||||
let stroke = art(paint.stroke);
|
||||
let id = n.id;
|
||||
match (&mut n.kind, stroke) {
|
||||
(NodeKind::Group { children, .. } | NodeKind::Layer { children, .. }, None) => *children = members,
|
||||
(NodeKind::Group { children, clip } | NodeKind::Layer { children, clip, .. }, Some(stroke)) => {
|
||||
*children = vec![Arc::new(Node::new(id, NodeKind::Group { children: members, clip: true })), stroke];
|
||||
*clip = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// `n` (see [`needs_outline`]) with its object-level geometry effects and its perspective applied:
|
||||
/// its [`outline_art`] reshaped as one piece, or for an image the image clipped by its reshaped
|
||||
/// frame. Type's own fills and strokes keep their effects on its whole outline. An image's own
|
||||
|
||||
@@ -8,7 +8,7 @@ use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, N
|
||||
use vectorcraft_geom::{PathData, PathEl, Point, Rect, Vec2};
|
||||
use vectorcraft_three_d::{Edge, Revolve, SolidFaces};
|
||||
|
||||
use crate::solids::wireframe::WireFace;
|
||||
use crate::solids::wireframe::{WireFace, wireframe_params};
|
||||
use crate::solids::{
|
||||
Profile, Scope, is_container, members, object_profile, past_effect, profile, reshape_members, resolve_for, solid_paints, union_bounds,
|
||||
};
|
||||
@@ -154,7 +154,12 @@ pub(crate) fn validate_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value)
|
||||
let b = union_bounds(&shapes).ok_or(EMPTY)?;
|
||||
let options = shared_options(&shapes, options_of(id, p))?;
|
||||
let mut copies = 0;
|
||||
shapes.iter().try_for_each(|s| check_surface(&s.path, b, options, surface_item(&s.items), &mut copies))?;
|
||||
// In wireframe mode the edges are drawn with no paint copies, graphics or shadow.
|
||||
let wire = wireframe_params(id, p);
|
||||
shapes.iter().try_for_each(|s| check_surface(&s.path, b, options, surface_item(&s.items).filter(|_| !wire), &mut copies))?;
|
||||
if wire {
|
||||
return Ok(());
|
||||
}
|
||||
// Without graphics, or without the document they draw from, no member is measured for them.
|
||||
if let Some(doc) = doc.filter(|_| id == REVOLVE && !crate::solids::mapped_graphics(p).is_empty()) {
|
||||
match graphics_member(&shapes, b, options).and_then(|k| shapes.get(k)) {
|
||||
@@ -176,7 +181,11 @@ pub(crate) fn validate_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value)
|
||||
let path = geometry(&path, before, &GeomContext::of(n));
|
||||
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
|
||||
let paints = solid_paints(n);
|
||||
check_surface(&path, b, options_of(id, p), surface_item(&paints), &mut 0)?;
|
||||
let wire = wireframe_params(id, p);
|
||||
check_surface(&path, b, options_of(id, p), surface_item(&paints).filter(|_| !wire), &mut 0)?;
|
||||
if wire {
|
||||
return Ok(());
|
||||
}
|
||||
check_graphics(doc, id, &path, b, options_of(id, p), p, &mut 0)?;
|
||||
check_shadow(p, [&path], b, options_of(id, p))
|
||||
}
|
||||
@@ -261,7 +270,11 @@ pub(crate) fn validate_item_of(doc: Option<&Document>, n: &Node, id: &str, p: &V
|
||||
let options = options_of(kind, ¶ms);
|
||||
let path = geometry(&path, at.and_then(|at| effects.get(..at)).unwrap_or(effects), &ctx.item(item));
|
||||
let b = path.bounds().ok_or(EMPTY)?;
|
||||
check_surface(&path, b, options, crate::paints(item).then_some(item), &mut copies)?;
|
||||
let wire = wireframe_params(kind, ¶ms);
|
||||
check_surface(&path, b, options, (crate::paints(item) && !wire).then_some(item), &mut copies)?;
|
||||
if wire {
|
||||
continue;
|
||||
}
|
||||
// The graphics of every item add their paints' copies to the graphics' budget, as they
|
||||
// are drawn; only the edited item's graphics are refused here.
|
||||
let graphics = check_graphics(doc, kind, &path, b, options, ¶ms, &mut graphic_copies);
|
||||
@@ -324,6 +337,7 @@ pub(crate) fn validate_revolve_params(p: &Value) -> Result<(), String> {
|
||||
pub(crate) fn validate_params_of(id: &str, p: &Value) -> Result<(), String> {
|
||||
validate_revolve_params(p)?;
|
||||
crate::solids::graphics::validate(id, p)?;
|
||||
crate::solids::wireframe::validate_render(id, p)?;
|
||||
if id == CLASSIC_REVOLVE {
|
||||
classic::validate(p)?;
|
||||
}
|
||||
@@ -728,14 +742,7 @@ pub(crate) fn revolve_meshes(n: &Node, total: &mut usize, limit: usize) -> Resul
|
||||
/// or stroke's Revolve are looked up in `doc`. Each face carries the width of the stroke the canvas
|
||||
/// draws around it ([`surface`]).
|
||||
pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<crate::solids::wireframe::FacesAndShadow, String> {
|
||||
let faces = |path: &PathData, b: Rect, fx: &Effect, options: Revolve| -> Result<Vec<WireFace>, String> {
|
||||
if fx.id == CLASSIC_REVOLVE {
|
||||
let p = merged_params(&fx.id, &fx.params);
|
||||
let facets = vectorcraft_three_d::revolve_facets(path, b, options)?;
|
||||
return Ok(facets.iter().map(|f| WireFace { path: f.path(), stroke: crate::solids::classic::stroke_width(f, &p) }).collect());
|
||||
}
|
||||
Ok(vectorcraft_three_d::revolve(path, b, options)?.iter().map(|f| WireFace { path: f.path(), stroke: SEAM_WIDTH }).collect())
|
||||
};
|
||||
let faces = wire_faces;
|
||||
if is_container(n) {
|
||||
let index = n
|
||||
.appearance
|
||||
@@ -752,7 +759,9 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<cra
|
||||
for s in shapes.iter().filter(|s| surface_item(&s.items).is_some()) {
|
||||
out.extend(faces(&s.path, b, fx, options)?);
|
||||
}
|
||||
return Ok((out, group_shadow(n.id, fx, &shapes, b, options, vec![])?));
|
||||
// In wireframe mode the canvas draws no shadow.
|
||||
let shadow = if crate::solids::renders_wireframe(fx) { vec![] } else { group_shadow(n.id, fx, &shapes, b, options, vec![])? };
|
||||
return Ok((out, shadow));
|
||||
}
|
||||
let (path, _) = object_profile(n, "Revolve")?;
|
||||
let ctx = GeomContext::of(n);
|
||||
@@ -763,7 +772,7 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<cra
|
||||
let item = surface_item(&paints).ok_or("the revolved object has no fill or stroke")?;
|
||||
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
|
||||
let surface = faces(&path, b, fx, fx_options(fx))?;
|
||||
let shadow = shadow_of(n.id, fx, &path, item, b, fx_options(fx), None)?;
|
||||
let shadow = if crate::solids::renders_wireframe(fx) { None } else { shadow_of(n.id, fx, &path, item, b, fx_options(fx), None)? };
|
||||
return Ok((surface, shadow.into_iter().map(Arc::new).collect()));
|
||||
}
|
||||
let path = geometry(&path, &n.appearance.effects, &ctx);
|
||||
@@ -781,6 +790,28 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<cra
|
||||
Ok((out, vec![]))
|
||||
}
|
||||
|
||||
/// The faces Revolve or Revolve (Classic) `fx` sweeps from `path` about the bounds `b` with
|
||||
/// `options`, each with the width of the stroke the canvas draws around it.
|
||||
fn wire_faces(path: &PathData, b: Rect, fx: &Effect, options: Revolve) -> Result<Vec<WireFace>, String> {
|
||||
if fx.id == CLASSIC_REVOLVE {
|
||||
let p = merged_params(&fx.id, &fx.params);
|
||||
let facets = vectorcraft_three_d::revolve_facets(path, b, options)?;
|
||||
return Ok(facets.iter().map(|f| WireFace { path: f.path(), stroke: crate::solids::classic::stroke_width(f, &p) }).collect());
|
||||
}
|
||||
Ok(vectorcraft_three_d::revolve(path, b, options)?.iter().map(|f| WireFace { path: f.path(), stroke: SEAM_WIDTH }).collect())
|
||||
}
|
||||
|
||||
/// The art Revolve `fx` in wireframe mode draws for object `id` from the profiles `paths`, all swept
|
||||
/// about the bounds `b` with `options`: the edges of their faces ([`crate::solids::wireframe::wire_art`]).
|
||||
/// `None` when a profile can't be swept or past the edge limit, when the canvas draws the source art.
|
||||
fn wire_art<'a>(id: NodeId, paths: impl IntoIterator<Item = &'a PathData>, b: Rect, fx: &Effect, options: Revolve) -> Option<Arc<Node>> {
|
||||
let mut faces = vec![];
|
||||
for path in paths {
|
||||
faces.extend(wire_faces(path, b, fx, options).ok()?.into_iter().map(|f| f.path));
|
||||
}
|
||||
crate::solids::wireframe::wire_art(id, &faces)
|
||||
}
|
||||
|
||||
/// [`revolve_meshes`] of group or layer `n`: one mesh for each member, about one shared axis.
|
||||
fn group_meshes(n: &Node, total: &mut usize, limit: usize) -> Result<Vec<crate::SolidMesh>, String> {
|
||||
let index = n
|
||||
@@ -824,13 +855,18 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
let paints = solid_paints(n);
|
||||
let item = surface_item(&paints)?;
|
||||
let expand = expand && !n.appearance.effects.iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let (surface, kept) = object_surface(doc, n, &path, fx, item, expand, &mut copies)?;
|
||||
if let Some(b) = path.bounds()
|
||||
&& let Some(shadow) = shadow_of(n.id, fx, &path, item, b, fx_options(fx), kept).ok()?
|
||||
{
|
||||
children.push(Arc::new(shadow));
|
||||
if crate::solids::renders_wireframe(fx) {
|
||||
// In wireframe mode the edges of the faces, with no shadow.
|
||||
children.push(wire_art(n.id, [&path], path.bounds()?, fx, fx_options(fx))?);
|
||||
} else {
|
||||
let (surface, kept) = object_surface(doc, n, &path, fx, item, expand, &mut copies)?;
|
||||
if let Some(b) = path.bounds()
|
||||
&& let Some(shadow) = shadow_of(n.id, fx, &path, item, b, fx_options(fx), kept).ok()?
|
||||
{
|
||||
children.push(Arc::new(shadow));
|
||||
}
|
||||
children.push(Arc::new(surface));
|
||||
}
|
||||
children.push(Arc::new(surface));
|
||||
// Later geometry acts on the projected art as a whole through container evaluation.
|
||||
remaining = past_effect(&n.appearance.effects, index);
|
||||
remaining.retain(|e| !is_revolve(&e.id));
|
||||
@@ -850,6 +886,13 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
let fx = item.effects().get(index)?;
|
||||
let p = geometry(&path, item.effects().get(..index)?, &ctx.item(original));
|
||||
let expand = expand && !item.effects().iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
if crate::solids::renders_wireframe(fx) {
|
||||
let mut art = wire_art(n.id, [&p], p.bounds()?, fx, fx_options(fx))?;
|
||||
Arc::make_mut(&mut art).appearance.effects =
|
||||
item.effects().iter().skip(index + 1).filter(|e| !is_revolve(&e.id)).cloned().collect();
|
||||
children.push(art);
|
||||
continue;
|
||||
}
|
||||
let (mut art, kept) = object_surface(doc, n, &p, fx, item, expand, &mut copies)?;
|
||||
art.appearance.effects = item.effects().iter().skip(index + 1).filter(|e| !is_revolve(&e.id)).cloned().collect();
|
||||
if let Some(b) = p.bounds()
|
||||
@@ -999,6 +1042,14 @@ fn group_art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
reshape_members(n, n.appearance.effects.get(..index)?, &mut shapes);
|
||||
let b = union_bounds(&shapes)?;
|
||||
let options = shared_options(&shapes, fx_options(fx)).ok()?;
|
||||
if crate::solids::renders_wireframe(fx) {
|
||||
// In wireframe mode the edges of every member's faces, with no shadow.
|
||||
let paths = shapes.iter().filter(|s| surface_item(&s.items).is_some()).map(|s| &s.path);
|
||||
let mut art = n.clone();
|
||||
art.kind = NodeKind::Group { children: vec![wire_art(n.id, paths, b, fx, options)?], clip: false };
|
||||
art.appearance = Appearance { effects: past_effect(&n.appearance.effects, index), ..Default::default() }.into();
|
||||
return Some(art);
|
||||
}
|
||||
let expand = expand && !n.appearance.effects.iter().skip(index + 1).any(|e| e.visible && is_geometry(&e.id));
|
||||
let mut copies = Copies::default();
|
||||
let mut surfaces = vec![];
|
||||
|
||||
@@ -36,7 +36,7 @@ pub use graphics::{
|
||||
pub(crate) mod shadow;
|
||||
|
||||
pub(crate) mod wireframe;
|
||||
pub use wireframe::three_d_wireframe_in;
|
||||
pub use wireframe::{RENDER, RENDER_MODES, renders_wireframe, three_d_wireframe_in};
|
||||
|
||||
pub const EXTRUDE: &str = "threeD.extrude";
|
||||
pub const INFLATE: &str = "threeD.inflate";
|
||||
@@ -274,6 +274,7 @@ fn validate_solid_params(id: &str, p: &Value) -> Result<(), String> {
|
||||
crate::lighting::validate(p)?;
|
||||
shadow::validate(p)?;
|
||||
graphics::validate(id, p)?;
|
||||
wireframe::validate_render(id, p)?;
|
||||
if let Some(v) = p.get("bevel")
|
||||
&& !v.as_str().is_some_and(|s| s == "none" || BevelShape::parse(s).is_some())
|
||||
{
|
||||
@@ -324,7 +325,7 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
let b = union_bounds(&shapes).ok_or_else(|| format!("{label} requires a group holding a shape with at least two anchors"))?;
|
||||
check_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
return cast_shadow(id, p, &shapes).map(|_| ());
|
||||
return check_cast(id, p, &shapes);
|
||||
}
|
||||
let (path, rule) = object_profile(n, label)?;
|
||||
let anchors = || format!("{label} requires a shape with at least two anchors");
|
||||
@@ -342,7 +343,16 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
let shapes = [Profile { path, rule, items, opacity: 1.0, scopes: vec![] }];
|
||||
check_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
cast_shadow(id, p, &shapes).map(|_| ())
|
||||
check_cast(id, p, &shapes)
|
||||
}
|
||||
|
||||
/// With the shadow of 3D effect `id` with parameters `p` on, an error when the shadow of `shapes`
|
||||
/// is past the limits ([`cast_shadow`]). In wireframe mode no shadow is drawn, and none is checked.
|
||||
fn check_cast(id: &str, p: &Value, shapes: &[Profile]) -> Result<(), String> {
|
||||
if wireframe::wireframe_params(id, p) {
|
||||
return Ok(());
|
||||
}
|
||||
cast_shadow(id, p, shapes).map(|_| ())
|
||||
}
|
||||
|
||||
/// Do the solids 3D effect `id` with parameters `p` builds from the profiles of `shapes` about `b`
|
||||
@@ -1420,6 +1430,10 @@ type ShadowAndSurfaces = (Vec<Arc<Node>>, Vec<Arc<Node>>);
|
||||
/// off), cast from the faces the solids drawn kept ([`cast_built`]), to be drawn behind every
|
||||
/// surface of the object, and the surfaces.
|
||||
fn art_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile]) -> Option<ShadowAndSurfaces> {
|
||||
// In wireframe mode the effect draws the edges of the faces it builds, with no shadow.
|
||||
if wireframe::renders_wireframe(fx) {
|
||||
return Some((vec![], wireframe::wire_surfaces(n.id, fx, shapes)?));
|
||||
}
|
||||
let p = crate::merged_params(&fx.id, &fx.params);
|
||||
let cast = !is_classic_three_d(&fx.id) && shadow::options(&p).is_some();
|
||||
let (art, built) = surfaces_for(doc, n, fx, shapes, cast)?;
|
||||
@@ -1434,8 +1448,11 @@ fn art_for(doc: Option<&Document>, n: &Node, fx: &Effect, shapes: &[Profile]) ->
|
||||
}
|
||||
|
||||
/// The shadow [`art_for`] draws for effect `fx` on `shapes` behind the surfaces of object `id`:
|
||||
/// none with the shadow off, and an error when it is past the limits.
|
||||
/// none with the shadow off or in wireframe mode, and an error when it is past the limits.
|
||||
pub(crate) fn shadow_for(id: NodeId, fx: &Effect, shapes: &[Profile]) -> Result<Vec<Arc<Node>>, String> {
|
||||
if wireframe::renders_wireframe(fx) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
Ok(match cast_shadow(&fx.id, &crate::merged_params(&fx.id, &fx.params), shapes)? {
|
||||
Some((caster, settings, cast)) => shadow::scoped_art(id, &caster, settings, cast, has_masks(shapes)),
|
||||
None => vec![],
|
||||
|
||||
@@ -1,22 +1,91 @@
|
||||
//! Expand as Wireframes: the edges of the faces a 3D effect draws, as open and closed paths that
|
||||
//! can be edited on their own.
|
||||
//! can be edited on their own. Also the 3D and Materials panel's Render Settings › Wireframe, which
|
||||
//! draws those edges in place of the shaded faces while the effect stays live.
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, Node, NodeId};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, LineJoin, Node, NodeId, StrokeLayer};
|
||||
use vectorcraft_geom::{Anchor, CubicBez, PathData, Point, SubPath};
|
||||
use vectorcraft_three_d::Facet;
|
||||
|
||||
use super::{
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, Profile, ROTATE, built_solids, facets, geometry, has_solid, is_container, is_plane, is_three_d, label_of,
|
||||
members, object_profile, region, reshape_members, resolve, resolve_for, solid, solid_paint_sources, solid_parts, union_bounds,
|
||||
CLASSIC_EXTRUDE, CLASSIC_ROTATE, EXTRUDE, INFLATE, Profile, ROTATE, built_solids, facets, geometry, has_solid, is_container, is_plane,
|
||||
is_three_d, label_of, members, object_profile, region, reshape_members, resolve, resolve_for, solid, solid_paint_sources, solid_parts,
|
||||
union_bounds,
|
||||
};
|
||||
use crate::{GeomContext, is_geometry};
|
||||
|
||||
/// The most distinct edges one object's wireframe may have.
|
||||
pub(crate) const MAX_WIRE_EDGES: usize = 1_000_000;
|
||||
|
||||
/// The parameter that holds a 3D effect's render settings: an object whose `mode` is one of
|
||||
/// [`RENDER_MODES`].
|
||||
pub const RENDER: &str = "render";
|
||||
|
||||
/// The render modes of a 3D effect: `shaded` draws the lit faces, `wireframe` the edges of every
|
||||
/// face ([`renders_wireframe`]).
|
||||
pub const RENDER_MODES: [&str; 2] = ["shaded", "wireframe"];
|
||||
|
||||
/// The 3D effects that take [`RENDER`]: the 3D and Materials panel's Extrude & Bevel, Revolve,
|
||||
/// Inflate and Rotate. The 3D (Classic) effects have their own Wireframe surface.
|
||||
const TAKES_RENDER: [&str; 4] = [EXTRUDE, crate::REVOLVE, INFLATE, ROTATE];
|
||||
|
||||
/// The weight of a rendered wireframe edge, in points: the weight Expand as Wireframes strokes its
|
||||
/// edges with (VectorCraft's own).
|
||||
pub(crate) const WIRE_WEIGHT: f64 = 0.5;
|
||||
|
||||
/// Does effect `fx` draw as a wireframe: is it one of the effects that take [`RENDER`], with its
|
||||
/// `render.mode` set to `wireframe`?
|
||||
pub fn renders_wireframe(fx: &Effect) -> bool {
|
||||
wireframe_params(&fx.id, &fx.params)
|
||||
}
|
||||
|
||||
/// Do 3D effect `id`'s parameters `p` set it to draw as a wireframe ([`renders_wireframe`])? A
|
||||
/// command that sets them is checked against the art that mode draws: the faces' edges, with no
|
||||
/// shadow, graphics or paint copies.
|
||||
pub(crate) fn wireframe_params(id: &str, p: &Value) -> bool {
|
||||
TAKES_RENDER.contains(&id) && p.get(RENDER).and_then(|r| r.get("mode")).and_then(Value::as_str) == Some("wireframe")
|
||||
}
|
||||
|
||||
/// Check the render settings in 3D effect `id`'s parameters `p`: `render`, when given to one of the
|
||||
/// effects in [`TAKES_RENDER`], is an object that holds only `mode`, one of [`RENDER_MODES`] (a
|
||||
/// missing `mode` is `shaded`). The 3D (Classic) effects refuse it as a parameter they do not take.
|
||||
pub(crate) fn validate_render(id: &str, p: &Value) -> Result<(), String> {
|
||||
let Some(render) = p.get(RENDER).filter(|_| TAKES_RENDER.contains(&id)) else { return Ok(()) };
|
||||
let valid = render
|
||||
.as_object()
|
||||
.is_some_and(|fields| fields.iter().all(|(key, value)| key == "mode" && value.as_str().is_some_and(|m| RENDER_MODES.contains(&m))));
|
||||
if !valid {
|
||||
return Err("`render` must be an object holding only `mode`, set to shaded or wireframe".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The art a 3D effect in wireframe mode draws for object `id` in place of its shaded faces: a group
|
||||
/// of the edges of `faces`, each edge once ([`edges`]), stroked [`WIRE_WEIGHT`] black with round
|
||||
/// joins and no fill, as Expand as Wireframes strokes them. Like Expand as Wireframes and the
|
||||
/// Outline view, the mesh shows every edge whatever the opacity, blending mode or opacity mask of
|
||||
/// the fills, strokes and group members it is built from; the object's own transparency applies to
|
||||
/// the art as a whole. `None` past [`MAX_WIRE_EDGES`] edges, when the canvas draws the source art,
|
||||
/// as it does for a solid past its limits.
|
||||
pub(crate) fn wire_art(id: NodeId, faces: &[PathData]) -> Option<Arc<Node>> {
|
||||
let mut stroke = StrokeLayer::new(Paint::solid(Color::BLACK), WIRE_WEIGHT);
|
||||
stroke.join = LineJoin::Round;
|
||||
let appearance = Appearance { items: vec![AppearanceItem::Stroke(stroke)], ..Default::default() };
|
||||
let children = edges(faces).ok()?.into_iter().map(|path| Arc::new(Node::path(id, path, appearance.clone()))).collect();
|
||||
Some(Arc::new(Node::group(id, children)))
|
||||
}
|
||||
|
||||
/// The art 3D effect `fx` in wireframe mode draws for object `id` from `shapes`: the edges of the
|
||||
/// faces it builds ([`wire_art`]). `None` when a solid can't be built or past the edge limit, when
|
||||
/// the canvas draws the source art.
|
||||
pub(crate) fn wire_surfaces(id: NodeId, fx: &Effect, shapes: &[Profile]) -> Option<Vec<Arc<Node>>> {
|
||||
let faces: Vec<PathData> = facet_paths(fx, shapes).ok()?.into_iter().map(|f| f.path).collect();
|
||||
Some(vec![wire_art(id, &faces)?])
|
||||
}
|
||||
|
||||
/// The grid points are rounded to before edges are compared, in points: two faces that share an
|
||||
/// edge give it once.
|
||||
const GRID: f64 = 1.0 / 1024.0;
|
||||
@@ -171,7 +240,7 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result<FacesAndShadow,
|
||||
/// as it is left out of the drawn art; past the limits of the object's solids together, the result
|
||||
/// is an error, as the canvas draws the source art. Each facet carries the width of the stroke the
|
||||
/// canvas draws around it.
|
||||
fn facet_paths(fx: &Effect, shapes: &[Profile]) -> Result<Vec<WireFace>, String> {
|
||||
pub(crate) fn facet_paths(fx: &Effect, shapes: &[Profile]) -> Result<Vec<WireFace>, String> {
|
||||
let label = label_of(&fx.id);
|
||||
let b = union_bounds(shapes).ok_or_else(|| format!("{label} requires a shape with at least two anchors"))?;
|
||||
let p = crate::merged_params(&fx.id, &fx.params);
|
||||
|
||||
@@ -281,6 +281,210 @@ fn a_clipping_group_with_revolve_and_rotate_expands_the_art_revolve_cuts() {
|
||||
assert_eq!(both, alone);
|
||||
}
|
||||
|
||||
/// The leaves `art` draws: each path with its fill and stroke.
|
||||
fn leaves(art: &Node) -> Vec<(PathData, bool, Option<StrokeLayer>)> {
|
||||
let mut out = vec![];
|
||||
art.walk(&mut |c| {
|
||||
if let Some(p) = c.path_data() {
|
||||
out.push((p.clone(), c.appearance.fill().is_some(), c.appearance.stroke().cloned()));
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// The art the canvas draws for `n`: Revolve's, else the other 3D effects'.
|
||||
fn drawn(n: &Node) -> Node {
|
||||
crate::revolve_art(n).or_else(|| super::super::solid_art(n)).unwrap_or_else(|| panic!("no art for {n:?}"))
|
||||
}
|
||||
|
||||
const WIRE: [&str; 4] = [super::super::EXTRUDE, crate::REVOLVE, super::super::INFLATE, ROTATE];
|
||||
|
||||
/// Render Settings › Wireframe: each of the panel's four effects draws the edges Expand as
|
||||
/// Wireframes gives, each a 0.5 pt black stroke with round joins and no fill, in place of its
|
||||
/// shaded faces; shaded again, it draws filled faces.
|
||||
#[test]
|
||||
fn wireframe_mode_draws_the_expanded_edges_as_strokes() {
|
||||
for id in WIRE {
|
||||
let n = solid_square(id, json!({"render": {"mode": "wireframe"}}));
|
||||
let art = leaves(&drawn(&n));
|
||||
let wire = three_d_wireframe_in(None, &n).unwrap_or_else(|e| panic!("{id}: {e}"));
|
||||
assert_eq!(art.len(), wire.len(), "{id}");
|
||||
for ((path, filled, stroke), edge) in art.iter().zip(&wire) {
|
||||
assert!(!filled, "{id}: an edge has no fill");
|
||||
let stroke = stroke.as_ref().unwrap_or_else(|| panic!("{id}: an edge is stroked"));
|
||||
assert_eq!((stroke.width, stroke.join, &stroke.paint), (WIRE_WEIGHT, LineJoin::Round, &Paint::solid(Color::BLACK)), "{id}");
|
||||
assert_eq!(segments(std::slice::from_ref(path)), segments(std::slice::from_ref(edge)), "{id}");
|
||||
}
|
||||
let shaded = leaves(&drawn(&solid_square(id, json!({"render": {"mode": "shaded"}}))));
|
||||
assert!(shaded.iter().any(|(_, filled, _)| *filled), "{id}: shaded faces are filled");
|
||||
}
|
||||
// The box turned off axis shows the nine edges of its three faces.
|
||||
let n = solid_square(super::super::EXTRUDE, json!({"render": {"mode": "wireframe"}}));
|
||||
let paths: Vec<PathData> = leaves(&drawn(&n)).into_iter().map(|(p, ..)| p).collect();
|
||||
assert_eq!(segments(&paths).len(), 9);
|
||||
}
|
||||
|
||||
/// The wireframe stays live: a deeper extrusion and a turned view redraw the edges.
|
||||
#[test]
|
||||
fn wireframe_mode_follows_the_effects_settings() {
|
||||
let paths = |p: Value| -> Vec<PathData> { leaves(&drawn(&solid_square(super::super::EXTRUDE, p))).into_iter().map(|(p, ..)| p).collect() };
|
||||
let flat = bounds(&paths(json!({"render": {"mode": "wireframe"}, "depth": 10.0})));
|
||||
let deep = bounds(&paths(json!({"render": {"mode": "wireframe"}, "depth": 200.0})));
|
||||
assert!(deep.width() > flat.width() + 1.0 || deep.height() > flat.height() + 1.0, "{flat:?} {deep:?}");
|
||||
let turned = paths(json!({"render": {"mode": "wireframe"}, "rotationY": 60.0}));
|
||||
assert_ne!(segments(&turned), segments(&paths(json!({"render": {"mode": "wireframe"}}))));
|
||||
}
|
||||
|
||||
/// In wireframe mode no shadow and no graphic is drawn: only the edges.
|
||||
#[test]
|
||||
fn wireframe_mode_draws_no_shadow() {
|
||||
for id in WIRE {
|
||||
let n = solid_square(id, json!({"render": {"mode": "wireframe"}, "shadow": true}));
|
||||
let art = leaves(&drawn(&n));
|
||||
assert!(!art.is_empty() && art.iter().all(|(_, filled, stroke)| !filled && stroke.is_some()), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The mode works where the effect is on a fill and on a group: the fill's solid and every member's
|
||||
/// solid draw as edges, and the geometry effects after a fill's effect still apply to them.
|
||||
#[test]
|
||||
fn wireframe_mode_on_a_fill_and_on_a_group() {
|
||||
for id in WIRE {
|
||||
let mut n = solid_square(id, json!({}));
|
||||
let mut fx = n.appearance.effects.remove(0);
|
||||
fx.params["render"] = json!({"mode": "wireframe"});
|
||||
let fill = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))).unwrap();
|
||||
fill.effects_mut().push(fx.clone());
|
||||
fill.effects_mut().push(crate::new_effect("distort.transform", &json!({"moveH": 500.0})).unwrap());
|
||||
let art = leaves(&drawn(&n));
|
||||
assert!(!art.is_empty() && art.iter().all(|(_, filled, stroke)| !filled && stroke.is_some()), "{id} on a fill");
|
||||
|
||||
let member = |x: f64, id: u64| {
|
||||
Arc::new(Node::path(NodeId(id), square(x, 100.0, 50.0), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0)))
|
||||
};
|
||||
let mut g = Node::group(NodeId(1), vec![member(100.0, 2), member(200.0, 3)]);
|
||||
g.appearance.effects.push(fx);
|
||||
let art = leaves(&drawn(&g));
|
||||
assert!(!art.is_empty() && art.iter().all(|(_, filled, stroke)| !filled && stroke.is_some()), "{id} on a group");
|
||||
// Both members' edges are drawn.
|
||||
let paths: Vec<PathData> = art.into_iter().map(|(p, ..)| p).collect();
|
||||
assert!(bounds(&paths).width() > 100.0, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The mesh shows every edge, as Expand as Wireframes gives them, whatever the opacity of the fill
|
||||
/// and the opacity masks of the group's members; the object's own opacity stays on the art. Two
|
||||
/// members that touch, one at 50% and one masked, draw their shared edge once: seven edges.
|
||||
#[test]
|
||||
fn the_mesh_shows_every_edge_whatever_the_members_transparency() {
|
||||
let member = |x: f64, id: u64, opacity: f32| {
|
||||
let mut n = Node::path(NodeId(id), square(x, 100.0, 50.0), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) {
|
||||
f.opacity = opacity;
|
||||
}
|
||||
n
|
||||
};
|
||||
let mut masked = member(150.0, 3, 1.0);
|
||||
masked.extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(member(0.0, 900, 1.0), true)));
|
||||
let mut g = Node::group(NodeId(1), vec![Arc::new(member(100.0, 2, 0.5)), Arc::new(masked)]);
|
||||
g.opacity = 0.75;
|
||||
g.appearance.effects.push(crate::new_effect(ROTATE, &json!({"render": {"mode": "wireframe"}})).unwrap());
|
||||
let art = drawn(&g);
|
||||
assert_eq!(art.opacity, 0.75);
|
||||
let paths: Vec<PathData> = leaves(&art).into_iter().map(|(p, ..)| p).collect();
|
||||
assert_eq!(segments(&paths).len(), 7, "{paths:?}");
|
||||
assert_eq!(segments(&paths).len(), segments(&three_d_wireframe_in(None, &g).unwrap()).len());
|
||||
let mut scoped = 0;
|
||||
for c in art.children().into_iter().flatten() {
|
||||
c.walk(&mut |c| scoped += usize::from(c.extras().mask.is_some() || c.opacity < 1.0));
|
||||
}
|
||||
assert_eq!(scoped, 0, "no part of the mesh is masked or faded");
|
||||
// A fill at 0% still shows its mesh.
|
||||
for id in WIRE {
|
||||
let mut n = solid_square(id, json!({"render": {"mode": "wireframe"}}));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Fill(_))) {
|
||||
f.opacity = 0.0;
|
||||
}
|
||||
assert!(!leaves(&drawn(&n)).is_empty(), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand Appearance of a Revolve in wireframe mode gives the stroked edges, not the shaded faces.
|
||||
#[test]
|
||||
fn expanding_a_revolve_in_wireframe_mode_gives_the_edges() {
|
||||
let n = solid_square(crate::REVOLVE, json!({"render": {"mode": "wireframe"}}));
|
||||
let art = leaves(&crate::revolve::expand_revolve_art(&n).unwrap());
|
||||
assert_eq!(art.len(), three_d_wireframe_in(None, &n).unwrap().len());
|
||||
assert!(art.iter().all(|(_, filled, stroke)| !filled && stroke.as_ref().is_some_and(|s| s.width == WIRE_WEIGHT)));
|
||||
}
|
||||
|
||||
/// `render` is checked before an edit: an object holding only `mode`, `shaded` or `wireframe`, and
|
||||
/// only on the panel's four effects.
|
||||
#[test]
|
||||
fn render_settings_are_validated() {
|
||||
for id in WIRE {
|
||||
for good in [json!({}), json!({"render": {}}), json!({"render": {"mode": "shaded"}}), json!({"render": {"mode": "wireframe"}})] {
|
||||
assert_eq!(crate::validate_three_d_params(id, &good), Ok(()), "{id} {good}");
|
||||
}
|
||||
for bad in [
|
||||
json!({"render": "wireframe"}),
|
||||
json!({"render": null}),
|
||||
json!({"render": {"mode": "raytrace"}}),
|
||||
json!({"render": {"mode": true}}),
|
||||
json!({"render": {"mode": "wireframe", "quality": "high"}}),
|
||||
] {
|
||||
let e = crate::validate_three_d_params(id, &bad).unwrap_err();
|
||||
assert_eq!(e, "`render` must be an object holding only `mode`, set to shaded or wireframe", "{id} {bad}");
|
||||
}
|
||||
// The command check on the object refuses it too.
|
||||
let n = solid_square(id, json!({}));
|
||||
assert!(crate::validate_three_d(&n, id, &json!({"render": {"mode": "x"}})).is_err(), "{id}");
|
||||
}
|
||||
for id in [super::super::CLASSIC_EXTRUDE, crate::CLASSIC_REVOLVE, super::super::CLASSIC_ROTATE] {
|
||||
let e = crate::validate_three_d_params(id, &json!({"render": {"mode": "wireframe"}})).unwrap_err();
|
||||
assert!(e.contains("`render`"), "{id}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A Revolve whose shaded surface is past the budget of paint copies (a gradient with many stops) is
|
||||
/// refused shaded, but in wireframe mode, which draws only the black edges, it is accepted and drawn:
|
||||
/// on the object and on a fill.
|
||||
#[test]
|
||||
fn wireframe_mode_is_checked_against_the_edges_it_draws() {
|
||||
use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop};
|
||||
let n = solid_square(crate::REVOLVE, json!({}));
|
||||
let path = n.path_data().unwrap().clone();
|
||||
let faces = vectorcraft_three_d::revolve(&path, path.bounds().unwrap(), crate::revolve::revolve_options(&json!({}))).unwrap().len();
|
||||
let stops = crate::revolve::MAX_PAINT_COPIES / (faces * 4) / std::mem::size_of::<GradientStop>() + 1;
|
||||
let stops = (0..stops).map(|i| GradientStop::new(i as f32 / stops as f32, Color::BLACK)).collect();
|
||||
let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
|
||||
let mut n = n;
|
||||
n.appearance = Appearance::basic(gradient, Paint::None, 0.0).into();
|
||||
let shaded = json!({});
|
||||
let wire = json!({"render": {"mode": "wireframe"}});
|
||||
let refused = crate::validate_revolve(&n, &shaded).unwrap_err();
|
||||
assert!(refused.contains("simplify the gradient"), "{refused}");
|
||||
assert_eq!(crate::validate_revolve(&n, &wire), Ok(()));
|
||||
n.appearance.effects.push(crate::new_effect(crate::REVOLVE, &wire).unwrap());
|
||||
assert!(leaves(&drawn(&n)).iter().all(|(_, filled, stroke)| !filled && stroke.is_some()));
|
||||
// On the fill.
|
||||
let fx = n.appearance.effects.remove(0);
|
||||
let at = n.appearance.items.iter().position(|i| matches!(i, AppearanceItem::Fill(_))).unwrap();
|
||||
assert!(crate::revolve::validate_item_revolve(&n, &shaded, at, 0).is_err());
|
||||
assert_eq!(crate::revolve::validate_item_revolve(&n, &wire, at, 0), Ok(()));
|
||||
n.appearance.items[at].effects_mut().push(fx);
|
||||
assert!(leaves(&drawn(&n)).iter().all(|(_, filled, stroke)| !filled && stroke.is_some()));
|
||||
}
|
||||
|
||||
/// A 3D (Classic) effect ignores a `render` it does not take in a loaded file: it draws as before.
|
||||
#[test]
|
||||
fn a_classic_effect_is_not_drawn_as_a_render_wireframe() {
|
||||
let mut n = solid_square(super::super::CLASSIC_EXTRUDE, json!({}));
|
||||
n.appearance.effects[0].params["render"] = json!({"mode": "wireframe"});
|
||||
assert!(!renders_wireframe(&n.appearance.effects[0]));
|
||||
assert!(leaves(&drawn(&n)).iter().any(|(_, filled, _)| *filled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_without_a_visible_3d_effect_is_an_error() {
|
||||
let mut n = solid_square(super::super::EXTRUDE, json!({}));
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(crate) mod graph;
|
||||
mod halftone;
|
||||
pub mod help;
|
||||
pub mod importmatches;
|
||||
mod info;
|
||||
pub(crate) mod inline;
|
||||
mod interactivity;
|
||||
mod intertwine;
|
||||
@@ -344,6 +345,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(docmenu::specs());
|
||||
v.extend(variablecmds::specs());
|
||||
v.extend(docinfo::specs());
|
||||
v.extend(info::specs());
|
||||
v.extend(panelcmds::specs());
|
||||
v.extend(artboardalign::specs());
|
||||
v.extend(buildcmds::specs());
|
||||
|
||||
@@ -393,8 +393,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
return Err(err(format!("can't open `{}`: {}", file_name(name), crate::cmd::place::dcs::LINK_ONLY)));
|
||||
}
|
||||
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
|
||||
// A `.ai` keeps the artboard its header gives; an EPS comes in on its page.
|
||||
let import = if matches!(format.id, "ai" | "ait") { vectorcraft_eps::import_ai } else { vectorcraft_eps::import_names };
|
||||
restore_or_import(editing, || {
|
||||
let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
|
||||
let r = import(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
|
||||
.map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
|
||||
Ok((r.document, r.warnings))
|
||||
})?
|
||||
|
||||
@@ -277,3 +277,17 @@ fn ps_and_epsf_files_open_and_place() {
|
||||
let e = writable_format(None, Some("out.ps")).unwrap_err();
|
||||
assert_eq!(e, "PostScript files can be opened but not written (see document.formats)");
|
||||
}
|
||||
|
||||
/// A program in the legacy `.ai` format with a template box and no art size (version 3) opens as a
|
||||
/// `.ai` or `.ait` on a US Letter artboard around the box's center, and as an EPS on its bounding
|
||||
/// box.
|
||||
#[test]
|
||||
fn a_legacy_ai_program_opens_on_its_artboard_and_an_eps_on_its_page() {
|
||||
let program = b"%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 30 30\n%AI3_TemplateBox: 100 100 100 100\n\
|
||||
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /k {setcmykcolor} def\n\
|
||||
%%EndProlog\n%%BeginSetup\n%%EndSetup\n0 0 0 1 k\n10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
|
||||
for (name, size) in [("old.ai", (612.0, 792.0)), ("old.ait", (612.0, 792.0)), ("old.eps", (20.0, 20.0))] {
|
||||
let r = load(name, program).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(r.doc.artboards[0].rect.size(), vectorcraft_geom::Size::new(size.0, size.1), "{name}: {:?}", r.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,10 +386,32 @@ fn replacing_a_clipped_psd_keeps_its_scale() {
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}");
|
||||
let (anchors, _, _) = clip_group(&s.doc().unwrap().doc, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
assert_eq!(anchors, [[200.0, 150.0], [300.0, 250.0], [100.0, 250.0]]);
|
||||
// Other art replacing the clip group takes its centre only, as before.
|
||||
// A plain image replacing the clip group keeps its scale too.
|
||||
let plain = vectorcraft_format::base64_encode(&red_psd(&[]));
|
||||
let r = s.execute("file.place", &json!({"name": "plain.psd", "dataBase64": plain, "replace": true})).unwrap();
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(100.0), Some(50.0)), "{r}");
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}");
|
||||
}
|
||||
|
||||
/// A plain image replacing a clipped PSD file takes the clipped image's scale and rotation.
|
||||
#[test]
|
||||
fn a_plain_image_replacing_a_clipped_psd_keeps_its_rotation() {
|
||||
let data = vectorcraft_format::base64_encode(&red_psd(&triangle_resources()));
|
||||
let plain = vectorcraft_format::base64_encode(&red_psd(&[]));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let r = s.execute("file.place", &json!({"name": "cut.psd", "dataBase64": data, "at": [200, 200]})).unwrap();
|
||||
let group = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
s.execute("object.scale", &json!({"sx": 200, "sy": 150})).unwrap();
|
||||
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
|
||||
let xf = |s: &Session, id: NodeId| match s.doc().unwrap().doc.node(id).map(|n| &n.kind) {
|
||||
Some(NodeKind::Image(im)) => im.xf.as_coeffs(),
|
||||
other => panic!("not an image: {other:?}"),
|
||||
};
|
||||
let (_, _, image) = clip_group(&s.doc().unwrap().doc, group);
|
||||
let before = xf(&s, image);
|
||||
let r = s.execute("file.place", &json!({"name": "plain.psd", "dataBase64": plain, "replace": true})).unwrap();
|
||||
let after = xf(&s, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
assert!(after.iter().zip(before).all(|(a, b)| (a - b).abs() < 1e-9), "{after:?} != {before:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Window › Info (`info.readout`): the selection's size and the active tool's readout, as the Info
|
||||
//! panel shows them.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
use super::{CommandSpec, Result, Session, always, cmd};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
query "info.readout",
|
||||
"Info",
|
||||
[],
|
||||
None,
|
||||
"{} → {tool: the active tool's id, width?, height?: the sides of the selection's bounding box in pt (the box around the anchors selected with Direct Selection, when there are some), measured as prefs usePreviewBounds says; left out with nothing selected, readout?: what the Info panel shows for the tool in use: {kind: \"drag\", dx, dy, distance, angle} while the Selection, Direct Selection or Group Selection tool moves the selection or the Gradient tool draws a gradient vector on the art; {kind: \"scale\", width, height} (percent) during a Scale tool drag; {kind: \"rotate\", center: [x, y], angle: deg counter-clockwise, as object.rotate takes it} and {kind: \"reflect\", center, angle: the axis in deg counter-clockwise from horizontal, as object.reflect takes it} during a Rotate or Reflect tool drag; {kind: \"shear\", center, axis: 0 for a horizontal shear and 90 for a vertical one, angle: deg, as object.shear takes it} during a Shear tool drag; {kind: \"brush\", brush: name|null} while the Paintbrush is the tool; left out for any other tool and between drags. Lengths are in pt, a drag's angle is in deg counter-clockwise from the x axis, and center is the center of the selection's bounding box as the drag has carried it, in document coordinates}",
|
||||
always,
|
||||
readout
|
||||
)]
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The sides of the selection's bounding box as the Info panel shows them: of the box around
|
||||
/// the anchors selected with Direct Selection when there are some, else of the bounding box
|
||||
/// rotated with rotated objects ([`Session::transform_box`]), measured as Use Preview Bounds
|
||||
/// says. None with nothing selected.
|
||||
pub fn info_size(&self) -> Option<(f64, f64)> {
|
||||
let st = self.active()?;
|
||||
let anchors = effects::preview::anchor_bounds_with(&st.selection, &st.doc, self.bounds_measure());
|
||||
let r = anchors.or_else(|| self.transform_box(&st.selection.objects).map(|b| b.rect))?;
|
||||
Some((r.width(), r.height()))
|
||||
}
|
||||
}
|
||||
|
||||
fn readout(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let mut out = json!({ "tool": s.tool_id() });
|
||||
if let Some((w, h)) = s.info_size() {
|
||||
out["width"] = json!(w);
|
||||
out["height"] = json!(h);
|
||||
}
|
||||
if let Some(r) = s.tool_readout() {
|
||||
out["readout"] = r.to_json();
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use crate::{Session, ViewInfo};
|
||||
|
||||
fn view() -> ViewInfo {
|
||||
ViewInfo { smart_guides: false, snap_to_point: false, ..Default::default() }
|
||||
}
|
||||
|
||||
/// A 100 by 60 rectangle at (50, 50), selected, with a 10 pt stroke.
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 60})).unwrap()["id"].clone();
|
||||
s.execute("select.set", &json!({ "ids": [id] })).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 10})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn tool(s: &mut Session, id: &str) {
|
||||
s.select_tool(id, view()).unwrap();
|
||||
}
|
||||
|
||||
fn pointer(s: &mut Session, kind: PointerKind, x: f64, y: f64) {
|
||||
s.pointer(&PointerEvent::new(kind, x, y), view()).unwrap();
|
||||
}
|
||||
|
||||
fn info(s: &mut Session) -> serde_json::Value {
|
||||
s.execute("info.readout", &json!({})).unwrap()
|
||||
}
|
||||
|
||||
fn near(v: &serde_json::Value, want: f64) -> bool {
|
||||
v.as_f64().is_some_and(|v| (v - want).abs() < 1e-6)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_size_follows_use_preview_bounds() {
|
||||
let mut s = session();
|
||||
let i = info(&mut s);
|
||||
assert!(near(&i["width"], 100.0) && near(&i["height"], 60.0), "{i}");
|
||||
assert!(i.get("readout").is_none(), "{i}");
|
||||
s.execute("prefs.set", &json!({"key": "usePreviewBounds", "value": true})).unwrap();
|
||||
let i = info(&mut s);
|
||||
assert!(near(&i["width"], 110.0) && near(&i["height"], 70.0), "{i}");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let i = info(&mut s);
|
||||
assert!(i.get("width").is_none() && i.get("height").is_none(), "{i}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_reads_how_far_it_has_gone_until_the_release() {
|
||||
let mut s = session();
|
||||
tool(&mut s, "selection");
|
||||
pointer(&mut s, PointerKind::Down, 100.0, 80.0);
|
||||
assert!(info(&mut s).get("readout").is_none());
|
||||
pointer(&mut s, PointerKind::Drag, 130.0, 40.0);
|
||||
let i = info(&mut s);
|
||||
let r = &i["readout"];
|
||||
assert_eq!(r["kind"], "drag", "{i}");
|
||||
assert!(near(&r["dx"], 30.0) && near(&r["dy"], -40.0) && near(&r["distance"], 50.0), "{i}");
|
||||
assert!(near(&r["angle"], 53.13010235415598), "{i}");
|
||||
pointer(&mut s, PointerKind::Up, 130.0, 40.0);
|
||||
let i = info(&mut s);
|
||||
assert!(i.get("readout").is_none(), "{i}");
|
||||
assert!(near(&i["width"], 100.0), "{i}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_transform_tool_reads_its_drag() {
|
||||
for (id, check) in [
|
||||
("scale", (|r| r["kind"] == "scale" && near(&r["width"], 200.0) && near(&r["height"], 200.0)) as fn(&serde_json::Value) -> bool),
|
||||
("rotate", |r| r["kind"] == "rotate" && near(&r["center"][0], 100.0) && near(&r["center"][1], 80.0) && near(&r["angle"], -90.0)),
|
||||
("reflect", |r| r["kind"] == "reflect" && near(&r["center"][0], 100.0) && near(&r["angle"], 0.0)),
|
||||
("shear", |r| {
|
||||
r["kind"] == "shear"
|
||||
&& near(&r["center"][0], 100.0)
|
||||
&& near(&r["center"][1], 80.0)
|
||||
&& near(&r["axis"], 0.0)
|
||||
&& near(&r["angle"], 45.0)
|
||||
}),
|
||||
] {
|
||||
let mut s = session();
|
||||
tool(&mut s, id);
|
||||
// The reference point is the box's center (100, 80).
|
||||
let from = if id == "shear" { (100.0, 60.0) } else { (120.0, 100.0) };
|
||||
pointer(&mut s, PointerKind::Down, from.0, from.1);
|
||||
let to = match id {
|
||||
"scale" => (140.0, 120.0),
|
||||
"rotate" => (80.0, 100.0),
|
||||
"reflect" => (160.0, 80.0),
|
||||
_ => (120.0, 60.0),
|
||||
};
|
||||
pointer(&mut s, PointerKind::Drag, to.0, to.1);
|
||||
let i = info(&mut s);
|
||||
assert_eq!(i["tool"], id);
|
||||
assert!(check(&i["readout"]), "{id}: {i}");
|
||||
pointer(&mut s, PointerKind::Up, to.0, to.1);
|
||||
assert!(info(&mut s).get("readout").is_none(), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_paintbrush_reads_its_brush() {
|
||||
let mut s = session();
|
||||
tool(&mut s, "paintbrush");
|
||||
let i = info(&mut s);
|
||||
assert_eq!(i["readout"], json!({"kind": "brush", "brush": null}), "{i}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_parameter_is_refused() {
|
||||
let mut s = session();
|
||||
let e = s.execute("info.readout", &json!({"tool": "scale"})).unwrap_err().to_string();
|
||||
assert!(e.contains("tool"), "{e}");
|
||||
}
|
||||
}
|
||||
@@ -495,14 +495,14 @@ fn at_param(p: &Value) -> Result<Option<Point>> {
|
||||
|
||||
/// The transform that puts new art with the natural box `natural` where `old` is: an image's scale
|
||||
/// and rotation relative to its 100% size (its resolution's) and its centre are kept (with
|
||||
/// `image_xf`), also for an image alone in a clip group when the new art is an image clipped by
|
||||
/// its file's clipping path (`clipped`); other objects keep their centre.
|
||||
fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool, clipped: bool) -> Affine {
|
||||
/// `image_xf`), also for an image alone in a clip group when the new art is an image, clipped by
|
||||
/// its file's clipping path or not (`new_image`); other objects keep their centre.
|
||||
fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool, new_image: bool) -> Affine {
|
||||
// An image alone in a clip group (as a PSD or PSB file's clipping path places it,
|
||||
// [`fileio::file_clipped`]) is measured as the image when the new art brings its own clip
|
||||
// group; a DCS file's plates are not.
|
||||
// [`fileio::file_clipped`]) is measured as the image when the new art is an image; a DCS
|
||||
// file's plates are not.
|
||||
let image = match &old.kind {
|
||||
NodeKind::Group { children, clip: true } if clipped => match &children[..] {
|
||||
NodeKind::Group { children, clip: true } if new_image => match &children[..] {
|
||||
[path, image] if matches!(path.kind, NodeKind::Path { clipping: true, .. }) => match &image.kind {
|
||||
NodeKind::Image(im) if d.images.get(&im.key).is_none_or(|b| b.separated().is_none()) => image,
|
||||
_ => old,
|
||||
@@ -681,7 +681,7 @@ fn apply(s: &mut Session, read: PlaceRead) -> Result<Value> {
|
||||
}
|
||||
let natural = loaded.natural;
|
||||
let m = match (old_node, rect) {
|
||||
(Some(o), _) => replace_xf(&st.doc, o, natural, !is_text, matches!(loaded.art, Art::Image(_, Some(_)))),
|
||||
(Some(o), _) => replace_xf(&st.doc, o, natural, !is_text, matches!(loaded.art, Art::Image(..))),
|
||||
(None, Some(r)) => fit(natural, r),
|
||||
// Its artboard's bottom-left corner on the target's.
|
||||
(None, None) if let (Some(src), Some(to)) = (loaded.board, target) => Affine::translate((to.x0 - src.x0, to.y1 - src.y1)),
|
||||
|
||||
@@ -206,6 +206,7 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"imageTrace.makeAndExpand",
|
||||
"imageTrace.release",
|
||||
"imageTrace.setView",
|
||||
"info.readout",
|
||||
"layer.clippingMask.toggle",
|
||||
"layer.collectInNew",
|
||||
"layer.delete",
|
||||
|
||||
@@ -84,6 +84,63 @@ fn release_and_ungroup_keep_the_clipping_paths_paint() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixels of `doc` rendered on white that are mostly blue and mostly green.
|
||||
fn blue_and_green(doc: &Document) -> (usize, usize) {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let r = Renderer::new().render(doc, 200, 200, Affine::IDENTITY, &opts);
|
||||
let (mut blue, mut green) = (0, 0);
|
||||
for y in 0..200 {
|
||||
for x in 0..200 {
|
||||
let p = r.pixel(x, y);
|
||||
blue += usize::from(p[2] > 200 && p[0] < 60 && p[1] < 60);
|
||||
green += usize::from(p[1] > 200 && p[0] < 60 && p[2] < 60);
|
||||
}
|
||||
}
|
||||
(blue, green)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn painted_type_clipping_inside_a_group_with_geometry_effects_keeps_its_paint() {
|
||||
// The canvas and the SVG and PDF output paint the type's fill and stroke as they did before
|
||||
// the effect. Type that stays type beside the clip group takes the second Transform case
|
||||
// through baking that keeps type.
|
||||
let cases =
|
||||
[("distort.transform", json!({"moveH": 5}), false), ("distort.transform", json!({"moveH": 5}), true), ("distort.roughen", json!({}), false)];
|
||||
for (effect, params, other_type) in cases {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
rect(&mut s, 0.0, 30.0, "#ff0000");
|
||||
let t = NodeId(s.execute("text.create", &json!({"x": 10, "y": 120, "text": "Clip", "size": 72})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let clip = NodeId(s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"color": "#0000ff", "ids": [t.0]})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"color": "#00ff00", "ids": [t.0]})).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 2, "ids": [t.0]})).unwrap();
|
||||
let before = blue_and_green(&s.doc().unwrap().doc);
|
||||
let mut ids = vec![clip.0];
|
||||
if other_type {
|
||||
ids.push(s.execute("text.create", &json!({"x": 10, "y": 190, "text": "x", "size": 10})).unwrap()["id"].as_u64().unwrap());
|
||||
}
|
||||
let group = NodeId(s.execute("object.group", &json!({"ids": ids})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("effect.apply", &json!({"effect": effect, "params": params, "ids": [group.0], "item": null})).unwrap();
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let (blue, green) = blue_and_green(&doc);
|
||||
assert!(
|
||||
blue * 2 > before.0 && green * 2 > before.1,
|
||||
"{effect} {other_type}: the canvas paints the type's fill and stroke: {blue} {green}, {before:?} before"
|
||||
);
|
||||
let svg = vectorcraft_svg::export(&doc, &Default::default());
|
||||
let pdf = vectorcraft_pdf::export(&doc, &Default::default()).unwrap();
|
||||
for (what, out) in [("SVG", vectorcraft_svg::import(&svg).unwrap()), ("PDF", vectorcraft_pdf::import(&pdf).unwrap())] {
|
||||
let (b, g) = blue_and_green(&out);
|
||||
assert!(
|
||||
b.abs_diff(blue) * 10 < blue && g.abs_diff(green) * 5 < green,
|
||||
"{effect} {other_type}, {what}: {b} {g} against the canvas's {blue} {green}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_as_a_clipping_path_paints_its_characters() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -45,6 +45,7 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects()
|
||||
assert!(libs.iter().any(|l| l["id"] == "game-hud" && l["name"] == "Game HUD"), "{libs:?}");
|
||||
assert!(libs.iter().any(|l| l["id"] == "status-effects" && l["name"] == "Status Effects"), "{libs:?}");
|
||||
assert!(libs.iter().any(|l| l["id"] == "textures" && l["name"] == "Textures" && l["count"] == 8), "{libs:?}");
|
||||
assert!(libs.iter().any(|l| l["id"] == "neon-glow" && l["name"] == "Neon & Glow" && l["count"] == 6), "{libs:?}");
|
||||
for l in libs {
|
||||
let id = l["id"].as_str().unwrap();
|
||||
let g = run(&mut s, "graphicStyle.library", json!({"library": id}));
|
||||
@@ -69,6 +70,9 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects()
|
||||
let texture_names: Vec<&str> = textures["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
|
||||
let expected = ["Fine Grain", "Canvas Weave", "Burlap Sack", "Cracked Glaze", "Mosaic Floor", "Quilted Squares", "Leaded Glass", "Damp Paper"];
|
||||
assert_eq!(texture_names, expected);
|
||||
let neon = run(&mut s, "graphicStyle.library", json!({"library": "neon-glow"}));
|
||||
let neon_names: Vec<&str> = neon["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(neon_names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]);
|
||||
let id = rect(&mut s, 10.0, 20.0, 40.0, 30.0);
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "game-hud", "name": "Mana Glow", "apply": true, "ids": [id.0]}));
|
||||
assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec!["Mana Glow"], Some("Mana Glow")));
|
||||
@@ -419,3 +423,53 @@ fn every_builtin_style_paints_visibly_on_a_rectangle() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neon_and_glow_library_adds_and_applies_multi_stroke_styles() {
|
||||
let mut s = session();
|
||||
let list = run(&mut s, "graphicStyle.libraries", json!({}));
|
||||
let libs = list["libraries"].as_array().unwrap();
|
||||
let at = libs.iter().position(|l| l["id"] == "neon-glow").expect("neon-glow listed");
|
||||
assert_eq!(libs[at]["name"], "Neon & Glow");
|
||||
assert_eq!(libs[at]["count"], 6);
|
||||
|
||||
let (_, lib) = cmd::stylelib::library(&s, "neon-glow").unwrap();
|
||||
assert_eq!(lib.len(), 6);
|
||||
|
||||
let want = [
|
||||
("Cyan Neon", 2, "stylize.dropShadow"),
|
||||
("Magenta Laser", 2, "stylize.outerGlow"),
|
||||
("Amber Tube", 2, "stylize.outerGlow"),
|
||||
("Electric Lime", 2, "stylize.outerGlow"),
|
||||
("Pulse Violet", 2, "stylize.innerGlow"),
|
||||
("Cobalt Phosphor", 2, "blur.gaussian"),
|
||||
];
|
||||
|
||||
for (i, (name, stroke_count, first_effect)) in want.iter().enumerate() {
|
||||
let id = rect(&mut s, 10.0 + 30.0 * i as f64, 15.0, 25.0, 25.0);
|
||||
let before_undo = undo_len(&s);
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": name, "apply": true, "ids": [id.0]}));
|
||||
assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec![*name], Some(*name)));
|
||||
assert_eq!(undo_len(&s), before_undo + 1, "{name}");
|
||||
|
||||
let node = doc(&s).node(id).unwrap();
|
||||
let strokes = node.appearance.items.iter().filter(|it| matches!(it, vectorcraft_doc::AppearanceItem::Stroke(_))).count();
|
||||
assert_eq!(strokes, *stroke_count, "{name}");
|
||||
assert_eq!(node.appearance.effects[0].id, *first_effect, "{name}");
|
||||
}
|
||||
|
||||
// Adding existing style dedupes and creates no new undo step.
|
||||
let before = (undo_len(&s), doc(&s).graphic_styles.len());
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": "Cyan Neon"}));
|
||||
assert_eq!((names(&r["added"]), names(&r["existing"])), (vec![], vec!["Cyan Neon"]));
|
||||
assert_eq!((undo_len(&s), doc(&s).graphic_styles.len()), before);
|
||||
|
||||
// Round-trip through serialize / open
|
||||
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
|
||||
run(&mut s, "document.open", json!({"name": "neon.vectorcraft", "dataBase64": b64}));
|
||||
for (name, _, _) in want {
|
||||
let d = doc(&s);
|
||||
let st = d.graphic_style(name).unwrap();
|
||||
assert!(st.same_look(lib.style(name).unwrap()), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1103,6 +1103,12 @@ impl Session {
|
||||
self.pointer_held
|
||||
}
|
||||
|
||||
/// What the Info panel shows for the active tool as it is now
|
||||
/// ([`vectorcraft_tools::Tool::readout`]), in the view of the last pointer event.
|
||||
pub fn tool_readout(&mut self) -> Option<vectorcraft_tools::Readout> {
|
||||
self.with_tool_cx(self.last_view, |t, cx| t.readout(cx))
|
||||
}
|
||||
|
||||
/// Is the active tool moving, scaling or rotating the selection with a drag?
|
||||
pub fn tool_transforming(&self) -> bool {
|
||||
self.tool.transforming()
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
//! from those layers alone, without its page.
|
||||
//!
|
||||
//! The page is the file's `%%BoundingBox` (`%%HiResBoundingBox` when it has one), the first page
|
||||
//! of a PostScript file without one. A file whose program can't be read (an operator the
|
||||
//! of a PostScript file without one. A `.ai` in the legacy format ([`import_ai`]) that comes in
|
||||
//! through its layers keeps the artboard its header gives, with its art outside it; an EPS comes in
|
||||
//! on its page. A file whose program can't be read (an operator the
|
||||
//! interpreter doesn't know, an error, a limit reached) or that draws nothing comes in from its
|
||||
//! layers when its program has them (a `.ai` in PostScript form whose prolog is named but not
|
||||
//! included, as other apps write it), or with its art on one layer when it is in the legacy format
|
||||
@@ -279,7 +281,20 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
|
||||
/// and data sets) read in `names`. The type of the document it gives that is set in families that
|
||||
/// aren't available shows in the fallback font, and a warning names them.
|
||||
pub fn import_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
let mut r = read_names(bytes, editing_data, trust, names)?;
|
||||
read_checked(bytes, editing_data, trust, names, false)
|
||||
}
|
||||
|
||||
/// [`import_names`] for a `.ai` (or `.ait`) in PostScript form: a program in the legacy format
|
||||
/// that comes in through its layers keeps the artboard its header gives, with its art outside it,
|
||||
/// where an EPS comes in on its page, its art's box. A `.ai` of versions 3 and 4, which doesn't
|
||||
/// give its artboard's size, has a US Letter artboard around its template box's center.
|
||||
pub fn import_ai(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
read_checked(bytes, editing_data, trust, names, true)
|
||||
}
|
||||
|
||||
/// [`import_names`] (a `.ai` when `ai_file`, see [`import_ai`]).
|
||||
fn read_checked(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding, ai_file: bool) -> Result<Imported, String> {
|
||||
let mut r = read_names(bytes, editing_data, trust, names, ai_file)?;
|
||||
let missing = missing_fonts(&r.document);
|
||||
if !missing.is_empty() {
|
||||
r.warnings.push(format!("{MISSING_FONTS_NOTE}: {}", missing.join(", ")));
|
||||
@@ -314,8 +329,8 @@ fn missing_fonts(d: &Document) -> Vec<String> {
|
||||
missing
|
||||
}
|
||||
|
||||
/// [`import_names`] before the type's fonts are checked.
|
||||
fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
/// [`import_names`] before the type's fonts are checked (a `.ai` when `ai_file`, see [`import_ai`]).
|
||||
fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding, ai_file: bool) -> Result<Imported, String> {
|
||||
let ps = program(bytes)?;
|
||||
let dsc = Dsc::read(ps);
|
||||
// A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no
|
||||
@@ -323,7 +338,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
|
||||
// printed).
|
||||
if editing_data
|
||||
&& dsc.own_legacy
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, names)
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, ai_file, names)
|
||||
{
|
||||
return Ok(Imported { document, warnings, preview: false });
|
||||
}
|
||||
@@ -343,7 +358,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
|
||||
}
|
||||
let drew = !out.drawn.is_empty();
|
||||
let why = match (&result, drew) {
|
||||
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, names)),
|
||||
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, ai_file, names)),
|
||||
(Ok(()), true) => return Ok(finish(out)),
|
||||
(Ok(()), false) => "it draws nothing VectorCraft's PostScript reader can show".to_string(),
|
||||
(Err(e), _) => reason(e, fault.as_ref()),
|
||||
@@ -351,7 +366,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
|
||||
// A file in the legacy Illustrator format whose program can't be run (a `.ai` that names the
|
||||
// prolog defining its operators without including it): its layers, which the program has, or
|
||||
// its art on one layer when it has none (Illustrator 3).
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, names)).flatten() {
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, ai_file, names)).flatten() {
|
||||
Some(Ok((document, warnings))) => return Ok(Imported { document, warnings, preview: false }),
|
||||
Some(Err(e)) => format!("{why}; its layers couldn't be read either ({e})"),
|
||||
None => why,
|
||||
|
||||
@@ -55,8 +55,8 @@ pub use read::{
|
||||
palette, read_beside, read_in, symbols, variables,
|
||||
};
|
||||
pub(crate) use read::{
|
||||
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with,
|
||||
symbol_art_member,
|
||||
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_program, read_style_list, slot_frame, slot_of,
|
||||
style_moves_with, symbol_art_member,
|
||||
};
|
||||
|
||||
pub(crate) use read::is_look_slot;
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
//! - State: hidden `Xw`, locked `A`, transparency `mode opacity isolate knockout shape Xy`.
|
||||
//! - Names: an `/ArtDictionary` after an object, its `AI10_ArtUID` the name's XML id.
|
||||
//! - Artboards: the `ArtboardArray` of the `/Document` dictionary, with each one's display
|
||||
//! options, else `%AI3_Cropmarks`, else the art size around the template box's centre
|
||||
//! (`%AI3_TemplateBox`, `%%TemplateBox` in older files), else (a file in the legacy format
|
||||
//! another app wrote) the bounding box.
|
||||
//! options, else `%AI3_Cropmarks`, else the art size (`%AI5_ArtSize`; VectorCraft's own default,
|
||||
//! US Letter, in a `.ai` of versions 3 and 4, which don't give it) around the template box's center (`%AI3_TemplateBox`,
|
||||
//! `%%TemplateBox` in older files), which the version 7 specification (§2.2) puts at the
|
||||
//! artboard's center, else (a file in the legacy format another app wrote) the bounding box.
|
||||
//! - Document settings: Document Raster Effects Settings from the `/Document` dictionary's
|
||||
//! `AI Auto Rasterize`; the author from the header's `%%For`; swatches for the custom colors of
|
||||
//! `%%CMYKCustomColor` and `%%RGBCustomColor` that the document has none of.
|
||||
@@ -273,12 +274,28 @@ pub(crate) fn read_over_page(data: &[u8], large_canvas: bool, names: NameEncodin
|
||||
read_with(data, large_canvas, true, names)
|
||||
}
|
||||
|
||||
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
/// [`read_in`] (or [`read_over_page`] when `page_art`) for the program of a `.ai` in the legacy
|
||||
/// format: one of versions 3 and 4 that has a template box but no art size has a US Letter
|
||||
/// artboard ([`Reader::sized_board`]).
|
||||
pub(crate) fn read_program(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
let mut r = Reader::with_names(data, names);
|
||||
r.large_canvas = large_canvas.then_some(1.0);
|
||||
r.page_art = page_art;
|
||||
r.run()?;
|
||||
r.finish()
|
||||
r.legacy_ai = true;
|
||||
r.read_as(large_canvas, page_art)
|
||||
}
|
||||
|
||||
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
Reader::with_names(data, names).read_as(large_canvas, page_art)
|
||||
}
|
||||
|
||||
impl Reader<'_> {
|
||||
/// The structure read, the declared large canvas scale applied when `large_canvas`, the page's
|
||||
/// art replacing the objects not read when `page_art` ([`read_over_page`]).
|
||||
fn read_as(mut self, large_canvas: bool, page_art: bool) -> Result<Structure, String> {
|
||||
self.large_canvas = large_canvas.then_some(1.0);
|
||||
self.page_art = page_art;
|
||||
self.run()?;
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// How many objects [`read`] counts against [`MAX_NODES`] in the editing data `data`.
|
||||
@@ -645,6 +662,11 @@ struct Reader<'a> {
|
||||
/// The artboard's size and centre in files without artboards or crop marks.
|
||||
art_size: Option<(f64, f64)>,
|
||||
template_center: Option<(f64, f64)>,
|
||||
/// The data is the program of a `.ai` in the legacy format ([`read_program`]), not an EPS's:
|
||||
/// without an art size, its artboard can be US Letter ([`Reader::sized_board`]).
|
||||
legacy_ai: bool,
|
||||
/// The header has `%AI5_FileFormat`, which files of version 5 and later write.
|
||||
file_format: bool,
|
||||
/// `%AI24_LargeCanvasScale`: a Large Canvas document's editing data is written at 1/N scale
|
||||
/// (see [`Reader::ensure_space`]); 1 (no scaling) when the file doesn't declare one, `None`
|
||||
/// when the declared factor is not applied ([`read`]).
|
||||
@@ -827,6 +849,8 @@ impl<'a> Reader<'a> {
|
||||
xml_id_bytes: 0,
|
||||
art_size: None,
|
||||
template_center: None,
|
||||
legacy_ai: false,
|
||||
file_format: false,
|
||||
large_canvas: None,
|
||||
page_art: false,
|
||||
canvas_scale: 1.0,
|
||||
|
||||
@@ -242,10 +242,13 @@ impl<'a> Reader<'a> {
|
||||
&& let Ok(n) = v.trim().parse::<u8>()
|
||||
{
|
||||
self.ruler_units = Some(n);
|
||||
} else if self.frames.is_empty() && line.starts_with("AI5_FileFormat") {
|
||||
self.file_format = true;
|
||||
} else if let Some(v) = line.strip_prefix("AI5_ArtSize:") {
|
||||
self.art_size = two(v);
|
||||
} else if let Some(v) = line.strip_prefix("AI3_TemplateBox:").or_else(|| line.strip_prefix("%TemplateBox:")) {
|
||||
self.template_center = two(v);
|
||||
// The center of the box (a file without a template writes it with no width or height).
|
||||
self.template_center = four(v).map(|[x0, y0, x1, y1]| ((x0 + x1) / 2.0, (y0 + y1) / 2.0)).or_else(|| two(v));
|
||||
self.template = four(v).or(self.template);
|
||||
} else if let Some(v) = line.strip_prefix("AI24_LargeCanvasScale:") {
|
||||
self.large_canvas_scale(v);
|
||||
|
||||
@@ -871,9 +871,15 @@ impl<'a> Reader<'a> {
|
||||
// ---- the document ----------------------------------------------------------------------
|
||||
|
||||
/// The artboard of a file without artboards or crop marks: its size around the template
|
||||
/// box's centre (half a point off the grid).
|
||||
/// box's center (half a point off the grid). The program of a `.ai` of versions 3 and 4
|
||||
/// ([`Reader::legacy_ai`], no `%AI5_FileFormat`), which don't record the artboard's size, has
|
||||
/// VectorCraft's own default there, a US Letter artboard (612 × 792 pt); an EPS, or a newer
|
||||
/// file without `%AI5_ArtSize`, has none (its page is its bounding box).
|
||||
fn sized_board(&self) -> Option<Rect> {
|
||||
let ((w, h), (cx, cy)) = (self.art_size?, self.template_center?);
|
||||
let (cx, cy) = self.template_center?;
|
||||
let [_, _, letter_w, letter_h] = crate::import::LETTER;
|
||||
let letter = self.legacy_ai && !self.file_format;
|
||||
let (w, h) = self.art_size.or(letter.then_some((letter_w, letter_h)))?;
|
||||
// Rounded out, but not off a whole point the decimals in the file only just miss.
|
||||
let near = |v: f64| (v - v.round()).abs() < 1e-6;
|
||||
let (left, top) = (cx - w / 2.0, cy + h / 2.0);
|
||||
|
||||
@@ -19,6 +19,16 @@ pub(super) enum Page {
|
||||
Art,
|
||||
/// A `.ai` is read on its (first and only) artboard.
|
||||
Artboard,
|
||||
/// A `.ai` in the legacy format (PostScript, not an EPS): its page is its art's bounding box, as
|
||||
/// an EPS's is, and its artboard is the one its header gives.
|
||||
Program,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
/// Is the page the art's bounding box?
|
||||
fn of_art(self) -> bool {
|
||||
matches!(self, Page::Art | Page::Program)
|
||||
}
|
||||
}
|
||||
|
||||
/// How art moved from a page to its place in the layers' document transforms: its pattern tiles
|
||||
@@ -62,8 +72,12 @@ pub(super) fn build(
|
||||
) -> Result<(Document, Vec<String>), String> {
|
||||
// An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art
|
||||
// space, as its PDF part may (#2031).
|
||||
let large_canvas = matches!(page, Page::Art);
|
||||
let structure = if visible.is_some() { ai::read_over_page(data, large_canvas, names)? } else { ai::read_in(data, large_canvas, names)? };
|
||||
let large_canvas = page.of_art();
|
||||
let structure = match page {
|
||||
Page::Program => ai::read_program(data, large_canvas, visible.is_some(), names)?,
|
||||
_ if visible.is_some() => ai::read_over_page(data, large_canvas, names)?,
|
||||
_ => ai::read_in(data, large_canvas, names)?,
|
||||
};
|
||||
build_from(structure, data, visible, page, outlined, trust, unit)
|
||||
}
|
||||
|
||||
@@ -184,7 +198,7 @@ fn page_scale_alone(structure: &Structure, pages: &[(f64, f64)], unit: f64) -> O
|
||||
/// page, which draws nothing for them, the linked images' `placeholders`. A PDF part draws those
|
||||
/// images' pixels, which the placeholders don't match.
|
||||
fn left_out_of_comparisons(page: Page, mut restyled: Vec<Rect>, placeholders: Vec<Rect>) -> Vec<Rect> {
|
||||
if matches!(page, Page::Art) {
|
||||
if page.of_art() {
|
||||
restyled.extend(placeholders);
|
||||
}
|
||||
restyled
|
||||
@@ -295,7 +309,7 @@ pub(super) fn build_weighing(
|
||||
let mut frame = None;
|
||||
if let Some(visible) = visible {
|
||||
let art_box = match page {
|
||||
Page::Art => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
|
||||
Page::Art | Page::Program => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
|
||||
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
|
||||
};
|
||||
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
|
||||
@@ -393,8 +407,9 @@ pub(super) fn build_weighing(
|
||||
// An EPS saved as its artboard has the art outside it left out of its page.
|
||||
let artboard_page =
|
||||
artboard.is_some_and(|a| [a.x0 - art_box.x0, a.y0 - art_box.y0, a.x1 - art_box.x1, a.y1 - art_box.y1].iter().all(|d| d.abs() <= 1.0));
|
||||
let whole = matches!(page, Page::Art) && !artboard_page;
|
||||
if whole {
|
||||
let whole = page.of_art() && !artboard_page;
|
||||
// A `.ai` keeps the artboard its editing copy gives, with the art outside it.
|
||||
if whole && matches!(page, Page::Art) {
|
||||
// Its page is the file's own (its bounding box), as when it comes in as its page: art
|
||||
// reaching past the editing data's artboard stays on it (#1102).
|
||||
doc.artboards.truncate(1);
|
||||
@@ -1815,7 +1830,7 @@ fn last_boards(boards: &[vectorcraft_doc::Artboard], pages: &[Rect]) -> Option<V
|
||||
/// the one whose art draws most like the page, the first of them on a tie. Else (and for an EPS,
|
||||
/// whose page is its art) the first artboard (0).
|
||||
fn shown_artboard(doc: &Document, visible: &Document, page: Page, page_rect: Rect, fits: impl Fn(&Rect) -> bool) -> usize {
|
||||
if matches!(page, Page::Art) || visible.artboards.len() >= doc.artboards.len() {
|
||||
if page.of_art() || visible.artboards.len() >= doc.artboards.len() {
|
||||
return 0;
|
||||
}
|
||||
let pages: Vec<Rect> = visible.artboards.iter().map(|a| a.rect).collect();
|
||||
|
||||
@@ -13,12 +13,16 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect
|
||||
/// ([`use_symbols`]) and its variable library ([`use_variables`]) when it could be read, and with
|
||||
/// its layer tree, empty, below the page's layer when that could be read (#2033). A program in the
|
||||
/// legacy format without layers (`illustrator`, see [`program_alone`]) is read with its art on one
|
||||
/// layer; when that can't be used, the file is `visible` as it was. `names`: the encoding of names
|
||||
/// that aren't UTF-8.
|
||||
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, names: ai::NameEncoding) -> Imported {
|
||||
/// layer; when that can't be used, the file is `visible` as it was. The program of a `.ai`
|
||||
/// (`ai_file`) in the legacy format keeps the artboard its header gives ([`Page::Program`]).
|
||||
/// `names`: the encoding of names that aren't UTF-8.
|
||||
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, ai_file: bool, names: ai::NameEncoding) -> Imported {
|
||||
let program_page = if ai_file { Page::Program } else { Page::Art };
|
||||
let Some(data) = editing_copy(ps) else {
|
||||
let read =
|
||||
illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0, names).ok());
|
||||
let read = illustrator
|
||||
.then(|| one_layer(ps))
|
||||
.flatten()
|
||||
.and_then(|p| build(&p, Some(&visible.document), program_page, false, trust, 1.0, names).ok());
|
||||
return match read {
|
||||
Some((document, warnings)) => with_page_notes(document, warnings, &visible),
|
||||
None => visible,
|
||||
@@ -28,11 +32,17 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus
|
||||
// can't be used.
|
||||
// Private data kept apart from the program: the program's own header gives the author and
|
||||
// custom colors the private data's header may lack.
|
||||
let read =
|
||||
|d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true, names) } else { ai::read_over_page(d, true, names) };
|
||||
let private = matches!(data, Ok(Cow::Owned(_)));
|
||||
let read = |d: &[u8]| match (private, program_page) {
|
||||
(true, _) => ai::read_beside(d, true, ps, true, names),
|
||||
(false, Page::Program) => ai::read_program(d, true, true, names),
|
||||
(false, _) => ai::read_over_page(d, true, names),
|
||||
};
|
||||
// Private data is an EPS's editing copy, whatever the file is named.
|
||||
let page = if private { Page::Art } else { program_page };
|
||||
let (tree, result) = match data.as_deref() {
|
||||
Ok(d) => match read(d) {
|
||||
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), Page::Art, false, trust, 1.0)),
|
||||
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), page, false, trust, 1.0)),
|
||||
Err(why) => (vec![], Err(why)),
|
||||
},
|
||||
Err(why) => (vec![], Err(why.clone())),
|
||||
@@ -607,16 +617,23 @@ fn editing_copy(ps: &[u8]) -> Option<Result<Cow<'_, [u8]>, String>> {
|
||||
/// but whose program has no layers (Illustrator 3 had none; image editors still export their
|
||||
/// paths so, storytold/photocraft#6036) is read with its art on one layer. `None` when the program
|
||||
/// has no layers and isn't in the format: other PostScript isn't read as the format's operators.
|
||||
/// The program of a `.ai` (`ai_file`) has the artboard its header gives ([`Page::Program`]).
|
||||
/// `names`: the encoding of names that aren't UTF-8.
|
||||
pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool, names: ai::NameEncoding) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
pub(in super::super) fn program_alone(
|
||||
ps: &[u8],
|
||||
illustrator: bool,
|
||||
ai_file: bool,
|
||||
names: ai::NameEncoding,
|
||||
) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
if ai::has_eps_data(ps) {
|
||||
return None;
|
||||
}
|
||||
let page = if ai_file { Page::Program } else { Page::Art };
|
||||
if has_layers(ps) {
|
||||
return Some(build(ps, None, Page::Art, false, false, 1.0, names));
|
||||
return Some(build(ps, None, page, false, false, 1.0, names));
|
||||
}
|
||||
let program = illustrator.then(|| one_layer(ps)).flatten()?;
|
||||
Some(build(&program, None, Page::Art, false, false, 1.0, names))
|
||||
Some(build(&program, None, page, false, false, 1.0, names))
|
||||
}
|
||||
|
||||
/// `ps` with its art (from its `%%EndSetup` to its `%%PageTrailer`, else its `%%Trailer`, else its
|
||||
|
||||
@@ -9,7 +9,7 @@ use vectorcraft_doc::clipnest::MAX_NEST;
|
||||
use vectorcraft_doc::{LayerColor, LineCap, Node, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use crate::import::import;
|
||||
use crate::import::{import, import_ai};
|
||||
|
||||
/// A minimal file in Illustrator's format: its header comments, a prolog of our own defining the
|
||||
/// operators `body` uses (`u` and `U` as `ugroup` and `Ugroup`), and `body`.
|
||||
@@ -329,6 +329,100 @@ fn a_legacy_file_with_its_prolog_comes_in_through_its_layers() {
|
||||
assert_eq!(page.document.layers[0].name.as_deref(), Some("Layer 1"));
|
||||
}
|
||||
|
||||
/// A `.ai` in the legacy format (version 8, `%AI5_FileFormat 4.0`) whose prolog is included: a
|
||||
/// square on its 200-point artboard and one far to its right. Its bounding box is the box of that
|
||||
/// art; its artboard is its art size around the template box's center (version 7 specification
|
||||
/// §2.2). Its first line names the EPSF version, as the format's files may.
|
||||
const LEGACY_AI_ART_OFF_ITS_ARTBOARD: &str = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 320 30\n\
|
||||
%%HiResBoundingBox: 10 10 320 30\n%AI5_FileFormat 4.0\n%AI3_TemplateBox: 100.5 99.5 100.5 99.5\n%AI5_ArtSize: 200 200\n\
|
||||
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def\n\
|
||||
/Lb {10 {pop} repeat} def /Ln {pop} def /LB {} def /A {pop} def /k {setcmykcolor} def\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n\
|
||||
%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Art) Ln\n0 A\n0 0 0 1 k\n\
|
||||
10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n300 10 m\n320 10 L\n320 30 L\n300 30 L\nf\nLB\n%AI5_EndLayer--\n\
|
||||
%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
|
||||
|
||||
/// `bytes` opened as a `.ai`.
|
||||
fn open_ai(bytes: &[u8]) -> crate::Imported {
|
||||
import_ai(bytes, true, false, crate::NameEncoding::default()).unwrap()
|
||||
}
|
||||
|
||||
/// Such a `.ai` keeps the artboard its header gives, with the square outside it, when it comes in
|
||||
/// through its layers. The same program opened as an EPS comes in on its page, its art's box
|
||||
/// (#1102).
|
||||
#[test]
|
||||
fn a_legacy_ai_keeps_its_artboard_and_the_art_outside_it() {
|
||||
let r = open_ai(LEGACY_AI_ART_OFF_ITS_ARTBOARD.as_bytes());
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let d = &r.document;
|
||||
let boards: Vec<_> = d.artboards.iter().map(|a| a.rect).collect();
|
||||
assert_eq!(boards, [Rect::new(0.0, 0.0, 200.0, 200.0)]);
|
||||
let art: Vec<_> = d.layers[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap()).collect();
|
||||
// Art space has y up from the artboard's bottom: (10, 10) is 10 above it.
|
||||
assert_eq!(art, [Rect::new(10.0, 170.0, 30.0, 190.0), Rect::new(300.0, 170.0, 320.0, 190.0)]);
|
||||
let eps = import(LEGACY_AI_ART_OFF_ITS_ARTBOARD.as_bytes()).unwrap();
|
||||
let boards: Vec<_> = eps.document.artboards.iter().map(|a| a.rect.size()).collect();
|
||||
assert_eq!(boards, [kurbo::Size::new(310.0, 20.0)], "{:?}", eps.warnings);
|
||||
}
|
||||
|
||||
/// A version 3 `.ai` doesn't give its artboard's size (`%AI5_ArtSize` came with version 5): its
|
||||
/// artboard is VectorCraft's own default, a US Letter page, around the template box's center, the
|
||||
/// artboard's center (version 7 specification §2.2). Without a template box it is the art's box,
|
||||
/// and so is the page of the same file opened as an EPS.
|
||||
#[test]
|
||||
fn a_version_3_ai_has_a_letter_artboard_around_its_template_boxs_center() {
|
||||
let v3 = PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_TemplateBox: 100 100 100 100\n");
|
||||
let r = open_ai(v3.as_bytes());
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let d = &r.document;
|
||||
let boards: Vec<_> = d.artboards.iter().map(|a| a.rect).collect();
|
||||
assert_eq!(boards, [Rect::new(0.0, 0.0, 612.0, 792.0)]);
|
||||
// The artboard runs from -206 to 406 across and from -296 to 496 up in art space.
|
||||
let art = d.layers[0].children().unwrap();
|
||||
assert_eq!(art[1].geometric_bounds(), Some(Rect::new(226.0, 446.0, 266.0, 476.0)));
|
||||
// The center of a template box with a size (a file with a template image).
|
||||
let r = open_ai(PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_TemplateBox: 50 50 150 150\n").as_bytes());
|
||||
assert_eq!(r.document.layers[0].children().unwrap()[1].geometric_bounds(), Some(Rect::new(226.0, 446.0, 266.0, 476.0)));
|
||||
let r = open_ai(PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_ColorUsage: Color\n").as_bytes());
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
|
||||
let r = import(v3.as_bytes()).unwrap();
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
|
||||
// A newer file (`%AI5_FileFormat`, version 5 on) without an art size keeps the art's box too.
|
||||
let r = open_ai(v3.replace("%%EndComments", "%AI5_FileFormat 1.0\n%%EndComments").as_bytes());
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A version 3 `.ai` whose prolog is included and whose program has no layers: its page draws, and
|
||||
/// its art comes in on one layer around a US Letter artboard, the square far to the right outside
|
||||
/// it.
|
||||
#[test]
|
||||
fn a_version_3_ai_that_draws_keeps_its_letter_artboard_and_the_art_outside_it() {
|
||||
let v3 = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 720 30\n%AI3_TemplateBox: 100 100 100 100\n\
|
||||
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /k {setcmykcolor} def\n\
|
||||
%%EndProlog\n%%BeginSetup\n%%EndSetup\n0 0 0 1 k\n10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n\
|
||||
700 10 m\n720 10 L\n720 30 L\n700 30 L\nf\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
|
||||
let r = open_ai(v3.as_bytes());
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let boards: Vec<_> = r.document.artboards.iter().map(|a| a.rect).collect();
|
||||
assert_eq!(boards, [Rect::new(0.0, 0.0, 612.0, 792.0)]);
|
||||
assert_eq!(r.document.layers.len(), 1);
|
||||
let art: Vec<_> = r.document.layers[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap()).collect();
|
||||
// The artboard runs from -206 to 406 across and from -296 to 496 up in art space.
|
||||
assert_eq!(art, [Rect::new(216.0, 466.0, 236.0, 486.0), Rect::new(906.0, 466.0, 926.0, 486.0)]);
|
||||
let eps = import(v3.as_bytes()).unwrap();
|
||||
assert_eq!(eps.document.artboards[0].rect, Rect::new(0.0, 0.0, 710.0, 20.0), "{:?}", eps.warnings);
|
||||
}
|
||||
|
||||
/// A `.ai` another app wrote (#1027) that gives a template box but no art size opens on a US Letter
|
||||
/// artboard around the box's center, from -18 to 594 across and -12 to 780 up in art space (as an
|
||||
/// EPS it opens on its bounding box, see above).
|
||||
#[test]
|
||||
fn a_postscript_ai_with_a_template_box_and_no_art_size_opens_on_a_letter_artboard() {
|
||||
let r = open_ai(NAMED_PROLOG.as_bytes());
|
||||
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 612.0, 792.0), "{:?}", r.warnings);
|
||||
let square = &r.document.layers[0].children().unwrap()[0];
|
||||
assert_eq!(square.geometric_bounds(), Some(Rect::new(28.0, 690.0, 108.0, 770.0)));
|
||||
}
|
||||
|
||||
/// A placed EPS in the legacy format (version 7 specification §16, no `%AI5_BeginPlace`), its file
|
||||
/// included whole: the layers come in, the placed file as the art the page draws in its box.
|
||||
#[test]
|
||||
|
||||
@@ -4791,6 +4791,9 @@ fn an_eps_page_of_its_art_stays_its_page_through_the_layers() {
|
||||
assert_eq!(boards.len(), 1);
|
||||
assert!((boards[0].width() - 4.0).abs() < 1e-6 && (boards[0].height() - 4.0).abs() < 1e-6, "{boards:?}");
|
||||
assert!(boards[0].inflate(1e-6, 1e-6).contains_rect(art), "{boards:?} round {art:?}");
|
||||
// Its private data is an EPS's editing copy, whatever the file is named.
|
||||
let named_ai = crate::import::import_ai(&file, true, false, crate::NameEncoding::default()).unwrap();
|
||||
assert_eq!(named_ai.document.artboards.iter().map(|a| a.rect).collect::<Vec<_>>(), boards, "{:?}", named_ai.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -49,7 +49,7 @@ mod tiff;
|
||||
pub use ai_editing::{AiEditingOptions, AiEditingOutput, CREATOR_VERSION, export_ai_editing, private_data as ai_private_data};
|
||||
pub use import::{
|
||||
AiLayers, Imported, NameEncoding, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard,
|
||||
family_style, import, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
|
||||
family_style, import, import_ai, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
|
||||
};
|
||||
pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai, writes_image_mask};
|
||||
pub use print::{PrintJob, PrintPage, print};
|
||||
|
||||
@@ -758,7 +758,15 @@ struct Ctx {
|
||||
/// The path each `<textPath>` read so far follows ([`Self::text_path`]); `None` where it has
|
||||
/// no usable one. The styles of a `<use>` instance share it, so each path is found and parsed
|
||||
/// once however many `<use>` elements show its text.
|
||||
paths: Rc<RefCell<HashMap<roxmltree::NodeId, Option<Rc<BezPath>>>>>,
|
||||
paths: Rc<RefCell<HashMap<roxmltree::NodeId, Option<Rc<FollowedPath>>>>>,
|
||||
}
|
||||
|
||||
/// The path a `<textPath>` follows ([`Ctx::text_path`]).
|
||||
struct FollowedPath {
|
||||
path: BezPath,
|
||||
/// The `pathLength` of the element the `<textPath>` points at, 0 or greater; `None` when the
|
||||
/// `<textPath>` has its own `path` or the element has no such value.
|
||||
author_length: Option<f64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1172,7 +1180,7 @@ impl Ctx {
|
||||
|
||||
/// The path a `<textPath>` follows, in the text's user space. It is read from the file once
|
||||
/// ([`Self::paths`]).
|
||||
fn text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<Rc<BezPath>> {
|
||||
fn text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<Rc<FollowedPath>> {
|
||||
if let Some(known) = self.paths.borrow().get(&tp.id()) {
|
||||
return known.clone();
|
||||
}
|
||||
@@ -1184,13 +1192,14 @@ impl Ctx {
|
||||
/// How many elements the path of `<textPath>` `tp` has, when a text built so far follows it
|
||||
/// ([`Self::text_path`]); 0 for any other node.
|
||||
fn path_elements(&self, tp: XNode) -> usize {
|
||||
self.paths.borrow().get(&tp.id()).and_then(|p| p.as_deref()).map_or(0, |p| p.elements().len())
|
||||
self.paths.borrow().get(&tp.id()).and_then(|p| p.as_deref()).map_or(0, |p| p.path.elements().len())
|
||||
}
|
||||
|
||||
/// Find and parse the path `<textPath>` `tp` follows.
|
||||
fn read_text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<BezPath> {
|
||||
fn read_text_path(&self, xml: &roxmltree::Document, tp: XNode) -> Option<FollowedPath> {
|
||||
#[cfg(test)]
|
||||
TEXT_PATH_READS.set(TEXT_PATH_READS.get() + 1);
|
||||
let mut author_length = None;
|
||||
let mut bp = match tp.attribute("path") {
|
||||
Some(d) => BezPath::from_svg(d).ok()?,
|
||||
None => {
|
||||
@@ -1202,6 +1211,8 @@ impl Ctx {
|
||||
};
|
||||
let mut bp = BezPath::from_svg(&d).ok()?;
|
||||
bp.apply_affine(parse_transform(el.attribute("transform")));
|
||||
author_length =
|
||||
el.attribute("pathLength").and_then(|v| v.trim().parse::<f64>().ok()).filter(|l| l.is_finite() && *l >= 0.0).map(f64::abs);
|
||||
bp
|
||||
}
|
||||
};
|
||||
@@ -1209,7 +1220,7 @@ impl Ctx {
|
||||
bp = bp.reverse_subpaths();
|
||||
}
|
||||
let usable = bp.segments().next().is_some();
|
||||
usable.then_some(bp)
|
||||
usable.then_some(FollowedPath { path: bp, author_length })
|
||||
}
|
||||
|
||||
/// The equivalent path of a basic shape (SVG 2 › Basic Shapes), as path data: what a
|
||||
@@ -1458,7 +1469,8 @@ impl Ctx {
|
||||
let mut obj = TextObject::point(Point::ZERO, "", CharStyle::default());
|
||||
let mut breaks: Vec<Break> = vec![];
|
||||
let mut para_styles: Vec<ParaStyle> = vec![];
|
||||
if let Some((tp, bp)) = path {
|
||||
if let Some((tp, followed)) = path {
|
||||
let bp = &followed.path;
|
||||
// Along the path: dx is extra advance, dy shifts off the path.
|
||||
let mut off = 0.0;
|
||||
for i in 0..n {
|
||||
@@ -1470,13 +1482,34 @@ impl Ctx {
|
||||
}
|
||||
let len: f64 = bp.segments().map(|s| s.arclen(1e-4)).sum();
|
||||
let em = self.font_size(tp);
|
||||
let offset = tp.attribute("startOffset").and_then(|v| self.length(v, em, len)).unwrap_or(0.0) + dx[0];
|
||||
// `startOffset` as a share of the path's length. A percentage is of the path's computed
|
||||
// length. Any other length is in the units of the referenced element's `pathLength`
|
||||
// when it has one, and a `pathLength` of 0 puts any offset other than 0 infinitely far
|
||||
// along (SVG 2 › Paths › The pathLength attribute). An offset infinitely far along
|
||||
// starts as far past the path's end, or before its start, as type may go.
|
||||
let share = tp
|
||||
.attribute("startOffset")
|
||||
.and_then(|v| {
|
||||
let o = self.length(v, em, len)?;
|
||||
let share = match followed.author_length {
|
||||
Some(p) if !v.trim().ends_with('%') => {
|
||||
if o == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
o / p
|
||||
}
|
||||
}
|
||||
_ => o / len,
|
||||
};
|
||||
Some(if share.is_infinite() { vectorcraft_doc::PATH_LEAD_MAX.copysign(share) } else { share })
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
let width = if anchor > 0.0 { advance(&point_text(&cells)) } else { 0.0 };
|
||||
// On a path of one closed subpath, text that starts before the path's start or past its
|
||||
// end wraps around the path. On an open path text may start before the path's start,
|
||||
// and its characters before the start are not drawn; text that starts past the end
|
||||
// starts at the end. Other paths start it at the path's start or end.
|
||||
let path = PathData::from_bezpath(&bp);
|
||||
let path = PathData::from_bezpath(bp);
|
||||
let wraps = path.subpaths.len() == 1 && path.is_closed();
|
||||
let open = !path.is_closed();
|
||||
let wrap = |s: f64| {
|
||||
@@ -1484,7 +1517,7 @@ impl Ctx {
|
||||
let s = s.rem_euclid(1.0);
|
||||
if s < 1.0 { s } else { 0.0 }
|
||||
};
|
||||
let start = Some((offset - anchor * width) / len).filter(|s| s.is_finite()).map_or(0.0, |s| {
|
||||
let start = Some(share + (dx[0] - anchor * width) / len).filter(|s| s.is_finite()).map_or(0.0, |s| {
|
||||
if wraps {
|
||||
wrap(s)
|
||||
} else if open {
|
||||
|
||||
@@ -21,4 +21,9 @@ fn uses_of_a_text_on_a_path_read_the_path_once() {
|
||||
// A path with no usable data is not read again either.
|
||||
let (texts, reads, _) = prepared(&format!(r##"<defs><text id="t"><textPath href="#none">Path</textPath></text></defs>{uses}"##));
|
||||
assert_eq!((texts, reads), (51, 1));
|
||||
// The path's pathLength is read with it.
|
||||
let (texts, reads, _) = prepared(&format!(
|
||||
r##"<defs><path id="p" d="M0 20L150 20" pathLength="10"/><text id="t"><textPath href="#p" startOffset="5">Path</textPath></text></defs>{uses}"##
|
||||
));
|
||||
assert_eq!((texts, reads), (51, 1));
|
||||
}
|
||||
|
||||
@@ -299,6 +299,38 @@ fn text_path_becomes_type_on_a_path() {
|
||||
assert_eq!(t[2].para.justify, Justify::Left);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_path_start_offset_is_scaled_by_the_path_length() {
|
||||
// A path 400 long whose pathLength is 100: a startOffset of 50 lies 200 along it. A
|
||||
// percentage stays a share of the computed length. Huge values of both still give their
|
||||
// ratio. With a pathLength of 0, an offset of 0 stays at the start and any other offset lies
|
||||
// past the end, as does an offset too large to scale on a short path.
|
||||
let d = open(
|
||||
r##"<path id="p" d="M0 100 H400" pathLength="100" fill="none" stroke="black"/>
|
||||
<path id="z" d="M0 200 H400" pathLength="0" fill="none" stroke="black"/>
|
||||
<path id="h" d="M0 300 H400" pathLength="1e308" fill="none" stroke="black"/>
|
||||
<path id="s" d="M0 400 H0.1" pathLength="1e-10" fill="none" stroke="black"/>
|
||||
<text font-size="10"><textPath href="#p" startOffset="50">Half</textPath></text>
|
||||
<text font-size="10"><textPath href="#p" startOffset="25%">Quarter</textPath></text>
|
||||
<text font-size="10"><textPath href="#h" startOffset="5e307">Huge</textPath></text>
|
||||
<text font-size="10"><textPath href="#z" startOffset="0">Zero</textPath></text>
|
||||
<text font-size="10"><textPath href="#z" startOffset="50">Fifty</textPath></text>
|
||||
<text font-size="10"><textPath href="#s" startOffset="1e308">Past</textPath></text>"##,
|
||||
);
|
||||
let t = texts(&d);
|
||||
assert_eq!(t.len(), 6);
|
||||
let start_of = |t: &TextObject| match t.kind {
|
||||
TextKind::OnPath { start, .. } => start,
|
||||
_ => panic!("{:?}", t.kind),
|
||||
};
|
||||
assert!(close(start_of(t[0]), 0.5, 1e-9), "{}", start_of(t[0]));
|
||||
assert!(close(start_of(t[1]), 0.25, 1e-9), "{}", start_of(t[1]));
|
||||
assert!(close(start_of(t[2]), 0.5, 1e-9), "{}", start_of(t[2]));
|
||||
assert_eq!(start_of(t[3]), 0.0);
|
||||
assert_eq!(start_of(t[4]), 1.0);
|
||||
assert_eq!(start_of(t[5]), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_on_a_closed_path_wraps_past_the_path_start() {
|
||||
// A closed path 400 long: text anchored before its start, or offset past its end, wraps
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::cache::RegionCache;
|
||||
use super::lookup::{
|
||||
connected_edges, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_at_where, lp_stroke_paint, node_outline, visible_edge_near,
|
||||
visible_face_at,
|
||||
connected_edges, edges, faces, flood_faces, is_live_paint, leaf_shapes, live_paint_at, live_paint_at_where, lp_stroke_paint, node_outline,
|
||||
visible_edge_near, visible_face_at,
|
||||
};
|
||||
use super::map::{HIGHLIGHT_RED, sorted_roots};
|
||||
use super::{
|
||||
@@ -711,8 +711,14 @@ pub const LP_SELECT_WIDTH_RANGE: (f64, f64) = (1.0, 20.0);
|
||||
/// with Shift held at the press, each of them leaves the selection if it is selected and joins it
|
||||
/// otherwise. A double-click on a face selects it and the faces of its group that are not parted
|
||||
/// from it by a stroked edge; on an edge, it selects the edge and the edges joined to it end to end
|
||||
/// that have its stroke paint ([`LivePaintSelectionTool::reach_at`]). With Shift held, each of them
|
||||
/// leaves the selection if it is selected and joins it otherwise.
|
||||
/// that have its stroke paint ([`LivePaintSelectionTool::reach_at`]). A triple-click on a face
|
||||
/// selects every face of its group with that face's fill; on an edge, every edge of its group with
|
||||
/// that edge's stroke paint ([`LivePaintSelectionTool::same_paint_at`]). With Shift held, each part a
|
||||
/// double-click reaches leaves the selection if it is selected and joins it otherwise, and after a
|
||||
/// Shift-triple-click each part with the clicked part's paint is toggled from the selection there
|
||||
/// was before the three clicks. Clicks of the three made without Shift select as they do alone, so
|
||||
/// when Shift is first held at a later click, the parts are toggled from the selection there was
|
||||
/// before that click.
|
||||
/// A click and a marquee change the selection on release, so Escape between the press
|
||||
/// and the release (which removes the marquee) leaves the selection as it was. The tool does not
|
||||
/// highlight or select the faces and edges of a Live Paint group that is locked, on a locked layer
|
||||
@@ -733,6 +739,33 @@ pub struct LivePaintSelectionTool {
|
||||
width: f64,
|
||||
/// The drag in progress, from the press to the release.
|
||||
drag: Option<LpDrag>,
|
||||
/// The part each of the last two clicks toggled with Shift held, the latest last (None for a
|
||||
/// click that toggled nothing). A click or marquee without Shift clears both.
|
||||
recent: [Option<NodeId>; 2],
|
||||
/// The parts the last double-click, the two clicks before it and the Shift-clicks after it
|
||||
/// toggled, each listed once when toggled an odd number of times. A Shift-triple-click toggles
|
||||
/// them back as it toggles the parts with the clicked part's paint, so the selection ends as it
|
||||
/// was before the clicks with those parts toggled.
|
||||
toggled: Vec<NodeId>,
|
||||
}
|
||||
|
||||
/// `a` (which lists each id once) with each id of `b` toggled in turn: an id `b` lists an odd number
|
||||
/// of times is removed when `a` holds it and added at the end otherwise, in `b`'s order.
|
||||
fn toggle_ids(a: &mut Vec<NodeId>, b: &[NodeId]) {
|
||||
use std::collections::HashSet;
|
||||
let mut odd = HashSet::new();
|
||||
for id in b {
|
||||
if !odd.insert(*id) {
|
||||
odd.remove(id);
|
||||
}
|
||||
}
|
||||
let held: HashSet<NodeId> = a.iter().copied().collect();
|
||||
a.retain(|x| !odd.contains(x));
|
||||
for id in b {
|
||||
if !held.contains(id) && odd.remove(id) {
|
||||
a.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A press and drag of the Live Paint Selection tool.
|
||||
@@ -770,6 +803,8 @@ impl Default for LivePaintSelectionTool {
|
||||
color: HIGHLIGHT_RED,
|
||||
width: LP_SELECT_WIDTH,
|
||||
drag: None,
|
||||
recent: [None; 2],
|
||||
toggled: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,6 +847,25 @@ impl LivePaintSelectionTool {
|
||||
let Some(group) = cx.doc.node(gid) else { return vec![] };
|
||||
if edge { connected_edges(group, part.id, &lp_stroke_paint(part)) } else { flood_faces(group, part.id) }
|
||||
}
|
||||
|
||||
/// What a triple-click at `p` selects, Alt held or not: on the face a click there picks
|
||||
/// ([`LivePaintSelectionTool::picks_edge`]), that face and every other face of its group that is
|
||||
/// not hidden and has its fill; on the edge a click picks, that edge and every other edge of its
|
||||
/// group that is not hidden and draws with the stroke paint it draws with ([`lp_stroke_paint`]).
|
||||
/// The part under the pointer comes first. Empty where a click selects no face or edge.
|
||||
fn same_paint_at(&self, cx: &ToolContext, p: Point, alt: bool) -> Vec<NodeId> {
|
||||
let Some(edge) = self.picks_edge(alt) else { return vec![] };
|
||||
let Some((gid, Some(part))) = lp_part_at(cx.doc, p, cx.tol(4.0), edge, &|id| lp_selectable(cx, id)) else { return vec![] };
|
||||
let Some(group) = cx.doc.node(gid) else { return vec![] };
|
||||
let others: Vec<NodeId> = if edge {
|
||||
let like = lp_stroke_paint(part);
|
||||
edges(group).into_iter().filter(|e| e.visible && e.id != part.id && lp_stroke_paint(e) == like).map(|e| e.id).collect()
|
||||
} else {
|
||||
let like = part.appearance.fill_paint();
|
||||
faces(group).into_iter().filter(|f| f.visible && f.id != part.id && f.appearance.fill_paint() == like).map(|f| f.id).collect()
|
||||
};
|
||||
std::iter::once(part.id).chain(others).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The faces (with `faces`) and edges (with `edges`) that marquee `r` touches, among the faces and
|
||||
@@ -918,6 +972,17 @@ impl Tool for LivePaintSelectionTool {
|
||||
let d = LpDrag { cur: p, ..d };
|
||||
let Some(r) = d.marquee(cx) else {
|
||||
// A click selects what the press picked.
|
||||
// A click without Shift replaces the selection, so what earlier clicks toggled
|
||||
// no longer counts.
|
||||
if d.shift {
|
||||
self.recent = [self.recent[1], d.hit];
|
||||
if let Some(id) = d.hit {
|
||||
toggle_ids(&mut self.toggled, &[id]);
|
||||
}
|
||||
} else {
|
||||
self.recent = [None; 2];
|
||||
self.toggled.clear();
|
||||
}
|
||||
return match (d.hit, d.shift) {
|
||||
(Some(id), true) => vec![Action::Exec("select.toggle".into(), json!({ "id": id.0 }))],
|
||||
(Some(id), false) => vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))],
|
||||
@@ -925,6 +990,8 @@ impl Tool for LivePaintSelectionTool {
|
||||
(None, false) => vec![Action::Exec("select.none".into(), json!({}))],
|
||||
};
|
||||
};
|
||||
self.recent = [None; 2];
|
||||
self.toggled.clear();
|
||||
let ids = lp_parts_in(cx, r, self.fills, self.strokes);
|
||||
match (ids.is_empty(), d.shift) {
|
||||
(true, true) => vec![],
|
||||
@@ -937,15 +1004,36 @@ impl Tool for LivePaintSelectionTool {
|
||||
// The two clicks before it already selected the part under the pointer (or, with
|
||||
// Shift, toggled it twice); the double-click selects the part with what it reaches.
|
||||
let ids = self.reach_at(cx, p, ev.mods.alt);
|
||||
self.toggled.clear();
|
||||
if ev.mods.shift {
|
||||
let clicks: Vec<NodeId> = self.recent.iter().flatten().copied().collect();
|
||||
toggle_ids(&mut self.toggled, &clicks);
|
||||
toggle_ids(&mut self.toggled, &ids);
|
||||
}
|
||||
match (ids.is_empty(), ev.mods.shift) {
|
||||
(true, _) => vec![],
|
||||
(false, true) => vec![Action::Exec("select.toggle".into(), json!({ "ids": json_ids(&ids) }))],
|
||||
(false, false) => vec![Action::Exec("select.set".into(), json!({ "ids": json_ids(&ids) }))],
|
||||
}
|
||||
}
|
||||
// The tool takes no action on a triple-click; its third press and release select as a
|
||||
// click does.
|
||||
PointerKind::TripleClick => vec![],
|
||||
PointerKind::TripleClick => {
|
||||
// The clicks before it already selected the part under the pointer; the
|
||||
// triple-click selects the part with every part of its group that has its paint.
|
||||
// With Shift held, the double-click and the third click toggled parts too, and the
|
||||
// triple-click toggles those back.
|
||||
let ids = self.same_paint_at(cx, p, ev.mods.alt);
|
||||
let toggled = std::mem::take(&mut self.toggled);
|
||||
self.recent = [None; 2];
|
||||
if ids.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
if !ev.mods.shift {
|
||||
return vec![Action::Exec("select.set".into(), json!({ "ids": json_ids(&ids) }))];
|
||||
}
|
||||
let mut flip = ids;
|
||||
toggle_ids(&mut flip, &toggled);
|
||||
if flip.is_empty() { vec![] } else { vec![Action::Exec("select.toggle".into(), json!({ "ids": json_ids(&flip) }))] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,6 +1051,8 @@ impl Tool for LivePaintSelectionTool {
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.drag = None;
|
||||
self.recent = [None; 2];
|
||||
self.toggled.clear();
|
||||
vec![]
|
||||
}
|
||||
|
||||
|
||||
@@ -2573,20 +2573,167 @@ fn live_paint_selection_double_click_selects_connected_faces_or_edges() {
|
||||
assert!(dbl(&mut t, 25.0, 1.0, alt).is_empty());
|
||||
}
|
||||
|
||||
/// A triple-click on a face selects every face of its group with that face's fill, joined to it or
|
||||
/// not; on an edge (Alt), every edge of its group that draws with that edge's stroke paint. Shift
|
||||
/// toggles them. Hidden parts, other groups and parts with another paint are left out.
|
||||
#[test]
|
||||
fn live_paint_selection_triple_click_adds_nothing_to_its_clicks() {
|
||||
fn live_paint_selection_triple_click_selects_every_part_of_the_same_paint() {
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let (_, [a, ..], _) = live_strip(&mut d);
|
||||
let (_, [a, b, c], [ab, bc, top_a, top_b, top_c, bottom]) = live_strip(&mut d);
|
||||
let (other, [_, other_top, ..]) = live_square(&mut d, Rect::new(200.0, 0.0, 300.0, 100.0));
|
||||
let other_face = faces(d.node(other).unwrap())[0].id;
|
||||
let red = Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0));
|
||||
let blue = Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0));
|
||||
// A and C are red and B is blue; the other group's face is red and its top edge black, too.
|
||||
for (id, fill) in [(a, &red), (b, &blue), (c, &red), (other_face, &red)] {
|
||||
d.node_mut(id).unwrap().appearance.set_fill(fill.clone());
|
||||
}
|
||||
d.node_mut(other_top).unwrap().appearance.set_stroke(Paint::solid(vectorcraft_color::Color::BLACK));
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let set = |ids: &[NodeId]| vec![Action::Exec("select.set".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))];
|
||||
let toggle = |ids: &[NodeId]| vec![Action::Exec("select.toggle".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))];
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let tri = |d: &Document, t: &mut LivePaintSelectionTool, x: f64, y: f64, m: Mods| {
|
||||
let cx = cx(d, &s, &p);
|
||||
t.pointer(&cx, &ev(PointerKind::TripleClick, x, y).with_mods(m))
|
||||
};
|
||||
let mut t = LivePaintSelectionTool::default();
|
||||
// A face: every face of its group with its fill, even across the stroked edge BC; the clicked face first.
|
||||
assert_eq!(tri(&d, &mut t, 125.0, 25.0, Mods::default()), set(&[c, a]));
|
||||
assert_eq!(tri(&d, &mut t, 75.0, 25.0, Mods::default()), set(&[b]));
|
||||
// An edge: every edge of its group that draws with its stroke paint, joined to it or not.
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 1.0, alt), set(&[top_a, top_b, bottom]));
|
||||
assert_eq!(tri(&d, &mut t, 125.0, 1.0, alt), set(&[top_c, bc]));
|
||||
assert_eq!(tri(&d, &mut t, 50.0, 25.0, alt), set(&[ab]));
|
||||
// With Shift held, the parts leave the selection or join it.
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 25.0, shift), toggle(&[a, c]));
|
||||
assert_eq!(tri(&d, &mut t, 125.0, 1.0, Mods { alt: true, shift: true, ..Default::default() }), toggle(&[top_c, bc]));
|
||||
// Away from the group, a triple-click adds nothing to what its clicks did.
|
||||
assert!(tri(&d, &mut t, 400.0, 400.0, Mods::default()).is_empty());
|
||||
// A hidden face of the same fill is left out.
|
||||
d.node_mut(c).unwrap().visible = false;
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 25.0, Mods::default()), set(&[a]));
|
||||
// On the other group, a triple-click selects that group's parts alone.
|
||||
assert_eq!(tri(&d, &mut t, 250.0, 50.0, Mods::default()), set(&[other_face]));
|
||||
assert_eq!(tri(&d, &mut t, 250.0, 1.0, alt), set(&[other_top]));
|
||||
// Select Fills off: the triple-click picks the edge nearest the pointer; both off: nothing.
|
||||
t.set_option("selectFills", &json!(false));
|
||||
assert_eq!(tri(&d, &mut t, 25.0, 1.0, Mods::default()), set(&[top_a, top_b, bottom]));
|
||||
t.set_option("selectStrokes", &json!(false));
|
||||
assert!(tri(&d, &mut t, 25.0, 25.0, Mods::default()).is_empty());
|
||||
assert!(tri(&d, &mut t, 25.0, 1.0, alt).is_empty());
|
||||
}
|
||||
|
||||
/// The whole event series of a triple-click (three presses and releases, the double-click after
|
||||
/// the second release and the triple-click after the third), applied to a selection: without
|
||||
/// Shift the selection ends as the parts with the clicked part's paint, and with Shift as the
|
||||
/// selection from before the clicks with those parts toggled, whatever the double-click reached.
|
||||
#[test]
|
||||
fn live_paint_selection_triple_click_series_ends_with_the_same_paint_parts() {
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let (_, [a, b, c], [_, bc, top_a, top_b, top_c, bottom]) = live_strip(&mut d);
|
||||
let red = Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0));
|
||||
let blue = Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0));
|
||||
// A and C are red and B is blue; a double-click on A also reaches B across the unstroked edge AB.
|
||||
for (id, fill) in [(a, &red), (b, &blue), (c, &red)] {
|
||||
d.node_mut(id).unwrap().appearance.set_fill(fill.clone());
|
||||
}
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let apply = |sel: &mut Vec<u64>, acts: Vec<Action>| {
|
||||
for act in acts {
|
||||
let Action::Exec(cmd, args) = act else { panic!("{act:?}") };
|
||||
let ids: Vec<u64> = match (args.get("ids"), args.get("id")) {
|
||||
(Some(v), _) => v.as_array().unwrap().iter().map(|i| i.as_u64().unwrap()).collect(),
|
||||
(None, Some(i)) => vec![i.as_u64().unwrap()],
|
||||
(None, None) => vec![],
|
||||
};
|
||||
match cmd.as_str() {
|
||||
"select.set" => *sel = ids,
|
||||
"select.none" => sel.clear(),
|
||||
"select.toggle" => {
|
||||
for i in ids {
|
||||
match sel.iter().position(|x| *x == i) {
|
||||
Some(k) => {
|
||||
sel.remove(k);
|
||||
}
|
||||
None => sel.push(i),
|
||||
}
|
||||
}
|
||||
}
|
||||
other => panic!("{other}"),
|
||||
}
|
||||
}
|
||||
};
|
||||
// The first click at `first`, every later event at (`x`, `y`).
|
||||
let series = |t: &mut LivePaintSelectionTool, sel: &mut Vec<u64>, first: (f64, f64), x: f64, y: f64, m: Mods| {
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
apply(sel, t.pointer(&cx, &ev(kind, first.0, first.1).with_mods(m)));
|
||||
}
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick, PointerKind::Down, PointerKind::Up, PointerKind::TripleClick] {
|
||||
apply(sel, t.pointer(&cx, &ev(kind, x, y).with_mods(m)));
|
||||
}
|
||||
};
|
||||
let sorted = |ids: &[NodeId]| {
|
||||
let mut v: Vec<u64> = ids.iter().map(|i| i.0).collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
};
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let mut t = LivePaintSelectionTool::default();
|
||||
// The third click's press and release select the face under the pointer, as a click does.
|
||||
assert_eq!(lp_click(&mut t, &cx, ev(PointerKind::Down, 25.0, 25.0)), vec![Action::Exec("select.set".into(), json!({ "ids": [a.0] }))]);
|
||||
for m in [Mods::default(), Mods { alt: true, ..Default::default() }, Mods { shift: true, ..Default::default() }] {
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::TripleClick, 25.0, 25.0).with_mods(m)).is_empty());
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::TripleClick, 25.0, 1.0).with_mods(m)).is_empty());
|
||||
for (start, first, x, y, m, want) in [
|
||||
(vec![], (25.0, 25.0), 25.0, 25.0, Mods::default(), sorted(&[a, c])),
|
||||
(vec![top_c.0], (25.0, 25.0), 25.0, 25.0, Mods::default(), sorted(&[a, c])),
|
||||
(vec![], (25.0, 25.0), 25.0, 25.0, shift, sorted(&[a, c])),
|
||||
(vec![top_c.0, c.0], (25.0, 25.0), 25.0, 25.0, shift, sorted(&[a, top_c])),
|
||||
(vec![], (25.0, 1.0), 25.0, 1.0, Mods { alt: true, shift: true, ..Default::default() }, sorted(&[top_a, top_b, bottom])),
|
||||
(vec![bc.0], (125.0, 1.0), 125.0, 1.0, alt, sorted(&[top_c, bc])),
|
||||
// The first click on A and the rest on B, beside it: B alone has B's fill.
|
||||
(vec![], (49.0, 25.0), 51.0, 25.0, shift, sorted(&[b])),
|
||||
(vec![c.0], (49.0, 25.0), 51.0, 25.0, shift, sorted(&[b, c])),
|
||||
] {
|
||||
let mut sel = start.clone();
|
||||
series(&mut t, &mut sel, first, x, y, m);
|
||||
sel.sort_unstable();
|
||||
assert_eq!(sel, want, "from {start:?} at {first:?} then ({x}, {y}) with {m:?}");
|
||||
}
|
||||
// Shift first held at the third click: the two clicks and the double-click select A and B, and
|
||||
// the parts with A's fill are toggled from that selection.
|
||||
let mut sel = vec![top_c.0];
|
||||
for (kind, m) in [
|
||||
(PointerKind::Down, Mods::default()),
|
||||
(PointerKind::Up, Mods::default()),
|
||||
(PointerKind::Down, Mods::default()),
|
||||
(PointerKind::Up, Mods::default()),
|
||||
(PointerKind::DoubleClick, Mods::default()),
|
||||
(PointerKind::Down, shift),
|
||||
(PointerKind::Up, shift),
|
||||
(PointerKind::TripleClick, shift),
|
||||
] {
|
||||
apply(&mut sel, t.pointer(&cx, &ev(kind, 25.0, 25.0).with_mods(m)));
|
||||
}
|
||||
sel.sort_unstable();
|
||||
assert_eq!(sel, sorted(&[b, c]));
|
||||
// A Shift-click on C, then a click on A without Shift, which selects A alone, and Shift held
|
||||
// from the double-click on: the parts with A's fill are toggled from that selection.
|
||||
let mut sel = vec![];
|
||||
for (kind, x, m) in [
|
||||
(PointerKind::Down, 125.0, shift),
|
||||
(PointerKind::Up, 125.0, shift),
|
||||
(PointerKind::Down, 25.0, Mods::default()),
|
||||
(PointerKind::Up, 25.0, Mods::default()),
|
||||
(PointerKind::DoubleClick, 25.0, shift),
|
||||
(PointerKind::Down, 25.0, shift),
|
||||
(PointerKind::Up, 25.0, shift),
|
||||
(PointerKind::TripleClick, 25.0, shift),
|
||||
] {
|
||||
apply(&mut sel, t.pointer(&cx, &ev(kind, x, 25.0).with_mods(m)));
|
||||
}
|
||||
assert_eq!(sel, vec![c.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -188,6 +188,8 @@ fn snapped(d: Vec2, unsnapped: Vec2, shift: bool, guides: &[Overlay]) -> bool {
|
||||
pub struct DirectSelectionTool {
|
||||
group: bool,
|
||||
state: State,
|
||||
/// How far the move's last preview carried the anchors or the objects ([`Tool::readout`]).
|
||||
moved: Option<Vec2>,
|
||||
/// The spine point last clicked (blend, anchor): its handles show and can be dragged.
|
||||
spine: Option<(NodeId, usize)>,
|
||||
/// Other points of that spine picked with it (Shift-click adds or takes one away); dragging
|
||||
@@ -251,6 +253,7 @@ impl DirectSelectionTool {
|
||||
Self {
|
||||
group,
|
||||
state: State::Idle,
|
||||
moved: None,
|
||||
spine: None,
|
||||
spine_more: vec![],
|
||||
mesh: MeshEdit::default(),
|
||||
@@ -733,6 +736,10 @@ impl Tool for DirectSelectionTool {
|
||||
fn busy(&self) -> bool {
|
||||
!matches!(self.state, State::Idle) || self.guide.busy()
|
||||
}
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<crate::Readout> {
|
||||
let moving = matches!(self.state, State::MoveAnchors { began: true, .. } | State::MoveObject { began: true, .. });
|
||||
self.moved.filter(|_| moving).map(crate::Readout::drag)
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
self.mesh.options()
|
||||
}
|
||||
@@ -1486,6 +1493,7 @@ impl DirectSelectionTool {
|
||||
(d, self.guides) = (q - grab, guides);
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y})));
|
||||
out
|
||||
}
|
||||
@@ -1538,6 +1546,7 @@ impl DirectSelectionTool {
|
||||
(d, self.guides) = (q - held, guides);
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y})));
|
||||
return out;
|
||||
}
|
||||
@@ -1553,6 +1562,7 @@ impl DirectSelectionTool {
|
||||
(d, self.guides) = if ev.mods.shift { snap.snap_along(cx, hold, d, ev.mods.alt) } else { snap.snap(cx, hold, d, ev.mods.alt) };
|
||||
}
|
||||
self.snapped = snapped(d, unsnapped, ev.mods.shift, &self.guides);
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview(
|
||||
"object.transform".into(),
|
||||
json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(d)), "copy": ev.mods.alt}),
|
||||
|
||||
@@ -378,9 +378,11 @@ impl PerspectiveGrid {
|
||||
if (self.origin[1] - self.horizon).abs() < 1e-6 {
|
||||
return Err("ground level must differ from the horizon".into());
|
||||
}
|
||||
// A moved plane stays in front of the viewer.
|
||||
let behind = |p: Plane| self.axes(p).is_some_and(|[.., n]| 1.0 + self.offset(p) * self.axis_col(n)[2] <= 1e-9);
|
||||
if Plane::ALL.iter().any(|p| !self.offset(*p).is_finite() || self.offset(*p).abs() > MAX_DEPTH || behind(*p)) {
|
||||
// A moved plane stays in front of the viewer, both where Automatic Plane Positioning holds
|
||||
// it and in its own place, which it goes back to.
|
||||
let behind = |p: Plane, d: f64| self.axes(p).is_some_and(|[.., n]| 1.0 + d * self.axis_col(n)[2] <= 1e-9);
|
||||
let bad = |p: Plane, d: f64| !d.is_finite() || d.abs() > MAX_DEPTH || behind(p, d);
|
||||
if Plane::ALL.iter().any(|p| bad(*p, self.offset(*p)) || bad(*p, self.placed_offset(*p))) {
|
||||
return Err("a plane is moved too far (behind the viewer)".into());
|
||||
}
|
||||
self.validate_definition()
|
||||
|
||||
@@ -131,17 +131,32 @@ impl PerspectiveGrid {
|
||||
for plane in self.shown_planes() {
|
||||
let depth = self.offset(plane);
|
||||
let Some(h) = self.homography_at(plane, depth) else { continue };
|
||||
let Some(q) = h.inverse().and_then(|hi| hi.apply(p)) else { continue };
|
||||
let Some(hi) = h.inverse() else { continue };
|
||||
let Some(((su, nu), (sv, nv))) = self.line_steps(plane) else { continue };
|
||||
// The drawn lines nearest the pointer along each axis, and their neighbors: the
|
||||
// projection skews and shrinks the cells, so the intersection nearest on the page can be
|
||||
// one over from the nearest in the plane.
|
||||
let near = |v: f64, step: f64, last: usize| {
|
||||
let i = (v / step).round().clamp(0.0, last as f64) as i64;
|
||||
(i - 2..=i + 2).filter(move |k| (0..=last as i64).contains(k)).map(move |k| k as f64 * step)
|
||||
// On each drawn line, the intersections either side of the point of the line nearest
|
||||
// the pointer on the page. The projection keeps the order of points along a line, so
|
||||
// the nearest intersection on that line is one of those two, however skewed the cells.
|
||||
let mut candidates = Vec::with_capacity(2 * (nu + nv + 2));
|
||||
let mut along = |line: &dyn Fn(f64) -> Point, step: f64, last: usize, coord: fn(Point) -> f64| {
|
||||
let (Some(a), Some(b)) = (h.apply(line(0.0)), h.apply(line(last as f64 * step))) else { return };
|
||||
let ab = b - a;
|
||||
let len2 = ab.x * ab.x + ab.y * ab.y;
|
||||
let t = if len2 > 0.0 { (((p - a).x * ab.x + (p - a).y * ab.y) / len2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let Some(at) = hi.apply(a + ab * t).map(coord).filter(|v| v.is_finite()) else { return };
|
||||
let k = (at / step).floor().clamp(0.0, last as f64);
|
||||
for k in [k, (k + 1.0).min(last as f64)] {
|
||||
candidates.push(line(k * step));
|
||||
}
|
||||
};
|
||||
for (u, v) in near(q.x, su, nu).flat_map(|u| near(q.y, sv, nv).map(move |v| (u, v))) {
|
||||
let q = Point::new(u, v);
|
||||
for i in 0..=nu {
|
||||
let u = i as f64 * su;
|
||||
along(&|v| Point::new(u, v), sv, nv, |q| q.y);
|
||||
}
|
||||
for i in 0..=nv {
|
||||
let v = i as f64 * sv;
|
||||
along(&|u| Point::new(u, v), su, nu, |q| q.x);
|
||||
}
|
||||
for q in candidates {
|
||||
let Some(d) = h.apply(q).map(|at| at.distance(p)) else { continue };
|
||||
if d <= tol
|
||||
&& best.is_none_or(|b| d < b.0)
|
||||
@@ -268,6 +283,33 @@ mod tests {
|
||||
assert!(g.infer_at(&d, past, 3.0, PlaneTargets::ALL).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_plane_still_checks_its_own_place() {
|
||||
// Held at x = 0, the left wall's own place must stay in front of the viewer too: it goes
|
||||
// back there once positioning ends.
|
||||
let g = PerspectiveGrid { plane: Plane::Left, inferred: Some([0.0; 3]), ..grid() };
|
||||
assert!(g.merged(&json!({"leftOffset": -800})).is_err());
|
||||
assert!(PerspectiveGrid { inferred: None, ..g.clone() }.merged(&json!({"leftOffset": -800})).is_err());
|
||||
let moved = g.merged(&json!({"leftOffset": 40})).unwrap();
|
||||
assert_eq!((moved.placed_offset(Plane::Left), moved.offset(Plane::Left), moved.inferred), (40.0, 0.0, Some([0.0; 3])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_nearest_intersection_on_the_page_is_found_on_a_skewed_plane() {
|
||||
// The left vanishing point next to the origin skews the left wall's cells hard: the
|
||||
// intersection nearest on the page lies many cells from the pointer's inverse.
|
||||
let g = PerspectiveGrid { vp_left: 399.0, plane: Plane::Left, inferred: Some([0.0; 3]), ..grid() };
|
||||
let d = Document::new(800.0, 600.0);
|
||||
let gridlines = PlaneTargets { anchors: false, gridlines: true };
|
||||
let p = Point::new(400.0, 219.761194);
|
||||
let want = Point::new(10.0 * g.cell, 10.0 * g.cell);
|
||||
assert!((g.to_page(Plane::Left, want).unwrap() - p).hypot() < 0.5);
|
||||
let (_, at) = g.infer_at(&d, p, 6.0, gridlines).unwrap();
|
||||
for (got, want) in at.iter().zip([0.0, want.y, want.x]) {
|
||||
assert!((got - want).abs() < 1e-6, "{at:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hidden_path_inside_art_in_perspective_has_no_anchor_to_take() {
|
||||
let g = PerspectiveGrid { plane: Plane::Right, ..grid() };
|
||||
|
||||
@@ -57,10 +57,12 @@ pub use catalog::{TOOL_GROUPS, ToolInfo, tool_info};
|
||||
mod context;
|
||||
mod cursor;
|
||||
mod input;
|
||||
mod readout;
|
||||
|
||||
pub use context::{GlyphSnap, MarqueeMode, PaintDefaults, PixelOp, PixelSnap, ScreenFrame, ToolContext};
|
||||
pub use cursor::Cursor;
|
||||
pub use input::{Action, Mods, PenPose, PointerEvent, PointerKind, ToolKey};
|
||||
pub use readout::Readout;
|
||||
|
||||
/// Visual feedback drawn by the UI in screen space (coordinates here are document space).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -265,6 +267,12 @@ pub trait Tool: Send {
|
||||
fn wants_rehover(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// What the Info panel shows for the tool as it is now: the drag in progress (a move of the
|
||||
/// selection, a gradient vector, a Scale, Rotate, Reflect or Shear tool drag), or the
|
||||
/// Paintbrush's brush. None for a tool with nothing of its own to show.
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<Readout> {
|
||||
None
|
||||
}
|
||||
/// Called when the user switches away (finish pending work).
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
vec![]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! What the tool in use gives the Info panel to show ([`crate::Tool::readout`]).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
/// The Info panel's values for the tool in use. Lengths and points are in document units, and
|
||||
/// angles are in degrees as the matching command takes them: counter-clockwise for
|
||||
/// `object.rotate`'s `angle` and `object.reflect`'s `axis`, and `object.shear`'s `angle`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Readout {
|
||||
/// A drag that moves the selection or draws a gradient vector: how far it has gone in x and y.
|
||||
Drag { dx: f64, dy: f64 },
|
||||
/// A Scale tool drag: the horizontal and vertical scale (1 leaves the size as it is).
|
||||
Scale { sx: f64, sy: f64 },
|
||||
/// A Rotate tool drag: where the center of the selection's bounding box is, and the angle
|
||||
/// turned.
|
||||
Rotate { center: Point, angle: f64 },
|
||||
/// A Reflect tool drag: where the center of the selection's bounding box is, and the angle of
|
||||
/// the axis.
|
||||
Reflect { center: Point, angle: f64 },
|
||||
/// A Shear tool drag: where the center of the selection's bounding box is, the angle of the
|
||||
/// shear axis (0 horizontal, 90 vertical) and the shear angle.
|
||||
Shear { center: Point, axis: f64, angle: f64 },
|
||||
/// The Paintbrush: the brush it paints with, if any.
|
||||
Brush { name: Option<String> },
|
||||
}
|
||||
|
||||
impl Readout {
|
||||
/// The readout of a drag by `d`.
|
||||
pub fn drag(d: Vec2) -> Self {
|
||||
Readout::Drag { dx: d.x, dy: d.y }
|
||||
}
|
||||
|
||||
/// The length of a [`Readout::Drag`] and its angle, as the Measure tool gives them.
|
||||
pub fn distance_angle(&self) -> Option<(f64, f64)> {
|
||||
match *self {
|
||||
Readout::Drag { dx, dy } => {
|
||||
let (dist, angle, ..) = crate::xform::measure(Point::ORIGIN, Point::new(dx, dy));
|
||||
Some((dist, angle))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The readout as `info.readout` reports it.
|
||||
pub fn to_json(&self) -> Value {
|
||||
let at = |p: Point| json!([p.x, p.y]);
|
||||
match self {
|
||||
Readout::Drag { dx, dy } => {
|
||||
let (distance, angle) = self.distance_angle().unwrap_or_default();
|
||||
json!({ "kind": "drag", "dx": dx, "dy": dy, "distance": distance, "angle": angle })
|
||||
}
|
||||
Readout::Scale { sx, sy } => json!({ "kind": "scale", "width": sx * 100.0, "height": sy * 100.0 }),
|
||||
Readout::Rotate { center, angle } => json!({ "kind": "rotate", "center": at(*center), "angle": angle }),
|
||||
Readout::Reflect { center, angle } => json!({ "kind": "reflect", "center": at(*center), "angle": angle }),
|
||||
Readout::Shear { center, axis, angle } => json!({ "kind": "shear", "center": at(*center), "axis": axis, "angle": angle }),
|
||||
Readout::Brush { name } => json!({ "kind": "brush", "brush": name }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_drag_gives_its_length_and_its_angle_up_the_page() {
|
||||
let r = Readout::drag(Vec2::new(30.0, -40.0));
|
||||
let (d, a) = r.distance_angle().unwrap();
|
||||
assert!((d - 50.0).abs() < 1e-9 && (a - 53.13010235415598).abs() < 1e-9, "{d} {a}");
|
||||
assert_eq!(r.to_json()["dx"], 30.0);
|
||||
assert_eq!(Readout::Scale { sx: 1.5, sy: 0.5 }.to_json(), json!({"kind": "scale", "width": 150.0, "height": 50.0}));
|
||||
assert_eq!(Readout::Scale { sx: 1.0, sy: 1.0 }.distance_angle(), None);
|
||||
assert_eq!(Readout::Brush { name: None }.to_json(), json!({"kind": "brush", "brush": null}));
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,8 @@ pub struct SelectionTool {
|
||||
/// The params of the move's last preview: in Shaper's Construction mode the move ends with
|
||||
/// them and `releaseOutside` ([`construction`]).
|
||||
last_move: Option<Value>,
|
||||
/// How far the move's last preview carried the selection ([`Tool::readout`]).
|
||||
moved: Option<Vec2>,
|
||||
/// Held state used for the most recent bounding-box preview.
|
||||
last_backquote: bool,
|
||||
/// Pointer position used for the most recent bounding-box preview.
|
||||
@@ -898,6 +900,10 @@ impl Tool for SelectionTool {
|
||||
matches!(self.state, State::Moving { began: true, .. })
|
||||
}
|
||||
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<crate::Readout> {
|
||||
self.moved.filter(|_| self.moving()).map(crate::Readout::drag)
|
||||
}
|
||||
|
||||
fn transforming(&self) -> bool {
|
||||
match &self.state {
|
||||
State::Moving { began: true, .. } | State::Scaling { .. } | State::Rotating { .. } => true,
|
||||
@@ -1753,6 +1759,7 @@ impl SelectionTool {
|
||||
params["patterns"] = json!(true);
|
||||
}
|
||||
self.last_move = Some(params.clone());
|
||||
self.moved = Some(d);
|
||||
out.push(Action::Preview("object.transform".into(), params));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -299,6 +299,9 @@ impl Tool for BrushPaintTool {
|
||||
fn busy(&self) -> bool {
|
||||
self.inner.busy()
|
||||
}
|
||||
fn readout(&self, cx: &ToolContext) -> Option<crate::Readout> {
|
||||
Some(crate::Readout::Brush { name: self.brush_name(cx) })
|
||||
}
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
let acts = self.inner.deactivate(cx);
|
||||
self.map(cx, acts)
|
||||
|
||||
@@ -545,6 +545,12 @@ impl Tool for GradientTool {
|
||||
self.gesture.as_ref().is_some_and(|g| g.began) || self.free.as_ref().is_some_and(freeform::Gesture::began)
|
||||
}
|
||||
|
||||
/// The vector a drag on the art has drawn so far.
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<crate::Readout> {
|
||||
let (start, end) = self.gesture.as_ref().filter(|g| g.began)?.vector?;
|
||||
Some(crate::Readout::drag(end - start))
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
// A freeform gradient: its points instead of the bar.
|
||||
match (ev.kind, &mut self.free) {
|
||||
|
||||
@@ -20,7 +20,7 @@ use super::target_overlays;
|
||||
use crate::bbox::rotate_for_drag;
|
||||
use crate::guides::{PointSnap, Targets, snap_pick};
|
||||
use crate::select::{matrix_json, selection_bounds, selection_box};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Readout, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TransformKind {
|
||||
@@ -98,6 +98,8 @@ pub struct TransformTool {
|
||||
origin: Option<(Point, Vec<NodeId>)>,
|
||||
drag: Option<Drag>,
|
||||
measure: Option<(Point, String)>,
|
||||
/// What the Info panel shows for the drag in progress.
|
||||
readout: Option<Readout>,
|
||||
/// Snap feedback while hovering ("anchor" / "center" label), and for the dragged pointer.
|
||||
hover: Vec<Overlay>,
|
||||
/// What the dragged pointer snaps to, gathered when the drag begins.
|
||||
@@ -126,7 +128,7 @@ pub fn reflect_matrix(o: Point, theta: f64) -> Affine {
|
||||
|
||||
impl TransformTool {
|
||||
pub fn new(kind: TransformKind) -> Self {
|
||||
Self { kind, origin: None, drag: None, measure: None, hover: vec![], snap: None }
|
||||
Self { kind, origin: None, drag: None, measure: None, readout: None, hover: vec![], snap: None }
|
||||
}
|
||||
|
||||
/// The effective reference point: the custom one if it belongs to the current selection,
|
||||
@@ -154,12 +156,44 @@ impl TransformTool {
|
||||
/// and Shift's 45° steps start from the box's axes. A Reflect axis drawn by the drag is the
|
||||
/// line the pointer draws on the page.
|
||||
pub fn matrix_in(&self, angle: f64, o: Point, start: Point, p: Point, shift: bool, bounds: Rect) -> (Affine, String) {
|
||||
let (m, a, b) = self.solve(angle, o, start, p, shift, bounds);
|
||||
(m, self.label(a, b))
|
||||
}
|
||||
|
||||
/// The canvas label for the two values [`Self::solve`] gives.
|
||||
fn label(&self, a: f64, b: f64) -> String {
|
||||
match self.kind {
|
||||
TransformKind::Rotate => format!("{:.1}°", -a),
|
||||
TransformKind::Reflect => format!("{a:.1}°"),
|
||||
TransformKind::Scale => format!("W: {:.1}%\nH: {:.1}%", a * 100.0, b * 100.0),
|
||||
TransformKind::Shear => format!("{:.1}°", if a == 0.0 { b } else { -b }),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the Info panel shows for the transform `m` of a selection with `bounds`, with the two
|
||||
/// values [`Self::solve`] gives.
|
||||
fn readout_of(&self, m: Affine, a: f64, b: f64, bounds: Rect) -> Readout {
|
||||
let center = m * bounds.center();
|
||||
match self.kind {
|
||||
TransformKind::Rotate => Readout::Rotate { center, angle: a },
|
||||
TransformKind::Reflect => Readout::Reflect { center, angle: a },
|
||||
TransformKind::Scale => Readout::Scale { sx: a, sy: b },
|
||||
TransformKind::Shear => Readout::Shear { center, axis: a, angle: b },
|
||||
}
|
||||
}
|
||||
|
||||
/// [`TransformTool::matrix_in`]'s transform, with the two values of its readout: the angle
|
||||
/// turned, in degrees counter-clockwise as `object.rotate` takes it (Rotate), the angle of the
|
||||
/// axis in degrees counter-clockwise (Reflect), the scale along the box's width and height
|
||||
/// (Scale), or the angle of the shear axis (0 horizontal, 90 vertical) and the shear angle as
|
||||
/// `object.shear` takes it (Shear).
|
||||
fn solve(&self, angle: f64, o: Point, start: Point, p: Point, shift: bool, bounds: Rect) -> (Affine, f64, f64) {
|
||||
let frame = OrientedBox { angle, rect: Rect::ZERO };
|
||||
let local = |v: Vec2| frame.to_doc().inverse() * v.to_point();
|
||||
match self.kind {
|
||||
TransformKind::Rotate => {
|
||||
let (a, deg) = rotate_for_drag(o, start, p, shift);
|
||||
(a, format!("{:.1}°", -deg))
|
||||
(a, deg, 0.0)
|
||||
}
|
||||
TransformKind::Reflect => {
|
||||
let mut v = p - o;
|
||||
@@ -169,7 +203,7 @@ impl TransformTool {
|
||||
v = (frame.to_doc() * vectorcraft_geom::constrain_angle(local(v).to_vec2(), 45.0).to_point()).to_vec2();
|
||||
}
|
||||
let theta = v.y.atan2(v.x);
|
||||
(reflect_matrix(o, theta), format!("{:.1}°", vectorcraft_geom::normalize_deg(-theta.to_degrees())))
|
||||
(reflect_matrix(o, theta), vectorcraft_geom::normalize_deg(-theta.to_degrees()), 0.0)
|
||||
}
|
||||
TransformKind::Scale => {
|
||||
let d0 = local(start - o).to_vec2();
|
||||
@@ -184,7 +218,7 @@ impl TransformTool {
|
||||
}
|
||||
let clamp = |s: f64| if s.abs() < 1e-4 { 1e-4f64.copysign(s) } else { s };
|
||||
let (sx, sy) = (clamp(sx), clamp(sy));
|
||||
(about(o, frame.conjugate(Affine::scale_non_uniform(sx, sy))), format!("W: {:.1}%\nH: {:.1}%", sx * 100.0, sy * 100.0))
|
||||
(about(o, frame.conjugate(Affine::scale_non_uniform(sx, sy))), sx, sy)
|
||||
}
|
||||
TransformKind::Shear => {
|
||||
// The shear axis follows the dominant drag direction. Shift keeps the drag on that
|
||||
@@ -223,7 +257,7 @@ impl TransformTool {
|
||||
};
|
||||
}
|
||||
}
|
||||
(m, format!("{:.1}°", (-k.atan()).to_degrees()))
|
||||
if horizontal { (m, 0.0, (-k.atan()).to_degrees()) } else { (m, 90.0, k.atan().to_degrees()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +265,7 @@ impl TransformTool {
|
||||
/// Ends the drag, its snapping and readouts with it.
|
||||
fn end_drag(&mut self) -> Option<Drag> {
|
||||
self.measure = None;
|
||||
self.readout = None;
|
||||
self.snap = None;
|
||||
self.hover.clear();
|
||||
self.drag.take()
|
||||
@@ -250,6 +285,10 @@ impl Tool for TransformTool {
|
||||
self.drag.is_some_and(|d| d.began)
|
||||
}
|
||||
|
||||
fn readout(&self, _cx: &ToolContext) -> Option<Readout> {
|
||||
self.readout.clone()
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
match ev.kind {
|
||||
@@ -294,10 +333,11 @@ impl Tool for TransformTool {
|
||||
}
|
||||
let (p, guides) = self.snap.as_ref().map_or((p, vec![]), |s| s.snap(cx, p));
|
||||
self.hover = guides;
|
||||
let (m, text) = self.matrix_in(d.angle_for(ev.mods.alt, ev.backquote), d.origin, d.pick, p, ev.mods.shift, d.bounds);
|
||||
let (m, a, b) = self.solve(d.angle_for(ev.mods.alt, ev.backquote), d.origin, d.pick, p, ev.mods.shift, d.bounds);
|
||||
(d.last, d.pointer, d.shift) = (m, p, ev.mods.shift);
|
||||
self.drag = Some(d);
|
||||
self.measure = Some((p, text));
|
||||
self.measure = Some((p, self.label(a, b)));
|
||||
self.readout = Some(self.readout_of(m, a, b, d.bounds));
|
||||
out.push(preview(m, ev.mods.alt, ev.backquote));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -133,6 +133,8 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo
|
||||
drag_released(app, ui, &f, d, release_pos(ui, button).unwrap_or(p));
|
||||
}
|
||||
} else if let Some(p) = hover_tool
|
||||
// A press on an artboard's name starts no drag: the moves while it is held are not hover.
|
||||
&& !(pointer.primary_down() && ui.data(|d| d.get_temp::<(usize, bool)>(name_press_id()).is_some()))
|
||||
&& (pointer.is_moving() || mods_changed || !was_over || tool_switched || (context_changed && app.session.tool_wants_rehover()))
|
||||
{
|
||||
let ev = PointerEvent {
|
||||
|
||||
@@ -1027,6 +1027,49 @@ fn shift_clicking_canvas_artboard_label_toggles_multi_selection() {
|
||||
assert_eq!(app.session.tool_options()["selected"], json!([2]), "Plain click selects that artboard alone");
|
||||
}
|
||||
|
||||
/// A press on an artboard's name starts no drag. Moving with the button still held is not hover,
|
||||
/// so Shift over a gridline intersection does not position the Perspective Selection tool's plane
|
||||
/// until the button is up.
|
||||
#[test]
|
||||
fn moves_while_an_artboard_name_is_pressed_do_not_hover_the_tool() {
|
||||
use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||||
app.select_tool("perspectiveSelection");
|
||||
app.session.execute("perspective.grid.show", &json!({"visible": true})).unwrap();
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.center = Point::new(200.0, 150.0);
|
||||
v.zoom = 1.0;
|
||||
v.fitted = true;
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let drawn = shapes(&mut app, &ctx);
|
||||
let label = drawn
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
Shape::Text(t) if t.galley.text() == "01 - Artboard 1" => Some(t.pos + vec2(10.0, 5.0)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let xf = Xf::new(rect, app.view().unwrap());
|
||||
let doc = |app: &VectorcraftApp| app.session.active().unwrap().doc.clone();
|
||||
let g = PerspectiveGrid::current(&doc(&app));
|
||||
let target = xf.to_screen(g.to_page(Plane::Left, Point::new(2.0 * g.cell, 3.0 * g.cell)).unwrap());
|
||||
assert!(rect.contains(label) && rect.contains(target), "{label:?} {target:?} {rect:?}");
|
||||
|
||||
let shift = egui::Modifiers::SHIFT;
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: shift };
|
||||
frame(&mut app, &ctx, vec![egui::Event::ModifiersChanged(shift), egui::Event::PointerMoved(label), button(label, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target + vec2(0.5, 0.0))]);
|
||||
assert!(!PerspectiveGrid::positioning(&doc(&app)), "the button is still down");
|
||||
frame(&mut app, &ctx, vec![button(target, false)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(target)]);
|
||||
assert!(PerspectiveGrid::positioning(&doc(&app)), "with the button up, Shift over the intersection positions the plane");
|
||||
}
|
||||
|
||||
/// Enable Touch Gestures (#585): a two-finger tap undoes and a three-finger one redoes, and the
|
||||
/// first finger's press (the pointer egui makes of it) draws nothing. Off, a tap does nothing.
|
||||
#[test]
|
||||
|
||||
@@ -37,10 +37,12 @@ pub(super) fn body(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let id = d.str("__effect");
|
||||
let defaults = vectorcraft_effects::default_params(&id).unwrap_or(json!({}));
|
||||
// Reset all resets the shape, view and light; the mapped graphics stay.
|
||||
// Reset all resets the shape, view and light; the mapped graphics and the render
|
||||
// settings stay.
|
||||
let defaults = match defaults {
|
||||
Value::Object(mut m) => {
|
||||
m.remove(vectorcraft_effects::GRAPHICS);
|
||||
m.remove(vectorcraft_effects::RENDER);
|
||||
Value::Object(m)
|
||||
}
|
||||
other => other,
|
||||
|
||||
@@ -1151,6 +1151,7 @@ font-technology Variable Променливи
|
||||
Rotate (Classic)… Ротация (класическа)…
|
||||
Surface Повърхност
|
||||
Wireframe Телена рамка
|
||||
Render Settings Настройки за рендериране
|
||||
No Shading Без засенчване
|
||||
Plastic Пластмаса
|
||||
Highlight Intensity Интензивност на отблясъка
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -513,6 +513,7 @@
|
||||
Rotate (Classic)… Rotiraj (klasično)…
|
||||
Surface Površina
|
||||
Wireframe Žičani model
|
||||
Render Settings Postavke renderovanja
|
||||
No Shading Bez sjenčenja
|
||||
Plastic Plastika
|
||||
Highlight Intensity Intenzitet odsjaja
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1626,6 +1626,7 @@ antiAlias Sharp Nítid
|
||||
Rotate (Classic)… Gira (clàssic)…
|
||||
Surface Superfície
|
||||
Wireframe Filferro
|
||||
Render Settings Configuració de renderització
|
||||
No Shading Sense ombra
|
||||
Plastic Plàstic
|
||||
Highlight Intensity Intensitat de la brillantor
|
||||
@@ -4688,6 +4689,48 @@ digits Malayalam Malaiàlam
|
||||
Composite Fonts Tipus de lletra compostos
|
||||
Export Composite Fonts Exporta tipus de lletra compostos
|
||||
Import Composite Fonts Importa tipus de lletra compostos
|
||||
@msg each font must be {name, parts} cada tipus de lletra ha de ser {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts clau desconeguda `{k}`; un tipus de lletra admet name i parts
|
||||
@msg each font needs a name cada tipus de lletra necessita un nom
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` no pot anomenar un tipus de lletra compost (d’1 a {COMPOSITE_NAME_MAX} caràcters, sense caràcters de control)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts ha de ser un objecte
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe desconeguda `{k}`; les classes són kanji, kana, punctuation, symbols, roman i numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} ha de ser un objecte
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: clau desconeguda `{k}` a {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: indiqueu la part kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un document conté com a màxim {COMPOSITE_FONTS_MAX} tipus de lletra compostos
|
||||
@msg two composite fonts are named `{_1}` dos tipus de lletra compostos es diuen `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` és el nom d’un tipus de lletra instal·lat
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, un tipus de lletra compost
|
||||
@msg give fonts: [{name, parts}…] indiqueu fonts: [{name, parts}…]
|
||||
@msg path is empty path és buit
|
||||
@msg fonts must be a list of composite fonts fonts ha de ser una llista de tipus de lletra compostos
|
||||
@msg names must be strings names ha de contenir cadenes
|
||||
@msg no composite font named `{n}` cap tipus de lletra compost anomenat `{n}`
|
||||
@msg names must be a list of names names ha de ser una llista de noms
|
||||
@msg no composite fonts to export cap tipus de lletra compost per exportar
|
||||
@msg not a composite fonts file: {e} no és un fitxer de tipus de lletra compostos: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list no és un fitxer de tipus de lletra compostos: no hi ha cap llista compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts el fitxer conté més de {COMPOSITE_FONTS_MAX} tipus de lletra compostos
|
||||
@msg the file is larger than 4 MB el fitxer ocupa més de 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 no és base64
|
||||
@msg give one of path, data or dataBase64 indiqueu path, data o dataBase64
|
||||
@msg the file holds no composite fonts el fitxer no conté cap tipus de lletra compost
|
||||
@msg the file holds two composite fonts named `{_1}` el fitxer conté dos tipus de lletra compostos anomenats `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` no pot anomenar un tipus de lletra compost
|
||||
@msg {k}: give a font {k}: indiqueu un tipus de lletra
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: indiqueu un tipus de lletra
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size ha d’estar entre {_1} i {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size ha d’estar entre {_1} i {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline ha d’estar entre {_1} i {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline ha d’estar entre {_1} i {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: l’escala ha d’estar entre {_1} i {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: l’escala ha d’estar entre {_1} i {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: només kana, roman i numerals es poden escalar
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: només kana, roman i numerals es poden escalar
|
||||
@msg {k}: only kana can be scaled at the center {k}: només kana es pot escalar des del centre
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: només kana es pot escalar des del centre
|
||||
@msg no composite font cap tipus de lletra compost
|
||||
Delete Font Suprimeix el tipus de lletra
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -6217,6 +6260,7 @@ digits Malayalam Malaiàlam
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un efecte 3D al farciment o al traç d’un grup no dibuixa res per exportar
|
||||
@msg The 3D object is too complex to export L’objecte 3D és massa complex per exportar-lo
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l’objecte no té cap efecte 3D (Extrudeix i bisella, Revoluciona, Infla o Gira)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` ha de ser un objecte que només contingui `mode`, amb el valor shaded o wireframe
|
||||
@msg its 3D effect builds no solid el seu efecte 3D no construeix cap sòlid
|
||||
@msg bevel must be none or one of {_1} bevel ha de ser none o un de {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} no pot construir sòlids a partir d’aquesta il·lustració: les seves instàncies de símbol, fusions i grups de retall contenen massa objectes
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -526,6 +526,7 @@
|
||||
Rotate (Classic)… Otočit (klasické)…
|
||||
Surface Povrch
|
||||
Wireframe Drátový model
|
||||
Render Settings Nastavení vykreslování
|
||||
No Shading Bez stínování
|
||||
Plastic Plast
|
||||
Highlight Intensity Intenzita odlesku
|
||||
@@ -1664,6 +1665,48 @@ digits Malayalam Malajálamské
|
||||
Composite Fonts Složená písma
|
||||
Export Composite Fonts Exportovat složená písma
|
||||
Import Composite Fonts Importovat složená písma
|
||||
@msg each font must be {name, parts} každé písmo musí být {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts neznámý klíč `{k}`; písmo přijímá name a parts
|
||||
@msg each font needs a name každé písmo potřebuje název
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nemůže být názvem složeného písma (1 až {COMPOSITE_NAME_MAX} znaků, bez řídicích znaků)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts musí být objekt
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: neznámá třída `{k}`; třídy jsou kanji, kana, punctuation, symbols, roman a numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} musí být objekt
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: neznámý klíč `{k}` v {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: zadejte část kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts dokument obsahuje nejvýše {COMPOSITE_FONTS_MAX} složených písem
|
||||
@msg two composite fonts are named `{_1}` dvě složená písma se jmenují `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` je název nainstalovaného písma
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} používá `{font}`, což je složené písmo
|
||||
@msg give fonts: [{name, parts}…] zadejte fonts: [{name, parts}…]
|
||||
@msg path is empty path je prázdné
|
||||
@msg fonts must be a list of composite fonts fonts musí být seznam složených písem
|
||||
@msg names must be strings names musí být řetězce
|
||||
@msg no composite font named `{n}` žádné složené písmo s názvem `{n}`
|
||||
@msg names must be a list of names names musí být seznam názvů
|
||||
@msg no composite fonts to export žádná složená písma k exportu
|
||||
@msg not a composite fonts file: {e} nejde o soubor složených písem: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list nejde o soubor složených písem: chybí seznam compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts soubor obsahuje více než {COMPOSITE_FONTS_MAX} složených písem
|
||||
@msg the file is larger than 4 MB soubor je větší než 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 není base64
|
||||
@msg give one of path, data or dataBase64 zadejte path, data nebo dataBase64
|
||||
@msg the file holds no composite fonts soubor neobsahuje žádná složená písma
|
||||
@msg the file holds two composite fonts named `{_1}` soubor obsahuje dvě složená písma s názvem `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` nemůže být názvem složeného písma
|
||||
@msg {k}: give a font {k}: zadejte písmo
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: zadejte písmo
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size musí být {_1} až {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size musí být {_1} až {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline musí být {_1} až {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline musí být {_1} až {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: měřítko musí být {_1} až {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: měřítko musí být {_1} až {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: měnit měřítko lze jen u kana, roman a numerals
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: měnit měřítko lze jen u kana, roman a numerals
|
||||
@msg {k}: only kana can be scaled at the center {k}: měřítko od středu lze měnit jen u kana
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: měřítko od středu lze měnit jen u kana
|
||||
@msg no composite font žádné složené písmo
|
||||
Delete Font Odstranit písmo
|
||||
Kanji Kandži
|
||||
Kana Kana
|
||||
@@ -6026,6 +6069,7 @@ mark-style Japanese Japonské
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D efekt na výplni nebo tahu skupiny nekreslí nic k exportu
|
||||
@msg The 3D object is too complex to export 3D objekt je pro export příliš složitý
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objekt nemá žádný 3D efekt (Vysunutí a zkosení, Rotační těleso, Nafouknutí ani Otočit)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` musí být objekt obsahující pouze `mode` s hodnotou shaded nebo wireframe
|
||||
@msg its 3D effect builds no solid jeho 3D efekt nevytváří žádné těleso
|
||||
@msg bevel must be none or one of {_1} bevel musí být none nebo jedno z {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} nemůže z této kresby vytvořit tělesa: její instance symbolů, prolnutí a ořezové skupiny obsahují příliš mnoho objektů
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -291,6 +291,7 @@
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export en 3D-effekt på en gruppes udfyldning eller streg tegner intet at eksportere
|
||||
@msg The 3D object is too complex to export 3D-objektet er for komplekst til at blive eksporteret
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektet har ingen 3D-effekt (Ekstrudér og facet, Omdrej, Pust op eller Rotér)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` skal være et objekt, der kun indeholder `mode` med værdien shaded eller wireframe
|
||||
@msg its 3D effect builds no solid dens 3D-effekt danner intet legeme
|
||||
@msg bevel must be none or one of {_1} bevel skal være none eller en af {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kan ikke danne legemer af denne grafik: dens symbolforekomster, blandinger og beskæringsgrupper indeholder for mange objekter
|
||||
@@ -1089,6 +1090,7 @@
|
||||
Rotate (Classic)… Rotér (klassisk)…
|
||||
Surface Overflade
|
||||
Wireframe Trådmodel
|
||||
Render Settings Renderingsindstillinger
|
||||
No Shading Ingen skyggelægning
|
||||
Plastic Plastik
|
||||
Highlight Intensity Højlysintensitet
|
||||
@@ -2179,6 +2181,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Sammensatte skrifttyper
|
||||
Export Composite Fonts Eksportér sammensatte skrifttyper
|
||||
Import Composite Fonts Importér sammensatte skrifttyper
|
||||
@msg each font must be {name, parts} hver skrifttype skal være {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts ukendt nøgle `{k}`; en skrifttype tager name og parts
|
||||
@msg each font needs a name hver skrifttype skal have et navn
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan ikke være navnet på en sammensat skrifttype (1 til {COMPOSITE_NAME_MAX} tegn, ingen kontroltegn)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts skal være et objekt
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: ukendt klasse `{k}`; klasserne er kanji, kana, punctuation, symbols, roman og numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} skal være et objekt
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: ukendt nøgle `{k}` i {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: angiv delen kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts et dokument har højst {COMPOSITE_FONTS_MAX} sammensatte skrifttyper
|
||||
@msg two composite fonts are named `{_1}` to sammensatte skrifttyper hedder `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` er navnet på en installeret skrifttype
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} er sat med `{font}`, en sammensat skrifttype
|
||||
@msg give fonts: [{name, parts}…] angiv fonts: [{name, parts}…]
|
||||
@msg path is empty path er tom
|
||||
@msg fonts must be a list of composite fonts fonts skal være en liste med sammensatte skrifttyper
|
||||
@msg names must be strings names skal være strenge
|
||||
@msg no composite font named `{n}` ingen sammensat skrifttype hedder `{n}`
|
||||
@msg names must be a list of names names skal være en liste med navne
|
||||
@msg no composite fonts to export ingen sammensatte skrifttyper at eksportere
|
||||
@msg not a composite fonts file: {e} ikke en fil med sammensatte skrifttyper: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ikke en fil med sammensatte skrifttyper: ingen liste compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts filen indeholder mere end {COMPOSITE_FONTS_MAX} sammensatte skrifttyper
|
||||
@msg the file is larger than 4 MB filen er større end 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 er ikke base64
|
||||
@msg give one of path, data or dataBase64 angiv én af path, data eller dataBase64
|
||||
@msg the file holds no composite fonts filen indeholder ingen sammensatte skrifttyper
|
||||
@msg the file holds two composite fonts named `{_1}` filen indeholder to sammensatte skrifttyper, der hedder `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kan ikke være navnet på en sammensat skrifttype
|
||||
@msg {k}: give a font {k}: angiv en skrifttype
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: angiv en skrifttype
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size skal være {_1} til {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size skal være {_1} til {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline skal være {_1} til {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline skal være {_1} til {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skaleringen skal være {_1} til {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skaleringen skal være {_1} til {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: kun kana, roman og numerals kan skaleres
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: kun kana, roman og numerals kan skaleres
|
||||
@msg {k}: only kana can be scaled at the center {k}: kun kana kan skaleres fra midten
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: kun kana kan skaleres fra midten
|
||||
@msg no composite font ingen sammensat skrifttype
|
||||
Delete Font Slet skrifttype
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1164,6 +1164,7 @@ font-technology Variable Variabel
|
||||
Rotate (Classic)… Drehen (klassisch)…
|
||||
Surface Oberfläche
|
||||
Wireframe Drahtmodell
|
||||
Render Settings Rendereinstellungen
|
||||
No Shading Keine Schattierung
|
||||
Plastic Kunststoff
|
||||
Highlight Intensity Glanzpunktintensität
|
||||
@@ -3655,6 +3656,48 @@ effect Points Punkte
|
||||
Composite Fonts Zusammengesetzte Schriften
|
||||
Export Composite Fonts Zusammengesetzte Schriften exportieren
|
||||
Import Composite Fonts Zusammengesetzte Schriften importieren
|
||||
@msg each font must be {name, parts} jede Schrift muss {name, parts} sein
|
||||
@msg unknown key `{k}`; a font takes name and parts unbekannter Schlüssel `{k}`; eine Schrift nimmt name und parts
|
||||
@msg each font needs a name jede Schrift braucht einen Namen
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kann keine zusammengesetzte Schrift benennen (1 bis {COMPOSITE_NAME_MAX} Zeichen, keine Steuerzeichen)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts muss ein Objekt sein
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: unbekannte Klasse `{k}`; die Klassen sind kanji, kana, punctuation, symbols, roman und numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} muss ein Objekt sein
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: unbekannter Schlüssel `{k}` in {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: geben Sie den Teil kanji an
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ein Dokument enthält höchstens {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften
|
||||
@msg two composite fonts are named `{_1}` zwei zusammengesetzte Schriften heißen `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` ist der Name einer installierten Schrift
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} ist in `{font}` gesetzt, einer zusammengesetzten Schrift
|
||||
@msg give fonts: [{name, parts}…] geben Sie fonts an: [{name, parts}…]
|
||||
@msg path is empty path ist leer
|
||||
@msg fonts must be a list of composite fonts fonts muss eine Liste zusammengesetzter Schriften sein
|
||||
@msg names must be strings names müssen Zeichenketten sein
|
||||
@msg no composite font named `{n}` keine zusammengesetzte Schrift namens `{n}`
|
||||
@msg names must be a list of names names muss eine Liste von Namen sein
|
||||
@msg no composite fonts to export keine zusammengesetzten Schriften zum Exportieren
|
||||
@msg not a composite fonts file: {e} keine Datei mit zusammengesetzten Schriften: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list keine Datei mit zusammengesetzten Schriften: keine Liste compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts die Datei enthält mehr als {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften
|
||||
@msg the file is larger than 4 MB die Datei ist größer als 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 ist kein Base64
|
||||
@msg give one of path, data or dataBase64 geben Sie path, data oder dataBase64 an
|
||||
@msg the file holds no composite fonts die Datei enthält keine zusammengesetzten Schriften
|
||||
@msg the file holds two composite fonts named `{_1}` die Datei enthält zwei zusammengesetzte Schriften namens `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kann keine zusammengesetzte Schrift benennen
|
||||
@msg {k}: give a font {k}: geben Sie eine Schrift an
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: geben Sie eine Schrift an
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size muss {_1} bis {_2} % sein
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size muss {_1} bis {_2} % sein
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline muss {_1} bis {_2} % sein
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline muss {_1} bis {_2} % sein
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: die Skalierung muss {_1} bis {_2} % sein
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: die Skalierung muss {_1} bis {_2} % sein
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: nur kana, roman und numerals lassen sich skalieren
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: nur kana, roman und numerals lassen sich skalieren
|
||||
@msg {k}: only kana can be scaled at the center {k}: nur kana lässt sich am Mittelpunkt skalieren
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: nur kana lässt sich am Mittelpunkt skalieren
|
||||
@msg no composite font keine zusammengesetzte Schrift
|
||||
Delete Font Schrift löschen
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5598,6 +5641,7 @@ lighting Light Licht
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ein 3D-Effekt auf der Fläche oder Kontur einer Gruppe zeichnet nichts, das exportiert werden kann
|
||||
@msg The 3D object is too complex to export Das 3D-Objekt ist zu komplex für den Export
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) das Objekt hat keinen 3D-Effekt (Extrudieren und abgeflachte Kante, Kreiseln, Aufblasen oder Drehen)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` muss ein Objekt sein, das nur `mode` mit dem Wert shaded oder wireframe enthält
|
||||
@msg its 3D effect builds no solid sein 3D-Effekt erzeugt keinen Körper
|
||||
@msg bevel must be none or one of {_1} bevel muss none oder einer der folgenden Werte sein: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kann aus diesem Bildmaterial keine Körper erstellen: Seine Symbolinstanzen, Angleichungen und Schnittgruppen enthalten zu viele Objekte
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1129,6 +1129,7 @@ font-technology Variable Μεταβλητές
|
||||
Rotate (Classic)… Περιστροφή (κλασικό)…
|
||||
Surface Επιφάνεια
|
||||
Wireframe Συρματόπλεγμα
|
||||
Render Settings Ρυθμίσεις απόδοσης
|
||||
No Shading Χωρίς σκίαση
|
||||
Plastic Πλαστικό
|
||||
Highlight Intensity Ένταση ανταύγειας
|
||||
@@ -4189,6 +4190,48 @@ digits Malayalam Μαλαγιάλαμ
|
||||
Composite Fonts Σύνθετες γραμματοσειρές
|
||||
Export Composite Fonts Εξαγωγή σύνθετων γραμματοσειρών
|
||||
Import Composite Fonts Εισαγωγή σύνθετων γραμματοσειρών
|
||||
@msg each font must be {name, parts} κάθε γραμματοσειρά πρέπει να είναι {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts άγνωστο κλειδί `{k}`· μια γραμματοσειρά δέχεται name και parts
|
||||
@msg each font needs a name κάθε γραμματοσειρά χρειάζεται όνομα
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) το `{raw}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς (1 έως {COMPOSITE_NAME_MAX} χαρακτήρες, χωρίς χαρακτήρες ελέγχου)
|
||||
@msg `{name}`: parts must be an object `{name}`: το parts πρέπει να είναι αντικείμενο
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: άγνωστη κλάση `{k}`· οι κλάσεις είναι kanji, kana, punctuation, symbols, roman και numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: το {_1} πρέπει να είναι αντικείμενο
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: άγνωστο κλειδί `{k}` στο {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: δώστε το τμήμα kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ένα έγγραφο περιέχει το πολύ {COMPOSITE_FONTS_MAX} σύνθετες γραμματοσειρές
|
||||
@msg two composite fonts are named `{_1}` δύο σύνθετες γραμματοσειρές ονομάζονται `{_1}`
|
||||
@msg `{_1}` is the name of an installed font το `{_1}` είναι όνομα εγκατεστημένης γραμματοσειράς
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: το {_2} χρησιμοποιεί την `{font}`, που είναι σύνθετη γραμματοσειρά
|
||||
@msg give fonts: [{name, parts}…] δώστε fonts: [{name, parts}…]
|
||||
@msg path is empty το path είναι κενό
|
||||
@msg fonts must be a list of composite fonts το fonts πρέπει να είναι λίστα σύνθετων γραμματοσειρών
|
||||
@msg names must be strings τα names πρέπει να είναι συμβολοσειρές
|
||||
@msg no composite font named `{n}` δεν υπάρχει σύνθετη γραμματοσειρά με όνομα `{n}`
|
||||
@msg names must be a list of names το names πρέπει να είναι λίστα ονομάτων
|
||||
@msg no composite fonts to export δεν υπάρχουν σύνθετες γραμματοσειρές για εξαγωγή
|
||||
@msg not a composite fonts file: {e} δεν είναι αρχείο σύνθετων γραμματοσειρών: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list δεν είναι αρχείο σύνθετων γραμματοσειρών: δεν υπάρχει λίστα compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts το αρχείο περιέχει περισσότερες από {COMPOSITE_FONTS_MAX} σύνθετες γραμματοσειρές
|
||||
@msg the file is larger than 4 MB το αρχείο είναι μεγαλύτερο από 4 MB
|
||||
@msg dataBase64 is not base64 το dataBase64 δεν είναι base64
|
||||
@msg give one of path, data or dataBase64 δώστε path, data ή dataBase64
|
||||
@msg the file holds no composite fonts το αρχείο δεν περιέχει σύνθετες γραμματοσειρές
|
||||
@msg the file holds two composite fonts named `{_1}` το αρχείο περιέχει δύο σύνθετες γραμματοσειρές με όνομα `{_1}`
|
||||
@msg `{_1}` can't name a composite font το `{_1}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς
|
||||
@msg {k}: give a font {k}: δώστε γραμματοσειρά
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: δώστε γραμματοσειρά
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: το size πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: το size πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: το baseline πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: το baseline πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα
|
||||
@msg {k}: only kana can be scaled at the center {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο
|
||||
@msg no composite font καμία σύνθετη γραμματοσειρά
|
||||
Delete Font Διαγραφή γραμματοσειράς
|
||||
Kanji Κάντζι
|
||||
Kana Κάνα
|
||||
@@ -5389,6 +5432,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ένα εφέ 3D στο γέμισμα ή στην πινελιά μιας ομάδας δεν σχεδιάζει τίποτα για εξαγωγή
|
||||
@msg The 3D object is too complex to export Το αντικείμενο 3D είναι πολύ σύνθετο για εξαγωγή
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) το αντικείμενο δεν έχει εφέ 3D (Εξώθηση και λοξότμηση, Περιστροφή γύρω από άξονα, Φούσκωμα ή Περιστροφή)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe το `render` πρέπει να είναι αντικείμενο που περιέχει μόνο `mode`, με τιμή shaded ή wireframe
|
||||
@msg its 3D effect builds no solid το εφέ 3D του δεν δημιουργεί στερεό
|
||||
@msg bevel must be none or one of {_1} το bevel πρέπει να είναι none ή ένα από τα {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: δεν μπορούν να δημιουργηθούν στερεά από αυτό το σχέδιο· οι παρουσίες συμβόλων, οι αναμείξεις και οι ομάδες αποκοπής του περιέχουν πάρα πολλά αντικείμενα
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1185,6 +1185,7 @@ font-technology Variable Variables
|
||||
Rotate (Classic)… Rotar (clásico)…
|
||||
Surface Superficie
|
||||
Wireframe Malla
|
||||
Render Settings Ajustes de renderizado
|
||||
No Shading Sin sombreado
|
||||
Plastic Plástico
|
||||
Highlight Intensity Intensidad del brillo
|
||||
@@ -3703,6 +3704,48 @@ digits Malayalam Malayálam
|
||||
Composite Fonts Fuentes compuestas
|
||||
Export Composite Fonts Exportar fuentes compuestas
|
||||
Import Composite Fonts Importar fuentes compuestas
|
||||
@msg each font must be {name, parts} cada fuente debe ser {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts clave desconocida `{k}`; una fuente admite name y parts
|
||||
@msg each font needs a name cada fuente necesita un nombre
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` no puede nombrar una fuente compuesta (de 1 a {COMPOSITE_NAME_MAX} caracteres, sin caracteres de control)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts debe ser un objeto
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: clase desconocida `{k}`; las clases son kanji, kana, punctuation, symbols, roman y numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} debe ser un objeto
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: clave desconocida `{k}` en {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: indique la parte kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un documento admite como máximo {COMPOSITE_FONTS_MAX} fuentes compuestas
|
||||
@msg two composite fonts are named `{_1}` dos fuentes compuestas se llaman `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` es el nombre de una fuente instalada
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, una fuente compuesta
|
||||
@msg give fonts: [{name, parts}…] indique fonts: [{name, parts}…]
|
||||
@msg path is empty path está vacío
|
||||
@msg fonts must be a list of composite fonts fonts debe ser una lista de fuentes compuestas
|
||||
@msg names must be strings names debe contener cadenas
|
||||
@msg no composite font named `{n}` no hay ninguna fuente compuesta llamada `{n}`
|
||||
@msg names must be a list of names names debe ser una lista de nombres
|
||||
@msg no composite fonts to export no hay fuentes compuestas para exportar
|
||||
@msg not a composite fonts file: {e} no es un archivo de fuentes compuestas: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list no es un archivo de fuentes compuestas: no hay lista compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts el archivo contiene más de {COMPOSITE_FONTS_MAX} fuentes compuestas
|
||||
@msg the file is larger than 4 MB el archivo ocupa más de 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 no es base64
|
||||
@msg give one of path, data or dataBase64 indique path, data o dataBase64
|
||||
@msg the file holds no composite fonts el archivo no contiene fuentes compuestas
|
||||
@msg the file holds two composite fonts named `{_1}` el archivo contiene dos fuentes compuestas llamadas `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` no puede nombrar una fuente compuesta
|
||||
@msg {k}: give a font {k}: indique una fuente
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: indique una fuente
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size debe estar entre {_1} y {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size debe estar entre {_1} y {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline debe estar entre {_1} y {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline debe estar entre {_1} y {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: la escala debe estar entre {_1} y {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: la escala debe estar entre {_1} y {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman y numerals se pueden escalar
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: solo kana, roman y numerals se pueden escalar
|
||||
@msg {k}: only kana can be scaled at the center {k}: solo kana se puede escalar desde el centro
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: solo kana se puede escalar desde el centro
|
||||
@msg no composite font ninguna fuente compuesta
|
||||
Delete Font Eliminar fuente
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5969,6 +6012,7 @@ mark-style Japanese Japonesas
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un efecto 3D en el relleno o el trazo de un grupo no dibuja nada que exportar
|
||||
@msg The 3D object is too complex to export El objeto 3D es demasiado complejo para exportarlo
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) el objeto no tiene ningún efecto 3D (Extrusión y biselado, Revolución, Inflar o Rotar)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` debe ser un objeto que solo contenga `mode`, con el valor shaded o wireframe
|
||||
@msg its 3D effect builds no solid su efecto 3D no construye ningún sólido
|
||||
@msg bevel must be none or one of {_1} bevel debe ser none o uno de estos: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} no puede construir sólidos a partir de esta ilustración: sus instancias de símbolo, fusiones y grupos de recorte contienen demasiados objetos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -523,6 +523,7 @@
|
||||
Rotate (Classic)… Kierrä (perinteinen)…
|
||||
Surface Pinta
|
||||
Wireframe Rautalanka
|
||||
Render Settings Renderöintiasetukset
|
||||
No Shading Ei varjostusta
|
||||
Plastic Muovi
|
||||
Highlight Intensity Kiillon voimakkuus
|
||||
@@ -1637,6 +1638,48 @@ digits Malayalam Malajalam
|
||||
Composite Fonts Yhdistelmäfontit
|
||||
Export Composite Fonts Vie yhdistelmäfontit
|
||||
Import Composite Fonts Tuo yhdistelmäfontit
|
||||
@msg each font must be {name, parts} jokaisen fontin on oltava {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts tuntematon avain `{k}`; fontti ottaa name ja parts
|
||||
@msg each font needs a name jokainen fontti tarvitsee nimen
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ei kelpaa yhdistelmäfontin nimeksi (1–{COMPOSITE_NAME_MAX} merkkiä, ei ohjausmerkkejä)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts on oltava objekti
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: tuntematon luokka `{k}`; luokat ovat kanji, kana, punctuation, symbols, roman ja numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} on oltava objekti
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: tuntematon avain `{k}` osassa {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: anna osa kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts asiakirjassa voi olla enintään {COMPOSITE_FONTS_MAX} yhdistelmäfonttia
|
||||
@msg two composite fonts are named `{_1}` kahden yhdistelmäfontin nimi on `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` on asennetun fontin nimi
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} käyttää fonttia `{font}`, joka on yhdistelmäfontti
|
||||
@msg give fonts: [{name, parts}…] anna fonts: [{name, parts}…]
|
||||
@msg path is empty path on tyhjä
|
||||
@msg fonts must be a list of composite fonts fonts on oltava luettelo yhdistelmäfonteista
|
||||
@msg names must be strings names on oltava merkkijonoja
|
||||
@msg no composite font named `{n}` yhdistelmäfonttia `{n}` ei ole
|
||||
@msg names must be a list of names names on oltava luettelo nimistä
|
||||
@msg no composite fonts to export ei vietäviä yhdistelmäfontteja
|
||||
@msg not a composite fonts file: {e} ei yhdistelmäfonttitiedosto: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ei yhdistelmäfonttitiedosto: luettelo compositeFonts puuttuu
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts tiedostossa on yli {COMPOSITE_FONTS_MAX} yhdistelmäfonttia
|
||||
@msg the file is larger than 4 MB tiedosto on suurempi kuin 4 Mt
|
||||
@msg dataBase64 is not base64 dataBase64 ei ole base64-muotoa
|
||||
@msg give one of path, data or dataBase64 anna path, data tai dataBase64
|
||||
@msg the file holds no composite fonts tiedostossa ei ole yhdistelmäfontteja
|
||||
@msg the file holds two composite fonts named `{_1}` tiedostossa on kaksi yhdistelmäfonttia nimeltä `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` ei kelpaa yhdistelmäfontin nimeksi
|
||||
@msg {k}: give a font {k}: anna fontti
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: anna fontti
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size on oltava {_1}–{_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size on oltava {_1}–{_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline on oltava {_1}–{_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline on oltava {_1}–{_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skaalauksen on oltava {_1}–{_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skaalauksen on oltava {_1}–{_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: vain kana, roman ja numerals voidaan skaalata
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: vain kana, roman ja numerals voidaan skaalata
|
||||
@msg {k}: only kana can be scaled at the center {k}: vain kana voidaan skaalata keskeltä
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: vain kana voidaan skaalata keskeltä
|
||||
@msg no composite font ei yhdistelmäfonttia
|
||||
Delete Font Poista fontti
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5911,6 +5954,7 @@ mark-style Japanese Japanilainen
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ryhmän täytön tai viivan 3D-tehoste ei piirrä mitään vietävää
|
||||
@msg The 3D object is too complex to export 3D-objekti on liian monimutkainen vietäväksi
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektilla ei ole 3D-tehostetta (Pursota ja viistä, Pyöräytä, Täytä ilmalla tai Kierrä)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` on oltava objekti, jossa on vain `mode` arvolla shaded tai wireframe
|
||||
@msg its 3D effect builds no solid sen 3D-tehoste ei muodosta kappaletta
|
||||
@msg bevel must be none or one of {_1} bevel-arvon on oltava none tai jokin seuraavista: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} ei voi muodostaa kappaleita tästä kuvituksesta: sen symbolien esiintymät, sulautukset ja leikkausryhmät sisältävät liikaa objekteja
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1180,6 +1180,7 @@ font-technology Variable Variables
|
||||
Rotate (Classic)… Rotation (classique)…
|
||||
Surface Surface
|
||||
Wireframe Fil de fer
|
||||
Render Settings Paramètres de rendu
|
||||
No Shading Sans ombrage
|
||||
Plastic Plastique
|
||||
Highlight Intensity Intensité de la brillance
|
||||
@@ -3695,6 +3696,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Polices composites
|
||||
Export Composite Fonts Exporter des polices composites
|
||||
Import Composite Fonts Importer des polices composites
|
||||
@msg each font must be {name, parts} chaque police doit être {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts clé inconnue `{k}` ; une police prend name et parts
|
||||
@msg each font needs a name chaque police doit avoir un nom
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ne peut pas nommer une police composite (1 à {COMPOSITE_NAME_MAX} caractères, sans caractères de contrôle)
|
||||
@msg `{name}`: parts must be an object `{name}` : parts doit être un objet
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}` : classe inconnue `{k}` ; les classes sont kanji, kana, punctuation, symbols, roman et numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}` : {_1} doit être un objet
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}` : clé inconnue `{k}` dans {_1}
|
||||
@msg `{name}`: give the kanji part `{name}` : indiquez la partie kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un document contient au plus {COMPOSITE_FONTS_MAX} polices composites
|
||||
@msg two composite fonts are named `{_1}` deux polices composites s’appellent `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` est le nom d’une police installée
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}` : {_2} utilise `{font}`, une police composite
|
||||
@msg give fonts: [{name, parts}…] indiquez fonts : [{name, parts}…]
|
||||
@msg path is empty path est vide
|
||||
@msg fonts must be a list of composite fonts fonts doit être une liste de polices composites
|
||||
@msg names must be strings names doit contenir des chaînes
|
||||
@msg no composite font named `{n}` aucune police composite nommée `{n}`
|
||||
@msg names must be a list of names names doit être une liste de noms
|
||||
@msg no composite fonts to export aucune police composite à exporter
|
||||
@msg not a composite fonts file: {e} ce n’est pas un fichier de polices composites : {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ce n’est pas un fichier de polices composites : pas de liste compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts le fichier contient plus de {COMPOSITE_FONTS_MAX} polices composites
|
||||
@msg the file is larger than 4 MB le fichier dépasse 4 Mo
|
||||
@msg dataBase64 is not base64 dataBase64 n’est pas du base64
|
||||
@msg give one of path, data or dataBase64 indiquez path, data ou dataBase64
|
||||
@msg the file holds no composite fonts le fichier ne contient aucune police composite
|
||||
@msg the file holds two composite fonts named `{_1}` le fichier contient deux polices composites nommées `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` ne peut pas nommer une police composite
|
||||
@msg {k}: give a font {k} : indiquez une police
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k} : indiquez une police
|
||||
@msg {k}: size must be {_1} to {_2}% {k} : size doit être compris entre {_1} et {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k} : size doit être compris entre {_1} et {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k} : baseline doit être compris entre {_1} et {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k} : baseline doit être compris entre {_1} et {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k} : l’échelle doit être comprise entre {_1} et {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k} : l’échelle doit être comprise entre {_1} et {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k} : seuls kana, roman et numerals peuvent être mis à l’échelle
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k} : seuls kana, roman et numerals peuvent être mis à l’échelle
|
||||
@msg {k}: only kana can be scaled at the center {k} : seul kana peut être mis à l’échelle depuis le centre
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k} : seul kana peut être mis à l’échelle depuis le centre
|
||||
@msg no composite font aucune police composite
|
||||
Delete Font Supprimer la police
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5594,6 +5637,7 @@ lighting Light Lumière
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un effet 3D sur le fond ou le contour d'un groupe ne dessine rien à exporter
|
||||
@msg The 3D object is too complex to export L'objet 3D est trop complexe pour être exporté
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l'objet n'a aucun effet 3D (Extrusion et biseautage, Révolution, Gonfler ou Rotation)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` doit être un objet ne contenant que `mode`, avec la valeur shaded ou wireframe
|
||||
@msg its 3D effect builds no solid son effet 3D ne construit aucun volume
|
||||
@msg bevel must be none or one of {_1} bevel doit valoir none ou l’une de ces valeurs : {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} ne peut pas construire de volumes à partir de cette illustration : ses occurrences de symbole, dégradés de formes et groupes de découpe contiennent trop d'objets
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4181,6 +4181,7 @@
|
||||
Width: પહોળાઈ:
|
||||
Window વિન્ડો
|
||||
Wireframe વાયરફ્રેમ
|
||||
Render Settings રેન્ડર સેટિંગ્સ
|
||||
Word Spacing: શબ્દ અંતર:
|
||||
Words Longer Than: આનાથી લાંબા શબ્દો:
|
||||
Working CMYK કાર્યકારી CMYK
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 72 and column 5.
|
@@ -1826,6 +1826,7 @@ antiAlias Sharp शार्प
|
||||
Rotate (Classic)… घुमाव (क्लासिक)…
|
||||
Surface सतह
|
||||
Wireframe वायरफ़्रेम
|
||||
Render Settings रेंडर सेटिंग्स
|
||||
No Shading कोई शेडिंग नहीं
|
||||
Plastic प्लास्टिक
|
||||
Highlight Intensity हाइलाइट तीव्रता
|
||||
@@ -5073,6 +5074,48 @@ antiAlias Sharp शार्प
|
||||
Composite Fonts कंपोज़िट फ़ॉन्ट
|
||||
Export Composite Fonts कंपोज़िट फ़ॉन्ट एक्सपोर्ट करें
|
||||
Import Composite Fonts कंपोज़िट फ़ॉन्ट इम्पोर्ट करें
|
||||
@msg each font must be {name, parts} हर फ़ॉन्ट {name, parts} होना चाहिए
|
||||
@msg unknown key `{k}`; a font takes name and parts अज्ञात कुंजी `{k}`; फ़ॉन्ट name और parts लेता है
|
||||
@msg each font needs a name हर फ़ॉन्ट का एक नाम होना चाहिए
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता (1 से {COMPOSITE_NAME_MAX} अक्षर, कोई नियंत्रण अक्षर नहीं)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts एक ऑब्जेक्ट होना चाहिए
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: अज्ञात वर्ग `{k}`; वर्ग kanji, kana, punctuation, symbols, roman और numerals हैं
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} एक ऑब्जेक्ट होना चाहिए
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} में अज्ञात कुंजी `{k}`
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji भाग दें
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts एक दस्तावेज़ में अधिकतम {COMPOSITE_FONTS_MAX} कंपोज़िट फ़ॉन्ट रहते हैं
|
||||
@msg two composite fonts are named `{_1}` दो कंपोज़िट फ़ॉन्ट का नाम `{_1}` है
|
||||
@msg `{_1}` is the name of an installed font `{_1}` एक इंस्टॉल किए गए फ़ॉन्ट का नाम है
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} `{font}` में सेट है, जो एक कंपोज़िट फ़ॉन्ट है
|
||||
@msg give fonts: [{name, parts}…] fonts दें: [{name, parts}…]
|
||||
@msg path is empty path खाली है
|
||||
@msg fonts must be a list of composite fonts fonts कंपोज़िट फ़ॉन्ट की सूची होनी चाहिए
|
||||
@msg names must be strings names स्ट्रिंग होने चाहिए
|
||||
@msg no composite font named `{n}` `{n}` नाम का कोई कंपोज़िट फ़ॉन्ट नहीं है
|
||||
@msg names must be a list of names names नामों की सूची होनी चाहिए
|
||||
@msg no composite fonts to export एक्सपोर्ट करने के लिए कोई कंपोज़िट फ़ॉन्ट नहीं है
|
||||
@msg not a composite fonts file: {e} यह कंपोज़िट फ़ॉन्ट फ़ाइल नहीं है: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list यह कंपोज़िट फ़ॉन्ट फ़ाइल नहीं है: compositeFonts सूची नहीं है
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts फ़ाइल में {COMPOSITE_FONTS_MAX} से अधिक कंपोज़िट फ़ॉन्ट हैं
|
||||
@msg the file is larger than 4 MB फ़ाइल 4 MB से बड़ी है
|
||||
@msg dataBase64 is not base64 dataBase64 base64 नहीं है
|
||||
@msg give one of path, data or dataBase64 path, data या dataBase64 में से एक दें
|
||||
@msg the file holds no composite fonts फ़ाइल में कोई कंपोज़िट फ़ॉन्ट नहीं है
|
||||
@msg the file holds two composite fonts named `{_1}` फ़ाइल में `{_1}` नाम के दो कंपोज़िट फ़ॉन्ट हैं
|
||||
@msg `{_1}` can't name a composite font `{_1}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता
|
||||
@msg {k}: give a font {k}: एक फ़ॉन्ट दें
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: एक फ़ॉन्ट दें
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size {_1} से {_2}% तक होना चाहिए
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size {_1} से {_2}% तक होना चाहिए
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline {_1} से {_2}% तक होना चाहिए
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline {_1} से {_2}% तक होना चाहिए
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: स्केल {_1} से {_2}% तक होना चाहिए
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: स्केल {_1} से {_2}% तक होना चाहिए
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: केवल kana, roman और numerals को स्केल किया जा सकता है
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: केवल kana, roman और numerals को स्केल किया जा सकता है
|
||||
@msg {k}: only kana can be scaled at the center {k}: केंद्र से केवल kana को स्केल किया जा सकता है
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: केंद्र से केवल kana को स्केल किया जा सकता है
|
||||
@msg no composite font कोई कंपोज़िट फ़ॉन्ट नहीं
|
||||
Delete Font फ़ॉन्ट हटाएँ
|
||||
Kanji कांजी
|
||||
Kana काना
|
||||
@@ -8047,6 +8090,7 @@ antiAlias Sharp शार्प
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ग्रुप के फ़िल या स्ट्रोक पर 3D इफ़ेक्ट एक्सपोर्ट करने लायक कुछ नहीं बनाता
|
||||
@msg The 3D object is too complex to export 3D ऑब्जेक्ट एक्सपोर्ट करने के लिए बहुत जटिल है
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) ऑब्जेक्ट पर कोई 3D इफ़ेक्ट नहीं है (एक्सट्रूडेड और बेवेल, रिवॉल्व, फुलाएँ या घुमाव)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` केवल `mode` वाला ऑब्जेक्ट होना चाहिए, जिसका मान shaded या wireframe हो
|
||||
@msg its 3D effect builds no solid इसका 3D इफ़ेक्ट कोई ठोस नहीं बनाता
|
||||
@msg bevel must be none or one of {_1} bevel को none या {_1} में से एक होना चाहिए
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} इस आर्ट से ठोस नहीं बना सकता: इसके सिंबल इंस्टेंस, ब्लेंड और क्लिपिंग ग्रुप में बहुत अधिक ऑब्जेक्ट हैं
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1479,6 +1479,7 @@ font-technology Variable Változó
|
||||
Rotate (Classic)… Elforgatás (klasszikus)…
|
||||
Surface Felület
|
||||
Wireframe Drótváz
|
||||
Render Settings Renderelési beállítások
|
||||
No Shading Nincs árnyékolás
|
||||
Plastic Műanyag
|
||||
Highlight Intensity Csillanás erőssége
|
||||
@@ -4371,6 +4372,48 @@ mark-style Japanese Japán
|
||||
Composite Fonts Összetett betűtípusok
|
||||
Export Composite Fonts Összetett betűtípusok exportálása
|
||||
Import Composite Fonts Összetett betűtípusok importálása
|
||||
@msg each font must be {name, parts} minden betűtípusnak {name, parts} alakúnak kell lennie
|
||||
@msg unknown key `{k}`; a font takes name and parts ismeretlen kulcs: `{k}`; egy betűtípus name és parts kulcsot fogad
|
||||
@msg each font needs a name minden betűtípusnak kell egy név
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nem lehet összetett betűtípus neve (1–{COMPOSITE_NAME_MAX} karakter, vezérlőkarakterek nélkül)
|
||||
@msg `{name}`: parts must be an object `{name}`: a parts értékének objektumnak kell lennie
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: ismeretlen osztály: `{k}`; az osztályok: kanji, kana, punctuation, symbols, roman és numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: a(z) {_1} értékének objektumnak kell lennie
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: ismeretlen kulcs: `{k}` itt: {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: adja meg a kanji részt
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts egy dokumentum legfeljebb {COMPOSITE_FONTS_MAX} összetett betűtípust tartalmazhat
|
||||
@msg two composite fonts are named `{_1}` két összetett betűtípus neve is `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` egy telepített betűtípus neve
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: a(z) {_2} a(z) `{font}` összetett betűtípust használja
|
||||
@msg give fonts: [{name, parts}…] adja meg: fonts: [{name, parts}…]
|
||||
@msg path is empty a path üres
|
||||
@msg fonts must be a list of composite fonts a fonts értékének összetett betűtípusok listájának kell lennie
|
||||
@msg names must be strings a names elemeinek karakterláncoknak kell lenniük
|
||||
@msg no composite font named `{n}` nincs `{n}` nevű összetett betűtípus
|
||||
@msg names must be a list of names a names értékének nevek listájának kell lennie
|
||||
@msg no composite fonts to export nincs exportálható összetett betűtípus
|
||||
@msg not a composite fonts file: {e} nem összetettbetűtípus-fájl: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list nem összetettbetűtípus-fájl: nincs compositeFonts lista
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts a fájl több mint {COMPOSITE_FONTS_MAX} összetett betűtípust tartalmaz
|
||||
@msg the file is larger than 4 MB a fájl nagyobb 4 MB-nál
|
||||
@msg dataBase64 is not base64 a dataBase64 nem base64
|
||||
@msg give one of path, data or dataBase64 adja meg a path, data vagy dataBase64 egyikét
|
||||
@msg the file holds no composite fonts a fájl nem tartalmaz összetett betűtípust
|
||||
@msg the file holds two composite fonts named `{_1}` a fájl két `{_1}` nevű összetett betűtípust tartalmaz
|
||||
@msg `{_1}` can't name a composite font `{_1}` nem lehet összetett betűtípus neve
|
||||
@msg {k}: give a font {k}: adjon meg egy betűtípust
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: adjon meg egy betűtípust
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: a size értéke {_1}–{_2}% lehet
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: a size értéke {_1}–{_2}% lehet
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: a baseline értéke {_1}–{_2}% lehet
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: a baseline értéke {_1}–{_2}% lehet
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: a méretezés {_1}–{_2}% lehet
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: a méretezés {_1}–{_2}% lehet
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: csak a kana, roman és numerals méretezhető
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: csak a kana, roman és numerals méretezhető
|
||||
@msg {k}: only kana can be scaled at the center {k}: csak a kana méretezhető a középpontból
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: csak a kana méretezhető a középpontból
|
||||
@msg no composite font nincs összetett betűtípus
|
||||
Delete Font Betűtípus törlése
|
||||
Kanji Kandzsi
|
||||
Kana Kana
|
||||
@@ -6009,6 +6052,7 @@ imageTrace Outline Körvonal
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export a csoport kitöltésén vagy körvonalán lévő 3D effektus nem rajzol semmi exportálhatót
|
||||
@msg The 3D object is too complex to export A 3D objektum túl összetett az exportáláshoz
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) az objektumnak nincs 3D effektusa (Kihúzás és élletörés, Forgatás tengely körül, Felfújás vagy Elforgatás)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe a `render` csak `mode` mezőt tartalmazó objektum lehet, shaded vagy wireframe értékkel
|
||||
@msg its 3D effect builds no solid a 3D effektusa nem hoz létre testet
|
||||
@msg bevel must be none or one of {_1} a bevel értéke none vagy ezek egyike lehet: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: ebből a rajzból nem hozható létre test: a szimbólumpéldányai, összemosásai és vágócsoportjai túl sok objektumot tartalmaznak
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -480,6 +480,7 @@
|
||||
Rotate (Classic)… Putar (klasik)…
|
||||
Surface Permukaan
|
||||
Wireframe Kerangka kawat
|
||||
Render Settings Pengaturan render
|
||||
No Shading Tanpa pengarsiran
|
||||
Plastic Plastik
|
||||
Highlight Intensity Intensitas kilau
|
||||
|
||||
|
@@ -1194,6 +1194,7 @@ font-technology Variable Variabili
|
||||
Rotate (Classic)… Ruota (classico)…
|
||||
Surface Superficie
|
||||
Wireframe Reticolo
|
||||
Render Settings Impostazioni di rendering
|
||||
No Shading Nessuna ombreggiatura
|
||||
Plastic Plastica
|
||||
Highlight Intensity Intensità lucentezza
|
||||
@@ -3749,6 +3750,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Font compositi
|
||||
Export Composite Fonts Esporta font compositi
|
||||
Import Composite Fonts Importa font compositi
|
||||
@msg each font must be {name, parts} ogni font deve essere {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts chiave sconosciuta `{k}`; un font accetta name e parts
|
||||
@msg each font needs a name ogni font deve avere un nome
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` non può essere il nome di un font composito (da 1 a {COMPOSITE_NAME_MAX} caratteri, senza caratteri di controllo)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts deve essere un oggetto
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe sconosciuta `{k}`; le classi sono kanji, kana, punctuation, symbols, roman e numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} deve essere un oggetto
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: chiave sconosciuta `{k}` in {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: indica la parte kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un documento contiene al massimo {COMPOSITE_FONTS_MAX} font compositi
|
||||
@msg two composite fonts are named `{_1}` due font compositi si chiamano `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` è il nome di un font installato
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, un font composito
|
||||
@msg give fonts: [{name, parts}…] indica fonts: [{name, parts}…]
|
||||
@msg path is empty path è vuoto
|
||||
@msg fonts must be a list of composite fonts fonts deve essere un elenco di font compositi
|
||||
@msg names must be strings names deve contenere stringhe
|
||||
@msg no composite font named `{n}` nessun font composito di nome `{n}`
|
||||
@msg names must be a list of names names deve essere un elenco di nomi
|
||||
@msg no composite fonts to export nessun font composito da esportare
|
||||
@msg not a composite fonts file: {e} non è un file di font compositi: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list non è un file di font compositi: manca l’elenco compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts il file contiene più di {COMPOSITE_FONTS_MAX} font compositi
|
||||
@msg the file is larger than 4 MB il file supera 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 non è base64
|
||||
@msg give one of path, data or dataBase64 indica path, data o dataBase64
|
||||
@msg the file holds no composite fonts il file non contiene font compositi
|
||||
@msg the file holds two composite fonts named `{_1}` il file contiene due font compositi di nome `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` non può essere il nome di un font composito
|
||||
@msg {k}: give a font {k}: indica un font
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: indica un font
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size deve essere tra {_1} e {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size deve essere tra {_1} e {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve essere tra {_1} e {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline deve essere tra {_1} e {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: la scala deve essere tra {_1} e {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: la scala deve essere tra {_1} e {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman e numerals si possono ridimensionare
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: solo kana, roman e numerals si possono ridimensionare
|
||||
@msg {k}: only kana can be scaled at the center {k}: solo kana si può ridimensionare dal centro
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: solo kana si può ridimensionare dal centro
|
||||
@msg no composite font nessun font composito
|
||||
Delete Font Elimina font
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5642,6 +5685,7 @@ lighting Light Luce
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export un effetto 3D sul riempimento o sulla traccia di un gruppo non disegna nulla da esportare
|
||||
@msg The 3D object is too complex to export L'oggetto 3D è troppo complesso per essere esportato
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) l'oggetto non ha alcun effetto 3D (Estrusione e smusso, Rivoluzione, Gonfia o Ruota)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` deve essere un oggetto che contiene solo `mode`, con valore shaded o wireframe
|
||||
@msg its 3D effect builds no solid il suo effetto 3D non costruisce alcun solido
|
||||
@msg bevel must be none or one of {_1} bevel deve essere none o uno dei seguenti: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} non può costruire solidi da questo disegno: le sue istanze di simbolo, fusioni e gruppi di ritaglio contengono troppi oggetti
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -570,6 +570,7 @@ digits Malayalam マラヤーラム数字
|
||||
Rotate (Classic)… 回転(クラシック)…
|
||||
Surface 表面
|
||||
Wireframe ワイヤーフレーム
|
||||
Render Settings レンダリング設定
|
||||
No Shading 陰影なし
|
||||
Plastic プラスチック
|
||||
Highlight Intensity ハイライトの強さ
|
||||
@@ -4077,6 +4078,48 @@ file-filter Templates テンプレート
|
||||
Composite Fonts 合成フォント
|
||||
Export Composite Fonts 合成フォントを書き出し
|
||||
Import Composite Fonts 合成フォントを読み込み
|
||||
@msg each font must be {name, parts} 各フォントは {name, parts} の形にしてください
|
||||
@msg unknown key `{k}`; a font takes name and parts `{k}` は不明なキーです。フォントに使えるのは name と parts です
|
||||
@msg each font needs a name 各フォントには名前が必要です
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` は合成フォントの名前にできません (1~{COMPOSITE_NAME_MAX} 文字、制御文字なし)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts はオブジェクトにしてください
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: `{k}` は不明なクラスです。クラスは kanji、kana、punctuation、symbols、roman、numerals です
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} はオブジェクトにしてください
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} の `{k}` は不明なキーです
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji の部分を指定してください
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 1 つのドキュメントに置ける合成フォントは {COMPOSITE_FONTS_MAX} 個までです
|
||||
@msg two composite fonts are named `{_1}` `{_1}` という名前の合成フォントが 2 つあります
|
||||
@msg `{_1}` is the name of an installed font `{_1}` はインストール済みのフォントの名前です
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} に合成フォントの `{font}` が設定されています
|
||||
@msg give fonts: [{name, parts}…] fonts を指定してください: [{name, parts}…]
|
||||
@msg path is empty path が空です
|
||||
@msg fonts must be a list of composite fonts fonts は合成フォントのリストにしてください
|
||||
@msg names must be strings names は文字列にしてください
|
||||
@msg no composite font named `{n}` `{n}` という合成フォントはありません
|
||||
@msg names must be a list of names names は名前のリストにしてください
|
||||
@msg no composite fonts to export 書き出す合成フォントがありません
|
||||
@msg not a composite fonts file: {e} 合成フォントのファイルではありません: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list 合成フォントのファイルではありません: compositeFonts のリストがありません
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ファイルに {COMPOSITE_FONTS_MAX} 個を超える合成フォントがあります
|
||||
@msg the file is larger than 4 MB ファイルが 4 MB を超えています
|
||||
@msg dataBase64 is not base64 dataBase64 が base64 ではありません
|
||||
@msg give one of path, data or dataBase64 path、data、dataBase64 のいずれかを指定してください
|
||||
@msg the file holds no composite fonts ファイルに合成フォントがありません
|
||||
@msg the file holds two composite fonts named `{_1}` ファイルに `{_1}` という名前の合成フォントが 2 つあります
|
||||
@msg `{_1}` can't name a composite font `{_1}` は合成フォントの名前にできません
|
||||
@msg {k}: give a font {k}: フォントを指定してください
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: フォントを指定してください
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size は {_1}~{_2}% にしてください
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size は {_1}~{_2}% にしてください
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline は {_1}~{_2}% にしてください
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline は {_1}~{_2}% にしてください
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: 比率は {_1}~{_2}% にしてください
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: 比率は {_1}~{_2}% にしてください
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: 比率を変更できるのは kana、roman、numerals だけです
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: 比率を変更できるのは kana、roman、numerals だけです
|
||||
@msg {k}: only kana can be scaled at the center {k}: 中央を基準に比率を変更できるのは kana だけです
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: 中央を基準に比率を変更できるのは kana だけです
|
||||
@msg no composite font 合成フォントがありません
|
||||
Delete Font フォントを削除
|
||||
Kanji 漢字
|
||||
Kana かな
|
||||
@@ -5684,6 +5727,7 @@ lighting Light ライト
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export グループの塗りまたは線にある 3D 効果には書き出せる描画がありません
|
||||
@msg The 3D object is too complex to export 3D オブジェクトが複雑すぎるため書き出せません
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) オブジェクトに 3D 効果 (押し出しとベベル、回転体、膨張、回転) がありません
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` は `mode` だけを持ち、その値が shaded または wireframe のオブジェクトにしてください
|
||||
@msg its 3D effect builds no solid 3D 効果からソリッドが作成されません
|
||||
@msg bevel must be none or one of {_1} bevel には none または {_1} のいずれかを指定してください
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} はこのアートからソリッドを作成できません。シンボルインスタンス、ブレンド、クリッピンググループに含まれるオブジェクトが多すぎます
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -452,6 +452,7 @@
|
||||
Rotate (Classic)… შემობრუნება (კლასიკური)…
|
||||
Surface ზედაპირი
|
||||
Wireframe მავთულის ჩარჩო
|
||||
Render Settings რენდერის პარამეტრები
|
||||
No Shading დაჩრდილვის გარეშე
|
||||
Plastic პლასტიკური
|
||||
Highlight Intensity ელვარების ინტენსივობა
|
||||
@@ -1508,6 +1509,48 @@ digits Malayalam მალაიალამური
|
||||
Composite Fonts კომპოზიტური შრიფტები
|
||||
Export Composite Fonts კომპოზიტური შრიფტების ექსპორტი
|
||||
Import Composite Fonts კომპოზიტური შრიფტების იმპორტი
|
||||
@msg each font must be {name, parts} თითოეული შრიფტი უნდა იყოს {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts უცნობი გასაღები `{k}`; შრიფტი იღებს name-სა და parts-ს
|
||||
@msg each font needs a name თითოეულ შრიფტს სახელი სჭირდება
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ვერ იქნება კომპოზიტური შრიფტის სახელი (1-დან {COMPOSITE_NAME_MAX} სიმბოლომდე, მმართველი სიმბოლოების გარეშე)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts უნდა იყოს ობიექტი
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: უცნობი კლასი `{k}`; კლასებია: kanji, kana, punctuation, symbols, roman და numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} უნდა იყოს ობიექტი
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: უცნობი გასაღები `{k}` ნაწილში {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: მიუთითეთ ნაწილი kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts დოკუმენტი შეიცავს არაუმეტეს {COMPOSITE_FONTS_MAX} კომპოზიტურ შრიფტს
|
||||
@msg two composite fonts are named `{_1}` ორ კომპოზიტურ შრიფტს ჰქვია `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` დაინსტალირებული შრიფტის სახელია
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} იყენებს `{font}`-ს, რომელიც კომპოზიტური შრიფტია
|
||||
@msg give fonts: [{name, parts}…] მიუთითეთ fonts: [{name, parts}…]
|
||||
@msg path is empty path ცარიელია
|
||||
@msg fonts must be a list of composite fonts fonts უნდა იყოს კომპოზიტური შრიფტების სია
|
||||
@msg names must be strings names უნდა იყოს სტრიქონები
|
||||
@msg no composite font named `{n}` კომპოზიტური შრიფტი სახელით `{n}` არ არსებობს
|
||||
@msg names must be a list of names names უნდა იყოს სახელების სია
|
||||
@msg no composite fonts to export საექსპორტო კომპოზიტური შრიფტები არ არის
|
||||
@msg not a composite fonts file: {e} ეს არ არის კომპოზიტური შრიფტების ფაილი: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list ეს არ არის კომპოზიტური შრიფტების ფაილი: სია compositeFonts არ არის
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ფაილი შეიცავს {COMPOSITE_FONTS_MAX}-ზე მეტ კომპოზიტურ შრიფტს
|
||||
@msg the file is larger than 4 MB ფაილი 4 MB-ზე დიდია
|
||||
@msg dataBase64 is not base64 dataBase64 არ არის base64
|
||||
@msg give one of path, data or dataBase64 მიუთითეთ path, data ან dataBase64
|
||||
@msg the file holds no composite fonts ფაილი კომპოზიტურ შრიფტებს არ შეიცავს
|
||||
@msg the file holds two composite fonts named `{_1}` ფაილი შეიცავს ორ კომპოზიტურ შრიფტს სახელით `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` ვერ იქნება კომპოზიტური შრიფტის სახელი
|
||||
@msg {k}: give a font {k}: მიუთითეთ შრიფტი
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: მიუთითეთ შრიფტი
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში
|
||||
@msg {k}: only kana can be scaled at the center {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში
|
||||
@msg no composite font კომპოზიტური შრიფტი არ არის
|
||||
Delete Font შრიფტის წაშლა
|
||||
Kanji კანძი
|
||||
Kana კანა
|
||||
@@ -5309,6 +5352,7 @@ font-technology Variable ცვლადი
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export ჯგუფის შევსებაზე ან კონტურზე 3D ეფექტი ექსპორტისთვის არაფერს ხატავს
|
||||
@msg The 3D object is too complex to export 3D ობიექტი ზედმეტად რთულია ექსპორტისთვის
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) ობიექტს არ აქვს 3D ეფექტი (ამოწევა და წაკვეთა, ბრუნვა, გაბერვა ან შემობრუნება)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` უნდა იყოს ობიექტი, რომელიც შეიცავს მხოლოდ `mode`-ს, მნიშვნელობით shaded ან wireframe
|
||||
@msg its 3D effect builds no solid მისი 3D ეფექტი სხეულს არ ქმნის
|
||||
@msg bevel must be none or one of {_1} bevel უნდა იყოს none ან ერთ-ერთი: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: ამ ნახატიდან სხეულების შექმნა შეუძლებელია: მისი სიმბოლოს ეგზემპლარები, გადასვლები და მომჭრელი ჯგუფები ზედმეტად ბევრ ობიექტს შეიცავს
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -563,6 +563,7 @@ digits Malayalam 말라얄람어
|
||||
Rotate (Classic)… 회전(클래식)…
|
||||
Surface 표면
|
||||
Wireframe 와이어프레임
|
||||
Render Settings 렌더링 설정
|
||||
No Shading 셰이딩 없음
|
||||
Plastic 플라스틱
|
||||
Highlight Intensity 하이라이트 강도
|
||||
@@ -4065,6 +4066,48 @@ file-filter Templates 템플릿
|
||||
Composite Fonts 합성 글꼴
|
||||
Export Composite Fonts 합성 글꼴 내보내기
|
||||
Import Composite Fonts 합성 글꼴 가져오기
|
||||
@msg each font must be {name, parts} 각 글꼴은 {name, parts} 형식이어야 합니다
|
||||
@msg unknown key `{k}`; a font takes name and parts 알 수 없는 키 `{k}`: 글꼴에는 name과 parts만 쓸 수 있습니다
|
||||
@msg each font needs a name 각 글꼴에는 이름이 필요합니다
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다(1~{COMPOSITE_NAME_MAX}자, 제어 문자 없음)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts는 객체여야 합니다
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: 알 수 없는 클래스 `{k}`: 클래스는 kanji, kana, punctuation, symbols, roman, numerals입니다
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1}은(는) 객체여야 합니다
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1}에 알 수 없는 키 `{k}`이(가) 있습니다
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji 부분을 지정하세요
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 문서에는 합성 글꼴을 최대 {COMPOSITE_FONTS_MAX}개까지 둘 수 있습니다
|
||||
@msg two composite fonts are named `{_1}` 이름이 `{_1}`인 합성 글꼴이 두 개 있습니다
|
||||
@msg `{_1}` is the name of an installed font `{_1}`은(는) 설치된 글꼴의 이름입니다
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2}이(가) 합성 글꼴인 `{font}`(으)로 설정되어 있습니다
|
||||
@msg give fonts: [{name, parts}…] fonts를 지정하세요: [{name, parts}…]
|
||||
@msg path is empty path가 비어 있습니다
|
||||
@msg fonts must be a list of composite fonts fonts는 합성 글꼴 목록이어야 합니다
|
||||
@msg names must be strings names는 문자열이어야 합니다
|
||||
@msg no composite font named `{n}` 이름이 `{n}`인 합성 글꼴이 없습니다
|
||||
@msg names must be a list of names names는 이름 목록이어야 합니다
|
||||
@msg no composite fonts to export 내보낼 합성 글꼴이 없습니다
|
||||
@msg not a composite fonts file: {e} 합성 글꼴 파일이 아닙니다: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list 합성 글꼴 파일이 아닙니다: compositeFonts 목록이 없습니다
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 파일에 합성 글꼴이 {COMPOSITE_FONTS_MAX}개보다 많습니다
|
||||
@msg the file is larger than 4 MB 파일이 4MB보다 큽니다
|
||||
@msg dataBase64 is not base64 dataBase64가 base64가 아닙니다
|
||||
@msg give one of path, data or dataBase64 path, data, dataBase64 중 하나를 지정하세요
|
||||
@msg the file holds no composite fonts 파일에 합성 글꼴이 없습니다
|
||||
@msg the file holds two composite fonts named `{_1}` 파일에 이름이 `{_1}`인 합성 글꼴이 두 개 있습니다
|
||||
@msg `{_1}` can't name a composite font `{_1}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다
|
||||
@msg {k}: give a font {k}: 글꼴을 지정하세요
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: 글꼴을 지정하세요
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size는 {_1}~{_2}%여야 합니다
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size는 {_1}~{_2}%여야 합니다
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline은 {_1}~{_2}%여야 합니다
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline은 {_1}~{_2}%여야 합니다
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: 비율은 {_1}~{_2}%여야 합니다
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: 비율은 {_1}~{_2}%여야 합니다
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다
|
||||
@msg {k}: only kana can be scaled at the center {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다
|
||||
@msg no composite font 합성 글꼴이 없습니다
|
||||
Delete Font 글꼴 삭제
|
||||
Kanji 한자
|
||||
Kana 가나
|
||||
@@ -5603,6 +5646,7 @@ lighting Light 광원
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 그룹의 칠이나 획에 있는 3D 효과는 내보낼 것을 그리지 않습니다
|
||||
@msg The 3D object is too complex to export 3D 개체가 너무 복잡하여 내보낼 수 없습니다
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) 개체에 3D 효과(돌출 및 경사, 회전체, 부풀리기 또는 회전)가 없습니다
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render`는 값이 shaded 또는 wireframe인 `mode`만 있는 객체여야 합니다
|
||||
@msg its 3D effect builds no solid 3D 효과가 입체를 만들지 않습니다
|
||||
@msg bevel must be none or one of {_1} bevel은 none 또는 {_1} 중 하나여야 합니다
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}은(는) 이 아트로 입체를 만들 수 없습니다. 심볼 인스턴스, 블렌드, 클리핑 그룹에 개체가 너무 많습니다
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -998,6 +998,7 @@
|
||||
Rotate (Classic)… Roteren (klassiek)…
|
||||
Surface Oppervlak
|
||||
Wireframe Draadmodel
|
||||
Render Settings Renderinstellingen
|
||||
No Shading Geen arcering
|
||||
Plastic Plastic
|
||||
Highlight Intensity Glansintensiteit
|
||||
@@ -2088,6 +2089,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Samengestelde lettertypen
|
||||
Export Composite Fonts Samengestelde lettertypen exporteren
|
||||
Import Composite Fonts Samengestelde lettertypen importeren
|
||||
@msg each font must be {name, parts} elk lettertype moet {name, parts} zijn
|
||||
@msg unknown key `{k}`; a font takes name and parts onbekende sleutel `{k}`; een lettertype neemt name en parts
|
||||
@msg each font needs a name elk lettertype heeft een naam nodig
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan geen naam van een samengesteld lettertype zijn (1 tot {COMPOSITE_NAME_MAX} tekens, geen besturingstekens)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts moet een object zijn
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: onbekende klasse `{k}`; de klassen zijn kanji, kana, punctuation, symbols, roman en numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} moet een object zijn
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: onbekende sleutel `{k}` in {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: geef het deel kanji op
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts een document bevat hoogstens {COMPOSITE_FONTS_MAX} samengestelde lettertypen
|
||||
@msg two composite fonts are named `{_1}` twee samengestelde lettertypen heten `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` is de naam van een geïnstalleerd lettertype
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} gebruikt `{font}`, een samengesteld lettertype
|
||||
@msg give fonts: [{name, parts}…] geef fonts op: [{name, parts}…]
|
||||
@msg path is empty path is leeg
|
||||
@msg fonts must be a list of composite fonts fonts moet een lijst van samengestelde lettertypen zijn
|
||||
@msg names must be strings names moeten tekenreeksen zijn
|
||||
@msg no composite font named `{n}` geen samengesteld lettertype met de naam `{n}`
|
||||
@msg names must be a list of names names moet een lijst van namen zijn
|
||||
@msg no composite fonts to export geen samengestelde lettertypen om te exporteren
|
||||
@msg not a composite fonts file: {e} geen bestand met samengestelde lettertypen: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list geen bestand met samengestelde lettertypen: geen lijst compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts het bestand bevat meer dan {COMPOSITE_FONTS_MAX} samengestelde lettertypen
|
||||
@msg the file is larger than 4 MB het bestand is groter dan 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 is geen base64
|
||||
@msg give one of path, data or dataBase64 geef path, data of dataBase64 op
|
||||
@msg the file holds no composite fonts het bestand bevat geen samengestelde lettertypen
|
||||
@msg the file holds two composite fonts named `{_1}` het bestand bevat twee samengestelde lettertypen met de naam `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kan geen naam van een samengesteld lettertype zijn
|
||||
@msg {k}: give a font {k}: geef een lettertype op
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: geef een lettertype op
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size moet tussen {_1} en {_2}% liggen
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size moet tussen {_1} en {_2}% liggen
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline moet tussen {_1} en {_2}% liggen
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline moet tussen {_1} en {_2}% liggen
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: de schaal moet tussen {_1} en {_2}% liggen
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: de schaal moet tussen {_1} en {_2}% liggen
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: alleen kana, roman en numerals kunnen worden geschaald
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: alleen kana, roman en numerals kunnen worden geschaald
|
||||
@msg {k}: only kana can be scaled at the center {k}: alleen kana kan vanuit het midden worden geschaald
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: alleen kana kan vanuit het midden worden geschaald
|
||||
@msg no composite font geen samengesteld lettertype
|
||||
Delete Font Lettertype verwijderen
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -7784,6 +7827,7 @@ mark-style Japanese Japans
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export een 3D-effect op de vulling of contour van een groep tekent niets om te exporteren
|
||||
@msg The 3D object is too complex to export Het 3D-object is te complex om te exporteren
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) het object heeft geen 3D-effect (Extruderen en afschuinen, Draaien, Opblazen of Roteren)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` moet een object zijn met alleen `mode`, ingesteld op shaded of wireframe
|
||||
@msg its 3D effect builds no solid het 3D-effect ervan maakt geen massief object
|
||||
@msg bevel must be none or one of {_1} bevel moet none of een van {_1} zijn
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kan van deze illustratie geen massieve objecten maken: de symboolinstanties, vermengingen en knipgroepen ervan bevatten te veel objecten
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -485,6 +485,7 @@
|
||||
Rotate (Classic)… Obróć (klasyczny)…
|
||||
Surface Powierzchnia
|
||||
Wireframe Szkielet
|
||||
Render Settings Ustawienia renderowania
|
||||
No Shading Bez cieniowania
|
||||
Plastic Plastik
|
||||
Highlight Intensity Intensywność połysku
|
||||
@@ -1576,6 +1577,48 @@ digits Malayalam Malajalam
|
||||
Composite Fonts Fonty złożone
|
||||
Export Composite Fonts Eksportuj fonty złożone
|
||||
Import Composite Fonts Importuj fonty złożone
|
||||
@msg each font must be {name, parts} każdy font musi mieć postać {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts nieznany klucz `{k}`; font przyjmuje name i parts
|
||||
@msg each font needs a name każdy font musi mieć nazwę
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nie może być nazwą fontu złożonego (od 1 do {COMPOSITE_NAME_MAX} znaków, bez znaków sterujących)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts musi być obiektem
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: nieznana klasa `{k}`; klasy to kanji, kana, punctuation, symbols, roman i numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} musi być obiektem
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: nieznany klucz `{k}` w {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: podaj część kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts dokument może zawierać najwyżej {COMPOSITE_FONTS_MAX} fontów złożonych
|
||||
@msg two composite fonts are named `{_1}` dwa fonty złożone mają nazwę `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` to nazwa zainstalowanego fontu
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} używa `{font}`, który jest fontem złożonym
|
||||
@msg give fonts: [{name, parts}…] podaj fonts: [{name, parts}…]
|
||||
@msg path is empty path jest puste
|
||||
@msg fonts must be a list of composite fonts fonts musi być listą fontów złożonych
|
||||
@msg names must be strings names muszą być ciągami znaków
|
||||
@msg no composite font named `{n}` brak fontu złożonego o nazwie `{n}`
|
||||
@msg names must be a list of names names musi być listą nazw
|
||||
@msg no composite fonts to export brak fontów złożonych do eksportu
|
||||
@msg not a composite fonts file: {e} to nie jest plik fontów złożonych: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list to nie jest plik fontów złożonych: brak listy compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts plik zawiera więcej niż {COMPOSITE_FONTS_MAX} fontów złożonych
|
||||
@msg the file is larger than 4 MB plik jest większy niż 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 nie jest w formacie base64
|
||||
@msg give one of path, data or dataBase64 podaj path, data lub dataBase64
|
||||
@msg the file holds no composite fonts plik nie zawiera fontów złożonych
|
||||
@msg the file holds two composite fonts named `{_1}` plik zawiera dwa fonty złożone o nazwie `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` nie może być nazwą fontu złożonego
|
||||
@msg {k}: give a font {k}: podaj font
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: podaj font
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size musi wynosić od {_1} do {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size musi wynosić od {_1} do {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline musi wynosić od {_1} do {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline musi wynosić od {_1} do {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skala musi wynosić od {_1} do {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skala musi wynosić od {_1} do {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: skalować można tylko kana, roman i numerals
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: skalować można tylko kana, roman i numerals
|
||||
@msg {k}: only kana can be scaled at the center {k}: od środka można skalować tylko kana
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: od środka można skalować tylko kana
|
||||
@msg no composite font brak fontu złożonego
|
||||
Delete Font Usuń font
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5857,6 +5900,7 @@ mark-style Japanese Japońskie
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export efekt 3D na wypełnieniu lub obrysie grupy nie rysuje niczego do eksportu
|
||||
@msg The 3D object is too complex to export Obiekt 3D jest zbyt złożony, aby go wyeksportować
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) obiekt nie ma efektu 3D (Wyciągnięcie i fazowanie, Obrót, Nadmuchanie ani Obróć)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` musi być obiektem zawierającym tylko `mode` o wartości shaded lub wireframe
|
||||
@msg its 3D effect builds no solid jego efekt 3D nie tworzy żadnej bryły
|
||||
@msg bevel must be none or one of {_1} bevel musi mieć wartość none lub jedną z: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} nie może utworzyć brył z tego rysunku: jego wystąpienia symboli, przejścia i grupy przycinające zawierają zbyt wiele obiektów
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1076,6 +1076,7 @@ antiAlias Sharp Nítido
|
||||
Rotate (Classic)… Girar (Clássico)…
|
||||
Surface Superfície
|
||||
Wireframe Aramado
|
||||
Render Settings Configurações de renderização
|
||||
No Shading Sem Sombreamento
|
||||
Plastic Plástico
|
||||
Highlight Intensity Intensidade do Realce
|
||||
@@ -5269,6 +5270,48 @@ axis Both Ambos
|
||||
Composite Fonts Fontes compostas
|
||||
Export Composite Fonts Exportar fontes compostas
|
||||
Import Composite Fonts Importar fontes compostas
|
||||
@msg each font must be {name, parts} cada fonte deve ser {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts chave desconhecida `{k}`; uma fonte aceita name e parts
|
||||
@msg each font needs a name cada fonte precisa de um nome
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` não pode nomear uma fonte composta (de 1 a {COMPOSITE_NAME_MAX} caracteres, sem caracteres de controle)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts deve ser um objeto
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe desconhecida `{k}`; as classes são kanji, kana, punctuation, symbols, roman e numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} deve ser um objeto
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: chave desconhecida `{k}` em {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: informe a parte kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts um documento contém no máximo {COMPOSITE_FONTS_MAX} fontes compostas
|
||||
@msg two composite fonts are named `{_1}` duas fontes compostas se chamam `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` é o nome de uma fonte instalada
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, uma fonte composta
|
||||
@msg give fonts: [{name, parts}…] informe fonts: [{name, parts}…]
|
||||
@msg path is empty path está vazio
|
||||
@msg fonts must be a list of composite fonts fonts deve ser uma lista de fontes compostas
|
||||
@msg names must be strings names deve conter strings
|
||||
@msg no composite font named `{n}` nenhuma fonte composta chamada `{n}`
|
||||
@msg names must be a list of names names deve ser uma lista de nomes
|
||||
@msg no composite fonts to export nenhuma fonte composta para exportar
|
||||
@msg not a composite fonts file: {e} não é um arquivo de fontes compostas: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list não é um arquivo de fontes compostas: não há lista compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts o arquivo contém mais de {COMPOSITE_FONTS_MAX} fontes compostas
|
||||
@msg the file is larger than 4 MB o arquivo tem mais de 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 não é base64
|
||||
@msg give one of path, data or dataBase64 informe path, data ou dataBase64
|
||||
@msg the file holds no composite fonts o arquivo não contém fontes compostas
|
||||
@msg the file holds two composite fonts named `{_1}` o arquivo contém duas fontes compostas chamadas `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` não pode nomear uma fonte composta
|
||||
@msg {k}: give a font {k}: informe uma fonte
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: informe uma fonte
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size deve estar entre {_1} e {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size deve estar entre {_1} e {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve estar entre {_1} e {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline deve estar entre {_1} e {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: a escala deve estar entre {_1} e {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: a escala deve estar entre {_1} e {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: só kana, roman e numerals podem ser escalados
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: só kana, roman e numerals podem ser escalados
|
||||
@msg {k}: only kana can be scaled at the center {k}: só kana pode ser escalado a partir do centro
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: só kana pode ser escalado a partir do centro
|
||||
@msg no composite font nenhuma fonte composta
|
||||
Delete Font Excluir fonte
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5978,6 +6021,7 @@ mark-style Japanese Japonesas
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export um efeito 3D no preenchimento ou traçado de um grupo não desenha nada para exportar
|
||||
@msg The 3D object is too complex to export O objeto 3D é complexo demais para ser exportado
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) o objeto não tem efeito 3D (Extrusão e Bisel, Revolver, Inflar ou Girar)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` deve ser um objeto que contenha apenas `mode`, com o valor shaded ou wireframe
|
||||
@msg its 3D effect builds no solid seu efeito 3D não constrói nenhum sólido
|
||||
@msg bevel must be none or one of {_1} bevel deve ser none ou um de {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} não pode construir sólidos a partir desta arte: suas instâncias de símbolo, mesclagens e grupos de recorte contêm objetos demais
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1183,6 +1183,7 @@ font-technology Variable Вариативные
|
||||
Rotate (Classic)… Поворот (классический)…
|
||||
Surface Поверхность
|
||||
Wireframe Каркас
|
||||
Render Settings Параметры рендеринга
|
||||
No Shading Без затенения
|
||||
Plastic Пластик
|
||||
Highlight Intensity Интенсивность блика
|
||||
@@ -3735,6 +3736,48 @@ digits Malayalam Малаялам
|
||||
Composite Fonts Составные шрифты
|
||||
Export Composite Fonts Экспорт составных шрифтов
|
||||
Import Composite Fonts Импорт составных шрифтов
|
||||
@msg each font must be {name, parts} каждый шрифт должен иметь вид {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts неизвестный ключ `{k}`; шрифт принимает name и parts
|
||||
@msg each font needs a name у каждого шрифта должно быть имя
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` не может быть именем составного шрифта (от 1 до {COMPOSITE_NAME_MAX} символов, без управляющих символов)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts должен быть объектом
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: неизвестный класс `{k}`; классы: kanji, kana, punctuation, symbols, roman и numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} должен быть объектом
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: неизвестный ключ `{k}` в {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: укажите часть kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts документ содержит не более {COMPOSITE_FONTS_MAX} составных шрифтов
|
||||
@msg two composite fonts are named `{_1}` два составных шрифта называются `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` является именем установленного шрифта
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} использует `{font}`, а это составной шрифт
|
||||
@msg give fonts: [{name, parts}…] укажите fonts: [{name, parts}…]
|
||||
@msg path is empty path пуст
|
||||
@msg fonts must be a list of composite fonts fonts должен быть списком составных шрифтов
|
||||
@msg names must be strings names должны быть строками
|
||||
@msg no composite font named `{n}` нет составного шрифта с именем `{n}`
|
||||
@msg names must be a list of names names должен быть списком имён
|
||||
@msg no composite fonts to export нет составных шрифтов для экспорта
|
||||
@msg not a composite fonts file: {e} это не файл составных шрифтов: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list это не файл составных шрифтов: нет списка compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts в файле больше {COMPOSITE_FONTS_MAX} составных шрифтов
|
||||
@msg the file is larger than 4 MB файл больше 4 МБ
|
||||
@msg dataBase64 is not base64 dataBase64 не в формате base64
|
||||
@msg give one of path, data or dataBase64 укажите path, data или dataBase64
|
||||
@msg the file holds no composite fonts в файле нет составных шрифтов
|
||||
@msg the file holds two composite fonts named `{_1}` в файле два составных шрифта с именем `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` не может быть именем составного шрифта
|
||||
@msg {k}: give a font {k}: укажите шрифт
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: укажите шрифт
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size должен быть от {_1} до {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size должен быть от {_1} до {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline должен быть от {_1} до {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline должен быть от {_1} до {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: масштаб должен быть от {_1} до {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: масштаб должен быть от {_1} до {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: масштабировать можно только kana, roman и numerals
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: масштабировать можно только kana, roman и numerals
|
||||
@msg {k}: only kana can be scaled at the center {k}: масштабировать от центра можно только kana
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: масштабировать от центра можно только kana
|
||||
@msg no composite font нет составного шрифта
|
||||
Delete Font Удалить шрифт
|
||||
Kanji Кандзи
|
||||
Kana Кана
|
||||
@@ -5629,6 +5672,7 @@ lighting Light Свет
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D-эффект на заливке или обводке группы не рисует ничего для экспорта
|
||||
@msg The 3D object is too complex to export 3D-объект слишком сложен для экспорта
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) у объекта нет 3D-эффекта (Выдавливание и фаска, Вращение, Надувание или Повернуть)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` должен быть объектом только с полем `mode` со значением shaded или wireframe
|
||||
@msg its 3D effect builds no solid его 3D-эффект не строит тела
|
||||
@msg bevel must be none or one of {_1} bevel должен быть none или одним из: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: нельзя построить тела из этой иллюстрации: её экземпляры символов, переходы и обтравочные группы содержат слишком много объектов
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1164,6 +1164,7 @@ font-technology Variable Variabla
|
||||
Rotate (Classic)… Rotera (klassisk)…
|
||||
Surface Yta
|
||||
Wireframe Trådram
|
||||
Render Settings Renderingsinställningar
|
||||
No Shading Ingen skuggning
|
||||
Plastic Plast
|
||||
Highlight Intensity Högdagerintensitet
|
||||
@@ -3625,6 +3626,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Sammansatta typsnitt
|
||||
Export Composite Fonts Exportera sammansatta typsnitt
|
||||
Import Composite Fonts Importera sammansatta typsnitt
|
||||
@msg each font must be {name, parts} varje typsnitt måste vara {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts okänd nyckel `{k}`; ett typsnitt tar name och parts
|
||||
@msg each font needs a name varje typsnitt behöver ett namn
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan inte vara namnet på ett sammansatt typsnitt (1 till {COMPOSITE_NAME_MAX} tecken, inga styrtecken)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts måste vara ett objekt
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: okänd klass `{k}`; klasserna är kanji, kana, punctuation, symbols, roman och numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} måste vara ett objekt
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: okänd nyckel `{k}` i {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: ange delen kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ett dokument har högst {COMPOSITE_FONTS_MAX} sammansatta typsnitt
|
||||
@msg two composite fonts are named `{_1}` två sammansatta typsnitt heter `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` är namnet på ett installerat typsnitt
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} använder `{font}`, ett sammansatt typsnitt
|
||||
@msg give fonts: [{name, parts}…] ange fonts: [{name, parts}…]
|
||||
@msg path is empty path är tom
|
||||
@msg fonts must be a list of composite fonts fonts måste vara en lista med sammansatta typsnitt
|
||||
@msg names must be strings names måste vara strängar
|
||||
@msg no composite font named `{n}` inget sammansatt typsnitt heter `{n}`
|
||||
@msg names must be a list of names names måste vara en lista med namn
|
||||
@msg no composite fonts to export inga sammansatta typsnitt att exportera
|
||||
@msg not a composite fonts file: {e} inte en fil med sammansatta typsnitt: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list inte en fil med sammansatta typsnitt: ingen lista compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts filen innehåller fler än {COMPOSITE_FONTS_MAX} sammansatta typsnitt
|
||||
@msg the file is larger than 4 MB filen är större än 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 är inte base64
|
||||
@msg give one of path, data or dataBase64 ange path, data eller dataBase64
|
||||
@msg the file holds no composite fonts filen innehåller inga sammansatta typsnitt
|
||||
@msg the file holds two composite fonts named `{_1}` filen innehåller två sammansatta typsnitt som heter `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` kan inte vara namnet på ett sammansatt typsnitt
|
||||
@msg {k}: give a font {k}: ange ett typsnitt
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: ange ett typsnitt
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size måste vara {_1} till {_2} %
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size måste vara {_1} till {_2} %
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline måste vara {_1} till {_2} %
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline måste vara {_1} till {_2} %
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: skalan måste vara {_1} till {_2} %
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skalan måste vara {_1} till {_2} %
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: bara kana, roman och numerals kan skalas
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: bara kana, roman och numerals kan skalas
|
||||
@msg {k}: only kana can be scaled at the center {k}: bara kana kan skalas från mitten
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: bara kana kan skalas från mitten
|
||||
@msg no composite font inget sammansatt typsnitt
|
||||
Delete Font Ta bort typsnitt
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5491,6 +5534,7 @@ lighting Light Ljus
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export en 3D-effekt på en grupps fyllning eller linje ritar ingenting att exportera
|
||||
@msg The 3D object is too complex to export 3D-objektet är för komplext för att exporteras
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) objektet har ingen 3D-effekt (Extrudera och fasa, Rotera runt axel, Blås upp eller Rotera)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` måste vara ett objekt som bara innehåller `mode`, med värdet shaded eller wireframe
|
||||
@msg its 3D effect builds no solid dess 3D-effekt bygger ingen solid
|
||||
@msg bevel must be none or one of {_1} bevel måste vara none eller något av {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} kan inte bygga solider av den här teckningen: dess symbolinstanser, blandningar och urklippsgrupper innehåller för många objekt
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2175,6 +2175,87 @@ fn recolor_notes_on_objects_left_unchanged_translate_with_their_counts() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every complete-message catalog has a row for each error that `type.compositeFonts.set`,
|
||||
/// `.export` and `.import`, the composite font checks behind them and the Composite Fonts dialog
|
||||
/// give, and translates them with the font name, the key and the class key kept, and the reason
|
||||
/// after a font's name translated in turn.
|
||||
#[test]
|
||||
fn composite_font_errors_translate_in_complete_languages() {
|
||||
const SOURCES: &[&str] = &[
|
||||
"each font must be {name, parts}",
|
||||
"unknown key `{k}`; a font takes name and parts",
|
||||
"each font needs a name",
|
||||
"`{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters)",
|
||||
"`{name}`: parts must be an object",
|
||||
"`{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals",
|
||||
"`{name}`: {_1} must be an object",
|
||||
"`{name}`: unknown key `{k}` in {_1}",
|
||||
"`{name}`: give the kanji part",
|
||||
"a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts",
|
||||
"two composite fonts are named `{_1}`",
|
||||
"`{_1}` is the name of an installed font",
|
||||
"`{_1}`: {_2} is set in `{font}`, a composite font",
|
||||
"give fonts: [{name, parts}…]",
|
||||
"path is empty",
|
||||
"fonts must be a list of composite fonts",
|
||||
"names must be strings",
|
||||
"no composite font named `{n}`",
|
||||
"names must be a list of names",
|
||||
"no composite fonts to export",
|
||||
"not a composite fonts file: {e}",
|
||||
"not a composite fonts file: no compositeFonts list",
|
||||
"the file holds more than {COMPOSITE_FONTS_MAX} composite fonts",
|
||||
"the file is larger than 4 MB",
|
||||
"dataBase64 is not base64",
|
||||
"give one of path, data or dataBase64",
|
||||
"the file holds no composite fonts",
|
||||
"the file holds two composite fonts named `{_1}`",
|
||||
"`{_1}` can't name a composite font",
|
||||
"{k}: give a font",
|
||||
"{k}: size must be {_1} to {_2}%",
|
||||
"{k}: baseline must be {_1} to {_2}%",
|
||||
"{k}: scale must be {_1} to {_2}%",
|
||||
"{k}: only kana, roman and numerals can be scaled",
|
||||
"{k}: only kana can be scaled at the center",
|
||||
"`{name}`: {k}: give a font",
|
||||
"`{name}`: {k}: size must be {_1} to {_2}%",
|
||||
"`{name}`: {k}: baseline must be {_1} to {_2}%",
|
||||
"`{name}`: {k}: scale must be {_1} to {_2}%",
|
||||
"`{name}`: {k}: only kana, roman and numerals can be scaled",
|
||||
"`{name}`: {k}: only kana can be scaled at the center",
|
||||
"no composite font",
|
||||
];
|
||||
// An error, English words its translation replaces, and the text it keeps.
|
||||
let cases: &[(&str, &str, &[&str])] = &[
|
||||
(
|
||||
"`Mincho Mix`: unknown class `kanjj`; the classes are kanji, kana, punctuation, symbols, roman and numerals",
|
||||
"the classes are",
|
||||
&["`Mincho Mix`", "`kanjj`", "kanji", "kana", "punctuation", "symbols", "roman", "numerals"],
|
||||
),
|
||||
("`Mincho Mix`: give the kanji part", "give the", &["`Mincho Mix`", "kanji"]),
|
||||
("`Mincho Mix`: parts must be an object", "must be an object", &["`Mincho Mix`", "parts"]),
|
||||
("`Mincho Mix`: kana must be an object", "must be an object", &["`Mincho Mix`", "kana"]),
|
||||
("`Mincho Mix`: unknown key `weight` in roman", "unknown key", &["`Mincho Mix`", "`weight`", "roman"]),
|
||||
("two composite fonts are named `Mincho Mix`", "are named", &["`Mincho Mix`"]),
|
||||
("`Mincho Mix`: kana: give a font", "give a font", &["`Mincho Mix`", "kana"]),
|
||||
("not a composite fonts file: the file is larger than 4 MB", "larger than", &["4"]),
|
||||
];
|
||||
for code in COMPLETE_MESSAGES {
|
||||
let lang = Lang::from_code(code).expect("registered");
|
||||
let (entries, _) = parse_entries(lang.0.source);
|
||||
let rows: Vec<&str> = entries.iter().filter(|(ctx, _, _)| ctx == "@msg").map(|(_, src, _)| src.as_str()).collect();
|
||||
let missing: Vec<&&str> = SOURCES.iter().filter(|s| !rows.contains(s)).collect();
|
||||
assert!(missing.is_empty(), "{code} has no row for {missing:?}");
|
||||
for (error, english, kept) in cases {
|
||||
let shown = message(lang, error);
|
||||
assert!(!shown.contains(english), "{code}: {shown}");
|
||||
for k in *kept {
|
||||
assert!(shown.contains(k), "{code}: {k} in {shown}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spanish shows the image encoders', color profiles', swatch exchange files', Revolve's, DXF
|
||||
/// import's, the script engine's and the parameter checks' errors in Spanish, with their values,
|
||||
/// backticked names and accepted values kept as the English writes them.
|
||||
@@ -4176,8 +4257,13 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("column must be true or a number from 1", &["column"]),
|
||||
("column: true floats the whole column and takes no onto, above or below", &["column", "onto", "above", "below"]),
|
||||
("control must be fillStroke, coloring, drawMode or screenMode", &["control", "fillStroke", "coloring", "drawMode", "screenMode"]),
|
||||
("dataBase64 is not base64", &["dataBase64"]),
|
||||
("decimals must be {_1}–{_2} (got {_3})", &["decimals"]),
|
||||
("detect must be true or false", &["detect"]),
|
||||
("each font must be {name, parts}", &["name", "parts"]),
|
||||
("fonts must be a list of composite fonts", &["fonts"]),
|
||||
("give fonts: [{name, parts}…]", &["fonts", "name", "parts"]),
|
||||
("give one of path, data or dataBase64", &["path", "data", "dataBase64"]),
|
||||
("give origin: [x, y], or reset: true", &["origin", "reset"]),
|
||||
("global must be true or false", &["global"]),
|
||||
("group must be true or false", &["group"]),
|
||||
@@ -4196,12 +4282,16 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("member is for order front, forward, backward or back", &["member", "order", "front", "forward", "backward", "back"]),
|
||||
("mode is set, add or toggle, not `{other}`", &["mode", "set", "add", "toggle"]),
|
||||
("names is empty", &["names"]),
|
||||
("names must be a list of names", &["names"]),
|
||||
("names must be strings", &["names"]),
|
||||
("not a composite fonts file: no compositeFonts list", &["compositeFonts"]),
|
||||
("on must be true or false", &["on"]),
|
||||
("onto must name a floating panel", &["onto"]),
|
||||
("others must be true or false", &["others"]),
|
||||
("page must be a whole number from 1", &["page"]),
|
||||
("panel is not in a dock group", &["panel"]),
|
||||
("panel must name a registered panel", &["panel"]),
|
||||
("path is empty", &["path"]),
|
||||
("paths must be strings", &["paths"]),
|
||||
("percent must be above 0 and at most 6400, not {p}", &["percent"]),
|
||||
("plane must be left, right or ground", &["plane", "left", "right", "ground"]),
|
||||
@@ -4217,6 +4307,7 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("span: \"selection\" places a linear, radial, conic or diamond gradient, not a freeform one", &["span"]),
|
||||
("thickness must be from 0.01 to 5000 pt", &["thickness"]),
|
||||
("title can't be empty", &["title"]),
|
||||
("unknown key `{k}`; a font takes name and parts", &["name", "parts"]),
|
||||
(
|
||||
"unknown light setting: use color, intensity, rotation, height, softness or behind",
|
||||
&["color", "intensity", "rotation", "height", "softness", "behind"],
|
||||
@@ -4229,6 +4320,20 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("`strokeMode` applies to strokes (stroke: true)", &["stroke"]),
|
||||
("`{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}`", &["symbol", "face", "x", "y", "scale", "rotation"]),
|
||||
("`{GRAPHICS}` face must be front or back", &["face", "front", "back"]),
|
||||
("`{name}`: give the kanji part", &["kanji"]),
|
||||
("`{name}`: parts must be an object", &["parts"]),
|
||||
(
|
||||
"`{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals",
|
||||
&["kanji", "kana", "punctuation", "symbols", "roman", "numerals"],
|
||||
),
|
||||
("{k}: baseline must be {_1} to {_2}%", &["baseline"]),
|
||||
("{k}: only kana can be scaled at the center", &["kana"]),
|
||||
("{k}: only kana, roman and numerals can be scaled", &["kana", "roman", "numerals"]),
|
||||
("{k}: size must be {_1} to {_2}%", &["size"]),
|
||||
("`{name}`: {k}: baseline must be {_1} to {_2}%", &["baseline"]),
|
||||
("`{name}`: {k}: only kana can be scaled at the center", &["kana"]),
|
||||
("`{name}`: {k}: only kana, roman and numerals can be scaled", &["kana", "roman", "numerals"]),
|
||||
("`{name}`: {k}: size must be {_1} to {_2}%", &["size"]),
|
||||
];
|
||||
|
||||
/// Every catalog's translation of a [`NAMED_KEYS`] message keeps each name the table lists, as
|
||||
|
||||
@@ -1156,6 +1156,7 @@ font-technology Variable Değişken
|
||||
Rotate (Classic)… Çevir (Klasik)…
|
||||
Surface Yüzey
|
||||
Wireframe Tel Kafes
|
||||
Render Settings Oluşturma Ayarları
|
||||
No Shading Gölgeleme Yok
|
||||
Plastic Plastik
|
||||
Highlight Intensity Parlama Yoğunluğu
|
||||
@@ -3489,6 +3490,48 @@ digits Malayalam Malayalam
|
||||
Composite Fonts Bileşik Fontlar
|
||||
Export Composite Fonts Bileşik Fontları Dışa Aktar
|
||||
Import Composite Fonts Bileşik Fontları İçe Aktar
|
||||
@msg each font must be {name, parts} her font {name, parts} biçiminde olmalı
|
||||
@msg unknown key `{k}`; a font takes name and parts bilinmeyen anahtar `{k}`; bir font name ve parts alır
|
||||
@msg each font needs a name her fontun bir adı olmalı
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` bir bileşik fontun adı olamaz (1 ile {COMPOSITE_NAME_MAX} karakter arası, denetim karakteri olmadan)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts bir nesne olmalı
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: bilinmeyen sınıf `{k}`; sınıflar: kanji, kana, punctuation, symbols, roman ve numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} bir nesne olmalı
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} içinde bilinmeyen anahtar `{k}`
|
||||
@msg `{name}`: give the kanji part `{name}`: kanji bölümünü verin
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts bir belge en fazla {COMPOSITE_FONTS_MAX} bileşik font tutar
|
||||
@msg two composite fonts are named `{_1}` iki bileşik fontun adı `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` yüklü bir fontun adı
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2}, bir bileşik font olan `{font}` ile ayarlı
|
||||
@msg give fonts: [{name, parts}…] fonts verin: [{name, parts}…]
|
||||
@msg path is empty path boş
|
||||
@msg fonts must be a list of composite fonts fonts bir bileşik font listesi olmalı
|
||||
@msg names must be strings names dizeler olmalı
|
||||
@msg no composite font named `{n}` `{n}` adlı bileşik font yok
|
||||
@msg names must be a list of names names bir ad listesi olmalı
|
||||
@msg no composite fonts to export dışa aktarılacak bileşik font yok
|
||||
@msg not a composite fonts file: {e} bileşik font dosyası değil: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list bileşik font dosyası değil: compositeFonts listesi yok
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts dosyada {COMPOSITE_FONTS_MAX} taneden fazla bileşik font var
|
||||
@msg the file is larger than 4 MB dosya 4 MB'tan büyük
|
||||
@msg dataBase64 is not base64 dataBase64 base64 değil
|
||||
@msg give one of path, data or dataBase64 path, data veya dataBase64 değerlerinden birini verin
|
||||
@msg the file holds no composite fonts dosyada bileşik font yok
|
||||
@msg the file holds two composite fonts named `{_1}` dosyada `{_1}` adlı iki bileşik font var
|
||||
@msg `{_1}` can't name a composite font `{_1}` bir bileşik fontun adı olamaz
|
||||
@msg {k}: give a font {k}: bir font verin
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: bir font verin
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size %{_1} ile %{_2} arasında olmalı
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size %{_1} ile %{_2} arasında olmalı
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline %{_1} ile %{_2} arasında olmalı
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline %{_1} ile %{_2} arasında olmalı
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: ölçek %{_1} ile %{_2} arasında olmalı
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: ölçek %{_1} ile %{_2} arasında olmalı
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: yalnızca kana, roman ve numerals ölçeklenebilir
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: yalnızca kana, roman ve numerals ölçeklenebilir
|
||||
@msg {k}: only kana can be scaled at the center {k}: merkezden yalnızca kana ölçeklenebilir
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: merkezden yalnızca kana ölçeklenebilir
|
||||
@msg no composite font bileşik font yok
|
||||
Delete Font Fontu Sil
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5354,6 +5397,7 @@ lighting Light Işık
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export bir grubun dolgusundaki veya konturundaki 3 boyutlu efekt dışa aktarılacak hiçbir şey çizmez
|
||||
@msg The 3D object is too complex to export 3 boyutlu nesne dışa aktarmak için çok karmaşık
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) nesnenin 3 boyutlu efekti yok (Ekstrüzyon ve Eğim, Döndür, Şişir veya Döndür)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render`, yalnızca değeri shaded veya wireframe olan `mode` içeren bir nesne olmalıdır
|
||||
@msg its 3D effect builds no solid 3 boyutlu efekti hiçbir katı oluşturmuyor
|
||||
@msg bevel must be none or one of {_1} bevel, none veya {_1} değerlerinden biri olmalıdır
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} bu resimden katı oluşturamaz: sembol örnekleri, karışımları ve kırpma grupları çok fazla nesne içeriyor
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1188,6 +1188,7 @@ font-technology Variable Варіативні
|
||||
Rotate (Classic)… Поворот (класичний)…
|
||||
Surface Поверхня
|
||||
Wireframe Каркас
|
||||
Render Settings Параметри рендерингу
|
||||
No Shading Без затінення
|
||||
Plastic Пластик
|
||||
Highlight Intensity Інтенсивність відблиску
|
||||
@@ -3742,6 +3743,48 @@ effect Points Точки
|
||||
Composite Fonts Складені шрифти
|
||||
Export Composite Fonts Експорт складених шрифтів
|
||||
Import Composite Fonts Імпорт складених шрифтів
|
||||
@msg each font must be {name, parts} кожен шрифт має бути у вигляді {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts невідомий ключ `{k}`; шрифт приймає name і parts
|
||||
@msg each font needs a name кожен шрифт повинен мати назву
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` не може бути назвою складеного шрифту (від 1 до {COMPOSITE_NAME_MAX} символів, без керівних символів)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts має бути об’єктом
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: невідомий клас `{k}`; класи: kanji, kana, punctuation, symbols, roman і numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} має бути об’єктом
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: невідомий ключ `{k}` у {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: вкажіть частину kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts документ містить щонайбільше {COMPOSITE_FONTS_MAX} складених шрифтів
|
||||
@msg two composite fonts are named `{_1}` два складені шрифти мають назву `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` є назвою встановленого шрифту
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} використовує `{font}`, а це складений шрифт
|
||||
@msg give fonts: [{name, parts}…] вкажіть fonts: [{name, parts}…]
|
||||
@msg path is empty path порожній
|
||||
@msg fonts must be a list of composite fonts fonts має бути списком складених шрифтів
|
||||
@msg names must be strings names мають бути рядками
|
||||
@msg no composite font named `{n}` немає складеного шрифту з назвою `{n}`
|
||||
@msg names must be a list of names names має бути списком назв
|
||||
@msg no composite fonts to export немає складених шрифтів для експорту
|
||||
@msg not a composite fonts file: {e} це не файл складених шрифтів: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list це не файл складених шрифтів: немає списку compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts у файлі понад {COMPOSITE_FONTS_MAX} складених шрифтів
|
||||
@msg the file is larger than 4 MB файл більший за 4 МБ
|
||||
@msg dataBase64 is not base64 dataBase64 не у форматі base64
|
||||
@msg give one of path, data or dataBase64 вкажіть path, data або dataBase64
|
||||
@msg the file holds no composite fonts у файлі немає складених шрифтів
|
||||
@msg the file holds two composite fonts named `{_1}` у файлі два складені шрифти з назвою `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` не може бути назвою складеного шрифту
|
||||
@msg {k}: give a font {k}: вкажіть шрифт
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: вкажіть шрифт
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size має бути від {_1} до {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size має бути від {_1} до {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline має бути від {_1} до {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline має бути від {_1} до {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: масштаб має бути від {_1} до {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: масштаб має бути від {_1} до {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: масштабувати можна лише kana, roman і numerals
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: масштабувати можна лише kana, roman і numerals
|
||||
@msg {k}: only kana can be scaled at the center {k}: масштабувати від центру можна лише kana
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: масштабувати від центру можна лише kana
|
||||
@msg no composite font немає складеного шрифту
|
||||
Delete Font Видалити шрифт
|
||||
Kanji Кандзі
|
||||
Kana Кана
|
||||
@@ -5622,6 +5665,7 @@ lighting Light Світло
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 3D-ефект на заливці або обведенні групи не малює нічого для експорту
|
||||
@msg The 3D object is too complex to export 3D-об’єкт надто складний для експорту
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) об’єкт не має 3D-ефекту (Видавлювання та фаска, Обертання, Надування або Повернути)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` має бути об’єктом лише з полем `mode` зі значенням shaded або wireframe
|
||||
@msg its 3D effect builds no solid його 3D-ефект не будує тіла
|
||||
@msg bevel must be none or one of {_1} bevel має бути none або одним з: {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}: не вдається побудувати тіла з цієї ілюстрації: її екземпляри символів, переходи та відсічні групи містять забагато об’єктів
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1364,6 +1364,7 @@ font-technology Variable Biến thiên
|
||||
Rotate (Classic)… Xoay (cổ điển)…
|
||||
Surface Bề mặt
|
||||
Wireframe Khung dây
|
||||
Render Settings Cài đặt kết xuất
|
||||
No Shading Không tô bóng
|
||||
Plastic Nhựa
|
||||
Highlight Intensity Cường độ điểm sáng
|
||||
@@ -4088,6 +4089,48 @@ mark-style Japanese Kiểu Nhật
|
||||
Composite Fonts Phông chữ tổ hợp
|
||||
Export Composite Fonts Xuất phông chữ tổ hợp
|
||||
Import Composite Fonts Nhập phông chữ tổ hợp
|
||||
@msg each font must be {name, parts} mỗi phông chữ phải là {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts khóa không xác định `{k}`; một phông chữ nhận name và parts
|
||||
@msg each font needs a name mỗi phông chữ cần có tên
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` không thể là tên phông chữ tổ hợp (từ 1 đến {COMPOSITE_NAME_MAX} ký tự, không có ký tự điều khiển)
|
||||
@msg `{name}`: parts must be an object `{name}`: parts phải là một đối tượng
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: lớp không xác định `{k}`; các lớp là kanji, kana, punctuation, symbols, roman và numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`: {_1} phải là một đối tượng
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`: khóa không xác định `{k}` trong {_1}
|
||||
@msg `{name}`: give the kanji part `{name}`: hãy cung cấp phần kanji
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts một tài liệu chứa tối đa {COMPOSITE_FONTS_MAX} phông chữ tổ hợp
|
||||
@msg two composite fonts are named `{_1}` có hai phông chữ tổ hợp cùng tên `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` là tên của một phông chữ đã cài đặt
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} dùng `{font}`, một phông chữ tổ hợp
|
||||
@msg give fonts: [{name, parts}…] hãy cung cấp fonts: [{name, parts}…]
|
||||
@msg path is empty path trống
|
||||
@msg fonts must be a list of composite fonts fonts phải là danh sách phông chữ tổ hợp
|
||||
@msg names must be strings names phải là chuỗi
|
||||
@msg no composite font named `{n}` không có phông chữ tổ hợp nào tên `{n}`
|
||||
@msg names must be a list of names names phải là danh sách tên
|
||||
@msg no composite fonts to export không có phông chữ tổ hợp nào để xuất
|
||||
@msg not a composite fonts file: {e} không phải tệp phông chữ tổ hợp: {e}
|
||||
@msg not a composite fonts file: no compositeFonts list không phải tệp phông chữ tổ hợp: không có danh sách compositeFonts
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts tệp chứa hơn {COMPOSITE_FONTS_MAX} phông chữ tổ hợp
|
||||
@msg the file is larger than 4 MB tệp lớn hơn 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 không phải base64
|
||||
@msg give one of path, data or dataBase64 hãy cung cấp path, data hoặc dataBase64
|
||||
@msg the file holds no composite fonts tệp không chứa phông chữ tổ hợp nào
|
||||
@msg the file holds two composite fonts named `{_1}` tệp chứa hai phông chữ tổ hợp cùng tên `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` không thể là tên phông chữ tổ hợp
|
||||
@msg {k}: give a font {k}: hãy cung cấp một phông chữ
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}: hãy cung cấp một phông chữ
|
||||
@msg {k}: size must be {_1} to {_2}% {k}: size phải từ {_1} đến {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size phải từ {_1} đến {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}: baseline phải từ {_1} đến {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline phải từ {_1} đến {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}: tỷ lệ phải từ {_1} đến {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: tỷ lệ phải từ {_1} đến {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}: chỉ kana, roman và numerals mới co giãn được
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: chỉ kana, roman và numerals mới co giãn được
|
||||
@msg {k}: only kana can be scaled at the center {k}: chỉ kana mới co giãn từ tâm được
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: chỉ kana mới co giãn từ tâm được
|
||||
@msg no composite font không có phông chữ tổ hợp
|
||||
Delete Font Xóa phông chữ
|
||||
Kanji Kanji
|
||||
Kana Kana
|
||||
@@ -5412,6 +5455,7 @@ theme Appearance Giao diện
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export hiệu ứng 3D trên phần tô hoặc nét của nhóm không vẽ gì để xuất
|
||||
@msg The 3D object is too complex to export Đối tượng 3D quá phức tạp để xuất
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) đối tượng không có hiệu ứng 3D (Kéo dài & vát, Xoay tròn, Phình hoặc Xoay)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` phải là một đối tượng chỉ chứa `mode`, với giá trị shaded hoặc wireframe
|
||||
@msg its 3D effect builds no solid hiệu ứng 3D của nó không tạo ra khối nào
|
||||
@msg bevel must be none or one of {_1} bevel phải là none hoặc một trong {_1}
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1} không thể tạo khối từ hình vẽ này: các phiên bản ký hiệu, hòa trộn và nhóm cắt của nó chứa quá nhiều đối tượng
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -1125,6 +1125,7 @@ antiAlias Sharp 锐利
|
||||
Rotate (Classic)… 旋转 (经典)…
|
||||
Surface 表面
|
||||
Wireframe 线框
|
||||
Render Settings 渲染设置
|
||||
No Shading 无底纹
|
||||
Plastic 塑料
|
||||
Highlight Intensity 高光强度
|
||||
@@ -3563,6 +3564,48 @@ digits Malayalam 马拉雅拉姆数字
|
||||
Composite Fonts 复合字体
|
||||
Export Composite Fonts 导出复合字体
|
||||
Import Composite Fonts 导入复合字体
|
||||
@msg each font must be {name, parts} 每个字体都必须是 {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts 未知键 `{k}`;字体可用的键为 name 和 parts
|
||||
@msg each font needs a name 每个字体都需要名称
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` 不能用作复合字体的名称(1 到 {COMPOSITE_NAME_MAX} 个字符,不含控制字符)
|
||||
@msg `{name}`: parts must be an object `{name}`:parts 必须是对象
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`:未知类别 `{k}`;类别为 kanji、kana、punctuation、symbols、roman 和 numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`:{_1} 必须是对象
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`:{_1} 中有未知键 `{k}`
|
||||
@msg `{name}`: give the kanji part `{name}`:请指定 kanji 部分
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 一个文档最多保存 {COMPOSITE_FONTS_MAX} 个复合字体
|
||||
@msg two composite fonts are named `{_1}` 有两个复合字体都名为 `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` 是已安装字体的名称
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`:{_2} 使用了复合字体 `{font}`
|
||||
@msg give fonts: [{name, parts}…] 请指定 fonts:[{name, parts}…]
|
||||
@msg path is empty path 为空
|
||||
@msg fonts must be a list of composite fonts fonts 必须是复合字体列表
|
||||
@msg names must be strings names 必须是字符串
|
||||
@msg no composite font named `{n}` 没有名为 `{n}` 的复合字体
|
||||
@msg names must be a list of names names 必须是名称列表
|
||||
@msg no composite fonts to export 没有可导出的复合字体
|
||||
@msg not a composite fonts file: {e} 不是复合字体文件:{e}
|
||||
@msg not a composite fonts file: no compositeFonts list 不是复合字体文件:没有 compositeFonts 列表
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 文件包含超过 {COMPOSITE_FONTS_MAX} 个复合字体
|
||||
@msg the file is larger than 4 MB 文件大于 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 不是 base64
|
||||
@msg give one of path, data or dataBase64 请指定 path、data 或 dataBase64 之一
|
||||
@msg the file holds no composite fonts 文件不含复合字体
|
||||
@msg the file holds two composite fonts named `{_1}` 文件中有两个复合字体都名为 `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` 不能用作复合字体的名称
|
||||
@msg {k}: give a font {k}:请指定字体
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}:请指定字体
|
||||
@msg {k}: size must be {_1} to {_2}% {k}:size 必须为 {_1}% 到 {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}:size 必须为 {_1}% 到 {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}:baseline 必须为 {_1}% 到 {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}:baseline 必须为 {_1}% 到 {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}:缩放必须为 {_1}% 到 {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}:缩放必须为 {_1}% 到 {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}:只有 kana、roman 和 numerals 可以缩放
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}:只有 kana、roman 和 numerals 可以缩放
|
||||
@msg {k}: only kana can be scaled at the center {k}:只有 kana 可以从中心缩放
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}:只有 kana 可以从中心缩放
|
||||
@msg no composite font 没有复合字体
|
||||
Delete Font 删除字体
|
||||
Kanji 汉字
|
||||
Kana 假名
|
||||
@@ -5896,6 +5939,7 @@ lighting Light 光源
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 编组填色或描边上的 3D 效果没有可导出的内容
|
||||
@msg The 3D object is too complex to export 3D 对象过于复杂,无法导出
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) 该对象没有 3D 效果(突出与斜角、回转、膨胀或旋转)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` 必须是只含 `mode` 的对象,值为 shaded 或 wireframe
|
||||
@msg its 3D effect builds no solid 其 3D 效果未生成任何实体
|
||||
@msg bevel must be none or one of {_1} bevel 必须是 none 或 {_1} 之一
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}无法从此图稿生成实体:其符号实例、混合和剪切组包含的对象过多
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1158,6 +1158,7 @@ font-technology Variable 可變
|
||||
Rotate (Classic)… 旋轉 (經典)…
|
||||
Surface 表面
|
||||
Wireframe 線框
|
||||
Render Settings 演算設定
|
||||
No Shading 無網底
|
||||
Plastic 塑膠
|
||||
Highlight Intensity 亮部強度
|
||||
@@ -4824,6 +4825,48 @@ digits Malayalam 馬來亞拉姆數字
|
||||
Composite Fonts 複合字體
|
||||
Export Composite Fonts 匯出複合字體
|
||||
Import Composite Fonts 匯入複合字體
|
||||
@msg each font must be {name, parts} 每個字體都必須是 {name, parts}
|
||||
@msg unknown key `{k}`; a font takes name and parts 未知的鍵 `{k}`;字體可用的鍵為 name 和 parts
|
||||
@msg each font needs a name 每個字體都需要名稱
|
||||
@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` 不能作為複合字體的名稱(1 到 {COMPOSITE_NAME_MAX} 個字元,不含控制字元)
|
||||
@msg `{name}`: parts must be an object `{name}`:parts 必須是物件
|
||||
@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`:未知的類別 `{k}`;類別為 kanji、kana、punctuation、symbols、roman 和 numerals
|
||||
@msg `{name}`: {_1} must be an object `{name}`:{_1} 必須是物件
|
||||
@msg `{name}`: unknown key `{k}` in {_1} `{name}`:{_1} 中有未知的鍵 `{k}`
|
||||
@msg `{name}`: give the kanji part `{name}`:請指定 kanji 部分
|
||||
@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 一份文件最多保存 {COMPOSITE_FONTS_MAX} 個複合字體
|
||||
@msg two composite fonts are named `{_1}` 有兩個複合字體都名為 `{_1}`
|
||||
@msg `{_1}` is the name of an installed font `{_1}` 是已安裝字體的名稱
|
||||
@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`:{_2} 使用了複合字體 `{font}`
|
||||
@msg give fonts: [{name, parts}…] 請指定 fonts:[{name, parts}…]
|
||||
@msg path is empty path 為空
|
||||
@msg fonts must be a list of composite fonts fonts 必須是複合字體清單
|
||||
@msg names must be strings names 必須是字串
|
||||
@msg no composite font named `{n}` 沒有名為 `{n}` 的複合字體
|
||||
@msg names must be a list of names names 必須是名稱清單
|
||||
@msg no composite fonts to export 沒有可匯出的複合字體
|
||||
@msg not a composite fonts file: {e} 不是複合字體檔案:{e}
|
||||
@msg not a composite fonts file: no compositeFonts list 不是複合字體檔案:沒有 compositeFonts 清單
|
||||
@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 檔案包含超過 {COMPOSITE_FONTS_MAX} 個複合字體
|
||||
@msg the file is larger than 4 MB 檔案大於 4 MB
|
||||
@msg dataBase64 is not base64 dataBase64 不是 base64
|
||||
@msg give one of path, data or dataBase64 請指定 path、data 或 dataBase64 其中之一
|
||||
@msg the file holds no composite fonts 檔案不含複合字體
|
||||
@msg the file holds two composite fonts named `{_1}` 檔案中有兩個複合字體都名為 `{_1}`
|
||||
@msg `{_1}` can't name a composite font `{_1}` 不能作為複合字體的名稱
|
||||
@msg {k}: give a font {k}:請指定字體
|
||||
@msg `{name}`: {k}: give a font `{name}`: {k}:請指定字體
|
||||
@msg {k}: size must be {_1} to {_2}% {k}:size 必須為 {_1}% 到 {_2}%
|
||||
@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}:size 必須為 {_1}% 到 {_2}%
|
||||
@msg {k}: baseline must be {_1} to {_2}% {k}:baseline 必須為 {_1}% 到 {_2}%
|
||||
@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}:baseline 必須為 {_1}% 到 {_2}%
|
||||
@msg {k}: scale must be {_1} to {_2}% {k}:縮放必須為 {_1}% 到 {_2}%
|
||||
@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}:縮放必須為 {_1}% 到 {_2}%
|
||||
@msg {k}: only kana, roman and numerals can be scaled {k}:只有 kana、roman 和 numerals 可以縮放
|
||||
@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}:只有 kana、roman 和 numerals 可以縮放
|
||||
@msg {k}: only kana can be scaled at the center {k}:只有 kana 可以從中心縮放
|
||||
@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}:只有 kana 可以從中心縮放
|
||||
@msg no composite font 沒有複合字體
|
||||
Delete Font 刪除字體
|
||||
Kanji 漢字
|
||||
Kana 假名
|
||||
@@ -5731,6 +5774,7 @@ lighting Light 光源
|
||||
@msg a 3D effect on a group's fill or stroke draws nothing to export 群組填色或筆畫上的 3D 效果沒有可匯出的內容
|
||||
@msg The 3D object is too complex to export 3D 物件過於複雜,無法匯出
|
||||
@msg the object has no 3D effect (Extrude & Bevel, Revolve, Inflate or Rotate) 此物件沒有 3D 效果 (突出與斜角、迴轉、膨脹或旋轉)
|
||||
@msg `render` must be an object holding only `mode`, set to shaded or wireframe `render` 必須是只含 `mode` 的物件,值為 shaded 或 wireframe
|
||||
@msg its 3D effect builds no solid 其 3D 效果沒有產生任何實體
|
||||
@msg bevel must be none or one of {_1} bevel 必須是 none 或 {_1} 其中之一
|
||||
@msg {_1} can't build solids from this art: its symbol instances, blends and clipping groups hold too many objects {_1}無法從此圖稿產生實體:其符號實例、漸變和剪裁群組包含的物件過多
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1,10 +1,15 @@
|
||||
//! Info panel: cursor X/Y, selection W/H, the pointer's distance/angle from the selection, and the
|
||||
//! selection's fill and stroke colour values.
|
||||
//! Info panel: cursor X/Y, selection W/H, the pointer's distance/angle from the selection, the
|
||||
//! readout of the tool in use (a move, a gradient vector, a Scale, Rotate, Reflect or Shear tool
|
||||
//! drag, the Zoom tool's magnification, the Paintbrush's brush) and the selection's fill and
|
||||
//! stroke colour values.
|
||||
|
||||
use egui::{Sense, Ui, vec2};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{NodeKind, Unit};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::Readout;
|
||||
|
||||
use super::{current_paints, first_selected, pstate, set_pstate};
|
||||
use super::{current_paints, mixed_paints, pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
@@ -26,10 +31,25 @@ pub fn color_values(c: &Color) -> Vec<(&'static str, String)> {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_block(ui: &mut Ui, p: &Paint) {
|
||||
/// The name Show Options adds for paint `p`: the swatch and the percentage of a tint, or the
|
||||
/// gradient swatch a gradient is linked to.
|
||||
fn paint_name(p: &Paint) -> Option<String> {
|
||||
match p {
|
||||
Paint::Solid { swatch: Some(n), tint, .. } if *tint < 1.0 => Some(format!("{n} {:.0}%", vectorcraft_color::tint_percent(*tint))),
|
||||
Paint::Gradient(g) => g.swatch.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// One paint's row: its tile and values, and with `options` its name ([`paint_name`]). A paint the
|
||||
/// selected objects do not share (`mixed`) leaves the row empty.
|
||||
fn paint_block(ui: &mut Ui, p: &Paint, mixed: bool, options: bool) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::hover());
|
||||
if mixed {
|
||||
return;
|
||||
}
|
||||
widgets::swatch_tile(ui, r, p, false, false);
|
||||
match p {
|
||||
Paint::Solid { color, .. } => {
|
||||
@@ -38,58 +58,143 @@ fn paint_block(ui: &mut Ui, p: &Paint) {
|
||||
}
|
||||
}
|
||||
other => {
|
||||
ui.label(egui::RichText::new(other.label()).color(t.text_dim));
|
||||
let name = paint_name(other).filter(|_| options).unwrap_or_else(|| other.label());
|
||||
ui.label(egui::RichText::new(name).color(t.text_dim));
|
||||
}
|
||||
}
|
||||
});
|
||||
if let (false, true, Paint::Solid { .. }, Some(name)) = (mixed, options, p, paint_name(p)) {
|
||||
ui.label(egui::RichText::new(name).color(t.text_dim));
|
||||
}
|
||||
}
|
||||
|
||||
/// What the panel reads from the document and the tool for its rows of numbers ([`Rows::of`]).
|
||||
struct Shown<'a> {
|
||||
units: Unit,
|
||||
/// The rulers' zero point: X and Y count from it.
|
||||
origin: Point,
|
||||
/// The document point under the pointer.
|
||||
pointer: Point,
|
||||
/// The sides and the center of the selection's bounding box.
|
||||
size: Option<(f64, f64, Point)>,
|
||||
readout: Option<&'a Readout>,
|
||||
/// Show Options is on.
|
||||
options: bool,
|
||||
}
|
||||
|
||||
/// The panel's rows of numbers: a left and a right column, row by row.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct Rows {
|
||||
left: Vec<String>,
|
||||
right: Vec<String>,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
/// X and Y are the pointer's position, and during a Rotate, Reflect or Shear tool drag the
|
||||
/// center of the selection's bounding box. W and H are the selection's size, the change in x
|
||||
/// and y while a drag moves the selection or draws a gradient vector (with the distance and the
|
||||
/// angle under them), and the percentages of a Scale tool drag. The transform tools' angles
|
||||
/// take their place during their drags.
|
||||
fn of(s: &Shown) -> Self {
|
||||
let len = |v: f64| s.units.format(v);
|
||||
let at = match s.readout {
|
||||
Some(Readout::Rotate { center, .. } | Readout::Reflect { center, .. } | Readout::Shear { center, .. }) => *center,
|
||||
_ => s.pointer,
|
||||
};
|
||||
let mut rows = Rows { left: vec![format!("X: {}", len(at.x - s.origin.x)), format!("Y: {}", len(at.y - s.origin.y))], right: vec![] };
|
||||
match s.readout {
|
||||
Some(r @ Readout::Drag { dx, dy }) => {
|
||||
rows.right = vec![format!("W: {}", len(*dx)), format!("H: {}", len(*dy))];
|
||||
let (d, a) = r.distance_angle().unwrap_or_default();
|
||||
rows.left.push(format!("D: {}", len(d)));
|
||||
rows.right.push(format!("A: {a:.1}°"));
|
||||
}
|
||||
Some(Readout::Scale { sx, sy }) => rows.right = vec![format!("W: {:.1}%", sx * 100.0), format!("H: {:.1}%", sy * 100.0)],
|
||||
Some(Readout::Rotate { angle, .. } | Readout::Reflect { angle, .. }) => rows.right = vec![format!("A: {angle:.1}°")],
|
||||
Some(Readout::Shear { axis, angle, .. }) => rows.right = vec![format!("A: {axis:.1}°"), format!("{}: {angle:.1}°", tl!("Shear"))],
|
||||
Some(Readout::Brush { .. }) | None => {
|
||||
let (w, h) = s.size.map(|(w, h, _)| (len(w), len(h))).unwrap_or_default();
|
||||
rows.right = vec![format!("W: {w}"), format!("H: {h}")];
|
||||
if let (true, Some((.., c))) = (s.options, s.size) {
|
||||
let (d, a) = Readout::drag(s.pointer - c).distance_angle().unwrap_or_default();
|
||||
rows.left.push(format!("D: {}", len(d)));
|
||||
rows.right.push(format!("A: {a:.1}°"));
|
||||
}
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
}
|
||||
|
||||
/// The font and size the selected objects share when every one of them is type: the first
|
||||
/// object's first style, as the Character panel shows its size. None when an object is not type or
|
||||
/// two of them differ.
|
||||
fn shared_type(app: &VectorcraftApp) -> Option<(String, f64, f64)> {
|
||||
let st = app.session.active()?;
|
||||
let mut shared = None;
|
||||
for id in &st.selection.objects {
|
||||
let NodeKind::Text(tx) = &st.doc.node(*id)?.kind else { return None };
|
||||
let raw = tx.first_style();
|
||||
// The size in document points, as the Character panel shows it (#1034); a collapsed
|
||||
// transform has none, so the stored size stands.
|
||||
let cs = tx.effective_char_style(&raw).unwrap_or(raw);
|
||||
let this = (format!("{} {}", cs.font_family, cs.font_style), cs.size, cs.tracking);
|
||||
match &shared {
|
||||
None => shared = Some(this),
|
||||
Some(first) if *first == this => {}
|
||||
Some(_) => return None,
|
||||
}
|
||||
}
|
||||
shared
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let units = app.session.general_unit();
|
||||
let p = app.hover_doc.unwrap_or_default();
|
||||
// X and Y count from the rulers' zero point, as the rulers and the Transform panel do.
|
||||
let origin = app.ruler_origin();
|
||||
let bounds = app.session.active().and_then(|st| st.doc.bounds_of(&st.selection.objects, false));
|
||||
let options = !pstate::<bool>(ui.ctx(), "info-hide-options");
|
||||
// W and H are the sides of the bounding box the Transform panel shows, measured as Use Preview
|
||||
// Bounds says.
|
||||
let size = app.selection_box().map(|b| (b.rect.width(), b.rect.height(), b.center()));
|
||||
let readout = app.session.tool_readout();
|
||||
let rows = Rows::of(&Shown {
|
||||
units: app.session.general_unit(),
|
||||
// X and Y count from the rulers' zero point, as the rulers and the Transform panel do.
|
||||
origin: app.ruler_origin(),
|
||||
pointer: app.hover_doc.unwrap_or_default(),
|
||||
size,
|
||||
readout: readout.as_ref(),
|
||||
options,
|
||||
});
|
||||
let mono = |ui: &mut Ui, s: String| {
|
||||
ui.label(egui::RichText::new(s).monospace().color(t.text));
|
||||
};
|
||||
ui.columns(2, |cols| {
|
||||
mono(&mut cols[0], format!("X: {}", units.format(p.x - origin.x)));
|
||||
mono(&mut cols[0], format!("Y: {}", units.format(p.y - origin.y)));
|
||||
let (w, h) = bounds.map(|b| (units.format(b.width()), units.format(b.height()))).unwrap_or_default();
|
||||
mono(&mut cols[1], format!("W: {w}"));
|
||||
mono(&mut cols[1], format!("H: {h}"));
|
||||
for s in rows.left {
|
||||
mono(&mut cols[0], s);
|
||||
}
|
||||
for s in rows.right {
|
||||
mono(&mut cols[1], s);
|
||||
}
|
||||
});
|
||||
if !pstate::<bool>(ui.ctx(), "info-hide-options")
|
||||
&& let Some(b) = bounds
|
||||
{
|
||||
let c = b.center();
|
||||
let d = ((p.x - c.x).powi(2) + (p.y - c.y).powi(2)).sqrt();
|
||||
let a = (-(p.y - c.y)).atan2(p.x - c.x).to_degrees();
|
||||
ui.columns(2, |cols| {
|
||||
mono(&mut cols[0], format!("D: {}", units.format(d)));
|
||||
mono(&mut cols[1], format!("A: {a:.1}°"));
|
||||
});
|
||||
if app.session.tool_id() == "zoom" {
|
||||
let zoom = app.view().map_or(1.0, |v| v.zoom);
|
||||
mono(ui, format!("{}: {}", tl!("Zoom"), crate::state::zoom_label(zoom, app.session.prefs.display_print_size)));
|
||||
}
|
||||
if let Some(Readout::Brush { name: Some(name) }) = readout {
|
||||
mono(ui, name);
|
||||
}
|
||||
widgets::divider(ui);
|
||||
let (f, s) = current_paints(app);
|
||||
paint_block(ui, &f);
|
||||
paint_block(ui, &s);
|
||||
if let Some(n) = first_selected(app)
|
||||
&& let vectorcraft_doc::NodeKind::Text(tx) = &n.kind
|
||||
{
|
||||
let (fill_mixed, stroke_mixed) = mixed_paints(app, ui.ctx());
|
||||
paint_block(ui, &f, fill_mixed, options);
|
||||
paint_block(ui, &s, stroke_mixed, options);
|
||||
if let Some((font, size, tracking)) = shared_type(app) {
|
||||
widgets::divider(ui);
|
||||
let raw = tx.first_style();
|
||||
// The size in document points, as the Character panel shows it (#1034); a collapsed
|
||||
// transform has none, so the stored size stands.
|
||||
let st = tx.effective_char_style(&raw).unwrap_or(raw);
|
||||
mono(ui, format!("{} {}", st.font_family, st.font_style));
|
||||
mono(ui, font);
|
||||
mono(
|
||||
ui,
|
||||
crate::i18n::fmt(
|
||||
tl!("Size: {size} Tracking: {tracking}"),
|
||||
&[("size", &app.session.type_unit().format(st.size)), ("tracking", &format!("{:.0}", st.tracking))],
|
||||
&[("size", &app.session.type_unit().format(size)), ("tracking", &format!("{tracking:.0}"))],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -104,7 +209,32 @@ pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::tests_appearance::{app_with_rect, frame_events, run};
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::ViewInfo;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
fn texts(app: &mut VectorcraftApp) -> Vec<String> {
|
||||
frame_events(&egui::Context::default(), app, vec![], show).into_iter().map(|(t, _)| t).collect()
|
||||
}
|
||||
|
||||
fn view() -> ViewInfo {
|
||||
ViewInfo { smart_guides: false, snap_to_point: false, ..Default::default() }
|
||||
}
|
||||
|
||||
fn pointer(app: &mut VectorcraftApp, kind: PointerKind, x: f64, y: f64) {
|
||||
app.session.pointer(&PointerEvent::new(kind, x, y), view()).unwrap();
|
||||
}
|
||||
|
||||
fn shown(readout: Option<&Readout>, options: bool) -> Rows {
|
||||
let size = Some((100.0, 60.0, Point::new(100.0, 80.0)));
|
||||
Rows::of(&Shown { units: Unit::Points, origin: Point::new(10.0, 20.0), pointer: Point::new(130.0, 40.0), size, readout, options })
|
||||
}
|
||||
|
||||
fn strings(v: &[&str]) -> Vec<String> {
|
||||
v.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_follow_model() {
|
||||
@@ -117,8 +247,6 @@ mod tests {
|
||||
/// #1882: scaled type shows its size in document points, as the Character panel does (#1034).
|
||||
#[test]
|
||||
fn scaled_type_shows_its_scaled_size() {
|
||||
use super::super::tests_appearance::{app_with_rect, frame_events, run};
|
||||
use serde_json::json;
|
||||
let mut app = app_with_rect();
|
||||
let id = run(&mut app, "text.create", json!({"x": 10, "y": 150, "text": "Hi", "size": 12}))["id"].clone();
|
||||
run(&mut app, "select.set", json!({ "ids": [id] }));
|
||||
@@ -127,4 +255,105 @@ mod tests {
|
||||
let size = texts.iter().find(|(t, _)| t.starts_with("Size:")).map(|(t, _)| t.as_str());
|
||||
assert_eq!(size, Some("Size: 24 pt Tracking: 0"), "{texts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rows_follow_the_tool_in_use() {
|
||||
// No tool readout: the pointer from the zero point, the selection's size, and with Show
|
||||
// Options the pointer's distance and angle from the selection's center.
|
||||
let idle = shown(None, true);
|
||||
assert_eq!(idle.left, strings(&["X: 120 pt", "Y: 20 pt", "D: 50 pt"]));
|
||||
assert_eq!(idle.right, strings(&["W: 100 pt", "H: 60 pt", "A: 53.1°"]));
|
||||
assert_eq!(shown(None, false), Rows { left: strings(&["X: 120 pt", "Y: 20 pt"]), right: strings(&["W: 100 pt", "H: 60 pt"]) });
|
||||
// The Paintbrush leaves the numbers as they are.
|
||||
assert_eq!(shown(Some(&Readout::Brush { name: None }), true), idle);
|
||||
// A drag: the change in x and y, the distance and the angle, with Show Options off too.
|
||||
let drag = shown(Some(&Readout::Drag { dx: -30.0, dy: 40.0 }), false);
|
||||
assert_eq!(drag.left, strings(&["X: 120 pt", "Y: 20 pt", "D: 50 pt"]));
|
||||
assert_eq!(drag.right, strings(&["W: -30 pt", "H: 40 pt", "A: -126.9°"]));
|
||||
let scale = shown(Some(&Readout::Scale { sx: 1.5, sy: -0.25 }), true);
|
||||
assert_eq!(scale.left, strings(&["X: 120 pt", "Y: 20 pt"]));
|
||||
assert_eq!(scale.right, strings(&["W: 150.0%", "H: -25.0%"]));
|
||||
// Rotate, Reflect and Shear: the center of the selection's box and the angles.
|
||||
let center = Point::new(60.0, 70.0);
|
||||
let rotate = shown(Some(&Readout::Rotate { center, angle: 30.0 }), true);
|
||||
assert_eq!(rotate, Rows { left: strings(&["X: 50 pt", "Y: 50 pt"]), right: strings(&["A: 30.0°"]) });
|
||||
assert_eq!(shown(Some(&Readout::Reflect { center, angle: 45.0 }), true).right, strings(&["A: 45.0°"]));
|
||||
let shear = shown(Some(&Readout::Shear { center, axis: 90.0, angle: -12.5 }), true);
|
||||
assert_eq!(shear, Rows { left: strings(&["X: 50 pt", "Y: 50 pt"]), right: strings(&["A: 90.0°", "Shear: -12.5°"]) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn w_and_h_follow_use_preview_bounds() {
|
||||
let mut app = app_with_rect();
|
||||
run(&mut app, "stroke.set", json!({"weight": 10}));
|
||||
let has = |app: &mut VectorcraftApp, s: &str| texts(app).iter().any(|t| t == s);
|
||||
assert!(has(&mut app, "W: 100 pt") && has(&mut app, "H: 100 pt"), "{:?}", texts(&mut app));
|
||||
run(&mut app, "prefs.set", json!({"key": "usePreviewBounds", "value": true}));
|
||||
assert!(has(&mut app, "W: 110 pt") && has(&mut app, "H: 110 pt"), "{:?}", texts(&mut app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_shows_the_change_until_the_release() {
|
||||
let mut app = app_with_rect();
|
||||
app.session.select_tool("selection", view()).unwrap();
|
||||
pointer(&mut app, PointerKind::Down, 60.0, 60.0);
|
||||
pointer(&mut app, PointerKind::Drag, 90.0, 20.0);
|
||||
let during = texts(&mut app);
|
||||
for want in ["W: 30 pt", "H: -40 pt", "D: 50 pt", "A: 53.1°"] {
|
||||
assert!(during.iter().any(|t| t == want), "{want}: {during:?}");
|
||||
}
|
||||
pointer(&mut app, PointerKind::Up, 90.0, 20.0);
|
||||
let after = texts(&mut app);
|
||||
assert!(after.iter().any(|t| t == "W: 100 pt") && !after.iter().any(|t| t == "W: 30 pt"), "{after:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_zoom_tool_shows_the_magnification() {
|
||||
let mut app = app_with_rect();
|
||||
assert!(!texts(&mut app).iter().any(|t| t.starts_with("Zoom:")));
|
||||
app.session.select_tool("zoom", view()).unwrap();
|
||||
let zoom = app.view().map_or(1.0, |v| v.zoom);
|
||||
let want = format!("Zoom: {}", crate::state::zoom_label(zoom, app.session.prefs.display_print_size));
|
||||
assert!(texts(&mut app).contains(&want), "{want}: {:?}", texts(&mut app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_paintbrush_shows_its_brush() {
|
||||
let mut app = app_with_rect();
|
||||
app.session.select_tool("paintbrush", view()).unwrap();
|
||||
let brush = run(&mut app, "brush.list", json!({}))["brushes"][0]["name"].as_str().map(str::to_string);
|
||||
let Some(brush) = brush else { panic!("the document has no brush") };
|
||||
assert!(!texts(&mut app).contains(&brush));
|
||||
app.session.set_tool_option("brush", &json!(brush));
|
||||
assert!(texts(&mut app).contains(&brush), "{brush}: {:?}", texts(&mut app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn several_objects_show_only_the_paint_they_share() {
|
||||
let mut app = app_with_rect();
|
||||
run(&mut app, "paint.setFill", json!({"color": "#ff0000"}));
|
||||
let other = run(&mut app, "shape.rectangle", json!({"x": 120, "y": 10, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut app, "paint.setFill", json!({"color": "#ff0000"}));
|
||||
run(&mut app, "select.all", json!({}));
|
||||
assert!(texts(&mut app).iter().any(|t| t == "R: 255"), "{:?}", texts(&mut app));
|
||||
run(&mut app, "select.set", json!({ "ids": [other] }));
|
||||
run(&mut app, "paint.setFill", json!({"color": "#00ff00"}));
|
||||
assert!(texts(&mut app).iter().any(|t| t == "G: 255"));
|
||||
run(&mut app, "select.all", json!({}));
|
||||
let all = texts(&mut app);
|
||||
assert!(!all.iter().any(|t| t == "G: 255" || t == "R: 255"), "{all:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_options_names_a_tint_and_a_gradient_swatch() {
|
||||
let tint = Paint::Solid { color: Color::rgb8(255, 128, 128), swatch: Some("Brand".into()), tint: 0.4 };
|
||||
assert_eq!(paint_name(&tint), Some("Brand 40%".into()));
|
||||
let full = Paint::Solid { color: Color::rgb8(255, 0, 0), swatch: Some("Brand".into()), tint: 1.0 };
|
||||
assert_eq!(paint_name(&full), None);
|
||||
assert_eq!(paint_name(&Paint::None), None);
|
||||
let mut g = vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default());
|
||||
assert_eq!(paint_name(&Paint::Gradient(Box::new(g.clone()))), None);
|
||||
g.swatch = Some("Sunset".into());
|
||||
assert_eq!(paint_name(&Paint::Gradient(Box::new(g))), Some("Sunset".into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,48 @@ fn the_panel_shows_the_dialog_controls_of_each_type() {
|
||||
assert!(!painted_text(&mut app, show).contains("Show axis"));
|
||||
}
|
||||
|
||||
/// Render Settings › Wireframe: shown for the panel's four effects, it sets `render.mode` on every
|
||||
/// selected 3D effect that takes it in one undo step, the type switch keeps it, and choosing it
|
||||
/// again draws the shaded surface. A 3D (Classic) effect beside them is left as it is, and the
|
||||
/// menu is not shown for it alone.
|
||||
#[test]
|
||||
fn render_settings_wireframe_turns_the_mode_on_and_off_for_the_selection() {
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
assert!(painted_text(&mut app, show).contains("Render Settings"));
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.revolve", "item": null})).unwrap();
|
||||
let c = app.run("shape.rectangle", json!({"x": 100, "y": 250, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.classicExtrude", "item": null})).unwrap();
|
||||
let mode = |app: &VectorcraftApp, id: u64| {
|
||||
let n = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||||
n.appearance.effects[0].params.get("render").and_then(|r| r.get("mode")).and_then(Value::as_str).map(str::to_string)
|
||||
};
|
||||
app.run("select.set", json!({"ids": [a, b, c]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let history = undo_len(&app);
|
||||
super::set_wireframe(&mut app, &ctx, true);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
assert_eq!((mode(&app, a).as_deref(), mode(&app, b).as_deref()), (Some("wireframe"), Some("wireframe")));
|
||||
assert_eq!(mode(&app, c), None, "the classic effect takes no render settings");
|
||||
assert!(super::wireframe_on(&target(&app).unwrap()));
|
||||
// The object type changes in place and keeps the mode.
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
set_type(&mut app, "inflate").unwrap();
|
||||
assert_eq!(mode(&app, a).as_deref(), Some("wireframe"));
|
||||
let history = undo_len(&app);
|
||||
super::set_wireframe(&mut app, &ctx, false);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
assert_eq!(mode(&app, a).as_deref(), Some("shaded"));
|
||||
assert!(!super::wireframe_on(&target(&app).unwrap()));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(mode(&app, a).as_deref(), Some("wireframe"));
|
||||
// The 3D (Classic) effect alone has no Render Settings.
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
assert!(!painted_text(&mut app, show).contains("Render Settings"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_previews_and_records_one_undo_step_when_released() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -231,6 +231,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
return;
|
||||
};
|
||||
if takes_render(&t.effect) {
|
||||
render_settings(app, ui, &ctx, &t);
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
// The applied effect's settings in the dialog's form, with the controls' own state.
|
||||
let mut fields = vectorcraft_effects::merged_params(&t.effect, &t.params).as_object().cloned().unwrap_or_default();
|
||||
mark_mixed(app, &mut fields);
|
||||
@@ -269,6 +273,45 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Does 3D effect `id` take render settings (`render`)? Extrude & Bevel, Revolve, Inflate and Rotate
|
||||
/// do; the 3D (Classic) effects have a Wireframe surface of their own.
|
||||
fn takes_render(id: &str) -> bool {
|
||||
vectorcraft_effects::default_params(id).is_some_and(|d| d.get(vectorcraft_effects::RENDER).is_some())
|
||||
}
|
||||
|
||||
/// Is target `t`'s 3D effect drawn as a wireframe (`render.mode` is `wireframe`)?
|
||||
pub(crate) fn wireframe_on(t: &Target) -> bool {
|
||||
let p = vectorcraft_effects::merged_params(&t.effect, &t.params);
|
||||
p.get(vectorcraft_effects::RENDER).and_then(|r| r.get("mode")).and_then(Value::as_str) == Some("wireframe")
|
||||
}
|
||||
|
||||
/// The Render Settings menu at the top of the panel: Wireframe, checked while the first selected
|
||||
/// object's 3D effect draws the edges of its faces. Choosing it turns wireframe on or off for every
|
||||
/// selected object's 3D effect ([`set_wireframe`]).
|
||||
fn render_settings(app: &mut VectorcraftApp, ui: &mut Ui, ctx: &egui::Context, t: &Target) {
|
||||
let on = wireframe_on(t);
|
||||
let mut picked = false;
|
||||
ui.menu_button(tl!("Render Settings"), |ui| {
|
||||
if widgets::menu_item(ui, tl!("Wireframe"), true, on) {
|
||||
picked = true;
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if picked {
|
||||
set_wireframe(app, ctx, !on);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render Settings › Wireframe: sets `render.mode` of each selected object's 3D effect to
|
||||
/// `wireframe` (`on`) or `shaded`, as one undo step (`effect.setParams`). Objects whose effect does
|
||||
/// not take render settings are left as they are.
|
||||
pub(crate) fn set_wireframe(app: &mut VectorcraftApp, ctx: &egui::Context, on: bool) {
|
||||
finish(app, ctx);
|
||||
let mut changed = Map::new();
|
||||
changed.insert(vectorcraft_effects::RENDER.into(), json!({"mode": if on { "wireframe" } else { "shaded" }}));
|
||||
send(app, ctx, changed, false);
|
||||
}
|
||||
|
||||
/// Runs what a click in the Materials tab's Graphics asked for: maps the graphic onto the selected
|
||||
/// objects, its row then selected, or keeps art as a new graphic. An error goes to the status bar.
|
||||
fn run_action(app: &mut VectorcraftApp, ctx: &egui::Context, action: materials::Action) {
|
||||
|
||||
@@ -180,7 +180,7 @@ VectorCraft choices; an original approximation, never compared with another appl
|
||||
|---|---|
|
||||
| Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, overlapping faces drawn in the order of their depths where they overlap, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) |
|
||||
| Extrude & Bevel, Inflate, Rotate | initial: live, round seam joins, depth, twist, taper, solid or hollow caps, five original bevel shapes, Inflate volume, both sides, twist, taper and cap, Rotate of every fill and stroke, groups and layers turned about one shared axis (#4506), contours that cross, touch or overlap merged first ([three-d.md](three-d.md)), Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) |
|
||||
| Materials panel, lighting, ray-traced render | Window › 3D and Materials panel with the object types (Plane, Extrude, Revolve, Inflate) and the Object, Rotation and Lighting settings of the applied effect, and Quick Actions › Expand as Wireframes ([three-d.md](three-d.md#3d-and-materials-panel)); one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; hard-edged shadows behind or below the object from the first light ([three-d.md](three-d.md#shadows)); the Materials tab's Graphics, document symbols mapped onto Extrude & Bevel's caps and onto a plane, moved with an artboard handle, and wrapped around Revolve's side ([three-d.md](three-d.md#graphics)); no base materials, graphics on Inflate or on Extrude & Bevel's walls and bevels, soft shadows or ray tracing |
|
||||
| Materials panel, lighting, ray-traced render | Window › 3D and Materials panel with the object types (Plane, Extrude, Revolve, Inflate) and the Object, Rotation and Lighting settings of the applied effect, Render Settings › Wireframe, which draws the live effect as the edges of its faces, and Quick Actions › Expand as Wireframes ([three-d.md](three-d.md#3d-and-materials-panel)); one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; hard-edged shadows behind or below the object from the first light ([three-d.md](three-d.md#shadows)); the Materials tab's Graphics, document symbols mapped onto Extrude & Bevel's caps and onto a plane, moved with an artboard handle, and wrapped around Revolve's side ([three-d.md](three-d.md#graphics)); no base materials, graphics on Inflate or on Extrude & Bevel's walls and bevels, soft shadows or ray tracing |
|
||||
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | Extrude & Bevel (Classic) initial: live, round seam joins, Extrude Depth, Cap, five original bevel shapes with Height and Bevel Extent In or Out, Position presets and rotation gizmo, Perspective, Wireframe, No Shading, Diffuse and Plastic surfaces with one to ten lights, Light Intensity, Ambient Light, Highlight Intensity and Size, Blend Steps, Black, Custom or no Shading Color, Draw Hidden Faces ([create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)); Revolve (Classic) initial: live, round seam joins, Angle, Cap, Offset from the left or right edge, Position presets and rotation gizmo, Perspective, the same four surfaces, lights, highlight, Blend Steps and Shading Color, groups and layers revolved about one shared axis (same page); Rotate (Classic) initial: live, round seam joins, every fill and stroke turned as one plane, Position presets and rotation gizmo, Perspective, No Shading and Diffuse surfaces with lights, Ambient Light, Blend Steps and Shading Color ([rotate objects in 3D](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/rotate-objects-in-3d.html)); Map Art: none |
|
||||
| Turntable (30.0 beta) | none |
|
||||
| Export 3D objects (OBJ, USDA, glTF) | initial: Asset Export's Export 3D Object collects an object with Extrude & Bevel, Revolve, Inflate or Rotate as a 3D asset, and Export writes its solids as a USDA (the default), USDZ, glTF or OBJ mesh with a material in each solid's paint color (`assets.add3D`, `assets.set3DFormat`, `assets.export3D`); no textures, map art or shading ([3D export help](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/export-3d-vector-artwork.html)) |
|
||||
|
||||
+7
-5
File diff suppressed because one or more lines are too long
+54
-1
@@ -2979,6 +2979,38 @@ changes neither option. Behavior reference: [View artwork](https://helpx.adobe.c
|
||||
{"name":"invoke_menu","arguments":{"command":"ui.navigatorPanelOptions","params":{"color":"Light Blue","solidDashes":true}}}
|
||||
```
|
||||
|
||||
## Info panel
|
||||
|
||||
`info.readout` takes no params and returns what Window › Info shows for the selection and the tool in use:
|
||||
`{tool, width?, height?, readout?}`. `tool` is the active tool's id. `width` and `height` are the sides of the
|
||||
selection's bounding box in points (of the box around the anchors selected with Direct Selection when there are
|
||||
some), measured as the `usePreviewBounds` preference says, and are left out with nothing selected. `readout` is left
|
||||
out unless the tool has something of its own to show:
|
||||
|
||||
- `{kind: "drag", dx, dy, distance, angle}` while the Selection, Direct Selection or Group Selection tool moves the
|
||||
selection, or the Gradient tool draws a gradient vector on the art.
|
||||
- `{kind: "scale", width, height}` during a Scale tool drag, in percent.
|
||||
- `{kind: "rotate", center, angle}` and `{kind: "reflect", center, angle}` during a Rotate or Reflect tool drag.
|
||||
`angle` is the angle turned, as `object.rotate` takes it, or the angle of the reflection axis, as `object.reflect`
|
||||
takes its `axis`. Both are in degrees counter-clockwise.
|
||||
- `{kind: "shear", center, axis, angle}` during a Shear tool drag. `axis` is 0 for a horizontal shear and 90 for a
|
||||
vertical one, and `angle` is the shear angle in degrees, as `object.shear` takes it.
|
||||
- `{kind: "brush", brush}` while the Paintbrush is the tool. `brush` is the brush it paints with, or null.
|
||||
|
||||
Lengths are in points. A drag's `angle` is in degrees counter-clockwise from the x axis. `center` is the center of the
|
||||
selection's bounding box as the drag has carried it, in document coordinates. A param is an error. In the app's panel, X and Y show the
|
||||
pointer's position from the rulers' zero point, or `center` during a Rotate, Reflect or Shear tool drag. W and H show
|
||||
the selection's size, `dx` and `dy` during a drag (with the distance and the angle under them), or the Scale tool's
|
||||
percentages. The Zoom tool adds the view's magnification and the Paintbrush its brush's name. Several selected objects
|
||||
show only the fill, stroke and type values they share, and Show Options adds a tint's swatch and percentage and the
|
||||
name of a gradient's swatch. The number formats, the rows shown between drags and the Direct Selection and Group
|
||||
Selection readouts are VectorCraft's own. Behavior reference: the Info panel overview in the
|
||||
[archived user guide](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=76).
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"info.readout","params":{}}}
|
||||
```
|
||||
|
||||
## Tools panel placement
|
||||
|
||||
By default the docked Tools panel sits at the window's left edge, outside any panels docked on the left.
|
||||
@@ -4133,6 +4165,26 @@ it as a path. Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic)
|
||||
{"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"shadow":true,"shadowPosition":"below","shadowDistance":10,"shadowBounds":150}}}}
|
||||
```
|
||||
|
||||
## 3D render mode
|
||||
|
||||
Revolve, Extrude & Bevel, Inflate and Rotate take `render`, an object whose `mode` is `"shaded"` (the default) or
|
||||
`"wireframe"` (**Window › 3D and Materials** › Render Settings › Wireframe), set with `effect.apply` or
|
||||
`effect.setParams` in one undo step. In wireframe mode the effect stays live and draws the edges of the faces it
|
||||
builds instead of its shaded surface: each edge once, in the paths [Expand as Wireframes](#expand-as-wireframes)
|
||||
gives for the 3D effect, with a 0.5 pt black stroke, round joins and no fill (VectorCraft's own choice), and no shadow or graphics.
|
||||
The edges are drawn whatever the opacity, blending mode or opacity mask of the fills, strokes and group members; the
|
||||
object's own opacity, blending mode and opacity mask apply to them as a whole.
|
||||
Geometry effects after the 3D effect reshape the drawn edges. Expand as Wireframes reshapes each face before it
|
||||
joins the edges, so with such an effect its paths can differ from the drawn ones. The mode is saved in the native file, SVG and PDF exports
|
||||
draw the edges, and `effect.expandAppearance` keeps them as paths. Past 1,000,000 edges the source path is drawn, as
|
||||
it is for a solid past its limits. A `render` that is not an object, a key other than `mode`, another mode, and
|
||||
`render` on Extrude & Bevel (Classic), Revolve (Classic) or Rotate (Classic) are refused before the document
|
||||
changes. See [three-d.md](three-d.md#3d-and-materials-panel).
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"render":{"mode":"wireframe"}}}}}
|
||||
```
|
||||
|
||||
## 3D object type
|
||||
|
||||
`effect.setParams {index: int, threeDType: "plane" | "extrude" | "revolve" | "inflate", params?, item?, ids?, aligned?}`
|
||||
@@ -8455,7 +8507,8 @@ that can be edited or released. `file.place` answers `image`, the id of the imag
|
||||
[import options help page](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html)
|
||||
says that a file imported as one flattened image keeps the document's clipping path when it has one. `links.update`
|
||||
and `links.relink` read the file's pixels again and leave the clip group and its clipping path as they are. Files that name no clipping path, or name a path
|
||||
they don't have, come in as a plain image.
|
||||
they don't have, come in as a plain image. `file.place` with `replace` on such a clip group puts a new image, clipped or
|
||||
not, at the scale and rotation of the image in the group.
|
||||
|
||||
`document.open` converts a PSD or PSB file's layers to objects. In
|
||||
[PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html),
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Tech Circuit brush library, G5; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (M10/G5: Neon & Glow built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); previously: Tech Circuit brush library, G5; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
>
|
||||
> Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865).
|
||||
>
|
||||
@@ -207,6 +207,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-15 | minor | M10/G5: Neon & Glow built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); estimates unchanged |
|
||||
| 2026-11-14 | minor | Tech Circuit adds three original procedural brushes (Circuit Trace, Bus Lines, Terminal Nodes); ten built-in brush libraries. G5 remaining estimate 3–13 h (estimated); parity scores and headline totals unchanged. |
|
||||
| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. |
|
||||
| 2026-11-12 | minor | Gujarati (`gu`): 4,286-row machine-assisted catalog reviewed by a native speaker, zero/one singular plurals, desktop font fallback and optional Gujarati craft-fonts fallback; all 72 translation tests pass; a web font and about 2,600 untranslated messages remain; scores and estimates unchanged |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,21 @@ pages describe it.
|
||||
3D effect of each other selected object in place (`effect.setParams` with `threeDType`): the settings both
|
||||
effects take keep their values, and the rest start at the new effect's defaults. One click is one undo step,
|
||||
and an object whose effect already has the type does not change.
|
||||
- **Render Settings › Wireframe**, under the object types, draws each selected 3D object as the edges of its
|
||||
faces instead of its shaded surface, as the public
|
||||
[Render 3D vector artwork](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/render-3d-vector-artwork.html)
|
||||
page describes for editing Depth, Taper and Twist in a mesh view. The item is checked while the first selected
|
||||
object's effect is in wireframe mode, and choosing it turns the mode on or off for every selected Extrude & Bevel,
|
||||
Revolve, Inflate and Rotate (`effect.setParams` with `render: {mode}`), as one undo step. The effect stays live:
|
||||
each edit to its settings redraws the edges. The edges are the ones Expand as Wireframes gives, each once, with a
|
||||
0.5 pt black stroke, round joins and no fill (VectorCraft's own choice). Geometry effects after the 3D effect
|
||||
reshape the drawn edges, as they reshape shaded art. Expand as Wireframes reshapes each face before it joins the
|
||||
edges, so with such an effect its paths can differ from the drawn ones. The mesh shows every edge whatever the
|
||||
opacity, blending mode or opacity mask of the fills, strokes and group members, as Expand as Wireframes does;
|
||||
the object's own opacity, blending mode and opacity mask apply to it as a whole. The shadow and the graphics are
|
||||
not drawn.
|
||||
The mode is saved with the effect and drawn in every output, and Expand Appearance gives the edges as paths.
|
||||
Choosing Wireframe again draws the shaded surface. The 3D (Classic) effects keep their own Wireframe surface.
|
||||
- Below them are the Object, Rotation and Lighting tabs of the effect's options dialog, with the same controls
|
||||
and ranges, showing the 3D effect of the first selected object (its own effects first, then those of its fills
|
||||
and strokes). A changed setting goes to the 3D effect of each selected object whose effect takes that setting;
|
||||
@@ -479,6 +494,9 @@ Commands validate the shape and the parameters before the document changes. All
|
||||
with `symbol` (the name of a document symbol), `face` (`front` or `back`, default `front`; `threeD.rotate` and
|
||||
`threeD.revolve` take `front` only), `x` and `y` (points, default 0), `scale` (1..10000%, default 100) and `rotation` (-360..360 degrees,
|
||||
default 0). Other keys, values out of range and graphics on the other 3D effects are rejected before editing.
|
||||
All four take `render` ([3D and Materials panel](#3d-and-materials-panel)): an object whose `mode` is `shaded` (the
|
||||
default) or `wireframe`. A `render` that is not an object, a key other than `mode`, another mode, and `render` on
|
||||
the 3D (Classic) effects are rejected before editing.
|
||||
|
||||
## What differs from the public documentation
|
||||
|
||||
|
||||
@@ -430,6 +430,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Control bar: Paragraph: link and Align left/center/right while type is selected (#5372) | done | After the Character controls, as the public documentation has the Control panel format paragraphs ([Paragraph panel overview](https://helpx.adobe.com/illustrator/using/formatting-paragraphs.html), [Align text](https://helpx.adobe.com/illustrator/using/align-text.html)); the link opens the Paragraph panel in a popover, as Character: does with the Character panel. The bar trims from its right end: X/Y/W/H, Distribute, Align, area type vertical alignment, then these buttons, then the link. The three-button set is our choice for room; all seven alignments are in the popover. `panels/paragraph/tests.rs` headless tests (`the_control_bar_shows_the_paragraph_alignment_of_the_selected_type`, `the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right`) |
|
||||
| Pathfinder shape results keep their source names and SVG IDs | done | #1293; Minus Front keeps the back name, Minus Back the front name; Unite/Intersect/Exclude use the backmost named input; compound output carries it once; geometry and paint unchanged |
|
||||
| Attributes fill-rule controls with a compound member selected | done | Edits the compound's rule; direction still edits only the member; rendered-hole and Undo/Redo regression tests (#1197) |
|
||||
| Info panel follows the tool in use: W and H show the change in x and y, with the distance and the angle, while the selection moves or the Gradient tool draws a vector; the Scale tool's percentages; the center and angles of a Rotate, Reflect or Shear tool drag; the Zoom tool's magnification; the Paintbrush's brush. W and H follow Use Preview Bounds, several selected objects show only the paint and type they share, and Show Options names a tint or a gradient swatch | done | `info.readout`; `Tool::readout`; behavior from the Info panel overview in the [archived user guide](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=76); number formats, the rows shown between drags and the Direct Selection and Group Selection readouts are VectorCraft's own |
|
||||
| Make Compound Path on a group (such as outlined type) keeps its backmost path's paint; compound paths joined into one keep their holes | done | #1537 |
|
||||
| Symbol libraries: Window › Symbol Libraries (Basic Shapes, UI Frames, Map Markers, Inventory Icons, Skill Icons, User Defined, Other Library…, Save Symbols…) and the Symbols panel's Symbol Libraries Menu button and its menu's Open Symbol Library / Save Symbol Library…; a click on a library symbol adds it to the document's Symbols (one undo step, it becomes the current symbol), Shift/Cmd-clicks select for Add to Symbols, and a symbol dragged onto the canvas is added and placed where it is dropped | done | [public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html), [Symbols panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), [placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html); `window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary` (dialog `saveSymbolLibrary`), `window.userSymbolLibrary1`…`10`; `panels::tests_symbol_libraries`; five built-in libraries (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons, Skill Icons; original, generated in code) |
|
||||
| Brush libraries: Window › Brush Libraries (Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media, Tech Circuit, User Defined, Other Library…, Save Brushes…) and the Brushes panel's Brush Libraries Menu button and its menu's Open Brush Library / Save Brush Library…; a click on a library brush adds it to the document's Brushes (one undo step, it becomes the current brush, and selected paths take it in the same step), Shift/Cmd-clicks select for Add to Brushes, a brush dragged onto a path is added and applied to it and one dragged onto the Brushes panel is added; thumbnails render with the library's patterns and symbols | done | [public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import brushes](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel overview](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html); `window.brushLibrary`, `window.brushLibrary.other`, `ui.saveBrushLibrary` (dialog `saveBrushLibrary`), `window.userBrushLibrary1`…`10`; `panels::tests_brush_libraries`; ten built-in libraries (original, generated in code) |
|
||||
|
||||
Reference in New Issue
Block a user