Skip to content

Commit

Permalink
Merge branch 'develop' into feature/zoom-aware-dialog-positions
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiaslehnertum authored Mar 24, 2024
2 parents 0edaf28 + fe75ff1 commit 454f926
Show file tree
Hide file tree
Showing 27 changed files with 504 additions and 181 deletions.
10 changes: 5 additions & 5 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Apollon
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build

Expand All @@ -17,4 +17,4 @@ help:
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
71 changes: 35 additions & 36 deletions docs/make.bat
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
set SPHINXPROJ=Apollon

if "%1" == "" goto help

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%

:end
popd
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)

if "%1" == "" goto help

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%

:end
popd
7 changes: 3 additions & 4 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
Sphinx==3.0.3
sphinx-rtd-theme==0.4.3
sphinx-copybutton==0.3.0
Jinja2<3.1
Sphinx==6.2.1
sphinx-rtd-theme==1.2.2
Jinja2==3.1.3
30 changes: 2 additions & 28 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,9 @@
# -- Project information -----------------------------------------------------

project = 'Apollon'
copyright = '2023, Stephan Krusche'
copyright = '2024, Stephan Krusche'
author = 'Stephan Krusche'

# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '3.3.0'


# -- General configuration ---------------------------------------------------

Expand Down Expand Up @@ -63,7 +58,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
Expand Down Expand Up @@ -129,14 +124,6 @@
# 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Apollon.tex', 'Apollon Documentation',
'Stephan Krusche', 'manual'),
]


# -- Options for manual page output ------------------------------------------

Expand All @@ -147,19 +134,6 @@
[author], 1)
]


# -- Options for Texinfo output ----------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Apollon', 'Apollon Documentation',
author, 'Apollon', 'One line description of project.',
'Miscellaneous'),
]


# -- Extension configuration -------------------------------------------------

# -- Options for intersphinx extension ---------------------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Welcome to Apollon's documentation!
user/getting-started
user/api
user/uml-model-helpers
user/realtime-collaboration

.. toctree::
:caption: Contributor Guide
Expand Down
28 changes: 26 additions & 2 deletions docs/source/user/api/apollon-editor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,35 @@ export declare class ApollonEditor {
unsubscribeFromDiscreteModelChange(subscriptionId: number): void;
/**
* Register callback which is executed when the model changes, receiving the changes to the model
* in [JSONPatch](http://jsonpatch.com/) format.
* in [JSONPatch](http://jsonpatch.com/) format. This callback is only executed for discrete changes to the model.
* Discrete changes are changes that should not be missed and are executed at the end of important user actions.
* @param callback function which is called when the model changes
* @return returns the subscription identifier which can be used to unsubscribe
* @returns the subscription identifier which can be used to unsubscribe
*/
subscribeToModelChangePatches(callback: (patch: Patch) => void): number;
/**
* Registers a callback which is executed when the model changes, receiving the changes to the model
* in [JSONPatch](http://jsonpatch.com/) format. This callback is executed for every change to the model, including
* discrete and continuous changes. Discrete changes are changes that should not be missed and are executed at
* the end of important user actions. Continuous changes are changes that are executed during user actions, and is
* ok to miss some of them. For example: moving of an element is a continuous change, while releasing the element
* is a discrete change.
* @param callback function which is called when the model changes
* @returns the subscription identifier which can be used to unsubscribe using `unsubscribeFromModelChangePatches()`.
*/
subscribeToAllModelChangePatches(callback: (patch: Patch) => void): number;
/**
* Registers a callback which is executed when the model changes, receiving only the continuous changes to the model.
* Continuous changes are changes that are executed during user actions, and is ok to miss some of them. For example:
* moving of an element is a continuous change, while releasing the element is a discrete change.
*
* **IMPORTANT**: If you want to keep proper track of the model, make sure that you subscribe to discrete changes
* as well, either via `subscribeToModelChangePatches()` or `subscribeToAllModelChangePatches()`.
*
* @param callback function which is called when the model changes
* @returns the subscription identifier which can be used to unsubscribe using `unsubscribeFromModelChangePatches()`.
*/
subscribeToModelContinuousChangePatches(callback: (patch: Patch) => void): number;
/**
* Remove model change subscription, so that the corresponding callback is no longer executed when the model is changed.
* @param subscriptionId subscription identifier
Expand Down
66 changes: 66 additions & 0 deletions docs/source/user/realtime-collaboration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.. _realtime-collaboration:

################
Realtime Collaboration
################

Apollon supports realtime collaboration by emitting patches when a model is changed, and importing
patches potentially emitted by other Apollon clients. Patches follow the `RFC 6902`_ standard (i.e. `JSON Patch`_),
so they can be applied to Apollon diagrams in any desired language.

.. code-block:: typescript
// This method subscribes to model change patches.
// The callback is called whenever a patch is emitted.
editor.subscribeToModelChangePatches(callback: (patch: Patch) => void): number;
// This method unsubscribes from model change patches.
// The subscriptionId is the return value of the subscribeToModelChangePatches method.
editor.unsubscribeFromModelChangePatches(subscriptionId: number): void;
// This method imports a patch. This can be used to
// apply patches emitted by other Apollon clients.
editor.importPatch(patch: Patch): void;
Apollon client takes care of detecting conflicts between clients and resolving them. There is no need for
users to manually implement any reconcilliation mechanism. The only requirements to ensure a convergent state
between all Apollon clients are as follows:

- Apply all patches on all clients in the same order,
- Apply all patches on all clients, including patches emitted by the same client.

This means, if client A emits patch P1 and client B emits patch P2, both clients must then apply P1 and P2 in the same order (using `importPatch()`). The order can be picked by the server, but it needs to be the same for all clients. This means client A should also receive P1, although it has emitted P1 itself. Similarly client B should receive P2, although it has emitted P2 itself.

Apollon clients sign the patches they emit and treat receiving their own patches as confirmation that the patch has been applied and ordered with patches from other clients. They also optimize based on this assumption, to recognize when they are ahead of the rest of the clients on some part of the state: when client A applies the effects of patch P1 locally, its state is ahead until other clients have also applied patch P1, so client A can safely ignore other effects on that same part of the state (as it will get overwritten by patch P1 anyway).

================
Displaying Remote Users
================

In realtime collaboration, it can be useful to display activities of other users active in the collaboration session within the diagram editor. Apollon provides methods to display other users' selections:

.. code-block:: typescript
// This method selects or deselects elements
// on part of a given remote user with given name and color.
// Provide the ids of the elements the remote user
// has selected/deselected.
editor.remoteSelect(
selectorName: string,
selectorColor: string,
select: string[],
deselect?: string[]
): void;
// This method clears the list of remote users displayed
// on the diagram editor, except allowed users.
// Use this in case some users disconnect from the collaboration session.
pruneRemoteSelectors(
allowedSelectors: {
name: string;
color: string;
}[]
): void;
.. _RFC 6902: https://tools.ietf.org/html/rfc6902
.. _JSON Patch: http://jsonpatch.com/
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ls1intum/apollon",
"version": "3.3.4",
"version": "3.3.9",
"description": "A UML diagram editor.",
"keywords": [],
"homepage": "https://github.com/ls1intum/apollon#readme",
Expand Down
4 changes: 1 addition & 3 deletions src/main/components/store/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ export function merge(oldState: ModelState, newState: ModelState): ModelState {
...oldState,
diagram: {
...oldState.diagram,
ownedElements: oldState.diagram.ownedElements.filter(
(id) => !!newState.elements[id] && !newState.elements[id].owner,
),
ownedElements: Object.keys(newState.elements).filter((id) => !newState.elements[id].owner),
ownedRelationships: oldState.diagram.ownedRelationships.filter((id) => !!newState.elements[id]),
},
elements: Object.keys(newState.elements).reduce((acc, id) => {
Expand Down
30 changes: 2 additions & 28 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, repositionRoots = true): PartialModelState {
static fromModel(compatModel: UMLModelCompat): PartialModelState {
const model = backwardsCompatibleModel(compatModel);

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

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
const diagram: UMLDiagram = new UMLDiagram();
diagram.type = model.type as UMLDiagramType;
Expand Down Expand Up @@ -131,7 +120,7 @@ export class ModelState {
};
}

static toModel(state: ModelState, repositionRoots = true): Apollon.UMLModel {
static toModel(state: ModelState): 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 @@ -172,21 +161,6 @@ export class ModelState {
relationship.serialize(),
);

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 = {
elements: arrayToInclusionMap(state.interactive.filter((id) => UMLElement.isUMLElement(state.elements[id]))),
relationships: arrayToInclusionMap(
Expand Down
5 changes: 3 additions & 2 deletions src/main/components/store/model-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export const createReduxStore = (
const patchReducer =
patcher &&
createPatcherReducer<UMLModel, ModelState>(patcher, {
transform: (model) => ModelState.fromModel(model, false) as ModelState,
transform: (model) => ModelState.fromModel(model) as ModelState,
transformInverse: (state) => ModelState.toModel(state),
merge,
});

Expand All @@ -73,7 +74,7 @@ export const createReduxStore = (
createPatcherMiddleware<UMLModel, Actions, ModelState>(patcher, {
selectDiscrete: (action) => isDiscreteAction(action) || isSelectionAction(action),
selectContinuous: (action) => isContinuousAction(action),
transform: (state) => ModelState.toModel(state, false),
transform: (state) => ModelState.toModel(state),
}),
]
: []),
Expand Down
Loading

0 comments on commit 454f926

Please sign in to comment.