Skip to content

Commit

Permalink
Merge branch 'main' of github.com:a-type/biscuits
Browse files Browse the repository at this point in the history
  • Loading branch information
a-type committed Jun 6, 2024
2 parents 3056ebb + 95df4cb commit fed20e2
Show file tree
Hide file tree
Showing 22 changed files with 578 additions and 574 deletions.
34 changes: 19 additions & 15 deletions apps/star-chart/web/src/components/canvas/BoxRegion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ import { useCanvasGestures } from './canvasHooks.js';
import { useRef, useState } from 'react';
import { Vector2 } from './types.js';
import { CanvasGestureInfo } from './Canvas.js';
import { useCanvas } from './CanvasProvider.jsx';

export interface BoxRegionProps {
onPending?: (objectIds: string[], info: CanvasGestureInfo) => void;
onEnd?: (
objectIds: string[],
endPosition: Vector2,
info: CanvasGestureInfo,
) => void;
onEnd?: (objectIds: string[], info: CanvasGestureInfo) => void;
tolerance?: number;
className?: string;
}
Expand All @@ -31,18 +28,25 @@ export function BoxRegion({

const previousPending = useRef<string[]>([]);

const canvas = useCanvas();

useCanvasGestures({
onDragStart: (pos) => {
onDragStart: (info) => {
previousPending.current = [];
originRef.current = pos;
spring.set({ x: pos.x, y: pos.y, width: 0, height: 0 });
originRef.current = info.worldPosition;
spring.set({
x: info.worldPosition.x,
y: info.worldPosition.y,
width: 0,
height: 0,
});
},
onDrag: (pos, { canvas, info }) => {
onDrag: (info) => {
const rect = {
x: Math.min(pos.x, originRef.current.x),
y: Math.min(pos.y, originRef.current.y),
width: Math.abs(pos.x - originRef.current.x),
height: Math.abs(pos.y - originRef.current.y),
x: Math.min(info.worldPosition.x, originRef.current.x),
y: Math.min(info.worldPosition.y, originRef.current.y),
width: Math.abs(info.worldPosition.x - originRef.current.x),
height: Math.abs(info.worldPosition.y - originRef.current.y),
};
spring.set(rect);
const objectIds = canvas.bounds.getIntersections(rect, tolerance);
Expand All @@ -65,7 +69,7 @@ export function BoxRegion({

previousPending.current = objectIds;
},
onDragEnd: (pos, { canvas, info }) => {
onDragEnd: (info) => {
const objectIds = canvas.bounds.getIntersections(
{
x: x.get(),
Expand All @@ -77,7 +81,7 @@ export function BoxRegion({
);

onPending?.([], info);
onCommit?.(objectIds, pos, info);
onCommit?.(objectIds, info);

spring.set({ x: 0, y: 0, width: 0, height: 0 });
originRef.current.x = 0;
Expand Down
4 changes: 2 additions & 2 deletions apps/star-chart/web/src/components/canvas/BoxSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ export function BoxSelect({ className, onCommit }: BoxSelectProps) {
onPending={(objectIds, info) => {
canvas.selections.setPending(objectIds);
}}
onEnd={(objectIds, endPosition, info) => {
onEnd={(objectIds, info) => {
if (info.shift) {
canvas.selections.addAll(objectIds);
} else {
canvas.selections.set(objectIds);
}
onCommit?.(objectIds, endPosition);
onCommit?.(objectIds, info.worldPosition);
}}
className={className}
/>
Expand Down
133 changes: 92 additions & 41 deletions apps/star-chart/web/src/components/canvas/Canvas.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,53 @@
import { EventSubscriber } from '@a-type/utils';
import { SpringValue, to } from '@react-spring/web';
import { snap } from './math.js';
import { clampVector, snap } from './math.js';
import { ObjectBounds } from './ObjectBounds.js';
import { ObjectPositions } from './ObjectPositions.js';
import { Selections } from './Selections.js';
import { Vector2 } from './types.js';
import { Viewport } from './Viewport.js';

type ActiveGestureState = {
targetObjectId: string | null;
position: Vector2 | null;
startPosition: Vector2 | null;
};
import { RectLimits, Vector2 } from './types.js';
import { Viewport, ViewportConfig } from './Viewport.js';

export interface CanvasOptions {
/** Snaps items to a world-unit grid after dropping them - defaults to 1. */
positionSnapIncrement?: number;
limits?: RectLimits;
viewportConfig?: Omit<ViewportConfig, 'canvas'>;
}

export interface CanvasGestureInfo {
shift: boolean;
alt: boolean;
ctrlOrMeta: boolean;
intentional: boolean;
delta: Vector2;
worldPosition: Vector2;
targetId?: string;
}

export interface CanvasGestureInput
extends Omit<CanvasGestureInfo, 'worldPosition'> {
screenPosition: Vector2;
}

const DEFAULT_LIMITS: RectLimits = {
max: { x: 1_000_000, y: 1_000_000 },
min: { x: -1_000_000, y: -1_000_000 },
};

export type CanvasEvents = {
[k: `objectDrop:${string}`]: (
newPosition: Vector2,
info: { source: 'gesture' | 'external' },
) => void;
[k: `objectDrag:${string}`]: (newPosition: Vector2) => void;
canvasTap: (position: Vector2, info: CanvasGestureInfo) => void;
canvasDragStart: (position: Vector2, info: CanvasGestureInfo) => void;
canvasDrag: (position: Vector2, info: CanvasGestureInfo) => void;
canvasDragEnd: (position: Vector2, info: CanvasGestureInfo) => void;
objectDragStart: (info: CanvasGestureInfo) => void;
objectDrag: (info: CanvasGestureInfo) => void;
objectDragEnd: (info: CanvasGestureInfo) => void;
canvasTap: (info: CanvasGestureInfo) => void;
canvasDragStart: (info: CanvasGestureInfo) => void;
canvasDrag: (info: CanvasGestureInfo) => void;
canvasDragEnd: (info: CanvasGestureInfo) => void;
resize: (size: RectLimits) => void;
};

/**
* This class encapsulates the logic which powers the movement and
* sizing of objects within a Room Canvas - the 2d space that makes
* up the standard With Room. It implements the required functionality
* for both CanvasContext and SizingContext
*/
export class Canvas extends EventSubscriber<CanvasEvents> {
readonly viewport: Viewport;
readonly limits: RectLimits;

readonly bounds = new ObjectBounds();
readonly selections = new Selections();

Expand All @@ -51,11 +56,10 @@ export class Canvas extends EventSubscriber<CanvasEvents> {

private _positionSnapIncrement = 1;

constructor(
private viewport: Viewport,
options?: CanvasOptions,
) {
constructor(options?: CanvasOptions) {
super();
this.viewport = new Viewport({ ...options?.viewportConfig, canvas: this });
this.limits = options?.limits ?? DEFAULT_LIMITS;
// @ts-ignore for debugging...
window.canvas = this;
this._positionSnapIncrement = options?.positionSnapIncrement ?? 1;
Expand All @@ -65,29 +69,76 @@ export class Canvas extends EventSubscriber<CanvasEvents> {
return this._positionSnapIncrement;
}

get boundary() {
return {
x: this.limits.min.x,
y: this.limits.min.y,
width: this.limits.max.x - this.limits.min.x,
height: this.limits.max.y - this.limits.min.y,
};
}

get center() {
return {
x: (this.limits.max.x + this.limits.min.x) / 2,
y: (this.limits.max.y + this.limits.min.y) / 2,
};
}

snapPosition = (position: Vector2) => ({
x: snap(position.x, this._positionSnapIncrement),
y: snap(position.y, this._positionSnapIncrement),
});

onCanvasTap = (screenPosition: Vector2, info: CanvasGestureInfo) => {
const worldPosition = this.viewport.viewportToWorld(screenPosition);
this.emit('canvasTap', worldPosition, info);
clampPosition = (position: Vector2) =>
clampVector(position, this.limits.min, this.limits.max);

resize = (size: RectLimits) => {
this.limits.min = size.min;
this.limits.max = size.max;
this.emit('resize', size);
};

private transformGesture = (
{ screenPosition, delta, ...rest }: CanvasGestureInput,
snap?: boolean,
): CanvasGestureInfo => {
let pos = this.viewport.viewportToWorld(screenPosition);
if (snap) {
pos = this.snapPosition(pos);
}
return Object.assign(rest, {
worldPosition: pos,
delta: this.viewport.viewportDeltaToWorld(delta),
});
};

onCanvasTap = (info: CanvasGestureInput) => {
this.emit('canvasTap', this.transformGesture(info));
};

onCanvasDragStart = (info: CanvasGestureInput) => {
this.emit('canvasDragStart', this.transformGesture(info));
};

onCanvasDrag = (info: CanvasGestureInput) => {
this.emit('canvasDrag', this.transformGesture(info));
};

onCanvasDragEnd = (info: CanvasGestureInput) => {
this.emit('canvasDragEnd', this.transformGesture(info));
};

onCanvasDragStart = (screenPosition: Vector2, info: CanvasGestureInfo) => {
const worldPosition = this.viewport.viewportToWorld(screenPosition);
this.emit('canvasDragStart', worldPosition, info);
onObjectDragStart = (info: CanvasGestureInput) => {
this.emit('objectDragStart', this.transformGesture(info));
};

onCanvasDrag = (screenPosition: Vector2, info: CanvasGestureInfo) => {
const worldPosition = this.viewport.viewportToWorld(screenPosition);
this.emit('canvasDrag', worldPosition, info);
onObjectDrag = (info: CanvasGestureInput) => {
this.emit('objectDrag', this.transformGesture(info));
};

onCanvasDragEnd = (screenPosition: Vector2, info: CanvasGestureInfo) => {
const worldPosition = this.viewport.viewportToWorld(screenPosition);
this.emit('canvasDragEnd', worldPosition, info);
onObjectDragEnd = (info: CanvasGestureInput) => {
this.emit('objectDragEnd', this.transformGesture(info));
};

/**
Expand Down
Loading

0 comments on commit fed20e2

Please sign in to comment.