Skip to content

Commit

Permalink
Precision drawing (#299)
Browse files Browse the repository at this point in the history
* Big revamp of box/line drawing logic.

Rather than just drawing a single special character and trying to work it out, actually draw the correct values on lines, and move snapping to draw time, not render time.

* Revamp line moving semantics to match new drawing logic

* Further improvements to line moving

* Various cleanups

* Remove drawing stringifier dependency on the store
  • Loading branch information
lewish authored May 29, 2024
1 parent 3767722 commit 8ac0365
Show file tree
Hide file tree
Showing 19 changed files with 4,702 additions and 4,179 deletions.
7,635 changes: 3,818 additions & 3,817 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

279 changes: 279 additions & 0 deletions client/characters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { UNICODE } from "#asciiflow/client/constants";
import { Direction } from "#asciiflow/client/direction";

export const BOX_DRAWING_VALUES = new Set([
"┌",
"┐",
"┘",
"└",
"─",
"│",
"┬",
"┴",
"┤",
"├",
"┼",
"◄",
"►",
"▲",
"▼",
]);

interface IBoxDrawingCharacterInfo {
connections: Set<Direction>;
connectables: Set<Direction>;
}

const BOX_DRAWING_INFO: { [key: string]: IBoxDrawingCharacterInfo } = {
"┌": {
connections: new Set([Direction.DOWN, Direction.RIGHT]),
connectables: new Set(Direction.ALL),
},
"┐": {
connections: new Set([Direction.DOWN, Direction.LEFT]),
connectables: new Set(Direction.ALL),
},
"┘": {
connections: new Set([Direction.UP, Direction.LEFT]),
connectables: new Set(Direction.ALL),
},
"└": {
connections: new Set([Direction.UP, Direction.RIGHT]),
connectables: new Set(Direction.ALL),
},
"─": {
connections: new Set([Direction.LEFT, Direction.RIGHT]),
connectables: new Set(Direction.ALL),
},
"│": {
connections: new Set([Direction.UP, Direction.DOWN]),
connectables: new Set(Direction.ALL),
},
"┬": {
connections: new Set([Direction.DOWN, Direction.LEFT, Direction.RIGHT]),
connectables: new Set(Direction.ALL),
},
"┴": {
connections: new Set([Direction.UP, Direction.LEFT, Direction.RIGHT]),
connectables: new Set(Direction.ALL),
},
"┤": {
connections: new Set([Direction.UP, Direction.DOWN, Direction.LEFT]),
connectables: new Set(Direction.ALL),
},
"├": {
connections: new Set([Direction.UP, Direction.DOWN, Direction.RIGHT]),
connectables: new Set(Direction.ALL),
},
"┼": {
connections: new Set([
Direction.UP,
Direction.DOWN,
Direction.LEFT,
Direction.RIGHT,
]),
connectables: new Set(Direction.ALL),
},
"◄": {
connections: new Set([Direction.RIGHT]),
connectables: new Set([Direction.RIGHT]),
},
"►": {
connections: new Set([Direction.LEFT]),
connectables: new Set([Direction.LEFT]),
},
"▲": {
connections: new Set([Direction.DOWN]),
connectables: new Set([Direction.DOWN]),
},
"▼": {
connections: new Set([Direction.UP]),
connectables: new Set([Direction.UP]),
},
};

export function connectsDown(value: string): boolean {
return BOX_DRAWING_INFO[value]?.connections.has(Direction.DOWN);
}

export function connectsUp(value: string): boolean {
return BOX_DRAWING_INFO[value]?.connections.has(Direction.UP);
}

export function connectsLeft(value: string): boolean {
return BOX_DRAWING_INFO[value]?.connections.has(Direction.LEFT);
}

export function connectsRight(value: string): boolean {
return BOX_DRAWING_INFO[value]?.connections.has(Direction.RIGHT);
}

export function isBoxDrawing(value: string): boolean {
return BOX_DRAWING_VALUES.has(value);
}

export function isArrow(value: string): boolean {
return (
value === UNICODE.arrowLeft ||
value === UNICODE.arrowRight ||
value === UNICODE.arrowUp ||
value === UNICODE.arrowDown
);
}

export function connects(value: string, direction: Direction): boolean {
return BOX_DRAWING_INFO[value]?.connections.has(direction);
}

export function connectable(value: string, direction: Direction): boolean {
return BOX_DRAWING_INFO[value]?.connectables.has(direction);
}

export function connections(value: string): Set<Direction> {
return BOX_DRAWING_INFO[value]?.connections ?? new Set();
}

export function connect(
value: string,
direction: Direction | Direction[]
): string {
if (Array.isArray(direction)) {
return direction.reduce(
(value, direction) => connect(value, direction),
value
);
}
if (connects(value, direction)) {
return value;
}
if (direction === Direction.UP) {
if (value === UNICODE.lineHorizontal) {
return UNICODE.junctionUp;
}
if (value === UNICODE.cornerTopLeft) {
return UNICODE.junctionRight;
}
if (value === UNICODE.cornerTopRight) {
return UNICODE.junctionLeft;
}
if (value === UNICODE.junctionDown) {
return UNICODE.junctionAll;
}
}
if (direction === Direction.DOWN) {
if (value === UNICODE.lineHorizontal) {
return UNICODE.junctionDown;
}
if (value === UNICODE.cornerBottomLeft) {
return UNICODE.junctionRight;
}
if (value === UNICODE.cornerBottomRight) {
return UNICODE.junctionLeft;
}
if (value === UNICODE.junctionUp) {
return UNICODE.junctionAll;
}
}
if (direction === Direction.LEFT) {
if (value === UNICODE.lineVertical) {
return UNICODE.junctionLeft;
}
if (value === UNICODE.cornerTopLeft) {
return UNICODE.junctionDown;
}
if (value === UNICODE.cornerBottomLeft) {
return UNICODE.junctionUp;
}
if (value === UNICODE.junctionRight) {
return UNICODE.junctionAll;
}
}
if (direction === Direction.RIGHT) {
if (value === UNICODE.lineVertical) {
return UNICODE.junctionRight;
}
if (value === UNICODE.cornerTopRight) {
return UNICODE.junctionDown;
}
if (value === UNICODE.cornerBottomRight) {
return UNICODE.junctionUp;
}
if (value === UNICODE.junctionLeft) {
return UNICODE.junctionAll;
}
}
throw new Error(`Can't connect ${value} in direction ${direction}`);
}

export function disconnect(
value: string,
direction: Direction | Direction[]
): string {
if (Array.isArray(direction)) {
return direction.reduce(
(value, direction) => disconnect(value, direction),
value
);
}
if (!connects(value, direction)) {
return value;
}
if (direction === Direction.UP) {
if (value === UNICODE.junctionUp) {
return UNICODE.lineHorizontal;
}
if (value === UNICODE.junctionRight) {
return UNICODE.cornerTopLeft;
}
if (value === UNICODE.junctionLeft) {
return UNICODE.cornerTopRight;
}
if (value === UNICODE.junctionAll) {
return UNICODE.junctionDown;
}
}
if (direction === Direction.DOWN) {
if (value === UNICODE.junctionDown) {
return UNICODE.lineHorizontal;
}
if (value === UNICODE.junctionRight) {
return UNICODE.cornerBottomLeft;
}
if (value === UNICODE.junctionLeft) {
return UNICODE.cornerBottomRight;
}
if (value === UNICODE.junctionUp) {
return UNICODE.junctionDown;
}
}
if (direction === Direction.LEFT) {
if (value === UNICODE.junctionLeft) {
return UNICODE.lineVertical;
}
if (value === UNICODE.junctionDown) {
return UNICODE.cornerTopLeft;
}
if (value === UNICODE.junctionUp) {
return UNICODE.cornerBottomLeft;
}
if (value === UNICODE.junctionRight) {
return UNICODE.junctionLeft;
}
}
if (direction === Direction.RIGHT) {
if (value === UNICODE.junctionRight) {
return UNICODE.lineVertical;
}
if (value === UNICODE.junctionDown) {
return UNICODE.cornerTopRight;
}
if (value === UNICODE.junctionUp) {
return UNICODE.cornerBottomRight;
}
if (value === UNICODE.junctionLeft) {
return UNICODE.junctionRight;
}
}
// There are a few cases where we just can't do this, and that has to be OK.
return value;
}
67 changes: 30 additions & 37 deletions client/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,50 @@ import { Vector } from "#asciiflow/client/vector";
export class Box {
constructor(public readonly start: Vector, public readonly end: Vector) {}

left() {
return Math.min(this.start.x, this.end.x);
}

right() {
return Math.max(this.start.x, this.end.x);
}

top() {
return Math.min(this.start.y, this.end.y);
}

bottom() {
return Math.max(this.start.y, this.end.y);
}

topLeft() {
return new Vector(
Math.min(this.start.x, this.end.x),
Math.min(this.start.y, this.end.y)
);
}

topRight() {
return new Vector(
Math.max(this.start.x, this.end.x),
Math.min(this.start.y, this.end.y)
);
}

bottomRight() {
return new Vector(
Math.max(this.start.x, this.end.x),
Math.max(this.start.y, this.end.y)
);
}

bottomLeft() {
return new Vector(
Math.min(this.start.x, this.end.x),
Math.max(this.start.y, this.end.y)
);
}

contains(position: Vector) {
const topLeft = this.topLeft();
const bottomRight = this.bottomRight();
Expand All @@ -33,28 +63,6 @@ export class Box {
}
}

/**
* An individual cell within the diagram and it's current value.
*/
export class Cell {
constructor(public value?: string, public scratchValue?: string) {}

getRawValue() {
return this.scratchValue != null ? this.scratchValue : this.value;
}

isSpecial() {
return ALL_SPECIAL_VALUES.includes(this.getRawValue());
}

isEmpty() {
return this.value == null && this.scratchValue == null;
}

hasScratch() {
return this.scratchValue != null;
}
}

export class CellContext {
constructor(
Expand All @@ -71,19 +79,4 @@ export class CellContext {
sum() {
return +this.left + +this.right + +this.up + +this.down;
}
/**
* Returns the total number of surrounding special cells.
*/
extendedSum() {
return (
+this.left +
+this.right +
+this.up +
+this.down +
+this.leftup +
+this.leftdown +
+this.rightup +
+this.rightdown
);
}
}
10 changes: 1 addition & 9 deletions client/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Vector } from "#asciiflow/client/vector";

export const MAX_GRID_WIDTH = 2000;
export const MAX_GRID_HEIGHT = 600;
Expand Down Expand Up @@ -135,11 +134,4 @@ export const KEY_RIGHT = "<right>";

// http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
export const TOUCH_ENABLED = () =>
"ontouchstart" in window || "onmsgesturechange" in window;

export const DIR_LEFT = new Vector(-1, 0);
export const DIR_RIGHT = new Vector(1, 0);
export const DIR_UP = new Vector(0, -1);
export const DIR_DOWN = new Vector(0, 1);

export const DIRECTIONS = [DIR_LEFT, DIR_RIGHT, DIR_UP, DIR_DOWN];
"ontouchstart" in window || "onmsgesturechange" in window;
Loading

0 comments on commit 8ac0365

Please sign in to comment.