@hraness/slopcamera 3.6.0 → 3.8.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.
- package/README.md +49 -33
- package/apps/desktop/README.md +3 -10
- package/apps/desktop/application/html-overlay-renderer.ts +6 -0
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/application/spatial-assets.ts +1 -1
- package/apps/desktop/cli/{menubar-status.ts → activity-status.ts} +24 -21
- package/apps/desktop/cli/args.ts +62 -10
- package/apps/desktop/cli/command-host-resources.ts +5 -1
- package/apps/desktop/cli/commands.ts +26 -9
- package/apps/desktop/cli/desktop-status.ts +271 -0
- package/apps/desktop/cli/help.ts +36 -28
- package/apps/desktop/cli/html-film-init.ts +158 -0
- package/apps/desktop/cli/html-film-names.ts +17 -0
- package/apps/desktop/cli/html-film.ts +471 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
- package/apps/desktop/cli/html-scene.ts +3 -3
- package/apps/desktop/cli/legacy-login.ts +184 -0
- package/apps/desktop/cli/main.ts +9 -0
- package/apps/desktop/cli/outputs.ts +19 -0
- package/apps/desktop/cli/portable-surface.ts +55 -1
- package/apps/desktop/cli/registry.ts +314 -0
- package/apps/desktop/cli/verbs.ts +193 -0
- package/apps/desktop/dist/cli/main.js +338 -316
- package/apps/desktop/html-overlay/film.ts +418 -0
- package/dist/cli.js +13 -2
- package/dist/generate.js +1 -1
- package/dist/{index-4z9y2wa2.js → index-842t02pp.js} +1 -1
- package/dist/{index-qxfexvj3.js → index-k9bjw80z.js} +20 -4
- package/dist/index-p74xy76d.js +3 -0
- package/dist/{index-fevf4ppk.js → index-y8spxs77.js} +10 -7
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +6 -6
- package/examples/html/launch-film/README.md +51 -0
- package/examples/html/launch-film/build.ts +158 -0
- package/examples/html/launch-film/film.css +593 -0
- package/examples/html/launch-film/film.html +85 -0
- package/examples/html/launch-film/film.js +410 -0
- package/examples/html/launch-film/film.json +63 -0
- package/examples/html/launch-film/mockups.tsx +74 -0
- package/examples/html/launch-film/timeline.ts +57 -0
- package/package.json +24 -11
- package/skills/slopcamera/SKILL.md +4 -3
- package/skills/slopcamera/references/install.md +3 -3
- package/skills/slopcamera/references/launch-films.md +158 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/visual-communication.md +1 -1
- package/src/cli.ts +125 -9
- package/src/generation-pricing.ts +21 -1
- package/src/icon-set.ts +1086 -0
- package/src/icon.ts +396 -59
- package/src/index.ts +1 -0
- package/src/operations.ts +52 -0
- package/src/version.ts +1 -1
- package/apps/desktop/cli/menubar.ts +0 -272
- package/dist/index-nxtmy18t.js +0 -3
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion helpers for HTML launch films.
|
|
3
|
+
*
|
|
4
|
+
* Every helper is a pure function of the frame time, or applies such a value
|
|
5
|
+
* to an element it is handed. A frame callback built on these renders the
|
|
6
|
+
* same picture whether a frame is requested first, last, twice, or alone,
|
|
7
|
+
* which is what `slopcamera html render` and `html still` rely on.
|
|
8
|
+
*
|
|
9
|
+
* This module has no imports so `slopcamera html init` can copy it into a
|
|
10
|
+
* film project as a self-contained file. Keep it that way.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The frame record `SlopcameraOverlay.onFrame` passes to each callback. */
|
|
14
|
+
export interface HtmlFilmFrame {
|
|
15
|
+
/** Zero-based frame index on the absolute render clock. */
|
|
16
|
+
readonly frame: number;
|
|
17
|
+
/** Frame time in milliseconds: `frame * 1000 / fps`. */
|
|
18
|
+
readonly timeMs: number;
|
|
19
|
+
/** Milliseconds since the previous frame, zero on the first frame. */
|
|
20
|
+
readonly deltaMs: number;
|
|
21
|
+
/** `timeMs / durationMs`, capped at 1. */
|
|
22
|
+
readonly progress: number;
|
|
23
|
+
readonly width: number;
|
|
24
|
+
readonly height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The page-visible `globalThis.SlopcameraOverlay` API a film uses. */
|
|
28
|
+
export interface HtmlFilmOverlay {
|
|
29
|
+
onFrame(callback: (frame: HtmlFilmFrame) => void | Promise<void>): void;
|
|
30
|
+
ready(work: Promise<unknown>): void;
|
|
31
|
+
asset(name: string): string;
|
|
32
|
+
readonly parameters?: Readonly<Record<string, unknown>>;
|
|
33
|
+
readonly width?: number;
|
|
34
|
+
readonly height?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ------------------------------------------------------------------ numbers
|
|
38
|
+
|
|
39
|
+
export const clamp = (value: number, min = 0, max = 1): number => Math.min(max, Math.max(min, value));
|
|
40
|
+
export const lerp = (from: number, to: number, amount: number): number => from + (to - from) * amount;
|
|
41
|
+
/** Progress of `t` through the window `[start, end]`, clamped to 0..1. */
|
|
42
|
+
export function prog(t: number, start: number, end: number): number {
|
|
43
|
+
if (end <= start) return t >= end ? 1 : 0;
|
|
44
|
+
return clamp((t - start) / (end - start));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Easing = (x: number) => number;
|
|
48
|
+
export const easings = Object.freeze({
|
|
49
|
+
linear: (x: number) => x,
|
|
50
|
+
inCubic: (x: number) => x ** 3,
|
|
51
|
+
outCubic: (x: number) => 1 - (1 - x) ** 3,
|
|
52
|
+
inOutCubic: (x: number) => (x < 0.5 ? 4 * x ** 3 : 1 - (-2 * x + 2) ** 3 / 2),
|
|
53
|
+
inQuart: (x: number) => x ** 4,
|
|
54
|
+
outQuart: (x: number) => 1 - (1 - x) ** 4,
|
|
55
|
+
inOutQuart: (x: number) => (x < 0.5 ? 8 * x ** 4 : 1 - (-2 * x + 2) ** 4 / 2),
|
|
56
|
+
outQuint: (x: number) => 1 - (1 - x) ** 5,
|
|
57
|
+
outExpo: (x: number) => (x >= 1 ? 1 : 1 - 2 ** (-10 * x)),
|
|
58
|
+
inOutSine: (x: number) => (1 - Math.cos(Math.PI * x)) / 2,
|
|
59
|
+
outBack: (x: number) => 1 + 2.70158 * (x - 1) ** 3 + 1.70158 * (x - 1) ** 2,
|
|
60
|
+
} satisfies Record<string, Easing>);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A damped spring from 0 to 1. `seconds` is the time since release; before
|
|
64
|
+
* release it is 0. Lower `damping` overshoots more; 1 or more never overshoots.
|
|
65
|
+
*/
|
|
66
|
+
export function spring(seconds: number, frequency = 1.5, damping = 0.7): number {
|
|
67
|
+
if (seconds <= 0) return 0;
|
|
68
|
+
const w = 2 * Math.PI * frequency;
|
|
69
|
+
if (damping >= 1) return 1 - Math.exp(-w * seconds) * (1 + w * seconds);
|
|
70
|
+
const wd = w * Math.sqrt(1 - damping * damping);
|
|
71
|
+
return 1 - Math.exp(-damping * w * seconds) * (Math.cos(wd * seconds) + ((damping * w) / wd) * Math.sin(wd * seconds));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A value that holds, then eases to the next key. Keys are `[atSeconds, value]`
|
|
76
|
+
* in time order; each change takes `duration` seconds.
|
|
77
|
+
*/
|
|
78
|
+
export function stepValue(keys: readonly (readonly [number, number])[], t: number, duration = 0.5, ease: Easing = easings.inOutCubic): number {
|
|
79
|
+
if (keys.length === 0) throw new RangeError("stepValue needs at least one key.");
|
|
80
|
+
let value = keys[0]![1];
|
|
81
|
+
for (let index = 1; index < keys.length; index++) {
|
|
82
|
+
const [at, to] = keys[index]!;
|
|
83
|
+
if (t < at) break;
|
|
84
|
+
value = lerp(value, to, ease(prog(t, at, at + duration)));
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Opacities for a crossfade that starts at `at` and lasts `duration` seconds. */
|
|
90
|
+
export function crossfade(t: number, at: number, duration = 0.6, ease: Easing = easings.inOutCubic): { readonly out: number; readonly in: number } {
|
|
91
|
+
const amount = ease(prog(t, at, at + duration));
|
|
92
|
+
return { out: 1 - amount, in: amount };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ----------------------------------------------------------------- timeline
|
|
96
|
+
|
|
97
|
+
export interface HtmlFilmActInput {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
/** Seconds the act is on screen, including its overlap with the next act. */
|
|
100
|
+
readonly duration: number;
|
|
101
|
+
/** Seconds this act starts before the previous one ends, for crossfades. */
|
|
102
|
+
readonly overlap?: number;
|
|
103
|
+
/** One short line said in the act, used for WebVTT captions and beats. */
|
|
104
|
+
readonly caption?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface HtmlFilmAct extends HtmlFilmActInput {
|
|
108
|
+
readonly start: number;
|
|
109
|
+
readonly end: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface HtmlFilmTimeline {
|
|
113
|
+
readonly acts: readonly HtmlFilmAct[];
|
|
114
|
+
readonly duration: number;
|
|
115
|
+
act(id: string): HtmlFilmAct;
|
|
116
|
+
/** Seconds since the act started; negative before it. */
|
|
117
|
+
local(t: number, id: string): number;
|
|
118
|
+
/** Progress through the act, 0..1. */
|
|
119
|
+
progress(t: number, id: string): number;
|
|
120
|
+
/** True while the act is on screen, widened by `pad` seconds on each side. */
|
|
121
|
+
active(t: number, id: string, pad?: number): boolean;
|
|
122
|
+
/** The latest act that has started at `t`. */
|
|
123
|
+
current(t: number): HtmlFilmAct;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Lays acts end to end. Overlaps pull an act earlier so it can crossfade. */
|
|
127
|
+
export function defineTimeline(inputs: readonly HtmlFilmActInput[]): HtmlFilmTimeline {
|
|
128
|
+
if (inputs.length === 0) throw new RangeError("A timeline needs at least one act.");
|
|
129
|
+
const seen = new Set<string>();
|
|
130
|
+
const acts: HtmlFilmAct[] = [];
|
|
131
|
+
let cursor = 0;
|
|
132
|
+
for (const input of inputs) {
|
|
133
|
+
if (seen.has(input.id)) throw new RangeError(`Duplicate act id: ${input.id}`);
|
|
134
|
+
if (!(input.duration > 0) || !Number.isFinite(input.duration)) throw new RangeError(`Act ${input.id} needs a positive duration.`);
|
|
135
|
+
const overlap = input.overlap ?? 0;
|
|
136
|
+
if (overlap < 0 || (acts.length === 0 && overlap > 0) || overlap >= input.duration) throw new RangeError(`Act ${input.id} has an invalid overlap.`);
|
|
137
|
+
seen.add(input.id);
|
|
138
|
+
const start = Math.max(0, cursor - overlap);
|
|
139
|
+
const end = start + input.duration;
|
|
140
|
+
acts.push(Object.freeze({ ...input, start, end }));
|
|
141
|
+
cursor = end;
|
|
142
|
+
}
|
|
143
|
+
const byId = new Map(acts.map(act => [act.id, act]));
|
|
144
|
+
const act = (id: string) => {
|
|
145
|
+
const found = byId.get(id);
|
|
146
|
+
if (found === undefined) throw new RangeError(`Unknown act: ${id}`);
|
|
147
|
+
return found;
|
|
148
|
+
};
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
acts: Object.freeze(acts),
|
|
151
|
+
duration: cursor,
|
|
152
|
+
act,
|
|
153
|
+
local: (t: number, id: string) => t - act(id).start,
|
|
154
|
+
progress: (t: number, id: string) => prog(t, act(id).start, act(id).end),
|
|
155
|
+
active: (t: number, id: string, pad = 0) => t >= act(id).start - pad && t < act(id).end + pad,
|
|
156
|
+
current: (t: number) => acts.reduce((found, candidate) => (candidate.start <= t ? candidate : found), acts[0]!),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function vttTime(seconds: number): string {
|
|
161
|
+
const ms = Math.round(seconds * 1000);
|
|
162
|
+
const h = Math.floor(ms / 3_600_000);
|
|
163
|
+
const m = Math.floor((ms % 3_600_000) / 60_000);
|
|
164
|
+
const s = Math.floor((ms % 60_000) / 1000);
|
|
165
|
+
const pad = (value: number, width = 2) => String(value).padStart(width, "0");
|
|
166
|
+
return `${pad(h)}:${pad(m)}:${pad(s)}.${pad(ms % 1000, 3)}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* WebVTT captions from each act's `caption`. A cue runs from its act's start
|
|
171
|
+
* until the next captioned act starts, so overlapping acts never stack cues.
|
|
172
|
+
*/
|
|
173
|
+
export function captionsFromTimeline(timeline: Pick<HtmlFilmTimeline, "acts" | "duration">): string {
|
|
174
|
+
const captioned = timeline.acts.filter(act => act.caption !== undefined && act.caption.trim() !== "");
|
|
175
|
+
const cues = captioned.map((act, index) => {
|
|
176
|
+
const next = captioned[index + 1];
|
|
177
|
+
const end = next === undefined ? Math.min(act.end, timeline.duration) : Math.min(act.end, next.start);
|
|
178
|
+
const text = act.caption!.replaceAll("-->", "->").replace(/\s*\n\s*/gu, "\n").trim();
|
|
179
|
+
return `${act.id}\n${vttTime(act.start)} --> ${vttTime(end)}\n${text}`;
|
|
180
|
+
});
|
|
181
|
+
return `WEBVTT\n\n${cues.join("\n\n")}${cues.length === 0 ? "" : "\n"}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ------------------------------------------------------------------ camera
|
|
185
|
+
|
|
186
|
+
export interface HtmlFilmRect { readonly x: number; readonly y: number; readonly w: number; readonly h: number }
|
|
187
|
+
export interface HtmlFilmCamera { readonly tx: number; readonly ty: number; readonly z: number }
|
|
188
|
+
|
|
189
|
+
export interface HtmlFilmCameraInput {
|
|
190
|
+
/** The visible window, for example a browser frame's page area. */
|
|
191
|
+
readonly viewport: { readonly w: number; readonly h: number };
|
|
192
|
+
/** The full content that moves inside the window. */
|
|
193
|
+
readonly content: { readonly w: number; readonly h: number };
|
|
194
|
+
/** The area to bring into view, in content coordinates. */
|
|
195
|
+
readonly focus: HtmlFilmRect;
|
|
196
|
+
/** Zoom; `"fit"` zooms out until the focus plus padding fits. Default 1. */
|
|
197
|
+
readonly zoom?: number | "fit";
|
|
198
|
+
/** Where the focus centre lands vertically, 0 top to 1 bottom. Default 0.5. */
|
|
199
|
+
readonly anchor?: number;
|
|
200
|
+
readonly padding?: number;
|
|
201
|
+
/** 0 keeps the resting view, 1 is fully on the focus. */
|
|
202
|
+
readonly mix?: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Where content sits inside a window to frame a focus rectangle, as a
|
|
207
|
+
* translate and a zoom. It never shows past the content's edges.
|
|
208
|
+
*/
|
|
209
|
+
export function camera(input: HtmlFilmCameraInput): HtmlFilmCamera {
|
|
210
|
+
const { viewport, content, focus } = input;
|
|
211
|
+
const padding = input.padding ?? 0;
|
|
212
|
+
const mix = clamp(input.mix ?? 1);
|
|
213
|
+
const z = input.zoom === "fit"
|
|
214
|
+
? Math.min(1, (viewport.w) / (focus.w + padding * 2), (viewport.h) / (focus.h + padding * 2))
|
|
215
|
+
: input.zoom ?? 1;
|
|
216
|
+
const fx = focus.x + focus.w / 2;
|
|
217
|
+
const fy = focus.y + focus.h / 2;
|
|
218
|
+
let tx = viewport.w / 2 - fx * z;
|
|
219
|
+
tx = content.w * z <= viewport.w ? (viewport.w - content.w * z) / 2 : clamp(tx, viewport.w - content.w * z, 0);
|
|
220
|
+
let ty = viewport.h * (input.anchor ?? 0.5) - fy * z;
|
|
221
|
+
ty = content.h * z <= viewport.h ? 0 : clamp(ty, viewport.h - content.h * z, 0);
|
|
222
|
+
const restX = content.w <= viewport.w ? (viewport.w - content.w) / 2 : 0;
|
|
223
|
+
return { tx: lerp(restX, tx, mix), ty: lerp(0, ty, mix), z: lerp(1, z, mix) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Interpolates two camera positions. */
|
|
227
|
+
export const cameraBetween = (from: HtmlFilmCamera, to: HtmlFilmCamera, amount: number): HtmlFilmCamera => ({
|
|
228
|
+
tx: lerp(from.tx, to.tx, amount), ty: lerp(from.ty, to.ty, amount), z: lerp(from.z, to.z, amount),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/** The CSS transform for a camera; pair it with `transform-origin: 0 0`. */
|
|
232
|
+
export const cameraTransform = (value: HtmlFilmCamera): string =>
|
|
233
|
+
`translate3d(${value.tx.toFixed(2)}px, ${value.ty.toFixed(2)}px, 0) scale(${value.z.toFixed(4)})`;
|
|
234
|
+
|
|
235
|
+
// ------------------------------------------------------------------ cursor
|
|
236
|
+
|
|
237
|
+
export interface HtmlFilmCursorKey {
|
|
238
|
+
readonly at: number;
|
|
239
|
+
readonly x: number;
|
|
240
|
+
readonly y: number;
|
|
241
|
+
/** A click that peaks at `at`. */
|
|
242
|
+
readonly click?: boolean;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface HtmlFilmCursorState {
|
|
246
|
+
readonly x: number;
|
|
247
|
+
readonly y: number;
|
|
248
|
+
readonly opacity: number;
|
|
249
|
+
/** 0..1, peaks at each click. */
|
|
250
|
+
readonly press: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The cursor position at `t`. It glides between keys with an ease, fades in
|
|
255
|
+
* over `fade` seconds before the first key and out after the last.
|
|
256
|
+
*/
|
|
257
|
+
export function cursor(t: number, keys: readonly HtmlFilmCursorKey[], options: { readonly fade?: number; readonly hold?: number; readonly ease?: Easing } = {}): HtmlFilmCursorState {
|
|
258
|
+
if (keys.length === 0) throw new RangeError("cursor needs at least one key.");
|
|
259
|
+
const fade = options.fade ?? 0.25;
|
|
260
|
+
const hold = options.hold ?? 0.6;
|
|
261
|
+
const ease = options.ease ?? easings.inOutCubic;
|
|
262
|
+
const first = keys[0]!;
|
|
263
|
+
const last = keys[keys.length - 1]!;
|
|
264
|
+
let x = first.x;
|
|
265
|
+
let y = first.y;
|
|
266
|
+
for (let index = 1; index < keys.length; index++) {
|
|
267
|
+
const from = keys[index - 1]!;
|
|
268
|
+
const to = keys[index]!;
|
|
269
|
+
if (t <= from.at) break;
|
|
270
|
+
const amount = ease(prog(t, from.at, to.at));
|
|
271
|
+
x = lerp(from.x, to.x, amount);
|
|
272
|
+
y = lerp(from.y, to.y, amount);
|
|
273
|
+
}
|
|
274
|
+
const opacity = prog(t, first.at - fade, first.at) * (1 - prog(t, last.at + hold, last.at + hold + fade));
|
|
275
|
+
let press = 0;
|
|
276
|
+
for (const key of keys) if (key.click === true) press = Math.max(press, Math.sin(Math.PI * prog(t, key.at - 0.08, key.at + 0.14)));
|
|
277
|
+
return { x, y, opacity, press };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -------------------------------------------------------------- DOM helpers
|
|
281
|
+
|
|
282
|
+
interface StyleLike {
|
|
283
|
+
visibility: string;
|
|
284
|
+
display: string;
|
|
285
|
+
opacity: string;
|
|
286
|
+
transform: string;
|
|
287
|
+
setProperty?(name: string, value: string): void;
|
|
288
|
+
}
|
|
289
|
+
interface StyledLike { readonly style: StyleLike }
|
|
290
|
+
interface AttributeLike { setAttribute(name: string, value: string): void }
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Shows or hides an element. It sets both visibility and display: a hidden
|
|
294
|
+
* parent does not hide children that set `visibility: visible` themselves.
|
|
295
|
+
*/
|
|
296
|
+
export function show(element: StyledLike, on: boolean): void {
|
|
297
|
+
element.style.visibility = on ? "visible" : "hidden";
|
|
298
|
+
element.style.display = on ? "" : "none";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Places a cursor element from a `cursor()` state. */
|
|
302
|
+
export function placeCursor(element: StyledLike, state: HtmlFilmCursorState): void {
|
|
303
|
+
element.style.opacity = String(state.opacity);
|
|
304
|
+
element.style.transform = `translate3d(${state.x.toFixed(2)}px, ${state.y.toFixed(2)}px, 0) scale(${(1 - 0.08 * state.press).toFixed(4)})`;
|
|
305
|
+
element.style.visibility = state.opacity > 0 ? "visible" : "hidden";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface HtmlFilmMark {
|
|
309
|
+
/** Positioned box the mark fills. */
|
|
310
|
+
readonly box: StyledLike;
|
|
311
|
+
readonly svg: AttributeLike;
|
|
312
|
+
/** A `<rect pathLength="1">` stroked with the mark colour. */
|
|
313
|
+
readonly rect: AttributeLike & StyledLike & { readonly style: StyleLike & { strokeWidth: string; strokeDasharray: string; strokeDashoffset: string } };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface HtmlFilmMarkState extends HtmlFilmRect {
|
|
317
|
+
readonly radius: number;
|
|
318
|
+
readonly stroke: number;
|
|
319
|
+
/** 0..1 of the outline drawn. */
|
|
320
|
+
readonly drawn: number;
|
|
321
|
+
readonly opacity: number;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Draws a rounded outline around a rectangle, `drawn` of the way round. */
|
|
325
|
+
export function drawMark(mark: HtmlFilmMark, state: HtmlFilmMarkState): void {
|
|
326
|
+
const { box, svg, rect } = mark;
|
|
327
|
+
const style = box.style as StyleLike & { left?: string; top?: string; width?: string; height?: string };
|
|
328
|
+
style.left = `${state.x}px`;
|
|
329
|
+
style.top = `${state.y}px`;
|
|
330
|
+
style.width = `${state.w}px`;
|
|
331
|
+
style.height = `${state.h}px`;
|
|
332
|
+
style.opacity = String(state.opacity);
|
|
333
|
+
style.visibility = state.opacity > 0 && state.drawn > 0 ? "visible" : "hidden";
|
|
334
|
+
svg.setAttribute("viewBox", `0 0 ${state.w} ${state.h}`);
|
|
335
|
+
const inset = state.stroke / 2;
|
|
336
|
+
rect.setAttribute("x", String(inset));
|
|
337
|
+
rect.setAttribute("y", String(inset));
|
|
338
|
+
rect.setAttribute("width", String(Math.max(0, state.w - state.stroke)));
|
|
339
|
+
rect.setAttribute("height", String(Math.max(0, state.h - state.stroke)));
|
|
340
|
+
rect.setAttribute("rx", String(state.radius));
|
|
341
|
+
rect.style.strokeWidth = `${state.stroke}px`;
|
|
342
|
+
rect.style.strokeDasharray = "1 1";
|
|
343
|
+
rect.style.strokeDashoffset = String(1 - clamp(state.drawn));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface NodeLike { textContent: string | null }
|
|
347
|
+
interface ElementLike extends NodeLike, StyledLike {
|
|
348
|
+
className: string;
|
|
349
|
+
append(...nodes: unknown[]): void;
|
|
350
|
+
readonly ownerDocument: {
|
|
351
|
+
createElement(tag: string): ElementLike;
|
|
352
|
+
createTextNode(text: string): unknown;
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const PARTS = new WeakMap<object, readonly StyledLike[]>();
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Wraps each word (or character) of an element in a mask span `.w` holding an
|
|
360
|
+
* inner span `.wi`, for `kin`. A newline in the text becomes a line break.
|
|
361
|
+
*/
|
|
362
|
+
export function split(element: ElementLike, unit: "word" | "char" = "word"): readonly StyledLike[] {
|
|
363
|
+
const text = element.textContent ?? "";
|
|
364
|
+
const doc = element.ownerDocument;
|
|
365
|
+
element.textContent = "";
|
|
366
|
+
const pieces = unit === "char" ? [...text] : text.split(/(\s+)/u);
|
|
367
|
+
const parts: ElementLike[] = [];
|
|
368
|
+
for (const piece of pieces) {
|
|
369
|
+
if (piece === "") continue;
|
|
370
|
+
if (/^\s+$/u.test(piece)) {
|
|
371
|
+
element.append(piece.includes("\n") ? doc.createElement("br") : doc.createTextNode(" "));
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const mask = doc.createElement("span");
|
|
375
|
+
mask.className = "w";
|
|
376
|
+
const inner = doc.createElement("span");
|
|
377
|
+
inner.className = "wi";
|
|
378
|
+
inner.textContent = piece;
|
|
379
|
+
mask.append(inner);
|
|
380
|
+
element.append(mask);
|
|
381
|
+
parts.push(inner);
|
|
382
|
+
}
|
|
383
|
+
PARTS.set(element, parts);
|
|
384
|
+
return parts;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface HtmlFilmKinOptions {
|
|
388
|
+
readonly stagger?: number;
|
|
389
|
+
readonly duration?: number;
|
|
390
|
+
readonly outStagger?: number;
|
|
391
|
+
readonly outDuration?: number;
|
|
392
|
+
/** Degrees each word starts rotated. */
|
|
393
|
+
readonly tilt?: number;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Kinetic type: the split words rise into their masks from `inAt` and leave
|
|
398
|
+
* upward from `outAt`. Returns whether any word is on screen.
|
|
399
|
+
*/
|
|
400
|
+
export function kin(element: StyledLike, t: number, inAt: number, outAt = Infinity, options: HtmlFilmKinOptions = {}): boolean {
|
|
401
|
+
const parts = PARTS.get(element);
|
|
402
|
+
if (parts === undefined) throw new Error("kin needs an element prepared with split().");
|
|
403
|
+
const stagger = options.stagger ?? 0.055;
|
|
404
|
+
const duration = options.duration ?? 0.9;
|
|
405
|
+
const outStagger = options.outStagger ?? 0.025;
|
|
406
|
+
const outDuration = options.outDuration ?? 0.5;
|
|
407
|
+
let visible = false;
|
|
408
|
+
parts.forEach((part, index) => {
|
|
409
|
+
const a = easings.outQuint(prog(t, inAt + index * stagger, inAt + index * stagger + duration));
|
|
410
|
+
const b = outAt === Infinity ? 0 : easings.inCubic(prog(t, outAt + index * outStagger, outAt + index * outStagger + outDuration));
|
|
411
|
+
const y = (1 - a) * 115 - b * 115;
|
|
412
|
+
const r = (1 - a) * (options.tilt ?? 0);
|
|
413
|
+
part.style.transform = r === 0 ? `translate3d(0, ${y.toFixed(2)}%, 0)` : `translate3d(0, ${y.toFixed(2)}%, 0) rotate(${r.toFixed(2)}deg)`;
|
|
414
|
+
if (a > 0 && b < 1) visible = true;
|
|
415
|
+
});
|
|
416
|
+
element.style.visibility = visible ? "visible" : "hidden";
|
|
417
|
+
return visible;
|
|
418
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import{SLOPCAMERA_VERSION,VISUAL_STYLE_PROFILES,artifactSummary,checkDiagramFile,checkDrawingFile,createVisualStyleDirection,getVisualStyleProfile,renderDiagramFile,renderDrawingFile,runMcpServer,starterDrawingSource}from"./index-
|
|
3
|
+
import{SLOPCAMERA_VERSION,VISUAL_STYLE_PROFILES,artifactSummary,checkDiagramFile,checkDrawingFile,createVisualStyleDirection,generateSlopcameraIconSet,getVisualStyleProfile,parseSlopcameraIconSetSpec,renderDiagramFile,renderDrawingFile,runMcpServer,slopcameraIconSetMaximumSetRounds,starterDrawingSource}from"./index-k9bjw80z.js";import{installSkill,pathExists}from"./index-grk47tgs.js";import"./index-z1kd1x8j.js";import"./index-3cxf2c58.js";import"./index-842t02pp.js";import{executeSlopcameraOperation,generateSlopcameraIcon,generateSlopcameraImageGallery,isSlopcameraOperationCode,parseSlopcameraGalleryVary,searchSlopcameraOperations,slopcameraGalleryKinds,slopcameraGalleryLimits,slopcameraIconContexts,slopcameraIconMaximumRounds,slopcameraOperationCodes,withSlopcameraOperationHostAdmission}from"./index-y8spxs77.js";import{vectorizeImage}from"./index-mnp0bftq.js";import{generateSlopcameraImageFile,slopcameraGatewayCredentialStatus,slopcameraImageModels}from"./index-p74xy76d.js";import"./index-b3geqczn.js";import{__require}from"./index-80a36bc0.js";import{readFile,writeFile}from"fs/promises";import{dirname,resolve}from"path";function reportUsefulResult(observer){try{if(observer!==undefined)Promise.resolve(observer()).catch(()=>{return})}catch{}}import{maybeShowSupportInvitation,runSupportCommand,supportAdvancedHelp,supportHelpLine}from"@hraness/support-foundation/node";var slopcameraSupportHelpLine=()=>supportHelpLine({command:["slopcamera"]});var slopcameraSupportAdvancedHelp=()=>supportAdvancedHelp({command:["slopcamera"]});var supportProfile={id:"slopcamera",name:"Slopcamera",valueProposition:"Support ongoing development of local media tools for agents.",updates:false};function standaloneSupportEnvironment(){const env={...process.env};process.env.HRANESS_SUPPORT_AUDIENCE="off";process.env.HRANESS_SUPPORT_EMAIL="off";return env}async function runProductSupportCommand(args,options={}){const result=await runSupportCommand(supportProfile,args,{command:["slopcamera"],...options,gitEmail:false});if(result.stdout!=="")process.stdout.write(result.stdout);if(result.stderr!=="")process.stderr.write(result.stderr);return result.exitCode}async function showProductSupportInvitation(options={}){try{await maybeShowSupportInvitation(supportProfile,{usefulResult:true,command:["slopcamera"],...options,gitEmail:false})}catch{}}var slopcameraCliVersion=SLOPCAMERA_VERSION;var help=()=>`slopcamera ${slopcameraCliVersion}
|
|
4
4
|
|
|
5
5
|
Turn source material into deterministic diagrams, images, and canvas assets.
|
|
6
6
|
|
|
@@ -15,7 +15,11 @@ Usage:
|
|
|
15
15
|
slopcamera image generate <prompt> --output <file.png|jpg|webp> [--model <provider/model>] [--json]
|
|
16
16
|
slopcamera image icon <subject> --output <file.svg> [--purpose <mark|illustration>]
|
|
17
17
|
[--model <provider/model>] [--ink <#rgb|#rrggbb>] [--rounds <1-${slopcameraIconMaximumRounds}>]
|
|
18
|
+
[--context <${slopcameraIconContexts.join("|")}>] [--candidates <1-${slopcameraIconMaximumRounds}>]
|
|
18
19
|
[--critique-model <provider/model>] [--keep-raster] [--json]
|
|
20
|
+
slopcamera image icon --set <file.json> --output-dir <directory>
|
|
21
|
+
[--context <${slopcameraIconContexts.join("|")}>] [--candidates <1-${slopcameraIconMaximumRounds}>]
|
|
22
|
+
[--set-rounds <1-${slopcameraIconSetMaximumSetRounds}>] [--keep-raster] [--json]
|
|
19
23
|
slopcamera image gallery <subject> --output-dir <directory> [--kind <${slopcameraGalleryKinds.join("|")}>]
|
|
20
24
|
[--count <1-${slopcameraGalleryLimits.candidates}>] [--vary <axis[=v1,v2][;axis...]>] [--candidates <file.json>]
|
|
21
25
|
[--model <provider/model>] [--cell <${slopcameraGalleryLimits.cellEdgeMin}-${slopcameraGalleryLimits.cellEdgeMax}>] [--tile|--no-tile] [--json]
|
|
@@ -59,6 +63,13 @@ vectorizer, and (when --rounds exceeds 1) critiqued by a vision model whose
|
|
|
59
63
|
feedback revises the prompt for the next attempt. Only Slopcamera's own
|
|
60
64
|
generated output is uploaded for critique \u2014 never user media.
|
|
61
65
|
|
|
66
|
+
Icon --set reads a bounded JSON manifest ({name?, ink?, context?, members:
|
|
67
|
+
[{slug, subject, purpose?, context?}]}) and generates the whole family at
|
|
68
|
+
once: each member keeps a small candidate pool, joint selection minimizes the
|
|
69
|
+
measured coverage and stroke-weight spread across the set, and a
|
|
70
|
+
contact-sheet critique reviews the family as a unit before SVGs publish.
|
|
71
|
+
Manifests and receipts stay local.
|
|
72
|
+
|
|
62
73
|
Gallery generates several bounded candidates in parallel and composes them
|
|
63
74
|
into one labelled contact sheet plus a receipt. Use it to review texture,
|
|
64
75
|
skybox, backdrop, sprite, or design alternatives, then promote a chosen
|
|
@@ -82,7 +93,7 @@ stdout.
|
|
|
82
93
|
${help()}`)}return args}async function main(args,dependencies={}){if(args[0]==="style"){const[action,...words]=args.slice(1);const json=words.at(-1)==="--json";const positionals=json?words.slice(0,-1):words;const log=dependencies.log??console.log;if(action==="list"&&positionals.length===0){log(json?JSON.stringify({schemaVersion:1,styles:VISUAL_STYLE_PROFILES}):["Visual style foundations (authoring guidance; no effects are applied):",...VISUAL_STYLE_PROFILES.map((style)=>`${style.id} ${style.name}
|
|
83
94
|
${style.summary}`),"Use slopcamera style show <id> for palette, cadence, materials, delivery, and review criteria."].join(`
|
|
84
95
|
`));return}if(action==="show"&&positionals.length===1&&!positionals[0].startsWith("-")){let style;try{style=getVisualStyleProfile(positionals[0])}catch{throw new Error(`Unknown visual style: ${JSON.stringify(positionals[0].slice(0,128))}. Use slopcamera style list.`)}log(json?JSON.stringify({schemaVersion:1,style}):createVisualStyleDirection(style.id));return}throw new Error("Use slopcamera style list [--json] or slopcamera style show <id> [--json].")}if(args[0]==="diagram"&&args[1]==="sheets"){await runDrawingSheets(args.slice(2),dependencies);return}if(args[0]==="support"){process.exitCode=await runProductSupportCommand(args.slice(1),dependencies.supportEnvironment===undefined?{}:{env:dependencies.supportEnvironment});return}const[command,...rest]=canonicalArguments(args);if(command===undefined||command==="--help"||command==="-h"){console.log(help());return}if(command==="help"){console.log(rest[0]==="advanced"?slopcameraSupportAdvancedHelp():help());return}if(command==="version"||command==="--version"||command==="-v"){console.log(slopcameraCliVersion);return}if(command==="init"){const parsed=parseArguments(rest,new Set);const filePath=resolve(parsed.positionals[0]??"diagram.diagram.json");if(await pathExists(filePath))throw new Error(`Refusing to overwrite existing file: ${filePath}`);await writeFile(filePath,`${JSON.stringify(starter,null,2)}
|
|
85
|
-
`);console.log(`Created ${filePath}`);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="check"){const parsed=parseArguments(rest,new Set(["config"]));const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.check",async()=>await checkDiagramFile({filePath:requiredPositional(parsed,0,"diagram file"),...parsed.options.config===undefined?{}:{configPath:parsed.options.config}}),hostAdmissionOptions(dependencies));console.log(`Valid diagram${result.configPath===null?"":` with ${result.configPath}`}.`);printFindings(result.findings);if(parsed.flags.has("strict")&&result.findings.length>0)process.exitCode=2;return}if(command==="render"){const parsed=parseArguments(rest,new Set(["out-dir","config","scale"]));const scale=parsed.options.scale===undefined?undefined:Number.parseFloat(parsed.options.scale);const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.render",async()=>await renderDiagramFile({filePath:requiredPositional(parsed,0,"diagram file"),...parsed.options["out-dir"]===undefined?{}:{outDirectory:parsed.options["out-dir"]},...parsed.options.config===undefined?{}:{configPath:parsed.options.config},...scale===undefined?{}:{scale}}),hostAdmissionOptions(dependencies));console.log(artifactSummary(result.artifacts));printFindings(result.findings);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="vectorize"){const parsed=parseArguments(rest,new Set(["output","duotone","alpha-cutoff","timeout-ms"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json");if(unknownFlags.length>0){throw new Error(`Unknown vectorize option: --${unknownFlags[0]}`)}if(parsed.positionals.length>1){throw new Error("slopcamera image vectorize accepts exactly one raster input")}const output=requiredOption(parsed,"output");if(!output.toLowerCase().endsWith(".svg")){throw new Error("--output must end in .svg")}const alphaCutoff=parsePositiveInteger(parsed.options["alpha-cutoff"],"alpha-cutoff");const timeoutMs=parsePositiveInteger(parsed.options["timeout-ms"],"timeout-ms");const duotone=parseDuotone(parsed.options.duotone);const result=await withSlopcameraOperationHostAdmission("slopcamera.image.vectorize",async(lease)=>await(dependencies.vectorize??vectorizeImage)(requiredPositional(parsed,0,"raster image"),{...alphaCutoff===undefined?{}:{alphaCutoff},...duotone===undefined?{}:{duotone},...timeoutMs===undefined?{}:{limits:{maxDurationMs:timeoutMs}},inheritedFileDescriptors:[lease.inheritedFileDescriptor],outputPath:output}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify({...result.receipt,outputPath:result.outputPath},null,2))}else{(dependencies.log??console.log)(`Vectorized ${result.receipt.width}\xD7${result.receipt.height} with `+`${result.receipt.profile}/${result.receipt.representation}: ${result.outputPath}`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="generate"){const parsed=parseArguments(rest,new Set(["model","output"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json");if(unknownFlags.length>0){throw new Error(`Unknown generate option: --${unknownFlags[0]}`)}if(parsed.positionals.length!==1){throw new Error("slopcamera image generate accepts exactly one prompt")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.generate",async()=>await(dependencies.generate??generateSlopcameraImageFile)({model,prompt:requiredPositional(parsed,0,"prompt"),outputPath:requiredOption(parsed,"output")}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{(dependencies.log??console.log)(`Generated ${result.mediaType} with ${result.model}: ${result.outputPath} (${result.bytes} bytes, request ${result.requestId})`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="icon"){const parsed=parseArguments(rest,new Set(["model","output","ink","purpose","rounds","critique-model"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json"&&flag!=="keep-raster");if(unknownFlags.length>0){throw new Error(`Unknown icon option: --${unknownFlags[0]}`)}if(parsed.positionals.length!==1){throw new Error("slopcamera image icon accepts exactly one subject")}const output=requiredOption(parsed,"output");if(!output.toLowerCase().endsWith(".svg")){throw new Error("--output must end in .svg")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const critiqueModel=parsed.options["critique-model"];if(critiqueModel!==undefined&&(critiqueModel.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(critiqueModel))){throw new Error("--critique-model must be a bounded Vercel AI Gateway provider/model id")}const ink=parsed.options.ink;if(ink!==undefined&&!/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(ink)){throw new Error("--ink must be a #rgb or #rrggbb color")}const purpose=parsed.options.purpose;if(purpose!==undefined&&purpose!=="mark"&&purpose!=="illustration"){throw new Error("--purpose must be mark or illustration")}const rounds=parsePositiveInteger(parsed.options.rounds,"rounds");if(rounds!==undefined&&rounds>slopcameraIconMaximumRounds){throw new Error(`--rounds must be at most ${slopcameraIconMaximumRounds}`)}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.icon",async(lease)=>await(dependencies.icon??generateSlopcameraIcon)({keepRaster:parsed.flags.has("keep-raster"),model,outputPath:output,subject:requiredPositional(parsed,0,"subject"),inheritedFileDescriptors:[lease.inheritedFileDescriptor],...critiqueModel===undefined?{}:{critiqueModel},...ink===undefined?{}:{ink},...purpose===undefined?{}:{purpose},...rounds===undefined?{}:{rounds}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{const scored=result.attempts.map((attempt)=>attempt.score===undefined?`#${attempt.round}`:`#${attempt.round}=${attempt.score}`).join(" ");(dependencies.log??console.log)(`Icon ${result.svgSha256.slice(0,12)} round ${result.selectedRound}/${result.attempts.length} (${scored}): ${result.outputPath}`)}return}if(command==="gallery"){const parsed=parseArguments(rest,new Set(["model","output-dir","kind","count","vary","candidates","cell","timeout-ms"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json"&&flag!=="tile"&&flag!=="no-tile");if(unknownFlags.length>0){throw new Error(`Unknown gallery option: --${unknownFlags[0]}`)}if(parsed.flags.has("tile")&&parsed.flags.has("no-tile")){throw new Error("--tile and --no-tile are mutually exclusive")}if(parsed.positionals.length!==1){throw new Error("slopcamera image gallery accepts exactly one subject")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const kind=parsed.options.kind;if(kind!==undefined&&!slopcameraGalleryKinds.includes(kind)){throw new Error(`--kind must be one of: ${slopcameraGalleryKinds.join(", ")}`)}const count=parsePositiveInteger(parsed.options.count,"count");if(count!==undefined&&count>slopcameraGalleryLimits.candidates){throw new Error(`--count must be at most ${slopcameraGalleryLimits.candidates}`)}const cell=parsePositiveInteger(parsed.options.cell,"cell");if(cell!==undefined&&(cell<slopcameraGalleryLimits.cellEdgeMin||cell>slopcameraGalleryLimits.cellEdgeMax)){throw new Error(`--cell must be between ${slopcameraGalleryLimits.cellEdgeMin} and ${slopcameraGalleryLimits.cellEdgeMax}`)}const timeoutMs=parsePositiveInteger(parsed.options["timeout-ms"],"timeout-ms");const vary=parseGalleryVary(parsed.options.vary);const candidates=parsed.options.candidates===undefined?undefined:await readGalleryCandidates(parsed.options.candidates);if(candidates!==undefined&&!Array.isArray(candidates)){throw new Error("--candidates JSON must be an array of {id, prompt|variant} entries")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.gallery",async()=>await(dependencies.gallery??generateSlopcameraImageGallery)({subject:requiredPositional(parsed,0,"subject"),outputDir:requiredOption(parsed,"output-dir"),model,...kind===undefined?{}:{kind},...count===undefined?{}:{count},...vary===undefined?{}:{vary},...candidates===undefined?{}:{candidates},...cell===undefined?{}:{cellEdge:cell},...timeoutMs===undefined?{}:{timeoutMs},...parsed.flags.has("tile")?{tiled:true}:parsed.flags.has("no-tile")?{tiled:false}:{}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{const rows=result.candidates.map((candidate)=>candidate.status==="generated"?`#${candidate.index} ${candidate.id} ${candidate.path}`:`#${candidate.index} ${candidate.id} failed`);(dependencies.log??console.log)(`Gallery ${result.counts.generated}/${result.counts.requested} generated (${result.model}): ${result.gallery.path}
|
|
96
|
+
`);console.log(`Created ${filePath}`);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="check"){const parsed=parseArguments(rest,new Set(["config"]));const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.check",async()=>await checkDiagramFile({filePath:requiredPositional(parsed,0,"diagram file"),...parsed.options.config===undefined?{}:{configPath:parsed.options.config}}),hostAdmissionOptions(dependencies));console.log(`Valid diagram${result.configPath===null?"":` with ${result.configPath}`}.`);printFindings(result.findings);if(parsed.flags.has("strict")&&result.findings.length>0)process.exitCode=2;return}if(command==="render"){const parsed=parseArguments(rest,new Set(["out-dir","config","scale"]));const scale=parsed.options.scale===undefined?undefined:Number.parseFloat(parsed.options.scale);const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.render",async()=>await renderDiagramFile({filePath:requiredPositional(parsed,0,"diagram file"),...parsed.options["out-dir"]===undefined?{}:{outDirectory:parsed.options["out-dir"]},...parsed.options.config===undefined?{}:{configPath:parsed.options.config},...scale===undefined?{}:{scale}}),hostAdmissionOptions(dependencies));console.log(artifactSummary(result.artifacts));printFindings(result.findings);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="vectorize"){const parsed=parseArguments(rest,new Set(["output","duotone","alpha-cutoff","timeout-ms"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json");if(unknownFlags.length>0){throw new Error(`Unknown vectorize option: --${unknownFlags[0]}`)}if(parsed.positionals.length>1){throw new Error("slopcamera image vectorize accepts exactly one raster input")}const output=requiredOption(parsed,"output");if(!output.toLowerCase().endsWith(".svg")){throw new Error("--output must end in .svg")}const alphaCutoff=parsePositiveInteger(parsed.options["alpha-cutoff"],"alpha-cutoff");const timeoutMs=parsePositiveInteger(parsed.options["timeout-ms"],"timeout-ms");const duotone=parseDuotone(parsed.options.duotone);const result=await withSlopcameraOperationHostAdmission("slopcamera.image.vectorize",async(lease)=>await(dependencies.vectorize??vectorizeImage)(requiredPositional(parsed,0,"raster image"),{...alphaCutoff===undefined?{}:{alphaCutoff},...duotone===undefined?{}:{duotone},...timeoutMs===undefined?{}:{limits:{maxDurationMs:timeoutMs}},inheritedFileDescriptors:[lease.inheritedFileDescriptor],outputPath:output}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify({...result.receipt,outputPath:result.outputPath},null,2))}else{(dependencies.log??console.log)(`Vectorized ${result.receipt.width}\xD7${result.receipt.height} with `+`${result.receipt.profile}/${result.receipt.representation}: ${result.outputPath}`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="generate"){const parsed=parseArguments(rest,new Set(["model","output"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json");if(unknownFlags.length>0){throw new Error(`Unknown generate option: --${unknownFlags[0]}`)}if(parsed.positionals.length!==1){throw new Error("slopcamera image generate accepts exactly one prompt")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.generate",async()=>await(dependencies.generate??generateSlopcameraImageFile)({model,prompt:requiredPositional(parsed,0,"prompt"),outputPath:requiredOption(parsed,"output")}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{(dependencies.log??console.log)(`Generated ${result.mediaType} with ${result.model}: ${result.outputPath} (${result.bytes} bytes, request ${result.requestId})`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="icon"){const parsed=parseArguments(rest,new Set(["model","output","output-dir","ink","purpose","rounds","critique-model","context","candidates","set","set-rounds"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json"&&flag!=="keep-raster");if(unknownFlags.length>0){throw new Error(`Unknown icon option: --${unknownFlags[0]}`)}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const critiqueModel=parsed.options["critique-model"];if(critiqueModel!==undefined&&(critiqueModel.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(critiqueModel))){throw new Error("--critique-model must be a bounded Vercel AI Gateway provider/model id")}const ink=parsed.options.ink;if(ink!==undefined&&!/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(ink)){throw new Error("--ink must be a #rgb or #rrggbb color")}const purpose=parsed.options.purpose;if(purpose!==undefined&&purpose!=="mark"&&purpose!=="illustration"){throw new Error("--purpose must be mark or illustration")}const context=parsed.options.context;if(context!==undefined&&!slopcameraIconContexts.includes(context)){throw new Error(`--context must be one of: ${slopcameraIconContexts.join(", ")}`)}const rounds=parsePositiveInteger(parsed.options.rounds,"rounds");if(rounds!==undefined&&rounds>slopcameraIconMaximumRounds){throw new Error(`--rounds must be at most ${slopcameraIconMaximumRounds}`)}const candidates=parsePositiveInteger(parsed.options.candidates,"candidates");if(candidates!==undefined&&candidates>slopcameraIconMaximumRounds){throw new Error(`--candidates must be at most ${slopcameraIconMaximumRounds}`)}const setRounds=parsePositiveInteger(parsed.options["set-rounds"],"set-rounds");if(setRounds!==undefined&&setRounds>slopcameraIconSetMaximumSetRounds){throw new Error(`--set-rounds must be at most ${slopcameraIconSetMaximumSetRounds}`)}if(parsed.options.set!==undefined){if(parsed.positionals.length!==0){throw new Error("slopcamera image icon --set takes no subject positional")}if(parsed.options.output!==undefined){throw new Error("--output cannot be combined with --set; use --output-dir")}const outputDir=requiredOption(parsed,"output-dir");const manifestPath=parsed.options.set;let manifestText;try{manifestText=await readFile(manifestPath,"utf8")}catch{throw new Error(`--set manifest could not be read: ${manifestPath}`)}if(Buffer.byteLength(manifestText,"utf8")>256*1024){throw new Error("--set manifest exceeds the 256 KiB bound")}let manifestJson;try{manifestJson=JSON.parse(manifestText)}catch{throw new Error("--set manifest must be valid JSON")}const spec=parseSlopcameraIconSetSpec(manifestJson);const result2=await withSlopcameraOperationHostAdmission("slopcamera.image.icon",async(lease)=>await(dependencies.iconSet??generateSlopcameraIconSet)({keepRaster:parsed.flags.has("keep-raster"),manifestDir:dirname(resolve(manifestPath)),model,outputDir,spec,inheritedFileDescriptors:[lease.inheritedFileDescriptor],...critiqueModel===undefined?{}:{critiqueModel},...ink===undefined?{}:{ink},...context===undefined?{}:{context},...purpose===undefined?{}:{purpose},...rounds===undefined?{}:{rounds},...candidates===undefined?{}:{candidatesPerMember:candidates},...setRounds===undefined?{}:{setRounds}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result2,null,2))}else{(dependencies.log??console.log)(`Icon set ${result2.name??"icon-set"}: ${result2.members.length} members to ${result2.outputDir} `+`(target coverage ${result2.target.coverageRatio.toFixed(3)}, `+`stroke ${result2.target.strokePx.toFixed(1)}px, ${result2.setRoundsUsed} set rounds)`)}reportUsefulResult(dependencies.onUsefulResult);return}if(parsed.positionals.length!==1){throw new Error("slopcamera image icon accepts exactly one subject")}const output=requiredOption(parsed,"output");if(!output.toLowerCase().endsWith(".svg")){throw new Error("--output must end in .svg")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.icon",async(lease)=>await(dependencies.icon??generateSlopcameraIcon)({keepRaster:parsed.flags.has("keep-raster"),model,outputPath:output,subject:requiredPositional(parsed,0,"subject"),inheritedFileDescriptors:[lease.inheritedFileDescriptor],...critiqueModel===undefined?{}:{critiqueModel},...ink===undefined?{}:{ink},...purpose===undefined?{}:{purpose},...rounds===undefined?{}:{rounds},...context===undefined?{}:{context},...candidates===undefined?{}:{candidatePool:candidates}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{const scored=result.attempts.map((attempt)=>attempt.score===undefined?`#${attempt.round}`:`#${attempt.round}=${attempt.score}`).join(" ");(dependencies.log??console.log)(`Icon ${result.svgSha256.slice(0,12)} round ${result.selectedRound}/${result.attempts.length} (${scored}): ${result.outputPath}`)}return}if(command==="gallery"){const parsed=parseArguments(rest,new Set(["model","output-dir","kind","count","vary","candidates","cell","timeout-ms"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json"&&flag!=="tile"&&flag!=="no-tile");if(unknownFlags.length>0){throw new Error(`Unknown gallery option: --${unknownFlags[0]}`)}if(parsed.flags.has("tile")&&parsed.flags.has("no-tile")){throw new Error("--tile and --no-tile are mutually exclusive")}if(parsed.positionals.length!==1){throw new Error("slopcamera image gallery accepts exactly one subject")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const kind=parsed.options.kind;if(kind!==undefined&&!slopcameraGalleryKinds.includes(kind)){throw new Error(`--kind must be one of: ${slopcameraGalleryKinds.join(", ")}`)}const count=parsePositiveInteger(parsed.options.count,"count");if(count!==undefined&&count>slopcameraGalleryLimits.candidates){throw new Error(`--count must be at most ${slopcameraGalleryLimits.candidates}`)}const cell=parsePositiveInteger(parsed.options.cell,"cell");if(cell!==undefined&&(cell<slopcameraGalleryLimits.cellEdgeMin||cell>slopcameraGalleryLimits.cellEdgeMax)){throw new Error(`--cell must be between ${slopcameraGalleryLimits.cellEdgeMin} and ${slopcameraGalleryLimits.cellEdgeMax}`)}const timeoutMs=parsePositiveInteger(parsed.options["timeout-ms"],"timeout-ms");const vary=parseGalleryVary(parsed.options.vary);const candidates=parsed.options.candidates===undefined?undefined:await readGalleryCandidates(parsed.options.candidates);if(candidates!==undefined&&!Array.isArray(candidates)){throw new Error("--candidates JSON must be an array of {id, prompt|variant} entries")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.gallery",async()=>await(dependencies.gallery??generateSlopcameraImageGallery)({subject:requiredPositional(parsed,0,"subject"),outputDir:requiredOption(parsed,"output-dir"),model,...kind===undefined?{}:{kind},...count===undefined?{}:{count},...vary===undefined?{}:{vary},...candidates===undefined?{}:{candidates},...cell===undefined?{}:{cellEdge:cell},...timeoutMs===undefined?{}:{timeoutMs},...parsed.flags.has("tile")?{tiled:true}:parsed.flags.has("no-tile")?{tiled:false}:{}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{const rows=result.candidates.map((candidate)=>candidate.status==="generated"?`#${candidate.index} ${candidate.id} ${candidate.path}`:`#${candidate.index} ${candidate.id} failed`);(dependencies.log??console.log)(`Gallery ${result.counts.generated}/${result.counts.requested} generated (${result.model}): ${result.gallery.path}
|
|
86
97
|
`+`${rows.join(`
|
|
87
98
|
`)}
|
|
88
99
|
receipt ${result.receiptPath}`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="code"){const[subcommand,...subcommandArgs]=rest;if(subcommand==="search"){const parsed=parseArguments(subcommandArgs,new Set(["limit"]));if(parsed.flags.size>0||parsed.positionals.length>1){throw new Error("Use slopcamera code search [query] [--limit <number>]")}const limit=parsePositiveInteger(parsed.options.limit,"limit")??slopcameraOperationCodes.length;const operations=searchSlopcameraOperations(parsed.positionals[0]??"",limit);console.log(JSON.stringify({operations},null,2));return}if(subcommand==="execute"){const parsed=parseArguments(subcommandArgs,new Set(["input"]));if(parsed.flags.size>0||parsed.positionals.length!==1){throw new Error("Use slopcamera code execute <operation> --input <JSON>")}const requestedOperation=parsed.positionals[0];const operation=isSlopcameraOperationCode(requestedOperation)?requestedOperation:undefined;if(operation===undefined){throw new Error(`Unknown Slopcamera operation code: ${requestedOperation}`)}const inputText=requiredOption(parsed,"input");if(Buffer.byteLength(inputText,"utf8")>64*1024){throw new Error("--input JSON must be no more than 65536 UTF-8 bytes")}let input;try{input=JSON.parse(inputText)}catch{throw new Error("--input must be valid JSON")}const result=await executeSlopcameraOperation(operation,input,{...hostAdmissionOptions(dependencies)});(dependencies.log??console.log)(JSON.stringify({operation,result},null,2));if(operation==="slopcamera.diagram.render"||operation==="slopcamera.image.vectorize"||operation==="slopcamera.image.generate"){reportUsefulResult(dependencies.onUsefulResult)}return}throw new Error("Use slopcamera code search [query] or slopcamera code execute <operation> --input <JSON>")}if(command==="mcp"){const parsed=parseArguments(rest,new Set(["root"]));if(parsed.positionals.length>0||parsed.flags.size>0){throw new Error("slopcamera mcp accepts only --root <workspace>")}await runMcpServer({rootDirectory:requiredOption(parsed,"root"),serverVersion:slopcameraCliVersion});return}if(command==="doctor"){console.log(`slopcamera ${slopcameraCliVersion}`);console.log(`Bun ${process.versions.bun??"not detected"}`);console.log("Headless diagram SVG/PNG/tldraw renderer ready");console.log(process.platform==="win32"?"Local raster-to-SVG vectorizer unavailable on Windows (fails closed with tool_platform)":"Local raster-to-SVG vectorizer ready without authentication (VTracer downloads on first use)");console.log("Root-relative MCP check/render/scene server ready (trusted local workspace)");const gateway=slopcameraGatewayCredentialStatus();console.log(gateway.available?`Vercel AI Gateway ready via ${gateway.source}`:"Vercel AI Gateway requires AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN");return}if(command==="skill"){const[subcommand,...subcommandArgs]=rest;if(subcommand==="path"){const{bundledSkillPath}=await import("./skill-install-gx7afxsd.js");console.log(bundledSkillPath());return}if(subcommand==="install"){const parsed=parseArguments(subcommandArgs,new Set(["target","scope","project"]));const target=parsed.options.target??"codex";const scope=parsed.options.scope??"user";if(!["codex","claude","agents"].includes(target)){throw new Error("--target must be codex, claude, or agents")}if(!["user","project"].includes(scope)){throw new Error("--scope must be user or project")}const destination=await installSkill({target,scope,...parsed.options.project===undefined?{}:{projectDirectory:parsed.options.project},force:parsed.flags.has("force")});console.log(`Installed slopcamera skill at ${destination}`);return}throw new Error("Use slopcamera skill path or install")}throw new Error(`Unknown command: ${command}
|
package/dist/generate.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-
|
|
2
|
+
import{createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-p74xy76d.js";import"./index-80a36bc0.js";export{slopcameraResponseMediaTypes,slopcameraMaximumRawImageBytes,slopcameraMaximumPromptBytes,slopcameraImageModels,slopcameraGatewayCredentialStatus,slopcameraGatewayApiBaseUrl,resolveSlopcameraGatewayCredential,generateSlopcameraImageFile,generateSlopcameraImage,createFixedGatewayFetch};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{executeSlopcameraOperationWithLease,isSlopcameraOperationCode,parseSlopcameraOperationInput,slopcameraOperationHostResourceClaims}from"./index-
|
|
2
|
+
import{executeSlopcameraOperationWithLease,isSlopcameraOperationCode,parseSlopcameraOperationInput,slopcameraOperationHostResourceClaims}from"./index-y8spxs77.js";import{createDefaultHostResourceCoordinator}from"./index-b3geqczn.js";var workflowIdPattern=/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;var workflowStepIdPattern=/^[A-Za-z0-9]+(?:[._:-][A-Za-z0-9]+)*$/u;var defaultMaximumSteps=64;var hardMaximumSteps=256;class SlopcameraWorkflowError extends Error{code;completedSteps;failedStep;constructor(code,message,options={}){super(`[${code}] ${message}`,{cause:options.cause});this.name="SlopcameraWorkflowError";this.code=code;this.completedSteps=Object.freeze([...options.completedSteps??[]].sort((left,right)=>left.index-right.index));if(options.failedStep!==undefined){this.failedStep=Object.freeze({...options.failedStep})}}}function workflowError(code,message){throw new SlopcameraWorkflowError(code,message)}function validateWorkflowId(id){if(typeof id!=="string"||id.length<1||id.length>80||!workflowIdPattern.test(id)){workflowError("INVALID_WORKFLOW","Workflow id must be 1 through 80 lowercase letters, numbers, dots, underscores, or hyphens.")}}function validateStepId(id,completedSteps=[]){if(typeof id!=="string"||id.length<1||id.length>80||!workflowStepIdPattern.test(id)){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP","Step id must be 1 through 80 letters, numbers, dots, underscores, colons, or hyphens.",{completedSteps})}}function defineSlopcameraWorkflow(options){if(typeof options!=="object"||options===null){workflowError("INVALID_WORKFLOW","Workflow definition must be an object.")}validateWorkflowId(options.id);if(!Number.isSafeInteger(options.version)||options.version<1){workflowError("INVALID_WORKFLOW","Workflow version must be a positive safe integer.")}if(typeof options.parseInput!=="function"||typeof options.run!=="function"){workflowError("INVALID_WORKFLOW","Workflow definition requires parseInput and run functions.")}return Object.freeze({id:options.id,version:options.version,parseInput:options.parseInput,run:options.run})}function maximumSteps(value){const resolved=value??defaultMaximumSteps;if(!Number.isSafeInteger(resolved)||resolved<1||resolved>hardMaximumSteps){workflowError("INVALID_WORKFLOW",`maximumSteps must be an integer from 1 through ${hardMaximumSteps}.`)}return resolved}function aborted(completedSteps,cause){return new SlopcameraWorkflowError("WORKFLOW_ABORTED","Workflow execution was aborted.",{cause,completedSteps})}async function runSlopcameraWorkflow(definition,value,options={}){const normalized=defineSlopcameraWorkflow(definition);const limit=maximumSteps(options.maximumSteps);const signal=options.signal??options.dependencies?.signal??new AbortController().signal;const invoked=new Set;const completed=[];const dispatched=[];let acceptingOperations=true;let nextIndex=0;if(signal.aborted)throw aborted(completed);let input;try{input=normalized.parseInput(value)}catch(cause){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_INPUT","Workflow input did not satisfy its parser.",{cause})}const executor=options.executor??((code,operationInput,context2)=>executeSlopcameraOperationWithLease(code,operationInput,context2.hostResourceLease,options.dependencies));const hostResourceCoordinator=options.hostResourceCoordinator??options.dependencies?.hostResourceCoordinator??createDefaultHostResourceCoordinator();const waitTimeoutMilliseconds=options.waitTimeoutMilliseconds??options.dependencies?.waitTimeoutMilliseconds;async function dispatchOperation(id,code,operationInput){if(signal.aborted)throw aborted(completed);validateStepId(id,completed);if(!isSlopcameraOperationCode(code)){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Workflow step ${id} names an unknown Slopcamera operation.`,{completedSteps:completed})}if(invoked.has(id)){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Duplicate workflow step id: ${id}.`,{completedSteps:completed})}if(nextIndex>=limit){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Workflow exceeds its ${String(limit)}-step limit.`,{completedSteps:completed})}const index=nextIndex;nextIndex+=1;invoked.add(id);let normalizedInput;try{normalizedInput=parseSlopcameraOperationInput(code,operationInput)}catch(cause){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Workflow step ${id} has invalid input for ${code}.`,{cause,completedSteps:completed})}try{const result=await hostResourceCoordinator.withLease(slopcameraOperationHostResourceClaims(code),async(hostResourceLease)=>await executor(code,normalizedInput,{hostResourceLease,signal,stepId:id}),{signal,...waitTimeoutMilliseconds===undefined?{}:{waitTimeoutMilliseconds}});completed.push(Object.freeze({id,index,operation:code}));if(signal.aborted)throw aborted(completed);return result}catch(cause){if(cause instanceof SlopcameraWorkflowError&&cause.code==="WORKFLOW_ABORTED"){throw cause}if(signal.aborted)throw aborted(completed,cause);throw new SlopcameraWorkflowError("WORKFLOW_STEP_FAILED",`Workflow step ${id} (${code}) failed.`,{cause,completedSteps:completed,failedStep:{id,operation:code}})}}const context=Object.freeze({signal,operation(id,code,operationInput){if(!acceptingOperations){const closed=Promise.reject(new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP","Workflow operations cannot start after authored workflow code has settled.",{completedSteps:completed}));closed.catch(()=>{return});return closed}const operation=dispatchOperation(id,code,operationInput);dispatched.push(operation);operation.catch(()=>{return});return operation}});let output;let runFailed=false;let runFailure;try{output=await normalized.run(context,input)}catch(cause){runFailed=true;runFailure=cause}finally{acceptingOperations=false}const operationResults=await Promise.allSettled(dispatched);if(signal.aborted)throw aborted(completed,runFailed?runFailure:undefined);if(runFailed){if(runFailure instanceof SlopcameraWorkflowError)throw runFailure;throw new SlopcameraWorkflowError("WORKFLOW_FAILED",`Workflow ${normalized.id} failed in authored code.`,{cause:runFailure,completedSteps:completed})}const operationFailure=operationResults.find((result)=>result.status==="rejected");if(operationFailure!==undefined){const cause=operationFailure.reason;if(cause instanceof SlopcameraWorkflowError)throw cause;throw new SlopcameraWorkflowError("WORKFLOW_STEP_FAILED","A dispatched workflow operation failed.",{cause,completedSteps:completed})}return Object.freeze({workflow:Object.freeze({id:normalized.id,version:normalized.version}),output,steps:Object.freeze([...completed].sort((left,right)=>left.index-right.index))})}
|
|
3
3
|
export{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow};
|