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
15 changes: 15 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,21 @@ button:disabled {
.tour-layer-guided {
pointer-events: auto;
}
.chart-svg.chart-svg-move {
cursor: crosshair;
}
.chart-increase-target {
fill: none;
stroke: #44936b;
stroke-width: 3px;
}
.increase-move-hint {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 4px 16px 10px;
}
.yoke-tour-editor {
background: var(--panel);
inset: 0;
Expand Down
66 changes: 47 additions & 19 deletions components/chart-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { PointerEvent, useRef } from "react";
import type { ChartPoint } from "@/lib/yoke-move-increase";
import {
cellAspectRatio,
CellSelection,
Expand All @@ -26,6 +27,9 @@ type Props = {
onPaint: (row: number, column: number, flood: boolean) => void;
yarnOnly?: boolean;
onStrokeEnd?: () => void;
onCellPick?: (point: ChartPoint) => void;
onCellPoint?: (point: ChartPoint | null) => void;
moveTarget?: ChartPoint;
};

export function ChartGrid({
Expand All @@ -39,6 +43,9 @@ export function ChartGrid({
onPaint,
yarnOnly = false,
onStrokeEnd,
onCellPick,
onCellPoint,
moveTarget,
}: Props) {
const svgRef = useRef<SVGSVGElement>(null);
const drawing = useRef(false);
Expand Down Expand Up @@ -79,6 +86,10 @@ export function ChartGrid({
const cell = cellFromPointer(event);
if (!cell) return;
event.currentTarget.setPointerCapture(event.pointerId);
if (onCellPick) {
onCellPick(cell);
return;
}
if (tool === "select") {
selecting.current = true;
drawing.current = false;
Expand All @@ -99,6 +110,10 @@ export function ChartGrid({
}

function handlePointerMove(event: PointerEvent<SVGSVGElement>) {
if (onCellPick) {
if (event.pointerType !== "touch") onCellPoint?.(cellFromPointer(event));
return;
}
if (drawing.current || selecting.current) event.preventDefault();
if (selecting.current && selectionStart.current) {
const cell = cellFromPointer(event);
Expand Down Expand Up @@ -170,7 +185,7 @@ export function ChartGrid({
</div>
<svg
ref={svgRef}
className="chart-svg"
className={onCellPick ? "chart-svg chart-svg-move" : "chart-svg"}
viewBox={`0 0 ${document.width * aspect} ${document.height}`}
style={{
aspectRatio: `${document.width * aspect} / ${document.height}`,
Expand Down Expand Up @@ -308,26 +323,39 @@ export function ChartGrid({
pointerEvents="none"
/>
) : null}
{moveTarget ? (
<rect
className="chart-increase-target"
x={moveTarget.column * aspect}
y={moveTarget.row}
width={aspect}
height={1}
vectorEffect="non-scaling-stroke"
pointerEvents="none"
/>
) : null}
</svg>
</div>
<p className="chart-hint">
{structureAction === "knit"
? "Click and drag to remove stitch symbols, keeping yarn colours. With Fill, click a connected group of symbols to remove them."
: structureAction
? tool === "fill"
? `Click an area to fill it with ${stitchActionLabel(structureAction)}.`
: `Click and drag to mark cells as ${stitchActionLabel(structureAction)}${document.guides.symmetry !== "none" ? " with live symmetry" : ""}.`
: tool === "fill"
? "Click an area to fill its yarn colour. Stitch symbols stay in place."
: tool === "select"
? yarnOnly
? "Drag a rectangle to fill, copy, cut, paste or move yarn colours."
: "Drag a rectangle to fill, copy, cut, paste, move, or apply a stitch action."
: `Click and drag to ${tool === "eraser" ? "erase the yarn colour" : "paint"}${document.guides.symmetry !== "none" ? " with live symmetry" : ""}.`}
{document.stitchActions && !structureAction && !yarnOnly
? " Use Remove stitch to erase symbols without changing yarn colours."
: ""}
</p>
{!onCellPick ? (
<p className="chart-hint">
{structureAction === "knit"
? "Click and drag to remove stitch symbols, keeping yarn colours. With Fill, click a connected group of symbols to remove them."
: structureAction
? tool === "fill"
? `Click an area to fill it with ${stitchActionLabel(structureAction)}.`
: `Click and drag to mark cells as ${stitchActionLabel(structureAction)}${document.guides.symmetry !== "none" ? " with live symmetry" : ""}.`
: tool === "fill"
? "Click an area to fill its yarn colour. Stitch symbols stay in place."
: tool === "select"
? yarnOnly
? "Drag a rectangle to fill, copy, cut, paste or move yarn colours."
: "Drag a rectangle to fill, copy, cut, paste, move, or apply a stitch action."
: `Click and drag to ${tool === "eraser" ? "erase the yarn colour" : "paint"}${document.guides.symmetry !== "none" ? " with live symmetry" : ""}.`}
{document.stitchActions && !structureAction && !yarnOnly
? " Use Remove stitch to erase symbols without changing yarn colours."
: ""}
</p>
) : null}
</div>
</div>
);
Expand Down
41 changes: 17 additions & 24 deletions components/stitch-palette-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type StitchAction,
type StitchPaletteAction,
} from "@/lib/chart";
const yokeStitchOptions = new Set<StitchAction>(["purl", "m1l", "m1r"]);
type Props = {
document: ChartDocument;
structureAction: StitchAction | null;
Expand All @@ -34,6 +35,12 @@ export function StitchPalettePanel({
removeStitchFromPalette,
resetStitchColor,
}: Props) {
const availableStitches = stitchActionOptions.filter(
(option): option is typeof option & { value: StitchPaletteAction } =>
option.value !== "knit" &&
(!document.yoke || yokeStitchOptions.has(option.value)) &&
!stitchPalette.includes(option.value),
);
return (
<details
className="settings-group stitch-settings"
Expand All @@ -59,30 +66,16 @@ export function StitchPalettePanel({
<div className="stitch-picker" aria-label="Stitch tools">
<p>Add a stitch type to this chart’s palette.</p>
<div>
{stitchActionOptions
.filter(
(
option,
): option is typeof option & {
value: StitchPaletteAction;
} =>
option.value !== "knit" &&
!stitchPalette.includes(
option.value as StitchPaletteAction,
),
)
.map((option) => (
<button
key={option.value}
title={option.help}
onClick={() => addStitchToPalette(option.value)}
>
{option.shortLabel}
</button>
))}
{stitchPalette.length ===
stitchActionOptions.filter((option) => option.value !== "knit")
.length ? (
{availableStitches.map((option) => (
<button
key={option.value}
title={option.help}
onClick={() => addStitchToPalette(option.value)}
>
{option.shortLabel}
</button>
))}
{!availableStitches.length ? (
<span>All stitch types have been added.</span>
) : null}
</div>
Expand Down
121 changes: 121 additions & 0 deletions components/use-yoke-increase-move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"use client";

import { useEffect, useState } from "react";
import { yokeChart } from "@/lib/yoke-chart";
import {
prepareYokeIncreaseMove,
type ChartPoint,
} from "@/lib/yoke-move-increase";
import type { YokeProject } from "@/lib/yoke-project";
import type { YokeRepeatScope } from "@/lib/yoke-repeat-scope";

const same = (a: ChartPoint | null, b: ChartPoint | null) =>
a === b || (!!a && !!b && a.row === b.row && a.column === b.column);

export function useYokeIncreaseMove(
project: YokeProject,
repeat: number,
scope: YokeRepeatScope,
enabled: boolean,
onCommit: (next: YokeProject) => void,
) {
const context = `${repeat}/${scope}`;
const [picked, setPicked] = useState<{
project: YokeProject;
context: string;
point: ChartPoint;
prepare: ReturnType<typeof prepareYokeIncreaseMove>;
} | null>(null);
const [target, setTarget] = useState<ChartPoint | null>(null);
const [preview, setPreview] = useState<{
picked: NonNullable<typeof picked>;
target: ChartPoint;
project?: YokeProject;
chart?: ReturnType<typeof yokeChart>["document"];
error?: string;
} | null>(null);
const [error, setError] = useState("");
const active =
enabled && picked?.project === project && picked.context === context
? picked
: null;
const result =
active && preview?.picked === active && same(preview.target, target)
? preview
: null;

useEffect(() => {
if (!active || !target || same(active.point, target)) return;
const timer = window.setTimeout(() => {
try {
const next = active.prepare(target);
setPreview({
picked: active,
target,
project: next,
chart: yokeChart(next, repeat).document,
});
} catch (cause) {
setPreview({
picked: active,
target,
error:
cause instanceof Error
? cause.message
: "Could not move this increase.",
});
}
}, 100);
return () => window.clearTimeout(timer);
}, [active, target, repeat]);

function cancel() {
setPicked(null);
setTarget(null);
setPreview(null);
setError("");
}
function apply() {
if (!result?.project) return;
onCommit(result.project);
cancel();
}
function pointAt(point: ChartPoint | null) {
if (active)
setTarget((current) => (same(current, point) ? current : point));
}
function click(point: ChartPoint) {
if (!enabled) return;
if (active) {
if (same(active.point, point)) cancel();
else if (result?.project && same(result.target, point)) apply();
else if (result?.error && same(result.target, point))
setError(result.error);
else pointAt(point);
return;
}
cancel();
try {
setPicked({
project,
context,
point,
prepare: prepareYokeIncreaseMove(project, repeat, scope, point),
});
} catch (cause) {
setError(
cause instanceof Error ? cause.message : "Choose an M1L or M1R.",
);
}
}
return {
click,
pointAt,
cancel,
apply,
source: active?.point,
target: result?.project ? result.target : undefined,
chart: result?.chart,
error: enabled ? error : "",
};
}
2 changes: 1 addition & 1 deletion components/use-yoke-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { mirrorYokeRepeat } from "@/lib/yoke-repeat-mirror";
import type { YokeRepeatScope } from "@/lib/yoke-repeat-scope";
import { yokeRepeatNumber } from "@/lib/yoke-repeat-labels";

export type YokeTool = Tool | "row-fill";
export type YokeTool = Tool | "row-fill" | "move-increase";
export function useYokeTools(
project: YokeProject,
view: YokeView,
Expand Down
18 changes: 18 additions & 0 deletions components/yoke-drawing-tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function YokeDrawingTools({
onErase,
onFillSelection,
selectionLabel,
moveIncreaseUnavailable,
}: {
tools: YokeTools;
color: number;
Expand All @@ -28,6 +29,7 @@ export function YokeDrawingTools({
onErase?: () => void;
onFillSelection?: () => void;
selectionLabel?: string;
moveIncreaseUnavailable?: string;
}) {
const { selection } = tools;
return (
Expand Down Expand Up @@ -55,6 +57,22 @@ export function YokeDrawingTools({
{label}
</button>
))}
<button
aria-pressed={tools.tool === "move-increase"}
className={tools.tool === "move-increase" ? "active" : ""}
disabled={Boolean(moveIncreaseUnavailable)}
title={
moveIncreaseUnavailable ??
"Pick an M1L or M1R, then choose its new cell"
}
onClick={() => {
tools.setSelection(null);
tools.setTool("move-increase");
onErase?.();
}}
>
Move increase
</button>
<button onClick={tools.clearAll}>Clear</button>
</div>
<span className="toolbar-divider" aria-hidden="true" />
Expand Down
Loading
Loading