Wire rails - #575
Merged
Merged
Conversation
The Drop fixture (renamed from Hole Drop) continues two selected rails and turns them vertically down. Its offset now moves the drop inward, shortening the two attached rails, and the collider extends their two floor faces straight down at the drop point while leaving the outer rails full length. Fixtures can also solder to the rails they touch. Includes hardening from review: consistent drop validation at the offset attachment across trims, caps, colliders and inspector; conflict detection that accounts for other drop offsets and ignores non-generatable drops; offset clamped to the spline length; and open-spline guards.
Collider vertices are generated in the spline container's local space, which also positions the render mesh. Building the playfield matrix from the component transform ignored the child's offset, so colliders drifted away from the visible rail whenever the spline container was moved. Derive the matrix from the container's localToWorldMatrix instead, and skip the extra VpxToWorld conversion since the vertices are already in VPX units. Falls back to the component transform when no container exists yet.
P-ROC hardware rules become input-action wires (e.g. "Left Flipper" -> flipper main coil). Those wires only fired through the managed Input System (WirePlayer.HandleKeyInput), which is inactive while native input polling owns the keys. Under native input the switch reached the game but the coil never energized, so flippers did not move. Register each hardware-rule wire whose coil supports the simulation-thread dispatch path against its native input action, and fire it from the simulation-thread input loop in the same tick as the key. This keeps the low-latency path flippers need. Coils without a simulation-thread path are unaffected and stay on the main-thread wire path.
Add a Wire Rail entry to the toolbox next to Ramp, using the ramp_wires icon, that creates a WireRailComponent under the playfield.
Add a per-fixture "Enabled" toggle to every wire-rail fixture. Disabling hides the fixture from the render mesh only (own geometry, rail trims, end fitting and solder) by feeding only enabled fixtures to the render generator; colliders keep using the full fixture list and are unaffected. The toggle lives in the shared fixture header, so it applies to all types. Add inspector preview panels for the Drop and Drop Loop fixtures. The Drop preview also shows a stretch of the rails leading into the drop for context, and the Drop Loop preview uses a top-down projection so its loop is drawn in the plane it actually lies in.
The brace preview drew only the ring arc, unlike the cross-wire, V-brace and leg previews which also show their rails. Carry the rail offsets and radii the brace profile already computes through to the cross-section, and draw the wrapped wire rails behind the ring so the preview shows what the brace encircles.
Replace the drop loop's lateral/vertical offset with a single along-rail offset. The offset shortens the two rails the loop connects to so the loop attaches that far before the endpoint and the rails follow it, keeping it attached; the outer rails are untouched. Clamped to non-negative. Weld the loop to those rails so the junction reads as one continuous tube: fitted rail ends are left open (no flat cap), and each loop mouth ring is stitched to the rail's end ring with a triangle band that reuses the ring vertices. The rings are matched by the rotation (and, for the far mouth, the reversed winding) that best aligns them, and their normals are averaged so shading stays continuous across the seam. Also size the drop loop inspector warning box to its text so the conflict message is no longer clipped.
…seam Generalize the drop-loop seam weld to serve both endpoint fittings and wire the Drop into it, so each of its two rail tubes is stitched to the rail it attaches to instead of leaving an open ring. Seed each fitting tube's rings from the attached rail's mouth frame so the rings coincide with the rail's rings vertex-for-vertex; the tube edges then run straight through the junction rather than twisting across a mismatched band. The drop's two tubes and the loop's near mouth align exactly; the loop's far mouth keeps the ring-matched bridge (one tube cannot seed both ends).
The generated spline child carries the VPX-to-world rotation and inverse scale that every drag-point consumer relies on. Mark its transform NotEditable when it is created and whenever an existing child is resolved, so it cannot be nudged by hand in the inspector. Also bring the duplicate-spline test in line with the guard that never destroys a child still holding knots: an empty duplicate is removed, a duplicate with knots is kept and warned about.
Fixtures are now named after their shape: Brace -> Ring (WireRailRingFixture) Cross + Arms -> Cradle (WireRailCradleFixture) Cross Wire -> Rung (WireRailRungFixture) Stand -> Stand (WireRailStandFixture, was WireRailLegFixture) Drop Loop -> Hairpin (WireRailHairpinFixture) Drop -> Elbow (WireRailElbowFixture) Rail Trim -> Rail Trim Each class carries a MovedFrom attribute for its previous name so existing scenes migrate on load. The Elbow's attribute now points at WireRailDropFixture; the pre-branch WireRailHoleDropFixture name no longer migrates. The spline child is resolved through a new WireRailSplineComponent marker instead of by name, and its transform is locked in the inspector since it holds the VPX-to-world conversion. Legacy children are found by name once and hardened in place. The Add buttons in the Fixtures section are fixed-size icon buttons that wrap with the inspector width, grouped into supports and end fittings. The Scene view panel gets a title with the wire rail icon and the object name, a one-line summary of rails, layouts, fixtures and route length, a collider preview toggle, and while editing a status line with a Done button that returns to the Wire Rail, the grade action with the horizon icon, and the shortcuts as a small table.
Replace the single reference page with five author-facing pages under creators-guide/editor/wire-rails: an overview with the route, layouts, fixtures and generated geometry model, then one page each for the route, wire layouts, fixtures and generated geometry. The uid wire_rail_authoring is kept on the overview so existing links resolve. Screenshots referenced by the pages are not included yet.
With a layout selected, every other layout dims, the selected span gets a translucent glow under its wires and spine, and its cross-section is outlined at both ends of the span so the governed stretch is visible. The rendered tubes of the span are tinted in the selection color; the render generator now records each span's index range in the mesh so the editor can draw just those triangles. Double-headed arrow handles at the start of the selected span and at the start of the next one slide those layouts along the route, snapping to the nearest point on the spline through the same distance setter the inspector uses. Labels under the arrows are hidden while they are shown.
A layout's position can now be any distance along the route; only the first layout stays pinned to the start. The physical list is sorted by distance whenever a position changes and on load, and the display order is remapped so each "Layout N" keeps its layout. Previously a position was clamped between its physical neighbors, which made the order limit where a layout could go. SetLayoutDistance returns the layout's new physical index so the inspector and the scene-view arrows keep the selection on the moved layout. Duplicating a layout always places the copy halfway to the next layout, or halfway to the end of the route for the last one, instead of before it.
The start and exit tapers of the ball channel collider are gone: the component's six widening fields and their setter, the widening struct and the parameter threaded through collider sampling and the elbow endpoint profile, the inspector controls, and the seven tests that pinned them. The channel radius is the reference ball radius everywhere.
Solder Size 1 now produces what 0.8 did. The proportions were tuned at 1.0 and judged a touch heavy, so the default is scaled inside the generator rather than retuned. The render-mesh golden hash in the stress test follows.
Selecting a fixture panel highlights its centerline, fades the other fixtures, tints its rendered wire in the selection color and shows an arrow handle: supports slide along the route, hairpins and elbows set their offset from the endpoint. The render generator records one index range per fixture so the editor can tint a single fixture, and the component gains a route distance setter for supports and offset setters for the two end fittings. The mesh tint caches are keyed on the mesh generation count rather than the geometry version, which advances when a rebuild is requested while the rebuild itself is deferred in the editor; the old key could freeze a stale mesh under a new version.
Each layout and fixture sits in a bordered box with a header band and an even inset on both sides, so it is clear where one ends and the next begins. The solder settings share one line, the ring's Apply to All is the last row of its panel, and the elbow's attached rails show as disabled cutoff fields instead of blank rows. End fitting panels measure their help box from the actual message, so a long notice no longer overflows the frame.
Adds the inspector and Scene view screenshots the wire rail pages reference, updates the ones that changed with the inspector work, and tightens the fixture page's wording on how fixtures relate to layouts.
Wire rails now survive a .vpe round trip. Before, the writer dropped the component, its spline and the marker with a warning each, leaving a frozen mesh with no collider in the player. Two packables carry a rail. WireRailSplineComponent packs the route (knots, tangents, rotations, tangent modes, closed flag) and restores the spline container on its own imported child, which keeps the child's transform, mesh and material in the glTF like every other renderable. WireRailComponent packs settings, layouts with per-wire offsets, activation and transition curves, and the fixture list with a kind discriminator for all seven types; its physics material assets go through the asset table. The two unpacks coordinate so layouts and fixtures are only synchronized once the route exists, whichever half arrives first. The component adopts an existing spline child without a container instead of creating a second one (the editor import adds the component before unpacking), and keeps an imported material rather than overwriting it. The writer skips SplineContainer silently. The payload version is threaded into the restore path for future gating, documented on WireRailPackable. Tests cover a full pack/unpack of every field in both orders, an export followed by a runtime import that ends with a collidable rail, and the frozen version 1 payloads.
Greptile SummaryThe PR introduces Unity-native wire rails with spline-authored routes, configurable wire layouts and fixtures, generated render and collision geometry, packaging support, editor tooling, tests, and documentation. Changes since the previous review:
Confidence Score: 5/5The PR appears safe to merge; the previous findings are resolved and the latest compatibility and fixture-conflict fixes introduce no actionable regressions. The endpoint-conflict change consistently excludes non-generatable elbows from trim ownership, and the legacy spline loader preserves the AutoSmooth curvature produced by the original version 1 loading behavior. No outstanding findings remain.
|
| Filename | Overview |
|---|---|
| VisualPinball.Unity/VisualPinball.Unity/VPT/WireRail/WireRailGeometryGenerator.cs | Endpoint conflict detection now ignores elbows that cannot generate, consistently across rendering and collider generation. |
| VisualPinball.Unity/VisualPinball.Unity/VPT/WireRail/WireRailPackable.cs | Version 1 spline payloads now restore the Catmull-Rom tension used by the original loading path. |
| VisualPinball.Unity/VisualPinball.Unity.Test/VPT/WireRail/WireRailComponentTests.cs | Adds regression coverage ensuring an omitted elbow does not suppress a valid hairpin. |
| VisualPinball.Unity/VisualPinball.Unity.Test/VPT/WireRail/WireRailPackagingTests.cs | Adds compatibility assertions for version 1 AutoSmooth tension and route shape. |
Reviews (6): Last reviewed commit: "wire-rail: omitted elbows no longer conf..." | Re-trigger Greptile
…tch once Hardware-rule wire coils were registered on whichever SimulationThread existed at that moment: a rule installed before the component's Start(), or kept across a stop/start cycle, never reached native-input dispatch. The component now keeps the registrations at its own scope and applies them to every thread it creates. While native polling owns the keys, a coil on the simulation-thread path also received the managed Input System event, so one press could reach it twice. The wire player now skips the managed path for destinations the simulation thread dispatches, and only while native polling is active, so the managed path remains the fallback without native input. Registration reports whether the action is one the native poller can dispatch, so unmapped actions stay on the managed path.
…remains The same hardware rule installed twice yields two wire assignments sharing one native coil registration. Removing one of them unregistered the coil while the survivor kept suppressing its managed fallback, so the rule went dead. Unregistering now leaves the native registration in place as long as another assignment on the same action still routes to that coil.
Restoring a route with Spline.Add reset every AutoSmooth knot to the default tension, so a rail authored with custom tension changed shape on import. The spline payload is now version 2 with a per-knot tension, and restoring uses the tension-aware insert. Version 1 payloads load with the default tension they were authored with. The test compares evaluated positions along the route, not only knot positions.
Trim collection checked a hairpin's rail pair and conflicts but not whether both rails exist and are active at its offset attachment, which the fitting generator does check. An omitted hairpin could therefore still shorten its surviving rail and the collision channel. The attachment validation is now shared, mirroring the elbow.
Double-clicking the closing curve inserted the new knot at index 0, which moved the route origin while layouts and fixtures kept their distances from it, so everything slid along the route. The knot is appended after the last knot instead, which lies on the same curve and leaves knot 0 in place.
Skipping the managed path for a coil the simulation thread fires also skipped the coil's enabled state and CoilStatusChanged, which coil sounds and animations rely on. The wire player now publishes the state to the main-thread listeners through a status-only path on DeviceCoil that keeps the physics callbacks with the simulation thread, whichever side runs first.
A hairpin or elbow offset at or beyond the route length clamped the attachment to the opposite endpoint while trimming used the raw offset, so the fitting appeared at the wrong end over rails that had vanished from both the render mesh and the collider. Such a fitting is now not generatable: the shared predicates refuse it, the trim collection, profile generators and flat-cap handling all follow them, and the inspector says why.
… with The version 1 spline payload carried no tension and was restored through Spline.Add, which assigns the Catmull-Rom tension of one half. The fallback used Unity's default of one third instead, so a version 1 package loaded with a different curve. The golden-payload test now checks the restored tension and compares the evaluated route against the old restoration.
The conflict check counted an elbow's positive offset on a shared rail even when that elbow is not generated, so an omitted elbow could still suppress a valid hairpin at the same endpoint. The check now receives the route context and skips elbows that IsElbowGeneratable refuses, the same rule the trims and the geometry follow.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a Wire Rail component: habitrails, wire ramps and guides authored from a native Unity spline, with generated wire tubes, fixtures, and a ball channel collider fitted to the space the ball actually occupies.
How it works
Editor
Packaging
Wire rails survive a
.vperound trip. The spline child packs the route and restores its container on its own imported node, the component packs settings, layouts and fixtures, physics materials go through the asset table. Covered by pack/unpack, export-then-runtime-import and frozen version 1 payload tests.Documentation
New authoring guide under
creators-guide/editor/wire-rails/(overview, route, layouts, fixtures, generated geometry), with screenshots.Also on this branch
hit_switchpulse for cylindrical magnets with a generated collider.Not yet
No
.vpximport or export of wire rails, and no branching routes.