Skip to content

v0.0.24

Choose a tag to compare

@github-actions github-actions released this 22 Apr 21:37
· 149 commits to main since this release

Parametric tube

  • 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.