Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Open the hosted chart maker at [knitplot.art](https://knitplot.art). Every non-A

- Draw, erase, flood-fill, and fill rectangular selections
- Choose up to eight custom colours
- Work with charts up to 200 stitches by 150 rows
- Work with regular charts up to 200 stitches by 300 rows
- Set stitch gauge, row gauge, and centimetres or inches
- Zoom the editable chart and use optional live-symmetry guides
- Flip, rotate, undo, and redo chart changes
Expand Down
8 changes: 4 additions & 4 deletions e2e/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,17 @@ test("chart dimensions allow typing intermediate values and reset on tab changes
});

for (const structural of [false, true]) {
test(`stitch bitmap exports exact yarn pixels${structural ? " with explicit no-stitch handling" : " at 114 × 150"}`, async ({
test(`stitch bitmap exports exact yarn pixels${structural ? " with explicit no-stitch handling" : " at 114 × 300"}`, async ({
page,
}) => {
const doc = cloneDocument(defaultDocument);
doc.width = 114;
doc.height = 150;
doc.height = structural ? 150 : 300;
doc.palette = [
{ id: "pink", hex: "#ff809a" },
{ id: "green", hex: "#123456" },
];
doc.cells = Array.from({ length: 150 }, (_, row) =>
doc.cells = Array.from({ length: doc.height }, (_, row) =>
Array.from({ length: 114 }, (_, column) =>
(row + column) % 3 === 0 ? "pink" : "green",
),
Expand Down Expand Up @@ -243,7 +243,7 @@ for (const structural of [false, true]) {
);
expect(result).toEqual({
width: 114,
height: 150,
height: doc.height,
mismatches: 0,
colours: 2,
});
Expand Down
6 changes: 3 additions & 3 deletions lib/ai-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ test("AI generation uses the user's dimensions, including large charts", () => {
readAiRequest(generateRequestSchema, {
...settings,
width: 200,
height: 150,
}).width,
200,
height: 300,
}).height,
300,
);
});
test("both AI routes reject null and invalid colour ranges", () => {
Expand Down
84 changes: 83 additions & 1 deletion lib/chart-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,59 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { cloneDocument, defaultDocument } from "./chart";
import { readProject } from "./project-file";
import { readCloudChartPayload } from "./cloud-charts";
import { readCloudChartPayload, readCloudChartRequest } from "./cloud-charts";
import { readChartDocument } from "./chart-schema";
import { newChartTab, readWorkspace, workspaceSave } from "./workspace";

const document = () => cloneDocument(defaultDocument);
test("a tall imported chart with long palette IDs fits the cloud request budget", async () => {
const longId = "a".repeat(50);
const otherId = "yarn-0";
const grid = <T>(value: T) =>
Array.from({ length: 300 }, () => Array<T>(200).fill(value));
const chart = cloneDocument({
...defaultDocument,
width: 200,
height: 300,
palette: [
{ id: longId, hex: "#aabbcc" },
{ id: otherId, hex: "#ffffff" },
],
cells: grid(longId),
stitchActions: grid("purl"),
stitchCellColors: grid("#aabbcc"),
});
chart.cells[299][199] = otherId;
const request = (doc: typeof chart) =>
new Request("http://localhost/api/charts", {
method: "POST",
body: JSON.stringify({ document: doc }),
});
await assert.rejects(readCloudChartRequest(request(chart)), {
status: 413,
});
const opened = readProject({
format: "colorwork-chart",
version: 2,
document: chart,
});
const saved = readCloudChartPayload(
await readCloudChartRequest(request(opened)),
).document;
assert.deepEqual(
saved.palette.map(({ hex }) => hex),
["#aabbcc", "#ffffff"],
);
assert.deepEqual(
saved.cells,
chart.cells.map((row) =>
row.map((id) => saved.palette[id === longId ? 0 : 1].id),
),
);
assert.deepEqual(saved.stitchActions, chart.stitchActions);
assert.deepEqual(saved.stitchCellColors, chart.stitchCellColors);
});

test("project and cloud round trips preserve a chart", () => {
const chart = document();
assert.deepEqual(
Expand All @@ -14,6 +63,39 @@ test("project and cloud round trips preserve a chart", () => {
);
assert.deepEqual(readCloudChartPayload({ document: chart }).document, chart);
});
test("a 200 by 300 regular chart and last-row progress survive storage round trips", () => {
const chart = cloneDocument({
...defaultDocument,
width: 200,
height: 300,
cells: Array.from({ length: 300 }, () =>
Array<string>(200).fill("background"),
),
});
const tab = newChartTab(chart, "tall-chart");
tab.knitProgress.row = 300;
assert.deepEqual(
readProject({ format: "colorwork-chart", version: 2, document: chart }),
chart,
);
assert.deepEqual(readCloudChartPayload(tab).document, chart);
assert.equal(readCloudChartPayload(tab).knitProgress.row, 300);
const reopened = readWorkspace(
JSON.parse(JSON.stringify(workspaceSave([tab], tab.id))),
).workspace.tabs[0];
assert.deepEqual(reopened.document, chart);
assert.equal(reopened.knitProgress.row, 300);
assert.throws(() =>
readChartDocument(
cloneDocument({
...defaultDocument,
width: chart.width,
height: 301,
cells: [...chart.cells, chart.cells[0]],
}),
),
);
});
for (const [name, mutate] of Object.entries({
"unknown action": (c: Record<string, unknown>) => {
c.stitchActions = Array.from({ length: defaultDocument.height }, () =>
Expand Down
2 changes: 1 addition & 1 deletion lib/chart.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type Tool = "pencil" | "fill" | "eraser" | "select";

export const MAX_CHART_STITCHES = 200;
export const MAX_CHART_ROWS = 150;
export const MAX_CHART_ROWS = 300;

export type CellSelection = {
startRow: number;
Expand Down
21 changes: 20 additions & 1 deletion lib/project-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,24 @@ export function readProject(value: unknown) {
) {
throw new HttpError("This is not a KnitPlot file.");
}
return readChartDocument(value.document);
const document = readChartDocument(value.document);
if (
document.yoke ||
document.palette.every(({ id }) => /^[a-z0-9_-]{1,20}$/i.test(id))
)
return document;

// IDs are internal and repeated in every cell. Compact the whole palette
// together so large imported charts fit within the cloud request budget.
const ids = new Map(
document.palette.map(({ id }, index) => [id, `yarn-${index}`]),
);
return {
...document,
palette: document.palette.map((colour) => ({
...colour,
id: ids.get(colour.id)!,
})),
cells: document.cells.map((row) => row.map((id) => ids.get(id)!)),
};
}
13 changes: 13 additions & 0 deletions lib/yoke-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import {
suggestIncreasePositions,
} from "./yoke-plan";

test("the yoke planner still rejects more than 150 rounds", () => {
assert.throws(
() =>
calculateYokeTargets({
...defaultYokeMeasurements,
depth:
(151 * defaultYokeMeasurements.gaugeMeasure) /
defaultYokeMeasurements.gaugeRows,
}),
/150 full rounds/,
);
});

test("repeat rounding cannot erase the body portion at the sleeve split", () => {
assert.throws(
() =>
Expand Down
Loading