gizmos
This commit is contained in:
parent
0a4debaaad
commit
7f61e53004
|
|
@ -2,6 +2,7 @@ import { getResizeCursorUrl, getRotateCursorUrl } from '@labs/utils/cursors';
|
|||
import { DOMRectTransform, DOMRectTransformReadonly, FolkElement, Point, TransformEvent, Vector } from '@lib';
|
||||
import { ResizeManager } from '@lib/resize-manger';
|
||||
import { html } from '@lib/tags';
|
||||
import { MAX_Z_INDEX } from '@lib/utils';
|
||||
import { css, PropertyValues } from '@lit/reactive-element';
|
||||
|
||||
const resizeManager = new ResizeManager();
|
||||
|
|
@ -70,7 +71,7 @@ const styles = css`
|
|||
|
||||
:host(:focus-within),
|
||||
:host(:focus-visible) {
|
||||
z-index: calc(infinity - 1);
|
||||
z-index: calc(${MAX_Z_INDEX} - 1);
|
||||
outline: solid 1px hsl(214, 84%, 56%);
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +93,7 @@ const styles = css`
|
|||
aspect-ratio: 1;
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: calc(infinity);
|
||||
z-index: calc(${MAX_Z_INDEX} - 1);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { FolkElement } from '@lib';
|
||||
import { DOMTransform } from '@lib/DOMTransform';
|
||||
import { html } from '@lib/tags';
|
||||
import { Point } from '@lib/types';
|
||||
import { css } from '@lit/reactive-element';
|
||||
|
|
@ -94,8 +93,6 @@ export class FolkSpace extends FolkElement {
|
|||
const matrix = new DOMMatrix()
|
||||
.translate(centerX, centerY)
|
||||
.multiply(new DOMMatrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / this.#perspective, 0, 0, 0, 1]))
|
||||
.translate(-centerX, -centerY)
|
||||
.translate(centerX, centerY)
|
||||
.multiply(face === 'front' ? this.#frontMatrix : this.#backMatrix)
|
||||
.translate(-centerX, -centerY);
|
||||
|
||||
|
|
@ -139,7 +136,6 @@ export class FolkSpace extends FolkElement {
|
|||
y: rect.y + rect.height / 2,
|
||||
};
|
||||
|
||||
// Transform center point
|
||||
const transformedCenter = this.localToScreen(center, face);
|
||||
|
||||
return {
|
||||
|
|
@ -167,18 +163,6 @@ export class FolkSpace extends FolkElement {
|
|||
return this.transformRect(rect, face);
|
||||
}
|
||||
|
||||
// Fallback to getBoundingClientRect
|
||||
const rect = element.getBoundingClientRect();
|
||||
const spaceRect = this.getBoundingClientRect();
|
||||
|
||||
return this.transformRect(
|
||||
{
|
||||
x: rect.x - spaceRect.x,
|
||||
y: rect.y - spaceRect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
},
|
||||
face,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import { FolkElement } from '@lib';
|
||||
import { FolkElement, type Point } from '@lib';
|
||||
import { Gizmos } from '@lib/folk-gizmos';
|
||||
import { html } from '@lib/tags';
|
||||
import { TransformEvent } from '@lib/TransformEvent';
|
||||
import { css } from '@lit/reactive-element';
|
||||
|
||||
interface TransformRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export class FolkTransformedSpace extends FolkElement {
|
||||
static override tagName = 'folk-transformed-space';
|
||||
|
||||
static #perspective = 1000;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
// perspective: 1000px;
|
||||
perspective: ${this.#perspective}px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -49,22 +59,80 @@ export class FolkTransformedSpace extends FolkElement {
|
|||
if (space instanceof HTMLElement) {
|
||||
space.style.transform = this.#matrix.toString();
|
||||
}
|
||||
|
||||
Gizmos.clear();
|
||||
}
|
||||
|
||||
#handleTransform = (event: TransformEvent) => {
|
||||
// Extract rotation angles from the transformation matrix
|
||||
const rotationX = Math.atan2(this.#matrix.m32, this.#matrix.m33);
|
||||
const rotationY = Math.atan2(
|
||||
-this.#matrix.m31,
|
||||
Math.sqrt(this.#matrix.m32 * this.#matrix.m32 + this.#matrix.m33 * this.#matrix.m33),
|
||||
);
|
||||
const previous = this.transformRect(event.previous);
|
||||
const current = this.transformRect(event.current);
|
||||
|
||||
// Calculate projection factors for both axes
|
||||
const projectionFactorY = 1 / Math.cos(rotationX);
|
||||
const projectionFactorX = 1 / Math.cos(rotationY);
|
||||
Gizmos.rect(event.current, {
|
||||
color: 'rgba(0, 0, 255, 0.1)',
|
||||
width: 2,
|
||||
layer: 'default',
|
||||
});
|
||||
|
||||
// Apply the transformed movement with both projection factors
|
||||
event.current.x *= projectionFactorX;
|
||||
event.current.y *= projectionFactorY;
|
||||
Gizmos.line(event.current, current, {
|
||||
color: 'gray',
|
||||
width: 2,
|
||||
layer: 'transformed',
|
||||
});
|
||||
Gizmos.point(event.current, {
|
||||
color: 'blue',
|
||||
size: 3,
|
||||
layer: 'transformed',
|
||||
});
|
||||
Gizmos.point(current, {
|
||||
color: 'red',
|
||||
size: 3,
|
||||
layer: 'transformed',
|
||||
});
|
||||
|
||||
const delta = {
|
||||
x: current.x - previous.x,
|
||||
y: current.y - previous.y,
|
||||
};
|
||||
|
||||
event.current.x += delta.x;
|
||||
event.current.y += delta.y;
|
||||
};
|
||||
|
||||
localToScreen(point: Point): Point {
|
||||
const spaceRect = this.getBoundingClientRect();
|
||||
const centerX = spaceRect.width / 2;
|
||||
const centerY = spaceRect.height / 2;
|
||||
|
||||
// Use the same matrix we're using for CSS
|
||||
const matrix = new DOMMatrix().translate(centerX, centerY).multiply(this.#matrix).translate(-centerX, -centerY);
|
||||
|
||||
const transformedPoint = matrix.transformPoint(new DOMPoint(point.x, point.y, 0, 1));
|
||||
|
||||
const w = transformedPoint.w || 1;
|
||||
return {
|
||||
x: transformedPoint.x / w,
|
||||
y: transformedPoint.y / w,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a rect from an element in either face to screen coordinates
|
||||
*/
|
||||
transformRect(rect: TransformRect): TransformRect {
|
||||
// Get center point
|
||||
const center = {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
};
|
||||
|
||||
// Transform center point
|
||||
const transformedCenter = this.localToScreen(center);
|
||||
|
||||
return {
|
||||
x: transformedCenter.x,
|
||||
y: transformedCenter.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
import { DOMRectTransform, FolkElement, type Point } from '@lib';
|
||||
import { html } from '@lib/tags';
|
||||
import { css } from '@lit/reactive-element';
|
||||
|
||||
interface GizmoOptions {
|
||||
color?: string;
|
||||
layer?: string;
|
||||
}
|
||||
|
||||
interface PointOptions extends GizmoOptions {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface LineOptions extends GizmoOptions {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface RectOptions extends LineOptions {
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual debugging system that renders canvas overlays in DOM containers.
|
||||
*
|
||||
* Creates full-size canvas overlays that can be placed anywhere in the DOM.
|
||||
* Supports multiple instances with isolated drawing layers.
|
||||
*
|
||||
* Usage:
|
||||
* ```html
|
||||
* <folk-gizmos layer="debug"></folk-gizmos>
|
||||
* ```
|
||||
*
|
||||
* Drawing methods:
|
||||
* ```ts
|
||||
* Gizmos.point({x, y});
|
||||
* Gizmos.line(start, end, { color: 'red' });
|
||||
* Gizmos.rect(domRect, { fill: 'blue' });
|
||||
* ```
|
||||
*/
|
||||
export class Gizmos extends FolkElement {
|
||||
static override tagName = 'folk-gizmos';
|
||||
static #layers = new Map<
|
||||
string,
|
||||
{
|
||||
ctx: CanvasRenderingContext2D;
|
||||
canvas: HTMLCanvasElement;
|
||||
}
|
||||
>();
|
||||
static #defaultLayer = 'default';
|
||||
static #hasLoggedDrawWarning = false;
|
||||
static #hasLoggedInitMessage = false;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: calc(Infinity);
|
||||
}
|
||||
|
||||
.gizmos-canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
readonly #layer: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#layer = this.getAttribute('layer') ?? Gizmos.#defaultLayer;
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
const root = super.createRenderRoot() as ShadowRoot;
|
||||
|
||||
root.setHTMLUnsafe(html` <canvas class="gizmos-canvas"></canvas> `);
|
||||
|
||||
const canvas = root.querySelector('.gizmos-canvas') as HTMLCanvasElement;
|
||||
const ctx = canvas?.getContext('2d') ?? null;
|
||||
|
||||
if (canvas && ctx) {
|
||||
Gizmos.#layers.set(this.#layer, { canvas, ctx });
|
||||
}
|
||||
|
||||
this.#handleResize();
|
||||
window.addEventListener('resize', () => this.#handleResize());
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Draws a point */
|
||||
static point(point: Point, { color = 'red', size = 5, layer = Gizmos.#defaultLayer }: PointOptions = {}) {
|
||||
const ctx = Gizmos.#getContext(layer);
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = color;
|
||||
ctx.arc(point.x, point.y, size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
/** Draws a line between two points */
|
||||
static line(start: Point, end: Point, { color = 'blue', width = 2, layer = Gizmos.#defaultLayer }: LineOptions = {}) {
|
||||
const ctx = Gizmos.#getContext(layer);
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.moveTo(start.x, start.y);
|
||||
ctx.lineTo(end.x, end.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/** Draws a rectangle, can be a regular DOMRect or a DOMRectTransform */
|
||||
static rect(
|
||||
rect: DOMRect | DOMRectTransform,
|
||||
{ color = 'blue', width = 2, fill, layer = Gizmos.#defaultLayer }: RectOptions = {},
|
||||
) {
|
||||
const ctx = Gizmos.#getContext(layer);
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
|
||||
if (rect instanceof DOMRectTransform) {
|
||||
// For transformed rectangles, draw using the vertices
|
||||
const vertices = rect.vertices().map((p) => rect.toParentSpace(p));
|
||||
ctx.moveTo(vertices[0].x, vertices[0].y);
|
||||
for (let i = 1; i < vertices.length; i++) {
|
||||
ctx.lineTo(vertices[i].x, vertices[i].y);
|
||||
}
|
||||
ctx.closePath();
|
||||
} else {
|
||||
// For regular DOMRects, draw a simple rectangle
|
||||
ctx.rect(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
if (fill) {
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/** Clears drawings from a specific layer or all layers if no layer specified */
|
||||
static clear(layer?: string) {
|
||||
if (layer) {
|
||||
const layerData = Gizmos.#layers.get(layer);
|
||||
if (!layerData) return;
|
||||
const { ctx, canvas } = layerData;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
} else {
|
||||
// Clear all layers
|
||||
Gizmos.#layers.forEach(({ ctx, canvas }) => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#handleResize() {
|
||||
const layerData = Gizmos.#layers.get(this.#layer);
|
||||
if (!layerData) return;
|
||||
|
||||
const { canvas, ctx } = layerData;
|
||||
const rect = this.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio;
|
||||
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
static #getContext(layer = Gizmos.#defaultLayer) {
|
||||
if (!Gizmos.#hasLoggedInitMessage) {
|
||||
const gizmos = document.querySelectorAll<Gizmos>(Gizmos.tagName);
|
||||
console.info(
|
||||
'%cGizmos',
|
||||
'font-weight: bold; color: #4CAF50;',
|
||||
'\n• Gizmo elements:',
|
||||
gizmos.length,
|
||||
'\n• Layers:',
|
||||
`[${Array.from(Gizmos.#layers.keys()).join(', ')}]`,
|
||||
'\n• Default layer:',
|
||||
Gizmos.#defaultLayer,
|
||||
);
|
||||
Gizmos.#hasLoggedInitMessage = true;
|
||||
}
|
||||
|
||||
const layerData = Gizmos.#layers.get(layer);
|
||||
if (!layerData && !Gizmos.#hasLoggedDrawWarning) {
|
||||
console.warn(`Gizmos cannot draw: layer "${layer}" not found`);
|
||||
Gizmos.#hasLoggedDrawWarning = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
return layerData?.ctx;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('folk-gizmos')) {
|
||||
customElements.define('folk-gizmos', Gizmos);
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
import type { Point } from './types.ts';
|
||||
import { Vector } from './Vector.ts';
|
||||
|
||||
export const MAX_Z_INDEX = 2147483647;
|
||||
|
||||
// Distance squared from a point p to the line segment vw
|
||||
function distanceToSegmentSq(p: Point, v: Point, w: Point): number {
|
||||
const l2 = Vector.distanceSquared(v, w);
|
||||
|
|
@ -49,7 +51,7 @@ function getPointsOnBezierCurveWithSplitting(
|
|||
points: readonly Point[],
|
||||
offset: number,
|
||||
tolerance: number,
|
||||
newPoints?: Point[]
|
||||
newPoints?: Point[],
|
||||
): Point[] {
|
||||
const outPoints = newPoints || [];
|
||||
if (flatness(points, offset) < tolerance) {
|
||||
|
|
@ -97,7 +99,7 @@ export function simplifyPoints(
|
|||
start: number,
|
||||
end: number,
|
||||
epsilon: number,
|
||||
newPoints?: Point[]
|
||||
newPoints?: Point[],
|
||||
): Point[] {
|
||||
const outPoints = newPoints || [];
|
||||
|
||||
|
|
@ -155,7 +157,7 @@ export function getSvgPathFromStroke(stroke: number[][]): string {
|
|||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
||||
return acc;
|
||||
},
|
||||
['M', ...stroke[0], 'Q']
|
||||
['M', ...stroke[0], 'Q'],
|
||||
);
|
||||
|
||||
d.push('Z');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<folk-gizmos></folk-gizmos>
|
||||
<folk-transformed-space>
|
||||
<folk-gizmos layer="transformed"></folk-gizmos>
|
||||
<folk-shape x="250" y="100" width="50" height="50"></folk-shape>
|
||||
<folk-shape x="200" y="200" width="75" height="75" rotation="90"></folk-shape>
|
||||
<folk-shape x="50" y="250" width="25" height="25" rotation="180"></folk-shape>
|
||||
|
|
|
|||
Loading…
Reference in New Issue