kipphi 2.0.0 → 2.0.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/operation.ts ADDED
@@ -0,0 +1,1378 @@
1
+
2
+ import TC from "./time";
3
+ import { EventType, EventValueESType, ExtendedEventTypeName, NoteType, RGB, TimeT } from "./chartTypes";
4
+ import { Chart, JudgeLineGroup, BasicEventName, UIName } from "./chart";
5
+ import { TemplateEasing, TemplateEasingLib } from "./easing";
6
+ import { EventEndNode, EventStartNode, EventNodeSequence, EventNode, EventNodeLike, NonLastStartNode, SpeedENS } from "./event";
7
+ import { JudgeLine, ExtendedLayer } from "./judgeline";
8
+ import { Note, notePropTypes, NoteNode, HNList, NNList } from "./note";
9
+ import { checkType, NodeType } from "./util";
10
+ import { EasedEvaluator, Evaluator, NumericEasedEvaluator } from "./evaluator";
11
+ import { err, ERROR_IDS, KPAError } from "./env";
12
+
13
+
14
+
15
+
16
+ // 有点怪异,感觉破坏了纯净性。不管了(
17
+ enum JudgeLinesEditorLayoutType {
18
+ ordered = 0b001,
19
+ tree = 0b010,
20
+ grouped = 0b100
21
+ }
22
+
23
+
24
+ class NeedsReflowEvent extends Event {
25
+ constructor(public condition: number) {
26
+ super("needsreflow");
27
+ }
28
+ }
29
+
30
+ class OperationEvent extends Event {
31
+ constructor(t: string, public operation: Operation) {
32
+ super(t);
33
+ }
34
+ }
35
+
36
+ class OperationErrorEvent extends OperationEvent {
37
+ constructor(operation: Operation, public error: Error) {
38
+ super("error", operation);
39
+ }
40
+ }
41
+
42
+ export class OperationList extends EventTarget {
43
+ operations: Operation[];
44
+ undoneOperations: Operation[];
45
+ constructor(public chart: Chart) {
46
+ super()
47
+ this.operations = [];
48
+ this.undoneOperations = [];
49
+ }
50
+ undo() {
51
+ const op = this.operations.pop()
52
+ if (op) {
53
+ if (!this.chart.modified){
54
+ this.chart.modified = true;
55
+ this.dispatchEvent(new Event("firstmodified"))
56
+ }
57
+
58
+ try {
59
+ op.undo(this.chart);
60
+ } catch (e) {
61
+ this.dispatchEvent(new OperationErrorEvent(op, e as Error))
62
+ return
63
+ }
64
+ this.undoneOperations.push(op)
65
+ this.dispatchEvent(new OperationEvent("undo", op))
66
+ this.processFlags(op);
67
+ } else {
68
+ this.dispatchEvent(new Event("noundo"))
69
+ }
70
+ }
71
+ redo() {
72
+ const op = this.undoneOperations.pop()
73
+ if (op) {
74
+ if (!this.chart.modified){
75
+ this.chart.modified = true;
76
+ this.dispatchEvent(new Event("firstmodified"))
77
+ }
78
+
79
+ try {
80
+ op.do(this.chart);
81
+ } catch (e) {
82
+ this.dispatchEvent(new OperationErrorEvent(op, e as Error))
83
+ return
84
+ }
85
+ this.operations.push(op)
86
+ this.dispatchEvent(new OperationEvent("redo", op))
87
+ this.processFlags(op);
88
+ } else {
89
+ this.dispatchEvent(new Event("noredo"))
90
+ }
91
+ }
92
+ do(operation: Operation) {
93
+ if (operation.ineffective) {
94
+ return
95
+ }
96
+ if (!this.chart.modified){
97
+ this.chart.modified = true;
98
+ this.dispatchEvent(new Event("firstmodified"))
99
+ }
100
+ // 如果上一个操作是同一个构造器的,那么修改上一个操作而不是推入新的操作
101
+ if (this.operations.length !== 0) {
102
+
103
+ const lastOp = this.operations[this.operations.length - 1]
104
+ if (operation.constructor === lastOp.constructor) {
105
+ // 返回值指示是否重写成功
106
+ if (lastOp.rewrite(operation)) {
107
+ this.processFlags(operation)
108
+ return;
109
+ }
110
+ }
111
+ }
112
+ try {
113
+ operation.do(this.chart);
114
+ } catch (e) {
115
+ this.dispatchEvent(new OperationErrorEvent(operation, e as Error))
116
+ return
117
+ }
118
+ this.dispatchEvent(new OperationEvent("do", operation));
119
+ this.processFlags(operation);
120
+ this.operations.push(operation);
121
+ }
122
+ processFlags(operation: Operation) {
123
+
124
+ if (operation.updatesEditor) {
125
+ this.dispatchEvent(new Event("needsupdate"));
126
+ }
127
+ if (operation.needsComboRecount) {
128
+ this.dispatchEvent(new Event("maxcombochanged"));
129
+ }
130
+ if (operation.reflows) {
131
+ this.dispatchEvent(new NeedsReflowEvent(operation.reflows))
132
+ }
133
+ }
134
+ clear() {
135
+ this.operations = [];
136
+ }
137
+ }
138
+
139
+
140
+
141
+ export abstract class Operation {
142
+ ineffective: boolean;
143
+ updatesEditor: boolean;
144
+ // 用于判定线编辑区的重排,若操作完成时的布局为这个值就会重排
145
+ reflows: number;
146
+ needsComboRecount: boolean;
147
+ constructor() {
148
+
149
+ }
150
+ abstract do(chart: Chart): void
151
+ abstract undo(chart: Chart): void
152
+ rewrite(op: typeof this): boolean {return false;}
153
+ toString(): string {
154
+ return this.constructor.name;
155
+ }
156
+ static lazy<C extends new (...args: any[]) => any = typeof this>(this: C, ...args: ConstructorParameters<C>) {
157
+ return new LazyOperation<C>(this, ...args)
158
+ }
159
+ }
160
+
161
+
162
+
163
+
164
+ /**
165
+ * 懒操作,实例化的时候不记录任何数据,do的时候才执行真正实例化
166
+ * 防止连续的操作中状态改变导致的错误
167
+ */
168
+ class LazyOperation<C extends new (...args: any[]) => any> extends Operation {
169
+ public operationClass: C;
170
+ public args: ConstructorParameters<C>;
171
+ public operation: InstanceType<C> | null = null;
172
+ constructor(
173
+ operationClass: C,
174
+ ...args: ConstructorParameters<C>
175
+ ) {
176
+ super();
177
+ this.operationClass = operationClass;
178
+ this.args = args;
179
+ }
180
+ do(chart: Chart) {
181
+ this.operation = new this.operationClass(...this.args);
182
+ this.operation.do(chart);
183
+ }
184
+ undo(chart: Chart) {
185
+ this.operation.undo(chart);
186
+ }
187
+ }
188
+
189
+
190
+ /**
191
+ * C语言借来的概念
192
+ *
193
+ * 一个不确定类型的子操作
194
+ *
195
+ * 注意这个操作不懒,会在构造时就实例化子操作
196
+ */
197
+ class UnionOperation<T extends Operation> extends Operation {
198
+ operation: T;
199
+ constructor(matcher: () => T) {
200
+ super();
201
+ this.operation = matcher();
202
+ if (!this.operation) {
203
+ this.ineffective = true;
204
+ }
205
+ }
206
+ // 这样子写不够严密,如果要继承这个类,并且子操作需要谱面,就要重写这个方法的签名
207
+ do(chart?: Chart) {
208
+ this.operation.do(chart);
209
+ }
210
+ undo(chart?: Chart) {
211
+ this.operation.undo(chart);
212
+ }
213
+ }
214
+
215
+
216
+ export class ComplexOperation<T extends Operation[]> extends Operation {
217
+ subOperations: T;
218
+ length: number;
219
+ constructor(...sub: T) {
220
+ super()
221
+ this.subOperations = sub
222
+ this.length = sub.length
223
+ this.reflows = sub.reduce((prev, op) => prev | op.reflows, 0);
224
+ this.updatesEditor = sub.some((op) => op.updatesEditor);
225
+ this.needsComboRecount = sub.some((op) => op.needsComboRecount);
226
+ }
227
+ // 这样子写不够严密,如果要继承这个类,并且子操作需要谱面,就要重写这个方法的签名
228
+ do(chart?: Chart) {
229
+ const length = this.length
230
+ for (let i = 0; i < length; i++) {
231
+ const op = this.subOperations[i]
232
+ if (op.ineffective) {
233
+ continue;
234
+ }
235
+ op.do(chart)
236
+ }
237
+ }
238
+ undo(chart?: Chart) {
239
+ const length = this.length
240
+ for (let i = length - 1; i >= 0; i--) {
241
+ const op = this.subOperations[i]
242
+ if (op.ineffective) { continue; }
243
+ op.undo(chart)
244
+ }
245
+ }
246
+ }
247
+
248
+ type NotePropNamePhiZone = "judgeSize" | "tint" | "tintHitEffects";
249
+
250
+ type NotePropName = "speed" | "type" | "positionX" | "startTime" | "endTime" | "alpha" | "size" | "visibleBeats" | "yOffset" | "above" | "isFake" | NotePropNamePhiZone;
251
+
252
+ export class NotePropChangeOperation<T extends NotePropName> extends Operation {
253
+ field: T;
254
+ note: Note;
255
+ previousValue: Note[T]
256
+ value: Note[T];
257
+ updatesEditor = true;
258
+ constructor(note: Note, field: T, value: Note[T]) {
259
+ super()
260
+ this.field = field
261
+ this.note = note;
262
+ this.value = value;
263
+ if (!checkType(value, notePropTypes[field])) {
264
+ throw err.INVALID_NOTE_PROP_TYPE(field, value, notePropTypes[field]);
265
+ }
266
+ this.previousValue = note[field]
267
+ if (field === "isFake") {
268
+ this.needsComboRecount = true;
269
+ }
270
+ if (value === note[field]) {
271
+ this.ineffective = true
272
+ }
273
+ }
274
+ do() {
275
+ this.note[this.field] = this.value
276
+ }
277
+ undo() {
278
+ this.note[this.field] = this.previousValue
279
+ }
280
+ override rewrite(operation: NotePropChangeOperation<T>): boolean {
281
+ if (operation.note === this.note && this.field === operation.field) {
282
+ this.value = operation.value;
283
+ this.note[this.field] = operation.value
284
+ return true;
285
+ }
286
+ return false;
287
+ }
288
+ }
289
+ export class NoteRemoveOperation extends Operation {
290
+ noteNode: NoteNode;
291
+ note: Note;
292
+ isHold: boolean;
293
+ override needsComboRecount = true;
294
+ constructor(note: Note) {
295
+ super()
296
+ this.note = note // In memory of forgettting to add this(
297
+ this.isHold = note.type === NoteType.hold;
298
+ if (!note.parentNode) {
299
+ this.ineffective = true
300
+ } else {
301
+ this.noteNode = note.parentNode
302
+ }
303
+ }
304
+ do() {
305
+ const {note, noteNode} = this;
306
+ noteNode.remove(note);
307
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime)
308
+ if (needsUpdate) {
309
+ const endBeats = TC.toBeats(note.endTime);
310
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
311
+ const updateFrom = tailJump.header
312
+ const updateTo = tailJump.tailer;
313
+ // tailJump.getPreviousOf(noteNode, endBeats);
314
+ tailJump.updateRange(updateFrom, noteNode.next);
315
+ }
316
+ }
317
+ undo() {
318
+ const {note, noteNode} = this;
319
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime);
320
+ if (needsUpdate) {
321
+ const endBeats = TC.toBeats(note.endTime);
322
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
323
+ const updateFrom = tailJump.getNodeAt(endBeats).previous;
324
+ noteNode.add(note)
325
+ tailJump.updateRange(updateFrom, noteNode.next);
326
+ } else {
327
+ noteNode.add(note);
328
+ }
329
+ }
330
+ }
331
+
332
+ /**
333
+ * 删除一个note
334
+ * 从语义上删除Note要用这个操作
335
+ * 结果上,这个会更新编辑器
336
+ */
337
+ export class NoteDeleteOperation extends NoteRemoveOperation {
338
+ updatesEditor = true
339
+ }
340
+
341
+ export class MultiNoteDeleteOperation extends ComplexOperation<NoteDeleteOperation[]> {
342
+ updatesEditor = true
343
+ constructor(notes: Set<Note> | Note[]) {
344
+ if (notes instanceof Set) {
345
+ notes = [...notes];
346
+ }
347
+ super(...notes.map(note => new NoteDeleteOperation(note)))
348
+ if (notes.length === 0) {
349
+ this.ineffective = true
350
+ }
351
+ }
352
+
353
+ }
354
+
355
+ export class NoteAddOperation extends Operation {
356
+ noteNode: NoteNode
357
+ note: Note;
358
+ isHold: boolean;
359
+ updatesEditor = true
360
+ needsComboRecount = true;
361
+ constructor(note: Note, node: NoteNode) {
362
+ super()
363
+ this.note = note;
364
+ this.isHold = note.type === NoteType.hold;
365
+ this.noteNode = node
366
+ }
367
+ do() {
368
+ const {note, noteNode} = this;
369
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime);
370
+ if (needsUpdate) {
371
+ const endBeats = TC.toBeats(note.endTime);
372
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
373
+ const updateFrom = tailJump.header
374
+ // tailJump.getNodeAt(endBeats).previous;
375
+ noteNode.add(note)
376
+ tailJump.updateRange(updateFrom, noteNode.next);
377
+ } else {
378
+ noteNode.add(note);
379
+ }
380
+ }
381
+ undo() {
382
+ const {note, noteNode} = this;
383
+ noteNode.remove(note);
384
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime)
385
+ if (needsUpdate) {
386
+ const endBeats = TC.toBeats(note.endTime);
387
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
388
+ const updateFrom = tailJump.getPreviousOf(noteNode, endBeats);
389
+ tailJump.updateRange(updateFrom, noteNode.next);
390
+ }
391
+ }
392
+ }
393
+
394
+ export class MultiNoteAddOperation extends ComplexOperation<NoteAddOperation[]> {
395
+ updatesEditor = true
396
+ needsComboRecount = true;
397
+ constructor(notes: Set<Note> | Note[], judgeLine: JudgeLine) {
398
+ if (notes instanceof Set) {
399
+ notes = [...notes];
400
+ }
401
+ super(...notes.map(note => {
402
+ const node = judgeLine.getNode(note, true)
403
+ return new NoteAddOperation(note, node);
404
+ }))
405
+ if (notes.length === 0) {
406
+ this.ineffective = true
407
+ }
408
+ }
409
+ }
410
+
411
+ export class NoteTimeChangeOperation extends ComplexOperation<[
412
+ NoteRemoveOperation,
413
+ NotePropChangeOperation<"startTime">,
414
+ NoteAddOperation,
415
+ UnionOperation<NotePropChangeOperation<"endTime"> | null>]> {
416
+ note: Note
417
+ constructor(note: Note, noteNode: NoteNode) {
418
+ super(
419
+ new NoteRemoveOperation(note),
420
+ new NotePropChangeOperation(note, "startTime", noteNode.startTime),
421
+ new NoteAddOperation(note, noteNode),
422
+ new UnionOperation(() => {
423
+ if (note.type !== NoteType.hold) { // 非hold,endTime跟随startTime
424
+ return new NotePropChangeOperation(note, "endTime", noteNode.startTime)
425
+ }
426
+ })
427
+ );
428
+ this.updatesEditor = true
429
+ this.needsComboRecount = false;
430
+ if (note.type === NoteType.hold && !TC.gt(note.endTime, noteNode.startTime)) {
431
+ this.ineffective = true
432
+ }
433
+ this.note = note
434
+ if (note.parentNode === noteNode) {
435
+ this.ineffective = true
436
+ }
437
+ }
438
+ // 真的是巨坑啊
439
+ rewrite(operation: NoteTimeChangeOperation): boolean {
440
+ if (operation.note === this.note) {
441
+ this.subOperations[0] = new NoteRemoveOperation(this.note)
442
+ if (!this.subOperations[0].ineffective) {
443
+ this.subOperations[0].do()
444
+ }
445
+ this.subOperations[1].value = operation.subOperations[1].value
446
+ this.subOperations[1].do()
447
+ this.subOperations[2].noteNode = operation.subOperations[2].noteNode
448
+ this.subOperations[2].do()
449
+ if (this.subOperations[3].operation) {
450
+ this.subOperations[3].operation.value = operation.subOperations[3].operation.value;
451
+ this.subOperations[3].operation.do();
452
+ } else {
453
+ this.subOperations[3] = operation.subOperations[3];
454
+ this.subOperations[3].do();
455
+ }
456
+ return true;
457
+ }
458
+ return false
459
+ }
460
+ }
461
+
462
+ export class HoldEndTimeChangeOperation extends NotePropChangeOperation<"endTime"> {
463
+
464
+ needsComboRecount = false;
465
+ constructor(note: Note, value: TimeT) {
466
+ super(note, "endTime", value)
467
+ if (!TC.gt(value, note.startTime)) {
468
+ this.ineffective = true
469
+ }
470
+ }
471
+ do() {
472
+ super.do()
473
+ const node = this.note.parentNode;
474
+ node.sort(this.note);
475
+ const tailJump = (node.parentSeq as HNList).holdTailJump;
476
+ tailJump.updateRange(tailJump.header, tailJump.tailer);
477
+ }
478
+ undo() {
479
+ super.undo()
480
+ const node = this.note.parentNode;
481
+ node.sort(this.note);
482
+ const tailJump = (node.parentSeq as HNList).holdTailJump;
483
+ tailJump.updateRange(tailJump.header, tailJump.tailer);
484
+ }
485
+ rewrite(operation: HoldEndTimeChangeOperation): boolean { // 看懂了,不重写的话会出问题
486
+ if (operation.note === this.note && this.field === operation.field) {
487
+ if (operation.value === this.value) {
488
+ return true;
489
+ }
490
+ this.value = operation.value;
491
+ this.note[this.field] = operation.value;
492
+ const tailJump = (this.note.parentNode.parentSeq as HNList).holdTailJump;
493
+ tailJump.updateRange(tailJump.header, tailJump.tailer);
494
+ return true;
495
+ }
496
+ return false;
497
+ }
498
+ }
499
+
500
+
501
+ export class NoteSpeedChangeOperation
502
+ extends ComplexOperation<[NotePropChangeOperation<"speed">, NoteRemoveOperation, NoteAddOperation]> {
503
+ updatesEditor = true
504
+ originalTree: NNList;
505
+ judgeLine: JudgeLine;
506
+ targetTree: NNList
507
+ constructor(note: Note, value: number, line: JudgeLine) {
508
+ const valueChange = new NotePropChangeOperation(note, "speed", value);
509
+ const tree = line.getNNList(value, note.yOffset, note.type === NoteType.hold, true)
510
+ const node = tree.getNodeOf(note.startTime);
511
+ const removal = new NoteRemoveOperation(note);
512
+ const insert = new NoteAddOperation(note, node)
513
+ super(valueChange, removal, insert);
514
+ }
515
+ }
516
+
517
+ export class NoteYOffsetChangeOperation
518
+ extends ComplexOperation<[NotePropChangeOperation<"yOffset">, NoteRemoveOperation, NoteAddOperation]> {
519
+ updatesEditor = true
520
+ originalTree: NNList;
521
+ judgeLine: JudgeLine;
522
+ targetTree: NNList
523
+ constructor(note: Note, value: number, line: JudgeLine) {
524
+ const valueChange = new NotePropChangeOperation(note, "yOffset", value);
525
+ const tree = line.getNNList(note.speed, value, note.type === NoteType.hold, true)
526
+ const node = tree.getNodeOf(note.startTime);
527
+ const removal = new NoteRemoveOperation(note);
528
+ const insert = new NoteAddOperation(note, node)
529
+ super(valueChange, removal, insert);
530
+ }
531
+ }
532
+
533
+
534
+ export class NoteTypeChangeOperation
535
+ extends ComplexOperation<[NotePropChangeOperation<"type">, NoteRemoveOperation, NoteAddOperation] | [NotePropChangeOperation<"type">]> {
536
+ constructor(note: Note, value: number) {
537
+ const isHold = note.type === NoteType.hold
538
+ const valueChange = new NotePropChangeOperation(note, "type", value);
539
+ if (isHold !== (value === NoteType.hold)) {
540
+ const tree = note.parentNode.parentSeq.parentLine.getNNList(note.speed, note.yOffset, !isHold, true)
541
+ const node = tree.getNodeOf(note.startTime);
542
+ const removal = new NoteRemoveOperation(note);
543
+ const insert = new NoteAddOperation(note, node);
544
+ super(valueChange, removal, insert);
545
+ } else {
546
+ super(valueChange);
547
+ }
548
+ this.updatesEditor = true;
549
+ }
550
+ }
551
+
552
+ class NoteTreeChangeOperation extends NoteAddOperation {
553
+
554
+ }
555
+
556
+ /**
557
+ * 移除一对节点(背靠背)
558
+ */
559
+ export class EventNodePairRemoveOperation extends Operation {
560
+ updatesEditor = true;
561
+ endNode: EventEndNode<any>;
562
+ startNode: EventStartNode<any>;
563
+ sequence: EventNodeSequence<any>;
564
+ originalPrev: EventStartNode<any>;
565
+ updatesFP = false;
566
+ constructor(node: EventStartNode<any>, updatesFP = true) {
567
+ super();
568
+ if (node.previous === null) {
569
+ this.ineffective = true;
570
+ return;
571
+ }
572
+ if (node.isFirstStart()) {
573
+ this.ineffective = true;
574
+ return;
575
+ }
576
+ if (node.isSpeed()) {
577
+ updatesFP = updatesFP;
578
+ }
579
+ [this.endNode, this.startNode] = EventNode.getEndStart(node)
580
+ this.sequence = this.startNode.parentSeq
581
+ this.originalPrev = (<EventEndNode>node.previous).previous
582
+ }
583
+ do(chart: Chart) {
584
+ this.sequence.updateJump(...EventNode.removeNodePair(this.endNode, this.startNode))
585
+ if (this.updatesFP) {
586
+ // updatesFP的校验确保了序列为速度序列
587
+ (this.sequence as SpeedENS).updateFloorPositionAfter(this.originalPrev, chart.timeCalculator)
588
+ }
589
+ }
590
+ undo(chart: Chart) {
591
+ this.sequence.updateJump(...EventNode.insert(this.startNode, this.originalPrev))
592
+ if (this.updatesFP) {
593
+ (this.sequence as SpeedENS).updateFloorPositionAfter(this.originalPrev, chart.timeCalculator)
594
+ }
595
+ }
596
+ }
597
+
598
+ /**
599
+ * 将一对孤立的节点对插入到一个开始节点之后的操作。
600
+ *
601
+ * 如果这个节点对的时刻与节点对的时刻相同,那么抛出错误。
602
+ */
603
+ export class EventNodePairInsertOperation<VT> extends Operation {
604
+ updatesEditor = true
605
+ node: EventStartNode<VT>;
606
+ tarPrev: EventStartNode<VT>;
607
+ sequence: EventNodeSequence<VT>;
608
+ originalValue: VT;
609
+ value: VT;
610
+ updatesFP = false;
611
+ /**
612
+ *
613
+ * @param node 要插入的节点 the node to insert
614
+ * @param targetPrevious 要插在谁后面 The node to insert after, accessed through `EventNodeSequence.getNodeAt(TC.toBeats(node))`
615
+ */
616
+ constructor(node: EventStartNode<VT>, targetPrevious: EventStartNode<VT>, updatesFP = true) {
617
+ super()
618
+ this.node = node;
619
+ this.tarPrev = targetPrevious
620
+ this.sequence = targetPrevious.parentSeq
621
+ if (TC.eq(node.time, targetPrevious.time)) {
622
+ throw err.SEQUENCE_NODE_TIME_OCCUPIED(node.time, this.sequence.id);
623
+ }
624
+ if (!this.sequence) {
625
+ throw err.PARENT_SEQUENCE_NOT_FOUND(targetPrevious?.time);
626
+ }
627
+ this.updatesFP = updatesFP && node.isSpeed();
628
+ }
629
+ do() {
630
+ const [endNode, startNode] = EventNode.insert(this.node, this.tarPrev);
631
+ this.node.parentSeq.updateJump(endNode, startNode)
632
+ }
633
+ undo() {
634
+ this.sequence.updateJump(...EventNode.removeNodePair(...EventNode.getEndStart(this.node)))
635
+ }
636
+ }
637
+
638
+ export class EventNodePairInsertOrOverwriteOperation<VT>
639
+ extends UnionOperation<EventNodePairInsertOperation<VT> | EventNodeValueChangeOperation<VT>> {
640
+ overlapping: boolean = false;
641
+ constructor(node: EventStartNode<VT>, targetPrevious: EventStartNode<VT>, updatesFP = true) {
642
+ super(() => {
643
+ if (TC.eq(node.time, targetPrevious.time)) {
644
+ this.overlapping = true;
645
+ return new EventNodeValueChangeOperation(targetPrevious, node.value);
646
+ } else {
647
+ return new EventNodePairInsertOperation(node, targetPrevious, updatesFP);
648
+ }
649
+ })
650
+ }
651
+ }
652
+
653
+
654
+ /**
655
+ * 批量添加节点对
656
+ *
657
+ * 节点对需要有序的,且不能有重叠
658
+
659
+ */
660
+ export class MultiNodeAddOperation<VT> extends ComplexOperation<EventNodePairInsertOrOverwriteOperation<VT>[]> {
661
+ updatesEditor = true
662
+ updatesFP = false;
663
+ constructor(public nodes: EventStartNode<VT>[], public seq: EventNodeSequence<VT>) {
664
+ let prev = seq.getNodeAt(TC.toBeats(nodes[0].time));
665
+ super(...nodes.map(node => {
666
+ const op = new EventNodePairInsertOrOverwriteOperation(node, prev,false);
667
+ if (!op.overlapping) prev = node; // 有种reduce的感觉
668
+ return op
669
+ }));
670
+ this.updatesFP = seq.type === EventType.speed
671
+ }
672
+ do(chart: Chart) {
673
+ super.do();
674
+ if (this.updatesFP) {
675
+ (this.seq as SpeedENS).updateFloorPositionAfter(this.nodes[0] as EventStartNode<number>, chart.timeCalculator)
676
+ }
677
+ }
678
+ undo(chart: Chart) {
679
+ super.undo();
680
+ if (this.updatesFP) {
681
+ (this.seq as SpeedENS).updateFloorPositionAfter(this.nodes[0] as EventStartNode<number>, chart.timeCalculator)
682
+ }
683
+ }
684
+ }
685
+
686
+ export class MultiNodeDeleteOperation extends ComplexOperation<LazyOperation<typeof EventNodePairRemoveOperation>[]> {
687
+ updatesEditor = true;
688
+ constructor(nodes: EventStartNode<any>[]) {
689
+ super(...nodes.map(node => EventNodePairRemoveOperation.lazy(node)));
690
+ }
691
+ }
692
+
693
+ export class EventNodeValueChangeOperation<VT> extends Operation {
694
+ updatesEditor = true
695
+ node: EventNode<VT>
696
+ value: VT;
697
+ originalValue: VT
698
+ constructor(node: EventNode<VT>, val: VT) {
699
+ super()
700
+ this.node = node
701
+ this.value = val;
702
+ this.originalValue = node.value
703
+ }
704
+ do() {
705
+ this.node.value = this.value
706
+ }
707
+ undo() {
708
+ this.node.value = this.originalValue
709
+ }
710
+ rewrite(operation: EventNodeValueChangeOperation<VT>): boolean {
711
+ if (operation.node === this.node) {
712
+ this.value = operation.value;
713
+ this.node.value = operation.value
714
+ return true;
715
+ }
716
+ return false;
717
+ }
718
+ }
719
+
720
+ export class EventNodeTimeChangeOperation extends Operation {
721
+ updatesEditor = true
722
+ sequence: EventNodeSequence;
723
+ /**
724
+ * 这里两个node不是面对面,而是背靠背
725
+ * i. e. EndNode -> StartNode
726
+ */
727
+ startNode: EventStartNode<any>;
728
+ endNode: EventEndNode<any>;
729
+ value: TimeT;
730
+ originalValue: TimeT;
731
+ originalPrevious: EventStartNode<any>;
732
+ newPrevious: EventStartNode<any>;
733
+ constructor(node: EventStartNode<any> | EventEndNode<any>, val: TimeT) {
734
+ super()
735
+ if (node.previous.type === NodeType.HEAD) {
736
+ this.ineffective = true;
737
+ return;
738
+ }
739
+ if (!TC.gt(val, [0, 0, 1])) {
740
+ this.ineffective = true;
741
+ return;
742
+ }
743
+ [this.endNode, this.startNode] = EventNode.getEndStart(node)
744
+ const seq = this.sequence = node.parentSeq
745
+ const mayBeThere = seq.getNodeAt(TC.toBeats(val))
746
+ if (mayBeThere && TC.eq(mayBeThere.time, val)) { // 不是arrayEq,这里踩坑
747
+ this.ineffective = true;
748
+ return;
749
+ }
750
+ this.originalPrevious = this.endNode.previous;
751
+ this.newPrevious = mayBeThere === this.startNode ? (<EventEndNode>this.startNode.previous).previous : mayBeThere
752
+ this.value = val;
753
+ this.originalValue = node.time
754
+ console.log("操作:", this)
755
+ }
756
+ do() { // 这里其实还要设计重新选址的问题
757
+ this.startNode.time = this.endNode.time = this.value;
758
+ if (this.newPrevious !== this.originalPrevious) {
759
+ this.sequence.updateJump(...EventNode.removeNodePair(this.endNode, this.startNode))
760
+ EventNode.insert(this.startNode, this.newPrevious)
761
+ }
762
+ this.sequence.updateJump(EventNode.previousStartOfStart(this.endNode.previous), EventNode.nextStartOfStart(this.startNode))
763
+ }
764
+ undo() {
765
+ this.endNode.time = this.startNode.time = this.originalValue;
766
+ if (this.newPrevious !== this.originalPrevious) {
767
+ this.sequence.updateJump(...EventNode.removeNodePair(this.endNode, this.startNode))
768
+ EventNode.insert(this.startNode, this.originalPrevious)
769
+ }
770
+ this.sequence.updateJump(this.endNode.previous, EventNode.nextStartOfStart(this.startNode))
771
+ }
772
+
773
+ }
774
+
775
+
776
+ export class EventNodeEvaluatorChangeOperation<VT> extends Operation {
777
+ updatesEditor = true
778
+ originalValue: Evaluator<VT>
779
+ constructor(public node: EventStartNode<VT>, public value: Evaluator<VT>) {
780
+ super();
781
+ this.originalValue = this.node.evaluator
782
+ }
783
+ do() {
784
+ this.node.evaluator = this.value
785
+ }
786
+ undo() {
787
+ this.node.evaluator = this.originalValue
788
+ }
789
+ }
790
+
791
+
792
+
793
+
794
+ // 这个地方得懒一下,不然每亩,导致撤回操作时只能撤回第一个插值节点。
795
+ export class EventInterpolationOperation<VT> extends ComplexOperation<LazyOperation<typeof EventNodePairInsertOperation>[]> {
796
+ updatesEditor = true;
797
+ constructor(public eventStartNode: EventStartNode<VT>, public step: TimeT) {
798
+ if (eventStartNode.next.type === NodeType.TAIL) {
799
+ throw err.CANNOT_INTERPOLATE_TAILING_START_NODE();
800
+ }
801
+ const subOps = [];
802
+ const endTime = eventStartNode.next.time;
803
+ let time = TC.validateIp(TC.add(eventStartNode.time, step));
804
+ let lastStart = eventStartNode;
805
+ for (; TC.lt(time, endTime); time = TC.validateIp(TC.add(time, step))) {
806
+ const value = eventStartNode.getValueAt(TC.toBeats(time));
807
+ const start = new EventStartNode(time, value);
808
+ const end = new EventEndNode(time, value);
809
+ EventNode.connect(end, start); // 之前搞成面对面了,写注释留念
810
+ subOps.push(EventNodePairInsertOperation.lazy(start, lastStart));
811
+ lastStart = start;
812
+ }
813
+ super(...subOps);
814
+ }
815
+ }
816
+
817
+
818
+
819
+
820
+ export class EventSubstituteOperation<VT extends EventValueESType> extends ComplexOperation<[...LazyOperation<typeof EventNodePairInsertOperation>[], EventNodeEvaluatorChangeOperation<VT>, EventNodeValueChangeOperation<VT>]> {
821
+ updatesEditor = true;
822
+ /**
823
+ *
824
+ * @param node
825
+ * @throws {KPAError<ERROR_IDS.CANNOT_SUBSTITUTE_EXPRESSION_EVALUATOR>}
826
+ * @throws {KPAError<ERROR_IDS.MUST_INTERPOLATE_TEMPLATE_EASING>}
827
+ */
828
+ constructor(public node: NonLastStartNode<VT>) {
829
+ const evaluator = node.evaluator;
830
+ if (!(evaluator instanceof EasedEvaluator)) {
831
+ throw err.CANNOT_SUBSTITUTE_EXPRESSION_EVALUATOR();
832
+ }
833
+ const easing = evaluator.easing;
834
+ if (!(easing instanceof TemplateEasing)) {
835
+ throw err.MUST_INTERPOLATE_TEMPLATE_EASING();
836
+ }
837
+ const srcSeq = easing.eventNodeSequence;
838
+ const srcStartTime = srcSeq.head.next.time;
839
+ const srcEndTime = srcSeq.tail.previous.time;
840
+ const srcTimeDelta = TC.vsub(srcEndTime, srcStartTime);
841
+ const startValue = node.value;
842
+ const endValue = node.next.value;
843
+ const startTime = node.time;
844
+ const timeDelta = TC.vsub(node.next.time, startTime);
845
+ const convert = (progress: number) => (evaluator as EasedEvaluator<VT>).convert(startValue, endValue, progress);
846
+
847
+ // startTime + (srcTime - srcStartTime) / srcTimeDelta * timeDelta
848
+ const convertTime = (srcTime: TimeT) => TC.vadd(startTime, TC.vmul(timeDelta, TC.div(TC.vsub(srcTime, srcStartTime), srcTimeDelta)));
849
+ const inserts = [];
850
+ let currentNode = srcSeq.head.next.next as EventEndNode;
851
+ let lastPos: EventStartNode<VT> = node;
852
+ while (true) {
853
+ const startNode = currentNode.next;
854
+ const next = startNode.next;
855
+ // 最后一个节点对在循环外处理
856
+ if (next.type === NodeType.TAIL) {
857
+ break;
858
+ }
859
+ const newEndNode = new EventEndNode(convertTime(currentNode.time), convert(currentNode.value));
860
+ const newStartNode = new EventStartNode(convertTime(startNode.time), convert(startNode.value));
861
+ const srcEvaluator = startNode.evaluator;
862
+ if (!(srcEvaluator instanceof EasedEvaluator)) {
863
+ throw new Error()
864
+ }
865
+ newStartNode.evaluator = (evaluator as EasedEvaluator<VT>).deriveWithEasing(srcEvaluator.easing)
866
+ EventNode.connect(newEndNode, newStartNode);
867
+ inserts.push(EventNodePairInsertOperation.lazy(newStartNode, lastPos));
868
+ lastPos = newStartNode;
869
+
870
+ currentNode = next;
871
+ }
872
+ const evaluatorChange = new EventNodeEvaluatorChangeOperation(node, evaluator.deriveWithEasing((srcSeq.head.next.evaluator as NumericEasedEvaluator).easing));
873
+ const valueChange = new EventNodeValueChangeOperation(node.next, convert(currentNode.value));
874
+ super(...inserts, evaluatorChange, valueChange);
875
+ }
876
+ }
877
+
878
+
879
+ export class EncapsuleOperation extends ComplexOperation<[MultiNodeDeleteOperation, EventNodeEvaluatorChangeOperation<number>, EventNodeValueChangeOperation<number>]> {
880
+ updatesEditor = true;
881
+ constructor(nodes: EventStartNode<number>[], easing: TemplateEasing) {
882
+ const len = nodes.length;
883
+ super(
884
+ new MultiNodeDeleteOperation(nodes.slice(1, -1)),
885
+ new EventNodeEvaluatorChangeOperation(nodes[0], (nodes[0].evaluator as NumericEasedEvaluator).deriveWithEasing(easing)),
886
+ // 这里nodes至少都有两个,最后一个node不可能是第一个StartNode
887
+ new EventNodeValueChangeOperation(<EventEndNode>nodes[len - 1].previous, nodes[len - 1].value)
888
+ )
889
+ }
890
+ /**
891
+ * 将一些来自sourceSequence的节点打包为一个用于模板缓动的事件序列
892
+ * 然后把sourceSequence中的源节点集合替换为单个使用了该模板的事件
893
+ * @param sourceSequence
894
+ * @param sourceNodes
895
+ */
896
+ static encapsule(templateEasingLib: TemplateEasingLib, sourceSequence: EventNodeSequence, sourceNodes: Set<EventStartNode>, name: string): EncapsuleOperation {
897
+ if (!EventNode.belongToSequence(sourceNodes, sourceSequence)) {
898
+ throw err.NODES_NOT_BELONG_TO_SAME_SEQUENCE();
899
+ }
900
+ const [oldArray, nodeArray] = EventNode.setToNewOrderedArray([0, 0, 1], sourceNodes);
901
+ if (Math.abs(nodeArray[0].value - nodeArray[nodeArray.length - 1].value) < 1e-10) {
902
+ throw err.NODES_HAS_ZERO_DELTA();
903
+ }
904
+ if (!EventNode.isContinuous(oldArray)) {
905
+ throw err.NODES_NOT_CONTINUOUS();
906
+ }
907
+ const easing = templateEasingLib.getOrNew(name);
908
+ const sequence = easing.eventNodeSequence;
909
+ sequence.effectiveBeats = TC.toBeats(nodeArray[nodeArray.length - 1].time);
910
+ // 直接do,这个不需要做成可撤销的
911
+ // @ts-expect-error 这里序列类型确定,为easing,不需要传入谱面
912
+ new MultiNodeAddOperation(nodeArray, sequence).do();
913
+
914
+ return new EncapsuleOperation(oldArray, easing);
915
+ }
916
+ }
917
+
918
+
919
+
920
+
921
+
922
+
923
+ export class JudgeLineInheritanceChangeOperation extends Operation {
924
+ originalValue: JudgeLine | null;
925
+ updatesEditor = true;
926
+ static REFLOWS = JudgeLinesEditorLayoutType.tree;
927
+ reflows = JudgeLineInheritanceChangeOperation.REFLOWS;
928
+ constructor(public chart: Chart, public judgeLine: JudgeLine, public value: JudgeLine | null) {
929
+ super();
930
+ this.originalValue = judgeLine.father;
931
+ // 这里只会让它静默失败,外面调用的时候能够在判断一次并抛错误才是最好的
932
+ if (JudgeLine.checkinterdependency(judgeLine, value)) {
933
+ this.ineffective = true;
934
+ }
935
+ }
936
+ do() {
937
+ const line = this.judgeLine;
938
+ line.father = this.value;
939
+ if (this.originalValue) {
940
+ this.originalValue.children.delete(line);
941
+ } else {
942
+ const index = this.chart.orphanLines.indexOf(line);
943
+ if (index >= 0) // Impossible to be false, theoretically
944
+ this.chart.orphanLines.splice(index, 1)
945
+ }
946
+ if (this.value) {
947
+ this.value.children.add(line);
948
+ } else {
949
+ this.chart.orphanLines.push(line);
950
+ }
951
+ }
952
+ undo() {
953
+ const line = this.judgeLine;
954
+ line.father = this.originalValue;
955
+ if (this.originalValue) {
956
+ this.originalValue.children.add(line);
957
+ } else {
958
+ this.chart.orphanLines.push(line);
959
+ }
960
+ if (this.value) {
961
+ this.value.children.delete(line);
962
+ } else {
963
+ const index = this.chart.orphanLines.indexOf(line);
964
+ if (index >= 0) // Impossible to be false, theoretically
965
+ this.chart.orphanLines.splice(index, 1)
966
+ }
967
+ }
968
+ }
969
+
970
+ export class JudgeLineRenameOperation extends Operation {
971
+ updatesEditor = true;
972
+ originalValue: string;
973
+ constructor(public judgeLine: JudgeLine, public value: string) {
974
+ super();
975
+ this.originalValue = judgeLine.name;
976
+ }
977
+ do() {
978
+ this.judgeLine.name = this.value;
979
+ }
980
+ undo() {
981
+ this.judgeLine.name = this.originalValue;
982
+ }
983
+ }
984
+
985
+ type JudgeLinePropName = "name" | "rotatesWithFather" | "anchor" | "texture" | "cover" | "zOrder";
986
+
987
+ export class JudgeLinePropChangeOperation<T extends JudgeLinePropName> extends Operation {
988
+ updatesEditor = true;
989
+ originalValue: JudgeLine[T];
990
+ constructor(public judgeLine: JudgeLine, public field: T, public value: JudgeLine[T]) {
991
+ super();
992
+ this.originalValue = judgeLine[field];
993
+ }
994
+ do() {
995
+ this.judgeLine[this.field] = this.value;
996
+ }
997
+ undo() {
998
+ this.judgeLine[this.field] = this.originalValue;
999
+ }
1000
+ }
1001
+
1002
+ export class JudgeLineRegroupOperation extends Operation {
1003
+ updatesEditor = true;
1004
+ reflows = JudgeLinesEditorLayoutType.grouped;
1005
+ originalValue: JudgeLineGroup;
1006
+ constructor(public judgeLine: JudgeLine, public value: JudgeLineGroup) {
1007
+ super();
1008
+ this.originalValue = judgeLine.group;
1009
+ }
1010
+ do() {
1011
+ this.judgeLine.group = this.value;
1012
+ this.value.add(this.judgeLine);
1013
+ this.originalValue.remove(this.judgeLine);
1014
+ }
1015
+ undo() {
1016
+ this.judgeLine.group = this.originalValue;
1017
+ this.originalValue.add(this.judgeLine);
1018
+ this.value.remove(this.judgeLine);
1019
+ }
1020
+ }
1021
+
1022
+ export class JudgeLineCreateOperation extends Operation {
1023
+ reflows = JudgeLinesEditorLayoutType.grouped | JudgeLinesEditorLayoutType.tree | JudgeLinesEditorLayoutType.ordered;
1024
+ // 之前把=写成了:半天不知道咋错了
1025
+ constructor(public chart: Chart, public judgeLine: JudgeLine) {
1026
+ super();
1027
+ }
1028
+ do() {
1029
+ const id = this.chart.judgeLines.length;
1030
+ this.judgeLine.id = id;
1031
+ this.chart.judgeLines.push(this.judgeLine);
1032
+ this.chart.orphanLines.push(this.judgeLine);
1033
+ this.chart.judgeLineGroups[0].add(this.judgeLine);
1034
+ }
1035
+ undo() {
1036
+ this.chart.judgeLineGroups[0].remove(this.judgeLine);
1037
+ this.chart.judgeLines.splice(this.chart.judgeLines.indexOf(this.judgeLine), 1);
1038
+ this.chart.orphanLines.splice(this.chart.orphanLines.indexOf(this.judgeLine), 1);
1039
+ }
1040
+ }
1041
+
1042
+ export class JudgeLineDeleteOperation extends Operation {
1043
+ readonly originalGroup: JudgeLineGroup;
1044
+ constructor(public chart: Chart, public judgeLine: JudgeLine) {
1045
+ super();
1046
+ if (!this.chart.judgeLines.includes(this.judgeLine)) {
1047
+ this.ineffective = true;
1048
+ }
1049
+ this.originalGroup = judgeLine.group;
1050
+ }
1051
+ do() {
1052
+ this.chart.judgeLines.splice(this.chart.judgeLines.indexOf(this.judgeLine), 1);
1053
+ if (this.chart.orphanLines.includes(this.judgeLine)) {
1054
+ this.chart.orphanLines.splice(this.chart.orphanLines.indexOf(this.judgeLine), 1);
1055
+ }
1056
+ this.originalGroup.remove(this.judgeLine);
1057
+ }
1058
+ undo() {
1059
+ this.chart.judgeLines.push(this.judgeLine);
1060
+ this.chart.orphanLines.push(this.judgeLine);
1061
+ this.originalGroup.add(this.judgeLine);
1062
+ }
1063
+ }
1064
+
1065
+
1066
+
1067
+ export class JudgeLineENSChangeOperation extends Operation {
1068
+ originalValue: EventNodeSequence;
1069
+ constructor(public judgeLine: JudgeLine, public layerId: number, public typeStr: BasicEventName, public value: EventNodeSequence) {
1070
+ super();
1071
+ this.originalValue = judgeLine.eventLayers[layerId][typeStr];
1072
+ }
1073
+ do() {
1074
+ this.judgeLine.eventLayers[this.layerId][this.typeStr] = this.value;
1075
+ }
1076
+ undo() {
1077
+ this.judgeLine.eventLayers[this.layerId][this.typeStr] = this.originalValue;
1078
+ }
1079
+ }
1080
+
1081
+
1082
+ export type ENSOfTypeName<T extends ExtendedEventTypeName> = {
1083
+ "scaleX": EventNodeSequence<number>,
1084
+ "scaleY": EventNodeSequence<number>
1085
+ "text": EventNodeSequence<string>,
1086
+ "color": EventNodeSequence<RGB>
1087
+ }[T]
1088
+ export class JudgeLineExtendENSChangeOperation<T extends ExtendedEventTypeName> extends Operation {
1089
+ originalValue: ENSOfTypeName<T>;
1090
+ constructor(public judgeLine: JudgeLine, public typeStr: T, public value: ENSOfTypeName<T> | null) {
1091
+ super();
1092
+ this.originalValue = judgeLine.extendedLayer[typeStr satisfies keyof ExtendedLayer] as ENSOfTypeName<T>;
1093
+ }
1094
+ do() {
1095
+ this.judgeLine.extendedLayer[this.typeStr] = this.value
1096
+ }
1097
+ undo() {
1098
+ this.judgeLine.extendedLayer[this.typeStr] = this.originalValue
1099
+ }
1100
+
1101
+ }
1102
+
1103
+ export class EventNodeSequenceRenameOperation extends Operation {
1104
+ updatesEditor: boolean = true;
1105
+ originalName: string;
1106
+ constructor(public sequence: EventNodeSequence, public newName: string) {
1107
+ super();
1108
+ this.originalName = sequence.id;
1109
+ if (this.originalName === newName) {
1110
+ this.ineffective = true;
1111
+ }
1112
+ }
1113
+ do(chart: Chart) {
1114
+ chart.sequenceMap.set(this.newName, this.sequence)
1115
+ chart.sequenceMap.delete(this.originalName);
1116
+ this.sequence.id = this.newName;
1117
+ }
1118
+ undo(chart: Chart) {
1119
+ chart.sequenceMap.set(this.originalName, this.sequence)
1120
+ chart.sequenceMap.delete(this.newName);
1121
+ this.sequence.id = this.originalName;
1122
+ }
1123
+ }
1124
+
1125
+ export class UIAttachOperation extends Operation {
1126
+ updatesEditor = true;
1127
+ constructor(public chart: Chart, public judgeLine: JudgeLine, public ui: UIName) {
1128
+ super();
1129
+ }
1130
+ do() {
1131
+ this.chart.attachUIToLine(this.ui, this.judgeLine);
1132
+ }
1133
+ undo() {
1134
+ this.chart.detachUI(this.ui);
1135
+ }
1136
+ }
1137
+
1138
+ export class UIDetachOperation extends Operation {
1139
+ updatesEditor = true;
1140
+ judgeLine: JudgeLine;
1141
+ constructor(public chart: Chart, public ui: UIName) {
1142
+ super();
1143
+ if (chart[`${ui}Attach` satisfies keyof Chart]) {
1144
+ this.judgeLine = chart[`${ui}Attach` satisfies keyof Chart];
1145
+ } else {
1146
+ this.ineffective = true;
1147
+ }
1148
+ }
1149
+ do() {
1150
+ this.chart.detachUI(this.ui);
1151
+ }
1152
+ undo() {
1153
+ this.chart.attachUIToLine(this.ui, this.judgeLine);
1154
+ }
1155
+ }
1156
+
1157
+ export class JudgeLineDetachAllUIOperation extends Operation {
1158
+ updatesEditor = true;
1159
+ uinames: UIName[];
1160
+ constructor(public chart: Chart, public judgeLine: JudgeLine) {
1161
+ super();
1162
+ this.uinames = chart.queryJudgeLineUI(this.judgeLine);
1163
+ }
1164
+ do() {
1165
+ for (const ui of this.uinames) {
1166
+ this.chart.detachUI(ui);
1167
+ }
1168
+ }
1169
+ undo() {
1170
+ for (const ui of this.uinames) {
1171
+ this.chart.attachUIToLine(ui, this.judgeLine);
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ type ChartPropName = "name" | "level" | "composer" | "illustrator" | "charter" | "offset"
1177
+
1178
+ export class ChartPropChangeOperation<T extends ChartPropName> extends Operation {
1179
+ originalValue: Chart[T];
1180
+ constructor(public chart: Chart, public field: T, public value: Chart[T]) {
1181
+ super();
1182
+ this.originalValue = chart[field];
1183
+ if (field === "level" || field === "name") {
1184
+ this.updatesEditor = true;
1185
+ }
1186
+ }
1187
+ do() {
1188
+ this.chart[this.field] = this.value;
1189
+ }
1190
+ undo() {
1191
+ this.chart[this.field] = this.originalValue;
1192
+ }
1193
+ }
1194
+
1195
+
1196
+
1197
+ type TimeRange = [TimeT, TimeT]
1198
+
1199
+ /**
1200
+ * 所有节点事件加上一个值。
1201
+ * 此操作假定了节点被偏移时不会产生“碰撞”。
1202
+ * 节点要有序
1203
+ * @private
1204
+ */
1205
+ export class MultiNodeOffsetOperation extends Operation {
1206
+ constructor(public nodes: readonly EventStartNode<any>[], public offset: TimeT) {
1207
+ super();
1208
+ }
1209
+ do() {
1210
+ const offset = this.offset;
1211
+ const nodes = this.nodes;
1212
+ const len = nodes.length;
1213
+ const node = nodes[0];
1214
+ if (node.previous.type !== NodeType.HEAD) {
1215
+ node.time = TC.validateIp(TC.add(node.time, offset));
1216
+ node.previous.time = TC.validateIp(TC.add(node.previous.time, offset));
1217
+ }
1218
+ for (let i = 1; i < len; i++) {
1219
+ const node = nodes[i];
1220
+ node.time = TC.validateIp(TC.add(node.time, offset));
1221
+ const previous = node.previous as EventEndNode<any>;
1222
+ previous.time = TC.validateIp(TC.add(previous.time, offset));
1223
+ }
1224
+ }
1225
+ undo() {
1226
+ const offset = this.offset;
1227
+ const nodes = this.nodes;
1228
+ const len = nodes.length;
1229
+ const node = nodes[0];
1230
+ if (node.previous.type !== NodeType.HEAD) {
1231
+ node.time = TC.vadd(node.time, offset);
1232
+ node.previous.time = TC.vadd(node.previous.time, offset);
1233
+ }
1234
+ for (let i = 1; i < len; i++) {
1235
+ const node = nodes[i];
1236
+ node.time = TC.vadd(node.time, offset);
1237
+ const previous = node.previous as EventEndNode<any>;
1238
+ previous.time = TC.vadd(previous.time, offset);
1239
+ }
1240
+ }
1241
+ }
1242
+
1243
+
1244
+ export class ENSTimeRangeDeleteOperation extends ComplexOperation<[MultiNodeDeleteOperation, MultiNodeOffsetOperation]> {
1245
+ beforeToStart: EventStartNode<any> | EventNodeLike<NodeType.HEAD, any>;
1246
+ constructor(public eventNodeSequence: EventNodeSequence<any>, public timeRange: TimeRange) {
1247
+ // 找出所有在范围内的节点并删除
1248
+ const node = eventNodeSequence.getNodeAt(TC.toBeats(timeRange[0]));
1249
+ const beforeToStart = EventNode.previousStartOfStart(node);
1250
+ const toBeDeleted = eventNodeSequence.getNodesFromOneAndRangeRight(node, timeRange[1]);
1251
+ // 将后续所有节点加入
1252
+ const toBeOffset = eventNodeSequence.getNodesAfterOne(toBeDeleted[toBeDeleted.length - 1]);
1253
+ super(new MultiNodeDeleteOperation(toBeDeleted), new MultiNodeOffsetOperation(toBeOffset, TC.vsub(timeRange[0], timeRange[1])))
1254
+ this.beforeToStart = beforeToStart;
1255
+ }
1256
+ do() {
1257
+ super.do();
1258
+ const ens = this.eventNodeSequence;
1259
+ ens.updateJump(this.beforeToStart, ens.tail);
1260
+ }
1261
+ undo() {
1262
+ super.undo();
1263
+ const ens = this.eventNodeSequence;
1264
+ ens.updateJump(this.beforeToStart, ens.tail);
1265
+ }
1266
+ }
1267
+
1268
+ export class ENSAddBlankOperation extends MultiNodeOffsetOperation {
1269
+ updatesEditor = true;
1270
+ constructor(public ens: EventNodeSequence<any>, pos: TimeT, length: TimeT) {
1271
+ super(
1272
+ ens.getNodesAfterOne(ens.getNodeAt(TC.toBeats(pos))),
1273
+ length
1274
+ );
1275
+ }
1276
+
1277
+ do() {
1278
+ super.do();
1279
+ const ens = this.ens;
1280
+ ens.updateJump(ens.head, ens.tail);
1281
+ }
1282
+ undo() {
1283
+ super.undo();
1284
+ const ens = this.ens;
1285
+ ens.updateJump(ens.head, ens.tail);
1286
+ }
1287
+ }
1288
+
1289
+ // 按照规矩,音符节点的时间不可变,所以不会对音符节点动手。
1290
+ // 依旧不用组合的NoteTimeChangeOperation的方式,因为那需要多次更新跳数组。
1291
+ /**
1292
+ * @author Zes Minkey Young
1293
+ */
1294
+ // @ts-expect-error 我需要明确禁用掉lazy静态方法,这不会破坏类型安全。
1295
+ export class MultiNoteOffsetOperation extends Operation {
1296
+ constructor(public nnList: NNList, public notes: readonly Note[], public offset: TimeT) {
1297
+ super();
1298
+ }
1299
+ do() {
1300
+ const offset = this.offset;
1301
+ const notes = this.notes;
1302
+ const len = notes.length;
1303
+ const nnList = this.nnList;
1304
+ for (let i = 0; i < len; i++) {
1305
+ const note = notes[i];
1306
+ const startTime = TC.vadd(note.startTime, offset);
1307
+ note.startTime = startTime;
1308
+ note.endTime = TC.vadd(note.endTime, offset);
1309
+ note.parentNode.remove(note);
1310
+ nnList.getNodeOf(startTime).add(note);
1311
+ }
1312
+ nnList.jump.updateRange(nnList.head, nnList.tail);
1313
+ if (nnList instanceof HNList) {
1314
+ nnList.holdTailJump.updateRange(nnList.head, nnList.tail);
1315
+ }
1316
+ }
1317
+ undo() {
1318
+ const offset = this.offset;
1319
+ const notes = this.notes;
1320
+ const len = notes.length;
1321
+ const nnList = this.nnList;
1322
+ for (let i = 0; i < len; i++) {
1323
+ const note = notes[i];
1324
+ const startTime = TC.vsub(note.startTime, offset);
1325
+ note.startTime = startTime;
1326
+ note.endTime = TC.vsub(note.endTime, offset);
1327
+ note.parentNode.remove(note);
1328
+ nnList.getNodeOf(startTime).add(note);
1329
+ }
1330
+ nnList.jump.updateRange(nnList.head, nnList.tail);
1331
+ if (nnList instanceof HNList) {
1332
+ nnList.holdTailJump.updateRange(nnList.head, nnList.tail);
1333
+ }
1334
+ }
1335
+ // 禁用,因为无效
1336
+ private static lazy() {}
1337
+ }
1338
+
1339
+
1340
+ export class NNListTimeRangeDeleteOperation extends ComplexOperation<[MultiNoteDeleteOperation, MultiNoteOffsetOperation]> {
1341
+ constructor(public nnList: NNList, public timeRange: TimeRange, public updatesJump: boolean = true) {
1342
+ const delNodes = nnList.getNodesFromOneAndRangeRight(nnList.getNodeOf(timeRange[0]), timeRange[1]);
1343
+ const delNotes = [];
1344
+ const dlen = delNodes.length;
1345
+ for (let i = 0; i < dlen; i++) {
1346
+ delNotes.push(...delNodes[i].notes);
1347
+ }
1348
+ const offsetNodes = nnList.getNodesAfterOne(delNodes[dlen - 1]);
1349
+ const offsetNotes = [];
1350
+ const olen = offsetNodes.length;
1351
+ for (let i = 0; i < olen; i++) {
1352
+ offsetNotes.push(...offsetNodes[i].notes);
1353
+ }
1354
+ super(new MultiNoteDeleteOperation(delNotes), new MultiNoteOffsetOperation(nnList, offsetNotes, TC.vsub(timeRange[0], timeRange[1])));
1355
+ }
1356
+ do() {
1357
+ super.do();
1358
+ this.nnList.clearEmptyNodes(this.updatesJump)
1359
+ }
1360
+ undo() {
1361
+ super.undo();
1362
+ this.nnList.clearEmptyNodes(this.updatesJump)
1363
+ }
1364
+ }
1365
+
1366
+ export class NNListAddBlankOperation extends MultiNoteOffsetOperation {
1367
+ updatesEditor = true;
1368
+ constructor(nnList: NNList, pos: TimeT, length: TimeT) {
1369
+ const nns = nnList.getNodesAfterOne(nnList.getNodeOf(pos));
1370
+ const notes = [];
1371
+ const len = nns.length;
1372
+ for (let i = 0; i < len; i++) {
1373
+ notes.push(...nns[i].notes);
1374
+ }
1375
+ super(nnList, notes, length);
1376
+ }
1377
+ }
1378
+