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

Add annotation #33

Merged
merged 1 commit into from
Dec 15, 2022
Merged
Changes from all commits
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
441 changes: 0 additions & 441 deletions examples/Untitled.ipynb

This file was deleted.

Binary file modified examples/box2.FCStd
Binary file not shown.
9 changes: 7 additions & 2 deletions jupytercad/fcstd_ydoc.py
Original file line number Diff line number Diff line change
@@ -10,28 +10,32 @@ def __init__(self, *args, **kwargs):
self._ysource = self._ydoc.get_text('source')
self._yobjects = self._ydoc.get_array('objects')
self._yoptions = self._ydoc.get_map('options')
self._ymeta = self._ydoc.get_map('metadata')
self._virtual_file = FCStd()

@property
def source(self):
fc_objects = self._yobjects.to_json()
options = self._yoptions.to_json()
self._virtual_file.save(fc_objects, options)
meta = self._ymeta.to_json()
self._virtual_file.save(fc_objects, options, meta)
return self._virtual_file.sources

@source.setter
def source(self, value):
virtual_file = self._virtual_file
virtual_file.load(value)
newObj = []

for obj in virtual_file.objects:
newObj.append(Y.YMap(obj))
with self._ydoc.begin_transaction() as t:
length = len(self._yobjects)
self._yobjects.delete_range(t, 0, length)

self._yobjects.extend(t, newObj)

self._yoptions.update(t, virtual_file.options.items())
self._ymeta.update(t, virtual_file.metadata.items())

def observe(self, callback):
self.unobserve()
@@ -41,3 +45,4 @@ def observe(self, callback):
callback
)
self._subscriptions[self._yoptions] = self._yoptions.observe(callback)
self._subscriptions[self._ymeta] = self._ymeta.observe_deep(callback)
10 changes: 9 additions & 1 deletion jupytercad/freecad/loader.py
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ def __init__(self) -> None:
self._sources = ''
self._objects = []
self._options = {}
self._metadata = {}
self._id = None
self._visible = True
self._prop_handlers: Dict[str, BaseProp] = {}
@@ -37,6 +38,10 @@ def sources(self):
def objects(self):
return self._objects

@property
def metadata(self):
return self._metadata

@property
def options(self):
return self._options
@@ -52,9 +57,10 @@ def load(self, base64_content: str) -> None:
fc_file = fc.app.openDocument(tmp.name)
for obj in fc_file.Objects:
self._objects.append(self._fc_to_jcad_obj(obj))
self._metadata = fc_file.Meta
os.remove(tmp.name)

def save(self, objects: List, options: Dict) -> None:
def save(self, objects: List, options: Dict, metadata: Dict) -> None:
try:

if not fc or len(self._sources) == 0:
@@ -66,6 +72,8 @@ def save(self, objects: List, options: Dict) -> None:
file_content = base64.b64decode(self._sources)
tmp.write(file_content)
fc_file = fc.app.openDocument(tmp.name)
fc_file.Meta = metadata

new_objs = dict([(o['name'], o) for o in objects])
current_objs = dict([(o.Name, o) for o in fc_file.Objects])
to_remove = [x for x in current_objs if x not in new_objs]
1 change: 1 addition & 0 deletions jupytercad/freecad/props/__init__.py
Original file line number Diff line number Diff line change
@@ -5,3 +5,4 @@
from .property_link import *
from .property_link_list import *
from .property_partshape import *
from .property_map import *
17 changes: 17 additions & 0 deletions jupytercad/freecad/props/property_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Any, Dict

from .base_prop import BaseProp


class App_PropertyMap(BaseProp):
@staticmethod
def name() -> str:
return 'App::PropertyMap'

@staticmethod
def fc_to_jcad(prop_value: Any, jcad_file=None, fc_file=None) -> Any:
return prop_value

@staticmethod
def jcad_to_fc(prop_value: Any, jcad_file=None, fc_file=None) -> Any:
return prop_value
8 changes: 7 additions & 1 deletion jupytercad/jcad_ydoc.py
Original file line number Diff line number Diff line change
@@ -9,12 +9,16 @@ def __init__(self, *args, **kwargs):
self._ysource = self._ydoc.get_text('source')
self._yobjects = self._ydoc.get_array('objects')
self._yoptions = self._ydoc.get_map('options')
self._ymeta = self._ydoc.get_map('metadata')

@property
def source(self):
objects = self._yobjects.to_json()
options = self._yoptions.to_json()
return json.dumps(dict(objects=objects, options=options), indent=2)
meta = self._ymeta.to_json()
return json.dumps(
dict(objects=objects, options=options, metadata=meta), indent=2
)

@source.setter
def source(self, value):
@@ -28,6 +32,7 @@ def source(self, value):

self._yobjects.extend(t, newObj)
self._yoptions.update(t, valueDict['options'].items())
self._ymeta.update(t, valueDict['metadata'].items())

def observe(self, callback):
self.unobserve()
@@ -37,3 +42,4 @@ def observe(self, callback):
callback
)
self._subscriptions[self._yoptions] = self._yoptions.observe(callback)
self._subscriptions[self._ymeta] = self._ymeta.observe(callback)
42 changes: 42 additions & 0 deletions src/annotation/message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { User } from '@jupyterlab/services';
import * as React from 'react';

interface IProps {
message: string;
index: number;
user?: User.IIdentity;
}

export const Message = (props: IProps): JSX.Element => {
const { index, message, user } = props;
const color = user?.color ?? 'black';
const author = user?.display_name ?? '';
const initials = user?.initials ?? '';
return (
<div
className="jcad-Annotation-Message"
style={{
flexFlow: index % 2 === 0 ? 'row' : 'row-reverse'
}}
>
<div
className="jcad-Annotation-User-Icon"
style={{
backgroundColor: color
}}
title={author}
>
<span style={{ width: 24, textAlign: 'center' }}>{initials}</span>
</div>
<div
style={{
background: '#3b3a3a',
borderRadius: 20,
flexGrow: 1
}}
>
<p style={{ padding: 7, margin: 0 }}>{message}</p>
</div>
</div>
);
};
80 changes: 80 additions & 0 deletions src/annotation/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { User } from '@jupyterlab/services';
import { ISignal, Signal } from '@lumino/signaling';

import { IDict, IJupyterCadDoc } from '../types';

export interface IAnnotationContent {
user?: User.IIdentity;
value: string;
}

export interface IAnnotation {
label: string;
position: [number, number, number];
contents: IAnnotationContent[];
}
export class AnnotationModel {
constructor(options: AnnotationModel.IOptions) {
this._getCoordinate = options.getCoordinate;
this._sharedModel = options.sharedModel;
const state = this._sharedModel.awareness.getLocalState();
this._user = state?.user;
}

get updateSignal(): ISignal<this, null> {
return this._updateSignal;
}

update(): void {
this._updateSignal.emit(null);
}
getAnnotation(id: string): IAnnotation | undefined {
const rawData = this._sharedModel.metadata.get(id);
if (rawData) {
return JSON.parse(rawData) as IAnnotation;
}
}

addAnnotation(key: string, value: IDict): void {
this._sharedModel.setMetadata(key, JSON.stringify(value));
}

removeAnnotation(key): void {
this._sharedModel.removeMetadata(key);
}

getCoordinate(id: string): [number, number] | undefined {
const annotation = this.getAnnotation(id);
if (annotation?.position) {
return this._getCoordinate(annotation.position);
}
}

addContent(id: string, value: string): void {
const newContent: IAnnotationContent = {
value,
user: this._user
};
const currentAnnotation = this.getAnnotation(id);
if (currentAnnotation) {
const newAnnotation: IAnnotation = {
...currentAnnotation,
contents: [...currentAnnotation.contents, newContent]
};

this._sharedModel.setMetadata(id, JSON.stringify(newAnnotation));
}
}

private _sharedModel: IJupyterCadDoc;
private _getCoordinate: (input: [number, number, number]) => [number, number];
private _updateSignal = new Signal<this, null>(this);
private _user?: User.IIdentity;
}

namespace AnnotationModel {
export interface IOptions {
sharedModel: IJupyterCadDoc;
getCoordinate: (input: [number, number, number]) => [number, number];
}
}
77 changes: 77 additions & 0 deletions src/annotation/view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { caretRightIcon } from '@jupyterlab/ui-components';
import * as React from 'react';

import { Message } from './message';
import { AnnotationModel } from './model';

interface IProps {
itemId: string;
model: AnnotationModel;
open: boolean;
}

export const Annotation = (props: IProps): JSX.Element => {
const { itemId, model } = props;
const annotation = model.getAnnotation(itemId);
const contents = React.useMemo(
() => annotation?.contents ?? [],
[annotation]
);

const [open, setOpen] = React.useState(props.open);
const [messageContent, setMessageContent] = React.useState<string>('');

if (!annotation) {
return <div></div>;
}
const submitMessage = () => {
model.addContent(itemId, messageContent);
setMessageContent('');
};
return (
<div>
<div
className="jcad-Annotation-Handler"
onClick={() => setOpen(!open)}
></div>
<div
className="jcad-Annotation"
style={{ visibility: open ? 'visible' : 'hidden' }}
>
<div
className="jcad-Annotation-CloseHandler"
onClick={() => {
model.removeAnnotation(itemId);
}}
/>
<div style={{ paddingBottom: 10, maxHeight: 400, overflow: 'auto' }}>
{contents.map((content, index) => {
return (
<Message
user={content.user}
message={content.value}
index={index}
/>
);
})}
</div>
<div className="jcad-Annotation-Message">
<textarea
rows={3}
placeholder={'Ctrl+Enter to submit'}
value={messageContent}
onChange={e => setMessageContent(e.currentTarget.value)}
onKeyDown={e => {
if (e.ctrlKey && e.key === 'Enter') {
submitMessage();
}
}}
/>
<div onClick={submitMessage}>
<caretRightIcon.react className="jcad-Annotation-Submit" />
</div>
</div>
</div>
</div>
);
};
Loading