Add comprehensive frontend UI and distributed infrastructure

Frontend Enhancements:
- Complete React TypeScript frontend with modern UI components
- Distributed workflows management interface with real-time updates
- Socket.IO integration for live agent status monitoring
- Agent management dashboard with cluster visualization
- Project management interface with metrics and task tracking
- Responsive design with proper error handling and loading states

Backend Infrastructure:
- Distributed coordinator for multi-agent workflow orchestration
- Cluster management API with comprehensive agent operations
- Enhanced database models for agents and projects
- Project service for filesystem-based project discovery
- Performance monitoring and metrics collection
- Comprehensive API documentation and error handling

Documentation:
- Complete distributed development guide (README_DISTRIBUTED.md)
- Comprehensive development report with architecture insights
- System configuration templates and deployment guides

The platform now provides a complete web interface for managing the distributed AI cluster
with real-time monitoring, workflow orchestration, and agent coordination capabilities.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-10 08:41:59 +10:00
parent fc0eec91ef
commit 85bf1341f3
28348 changed files with 2646896 additions and 69 deletions

View File

@@ -0,0 +1,93 @@
import { circOut } from '../../easing/circ.mjs';
import { progress } from '../../utils/progress.mjs';
import { mix } from '../../utils/mix.mjs';
import { noop } from '../../utils/noop.mjs';
import { percent, px } from '../../value/types/numbers/units.mjs';
const borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"];
const numBorders = borders.length;
const asNumber = (value) => typeof value === "string" ? parseFloat(value) : value;
const isPx = (value) => typeof value === "number" || px.test(value);
function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
if (shouldCrossfadeOpacity) {
target.opacity = mix(0,
// TODO Reinstate this if only child
lead.opacity !== undefined ? lead.opacity : 1, easeCrossfadeIn(progress));
target.opacityExit = mix(follow.opacity !== undefined ? follow.opacity : 1, 0, easeCrossfadeOut(progress));
}
else if (isOnlyMember) {
target.opacity = mix(follow.opacity !== undefined ? follow.opacity : 1, lead.opacity !== undefined ? lead.opacity : 1, progress);
}
/**
* Mix border radius
*/
for (let i = 0; i < numBorders; i++) {
const borderLabel = `border${borders[i]}Radius`;
let followRadius = getRadius(follow, borderLabel);
let leadRadius = getRadius(lead, borderLabel);
if (followRadius === undefined && leadRadius === undefined)
continue;
followRadius || (followRadius = 0);
leadRadius || (leadRadius = 0);
const canMix = followRadius === 0 ||
leadRadius === 0 ||
isPx(followRadius) === isPx(leadRadius);
if (canMix) {
target[borderLabel] = Math.max(mix(asNumber(followRadius), asNumber(leadRadius), progress), 0);
if (percent.test(leadRadius) || percent.test(followRadius)) {
target[borderLabel] += "%";
}
}
else {
target[borderLabel] = leadRadius;
}
}
/**
* Mix rotation
*/
if (follow.rotate || lead.rotate) {
target.rotate = mix(follow.rotate || 0, lead.rotate || 0, progress);
}
}
function getRadius(values, radiusName) {
return values[radiusName] !== undefined
? values[radiusName]
: values.borderRadius;
}
// /**
// * We only want to mix the background color if there's a follow element
// * that we're not crossfading opacity between. For instance with switch
// * AnimateSharedLayout animations, this helps the illusion of a continuous
// * element being animated but also cuts down on the number of paints triggered
// * for elements where opacity is doing that work for us.
// */
// if (
// !hasFollowElement &&
// latestLeadValues.backgroundColor &&
// latestFollowValues.backgroundColor
// ) {
// /**
// * This isn't ideal performance-wise as mixColor is creating a new function every frame.
// * We could probably create a mixer that runs at the start of the animation but
// * the idea behind the crossfader is that it runs dynamically between two potentially
// * changing targets (ie opacity or borderRadius may be animating independently via variants)
// */
// leadState.backgroundColor = followState.backgroundColor = mixColor(
// latestFollowValues.backgroundColor as string,
// latestLeadValues.backgroundColor as string
// )(p)
// }
const easeCrossfadeIn = compress(0, 0.5, circOut);
const easeCrossfadeOut = compress(0.5, 0.95, noop);
function compress(min, max, easing) {
return (p) => {
// Could replace ifs with clamp
if (p < min)
return 0;
if (p > max)
return 1;
return easing(progress(min, max, p));
};
}
export { mixValues };

View File

@@ -0,0 +1,33 @@
/**
* Bounding boxes tend to be defined as top, left, right, bottom. For various operations
* it's easier to consider each axis individually. This function returns a bounding box
* as a map of single-axis min/max values.
*/
function convertBoundingBoxToBox({ top, left, right, bottom, }) {
return {
x: { min: left, max: right },
y: { min: top, max: bottom },
};
}
function convertBoxToBoundingBox({ x, y }) {
return { top: y.min, right: x.max, bottom: y.max, left: x.min };
}
/**
* Applies a TransformPoint function to a bounding box. TransformPoint is usually a function
* provided by Framer to allow measured points to be corrected for device scaling. This is used
* when measuring DOM elements and DOM event points.
*/
function transformBoxPoints(point, transformPoint) {
if (!transformPoint)
return point;
const topLeft = transformPoint({ x: point.left, y: point.top });
const bottomRight = transformPoint({ x: point.right, y: point.bottom });
return {
top: topLeft.y,
left: topLeft.x,
bottom: bottomRight.y,
right: bottomRight.x,
};
}
export { convertBoundingBoxToBox, convertBoxToBoundingBox, transformBoxPoints };

View File

@@ -0,0 +1,20 @@
/**
* Reset an axis to the provided origin box.
*
* This is a mutative operation.
*/
function copyAxisInto(axis, originAxis) {
axis.min = originAxis.min;
axis.max = originAxis.max;
}
/**
* Reset a box to the provided origin box.
*
* This is a mutative operation.
*/
function copyBoxInto(box, originBox) {
copyAxisInto(box.x, originBox.x);
copyAxisInto(box.y, originBox.y);
}
export { copyAxisInto, copyBoxInto };

View File

@@ -0,0 +1,122 @@
import { mix } from '../../utils/mix.mjs';
import { hasTransform } from '../utils/has-transform.mjs';
/**
* Scales a point based on a factor and an originPoint
*/
function scalePoint(point, scale, originPoint) {
const distanceFromOrigin = point - originPoint;
const scaled = scale * distanceFromOrigin;
return originPoint + scaled;
}
/**
* Applies a translate/scale delta to a point
*/
function applyPointDelta(point, translate, scale, originPoint, boxScale) {
if (boxScale !== undefined) {
point = scalePoint(point, boxScale, originPoint);
}
return scalePoint(point, scale, originPoint) + translate;
}
/**
* Applies a translate/scale delta to an axis
*/
function applyAxisDelta(axis, translate = 0, scale = 1, originPoint, boxScale) {
axis.min = applyPointDelta(axis.min, translate, scale, originPoint, boxScale);
axis.max = applyPointDelta(axis.max, translate, scale, originPoint, boxScale);
}
/**
* Applies a translate/scale delta to a box
*/
function applyBoxDelta(box, { x, y }) {
applyAxisDelta(box.x, x.translate, x.scale, x.originPoint);
applyAxisDelta(box.y, y.translate, y.scale, y.originPoint);
}
/**
* Apply a tree of deltas to a box. We do this to calculate the effect of all the transforms
* in a tree upon our box before then calculating how to project it into our desired viewport-relative box
*
* This is the final nested loop within updateLayoutDelta for future refactoring
*/
function applyTreeDeltas(box, treeScale, treePath, isSharedTransition = false) {
const treeLength = treePath.length;
if (!treeLength)
return;
// Reset the treeScale
treeScale.x = treeScale.y = 1;
let node;
let delta;
for (let i = 0; i < treeLength; i++) {
node = treePath[i];
delta = node.projectionDelta;
/**
* TODO: Prefer to remove this, but currently we have motion components with
* display: contents in Framer.
*/
const instance = node.instance;
if (instance &&
instance.style &&
instance.style.display === "contents") {
continue;
}
if (isSharedTransition &&
node.options.layoutScroll &&
node.scroll &&
node !== node.root) {
transformBox(box, {
x: -node.scroll.offset.x,
y: -node.scroll.offset.y,
});
}
if (delta) {
// Incoporate each ancestor's scale into a culmulative treeScale for this component
treeScale.x *= delta.x.scale;
treeScale.y *= delta.y.scale;
// Apply each ancestor's calculated delta into this component's recorded layout box
applyBoxDelta(box, delta);
}
if (isSharedTransition && hasTransform(node.latestValues)) {
transformBox(box, node.latestValues);
}
}
/**
* Snap tree scale back to 1 if it's within a non-perceivable threshold.
* This will help reduce useless scales getting rendered.
*/
treeScale.x = snapToDefault(treeScale.x);
treeScale.y = snapToDefault(treeScale.y);
}
function snapToDefault(scale) {
if (Number.isInteger(scale))
return scale;
return scale > 1.0000000000001 || scale < 0.999999999999 ? scale : 1;
}
function translateAxis(axis, distance) {
axis.min = axis.min + distance;
axis.max = axis.max + distance;
}
/**
* Apply a transform to an axis from the latest resolved motion values.
* This function basically acts as a bridge between a flat motion value map
* and applyAxisDelta
*/
function transformAxis(axis, transforms, [key, scaleKey, originKey]) {
const axisOrigin = transforms[originKey] !== undefined ? transforms[originKey] : 0.5;
const originPoint = mix(axis.min, axis.max, axisOrigin);
// Apply the axis delta to the final axis
applyAxisDelta(axis, transforms[key], transforms[scaleKey], originPoint, transforms.scale);
}
/**
* The names of the motion values we want to apply as translation, scale and origin.
*/
const xKeys = ["x", "scaleX", "originX"];
const yKeys = ["y", "scaleY", "originY"];
/**
* Apply a transform to a box from the latest resolved motion values.
*/
function transformBox(box, transform) {
transformAxis(box.x, transform, xKeys);
transformAxis(box.y, transform, yKeys);
}
export { applyAxisDelta, applyBoxDelta, applyPointDelta, applyTreeDeltas, scalePoint, transformAxis, transformBox, translateAxis };

View File

@@ -0,0 +1,41 @@
import { mix } from '../../utils/mix.mjs';
function calcLength(axis) {
return axis.max - axis.min;
}
function isNear(value, target = 0, maxDistance = 0.01) {
return Math.abs(value - target) <= maxDistance;
}
function calcAxisDelta(delta, source, target, origin = 0.5) {
delta.origin = origin;
delta.originPoint = mix(source.min, source.max, delta.origin);
delta.scale = calcLength(target) / calcLength(source);
if (isNear(delta.scale, 1, 0.0001) || isNaN(delta.scale))
delta.scale = 1;
delta.translate =
mix(target.min, target.max, delta.origin) - delta.originPoint;
if (isNear(delta.translate) || isNaN(delta.translate))
delta.translate = 0;
}
function calcBoxDelta(delta, source, target, origin) {
calcAxisDelta(delta.x, source.x, target.x, origin ? origin.originX : undefined);
calcAxisDelta(delta.y, source.y, target.y, origin ? origin.originY : undefined);
}
function calcRelativeAxis(target, relative, parent) {
target.min = parent.min + relative.min;
target.max = target.min + calcLength(relative);
}
function calcRelativeBox(target, relative, parent) {
calcRelativeAxis(target.x, relative.x, parent.x);
calcRelativeAxis(target.y, relative.y, parent.y);
}
function calcRelativeAxisPosition(target, layout, parent) {
target.min = layout.min - parent.min;
target.max = target.min + calcLength(layout);
}
function calcRelativePosition(target, layout, parent) {
calcRelativeAxisPosition(target.x, layout.x, parent.x);
calcRelativeAxisPosition(target.y, layout.y, parent.y);
}
export { calcAxisDelta, calcBoxDelta, calcLength, calcRelativeAxis, calcRelativeAxisPosition, calcRelativeBox, calcRelativePosition, isNear };

View File

@@ -0,0 +1,54 @@
import { mix } from '../../utils/mix.mjs';
import { percent } from '../../value/types/numbers/units.mjs';
import { scalePoint } from './delta-apply.mjs';
/**
* Remove a delta from a point. This is essentially the steps of applyPointDelta in reverse
*/
function removePointDelta(point, translate, scale, originPoint, boxScale) {
point -= translate;
point = scalePoint(point, 1 / scale, originPoint);
if (boxScale !== undefined) {
point = scalePoint(point, 1 / boxScale, originPoint);
}
return point;
}
/**
* Remove a delta from an axis. This is essentially the steps of applyAxisDelta in reverse
*/
function removeAxisDelta(axis, translate = 0, scale = 1, origin = 0.5, boxScale, originAxis = axis, sourceAxis = axis) {
if (percent.test(translate)) {
translate = parseFloat(translate);
const relativeProgress = mix(sourceAxis.min, sourceAxis.max, translate / 100);
translate = relativeProgress - sourceAxis.min;
}
if (typeof translate !== "number")
return;
let originPoint = mix(originAxis.min, originAxis.max, origin);
if (axis === originAxis)
originPoint -= translate;
axis.min = removePointDelta(axis.min, translate, scale, originPoint, boxScale);
axis.max = removePointDelta(axis.max, translate, scale, originPoint, boxScale);
}
/**
* Remove a transforms from an axis. This is essentially the steps of applyAxisTransforms in reverse
* and acts as a bridge between motion values and removeAxisDelta
*/
function removeAxisTransforms(axis, transforms, [key, scaleKey, originKey], origin, sourceAxis) {
removeAxisDelta(axis, transforms[key], transforms[scaleKey], transforms[originKey], transforms.scale, origin, sourceAxis);
}
/**
* The names of the motion values we want to apply as translation, scale and origin.
*/
const xKeys = ["x", "scaleX", "originX"];
const yKeys = ["y", "scaleY", "originY"];
/**
* Remove a transforms from an box. This is essentially the steps of applyAxisBox in reverse
* and acts as a bridge between motion values and removeAxisDelta
*/
function removeBoxTransforms(box, transforms, originBox, sourceBox) {
removeAxisTransforms(box.x, transforms, xKeys, originBox ? originBox.x : undefined, sourceBox ? sourceBox.x : undefined);
removeAxisTransforms(box.y, transforms, yKeys, originBox ? originBox.y : undefined, sourceBox ? sourceBox.y : undefined);
}
export { removeAxisDelta, removeAxisTransforms, removeBoxTransforms, removePointDelta };

View File

@@ -0,0 +1,17 @@
const createAxisDelta = () => ({
translate: 0,
scale: 1,
origin: 0,
originPoint: 0,
});
const createDelta = () => ({
x: createAxisDelta(),
y: createAxisDelta(),
});
const createAxis = () => ({ min: 0, max: 0 });
const createBox = () => ({
x: createAxis(),
y: createAxis(),
});
export { createAxis, createAxisDelta, createBox, createDelta };

View File

@@ -0,0 +1,25 @@
import { calcLength } from './delta-calc.mjs';
function isAxisDeltaZero(delta) {
return delta.translate === 0 && delta.scale === 1;
}
function isDeltaZero(delta) {
return isAxisDeltaZero(delta.x) && isAxisDeltaZero(delta.y);
}
function boxEquals(a, b) {
return (a.x.min === b.x.min &&
a.x.max === b.x.max &&
a.y.min === b.y.min &&
a.y.max === b.y.max);
}
function boxEqualsRounded(a, b) {
return (Math.round(a.x.min) === Math.round(b.x.min) &&
Math.round(a.x.max) === Math.round(b.x.max) &&
Math.round(a.y.min) === Math.round(b.y.min) &&
Math.round(a.y.max) === Math.round(b.y.max));
}
function aspectRatio(box) {
return calcLength(box.x) / calcLength(box.y);
}
export { aspectRatio, boxEquals, boxEqualsRounded, isDeltaZero };

View File

@@ -0,0 +1,13 @@
import { createProjectionNode } from './create-projection-node.mjs';
import { addDomEvent } from '../../events/add-dom-event.mjs';
const DocumentProjectionNode = createProjectionNode({
attachResizeListener: (ref, notify) => addDomEvent(ref, "resize", notify),
measureScroll: () => ({
x: document.documentElement.scrollLeft || document.body.scrollLeft,
y: document.documentElement.scrollTop || document.body.scrollTop,
}),
checkIsScrollRoot: () => true,
});
export { DocumentProjectionNode };

View File

@@ -0,0 +1,27 @@
import { createProjectionNode } from './create-projection-node.mjs';
import { DocumentProjectionNode } from './DocumentProjectionNode.mjs';
const rootProjectionNode = {
current: undefined,
};
const HTMLProjectionNode = createProjectionNode({
measureScroll: (instance) => ({
x: instance.scrollLeft,
y: instance.scrollTop,
}),
defaultParent: () => {
if (!rootProjectionNode.current) {
const documentNode = new DocumentProjectionNode({});
documentNode.mount(window);
documentNode.setOptions({ layoutScroll: true });
rootProjectionNode.current = documentNode;
}
return rootProjectionNode.current;
},
resetTransform: (instance, value) => {
instance.style.transform = value !== undefined ? value : "none";
},
checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
});
export { HTMLProjectionNode, rootProjectionNode };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
const notify = (node) => !node.isLayoutDirty && node.willUpdate(false);
function nodeGroup() {
const nodes = new Set();
const subscriptions = new WeakMap();
const dirtyAll = () => nodes.forEach(notify);
return {
add: (node) => {
nodes.add(node);
subscriptions.set(node, node.addEventListener("willUpdate", dirtyAll));
},
remove: (node) => {
nodes.delete(node);
const unsubscribe = subscriptions.get(node);
if (unsubscribe) {
unsubscribe();
subscriptions.delete(node);
}
dirtyAll();
},
dirty: dirtyAll,
};
}
export { nodeGroup };

View File

@@ -0,0 +1,19 @@
/**
* This should only ever be modified on the client otherwise it'll
* persist through server requests. If we need instanced states we
* could lazy-init via root.
*/
const globalProjectionState = {
/**
* Global flag as to whether the tree has animated since the last time
* we resized the window
*/
hasAnimatedSinceResize: true,
/**
* We set this to true once, on the first update. Any nodes added to the tree beyond that
* update will be given a `data-projection-id` attribute.
*/
hasEverUpdated: false,
};
export { globalProjectionState };

View File

@@ -0,0 +1,112 @@
import { addUniqueItem, removeItem } from '../../utils/array.mjs';
class NodeStack {
constructor() {
this.members = [];
}
add(node) {
addUniqueItem(this.members, node);
node.scheduleRender();
}
remove(node) {
removeItem(this.members, node);
if (node === this.prevLead) {
this.prevLead = undefined;
}
if (node === this.lead) {
const prevLead = this.members[this.members.length - 1];
if (prevLead) {
this.promote(prevLead);
}
}
}
relegate(node) {
const indexOfNode = this.members.findIndex((member) => node === member);
if (indexOfNode === 0)
return false;
/**
* Find the next projection node that is present
*/
let prevLead;
for (let i = indexOfNode; i >= 0; i--) {
const member = this.members[i];
if (member.isPresent !== false) {
prevLead = member;
break;
}
}
if (prevLead) {
this.promote(prevLead);
return true;
}
else {
return false;
}
}
promote(node, preserveFollowOpacity) {
const prevLead = this.lead;
if (node === prevLead)
return;
this.prevLead = prevLead;
this.lead = node;
node.show();
if (prevLead) {
prevLead.instance && prevLead.scheduleRender();
node.scheduleRender();
node.resumeFrom = prevLead;
if (preserveFollowOpacity) {
node.resumeFrom.preserveOpacity = true;
}
if (prevLead.snapshot) {
node.snapshot = prevLead.snapshot;
node.snapshot.latestValues =
prevLead.animationValues || prevLead.latestValues;
}
if (node.root && node.root.isUpdating) {
node.isLayoutDirty = true;
}
const { crossfade } = node.options;
if (crossfade === false) {
prevLead.hide();
}
/**
* TODO:
* - Test border radius when previous node was deleted
* - boxShadow mixing
* - Shared between element A in scrolled container and element B (scroll stays the same or changes)
* - Shared between element A in transformed container and element B (transform stays the same or changes)
* - Shared between element A in scrolled page and element B (scroll stays the same or changes)
* ---
* - Crossfade opacity of root nodes
* - layoutId changes after animation
* - layoutId changes mid animation
*/
}
}
exitAnimationComplete() {
this.members.forEach((node) => {
const { options, resumingFrom } = node;
options.onExitComplete && options.onExitComplete();
if (resumingFrom) {
resumingFrom.options.onExitComplete &&
resumingFrom.options.onExitComplete();
}
});
}
scheduleRender() {
this.members.forEach((node) => {
node.instance && node.scheduleRender(false);
});
}
/**
* Clear any leads that have been removed this render to prevent them from being
* used in future animations and to prevent memory leaks
*/
removeLeadSnapshot() {
if (this.lead && this.lead.snapshot) {
this.lead.snapshot = undefined;
}
}
}
export { NodeStack };

View File

@@ -0,0 +1,41 @@
import { px } from '../../value/types/numbers/units.mjs';
function pixelsToPercent(pixels, axis) {
if (axis.max === axis.min)
return 0;
return (pixels / (axis.max - axis.min)) * 100;
}
/**
* We always correct borderRadius as a percentage rather than pixels to reduce paints.
* For example, if you are projecting a box that is 100px wide with a 10px borderRadius
* into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
* borderRadius in both states. If we animate between the two in pixels that will trigger
* a paint each time. If we animate between the two in percentage we'll avoid a paint.
*/
const correctBorderRadius = {
correct: (latest, node) => {
if (!node.target)
return latest;
/**
* If latest is a string, if it's a percentage we can return immediately as it's
* going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
*/
if (typeof latest === "string") {
if (px.test(latest)) {
latest = parseFloat(latest);
}
else {
return latest;
}
}
/**
* If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
* pixel value as a percentage of each axis
*/
const x = pixelsToPercent(latest, node.target.x);
const y = pixelsToPercent(latest, node.target.y);
return `${x}% ${y}%`;
},
};
export { correctBorderRadius, pixelsToPercent };

View File

@@ -0,0 +1,35 @@
import { mix } from '../../utils/mix.mjs';
import { complex } from '../../value/types/complex/index.mjs';
const correctBoxShadow = {
correct: (latest, { treeScale, projectionDelta }) => {
const original = latest;
const shadow = complex.parse(latest);
// TODO: Doesn't support multiple shadows
if (shadow.length > 5)
return original;
const template = complex.createTransformer(latest);
const offset = typeof shadow[0] !== "number" ? 1 : 0;
// Calculate the overall context scale
const xScale = projectionDelta.x.scale * treeScale.x;
const yScale = projectionDelta.y.scale * treeScale.y;
shadow[0 + offset] /= xScale;
shadow[1 + offset] /= yScale;
/**
* Ideally we'd correct x and y scales individually, but because blur and
* spread apply to both we have to take a scale average and apply that instead.
* We could potentially improve the outcome of this by incorporating the ratio between
* the two scales.
*/
const averageScale = mix(xScale, yScale, 0.5);
// Blur
if (typeof shadow[2 + offset] === "number")
shadow[2 + offset] /= averageScale;
// Spread
if (typeof shadow[3 + offset] === "number")
shadow[3 + offset] /= averageScale;
return template(shadow);
},
};
export { correctBoxShadow };

View File

@@ -0,0 +1,6 @@
const scaleCorrectors = {};
function addScaleCorrector(correctors) {
Object.assign(scaleCorrectors, correctors);
}
export { addScaleCorrector, scaleCorrectors };

View File

@@ -0,0 +1,42 @@
function buildProjectionTransform(delta, treeScale, latestTransform) {
let transform = "";
/**
* The translations we use to calculate are always relative to the viewport coordinate space.
* But when we apply scales, we also scale the coordinate space of an element and its children.
* For instance if we have a treeScale (the culmination of all parent scales) of 0.5 and we need
* to move an element 100 pixels, we actually need to move it 200 in within that scaled space.
*/
const xTranslate = delta.x.translate / treeScale.x;
const yTranslate = delta.y.translate / treeScale.y;
if (xTranslate || yTranslate) {
transform = `translate3d(${xTranslate}px, ${yTranslate}px, 0) `;
}
/**
* Apply scale correction for the tree transform.
* This will apply scale to the screen-orientated axes.
*/
if (treeScale.x !== 1 || treeScale.y !== 1) {
transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
}
if (latestTransform) {
const { rotate, rotateX, rotateY } = latestTransform;
if (rotate)
transform += `rotate(${rotate}deg) `;
if (rotateX)
transform += `rotateX(${rotateX}deg) `;
if (rotateY)
transform += `rotateY(${rotateY}deg) `;
}
/**
* Apply scale to match the size of the element to the size we want it.
* This will apply scale to the element-orientated axes.
*/
const elementScaleX = delta.x.scale * treeScale.x;
const elementScaleY = delta.y.scale * treeScale.y;
if (elementScaleX !== 1 || elementScaleY !== 1) {
transform += `scale(${elementScaleX}, ${elementScaleY})`;
}
return transform || "none";
}
export { buildProjectionTransform };

View File

@@ -0,0 +1,14 @@
import { rootProjectionNode } from './node/HTMLProjectionNode.mjs';
function useInstantLayoutTransition() {
return startTransition;
}
function startTransition(callback) {
if (!rootProjectionNode.current)
return;
rootProjectionNode.current.isUpdating = false;
rootProjectionNode.current.blockUpdate();
callback && callback();
}
export { useInstantLayoutTransition };

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import { rootProjectionNode } from './node/HTMLProjectionNode.mjs';
function useResetProjection() {
const reset = React.useCallback(() => {
const root = rootProjectionNode.current;
if (!root)
return;
root.resetTree();
}, []);
return reset;
}
export { useResetProjection };

View File

@@ -0,0 +1,5 @@
function eachAxis(callback) {
return [callback("x"), callback("y")];
}
export { eachAxis };

View File

@@ -0,0 +1,24 @@
function isIdentityScale(scale) {
return scale === undefined || scale === 1;
}
function hasScale({ scale, scaleX, scaleY }) {
return (!isIdentityScale(scale) ||
!isIdentityScale(scaleX) ||
!isIdentityScale(scaleY));
}
function hasTransform(values) {
return (hasScale(values) ||
has2DTranslate(values) ||
values.z ||
values.rotate ||
values.rotateX ||
values.rotateY);
}
function has2DTranslate(values) {
return is2DTranslate(values.x) || is2DTranslate(values.y);
}
function is2DTranslate(value) {
return value && value !== "0%";
}
export { has2DTranslate, hasScale, hasTransform };

View File

@@ -0,0 +1,17 @@
import { convertBoundingBoxToBox, transformBoxPoints } from '../geometry/conversion.mjs';
import { translateAxis } from '../geometry/delta-apply.mjs';
function measureViewportBox(instance, transformPoint) {
return convertBoundingBoxToBox(transformBoxPoints(instance.getBoundingClientRect(), transformPoint));
}
function measurePageBox(element, rootProjectionNode, transformPagePoint) {
const viewportBox = measureViewportBox(element, transformPagePoint);
const { scroll } = rootProjectionNode;
if (scroll) {
translateAxis(viewportBox.x, scroll.offset.x);
translateAxis(viewportBox.y, scroll.offset.y);
}
return viewportBox;
}
export { measurePageBox, measureViewportBox };