@canvist/headless 0.0.0-stage → 0.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.
- package/dist/calls.d.ts +15 -0
- package/dist/calls.js +20 -0
- package/dist/create.d.ts +17 -0
- package/dist/create.js +29 -0
- package/dist/engine.d.ts +125 -0
- package/dist/engine.js +176 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +5 -0
- package/dist/extensions.generated.d.ts +1022 -0
- package/dist/extensions.generated.js +744 -0
- package/dist/extras.generated.d.ts +5803 -0
- package/dist/extras.generated.js +2600 -0
- package/dist/full.d.ts +21 -0
- package/dist/full.js +63 -0
- package/dist/gemoji.generated.d.ts +29 -0
- package/dist/gemoji.generated.js +30 -0
- package/dist/load.d.ts +21 -0
- package/dist/load.js +41 -0
- package/dist/mod.d.ts +25 -0
- package/dist/mod.js +24 -0
- package/dist/native.d.ts +115 -0
- package/dist/native.js +235 -0
- package/dist/scene-types.d.ts +223 -0
- package/dist/scene-types.js +1 -0
- package/dist/scene.d.ts +105 -0
- package/dist/scene.js +153 -0
- package/dist/ssr.d.ts +66 -0
- package/dist/ssr.js +98 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.js +1 -0
- package/package.json +89 -5
- package/readme.md +96 -0
- package/wasm/canvist_headless_wasm.d.ts +216 -0
- package/wasm/canvist_headless_wasm.js +910 -0
- package/wasm/canvist_headless_wasm_bg.wasm +0 -0
- package/wasm/canvist_headless_wasm_bg.wasm.d.ts +44 -0
- package/wasm-full/canvist_headless_full.d.ts +223 -0
- package/wasm-full/canvist_headless_full.js +923 -0
- package/wasm-full/canvist_headless_full_bg.wasm +0 -0
- package/wasm-full/canvist_headless_full_bg.wasm.d.ts +45 -0
- package/wasm-scene/canvist_headless_scene.d.ts +332 -0
- package/wasm-scene/canvist_headless_scene.js +1231 -0
- package/wasm-scene/canvist_headless_scene_bg.wasm +0 -0
- package/wasm-scene/canvist_headless_scene_bg.wasm.d.ts +66 -0
- package/wasm-ssr/canvist_headless_ssr.d.ts +250 -0
- package/wasm-ssr/canvist_headless_ssr.js +1025 -0
- package/wasm-ssr/canvist_headless_ssr_bg.wasm +0 -0
- package/wasm-ssr/canvist_headless_ssr_bg.wasm.d.ts +50 -0
- package/README.md +0 -3
package/dist/native.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed editor for JavaScript hosts that run canvist natively.
|
|
3
|
+
*
|
|
4
|
+
* `@canvist/react-native` and `@canvist/lynx` show the iOS and Android
|
|
5
|
+
* `CanvistView`s, whose editors live in Rust, and reach each one through a
|
|
6
|
+
* {@link NativeBridge}: a handful of synchronous calls that pass names and
|
|
7
|
+
* JSON. {@link NativeEditor} puts the same typed API as `@canvist/web` and
|
|
8
|
+
* `@canvist/headless` over it, so commands, queries, and documents are
|
|
9
|
+
* typed from the extensions you register and calling one you did not
|
|
10
|
+
* register is a type error.
|
|
11
|
+
*
|
|
12
|
+
* This module loads no WebAssembly: it is types, metadata, and a little
|
|
13
|
+
* glue.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { calls, encode } from "./calls.js";
|
|
18
|
+
import { core } from "./extensions.generated.js";
|
|
19
|
+
export { isCanvistError } from "./errors.js";
|
|
20
|
+
/**
|
|
21
|
+
* A native editor with commands, queries, and document types that follow
|
|
22
|
+
* its extensions. Hosts create it once their view is ready and report
|
|
23
|
+
* changes with {@link NativeEditor.notifyChange} and
|
|
24
|
+
* {@link NativeEditor.notifyFocus}; subscribers then hear of every edit,
|
|
25
|
+
* whether it came from the keyboard or from JavaScript. Edits made from
|
|
26
|
+
* JavaScript notify subscribers once the calls in the current task are
|
|
27
|
+
* done, and again when the view reports them.
|
|
28
|
+
*/
|
|
29
|
+
export class NativeEditor {
|
|
30
|
+
/** Run a registered command against the selection; returns whether it applied. */
|
|
31
|
+
commands;
|
|
32
|
+
/** Whether a registered command would apply. Nothing changes. */
|
|
33
|
+
can;
|
|
34
|
+
/** Answer a registered query. */
|
|
35
|
+
query;
|
|
36
|
+
#bridge;
|
|
37
|
+
#extensions;
|
|
38
|
+
#subscribers = new Set();
|
|
39
|
+
#focused = false;
|
|
40
|
+
/** Whether subscribers will hear of edits made from JavaScript. */
|
|
41
|
+
#pending = false;
|
|
42
|
+
/** An editor over `bridge`, whose view registered `extensions`. */
|
|
43
|
+
constructor(bridge, extensions) {
|
|
44
|
+
this.#bridge = bridge;
|
|
45
|
+
this.#extensions = [...core, ...extensions];
|
|
46
|
+
const commands = this.#extensions.flatMap((extension) => extension.commands.map((command) => command.name));
|
|
47
|
+
const queries = this.#extensions.flatMap((extension) => extension.queries.map((query) => query.name));
|
|
48
|
+
this.commands = calls(commands, (name, args) => this.#edit(() => bridge.run(name, encode(args))));
|
|
49
|
+
this.can = calls(commands, (name, args) => this.#call(() => bridge.can(name, encode(args))));
|
|
50
|
+
this.query = calls(queries, (name, args) => JSON.parse(this.#call(() => bridge.query(name, encode(args)))));
|
|
51
|
+
}
|
|
52
|
+
/** The registered extensions, including the ones every editor has. */
|
|
53
|
+
get extensions() {
|
|
54
|
+
return this.#extensions;
|
|
55
|
+
}
|
|
56
|
+
/** Run the command named `name`, for callers that pick commands at runtime. */
|
|
57
|
+
run(name, ...args) {
|
|
58
|
+
return this.#edit(() => this.#bridge.run(name, encode(args[0])));
|
|
59
|
+
}
|
|
60
|
+
/** Whether the command named `name` would apply. Nothing changes. */
|
|
61
|
+
canRun(name, ...args) {
|
|
62
|
+
return this.#call(() => this.#bridge.can(name, encode(args[0])));
|
|
63
|
+
}
|
|
64
|
+
/** Answer the query named `name`, for callers that pick queries at runtime. */
|
|
65
|
+
ask(name, ...args) {
|
|
66
|
+
return JSON.parse(this.#call(() => this.#bridge.query(name, encode(args[0]))));
|
|
67
|
+
}
|
|
68
|
+
/** Replace the document. */
|
|
69
|
+
setContent(content) {
|
|
70
|
+
const [format, input] = "format" in content
|
|
71
|
+
? [content.format, content.input]
|
|
72
|
+
: ["json", JSON.stringify(content)];
|
|
73
|
+
this.#edit(() => this.#bridge.load(format, input));
|
|
74
|
+
}
|
|
75
|
+
/** The document as `format`. */
|
|
76
|
+
export(format) {
|
|
77
|
+
return this.#call(() => this.#bridge.export(format));
|
|
78
|
+
}
|
|
79
|
+
/** The document as ProseMirror JSON, with block ids in `attrs.id`. */
|
|
80
|
+
toJson() {
|
|
81
|
+
return JSON.parse(this.#call(() => this.#bridge.export("json")));
|
|
82
|
+
}
|
|
83
|
+
/** The text selection, or `null` for a block selection. */
|
|
84
|
+
get selection() {
|
|
85
|
+
return JSON.parse(this.#call(() => this.#bridge.selection()));
|
|
86
|
+
}
|
|
87
|
+
/** Select from `anchor` to `head`, or place a caret at `anchor`. Offsets count UTF-16 code units. */
|
|
88
|
+
select(anchor, head = anchor) {
|
|
89
|
+
const range = { anchor, head };
|
|
90
|
+
this.#edit(() => this.#bridge.select(JSON.stringify(range)));
|
|
91
|
+
}
|
|
92
|
+
/** Select the whole document. */
|
|
93
|
+
selectAll() {
|
|
94
|
+
this.#edit(() => this.#bridge.selectAll());
|
|
95
|
+
}
|
|
96
|
+
/** Replace the selection with `text`, as typing does (input rules apply). */
|
|
97
|
+
insertText(text) {
|
|
98
|
+
return this.#edit(() => this.#bridge.insertText(text));
|
|
99
|
+
}
|
|
100
|
+
/** Undo the last change. */
|
|
101
|
+
undo() {
|
|
102
|
+
return this.#edit(() => this.#bridge.undo());
|
|
103
|
+
}
|
|
104
|
+
/** Redo the last undone change. */
|
|
105
|
+
redo() {
|
|
106
|
+
return this.#edit(() => this.#bridge.redo());
|
|
107
|
+
}
|
|
108
|
+
/** Whether {@link NativeEditor.undo} has anything to undo. */
|
|
109
|
+
canUndo() {
|
|
110
|
+
return this.#call(() => this.#bridge.canUndo());
|
|
111
|
+
}
|
|
112
|
+
/** Whether {@link NativeEditor.redo} has anything to redo. */
|
|
113
|
+
canRedo() {
|
|
114
|
+
return this.#call(() => this.#bridge.canRedo());
|
|
115
|
+
}
|
|
116
|
+
/** Focus the editor and show the keyboard. */
|
|
117
|
+
focus() {
|
|
118
|
+
this.#call(() => this.#bridge.focus());
|
|
119
|
+
}
|
|
120
|
+
/** Take focus away from the editor and hide the keyboard. */
|
|
121
|
+
blur() {
|
|
122
|
+
this.#call(() => this.#bridge.blur());
|
|
123
|
+
}
|
|
124
|
+
/** Whether the editor has focus, as the view last reported. */
|
|
125
|
+
get focused() {
|
|
126
|
+
return this.#focused;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Call `listener` after every change to the document, the selection, or
|
|
130
|
+
* focus. Returns a function that stops it.
|
|
131
|
+
*/
|
|
132
|
+
subscribe(listener) {
|
|
133
|
+
this.#subscribers.add(listener);
|
|
134
|
+
return () => {
|
|
135
|
+
this.#subscribers.delete(listener);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** For hosts: the view reported that the document or selection changed. */
|
|
139
|
+
notifyChange() {
|
|
140
|
+
this.#notify();
|
|
141
|
+
}
|
|
142
|
+
/** For hosts: the view gained or lost focus. */
|
|
143
|
+
notifyFocus(focused) {
|
|
144
|
+
if (focused === this.#focused) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.#focused = focused;
|
|
148
|
+
this.#notify();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Make `call`, which may change the document or selection, and tell
|
|
152
|
+
* subscribers once the current task's calls are done: a handler that
|
|
153
|
+
* selects and then formats re-renders once. The view reports the change
|
|
154
|
+
* too, a moment later, but a command that only changes what typing will
|
|
155
|
+
* insert (a toggled mark at the caret) has nothing else to report.
|
|
156
|
+
*/
|
|
157
|
+
#edit(call) {
|
|
158
|
+
const result = this.#call(call);
|
|
159
|
+
if (!this.#pending) {
|
|
160
|
+
this.#pending = true;
|
|
161
|
+
// A microtask; Lynx's JavaScript engine has no `queueMicrotask`.
|
|
162
|
+
void Promise.resolve().then(() => {
|
|
163
|
+
this.#pending = false;
|
|
164
|
+
this.#notify();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/** Make `call` on the bridge, rethrowing its failures as `CanvistError`s. */
|
|
170
|
+
#call(call) {
|
|
171
|
+
try {
|
|
172
|
+
return call();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw nativeError(error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
#notify() {
|
|
179
|
+
for (const listener of this.#subscribers) {
|
|
180
|
+
listener();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const NATIVE_ERROR = /\[canvist:(\w+)\] ([\s\S]*)$/;
|
|
185
|
+
/** Every error code, so a code from native text can be checked. */
|
|
186
|
+
const ERROR_CODES = {
|
|
187
|
+
config: true,
|
|
188
|
+
parse: true,
|
|
189
|
+
schema: true,
|
|
190
|
+
unknownName: true,
|
|
191
|
+
invalidArgs: true,
|
|
192
|
+
invalidPosition: true,
|
|
193
|
+
invalidKey: true,
|
|
194
|
+
editor: true,
|
|
195
|
+
snapshot: true,
|
|
196
|
+
notShared: true,
|
|
197
|
+
alreadyShared: true,
|
|
198
|
+
collab: true,
|
|
199
|
+
render: true,
|
|
200
|
+
};
|
|
201
|
+
function isErrorCode(code) {
|
|
202
|
+
return Object.hasOwn(ERROR_CODES, code);
|
|
203
|
+
}
|
|
204
|
+
/** A failure on the native side, with the code its message carries. */
|
|
205
|
+
class NativeCanvistError extends Error {
|
|
206
|
+
name = "CanvistError";
|
|
207
|
+
code;
|
|
208
|
+
constructor(code, reason, cause) {
|
|
209
|
+
super(reason, cause === undefined ? {} : { cause });
|
|
210
|
+
this.code = code;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* `error` as a `CanvistError` when its message follows the bridge's
|
|
215
|
+
* `[canvist:<code>] <reason>` contract, and unchanged otherwise.
|
|
216
|
+
*/
|
|
217
|
+
function nativeError(error) {
|
|
218
|
+
if (!(error instanceof Error)) {
|
|
219
|
+
return error;
|
|
220
|
+
}
|
|
221
|
+
const [, code = "", reason = ""] = NATIVE_ERROR.exec(error.message) ?? [];
|
|
222
|
+
return isErrorCode(code)
|
|
223
|
+
? new NativeCanvistError(code, reason, error)
|
|
224
|
+
: error;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* A `CanvistError` for a failure a native view reports outside a call,
|
|
228
|
+
* such as an editor it could not create. A code canvist does not define
|
|
229
|
+
* is reported as `editor`, with the code kept in the message.
|
|
230
|
+
*/
|
|
231
|
+
export function nativeCanvistError(code, reason) {
|
|
232
|
+
return isErrorCode(code)
|
|
233
|
+
? new NativeCanvistError(code, reason)
|
|
234
|
+
: new NativeCanvistError("editor", `${code}: ${reason}`);
|
|
235
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene JSON format: canvist canvas documents of pages, frames, shapes,
|
|
3
|
+
* images, vector paths, and text frames.
|
|
4
|
+
*
|
|
5
|
+
* These types mirror `canvist_scene`'s JSON codec. A scene can be written
|
|
6
|
+
* nested, with `children` and without ids or keys, which is how people and
|
|
7
|
+
* agents write one; {@link SceneDoc.toJson} returns the canonical flat form,
|
|
8
|
+
* where every node names its `parent` and `key`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { DocJson, Registered, StarterKit } from "./types.ts";
|
|
13
|
+
/** A colour as `#rrggbb` or `#rrggbbaa`. */
|
|
14
|
+
export type HexColor = `#${string}`;
|
|
15
|
+
/** A node id, `client.seq` in base 36, such as `"1.a"`. */
|
|
16
|
+
export type NodeId = string;
|
|
17
|
+
/** A 2D point or vector: `[x, y]`. */
|
|
18
|
+
export type Vec2 = readonly [number, number];
|
|
19
|
+
/** An affine transform's coefficients: `[a, b, c, d, e, f]`. */
|
|
20
|
+
export type Affine = readonly [number, number, number, number, number, number];
|
|
21
|
+
/** How a paint or layer blends with what is below it. */
|
|
22
|
+
export type BlendMode = "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity";
|
|
23
|
+
/** One stop of a gradient. */
|
|
24
|
+
export interface GradientStop {
|
|
25
|
+
/** Where the stop sits, 0 to 1. */
|
|
26
|
+
readonly offset: number;
|
|
27
|
+
readonly color: HexColor;
|
|
28
|
+
}
|
|
29
|
+
/** Fields every paint may have. */
|
|
30
|
+
interface PaintCommon {
|
|
31
|
+
/** 0 to 1. Defaults to 1. */
|
|
32
|
+
readonly opacity?: number;
|
|
33
|
+
/** Defaults to `true`. */
|
|
34
|
+
readonly visible?: boolean;
|
|
35
|
+
/** Defaults to `"normal"`. */
|
|
36
|
+
readonly blend?: BlendMode;
|
|
37
|
+
}
|
|
38
|
+
/** A fill or stroke. Gradient points are in the node's unit box. */
|
|
39
|
+
export type PaintJson = PaintCommon & ({
|
|
40
|
+
readonly type: "solid";
|
|
41
|
+
readonly color: HexColor;
|
|
42
|
+
} | {
|
|
43
|
+
readonly type: "linear";
|
|
44
|
+
readonly from: Vec2;
|
|
45
|
+
readonly to: Vec2;
|
|
46
|
+
readonly stops: readonly GradientStop[];
|
|
47
|
+
} | {
|
|
48
|
+
readonly type: "radial";
|
|
49
|
+
readonly center: Vec2;
|
|
50
|
+
readonly radius: Vec2;
|
|
51
|
+
readonly stops: readonly GradientStop[];
|
|
52
|
+
} | {
|
|
53
|
+
readonly type: "sweep";
|
|
54
|
+
readonly center: Vec2;
|
|
55
|
+
/** Degrees clockwise from the positive x axis. */
|
|
56
|
+
readonly angle: number;
|
|
57
|
+
readonly stops: readonly GradientStop[];
|
|
58
|
+
} | {
|
|
59
|
+
readonly type: "image";
|
|
60
|
+
/** The host's id for the image, such as a URL or content hash. */
|
|
61
|
+
readonly asset: string;
|
|
62
|
+
/** Defaults to `"fill"`. */
|
|
63
|
+
readonly fit?: "fill" | "fit" | "stretch" | "tile";
|
|
64
|
+
});
|
|
65
|
+
/** How a node's strokes are drawn. Every field has a default. */
|
|
66
|
+
export interface StrokeJson {
|
|
67
|
+
readonly width?: number;
|
|
68
|
+
readonly align?: "inside" | "center" | "outside";
|
|
69
|
+
readonly cap?: "butt" | "round" | "square";
|
|
70
|
+
readonly join?: "miter" | "round" | "bevel";
|
|
71
|
+
readonly miterLimit?: number;
|
|
72
|
+
/** Alternating dash and gap lengths; empty for a solid line. */
|
|
73
|
+
readonly dashes?: readonly number[];
|
|
74
|
+
}
|
|
75
|
+
/** A shadow's colour, offset, blur, and spread. */
|
|
76
|
+
interface ShadowJson {
|
|
77
|
+
readonly color: HexColor;
|
|
78
|
+
readonly offset?: Vec2;
|
|
79
|
+
readonly blur?: number;
|
|
80
|
+
readonly spread?: number;
|
|
81
|
+
}
|
|
82
|
+
/** A shadow or blur on a node. */
|
|
83
|
+
export type EffectJson = {
|
|
84
|
+
readonly visible?: boolean;
|
|
85
|
+
} & (({
|
|
86
|
+
readonly type: "drop-shadow";
|
|
87
|
+
} & ShadowJson) | ({
|
|
88
|
+
readonly type: "inner-shadow";
|
|
89
|
+
} & ShadowJson) | {
|
|
90
|
+
readonly type: "layer-blur";
|
|
91
|
+
readonly radius: number;
|
|
92
|
+
} | {
|
|
93
|
+
readonly type: "background-blur";
|
|
94
|
+
readonly radius: number;
|
|
95
|
+
});
|
|
96
|
+
/** A node's own geometry. Most node types imply theirs. */
|
|
97
|
+
export type ShapeJson = {
|
|
98
|
+
readonly type: "none";
|
|
99
|
+
} | {
|
|
100
|
+
readonly type: "rect";
|
|
101
|
+
} | {
|
|
102
|
+
readonly type: "ellipse";
|
|
103
|
+
/** Arc start in degrees. */
|
|
104
|
+
readonly start?: number;
|
|
105
|
+
/** Arc sweep in degrees; 360 for a full ellipse. */
|
|
106
|
+
readonly sweep?: number;
|
|
107
|
+
/** Inner radius as a fraction of the outer; 0 for a disc. */
|
|
108
|
+
readonly inner?: number;
|
|
109
|
+
} | {
|
|
110
|
+
readonly type: "polygon";
|
|
111
|
+
/** Outer points, at least 3. */
|
|
112
|
+
readonly points?: number;
|
|
113
|
+
/** Inner radius as a fraction of the outer; 1 for a plain polygon. */
|
|
114
|
+
readonly ratio?: number;
|
|
115
|
+
} | {
|
|
116
|
+
readonly type: "path";
|
|
117
|
+
/** SVG path data in the node's own coordinates. */
|
|
118
|
+
readonly d: string;
|
|
119
|
+
readonly rule?: "nonzero" | "evenodd";
|
|
120
|
+
} | {
|
|
121
|
+
readonly type: "boolean";
|
|
122
|
+
readonly op: "union" | "subtract" | "intersect" | "exclude";
|
|
123
|
+
} | {
|
|
124
|
+
readonly type: "text";
|
|
125
|
+
readonly sizing?: "fixed" | "auto-height" | "auto-width";
|
|
126
|
+
} | {
|
|
127
|
+
readonly type: "instance";
|
|
128
|
+
readonly component: NodeId;
|
|
129
|
+
};
|
|
130
|
+
/** How a node follows its parent frame when the frame is resized. */
|
|
131
|
+
export type Constraint = "start" | "end" | "both" | "center" | "scale";
|
|
132
|
+
/** A frame's auto-layout. */
|
|
133
|
+
export interface AutoLayoutJson {
|
|
134
|
+
readonly direction?: "row" | "column";
|
|
135
|
+
readonly gap?: number;
|
|
136
|
+
/** One number for every side, or `[top, right, bottom, left]`. */
|
|
137
|
+
readonly padding?: number | readonly [number, number, number, number];
|
|
138
|
+
readonly align?: Align;
|
|
139
|
+
readonly crossAlign?: Align;
|
|
140
|
+
readonly wrap?: boolean;
|
|
141
|
+
}
|
|
142
|
+
/** How auto-layout places children along an axis. */
|
|
143
|
+
export type Align = "start" | "center" | "end" | "space-between" | "stretch";
|
|
144
|
+
/** How a child of an auto-layout frame is sized on each axis. */
|
|
145
|
+
export type Sizing = "fixed" | "fill" | "hug";
|
|
146
|
+
/**
|
|
147
|
+
* A node's properties. Every one is optional and defaults by node type, so
|
|
148
|
+
* a node lists only what differs. Custom properties are `c:<name>`, and an
|
|
149
|
+
* instance's overrides are `o:<node id>:<property>`.
|
|
150
|
+
*/
|
|
151
|
+
export interface PropsJson {
|
|
152
|
+
readonly name?: string;
|
|
153
|
+
readonly visible?: boolean;
|
|
154
|
+
readonly locked?: boolean;
|
|
155
|
+
/** Position, rotation, and skew; size lives in `size`. */
|
|
156
|
+
readonly transform?: Affine;
|
|
157
|
+
readonly size?: Vec2;
|
|
158
|
+
readonly opacity?: number;
|
|
159
|
+
readonly blend?: BlendMode;
|
|
160
|
+
readonly fills?: readonly PaintJson[];
|
|
161
|
+
readonly strokes?: readonly PaintJson[];
|
|
162
|
+
readonly stroke?: StrokeJson;
|
|
163
|
+
readonly effects?: readonly EffectJson[];
|
|
164
|
+
/** Corner radii: top left, top right, bottom right, bottom left. */
|
|
165
|
+
readonly radius?: readonly [number, number, number, number];
|
|
166
|
+
/** Whether a frame clips its children. */
|
|
167
|
+
readonly clip?: boolean;
|
|
168
|
+
readonly constraints?: {
|
|
169
|
+
readonly horizontal?: Constraint;
|
|
170
|
+
readonly vertical?: Constraint;
|
|
171
|
+
};
|
|
172
|
+
/** A frame's auto-layout, or `null` for none. */
|
|
173
|
+
readonly layout?: AutoLayoutJson | null;
|
|
174
|
+
readonly layoutChild?: {
|
|
175
|
+
readonly width?: Sizing;
|
|
176
|
+
readonly height?: Sizing;
|
|
177
|
+
readonly absolute?: boolean;
|
|
178
|
+
};
|
|
179
|
+
readonly shape?: ShapeJson;
|
|
180
|
+
readonly [custom: `c:${string}`]: unknown;
|
|
181
|
+
readonly [override: `o:${string}`]: unknown;
|
|
182
|
+
}
|
|
183
|
+
/** The built-in node types. */
|
|
184
|
+
export type NodeType = "page" | "frame" | "group" | "rectangle" | "ellipse" | "polygon" | "path" | "boolean" | "image" | "text" | "component" | "instance";
|
|
185
|
+
/** A text frame's document: StarterKit ProseMirror JSON. */
|
|
186
|
+
export type TextJson = DocJson<Registered<StarterKit>>;
|
|
187
|
+
/** Fields every node has, in either form. */
|
|
188
|
+
interface NodeCommon {
|
|
189
|
+
readonly type: NodeType;
|
|
190
|
+
readonly props?: PropsJson;
|
|
191
|
+
/** For text frames. */
|
|
192
|
+
readonly text?: TextJson;
|
|
193
|
+
}
|
|
194
|
+
/** A node in the nested form: ids and keys optional, children inline. */
|
|
195
|
+
export interface NestedNodeJson extends NodeCommon {
|
|
196
|
+
readonly id?: NodeId;
|
|
197
|
+
/** A fractional key among its siblings; list order when omitted. */
|
|
198
|
+
readonly key?: string;
|
|
199
|
+
readonly children?: readonly NestedNodeJson[];
|
|
200
|
+
}
|
|
201
|
+
/** A node in the canonical flat form {@link SceneDoc.toJson} writes. */
|
|
202
|
+
export interface FlatNodeJson extends NodeCommon {
|
|
203
|
+
readonly id: NodeId;
|
|
204
|
+
/** The parent's id, or `"root"` for a page. */
|
|
205
|
+
readonly parent: NodeId;
|
|
206
|
+
readonly key: string;
|
|
207
|
+
}
|
|
208
|
+
/** A scene JSON document. */
|
|
209
|
+
export interface SceneJson<N extends NestedNodeJson | FlatNodeJson = NestedNodeJson | FlatNodeJson> {
|
|
210
|
+
readonly type: "canvist-scene";
|
|
211
|
+
readonly version: 1;
|
|
212
|
+
readonly nodes: readonly N[];
|
|
213
|
+
}
|
|
214
|
+
/** What an export draws. Every field is optional. */
|
|
215
|
+
export interface ExportOptions {
|
|
216
|
+
/** The nodes to draw, with everything inside them. Omit for the first page with its background. */
|
|
217
|
+
readonly nodes?: readonly NodeId[];
|
|
218
|
+
/** Output pixels per world unit. Defaults to 1. */
|
|
219
|
+
readonly scale?: number;
|
|
220
|
+
/** Space around the drawing, in world units. Defaults to 0. */
|
|
221
|
+
readonly padding?: number;
|
|
222
|
+
}
|
|
223
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/scene.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canvist canvas scenes on a server: load scene JSON or Yjs state, run
|
|
3
|
+
* canvas commands and inspector edits, and export SVG and PNG with the
|
|
4
|
+
* renderer the canvas draws with.
|
|
5
|
+
*
|
|
6
|
+
* Scenes live in their own WebAssembly module, so the document entry point
|
|
7
|
+
* stays small.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createSceneEngine } from "@canvist/headless/scene";
|
|
11
|
+
*
|
|
12
|
+
* const engine = await createSceneEngine();
|
|
13
|
+
* using doc = engine.fromJson({
|
|
14
|
+
* type: "canvist-scene",
|
|
15
|
+
* version: 1,
|
|
16
|
+
* nodes: [{
|
|
17
|
+
* type: "page",
|
|
18
|
+
* children: [{
|
|
19
|
+
* type: "rectangle",
|
|
20
|
+
* props: { size: [120, 80], fills: [{ type: "solid", color: "#3b2cf5" }] },
|
|
21
|
+
* }],
|
|
22
|
+
* }],
|
|
23
|
+
* });
|
|
24
|
+
* const png = doc.exportPng({ scale: 2 });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { WasmSceneDoc } from "../wasm-scene/canvist_headless_scene.js";
|
|
30
|
+
import { type WasmSource } from "./load.ts";
|
|
31
|
+
import type { ExportOptions, NodeId, SceneJson } from "./scene-types.ts";
|
|
32
|
+
export { type CanvistError, isCanvistError } from "./engine.ts";
|
|
33
|
+
export type { WasmSource } from "./load.ts";
|
|
34
|
+
export type * from "./scene-types.ts";
|
|
35
|
+
/**
|
|
36
|
+
* Load the scene WebAssembly module once. Later calls return the first
|
|
37
|
+
* call's promise. Without a source, Node, Deno, and Bun read the bundled
|
|
38
|
+
* file; runtimes without a file system pass the module they imported.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadScene(source?: WasmSource): Promise<void>;
|
|
41
|
+
/** How to create a scene engine. */
|
|
42
|
+
export interface SceneEngineOptions {
|
|
43
|
+
/** Allocate node ids, and act as Yjs client, as this id. Defaults to 1. */
|
|
44
|
+
readonly client?: number;
|
|
45
|
+
/** Where the scene module comes from; see {@link loadScene}. */
|
|
46
|
+
readonly wasm?: WasmSource;
|
|
47
|
+
}
|
|
48
|
+
/** Load the scene module and create an engine with every built-in canvas extension. */
|
|
49
|
+
export declare function createSceneEngine(options?: SceneEngineOptions): Promise<SceneEngine>;
|
|
50
|
+
/** A set of canvas extensions that creates scenes. Create one with {@link createSceneEngine}. */
|
|
51
|
+
export declare class SceneEngine {
|
|
52
|
+
#private;
|
|
53
|
+
/** @internal Use {@link createSceneEngine}, which loads the module first. */
|
|
54
|
+
constructor(client: number);
|
|
55
|
+
/** Tools, commands, node types, and inspector sections, as Rust reports them. */
|
|
56
|
+
metadata(): unknown;
|
|
57
|
+
/** A scene with one empty page. */
|
|
58
|
+
create(): SceneDoc;
|
|
59
|
+
/** A scene from scene JSON, nested or flat. */
|
|
60
|
+
fromJson(json: SceneJson): SceneDoc;
|
|
61
|
+
/** A scene joined from a peer's v1 Yjs state. It is shared, so it produces updates. */
|
|
62
|
+
fromYjs(state: Uint8Array): SceneDoc;
|
|
63
|
+
/** Release the engine's memory now rather than when it is collected. */
|
|
64
|
+
[Symbol.dispose](): void;
|
|
65
|
+
}
|
|
66
|
+
/** One scene. Every edit is one undo step. */
|
|
67
|
+
export declare class SceneDoc {
|
|
68
|
+
#private;
|
|
69
|
+
/** @internal Scenes come from a {@link SceneEngine}. */
|
|
70
|
+
constructor(wasm: WasmSceneDoc, client: number);
|
|
71
|
+
/** The scene in the canonical flat form. */
|
|
72
|
+
toJson(): SceneJson;
|
|
73
|
+
/** Run canvas command `name` (see {@link SceneEngine.metadata}) against the selection. */
|
|
74
|
+
run(name: string, args?: Readonly<Record<string, unknown>>): void;
|
|
75
|
+
/** Select nodes by id, for commands and inspector fields. */
|
|
76
|
+
select(ids: readonly NodeId[]): void;
|
|
77
|
+
/** The selected nodes' ids. */
|
|
78
|
+
selection(): NodeId[];
|
|
79
|
+
/** The inspector for the selection: sections of fields and their values. */
|
|
80
|
+
inspect(): unknown;
|
|
81
|
+
/** Set inspector field `key` on every selected node. */
|
|
82
|
+
setField(key: string, value: unknown): void;
|
|
83
|
+
/** Undo the latest change; returns whether there was one. */
|
|
84
|
+
undo(): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Provide an image asset's pixels, `width` by `height` RGBA8. Exports
|
|
87
|
+
* draw an image without pixels as a grey placeholder; SVG exports
|
|
88
|
+
* reference provided images by asset id.
|
|
89
|
+
*/
|
|
90
|
+
addImage(asset: string, width: number, height: number, rgba: Uint8Array): void;
|
|
91
|
+
/** The scene, or the nodes `options` names, as an SVG document. */
|
|
92
|
+
exportSvg(options?: ExportOptions): string;
|
|
93
|
+
/** The scene, or the nodes `options` names, as a PNG image. */
|
|
94
|
+
exportPng(options?: ExportOptions): Uint8Array;
|
|
95
|
+
/** Share the scene as Yjs client `clientId` (the engine's client by default). */
|
|
96
|
+
share(clientId?: number): void;
|
|
97
|
+
/** The whole shared scene as one v1 Yjs update. */
|
|
98
|
+
encodeState(): Uint8Array;
|
|
99
|
+
/** Merge a peer's v1 update; returns whether the scene changed. */
|
|
100
|
+
applyUpdate(update: Uint8Array): boolean;
|
|
101
|
+
/** The v1 updates this scene produced since the last call. */
|
|
102
|
+
takeUpdates(): Uint8Array[];
|
|
103
|
+
/** Release the scene's memory now rather than when it is collected. */
|
|
104
|
+
[Symbol.dispose](): void;
|
|
105
|
+
}
|