feat(datums): make ellipses user-flaggable as the primary datum
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

Previously only rectangles (`isAxisReference`) and lines (`axisRole`)
could be flagged as the gauge primary; ellipses fell through to the
auto-pick by type rank. Add `EllipseDatum.isPrimary` so the user can
pick a circle/ellipse as the primary datum and override the type-rank
heuristic. Mutual exclusion with the existing rect/line flags is
enforced through `setAxisRole`. Adds a "Primary reference" toggle in
the ellipse datum row and a "primary" badge next to the datum name so
the active primary is visible at a glance.
This commit is contained in:
Samuel Prevost 2026-04-30 23:39:26 +02:00
parent 590ba16596
commit c2f7bf0df2
4 changed files with 53 additions and 6 deletions

View File

@ -96,6 +96,7 @@ function axisBadge(datum: Datum): string | null {
if (datum.type === "rectangle" && datum.isAxisReference) return "axis"
if (datum.type === "line" && datum.axisRole === "x") return "+x"
if (datum.type === "line" && datum.axisRole === "y") return "+y"
if (datum.type === "ellipse" && datum.isPrimary) return "primary"
return null
}
</script>
@ -407,6 +408,35 @@ function axisBadge(datum: Datum): string | null {
</button>
</div>
</div>
<div
v-else-if="datum.type === 'ellipse'"
class="flex items-center justify-between"
>
<Label class="text-xs">Primary reference</Label>
<label
class="flex cursor-pointer items-center gap-1.5"
@click.stop
>
<input
type="checkbox"
class="accent-primary"
:checked="datum.isPrimary ?? false"
@change="
(e) =>
store.setAxisRole(
datum.id,
(e.target as HTMLInputElement)
.checked
? 'ellipse'
: null,
)
"
/>
<span class="text-xs text-muted-foreground"
>Use</span
>
</label>
</div>
<!-- Confidence -->
<div>

View File

@ -268,7 +268,10 @@ type Primary =
function pickPrimary(datums: Datum[]): Primary {
if (datums.length === 0) throw new Error("No datums provided.")
// User-flagged world-axis reference wins regardless of type priority.
// User-flagged primary wins regardless of type priority. Rect's
// `isAxisReference` and line's `axisRole` carry axis semantics on top
// of "primary"; ellipse's `isPrimary` is a pure primary flag (ellipses
// don't define axis directions on their own).
for (const d of datums) {
if (d.type === "rectangle" && d.isAxisReference) {
return { kind: "rect", datum: d }
@ -276,6 +279,9 @@ function pickPrimary(datums: Datum[]): Primary {
if (d.type === "line" && d.axisRole) {
return { kind: "line", datum: d }
}
if (d.type === "ellipse" && d.isPrimary) {
return { kind: "ellipse", datum: d }
}
}
const typeRank = (d: Datum): number =>

View File

@ -103,14 +103,15 @@ export const useAppStore = defineStore("app", () => {
}
}
/** Set (or clear) the world-axis role on a datum, enforcing that at
/** Set (or clear) the gauge-primary role on a datum, enforcing that at
* most one datum holds the role at a time.
* `role`: "rect" rectangle.isAxisReference = true
* "x"/"y" line.axisRole = "x"|"y"
* null clear the role on `id` (no-op if it wasn't set). */
* `role`: "rect" rectangle.isAxisReference = true
* "x"/"y" line.axisRole = "x"|"y"
* "ellipse" ellipse.isPrimary = true
* null clear the role on `id` (no-op if it wasn't set). */
function setAxisRole(
id: string,
role: "rect" | "x" | "y" | null,
role: "rect" | "x" | "y" | "ellipse" | null,
) {
// Clear any existing flag on other datums.
for (let i = 0; i < datums.value.length; i++) {
@ -120,6 +121,8 @@ export const useAppStore = defineStore("app", () => {
datums.value[i] = { ...d, isAxisReference: false }
} else if (d.type === "line" && d.axisRole) {
datums.value[i] = { ...d, axisRole: null }
} else if (d.type === "ellipse" && d.isPrimary) {
datums.value[i] = { ...d, isPrimary: false }
}
}
const idx = datums.value.findIndex((d) => d.id === id)
@ -131,6 +134,8 @@ export const useAppStore = defineStore("app", () => {
datums.value[idx] = { ...target, isAxisReference: false }
} else if (target.type === "line") {
datums.value[idx] = { ...target, axisRole: null }
} else {
datums.value[idx] = { ...target, isPrimary: false }
}
return
}
@ -138,6 +143,8 @@ export const useAppStore = defineStore("app", () => {
datums.value[idx] = { ...target, isAxisReference: true }
} else if ((role === "x" || role === "y") && target.type === "line") {
datums.value[idx] = { ...target, axisRole: role }
} else if (role === "ellipse" && target.type === "ellipse") {
datums.value[idx] = { ...target, isPrimary: true }
}
}

View File

@ -49,6 +49,10 @@ export interface EllipseDatum {
diameterMm: number
confidence: 1 | 2 | 3 | 4 | 5
label: string
/** When true, this ellipse is the gauge primary overrides the
* type-rank auto-pick. Mutually exclusive with `RectDatum.isAxisReference`
* and `LineDatum.axisRole`; setting any of those clears the others. */
isPrimary?: boolean
}
export type Datum = RectDatum | LineDatum | EllipseDatum