Round seams: Classic Rotate and Revolve tests, Wireframe edge, docs

- a_faces_seam_stays_within_half_its_width_of_the_face checks Rotate
  (Classic)'s branches too: No Shading keeps its paint, a gradient keeps
  the gradient, and Diffuse shades its plane darker at some view.
- classic_revolves_seams_are_round checks each seam's reach as well as
  its join, and covers a gradient.
- The Wireframe edge takes seam_stroke (the same stroke it built by hand).
- Docs: which seams went from miter to round and which already were;
  the white-light difference from the 2026-10-17 comparison is expected,
  not measured; the 3D area rows record the change; the performance note
  says its measurement predates the Classic Rotate and Revolve paths.
This commit is contained in:
Drew Dibble
2026-11-12 16:28:51 -07:00
parent 05fd55bde1
commit 1032fb3979
7 changed files with 59 additions and 30 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ use std::sync::Arc;
use serde_json::Value;
use vectorcraft_doc::color::{BlendMode, Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, StrokeLayer};
use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node};
use vectorcraft_geom::Rect;
use vectorcraft_three_d::{Bevel, BevelShape, ClassicShading, ClassicSurface, Extrude, Facet, Light};
@@ -207,8 +207,8 @@ pub(crate) fn surface(n: &Node, facets: Vec<Facet>, source: &Paint, opacity: f32
.into_iter()
.map(|face| {
if wire {
let mut edge = StrokeLayer::new(paint.clone(), WIRE);
edge.join = vectorcraft_doc::LineJoin::Round;
// A wire edge is drawn as a seam is: a round-joined stroke of the paint.
let edge = crate::lighting::seam_stroke(paint.clone(), WIRE);
return Arc::new(Node::path(n.id, face.path(), Appearance { items: vec![AppearanceItem::Stroke(edge)], ..Default::default() }));
}
let (tone, spec) = shading.tone(face.normal, &lights);
+47 -18
View File
@@ -1038,8 +1038,9 @@ fn classic_rotate_parameters_are_checked() {
}
}
/// Under a white light and under colored ones, in Extrude & Bevel, Inflate, Rotate and Extrude &
/// Bevel (Classic, each of its shading branches), a face seen almost edge-on (a sharp sliver) has
/// Under a white light and under colored ones, in Extrude & Bevel, Inflate, Rotate, Extrude &
/// Bevel (Classic, each of its shading branches) and Rotate (Classic, its No Shading and Diffuse
/// surfaces and a gradient), a face seen almost edge-on (a sharp sliver) has
/// its seam stroke rounded at the corners: every seam's joins are round, and none reaches past its
/// face more than half its width as it is outlined (a mitered seam would reach past the solid as a
/// thorn up to ten times that beyond the face's tip). Each case's view makes some face with a
@@ -1116,9 +1117,15 @@ fn a_faces_seam_stays_within_half_its_width_of_the_face() {
p["surface"] = json!(surface);
cases.push((CLASSIC_ROTATE, x, y, p, None));
}
let mut p = flat.clone();
p["surface"] = json!("diffuse");
cases.push((CLASSIC_ROTATE, x, y, p, Some(gradient.clone())));
}
// Classic's own paint for its fill: a swatch in CMYK, which its unshaded faces keep as it is.
let brick = Paint::Solid { color: Color::Cmyk { c: 0.0, m: 0.75, y: 0.9, k: 0.2 }, swatch: Some("Brick".into()), tint: 1.0 };
// Rotate (Classic)'s one plane is shaded at some views, not at every one (the light can leave
// it as its paint), so its Diffuse case is held to a darkened face at one view at least.
let mut rotate_shaded = false;
for (id, x, y, params, paint) in cases {
let mut n = if id == ROTATE || id == CLASSIC_ROTATE {
let mut b = BezPath::new();
@@ -1134,7 +1141,7 @@ fn a_faces_seam_stays_within_half_its_width_of_the_face() {
for (k, item) in n.appearance.items.iter_mut().enumerate() {
match &paint {
Some(paint) => *item.paint_mut() = paint.clone(),
None if id == CLASSIC_EXTRUDE && k == 0 => *item.paint_mut() = brick.clone(),
None if (id == CLASSIC_EXTRUDE || id == CLASSIC_ROTATE) && k == 0 => *item.paint_mut() = brick.clone(),
None => {}
}
}
@@ -1155,10 +1162,10 @@ fn a_faces_seam_stays_within_half_its_width_of_the_face() {
seamed.push((path.clone(), face.appearance.items.clone()));
}
});
// Each Classic case reaches its own branch: an unshaded face keeps the paint itself (its
// swatch and color space too), a shaded one is the paint darkened, and a gradient's
// face keeps the gradient.
if id == CLASSIC_EXTRUDE {
// Each Classic case (Extrude & Bevel and Rotate) reaches its own branch: an unshaded face
// keeps the paint itself (its swatch and color space too), a shaded one is the paint
// darkened, and a gradient's face keeps the gradient.
if id == CLASSIC_EXTRUDE || id == CLASSIC_ROTATE {
let fills: Vec<&Paint> = seamed
.iter()
.filter_map(|(_, items)| match items.first() {
@@ -1178,7 +1185,11 @@ fn a_faces_seam_stays_within_half_its_width_of_the_face() {
}
_ => false,
});
assert!(darkened, "{what}: some face shaded darker than its paint");
if id == CLASSIC_ROTATE {
rotate_shaded |= darkened;
} else {
assert!(darkened, "{what}: some face shaded darker than its paint");
}
}
}
// Every seamed fill (the paint's, a shadow's over it) has its seams checked, and on some
@@ -1224,32 +1235,50 @@ fn a_faces_seam_stays_within_half_its_width_of_the_face() {
}
}
}
assert!(rotate_shaded, "Rotate (Classic), Diffuse: no view shades its plane darker than its paint");
}
/// Revolve (Classic) draws its faces through Classic's surfaces: every face's seam has round joins,
/// on the plastic, diffuse and unshaded surfaces.
/// Revolve (Classic) draws its faces through Classic's surfaces: on the plastic, diffuse and
/// unshaded surfaces, and with a gradient, every face's seam has round joins and reaches no more
/// than half its width past its face as it is outlined (the reach Revolve's overlap test counts on).
#[test]
fn classic_revolves_seams_are_round() {
use vectorcraft_doc::color::{Gradient, GradientKind, GradientPaint, GradientStop};
let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(
GradientKind::Linear,
vec![GradientStop::new(0.0, RED), GradientStop::new(1.0, BLUE)],
))));
let mut b = BezPath::new();
b.move_to((100.0, 40.0));
b.line_to((130.0, 60.0));
b.line_to((130.0, 140.0));
b.line_to((100.0, 160.0));
for surface in ["plastic", "diffuse", "none"] {
let mut n = Node::path(NodeId(1), PathData::from_bezpath(&b), Appearance::basic(Paint::solid(BLUE), Paint::None, 0.0));
let cases = [("plastic", Paint::solid(BLUE)), ("diffuse", Paint::solid(BLUE)), ("none", Paint::solid(BLUE)), ("plastic", gradient)];
for (surface, paint) in cases {
let what = format!("{surface} gradient {}", matches!(paint, Paint::Gradient(_)));
let mut n = Node::path(NodeId(1), PathData::from_bezpath(&b), Appearance::basic(paint, Paint::None, 0.0));
n.appearance.effects.push(fx(crate::CLASSIC_REVOLVE, json!({"surface": surface})));
let art = crate::revolve_art(&n).unwrap();
let mut seams = 0;
art.walk(&mut |face| {
let Some(path) = face.path_data() else { return };
for item in &face.appearance.items {
if let AppearanceItem::Stroke(s) = item
&& s.width > 0.0
{
seams += 1;
assert_eq!(s.join, vectorcraft_doc::LineJoin::Round, "{surface}: a seam's joins");
let AppearanceItem::Stroke(s) = item else { continue };
if s.width <= 0.0 {
continue;
}
seams += 1;
assert_eq!(s.join, vectorcraft_doc::LineJoin::Round, "{what}: a seam's joins");
let drawn = crate::stroke::line_outline(&path.to_bezpath(), s, s.width, s.width * 1e-3);
let mut farthest: f64 = 0.0;
kurbo::flatten(drawn.iter(), s.width * 1e-4, |el| {
if let kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) = el {
farthest = farthest.max(path.nearest(p).map_or(f64::INFINITY, |n| n.4));
}
});
assert!(farthest <= s.width / 2.0 * 1.01, "{what}: a seam {} wide reaches {farthest} from its face", s.width);
}
});
assert!(seams > 0, "{surface}: seams checked");
assert!(seams > 0, "{what}: seams checked");
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ Four original synthetic scenes were exported at 1200 x 1200 from unchanged main
## Round seam joins
Measured once, on 2026-11-12, on another machine (an Apple M4 Pro with 14 cores) that was not idle: its one-minute load average was 6 to 13, so these are one loaded run, not the idle measurements above. Two `vectorcraft-cli` builds, each in release (`cargo build --release -p vectorcraft-cli`): main at `6d6bbff53`, and that commit with the round seam joins, run in turn, three times each. `bench` fits the artboard to 95% of 2880 x 1800 pixels, draws one frame to warm each renderer and reports the average of the frames after it: `threads 8` with the renderer's default workers (one per core it may use, up to 8, one core left free), `threads 0` on the calling thread alone. A cold frame is a whole `run` that opens the scene, builds its 3D art and draws it once.
Measured once, on 2026-11-12, on another machine (an Apple M4 Pro with 14 cores) that was not idle: its one-minute load average was 6 to 13, so these are one loaded run, not the idle measurements above. Two `vectorcraft-cli` builds, each in release (`cargo build --release -p vectorcraft-cli`): main at `6d6bbff53`, and that commit with the round seam joins, run in turn, three times each. The measurement predates the change's rebase onto a main whose Rotate (Classic) and Revolve (Classic) draw their faces through Classic's surfaces, so it does not cover their seams. `bench` fits the artboard to 95% of 2880 x 1800 pixels, draws one frame to warm each renderer and reports the average of the frames after it: `threads 8` with the renderer's default workers (one per core it may use, up to 8, one core left free), `threads 0` on the calling thread alone. A cold frame is a whole `run` that opens the scene, builds its 3D art and draws it once.
White light: extruded type with a cove bevel, an Inflate, an Extrude & Bevel (Classic) star with a classic bevel and a Revolve, no strokes (6,283 nodes drawn). The median of three runs of 20 frames, on 8 threads and on one: main 8.0 ms and 18.3 ms; with the round joins 7.5 ms and 17.7 ms. Cold frames took 0.08 s of processor time on each. The differences are within noise for one loaded run. Not measured: `vectorcraft-cli perf`, the legacy white-light Inflate row above (still the 2026-10-17 figures), frames whose 3D art is rebuilt (while dragging a 3D slider, say) apart from the cold run, and SVG/PDF exports.
+2 -2
View File
@@ -181,7 +181,7 @@ VectorCraft choices; an original approximation, never compared with another appl
| Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) |
| Extrude & Bevel, Inflate, Rotate | initial: live, round seam joins, depth, twist, taper, solid or hollow caps, five original bevel shapes, Inflate volume, both sides, twist, taper and cap, Rotate of every fill and stroke, groups and layers turned about one shared axis (#4506), contours that cross, touch or overlap merged first ([three-d.md](three-d.md)), Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) |
| Materials panel, lighting, ray-traced render | Window › 3D and Materials panel with the object types (Plane, Extrude, Revolve, Inflate) and the Object, Rotation and Lighting settings of the applied effect, and Quick Actions › Expand as Wireframes ([three-d.md](three-d.md#3d-and-materials-panel)); one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; hard-edged shadows behind or below the object from the first light ([three-d.md](three-d.md#shadows)); no materials, soft shadows or ray tracing |
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | Extrude & Bevel (Classic) initial: live, round seam joins, Extrude Depth, Cap, five original bevel shapes with Height and Bevel Extent In or Out, Position presets and rotation gizmo, Perspective, Wireframe, No Shading, Diffuse and Plastic surfaces with one to ten lights, Light Intensity, Ambient Light, Highlight Intensity and Size, Blend Steps, Black, Custom or no Shading Color, Draw Hidden Faces ([create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)); Revolve (Classic) initial: live, Angle, Cap, Offset from the left or right edge, Position presets and rotation gizmo, Perspective, the same four surfaces, lights, highlight, Blend Steps and Shading Color, groups and layers revolved about one shared axis (same page); Rotate (Classic) initial: live, every fill and stroke turned as one plane, Position presets and rotation gizmo, Perspective, No Shading and Diffuse surfaces with lights, Ambient Light, Blend Steps and Shading Color ([rotate objects in 3D](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/rotate-objects-in-3d.html)); Map Art: none |
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | Extrude & Bevel (Classic) initial: live, round seam joins, Extrude Depth, Cap, five original bevel shapes with Height and Bevel Extent In or Out, Position presets and rotation gizmo, Perspective, Wireframe, No Shading, Diffuse and Plastic surfaces with one to ten lights, Light Intensity, Ambient Light, Highlight Intensity and Size, Blend Steps, Black, Custom or no Shading Color, Draw Hidden Faces ([create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)); Revolve (Classic) initial: live, round seam joins, Angle, Cap, Offset from the left or right edge, Position presets and rotation gizmo, Perspective, the same four surfaces, lights, highlight, Blend Steps and Shading Color, groups and layers revolved about one shared axis (same page); Rotate (Classic) initial: live, round seam joins, every fill and stroke turned as one plane, Position presets and rotation gizmo, Perspective, No Shading and Diffuse surfaces with lights, Ambient Light, Blend Steps and Shading Color ([rotate objects in 3D](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/rotate-objects-in-3d.html)); Map Art: none |
| Turntable (30.0 beta) | none |
| Export 3D objects (OBJ, USDA, glTF) | initial: Asset Export's Export 3D Object collects an object with Extrude & Bevel, Revolve, Inflate or Rotate as a 3D asset, and Export writes its solids as a USDA (the default), USDZ, glTF or OBJ mesh with a material in each solid's paint color (`assets.add3D`, `assets.set3DFormat`, `assets.export3D`); no textures, map art or shading ([3D export help](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/export-3d-vector-artwork.html)) |
@@ -193,7 +193,7 @@ angular cut caps, exact visibility, the rest of 3D Classic (Map Art), Turntable)
| Date | Change | Summary |
|---|---|---|
| 2026-11-12 | minor | The seams of Extrude & Bevel, Inflate and Rotate under a white light, and every 3D (Classic) seam, went from miter to round joins (colored-light seams were already round): no thorns on edge-on facets, and facets with no neighbor now have rounded convex corners ([three-d.md](three-d.md#seams)); scores and estimates unchanged |
| 2026-11-12 | minor | The seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins (the seams of Revolve and of colored lights, and 3D (Classic) Wireframe edges, were already round): no thorns on edge-on facets, and facets with no neighbor now have rounded convex corners ([three-d.md](three-d.md#seams)); scores and estimates unchanged |
| 2026-11-10 | minor | Effect Gallery: each filter has a thumbnail, and the dialog has a preview of the visible stack (or the browsed filter when the stack is empty). Both are an original landscape drawing passed through the filter ([Filter Gallery overview](https://helpx.adobe.com/photoshop/desktop/effects-filters/get-started-with-filters/filter-gallery.html)). The canvas preview and one-undo OK are unchanged. Remaining hours for the gallery's thumbnails and in-dialog preview: 1–2 → none. Counts unchanged (57/57). |
| 2026-11-08 | minor | Extrude & Bevel, Inflate and Rotate merge contours that cross, touch or overlap (a glyph's arm across its stem) before building a solid, so the solid follows the shape as it is filled; others are built as drawn ([three-d.md](three-d.md)); scores and estimates unchanged |
| 2026-11-06 | minor | Extrude & Bevel, Inflate and Rotate apply to a group or layer: a solid for every path, compound path and type in it, all turned about one shared axis as the public documentation describes for groups ([three-d.md](three-d.md#groups), #4506); scores and estimates unchanged |
+1 -1
View File
@@ -865,7 +865,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
| Date | Change | Summary |
|---|---|---|
| 2026-11-12 | minor | 3D: The seams of Extrude & Bevel, Inflate and Rotate under a white light, and every 3D (Classic) seam, went from miter to round joins (colored-light seams were already round): no thorns on edge-on facets, and facets with no neighbour now have rounded convex corners ([three-d.md](three-d.md#seams)). Scores and estimates unchanged. |
| 2026-11-12 | minor | 3D: The seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins (the seams of Revolve and of colored lights, and 3D (Classic) Wireframe edges, were already round): no thorns on edge-on facets, and facets with no neighbour now have rounded convex corners ([three-d.md](three-d.md#seams)). Scores and estimates unchanged. |
| 2026-11-12 | minor | Explicit editing-data import keeps a single artboard beyond a capped 14,400 pt PDF page (#6865), with a warning; automatic mode stays unchanged. Scores and estimates unchanged. |
| 2026-11-09 | minor | G4 review (#6084): positional variants no supported feature draws can be applied as glyph choices in both Glyphs views; unlisted glyphs still respect name ownership. OpenType style options keep only a valid last on-value when enabling a set. Existing contextual/ligature lookup and unsupported-feature gaps remain; estimate unchanged. |
| 2026-11-07 | minor | G4: the Glyphs panel's Alternates for Current Selection applies the OpenType feature that draws a variant to that letter alone (`salt=2`; the style commands take `tag=N`; runs that differ only in their features shape as one piece of text), so an Arabic word stays joined, and Show › Entire Font lists the unencoded glyphs (#5514). Estimate unchanged. |
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -387,7 +387,7 @@ Release measurements and a repeatable CLI scene are in [3D lighting performance]
| Date | Change |
|---|---|
| 2026-11-12 | The seams of Extrude & Bevel, Inflate and Rotate under a white light, and every 3D (Classic) seam, went from miter to round joins (colored-light seams were already round): no thorns on edge-on facets, and facets with no neighbor now have rounded convex corners ([Seams](#seams)). |
| 2026-11-12 | The seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins (the seams of Revolve and of colored lights, and 3D (Classic) Wireframe edges, were already round): no thorns on edge-on facets, and facets with no neighbor now have rounded convex corners ([Seams](#seams)). |
| 2026-11-08 | Contours that cross, touch or overlap are merged by vectorcraft-pathops before Extrude & Bevel, Inflate and Rotate build a solid, sharing its limits; others are built as drawn. |
| 2026-11-06 | Extrude & Bevel, Inflate and Rotate apply to groups and layers: every path, compound path and type in them turns about one shared axis (#4506). |
| 2026-10-21 | Four original lighting presets, shared by all four 3D effects and commands, with effective-setting recognition and existing preview/history behavior. |