|
618 | 618 |
|
619 | 619 | (defn- build-segment-mesh |
620 | 620 | "Build a mesh from sweep rings (no caps - for segments that will be joined). |
621 | | - Returns nil if not enough rings." |
622 | | - [rings] |
623 | | - (let [n-rings (count rings) |
624 | | - n-verts (count (first rings))] |
625 | | - (when (and (>= n-rings 2) (>= n-verts 3)) |
626 | | - (let [vertices (vec (apply concat rings)) |
627 | | - side-faces (vec |
628 | | - (mapcat |
629 | | - (fn [ring-idx] |
630 | | - (mapcat |
631 | | - (fn [vert-idx] |
632 | | - (let [next-vert (mod (inc vert-idx) n-verts) |
633 | | - base (* ring-idx n-verts) |
634 | | - next-base (* (inc ring-idx) n-verts) |
635 | | - b0 (+ base vert-idx) |
636 | | - b1 (+ base next-vert) |
637 | | - t0 (+ next-base vert-idx) |
638 | | - t1 (+ next-base next-vert)] |
639 | | - ;; CCW winding from outside |
640 | | - [[b0 t1 b1] [b0 t0 t1]])) |
641 | | - (range n-verts))) |
642 | | - (range (dec n-rings))))] |
643 | | - {:type :mesh |
644 | | - :primitive :segment |
645 | | - :vertices vertices |
646 | | - :faces side-faces})))) |
| 621 | + Returns nil if not enough rings. |
| 622 | + flip-winding? reverses face winding for backward extrusions." |
| 623 | + ([rings] (build-segment-mesh rings false)) |
| 624 | + ([rings flip-winding?] |
| 625 | + (let [n-rings (count rings) |
| 626 | + n-verts (count (first rings))] |
| 627 | + (when (and (>= n-rings 2) (>= n-verts 3)) |
| 628 | + (let [vertices (vec (apply concat rings)) |
| 629 | + side-faces (vec |
| 630 | + (mapcat |
| 631 | + (fn [ring-idx] |
| 632 | + (mapcat |
| 633 | + (fn [vert-idx] |
| 634 | + (let [next-vert (mod (inc vert-idx) n-verts) |
| 635 | + base (* ring-idx n-verts) |
| 636 | + next-base (* (inc ring-idx) n-verts) |
| 637 | + b0 (+ base vert-idx) |
| 638 | + b1 (+ base next-vert) |
| 639 | + t0 (+ next-base vert-idx) |
| 640 | + t1 (+ next-base next-vert)] |
| 641 | + ;; CCW winding from outside |
| 642 | + ;; Flip when extrusion goes backward |
| 643 | + (if flip-winding? |
| 644 | + [[b0 b1 t1] [b0 t1 t0]] |
| 645 | + [[b0 t1 b1] [b0 t0 t1]]))) |
| 646 | + (range n-verts))) |
| 647 | + (range (dec n-rings))))] |
| 648 | + {:type :mesh |
| 649 | + :primitive :segment |
| 650 | + :vertices vertices |
| 651 | + :faces side-faces}))))) |
647 | 652 |
|
648 | 653 | (defn- build-corner-mesh |
649 | 654 | "Build a corner mesh connecting two rings (no caps). |
650 | | - ring1 and ring2 must have the same number of vertices." |
651 | | - [ring1 ring2] |
652 | | - (let [n-verts (count ring1)] |
653 | | - (when (and (>= n-verts 3) (= n-verts (count ring2))) |
654 | | - (let [vertices (vec (concat ring1 ring2)) |
655 | | - side-faces (vec |
656 | | - (mapcat |
657 | | - (fn [i] |
658 | | - (let [next-i (mod (inc i) n-verts) |
659 | | - b0 i |
660 | | - b1 next-i |
661 | | - t0 (+ n-verts i) |
662 | | - t1 (+ n-verts next-i)] |
663 | | - ;; CCW winding from outside |
664 | | - [[b0 t1 b1] [b0 t0 t1]])) |
665 | | - (range n-verts)))] |
666 | | - {:type :mesh |
667 | | - :primitive :corner |
668 | | - :vertices vertices |
669 | | - :faces side-faces})))) |
| 655 | + ring1 and ring2 must have the same number of vertices. |
| 656 | + flip-winding? reverses face winding for backward extrusions." |
| 657 | + ([ring1 ring2] (build-corner-mesh ring1 ring2 false)) |
| 658 | + ([ring1 ring2 flip-winding?] |
| 659 | + (let [n-verts (count ring1)] |
| 660 | + (when (and (>= n-verts 3) (= n-verts (count ring2))) |
| 661 | + (let [vertices (vec (concat ring1 ring2)) |
| 662 | + side-faces (vec |
| 663 | + (mapcat |
| 664 | + (fn [i] |
| 665 | + (let [next-i (mod (inc i) n-verts) |
| 666 | + b0 i |
| 667 | + b1 next-i |
| 668 | + t0 (+ n-verts i) |
| 669 | + t1 (+ n-verts next-i)] |
| 670 | + ;; CCW winding from outside |
| 671 | + ;; Flip when extrusion goes backward |
| 672 | + (if flip-winding? |
| 673 | + [[b0 b1 t1] [b0 t1 t0]] |
| 674 | + [[b0 t1 b1] [b0 t0 t1]]))) |
| 675 | + (range n-verts)))] |
| 676 | + {:type :mesh |
| 677 | + :primitive :corner |
| 678 | + :vertices vertices |
| 679 | + :faces side-faces}))))) |
670 | 680 |
|
671 | 681 | ;; --- Joint mode helper functions --- |
672 | 682 |
|
|
2023 | 2033 | first-ring (:sweep-first-ring state)] |
2024 | 2034 | (if (>= (count rings) 2) |
2025 | 2035 | ;; Build final segment from remaining rings |
2026 | | - (let [final-segment (build-segment-mesh rings) |
| 2036 | + ;; Detect if extrusion went backward by comparing first and last ring centroids |
| 2037 | + ;; with the sweep-initial-heading |
| 2038 | + (let [ring-centroid-fn (fn [ring] |
| 2039 | + (let [n (count ring)] |
| 2040 | + (v* (reduce v+ ring) (/ 1.0 n)))) |
| 2041 | + first-centroid (ring-centroid-fn (first rings)) |
| 2042 | + last-centroid (ring-centroid-fn (last rings)) |
| 2043 | + extrusion-dir (v- last-centroid first-centroid) |
| 2044 | + initial-heading (or (:sweep-initial-heading state) (:heading state)) |
| 2045 | + ;; If dot product is negative, extrusion went backward |
| 2046 | + backward? (neg? (dot extrusion-dir initial-heading)) |
| 2047 | + ;; Flip winding for backward extrusion to correct normals |
| 2048 | + flip-winding? backward? |
| 2049 | + final-segment (build-segment-mesh rings flip-winding?) |
2027 | 2050 | ;; Determine the actual first and last rings for caps |
2028 | 2051 | actual-first-ring (or first-ring (first rings)) |
2029 | 2052 | actual-last-ring (last rings) |
|
2053 | 2076 | top-normal (when (>= n-verts 3) |
2054 | 2077 | (normalize (v- (ring-centroid actual-last-ring) |
2055 | 2078 | (ring-centroid second-to-last-ring)))) |
2056 | | - ;; Bottom cap mesh: flip=false for normal pointing back |
| 2079 | + ;; Cap flip logic: XOR with backward? to handle backward extrusion |
| 2080 | + ;; Forward (backward?=false): bottom=false, top=true |
| 2081 | + ;; Backward (backward?=true): bottom=true, top=false |
| 2082 | + bottom-cap-flip backward? |
| 2083 | + top-cap-flip (not backward?) |
| 2084 | + ;; Bottom cap mesh |
2057 | 2085 | bottom-cap-mesh (when (>= n-verts 3) |
2058 | 2086 | {:type :mesh |
2059 | 2087 | :primitive :cap |
2060 | 2088 | :vertices (vec actual-first-ring) |
2061 | | - :faces (triangulate-cap actual-first-ring 0 bottom-normal false)}) |
2062 | | - ;; Top cap mesh: flip=true for normal pointing forward |
| 2089 | + :faces (triangulate-cap actual-first-ring 0 bottom-normal bottom-cap-flip)}) |
| 2090 | + ;; Top cap mesh |
2063 | 2091 | top-cap-mesh (when (>= n-verts 3) |
2064 | 2092 | {:type :mesh |
2065 | 2093 | :primitive :cap |
2066 | 2094 | :vertices (vec actual-last-ring) |
2067 | | - :faces (triangulate-cap actual-last-ring 0 top-normal true)})] |
| 2095 | + :faces (triangulate-cap actual-last-ring 0 top-normal top-cap-flip)})] |
2068 | 2096 | (-> state |
2069 | 2097 | (assoc :meshes (cond-> all-meshes |
2070 | 2098 | bottom-cap-mesh (conj bottom-cap-mesh) |
|
3532 | 3560 | second-ring (nth all-rings 1) |
3533 | 3561 | second-to-last-ring (nth all-rings (- n-rings 2)) |
3534 | 3562 |
|
| 3563 | + ;; Detect backward extrusion - only for simple straight paths |
| 3564 | + ;; For curved paths (arcs, multiple segments), don't flip |
| 3565 | + is-simple-straight? (= n-segments 1) |
| 3566 | + overall-extrusion-dir (v- (ring-centroid last-ring) (ring-centroid first-ring)) |
| 3567 | + initial-heading (:heading state) |
| 3568 | + backward? (and is-simple-straight? |
| 3569 | + (neg? (dot overall-extrusion-dir initial-heading))) |
| 3570 | + |
3535 | 3571 | ;; Bottom cap: normal points opposite to extrusion direction |
3536 | 3572 | bottom-extrusion-dir (normalize (v- (ring-centroid second-ring) |
3537 | 3573 | (ring-centroid first-ring))) |
|
3542 | 3578 | (ring-centroid second-to-last-ring))) |
3543 | 3579 | top-normal top-extrusion-dir |
3544 | 3580 |
|
3545 | | - ;; Bottom cap: flip=false produces normal pointing back (-X) |
3546 | | - bottom-cap-faces (triangulate-cap first-ring 0 bottom-normal false) |
| 3581 | + ;; Cap flip: XOR with backward? to handle backward extrusion |
| 3582 | + bottom-cap-flip backward? |
| 3583 | + top-cap-flip (not backward?) |
| 3584 | + bottom-cap-faces (triangulate-cap first-ring 0 bottom-normal bottom-cap-flip) |
3547 | 3585 |
|
3548 | 3586 | ;; Side faces connecting consecutive rings |
3549 | 3587 | ;; Ring i vertices: i*n-verts to (i+1)*n-verts - 1 |
3550 | 3588 | ;; Use shorter diagonal to split each quad, preventing inverted |
3551 | 3589 | ;; triangles at tight curve bends where quads become non-planar. |
| 3590 | + ;; Flip winding for backward extrusion to correct normals. |
3552 | 3591 | side-faces (vec |
3553 | 3592 | (mapcat |
3554 | 3593 | (fn [ring-idx] |
|
3566 | 3605 | db1t0 (v- (nth vertices b1) (nth vertices t0))] |
3567 | 3606 | (if (<= (dot db0t1 db0t1) (dot db1t0 db1t0)) |
3568 | 3607 | ;; Diagonal b0-t1 (shorter) |
3569 | | - [[b0 t0 t1] [b0 t1 b1]] |
| 3608 | + (if backward? |
| 3609 | + [[b0 t1 t0] [b0 b1 t1]] ;; flipped |
| 3610 | + [[b0 t0 t1] [b0 t1 b1]]) ;; normal |
3570 | 3611 | ;; Diagonal b1-t0 (shorter) |
3571 | | - [[b0 t0 b1] [t0 t1 b1]]))) |
| 3612 | + (if backward? |
| 3613 | + [[b0 b1 t0] [t0 b1 t1]] ;; flipped |
| 3614 | + [[b0 t0 b1] [t0 t1 b1]])))) ;; normal |
3572 | 3615 | (range n-verts))) |
3573 | 3616 | (range (dec n-rings)))) |
3574 | 3617 |
|
3575 | | - ;; Top cap: flip=true produces normal pointing forward (+X) |
| 3618 | + ;; Top cap: flip based on backward? |
3576 | 3619 | last-ring-base (* (dec n-rings) n-verts) |
3577 | | - top-cap-faces (triangulate-cap last-ring last-ring-base top-normal true) |
| 3620 | + top-cap-faces (triangulate-cap last-ring last-ring-base top-normal top-cap-flip) |
3578 | 3621 |
|
3579 | 3622 | all-faces (vec (concat bottom-cap-faces side-faces top-cap-faces)) |
3580 | 3623 |
|
|
3596 | 3639 | Creates a solid of revolution (like a lathe operation). |
3597 | 3640 |
|
3598 | 3641 | The profile is interpreted as: |
3599 | | - - 2D X = radial distance from axis (perpendicular to heading) |
3600 | | - - 2D Y = position along axis (in heading direction) |
| 3642 | + - 2D X = radial distance from axis (swept around up axis) |
| 3643 | + - 2D Y = position along axis (in up direction) |
| 3644 | +
|
| 3645 | + At θ=0 the stamp matches extrude: shape-X → right, shape-Y → up. |
| 3646 | + Revolution axis = turtle's up vector. Use (tv) to change the axis. |
3601 | 3647 |
|
3602 | 3648 | The axis of revolution passes through the turtle's current position. |
3603 | 3649 |
|
|
3624 | 3670 | ;; Get profile points |
3625 | 3671 | profile-points (:points shape) |
3626 | 3672 | n-profile (count profile-points) |
| 3673 | + ;; Calculate shape winding using signed area |
| 3674 | + ;; Positive = CCW, Negative = CW |
| 3675 | + shape-signed-area (let [pts profile-points |
| 3676 | + n (count pts)] |
| 3677 | + (/ (reduce + (for [i (range n)] |
| 3678 | + (let [[x1 y1] (nth pts i) |
| 3679 | + [x2 y2] (nth pts (mod (inc i) n))] |
| 3680 | + (- (* x1 y2) (* x2 y1))))) |
| 3681 | + 2)) |
| 3682 | + ;; Determine if we need to flip face winding |
| 3683 | + ;; Flip when: (CCW shape AND positive angle) OR (CW shape AND negative angle) |
| 3684 | + shape-is-ccw? (pos? shape-signed-area) |
| 3685 | + flip-winding? (if shape-is-ccw? (pos? angle) (neg? angle)) |
3627 | 3686 | ;; Calculate number of segments based on resolution |
3628 | 3687 | ;; Use same logic as arc: resolution based on angle |
3629 | 3688 | steps (calc-arc-steps state (* 2 Math/PI) (Math/abs angle)) |
|
3639 | 3698 | ;; Right vector = heading × up (initial radial direction at θ=0) |
3640 | 3699 | right (normalize (cross heading up)) |
3641 | 3700 | ;; Transform profile point [px py] at angle θ to 3D: |
3642 | | - ;; pos + py * heading + px * (cos(θ) * right + sin(θ) * up) |
| 3701 | + ;; pos + py * up + px * (cos(θ) * right + sin(θ) * heading) |
| 3702 | + ;; At θ=0 this matches extrude's stamp: px*right + py*up |
| 3703 | + ;; Revolution axis = up; radial sweeps from right toward heading |
| 3704 | + ;; shape-X = radial distance (swept around up axis) |
| 3705 | + ;; shape-Y = axial position (along up / revolution axis) |
3643 | 3706 | transform-point (fn [[px py] theta] |
3644 | 3707 | (let [cos-t (Math/cos theta) |
3645 | 3708 | sin-t (Math/sin theta) |
3646 | | - ;; Radial direction at this angle |
3647 | | - radial-x (+ (* cos-t (nth right 0)) (* sin-t (nth up 0))) |
3648 | | - radial-y (+ (* cos-t (nth right 1)) (* sin-t (nth up 1))) |
3649 | | - radial-z (+ (* cos-t (nth right 2)) (* sin-t (nth up 2)))] |
3650 | | - [(+ (nth pos 0) (* py (nth heading 0)) (* px radial-x)) |
3651 | | - (+ (nth pos 1) (* py (nth heading 1)) (* px radial-y)) |
3652 | | - (+ (nth pos 2) (* py (nth heading 2)) (* px radial-z))])) |
| 3709 | + ;; Radial direction at this angle (sweeps in right-heading plane) |
| 3710 | + radial-x (+ (* cos-t (nth right 0)) (* sin-t (nth heading 0))) |
| 3711 | + radial-y (+ (* cos-t (nth right 1)) (* sin-t (nth heading 1))) |
| 3712 | + radial-z (+ (* cos-t (nth right 2)) (* sin-t (nth heading 2)))] |
| 3713 | + [(+ (nth pos 0) (* py (nth up 0)) (* px radial-x)) |
| 3714 | + (+ (nth pos 1) (* py (nth up 1)) (* px radial-y)) |
| 3715 | + (+ (nth pos 2) (* py (nth up 2)) (* px radial-z))])) |
3653 | 3716 | ;; Generate all rings |
3654 | 3717 | rings (vec (for [i (range n-rings)] |
3655 | 3718 | (let [theta (* i angle-step)] |
|
3677 | 3740 | t0 (+ next-base vert-idx) |
3678 | 3741 | t1 (+ next-base next-vert)] |
3679 | 3742 | ;; CCW winding for outward-facing normals |
3680 | | - [[b0 t1 t0] [b0 b1 t1]])) |
| 3743 | + ;; Flip based on shape winding and angle sign |
| 3744 | + (if flip-winding? |
| 3745 | + [[b0 t0 t1] [b0 t1 b1]] |
| 3746 | + [[b0 t1 t0] [b0 b1 t1]]))) |
3681 | 3747 | (range n-profile))))) |
3682 | 3748 | (range (if is-closed n-rings (dec n-rings))))) |
3683 | 3749 | ;; Caps for open revolve (angle < 360) |
|
3686 | 3752 | (let [first-ring (first rings) |
3687 | 3753 | last-ring (last rings) |
3688 | 3754 | last-ring-base (* (dec n-rings) n-profile) |
3689 | | - ;; Calculate normals from ring geometry |
3690 | | - ;; Start cap normal: opposite to initial right direction |
3691 | | - start-normal (v* right -1) |
3692 | | - ;; End cap normal: rotated right direction |
3693 | | - end-theta (* (dec n-rings) angle-step) |
3694 | | - end-cos (Math/cos end-theta) |
3695 | | - end-sin (Math/sin end-theta) |
3696 | | - end-normal [(+ (* end-cos (nth right 0)) (* end-sin (nth up 0))) |
3697 | | - (+ (* end-cos (nth right 1)) (* end-sin (nth up 1))) |
3698 | | - (+ (* end-cos (nth right 2)) (* end-sin (nth up 2)))] |
3699 | | - ;; Triangulate caps |
3700 | | - start-cap (triangulate-cap first-ring 0 start-normal false) |
3701 | | - end-cap (triangulate-cap last-ring last-ring-base end-normal true)] |
| 3755 | + ;; For triangulation, we need the normal to the ring PLANE |
| 3756 | + ;; (not the cap face normal). Ring plane is spanned by up and right, |
| 3757 | + ;; so plane normal = cross(up, right) = -heading (or heading depending on order) |
| 3758 | + ;; At theta=0, ring plane normal is along heading direction |
| 3759 | + start-proj-normal heading |
| 3760 | + ;; At theta=end, ring plane normal is still along heading |
| 3761 | + ;; (revolution around up doesn't change the ring plane normal direction) |
| 3762 | + end-proj-normal heading |
| 3763 | + ;; Cap flip determines which way the triangles face |
| 3764 | + ;; Try inverted flip logic |
| 3765 | + start-cap-flip (not flip-winding?) |
| 3766 | + end-cap-flip flip-winding? |
| 3767 | + ;; Triangulate caps using ring plane normal for projection |
| 3768 | + start-cap (triangulate-cap first-ring 0 start-proj-normal start-cap-flip) |
| 3769 | + end-cap (triangulate-cap last-ring last-ring-base end-proj-normal end-cap-flip)] |
3702 | 3770 | (vec (concat start-cap end-cap)))) |
3703 | 3771 | all-faces (if cap-faces |
3704 | 3772 | (vec (concat side-faces cap-faces)) |
|
0 commit comments