You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Miter-joined cross-section frames at interior spine points. Previously the ring at each spine point was placed perpendicular to the central-difference tangent with uniform cross-section dimensions, which collapsed two failure modes onto wide beads around sharp corners: (a) a fan of triangles exploding off the outside of the corner as adjacent frames rotated through large angles between closely-spaced samples, and (b) an inside crease where the rings' inner edges penetrated each other. The frame derivation now matches what 2D stroke rendering does at a miter join — tangent is the unit bisector of the incoming/outgoing segment directions (not the central-difference average, which diverges for unequal-length neighboring segments), and the cross-section's width axis is scaled by 1 / cos(half_turn_angle) so the outer surface stays at constant half-width from each adjacent segment's centerline. Past a miter limit of 4 (SVG default), corners fall back to scale = 1 — strictly no worse than the pre-miter build at near-hairpins where the bisector is anyway ill-defined. The miter scale is applied only to position writes; normals continue to use the orthonormal U, V frame, so shading of the miter patch is correctly the bisector of the two adjacent-segment surface normals without any extra work. Build overhead on a 1M-point tube is ~16% (48 ms → off the main thread via the LOD worker for any tube ≥ 25k points, so not a UI hit in practice). add_parametric_tube(orientations=...) overrides bypass miter entirely — explicit per-point quaternion frames are used as-is and miter scale is pinned to 1.
Twisted tube at 180° tangent reversals fixed. Spines that hairpin back on themselves (retrace, zigzag infill) produced a self-intersecting figure-8 twist across the reversal. Constant-up frames derive U = V × T, so when T flips sign U flips too and adjacent rings indexed their cross-section vertices at opposite physical points. The frame-build now sweeps localFrames after the main loop and — where U_i · U_{i-1} < -0.95 — negates U on that ring and rewrites its vertices. V is untouched (even in T), so no cascading changes; the sweep is sticky, so the whole return leg stays aligned with the forward leg. Mirrored in the LOD worker so reduced and full-resolution geometries agree at reversals. Threshold -0.95 restricts the flip to true hairpins — gradual curves and non-reversal sharp corners stay untouched. Skipped when the caller supplies explicit orientations. Revolution caps and the animated end cap now read tangent from the precomputed central-difference array instead of deriving T = U × V, so caps on retraces no longer extrude backward into the tube body. examples/20_toolpath_zigzag.py covers the regression — a C-shape retrace and a six-pass zigzag infill.
Fixes
Gizmo + Home button no longer overlap the animation toolbar after a viewport resize. The cached _animLiftCss (used by the ViewHelper render shim, gizmo hit-test, and the Home button via --tjsv-anim-lift) was only written on animation load/unload. But the toolbar's height also depends on viewport width — the timeline-row wraps when controls don't fit — so resizing after load left the cache stale and the gizmo/Home button lifted by the old, smaller height, overlapping the toolbar and making gizmo clicks land in the wrong place. A ResizeObserver on .tjsv-animation-controls now pushes the current height into the cache and the CSS variable on every wrap/unwrap/show/hide, using the observer's own borderBoxSize to avoid a second synchronous layout read. Load/unload still prime synchronously so the first frame after a show already has the correct lift. Regression covered by tests/test_browser.py::test_anim_lift_tracks_toolbar_reflow_on_resize.