Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/lib/components/PortTooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script lang="ts">
import type { InterfaceTemplate, InterfaceType } from "$lib/types";
import { getPortTooltipState } from "$lib/stores/portTooltip.svelte";
import { inferDirection } from "$lib/utils/port-utils";
import { inferDirection, deriveGender } from "$lib/utils/port-utils";

// Get reactive tooltip state from store
const tooltipState = $derived(getPortTooltipState());
Expand Down Expand Up @@ -68,6 +68,17 @@
return direction ? DIRECTION_LABELS[direction] : null;
}

const GENDER_LABELS = { male: "Male", female: "Female" } as const;

// Gender label from the connector convention and the (explicit or
// inferred) direction; null hides the row for underivable connectors.
function getGenderLabel(port: InterfaceTemplate): string | null {
const direction =
port.direction ?? inferDirection(port.type, port.mgmt_only);
const gender = deriveGender(port.type, direction);
return gender ? GENDER_LABELS[gender] : null;
}

// Get PoE label
function getPoELabel(
poeMode?: "pd" | "pse",
Expand All @@ -88,12 +99,16 @@
</script>

{#if visible && port}
{@const genderLabel = getGenderLabel(port)}
<div class="port-tooltip" role="tooltip" style="left: {x}px; top: {y}px;">
<div class="port-tooltip-name">{port.label ?? port.name}</div>
<div class="port-tooltip-type">{getTypeLabel(port.type)}</div>
{#if getDirectionLabel(port)}
<div class="port-tooltip-direction">{getDirectionLabel(port)}</div>
{/if}
Comment thread
githappens marked this conversation as resolved.
{#if genderLabel}
<div class="port-tooltip-gender">{genderLabel}</div>
{/if}
{#if port.mgmt_only}
<div class="port-tooltip-badge mgmt">Management Only</div>
{/if}
Expand Down Expand Up @@ -149,7 +164,8 @@
font-size: var(--font-size-xs);
}

.port-tooltip-direction {
.port-tooltip-direction,
.port-tooltip-gender {
color: var(--colour-text-muted-inverse, rgba(255, 255, 255, 0.7));
font-size: var(--font-size-xs);
}
Expand Down
34 changes: 34 additions & 0 deletions src/lib/utils/port-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,40 @@ export function inferDirection(
return "bidirectional";
}

/** Connector gender for display. Computed only, never stored (spike #1927). */
export type ConnectorGender = "male" | "female";

/**
* Derive the connector gender for a port from its type and resolved
* direction, for connectors with a strong convention (spike #1927,
* Finding 6):
* - XLR-3 follows AES14 (signal flows out of male pins) so an output port is
* male and an input port is female; XLR-5 follows the same convention.
* Without a resolved in/out direction the gender is not derivable.
* - Speakon chassis connectors are male regardless of direction (the mating
* cable end is female); a port models the chassis side.
* - dmx-xlr is deliberately not derived: DMX512 (ANSI E1.11) reverses the
* AES14 convention (transmitters are female), so the XLR rule would
* mislead here.
* Ambiguous connectors (TRS, TS, RCA, ...) return undefined.
*/
export function deriveGender(
type: string,
direction?: PortDirection,
): ConnectorGender | undefined {
switch (type) {
case "xlr-3":
case "xlr-5":
if (direction === "output") return "male";
if (direction === "input") return "female";
return undefined;
case "speakon":
return "male";
default:
return undefined;
}
}

/**
* Instantiate ports from a DeviceType's interface templates
* Creates PlacedPort instances with stable UUIDs for each interface
Expand Down
35 changes: 35 additions & 0 deletions src/tests/derive-gender.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { deriveGender } from "$lib/utils/port-utils";

describe("deriveGender", () => {
it("derives XLR gender per AES14 from direction", () => {
expect(deriveGender("xlr-3", "output")).toBe("male");
expect(deriveGender("xlr-3", "input")).toBe("female");
expect(deriveGender("xlr-5", "output")).toBe("male");
expect(deriveGender("xlr-5", "input")).toBe("female");
});

it("returns undefined for XLR without a resolved direction", () => {
expect(deriveGender("xlr-3")).toBeUndefined();
expect(deriveGender("xlr-3", "bidirectional")).toBeUndefined();
});

it("derives Speakon chassis gender regardless of direction", () => {
expect(deriveGender("speakon")).toBe("male");
expect(deriveGender("speakon", "input")).toBe("male");
expect(deriveGender("speakon", "output")).toBe("male");
});

it("returns undefined for ambiguous or non-AV connectors", () => {
expect(deriveGender("trs-1-4")).toBeUndefined();
expect(deriveGender("ts-1-4")).toBeUndefined();
expect(deriveGender("rca")).toBeUndefined();
expect(deriveGender("hdmi")).toBeUndefined();
expect(deriveGender("1000base-t")).toBeUndefined();
});

it("does not derive dmx-xlr (DMX512 reverses the AES14 convention)", () => {
expect(deriveGender("dmx-xlr", "output")).toBeUndefined();
expect(deriveGender("dmx-xlr", "input")).toBeUndefined();
});
});