From 8be7198f95c7156ac9066db35ffe5401da37e4df Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 2 Jun 2026 15:04:11 +0100 Subject: [PATCH] PlaceholderTagAdder: Allow insertion of multiple vf positions --- src/components/PlaceholderTagAdder.vue | 124 ++++++++++++++++++------- src/components/TagsByCategories.vue | 4 + 2 files changed, 96 insertions(+), 32 deletions(-) diff --git a/src/components/PlaceholderTagAdder.vue b/src/components/PlaceholderTagAdder.vue index 1769161..0a20843 100644 --- a/src/components/PlaceholderTagAdder.vue +++ b/src/components/PlaceholderTagAdder.vue @@ -3,12 +3,24 @@ import { ref } from 'vue'; import { GF, StaticTagging, VariableTagging } from '../models'; import type { Location } from '../models'; +interface AxisPosition { + axisValue: number; + score: number; +} + interface AxisSpec { axisName: string; - minAxis: number; - maxAxis: number; - minScore: number; - maxScore: number; + positions: AxisPosition[]; +} + +function makePositions(count: number): AxisPosition[] { + const n = Math.max(2, Math.floor(count || 2)); + const positions: AxisPosition[] = []; + for (let i = 0; i < n; i++) { + const t = i / (n - 1); + positions.push({ axisValue: 0, score: Math.round(t * 100) }); + } + return positions; } const props = defineProps({ @@ -24,28 +36,47 @@ const mode = ref<'static' | 'variable'>('static'); const selectedCategories = ref([]); const placeholderScore = ref(0); const overwriteExisting = ref(false); +const onlyReplaceExisting = ref(false); const axisSpecs = ref([]); function addAxisSpec() { - axisSpecs.value.push({ axisName: '', minAxis: 0, maxAxis: 0, minScore: 0, maxScore: 100 }); + axisSpecs.value.push({ axisName: '', positions: makePositions(2) }); } function removeAxisSpec(idx: number) { axisSpecs.value.splice(idx, 1); } +function setPositionCount(spec: AxisSpec, event: Event) { + const count = (event.target as HTMLInputElement).valueAsNumber; + const n = Math.max(2, Math.floor(count || 2)); + while (spec.positions.length < n) { + spec.positions.push({ axisValue: 0, score: 0 }); + } + if (spec.positions.length > n) { + spec.positions.splice(n); + } +} + +function removePosition(spec: AxisSpec, idx: number) { + if (spec.positions.length <= 2) return; + spec.positions.splice(idx, 1); +} + function submitStatic() { for (const categoryName of selectedCategories.value) { const tag = props.gf.tags[categoryName]; if (!tag) continue; for (const family of props.gf.families) { if (family.hasTagging(categoryName)) { - if (overwriteExisting.value) { + if (onlyReplaceExisting.value || overwriteExisting.value) { const existing = family.taggings.find(t => t.tag.name === categoryName); if (existing) family.removeTagging(existing); } else { continue; } + } else if (onlyReplaceExisting.value) { + continue; } family.taggings.push(new StaticTagging(family, tag, placeholderScore.value)); } @@ -62,25 +93,41 @@ function submitVariable() { return axisSpecs.value.every(spec => { const axis = family.axis(spec.axisName); if (!axis) return false; - return axis.min <= spec.minAxis && axis.max >= spec.maxAxis; + const axisValues = spec.positions.map(p => p.axisValue); + const minAxis = Math.min(...axisValues); + const maxAxis = Math.max(...axisValues); + return axis.min <= minAxis && axis.max >= maxAxis; }); }); for (const family of matchingFamilies) { + // If we're replacing a static tagging, inherit its score at the default + // location (wght=400) so the variable tagging keeps the existing value there. + let inheritedDefaultScore: number | null = null; if (family.hasTagging(categoryName)) { - if (overwriteExisting.value) { + if (onlyReplaceExisting.value || overwriteExisting.value) { const existing = family.taggings.find(t => t.tag.name === categoryName); - if (existing) family.removeTagging(existing); + if (existing) { + if (existing instanceof StaticTagging && existing.score !== null) { + inheritedDefaultScore = existing.score; + } + family.removeTagging(existing); + } } else { continue; } + } else if (onlyReplaceExisting.value) { + continue; } const scores: { location: Location; score: number }[] = []; for (const spec of axisSpecs.value) { - scores.push( - { location: { [spec.axisName]: spec.minAxis }, score: spec.minScore }, - { location: { [spec.axisName]: spec.maxAxis }, score: spec.maxScore } - ); + for (const pos of spec.positions) { + const inherit = inheritedDefaultScore !== null && spec.axisName === 'wght' && pos.axisValue === 400; + scores.push({ + location: { [spec.axisName]: pos.axisValue }, + score: inherit ? inheritedDefaultScore! : pos.score + }); + } } family.taggings.push(new VariableTagging(family, tag, scores)); } @@ -102,7 +149,8 @@ function submitVariable() {

Categories

- + +
@@ -111,13 +159,22 @@ function submitVariable() {
-
- - - - - - +
+
+ + + + + +
+
+ + + + + +

@@ -155,21 +212,24 @@ function submitVariable() { overflow: auto; } -.axis-spec-row { - display: grid; - grid-template-columns: auto 1fr; - column-gap: 8px; - row-gap: 4px; - align-items: center; +.axis-spec { + border: 1px solid #ddd; + border-radius: 6px; + padding: 8px 12px; margin-bottom: 12px; } -.axis-spec-row label { - text-align: right; +.axis-spec-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; } -.axis-spec-row button { - grid-column: 1 / -1; - justify-self: start; +.position-row { + display: flex; + align-items: center; + gap: 8px; + margin: 4px 0 4px 16px; } diff --git a/src/components/TagsByCategories.vue b/src/components/TagsByCategories.vue index 70560e4..01e99b3 100644 --- a/src/components/TagsByCategories.vue +++ b/src/components/TagsByCategories.vue @@ -53,6 +53,10 @@ const filteredTaggings: ComputedRef = computed(() => { const sortScore = (t: Tagging) => { if ('scores' in t) { const scores = t.scores.map(s => s.score); + if (scores.length === 0) return 0; + // Rank each variable tagging by the extreme of its whole set of points + // (including any midpoints): its peak when sorting high→low, its valley + // when sorting low→high. return reverseTags.value ? Math.min(...scores) : Math.max(...scores); } return t.score || 0;