diff --git a/demo/arrows.html b/demo/arrows.html
index 914f331..aa6aa63 100644
--- a/demo/arrows.html
+++ b/demo/arrows.html
@@ -25,11 +25,15 @@
position: absolute;
inset: 0 0 0 0;
}
+
+ #box2 {
+ max-width: 10ch;
+ }
- Hello World
+ Hello World
diff --git a/demo/chains-of-thought/index.html b/demo/chains-of-thought/index.html
index e85882f..4832094 100644
--- a/demo/chains-of-thought/index.html
+++ b/demo/chains-of-thought/index.html
@@ -59,10 +59,14 @@
border: solid 1px light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
display: block;
height: 100%;
- padding: 0.5em 0.25em;
position: relative;
text-align: center;
width: 100%;
+ width: 30ch;
+
+ > [name='text'] {
+ padding: 0.5em 0.25em;
+ }
> button[name='delete'] {
font-size: 2rem;
diff --git a/demo/chains-of-thought/main.ts b/demo/chains-of-thought/main.ts
index 99fc521..8225c91 100644
--- a/demo/chains-of-thought/main.ts
+++ b/demo/chains-of-thought/main.ts
@@ -16,7 +16,7 @@ class SpatialThought extends HTMLElement {
}
#deleteButton = this.querySelector('button[name="delete"]') as HTMLButtonElement;
- #text = this.querySelector('span[name="text"]') as HTMLSpanElement;
+ #text = this.querySelector('[name="text"]') as HTMLElement;
#geometry = this.parentElement as SpatialGeometry;
@@ -27,7 +27,7 @@ class SpatialThought extends HTMLElement {
}
get text() {
- return this.#text.innerText;
+ return this.#text.innerHTML;
}
handleEvent(event: PointerEvent): void {
@@ -72,9 +72,9 @@ function parseHTML(html: string): Element {
}
function renderThought({ id, x, y, text }: Thought) {
- return html`
-
- ${text}
+ return html`
+
+ ${text}
`;
diff --git a/demo/perf.html b/demo/perf.html
new file mode 100644
index 0000000..a682195
--- /dev/null
+++ b/demo/perf.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ Perf
+
+
+
+
+
+
diff --git a/demo/vite.config.ts b/demo/vite.config.ts
index 13ba4fd..74776d2 100644
--- a/demo/vite.config.ts
+++ b/demo/vite.config.ts
@@ -12,6 +12,9 @@ export default defineConfig({
acc[file.replace('.html', '')] = resolve(__dirname, file);
return acc;
}, {} as Record),
+ // input: {
+ // thoughts: resolve(__dirname, 'chains-of-thought/index.html'),
+ // },
},
modulePreload: {
polyfill: false,
diff --git a/src/canvas/spatial-geometry.ts b/src/canvas/spatial-geometry.ts
index 56bca03..f1166c8 100644
--- a/src/canvas/spatial-geometry.ts
+++ b/src/canvas/spatial-geometry.ts
@@ -1,3 +1,48 @@
+export type ResizeObserverEntryCallback = (entry: ResizeObserverEntry) => void;
+
+class ResizeObserverManager {
+ #elementMap = new WeakMap>();
+ #elementEntry = new WeakMap();
+
+ #vo = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ this.#elementEntry.set(entry.target, entry);
+ this.#elementMap.get(entry.target)?.forEach((callback) => callback(entry));
+ }
+ });
+
+ observe(target: Element, callback: ResizeObserverEntryCallback): void {
+ let callbacks = this.#elementMap.get(target);
+
+ if (callbacks === undefined) {
+ this.#vo.observe(target);
+ this.#elementMap.set(target, (callbacks = new Set()));
+ } else {
+ const entry = this.#elementEntry.get(target);
+ if (entry) {
+ callback(entry);
+ }
+ }
+
+ callbacks.add(callback);
+ }
+
+ unobserve(target: Element, callback: ResizeObserverEntryCallback): void {
+ let callbacks = this.#elementMap.get(target);
+
+ if (callbacks === undefined) return;
+
+ callbacks.delete(callback);
+
+ if (callbacks.size === 0) {
+ this.#vo.unobserve(target);
+ this.#elementMap.delete(target);
+ }
+ }
+}
+
+const resizeObserver = new ResizeObserverManager();
+
export type Shape = 'rectangle' | 'circle' | 'triangle';
export type MoveEventDetail = { movementX: number; movementY: number };
@@ -24,6 +69,8 @@ export class RotateEvent extends CustomEvent {
}
}
+export type Dimension = number | 'auto';
+
const styles = new CSSStyleSheet();
styles.replaceSync(`
:host {
@@ -61,57 +108,49 @@ styles.replaceSync(`
user-select: none;
}
-:host(:not(:focus-within)) [part^="resize"], :host(:not(:focus-within)) [part="rotate"] {
- opacity: 0;
-}
-
-[part^="resize"] {
+[part="resize-nw"],
+[part="resize-ne"],
+[part="resize-se"],
+[part="resize-sw"] {
display: block;
position: absolute;
box-sizing: border-box;
padding: 0;
background: hsl(210, 20%, 98%);
z-index: calc(infinity);
+ width: 13px;
+ aspect-ratio: 1;
+ transform: translate(-50%, -50%);
+ border: 1.5px solid hsl(214, 84%, 56%);
+ border-radius: 2px;
+}
-
- &[part="resize-nw"],
- &[part="resize-ne"],
- &[part="resize-se"],
- &[part="resize-sw"] {
- width: 13px;
- aspect-ratio: 1;
- transform: translate(-50%, -50%);
- border: 1.5px solid hsl(214, 84%, 56%);
- border-radius: 2px;
- }
-
- &[part="resize-nw"] {
- top: 0;
- left: 0;
- }
+[part="resize-nw"] {
+ top: 0;
+ left: 0;
+}
- &[part="resize-ne"] {
- top: 0;
- left: 100%;
- }
+[part="resize-ne"] {
+ top: 0;
+ left: 100%;
+}
- &[part="resize-se"] {
- top: 100%;
- left: 100%;
- }
+[part="resize-se"] {
+ top: 100%;
+ left: 100%;
+}
- &[part="resize-sw"] {
- top: 100%;
- left: 0;
- }
+[part="resize-sw"] {
+ top: 100%;
+ left: 0;
+}
- &[part="resize-nw"], &[part="resize-se"] {
- cursor: var(--fc-nwse-resize, nwse-resize)
- }
+[part="resize-nw"], [part="resize-se"] {
+ cursor: var(--fc-nwse-resize, nwse-resize)
+}
- &[part="resize-ne"], &[part="resize-sw"] {
- cursor: var(--fc-nesw-resize, nesw-resize)
- }
+[part="resize-ne"], [part="resize-sw"] {
+ cursor: var(--fc-nesw-resize, nesw-resize)
}
[part="rotate"] {
@@ -129,7 +168,13 @@ styles.replaceSync(`
left: 50%;
translate: -50% -150%;
cursor: url("data:image/svg+xml,") 16 16, pointer;
-}`);
+}
+
+:host(:not(:focus-within)) [part^="resize"], :host(:not(:focus-within)) [part="rotate"] {
+ opacity: 0;
+ cursor: default;
+}
+`);
declare global {
interface HTMLElementTagNameMap {
@@ -147,6 +192,94 @@ export class SpatialGeometry extends HTMLElement {
#internals = this.attachInternals();
+ #type = (this.getAttribute('type') || 'rectangle') as Shape;
+ get type(): Shape {
+ return this.#type;
+ }
+
+ set type(type: Shape) {
+ this.setAttribute('type', type);
+ }
+
+ #previousX = 0;
+ #x = Number(this.getAttribute('x')) || 0;
+ get x() {
+ return this.#x;
+ }
+
+ set x(x) {
+ this.#previousX = this.#x;
+ this.#x = x;
+ this.#requestUpdate('x');
+ }
+
+ #previousY = 0;
+ #y = Number(this.getAttribute('y')) || 0;
+ get y() {
+ return this.#y;
+ }
+
+ set y(y) {
+ this.#previousY = this.#y;
+ this.#y = y;
+ this.#requestUpdate('y');
+ }
+
+ #autoContentRect = this.getBoundingClientRect();
+
+ #previousWidth: Dimension = 0;
+ #width: Dimension = 0;
+ get width(): number {
+ if (this.#width === 'auto') {
+ return this.#autoContentRect.width;
+ }
+ return this.#width;
+ }
+
+ set width(width: Dimension) {
+ if (width === 'auto') {
+ resizeObserver.observe(this, this.#onResize);
+ } else if (this.#width === 'auto' && this.#height !== 'auto') {
+ resizeObserver.unobserve(this, this.#onResize);
+ }
+ this.#previousWidth = this.#width;
+ this.#width = width;
+ this.#requestUpdate('width');
+ }
+
+ #previousHeight: Dimension = 0;
+ #height: Dimension = 0;
+ get height(): number {
+ if (this.#height === 'auto') {
+ return this.#autoContentRect.height;
+ }
+ return this.#height;
+ }
+
+ set height(height: Dimension) {
+ if (height === 'auto') {
+ resizeObserver.observe(this, this.#onResize);
+ } else if (this.#height === 'auto' && this.#width !== 'auto') {
+ resizeObserver.unobserve(this, this.#onResize);
+ }
+
+ this.#previousHeight = this.#height;
+ this.#height = height;
+ this.#requestUpdate('height');
+ }
+
+ #previousRotate = 0;
+ #rotate = Number(this.getAttribute('rotate')) || 0;
+ get rotate(): number {
+ return this.#rotate;
+ }
+
+ set rotate(rotate: number) {
+ this.#previousRotate = this.#rotate;
+ this.#rotate = rotate;
+ this.#requestUpdate('rotate');
+ }
+
constructor() {
super();
@@ -164,75 +297,9 @@ export class SpatialGeometry extends HTMLElement {
`;
- }
- #type = (this.getAttribute('type') || 'rectangle') as Shape;
- get type(): Shape {
- return this.#type;
- }
-
- set type(type: Shape) {
- this.setAttribute('type', type);
- }
-
- #previousX = 0;
- #x = Number(this.getAttribute('x')) || 0;
- get x(): number {
- return this.#x;
- }
-
- set x(x: number) {
- this.#previousX = this.#x;
- this.#x = x;
- this.#requestUpdate('x');
- }
-
- #previousY = 0;
- #y = Number(this.getAttribute('y')) || 0;
- get y(): number {
- return this.#y;
- }
-
- set y(y: number) {
- this.#previousY = this.#y;
- this.#y = y;
- this.#requestUpdate('y');
- }
-
- #previousWidth = 0;
- #width = Number(this.getAttribute('width')) || 1;
- get width(): number {
- return this.#width;
- }
-
- set width(width: number) {
- this.#previousWidth = this.#width;
- this.#width = width;
- this.#requestUpdate('width');
- }
-
- #previousHeight = 0;
- #height = Number(this.getAttribute('height')) || 1;
- get height(): number {
- return this.#height;
- }
-
- set height(height: number) {
- this.#previousHeight = this.#height;
- this.#height = height;
- this.#requestUpdate('height');
- }
-
- #previousRotate = 0;
- #rotate = Number(this.getAttribute('rotate')) || 0;
- get rotate(): number {
- return this.#rotate;
- }
-
- set rotate(rotate: number) {
- this.#previousRotate = this.#rotate;
- this.#rotate = rotate;
- this.#requestUpdate('rotate');
+ this.height = Number(this.getAttribute('height')) || 'auto';
+ this.width = Number(this.getAttribute('width')) || 'auto';
}
connectedCallback() {
@@ -311,8 +378,8 @@ export class SpatialGeometry extends HTMLElement {
}
if (part === 'rotate') {
- const centerX = (this.#x + this.#width) / 2;
- const centerY = (this.#y + this.#height) / 2;
+ const centerX = (this.#x + this.width) / 2;
+ const centerY = (this.#y + this.height) / 2;
var newAngle = ((Math.atan2(event.clientY - centerY, event.clientX - centerX) + Math.PI / 2) * 180) / Math.PI;
this.rotate = newAngle;
return;
@@ -386,17 +453,17 @@ export class SpatialGeometry extends HTMLElement {
// Although the change in resize isn't useful inside this component, the outside world might find it helpful to calculate acceleration and other physics
const notCancelled = this.dispatchEvent(
new ResizeEvent({
- movementX: this.#width - this.#previousWidth,
- movementY: this.#height - this.#previousHeight,
+ movementX: this.width - (this.#previousWidth === 'auto' ? 0 : this.#previousWidth),
+ movementY: this.height - (this.#previousHeight === 'auto' ? 0 : this.#previousHeight),
})
);
if (notCancelled) {
if (updatedProperties.has('width')) {
- this.style.width = `${this.#width}px`;
+ this.style.width = this.#width === 'auto' ? '' : `${this.#width}px`;
}
if (updatedProperties.has('height')) {
- this.style.height = `${this.#height}px`;
+ this.style.height = this.#height === 'auto' ? '' : `${this.#height}px`;
}
} else {
// TODO: Revert changes to position too
@@ -418,4 +485,26 @@ export class SpatialGeometry extends HTMLElement {
}
}
}
+
+ #onResize = (entry: ResizeObserverEntry) => {
+ const previousRect = this.#autoContentRect;
+ this.#autoContentRect = entry.contentRect;
+
+ const notCancelled = this.dispatchEvent(
+ new ResizeEvent({
+ movementX: this.width - (this.#previousWidth === 'auto' ? previousRect.width : this.#previousWidth),
+ movementY: this.height - (this.#previousHeight === 'auto' ? previousRect.height : this.#previousHeight),
+ })
+ );
+
+ if (!notCancelled) {
+ if (this.#height === 'auto') {
+ this.height = previousRect?.height || 0;
+ }
+
+ if (this.#width === 'auto') {
+ this.width = previousRect?.width || 0;
+ }
+ }
+ };
}