Skip to content

Commit 7aa9654

Browse files
committed
feat(language): editor support for timing modes + deprecation hints
1 parent c7a077b commit 7aa9654

7 files changed

Lines changed: 73 additions & 12 deletions

File tree

editors/vscode/syntaxes/posecode.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
},
4646
"constants": {
4747
"name": "constant.language.posecode",
48-
"match": "\\b(ease-in-out|ease-in|ease-out|linear|neutral|standing|plank|hands|feet|humanoid)\\b"
48+
"match": "\\b(flow|settle|drive|snap|linear|ease-in-out|ease-in|ease-out|neutral|standing|plank|hands|feet|humanoid)\\b"
4949
},
5050
"numbers": {
5151
"name": "constant.numeric.posecode",

packages/posecode-language/src/completion.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
POSES,
1111
EFFECTORS,
1212
REACH_EFFECTORS,
13-
EASINGS,
13+
MODES,
1414
JOINT_NAMES,
1515
ACTION_NAMES,
1616
TOP_KEYWORDS,
@@ -80,7 +80,7 @@ export function getCompletions(
8080
case "pose":
8181
return POSES.map((p) => item(p, "pose"));
8282
case "easing":
83-
return EASINGS.map((e) => item(e, "easing"));
83+
return MODES.map((e) => item(e, "easing"));
8484
case "effector":
8585
return EFFECTORS.map((e) => item(e, "effector"));
8686
case "reach-effector":

packages/posecode-language/src/diagnostics.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
*/
66

77
import { parse, boneType } from "posecode-parser";
8+
import { LEGACY_MODE_ALIASES } from "./vocab.js";
89

9-
export type Severity = "error" | "warning";
10+
export type Severity = "error" | "warning" | "hint";
1011

1112
export interface Diagnostic {
1213
/** 1-based source line. */
@@ -36,5 +37,21 @@ export function getDiagnostics(text: string): Diagnostic[] {
3637
});
3738
}
3839

40+
// Deprecation hints: legacy easing names still parse (via aliases) but nudge
41+
// authors toward the canonical timing modes. Scanned lexically so the hint
42+
// survives even when the rest of the document has errors.
43+
const lines = text.split(/\r?\n/);
44+
lines.forEach((lineText, idx) => {
45+
const m = /^\s*step\s+"[^"]*"\s+[0-9.]+s\s+([\w-]+)\s*:/.exec(lineText);
46+
const tok = m?.[1];
47+
if (tok && tok !== "linear" && tok in LEGACY_MODE_ALIASES) {
48+
diagnostics.push({
49+
line: idx + 1,
50+
severity: "hint",
51+
message: `"${tok}" is deprecated; use "${LEGACY_MODE_ALIASES[tok]}"`,
52+
});
53+
}
54+
});
55+
3956
return diagnostics;
4057
}

packages/posecode-language/src/hover.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { expandJoint, romFor, boneType } from "posecode-parser";
88
import {
99
JOINT_NAMES,
1010
ACTION_NAMES,
11-
EASINGS,
11+
MODES,
1212
KINDS,
1313
POSES,
1414
KEYWORD_DOCS,
@@ -73,8 +73,8 @@ export function getHover(
7373
if (keywordDoc) return md(`**${token}**: ${keywordDoc}`);
7474
if (KINDS.includes(token)) return md(`Movement kind **${token}**.`);
7575
if (POSES.includes(token)) return md(`Start pose **${token}**.`);
76-
if ((EASINGS as readonly string[]).includes(token)) {
77-
return md(`Easing **${token}**.`);
76+
if ((MODES as readonly string[]).includes(token)) {
77+
return md(`**${token}**${KEYWORD_DOCS[token] ?? "timing mode"}`);
7878
}
7979
return null;
8080
}

packages/posecode-language/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ export {
2222
JOINT_NAMES,
2323
ACTION_NAMES,
2424
EASINGS,
25+
MODES,
2526
} from "./vocab.js";

packages/posecode-language/src/vocab.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44
* drift from what the language actually accepts.
55
*/
66

7-
import { JOINT_NAMES, ACTION_NAMES, EASINGS, EFFECTOR_NAMES } from "posecode-parser";
7+
import {
8+
JOINT_NAMES,
9+
ACTION_NAMES,
10+
EASINGS,
11+
MODES,
12+
LEGACY_MODE_ALIASES,
13+
EFFECTOR_NAMES,
14+
} from "posecode-parser";
815

9-
export { JOINT_NAMES, ACTION_NAMES, EASINGS };
16+
export { JOINT_NAMES, ACTION_NAMES, EASINGS, MODES, LEGACY_MODE_ALIASES };
1017

1118
/** Movement kinds in the header (`posecode <kind> "..."`). */
1219
export const KINDS = ["exercise", "stretch", "posture"];
@@ -35,7 +42,12 @@ export const KEYWORD_DOCS: Record<string, string> = {
3542
pose: "Sets the starting pose: `pose start = standing | neutral | plank | supine | prone | seated`.",
3643
start: "Used in `pose start = <pose>`.",
3744
clip: 'Optional mocap clip: `clip "walk"`. A renderer with a matching retargeted animation plays it crossfaded over the procedural pose; others ignore it.',
38-
step: 'A movement phase: `step "<name>" <Ns> <easing>:`.',
45+
step: 'A movement phase: `step "<name>" <Ns> <mode>:` where mode is flow | settle | drive | snap | linear.',
46+
flow: "Timing mode: pass through this pose with continuous velocity (flowing motion).",
47+
settle: "Timing mode: decelerate to a genuine rest at this pose (a deliberate pause).",
48+
drive: "Timing mode: accelerate from rest — the concentric effort of a rep.",
49+
snap: "Timing mode: fast, near-immediate arrival — an accent.",
50+
linear: "Timing mode: constant velocity — intentionally mechanical.",
3951
repeat: "How many times the movement loops.",
4052
"ground-lock": "Pins effectors (hands / feet) to the floor for this phase.",
4153
reach:

packages/posecode-language/test/language.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ describe("getCompletions", () => {
6363
);
6464
});
6565

66-
it("suggests easings inside a step header", () => {
66+
it("suggests timing modes inside a step header", () => {
6767
expect(onLine(' step "y" 2s ', 14)).toEqual(
68-
expect.arrayContaining(["ease-in", "linear"]),
68+
expect.arrayContaining(["flow", "settle", "linear"]),
6969
);
7070
});
7171

@@ -104,3 +104,34 @@ describe("getHover", () => {
104104
expect(getHover(" ", 0, 2)).toBeNull();
105105
});
106106
});
107+
108+
describe("timing modes (L2)", () => {
109+
const modeDoc = ['posecode exercise "x"', " rig humanoid", " step \"A\" 1s "].join("\n");
110+
111+
it("completes timing modes after a step duration", () => {
112+
const lineText = modeDoc.split("\n")[2]!;
113+
const got = getCompletions(modeDoc, 2, lineText.length).map((i) => i.label);
114+
expect(got).toEqual(
115+
expect.arrayContaining(["flow", "settle", "drive", "snap", "linear"]),
116+
);
117+
});
118+
119+
it("hovers a mode", () => {
120+
const doc = 'posecode exercise "x"\n rig humanoid\n step "A" 1s flow:';
121+
const line = doc.split("\n")[2]!;
122+
const h = getHover(doc, 2, line.indexOf("flow") + 1);
123+
expect(h?.contents.toLowerCase()).toContain("flow");
124+
});
125+
126+
it("flags a deprecated easing name with a hint", () => {
127+
const doc = [
128+
'posecode exercise "x"',
129+
" rig humanoid",
130+
' step "A" 1s ease-in-out:',
131+
" knees: flex 10",
132+
].join("\n");
133+
const diags = getDiagnostics(doc);
134+
const hint = diags.find((d) => d.severity === "hint");
135+
expect(hint?.message).toContain("settle");
136+
});
137+
});

0 commit comments

Comments
 (0)