Skip to content

Commit

Permalink
Make serialized layer conversion work for version 1/0 and v2 layers
Browse files Browse the repository at this point in the history
  • Loading branch information
lewish committed Aug 6, 2024
1 parent 8b6cd54 commit c0d96d4
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 91 deletions.
33 changes: 33 additions & 0 deletions client/layer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Layer } from "#asciiflow/client/layer";
import { layerToText } from "#asciiflow/client/text_utils";
import { Vector } from "#asciiflow/client/vector";
import { expect } from "chai";
import { describe, it } from "mocha";

describe("layer", () => {

it("converts v1 to v2", () => {
const v1Encoded =
'{"x":987,"y":286,"text":" ┼ \\n┼┼┼┼┼┼┼┼┼┼┼┼\\n┼ ┼ Hi ┼\\n┼ ┼ ┼\\n┼ ┼┼┼┼┼┼►┼\\n┼ ┼\\n┼┼┼┼┼┼┼┼┼┼┼┼"}';
const decoded = Layer.deserialize(v1Encoded);
const asText = layerToText(decoded);
expect(asText).equals(` │
┌───┼──────┐
│ │ Hi │
│ │ │
│ └─────►│
│ │
└──────────┘`);
});

it("serialize and deserialize v2", () => {
const layer = new Layer();
// This should stay as is and not be processed. by the legacy render layer.
layer.set(new Vector(5, 10), "++");
const encoded = Layer.serialize(layer);
expect(JSON.parse(encoded).version).equals(2);
const decoded = Layer.deserialize(encoded);
const asText = layerToText(decoded);
expect(asText).equals(`++`);
});
});
4 changes: 2 additions & 2 deletions client/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export class Layer implements ILayerView {

public static deserialize(value: string) {
const object = JSON.parse(value) as ILayerJSON;
// Version 1 is the original format.
if (!!object.version) {
// The original version of the serialized layer did not have a version number.
if (!object.version) {
const fixedLayer = new Layer();
const legacyRenderedLayer = new LegacyRenderLayer(
textToLayer(object.text, new Vector(object.x, object.y))
Expand Down
62 changes: 22 additions & 40 deletions client/store/canvas.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { Box } from "#asciiflow/client/common";
import * as constants from "#asciiflow/client/constants";
import { Layer, LayerView } from "#asciiflow/client/layer";
import { DrawingId } from "#asciiflow/client/store";
import { DrawingId, storageKey } from "#asciiflow/client/store";
import { DrawingStringifier } from "#asciiflow/client/store/drawing_stringifier";
import { Persistent } from "#asciiflow/client/store/persistent";
import { ArrayStringifier } from "#asciiflow/client/store/stringifiers";
import { ArrayStringifier } from "#asciiflow/common/stringifiers";
import { IVector, Vector } from "#asciiflow/client/vector";
import {
WatchableAdapter,
watchableAdapter,
watchableValue,
} from "#asciiflow/common/watchable";
import { WatchableAdapter, watchableValue } from "#asciiflow/common/watchable";

/**
* Holds the entire state of the diagram as a 2D array of cells
Expand All @@ -24,42 +20,28 @@ export class CanvasStore {
private _offset: WatchableAdapter<IVector>;

constructor(public readonly drawingId: DrawingId) {
this.persistentCommitted = watchableAdapter(
Persistent.custom(
this.persistentKey("committed-layer"),
this.drawingId.shareSpec
? new DrawingStringifier().deserialize(this.drawingId.shareSpec).layer
: new Layer(),
Layer
)
this.persistentCommitted = Persistent.custom(
storageKey(drawingId, "committed-layer"),
this.drawingId.shareSpec
? new DrawingStringifier().deserialize(this.drawingId.shareSpec).layer
: new Layer(),
Layer
);
this.undoLayers = watchableAdapter(
Persistent.custom(
this.persistentKey("undo-layers"),
[],
new ArrayStringifier(Layer)
)
this.undoLayers = Persistent.custom(
storageKey(drawingId, "undo-layers"),
[],
new ArrayStringifier(Layer)
);
this.redoLayers = watchableAdapter(
Persistent.custom(
this.persistentKey("redo-layers"),
[],
new ArrayStringifier(Layer)
)
this.redoLayers = Persistent.custom(
storageKey(drawingId, "redo-layers"),
[],
new ArrayStringifier(Layer)
);
this._zoom = watchableAdapter(
Persistent.json(this.persistentKey("zoom"), 1)
);
this._offset = watchableAdapter(
Persistent.json<IVector>(this.persistentKey("offset"), {
x: (constants.MAX_GRID_WIDTH * constants.CHAR_PIXELS_H) / 2,
y: (constants.MAX_GRID_HEIGHT * constants.CHAR_PIXELS_V) / 2,
})
);
}

public persistentKey(...values: string[]) {
return Persistent.key("drawing", this.drawingId.persistentKey, ...values);
this._zoom = Persistent.json(storageKey(drawingId, "zoom"), 1);
this._offset = Persistent.json<IVector>(storageKey(drawingId, "offset"), {
x: (constants.MAX_GRID_WIDTH * constants.CHAR_PIXELS_H) / 2,
y: (constants.MAX_GRID_HEIGHT * constants.CHAR_PIXELS_V) / 2,
});
}

public get zoom() {
Expand Down
15 changes: 15 additions & 0 deletions client/store/drawing_stringifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DrawingStringifier,
IDrawing,
} from "#asciiflow/client/store/drawing_stringifier";
import { layerToText } from "#asciiflow/client/text_utils";
import { Vector } from "#asciiflow/client/vector";
import { expect } from "chai";
import { describe, it } from "mocha";
Expand All @@ -21,4 +22,18 @@ describe("drawing_stringifier", () => {
expect(decoded.name).equals(drawing.name);
expect(decoded.layer.get(new Vector(5, 10))).equals("X");
});

it("converts v1 to v2", () => {
const v1Encoded =
'{"x":987,"y":286,"text":" ┼ \\n┼┼┼┼┼┼┼┼┼┼┼┼\\n┼ ┼ Hi ┼\\n┼ ┼ ┼\\n┼ ┼┼┼┼┼┼►┼\\n┼ ┼\\n┼┼┼┼┼┼┼┼┼┼┼┼"}';
const decoded = Layer.deserialize(v1Encoded);
const asText = layerToText(decoded);
expect(asText).equals(` │
┌───┼──────┐
│ │ Hi │
│ │ │
│ └─────►│
│ │
└──────────┘`);
});
});
2 changes: 1 addition & 1 deletion client/store/drawing_stringifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Layer } from "#asciiflow/client/layer";
import {
IStringifier,
JSONStringifier,
} from "#asciiflow/client/store/stringifiers";
} from "#asciiflow/common/stringifiers";
import { Base64 } from "js-base64";
import pako from "pako";

Expand Down
72 changes: 29 additions & 43 deletions client/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import * as constants from "#asciiflow/client/constants";
import { DrawBox } from "#asciiflow/client/draw/box";
import { DrawFreeform } from "#asciiflow/client/draw/freeform";
import {
AbstractDrawFunction,
IDrawFunction,
IDrawFunction
} from "#asciiflow/client/draw/function";
import { DrawLine } from "#asciiflow/client/draw/line";
import { DrawNull } from "#asciiflow/client/draw/null";
import { DrawSelect } from "#asciiflow/client/draw/select";
import { DrawText } from "#asciiflow/client/draw/text";
import { IExportConfig } from "#asciiflow/client/export";
import { CanvasStore } from "#asciiflow/client/store/canvas";
import { Persistent } from "#asciiflow/client/store/persistent";
import {
ArrayStringifier,
IStringifier,
JSONStringifier,
} from "#asciiflow/client/store/stringifiers";
import { Persistent } from "#asciiflow/client/store/persistent";
import * as uuid from "uuid";
import { watchableAdapter, watchableValue } from "#asciiflow/common/watchable";
} from "#asciiflow/common/stringifiers";
import { watchableValue } from "#asciiflow/common/watchable";

export enum ToolMode {
BOX = 1,
Expand Down Expand Up @@ -122,29 +119,20 @@ export class Store {
return this.selectedToolMode.get();
}

public readonly unicode = watchableAdapter(Persistent.json("unicode", true));
public readonly controlsOpen = watchableAdapter(
Persistent.json("controlsOpen", true)
);
public readonly fileControlsOpen = watchableAdapter(
Persistent.json("fileControlsOpen", true)
);
public readonly editControlsOpen = watchableAdapter(
Persistent.json("editControlsOpen", true)
);
public readonly helpControlsOpen = watchableAdapter(
Persistent.json("editControlsOpen", true)
);
public readonly exportConfig = watchableAdapter(
Persistent.json("exportConfig", {} as IExportConfig)
public readonly unicode = Persistent.json("unicode", true);
public readonly controlsOpen = Persistent.json("controlsOpen", true);
public readonly fileControlsOpen = Persistent.json("fileControlsOpen", true);
public readonly editControlsOpen = Persistent.json("editControlsOpen", true);
public readonly helpControlsOpen = Persistent.json("editControlsOpen", true);
public readonly exportConfig = Persistent.json(
"exportConfig",
{} as IExportConfig
);

public readonly localDrawingIds = watchableAdapter(
Persistent.custom(
"localDrawingIds",
[],
new ArrayStringifier(DrawingId.STRINGIFIER)
)
public readonly localDrawingIds = Persistent.custom(
"localDrawingIds",
[],
new ArrayStringifier(DrawingId.STRINGIFIER)
);

public readonly panning = watchableValue(false);
Expand All @@ -153,13 +141,13 @@ export class Store {

public readonly currentCursor = watchableValue("default");

public readonly darkMode = watchableAdapter(
public readonly darkMode =
Persistent.json(
"darkMode",
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
)
);
;

get currentTool(): IDrawFunction {
return this.toolMode() === ToolMode.BOX
Expand Down Expand Up @@ -243,35 +231,30 @@ export class Store {
);
// Also delete other local storage.
Object.keys(localStorage)
.filter((key) => key.startsWith(this.canvas(drawingId).persistentKey()))
.filter((key) => key.startsWith(storagePrefix(drawingId)))
.forEach((key) => localStorage.removeItem(key));
this.canvases.delete(drawingId.toString());
}

public renameDrawing(originalLocalId: string, newLocalId: string) {
const originalId = DrawingId.local(originalLocalId);
const newId = DrawingId.local(newLocalId);
Object.keys(localStorage)
.filter((key) =>
key.startsWith(this.canvas(originalId).persistentKey() + "/")
)
.filter((key) => key.startsWith(storagePrefix(originalId)))
.forEach((key) => {
localStorage.setItem(
key.replace(
this.canvas(originalId).persistentKey(),
this.canvas(newId).persistentKey()
),
key.replace(storagePrefix(originalId), storagePrefix(newId)),
localStorage.getItem(key)
);
localStorage.removeItem(key);
});
this.canvases.delete(newId.toString());
this.canvases.delete(originalId.toString());
this.localDrawingIds.set([
...this.localDrawingIds
.get()
.filter((drawingId) => drawingId.toString() !== originalId.toString()),
newId,
]);
this.canvases.delete(originalId.toString());
window.location.hash = newId.href;
}

Expand All @@ -288,9 +271,12 @@ export class Store {
}
}

function generateId() {
const hex = uuid.v4().replace(/\-/g, "");
return hex.substring(0, 16);
export function storagePrefix(drawingId: DrawingId) {
return `drawing/${encodeURIComponent(drawingId.persistentKey)}/`;
}

export function storageKey(drawingId: DrawingId, key: string) {
return storagePrefix(drawingId) + key;
}

export const store = new Store();
8 changes: 4 additions & 4 deletions client/store/persistent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IStringifier, JSONStringifier } from "#asciiflow/client/store/stringifiers";

import { IStringifier, JSONStringifier } from "#asciiflow/common/stringifiers";
import { watchable } from "#asciiflow/common/watchable";

export class Persistent<T> {
public static json<T>(key: string, defaultValue: T) {
return new Persistent<T>(new JSONStringifier(), key, defaultValue);
return watchable(new Persistent<T>(new JSONStringifier(), key, defaultValue));
}

public static key(...parts: string[]) {
Expand All @@ -15,7 +15,7 @@ export class Persistent<T> {
defaultValue: T,
stringifier: IStringifier<T>
) {
return new Persistent<T>(stringifier, key, defaultValue);
return watchable(new Persistent<T>(stringifier, key, defaultValue));
}

private value: T;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion common/watchable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class Reference<T> {
constructor(public value: T) {}
}

export function watchableAdapter<T>(obj: {
export function watchable<T>(obj: {
get: () => T;
set: (value: T) => any;
}): WatchableAdapter<T> {
Expand Down

0 comments on commit c0d96d4

Please sign in to comment.