Merge pull request #1788 from osamaamin2011/dimension-tool

Fix #1372: Dimension tool
This commit is contained in:
BFlat
2026-10-16 01:57:45 +07:00
committed by GitHub
38 changed files with 2829 additions and 4 deletions
+1
View File
@@ -117,6 +117,7 @@ Generated-in-code art is original and has no file to list. This covers the defau
| `assets/icons/dc-cube.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
| `assets/icons/dc-dash-align.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
| `assets/icons/dc-dash-exact.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
| `assets/icons/dc-dimension.svg` | Osama Amin | Original work, drawn for VectorCraft: a dimension line with arrowheads between two extension lines | MIT OR Apache-2.0 | |
| `assets/icons/dc-dir-off.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
| `assets/icons/dc-dir-on.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
| `assets/icons/dc-direct.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5v14M20 5v14"/><path d="M4 12h16"/><path d="M7.5 9 4.5 12l3 3M16.5 9l3 3-3 3"/></svg>

After

Width:  |  Height:  |  Size: 281 B

+5
View File
@@ -98,3 +98,8 @@ sync_github_name = false
real_name = "Tomasz Lewandowski"
display_name = "Tomasz Lewandowski"
sync_github_name = false
[people.osamaamin2011]
real_name = "Osama Amin"
display_name = "Osama Amin"
sync_github_name = false
+573
View File
@@ -0,0 +1,573 @@
//! Persistent dimension recipes. Children are ordinary paths and type for all renderers/exporters.
use crate::{Document, NodeId, Unit};
use serde::{Deserialize, Serialize};
use vectorcraft_geom::{Affine, Point, Vec2};
/// What a dimension measures.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DimensionMode {
#[default]
Linear,
Angular,
Radius,
Diameter,
}
impl DimensionMode {
pub fn id(self) -> &'static str {
match self {
Self::Linear => "linear",
Self::Angular => "angular",
Self::Radius => "radius",
Self::Diameter => "diameter",
}
}
}
/// The unit a length is labelled in.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LengthUnit {
#[default]
Pt,
Pc,
Px,
In,
Mm,
Cm,
M,
Ft,
Yd,
}
impl LengthUnit {
pub const ALL: [LengthUnit; 9] = [
LengthUnit::In,
LengthUnit::Mm,
LengthUnit::Pt,
LengthUnit::Pc,
LengthUnit::Px,
LengthUnit::Cm,
LengthUnit::M,
LengthUnit::Ft,
LengthUnit::Yd,
];
pub fn unit(self) -> Unit {
match self {
Self::Pt => Unit::Points,
Self::Pc => Unit::Picas,
Self::Px => Unit::Pixels,
Self::In => Unit::Inches,
Self::Mm => Unit::Millimeters,
Self::Cm => Unit::Centimeters,
Self::M => Unit::Meters,
Self::Ft => Unit::Feet,
Self::Yd => Unit::Yards,
}
}
/// The suffix written after a length.
pub fn suffix(self) -> &'static str {
match self {
Self::Pt => "pt",
Self::Pc => "pc",
Self::Px => "px",
Self::In => "in",
Self::Mm => "mm",
Self::Cm => "cm",
Self::M => "m",
Self::Ft => "ft",
Self::Yd => "yd",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LineType {
#[default]
Solid,
Dashed,
Dotted,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ArrowStyle {
#[default]
Filled,
Open,
Tick,
None,
}
/// Where the label sits across the dimension line.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TextPosition {
/// On the side of the dimension line away from the measured object.
#[default]
Away,
/// Between the dimension line and the measured object.
Between,
}
/// Where the label sits along the dimension line.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TextAlignment {
Left,
#[default]
Center,
Right,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct DimensionOptions {
pub units: LengthUnit,
pub precision: usize,
/// Drawing : real size. 1:10 multiplies the drawn length by ten.
pub scale: String,
pub hide_units: bool,
pub line_weight: f64,
pub line_type: LineType,
pub line_color: [u8; 3],
pub arrow_style: ArrowStyle,
pub arrow_scale: f64,
pub extension_weight: f64,
pub extension_type: LineType,
pub extension_color: [u8; 3],
pub extension_offset: f64,
pub extension_length: f64,
pub hide_extensions: bool,
pub font_family: String,
pub font_style: String,
pub font_size: f64,
pub text_color: [u8; 3],
pub text_position: TextPosition,
pub text_alignment: TextAlignment,
pub text_offset: f64,
}
impl Default for DimensionOptions {
fn default() -> Self {
// The document's own type defaults: a font every build has, black, in points.
let type_style = crate::CharStyle::default();
Self {
units: LengthUnit::Pt,
precision: 2,
scale: "1:1".into(),
hide_units: false,
line_weight: 0.5,
line_type: LineType::Solid,
line_color: [0, 0, 0],
arrow_style: ArrowStyle::Filled,
arrow_scale: 100.0,
extension_weight: 0.5,
extension_type: LineType::Solid,
extension_color: [0, 0, 0],
extension_offset: 4.0,
extension_length: 6.0,
hide_extensions: false,
font_family: type_style.font_family,
font_style: type_style.font_style,
font_size: type_style.size,
text_color: [0, 0, 0],
text_position: TextPosition::Away,
text_alignment: TextAlignment::Center,
text_offset: 4.0,
}
}
}
impl DimensionOptions {
/// Options given to a command or a tool: unknown keys are refused there, while files load
/// tolerantly so a newer version's options don't stop a document opening.
pub fn from_params(v: serde_json::Value) -> Result<Self, String> {
if let (Some(given), Ok(serde_json::Value::Object(known))) = (v.as_object(), serde_json::to_value(Self::default()))
&& let Some(k) = given.keys().find(|k| !known.contains_key(*k))
{
return Err(format!("Unknown dimension option {k}"));
}
let o: Self = serde_json::from_value(v).map_err(|e| e.to_string())?;
o.validate()?;
Ok(o)
}
pub fn factor(&self) -> Result<f64, String> {
let (a, b) = self.scale.split_once(':').ok_or("Scale must be drawing:real, for example 1:10")?;
let parse = |s: &str| s.trim().parse::<f64>().ok().filter(|n| n.is_finite() && *n > 0.0 && *n <= 1e9);
let a = parse(a).ok_or("Invalid drawing scale")?;
let b = parse(b).ok_or("Invalid real scale")?;
let f = b / a;
if !(1e-6..=1e6).contains(&f) {
return Err("Scale ratio must be between 1:1000000 and 1000000:1".into());
}
Ok(f)
}
pub fn validate(&self) -> Result<(), String> {
self.factor()?;
if self.precision > 6 {
return Err("Choose a precision from 0 to 6".into());
}
for (v, min, max) in [
(self.line_weight, 0.1, 100.0),
(self.extension_weight, 0.1, 100.0),
(self.arrow_scale, 1.0, 1000.0),
(self.font_size, 1.0, 1000.0),
(self.extension_offset, 0.0, 10000.0),
(self.extension_length, 0.0, 10000.0),
(self.text_offset, 0.0, 10000.0),
] {
if !v.is_finite() || v < min || v > max {
return Err("Dimension size is outside its supported range".into());
}
}
if self.font_family.is_empty() || self.font_family.len() > 256 || self.font_style.len() > 128 {
return Err("Invalid dimension font name".into());
}
Ok(())
}
/// `value` in points for a length, in degrees for an angle.
pub fn label(&self, value: f64, angle: bool) -> Result<String, String> {
self.validate()?;
let value = if angle { value } else { value * self.factor()? / self.units.unit().points() };
if !value.is_finite() {
return Err("Non-finite dimension".into());
}
let suffix = if self.hide_units {
""
} else if angle {
"°"
} else {
self.units.suffix()
};
let sep = if suffix.is_empty() || angle { "" } else { " " };
Ok(format!("{:.*}{sep}{suffix}", self.precision, value))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SegmentRef {
pub node: NodeId,
pub subpath: usize,
pub segment: usize,
}
impl SegmentRef {
/// The segment's two end points.
pub fn endpoints(&self, doc: &Document) -> Option<(Point, Point)> {
let sp = doc.node(self.node)?.path_data()?.subpaths.get(self.subpath)?;
if self.segment >= sp.segment_count() {
return None;
}
let seg = sp.segment(self.segment);
Some((seg.p0, seg.p3))
}
/// Whether the segment is a straight line.
pub fn is_straight(&self, doc: &Document) -> bool {
doc.node(self.node)
.and_then(|n| n.path_data())
.and_then(|p| p.subpaths.get(self.subpath))
.is_some_and(|sp| self.segment < sp.segment_count() && sp.segment_is_line(self.segment))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum DimensionSource {
Segment {
edge: SegmentRef,
},
Angle {
first: SegmentRef,
second: SegmentRef,
},
Circle {
node: NodeId,
},
/// One segment that is a perfect arc.
Arc {
edge: SegmentRef,
},
Points {
a: Option<PointRef>,
b: Option<PointRef>,
vertex: Option<PointRef>,
},
}
impl DimensionSource {
pub fn nodes(&self) -> Vec<NodeId> {
match self {
Self::Segment { edge } | Self::Arc { edge } => vec![edge.node],
Self::Angle { first, second } => vec![first.node, second.node],
Self::Circle { node } => vec![*node],
Self::Points { a, b, vertex } => [a, b, vertex].into_iter().filter_map(|p| p.as_ref().map(|r| r.node)).collect(),
}
}
/// Anchor indexes cannot safely follow a topology replacement. Ordinary moves and handle
/// edits keep their topology and remain linked.
pub fn topology_changed(&self, before: &Document, after: &Document) -> bool {
if matches!(self, Self::Circle { .. }) {
return false;
}
self.nodes().iter().any(|id| {
let counts = |doc: &Document| {
doc.node(*id).and_then(|n| n.path_data()).map(|p| p.subpaths.iter().map(|sp| (sp.anchors.len(), sp.closed)).collect::<Vec<_>>())
};
let old = counts(before);
old.is_some() && old != counts(after)
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PointRef {
pub node: NodeId,
pub subpath: usize,
pub anchor: usize,
}
impl PointRef {
pub fn position(&self, doc: &Document) -> Option<Point> {
Some(doc.node(self.node)?.path_data()?.subpaths.get(self.subpath)?.anchors.get(self.anchor)?.p)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Dimension {
pub mode: DimensionMode,
pub a: Point,
pub b: Point,
/// Angular vertex. For radius/diameter a is the centre and b lies on the circle.
pub vertex: Point,
pub offset: f64,
pub length: f64,
pub options: DimensionOptions,
pub source: Option<DimensionSource>,
/// How far a linked dimension was moved on its own since the last refresh: the refresh turns
/// it into a new placement unless the measured art moved too.
#[serde(skip)]
pub moved: Option<Vec2>,
}
impl Dimension {
/// A point on the drawn dimension line (linear), arc (angular) or radius line.
pub fn handle(&self) -> Point {
match self.mode {
DimensionMode::Linear => self.a.midpoint(self.b) + self.normal() * self.offset,
DimensionMode::Angular => self.vertex + self.bisector() * self.offset.abs().max(12.0),
DimensionMode::Radius | DimensionMode::Diameter => self.b,
}
}
/// The left normal of a → b.
pub fn normal(&self) -> Vec2 {
let u = self.b - self.a;
let len = u.hypot();
if len > 1e-9 { Vec2::new(-u.y, u.x) / len } else { Vec2::new(0.0, 1.0) }
}
fn bisector(&self) -> Vec2 {
let start = (self.a - self.vertex).atan2();
Vec2::from_angle(start + self.angle() * 0.5)
}
/// Places the dimension through `p`: the line's distance from the art (linear), the arc's
/// radius (angular) or the label's direction (radial).
pub fn place(&mut self, p: Point) {
match self.mode {
DimensionMode::Linear => self.offset = (p - self.a).dot(self.normal()),
DimensionMode::Angular => self.offset = p.distance(self.vertex).max(12.0),
DimensionMode::Radius | DimensionMode::Diameter => {
if p.distance(self.a) > 1e-6 {
let r = self.a.distance(self.b);
let u = p - self.a;
self.b = self.a + u / u.hypot() * r;
}
}
}
}
pub fn transform(&mut self, xf: Affine) {
if self.source.is_some() {
// The art it measures decides where its ends are: record the move, and the refresh
// after this edit turns it into a new placement unless the art moved with it.
let at = self.handle();
self.moved = Some((xf * at) - at + self.moved.unwrap_or_default());
return;
}
self.a = xf * self.a;
self.b = xf * self.b;
self.vertex = xf * self.vertex;
if self.mode == DimensionMode::Linear {
self.length = self.a.distance(self.b);
}
}
pub fn resolve(&mut self, doc: &Document) -> Result<(), String> {
match &self.source {
Some(DimensionSource::Points { a, b, vertex }) => {
for (reference, point) in [(a, &mut self.a), (b, &mut self.b), (vertex, &mut self.vertex)] {
if let Some(r) = reference {
*point = r.position(doc).ok_or("Measured anchor no longer exists")?;
}
}
self.length = self.a.distance(self.b);
}
Some(DimensionSource::Segment { edge }) => {
let (a, b) = edge.endpoints(doc).ok_or("Measured segment no longer exists")?;
self.a = a;
self.b = b;
self.length = a.distance(b);
}
Some(DimensionSource::Angle { first, second }) => {
for edge in [first, second] {
if !edge.is_straight(doc) {
return Err("Angles require two straight segments".into());
}
}
let (a, b) = first.endpoints(doc).ok_or("First measured segment no longer exists")?;
let (c, d) = second.endpoints(doc).ok_or("Second measured segment no longer exists")?;
let u = b - a;
let v = d - c;
let den = u.cross(v);
if den.abs() < 1e-9 {
return Err("Choose two non-parallel straight segments".into());
}
let p = a + u * ((c - a).cross(v) / den);
self.vertex = p;
self.a = if a.distance(p) > b.distance(p) { a } else { b };
self.b = if c.distance(p) > d.distance(p) { c } else { d };
}
Some(DimensionSource::Circle { node }) => {
let (center, radius) = circle(doc, *node).ok_or("Choose a circle or a perfect arc")?;
self.set_radial(center, radius);
}
Some(DimensionSource::Arc { edge }) => {
let (center, radius) = arc(doc, edge).ok_or("Choose a circle or a perfect arc")?;
self.set_radial(center, radius);
}
None => {}
}
self.validate()
}
fn set_radial(&mut self, center: Point, radius: f64) {
let direction = self.b - self.a;
let dir = if direction.hypot() > 1e-9 { direction / direction.hypot() } else { Vec2::new(1.0, 0.0) };
self.a = center;
self.b = center + dir * radius;
self.length = radius;
}
pub fn validate(&self) -> Result<(), String> {
self.options.validate()?;
for p in [self.a, self.b, self.vertex] {
if !p.x.is_finite() || !p.y.is_finite() || p.x.abs() > 1e8 || p.y.abs() > 1e8 {
return Err("Dimension point is outside the canvas".into());
}
}
if !self.offset.is_finite() || self.offset.abs() > 1e6 || !self.length.is_finite() || self.length < 0.0 || self.length > 1e9 {
return Err("Invalid dimension distance".into());
}
if self.a.distance(self.b) < 1e-6 {
return Err("Choose two different points".into());
}
if self.mode == DimensionMode::Angular && (self.a.distance(self.vertex) < 1e-6 || self.b.distance(self.vertex) < 1e-6) {
return Err("Choose two nonzero angle arms".into());
}
Ok(())
}
pub fn angle(&self) -> f64 {
let u = self.a - self.vertex;
let v = self.b - self.vertex;
u.cross(v).atan2(u.dot(v))
}
pub fn label(&self) -> Result<String, String> {
let value = match self.mode {
DimensionMode::Angular => self.angle().abs().to_degrees(),
DimensionMode::Radius => self.a.distance(self.b),
DimensionMode::Diameter => self.a.distance(self.b) * 2.0,
DimensionMode::Linear => self.a.distance(self.b),
};
self.options.label(value, self.mode == DimensionMode::Angular)
}
}
/// Accept circular paths (including imported circles), reject ellipses and arbitrary closed shapes.
pub fn circle(doc: &Document, id: NodeId) -> Option<(Point, f64)> {
use vectorcraft_geom::kurbo::ParamCurve;
let path = doc.node(id)?.path_data()?;
let sp = path.subpaths.first()?;
if path.subpaths.len() != 1 || !sp.closed || !(2..=64).contains(&sp.segment_count()) {
return None;
}
let bounds = path.bounds()?;
let center = bounds.center();
let radius = bounds.width() * 0.5;
if radius < 1e-6 || (bounds.height() * 0.5 - radius).abs() > radius * 0.002 {
return None;
}
for i in 0..sp.segment_count() {
for t in [0.0, 0.25, 0.5, 0.75] {
if (sp.segment(i).eval(t).distance(center) - radius).abs() > radius * 0.002 {
return None;
}
}
}
Some((center, radius))
}
/// A curved segment that follows one circle (a perfect arc): its centre and radius.
pub fn arc(doc: &Document, edge: &SegmentRef) -> Option<(Point, f64)> {
use vectorcraft_geom::kurbo::ParamCurve;
let sp = doc.node(edge.node)?.path_data()?.subpaths.get(edge.subpath)?;
if edge.segment >= sp.segment_count() || sp.segment_is_line(edge.segment) {
return None;
}
let seg = sp.segment(edge.segment);
let (p, q, r) = (seg.p0, seg.eval(0.5), seg.p3);
// The circle through the ends and the middle of the segment.
let d = 2.0 * (p.x * (q.y - r.y) + q.x * (r.y - p.y) + r.x * (p.y - q.y));
if d.abs() < 1e-9 {
return None;
}
let sq = |a: Point| a.x * a.x + a.y * a.y;
let center = Point::new(
(sq(p) * (q.y - r.y) + sq(q) * (r.y - p.y) + sq(r) * (p.y - q.y)) / d,
(sq(p) * (r.x - q.x) + sq(q) * (p.x - r.x) + sq(r) * (q.x - p.x)) / d,
);
let radius = center.distance(p);
if !radius.is_finite() || !(1e-6..=1e8).contains(&radius) {
return None;
}
for t in [0.125, 0.25, 0.375, 0.625, 0.75, 0.875] {
if (seg.eval(t).distance(center) - radius).abs() > radius * 0.002 {
return None;
}
}
Some((center, radius))
}
/// The two straight segments that meet at a corner anchor, if both are straight.
pub fn corner(doc: &Document, node: NodeId, subpath: usize, anchor: usize) -> Option<(SegmentRef, SegmentRef)> {
let sp = doc.node(node)?.path_data()?.subpaths.get(subpath)?;
let count = sp.segment_count();
let n = sp.anchors.len();
if count < 2 || anchor >= n {
return None;
}
// Segment i runs from anchor i to anchor i + 1 (wrapping on closed paths).
let before = if anchor > 0 {
anchor - 1
} else if sp.closed {
count - 1
} else {
return None;
};
let after = anchor;
if after >= count {
return None;
}
let first = SegmentRef { node, subpath, segment: before };
let second = SegmentRef { node, subpath, segment: after };
(first.is_straight(doc) && second.is_straight(doc)).then_some((first, second))
}
+13
View File
@@ -12,6 +12,7 @@ pub mod clipnest;
pub mod cmyk;
pub mod corners;
pub mod depth;
pub mod dimension;
pub mod graph;
pub mod hit;
mod id_index;
@@ -686,6 +687,10 @@ pub struct Document {
/// state, never saved.
#[serde(skip)]
pub puppet: Option<Arc<PuppetPins>>,
/// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this
/// is set, so documents without any pay nothing for them.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub dimensions: bool,
/// Where each node is in the tree, for lookups by id ([`id_index`]): a cache, never saved.
#[serde(skip)]
index: id_index::IdIndex,
@@ -766,6 +771,7 @@ impl Document {
print_setup: None,
assets: vec![],
puppet: None,
dimensions: false,
index: Default::default(),
};
let id = d.alloc_id();
@@ -1399,6 +1405,13 @@ impl Document {
pub fn reid(&mut self, node: &Node) -> Node {
let mut n = node.clone();
n.id = self.alloc_id();
// A copied annotation must never bind its source ids to unrelated art in a different
// document. Keep the measured geometry; new measurements can be linked after pasting.
if let Some(e) = n.extras_if_any()
&& let Some(dimension) = &mut e.dimension
{
dimension.source = None;
}
if let Some(ch) = n.children_mut() {
let old: Vec<Arc<Node>> = std::mem::take(ch);
*ch = old.iter().map(|c| Arc::new(self.reid(c))).collect();
+15
View File
@@ -555,6 +555,9 @@ pub struct NodeExtras {
pub slice: Option<Box<crate::SliceOptions>>,
/// Object › Perspective: the perspective grid plane the object is attached to.
pub perspective: Option<Box<crate::PerspectiveAttachment>>,
/// Dimension object: the live measurement it draws; its ordinary group children hold its
/// exportable appearance (Object › Dimensions, the Dimension tool).
pub dimension: Option<Box<crate::dimension::Dimension>>,
}
/// No extras: what [`Node::extras`] gives for a node without any.
@@ -569,6 +572,7 @@ static NO_EXTRAS: NodeExtras = NodeExtras {
attrs: None,
slice: None,
perspective: None,
dimension: None,
};
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
@@ -699,6 +703,7 @@ impl Serialize for Node {
e.slice.is_none(),
is_default(&self.bbox_angle),
e.perspective.is_none(),
e.dimension.is_none(),
];
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
let mut skip = skips.into_iter();
@@ -733,6 +738,7 @@ impl Serialize for Node {
field!("slice", &e.slice);
field!("bbox_angle", &self.bbox_angle);
field!("perspective", &e.perspective);
field!("dimension", &e.dimension);
st.end()
}
}
@@ -784,6 +790,8 @@ struct NodeWire {
bbox_angle: f64,
#[serde(default)]
perspective: Option<Box<crate::PerspectiveAttachment>>,
#[serde(default)]
dimension: Option<Box<crate::dimension::Dimension>>,
}
impl From<NodeWire> for Node {
@@ -799,6 +807,7 @@ impl From<NodeWire> for Node {
attrs: w.attrs,
slice: w.slice,
perspective: w.perspective,
dimension: w.dimension,
};
Node {
id: w.id,
@@ -1159,6 +1168,12 @@ impl Node {
self.transform_scaled(a, &scaling.into());
}
fn transform_scaled(&mut self, a: Affine, sc: &Scaling) {
// A dimension's measurement moves with it.
if let Some(e) = self.extras_if_any()
&& let Some(dimension) = &mut e.dimension
{
dimension.transform(a);
}
if !self.is_layer() {
self.bbox_angle = crate::orient::transformed_angle(self.bbox_angle, a);
}
+369
View File
@@ -0,0 +1,369 @@
//! Linked dimensions: generated groups share the existing text/path render and export pipeline.
use super::*;
use serde_json::{Value, json};
use std::sync::Arc;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::dimension::{ArrowStyle, Dimension, DimensionMode, DimensionOptions, DimensionSource, LineType, TextAlignment, TextPosition};
use vectorcraft_doc::{Appearance, Arrowhead, CharStyle, Dash, Document, Node, NodeId, NodeKind, TextObject};
use vectorcraft_geom::{Affine, BezPath, PathData, Point, Vec2};
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"dimension.create",
"Create Dimension",
[],
None,
"{mode?: linear|angular|radius|diameter, a?: [x,y], b?: [x,y], vertex?: [x,y], source?: {kind: segment, edge: {node,subpath,segment}}|{kind: angle, first: edge, second: edge}|{kind: circle, node}|{kind: arc, edge}|{kind: points, a?, b?, vertex?: {node,subpath,anchor}}, offset?: pt, options?: {units: in|mm|pt|pc|px|cm|m|ft|yd, precision: 0-6, scale: drawing:real, hideUnits, lineWeight, lineType: solid|dashed|dotted, lineColor: [r,g,b], arrowStyle: filled|open|tick|none, arrowScale: %, extensionWeight, extensionType, extensionColor, extensionOffset, extensionLength, hideExtensions, fontFamily, fontStyle, fontSize, textColor, textPosition: away|between, textAlignment: left|center|right, textOffset}} → {id,label}; linked measurements follow source edits",
has_doc,
create
),
cmd!(
"dimension.update",
"Update Dimensions",
[],
None,
"{ids?: [dimension ids] (default selection), options?: dimension options, offset?: pt} restyle or move existing dimensions → {ids}",
has_doc,
update
),
cmd!(
"dimension.applyToAll",
"Apply to All",
[],
None,
"{id?: dimension id (default the first selected dimension)} gives every dimension in the document that dimension's style (its scale too), as one undo step → {ids}",
has_doc,
apply_to_all
),
]
}
fn error(s: impl ToString) -> EngineError {
EngineError::Other(s.to_string())
}
fn options(s: &Session, p: &Value) -> Result<DimensionOptions> {
let default = s.tool_options_of("dimension");
let value = p.get("options").cloned().unwrap_or_else(|| default.get("style").cloned().unwrap_or(json!({})));
DimensionOptions::from_params(value).map_err(error)
}
fn point(p: &Value, key: &str) -> Result<Point> {
let a = p.get(key).and_then(Value::as_array).filter(|a| a.len() == 2).ok_or_else(|| error(format!("{key} must be [x,y]")))?;
let x = a.first().and_then(Value::as_f64).ok_or_else(|| error("Invalid point"))?;
let y = a.get(1).and_then(Value::as_f64).ok_or_else(|| error("Invalid point"))?;
Ok(Point::new(x, y))
}
fn distance(p: &Value, fallback: f64) -> Result<f64> {
match p.get("offset") {
Some(v) => v.as_f64().filter(|n| n.is_finite() && n.abs() <= 1e6).ok_or_else(|| error("Invalid dimension offset")),
None => Ok(fallback),
}
}
fn recipe(s: &Session, p: &Value) -> Result<Dimension> {
let mode: DimensionMode =
p.get("mode").map(|v| serde_json::from_value(v.clone()).map_err(|_| error("Unknown dimension mode"))).transpose()?.unwrap_or_default();
let source: Option<DimensionSource> = p.get("source").map(|v| serde_json::from_value(v.clone()).map_err(error)).transpose()?;
if let Some(source) = &source {
let doc = &s.doc()?.doc;
for id in source.nodes() {
if doc.ancestry(id).is_some_and(|path| path.iter().any(|id| doc.node(*id).is_some_and(|n| n.extras().dimension.is_some()))) {
return Err(error("Choose original artwork, not a dimension's generated paths"));
}
}
let compatible = matches!(
(mode, source),
(DimensionMode::Linear, DimensionSource::Segment { .. })
| (DimensionMode::Angular, DimensionSource::Angle { .. })
| (DimensionMode::Radius | DimensionMode::Diameter, DimensionSource::Circle { .. } | DimensionSource::Arc { .. })
| (DimensionMode::Linear | DimensionMode::Angular, DimensionSource::Points { .. })
);
if !compatible {
return Err(error("Dimension mode does not match its source"));
}
}
let a = if source.is_none() || p.get("a").is_some() { point(p, "a")? } else { Point::ZERO };
let b = if source.is_none() || p.get("b").is_some() { point(p, "b")? } else { Point::new(1.0, 0.0) };
let vertex = if mode == DimensionMode::Angular && (source.is_none() || matches!(source, Some(DimensionSource::Points { .. }))) {
point(p, "vertex")?
} else {
Point::ZERO
};
let mut d = Dimension { mode, a, b, vertex, offset: distance(p, 30.0)?, length: a.distance(b), options: options(s, p)?, source, moved: None };
d.resolve(&s.doc()?.doc).map_err(error)?;
Ok(d)
}
fn insert(doc: &mut Document, parent: Option<NodeId>, dim: Dimension) -> Result<NodeId> {
let id = doc.alloc_id();
let mut group = Node::new(id, NodeKind::Group { children: vec![], clip: false });
group.name = Some(format!("Dimension · {}", dim.label().map_err(error)?));
group.kind = NodeKind::Group { children: artwork(doc, &dim, &[])?, clip: false };
doc.dimensions = true;
group.extras_mut().dimension = Some(Box::new(dim));
doc.insert(parent, usize::MAX, group)?;
Ok(id)
}
fn create(s: &mut Session, p: &Value) -> Result<Value> {
let dim = recipe(s, p)?;
let label = dim.label().map_err(error)?;
let parent = s.doc()?.insertion_parent();
s.note_journal("options", serde_json::to_value(&dim.options).map_err(error)?);
let id = s.edit("Create Dimension", |doc, sel| {
let id = insert(doc, parent, dim)?;
sel.set([id]);
Ok(id)
})?;
Ok(json!({"id":id.0,"label":label}))
}
fn dimension_of(doc: &Document, id: NodeId) -> Result<Dimension> {
doc.node(id).and_then(|n| n.extras().dimension.as_deref()).cloned().ok_or_else(|| error("Select only dimensions"))
}
fn update(s: &mut Session, p: &Value) -> Result<Value> {
let ids: Vec<NodeId> = match p.get("ids") {
Some(v) => serde_json::from_value(v.clone()).map_err(error)?,
None => s.doc()?.selection.objects.clone(),
};
if ids.is_empty() || ids.len() > 10_000 {
return Err(error("Select dimensions to update"));
}
let o = if p.get("options").is_some() { Some(options(s, p)?) } else { None };
s.edit("Update Dimensions", |doc, _| {
for id in &ids {
if !doc.is_editable(*id) {
return Err(error("Dimension is locked or hidden"));
}
let mut dim = dimension_of(doc, *id)?;
if let Some(o) = &o {
dim.options = o.clone();
}
dim.offset = distance(p, dim.offset)?;
dim.resolve(doc).map_err(error)?;
replace(doc, *id, dim)?;
}
Ok(())
})?;
Ok(json!({"ids":ids}))
}
fn apply_to_all(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let from = match p.get("id") {
Some(v) => NodeId(v.as_u64().ok_or_else(|| error("id must be a dimension id"))?),
None => *st
.selection
.objects
.iter()
.find(|id| st.doc.node(**id).is_some_and(|n| n.extras().dimension.is_some()))
.ok_or_else(|| error("Select a dimension first"))?,
};
let o = dimension_of(&st.doc, from)?.options;
let mut ids = vec![];
st.doc.walk(|n| {
if n.extras().dimension.is_some() && st.doc.is_editable(n.id) {
ids.push(n.id);
}
});
s.edit("Apply to All", |doc, _| {
for id in &ids {
let mut dim = dimension_of(doc, *id)?;
dim.options = o.clone();
dim.resolve(doc).map_err(error)?;
replace(doc, *id, dim)?;
}
Ok(())
})?;
Ok(json!({"ids":ids}))
}
fn replace(doc: &mut Document, id: NodeId, dim: Dimension) -> Result<()> {
let old: Vec<NodeId> = doc.node(id).and_then(Node::children).map(|ch| ch.iter().map(|n| n.id).collect()).unwrap_or_default();
let children = artwork(doc, &dim, &old)?;
if let Some(n) = doc.node_mut(id) {
n.kind = NodeKind::Group { children, clip: false };
n.name = Some(format!("Dimension · {}", dim.label().map_err(error)?));
n.extras_mut().dimension = Some(Box::new(dim));
}
Ok(())
}
/// Called inside every edit, so dimensions and their undo history stay in step with the art they
/// measure. Returns at once in documents without dimensions.
pub(crate) fn refresh(before: &Document, doc: &mut Document) -> Result<()> {
if !doc.dimensions {
return Ok(());
}
let mut dimensions = Vec::new();
doc.walk(|n| {
if let Some(d) = &n.extras().dimension {
dimensions.push((n.id, (**d).clone()));
}
});
doc.dimensions = !dimensions.is_empty();
for (id, old) in dimensions {
let mut next = old.clone();
let moved = next.moved.take();
if next.source.as_ref().is_some_and(|s| s.topology_changed(before, doc)) || next.resolve(doc).is_err() {
// A deleted/degenerate source has no valid measurement. Remove its annotation in
// this same undo step instead of keeping a silently wrong number.
doc.remove(id)?;
continue;
}
// Moved on its own (the art it measures stayed put): it keeps its new place.
if let Some(v) = moved
&& (next.a, next.b, next.vertex) == (old.a, old.b, old.vertex)
{
next.place(next.handle() + v);
if next.resolve(doc).is_err() {
next = old.clone();
next.moved = None;
}
}
if next != old || before.node(id).and_then(|n| n.extras().dimension.as_deref()).is_some_and(|d| d != &next) {
replace(doc, id, next)?;
}
}
Ok(())
}
fn paint(c: [u8; 3]) -> Paint {
Paint::solid(Color::rgb8(c[0], c[1], c[2]))
}
fn line(a: Point, b: Point) -> BezPath {
let mut p = BezPath::new();
p.move_to(a);
p.line_to(b);
p
}
fn appearance(o: &DimensionOptions, ext: bool, arrows: bool) -> Appearance {
let (color, width, kind) =
if ext { (o.extension_color, o.extension_weight, o.extension_type) } else { (o.line_color, o.line_weight, o.line_type) };
let mut a = Appearance::basic(Paint::None, paint(color), width);
if let Some(st) = a.stroke_mut() {
st.dash = match kind {
LineType::Dashed => Some(Dash { pattern: vec![width * 5.0, width * 3.0], ..Default::default() }),
LineType::Dotted => {
st.cap = vectorcraft_doc::LineCap::Round;
Some(Dash { pattern: vec![0.0, width * 3.0], ..Default::default() })
}
LineType::Solid => None,
};
if arrows {
let head = match o.arrow_style {
ArrowStyle::Filled => Some(Arrowhead::Triangle),
ArrowStyle::Open => Some(Arrowhead::TriangleOpen),
ArrowStyle::Tick => Some(Arrowhead::Slash),
ArrowStyle::None => None,
};
st.start_arrow = head;
st.end_arrow = head;
st.arrow_scale = (o.arrow_scale, o.arrow_scale);
st.arrow_align = vectorcraft_doc::appearance::ArrowAlign::Tip;
}
}
a
}
/// How far along the line (0 start, 0.5 middle, 1 end) the label sits, kept clear of the arrows.
fn along(o: &DimensionOptions) -> f64 {
match o.text_alignment {
TextAlignment::Left => 0.2,
TextAlignment::Center => 0.5,
TextAlignment::Right => 0.8,
}
}
fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<Node>>> {
d.validate().map_err(error)?;
let o = &d.options;
let label = d.label().map_err(error)?;
let mut paths: Vec<(BezPath, Appearance)> = Vec::new();
let mut angle = 0.0;
let text_at;
// Away from the object: past the dimension line; between: on the object's side of it.
let away = o.text_position == TextPosition::Away;
if d.mode == DimensionMode::Angular {
let start = (d.a - d.vertex).atan2();
let sweep = d.angle();
let r = d.offset.abs().max(12.0);
let on = |t: f64| d.vertex + Vec2::from_angle(start + sweep * t) * r;
let mut arc = BezPath::new();
arc.move_to(on(0.0));
for i in 1..=64 {
arc.line_to(on(i as f64 / 64.0));
}
paths.push((arc, appearance(o, false, true)));
if !o.hide_extensions {
for p in [d.a, d.b] {
let dir = (p - d.vertex) / (p - d.vertex).hypot();
paths.push((line(d.vertex + dir * o.extension_offset, d.vertex + dir * (r + o.extension_length)), appearance(o, true, false)));
}
}
let dir = Vec2::from_angle(start + sweep * along(o));
let gap = o.text_offset + o.font_size * 0.5;
text_at = d.vertex + dir * if away { r + gap } else { (r - gap).max(o.font_size * 0.5) };
} else {
let delta = d.b - d.a;
let u = delta / delta.hypot();
let n = Vec2::new(-u.y, u.x);
let (a, b) = match d.mode {
DimensionMode::Radius => (d.a, d.b),
DimensionMode::Diameter => (d.a - delta, d.b),
_ => (d.a + n * d.offset, d.b + n * d.offset),
};
// The side of the line facing away from the object.
let outward = match d.mode {
DimensionMode::Linear => {
if d.offset < 0.0 {
-n
} else {
n
}
}
_ => n,
};
if d.mode == DimensionMode::Linear && !o.hide_extensions {
for (p, q) in [(d.a, a), (d.b, b)] {
paths.push((line(p + outward * o.extension_offset, q + outward * o.extension_length), appearance(o, true, false)));
}
}
angle = u.atan2();
if !(-std::f64::consts::FRAC_PI_2..=std::f64::consts::FRAC_PI_2).contains(&angle) {
angle += std::f64::consts::PI;
}
let shift = o.text_offset + o.font_size * 0.5;
text_at = a.lerp(b, along(o)) + outward * if away { shift } else { -shift };
paths.push((line(a, b), appearance(o, false, true)));
}
let style = CharStyle {
font_family: o.font_family.clone(),
font_style: o.font_style.clone(),
size: o.font_size,
fill: paint(o.text_color),
..Default::default()
};
let mut text = TextObject::point(Point::ZERO, &label, style);
text.para.justify = vectorcraft_doc::Justify::Center;
super::typecmd::refresh_bounds(&mut text);
// Centre the label's box on its anchor point, along the line.
let bounds = text.cached_bounds.unwrap_or_else(|| text.estimate_bounds());
let middle = Affine::translate(-bounds.center().to_vec2());
text.xf = Affine::translate(text_at.to_vec2()) * Affine::rotate(angle) * middle;
let mut children = Vec::new();
for (path, a) in paths {
let id = old.get(children.len()).copied().unwrap_or_else(|| doc.alloc_id());
children.push(Arc::new(Node::path(id, PathData::from_bezpath(&path), a)));
}
let id = old.get(children.len()).copied().unwrap_or_else(|| doc.alloc_id());
let mut node = Node::new(id, NodeKind::Text(Box::new(text)));
node.appearance = Appearance::basic(paint(o.text_color), Paint::None, 0.0).into();
children.push(Arc::new(node));
Ok(children)
}
+2
View File
@@ -12,6 +12,7 @@ pub mod colortheme;
mod create;
mod css;
mod cut;
pub(crate) mod dimension;
pub(crate) mod distortcmds;
mod docinfo;
mod docmenu;
@@ -202,6 +203,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(recolor::specs());
v.extend(fileio::specs());
v.extend(create::specs());
v.extend(dimension::specs());
v.extend(object::specs());
v.extend(path::specs());
v.extend(select::specs());
+5 -1
View File
@@ -1462,7 +1462,11 @@ impl Session {
let before = st.doc.clone();
let before_sel = st.selection.clone();
let doc = Arc::make_mut(&mut st.doc);
let result = match f(doc, &mut st.selection) {
// Dimensions follow the art they measure.
let result = match f(doc, &mut st.selection).and_then(|v| {
cmd::dimension::refresh(&before, doc)?;
Ok(v)
}) {
Ok(v) => match cmd::shaper::refresh(&before, Arc::make_mut(&mut st.doc)) {
Err(e) => Err(e),
Ok(()) => {
+10
View File
@@ -453,6 +453,16 @@ impl Session {
if let Some(k) = p.get("key").and_then(Value::as_str) {
values.insert(k.to_string(), p.get("value").cloned().unwrap_or(Value::Null));
}
if tool.unwrap_or(self.tool_id()) == "dimension" {
if let Some(style) = values.get("style") {
vectorcraft_doc::dimension::DimensionOptions::from_params(style.clone())?;
}
if let Some(mode) = values.get("mode")
&& !mode.as_str().is_some_and(|m| ["linear", "angular", "radial", "radius", "diameter"].contains(&m))
{
return Err("Unknown dimension mode".into());
}
}
Ok(self.set_tool_options(tool, &values))
}
+391
View File
@@ -0,0 +1,391 @@
//! Linked measurements, history, portable artwork and hostile command input.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use serde_json::{Value, json};
use vectorcraft_engine::{
Session, ViewInfo,
doc::{Document, NodeId, NodeKind},
tools::{Mods, PointerEvent, PointerKind, ToolKey},
};
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width":600,"height":500})).unwrap();
s
}
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap()
}
fn edge(s: &mut Session) -> u64 {
run(s, "shape.line", json!({"x1":100,"y1":100,"x2":172,"y2":100}))["id"].as_u64().unwrap()
}
fn dim(s: &mut Session, node: u64) -> u64 {
run(
s,
"dimension.create",
json!({"source":{"kind":"segment","edge":{"node":node,"subpath":0,"segment":0}},"options":{"units":"in","precision":2,"scale":"1:10"}}),
)["id"]
.as_u64()
.unwrap()
}
fn text(s: &Session, id: u64) -> String {
let n = s.doc().unwrap().doc.node(NodeId(id)).unwrap();
n.children().unwrap().iter().find_map(|n| if let NodeKind::Text(t) = &n.kind { Some(t.plain_text()) } else { None }).unwrap()
}
fn recipe(s: &Session, id: u64) -> vectorcraft_doc::dimension::Dimension {
(**s.doc().unwrap().doc.node(NodeId(id)).unwrap().extras().dimension.as_ref().unwrap()).clone()
}
fn ev(s: &mut Session, kind: PointerKind, x: f64, y: f64) {
s.pointer(&PointerEvent::new(kind, x, y), ViewInfo::default()).unwrap();
}
#[test]
fn dimension_length_scale_live_edit_undo_and_roundtrip() {
let mut s = session();
let line = edge(&mut s);
let id = dim(&mut s, line);
assert_eq!(text(&s, id), "10.00 in");
run(&mut s, "select.set", json!({"ids":[line]}));
run(&mut s, "object.scale", json!({"sx":200,"sy":100}));
assert_eq!(text(&s, id), "20.00 in");
run(&mut s, "edit.undo", json!({}));
assert_eq!(text(&s, id), "10.00 in");
run(&mut s, "edit.redo", json!({}));
assert_eq!(text(&s, id), "20.00 in");
let json = serde_json::to_string(&*s.doc().unwrap().doc).unwrap();
let doc: Document = serde_json::from_str(&json).unwrap();
assert!(doc.dimensions, "saved with the document");
let mut reopened = Session::new();
reopened.add_document(doc, None);
run(&mut reopened, "select.set", json!({"ids":[line]}));
run(&mut reopened, "object.scale", json!({"sx":50,"sy":100}));
assert_eq!(text(&reopened, id), "10.00 in");
run(&mut reopened, "edit.clear", json!({}));
assert!(reopened.doc().unwrap().doc.node(NodeId(id)).is_none());
run(&mut reopened, "edit.undo", json!({}));
assert_eq!(text(&reopened, id), "10.00 in");
}
/// Documents without dimensions skip the refresh, and lose the flag when the last goes.
#[test]
fn documents_without_dimensions_skip_the_refresh() {
let mut s = session();
let line = edge(&mut s);
assert!(!s.doc().unwrap().doc.dimensions);
let id = dim(&mut s, line);
assert!(s.doc().unwrap().doc.dimensions);
run(&mut s, "select.set", json!({"ids":[id]}));
run(&mut s, "edit.clear", json!({}));
run(&mut s, "select.set", json!({"ids":[line]}));
run(&mut s, "object.move", json!({"dx":1,"dy":0}));
assert!(!s.doc().unwrap().doc.dimensions);
}
#[test]
fn apply_to_all_restyles_every_dimension_in_one_undo_step() {
let mut s = session();
let line = edge(&mut s);
let first = dim(&mut s, line);
let second = run(&mut s, "dimension.create", json!({"a":[0,0],"b":[72,0],"options":{"units":"pt","precision":0}}))["id"].as_u64().unwrap();
assert_eq!(text(&s, second), "72 pt");
run(&mut s, "dimension.applyToAll", json!({"id":first}));
assert_eq!(text(&s, second), "10.00 in", "the first dimension's style, its scale too");
run(&mut s, "edit.undo", json!({}));
assert_eq!(text(&s, second), "72 pt");
assert!(s.execute("dimension.applyToAll", &json!({"id":line})).is_err(), "not a dimension");
}
#[test]
fn dimension_angles_circles_arcs_and_curves() {
let mut s = session();
let a = run(
&mut s,
"dimension.create",
json!({"mode":"angular","a":[100,0],"vertex":[0,0],"b":[0,100],"options":{"precision":1,"scale":"1:10"}}),
)["id"]
.as_u64()
.unwrap();
assert_eq!(text(&s, a), "90.0°");
let circle = run(&mut s, "shape.ellipse", json!({"x":100,"y":100,"width":144,"height":144}))["id"].as_u64().unwrap();
for (mode, want) in [("radius", "1.0 in"), ("diameter", "2.0 in")] {
let id = run(
&mut s,
"dimension.create",
json!({"mode":mode,"source":{"kind":"circle","node":circle},"options":{"units":"in","precision":1}}),
)["id"]
.as_u64()
.unwrap();
assert_eq!(text(&s, id), want);
}
// One quarter of the circle is a perfect arc.
let id = run(
&mut s,
"dimension.create",
json!({"mode":"radius","source":{"kind":"arc","edge":{"node":circle,"subpath":0,"segment":0}},"options":{"units":"in","precision":1}}),
)["id"]
.as_u64()
.unwrap();
assert_eq!(text(&s, id), "1.0 in");
// A curve that isn't an arc: no radius, and its length is measured end to end.
let curve = run(&mut s, "path.create", json!({"d":"M 0 0 C 0 100 100 100 100 0"}))["id"].as_u64().unwrap();
let arc = json!({"mode":"radius","source":{"kind":"arc","edge":{"node":curve,"subpath":0,"segment":0}}});
assert!(s.execute("dimension.create", &arc).is_err());
let id = run(
&mut s,
"dimension.create",
json!({"source":{"kind":"segment","edge":{"node":curve,"subpath":0,"segment":0}},"options":{"units":"pt","precision":0}}),
)["id"]
.as_u64()
.unwrap();
assert_eq!(text(&s, id), "100 pt");
}
#[test]
fn dimension_rejects_bad_input_without_mutation() {
let mut s = session();
let line = edge(&mut s);
let id = dim(&mut s, line);
let original = serde_json::to_value(&*s.doc().unwrap().doc).unwrap();
for p in [
json!({}),
json!({"a":[0,0],"b":[0,0]}),
json!({"a":[0,0],"b":[1e100,0]}),
json!({"a":[0,0],"b":[10,0],"options":{"precision":99999}}),
json!({"a":[0,0],"b":[10,0],"options":{"scale":"0:10"}}),
json!({"a":[0,0],"b":[10,0],"options":{"scale":"1:NaN"}}),
json!({"a":[0,0],"b":[10,0],"options":{"units":"furlongs"}}),
json!({"a":[0,0],"b":[10,0],"options":{"textPosition":"sideways"}}),
json!({"a":[0,0],"b":[10,0],"mode":"spiral"}),
json!({"source":{"kind":"segment","edge":{"node":line,"subpath":0,"segment":18446744073709551615u64}}}),
json!({"mode":"angular","source":{"kind":"segment","edge":{"node":line,"subpath":0,"segment":0}}}),
] {
assert!(s.execute("dimension.create", &p).is_err(), "{p}");
assert_eq!(serde_json::to_value(&*s.doc().unwrap().doc).unwrap(), original);
}
assert!(s.execute("dimension.update", &json!({"ids":[id],"options":{"fontSize":-1}})).is_err());
assert_eq!(serde_json::to_value(&*s.doc().unwrap().doc).unwrap(), original);
}
/// Hover a segment and drag outwards: releasing places the dimension; Escape cancels the drag.
#[test]
fn dragging_out_from_a_segment_places_its_dimension() {
let mut s = session();
edge(&mut s);
s.select_tool("dimension", ViewInfo::default()).unwrap();
let before = s.doc().unwrap().doc.node_count();
ev(&mut s, PointerKind::Move, 130.0, 100.0);
ev(&mut s, PointerKind::Down, 130.0, 100.0);
ev(&mut s, PointerKind::Drag, 130.0, 70.0);
assert!(s.in_interaction());
s.tool_key(ToolKey::Escape, Mods::default(), ViewInfo::default()).unwrap();
assert_eq!(s.doc().unwrap().doc.node_count(), before);
assert!(!s.in_interaction());
ev(&mut s, PointerKind::Up, 130.0, 70.0);
ev(&mut s, PointerKind::Down, 130.0, 100.0);
ev(&mut s, PointerKind::Drag, 130.0, 80.0);
ev(&mut s, PointerKind::Drag, 130.0, 60.0);
ev(&mut s, PointerKind::Up, 130.0, 60.0);
assert!(!s.in_interaction());
assert!(s.doc().unwrap().doc.node_count() > before);
let id = s.doc().unwrap().selection.objects[0].0;
assert_eq!(text(&s, id), "72.00 pt");
assert!((recipe(&s, id).offset.abs() - 40.0).abs() < 1e-6, "placed where it was released");
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc().unwrap().doc.node_count(), before);
}
/// Plain clicks pick two points, and a third click places the dimension.
#[test]
fn clicking_two_points_then_placing_measures_between_them() {
let mut s = session();
s.select_tool("dimension", ViewInfo::default()).unwrap();
for (x, y) in [(300.0, 300.0), (372.0, 300.0)] {
ev(&mut s, PointerKind::Down, x, y);
ev(&mut s, PointerKind::Up, x, y);
}
ev(&mut s, PointerKind::Move, 336.0, 330.0);
assert!(s.in_interaction());
ev(&mut s, PointerKind::Down, 336.0, 330.0);
ev(&mut s, PointerKind::Up, 336.0, 330.0);
assert!(!s.in_interaction());
let id = s.doc().unwrap().selection.objects[0].0;
assert_eq!(text(&s, id), "72.00 pt");
}
/// Dragging out from a corner of two straight segments measures the angle between them.
#[test]
fn dragging_out_from_a_corner_measures_its_angle() {
let mut s = session();
run(&mut s, "shape.rectangle", json!({"x":100,"y":100,"width":100,"height":50}));
s.select_tool("dimension", ViewInfo::default()).unwrap();
s.set_tool_option_cmd(&json!({"tool":"dimension","values":{"mode":"angular"}})).unwrap();
ev(&mut s, PointerKind::Move, 100.0, 100.0);
ev(&mut s, PointerKind::Down, 100.0, 100.0);
ev(&mut s, PointerKind::Drag, 120.0, 120.0);
ev(&mut s, PointerKind::Drag, 130.0, 130.0);
ev(&mut s, PointerKind::Up, 130.0, 130.0);
let id = s.doc().unwrap().selection.objects[0].0;
assert_eq!(text(&s, id), "90.00°");
}
/// Dragging out from a circle gives its radius; dragging inwards, its diameter.
#[test]
fn dragging_from_a_circle_gives_its_radius_or_diameter() {
let mut s = session();
run(&mut s, "shape.ellipse", json!({"x":100,"y":100,"width":144,"height":144}));
s.select_tool("dimension", ViewInfo::default()).unwrap();
s.set_tool_option_cmd(&json!({"tool":"dimension","values":{"mode":"radial","style":{"units":"in","precision":0}}})).unwrap();
for ((x0, y0), (x1, y1), want) in [((244.0, 172.0), (280.0, 172.0), "1 in"), ((244.0, 172.0), (200.0, 172.0), "2 in")] {
ev(&mut s, PointerKind::Move, x0, y0);
ev(&mut s, PointerKind::Down, x0, y0);
ev(&mut s, PointerKind::Drag, (x0 + x1) / 2.0, y0);
ev(&mut s, PointerKind::Drag, x1, y1);
ev(&mut s, PointerKind::Up, x1, y1);
let id = s.doc().unwrap().selection.objects[0].0;
assert_eq!(text(&s, id), want);
}
}
#[test]
fn dimension_anchor_links_and_native_file_survive_source_moves() {
let mut s = session();
let first = edge(&mut s);
let second = edge(&mut s);
let id=run(&mut s,"dimension.create",json!({"a":[100,100],"b":[172,100],"source":{"kind":"points","a":{"node":first,"subpath":0,"anchor":0},"b":{"node":second,"subpath":0,"anchor":1},"vertex":null},"options":{"units":"pt","precision":0}}))["id"].as_u64().unwrap();
assert_eq!(text(&s, id), "72 pt");
let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false);
let doc = vectorcraft_format::load(&bytes).unwrap();
let mut s = Session::new();
s.add_document(doc, None);
run(&mut s, "select.set", json!({"ids":[second]}));
run(&mut s, "object.move", json!({"dx":28,"dy":0}));
assert_eq!(text(&s, id), "100 pt");
}
/// A linked dimension moved on its own keeps its new place when its art changes later; moved with
/// its art, it keeps its distance from it.
#[test]
fn a_moved_linked_dimension_stays_where_it_was_dropped() {
let mut s = session();
let line = edge(&mut s);
let id = dim(&mut s, line);
let offset = recipe(&s, id).offset;
run(&mut s, "select.set", json!({"ids":[id]}));
run(&mut s, "object.move", json!({"dx":0,"dy":-20}));
let moved = recipe(&s, id).offset;
assert!((moved - offset).abs() - 20.0 < 1e-6 && (moved - offset).abs() > 19.0, "{offset} → {moved}");
run(&mut s, "select.set", json!({"ids":[line]}));
run(&mut s, "object.scale", json!({"sx":200,"sy":100}));
assert!((recipe(&s, id).offset - moved).abs() < 1e-6, "no jump back");
run(&mut s, "select.set", json!({"ids":[line, id]}));
run(&mut s, "object.move", json!({"dx":0,"dy":50}));
assert!((recipe(&s, id).offset - moved).abs() < 1e-6, "moved together");
}
#[test]
fn dimension_options_are_persistent_and_invalid_updates_report_errors() {
let mut s = session();
s.set_tool_option_cmd(&json!({"tool":"dimension","values":{"style":{"scale":"10:1","units":"mm","precision":3},"mode":"radial"}})).unwrap();
assert_eq!(s.tool_options_of("dimension")["style"]["scale"], "10:1");
assert!(s.set_tool_option_cmd(&json!({"tool":"dimension","values":{"style":{"scale":"1:0"}}})).is_err());
let o: vectorcraft_doc::dimension::DimensionOptions = serde_json::from_value(s.tool_options_of("dimension")["style"].clone()).unwrap();
assert_eq!(o.label(72.0, false).unwrap(), "2.540 mm");
let picas: vectorcraft_doc::dimension::DimensionOptions = serde_json::from_value(json!({"units":"pc","precision":1})).unwrap();
assert_eq!(picas.label(72.0, false).unwrap(), "6.0 pc");
let prefs = serde_json::to_value(&s.prefs).unwrap();
let mut fresh = Session::new();
fresh.prefs = serde_json::from_value(prefs).unwrap();
assert_eq!(fresh.tool_options_of("dimension")["style"], s.tool_options_of("dimension")["style"]);
}
#[test]
fn dimension_unlinked_geometry_scales_and_updates_its_label() {
let mut s = session();
let id = run(&mut s, "dimension.create", json!({"a":[100,100],"b":[172,100],"options":{"units":"in","precision":0}}))["id"].as_u64().unwrap();
run(&mut s, "object.scale", json!({"sx":200,"sy":200}));
assert_eq!(text(&s, id), "2 in");
}
#[test]
fn dimension_linked_angle_and_circle_update_and_reject_non_circles() {
let mut s = session();
let first = edge(&mut s);
let second = run(&mut s, "shape.line", json!({"x1":100,"y1":100,"x2":100,"y2":172}))["id"].as_u64().unwrap();
let id=run(&mut s,"dimension.create",json!({"mode":"angular","source":{"kind":"angle","first":{"node":first,"subpath":0,"segment":0},"second":{"node":second,"subpath":0,"segment":0}},"options":{"precision":1}}))["id"].as_u64().unwrap();
assert_eq!(text(&s, id), "90.0°");
run(&mut s, "select.set", json!({"ids":[second]}));
run(&mut s, "object.rotate", json!({"angle":45,"origin":[100,100]}));
assert!(["45.0°", "135.0°"].contains(&text(&s, id).as_str()));
let circle = run(&mut s, "shape.ellipse", json!({"x":100,"y":100,"width":144,"height":144}))["id"].as_u64().unwrap();
let id = run(
&mut s,
"dimension.create",
json!({"mode":"diameter","source":{"kind":"circle","node":circle},"options":{"units":"in","precision":0}}),
)["id"]
.as_u64()
.unwrap();
run(&mut s, "select.set", json!({"ids":[circle]}));
run(&mut s, "object.scale", json!({"sx":200,"sy":200}));
assert_eq!(text(&s, id), "4 in");
run(&mut s, "object.scale", json!({"sx":200,"sy":100}));
assert!(s.doc().unwrap().doc.node(NodeId(id)).is_none());
assert!(s.execute("dimension.create", &json!({"mode":"radius","source":{"kind":"circle","node":circle}})).is_err());
}
#[test]
fn dimension_style_update_keeps_links_and_can_be_undone() {
let mut s = session();
let line = edge(&mut s);
let id = dim(&mut s, line);
run(
&mut s,
"dimension.update",
json!({"ids":[id],"offset":-42,"options":{"units":"mm","precision":2,"hideUnits":true,"textPosition":"between","textAlignment":"right","lineColor":[10,20,30]}}),
);
assert_eq!(text(&s, id), "25.40");
let d = recipe(&s, id);
assert!(d.source.is_some());
assert_eq!(d.offset, -42.0);
run(&mut s, "edit.undo", json!({}));
assert_eq!(text(&s, id), "10.00 in");
}
#[test]
fn dimension_cannot_measure_its_own_generated_art() {
let mut s = session();
let line = edge(&mut s);
let id = dim(&mut s, line);
let child = s.doc().unwrap().doc.node(NodeId(id)).unwrap().children().unwrap()[0].id;
assert!(s.execute("dimension.create", &json!({"source":{"kind":"segment","edge":{"node":child,"subpath":0,"segment":0}}})).is_err());
}
#[test]
fn unknown_dimension_options_are_refused_by_commands_but_a_newer_file_still_opens() {
let mut s = session();
let line = edge(&mut s);
let source = json!({"kind":"segment","edge":{"node":line,"subpath":0,"segment":0}});
let bad = s.execute("dimension.create", &json!({"source":source,"options":{"units":"in","color":"red"}}));
assert!(bad.unwrap_err().to_string().contains("Unknown dimension option color"));
assert!(s.set_tool_option_cmd(&json!({"tool":"dimension","values":{"style":{"precison":2}}})).is_err());
let id = dim(&mut s, line);
let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false);
let mut v: Value = serde_json::from_slice(&saved).unwrap();
// A later version's dimension carries fields this one doesn't know.
fn add_unknown(v: &mut Value) {
match v {
Value::Object(m) => {
if let Some(Value::Object(d)) = m.get_mut("dimension") {
d.insert("later".into(), json!(1));
if let Some(Value::Object(o)) = d.get_mut("options") {
o.insert("later".into(), json!(true));
}
}
m.values_mut().for_each(add_unknown);
}
Value::Array(a) => a.iter_mut().for_each(add_unknown),
_ => {}
}
}
add_unknown(&mut v);
let doc = vectorcraft_format::load(&serde_json::to_vec(&v).unwrap()).unwrap();
assert!(doc.node(NodeId(id)).unwrap().extras().dimension.is_some());
}
+1
View File
@@ -102,6 +102,7 @@ pub const TOOL_GROUPS: &[&[ToolInfo]] = &[
&[t("gradient", "Gradient Tool", Some("G"), "tool-gradient")],
&[t("eyedropper", "Eyedropper Tool", Some("I"), "tool-eyedropper"), t("measure", "Measure Tool", None, "tool-measure")],
&[t("blend", "Blend Tool", Some("W"), "tool-blend")],
&[t("dimension", "Dimension Tool", None, "tool-dimension")],
&[
t("symbolSprayer", "Symbol Sprayer Tool", Some("Shift+S"), "tool-symbol"),
t("symbolShifter", "Symbol Shifter Tool", None, "tool-symbol"),
+400
View File
@@ -0,0 +1,400 @@
//! The Dimension tool. Hover a segment (linear), a corner (angular) or a circle or perfect arc
//! (radial) and drag outwards: releasing places the linked dimension. Plain clicks pick what to
//! measure instead: two points (linear) or two straight lines (angular), then a click places it;
//! a click on a circle or arc gives its radius. Escape cancels without leaving any artwork.
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
use serde_json::{Value, json};
use vectorcraft_doc::{
Node,
dimension::{self, Dimension, DimensionMode, DimensionOptions, DimensionSource, PointRef, SegmentRef},
};
use vectorcraft_geom::{BezPath, Point, Vec2};
/// The three kinds of dimension the tool draws.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Kind {
#[default]
Linear,
Angular,
Radial,
}
impl Kind {
pub fn id(self) -> &'static str {
match self {
Kind::Linear => "linear",
Kind::Angular => "angular",
Kind::Radial => "radial",
}
}
/// `radius` and `diameter` are the radial kind.
pub fn parse(s: &str) -> Option<Kind> {
match s {
"linear" => Some(Kind::Linear),
"angular" => Some(Kind::Angular),
"radial" | "radius" | "diameter" => Some(Kind::Radial),
_ => None,
}
}
}
/// What the pointer is over.
#[derive(Clone, Debug, PartialEq)]
enum Target {
Segment(SegmentRef),
Corner { first: SegmentRef, second: SegmentRef, at: Point },
Radial { source: DimensionSource, center: Point, radius: f64 },
}
#[derive(Default)]
pub struct DimensionTool {
kind: Kind,
style: DimensionOptions,
/// Where the button went down, and what was under it.
press: Option<(Point, Option<Target>)>,
/// Plain-click picks: points (linear) or straight lines (angular).
points: Vec<(Point, Option<PointRef>)>,
lines: Vec<SegmentRef>,
/// A dimension being placed: previews follow the pointer.
pending: Option<Dimension>,
dragging: bool,
hover: Option<Target>,
at: Point,
began: bool,
}
/// Nearest editable segment, with bounded work and no snapping to dimension artwork itself.
pub fn pick(cx: &ToolContext, p: Point) -> Option<SegmentRef> {
let mut best = None;
let mut dist = cx.tol(8.0);
let mut budget = 20_000usize;
fn visit(n: &Node, p: Point, budget: &mut usize, dist: &mut f64, best: &mut Option<SegmentRef>) {
if *budget == 0 || !n.visible || n.locked || n.extras().dimension.is_some() {
return;
}
if let Some(path) = n.path_data() {
for (si, sp) in path.subpaths.iter().enumerate() {
for i in 0..sp.segment_count() {
if *budget == 0 {
return;
}
*budget -= 1;
use vectorcraft_geom::kurbo::ParamCurveNearest;
let near = sp.segment(i).nearest(p, 1e-4);
if near.distance_sq < *dist * *dist {
*dist = near.distance_sq.sqrt();
*best = Some(SegmentRef { node: n.id, subpath: si, segment: i });
}
}
}
}
if let Some(ch) = n.children() {
for c in ch.iter().rev() {
visit(c, p, budget, dist, best);
}
}
}
for n in cx.doc.layers.iter().rev() {
visit(n, p, &mut budget, &mut dist, &mut best);
}
best
}
/// The anchor of `edge` within reach of `p`, if any.
fn anchor_near(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<(usize, Point)> {
let sp = cx.doc.node(edge.node)?.path_data()?.subpaths.get(edge.subpath)?;
let next = if edge.segment + 1 < sp.anchors.len() { edge.segment + 1 } else { 0 };
[edge.segment, next]
.into_iter()
.filter_map(|i| Some((i, sp.anchors.get(i)?.p)))
.filter(|(_, a)| a.distance(p) <= cx.tol(8.0))
.min_by(|(_, a), (_, b)| a.distance(p).total_cmp(&b.distance(p)))
}
impl DimensionTool {
fn target(&self, cx: &ToolContext, p: Point) -> Option<Target> {
let edge = pick(cx, p)?;
match self.kind {
Kind::Linear => Some(Target::Segment(edge)),
Kind::Angular => {
let (anchor, at) = anchor_near(cx, &edge, p)?;
let (first, second) = dimension::corner(cx.doc, edge.node, edge.subpath, anchor)?;
Some(Target::Corner { first, second, at })
}
Kind::Radial => {
if let Some((center, radius)) = dimension::circle(cx.doc, edge.node) {
return Some(Target::Radial { source: DimensionSource::Circle { node: edge.node }, center, radius });
}
let (center, radius) = dimension::arc(cx.doc, &edge)?;
Some(Target::Radial { source: DimensionSource::Arc { edge }, center, radius })
}
}
}
fn recipe(&self, mode: DimensionMode, a: Point, b: Point, vertex: Point, source: Option<DimensionSource>) -> Dimension {
Dimension { mode, a, b, vertex, offset: 0.0, length: a.distance(b), options: self.style.clone(), source, moved: None }
}
/// The dimension a target gives when dragged from.
fn dimension_for(&self, target: &Target, p: Point) -> Option<Dimension> {
let mut d = match target {
Target::Segment(edge) => self.recipe(
DimensionMode::Linear,
Point::ZERO,
Point::new(1.0, 0.0),
Point::ZERO,
Some(DimensionSource::Segment { edge: edge.clone() }),
),
Target::Corner { first, second, .. } => self.recipe(
DimensionMode::Angular,
Point::ZERO,
Point::new(1.0, 0.0),
Point::ZERO,
Some(DimensionSource::Angle { first: first.clone(), second: second.clone() }),
),
Target::Radial { source, center, radius } => {
// Dragged outwards: the radius; inwards: the diameter.
let mode = if p.distance(*center) >= *radius { DimensionMode::Radius } else { DimensionMode::Diameter };
self.recipe(mode, *center, p, Point::ZERO, Some(source.clone()))
}
};
d.offset = 30.0;
Some(d)
}
fn params(d: &Dimension) -> Value {
let mut p = json!({"mode":d.mode,"a":[d.a.x,d.a.y],"b":[d.b.x,d.b.y],"vertex":[d.vertex.x,d.vertex.y],"offset":d.offset,"options":d.options});
if let Some(source) = &d.source {
p["source"] = json!(source);
}
p
}
fn preview(&mut self, cx: &ToolContext, p: Point) -> Vec<Action> {
let Some(d) = &mut self.pending else { return vec![] };
if matches!(d.mode, DimensionMode::Radius | DimensionMode::Diameter)
&& let Some(Target::Radial { center, radius, .. }) = self.press.as_ref().and_then(|(_, t)| t.as_ref())
{
d.mode = if p.distance(*center) >= *radius { DimensionMode::Radius } else { DimensionMode::Diameter };
}
d.place(p);
if d.resolve(cx.doc).is_err() {
return vec![];
}
let mut actions = vec![];
if !self.began {
actions.push(Action::Begin("Create Dimension".into()));
self.began = true;
}
actions.push(Action::Preview("dimension.create".into(), Self::params(d)));
actions
}
/// Places the pending dimension at `p` and commits it.
fn place(&mut self, cx: &ToolContext, p: Point) -> Vec<Action> {
let mut out = self.preview(cx, p);
if self.began {
out.push(Action::Commit);
}
self.reset();
out
}
fn reset(&mut self) {
self.pending = None;
self.press = None;
self.points.clear();
self.lines.clear();
self.dragging = false;
self.began = false;
}
/// A plain click: picks a point or a line, or measures a circle's radius.
fn click(&mut self, cx: &ToolContext, p: Point, target: Option<Target>, mods: Mods) -> Vec<Action> {
match self.kind {
Kind::Linear => {
let (mut at, _) = crate::guides::snap_pick(cx, p);
let mut reference = None;
if let Some(edge) = pick(cx, p)
&& let Some((anchor, point)) = anchor_near(cx, &edge, p)
{
at = point;
reference = Some(PointRef { node: edge.node, subpath: edge.subpath, anchor });
}
if mods.shift
&& let Some((last, _)) = self.points.last()
{
at = *last + vectorcraft_geom::constrain_angle_from(at - *last, 45.0, cx.constrain_angle);
reference = reference.filter(|r| r.position(cx.doc) == Some(at));
}
self.points.push((at, reference));
if let [(a, ra), (b, rb)] = self.points.as_slice() {
let source = (ra.is_some() || rb.is_some()).then(|| DimensionSource::Points { a: ra.clone(), b: rb.clone(), vertex: None });
let mut d = self.recipe(DimensionMode::Linear, *a, *b, Point::ZERO, source);
if d.resolve(cx.doc).is_ok() {
self.pending = Some(d);
}
self.points.clear();
}
vec![]
}
Kind::Angular => {
let Some(edge) = pick(cx, p).filter(|e| e.is_straight(cx.doc)) else { return vec![] };
if self.lines.first().is_some_and(|first| *first == edge) {
return vec![];
}
self.lines.push(edge);
if let [first, second] = self.lines.as_slice() {
let mut d = self.recipe(
DimensionMode::Angular,
Point::ZERO,
Point::new(1.0, 0.0),
Point::ZERO,
Some(DimensionSource::Angle { first: first.clone(), second: second.clone() }),
);
if d.resolve(cx.doc).is_ok() {
d.offset = 30.0;
self.pending = Some(d);
}
self.lines.clear();
}
vec![]
}
Kind::Radial => {
let Some(target @ Target::Radial { .. }) = target else { return vec![] };
let Some(mut d) = self.dimension_for(&target, p) else { return vec![] };
d.mode = DimensionMode::Radius;
self.pending = Some(d);
self.press = Some((p, Some(target)));
self.place(cx, p)
}
}
}
}
impl Tool for DimensionTool {
fn id(&self) -> &'static str {
"dimension"
}
fn busy(&self) -> bool {
self.pending.is_some() || !self.points.is_empty() || !self.lines.is_empty()
}
fn cursor(&self, _: &ToolContext, _: Point, _: Mods) -> Cursor {
Cursor::Crosshair
}
fn options(&self) -> Value {
json!({"mode":self.kind.id(),"style":self.style})
}
fn set_option(&mut self, key: &str, value: &Value) {
if key == "mode"
&& let Some(kind) = value.as_str().and_then(Kind::parse)
{
self.kind = kind;
}
if key == "style"
&& let Ok(o) = serde_json::from_value::<DimensionOptions>(value.clone())
&& o.validate().is_ok()
{
self.style = o;
}
}
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
self.at = ev.pos;
match ev.kind {
PointerKind::Move => {
if self.pending.is_some() {
return self.preview(cx, ev.pos);
}
self.hover = self.target(cx, ev.pos);
vec![]
}
PointerKind::Down => {
if self.pending.is_some() && !self.dragging {
// The second click of a picked measurement places it.
return self.place(cx, ev.pos);
}
self.press = Some((ev.pos, self.target(cx, ev.pos)));
vec![]
}
PointerKind::Drag => {
if self.pending.is_none()
&& let Some((start, Some(target))) = self.press.clone()
&& start.distance(ev.pos) > cx.tol(3.0)
&& self.points.is_empty()
&& self.lines.is_empty()
{
self.pending = self.dimension_for(&target, ev.pos);
self.dragging = self.pending.is_some();
}
if self.dragging { self.preview(cx, ev.pos) } else { vec![] }
}
PointerKind::Up => {
if self.dragging {
return self.place(cx, ev.pos);
}
match self.press.take() {
Some((start, target)) if self.pending.is_none() => self.click(cx, start, target, ev.mods),
_ => vec![],
}
}
_ => vec![],
}
}
fn key(&mut self, _: &ToolContext, key: ToolKey, _: Mods) -> Vec<Action> {
if key == ToolKey::Escape {
let cancel = self.began;
self.reset();
return if cancel { vec![Action::Cancel] } else { vec![] };
}
vec![]
}
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
self.key(cx, ToolKey::Escape, Mods::default())
}
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
let color = [60, 150, 255];
let mut out = vec![];
let segment = |edge: &SegmentRef, out: &mut Vec<Overlay>| {
if let Some(sp) = cx.doc.node(edge.node).and_then(Node::path_data).and_then(|p| p.subpaths.get(edge.subpath))
&& edge.segment < sp.segment_count()
{
let c = sp.segment(edge.segment);
let mut path = BezPath::new();
path.move_to(c.p0);
path.curve_to(c.p1, c.p2, c.p3);
out.push(Overlay::Path { path, color, width: 3.0, dashed: false });
}
};
if self.pending.is_none() {
match &self.hover {
Some(Target::Segment(edge)) => {
segment(edge, &mut out);
if let Some((a, b)) = edge.endpoints(cx.doc) {
for p in [a, b] {
out.push(Overlay::Anchor { p, color, filled: true, size: 6.0 });
}
if let Ok(text) = self.style.label(a.distance(b), false) {
out.push(Overlay::Measure { p: self.at + Vec2::new(12.0, 12.0), text });
}
}
}
Some(Target::Corner { first, second, at }) => {
segment(first, &mut out);
segment(second, &mut out);
out.push(Overlay::Anchor { p: *at, color, filled: true, size: 7.0 });
}
Some(Target::Radial { center, radius, .. }) => {
out.push(Overlay::Anchor { p: *center, color, filled: false, size: 6.0 });
let toward = self.at - *center;
if toward.hypot() > 1e-9 {
out.push(Overlay::Line { a: *center, b: *center + toward / toward.hypot() * *radius, color, dashed: true });
}
}
None => {}
}
}
for (p, _) in &self.points {
out.push(Overlay::Anchor { p: *p, color, filled: true, size: 6.0 });
}
for edge in &self.lines {
segment(edge, &mut out);
}
if let Some((p, _)) = self.points.last() {
out.push(Overlay::Line { a: *p, b: self.at, color, dashed: true });
}
out
}
}
+2
View File
@@ -13,6 +13,7 @@ pub mod catalog;
pub mod corners;
pub mod cropimage;
pub mod cut;
pub mod dimension;
pub mod direct;
pub mod distort;
pub mod draw2;
@@ -601,6 +602,7 @@ pub fn create(id: &str) -> Box<dyn Tool> {
"directSelection" => Box::new(direct::DirectSelectionTool::new(false)),
"groupSelection" => Box::new(direct::DirectSelectionTool::new(true)),
"pen" => Box::new(pen::PenTool::default()),
"dimension" => Box::new(dimension::DimensionTool::default()),
"type" | "areaType" | "typeOnPath" | "verticalType" | "verticalAreaType" | "verticalTypeOnPath" => Box::new(text::TypeTool::new(id)),
"rectangle" | "roundedRectangle" | "ellipse" | "polygon" | "star" | "lineSegment" => Box::new(shape::ShapeTool::new(id)),
// Not in the toolbar: `file.place.queue` loads it.
+1
View File
@@ -19,6 +19,7 @@ pub const SYMBOLISM: &[&str] =
&["symbolSprayer", "symbolShifter", "symbolScruncher", "symbolSizer", "symbolSpinner", "symbolStainer", "symbolScreener", "symbolStyler"];
const ROWS: &[Row] = &[
(&["dimension"], None, &["mode", "style"]),
// Global Brush Dimensions.
(LIQUIFY, Some("liquify"), &["width", "height", "angle", "intensity", "usePressure", "showBrush"]),
(LIQUIFY, None, &["detail", "simplify", "simplifyOn", "rate", "complexity", "horizontal", "vertical", "affectAnchors", "affectIn", "affectOut"]),
+6
View File
@@ -610,6 +610,12 @@ const ANIMATED_ZOOM_HINT: &str = "zoom.animated";
fn hint_for(tool: &str) -> Option<&'static [(&'static str, bool)]> {
Some(match tool {
"dimension" => &[
("Drag", true),
(" out from a highlighted edge to place its dimension | ", false),
("Esc", true),
(" cancels | Double-click the tool for options", false),
],
"selection" => &[
("Click", true),
(" the object to select | ", false),
+360
View File
@@ -0,0 +1,360 @@
//! Dimension Tool Options, the tool's control strip, and the Properties panel's Dimension section.
//! Dialog values are drafts until Save, which sets the style of new dimensions.
use super::DialogSpec;
use crate::{VectorcraftApp, state::Dialog, theme::Tokens, widgets};
use egui::{RichText, Ui};
use serde_json::{Value, json};
use vectorcraft_doc::dimension::DimensionOptions;
pub const KIND: &str = "dimensionOptions";
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Dimension Tool Options").into(),
body,
confirm,
ok: Some("Save"),
min_width: 520.0,
max_width: Some(560.0),
..DialogSpec::FORM
};
/// (value, label) of each choice.
type Choices = &'static [(&'static str, &'static str)];
const UNITS: Choices = &[
("in", "Inches"),
("mm", "Millimeters"),
("pt", "Points"),
("pc", "Picas"),
("px", "Pixels"),
("cm", "Centimeters"),
("m", "Meters"),
("ft", "Feet"),
("yd", "Yards"),
];
const LINE_TYPES: Choices = &[("solid", "Solid"), ("dashed", "Dashed"), ("dotted", "Dotted")];
const ARROWS: Choices = &[("filled", "Filled"), ("open", "Open"), ("tick", "Tick"), ("none", "None")];
const POSITIONS: Choices = &[("away", "Away from the Line"), ("between", "Between Line and Object")];
const ALIGNMENTS: Choices = &[("left", "Left"), ("center", "Center"), ("right", "Right")];
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
if app.session.tool_id() == "dimension" && app.session.tool_busy() {
app.run("tool.select", json!({"tool":"selection"}))?;
app.run("tool.select", json!({"tool":"dimension"}))?;
}
let style = app.session.tool_options_of("dimension")["style"].clone();
let o: DimensionOptions = serde_json::from_value(style).unwrap_or_default();
let fields = serde_json::to_value(o).map_err(|e| e.to_string())?.as_object().cloned().unwrap_or_default();
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields });
Ok(json!({"dialog":KIND}))
}
fn read(fields: &serde_json::Map<String, Value>) -> Result<DimensionOptions, String> {
let o: DimensionOptions = serde_json::from_value(Value::Object(fields.clone())).map_err(|e| e.to_string())?;
o.validate()?;
Ok(o)
}
/// A dropdown row: `fields[key]` is one of `choices`' values.
fn choose(ui: &mut Ui, fields: &mut serde_json::Map<String, Value>, key: &str, label: &str, choices: Choices) -> bool {
let mut changed = false;
widgets::label_row(ui, tl!(label), 96.0, |ui| {
let current = fields.get(key).and_then(Value::as_str).unwrap_or_default().to_owned();
let text = choices.iter().find(|(v, _)| *v == current).map_or(current.as_str(), |(_, label)| *label);
if let Some(index) =
widgets::dropdown_names(ui, ("dimension", key), tl!(text), &choices.iter().map(|(_, s)| tl!(*s)).collect::<Vec<_>>(), 150.0)
&& let Some((value, _)) = choices.get(index)
{
fields.insert(key.into(), json!(value));
changed = true;
}
});
changed
}
fn number(ui: &mut Ui, fields: &mut serde_json::Map<String, Value>, key: &str, label: &str, range: (f64, f64), suffix: &str) -> bool {
let mut changed = false;
widgets::label_row(ui, tl!(label), 96.0, |ui| {
let value = fields.get(key).and_then(Value::as_f64).unwrap_or_default();
if let Some(v) = widgets::plain_field(ui, ("dimension", key), value, suffix, 2, 90.0) {
fields.insert(key.into(), json!(v.clamp(range.0, range.1)));
changed = true;
}
});
changed
}
fn check(ui: &mut Ui, fields: &mut serde_json::Map<String, Value>, key: &str, label: &str) -> bool {
let mut value = fields.get(key).and_then(Value::as_bool).unwrap_or_default();
let changed = ui.checkbox(&mut value, tl!(label)).changed();
if changed {
fields.insert(key.into(), json!(value));
}
changed
}
fn color(ui: &mut Ui, fields: &mut serde_json::Map<String, Value>, key: &str) -> bool {
let mut value: [u8; 3] = serde_json::from_value(fields.get(key).cloned().unwrap_or(Value::Null)).unwrap_or([0, 0, 0]);
let changed = ui.push_id(key, |ui| ui.color_edit_button_srgb(&mut value).changed()).inner;
if changed {
fields.insert(key.into(), json!(value));
}
changed
}
fn precision(ui: &mut Ui, fields: &mut serde_json::Map<String, Value>, id: &str) -> bool {
let before = fields.get("precision").and_then(Value::as_u64).unwrap_or(2) as u32;
let mut value = before;
ui.label(tl!("Precision:"));
egui::ComboBox::from_id_salt(id).selected_text(value.to_string()).width(55.0).show_ui(ui, |ui| {
for n in 0..=6 {
ui.selectable_value(&mut value, n, n.to_string());
}
});
fields.insert("precision".into(), json!(value));
value != before
}
fn section(ui: &mut Ui, title: &str, body: impl FnOnce(&mut Ui)) {
let t = Tokens::get(ui.ctx());
ui.group(|ui| {
ui.set_min_width(486.0);
ui.label(RichText::new(tl!(title)).color(t.text_dim).strong());
ui.add_space(5.0);
body(ui);
});
ui.add_space(7.0);
}
fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let f = &mut d.fields;
section(ui, "Units & Scale", |ui| {
ui.horizontal(|ui| {
choose(ui, f, "units", "Units:", UNITS);
precision(ui, f, "dimension-precision");
});
ui.horizontal(|ui| {
widgets::label_row(ui, tl!("Scale:"), 96.0, |ui| {
let mut scale = f.get("scale").and_then(Value::as_str).unwrap_or_default().to_owned();
if ui.add(egui::TextEdit::singleline(&mut scale).desired_width(72.0)).changed() {
f.insert("scale".into(), json!(scale));
}
egui::ComboBox::from_id_salt("dimension-scales").selected_text(tl!("Presets")).width(76.0).show_ui(ui, |ui| {
for scale in ["1:1", "1:2", "1:5", "1:10", "1:20", "1:50", "1:100", "1:200", "2:1", "5:1", "10:1"] {
if ui.selectable_label(false, scale).clicked() {
f.insert("scale".into(), json!(scale));
}
}
});
});
check(ui, f, "hideUnits", "Hide Units");
});
ui.label(RichText::new(tl!("Drawing : real size — 1:10 labels 1 unit on the page as 10.")).small());
});
section(ui, "Dimension Lines", |ui| {
ui.horizontal(|ui| {
number(ui, f, "lineWeight", "Line Weight:", (0.1, 100.0), "pt");
choose(ui, f, "lineType", "Line Type:", LINE_TYPES);
color(ui, f, "lineColor");
});
ui.horizontal(|ui| {
choose(ui, f, "arrowStyle", "Arrow Style:", ARROWS);
number(ui, f, "arrowScale", "Arrow Scale:", (1.0, 1000.0), "%");
});
});
section(ui, "Extension Lines", |ui| {
ui.horizontal(|ui| {
number(ui, f, "extensionWeight", "Line Weight:", (0.1, 100.0), "pt");
choose(ui, f, "extensionType", "Line Type:", LINE_TYPES);
color(ui, f, "extensionColor");
});
ui.horizontal(|ui| {
number(ui, f, "extensionOffset", "Offset:", (0.0, 10000.0), "pt");
number(ui, f, "extensionLength", "Extension:", (0.0, 10000.0), "pt");
});
check(ui, f, "hideExtensions", "Hide Extension Lines");
});
section(ui, "Dimension Text", |ui| {
widgets::label_row(ui, tl!("Font Family:"), 96.0, |ui| {
let family = f.get("fontFamily").and_then(Value::as_str).unwrap_or_default().to_owned();
if let Some(crate::font_menu::FontPick::Chosen(family, style)) =
crate::font_menu::font_menu(ui, "dimension-font", &family, 280.0, Some("123.45"), crate::font_menu::MenuLook::of(app))
{
f.insert("fontFamily".into(), json!(family));
f.insert("fontStyle".into(), json!(style.unwrap_or_else(|| "Regular".into())));
}
});
widgets::label_row(ui, tl!("Font Style:"), 96.0, |ui| {
let family = f.get("fontFamily").and_then(Value::as_str).unwrap_or_default().to_owned();
let current = f.get("fontStyle").and_then(Value::as_str).unwrap_or_default().to_owned();
let styles = vectorcraft_text::FontDb::global().styles(&family);
let names: Vec<&str> = styles.iter().map(|s| s.as_str()).collect();
if let Some(i) = widgets::dropdown_names(ui, "dimension-font-style", &current, &names, 180.0)
&& let Some(style) = styles.get(i)
{
f.insert("fontStyle".into(), json!(style));
}
});
ui.horizontal(|ui| {
number(ui, f, "fontSize", "Font Size:", (1.0, 1000.0), "pt");
color(ui, f, "textColor");
});
ui.horizontal(|ui| {
choose(ui, f, "textPosition", "Position:", POSITIONS);
number(ui, f, "textOffset", "Offset:", (0.0, 10000.0), "pt");
});
choose(ui, f, "textAlignment", "Alignment:", ALIGNMENTS);
});
match read(f) {
Ok(o) => {
let sample = o.label(72.0, false).unwrap_or_default();
let t = Tokens::get(ui.ctx());
ui.label(RichText::new(format!("{}: {sample}", tl!("Preview (1 inch on the page)"))).color(t.text));
}
Err(e) => {
ui.colored_label(Tokens::get(ui.ctx()).text, e);
}
}
if ui.button(tl!("Reset")).clicked()
&& let Ok(Value::Object(fields)) = serde_json::to_value(DimensionOptions::default())
{
d.fields = fields;
}
false
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let o = read(&d.fields)?;
let r = app.run("tool.setOption", json!({"tool":"dimension","values":{"style":o}}))?;
app.ui.dialog = None;
Ok(r)
}
/// The Dimension tool's strip: the kind of dimension, and Tool Options.
pub fn controls(app: &mut VectorcraftApp, ui: &mut Ui) {
let mode = app.session.tool_options()["mode"].as_str().unwrap_or("linear").to_owned();
for (id, label) in [("linear", "Linear"), ("angular", "Angular"), ("radial", "Radial")] {
if ui.selectable_label(mode == id, tl!(label)).clicked() {
// Switching away cancels previews and unfinished picks.
if let Err(e) = app
.run("tool.select", json!({"tool":"selection"}))
.and_then(|_| app.run("tool.select", json!({"tool":"dimension"})))
.and_then(|_| app.run("tool.setOption", json!({"tool":"dimension","values":{"mode":id}})))
{
app.ui.status = e;
}
}
}
if ui.button(tl!("Tool Options…")).clicked()
&& let Err(e) = open(app)
{
app.ui.status = e;
}
}
/// Always available while measuring, including workspaces with the main Control bar hidden.
pub fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
if app.session.tool_id() != "dimension" || app.ui.dialog.is_some() {
return;
}
egui::Window::new(tl!("Dimension Tool"))
.id(egui::Id::new("dimension-strip"))
.default_pos(egui::pos2(90.0, 120.0))
.resizable(false)
.collapsible(false)
.show(ctx, |ui| {
ui.horizontal(|ui| controls(app, ui));
let mode = app.session.tool_options()["mode"].as_str().unwrap_or("linear").to_owned();
let hint = match mode.as_str() {
"angular" => "Drag out from a corner, or click two straight lines, then click to place.",
"radial" | "radius" | "diameter" => "Drag out from a circle or arc for its radius, inwards for its diameter.",
_ => "Drag out from a segment, or click two points, then click to place.",
};
ui.label(RichText::new(tl!(hint)).small().color(Tokens::get(ctx).text_dim));
});
}
/// The selected dimension, if one is selected alone: its id and style.
pub fn selected(app: &VectorcraftApp) -> Option<(u64, DimensionOptions)> {
let st = app.session.active()?;
let [id] = st.selection.objects.as_slice() else { return None };
let d = st.doc.node(*id)?.extras().dimension.as_deref()?;
Some((id.0, d.options.clone()))
}
/// The Properties panel's Dimension section: the selected dimension's style, Apply to All and Set
/// as Default. Its scale is set in the Tool Options.
pub fn properties_section(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some((id, o)) = selected(app) else { return };
let Ok(Value::Object(mut f)) = serde_json::to_value(&o) else { return };
widgets::section_header(ui, tl!("Dimension"));
let mut changed = false;
ui.horizontal(|ui| {
changed |= choose(ui, &mut f, "units", "Units:", UNITS);
});
ui.horizontal(|ui| changed |= precision(ui, &mut f, "props-dimension-precision"));
ui.horizontal(|ui| {
changed |= number(ui, &mut f, "lineWeight", "Line Weight:", (0.1, 100.0), "pt");
changed |= color(ui, &mut f, "lineColor");
});
ui.horizontal(|ui| changed |= choose(ui, &mut f, "arrowStyle", "Arrow Style:", ARROWS));
ui.horizontal(|ui| {
changed |= number(ui, &mut f, "fontSize", "Font Size:", (1.0, 1000.0), "pt");
changed |= color(ui, &mut f, "textColor");
});
ui.horizontal(|ui| changed |= choose(ui, &mut f, "textPosition", "Position:", POSITIONS));
ui.horizontal(|ui| changed |= choose(ui, &mut f, "textAlignment", "Alignment:", ALIGNMENTS));
if changed {
match read(&f) {
Ok(o) => {
if let Err(e) = app.run("dimension.update", json!({"ids":[id],"options":o})) {
app.ui.status = e;
}
}
Err(e) => app.ui.status = e,
}
}
let w = (ui.available_width() - 6.0) / 2.0;
ui.horizontal(|ui| {
if widgets::flat_button(ui, tl!("Apply to All"), w).clicked()
&& let Err(e) = app.run("dimension.applyToAll", json!({"id":id}))
{
app.ui.status = e;
}
if widgets::flat_button(ui, tl!("Set as Default"), w).clicked()
&& let Err(e) = app.run("tool.setOption", json!({"tool":"dimension","values":{"style":o}}))
{
app.ui.status = e;
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dimension_dialog_fields_save_and_cancel() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("tool.options", json!({"tool":"dimension"})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
for label in ["Dimension Tool Options", "Units & Scale", "Dimension Lines", "Extension Lines", "Dimension Text", "Save"] {
assert!(text.contains(label), "{label}: {text}");
}
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("scale".into(), json!("1:10"));
d.fields.insert("units".into(), json!("pc"));
super::super::confirm(&mut app).unwrap();
assert_eq!(app.session.tool_options_of("dimension")["style"]["scale"], "1:10");
assert_eq!(app.session.tool_options_of("dimension")["style"]["units"], "pc");
open(&mut app).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("scale".into(), json!("1:0"));
assert!(super::super::confirm(&mut app).is_err());
app.ui.dialog = None;
assert_eq!(app.session.tool_options_of("dimension")["style"]["scale"], "1:10");
}
/// A selected dimension shows its style in Properties, with Apply to All and Set as Default.
#[test]
fn properties_show_the_selected_dimension() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("dimension.create", json!({"a":[10,10],"b":[110,10]})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, properties_section);
for label in ["Dimension", "Units:", "Apply to All", "Set as Default"] {
assert!(text.contains(label), "{label}: {text}");
}
}
}
+2
View File
@@ -17,6 +17,7 @@ mod color_picker;
mod command;
pub mod confirm;
pub mod corners;
pub mod dimension;
mod document_setup;
pub mod dxf_import;
pub mod dxf_options;
@@ -212,6 +213,7 @@ macro_rules! registry {
}
registry! {
DimensionOptions: [dimension::KIND] => dimension::SPEC,
NewDocument: [new_document::KIND] => new_document::SPEC,
Shape: ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment", "rectangularGrid", "polarGrid"] => shapes::SPEC,
Transform: ["move", "rotate", "scale", "reflect", "shear"] => transform::SPEC,
+45
View File
@@ -4808,6 +4808,51 @@ mark-style Japanese Japonské
Softness Měkkost
Ambient Light Okolní světlo
Light behind the object Světlo za objektem
# Dimension tool (linked dimension annotations)
Display Zobrazení
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Rozměr
Drawing : real size — 1:10 labels 1 unit on the page as 10. Výkres : skutečná velikost — 1:10 označí 1 jednotku na stránce jako 10.
Preview (1 inch on the page) Náhled (1 palec na stránce)
Set as Default Nastavit jako výchozí
out from a highlighted edge to place its dimension | ze zvýrazněné hrany ven a umístíte její kótu |
cancels | Double-click the tool for options zruší | Dvojitým kliknutím na nástroj otevřete volby
Angular Úhlová
Away from the Line Dál od čáry
Between Line and Object Mezi čárou a objektem
Drag out from a segment, or click two points, then click to place. Táhněte ven od úsečky, nebo klikněte na dva body a potom klikněním umístěte.
Drag out from a corner, or click two straight lines, then click to place. Táhněte ven od rohu, nebo klikněte na dvě rovné čáry a potom klikněním umístěte.
Drag out from a circle or arc for its radius, inwards for its diameter. Táhněte od kruhu nebo oblouku ven pro poloměr, dovnitř pro průměr.
@msg Choose a circle or a perfect arc Vyberte kruh nebo dokonalý oblouk
@msg Choose a precision from 0 to 6 Zvolte přesnost od 0 do 6
Create Dimension Vytvořit kótu
Dimension Tool Nástroj Kóta
Dimension Tool Options Volby nástroje Kóta
Font Family: Rodina písma:
Font Style: Řez písma:
Precision: Přesnost:
Update Dimensions Aktualizovat kóty
@msg Angles require two straight segments Úhly vyžadují dva přímé segmenty
@msg Choose two different points Vyberte dva různé body
@msg Choose two non-parallel straight segments Vyberte dva nerovnoběžné přímé segmenty
@msg Choose two nonzero angle arms Vyberte dvě ramena úhlu nenulové délky
@msg Dimension point is outside the canvas Bod kóty leží mimo plátno
@msg Dimension size is outside its supported range Velikost kóty je mimo podporovaný rozsah
@msg First measured segment no longer exists První měřený segment už neexistuje
@msg Invalid dimension distance Neplatná vzdálenost kóty
@msg Invalid dimension font name Neplatný název písma kóty
@msg Invalid drawing scale Neplatné měřítko výkresu
@msg Invalid real scale Neplatné skutečné měřítko
@msg Measured anchor no longer exists Měřený kotevní bod už neexistuje
@msg Measured segment no longer exists Měřený segment už neexistuje
@msg Non-finite dimension Kóta není konečné číslo
@msg Scale must be drawing:real, for example 1:10 Měřítko musí mít tvar výkres:skutečnost, například 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Poměr měřítka musí být mezi 1:1000000 a 1000000:1
@msg Second measured segment no longer exists Druhý měřený segment už neexistuje
@msg Unknown dimension mode Neznámý režim kóty
@msg Unknown dimension option {k} Neznámá volba kóty {k}
@msg Unknown dimension style Neznámý styl kóty
@msg Unknown unit Neznámá jednotka
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Uzavře kruhové konce otevřených profilů. Boční řezy částečného otočení zůstávají otevřené.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -4336,5 +4336,52 @@ theme Appearance Erscheinungsbild
Ambient Light Umgebungslicht
Light behind the object Licht hinter dem Objekt
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Bemaßung
Drawing : real size — 1:10 labels 1 unit on the page as 10. Zeichnung : reale Größe — 1:10 beschriftet 1 Einheit auf der Seite als 10.
Preview (1 inch on the page) Vorschau (1 Zoll auf der Seite)
Set as Default Als Standard festlegen
out from a highlighted edge to place its dimension | von einer hervorgehobenen Kante nach außen, um ihre Bemaßung zu platzieren |
cancels | Double-click the tool for options bricht ab | Doppelklick auf das Werkzeug für Optionen
Linear Linear
Angular Winkel
Away from the Line Abseits der Linie
Between Line and Object Zwischen Linie und Objekt
Drag out from a segment, or click two points, then click to place. Von einem Segment nach außen ziehen oder zwei Punkte anklicken, dann zum Platzieren klicken.
Drag out from a corner, or click two straight lines, then click to place. Von einer Ecke nach außen ziehen oder zwei gerade Linien anklicken, dann zum Platzieren klicken.
Drag out from a circle or arc for its radius, inwards for its diameter. Von einem Kreis oder Bogen nach außen ziehen für den Radius, nach innen für den Durchmesser.
@msg Choose a circle or a perfect arc Wählen Sie einen Kreis oder einen perfekten Bogen
@msg Choose a precision from 0 to 6 Wählen Sie eine Genauigkeit von 0 bis 6
Create Dimension Bemaßung erstellen
Dimension Tool Bemaßungswerkzeug
Dimension Tool Options Optionen für Bemaßungswerkzeug
Display Anzeige
Font Family: Schriftfamilie:
Font Style: Schriftschnitt:
Precision: Genauigkeit:
Update Dimensions Bemaßungen aktualisieren
X: X:
Y: Y:
@msg Angles require two straight segments Winkel erfordern zwei gerade Segmente
@msg Choose two different points Wählen Sie zwei verschiedene Punkte
@msg Choose two non-parallel straight segments Wählen Sie zwei nicht parallele gerade Segmente
@msg Choose two nonzero angle arms Wählen Sie zwei Winkelschenkel mit einer Länge ungleich null
@msg Dimension point is outside the canvas Der Bemaßungspunkt liegt außerhalb der Arbeitsfläche
@msg Dimension size is outside its supported range Die Bemaßungsgröße liegt außerhalb des unterstützten Bereichs
@msg First measured segment no longer exists Das erste gemessene Segment existiert nicht mehr
@msg Invalid dimension distance Ungültiger Bemaßungsabstand
@msg Invalid dimension font name Ungültiger Schriftname für die Bemaßung
@msg Invalid drawing scale Ungültiger Zeichnungsmaßstab
@msg Invalid real scale Ungültiger realer Maßstab
@msg Measured anchor no longer exists Der gemessene Ankerpunkt existiert nicht mehr
@msg Measured segment no longer exists Das gemessene Segment existiert nicht mehr
@msg Non-finite dimension Die Bemaßung ist keine endliche Zahl
@msg Scale must be drawing:real, for example 1:10 Der Maßstab muss Zeichnung:Realität sein, zum Beispiel 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Das Maßstabsverhältnis muss zwischen 1:1000000 und 1000000:1 liegen
@msg Second measured segment no longer exists Das zweite gemessene Segment existiert nicht mehr
@msg Unknown dimension mode Unbekannter Bemaßungsmodus
@msg Unknown dimension option {k} Unbekannte Bemaßungsoption {k}
@msg Unknown dimension style Unbekannter Bemaßungsstil
@msg Unknown unit Unbekannte Einheit
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Schließt die kreisförmigen Enden offener Profile. Bei einer Teildrehung bleiben die Schnittseiten offen.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+45
View File
@@ -4749,6 +4749,51 @@ mark-style Japanese Japonesas
Softness Suavidad
Ambient Light Luz ambiental
Light behind the object Luz detrás del objeto
# Dimension tool (linked dimension annotations)
Create Dimension Crear cota
Dimension Tool Herramienta Cota
Dimension Tool Options Opciones de la herramienta Cota
Font Family: Familia de fuentes:
Font Style: Estilo de fuente:
Precision: Precisión:
Update Dimensions Actualizar cotas
Display Visualización
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Cota
Drawing : real size — 1:10 labels 1 unit on the page as 10. Dibujo : tamaño real — 1:10 rotula 1 unidad de la página como 10.
Preview (1 inch on the page) Vista previa (1 pulgada en la página)
Set as Default Establecer como predeterminado
out from a highlighted edge to place its dimension | hacia fuera desde un borde resaltado para colocar su cota |
cancels | Double-click the tool for options cancela | Doble clic en la herramienta para ver las opciones
Angular Angular
Away from the Line Lejos de la línea
Between Line and Object Entre la línea y el objeto
Drag out from a segment, or click two points, then click to place. Arrastre hacia fuera desde un segmento, o haga clic en dos puntos y luego haga clic para colocar.
Drag out from a corner, or click two straight lines, then click to place. Arrastre hacia fuera desde una esquina, o haga clic en dos líneas rectas y luego haga clic para colocar.
Drag out from a circle or arc for its radius, inwards for its diameter. Arrastre desde un círculo o arco hacia fuera para el radio, hacia dentro para el diámetro.
@msg Choose a circle or a perfect arc Elija un círculo o un arco perfecto
@msg Choose a precision from 0 to 6 Elija una precisión de 0 a 6
@msg Angles require two straight segments Los ángulos requieren dos segmentos rectos
@msg Choose two different points Elija dos puntos distintos
@msg Choose two non-parallel straight segments Elija dos segmentos rectos no paralelos
@msg Choose two nonzero angle arms Elija dos lados del ángulo de longitud distinta de cero
@msg Dimension point is outside the canvas El punto de la cota está fuera del lienzo
@msg Dimension size is outside its supported range El tamaño de la cota está fuera del intervalo admitido
@msg First measured segment no longer exists El primer segmento medido ya no existe
@msg Invalid dimension distance Distancia de cota no válida
@msg Invalid dimension font name Nombre de fuente de cota no válido
@msg Invalid drawing scale Escala de dibujo no válida
@msg Invalid real scale Escala real no válida
@msg Measured anchor no longer exists El punto de ancla medido ya no existe
@msg Measured segment no longer exists El segmento medido ya no existe
@msg Non-finite dimension Cota no finita
@msg Scale must be drawing:real, for example 1:10 La escala debe ser dibujo:real, por ejemplo 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 La relación de escala debe estar entre 1:1000000 y 1000000:1
@msg Second measured segment no longer exists El segundo segmento medido ya no existe
@msg Unknown dimension mode Modo de cota desconocido
@msg Unknown dimension option {k} Opción de cota desconocida {k}
@msg Unknown dimension style Estilo de cota desconocido
@msg Unknown unit Unidad desconocida
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Cierra los extremos circulares de los perfiles abiertos. Los giros parciales mantienen abiertos sus lados de corte.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+44
View File
@@ -4809,5 +4809,49 @@ theme Appearance Apparence
Ambient Light Lumière ambiante
Light behind the object Lumière derrière l'objet
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Cote
Drawing : real size — 1:10 labels 1 unit on the page as 10. Dessin : taille réelle — 1:10 étiquette 1 unité de la page comme 10.
Preview (1 inch on the page) Aperçu (1 pouce sur la page)
Set as Default Définir par défaut
out from a highlighted edge to place its dimension | vers l'extérieur depuis un bord mis en évidence pour placer sa cote |
cancels | Double-click the tool for options annule | Double-clic sur l'outil pour les options
Angular Angulaire
Away from the Line À l'écart de la ligne
Between Line and Object Entre la ligne et l'objet
Drag out from a segment, or click two points, then click to place. Faites glisser vers l'extérieur depuis un segment, ou cliquez sur deux points, puis cliquez pour placer.
Drag out from a corner, or click two straight lines, then click to place. Faites glisser vers l'extérieur depuis un angle, ou cliquez sur deux lignes droites, puis cliquez pour placer.
Drag out from a circle or arc for its radius, inwards for its diameter. Faites glisser depuis un cercle ou un arc vers l'extérieur pour le rayon, vers l'intérieur pour le diamètre.
@msg Choose a circle or a perfect arc Choisissez un cercle ou un arc parfait
@msg Choose a precision from 0 to 6 Choisissez une précision de 0 à 6
Create Dimension Créer une cote
Dimension Tool Outil Cotation
Dimension Tool Options Options de l’outil Cotation
Display Affichage
Font Family: Famille de polices :
Font Style: Style de police :
Precision: Précision :
Update Dimensions Mettre à jour les cotes
@msg Angles require two straight segments Les angles nécessitent deux segments droits
@msg Choose two different points Choisissez deux points différents
@msg Choose two non-parallel straight segments Choisissez deux segments droits non parallèles
@msg Choose two nonzero angle arms Choisissez deux côtés d’angle de longueur non nulle
@msg Dimension point is outside the canvas Le point de cote est en dehors de la zone de travail
@msg Dimension size is outside its supported range La taille de la cote est hors de la plage prise en charge
@msg First measured segment no longer exists Le premier segment mesuré n’existe plus
@msg Invalid dimension distance Distance de cote non valide
@msg Invalid dimension font name Nom de police de cote non valide
@msg Invalid drawing scale Échelle du dessin non valide
@msg Invalid real scale Échelle réelle non valide
@msg Measured anchor no longer exists Le point d’ancrage mesuré n’existe plus
@msg Measured segment no longer exists Le segment mesuré n’existe plus
@msg Non-finite dimension Cote non finie
@msg Scale must be drawing:real, for example 1:10 L’échelle doit être dessin:réel, par exemple 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Le rapport d’échelle doit être compris entre 1:1000000 et 1000000:1
@msg Second measured segment no longer exists Le second segment mesuré n’existe plus
@msg Unknown dimension mode Mode de cote inconnu
@msg Unknown dimension option {k} Option de cote inconnue {k}
@msg Unknown dimension style Style de cote inconnu
@msg Unknown unit Unité inconnue
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Ferme les extrémités circulaires des profils ouverts. Les rotations partielles conservent leurs faces de coupe ouvertes.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+44
View File
@@ -4776,5 +4776,49 @@ imageTrace Outline Körvonal
Ambient Light Környezeti fény
Light behind the object Fény az objektum mögött
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Méret
Drawing : real size — 1:10 labels 1 unit on the page as 10. Rajz : valós méret — 1:10 esetén az oldalon 1 egység 10-ként jelenik meg.
Preview (1 inch on the page) Előnézet (1 hüvelyk az oldalon)
Set as Default Beállítás alapértelmezettként
out from a highlighted edge to place its dimension | egy kiemelt élről kifelé a méret elhelyezéséhez |
cancels | Double-click the tool for options megszakítja | Dupla kattintás az eszközre a beállításokhoz
Angular Szög
Away from the Line A vonaltól távol
Between Line and Object A vonal és az objektum között
Drag out from a segment, or click two points, then click to place. Húzzon kifelé egy szakaszról, vagy kattintson két pontra, majd kattintson az elhelyezéshez.
Drag out from a corner, or click two straight lines, then click to place. Húzzon kifelé egy sarokról, vagy kattintson két egyenes vonalra, majd kattintson az elhelyezéshez.
Drag out from a circle or arc for its radius, inwards for its diameter. Húzzon egy körről vagy ívről kifelé a sugárhoz, befelé az átmérőhöz.
@msg Choose a circle or a perfect arc Válasszon egy kört vagy egy tökéletes ívet
@msg Choose a precision from 0 to 6 Válasszon 0 és 6 közötti pontosságot
Create Dimension Méretvonal létrehozása
Dimension Tool Méretezés eszköz
Dimension Tool Options Méretezés eszköz beállításai
Display Megjelenítés
Font Family: Betűcsalád:
Font Style: Betűstílus:
Precision: Pontosság:
Update Dimensions Méretek frissítése
@msg Angles require two straight segments A szöghöz két egyenes szakasz kell
@msg Choose two different points Válassz két különböző pontot
@msg Choose two non-parallel straight segments Válassz két nem párhuzamos egyenes szakaszt
@msg Choose two nonzero angle arms Válassz két nem nulla hosszúságú szögszárat
@msg Dimension point is outside the canvas A méretvonal pontja a vásznon kívül esik
@msg Dimension size is outside its supported range A méret a támogatott tartományon kívül esik
@msg First measured segment no longer exists Az első mért szakasz már nem létezik
@msg Invalid dimension distance Érvénytelen méretezési távolság
@msg Invalid dimension font name Érvénytelen méretezési betűtípusnév
@msg Invalid drawing scale Érvénytelen rajzlépték
@msg Invalid real scale Érvénytelen valós lépték
@msg Measured anchor no longer exists A mért horgonypont már nem létezik
@msg Measured segment no longer exists A mért szakasz már nem létezik
@msg Non-finite dimension Nem véges méret
@msg Scale must be drawing:real, for example 1:10 A léptéknek rajz:valós alakúnak kell lennie, például 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 A lépték aránya 1:1000000 és 1000000:1 között lehet
@msg Second measured segment no longer exists A második mért szakasz már nem létezik
@msg Unknown dimension mode Ismeretlen méretezési mód
@msg Unknown dimension option {k} Ismeretlen méretezési beállítás: {k}
@msg Unknown dimension style Ismeretlen méretezési stílus
@msg Unknown unit Ismeretlen mértékegység
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Nyitott profilok kör alakú végeinek lezárása. Részleges körforgatás esetén a vágott oldalak nyitva maradnak.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -4381,5 +4381,52 @@ lighting Light Luce
Ambient Light Luce ambientale
Light behind the object Luce dietro l'oggetto
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Quota
Drawing : real size — 1:10 labels 1 unit on the page as 10. Disegno : dimensione reale — 1:10 indica 1 unità sulla pagina come 10.
Preview (1 inch on the page) Anteprima (1 pollice sulla pagina)
Set as Default Imposta come predefinito
out from a highlighted edge to place its dimension | verso l'esterno da un bordo evidenziato per posizionarne la quota |
cancels | Double-click the tool for options annulla | Doppio clic sullo strumento per le opzioni
Linear Lineare
Angular Angolare
Away from the Line Lontano dalla linea
Between Line and Object Tra linea e oggetto
Drag out from a segment, or click two points, then click to place. Trascina verso l'esterno da un segmento, oppure fai clic su due punti, poi fai clic per posizionare.
Drag out from a corner, or click two straight lines, then click to place. Trascina verso l'esterno da un angolo, oppure fai clic su due linee rette, poi fai clic per posizionare.
Drag out from a circle or arc for its radius, inwards for its diameter. Trascina da un cerchio o un arco verso l'esterno per il raggio, verso l'interno per il diametro.
@msg Choose a circle or a perfect arc Scegli un cerchio o un arco perfetto
@msg Choose a precision from 0 to 6 Scegli una precisione da 0 a 6
Create Dimension Crea quota
Dimension Tool Strumento Quota
Dimension Tool Options Opzioni strumento Quota
Display Visualizzazione
Font Family: Famiglia di font:
Font Style: Stile font:
Precision: Precisione:
Update Dimensions Aggiorna quote
X: X:
Y: Y:
@msg Angles require two straight segments Gli angoli richiedono due segmenti rettilinei
@msg Choose two different points Scegli due punti diversi
@msg Choose two non-parallel straight segments Scegli due segmenti rettilinei non paralleli
@msg Choose two nonzero angle arms Scegli due lati dell’angolo di lunghezza non nulla
@msg Dimension point is outside the canvas Il punto della quota è fuori dalla tela
@msg Dimension size is outside its supported range La dimensione della quota è fuori dall’intervallo supportato
@msg First measured segment no longer exists Il primo segmento misurato non esiste più
@msg Invalid dimension distance Distanza della quota non valida
@msg Invalid dimension font name Nome del font della quota non valido
@msg Invalid drawing scale Scala del disegno non valida
@msg Invalid real scale Scala reale non valida
@msg Measured anchor no longer exists Il punto di ancoraggio misurato non esiste più
@msg Measured segment no longer exists Il segmento misurato non esiste più
@msg Non-finite dimension Quota non finita
@msg Scale must be drawing:real, for example 1:10 La scala deve essere disegno:reale, ad esempio 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Il rapporto di scala deve essere compreso tra 1:1000000 e 1000000:1
@msg Second measured segment no longer exists Il secondo segmento misurato non esiste più
@msg Unknown dimension mode Modalità di quota sconosciuta
@msg Unknown dimension option {k} Opzione di quota sconosciuta {k}
@msg Unknown dimension style Stile di quota sconosciuto
@msg Unknown unit Unità sconosciuta
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Chiude le estremità circolari dei profili aperti. Le rotazioni parziali mantengono aperti i lati di taglio.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+48
View File
@@ -4374,6 +4374,54 @@ lighting Light ライト
Softness 柔らかさ
Ambient Light 環境光
Light behind the object オブジェクトの背後のライト
# Dimension tool (linked dimension annotations)
Create Dimension 寸法を作成
Dimension Tool 寸法ツール
Dimension Tool Options 寸法ツールオプション
Font Family: フォントファミリー:
Font Style: フォントスタイル:
Precision: 精度:
Update Dimensions 寸法を更新
Display 表示
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 寸法
Drawing : real size — 1:10 labels 1 unit on the page as 10. 図面 : 実寸 — 1:10 では、ページ上の 1 単位を 10 と表示します。
Preview (1 inch on the page) プレビュー(ページ上の 1 インチ)
Set as Default デフォルトに設定
out from a highlighted edge to place its dimension | でハイライトされた辺から外側へ寸法を配置 |
cancels | Double-click the tool for options でキャンセル | ツールをダブルクリックしてオプション
Linear 直線
Angular 角度
Away from the Line 寸法線から離す
Between Line and Object 寸法線とオブジェクトの間
Drag out from a segment, or click two points, then click to place. セグメントから外側へドラッグするか、2 点をクリックしてからクリックして配置します。
Drag out from a corner, or click two straight lines, then click to place. 角から外側へドラッグするか、2 本の直線をクリックしてからクリックして配置します。
Drag out from a circle or arc for its radius, inwards for its diameter. 円または円弧から外側へドラッグすると半径、内側へドラッグすると直径になります。
@msg Choose a circle or a perfect arc 円または正確な円弧を選択してください
@msg Choose a precision from 0 to 6 精度は 0〜6 で選択してください
X: X:
Y: Y:
@msg Angles require two straight segments 角度には直線セグメントが 2 つ必要です
@msg Choose two different points 異なる 2 点を選択してください
@msg Choose two non-parallel straight segments 平行でない直線セグメントを 2 つ選択してください
@msg Choose two nonzero angle arms 長さが 0 でない角度の辺を 2 つ選択してください
@msg Dimension point is outside the canvas 寸法の点がカンバスの外にあります
@msg Dimension size is outside its supported range 寸法のサイズがサポート範囲外です
@msg First measured segment no longer exists 最初の測定セグメントはもう存在しません
@msg Invalid dimension distance 寸法の距離が無効です
@msg Invalid dimension font name 寸法のフォント名が無効です
@msg Invalid drawing scale 図面の縮尺が無効です
@msg Invalid real scale 実寸の縮尺が無効です
@msg Measured anchor no longer exists 測定したアンカーポイントはもう存在しません
@msg Measured segment no longer exists 測定したセグメントはもう存在しません
@msg Non-finite dimension 寸法が有限ではありません
@msg Scale must be drawing:real, for example 1:10 縮尺は 図面:実寸 の形式にしてください (例: 1:10)
@msg Scale ratio must be between 1:1000000 and 1000000:1 縮尺の比率は 1:1000000 から 1000000:1 の間にしてください
@msg Second measured segment no longer exists 2 番目の測定セグメントはもう存在しません
@msg Unknown dimension mode 寸法のモードが不明です
@msg Unknown dimension option {k} 不明な寸法オプション {k}
@msg Unknown dimension style 寸法のスタイルが不明です
@msg Unknown unit 単位が不明です
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 開いたプロファイルの円形の端を閉じます。部分的な回転では切断面は開いたままです。
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -4345,5 +4345,52 @@ lighting Light 광원
Ambient Light 주변광
Light behind the object 개체 뒤의 조명
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 치수
Drawing : real size — 1:10 labels 1 unit on the page as 10. 도면 : 실제 크기 — 1:10은 페이지의 1단위를 10으로 표시합니다.
Preview (1 inch on the page) 미리 보기 (페이지의 1인치)
Set as Default 기본값으로 설정
out from a highlighted edge to place its dimension | 강조된 가장자리에서 바깥쪽으로 끌어 치수 배치 |
cancels | Double-click the tool for options 취소 | 도구를 두 번 클릭하면 옵션
Linear 선형
Angular 각도
Away from the Line 선에서 떨어짐
Between Line and Object 선과 개체 사이
Drag out from a segment, or click two points, then click to place. 세그먼트에서 바깥쪽으로 끌거나, 두 점을 클릭한 다음 클릭하여 배치합니다.
Drag out from a corner, or click two straight lines, then click to place. 모서리에서 바깥쪽으로 끌거나, 두 직선을 클릭한 다음 클릭하여 배치합니다.
Drag out from a circle or arc for its radius, inwards for its diameter. 원이나 호에서 바깥쪽으로 끌면 반지름, 안쪽으로 끌면 지름입니다.
@msg Choose a circle or a perfect arc 원 또는 완전한 호를 선택하십시오
@msg Choose a precision from 0 to 6 정밀도를 0에서 6 사이로 선택하십시오
Create Dimension 치수 만들기
Dimension Tool 치수 도구
Dimension Tool Options 치수 도구 옵션
Display 표시
Font Family: 글꼴 패밀리:
Font Style: 글꼴 스타일:
Precision: 정밀도:
Update Dimensions 치수 업데이트
X: X:
Y: Y:
@msg Angles require two straight segments 각도에는 직선 세그먼트 두 개가 필요합니다
@msg Choose two different points 서로 다른 두 점을 선택하십시오
@msg Choose two non-parallel straight segments 평행하지 않은 직선 세그먼트 두 개를 선택하십시오
@msg Choose two nonzero angle arms 길이가 0이 아닌 각도 변 두 개를 선택하십시오
@msg Dimension point is outside the canvas 치수 점이 캔버스 밖에 있습니다
@msg Dimension size is outside its supported range 치수 크기가 지원 범위를 벗어났습니다
@msg First measured segment no longer exists 첫 번째 측정 세그먼트가 더 이상 없습니다
@msg Invalid dimension distance 잘못된 치수 거리
@msg Invalid dimension font name 잘못된 치수 글꼴 이름
@msg Invalid drawing scale 잘못된 도면 축척
@msg Invalid real scale 잘못된 실제 축척
@msg Measured anchor no longer exists 측정한 고정점이 더 이상 없습니다
@msg Measured segment no longer exists 측정한 세그먼트가 더 이상 없습니다
@msg Non-finite dimension 유한하지 않은 치수
@msg Scale must be drawing:real, for example 1:10 축척은 도면:실제 형식이어야 합니다(예: 1:10)
@msg Scale ratio must be between 1:1000000 and 1000000:1 축척 비율은 1:1000000에서 1000000:1 사이여야 합니다
@msg Second measured segment no longer exists 두 번째 측정 세그먼트가 더 이상 없습니다
@msg Unknown dimension mode 알 수 없는 치수 모드
@msg Unknown dimension option {k} 알 수 없는 치수 옵션 {k}
@msg Unknown dimension style 알 수 없는 치수 스타일
@msg Unknown unit 알 수 없는 단위
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 열린 프로파일의 원형 끝을 닫습니다. 부분 회전에서는 절단 측면이 열린 상태로 유지됩니다.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+44 -1
View File
@@ -4651,6 +4651,49 @@ mark-style Japanese Japońskie
Softness Miękkość
Ambient Light Światło otoczenia
Light behind the object Światło za obiektem
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Wymiar
Drawing : real size — 1:10 labels 1 unit on the page as 10. Rysunek : rozmiar rzeczywisty — 1:10 opisuje 1 jednostkę na stronie jako 10.
Preview (1 inch on the page) Podgląd (1 cal na stronie)
Set as Default Ustaw jako domyślne
out from a highlighted edge to place its dimension | od podświetlonej krawędzi na zewnątrz, aby umieścić jej wymiar |
cancels | Double-click the tool for options anuluje | Kliknij dwukrotnie narzędzie, aby wyświetlić opcje
Angular Kątowy
Away from the Line Z dala od linii
Between Line and Object Między linią a obiektem
Drag out from a segment, or click two points, then click to place. Przeciągnij na zewnątrz od odcinka albo kliknij dwa punkty, a następnie kliknij, aby umieścić.
Drag out from a corner, or click two straight lines, then click to place. Przeciągnij na zewnątrz od narożnika albo kliknij dwie proste linie, a następnie kliknij, aby umieścić.
Drag out from a circle or arc for its radius, inwards for its diameter. Przeciągnij od okręgu lub łuku na zewnątrz, aby uzyskać promień, do wewnątrz — średnicę.
@msg Choose a circle or a perfect arc Wybierz okrąg lub idealny łuk
@msg Choose a precision from 0 to 6 Wybierz dokładność od 0 do 6
Create Dimension Utwórz wymiar
Dimension Tool Narzędzie Wymiar
Dimension Tool Options Opcje narzędzia Wymiar
Display Wyświetlanie
Font Family: Rodzina czcionek:
Font Style: Styl czcionki:
Precision: Dokładność:
Update Dimensions Aktualizuj wymiary
@msg Angles require two straight segments Kąty wymagają dwóch prostych odcinków
@msg Choose two different points Wybierz dwa różne punkty
@msg Choose two non-parallel straight segments Wybierz dwa nierównoległe proste odcinki
@msg Choose two nonzero angle arms Wybierz dwa niezerowe ramiona kąta
@msg Dimension point is outside the canvas Punkt wymiaru leży poza obszarem roboczym
@msg Dimension size is outside its supported range Rozmiar wymiaru jest poza obsługiwanym zakresem
@msg First measured segment no longer exists Pierwszy mierzony odcinek już nie istnieje
@msg Invalid dimension distance Nieprawidłowa odległość wymiaru
@msg Invalid dimension font name Nieprawidłowa nazwa czcionki wymiaru
@msg Invalid drawing scale Nieprawidłowa skala rysunku
@msg Invalid real scale Nieprawidłowa skala rzeczywista
@msg Measured anchor no longer exists Mierzony punkt kontrolny już nie istnieje
@msg Measured segment no longer exists Mierzony odcinek już nie istnieje
@msg Non-finite dimension Wymiar nie jest liczbą skończoną
@msg Scale must be drawing:real, for example 1:10 Skala musi mieć postać rysunek:rzeczywistość, na przykład 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Współczynnik skali musi mieścić się między 1:1000000 a 1000000:1
@msg Second measured segment no longer exists Drugi mierzony odcinek już nie istnieje
@msg Unknown dimension mode Nieznany tryb wymiaru
@msg Unknown dimension option {k} Nieznana opcja wymiaru {k}
@msg Unknown dimension style Nieznany styl wymiaru
@msg Unknown unit Nieznana jednostka
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Zamknij okrągłe końce otwartych profili. Przy częściowym obrocie boki przekroju pozostają otwarte.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -1
View File
@@ -3763,6 +3763,9 @@ axis Both Ambos
Softness Suavidade
Ambient Light Luz ambiente
Light behind the object Luz atrás do objeto
# Dimension tool (linked dimension annotations)
Dimension Tool Ferramenta Cota
Create Dimension Criar Cota
Update Dimensions Atualizar Cotas
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Fecha as extremidades circulares dos perfis abertos. Giros parciais mantêm as faces de corte abertas.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+47
View File
@@ -4377,5 +4377,52 @@ lighting Light Свет
Ambient Light Окружающий свет
Light behind the object Свет позади объекта
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Размер
Drawing : real size — 1:10 labels 1 unit on the page as 10. Чертёж : реальный размер — при 1:10 1 единица на странице подписывается как 10.
Preview (1 inch on the page) Просмотр (1 дюйм на странице)
Set as Default Установить по умолчанию
out from a highlighted edge to place its dimension | наружу от подсвеченного края, чтобы разместить его размер |
cancels | Double-click the tool for options отменяет | Двойной щелчок по инструменту открывает параметры
Linear Линейный
Angular Угловой
Away from the Line Вдали от линии
Between Line and Object Между линией и объектом
Drag out from a segment, or click two points, then click to place. Перетащите наружу от сегмента или щёлкните две точки, затем щёлкните, чтобы разместить.
Drag out from a corner, or click two straight lines, then click to place. Перетащите наружу от угла или щёлкните две прямые линии, затем щёлкните, чтобы разместить.
Drag out from a circle or arc for its radius, inwards for its diameter. Перетащите от окружности или дуги наружу — радиус, внутрь — диаметр.
@msg Choose a circle or a perfect arc Выберите окружность или правильную дугу
@msg Choose a precision from 0 to 6 Выберите точность от 0 до 6
Create Dimension Создать размер
Dimension Tool Инструмент «Размер»
Dimension Tool Options Параметры инструмента «Размер»
Display Отображение
Font Family: Семейство шрифтов:
Font Style: Начертание:
Precision: Точность:
Update Dimensions Обновить размеры
X: X:
Y: Y:
@msg Angles require two straight segments Для угла нужны два прямых сегмента
@msg Choose two different points Выберите две разные точки
@msg Choose two non-parallel straight segments Выберите два непараллельных прямых сегмента
@msg Choose two nonzero angle arms Выберите две стороны угла ненулевой длины
@msg Dimension point is outside the canvas Точка размера находится за пределами холста
@msg Dimension size is outside its supported range Размер выходит за поддерживаемые пределы
@msg First measured segment no longer exists Первый измеряемый сегмент больше не существует
@msg Invalid dimension distance Недопустимое расстояние размера
@msg Invalid dimension font name Недопустимое имя шрифта размера
@msg Invalid drawing scale Недопустимый масштаб чертежа
@msg Invalid real scale Недопустимый реальный масштаб
@msg Measured anchor no longer exists Измеряемая опорная точка больше не существует
@msg Measured segment no longer exists Измеряемый сегмент больше не существует
@msg Non-finite dimension Размер не является конечным числом
@msg Scale must be drawing:real, for example 1:10 Масштаб должен быть в виде чертёж:реальность, например 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Соотношение масштаба должно быть от 1:1000000 до 1000000:1
@msg Second measured segment no longer exists Второй измеряемый сегмент больше не существует
@msg Unknown dimension mode Неизвестный режим размера
@msg Unknown dimension option {k} Неизвестный параметр размера {k}
@msg Unknown dimension style Неизвестный стиль размера
@msg Unknown unit Неизвестная единица
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Закрывает круглые торцы открытых профилей. При неполном вращении боковые срезы остаются открытыми.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -4361,5 +4361,52 @@ lighting Light Світло
Ambient Light Навколишнє світло
Light behind the object Світло позаду об'єкта
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Розмір
Drawing : real size — 1:10 labels 1 unit on the page as 10. Креслення : реальний розмір — при 1:10 1 одиниця на сторінці підписується як 10.
Preview (1 inch on the page) Попередній перегляд (1 дюйм на сторінці)
Set as Default Встановити за замовчуванням
out from a highlighted edge to place its dimension | назовні від підсвіченого краю, щоб розмістити його розмір |
cancels | Double-click the tool for options скасовує | Подвійне клацання по інструменту відкриває параметри
Linear Лінійний
Angular Кутовий
Away from the Line Далі від лінії
Between Line and Object Між лінією та об'єктом
Drag out from a segment, or click two points, then click to place. Перетягніть назовні від сегмента або клацніть дві точки, потім клацніть, щоб розмістити.
Drag out from a corner, or click two straight lines, then click to place. Перетягніть назовні від кута або клацніть дві прямі лінії, потім клацніть, щоб розмістити.
Drag out from a circle or arc for its radius, inwards for its diameter. Перетягніть від кола або дуги назовні — радіус, усередину — діаметр.
@msg Choose a circle or a perfect arc Виберіть коло або правильну дугу
@msg Choose a precision from 0 to 6 Виберіть точність від 0 до 6
Create Dimension Створити розмір
Dimension Tool Інструмент «Розмір»
Dimension Tool Options Параметри інструмента «Розмір»
Display Відображення
Font Family: Сімейство шрифтів:
Font Style: Накреслення:
Precision: Точність:
Update Dimensions Оновити розміри
X: X:
Y: Y:
@msg Angles require two straight segments Для кута потрібні два прямі сегменти
@msg Choose two different points Виберіть дві різні точки
@msg Choose two non-parallel straight segments Виберіть два непаралельні прямі сегменти
@msg Choose two nonzero angle arms Виберіть дві сторони кута ненульової довжини
@msg Dimension point is outside the canvas Точка розміру знаходиться за межами полотна
@msg Dimension size is outside its supported range Розмір виходить за підтримувані межі
@msg First measured segment no longer exists Перший виміряний сегмент більше не існує
@msg Invalid dimension distance Неприпустима відстань розміру
@msg Invalid dimension font name Неприпустима назва шрифту розміру
@msg Invalid drawing scale Неприпустимий масштаб креслення
@msg Invalid real scale Неприпустимий реальний масштаб
@msg Measured anchor no longer exists Виміряна опорна точка більше не існує
@msg Measured segment no longer exists Виміряний сегмент більше не існує
@msg Non-finite dimension Розмір не є скінченним числом
@msg Scale must be drawing:real, for example 1:10 Масштаб має бути у вигляді креслення:реальність, наприклад 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Співвідношення масштабу має бути від 1:1000000 до 1000000:1
@msg Second measured segment no longer exists Другий виміряний сегмент більше не існує
@msg Unknown dimension mode Невідомий режим розміру
@msg Unknown dimension option {k} Невідомий параметр розміру {k}
@msg Unknown dimension style Невідомий стиль розміру
@msg Unknown unit Невідома одиниця
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Закриває круглі торці відкритих профілів. За неповного обертання бічні зрізи залишаються відкритими.
Can't render this file because it contains an unexpected character in line 713 and column 40.
+45 -1
View File
@@ -4362,6 +4362,50 @@ theme Appearance Giao diện
Softness Độ mềm
Ambient Light Ánh sáng môi trường
Light behind the object Ánh sáng phía sau đối tượng
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension Kích thước
Drawing : real size — 1:10 labels 1 unit on the page as 10. Bản vẽ : kích thước thực — 1:10 ghi 1 đơn vị trên trang thành 10.
Preview (1 inch on the page) Xem trước (1 inch trên trang)
Set as Default Đặt làm mặc định
out from a highlighted edge to place its dimension | ra ngoài từ một cạnh được tô sáng để đặt kích thước của nó |
cancels | Double-click the tool for options để hủy | Nhấp đúp vào công cụ để mở tùy chọn
Linear Tuyến tính
Angular Góc
Away from the Line Cách xa đường
Between Line and Object Giữa đường và đối tượng
Drag out from a segment, or click two points, then click to place. Kéo ra ngoài từ một đoạn, hoặc nhấp hai điểm, rồi nhấp để đặt.
Drag out from a corner, or click two straight lines, then click to place. Kéo ra ngoài từ một góc, hoặc nhấp hai đường thẳng, rồi nhấp để đặt.
Drag out from a circle or arc for its radius, inwards for its diameter. Kéo từ đường tròn hoặc cung ra ngoài để lấy bán kính, vào trong để lấy đường kính.
@msg Choose a circle or a perfect arc Hãy chọn một đường tròn hoặc một cung hoàn hảo
@msg Choose a precision from 0 to 6 Hãy chọn độ chính xác từ 0 đến 6
Create Dimension Tạo kích thước
Dimension Tool Công cụ Kích thước
Dimension Tool Options Tùy chọn công cụ Kích thước
Display Hiển thị
Font Family: Họ phông chữ:
Font Style: Kiểu phông chữ:
Precision: Độ chính xác:
Update Dimensions Cập nhật kích thước
@msg Angles require two straight segments Góc cần hai đoạn thẳng
@msg Choose two different points Hãy chọn hai điểm khác nhau
@msg Choose two non-parallel straight segments Hãy chọn hai đoạn thẳng không song song
@msg Choose two nonzero angle arms Hãy chọn hai cạnh góc có độ dài khác 0
@msg Dimension point is outside the canvas Điểm kích thước nằm ngoài vùng vẽ
@msg Dimension size is outside its supported range Kích thước nằm ngoài phạm vi được hỗ trợ
@msg First measured segment no longer exists Đoạn được đo thứ nhất không còn tồn tại
@msg Invalid dimension distance Khoảng cách kích thước không hợp lệ
@msg Invalid dimension font name Tên phông chữ của kích thước không hợp lệ
@msg Invalid drawing scale Tỷ lệ bản vẽ không hợp lệ
@msg Invalid real scale Tỷ lệ thực không hợp lệ
@msg Measured anchor no longer exists Điểm neo được đo không còn tồn tại
@msg Measured segment no longer exists Đoạn được đo không còn tồn tại
@msg Non-finite dimension Kích thước không phải là số hữu hạn
@msg Scale must be drawing:real, for example 1:10 Tỷ lệ phải có dạng bản vẽ:thực tế, ví dụ 1:10
@msg Scale ratio must be between 1:1000000 and 1000000:1 Tỷ lệ phải nằm trong khoảng 1:1000000 đến 1000000:1
@msg Second measured segment no longer exists Đoạn được đo thứ hai không còn tồn tại
@msg Unknown dimension mode Chế độ kích thước không xác định
@msg Unknown dimension option {k} Tùy chọn kích thước không xác định {k}
@msg Unknown dimension style Kiểu kích thước không xác định
@msg Unknown unit Đơn vị không xác định
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Đóng các đầu tròn của biên dạng mở. Khi xoay một phần, các mặt cắt bên vẫn mở.
Can't render this file because it contains an unexpected character in line 3682 and column 10.
+25
View File
@@ -3957,6 +3957,31 @@ lighting Light 光源
Softness 柔和度
Ambient Light 环境光
Light behind the object 光源位于对象后方
# Dimension tool (linked dimension annotations)
Create Dimension 创建尺寸标注
Dimension Tool 尺寸标注工具
Dimension Tool Options 尺寸标注工具选项
Font Family: 字体系列:
Font Style: 字体样式:
Precision: 精度:
Update Dimensions 更新尺寸标注
Display 显示
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 尺寸
Drawing : real size — 1:10 labels 1 unit on the page as 10. 图纸 : 实际尺寸 — 1:10 会将页面上的 1 个单位标注为 10。
Preview (1 inch on the page) 预览(页面上 1 英寸)
Set as Default 设为默认
out from a highlighted edge to place its dimension | 从高亮的边向外拖动以放置其尺寸 |
cancels | Double-click the tool for options 取消 | 双击工具打开选项
Linear 线性
Angular 角度
Away from the Line 远离尺寸线
Between Line and Object 在尺寸线与对象之间
Drag out from a segment, or click two points, then click to place. 从线段向外拖动,或单击两个点,然后单击以放置。
Drag out from a corner, or click two straight lines, then click to place. 从角向外拖动,或单击两条直线,然后单击以放置。
Drag out from a circle or arc for its radius, inwards for its diameter. 从圆或圆弧向外拖动得到半径,向内拖动得到直径。
@msg Choose a circle or a perfect arc 请选择圆或标准圆弧
@msg Choose a precision from 0 to 6 请选择 0 到 6 之间的精度
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 封闭开放轮廓的圆形端部。部分旋转的切割侧面保持开放。
Can't render this file because it contains an unexpected character in line 2 and column 73.
+27
View File
@@ -3741,6 +3741,33 @@ lighting Light 光源
Softness 柔和度
Ambient Light 環境光
Light behind the object 光源位於物件後方
# Dimension tool (linked dimension annotations)
Create Dimension 建立尺寸標註
Dimension Tool 尺寸標註工具
Dimension Tool Options 尺寸標註工具選項
Font Family: 字體系列:
Font Style: 字體樣式:
Precision: 精確度:
Update Dimensions 更新尺寸標註
Display 顯示
# Dimension tool and Object › Dimensions (dimension groups, their dialog and messages).
Dimension 尺寸
Drawing : real size — 1:10 labels 1 unit on the page as 10. 圖紙 : 實際尺寸 — 1:10 會將頁面上的 1 個單位標示為 10。
Preview (1 inch on the page) 預覽(頁面上 1 英寸)
Set as Default 設為預設
out from a highlighted edge to place its dimension | 從醒目提示的邊緣向外拖曳以放置其尺寸 |
cancels | Double-click the tool for options 取消 | 按兩下工具開啟選項
Linear 線性
Angular 角度
Away from the Line 遠離尺寸線
Between Line and Object 在尺寸線與物件之間
Drag out from a segment, or click two points, then click to place. 從線段向外拖曳,或按一下兩個點,然後按一下以放置。
Drag out from a corner, or click two straight lines, then click to place. 從角向外拖曳,或按一下兩條直線,然後按一下以放置。
Drag out from a circle or arc for its radius, inwards for its diameter. 從圓形或圓弧向外拖曳得到半徑,向內拖曳得到直徑。
@msg Choose a circle or a perfect arc 請選擇圓形或標準圓弧
@msg Choose a precision from 0 to 6 請選擇 0 到 6 之間的精度
X: X:
Y: Y:
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 封閉開放輪廓的圓形端部。部分旋轉的切割側面保持開放。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -64,6 +64,7 @@ pub static ICONS: &[(&str, &[u8])] = &[
("dc-cube", include_bytes!("../../../assets/icons/dc-cube.svg")),
("dc-dash-align", include_bytes!("../../../assets/icons/dc-dash-align.svg")),
("dc-dash-exact", include_bytes!("../../../assets/icons/dc-dash-exact.svg")),
("dc-dimension", include_bytes!("../../../assets/icons/dc-dimension.svg")),
("dc-dir-off", include_bytes!("../../../assets/icons/dc-dir-off.svg")),
("dc-dir-on", include_bytes!("../../../assets/icons/dc-dir-on.svg")),
("dc-direct", include_bytes!("../../../assets/icons/dc-direct.svg")),
+1
View File
@@ -82,6 +82,7 @@ pub fn tool_icon(name: &str) -> &'static str {
"tool-gradient" => "dc-gradient",
"tool-eyedropper" => "pipette",
"tool-measure" => "dc-measure",
"tool-dimension" => "dc-dimension",
"tool-blend" => "dc-blend",
"tool-symbol" => "dc-symbol-sprayer",
"tool-graph" => "chart-column",
+4
View File
@@ -60,6 +60,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
type_sections(app, ui);
divider(ui);
}
if crate::dialogs::dimension::selected(app).is_some() {
crate::dialogs::dimension::properties_section(app, ui);
divider(ui);
}
appearance_section(app, ui);
divider(ui);
section_header(ui, tl!("Align"));
+6
View File
@@ -58,6 +58,7 @@ pub const BASIC: &[(&str, &[&[&str]])] = &[
&["width", "warp", "twirl", "pucker", "bloat", "scallop", "crystallize", "wrinkle"],
&["rotate", "reflect", "scale", "shear", "reshape", "freeTransform", "puppetWarp"],
&["shapeBuilder", "livePaintBucket", "livePaintSelection", "blend"],
&["dimension"],
],
),
("Type", &[&["areaType", "typeOnPath", "verticalAreaType", "verticalTypeOnPath"], &["type", "verticalType", "touchType"]]),
@@ -129,6 +130,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
// Before the flyout: one torn off this frame floats from the next (its bar mustn't be in two
// layers in one frame).
floating(app, ui.ctx());
crate::dialogs::dimension::floating(app, ui.ctx());
flyout(app, ui.ctx());
crate::edit_toolbar::drawer(app, ui.ctx());
}
@@ -324,6 +326,7 @@ fn flyout_press(ui: &Ui, resp: &egui::Response, rect: egui::Rect) -> Option<Flyo
/// Options.
pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::Value, String> {
match tool {
"dimension" => crate::dialogs::dimension::open(app),
"hand" => app.run("view.fitArtboard", json!({})),
"zoom" => app.run("view.actualSize", json!({})),
"rotate" | "scale" | "reflect" | "shear" | "selection" | "directSelection" | "groupSelection" => {
@@ -372,6 +375,9 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
/// Warp's mesh and pins, the Artboard tool's Move and Scale Artwork with Artboard (set through
/// `tool.setOption`).
pub fn control_bar_options(app: &mut VectorcraftApp, ui: &mut Ui) {
if app.session.tool_id() == "dimension" {
return crate::dialogs::dimension::controls(app, ui);
}
/// (value, label) of each choice.
type Choices = &'static [(&'static str, &'static str)];
const MIRROR: [(&str, &str, Choices); 2] = [
+54
View File
@@ -0,0 +1,54 @@
# Dimensions
The **Dimension Tool** is in the Modify section of the toolbar. Its strip offers **Linear**,
**Angular**, **Radial** and **Tool Options…** (double-clicking the tool opens Tool Options too).
- **Linear:** hover a path segment and drag outwards: releasing places the dimension. Or click two
points (anchors snap, Shift constrains to 45°), then click to place. A curved segment is measured
from end to end.
- **Angular:** hover a corner where two straight segments meet and drag outwards. Or click two
straight lines that meet (or would, extended), then click to place.
- **Radial:** hover a circle, or a segment that is a perfect arc, and drag: outwards gives its
radius, inwards its diameter. Clicking it gives its radius. Ellipses have no single radius.
- **Escape** or switching tools cancels a dimension being placed.
**Tool Options** set the style of new dimensions: units (inches, millimetres, points, picas,
pixels, centimetres, metres, feet, yards), precision (0–6 decimals), scale, the dimension and
extension lines (weight, type, colour, arrows), and the text (font, size, colour, position and
alignment). **Position** is **Away from the Line** (the label on the far side of the dimension line
from the object) or **Between Line and Object**; **Alignment** is **Left**, **Center** or
**Right** along the line. **Save** keeps them for new dimensions; existing ones don't change.
Scale means **drawing : real size**, independent of zoom. At **1:10** a 1 in line on the page is
labelled 10 in; at **10:1**, 0.1 in. It scales lengths, not angles or the size of text and arrows.
**Properties** shows a selected dimension's units, precision, line, arrows, text size, colour,
position and alignment; changing one changes that dimension. **Apply to All** gives every
dimension in the document its style; **Set as Default** makes it the style of new dimensions.
Measurements follow the art they measure through moves, scaling and anchor and handle edits,
undo and redo included. Deleting the art, or changing its number of anchors or whether it is
closed, removes its dimensions in the same undo step rather than attaching them to a different
edge. A linked dimension moved on its own stays where it was dropped (its distance from the art
changes); moved together with its art, it keeps its distance.
Native `.vectorcraft` files keep the recipes and links. SVG, PDF and bitmap output contain
ordinary paths and text, so exported dimensions look the same. Ungrouping a dimension leaves its
paths and text as ordinary art. Pasted dimensions keep their geometry and drop their links, so
they never bind to unrelated art in another document.
## Commands
- `dimension.create`: two points, or a source (segment, angle, circle, arc, points), the mode,
offset and style; returns the group id and label.
- `dimension.update`: restyle or move existing dimensions.
- `dimension.applyToAll`: every dimension takes one dimension's style, in one undo step.
- `tool.select {"tool":"dimension"}` and
`tool.setOption {"tool":"dimension","values":{"mode":"linear","style":{"units":"mm","scale":"1:10"}}}`
(`mode`: `linear`, `angular` or `radial`).
- `tool.options {"tool":"dimension"}` opens the `dimensionOptions` dialog.
Recipes live on ordinary groups (the node's `dimension` field); rendering and export use their
generated children. Commands check options and bounds before changing anything. Linked
dimensions refresh inside `Session::edit`, sharing the source edit's undo step; documents without
any (`Document::dimensions` unset) skip that refresh.