@lalalic/markcut 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +5 -1
  2. package/skills/markcut/SKILL.md +14 -17
  3. package/skills/markcut/docs/map-dynamic-camera.md +92 -8
  4. package/skills/markcut/docs/markdown-descriptive.md +1 -1
  5. package/skills/markcut/review.md +480 -0
  6. package/src/descriptive/compiler.ts +60 -1
  7. package/src/descriptive/dsl.ts +27 -7
  8. package/src/descriptive/markdown.ts +2 -1
  9. package/src/descriptive/resolve.test.ts +98 -0
  10. package/src/descriptive/resolve.ts +156 -12
  11. package/src/player/bundle/player.js +222961 -222169
  12. package/src/player/pipeline.mjs +255 -17
  13. package/src/schema/index.ts +4 -0
  14. package/src/types/Effect.tsx +12 -1
  15. package/src/types/Map.tsx +649 -75
  16. package/src/utils/directions.ts +101 -0
  17. package/src/utils/index.ts +11 -0
  18. package/src/utils/route-legs.ts +199 -0
  19. package/tests/dsl.test.ts +35 -0
  20. package/tests/evals/README.md +41 -0
  21. package/tests/evals/dataset.json +170 -0
  22. package/tests/evals/gen_dataset.py +63 -0
  23. package/tests/evals/metrics.py +70 -0
  24. package/tests/evals/openrouter_model.py +143 -0
  25. package/tests/evals/storyboard_app.py +55 -0
  26. package/tests/evals/test_storyboard.py +32 -0
  27. package/tests/fixtures/map-overlay.json +56 -0
  28. package/tests/fixtures/md/map-all-views.md +8 -1
  29. package/tests/fixtures/md/map-children.md +11 -0
  30. package/tests/fixtures/md/map-multimode.md +9 -0
  31. package/tests/fixtures/streetview-walk.json +36 -0
  32. package/tests/md-descriptive.test.ts +75 -0
  33. package/tests/render.test.ts +92 -0
  34. package/tests/route-legs.test.ts +178 -0
  35. package/tests/schema.test.ts +18 -0
  36. package/.vscode/settings.json +0 -3
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ haversineKm,
4
+ greatCirclePath,
5
+ makeSyntheticLeg,
6
+ modeEmoji,
7
+ routePositionAtLegs,
8
+ isSyntheticMode,
9
+ type RouteLeg,
10
+ } from "../src/utils/route-legs";
11
+
12
+ const SF = { lat: 37.7749, lng: -122.4194 };
13
+ const LA = { lat: 34.0522, lng: -118.2437 };
14
+ const PIER = { lat: 34.0094, lng: -118.4969 };
15
+
16
+ describe("haversineKm", () => {
17
+ it("is ~0 for identical points", () => {
18
+ expect(haversineKm(SF, SF)).toBeCloseTo(0, 6);
19
+ });
20
+
21
+ it("is ~560 km for SF→LA (great-circle)", () => {
22
+ expect(haversineKm(SF, LA)).toBeGreaterThan(500);
23
+ expect(haversineKm(SF, LA)).toBeLessThan(620);
24
+ });
25
+ });
26
+
27
+ describe("greatCirclePath", () => {
28
+ it("includes both endpoints and is monotonic in length", () => {
29
+ const path = greatCirclePath(SF, LA, 32);
30
+ expect(path.length).toBe(33);
31
+ expect(path[0]).toEqual(SF);
32
+ const last = path[path.length - 1]!;
33
+ expect(last.lat).toBeCloseTo(LA.lat, 4);
34
+ expect(last.lng).toBeCloseTo(LA.lng, 4);
35
+ });
36
+
37
+ it("handles identical endpoints without NaN", () => {
38
+ const path = greatCirclePath(SF, SF, 8);
39
+ expect(path.every((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))).toBe(true);
40
+ expect(path[0]).toEqual(SF);
41
+ });
42
+ });
43
+
44
+ describe("makeSyntheticLeg", () => {
45
+ it("builds a FLIGHT leg with a cruise-speed duration and single arc step", () => {
46
+ const leg = makeSyntheticLeg(SF, LA, "FLIGHT");
47
+ expect(leg.mode).toBe("FLIGHT");
48
+ expect(leg.durationSec).toBeGreaterThan(0);
49
+ // SF→LA ~559 km @ 850 km/h ≈ 0.66 h ≈ 2370 s
50
+ expect(leg.durationSec).toBeGreaterThan(2000);
51
+ expect(leg.durationSec).toBeLessThan(2700);
52
+ expect(leg.steps).toHaveLength(1);
53
+ expect(leg.steps[0]!.path.length).toBeGreaterThan(10);
54
+ });
55
+
56
+ it("builds a BOAT leg with a slower speed (longer duration for same distance)", () => {
57
+ const boat = makeSyntheticLeg(SF, LA, "BOAT");
58
+ const flight = makeSyntheticLeg(SF, LA, "FLIGHT");
59
+ expect(boat.mode).toBe("BOAT");
60
+ expect(boat.durationSec).toBeGreaterThan(flight.durationSec);
61
+ });
62
+ });
63
+
64
+ describe("isSyntheticMode / modeEmoji", () => {
65
+ it("flags FLIGHT and BOAT as synthetic", () => {
66
+ expect(isSyntheticMode("FLIGHT")).toBe(true);
67
+ expect(isSyntheticMode("BOAT")).toBe(true);
68
+ expect(isSyntheticMode("flight")).toBe(true);
69
+ expect(isSyntheticMode("DRIVING")).toBe(false);
70
+ expect(isSyntheticMode("WALKING")).toBe(false);
71
+ expect(isSyntheticMode(undefined)).toBe(false);
72
+ });
73
+
74
+ it("maps modes to emoji", () => {
75
+ expect(modeEmoji("FLIGHT")).toBe("✈️");
76
+ expect(modeEmoji("BOAT")).toBe("🚢");
77
+ expect(modeEmoji("WALKING")).toBe("🚶");
78
+ expect(modeEmoji("BICYCLING")).toBe("🚲");
79
+ expect(modeEmoji("TRANSIT")).toBe("🚌");
80
+ expect(modeEmoji("DRIVING")).toBe("🚗");
81
+ expect(modeEmoji(undefined)).toBe("📍");
82
+ });
83
+ });
84
+
85
+ describe("routePositionAtLegs", () => {
86
+ const makeLeg = (mode: string, durationSec: number, path: { lat: number; lng: number }[]): RouteLeg => ({
87
+ mode,
88
+ from: path[0]!,
89
+ to: path[path.length - 1]!,
90
+ durationSec,
91
+ steps: [{ path, durationSec }],
92
+ });
93
+
94
+ const straight = (a: { lat: number; lng: number }, b: { lat: number; lng: number }, n = 20) => {
95
+ const out: { lat: number; lng: number }[] = [];
96
+ for (let i = 0; i <= n; i++) {
97
+ const t = i / n;
98
+ out.push({ lat: a.lat + (b.lat - a.lat) * t, lng: a.lng + (b.lng - a.lng) * t });
99
+ }
100
+ return out;
101
+ };
102
+
103
+ const legs: RouteLeg[] = [
104
+ makeLeg("FLIGHT", 10, straight(SF, LA)),
105
+ makeLeg("DRIVING", 30, straight(LA, PIER)),
106
+ ];
107
+
108
+ it("returns null for no legs", () => {
109
+ expect(routePositionAtLegs([], 40, 0)).toBeNull();
110
+ });
111
+
112
+ it("starts at the first waypoint and ends at the last", () => {
113
+ const start = routePositionAtLegs(legs, 40, 0)!;
114
+ expect(start.lat).toBeCloseTo(SF.lat, 5);
115
+ expect(start.lng).toBeCloseTo(SF.lng, 5);
116
+ expect(start.mode).toBe("FLIGHT");
117
+
118
+ const end = routePositionAtLegs(legs, 40, 40)!;
119
+ expect(end.lat).toBeCloseTo(PIER.lat, 5);
120
+ expect(end.lng).toBeCloseTo(PIER.lng, 5);
121
+ expect(end.mode).toBe("DRIVING");
122
+ });
123
+
124
+ it("splits time proportionally to leg duration and reports the current leg mode", () => {
125
+ // leg durations 10 + 30 = 40 total; actionDuration 40s
126
+ // → flight leg occupies t∈[0,10], driving leg t∈[10,40]
127
+ const atFlightEnd = routePositionAtLegs(legs, 40, 9.99)!;
128
+ expect(atFlightEnd.mode).toBe("FLIGHT");
129
+
130
+ const atDrivingStart = routePositionAtLegs(legs, 40, 10.01)!;
131
+ expect(atDrivingStart.mode).toBe("DRIVING");
132
+ expect(atDrivingStart.lat).toBeCloseTo(LA.lat, 4);
133
+ });
134
+
135
+ it("mid-leg position tracks the leg's path (proportional timing)", () => {
136
+ // leg durations [10,30]; actionDuration 40. At t=20 the driving leg is
137
+ // (20-10)/30 = 1/3 of the way from LA → PIER (with point quantization).
138
+ const atThird = 1 / 3;
139
+ const mid = routePositionAtLegs(legs, 40, 20)!;
140
+ expect(mid.mode).toBe("DRIVING");
141
+ expect(mid.lat).toBeCloseTo(LA.lat + (PIER.lat - LA.lat) * atThird, 2);
142
+ expect(mid.lng).toBeCloseTo(LA.lng + (PIER.lng - LA.lng) * atThird, 2);
143
+ });
144
+
145
+ it("clamps past-the-end seconds to the destination", () => {
146
+ const end = routePositionAtLegs(legs, 40, 100)!;
147
+ expect(end.lat).toBeCloseTo(PIER.lat, 5);
148
+ expect(end.mode).toBe("DRIVING");
149
+ });
150
+
151
+ it("holds at a waypoint during a dwell window", () => {
152
+ const stops = [{ label: "LA", at: LA, mode: "FLIGHT", fromSec: 8, toSec: 12 }];
153
+ // Inside the dwell → pin holds at LA, not on the flight arc.
154
+ const during = routePositionAtLegs(legs, 40, 10, stops)!;
155
+ expect(during.mode).toBe("FLIGHT");
156
+ expect(during.lat).toBeCloseTo(LA.lat, 4);
157
+ expect(during.lng).toBeCloseTo(LA.lng, 4);
158
+
159
+ // At toSec the dwell is over and motion resumes (no longer holding at LA).
160
+ const resumed = routePositionAtLegs(legs, 40, 12, stops)!;
161
+ expect(resumed.mode).toBe("FLIGHT");
162
+ expect(resumed.lat).toBeGreaterThan(LA.lat);
163
+ });
164
+
165
+ it("compresses drive time around dwells (dwell removed from drive budget)", () => {
166
+ const stops = [{ label: "LA", at: LA, mode: "FLIGHT", fromSec: 8, toSec: 12 }];
167
+ // With the 4s dwell at LA, at wall-clock t=12 the pin is only ~89% along
168
+ // the flight arc (still north of LA) because the drive budget shrank.
169
+ const withStop = routePositionAtLegs(legs, 40, 12, stops)!;
170
+ expect(withStop.mode).toBe("FLIGHT");
171
+ expect(withStop.lat).toBeGreaterThan(LA.lat);
172
+ expect(withStop.lat).toBeLessThan(SF.lat);
173
+
174
+ // Without the stop, t=12 is already 20% into the LA→PIER driving leg.
175
+ const noStop = routePositionAtLegs(legs, 40, 12)!;
176
+ expect(noStop.mode).toBe("DRIVING");
177
+ });
178
+ });
@@ -125,4 +125,22 @@ describe("map stream — dynamic camera views", () => {
125
125
  it("rejects an invalid view", () => {
126
126
  expect(() => mapStream.parse({ ...base, view: "drone" })).toThrow();
127
127
  });
128
+
129
+ it("carries per-waypoint travel modes (FLIGHT/BOAT/DRIVING...)", () => {
130
+ const m = mapStream.parse({
131
+ ...base,
132
+ waypoints: [
133
+ { lat: 37.77, lng: -122.41, mode: "FLIGHT" },
134
+ { lat: 34.05, lng: -118.25, mode: "BOAT" },
135
+ { lat: 34.01, lng: -118.5, mode: "WALKING" },
136
+ ],
137
+ });
138
+ expect(m.waypoints.map((w) => w.mode)).toEqual(["FLIGHT", "BOAT", "WALKING"]);
139
+ });
140
+
141
+ it("rejects an invalid waypoint travel mode", () => {
142
+ expect(() =>
143
+ mapStream.parse({ ...base, waypoints: [{ lat: 37.77, lng: -122.41, mode: "TELEPORT" }] }),
144
+ ).toThrow();
145
+ });
128
146
  });
@@ -1,3 +0,0 @@
1
- {
2
- "copilot-infinite.discord.enabled": true
3
- }