kipphi 2.0.0 → 2.1.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/note.ts CHANGED
@@ -5,30 +5,17 @@ import type { Chart } from "./chart";
5
5
  import { NoteType, type NNListDataKPA, type NoteDataKPA, type NoteDataRPE, type NoteNodeDataKPA, type TimeT } from "./chartTypes";
6
6
  import type { JudgeLine } from "./judgeline";
7
7
  import { JumpArray } from "./jumparray";
8
- import { TimeCalculator } from "./time";
8
+ import { type TimeCalculator } from "./bpm";
9
+ import TC from "./time";
9
10
  import { hex2rgb, NodeType, rgb2hex } from "./util";
10
11
 
11
12
  /// #declaration:global
12
13
 
13
- const TC = TimeCalculator;
14
14
 
15
15
  export type HEX = number;
16
16
 
17
17
 
18
18
 
19
- const node2string = (node: AnyNN) => {
20
- if (!node) {
21
- return "" + node
22
- }
23
- if (node.type === NodeType.HEAD || node.type === NodeType.TAIL) {
24
- return node.type === NodeType.HEAD ? "H" : node.type === NodeType.TAIL ? "T" : "???"
25
- }
26
- if (!node.notes) {
27
- return "EventNode"
28
- }
29
- return `NN(${node.notes.length}) at ${node.startTime}`
30
- }
31
-
32
19
 
33
20
 
34
21
  export const notePropTypes = {
@@ -98,18 +85,18 @@ export class Note {
98
85
  // posNext?: Note;
99
86
  // posPreviousSibling?: Note;
100
87
  // posNextSibling: Note;
101
- constructor(data: NoteDataRPE) {
88
+ constructor(data: NoteDataRPE | NoteDataKPA) {
102
89
  this.above = data.above === 1;
103
90
  this.alpha = data.alpha ?? 255;
104
- this.endTime = data.type === NoteType.hold ? TimeCalculator.validateIp(data.endTime) : TimeCalculator.validateIp([...data.startTime]);
91
+ this.endTime = data.type === NoteType.hold ? TC.validateIp(data.endTime) : TC.validateIp([...data.startTime]);
105
92
  this.isFake = Boolean(data.isFake);
106
93
  this.positionX = data.positionX;
107
94
  this.size = data.size ?? 1.0;
108
95
  this.speed = data.speed ?? 1.0;
109
- this.startTime = TimeCalculator.validateIp(data.startTime);
96
+ this.startTime = TC.validateIp(data.startTime);
110
97
  this.type = data.type;
111
98
  this.visibleTime = data.visibleTime;
112
- // @ts-expect-error
99
+ // @ts-expect-error 若data是RPE数据,则为undefined,无影响。
113
100
  this.yOffset = data.absoluteYOffset ?? data.yOffset * this.speed;
114
101
  // @ts-expect-error 若data是RPE数据,则为undefined,无影响。
115
102
  // 当然也有可能是KPA数据但是就是没有给
@@ -137,7 +124,7 @@ export class Note {
137
124
  this.visibleBeats = Infinity;
138
125
  return;
139
126
  }
140
- const hitBeats = TimeCalculator.toBeats(this.startTime);
127
+ const hitBeats = TC.toBeats(this.startTime);
141
128
  const hitSeconds = timeCalculator.toSeconds(hitBeats);
142
129
  const visabilityChangeSeconds = hitSeconds - this.visibleTime;
143
130
  const visabilityChangeBeats = timeCalculator.secondsToBeats(visabilityChangeSeconds);
@@ -150,8 +137,8 @@ export class Note {
150
137
  */
151
138
  clone(offset: TimeT) {
152
139
  const data = this.dumpKPA();
153
- data.startTime = TimeCalculator.add(data.startTime, offset);
154
- data.endTime = TimeCalculator.add(data.endTime, offset); // 踩坑
140
+ data.startTime = TC.add(data.startTime, offset);
141
+ data.endTime = TC.add(data.endTime, offset); // 踩坑
155
142
  return new Note(data);
156
143
  }
157
144
  /*
@@ -167,7 +154,7 @@ export class Note {
167
154
  dumpRPE(timeCalculator: TimeCalculator): NoteDataRPE {
168
155
  let visibleTime: number;
169
156
  if (this.visibleBeats !== Infinity) {
170
- const beats = TimeCalculator.toBeats(this.startTime);
157
+ const beats = TC.toBeats(this.startTime);
171
158
  this.visibleBeats = timeCalculator.segmentToSeconds(beats - this.visibleBeats, beats);
172
159
  } else {
173
160
  visibleTime = 99999.0
@@ -198,7 +185,7 @@ export class Note {
198
185
  size: this.size,
199
186
  startTime: this.startTime,
200
187
  type: this.type,
201
- visibleBeats: this.visibleBeats,
188
+ visibleBeats: this.visibleBeats === Infinity ? undefined : this.visibleBeats, // 无穷不要保存,节约空间
202
189
  yOffset: this.yOffset / this.speed,
203
190
  /** 新KPAJSON认为YOffset就应该是个绝对的值,不受速度影响 */
204
191
  /** 但是有历史包袱,所以加字段 */
@@ -211,7 +198,6 @@ export class Note {
211
198
  }
212
199
  }
213
200
 
214
- type Connectee = NoteNode | NNNode
215
201
 
216
202
 
217
203
 
@@ -254,13 +240,13 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
254
240
  id: number;
255
241
  constructor(time: TimeT) {
256
242
  super(NodeType.MIDDLE);
257
- this.startTime = TimeCalculator.validateIp([...time]);
243
+ this.startTime = TC.validateIp([...time]);
258
244
  this.notes = [];
259
245
  this.id = NoteNode.count++;
260
246
  }
261
247
  static fromKPAJSON(data: NoteNodeDataKPA, timeCalculator: TimeCalculator) {
262
248
  const node = new NoteNode(data.startTime);
263
- for (let noteData of data.notes) {
249
+ for (const noteData of data.notes) {
264
250
  const note = Note.fromKPAJSON(noteData, timeCalculator);
265
251
  node.add(note);
266
252
  }
@@ -276,7 +262,7 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
276
262
  return (this.notes.length === 0 || this.notes[0].type !== NoteType.hold) ? this.startTime : this.notes[0].endTime
277
263
  }
278
264
  add(note: Note) {
279
- if (!TimeCalculator.eq(note.startTime, this.startTime)) {
265
+ if (!TC.eq(note.startTime, this.startTime)) {
280
266
  console.warn("Wrong addition!")
281
267
  }
282
268
  this.notes.push(note);
@@ -303,7 +289,7 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
303
289
  const note = notes[index];
304
290
  for (let i = index; i > 0; i--) {
305
291
  const prev = notes[i - 1];
306
- if (TimeCalculator.lt(prev.endTime, note.endTime)) {
292
+ if (TC.lt(prev.endTime, note.endTime)) {
307
293
  // swap
308
294
  notes[i] = prev;
309
295
  notes[i - 1] = note;
@@ -313,7 +299,7 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
313
299
  }
314
300
  for (let i = index; i < notes.length - 1; i++) {
315
301
  const next = notes[i + 1];
316
- if (TimeCalculator.gt(next.endTime, note.endTime)) {
302
+ if (TC.gt(next.endTime, note.endTime)) {
317
303
  // swap
318
304
  notes[i] = next;
319
305
  notes[i + 1] = note;
@@ -410,7 +396,7 @@ export class NNList {
410
396
  if (prev.type === NodeType.HEAD) {
411
397
  return;
412
398
  }
413
- this.effectiveBeats = TimeCalculator.toBeats(prev.endTime)
399
+ this.effectiveBeats = TC.toBeats(prev.endTime)
414
400
  }
415
401
  const effectiveBeats: number = this.effectiveBeats;
416
402
  this.jump = new JumpArray<AnyNN>(
@@ -423,12 +409,11 @@ export class NNList {
423
409
  return [null, null]
424
410
  }
425
411
  const nextNode = node.next;
426
- const startTime = (node.type === NodeType.HEAD) ? 0 : TimeCalculator.toBeats(node.startTime)
412
+ const startTime = (node.type === NodeType.HEAD) ? 0 : TC.toBeats(node.startTime)
427
413
  return [startTime, nextNode]
428
414
  },
429
- // @ts-ignore
430
415
  (note: NoteNode, beats: number) => {
431
- return TimeCalculator.toBeats(note.startTime) >= beats ? false : <NoteNode>note.next; // getNodeAt有guard
416
+ return TC.toBeats(note.startTime) >= beats ? false : <NoteNode>note.next; // getNodeAt有guard
432
417
  })
433
418
  }
434
419
  /**
@@ -447,12 +432,12 @@ export class NNList {
447
432
  * @returns
448
433
  */
449
434
  getNodeOf(time: TimeT): NoteNode {
450
- let node = this.getNodeAt(TimeCalculator.toBeats(time), false)
435
+ let node = this.getNodeAt(TC.toBeats(time), false)
451
436
  .previous;
452
437
 
453
438
 
454
- let isEqual = node.type !== NodeType.HEAD && TimeCalculator.eq((node as NoteNode).startTime, time)
455
- if (node.next.type !== NodeType.TAIL && TimeCalculator.eq((node.next as NoteNode).startTime, time)) {
439
+ let isEqual = node.type !== NodeType.HEAD && TC.eq((node as NoteNode).startTime, time)
440
+ if (node.next.type !== NodeType.TAIL && TC.eq((node.next as NoteNode).startTime, time)) {
456
441
  isEqual = true;
457
442
  node = node.next;
458
443
  }
@@ -567,14 +552,12 @@ export class HNList extends NNList {
567
552
  if (node.type === NodeType.TAIL) {
568
553
  return [null, null]
569
554
  }
570
- if (!node) debugger
571
555
  const nextNode = node.next;
572
- const endTime = node.type === NodeType.HEAD ? 0 : TimeCalculator.toBeats(node.endTime)
556
+ const endTime = node.type === NodeType.HEAD ? 0 : TC.toBeats(node.endTime)
573
557
  return [endTime, nextNode]
574
558
  },
575
- // @ts-ignore
576
559
  (node: NoteNode, beats: number) => {
577
- return TimeCalculator.toBeats(node.endTime) >= beats ? false : <NoteNode>node.next; // getNodeAt有guard
560
+ return TC.toBeats(node.endTime) >= beats ? false : <NoteNode>node.next; // getNodeAt有guard
578
561
  }
579
562
  )
580
563
  }
@@ -596,7 +579,7 @@ export type NNNOrHead = NNNode | NNNodeLike<NodeType.HEAD>;
596
579
  export type NNNOrTail = NNNode | NNNodeLike<NodeType.TAIL>;
597
580
  type AnyNNN = NNNode | NNNodeLike<NodeType.HEAD> | NNNodeLike<NodeType.TAIL>;
598
581
 
599
- class NNNodeLike<T extends NodeType> {
582
+ export class NNNodeLike<T extends NodeType> {
600
583
  previous: NNNOrHead;
601
584
  next: NNNOrTail;
602
585
  startTime: TimeT;
@@ -618,7 +601,7 @@ export class NNNode extends NNNodeLike<NodeType.MIDDLE> {
618
601
  super(NodeType.MIDDLE);
619
602
  this.noteNodes = []
620
603
  this.holdNodes = [];
621
- this.startTime = TimeCalculator.validateIp([...time])
604
+ this.startTime = TC.validateIp([...time])
622
605
  }
623
606
  get endTime() {
624
607
  let latest: TimeT = this.startTime;
@@ -681,7 +664,7 @@ export class NNNList {
681
664
  const originalListLength = this.timesWithNotes || 512;
682
665
  /*
683
666
  if (!this.effectiveBeats) {
684
- this.effectiveBeats = TimeCalculator.toBeats(this.tail.previous.endTime)
667
+ this.effectiveBeats = TC.toBeats(this.tail.previous.endTime)
685
668
  }
686
669
  */
687
670
  const effectiveBeats: number = this.effectiveBeats;
@@ -695,25 +678,19 @@ export class NNNList {
695
678
  return [null, null]
696
679
  }
697
680
  const nextNode = node.next;
698
- const startTime = node.type === NodeType.HEAD ? 0 : TimeCalculator.toBeats((node as NNNode).startTime)
681
+ const startTime = node.type === NodeType.HEAD ? 0 : TC.toBeats((node as NNNode).startTime)
699
682
  return [startTime, nextNode]
700
683
  },
701
- // @ts-ignore
702
684
  (note: NNNode, beats: number) => {
703
- return TimeCalculator.toBeats(note.startTime) >= beats ? false : <NNNode>note.next; // getNodeAt有guard
704
- }
705
- /*,
706
- (note: Note) => {
707
- const prev = note.previous;
708
- return prev.type === NodeType.HEAD ? note : prev
709
- })*/)
685
+ return TC.toBeats(note.startTime) >= beats ? false : <NNNode>note.next; // getNodeAt有guard
686
+ })
710
687
  }
711
688
  getNodeAt(beats: number, beforeEnd=false): NNNode | NNNodeLike<NodeType.TAIL> {
712
689
  return this.jump.getNodeAt(beats) as NNNode | NNNodeLike<NodeType.TAIL>;
713
690
  }
714
691
  getNode(time: TimeT): NNNode {
715
- const node = this.getNodeAt(TimeCalculator.toBeats(time), false).previous;
716
- if (node.type === NodeType.HEAD || TimeCalculator.ne((node as NNNode).startTime, time)) {
692
+ const node = this.getNodeAt(TC.toBeats(time), false).previous;
693
+ if (node.type === NodeType.HEAD || TC.ne((node as NNNode).startTime, time)) {
717
694
  const newNode = new NNNode(time);
718
695
  const next = node.next
719
696
  NNNode.insert(node, newNode, next);
@@ -0,0 +1,285 @@
1
+ import { Chart } from "../chart";
2
+
3
+
4
+
5
+
6
+ export type OpEventType = "do" | "undo" | "redo" | "error" | "needsupdate" | "maxcombochanged" | "noundo" | "noredo" | "firstmodified" | "needsreflow";
7
+
8
+ // 最讲类型安全的一集(
9
+ // 当然要有,不然的话编辑器那边检测的时候逆变会出问题
10
+
11
+ interface DirectlyInstaciableEventMap {
12
+ "noundo": OpEvent;
13
+ "noredo": OpEvent;
14
+ "firstmodified": OpEvent;
15
+ }
16
+
17
+ // 创建一个类型来检测意外的 override
18
+ // AI太好用了你知道吗
19
+ type CheckFinalOverrides<T> = {
20
+ [K in keyof T]: K extends keyof OpEventMap ?
21
+ T[K] extends OpEventMap[K] ? T[K] : never :
22
+ T[K]
23
+ };
24
+
25
+ interface OpEventMap extends CheckFinalOverrides<DirectlyInstaciableEventMap> {
26
+ "error": OperationErrorEvent;
27
+ "maxcombochanged": MaxComboChangeEvent;
28
+ "undo": OperationEvent;
29
+ "redo": OperationEvent;
30
+ "do": OperationEvent;
31
+ "needsupdate": OperationEvent;
32
+ "needsreflow": NeedsReflowEvent;
33
+ }
34
+
35
+
36
+ class OpEvent extends Event {
37
+ protected constructor(type: OpEventType) {
38
+ super(type);
39
+ }
40
+ /**
41
+ * 如果这个类型没有对应子类应该用这个
42
+ */
43
+ static create(type: keyof DirectlyInstaciableEventMap) {
44
+ return new OpEvent(type);
45
+ }
46
+ }
47
+
48
+ export class NeedsReflowEvent extends OpEvent {
49
+ constructor(public condition: number) {
50
+ super("needsreflow");
51
+ }
52
+ }
53
+
54
+ export class OperationEvent extends OpEvent {
55
+ constructor(t: "do" | "undo" | "redo" | "error" | "needsupdate", public operation: Operation) {
56
+ super(t);
57
+ }
58
+ }
59
+
60
+ export class OperationErrorEvent extends OperationEvent {
61
+ constructor(operation: Operation, public error: Error) {
62
+ super("error", operation);
63
+ }
64
+ }
65
+
66
+ export class MaxComboChangeEvent extends OpEvent {
67
+ constructor(public comboDelta: number) {
68
+ super("maxcombochanged");
69
+ }
70
+ }
71
+
72
+
73
+
74
+ export class OperationList extends EventTarget {
75
+ operations: Operation[];
76
+ undoneOperations: Operation[];
77
+ constructor(public readonly chart: Chart) {
78
+ super()
79
+ this.operations = [];
80
+ this.undoneOperations = [];
81
+ }
82
+ undo() {
83
+ const op = this.operations.pop()
84
+ if (op) {
85
+ if (!this.chart.modified){
86
+ this.chart.modified = true;
87
+ this.dispatchEvent(OpEvent.create("firstmodified"))
88
+ }
89
+
90
+ try {
91
+ op.undo(this.chart);
92
+ } catch (e) {
93
+ this.dispatchEvent(new OperationErrorEvent(op, e as Error))
94
+ return
95
+ }
96
+ this.undoneOperations.push(op)
97
+ this.dispatchEvent(new OperationEvent("undo", op))
98
+ this.processFlags(op);
99
+ } else {
100
+ this.dispatchEvent(OpEvent.create("noundo"))
101
+ }
102
+ }
103
+ redo() {
104
+ const op = this.undoneOperations.pop()
105
+ if (op) {
106
+ if (!this.chart.modified){
107
+ this.chart.modified = true;
108
+ this.dispatchEvent(OpEvent.create("firstmodified"))
109
+ }
110
+
111
+ try {
112
+ op.do(this.chart);
113
+ } catch (e) {
114
+ this.dispatchEvent(new OperationErrorEvent(op, e as Error))
115
+ return
116
+ }
117
+ this.operations.push(op)
118
+ this.dispatchEvent(new OperationEvent("redo", op))
119
+ this.processFlags(op);
120
+ } else {
121
+ this.dispatchEvent(OpEvent.create("noredo"))
122
+ }
123
+ }
124
+ do(operation: Operation) {
125
+ if (operation.ineffective) {
126
+ return
127
+ }
128
+ if (!this.chart.modified){
129
+ this.chart.modified = true;
130
+ this.dispatchEvent(OpEvent.create("firstmodified"))
131
+ }
132
+ // 如果上一个操作是同一个构造器的,那么试图修改上一个操作而不是立即推入新的操作
133
+ if (this.operations.length !== 0) {
134
+
135
+ const lastOp = this.operations[this.operations.length - 1]
136
+ if (operation.constructor === lastOp.constructor) {
137
+ // 返回值指示是否重写成功
138
+ if (lastOp.rewrite(operation, this.chart)) {
139
+ this.processFlags(operation)
140
+ return;
141
+ }
142
+ }
143
+ }
144
+ try {
145
+ operation.do(this.chart);
146
+ } catch (e) {
147
+ this.dispatchEvent(new OperationErrorEvent(operation, e as Error))
148
+ return
149
+ }
150
+ this.dispatchEvent(new OperationEvent("do", operation));
151
+ this.processFlags(operation);
152
+ this.operations.push(operation);
153
+ }
154
+ processFlags(operation: Operation) {
155
+
156
+ if (operation.updatesEditor) {
157
+ this.dispatchEvent(new OperationEvent("needsupdate", operation));
158
+ }
159
+ if (operation.comboDelta) {
160
+ this.dispatchEvent(new MaxComboChangeEvent(operation.comboDelta));
161
+ }
162
+ if (operation.reflows) {
163
+ this.dispatchEvent(new NeedsReflowEvent(operation.reflows))
164
+ }
165
+ }
166
+ clear() {
167
+ this.operations = [];
168
+ }
169
+ addEventListener<T extends OpEventType>(type: T, listener: (event: OpEventMap[T]) => void, options?: boolean | AddEventListenerOptions): void {
170
+ super.addEventListener(type, listener, options);
171
+ }
172
+ }
173
+
174
+
175
+ export abstract class Operation {
176
+ ineffective: boolean;
177
+ updatesEditor: boolean;
178
+ // 用于判定线编辑区的重排,若操作完成时的布局为这个值就会重排
179
+ reflows: number;
180
+ /**
181
+ * 此操作对谱面总物量产生了多少影响,正增负减。
182
+ *
183
+ * 如果操作自身无法评估,应返回NaN,导致全谱重新数清物量
184
+ */
185
+ comboDelta: number;
186
+ constructor() {
187
+
188
+ }
189
+ abstract do(chart: Chart): void
190
+ abstract undo(chart: Chart): void
191
+ rewrite(op: this, chart: Chart): boolean {return false;}
192
+ toString(): string {
193
+ return this.constructor.name;
194
+ }
195
+ static lazy<C extends new (...args: any[]) => any = typeof this>(this: C, ...args: ConstructorParameters<C>) {
196
+ return new LazyOperation<C>(this, ...args)
197
+ }
198
+ }
199
+
200
+
201
+
202
+
203
+ /**
204
+ * 懒操作,实例化的时候不记录任何数据,do的时候才执行真正实例化
205
+ * 防止连续的操作中状态改变导致的错误
206
+ */
207
+ export class LazyOperation<C extends new (...args: any[]) => any> extends Operation {
208
+ public operationClass: C;
209
+ public args: ConstructorParameters<C>;
210
+ public operation: InstanceType<C> | null = null;
211
+ constructor(
212
+ operationClass: C,
213
+ ...args: ConstructorParameters<C>
214
+ ) {
215
+ super();
216
+ this.operationClass = operationClass;
217
+ this.args = args;
218
+ }
219
+ do(chart: Chart) {
220
+ this.operation = new this.operationClass(...this.args);
221
+ this.operation.do(chart);
222
+ }
223
+ undo(chart: Chart) {
224
+ this.operation.undo(chart);
225
+ }
226
+ }
227
+
228
+
229
+ /**
230
+ * C语言借来的概念
231
+ *
232
+ * 一个不确定类型的子操作
233
+ *
234
+ * 注意这个操作不懒,会在构造时就实例化子操作
235
+ */
236
+ export class UnionOperation<T extends Operation> extends Operation {
237
+ operation: T;
238
+ constructor(matcher: () => T) {
239
+ super();
240
+ this.operation = matcher();
241
+ if (!this.operation) {
242
+ this.ineffective = true;
243
+ }
244
+ }
245
+ // 这样子写不够严密,如果要继承这个类,并且子操作需要谱面,就要重写这个方法的签名
246
+ do(chart?: Chart) {
247
+ this.operation?.do(chart);
248
+ }
249
+ undo(chart?: Chart) {
250
+ this.operation?.undo(chart);
251
+ }
252
+ }
253
+
254
+
255
+ export class ComplexOperation<T extends Operation[]> extends Operation {
256
+ subOperations: T;
257
+ length: number;
258
+ constructor(...sub: T) {
259
+ super()
260
+ this.subOperations = sub
261
+ this.length = sub.length
262
+ this.reflows = sub.reduce((prev, op) => prev | op.reflows, 0);
263
+ this.updatesEditor = sub.some((op) => op.updatesEditor);
264
+ this.comboDelta = sub.reduce((prev, op) => prev + op.comboDelta, 0);
265
+ }
266
+ // 这样子写不够严密,如果要继承这个类,并且子操作需要谱面,就要重写这个方法的签名
267
+ do(chart?: Chart) {
268
+ const length = this.length
269
+ for (let i = 0; i < length; i++) {
270
+ const op = this.subOperations[i]
271
+ if (op.ineffective) {
272
+ continue;
273
+ }
274
+ op.do(chart)
275
+ }
276
+ }
277
+ undo(chart?: Chart) {
278
+ const length = this.length
279
+ for (let i = length - 1; i >= 0; i--) {
280
+ const op = this.subOperations[i]
281
+ if (op.ineffective) { continue; }
282
+ op.undo(chart)
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,21 @@
1
+ import { Chart } from "../chart";
2
+ import { Operation } from "./basic";
3
+
4
+ export type ChartPropName = "name" | "level" | "composer" | "illustrator" | "charter" | "offset"
5
+
6
+ export class ChartPropChangeOperation<T extends ChartPropName> extends Operation {
7
+ originalValue: Chart[T];
8
+ constructor(public chart: Chart, public field: T, public value: Chart[T]) {
9
+ super();
10
+ this.originalValue = chart[field];
11
+ if (field === "level" || field === "name") {
12
+ this.updatesEditor = true;
13
+ }
14
+ }
15
+ do() {
16
+ this.chart[this.field] = this.value;
17
+ }
18
+ undo() {
19
+ this.chart[this.field] = this.originalValue;
20
+ }
21
+ }