mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 14:16:56 -06:00
Dimension tool: the documented gestures, arcs, picas and text positions; Properties section; review fixes
Drag out from a segment, corner, circle or arc to place a dimension; plain clicks pick two points or two lines. Radial dimensions on perfect arcs, picas, Away/Between and Left/Center/Right text placement. The Properties panel edits a selected dimension, with Apply to All (dimension.applyToAll) and Set as Default. Dimension Selected Paths, the Arc/R/Ø prefixes and the custom defaults are left out. The refresh returns at once in documents without dimensions (Document::dimensions); a linked dimension moved on its own keeps its place; modes, units, line types, arrows and text placement are enums; the tool has its own icon.
This commit is contained in:
+311
-91
@@ -1,23 +1,142 @@
|
||||
//! 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};
|
||||
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", deny_unknown_fields)]
|
||||
pub struct DimensionOptions {
|
||||
pub units: String,
|
||||
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: String,
|
||||
pub line_type: LineType,
|
||||
pub line_color: [u8; 3],
|
||||
pub arrow_style: String,
|
||||
pub arrow_style: ArrowStyle,
|
||||
pub arrow_scale: f64,
|
||||
pub extension_weight: f64,
|
||||
pub extension_type: String,
|
||||
pub extension_type: LineType,
|
||||
pub extension_color: [u8; 3],
|
||||
pub extension_offset: f64,
|
||||
pub extension_length: f64,
|
||||
@@ -26,54 +145,43 @@ pub struct DimensionOptions {
|
||||
pub font_style: String,
|
||||
pub font_size: f64,
|
||||
pub text_color: [u8; 3],
|
||||
pub text_position: String,
|
||||
pub text_alignment: String,
|
||||
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: "cm".into(),
|
||||
precision: 1,
|
||||
units: LengthUnit::Pt,
|
||||
precision: 2,
|
||||
scale: "1:1".into(),
|
||||
hide_units: false,
|
||||
line_weight: 1.0,
|
||||
line_type: "solid".into(),
|
||||
line_color: [224, 55, 65],
|
||||
arrow_style: "filled".into(),
|
||||
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: "solid".into(),
|
||||
extension_color: [224, 55, 65],
|
||||
extension_offset: 5.0,
|
||||
extension_length: 9.0,
|
||||
extension_type: LineType::Solid,
|
||||
extension_color: [0, 0, 0],
|
||||
extension_offset: 4.0,
|
||||
extension_length: 6.0,
|
||||
hide_extensions: false,
|
||||
font_family: "Arial".into(),
|
||||
font_style: "Regular".into(),
|
||||
font_size: 14.0,
|
||||
text_color: [224, 55, 65],
|
||||
text_position: "above".into(),
|
||||
text_alignment: "aligned".into(),
|
||||
text_offset: 9.0,
|
||||
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 {
|
||||
pub fn unit(&self) -> Option<Unit> {
|
||||
Some(match self.units.as_str() {
|
||||
"pt" => Unit::Points,
|
||||
"px" => Unit::Pixels,
|
||||
"mm" => Unit::Millimeters,
|
||||
"cm" => Unit::Centimeters,
|
||||
"m" => Unit::Meters,
|
||||
"in" => Unit::Inches,
|
||||
"ft" => Unit::Feet,
|
||||
"yd" => Unit::Yards,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
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);
|
||||
@@ -87,8 +195,8 @@ impl DimensionOptions {
|
||||
}
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
self.factor()?;
|
||||
if self.unit().is_none() || self.precision > 6 {
|
||||
return Err("Choose a length unit and precision from 0 to 6".into());
|
||||
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),
|
||||
@@ -103,22 +211,15 @@ impl DimensionOptions {
|
||||
return Err("Dimension size is outside its supported range".into());
|
||||
}
|
||||
}
|
||||
if !["solid", "dashed", "dotted"].contains(&self.line_type.as_str())
|
||||
|| !["solid", "dashed", "dotted"].contains(&self.extension_type.as_str())
|
||||
|| !["filled", "open", "tick", "none"].contains(&self.arrow_style.as_str())
|
||||
|| !["above", "center", "below"].contains(&self.text_position.as_str())
|
||||
|| !["aligned", "horizontal"].contains(&self.text_alignment.as_str())
|
||||
{
|
||||
return Err("Unknown dimension style".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(())
|
||||
}
|
||||
pub fn label(&self, points: f64, angle: bool) -> Result<String, String> {
|
||||
/// `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 { points } else { points * self.factor()? / self.unit().ok_or("Unknown unit")?.points() };
|
||||
let value = if angle { value } else { value * self.factor()? / self.units.unit().points() };
|
||||
if !value.is_finite() {
|
||||
return Err("Non-finite dimension".into());
|
||||
}
|
||||
@@ -127,7 +228,7 @@ impl DimensionOptions {
|
||||
} else if angle {
|
||||
"°"
|
||||
} else {
|
||||
self.units.as_str()
|
||||
self.units.suffix()
|
||||
};
|
||||
let sep = if suffix.is_empty() || angle { "" } else { " " };
|
||||
Ok(format!("{:.*}{sep}{suffix}", self.precision, value))
|
||||
@@ -143,30 +244,52 @@ pub struct SegmentRef {
|
||||
}
|
||||
|
||||
impl SegmentRef {
|
||||
pub fn endpoints(&self, doc: &Document) -> Option<(Point, Point, f64)> {
|
||||
/// 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);
|
||||
use vectorcraft_geom::kurbo::ParamCurveArclen;
|
||||
Some((seg.p0, seg.p3, if sp.segment_is_line(self.segment) { seg.p0.distance(seg.p3) } else { seg.arclen(1e-5) }))
|
||||
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", deny_unknown_fields)]
|
||||
pub enum DimensionSource {
|
||||
Segment { edge: SegmentRef },
|
||||
Angle { first: SegmentRef, second: SegmentRef },
|
||||
Circle { node: NodeId },
|
||||
Points { a: Option<PointRef>, b: Option<PointRef>, vertex: Option<PointRef> },
|
||||
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 } => vec![edge.node],
|
||||
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(),
|
||||
@@ -205,8 +328,7 @@ impl PointRef {
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Dimension {
|
||||
/// linear, angular, radius or diameter.
|
||||
pub mode: String,
|
||||
pub mode: DimensionMode,
|
||||
pub a: Point,
|
||||
pub b: Point,
|
||||
/// Angular vertex. For radius/diameter a is the centre and b lies on the circle.
|
||||
@@ -215,15 +337,58 @@ pub struct Dimension {
|
||||
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) {
|
||||
// Linked recipes are resolved again from the source by the engine.
|
||||
if self.source.is_none() {
|
||||
self.a = xf * self.a;
|
||||
self.b = xf * self.b;
|
||||
self.vertex = xf * self.vertex;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -238,24 +403,19 @@ impl Dimension {
|
||||
self.length = self.a.distance(self.b);
|
||||
}
|
||||
Some(DimensionSource::Segment { edge }) => {
|
||||
let (a, b, length) = edge.endpoints(doc).ok_or("Measured segment no longer exists")?;
|
||||
let (a, b) = edge.endpoints(doc).ok_or("Measured segment no longer exists")?;
|
||||
self.a = a;
|
||||
self.b = b;
|
||||
self.length = length;
|
||||
self.length = a.distance(b);
|
||||
}
|
||||
Some(DimensionSource::Angle { first, second }) => {
|
||||
for edge in [first, second] {
|
||||
let sp = doc
|
||||
.node(edge.node)
|
||||
.and_then(|n| n.path_data())
|
||||
.and_then(|p| p.subpaths.get(edge.subpath))
|
||||
.ok_or("Measured path no longer exists")?;
|
||||
if edge.segment >= sp.segment_count() || !sp.segment_is_line(edge.segment) {
|
||||
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 (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);
|
||||
@@ -268,17 +428,24 @@ impl Dimension {
|
||||
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 circular path")?;
|
||||
let direction = self.b - self.a;
|
||||
let dir = if direction.hypot() > 1e-9 { direction / direction.hypot() } else { vectorcraft_geom::Vec2::new(1.0, 0.0) };
|
||||
self.a = center;
|
||||
self.b = center + dir * radius;
|
||||
self.length = radius;
|
||||
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] {
|
||||
@@ -289,13 +456,10 @@ impl Dimension {
|
||||
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 !["linear", "angular", "radius", "diameter"].contains(&self.mode.as_str()) {
|
||||
return Err("Unknown dimension mode".into());
|
||||
}
|
||||
if self.a.distance(self.b) < 1e-6 {
|
||||
return Err("Choose two different points".into());
|
||||
}
|
||||
if self.mode == "angular" && (self.a.distance(self.vertex) < 1e-6 || self.b.distance(self.vertex) < 1e-6) {
|
||||
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(())
|
||||
@@ -306,13 +470,13 @@ impl Dimension {
|
||||
u.cross(v).atan2(u.dot(v))
|
||||
}
|
||||
pub fn label(&self) -> Result<String, String> {
|
||||
let (value, prefix) = match self.mode.as_str() {
|
||||
"angular" => (self.angle().abs().to_degrees(), ""),
|
||||
"radius" => (self.a.distance(self.b), "R "),
|
||||
"diameter" => (self.a.distance(self.b) * 2.0, "Ø "),
|
||||
_ => (self.length, if self.length > self.a.distance(self.b) + 0.01 { "Arc " } else { "" }),
|
||||
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),
|
||||
};
|
||||
Ok(format!("{prefix}{}", self.options.label(value, self.mode == "angular")?))
|
||||
self.options.label(value, self.mode == DimensionMode::Angular)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,3 +503,59 @@ pub fn circle(doc: &Document, id: NodeId) -> Option<(Point, f64)> {
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -687,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,
|
||||
@@ -767,6 +771,7 @@ impl Document {
|
||||
print_setup: None,
|
||||
assets: vec![],
|
||||
puppet: None,
|
||||
dimensions: false,
|
||||
index: Default::default(),
|
||||
};
|
||||
let id = d.alloc_id();
|
||||
|
||||
Reference in New Issue
Block a user