@atom.io/template-preact-svg-editor 0.1.0 → 0.1.1
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/CHANGELOG.md +14 -0
- package/COLLABORATION.md +132 -0
- package/README.md +43 -10
- package/eslint.config.ts +1 -1
- package/package.json +30 -16
- package/src/BezierPlayground.tsx +179 -320
- package/src/preact-logo.ts +155 -0
- package/src/svg-contour.test.ts +89 -0
- package/src/svg-contour.ts +579 -0
- package/src/svg-convergence.test.ts +264 -0
- package/src/svg-convergence.ts +566 -0
- package/src/svg-domain-realtime.test.ts +369 -0
- package/src/svg-domain.test.ts +618 -0
- package/src/svg-domain.ts +939 -0
- package/src/svg-editor-state.test.ts +401 -0
- package/src/svg-editor-state.ts +1102 -0
- package/vite.config.ts +1 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mosaicDomain,
|
|
3
|
+
type MosaicDomainHistoryRequestResult,
|
|
4
|
+
type MosaicDomainMemberAddress,
|
|
5
|
+
type MosaicDomainMemberHistoryPolicy,
|
|
6
|
+
type MosaicDomainValueModel,
|
|
7
|
+
} from "@atom.io/realtime"
|
|
8
|
+
import type {
|
|
9
|
+
MosaicDomainBatchClient,
|
|
10
|
+
MosaicDomainPresenceClient,
|
|
11
|
+
} from "@atom.io/realtime/client"
|
|
12
|
+
import type { Silo } from "atom.io"
|
|
13
|
+
import { getState, setState } from "atom.io"
|
|
14
|
+
import type { Json } from "atom.io/foundations/json"
|
|
15
|
+
import { z } from "zod"
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
materializeSvgOrder,
|
|
19
|
+
placeSvgOrderEntry,
|
|
20
|
+
readSvgRegister,
|
|
21
|
+
removeSvgOrderEntry,
|
|
22
|
+
type SvgHistoryOperation,
|
|
23
|
+
type SvgOrderOperation,
|
|
24
|
+
type SvgOrderState,
|
|
25
|
+
type SvgRegisterOperation,
|
|
26
|
+
} from "./svg-convergence.ts"
|
|
27
|
+
import {
|
|
28
|
+
activeDragAtom,
|
|
29
|
+
collaborationPresenceAtoms,
|
|
30
|
+
dragPresenceAtoms,
|
|
31
|
+
edgeAtoms,
|
|
32
|
+
nodeAtoms,
|
|
33
|
+
pathAtoms,
|
|
34
|
+
pathDrawSelectors,
|
|
35
|
+
pathOrderAtom,
|
|
36
|
+
pointerCaptureAtom,
|
|
37
|
+
type PointXY,
|
|
38
|
+
projectedEdgeSelectors,
|
|
39
|
+
projectedNodeSelectors,
|
|
40
|
+
structureViolationsSelector,
|
|
41
|
+
subpathAtoms,
|
|
42
|
+
subpathOrderAtoms,
|
|
43
|
+
SVG_MEMBER_MODEL_SEAMS,
|
|
44
|
+
type SvgCollaborationPresence,
|
|
45
|
+
type SvgDragPresence,
|
|
46
|
+
svgDragPresenceKey,
|
|
47
|
+
type SvgDragTarget,
|
|
48
|
+
type SvgDrawingFixture,
|
|
49
|
+
type SvgEdge,
|
|
50
|
+
svgElementAtom,
|
|
51
|
+
type SvgGesture,
|
|
52
|
+
svgOperationId,
|
|
53
|
+
type SvgSubpath,
|
|
54
|
+
viewportAtom,
|
|
55
|
+
workspaceAtom,
|
|
56
|
+
} from "./svg-editor-state.ts"
|
|
57
|
+
|
|
58
|
+
const keySchema = z.string().min(1)
|
|
59
|
+
const pointSchema = z
|
|
60
|
+
.object({ x: z.number().finite(), y: z.number().finite() })
|
|
61
|
+
.strict()
|
|
62
|
+
const dragTargetSchema = z.discriminatedUnion(`kind`, [
|
|
63
|
+
z.object({ kind: z.literal(`node`), subpathId: keySchema }).strict(),
|
|
64
|
+
z
|
|
65
|
+
.object({
|
|
66
|
+
control: z.enum([`c`, `s`]),
|
|
67
|
+
kind: z.literal(`edge-control`),
|
|
68
|
+
subpathId: keySchema,
|
|
69
|
+
})
|
|
70
|
+
.strict(),
|
|
71
|
+
])
|
|
72
|
+
export const svgDragPresenceSchema: z.ZodType<SvgDragPresence> = z
|
|
73
|
+
.object({
|
|
74
|
+
actor: keySchema,
|
|
75
|
+
gestureId: keySchema,
|
|
76
|
+
point: pointSchema,
|
|
77
|
+
sequence: z.number().int().nonnegative(),
|
|
78
|
+
session: keySchema,
|
|
79
|
+
target: dragTargetSchema,
|
|
80
|
+
})
|
|
81
|
+
.strict()
|
|
82
|
+
|
|
83
|
+
export const svgCollaborationPresenceSchema: z.ZodType<SvgCollaborationPresence> =
|
|
84
|
+
z
|
|
85
|
+
.object({
|
|
86
|
+
activePathId: keySchema.nullable(),
|
|
87
|
+
actor: keySchema,
|
|
88
|
+
color: keySchema.max(64),
|
|
89
|
+
name: keySchema.max(128),
|
|
90
|
+
pointer: pointSchema.nullable(),
|
|
91
|
+
selectedSubpathId: keySchema.nullable(),
|
|
92
|
+
session: keySchema,
|
|
93
|
+
})
|
|
94
|
+
.strict()
|
|
95
|
+
|
|
96
|
+
type SvgReceiptState = {
|
|
97
|
+
readonly operations: Readonly<
|
|
98
|
+
Record<string, { readonly actor?: string | undefined }>
|
|
99
|
+
>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type AuthoredSvgOperation = {
|
|
103
|
+
readonly actor?: string | undefined
|
|
104
|
+
readonly targetOperationIds?: readonly string[] | undefined
|
|
105
|
+
readonly type?: string | undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const valueModel = <
|
|
109
|
+
Value extends SvgReceiptState,
|
|
110
|
+
Operation extends AuthoredSvgOperation,
|
|
111
|
+
>(seam: {
|
|
112
|
+
readonly identity: { readonly key: string; readonly version: number }
|
|
113
|
+
readonly history: object
|
|
114
|
+
readonly kind: `value`
|
|
115
|
+
readonly operationSchema: z.ZodType<Operation>
|
|
116
|
+
readonly reduce: (value: Value, operation: Operation) => Value
|
|
117
|
+
}): MosaicDomainValueModel<
|
|
118
|
+
Json.Serializable & Value,
|
|
119
|
+
Json.Serializable & Operation
|
|
120
|
+
> => ({
|
|
121
|
+
history: seam.history as MosaicDomainMemberHistoryPolicy<
|
|
122
|
+
Json.Serializable & Value,
|
|
123
|
+
Json.Serializable & Operation
|
|
124
|
+
>,
|
|
125
|
+
identity: seam.identity,
|
|
126
|
+
kind: `value`,
|
|
127
|
+
operationSchema: seam.operationSchema as z.ZodType<
|
|
128
|
+
Json.Serializable & Operation
|
|
129
|
+
>,
|
|
130
|
+
reduce: (value, operation, context) => {
|
|
131
|
+
if (operation.actor !== context.actor) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`An SVG member operation must name its authenticated actor.`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
if (operation.targetOperationIds !== undefined) {
|
|
137
|
+
for (const target of operation.targetOperationIds) {
|
|
138
|
+
if (value.operations[target]?.actor !== context.actor) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`An SVG compensation can target only the authenticated actor's operations.`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return seam.reduce(value, operation) as Json.Serializable & Value
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
/** Public Domain inventory; render and workspace state remain ordinary Atom.io tokens. */
|
|
150
|
+
export const svgDesignDomain = mosaicDomain({
|
|
151
|
+
configSchema: z.object({}).strict(),
|
|
152
|
+
key: `svg-design`,
|
|
153
|
+
members: {
|
|
154
|
+
activeDrag: { role: `local`, token: activeDragAtom },
|
|
155
|
+
collaborator: {
|
|
156
|
+
keySchema,
|
|
157
|
+
role: `ephemeral`,
|
|
158
|
+
schema: svgCollaborationPresenceSchema,
|
|
159
|
+
token: collaborationPresenceAtoms,
|
|
160
|
+
},
|
|
161
|
+
dragPresence: {
|
|
162
|
+
keySchema,
|
|
163
|
+
role: `ephemeral`,
|
|
164
|
+
schema: svgDragPresenceSchema,
|
|
165
|
+
token: dragPresenceAtoms,
|
|
166
|
+
},
|
|
167
|
+
edges: {
|
|
168
|
+
keySchema,
|
|
169
|
+
model: valueModel(SVG_MEMBER_MODEL_SEAMS.edge),
|
|
170
|
+
role: `durable`,
|
|
171
|
+
schema: SVG_MEMBER_MODEL_SEAMS.edge.stateSchema,
|
|
172
|
+
token: edgeAtoms,
|
|
173
|
+
},
|
|
174
|
+
nodes: {
|
|
175
|
+
keySchema,
|
|
176
|
+
model: valueModel(SVG_MEMBER_MODEL_SEAMS.node),
|
|
177
|
+
role: `durable`,
|
|
178
|
+
schema: SVG_MEMBER_MODEL_SEAMS.node.stateSchema,
|
|
179
|
+
token: nodeAtoms,
|
|
180
|
+
},
|
|
181
|
+
pathDrawing: { keySchema, role: `derived`, token: pathDrawSelectors },
|
|
182
|
+
pathOrder: {
|
|
183
|
+
model: valueModel(SVG_MEMBER_MODEL_SEAMS.order),
|
|
184
|
+
role: `durable`,
|
|
185
|
+
schema: SVG_MEMBER_MODEL_SEAMS.order.stateSchema,
|
|
186
|
+
token: pathOrderAtom,
|
|
187
|
+
},
|
|
188
|
+
paths: {
|
|
189
|
+
keySchema,
|
|
190
|
+
model: valueModel(SVG_MEMBER_MODEL_SEAMS.path),
|
|
191
|
+
role: `durable`,
|
|
192
|
+
schema: SVG_MEMBER_MODEL_SEAMS.path.stateSchema,
|
|
193
|
+
token: pathAtoms,
|
|
194
|
+
},
|
|
195
|
+
pointerCapture: { role: `local`, token: pointerCaptureAtom },
|
|
196
|
+
projectedEdges: {
|
|
197
|
+
keySchema,
|
|
198
|
+
role: `derived`,
|
|
199
|
+
token: projectedEdgeSelectors,
|
|
200
|
+
},
|
|
201
|
+
projectedNodes: {
|
|
202
|
+
keySchema,
|
|
203
|
+
role: `derived`,
|
|
204
|
+
token: projectedNodeSelectors,
|
|
205
|
+
},
|
|
206
|
+
structureViolations: { role: `derived`, token: structureViolationsSelector },
|
|
207
|
+
subpathOrder: {
|
|
208
|
+
keySchema,
|
|
209
|
+
model: valueModel(SVG_MEMBER_MODEL_SEAMS.order),
|
|
210
|
+
role: `durable`,
|
|
211
|
+
schema: SVG_MEMBER_MODEL_SEAMS.order.stateSchema,
|
|
212
|
+
token: subpathOrderAtoms,
|
|
213
|
+
},
|
|
214
|
+
subpaths: {
|
|
215
|
+
keySchema,
|
|
216
|
+
model: valueModel(SVG_MEMBER_MODEL_SEAMS.subpath),
|
|
217
|
+
role: `durable`,
|
|
218
|
+
schema: SVG_MEMBER_MODEL_SEAMS.subpath.stateSchema,
|
|
219
|
+
token: subpathAtoms,
|
|
220
|
+
},
|
|
221
|
+
svgElement: { role: `local`, token: svgElementAtom },
|
|
222
|
+
viewport: { role: `local`, token: viewportAtom },
|
|
223
|
+
workspace: { role: `local`, token: workspaceAtom },
|
|
224
|
+
},
|
|
225
|
+
version: 2,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
/** Install the ordinary template tokens before activating in an isolated Silo. */
|
|
229
|
+
export const SVG_DOMAIN_TOKENS = [
|
|
230
|
+
activeDragAtom,
|
|
231
|
+
collaborationPresenceAtoms,
|
|
232
|
+
dragPresenceAtoms,
|
|
233
|
+
edgeAtoms,
|
|
234
|
+
nodeAtoms,
|
|
235
|
+
pathDrawSelectors,
|
|
236
|
+
pathOrderAtom,
|
|
237
|
+
pathAtoms,
|
|
238
|
+
pointerCaptureAtom,
|
|
239
|
+
projectedEdgeSelectors,
|
|
240
|
+
projectedNodeSelectors,
|
|
241
|
+
structureViolationsSelector,
|
|
242
|
+
subpathOrderAtoms,
|
|
243
|
+
subpathAtoms,
|
|
244
|
+
svgElementAtom,
|
|
245
|
+
viewportAtom,
|
|
246
|
+
workspaceAtom,
|
|
247
|
+
] as const
|
|
248
|
+
|
|
249
|
+
export async function activateSvgDesignDomain(options: {
|
|
250
|
+
readonly instance: string
|
|
251
|
+
readonly silo: Pick<Silo, `install` | `store`>
|
|
252
|
+
}): Promise<SvgDesignDomain> {
|
|
253
|
+
options.silo.install([...SVG_DOMAIN_TOKENS])
|
|
254
|
+
return svgDesignDomain.activate({
|
|
255
|
+
config: {},
|
|
256
|
+
instance: options.instance,
|
|
257
|
+
store: options.silo.store,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type SvgDesignDomain = Awaited<
|
|
262
|
+
ReturnType<typeof svgDesignDomain.activate>
|
|
263
|
+
>
|
|
264
|
+
|
|
265
|
+
export type SvgDomainState = {
|
|
266
|
+
readonly getState: (...parameters: any[]) => any
|
|
267
|
+
readonly setState: (...parameters: any[]) => void
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const implicitState: SvgDomainState = {
|
|
271
|
+
getState: getState as SvgDomainState[`getState`],
|
|
272
|
+
setState: setState as SvgDomainState[`setState`],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type Planned = {
|
|
276
|
+
readonly address: MosaicDomainMemberAddress
|
|
277
|
+
readonly id: string
|
|
278
|
+
readonly operation:
|
|
279
|
+
| SvgHistoryOperation
|
|
280
|
+
| SvgOrderOperation
|
|
281
|
+
| SvgRegisterOperation<any>
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type SvgDomainEditor = {
|
|
285
|
+
beginDrag(options: {
|
|
286
|
+
readonly element: Element
|
|
287
|
+
readonly gesture: SvgGesture
|
|
288
|
+
readonly point: PointXY
|
|
289
|
+
readonly pointerId: number
|
|
290
|
+
readonly target: SvgDragTarget
|
|
291
|
+
}): Promise<void>
|
|
292
|
+
commitGeometry(input: {
|
|
293
|
+
readonly gesture: SvgGesture
|
|
294
|
+
readonly point: PointXY
|
|
295
|
+
readonly target: SvgDragTarget
|
|
296
|
+
}): Promise<void>
|
|
297
|
+
deletePath(input: {
|
|
298
|
+
readonly gesture: SvgGesture
|
|
299
|
+
readonly pathId: string
|
|
300
|
+
}): Promise<void>
|
|
301
|
+
deleteSubpath(input: {
|
|
302
|
+
readonly gesture: SvgGesture
|
|
303
|
+
readonly pathId: string
|
|
304
|
+
readonly subpathId: string
|
|
305
|
+
}): Promise<void>
|
|
306
|
+
finishDrag(options: { readonly commit: boolean }): Promise<void>
|
|
307
|
+
insertPath(input: {
|
|
308
|
+
readonly gesture: SvgGesture
|
|
309
|
+
readonly index: number
|
|
310
|
+
readonly pathId: string
|
|
311
|
+
}): Promise<void>
|
|
312
|
+
insertSubpath(input: {
|
|
313
|
+
readonly edge: SvgEdge
|
|
314
|
+
readonly gesture: SvgGesture
|
|
315
|
+
readonly index: number
|
|
316
|
+
readonly node: PointXY | null
|
|
317
|
+
readonly pathId: string
|
|
318
|
+
readonly subpathId: string
|
|
319
|
+
}): Promise<void>
|
|
320
|
+
previewDrag(point: PointXY): Promise<void>
|
|
321
|
+
reorderPath(input: {
|
|
322
|
+
readonly gesture: SvgGesture
|
|
323
|
+
readonly index: number
|
|
324
|
+
readonly pathId: string
|
|
325
|
+
}): Promise<void>
|
|
326
|
+
reorderSubpath(input: {
|
|
327
|
+
readonly gesture: SvgGesture
|
|
328
|
+
readonly index: number
|
|
329
|
+
readonly pathId: string
|
|
330
|
+
readonly subpathId: string
|
|
331
|
+
}): Promise<void>
|
|
332
|
+
replaceDrawing(input: {
|
|
333
|
+
readonly drawing: SvgDrawingFixture
|
|
334
|
+
readonly gesture: SvgGesture
|
|
335
|
+
}): Promise<void>
|
|
336
|
+
redo(gesture: SvgGesture): Promise<boolean>
|
|
337
|
+
splitSubpath(input: {
|
|
338
|
+
readonly continuationEdge: SvgEdge
|
|
339
|
+
readonly gesture: SvgGesture
|
|
340
|
+
readonly inserted: {
|
|
341
|
+
readonly edge: SvgEdge
|
|
342
|
+
readonly node: PointXY | null
|
|
343
|
+
readonly subpathId: string
|
|
344
|
+
}
|
|
345
|
+
readonly pathId: string
|
|
346
|
+
readonly targetSubpathId: string
|
|
347
|
+
}): Promise<void>
|
|
348
|
+
undo(gesture: SvgGesture): Promise<boolean>
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const operationAt = <Operation extends { readonly id: string }>(
|
|
352
|
+
state: { readonly operations: Readonly<Record<string, Operation>> },
|
|
353
|
+
id: string,
|
|
354
|
+
): Operation => {
|
|
355
|
+
const operation = state.operations[id]
|
|
356
|
+
if (operation === undefined)
|
|
357
|
+
throw new Error(`SVG operation ${id} was not produced.`)
|
|
358
|
+
return operation
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function assertClosePair(edge: SvgEdge, node: PointXY | null): void {
|
|
362
|
+
if ((edge.kind === `close`) !== (node === null)) {
|
|
363
|
+
throw new Error(`Only a close edge may have a null SVG node`)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Build and submit explicit public Domain batches; no private transaction bridge. */
|
|
368
|
+
export function createSvgDomainEditor(options: {
|
|
369
|
+
readonly batch: MosaicDomainBatchClient
|
|
370
|
+
readonly domain: SvgDesignDomain
|
|
371
|
+
readonly history?: {
|
|
372
|
+
redo(): Promise<MosaicDomainHistoryRequestResult>
|
|
373
|
+
undo(): Promise<MosaicDomainHistoryRequestResult>
|
|
374
|
+
}
|
|
375
|
+
readonly presence?: MosaicDomainPresenceClient
|
|
376
|
+
readonly state?: SvgDomainState
|
|
377
|
+
}): SvgDomainEditor {
|
|
378
|
+
const state = options.state ?? implicitState
|
|
379
|
+
const sharePresence = async (
|
|
380
|
+
work: Promise<void> | undefined,
|
|
381
|
+
): Promise<void> => {
|
|
382
|
+
try {
|
|
383
|
+
await work
|
|
384
|
+
} catch {
|
|
385
|
+
// Presence is advisory; its controller retains the actionable status.
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const op = (
|
|
389
|
+
address: Planned[`address`],
|
|
390
|
+
operation: Planned[`operation`],
|
|
391
|
+
): Planned => ({ address, id: operation.id, operation })
|
|
392
|
+
const submit = async (
|
|
393
|
+
gesture: SvgGesture,
|
|
394
|
+
operations: readonly Planned[],
|
|
395
|
+
): Promise<void> => {
|
|
396
|
+
const authored = operations.map((planned) => ({
|
|
397
|
+
...planned,
|
|
398
|
+
operation: { ...planned.operation, actor: gesture.actor },
|
|
399
|
+
}))
|
|
400
|
+
await options.batch.submit(authored, gesture.id)
|
|
401
|
+
if (options.batch.state.status === `rejected`) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
options.batch.state.problem?.reason ??
|
|
404
|
+
`The SVG Domain batch was rejected.`,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const orderPlacement = (
|
|
409
|
+
current: SvgOrderState,
|
|
410
|
+
gesture: SvgGesture,
|
|
411
|
+
ordinal: number,
|
|
412
|
+
entryId: string,
|
|
413
|
+
index: number,
|
|
414
|
+
value = entryId,
|
|
415
|
+
): SvgOrderOperation => {
|
|
416
|
+
const id = svgOperationId(gesture, ordinal)
|
|
417
|
+
return operationAt(
|
|
418
|
+
placeSvgOrderEntry(current, { entryId, id, index, value }),
|
|
419
|
+
id,
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
const orderRemoval = (
|
|
423
|
+
current: SvgOrderState,
|
|
424
|
+
gesture: SvgGesture,
|
|
425
|
+
ordinal: number,
|
|
426
|
+
entryId: string,
|
|
427
|
+
): SvgOrderOperation => {
|
|
428
|
+
const id = svgOperationId(gesture, ordinal)
|
|
429
|
+
return operationAt(removeSvgOrderEntry(current, entryId, id), id)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const editor: SvgDomainEditor = {
|
|
433
|
+
async beginDrag(input) {
|
|
434
|
+
state.setState(activeDragAtom, {
|
|
435
|
+
gesture: input.gesture,
|
|
436
|
+
pointerId: input.pointerId,
|
|
437
|
+
target: input.target,
|
|
438
|
+
})
|
|
439
|
+
state.setState(pointerCaptureAtom, {
|
|
440
|
+
element: input.element,
|
|
441
|
+
pointerId: input.pointerId,
|
|
442
|
+
})
|
|
443
|
+
const presence: SvgDragPresence = {
|
|
444
|
+
actor: input.gesture.actor,
|
|
445
|
+
gestureId: input.gesture.id,
|
|
446
|
+
point: input.point,
|
|
447
|
+
sequence: 0,
|
|
448
|
+
session: input.gesture.session,
|
|
449
|
+
target: input.target,
|
|
450
|
+
}
|
|
451
|
+
state.setState(
|
|
452
|
+
dragPresenceAtoms,
|
|
453
|
+
svgDragPresenceKey(input.gesture),
|
|
454
|
+
presence,
|
|
455
|
+
)
|
|
456
|
+
await sharePresence(
|
|
457
|
+
options.presence?.publish(
|
|
458
|
+
options.domain.address(
|
|
459
|
+
`dragPresence`,
|
|
460
|
+
svgDragPresenceKey(input.gesture),
|
|
461
|
+
),
|
|
462
|
+
presence,
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
},
|
|
466
|
+
async commitGeometry(input) {
|
|
467
|
+
if (input.target.kind === `node`) {
|
|
468
|
+
if (
|
|
469
|
+
readSvgRegister(state.getState(nodeAtoms, input.target.subpathId)) ==
|
|
470
|
+
null
|
|
471
|
+
) {
|
|
472
|
+
throw new Error(`Cannot move a missing or closing SVG node`)
|
|
473
|
+
}
|
|
474
|
+
const operation = {
|
|
475
|
+
id: svgOperationId(input.gesture, 0),
|
|
476
|
+
value: input.point,
|
|
477
|
+
}
|
|
478
|
+
await submit(input.gesture, [
|
|
479
|
+
op(options.domain.address(`nodes`, input.target.subpathId), operation),
|
|
480
|
+
])
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
const edge = readSvgRegister<SvgEdge | null>(
|
|
484
|
+
state.getState(edgeAtoms, input.target.subpathId),
|
|
485
|
+
)
|
|
486
|
+
if (edge?.kind !== `cubic`) {
|
|
487
|
+
throw new Error(`Cannot move a control on a non-cubic SVG edge`)
|
|
488
|
+
}
|
|
489
|
+
const operation = {
|
|
490
|
+
id: svgOperationId(input.gesture, 0),
|
|
491
|
+
value:
|
|
492
|
+
input.target.control === `c`
|
|
493
|
+
? { ...edge, c: input.point }
|
|
494
|
+
: { ...edge, s: input.point },
|
|
495
|
+
}
|
|
496
|
+
await submit(input.gesture, [
|
|
497
|
+
op(options.domain.address(`edges`, input.target.subpathId), operation),
|
|
498
|
+
])
|
|
499
|
+
},
|
|
500
|
+
async deletePath(input) {
|
|
501
|
+
if (readSvgRegister(state.getState(pathAtoms, input.pathId)) == null) {
|
|
502
|
+
throw new Error(`SVG path ${input.pathId} does not exist`)
|
|
503
|
+
}
|
|
504
|
+
let ordinal = 0
|
|
505
|
+
const operations: Planned[] = []
|
|
506
|
+
let subpathOrder = state.getState(subpathOrderAtoms, input.pathId)
|
|
507
|
+
for (const { entryId, value: subpathId } of materializeSvgOrder(
|
|
508
|
+
subpathOrder,
|
|
509
|
+
)) {
|
|
510
|
+
const removal = orderRemoval(
|
|
511
|
+
subpathOrder,
|
|
512
|
+
input.gesture,
|
|
513
|
+
ordinal++,
|
|
514
|
+
entryId,
|
|
515
|
+
)
|
|
516
|
+
subpathOrder = {
|
|
517
|
+
operations: { ...subpathOrder.operations, [removal.id]: removal },
|
|
518
|
+
}
|
|
519
|
+
operations.push(
|
|
520
|
+
op(options.domain.address(`subpathOrder`, input.pathId), removal),
|
|
521
|
+
op(options.domain.address(`subpaths`, subpathId), {
|
|
522
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
523
|
+
value: null,
|
|
524
|
+
}),
|
|
525
|
+
op(options.domain.address(`nodes`, subpathId), {
|
|
526
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
527
|
+
value: null,
|
|
528
|
+
}),
|
|
529
|
+
op(options.domain.address(`edges`, subpathId), {
|
|
530
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
531
|
+
value: null,
|
|
532
|
+
}),
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
operations.push(
|
|
536
|
+
op(options.domain.address(`paths`, input.pathId), {
|
|
537
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
538
|
+
value: null,
|
|
539
|
+
}),
|
|
540
|
+
op(
|
|
541
|
+
options.domain.address(`pathOrder`),
|
|
542
|
+
orderRemoval(
|
|
543
|
+
state.getState(pathOrderAtom),
|
|
544
|
+
input.gesture,
|
|
545
|
+
ordinal,
|
|
546
|
+
input.pathId,
|
|
547
|
+
),
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
await submit(input.gesture, operations)
|
|
551
|
+
},
|
|
552
|
+
async deleteSubpath(input) {
|
|
553
|
+
const subpath = readSvgRegister<SvgSubpath | null>(
|
|
554
|
+
state.getState(subpathAtoms, input.subpathId),
|
|
555
|
+
)
|
|
556
|
+
if (subpath?.pathId !== input.pathId) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`SVG subpath ${input.subpathId} is not in ${input.pathId}`,
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
await submit(input.gesture, [
|
|
562
|
+
op(
|
|
563
|
+
options.domain.address(`subpathOrder`, input.pathId),
|
|
564
|
+
orderRemoval(
|
|
565
|
+
state.getState(subpathOrderAtoms, input.pathId),
|
|
566
|
+
input.gesture,
|
|
567
|
+
0,
|
|
568
|
+
input.subpathId,
|
|
569
|
+
),
|
|
570
|
+
),
|
|
571
|
+
op(options.domain.address(`subpaths`, input.subpathId), {
|
|
572
|
+
id: svgOperationId(input.gesture, 1),
|
|
573
|
+
value: null,
|
|
574
|
+
}),
|
|
575
|
+
op(options.domain.address(`nodes`, input.subpathId), {
|
|
576
|
+
id: svgOperationId(input.gesture, 2),
|
|
577
|
+
value: null,
|
|
578
|
+
}),
|
|
579
|
+
op(options.domain.address(`edges`, input.subpathId), {
|
|
580
|
+
id: svgOperationId(input.gesture, 3),
|
|
581
|
+
value: null,
|
|
582
|
+
}),
|
|
583
|
+
])
|
|
584
|
+
},
|
|
585
|
+
async finishDrag({ commit }) {
|
|
586
|
+
const active = state.getState(activeDragAtom)
|
|
587
|
+
if (active === null) return
|
|
588
|
+
const key = svgDragPresenceKey(active.gesture)
|
|
589
|
+
const presence = state.getState(dragPresenceAtoms, key)
|
|
590
|
+
state.setState(dragPresenceAtoms, key, null)
|
|
591
|
+
state.setState(activeDragAtom, null)
|
|
592
|
+
state.setState(pointerCaptureAtom, null)
|
|
593
|
+
const work: Promise<unknown>[] = []
|
|
594
|
+
if (options.presence !== undefined) {
|
|
595
|
+
work.push(
|
|
596
|
+
sharePresence(
|
|
597
|
+
options.presence.clear(options.domain.address(`dragPresence`, key)),
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
if (commit && presence?.gestureId === active.gesture.id) {
|
|
602
|
+
work.push(
|
|
603
|
+
editor.commitGeometry({
|
|
604
|
+
gesture: active.gesture,
|
|
605
|
+
point: presence.point,
|
|
606
|
+
target: active.target,
|
|
607
|
+
}),
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
await Promise.all(work)
|
|
611
|
+
},
|
|
612
|
+
async insertPath(input) {
|
|
613
|
+
if (readSvgRegister(state.getState(pathAtoms, input.pathId)) != null) {
|
|
614
|
+
throw new Error(`SVG path ${input.pathId} already exists`)
|
|
615
|
+
}
|
|
616
|
+
await submit(input.gesture, [
|
|
617
|
+
op(options.domain.address(`paths`, input.pathId), {
|
|
618
|
+
id: svgOperationId(input.gesture, 0),
|
|
619
|
+
value: { id: input.pathId },
|
|
620
|
+
}),
|
|
621
|
+
op(
|
|
622
|
+
options.domain.address(`pathOrder`),
|
|
623
|
+
orderPlacement(
|
|
624
|
+
state.getState(pathOrderAtom),
|
|
625
|
+
input.gesture,
|
|
626
|
+
1,
|
|
627
|
+
input.pathId,
|
|
628
|
+
input.index,
|
|
629
|
+
),
|
|
630
|
+
),
|
|
631
|
+
])
|
|
632
|
+
},
|
|
633
|
+
async insertSubpath(input) {
|
|
634
|
+
if (readSvgRegister(state.getState(pathAtoms, input.pathId)) == null) {
|
|
635
|
+
throw new Error(`SVG path ${input.pathId} does not exist`)
|
|
636
|
+
}
|
|
637
|
+
if (
|
|
638
|
+
readSvgRegister(state.getState(subpathAtoms, input.subpathId)) != null
|
|
639
|
+
) {
|
|
640
|
+
throw new Error(`SVG subpath ${input.subpathId} already exists`)
|
|
641
|
+
}
|
|
642
|
+
assertClosePair(input.edge, input.node)
|
|
643
|
+
await submit(input.gesture, [
|
|
644
|
+
op(options.domain.address(`subpaths`, input.subpathId), {
|
|
645
|
+
id: svgOperationId(input.gesture, 0),
|
|
646
|
+
value: { id: input.subpathId, pathId: input.pathId },
|
|
647
|
+
}),
|
|
648
|
+
op(options.domain.address(`nodes`, input.subpathId), {
|
|
649
|
+
id: svgOperationId(input.gesture, 1),
|
|
650
|
+
value: input.node,
|
|
651
|
+
}),
|
|
652
|
+
op(options.domain.address(`edges`, input.subpathId), {
|
|
653
|
+
id: svgOperationId(input.gesture, 2),
|
|
654
|
+
value: input.edge,
|
|
655
|
+
}),
|
|
656
|
+
op(
|
|
657
|
+
options.domain.address(`subpathOrder`, input.pathId),
|
|
658
|
+
orderPlacement(
|
|
659
|
+
state.getState(subpathOrderAtoms, input.pathId),
|
|
660
|
+
input.gesture,
|
|
661
|
+
3,
|
|
662
|
+
input.subpathId,
|
|
663
|
+
input.index,
|
|
664
|
+
),
|
|
665
|
+
),
|
|
666
|
+
])
|
|
667
|
+
},
|
|
668
|
+
async previewDrag(point) {
|
|
669
|
+
const active = state.getState(activeDragAtom)
|
|
670
|
+
if (active === null) return
|
|
671
|
+
const key = svgDragPresenceKey(active.gesture)
|
|
672
|
+
const previous = state.getState(dragPresenceAtoms, key)
|
|
673
|
+
const presence: SvgDragPresence = {
|
|
674
|
+
actor: active.gesture.actor,
|
|
675
|
+
gestureId: active.gesture.id,
|
|
676
|
+
point,
|
|
677
|
+
sequence: (previous?.sequence ?? -1) + 1,
|
|
678
|
+
session: active.gesture.session,
|
|
679
|
+
target: active.target,
|
|
680
|
+
}
|
|
681
|
+
state.setState(dragPresenceAtoms, key, presence)
|
|
682
|
+
await sharePresence(
|
|
683
|
+
options.presence?.publish(
|
|
684
|
+
options.domain.address(`dragPresence`, key),
|
|
685
|
+
presence,
|
|
686
|
+
),
|
|
687
|
+
)
|
|
688
|
+
},
|
|
689
|
+
async reorderPath(input) {
|
|
690
|
+
if (readSvgRegister(state.getState(pathAtoms, input.pathId)) == null) {
|
|
691
|
+
throw new Error(`Cannot reorder a missing SVG path`)
|
|
692
|
+
}
|
|
693
|
+
await submit(input.gesture, [
|
|
694
|
+
op(
|
|
695
|
+
options.domain.address(`pathOrder`),
|
|
696
|
+
orderPlacement(
|
|
697
|
+
state.getState(pathOrderAtom),
|
|
698
|
+
input.gesture,
|
|
699
|
+
0,
|
|
700
|
+
input.pathId,
|
|
701
|
+
input.index,
|
|
702
|
+
),
|
|
703
|
+
),
|
|
704
|
+
])
|
|
705
|
+
},
|
|
706
|
+
async redo(_gesture) {
|
|
707
|
+
if (options.history === undefined) return false
|
|
708
|
+
const result = await options.history.redo()
|
|
709
|
+
if (result.status === `rejected`) throw new Error(result.reason)
|
|
710
|
+
return result.status === `accepted`
|
|
711
|
+
},
|
|
712
|
+
async reorderSubpath(input) {
|
|
713
|
+
const subpath = readSvgRegister<SvgSubpath | null>(
|
|
714
|
+
state.getState(subpathAtoms, input.subpathId),
|
|
715
|
+
)
|
|
716
|
+
if (subpath?.pathId !== input.pathId) {
|
|
717
|
+
throw new Error(`Cannot reorder a missing SVG subpath`)
|
|
718
|
+
}
|
|
719
|
+
const node = readSvgRegister<PointXY | null>(
|
|
720
|
+
state.getState(nodeAtoms, input.subpathId),
|
|
721
|
+
)
|
|
722
|
+
const edge = readSvgRegister<SvgEdge | null>(
|
|
723
|
+
state.getState(edgeAtoms, input.subpathId),
|
|
724
|
+
)
|
|
725
|
+
if (edge == null || node === undefined) {
|
|
726
|
+
throw new Error(`Cannot reorder an incomplete SVG subpath`)
|
|
727
|
+
}
|
|
728
|
+
await submit(input.gesture, [
|
|
729
|
+
op(options.domain.address(`subpaths`, input.subpathId), {
|
|
730
|
+
id: svgOperationId(input.gesture, 0),
|
|
731
|
+
value: subpath,
|
|
732
|
+
}),
|
|
733
|
+
op(options.domain.address(`nodes`, input.subpathId), {
|
|
734
|
+
id: svgOperationId(input.gesture, 1),
|
|
735
|
+
value: node,
|
|
736
|
+
}),
|
|
737
|
+
op(options.domain.address(`edges`, input.subpathId), {
|
|
738
|
+
id: svgOperationId(input.gesture, 2),
|
|
739
|
+
value: edge,
|
|
740
|
+
}),
|
|
741
|
+
op(
|
|
742
|
+
options.domain.address(`subpathOrder`, input.pathId),
|
|
743
|
+
orderPlacement(
|
|
744
|
+
state.getState(subpathOrderAtoms, input.pathId),
|
|
745
|
+
input.gesture,
|
|
746
|
+
3,
|
|
747
|
+
input.subpathId,
|
|
748
|
+
input.index,
|
|
749
|
+
),
|
|
750
|
+
),
|
|
751
|
+
])
|
|
752
|
+
},
|
|
753
|
+
async replaceDrawing(input) {
|
|
754
|
+
const pathIds = input.drawing.paths.map(({ id }) => id)
|
|
755
|
+
if (new Set(pathIds).size !== pathIds.length) {
|
|
756
|
+
throw new Error(`An SVG drawing cannot contain duplicate path IDs`)
|
|
757
|
+
}
|
|
758
|
+
const subpathIds = new Set<string>()
|
|
759
|
+
for (const path of input.drawing.paths) {
|
|
760
|
+
for (const subpath of path.subpaths) {
|
|
761
|
+
if (subpathIds.has(subpath.id)) {
|
|
762
|
+
throw new Error(`SVG subpath IDs must be globally unique`)
|
|
763
|
+
}
|
|
764
|
+
subpathIds.add(subpath.id)
|
|
765
|
+
assertClosePair(subpath.edge, subpath.node)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
let ordinal = 0
|
|
769
|
+
const operations: Planned[] = []
|
|
770
|
+
const clearedSubpathOrders = new Map<string, SvgOrderState>()
|
|
771
|
+
let pathOrder = state.getState(pathOrderAtom)
|
|
772
|
+
for (const { entryId, value: pathId } of materializeSvgOrder(pathOrder)) {
|
|
773
|
+
let order = state.getState(subpathOrderAtoms, pathId)
|
|
774
|
+
for (const {
|
|
775
|
+
entryId: subpathEntry,
|
|
776
|
+
value: subpathId,
|
|
777
|
+
} of materializeSvgOrder(order)) {
|
|
778
|
+
const removal = orderRemoval(
|
|
779
|
+
order,
|
|
780
|
+
input.gesture,
|
|
781
|
+
ordinal++,
|
|
782
|
+
subpathEntry,
|
|
783
|
+
)
|
|
784
|
+
order = { operations: { ...order.operations, [removal.id]: removal } }
|
|
785
|
+
operations.push(
|
|
786
|
+
op(options.domain.address(`subpathOrder`, pathId), removal),
|
|
787
|
+
op(options.domain.address(`subpaths`, subpathId), {
|
|
788
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
789
|
+
value: null,
|
|
790
|
+
}),
|
|
791
|
+
op(options.domain.address(`nodes`, subpathId), {
|
|
792
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
793
|
+
value: null,
|
|
794
|
+
}),
|
|
795
|
+
op(options.domain.address(`edges`, subpathId), {
|
|
796
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
797
|
+
value: null,
|
|
798
|
+
}),
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
clearedSubpathOrders.set(pathId, order)
|
|
802
|
+
operations.push(
|
|
803
|
+
op(options.domain.address(`paths`, pathId), {
|
|
804
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
805
|
+
value: null,
|
|
806
|
+
}),
|
|
807
|
+
)
|
|
808
|
+
const removal = orderRemoval(
|
|
809
|
+
pathOrder,
|
|
810
|
+
input.gesture,
|
|
811
|
+
ordinal++,
|
|
812
|
+
entryId,
|
|
813
|
+
)
|
|
814
|
+
pathOrder = {
|
|
815
|
+
operations: { ...pathOrder.operations, [removal.id]: removal },
|
|
816
|
+
}
|
|
817
|
+
operations.push(op(options.domain.address(`pathOrder`), removal))
|
|
818
|
+
}
|
|
819
|
+
for (const [pathIndex, path] of input.drawing.paths.entries()) {
|
|
820
|
+
operations.push(
|
|
821
|
+
op(options.domain.address(`paths`, path.id), {
|
|
822
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
823
|
+
value: { id: path.id },
|
|
824
|
+
}),
|
|
825
|
+
)
|
|
826
|
+
const pathPlacement = orderPlacement(
|
|
827
|
+
pathOrder,
|
|
828
|
+
input.gesture,
|
|
829
|
+
ordinal++,
|
|
830
|
+
path.id,
|
|
831
|
+
pathIndex,
|
|
832
|
+
)
|
|
833
|
+
pathOrder = {
|
|
834
|
+
operations: {
|
|
835
|
+
...pathOrder.operations,
|
|
836
|
+
[pathPlacement.id]: pathPlacement,
|
|
837
|
+
},
|
|
838
|
+
}
|
|
839
|
+
operations.push(op(options.domain.address(`pathOrder`), pathPlacement))
|
|
840
|
+
let order =
|
|
841
|
+
clearedSubpathOrders.get(path.id) ??
|
|
842
|
+
state.getState(subpathOrderAtoms, path.id)
|
|
843
|
+
for (const [index, subpath] of path.subpaths.entries()) {
|
|
844
|
+
operations.push(
|
|
845
|
+
op(options.domain.address(`subpaths`, subpath.id), {
|
|
846
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
847
|
+
value: { id: subpath.id, pathId: path.id },
|
|
848
|
+
}),
|
|
849
|
+
op(options.domain.address(`nodes`, subpath.id), {
|
|
850
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
851
|
+
value: subpath.node,
|
|
852
|
+
}),
|
|
853
|
+
op(options.domain.address(`edges`, subpath.id), {
|
|
854
|
+
id: svgOperationId(input.gesture, ordinal++),
|
|
855
|
+
value: subpath.edge,
|
|
856
|
+
}),
|
|
857
|
+
)
|
|
858
|
+
const subpathPlacement = orderPlacement(
|
|
859
|
+
order,
|
|
860
|
+
input.gesture,
|
|
861
|
+
ordinal++,
|
|
862
|
+
subpath.id,
|
|
863
|
+
index,
|
|
864
|
+
)
|
|
865
|
+
order = {
|
|
866
|
+
operations: {
|
|
867
|
+
...order.operations,
|
|
868
|
+
[subpathPlacement.id]: subpathPlacement,
|
|
869
|
+
},
|
|
870
|
+
}
|
|
871
|
+
operations.push(
|
|
872
|
+
op(
|
|
873
|
+
options.domain.address(`subpathOrder`, path.id),
|
|
874
|
+
subpathPlacement,
|
|
875
|
+
),
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
await submit(input.gesture, operations)
|
|
880
|
+
},
|
|
881
|
+
async splitSubpath(input) {
|
|
882
|
+
const order = materializeSvgOrder(
|
|
883
|
+
state.getState(subpathOrderAtoms, input.pathId),
|
|
884
|
+
)
|
|
885
|
+
const targetIndex = order.findIndex(
|
|
886
|
+
({ value }) => value === input.targetSubpathId,
|
|
887
|
+
)
|
|
888
|
+
if (targetIndex === -1)
|
|
889
|
+
throw new Error(`Cannot split a missing SVG subpath`)
|
|
890
|
+
if (
|
|
891
|
+
readSvgRegister(
|
|
892
|
+
state.getState(subpathAtoms, input.inserted.subpathId),
|
|
893
|
+
) != null
|
|
894
|
+
) {
|
|
895
|
+
throw new Error(`The inserted SVG subpath already exists`)
|
|
896
|
+
}
|
|
897
|
+
assertClosePair(input.inserted.edge, input.inserted.node)
|
|
898
|
+
const targetNode = readSvgRegister<PointXY | null>(
|
|
899
|
+
state.getState(nodeAtoms, input.targetSubpathId),
|
|
900
|
+
)
|
|
901
|
+
assertClosePair(input.continuationEdge, targetNode ?? null)
|
|
902
|
+
await submit(input.gesture, [
|
|
903
|
+
op(options.domain.address(`subpaths`, input.inserted.subpathId), {
|
|
904
|
+
id: svgOperationId(input.gesture, 0),
|
|
905
|
+
value: { id: input.inserted.subpathId, pathId: input.pathId },
|
|
906
|
+
}),
|
|
907
|
+
op(options.domain.address(`nodes`, input.inserted.subpathId), {
|
|
908
|
+
id: svgOperationId(input.gesture, 1),
|
|
909
|
+
value: input.inserted.node,
|
|
910
|
+
}),
|
|
911
|
+
op(options.domain.address(`edges`, input.inserted.subpathId), {
|
|
912
|
+
id: svgOperationId(input.gesture, 2),
|
|
913
|
+
value: input.inserted.edge,
|
|
914
|
+
}),
|
|
915
|
+
op(options.domain.address(`edges`, input.targetSubpathId), {
|
|
916
|
+
id: svgOperationId(input.gesture, 3),
|
|
917
|
+
value: input.continuationEdge,
|
|
918
|
+
}),
|
|
919
|
+
op(
|
|
920
|
+
options.domain.address(`subpathOrder`, input.pathId),
|
|
921
|
+
orderPlacement(
|
|
922
|
+
state.getState(subpathOrderAtoms, input.pathId),
|
|
923
|
+
input.gesture,
|
|
924
|
+
4,
|
|
925
|
+
input.inserted.subpathId,
|
|
926
|
+
targetIndex,
|
|
927
|
+
),
|
|
928
|
+
),
|
|
929
|
+
])
|
|
930
|
+
},
|
|
931
|
+
async undo(_gesture) {
|
|
932
|
+
if (options.history === undefined) return false
|
|
933
|
+
const result = await options.history.undo()
|
|
934
|
+
if (result.status === `rejected`) throw new Error(result.reason)
|
|
935
|
+
return result.status === `accepted`
|
|
936
|
+
},
|
|
937
|
+
}
|
|
938
|
+
return editor
|
|
939
|
+
}
|