add set and hull elements

This commit is contained in:
“chrisshank” 2024-11-27 18:04:09 -08:00
parent f3e916ef1c
commit 4e5496839b
7 changed files with 240 additions and 26 deletions

49
demo/hull.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shapes</title>
<style>
html {
height: 100%;
}
body {
min-height: 100%;
position: relative;
margin: 0;
}
fc-geometry {
background: rgb(187, 178, 178);
}
folk-hull {
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
background-color: #b4d8f669;
}
</style>
</head>
<body>
<fc-geometry x="50" y="100" width="50" height="50"></fc-geometry>
<fc-geometry x="200" y="200" width="50" height="50"></fc-geometry>
<fc-geometry x="100" y="300" width="50" height="50"></fc-geometry>
<folk-hull sources="fc-geometry"></folk-hull>
<script type="module">
import { FolkGeometry } from '../src/canvas/fc-geometry.ts';
import { FolkHull } from '../src/folk-hull.ts';
FolkGeometry.register();
FolkHull.register();
</script>
</body>
</html>

View File

@ -17,13 +17,6 @@
fc-geometry {
background: rgb(187, 178, 178);
box-shadow: rgba(0, 0, 0, 0.2) 1.95px 1.95px 2.6px;
transition: scale 100ms ease-out, box-shadow 100ms ease-out;
}
fc-geometry:state(move) {
scale: 1.05;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}
</style>
</head>

View File

@ -1,22 +1,9 @@
import { FolkGeometry } from '../canvas/fc-geometry';
import { Vertex } from './utils';
import { parseVertex } from './utils';
import { ClientRectObserverEntry, ClientRectObserverManager } from '../client-rect-observer.ts';
const clientRectObserver = new ClientRectObserverManager();
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([0-9]*[.])?[0-9]+)/;
function parseVertex(str: string): Vertex | null {
const results = vertexRegex.exec(str);
if (results === null) return null;
return {
x: Number(results.groups?.x),
y: Number(results.groups?.y),
};
}
function parseCSSSelector(selector: string): string[] {
return selector.split('>>>').map((s) => s.trim());
}
@ -188,11 +175,6 @@ export class AbstractArrow extends HTMLElement {
this.unobserveTarget();
}
// TODO: why reparse the vertex?
setSourceVertex(vertex: Vertex) {
this.target = `${vertex.x},${vertex.y}`;
}
observeSource() {
this.unobserveSource();

View File

@ -181,3 +181,16 @@ export function verticesToPolygon(vertices: Vertex[]): string {
return `polygon(${vertices.map((vertex) => `${vertex.x}px ${vertex.y}px`).join(', ')})`;
}
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([0-9]*[.])?[0-9]+)/;
export function parseVertex(str: string): Vertex | null {
const results = vertexRegex.exec(str);
if (results === null) return null;
return {
x: Number(results.groups?.x),
y: Number(results.groups?.y),
};
}

View File

@ -204,6 +204,16 @@ export class ClientRectObserver {
export type ClientRectObserverEntryCallback = (entry: ClientRectObserverEntry) => void;
export class ClientRectObserverManager {
static #instance: ClientRectObserverManager | null = null;
// singleton so we only observe elements once
constructor() {
if (ClientRectObserverManager.#instance === null) {
ClientRectObserverManager.#instance = this;
}
return ClientRectObserverManager.#instance;
}
#elementMap = new WeakMap<Element, Set<ClientRectObserverEntryCallback>>();
#vo = new ClientRectObserver((entries) => {

100
src/folk-hull.ts Normal file
View File

@ -0,0 +1,100 @@
import { FolkSet } from './folk-set';
import { Vertex, verticesToPolygon } from './arrows/utils';
export class FolkHull extends FolkSet {
static tagName = 'folk-hull';
update() {
if (this.sourcesMap.size === 0) {
this.style.clipPath = '';
return;
}
const rects = Array.from(this.sourcesMap.values());
const hull = makeHull(rects);
this.style.clipPath = verticesToPolygon(hull);
}
}
/* This code has been modified from the original source, see the original source below. */
/*
* Convex hull algorithm - Library (TypeScript)
*
* Copyright (c) 2021 Project Nayuki
* https://www.nayuki.io/page/convex-hull-algorithm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program (see COPYING.txt and COPYING.LESSER.txt).
* If not, see <http://www.gnu.org/licenses/>.
*/
function comparePoints(a: Vertex, b: Vertex): number {
if (a.x < b.x) return -1;
if (a.x > b.x) return 1;
if (a.y < b.y) return -1;
if (a.y > b.y) return 1;
return 0;
}
export function makeHull(rects: DOMRectReadOnly[]): Vertex[] {
const points: Vertex[] = rects
.flatMap((rect) => [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
{ x: rect.left, y: rect.bottom },
{ x: rect.right, y: rect.bottom },
])
.sort(comparePoints);
if (points.length <= 1) return points;
// Andrew's monotone chain algorithm. Positive y coordinates correspond to "up"
// as per the mathematical convention, instead of "down" as per the computer
// graphics convention. This doesn't affect the correctness of the result.
const upperHull: Array<Vertex> = [];
for (let i = 0; i < points.length; i++) {
const p: Vertex = points[i];
while (upperHull.length >= 2) {
const q: Vertex = upperHull[upperHull.length - 1];
const r: Vertex = upperHull[upperHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
else break;
}
upperHull.push(p);
}
upperHull.pop();
const lowerHull: Array<Vertex> = [];
for (let i = points.length - 1; i >= 0; i--) {
const p: Vertex = points[i];
while (lowerHull.length >= 2) {
const q: Vertex = lowerHull[lowerHull.length - 1];
const r: Vertex = lowerHull[lowerHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
else break;
}
lowerHull.push(p);
}
lowerHull.pop();
if (
upperHull.length === 1 &&
lowerHull.length === 1 &&
upperHull[0].x === lowerHull[0].x &&
upperHull[0].y === lowerHull[0].y
)
return upperHull;
return upperHull.concat(lowerHull);
}

67
src/folk-set.ts Normal file
View File

@ -0,0 +1,67 @@
import { ClientRectObserverEntry, ClientRectObserverManager } from './client-rect-observer.ts';
const clientRectObserver = new ClientRectObserverManager();
export class FolkSet extends HTMLElement {
static tagName = 'folk-set';
static register() {
customElements.define(this.tagName, this);
}
#sources = '';
/** A CSS selector for the sources of the arrow. */
get sources() {
return this.#sources;
}
set sources(sources) {
this.#sources = sources;
this.observeSources();
}
#sourcesMap = new Map<Element, DOMRectReadOnly>();
get sourcesMap() {
return this.#sourcesMap;
}
#sourcesCallback = (entry: ClientRectObserverEntry) => {
this.#sourcesMap.set(entry.target, entry.contentRect);
this.update();
};
connectedCallback() {
this.sources = this.getAttribute('sources') || this.#sources;
}
disconnectedCallback() {
this.unobserveSources();
}
observeSources() {
const sourceElements = new Set(document.querySelectorAll(this.sources));
const currentElements = new Set(this.#sourcesMap.keys());
const elementsToObserve = sourceElements.difference(currentElements);
const elementsToUnobserve = currentElements.difference(sourceElements);
this.unobserveSources(elementsToUnobserve);
for (const el of elementsToObserve) {
clientRectObserver.observe(el, this.#sourcesCallback);
}
this.update();
}
unobserveSources(elements: Iterable<Element> = this.#sourcesMap.keys()) {
for (const el of elements) {
clientRectObserver.unobserve(el, this.#sourcesCallback);
this.#sourcesMap.delete(el);
}
}
update() {}
}