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
13 changes: 13 additions & 0 deletions app/yoke-designer/yoke.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@
font-weight: 600;
margin-bottom: 8px;
}
.yoke-plan-comparison .yoke-canvas-navigable {
height: min(48vh, 440px);
}
.yoke-plan-zoom {
justify-content: flex-start;
flex-wrap: wrap;
margin-bottom: 8px;
margin-left: 0;
}
.yoke-plan-zoom output {
min-width: 42px;
text-align: center;
}
.yoke-transfer-options {
padding: 16px;
border: 1px solid var(--line);
Expand Down
2 changes: 1 addition & 1 deletion components/use-yoke-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function useYokeNavigation(
}
const fit = useEffectEvent(() => {
const parent = canvas.current?.parentElement;
if (!enabled || !parent || view !== "repeat") return;
if (!enabled || !parent || view === "chart") return;
anchor.current = null;
options.onZoom?.(
fitYokeZoom(
Expand Down
90 changes: 69 additions & 21 deletions components/yoke-plan-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type YokeMeasurements,
type IncreaseEvent,
} from "@/lib/yoke-plan";
import { createYokeProject } from "@/lib/yoke-project";
import { createYokeProject, type YokeProject } from "@/lib/yoke-project";
import {
reshapeYoke,
yokeDocument,
Expand All @@ -23,6 +23,64 @@ import type { YokeField } from "@/lib/yoke-measurement-fields";
import type { YokeMeasurementMode } from "@/lib/yoke-measurement-mode";
import type { YokeShortRows } from "@/lib/yoke-short-rows";

const noEdit = () => {};

function PlanCanvas({
project,
view,
label,
}: {
project: YokeProject;
view: "repeat" | "whole";
label: string;
}) {
const [zoom, setZoom] = useState(25);
const [fitRequest, setFitRequest] = useState(1);
return (
<figure>
<figcaption>{label}</figcaption>
<div
className="zoom-controls yoke-plan-zoom"
role="group"
aria-label={`${label} zoom`}
>
<button
type="button"
aria-label="Zoom out"
disabled={zoom <= 1}
onClick={() => setZoom(Math.max(1, Math.floor(zoom / 1.25)))}
>
−
</button>
<output aria-label="Preview zoom">{zoom}%</output>
<button
type="button"
aria-label="Zoom in"
disabled={zoom >= 400}
onClick={() => setZoom(Math.min(400, Math.ceil(zoom * 1.25)))}
>
+
</button>
<button
type="button"
onClick={() => setFitRequest((request) => request + 1)}
>
Fit preview
</button>
</div>
<YokeCanvas
project={project}
view={view}
zoom={zoom}
fitRequest={fitRequest}
onZoomChange={setZoom}
onPaint={noEdit}
onStrokeStart={noEdit}
/>
</figure>
);
}

export function YokePlanDialog({
document,
initialMeasurements,
Expand Down Expand Up @@ -194,27 +252,17 @@ export function YokePlanDialog({
) : null}
<div className="yoke-plan-comparison">
{base?.yoke ? (
<figure>
<figcaption>Original · unchanged</figcaption>
<YokeCanvas
project={base.yoke}
view="repeat"
zoom={100}
onPaint={() => {}}
onStrokeStart={() => {}}
/>
</figure>
) : null}
<figure>
<figcaption>{base ? "Proposed" : "Your yoke"}</figcaption>
<YokeCanvas
project={proposal}
view={base ? "repeat" : "whole"}
zoom={100}
onPaint={() => {}}
onStrokeStart={() => {}}
<PlanCanvas
project={base.yoke}
view="repeat"
label="Original · unchanged"
/>
</figure>
) : null}
<PlanCanvas
project={proposal}
view={base ? "repeat" : "whole"}
label={base ? "Proposed" : "Your yoke"}
/>
</div>
<details>
<summary>Neckline short rows</summary>
Expand Down
35 changes: 34 additions & 1 deletion e2e/yoke-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const active = async (page: Page) => {
const state = await saved(page);
return state.tabs.find((tab) => tab.id === state.activeTabId)!;
};
async function create(page: Page) {
async function previewPlan(page: Page) {
await page.addInitScript(() =>
localStorage.setItem("knitplot-tutorial-v1", "seen"),
);
Expand All @@ -42,6 +42,10 @@ async function create(page: Page) {
await dialog
.getByRole("button", { name: "Preview yoke", exact: true })
.click();
return dialog;
}
async function create(page: Page) {
const dialog = await previewPlan(page);
await dialog
.getByRole("button", { name: "Create yoke", exact: true })
.click();
Expand All @@ -53,6 +57,35 @@ async function create(page: Page) {
.toBe(true);
}

test("the planning preview fits on a small screen and can zoom while editing short rows", async ({
page,
}) => {
await page.setViewportSize({ width: 700, height: 600 });
const dialog = await previewPlan(page);
await dialog
.locator("summary")
.filter({ hasText: "Neckline short rows" })
.click();
const zoom = dialog.getByRole("group", { name: "Your yoke zoom" });
const canvas = dialog.getByRole("img", { name: "Whole yoke drawing canvas" });
const fits = () =>
canvas.evaluate((element) => {
const box = element.getBoundingClientRect();
const parent = element.parentElement!;
return (
box.width <= parent.clientWidth && box.height <= parent.clientHeight
);
});
const width = async () => (await canvas.boundingBox())!.width;
await expect.poll(fits).toBe(true);
const fitted = await width();
await zoom.getByRole("button", { name: "Zoom out" }).click();
expect(await width()).toBeLessThan(fitted);
await zoom.getByRole("button", { name: "Zoom in" }).click();
await zoom.getByRole("button", { name: "Fit preview" }).click();
await expect.poll(width).toBeCloseTo(fitted, 1);
});

for (const custom of [false, true]) {
test(`complete yoke workflow: ${custom ? "non-repeating custom shaping" : "identical repeats and short rows"}`, async ({
page,
Expand Down
Loading