| … | … | @@ -855,3 +855,162 @@ collisions. |
| 855 | 855 | 6. Whole-selection targets, the Cutters collection, repeat: `gate_box_cutter` H-checks. |
| 856 | 856 | 7. Q menu Change cut, Extract, Apply up to here: `gate_q_stack`. |
| 857 | 857 | 8. Stack scroll preview: `gate_q_stack` S-checks. |
| 858 | |
| 859 | ## 14. L3 — cable extras (DESIGN, user answers 2026-10-19) |
| 860 | |
| 861 | #120 L3's remaining cable items: cables from edges, a bundle between two face sets, drop to |
| 862 | the ground, custom profiles. Cablerator 1.4.0's behaviour, read from its operators' settings |
| 863 | and modal keys only (GPL: clean-room, no code or assets). Decided by the user: |
| 864 | |
| 865 | - **Bundle** = N ordinary curve + cable shapes in a new "Bundle" collection, ONE gesture. |
| 866 | Each cable stays editable; count / seed / tension are tuned in the tool and by Adjust |
| 867 | Last, not afterwards. |
| 868 | - **Edge set** = Shift-click edges in edge mode (a cross-brush SET, as face mode has). |
| 869 | - **Drop** moves every point but an open curve's two ends. |
| 870 | - **Custom profile** = a closed CURVE the cable names, live; plus strands / spread |
| 871 | (Cablerator's multi-profile). |
| 872 | |
| 873 | ### 14.1 Strands (every profile) |
| 874 | |
| 875 | Cablerator's multi-profile repeats the profile around the cable's axis. Ours, on |
| 876 | `param_cable.gd`: |
| 877 | |
| 878 | | param | default | | |
| 879 | |---|---|---| |
| 880 | | `strands` | 1 | 1–12 copies of the profile around the axis | |
| 881 | | `spread` | 1.0 | each copy's centre sits `spread` × r off the axis (strands > 1 only) | |
| 882 | | `twist` | 0 | degrees the strands turn per metre (a rope) | |
| 883 | |
| 884 | - Strand j's ring k is the profile at `c + (n cos θ + b sin θ) · spread · r`, with |
| 885 | θ = 2πj / strands + twist · s, the profile itself unrotated in (n, b). Each strand's |
| 886 | segment k = the hull of ITS rings k and k+1: one convex piece per strand per segment. |
| 887 | - `strands = 1` takes no offset and no twist, so every cable recorded before this folds to |
| 888 | the same brushes (gv stays 1; the new params have defaults). |
| 889 | - Caps and rings stay one piece around the whole bundle (they read the cable's radius × |
| 890 | (1 + spread) when strands > 1, so a plug still covers every strand). |
| 891 | - Pieces grow × strands. Measured in the gate, not guessed: a 10 m cable at the defaults |
| 892 | with 7 strands. |
| 893 | |
| 894 | ### 14.2 Custom profile (a closed curve, live) |
| 895 | |
| 896 | - `profile` gains `custom`, with a hidden `profile_curve` (the curve's dot), set by the |
| 897 | Shape panel's **Set profile** picker (pick a curve in the viewport; Cablerator's A). |
| 898 | - **The 2D outline**, a pure function `CurveMath.profile_outline(curve)`: |
| 899 | - sample the closed curve (4 samples per segment, so a `vector`-handle corner is exact); |
| 900 | - its plane: Newell normal of the samples, origin = their centroid, u = the direction |
| 901 | to the first sample, v = normal × u (deterministic, no raycast); |
| 902 | - the 2D convex hull (a CONCAVE outline becomes its hull, as BoxCutter's custom |
| 903 | footprint does), collinear points dropped, at most 24 vertices (beyond that, every |
| 904 | other one is dropped until it fits: a hull of two 24-gons stays well under the plane |
| 905 | count Jolt refused at 63); |
| 906 | - normalised so the farthest vertex is 1: `radius` keeps meaning half the width, and |
| 907 | the taper and caps work unchanged. |
| 908 | - The ring = the outline placed in (n, b) at c, × r. |
| 909 | - **Live:** `_ix_followed_curve` returns BOTH `params.curve` and `params.profile_curve` |
| 910 | (it becomes a list), so an edit of the profile curve re-folds every cable using it, |
| 911 | and no other curve does. Flatten remaps `profile_curve` as it does `curve`. |
| 912 | - Profile curve removed, open, or under 3 distinct points: the cable folds as `round` |
| 913 | (it does not vanish); undo brings the curve back and the profile with it. |
| 914 | - X on a cable never removes its profile curve (only its own path curve, as today). |
| 915 | |
| 916 | ### 14.3 Drop to the ground (`builder.drop_cable`) |
| 917 | |
| 918 | Cablerator's Drop the Cable, adapted: |
| 919 | - **Acts on** the selected cables' curves and selected curves. One gesture: one |
| 920 | `curve.set {points}` per curve. |
| 921 | - **Which points:** all but an open curve's two ends (they stay on their walls). A closed |
| 922 | curve drops every point. |
| 923 | - **Step first** (the tool's one value, `step`, default 1 m, 0 = off): spans longer than |
| 924 | `step` get interior points first (at even arc length, CurveMath.sample), so the cable can |
| 925 | follow the ground rather than bridge it. Cablerator's Add Points. |
| 926 | - **Each point:** a physics ray straight down (−Y), 200 m, players excluded, as |
| 927 | `_crosshair_hit`. A hit INSIDE the cable's own pieces (within FaceKit.ON_PLANE of all |
| 928 | of one piece's planes) is skipped: the ray restarts 1 mm below it. On a hit, the point |
| 929 | moves to `hit + hit_normal × r_here` (the cable's radius × the point's radius; 0 for a |
| 930 | bare curve) and its `n` becomes the hit normal. A miss leaves the point where it was. |
| 931 | - **Handles:** `auto` / `vector` points re-derive from their new neighbours. `aligned` / |
| 932 | `free` handles are projected onto the hit plane, keeping their length (Cablerator). |
| 933 | - The tool is modal (F3) only for `step`; Enter writes. Adjust Last re-runs it. |
| 934 | |
| 935 | ### 14.4 The edge set (edge mode) |
| 936 | |
| 937 | - `_sel_edges: Array[Dictionary]` = `{b: brush key, p: [plane key, plane key]}` (the pair |
| 938 | sorted), durable like `_sel_faces`. Plain click restarts it; Shift toggles; it clears |
| 939 | outside edge mode. The active edge stays `_sel_edge_a/_b` for the existing tools. |
| 940 | - **Resolving an edge** never needs the pick: its endpoints are the brush's corners lying |
| 941 | on BOTH planes (FaceKit.corners, ON_PLANE). An edge whose planes no longer meet in a |
| 942 | segment drops out of the set. |
| 943 | - The highlight draws every edge in the set. |
| 944 | |
| 945 | ### 14.5 Cables from edges (`builder.cable_from_edges`) |
| 946 | |
| 947 | - **Chains:** edges whose endpoints coincide (ON_PLANE) join into chains; a chain that |
| 948 | returns to its start is closed. At a vertex where 3+ set edges meet the chain stops |
| 949 | (Cablerator makes one spline per path; we make one cable per chain). |
| 950 | - **The tool** (modal, keys from Cablerator's from-edges where free in the tool): |
| 951 | - P — points at Vertices / Centres (edge midpoints, which rounds corners off); |
| 952 | - O — offset: each point moves along its outward normal = the mean of the incident |
| 953 | faces' normals at it, normalised; default = the radius, so the cable sits ON the edge |
| 954 | rather than half inside; |
| 955 | - H — corners Round (`auto`) / Sharp (`vector`); an open chain's two ends are always |
| 956 | `vector`, so they run straight off the end instead of turning out along the normal |
| 957 | (the auto rule for a cable placed on a wall); |
| 958 | - S — radius, and the cable's profile params as Add → Cable has. |
| 959 | - Enter = ONE gesture: a curve + cable per chain (n stored = the outward normal). The new |
| 960 | cables are selected. Adjust Last re-runs it. |
| 961 | - Refused, with a status line: an empty edge set. |
| 962 | |
| 963 | ### 14.6 Bundle between two face sets (`builder.cable_bundle`) |
| 964 | |
| 965 | Cablerator's Create a Cable Mass: |
| 966 | - **Input:** the face set (`_sel_faces`, Shift-click across brushes). Its ISLANDS are the |
| 967 | connected components by shared corners (ON_PLANE). Exactly two, or it refuses ("select |
| 968 | exactly 2 face islands, found N"), as Cablerator does. |
| 969 | - **End points** (Pcg32, `seed`): |
| 970 | - R on (default): a random point on the island — a face picked by area, then a |
| 971 | triangle of its fan by area, then a uniform point in it; |
| 972 | - R off: face centres, cycled (cable i uses centre i mod faces). |
| 973 | - Cable i runs from point A_i to point B_i; `n` = that face's normal. |
| 974 | - **Shape of each cable:** both ends `free`: the end handle leaves along n with length |
| 975 | `tension` × chord / 3, each end's tension × (1 + `tension_jitter` × u), u ∈ [−1, 1] from |
| 976 | the same Pcg32. `slack` hangs it with `CurveOperator.hung` (interior points). |
| 977 | - **Tool values** (tool-local keys, BoxCutter's rule): Z count (1–64, default 8), |
| 978 | D tension (1.0), N tension jitter (0.3), S radius, R random points, Q a new seed, |
| 979 | [ ] slack, plus the profile params. |
| 980 | - **Enter** = ONE gesture: N × (`curve.create` + `shape.create {cable}`), then |
| 981 | `col.create "Bundle"` (under Scene) and `col.link` of the N `g:<shape dot>`. The bundle |
| 982 | is selected. Adjust Last re-runs with the same seed. |
| 983 | - The map's `add_cable` splits into `_record_cable(points, closed, params)` (records only) |
| 984 | and the gesture around it, so from-edges and the bundle write many cables in one |
| 985 | gesture. |
| 986 | |
| 987 | ### 14.7 Menus and keys |
| 988 | |
| 989 | Every command in a menu and on a key (standing rule): |
| 990 | - Add ▸ Cable ▸ **From edges** and **Bundle**; Curve ▸ **Drop to ground**; the Q menu on |
| 991 | a cable or curve offers Drop. |
| 992 | - Keys picked at build time against `keymap_gate` (no collisions, Blender meaning first) |
| 993 | and recorded here. |
| 994 | |
| 995 | ### 14.8 Order and gates (each red first) |
| 996 | |
| 997 | 1. Strands / spread / twist: `gate_cable_gen` S-checks — strands × segments pieces, each |
| 998 | convex; strand centres at spread · r; twist turns strand 0 by twist · s; **strands = 1 |
| 999 | is byte-identical to before**. |
| 1000 | 2. Custom profile: `gate_cable_profile` — a square closed curve gives the same pieces as |
| 1001 | `square` (after normalising); an L outline gives its hull; editing the profile curve |
| 1002 | re-folds the cable and an unrelated curve does not; profile removed → round; undo; flatten |
| 1003 | remaps it. |
| 1004 | 3. Drop: `gate_cable_drop` — a cable across a floor with a box on it: interior points land |
| 1005 | at surface + r with n = the surface normal; ends unchanged; the cable's own pieces are |
| 1006 | skipped; a miss stays; `step` adds points; one Ctrl+Z. |
| 1007 | 4. Edge set: `gate_edge_set` — Shift-click 3 edges of two brushes; the set survives a |
| 1008 | re-fold; endpoints resolve from the plane pair; plain click restarts. |
| 1009 | 5. From edges: `gate_cable_edges` — a box top's 4 edges → one CLOSED cable of 4 points; |
| 1010 | two disjoint edges → two cables; Centres; offset along the mean normal; one gesture. |
| 1011 | 6. Bundle: `gate_cable_bundle` — two islands → N cables; the same seed gives the same |
| 1012 | points; every end lies on its island's face (on the plane, inside the polygon) and |
| 1013 | leaves along its normal; the collection holds the N; one Ctrl+Z; 1 or 3 islands are |
| 1014 | refused. |
| 1015 | 7. Measure: pieces and full-fold time for a 20-cable bundle and a 7-strand 10 m cable |
| 1016 | (the §8 perf item: 500 stacked objects ≈ 1.7 s per edit). |