Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement client-only layout. #195

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/klighd-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
"dependencies": {
"@kieler/klighd-interactive": "^0.5.0",
"@types/file-saver": "^2.0.7",
"elkjs": "^0.8.2",
"feather-icons": "^4.29.1",
"file-saver": "^2.0.5",
"inversify": "^6.0.2",
"snabbdom": "^3.5.1",
"sprotty": "^1.3.0",
"sprotty-elk": "^1.3.0",
"sprotty-protocol": "^1.3.0"
},
"devDependencies": {
Expand Down
21 changes: 19 additions & 2 deletions packages/klighd-core/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
*/

import { interactiveModule } from '@kieler/klighd-interactive/lib/interactive-module'
import ElkConstructor from 'elkjs/lib/elk.bundled'
import { Container, ContainerModule, interfaces } from 'inversify'
import {
boundsModule,
configureActionHandler,
configureModelElement,
ConsoleLogger,
Expand All @@ -44,6 +46,7 @@ import {
viewportModule,
ViewRegistry,
} from 'sprotty'
import { ElkFactory, ElkLayoutEngine, elkLayoutModule, ILayoutConfigurator } from 'sprotty-elk'
import actionModule from './actions/actions-module'
// import bookmarkModule from './bookmarks/bookmark-module';
import { DISymbol } from './di.symbols'
Expand All @@ -53,6 +56,7 @@ import { KlighdHoverMouseListener } from './hover/hover'
import { PopupModelProvider } from './hover/popup-provider'
import { KlighdMouseTool } from './klighd-mouse-tool'
import { KlighdSvgExporter } from './klighd-svg-exporter'
import { KielerLayoutConfigurator } from './layout-config'
import { KlighdModelViewer } from './model-viewer'
import { ResetPreferencesAction, SetPreferencesAction } from './options/actions'
import { optionsModule } from './options/options-module'
Expand All @@ -74,6 +78,16 @@ const kGraphDiagramModule = new ContainerModule(
bind(TYPES.ModelSource).to(KlighdDiagramServer).inSingletonScope()
rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope()
rebind(TYPES.LogLevel).toConstantValue(LogLevel.warn)

// required binding for elkjs to work
bind(TYPES.IModelLayoutEngine).toService(ElkLayoutEngine)

// Our own layout configurator that just copies the element's poperties as the layout options.
bind(KielerLayoutConfigurator).toSelf().inSingletonScope()
rebind(ILayoutConfigurator).to(KielerLayoutConfigurator).inSingletonScope()
const elkFactory: ElkFactory = () => new ElkConstructor({ algorithms: ['layered'] }) // See elkjs documentation
bind(ElkFactory).toConstantValue(elkFactory)

rebind(TYPES.CommandStackOptions).toConstantValue({
// Override the default animation speed to be 500 ms, as the default value is too quick.
defaultDuration: 500,
Expand Down Expand Up @@ -126,6 +140,8 @@ export default function createContainer(widgetId: string): Container {
const container = new Container()
container.load(
defaultModule,
boundsModule,
elkLayoutModule,
selectModule,
interactiveModule,
viewportModule,
Expand All @@ -144,8 +160,9 @@ export default function createContainer(widgetId: string): Container {
)
// FIXME: bookmarkModule is currently broken due to wrong usage of Sprotty commands. action handling needs to be reimplemented for this to work.
overrideViewerOptions(container, {
needsClientLayout: false,
needsServerLayout: true,
// TODO: need some configuration switch to enable/disable client layout
needsClientLayout: true, // client layout = micro layout (Sprotty/Sprotty+KLighD)
needsServerLayout: false, // server layout = macro layout (ELK/elkjs). false here to not forward it to the Java server (the model source), but keep and handle it directly on the diagram server proxy manually
baseDiv: widgetId,
hiddenDiv: `${widgetId}_hidden`,
// TODO: We should be able to completely deactivate Sprotty's zoom limits to not limit top down layout.
Expand Down
36 changes: 36 additions & 0 deletions packages/klighd-core/src/diagram-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ import {
import {
Action,
ActionMessage,
ComputedBoundsAction,
BringToFrontAction,
findElement,
generateRequestId,
IModelLayoutEngine,
RequestModelAction,
GetViewportAction,
RequestPopupModelAction,
SelectAction,
SetModelAction,
SetPopupModelAction,
SModelRoot,
UpdateModelAction,
ViewportResult,
} from 'sprotty-protocol'
Expand Down Expand Up @@ -106,6 +111,8 @@ export class KlighdDiagramServer extends DiagramServerProxy {

@inject(DISymbol.BookmarkRegistry) @optional() private bookmarkRegistry: BookmarkRegistry

@inject(TYPES.IModelLayoutEngine) @optional() protected layoutEngine?: IModelLayoutEngine

constructor(@inject(ServiceTypes.Connection) connection: Connection) {
super()
this._connection = connection
Expand Down Expand Up @@ -301,6 +308,35 @@ export class KlighdDiagramServer extends DiagramServerProxy {
return false
}

// Behavior adapted from the super class and modified to the behavior of the DiagramServer to allow this proxy to the Java diagram server to still be able to perform the layout locally.
handleComputedBounds(action: ComputedBoundsAction): boolean {
if (this.viewerOptions.needsServerLayout) {
return false
}
const root = this.currentRoot
this.computedBoundsApplicator.apply(root, action)
this.doSubmitModel(root, root.type === this.lastSubmittedModelType, action)
return false
}

// Behavior taken from the DiagramServer to allow this proxy to the Java diagram server to still be able to perform the layout locally.
private async doSubmitModel(newRoot: SModelRoot, update: boolean, cause?: Action): Promise<void> {
if (this.layoutEngine) {
newRoot = await this.layoutEngine.layout(newRoot)
}
const modelType = newRoot.type
if (cause && cause.kind === RequestModelAction.KIND) {
const { requestId } = cause as RequestModelAction
const response = SetModelAction.create(newRoot, requestId)
this.actionDispatcher.dispatch(response)
} else if (update && modelType === this.lastSubmittedModelType) {
this.actionDispatcher.dispatch(UpdateModelAction.create(newRoot))
} else {
this.actionDispatcher.dispatch(SetModelAction.create(newRoot))
}
this.lastSubmittedModelType = modelType
}

handleRequestDiagramPiece(action: RequestDiagramPieceAction): void {
this.forwardToServer(action)
}
Expand Down
41 changes: 41 additions & 0 deletions packages/klighd-core/src/layout-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* KIELER - Kiel Integrated Environment for Layout Eclipse RichClient
*
* http://rtsys.informatik.uni-kiel.de/kieler
*
* Copyright 2024 by
* + Kiel University
* + Department of Computer Science
* + Real-Time and Embedded Systems Group
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*/

import { LayoutOptions } from 'elkjs'
import { DefaultLayoutConfigurator } from 'sprotty-elk'
import { SModelElement, SModelIndex } from 'sprotty-protocol'

/**
* This layout configurator copies all layout options from the KGraph element's properties.
*/
export class KielerLayoutConfigurator extends DefaultLayoutConfigurator {
override apply(element: SModelElement, _index: SModelIndex): LayoutOptions | undefined {
// Only apply to elements with properties.
if ((element as any).properties === undefined) {
return undefined
}
const properties = (element as any).properties as Record<string, unknown>

// map properties to layout options and stringify values
const layoutOptions: LayoutOptions = {}
Object.entries(properties).forEach(([key, value]) => {
layoutOptions[key] = JSON.stringify(value)
})

return layoutOptions
}
}
5 changes: 4 additions & 1 deletion packages/klighd-core/src/skgraph-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { KEdge, KGraphData, KNode, SKGraphElement } from '@kieler/klighd-interactive/lib/constraint-classes'
import {
boundsFeature,
layoutContainerFeature,
moveFeature,
popupFeature,
RectangularPort,
Expand All @@ -40,6 +41,8 @@ export class SKNode extends KNode {
hasFeature(feature: symbol): boolean {
return (
feature === selectFeature ||
feature === boundsFeature ||
feature === layoutContainerFeature ||
(feature === moveFeature &&
(this.parent as SKNode).properties &&
((this.parent as SKNode).properties['org.eclipse.elk.interactiveLayout'] as boolean)) ||
Expand All @@ -61,7 +64,7 @@ export class SKPort extends RectangularPort implements SKGraphElement {
areNonChildAreaChildrenRendered = false

hasFeature(feature: symbol): boolean {
return feature === selectFeature || feature === popupFeature
return feature === selectFeature || feature === boundsFeature || feature === popupFeature
}

properties: Record<string, unknown>
Expand Down
51 changes: 34 additions & 17 deletions packages/klighd-core/src/views-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* http://rtsys.informatik.uni-kiel.de/kieler
*
* Copyright 2019-2023 by
* Copyright 2019-2024 by
* + Kiel University
* + Department of Computer Science
* + Real-Time and Embedded Systems Group
Expand Down Expand Up @@ -420,14 +420,14 @@ export function findBoundsAndTransformationData(
}
}
}
// Error check: If there are no bounds or decoration, at least try to fall back to possible size and position attributes in the parent element.
// Error check: If there are no bounds or decoration, at least try to fall back to possible position attributes in the parent element.
// If the parent element has no bounds either, the object can not be rendered.
if (decoration === undefined && bounds === undefined && 'size' in parent && 'position' in parent) {
if (decoration === undefined && bounds === undefined && 'bounds' in parent) {
bounds = {
x: (parent as any).position.x,
y: (parent as any).position.y,
width: (parent as any).size.width,
height: (parent as any).size.height,
x: 0,
y: 0,
width: (parent as any).bounds.width,
height: (parent as any).bounds.height,
}
} else if (decoration === undefined && bounds === undefined) {
return undefined
Expand Down Expand Up @@ -573,16 +573,33 @@ export function findTextBoundsAndTransformationData(
bounds.height = decoration.bounds.height
}
}
// Error check: If there are no bounds or decoration, at least try to fall back to possible size and position attributes in the parent element.
// If the parent element has no bounds either, the object can not be rendered.
if (decoration === undefined && bounds.x === undefined && 'size' in parent && 'position' in parent) {
bounds.x = (parent as any).position.x
bounds.y = (parent as any).position.y
bounds.width = (parent as any).size.width
bounds.height = (parent as any).size.height
} else if (decoration === undefined && bounds.x === undefined) {
return undefined
}
}
// Error check: If there are no bounds or decoration, at least try to fall back to possible size attributes in the parent element.
// If the parent element has no bounds either, the object can not be rendered.
if (decoration === undefined && bounds.x === undefined && 'bounds' in parent) {
const parentBounds = {
x: 0,
y: 0,
width: (parent as any).bounds.width,
height: (parent as any).bounds.height,
}

bounds.x = calculateX(
parentBounds.x,
parentBounds.width,
styles.kHorizontalAlignment ?? DEFAULT_K_HORIZONTAL_ALIGNMENT,
parentBounds.width
)
bounds.y = calculateY(
parentBounds.y,
parentBounds.height,
styles.kVerticalAlignment ?? DEFAULT_K_VERTICAL_ALIGNMENT,
lines
)
bounds.width = parent.bounds.width
bounds.height = parent.bounds.height
} else if (decoration === undefined && bounds.x === undefined) {
return undefined
}

// If still no bounds are found, set all by default to 0.
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3827,6 +3827,11 @@ electron-to-chromium@^1.4.535:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.585.tgz#7b3cb6846bb5cc10a8d5904c351a9b8aaa76ea90"
integrity sha512-B4yBlX0azdA3rVMxpYwLQfDpdwOgcnLCkpvSOd68iFmeedo+WYjaBJS3/W58LVD8CB2nf+o7C4K9xz1l09RkWg==

elkjs@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==

elliptic@^6.5.3:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
Expand Down Expand Up @@ -8685,6 +8690,16 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=

sprotty-elk@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sprotty-elk/-/sprotty-elk-1.3.0.tgz#2cffc8ff280f899ca791c011d67ccb4a0cd50e5e"
integrity sha512-P8AZxWj99RziRK+mA6D/J99PvpT4z+gAVbfvaHJHhYXW+1jDP5vJSNzhW305BykfFugN+2V0HVSLZKana+aUHg==
dependencies:
elkjs "^0.8.2"
sprotty-protocol "^1.3.0"
optionalDependencies:
inversify "~6.0.2"

sprotty-protocol@^1.0.0, sprotty-protocol@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sprotty-protocol/-/sprotty-protocol-1.3.0.tgz#78a6a69cc5eb8b94b352882a83d0f339fd828a0d"
Expand Down
Loading