Skip to content

Commit

Permalink
Improvements to Realtime Collaboration (Patching) (#331)
Browse files Browse the repository at this point in the history
  • Loading branch information
loreanvictor authored Dec 10, 2023
1 parent 1c2aa37 commit 9abbc29
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 26 deletions.
44 changes: 24 additions & 20 deletions src/main/components/store/model-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface ModelState {
// the boundary of the diagram is determined by some relationship.

export class ModelState {
static fromModel(compatModel: UMLModelCompat): PartialModelState {
static fromModel(compatModel: UMLModelCompat, repositionRoots = true): PartialModelState {
const model = backwardsCompatibleModel(compatModel);

const apollonElements = model.elements;
Expand Down Expand Up @@ -89,13 +89,15 @@ export class ModelState {
return relationship;
});

const roots = [...elements.filter((element) => !element.owner), ...relationships];
const bounds = computeBoundingBoxForElements(roots);
bounds.width = Math.ceil(bounds.width / 20) * 20;
bounds.height = Math.ceil(bounds.height / 20) * 20;
for (const element of roots) {
element.bounds.x -= bounds.x + bounds.width / 2;
element.bounds.y -= bounds.y + bounds.height / 2;
if (repositionRoots) {
const roots = [...elements.filter((element) => !element.owner), ...relationships];
const bounds = computeBoundingBoxForElements(roots);
bounds.width = Math.ceil(bounds.width / 20) * 20;
bounds.height = Math.ceil(bounds.height / 20) * 20;
for (const element of roots) {
element.bounds.x -= bounds.x + bounds.width / 2;
element.bounds.y -= bounds.y + bounds.height / 2;
}
}

// set diagram to keep diagram type
Expand Down Expand Up @@ -129,7 +131,7 @@ export class ModelState {
};
}

static toModel(state: ModelState): Apollon.UMLModel {
static toModel(state: ModelState, repositionRoots = true): Apollon.UMLModel {
const elements = Object.values(state.elements)
.map<UMLElement | null>((element) => UMLElementRepository.get(element))
.reduce<{ [id: string]: UMLElement }>((acc, val) => ({ ...acc, ...(val && { [val.id]: val }) }), {});
Expand Down Expand Up @@ -170,17 +172,19 @@ export class ModelState {
relationship.serialize(),
);

const roots = [...apollonElementsArray, ...apollonRelationships].filter((element) => !element.owner);
const bounds = computeBoundingBoxForElements(roots);
bounds.width = Math.ceil(bounds.width / 20) * 20;
bounds.height = Math.ceil(bounds.height / 20) * 20;
for (const element of apollonElementsArray) {
element.bounds.x -= bounds.x;
element.bounds.y -= bounds.y;
}
for (const element of apollonRelationships) {
element.bounds.x -= bounds.x;
element.bounds.y -= bounds.y;
if (repositionRoots) {
const roots = [...apollonElementsArray, ...apollonRelationships].filter((element) => !element.owner);
const bounds = computeBoundingBoxForElements(roots);
bounds.width = Math.ceil(bounds.width / 20) * 20;
bounds.height = Math.ceil(bounds.height / 20) * 20;
for (const element of apollonElementsArray) {
element.bounds.x -= bounds.x;
element.bounds.y -= bounds.y;
}
for (const element of apollonRelationships) {
element.bounds.x -= bounds.x;
element.bounds.y -= bounds.y;
}
}

const interactive: Apollon.Selection = {
Expand Down
4 changes: 2 additions & 2 deletions src/main/components/store/model-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const createReduxStore = (
const patchReducer =
patcher &&
createPatcherReducer<UMLModel, ModelState>(patcher, {
transform: (model) => ModelState.fromModel(model) as ModelState,
transform: (model) => ModelState.fromModel(model, false) as ModelState,
});

const reducer: Reducer<ModelState, Actions> = (state, action) => {
Expand All @@ -69,7 +69,7 @@ export const createReduxStore = (
? [
createPatcherMiddleware<UMLModel, Actions, ModelState>(patcher, {
select: (action) => isDiscreteAction(action) || isSelectionAction(action),
transform: ModelState.toModel,
transform: (state) => ModelState.toModel(state, false),
}),
]
: []),
Expand Down
4 changes: 2 additions & 2 deletions src/main/components/uml-element/resizable/resizable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const resizable =
}

this.setState({ resizing: true, offset: offset.scale(1 / this.props.zoomFactor) });
this.props.start();
this.props.start(this.props.id);
const element = event.currentTarget;
element.setPointerCapture(event.pointerId);
element.addEventListener('pointermove', this.onPointerMove);
Expand Down Expand Up @@ -199,7 +199,7 @@ export const resizable =
element.releasePointerCapture(event.pointerId);
element.removeEventListener('pointermove', this.onPointerMove);
this.setState(initialState);
this.props.end();
this.props.end(this.props.id);
event.stopPropagation();
};
}
Expand Down
41 changes: 40 additions & 1 deletion src/main/services/patcher/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,44 @@ import { Patch } from './patcher-types';
* in the form of a [JSON patch](http://jsonpatch.com/)
*/
export function compare<T>(a: T, b: T): Patch {
return comparePatches(a as any, b as any).filter((op) => !op.path.startsWith('/size'));
const patch = comparePatches(a as any, b as any).filter((op) => !op.path.startsWith('/size'));

const relationshipIdsWithAffectedPaths: string[] = [];

patch.forEach((op) => {
const match = /\/relationships\/(?<id>[\w-]+)\/path/g.exec(op.path);
if (match?.groups?.id && !relationshipIdsWithAffectedPaths.includes(match.groups.id)) {
relationshipIdsWithAffectedPaths.push(match.groups.id);
}
});

const cleanedPatch = patch.filter((op) => {
const match = /\/relationships\/(?<id>[\w-]+)\//g.exec(op.path);

return !match?.groups?.id || !relationshipIdsWithAffectedPaths.includes(match.groups.id);
});

relationshipIdsWithAffectedPaths.forEach((id) => {
const brel = (b as any).relationships[id];

cleanedPatch.push({
op: 'replace',
path: `/relationships/${id}/isManuallyLayouted`,
value: brel.isManuallyLayouted,
});

cleanedPatch.push({
op: 'replace',
path: `/relationships/${id}/path`,
value: brel.path,
});

cleanedPatch.push({
op: 'replace',
path: `/relationships/${id}/bounds`,
value: brel.bounds,
});
});

return cleanedPatch;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Actions } from '../../actions';
import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type';

const sameSelector = (a: UMLElementSelectorType, b: UMLElementSelectorType) => {
return a.name === b.name && a.color === b.color;
return a && b && a.name === b.name && a.color === b.color;
};

export const RemoteSelectionReducer: Reducer<RemoteSelectionState, Actions> = (state = {}, action) => {
Expand Down
67 changes: 67 additions & 0 deletions src/tests/unit/components/store/model-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ModelState } from '../../../../main/components/store/model-state';
import { UMLRelationship } from '../../../../main/services/uml-relationship/uml-relationship';
import { computeBoundingBoxForElements } from '../../../../main/utils/geometry/boundary';
import diagram from '../../test-resources/class-diagram.json';

describe('model state.', () => {
it('centers a model when imported.', () => {
const state = ModelState.fromModel(diagram as any);
const bounds = computeBoundingBoxForElements(Object.values(state.elements as any));

expect(Math.abs(bounds.x + bounds.width / 2)).toBeLessThan(40);
expect(Math.abs(bounds.y + bounds.height / 2)).toBeLessThan(40);
});

it('does not center the model when imported, given the option.', () => {
const state = ModelState.fromModel(diagram as any, false);
const bounds = computeBoundingBoxForElements(Object.values(state.elements as any));

expect(bounds.x).toBe(0);
});

it('puts model on 0,0 when exporting.', () => {
const state = ModelState.fromModel(diagram as any);
expect(state.elements).toBeDefined();
state.elements &&
Object.values(state.elements).forEach((element) => {
if (UMLRelationship.isUMLRelationship(element)) {
element.path.forEach((point) => {
point.x += 100;
point.y += 100;
});
}

element.bounds.x += 100;
element.bounds.y += 100;
});

const exp = ModelState.toModel(state as any);
const bounds = computeBoundingBoxForElements([...Object.values(exp.elements), ...Object.values(exp.relationships)]);

expect(bounds.x).toBe(0);
expect(bounds.y).toBe(0);
});

it('deos not put model on 0,0 when exporting, given the option.', () => {
const state = ModelState.fromModel(diagram as any);
expect(state.elements).toBeDefined();
state.elements &&
Object.values(state.elements).forEach((element) => {
if (UMLRelationship.isUMLRelationship(element)) {
element.path.forEach((point) => {
point.x += 100;
point.y += 100;
});
}

element.bounds.x += 100;
element.bounds.y += 100;
});

const exp = ModelState.toModel(state as any, false);
const bounds = computeBoundingBoxForElements([...Object.values(exp.elements), ...Object.values(exp.relationships)]);

expect(bounds.x).not.toBe(0);
expect(bounds.y).not.toBe(0);
});
});
38 changes: 38 additions & 0 deletions src/tests/unit/services/patcher/compare.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { compare } from '../../../../main/services/patcher/compare';

describe('compare models to extract patches.', () => {
it('groups up relationship paths.', () => {
const a = {
relationships: {
x: {
path: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
],
isManuallyLayouted: false,
bounds: { x: 0, y: 0, width: 1, height: 1 },
},
},
};

const b = {
relationships: {
x: {
path: [
{ x: 1, y: 0 },
{ x: 0, y: 2 },
],
isManuallyLayouted: true,
bounds: { x: 0, y: 0, width: 1, height: 2 },
},
},
};

const patches = compare(a, b);

expect(patches.length).toBe(3);
expect(patches[0].path).toBe('/relationships/x/isManuallyLayouted');
expect(patches[1].path).toBe('/relationships/x/path');
expect(patches[2].path).toBe('/relationships/x/bounds');
});
});

0 comments on commit 9abbc29

Please sign in to comment.