From 6774e8377423fecb4686d791a8c3915469ea80c1 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 16 Dec 2024 03:39:28 -0500 Subject: [PATCH 1/3] setup --- demo/effect-integrator.html | 61 +++++++++++++++++ src/common/EffectIntegrator.ts | 119 +++++++++++++++++++++++++++++++++ src/folk-base-set.ts | 5 ++ src/folk-graph.ts | 2 + 4 files changed, 187 insertions(+) create mode 100644 demo/effect-integrator.html create mode 100644 src/common/EffectIntegrator.ts diff --git a/demo/effect-integrator.html b/demo/effect-integrator.html new file mode 100644 index 0000000..0927722 --- /dev/null +++ b/demo/effect-integrator.html @@ -0,0 +1,61 @@ + + + + + + Physics + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/common/EffectIntegrator.ts b/src/common/EffectIntegrator.ts new file mode 100644 index 0000000..f50e849 --- /dev/null +++ b/src/common/EffectIntegrator.ts @@ -0,0 +1,119 @@ +import type { FolkShape } from '../folk-shape'; + +/** + * Coordinates effects between multiple systems, integrating their proposals into a single result. + * Systems register, yield effects, and await integration when all systems are ready. + */ +export class EffectIntegrator { + private pending = new Map(); + private systems = new Set(); + private waiting = new Set(); + private resolvers: ((value: Map) => void)[] = []; + + /** Register a system to participate in effect integration */ + register(id: string) { + this.systems.add(id); + return { + yield: (element: Element, effect: T) => { + if (!this.pending.has(element)) { + this.pending.set(element, []); + } + (this.pending.get(element)! as T[]).push(effect); + }, + + /** Wait for all systems to submit effects, then receive integrated results */ + integrate: async (): Promise> => { + this.waiting.add(id); + + if (this.waiting.size === this.systems.size) { + // Last system to call integrate - do integration + for (const [element, effects] of this.pending) { + this.pending.set(element, (this.constructor as typeof EffectIntegrator).integrate(element, effects as T[])); + } + const results = this.pending as Map; + + // Reset for next frame + this.pending = new Map(); + this.waiting.clear(); + + // Resolve all waiting systems + this.resolvers.forEach((resolve) => resolve(results)); + this.resolvers = []; + + return results; + } + + // Not all systems ready - wait for integration + return new Promise((resolve) => { + this.resolvers.push(resolve); + }); + }, + }; + } + + /** Integrate multiple effects into a single result. Must be implemented by derived classes. */ + protected static integrate(element: Element, effects: any[]): any { + throw new Error('Derived class must implement static integrate'); + } +} + +// Transform-specific integrator +interface TransformEffect { + x: number; + y: number; + rotation: number; + width: number; + height: number; +} + +export class TransformIntegrator extends EffectIntegrator { + private static instance: TransformIntegrator; + + static register(id: string) { + if (!TransformIntegrator.instance) { + TransformIntegrator.instance = new TransformIntegrator(); + } + return TransformIntegrator.instance.register(id); + } + + /** If the element is focused, return the elements rect, otherwise average the effects */ + protected static override integrate(element: FolkShape, effects: TransformEffect[]): TransformEffect { + if (element === document.activeElement) { + // If the element is focused, we don't want to apply any effects and just return the current state + const rect = element.getTransformDOMRect(); + return { + x: rect.x, + y: rect.y, + rotation: rect.rotation, + width: rect.width, + height: rect.height, + }; + } + + // Accumulate all effects + const result = effects.reduce( + (acc, effect) => ({ + x: acc.x + effect.x, + y: acc.y + effect.y, + rotation: acc.rotation + effect.rotation, + width: effect.width, + height: effect.height, + }), + { x: 0, y: 0, rotation: 0, width: effects[0].width, height: effects[0].height } + ); + + // Average all effects + const count = effects.length; + result.x /= count; + result.y /= count; + result.rotation /= count; + + // Apply averaged results to element + element.x = result.x; + element.y = result.y; + element.rotation = result.rotation; + + // Return averaged result + return result; + } +} diff --git a/src/folk-base-set.ts b/src/folk-base-set.ts index 794cddf..6be36a0 100644 --- a/src/folk-base-set.ts +++ b/src/folk-base-set.ts @@ -63,12 +63,17 @@ export class FolkBaseSet extends FolkElement { #onSlotchange = () => this.#observeSources(); #observeSources() { + console.log('observeSources'); const childElements = new Set(this.children); const elements = this.sources ? document.querySelectorAll(this.sources) : []; const sourceElements = new Set(elements).union(childElements); const elementsToObserve = sourceElements.difference(this.sourceElements); const elementsToUnobserve = this.sourceElements.difference(sourceElements); + console.log('sourceElements', sourceElements); + console.log('elementsToObserve', elementsToObserve); + console.log('elementsToUnobserve', elementsToUnobserve); + this.unobserveSources(elementsToUnobserve); for (const el of elementsToObserve) { diff --git a/src/folk-graph.ts b/src/folk-graph.ts index 6c5e5e1..2e0a968 100644 --- a/src/folk-graph.ts +++ b/src/folk-graph.ts @@ -58,6 +58,8 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo const colaNodes = this.createNodes(); const colaLinks = this.createLinks(); + console.log(colaNodes, colaLinks); + this.graphSim.nodes(colaNodes).links(colaLinks).linkDistance(250).avoidOverlaps(true).handleDisconnected(true); } From 86fc39a2f43704f938c8742f974da247c139c5e0 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 16 Dec 2024 03:58:02 -0500 Subject: [PATCH 2/3] oh snap --- demo/effect-integrator.html | 4 ++-- src/common/EffectIntegrator.ts | 14 ++++++------ src/folk-base-set.ts | 5 ----- src/folk-graph.ts | 36 ++++++++++++++++++++----------- src/folk-physics.ts | 39 +++++++++++++++++++++++++--------- 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/demo/effect-integrator.html b/demo/effect-integrator.html index 0927722..5e454ab 100644 --- a/demo/effect-integrator.html +++ b/demo/effect-integrator.html @@ -43,9 +43,9 @@ - + > diff --git a/src/common/EffectIntegrator.ts b/src/common/EffectIntegrator.ts index f50e849..3adf9a1 100644 --- a/src/common/EffectIntegrator.ts +++ b/src/common/EffectIntegrator.ts @@ -4,17 +4,17 @@ import type { FolkShape } from '../folk-shape'; * Coordinates effects between multiple systems, integrating their proposals into a single result. * Systems register, yield effects, and await integration when all systems are ready. */ -export class EffectIntegrator { - private pending = new Map(); +export class EffectIntegrator { + private pending = new Map(); private systems = new Set(); private waiting = new Set(); - private resolvers: ((value: Map) => void)[] = []; + private resolvers: ((value: Map) => void)[] = []; /** Register a system to participate in effect integration */ register(id: string) { this.systems.add(id); return { - yield: (element: Element, effect: T) => { + yield: (element: E, effect: T) => { if (!this.pending.has(element)) { this.pending.set(element, []); } @@ -22,7 +22,7 @@ export class EffectIntegrator { }, /** Wait for all systems to submit effects, then receive integrated results */ - integrate: async (): Promise> => { + integrate: async (): Promise> => { this.waiting.add(id); if (this.waiting.size === this.systems.size) { @@ -30,7 +30,7 @@ export class EffectIntegrator { for (const [element, effects] of this.pending) { this.pending.set(element, (this.constructor as typeof EffectIntegrator).integrate(element, effects as T[])); } - const results = this.pending as Map; + const results = this.pending as Map; // Reset for next frame this.pending = new Map(); @@ -66,7 +66,7 @@ interface TransformEffect { height: number; } -export class TransformIntegrator extends EffectIntegrator { +export class TransformIntegrator extends EffectIntegrator { private static instance: TransformIntegrator; static register(id: string) { diff --git a/src/folk-base-set.ts b/src/folk-base-set.ts index 6be36a0..794cddf 100644 --- a/src/folk-base-set.ts +++ b/src/folk-base-set.ts @@ -63,17 +63,12 @@ export class FolkBaseSet extends FolkElement { #onSlotchange = () => this.#observeSources(); #observeSources() { - console.log('observeSources'); const childElements = new Set(this.children); const elements = this.sources ? document.querySelectorAll(this.sources) : []; const sourceElements = new Set(elements).union(childElements); const elementsToObserve = sourceElements.difference(this.sourceElements); const elementsToUnobserve = this.sourceElements.difference(sourceElements); - console.log('sourceElements', sourceElements); - console.log('elementsToObserve', elementsToObserve); - console.log('elementsToUnobserve', elementsToUnobserve); - this.unobserveSources(elementsToUnobserve); for (const el of elementsToObserve) { diff --git a/src/folk-graph.ts b/src/folk-graph.ts index 2e0a968..5f0fe5c 100644 --- a/src/folk-graph.ts +++ b/src/folk-graph.ts @@ -4,6 +4,7 @@ import { Layout } from 'webcola'; import { FolkShape } from './folk-shape.ts'; import { AnimationFrameController, AnimationFrameControllerHost } from './common/animation-frame-controller.ts'; import { FolkBaseConnection } from './folk-base-connection'; +import { TransformIntegrator } from './common/EffectIntegrator.ts'; export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHost { static override tagName = 'folk-graph'; @@ -11,6 +12,7 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo private graphSim = new Layout(); private nodes = new Map(); private arrows = new Set(); + private integrator = TransformIntegrator.register('graph'); #rAF = new AnimationFrameController(this); connectedCallback() { @@ -34,21 +36,31 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo } } - tick() { - // TODO: figure out how to let cola continue running. I was never able to do that in the past... + async tick() { this.graphSim.start(1, 0, 0, 0, true, false); - this.graphSim.nodes().forEach((node: any) => { + // Yield graph layout effects + for (const node of this.graphSim.nodes() as any[]) { const shape = node.id; - if (shape === document.activeElement) { - const rect = shape.getTransformDOMRect(); - node.x = rect.center.x; - node.y = rect.center.y; - } else { - shape.x = node.x - shape.width / 2; - shape.y = node.y - shape.height / 2; + this.integrator.yield(shape, { + x: node.x - shape.width / 2, + y: node.y - shape.height / 2, + rotation: shape.rotation, + width: shape.width, + height: shape.height, + }); + } + + // Get integrated results and update graph state + const results = await this.integrator.integrate(); + for (const [shape, result] of results) { + // TODO: this is a hack to get the node from the graph + const node = this.graphSim.nodes().find((n: any) => n.id === shape); + if (node) { + node.x = result.x + shape.width / 2; + node.y = result.y + shape.height / 2; } - }); + } } private createGraph() { @@ -60,7 +72,7 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo console.log(colaNodes, colaLinks); - this.graphSim.nodes(colaNodes).links(colaLinks).linkDistance(250).avoidOverlaps(true).handleDisconnected(true); + this.graphSim.nodes(colaNodes).links(colaLinks).linkDistance(150).avoidOverlaps(true).handleDisconnected(true); } private createNodes() { diff --git a/src/folk-physics.ts b/src/folk-physics.ts index 3fd0709..701d1ba 100644 --- a/src/folk-physics.ts +++ b/src/folk-physics.ts @@ -3,6 +3,7 @@ import { FolkBaseSet } from './folk-base-set.ts'; import { PropertyValues } from '@lit/reactive-element'; import { FolkShape } from './folk-shape'; import RAPIER, { init } from '@dimforge/rapier2d-compat'; +import { TransformIntegrator } from './common/EffectIntegrator.ts'; await init(); export class FolkPhysics extends FolkBaseSet { @@ -15,6 +16,7 @@ export class FolkPhysics extends FolkBaseSet { private elementToRect: Map = new Map(); private animationFrameId?: number; private lastTimestamp?: number; + private integrator = TransformIntegrator.register('physics'); connectedCallback() { super.connectedCallback(); @@ -119,7 +121,7 @@ export class FolkPhysics extends FolkBaseSet { } private startSimulation() { - const step = (timestamp: number) => { + const step = async (timestamp: number) => { if (!this.lastTimestamp) { this.lastTimestamp = timestamp; } @@ -127,16 +129,33 @@ export class FolkPhysics extends FolkBaseSet { if (this.world) { this.world.step(); - // Update visual elements based on physics - this.bodies.forEach((body, shape) => { - if (shape !== document.activeElement) { - const position = body.translation(); - // Scale up the position when applying to visual elements - shape.x = position.x / FolkPhysics.PHYSICS_SCALE - shape.width / 2; - shape.y = position.y / FolkPhysics.PHYSICS_SCALE - shape.height / 2; - shape.rotation = body.rotation(); + // Yield physics effects + for (const [shape, body] of this.bodies) { + const position = body.translation(); + this.integrator.yield(shape, { + x: position.x / FolkPhysics.PHYSICS_SCALE - shape.width / 2, + y: position.y / FolkPhysics.PHYSICS_SCALE - shape.height / 2, + rotation: body.rotation(), + width: shape.width, + height: shape.height, + }); + } + + // Get integrated results and update physics state + const results = await this.integrator.integrate(); + for (const [shape, result] of results) { + const body = this.bodies.get(shape); + if (body) { + body.setTranslation( + { + x: (result.x + result.width / 2) * FolkPhysics.PHYSICS_SCALE, + y: (result.y + result.height / 2) * FolkPhysics.PHYSICS_SCALE, + }, + true + ); + body.setRotation(result.rotation, true); } - }); + } } this.animationFrameId = requestAnimationFrame(step); From 19453de1cbf5c829d9764c36f7160a6888b73537 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 16 Dec 2024 04:25:40 -0500 Subject: [PATCH 3/3] cleanup --- demo/effect-integrator.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/demo/effect-integrator.html b/demo/effect-integrator.html index 5e454ab..db78072 100644 --- a/demo/effect-integrator.html +++ b/demo/effect-integrator.html @@ -44,11 +44,9 @@ - +