/**
* @fileoverview
* Base types and classes used by chem editor.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /widgets/operation/kekule.operations.js
* requires /render/kekule.render.base.js
* requires /render/kekule.render.boundInfoRecorder.js
* requires /html/xbrowsers/kekule.x.js
* requires /widgets/kekule.widget.base.js
* requires /widgets/chem/kekule.chemWidget.chemObjDisplayers.js
* requires /widgets/chem/editor/kekule.chemEditor.extensions.js
* requires /widgets/chem/editor/kekule.chemEditor.editorUtils.js
* requires /widgets/chem/editor/kekule.chemEditor.configs.js
* requires /widgets/chem/editor/kekule.chemEditor.operations.js
*/
(function(){
"use strict";
var AU = Kekule.ArrayUtils;
var EU = Kekule.HtmlElementUtils;
var CNS = Kekule.Widget.HtmlClassNames;
var CCNS = Kekule.ChemWidget.HtmlClassNames;
/** @ignore */
Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, {
EDITOR: 'K-Chem-Editor',
EDITOR_CLIENT: 'K-Chem-Editor-Client',
EDITOR_UIEVENT_RECEIVER: 'K-Chem-Editor-UiEvent-Receiver',
EDITOR2D: 'K-Chem-Editor2D',
EDITOR3D: 'K-Chem-Editor3D'
});
/**
* Namespace for chem editor.
* @namespace
*/
Kekule.ChemWidget.Editor = {};
/**
* Alias to {@link Kekule.ChemWidget.Editor}.
* @namespace
*/
Kekule.Editor = Kekule.ChemWidget.Editor;
/**
* In editor, there exist three types of coord: one based on object system (inner coord),
* another one based on context of editor (outer coord, context coord),
* and the third based on screen.
* This enum is an alias of Kekule.Render.CoordSystem
* @class
*/
Kekule.Editor.CoordSys = Kekule.Render.CoordSystem;
/**
* Enumeration of regions in/out box.
* @enum
* @ignore
*/
Kekule.Editor.BoxRegion = {
OUTSIDE: 0,
CORNER_TL: 1,
CORNER_TR: 2,
CORNER_BL: 3,
CORNER_BR: 4,
EDGE_TOP: 11,
EDGE_LEFT: 12,
EDGE_BOTTOM: 13,
EDGE_RIGHT: 14,
INSIDE: 20
};
/**
* Enumeration of mode in selecting object in editor.
* @enum
* @ignore
*/
Kekule.Editor.SelectMode = {
/** Draw a box in editor when selecting, select all object inside a box. **/
RECT: 0,
/** Draw a curve in editor when selecting, select all object inside this curve polygon. **/
POLYGON: 1,
/** Draw a curve in editor when selecting, select all object intersecting this curve. **/
POLYLINE: 2,
/** Click on a child object to select the whole standalone ancestor. **/
ANCESTOR: 10
};
/**
* A base chem editor.
* @class
* @augments Kekule.ChemWidget.ChemObjDisplayer
* @param {Variant} parentOrElementOrDocument
* @param {Kekule.ChemObject} chemObj initially loaded chemObj.
* @param {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}.
* @param {Kekule.Editor.BaseEditorConfigs} editorConfigs Configuration of this editor.
*
* @property {Kekule.Editor.BaseEditorConfigs} editorConfigs Configuration of this editor.
* @property {Bool} enableCreateNewDoc Whether create new object in editor is allowed.
* @property {Bool} initOnNewDoc Whether create a new doc when editor instance is initialized.
* Note, the new doc will only be created when property enableCreateNewDoc is true.
* @property {Bool} enableOperHistory Whether undo/redo is enabled.
* @property {Kekule.OperationHistory} operHistory History of operations. Used to enable undo/redo function.
* @property {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}.
* @property {Kekule.ChemObject} chemObj The root object in editor.
* @property {Bool} enableOperContext If this property is set to true, object being modified will be drawn in a
* separate context to accelerate the interface refreshing.
* @property {Object} objContext Context to draw basic chem objects. Can be 2D or 3D context. Alias of property drawContext
* @property {Object} operContext Context to draw objects being operated. Can be 2D or 3D context.
* @property {Object} uiContext Context to draw UI marks. Usually this is a 2D context.
* @property {Object} objDrawBridge Bridge to draw chem objects. Alias of property drawBridge.
* @property {Object} uiDrawBridge Bridge to draw UI markers.
* @property {Int} selectMode Value from Kekule.Editor.SelectMode, set the mode of selecting operation in editor.
* @property {Array} selection An array of selected basic object.
* @property {Hash} zoomCenter The center coord (based on client element) when zooming editor.
* //@property {Bool} standardizeObjectsBeforeSaving Whether standardize molecules (and other possible objects) before saving them.
*/
/**
* Invoked when the an chem object is loaded into editor.
* event param of it has one fields: {obj: Object}
* @name Kekule.Editor.BaseEditor#load
* @event
*/
/**
* Invoked when the chem object inside editor is changed.
* event param of it has one fields: {obj: Object, propNames: Array}
* @name Kekule.Editor.BaseEditor#editObjChanged
* @event
*/
/**
* Invoked when multiple chem objects inside editor is changed.
* event param of it has one fields: {objChangeDetails}.
* @name Kekule.Editor.BaseEditor#editObjsChanged
* @event
*/
/**
* Invoked when chem objects inside editor is changed and the changes has been updated by editor.
* event param of it has one fields: {objChangeDetails}.
* Note: this event is not the same as editObjsChanged. When beginUpdateObj is called, editObjsChanged
* event still will be invoked but editObjsUpdated event will be suppressed.
* @name Kekule.Editor.BaseEditor#editObjsUpdated
* @event
*/
/**
* Invoked when the selected objects in editor has been changed.
* When beginUpdateObj is called, selectedObjsUpdated event will be suppressed.
* event param of it has one fields: {objs}.
* @name Kekule.Editor.BaseEditor#selectedObjsUpdated
* @event
*/
/**
* Invoked when the selection in editor has been changed.
* @name Kekule.Editor.BaseEditor#selectionChange
* @event
*/
/**
* Invoked when the operation history has modifications.
* @name Kekule.Editor.BaseEditor#operChange
* @event
*/
/**
* Invoked when the an operation is pushed into operation history.
* event param of it has one fields: {operation: Kekule.Operation}
* @name Kekule.Editor.BaseEditor#operPush
* @event
*/
/**
* Invoked when the an operation is popped from history.
* event param of it has one fields: {operation: Kekule.Operation}
* @name Kekule.Editor.BaseEditor#operPop
* @event
*/
/**
* Invoked when one operation is undone.
* event param of it has two fields: {operation: Kekule.Operation, currOperIndex: Int}
* @name Kekule.Editor.BaseEditor#operUndo
* @event
*/
/**
* Invoked when one operation is redone.
* event param of it has two fields: {operation: Kekule.Operation, currOperIndex: Int}
* @name Kekule.Editor.BaseEditor#operRedo
* @event
*/
/**
* Invoked when the operation history is cleared.
* event param of it has one field: {currOperIndex: Int}
* @name Kekule.Editor.BaseEditor#operHistoryClear
* @event
*/
Kekule.Editor.BaseEditor = Class.create(Kekule.ChemWidget.ChemObjDisplayer,
/** @lends Kekule.Editor.BaseEditor# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BaseEditor',
/** @private */
BINDABLE_TAG_NAMES: ['div', 'span'],
/** @private */
OBSERVING_GESTURES: ['rotate', 'rotatestart', 'rotatemove', 'rotateend', 'rotatecancel',
'pinch', 'pinchstart', 'pinchmove', 'pinchend', 'pinchcancel', 'pinchin', 'pinchout'],
/** @constructs */
initialize: function($super, parentOrElementOrDocument, chemObj, renderType, editorConfigs)
{
this._objSelectFlag = 0; // used internally
this._objectUpdateFlag = 0; // used internally
this._objectManipulateFlag = 0; // used internally
this._uiMarkerUpdateFlag = 0; // used internally
this._updatedObjectDetails = []; // used internally
this._operatingObjs = []; // used internally
this._operatingRenderers = []; // used internally
this._initialRenderTransformParams = null; // used internally, must init before $super
// as in $super, chemObj may be loaded and _initialRenderTransformParams will be set at that time
this._objChanged = false; // used internally, mark whether some changes has been made to chem object
this._lengthCaches = {}; // used internally, stores some value related to distance and length
this.setPropStoreFieldValue('enableCreateNewDoc', true);
this.setPropStoreFieldValue('enableOperHistory', true);
this.setPropStoreFieldValue('enableOperContext', true);
this.setPropStoreFieldValue('initOnNewDoc', true);
//this.setPropStoreFieldValue('initialZoom', 1.5);
//this.setPropStoreFieldValue('selectMode', Kekule.Editor.SelectMode.POLYGON); // debug
$super(parentOrElementOrDocument, chemObj, renderType);
//this.initEventHandlers();
if (!this.getChemObj() && this.getInitOnNewDoc() && this.getEnableCreateNewDoc())
this.newDoc();
this.setPropStoreFieldValue('editorConfigs', editorConfigs || this.createDefaultConfigs());
//this.setPropStoreFieldValue('uiMarkers', []);
this.setEnableGesture(true);
},
/** @private */
initProperties: function()
{
this.defineProp('editorConfigs', {'dataType': 'Kekule.Editor.BaseEditorConfigs', 'serializable': false,
'getter': function() { return this.getDisplayerConfigs(); },
'setter': function(value) { return this.setDisplayerConfigs(value); }
});
this.defineProp('defBondLength', {'dataType': DataType.FLOAT, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('defBondLength');
if (!result)
result = this.getEditorConfigs().getStructureConfigs().getDefBondLength();
return result;
}
});
this.defineProp('defBondScreenLength', {'dataType': DataType.FLOAT, 'serializable': false, 'setter': null,
'getter': function()
{
/*
var result = this.getPropStoreFieldValue('defBondScreenLength');
if (!result)
{
var bLength = this.getDefBondLength();
result = this.translateDistance(bLength, Kekule.Render.CoordSys.CHEM, Kekule.Render.CoordSys.SCREEN);
}
return result;
*/
var cached = this._lengthCaches.defBondScreenLength;
if (cached)
return cached;
else
{
var bLength = this.getDefBondLength() || 0;
var result = this.translateDistance(bLength, Kekule.Render.CoordSystem.CHEM, Kekule.Render.CoordSystem.SCREEN);
this._lengthCaches.defBondScreenLength = result;
return result;
}
}
});
// Different pointer event (mouse, touch) has different bound inflation settings, stores here
this.defineProp('currBoundInflation', {'dataType': DataType.NUMBER, 'serializable': false, 'setter': null,
'getter': function(){
var pType = this.getCurrPointerType();
return this.getInteractionBoundInflation(pType);
}
});
// The recent pointer device interacted with this editor
this.defineProp('currPointerType', {'dataType': DataType.STRING, 'serializable': false});
//this.defineProp('standardizeObjectsBeforeSaving', {'dataType': DataType.BOOL});
this.defineProp('enableCreateNewDoc', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('initOnNewDoc', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableOperHistory', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('operHistory', {
'dataType': 'Kekule.OperationHistory', 'serializable': false,
'getter': function()
{
/*
if (!this.getEnableOperHistory())
return null;
*/
var result = this.getPropStoreFieldValue('operHistory');
if (!result)
{
result = new Kekule.OperationHistory();
this.setPropStoreFieldValue('operHistory', result);
// install event handlers
result.addEventListener('push', this.reactOperHistoryPush, this);
result.addEventListener('pop', this.reactOperHistoryPop, this);
result.addEventListener('undo', this.reactOperHistoryUndo, this);
result.addEventListener('redo', this.reactOperHistoryRedo, this);
result.addEventListener('clear', this.reactOperHistoryClear, this);
result.addEventListener('change', this.reactOperHistoryChange, this);
}
return result;
},
'setter': null
});
this.defineProp('selection', {'dataType': DataType.ARRAY, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('selection');
if (!result)
{
result = [];
this.setPropStoreFieldValue('selection', result);
}
return result;
},
'setter': function(value)
{
this.setPropStoreFieldValue('selection', value);
this.selectionChanged();
}
});
this.defineProp('selectMode', {'dataType': DataType.INT,
'getter': function()
{
var result = this.getPropStoreFieldValue('selectMode');
if (Kekule.ObjUtils.isUnset(result))
result = Kekule.Editor.SelectMode.RECT; // default value
return result;
},
'setter': function(value)
{
if (this.getSelectMode() !== value)
{
//console.log('set select mode', value);
this.setPropStoreFieldValue('selectMode', value);
this.hideSelectingMarker();
}
}
});
// private, whether defaultly select in toggle mode
this.defineProp('isToggleSelectOn', {'dataType': DataType.BOOL});
this.defineProp('hotTrackedObjs', {'dataType': DataType.ARRAY, 'serializable': false,
'setter': function(value)
{
/*
if (this.getHotTrackedObjs() === value)
return;
*/
var objs = value? Kekule.ArrayUtils.toArray(value): [];
//console.log('setHotTrackedObjs', objs);
if (this.getEditorConfigs() && this.getEditorConfigs().getInteractionConfigs().getEnableHotTrack())
{
this.setPropStoreFieldValue('hotTrackedObjs', objs);
var bounds;
if (objs && objs.length)
{
bounds = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var bound = this.getBoundInfoRecorder().getBound(this.getObjContext(), objs[i]);
if (bounds)
{
//bounds.push(bound);
Kekule.ArrayUtils.pushUnique(bounds, bound); // bound may be an array of composite shape
}
}
}
if (bounds)
{
this.changeHotTrackMarkerBounds(bounds);
//console.log('show');
}
else
{
if (this.getUiHotTrackMarker().getVisible())
this.hideHotTrackMarker();
//console.log('hide');
}
}
}
});
this.defineProp('hotTrackedObj', {'dataType': DataType.OBJECT, 'serializable': false,
'getter': function() { return this.getHotTrackedObjs() && this.getHotTrackedObjs()[0]; },
'setter': function(value) { this.setHotTrackedObjs(value); }
});
this.defineProp('enableOperContext', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('enableOperContext', !!value);
if (!value) // release operContext
{
var ctx = this.getPropStoreFieldValue('operContext');
var b = this.getPropStoreFieldValue('drawBridge');
if (b && ctx)
b.releaseContext(ctx);
}
}
});
this.defineProp('enableGesture', {'dataType': DataType.BOOL,
'setter': function(value)
{
var bValue = !!value;
if (this.getEnableGesture() !== bValue)
{
this.setPropStoreFieldValue('enableGesture', bValue);
if (bValue)
{
this.startObservingGestureEvents(this.OBSERVING_GESTURES);
}
else
{
this.startObservingGestureEvents(this.OBSERVING_GESTURES);
}
}
}
});
// private
this.defineProp('uiEventReceiverElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
// context parent properties, private
this.defineProp('objContextParentElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
this.defineProp('operContextParentElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
this.defineProp('uiContextParentElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
this.defineProp('objContext', {'dataType': DataType.OBJECT, 'serializable': false, setter: null,
'getter': function() { return this.getDrawContext(); }
});
this.defineProp('operContext', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function()
{
if (!this.getEnableOperContext())
return null;
else
{
var result = this.getPropStoreFieldValue('operContext');
if (!result)
{
var bridge = this.getDrawBridge();
if (bridge)
{
var elem = this.getOperContextParentElem();
if (!elem)
return null;
else
{
var dim = Kekule.HtmlElementUtils.getElemScrollDimension(elem);
result = bridge.createContext(elem, dim.width, dim.height);
this.setPropStoreFieldValue('operContext', result);
}
}
}
return result;
}
}
});
this.defineProp('uiContext', {'dataType': DataType.OBJECT, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiContext');
if (!result)
{
var bridge = this.getUiDrawBridge();
if (bridge)
{
var elem = this.getUiContextParentElem();
if (!elem)
return null;
else
{
var dim = Kekule.HtmlElementUtils.getElemScrollDimension(elem);
//var dim = Kekule.HtmlElementUtils.getElemClientDimension(elem);
result = bridge.createContext(elem, dim.width, dim.height);
this.setPropStoreFieldValue('uiContext', result);
}
}
}
return result;
}
});
this.defineProp('objDrawBridge', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function() { return this.getDrawBridge(); }
});
this.defineProp('uiDrawBridge', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiDrawBridge');
if (!result)
{
result = this.createUiDrawBridge();
this.setPropStoreFieldValue('uiDrawBridge', result);
}
return result;
}
});
this.defineProp('uiPainter', {'dataType': 'Kekule.Render.ChemObjPainter', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiPainter');
if (!result)
{
// ui painter will always in 2D mode
var markers = this.getUiMarkers();
result = new Kekule.Render.ChemObjPainter(Kekule.Render.RendererType.R2D, markers, this.getUiDrawBridge());
result.setCanModifyTargetObj(true);
this.setPropStoreFieldValue('uiPainter', result);
return result;
}
return result;
}
});
this.defineProp('uiRenderer', {'dataType': 'Kekule.Render.AbstractRenderer', 'serializable': false, 'setter': null,
'getter': function()
{
var p = this.getUiPainter();
if (p)
{
var r = p.getRenderer();
if (!r)
p.prepareRenderer();
return p.getRenderer() || null;
}
else
return null;
}
});
// private ui marks properties
//this.defineProp('uiMarkers', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null});
this.defineProp('uiMarkers', {'dataType': 'Kekule.ChemWidget.UiMarkerCollection', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiMarkers');
if (!result)
{
result = new Kekule.ChemWidget.UiMarkerCollection();
this.setPropStoreFieldValue('uiMarkers', result);
}
return result;
}
});
/*
this.defineProp('uiHotTrackMarker', {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function() { return this.getUiMarkers().hotTrackMarker; },
'setter': function(value) { this.getUiMarkers().hotTrackMarker = value; }
});
this.defineProp('uiSelectionAreaMarker', {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function() { return this.getUiMarkers().selectionAreaMarker; },
'setter': function(value) { this.getUiMarkers().selectionAreaMarker = value; }
});
this.defineProp('uiSelectingMarker', {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function() { return this.getUiMarkers().selectingMarker; },
'setter': function(value) { this.getUiMarkers().selectingMarker = value; }
}); // marker of selecting rubber band
*/
this._defineUiMarkerProp('uiHotTrackMarker');
this._defineUiMarkerProp('uiSelectionAreaMarker'); // marker of selected range
this._defineUiMarkerProp('uiSelectingMarker'); // marker of selecting rubber band
this.defineProp('uiSelectionAreaContainerBox',
{'dataType': DataType.Object, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
// a private chemObj-renderer map
this.defineProp('objRendererMap', {'dataType': 'Kekule.MapEx', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('objRendererMap');
if (!result)
{
result = new Kekule.MapEx(true);
this.setPropStoreFieldValue('objRendererMap', result);
}
return result;
}
});
// private object to record all bound infos
this.defineProp('boundInfoRecorder', {'dataType': 'Kekule.Render.BoundInfoRecorder', 'serializable': false, 'setter': null});
this.defineProp('zoomCenter', {'dataType': DataType.HASH});
},
/** @private */
_defineUiMarkerProp: function(propName, uiMarkerCollection)
{
return this.defineProp(propName, {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue(propName);
if (!result)
{
result = this.createShapeBasedMarker(propName, null, null, false); // prop value already be set in createShapeBasedMarker method
}
return result;
},
'setter': function(value)
{
if (!uiMarkerCollection)
uiMarkerCollection = this.getUiMarkers();
var old = this.getPropValue(propName);
if (old)
{
uiMarkerCollection.removeMarker(old);
old.finalize();
}
uiMarkerCollection.addMarker(value);
this.setPropStoreFieldValue(propName, value);
}
});
},
/** @private */
doFinalize: function($super)
{
var h = this.getPropStoreFieldValue('operHistory');
if (h)
{
h.finalize();
this.setPropStoreFieldValue('operHistory', null);
}
var b = this.getPropStoreFieldValue('objDrawBridge');
var ctx = this.getPropStoreFieldValue('operContext');
if (b && ctx)
{
b.releaseContext(ctx);
}
this.setPropStoreFieldValue('operContext', null);
var b = this.getPropStoreFieldValue('uiDrawBridge');
var ctx = this.getPropStoreFieldValue('uiContext');
if (b && ctx)
{
b.releaseContext(ctx);
}
this.setPropStoreFieldValue('uiDrawBridge', null);
this.setPropStoreFieldValue('uiContext', null);
var r = this.getPropStoreFieldValue('boundInfoRecorder');
if (r)
r.finalize();
this.setPropStoreFieldValue('boundInfoRecorder', null);
var m = this.getPropStoreFieldValue('objRendererMap');
if (m)
m.finalize();
this.setPropStoreFieldValue('objRendererMap', null);
$super();
},
/**
* Create a default editor config object.
* Descendants may override this method.
* @returns {Kekule.Editor.BaseEditorConfigs}
* @ignore
*/
createDefaultConfigs: function()
{
return new Kekule.Editor.BaseEditorConfigs();
},
/** @ignore */
doCreateRootElement: function(doc)
{
var result = doc.createElement('div');
return result;
},
/** @ignore */
doCreateSubElements: function(doc, rootElem)
{
var elem = doc.createElement('div');
elem.className = CCNS.EDITOR_CLIENT;
rootElem.appendChild(elem);
this._editClientElem = elem;
return [elem];
},
/** @ignore */
getCoreElement: function($super)
{
return this._editClientElem || $super();
},
/** @private */
getEditClientElem: function()
{
return this._editClientElem;
},
/** @ignore */
doGetWidgetClassName: function($super)
{
var result = $super() + ' ' + CCNS.EDITOR;
var additional = (this.getRenderType() === Kekule.Render.RendererType.R3D)?
CCNS.EDITOR3D: CCNS.EDITOR2D;
result += ' ' + additional;
return result;
},
/** @private */
doBindElement: function(element)
{
this.createContextParentElems();
this.createUiEventReceiverElem();
},
// override getter and setter of intialZoom property
/** @ignore */
doGetInitialZoom: function($super)
{
var result;
var config = this.getEditorConfigs();
if (config)
result = config.getInteractionConfigs().getEditorInitialZoom();
if (!result)
result = $super();
return result;
},
/** @ignore */
doSetInitialZoom: function($super, value)
{
var config = this.getEditorConfigs();
if (config)
config.getInteractionConfigs().setEditorInitialZoom(value);
$super(value);
},
/** @ignore */
zoomTo: function($super, value, suspendRendering, zoomCenterCoord)
{
var CU = Kekule.CoordUtils;
var currZoomLevel = this.getCurrZoom();
var zoomLevel = value;
var result = $super(value, suspendRendering);
// adjust zoom center
var selfElem = this.getElement();
var currScrollCoord = {'x': selfElem.scrollLeft, 'y': selfElem.scrollTop};
if (!zoomCenterCoord)
zoomCenterCoord = this.getZoomCenter();
if (!zoomCenterCoord ) // use the center of client as the zoom center
{
zoomCenterCoord = CU.add(currScrollCoord, {'x': selfElem.clientWidth / 2, 'y': selfElem.clientHeight / 2});
}
//console.log('zoom center info', this.getZoomCenter(), zoomCenterCoord);
//if (zoomCenterCoord)
{
var scrollDelta = CU.multiply(zoomCenterCoord, zoomLevel / currZoomLevel - 1);
selfElem.scrollLeft += scrollDelta.x;
selfElem.scrollTop += scrollDelta.y;
}
return result;
},
/**
* Zoom in.
*/
zoomIn: function(step, zoomCenterCoord)
{
var curr = this.getCurrZoom();
var ratio = Kekule.ZoomUtils.getNextZoomInRatio(curr, step || 1);
return this.zoomTo(ratio, null, zoomCenterCoord);
},
/**
* Zoom out.
*/
zoomOut: function(step, zoomCenterCoord)
{
var curr = this.getCurrZoom();
var ratio = Kekule.ZoomUtils.getNextZoomOutRatio(curr, step || 1);
return this.zoomTo(ratio, null, zoomCenterCoord);
},
/**
* Reset to normal size.
*/
resetZoom: function(zoomCenterCoord)
{
return this.zoomTo(this.getInitialZoom() || 1, null, zoomCenterCoord);
},
/**
* Change the size of client element.
* Width and height is based on px.
* @private
*/
changeClientSize: function(width, height, zoomLevel)
{
this._initialRenderTransformParams = null;
var elem = this.getCoreElement();
var style = elem.style;
if (!zoomLevel)
zoomLevel = 1;
var w = width * zoomLevel;
var h = height * zoomLevel;
if (w)
style.width = w + 'px';
if (h)
style.height = h + 'px';
var ctxes = [this.getObjContext(), this.getOperContext(), this.getUiContext()];
for (var i = 0, l = ctxes.length; i < l; ++i)
{
var ctx = ctxes[i];
if (ctx) // change ctx size also
{
this.getDrawBridge().setContextDimension(ctx, w, h);
}
}
this.repaint();
},
/**
* Returns whether the chem object inside editor has been modified since load.
* @returns {Bool}
*/
isDirty: function()
{
if (this.getEnableOperHistory())
return this.getOperHistory().getCurrIndex() >= 0;
else
return this._objChanged;
},
/**
* Returns srcInfo of chemObj. If editor is dirty (object been modified), srcInfo will be unavailable.
* @param {Kekule.ChemObject} chemObj
* @returns {Object}
*/
getChemObjSrcInfo: function(chemObj)
{
if (this.isDirty())
return null;
else
return chemObj.getSrcInfo? chemObj.getSrcInfo(): null;
},
/* @private */
/*
_calcPreferedTransformOptions: function()
{
var drawOptions = this.getDrawOptions();
return this.getPainter().calcPreferedTransformOptions(
this.getObjContext(), this.calcDrawBaseCoord(drawOptions), drawOptions);
},
*/
/** @private */
getActualDrawOptions: function($super)
{
var old = $super();
if (this._initialRenderTransformParams)
{
var result = Object.extend({}, this._initialRenderTransformParams);
result = Object.extend(result, old);
//var result = Object.create(old);
//result.initialRenderTransformParams = this._initialRenderTransformParams;
//console.log('extended', this._initialRenderTransformParams, result);
return result;
}
else
return old;
},
/** @ignore */
/*
getDrawClientDimension: function()
{
},
*/
/** @ignore */
repaint: function($super, overrideOptions)
{
var ops = overrideOptions;
//console.log('repaint called', overrideOptions);
//console.log('repaint', this._initialRenderTransformParams);
/*
if (this._initialRenderTransformParams)
{
ops = Object.create(overrideOptions || {});
//console.log(this._initialRenderTransformParams);
ops = Object.extend(ops, this._initialRenderTransformParams);
}
else
{
ops = overrideOptions;
//this._initialRenderTransformParams = this._calcPreferedTransformOptions();
//console.log('init params: ', this._initialRenderTransformParams, drawOptions);
}
*/
var result = $super(ops);
// after paint the new obj the first time, save up the transform params (especially the translates)
if (!this._initialRenderTransformParams)
{
this._initialRenderTransformParams = this.getPainter().getActualInitialRenderTransformOptions(this.getObjContext());
/*
if (transParam)
{
var trans = {}
var unitLength = transParam.unitLength || 1;
if (Kekule.ObjUtils.notUnset(transParam.translateX))
trans.translateX = transParam.translateX / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateY))
trans.translateY = transParam.translateY / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateZ))
trans.translateZ = transParam.translateZ / unitLength;
if (transParam.center)
trans.center = transParam.center;
//var zoom = transParam.zoom || 1;
var zoom = 1;
trans.scaleX = transParam.scaleX / zoom;
trans.scaleY = transParam.scaleY / zoom;
trans.scaleZ = transParam.scaleZ / zoom;
this._initialRenderTransformParams = trans;
console.log(this._initialRenderTransformParams, this);
}
*/
}
// redraw ui markers
this.recalcUiMarkers();
return result;
},
/**
* Create a new object and load it in editor.
*/
newDoc: function()
{
//if (this.getEnableCreateNewDoc()) // enable property only affects UI, always could create new doc in code
this.load(this.doCreateNewDocObj());
},
/**
* Create a new object for new document.
* Descendants may override this method.
* @private
*/
doCreateNewDocObj: function()
{
return new Kekule.Molecule();
},
/**
* Returns array of classes that can be exported (saved) from editor.
* Descendants can override this method.
* @returns {Array}
*/
getExportableClasses: function()
{
var obj = this.getChemObj();
if (!obj)
return [];
else
return obj.getClass? obj.getClass(): null;
},
/**
* Returns exportable object for specified class.
* Descendants can override this method.
* @param {Class} objClass Set null to export default object.
* @returns {Object}
*/
exportObj: function(objClass)
{
return this.exportObjs(objClass)[0];
},
/**
* Returns all exportable objects for specified class.
* Descendants can override this method.
* @param {Class} objClass Set null to export default object.
* @returns {Array}
*/
exportObjs: function(objClass)
{
var obj = this.getChemObj();
if (!objClass)
return [obj];
else
{
return (obj && (obj instanceof objClass))? [obj]: [];
}
},
/** @private */
doLoad: function($super, chemObj)
{
// deselect all old objects first
this.deselectAll();
this._initialRenderTransformParams = null;
// clear rendererMap so that all old renderer info is removed
this.getObjRendererMap().clear();
if (this.getOperHistory())
this.getOperHistory().clear();
$super(chemObj);
this._objChanged = false;
},
/** @private */
doLoadEnd: function($super, chemObj)
{
$super();
//console.log('loadend: ', chemObj);
if (!chemObj)
this._initialRenderTransformParams = null;
/*
else
{
// after load the new obj the first time, save up the transform params (especially the translates)
var transParam = this.getPainter().getActualRenderTransformParams(this.getObjContext());
if (transParam)
{
var trans = {}
var unitLength = transParam.unitLength || 1;
if (Kekule.ObjUtils.notUnset(transParam.translateX))
trans.translateX = transParam.translateX / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateY))
trans.translateY = transParam.translateY / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateZ))
trans.translateZ = transParam.translateZ / unitLength;
this._initialRenderTransformParams = trans;
console.log(this._initialRenderTransformParams, this);
}
}
*/
},
/** @private */
doResize: function($super)
{
//console.log('doResize');
this._initialRenderTransformParams = null; // transform should be recalculated after resize
$super();
},
/** @ignore */
geometryOptionChanged: function($super)
{
var zoom = this.getDrawOptions().zoom;
this.zoomChanged(zoom);
// clear some length related caches
this._clearLengthCaches();
$super();
},
/** @private */
zoomChanged: function(zoomLevel)
{
// do nothing here
},
/** @private */
_clearLengthCaches: function()
{
this._lengthCaches = {};
},
/**
* @private
*/
chemObjChanged: function($super, newObj, oldObj)
{
$super(newObj, oldObj);
if (newObj !== oldObj)
{
if (oldObj)
this._uninstallChemObjEventListener(oldObj);
if (newObj)
this._installChemObjEventListener(newObj);
}
},
/** @private */
_installChemObjEventListener: function(chemObj)
{
chemObj.addEventListener('change', this.reactChemObjChange, this);
},
/** @private */
_uninstallChemObjEventListener: function(chemObj)
{
chemObj.removeEventListener('change', this.reactChemObjChange, this);
},
/**
* Create a transparent div element above all other elems of editor,
* this element is used to receive all UI events.
*/
createUiEventReceiverElem: function()
{
var parent = this.getCoreElement();
if (parent)
{
var result = parent.ownerDocument.createElement('div');
result.className = CCNS.EDITOR_UIEVENT_RECEIVER;
/*
result.id = 'overlayer';
*/
/*
var style = result.style;
style.background = 'transparent';
//style.background = 'yellow';
//style.opacity = 0;
style.position = 'absolute';
style.left = 0;
style.top = 0;
style.width = '100%';
style.height = '100%';
*/
//style.zIndex = 1000;
parent.appendChild(result);
EU.addClass(result, CNS.DYN_CREATED);
this.setPropStoreFieldValue('uiEventReceiverElem', result);
return result;
}
},
/** @private */
createContextParentElems: function()
{
var parent = this.getCoreElement();
if (parent)
{
var doc = parent.ownerDocument;
this._createContextParentElem(doc, parent, 'objContextParentElem');
this._createContextParentElem(doc, parent, 'operContextParentElem');
this._createContextParentElem(doc, parent, 'uiContextParentElem');
}
},
/** @private */
_createContextParentElem: function(doc, parentElem, contextElemPropName)
{
var result = doc.createElement('div');
result.style.position = 'absolute';
result.style.width = '100%';
result.style.height = '100%';
result.className = contextElemPropName + ' ' + CNS.DYN_CREATED; // debug
this.setPropStoreFieldValue(contextElemPropName, result);
parentElem.appendChild(result);
return result;
},
/** @private */
createNewPainter: function($super, chemObj)
{
var result = $super(chemObj);
if (result)
{
result.setCanModifyTargetObj(true);
this.installPainterEventHandlers(result);
// create new bound info recorder
this.createNewBoundInfoRecorder(this.getPainter());
}
return result;
},
/** @private */
createNewBoundInfoRecorder: function(renderer)
{
var old = this.getPropStoreFieldValue('boundInfoRecorder');
if (old)
old.finalize();
var recorder = new Kekule.Render.BoundInfoRecorder(renderer);
//recorder.setTargetContext(this.getObjContext());
this.setPropStoreFieldValue('boundInfoRecorder', recorder);
},
/** @private */
getDrawContextParentElem: function()
{
return this.getObjContextParentElem();
},
/** @private */
createUiDrawBridge: function()
{
// UI marker will always be in 2D
var result = Kekule.Render.DrawBridge2DMananger.getPreferredBridgeInstance();
if (!result) // can not find suitable draw bridge
{
Kekule.error(/*Kekule.ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED*/Kekule.$L('ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED'));
}
return result;
},
/* @private */
/*
refitDrawContext: function($super, doNotRepaint)
{
//var dim = Kekule.HtmlElementUtils.getElemScrollDimension(this.getElement());
var dim = Kekule.HtmlElementUtils.getElemClientDimension(this.getElement());
//this._resizeContext(this.getObjDrawContext(), this.getObjDrawBridge(), dim.width, dim.height);
this._resizeContext(this.getOperContext(), this.getObjDrawBridge(), dim.width, dim.height);
this._resizeContext(this.getUiContext(), this.getUiDrawBridge(), dim.width, dim.height);
$super(doNotRepaint);
},
*/
/** @private */
changeContextDimension: function($super, newDimension)
{
var result = $super(newDimension);
if (result)
{
this._resizeContext(this.getOperContext(), this.getObjDrawBridge(), newDimension.width, newDimension.height);
this._resizeContext(this.getUiContext(), this.getUiDrawBridge(), newDimension.width, newDimension.height);
}
return result;
},
/** @private */
_resizeContext: function(context, bridge, width, height)
{
if (context && bridge)
bridge.setContextDimension(context, width, height);
},
/** @private */
_clearSpecContext: function(context, bridge)
{
if (bridge && context)
bridge.clearContext(context);
},
/**
* Clear the main context.
* @private
*/
clearObjContext: function()
{
//console.log('clear obj context', this.getObjContext() === this.getDrawContext());
this._clearSpecContext(this.getObjContext(), this.getDrawBridge());
if (this.getBoundInfoRecorder())
this.getBoundInfoRecorder().clear(this.getObjContext());
},
/**
* Clear the operating context.
* @private
*/
clearOperContext: function()
{
this._clearSpecContext(this.getOperContext(), this.getDrawBridge());
},
/**
* Clear the UI layer context.
* @private
*/
clearUiContext: function()
{
this._clearSpecContext(this.getUiContext(), this.getUiDrawBridge());
},
/** @private */
clearContext: function()
{
this.clearObjContext();
if (this._operatingRenderers)
this.clearOperContext();
},
/**
* Repaint the operating context only (not the whole obj context).
* @private
*/
repaintOperContext: function(ignoreUiMarker)
{
if (this._operatingRenderers && this._operatingObjs)
{
this.clearOperContext();
var options = {'partialDrawObjs': this._operatingObjs, 'doNotClear': true};
this.repaint(options);
/*
var context = this.getObjContext();
//console.log(this._operatingRenderers.length);
for (var i = 0, l = this._operatingRenderers.length; i < l; ++i)
{
var renderer = this._operatingRenderers[i];
console.log('repaint oper', renderer.getClassName(), renderer.getChemObj().getId(), !!renderer.getRedirectContext(), this._operatingRenderers.length);
renderer.redraw(context);
}
if (!ignoreUiMarker)
this.recalcUiMarkers();
*/
}
},
/** @private */
getOperatingRenderers: function()
{
if (!this._operatingRenderers)
this._operatingRenderers = [];
return this._operatingRenderers;
},
/** @private */
setOperatingRenderers: function(value)
{
this._operatingRenderers = value;
},
//////////////////////////////////////////////////////////////////////
/////////////////// methods about painter ////////////////////////////
/** @private */
installPainterEventHandlers: function(painter)
{
painter.addEventListener('prepareDrawing', this.reactChemObjPrepareDrawing, this);
painter.addEventListener('clear', this.reactChemObjClear, this);
},
/** @private */
reactChemObjPrepareDrawing: function(e)
{
var ctx = e.context;
var obj = e.obj;
if (obj && ((ctx === this.getObjContext()) || (ctx === this.getOperContext())))
{
var renderer = e.target;
this.getObjRendererMap().set(obj, renderer);
//console.log('object drawn', obj, obj.getClassName(), renderer, renderer.getClassName());
// check if renderer should be redirected to oper context
if (this.getEnableOperContext())
{
var operObjs = this._operatingObjs || [];
var needRedirect = false;
for (var i = 0, l = operObjs.length; i < l; ++i)
{
if (this._isChemObjDirectlyRenderedByRenderer(this.getObjContext(), operObjs[i], renderer))
{
needRedirect = true;
break;
}
}
if (needRedirect)
{
this._setRendererToOperContext(renderer);
//console.log('do redirect', renderer.getClassName(), obj && obj.getId && obj.getId());
AU.pushUnique(this.getOperatingRenderers(), renderer);
}
/*
else
{
this._unsetRendererToOperContext(renderer);
console.log('unset redirect', renderer.getClassName(), obj && obj.getId && obj.getId());
}
*/
}
}
},
/** @private */
reactChemObjClear: function(e)
{
var ctx = e.context;
var obj = e.obj;
if (obj && ((ctx === this.getObjContext()) || (ctx === this.getOperContext())))
{
var renderer = e.target;
this.getObjRendererMap().remove(obj);
AU.remove(this.getOperatingRenderers(), renderer);
}
},
//////////////////////////////////////////////////////////////////////
/////////////////// event handlers of nested objects ///////////////////////
/**
* React to change event of loaded chemObj.
* @param {Object} e
*/
reactChemObjChange: function(e)
{
var target = e.target;
var propNames = e.changedPropNames || [];
var bypassPropNames = ['id', 'owner', 'ownedObjs']; // these properties do not affect rendering
propNames = Kekule.ArrayUtils.exclude(propNames, bypassPropNames);
if (propNames.length || !e.changedPropNames) // when changedPropNames is not set, may be change event invoked by parent when suppressing child objects
{
//console.log('chem obj change', target.getClassName(), propNames, e);
this.objectChanged(target, propNames);
}
},
/** @private */
reactOperHistoryPush: function(e)
{
this.invokeEvent('operPush', e);
},
/** @private */
reactOperHistoryPop: function(e)
{
this.invokeEvent('operPop', e);
},
/** @private */
reactOperHistoryUndo: function(e)
{
this.invokeEvent('operUndo', e);
},
/** @private */
reactOperHistoryRedo: function(e)
{
this.invokeEvent('operRedo', e);
},
reactOperHistoryClear: function(e)
{
this.invokeEvent('operHistoryClear', e);
},
reactOperHistoryChange: function(e)
{
this.invokeEvent('operChange', e);
},
/////////////////////////////////////////////////////////////////////////////
///////////// Methods about object changing notification ////////////////////
/**
* Call this method to temporarily suspend object change notification.
*/
beginUpdateObject: function()
{
if (this._objectUpdateFlag >= 0)
{
this.invokeEvent('beginUpdateObject');
}
--this._objectUpdateFlag;
},
/**
* Call this method to indicate the update process is over and objectChanged will be immediately called.
*/
endUpdateObject: function()
{
++this._objectUpdateFlag;
if (!this.isUpdatingObject())
{
if ((this._updatedObjectDetails && this._updatedObjectDetails.length))
{
this.objectsChanged(this._updatedObjectDetails);
this._updatedObjectDetails = [];
}
this.invokeEvent('endUpdateObject'/*, {'details': Object.extend({}, this._updatedObjectDetails)}*/);
}
},
/**
* Check if beginUpdateObject is called and should not send object change notification immediately.
*/
isUpdatingObject: function()
{
return (this._objectUpdateFlag < 0);
},
/** @private */
_mergeObjUpdatedDetails: function(dest, target)
{
for (var i = 0, l = target.length; i < l; ++i)
{
this._mergeObjUpdatedDetailItem(dest, target[i]);
}
},
/** @private */
_mergeObjUpdatedDetailItem: function(dest, targetItem)
{
for (var i = 0, l = dest.length; i < l; ++i)
{
var destItem = dest[i];
// can merge
if (destItem.obj === targetItem.obj)
{
if (!destItem.propNames)
destItem.propNames = [];
if (targetItem.propNames)
Kekule.ArrayUtils.pushUnique(destItem.propNames, targetItem.propNames);
return;
}
}
// can not merge
dest.push(targetItem);
},
/** @private */
_logUpdatedDetail: function(details)
{
var msg = '';
details.forEach(function(d){
msg += 'Obj: ' + d.obj.getId() + '[' + d.obj.getClassName() + '] ';
msg += 'Props: [' + d.propNames.join(', ') + ']';
msg += '\n';
});
console.log(msg);
},
/**
* Notify the object(s) property has been changed and need to be updated.
* @param {Variant} obj An object or a object array.
* @param {Array} changedPropNames
* @private
*/
objectChanged: function(obj, changedPropNames)
{
var data = {'obj': obj, 'propNames': changedPropNames};
//console.log('obj changed', obj.getClassName(), obj.getId(), changedPropNames);
var result = this.objectsChanged(data);
this.invokeEvent('editObjChanged', Object.extend({}, data)); // avoid change data
return result;
},
/**
* Notify the object(s) property has been changed and need to be updated.
* @param {Variant} objDetails An object detail or an object detail array.
* @private
*/
objectsChanged: function(objDetails)
{
var a = DataType.isArrayValue(objDetails)? objDetails: [objDetails];
if (this.isUpdatingObject()) // suspend notification, just push objs in cache
{
//Kekule.ArrayUtils.pushUnique(this._updatedObjectDetails, a);
this._mergeObjUpdatedDetails(this._updatedObjectDetails, a);
//console.log('updating objects, suspending...', this._updatedObjectDetails);
//this._logUpdatedDetail(this._updatedObjectDetails);
}
else
{
//console.log('object changed');
var updateObjs = Kekule.Render.UpdateObjUtils._extractObjsOfUpdateObjDetails(a);
this.doObjectsChanged(a, updateObjs);
this.invokeEvent('editObjsUpdated', {'details': Object.extend({}, objDetails)});
}
this._objChanged = true; // mark object changed
this.invokeEvent('editObjsChanged', {'details': Object.extend({}, objDetails)});
var selectedObjs = this.getSelection();
if (selectedObjs && updateObjs)
{
var changedSelectedObjs = AU.intersect(selectedObjs, updateObjs);
if (changedSelectedObjs.length)
this.invokeEvent('selectedObjsUpdated', {'objs': changedSelectedObjs});
}
},
/**
* Do actual job of objectsChanged. Descendants should override this method.
* @private
*/
doObjectsChanged: function(objDetails, updateObjs)
{
var oDetails = Kekule.ArrayUtils.clone(objDetails);
if (!updateObjs)
updateObjs = Kekule.Render.UpdateObjUtils._extractObjsOfUpdateObjDetails(oDetails);
//console.log('origin updateObjs', updateObjs);
var additionalObjs = this._getAdditionalRenderRelatedObjs(updateObjs);
// also push related objects into changed objs list
if (additionalObjs.length)
{
var additionalDetails = Kekule.Render.UpdateObjUtils._createUpdateObjDetailsFromObjs(additionalObjs);
Kekule.ArrayUtils.pushUnique(oDetails, additionalDetails);
}
// merge updateObjs and additionalObjs
//updateObjs = updateObjs.concat(additionalObjs);
Kekule.ArrayUtils.pushUnique(updateObjs, additionalObjs);
//console.log('changed objects', updateObjs);
var operRenderers = this._operatingRenderers;
var updateOperContextOnly = operRenderers && this._isAllObjsRenderedByRenderers(this.getObjContext(), updateObjs, operRenderers);
var canDoPartialUpdate = this.canModifyPartialGraphic();
//console.log('update objs and operRenderers', updateObjs, operRenderers);
//console.log('object changed', updateOperContextOnly, canDoPartialUpdate);
if (canDoPartialUpdate) // partial update
{
//var updateObjDetails = Kekule.Render.UpdateObjUtils._createUpdateObjDetailsFromObjs(updateObjs);
this.getRootRenderer().modify(this.getObjContext(),/* updateObjDetails*/oDetails);
// always repaint UI markers
this.recalcUiMarkers();
//console.log('partial update', oDetails);
}
else // update whole context
{
if (updateOperContextOnly)
{
//console.log('repaint oper context only');
this.repaintOperContext();
}
else // need to update whole context
{
//console.log('[repaint whole]');
this.repaint();
/*
var self = this;
(function(){ self.repaint(); }).defer();
*/
}
}
},
/**
* Call this method to indicate a continuous manipulation operation is doing (e.g. moving or rotating objects).
*/
beginManipulateObject: function()
{
//console.log('[Call begin update]', this._objectManipulateFlag);
if (this._objectManipulateFlag >= 0)
{
//console.log('[BEGIN MANIPULATE]');
this.invokeEvent('beginManipulateObject');
}
--this._objectManipulateFlag;
},
/**
* Call this method to indicate the update process is over and objectChanged will be immediately called.
*/
endManipulateObject: function()
{
++this._objectManipulateFlag;
//console.log('[END MANIPULATE]');
if (!this.isManipulatingObject())
{
this._objectManipulateFlag = 0;
//console.log('[MANIPULATE DONE]');
this.invokeEvent('endManipulateObject'/*, {'details': Object.extend({}, this._updatedObjectDetails)}*/);
}
},
/**
* Check if beginUpdateObject is called and should not send object change notification immediately.
*/
isManipulatingObject: function()
{
return (this._objectManipulateFlag < 0);
},
/** @private */
_needToCanonicalizeBeforeSaving: function()
{
return true; // !!this.getStandardizeObjectsBeforeSaving();
},
/** @private */
_getAdditionalRenderRelatedObjs: function(objs)
{
var result = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
//Kekule.ArrayUtils.pushUnique(result, obj);
var relatedObjs = obj.getCoordDeterminateObjects? obj.getCoordDeterminateObjects(): [];
//console.log('obj', obj.getClassName(), 'related', relatedObjs);
Kekule.ArrayUtils.pushUnique(result, relatedObjs);
}
return result;
},
/** @private */
_isAllObjsRenderedByRenderers: function(context, objs, renders)
{
//console.log('check objs by renderers', objs, renders);
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var isRendered = false;
for (var j = 0, k = renders.length; j < k; ++j)
{
var renderer = renders[j];
if (renderer.isChemObjRenderedBySelf(context, obj))
{
isRendered = true;
break;
}
}
if (!isRendered)
return false;
}
return true;
},
/////////////////////////////////////////////////////////////////////////////
////////////// Method about operContext rendering ///////////////////////////
/**
* Prepare to do a modification work in editor (e.g., move some atoms).
* The objs to be modified will be rendered in operContext separately (if enableOperContext is true).
* @param {Array} objs
*/
prepareOperatingObjs: function(objs)
{
// Check if already has old operating renderers. If true, just end them.
if (this._operatingRenderers && this._operatingRenderers.length)
this.endOperatingObjs(true);
if (this.getEnableOperContext())
{
// prepare operating renderers
this._prepareRenderObjsInOperContext(objs);
this._operatingObjs = objs;
//console.log('oper objs', this._operatingObjs);
//console.log('oper renderers', this._operatingRenderers);
}
// finally force repaint the whole client area, both objContext and operContext
this.repaint();
},
/**
* Modification work in editor (e.g., move some atoms) is done.
* The objs to be modified will be rendered back into objContext.
* @param {Bool} noRepaint
*/
endOperatingObjs: function(noRepaint)
{
// notify to render all objs in main context
if (this.getEnableOperContext())
{
this._endRenderObjsInOperContext();
this._operatingObjs = null;
if (!noRepaint)
{
//console.log('end operation objs');
this.repaint();
}
}
},
/** @private */
_isChemObjDirectlyRenderedByRenderer: function(context, obj, renderer)
{
var standaloneObj = obj.getStandaloneAncestor? obj.getStandaloneAncestor(): obj;
return renderer.isChemObjRenderedDirectlyBySelf(context, standaloneObj);
},
/** @private */
_getStandaloneRenderObjsInOperContext: function(objs)
{
var standAloneObjs = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (obj.getStandaloneAncestor)
obj = obj.getStandaloneAncestor();
Kekule.ArrayUtils.pushUnique(standAloneObjs, obj);
}
return standAloneObjs;
},
/** @private */
_prepareRenderObjsInOperContext: function(objs)
{
//console.log('redirect objs', objs);
var renderers = [];
var map = this.getObjRendererMap();
var rs = map.getValues();
var context = this.getObjContext();
var standAloneObjs = this._getStandaloneRenderObjsInOperContext(objs);
/*
if (standAloneObjs.length)
console.log(standAloneObjs[0].getId(), standAloneObjs);
else
console.log('(no standalone)');
*/
//for (var i = 0, l = objs.length; i < l; ++i)
for (var i = 0, l = standAloneObjs.length; i < l; ++i)
{
//var obj = objs[i];
var obj = standAloneObjs[i];
for (var j = 0, k = rs.length; j < k; ++j)
{
var renderer = rs[j];
//if (renderer.isChemObjRenderedBySelf(context, obj))
if (renderer.isChemObjRenderedDirectlyBySelf(context, obj))
{
//console.log('direct rendered by', obj.getClassName(), renderer.getClassName());
Kekule.ArrayUtils.pushUnique(renderers, renderer);
}
/*
if (parentFragment && renderer.isChemObjRenderedDirectlyBySelf(context, parentFragment))
Kekule.ArrayUtils.pushUnique(renderers, renderer); // when modify node or connector, mol will also be changed
*/
}
/*
var renderer = map.get(objs[i]);
if (renderer)
Kekule.ArrayUtils.pushUnique(renderers, renderer);
*/
}
//console.log('oper renderers', renderers);
if (renderers.length > 0)
{
for (var i = 0, l = renderers.length; i < l; ++i)
{
var renderer = renderers[i];
this._setRendererToOperContext(renderer);
//console.log('begin context redirect', renderer.getClassName());
//console.log(renderer.getRedirectContext());
}
this._operatingRenderers = renderers;
}
else
this._operatingRenderers = null;
//console.log('<total renderer count>', rs.length, '<redirected>', renderers.length);
/*
if (renderers.length)
console.log('redirected obj 0: ', renderers[0].getChemObj().getId());
*/
},
/** @private */
_endRenderObjsInOperContext: function()
{
var renderers = this._operatingRenderers;
if (renderers && renderers.length)
{
for (var i = 0, l = renderers.length; i < l; ++i)
{
var renderer = renderers[i];
//renderer.setRedirectContext(null);
this._unsetRendererToOperContext(renderer);
//console.log('end context redirect', renderer.getClassName());
}
this.clearOperContext();
}
this._operatingRenderers = null;
},
/** @private */
_setRendererToOperContext: function(renderer)
{
renderer.setRedirectContext(this.getOperContext());
},
/** @private */
_unsetRendererToOperContext: function(renderer)
{
renderer.setRedirectContext(null);
},
/////////////////////////////////////////////////////////////////////////////
//////////////////////// methods about bound maps ////////////////////////////
/**
* Returns bound inflation for interaction with a certain pointer device (mouse, touch, etc.)
* @param {String} pointerType
*/
getInteractionBoundInflation: function(pointerType)
{
var cache = this._lengthCaches.interactionBoundInflations;
var cacheKey = pointerType || 'default';
if (cache)
{
if (cache[cacheKey])
{
//console.log('cached!')
return cache[cacheKey];
}
}
// no cache, calculate
var iaConfigs = this.getEditorConfigs().getInteractionConfigs();
var defRatioPropName = 'objBoundTrackInflationRatio';
var typedRatio, defRatio = iaConfigs.getPropValue(defRatioPropName);
if (pointerType)
{
var sPointerType = pointerType.upperFirst();
var typedRatioPropName = defRatioPropName + sPointerType;
if (iaConfigs.hasProperty(typedRatioPropName))
typedRatio = iaConfigs.getPropValue(typedRatioPropName);
}
var actualRatio = typedRatio || defRatio;
var ratioValue = actualRatio && this.getDefBondScreenLength() * actualRatio;
var minValuePropName = 'objBoundTrackMinInflation';
var typedMinValue, defMinValue = iaConfigs.getPropValue(minValuePropName);
if (pointerType)
{
var typedMinValuePropName = minValuePropName + sPointerType;
if (iaConfigs.hasProperty(typedMinValuePropName))
typedMinValue = iaConfigs.getPropValue(typedMinValuePropName);
}
var actualMinValue = typedMinValue || defMinValue;
var actualValue = Math.max(ratioValue || 0, actualMinValue);
// stores to cache
if (!cache)
{
cache = {};
this._lengthCaches.interactionBoundInflations = cache;
}
cache[cacheKey] = actualValue;
//console.log('to cache');
return actualValue;
},
/** @private */
clearBoundMap: function()
{
this.getBoundInfoRecorder().clear(this.getObjContext());
},
/**
* Returns topmost bound item in z-index.
* Descendants may override this method to implement more accurate algorithm.
* @param {Array} boundItems
* @param {Array} excludeObjs Objects in this array will not be returned.
* @returns {Object}
* @private
*/
findTopmostBoundInfo: function(boundItems, excludeObjs)
{
if (boundItems && boundItems.length)
{
var result = null;
var index = boundItems.length - 1;
result = boundItems[index];
while ((index >= 0) && (excludeObjs && (excludeObjs.indexOf(result.obj) >= 0)))
{
--index;
result = boundItems[index];
}
return result;
}
else
return null;
},
/**
* Returns all bound map item at x/y.
* Input coord is based on the screen coord system.
* @returns {Array}
* @private
*/
getBoundInfosAtCoord: function(screenCoord, filterFunc, boundInflation)
{
/*
if (!boundInflation)
throw 'boundInflation not set!';
*/
var boundRecorder = this.getBoundInfoRecorder();
var delta = boundInflation || this.getCurrBoundInflation() || this.getEditorConfigs().getInteractionConfigs().getObjBoundTrackMinInflation();
//var coord = this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), screenCoord);
var coord = this.screenCoordToContext(screenCoord);
var refCoord = (this.getRenderType() === Kekule.Render.RendererType.R3D)? {'x': 0, 'y': 0}: null;
//console.log(coord, delta);
var matchedInfos = boundRecorder.getIntersectionInfos(this.getObjContext(), coord, refCoord, delta, filterFunc);
return matchedInfos;
},
/**
* returns the topmost bound map item at x/y.
* Input coord is based on the screen coord system.
* @param {Hash} screenCoord
* @param {Array} excludeObjs Objects in this array will not be returned.
* @returns {Object}
*/
getTopmostBoundInfoAtCoord: function(screenCoord, excludeObjs, boundInflation)
{
var enableTrackNearest = this.getEditorConfigs().getInteractionConfigs().getEnableTrackOnNearest();
if (!enableTrackNearest)
return this.findTopmostBoundInfo(this.getBoundInfosAtCoord(screenCoord, null, boundInflation), excludeObjs, boundInflation);
// else, track on nearest
// new approach, find nearest boundInfo at coord
var SU = Kekule.Render.MetaShapeUtils;
var boundInfos = this.getBoundInfosAtCoord(screenCoord, null, boundInflation);
//var filteredBoundInfos = [];
var result, lastShapeInfo, lastDistance;
var setResult = function(boundInfo, shapeInfo, distance)
{
result = boundInfo;
lastShapeInfo = shapeInfo || boundInfo.boundInfo;
if (Kekule.ObjUtils.notUnset(distance))
lastDistance = distance;
else
lastDistance = SU.getDistance(screenCoord, lastShapeInfo);
};
for (var i = boundInfos.length - 1; i >= 0; --i)
{
var info = boundInfos[i];
if (excludeObjs && (excludeObjs.indexOf(info.obj) >= 0))
continue;
if (!result)
setResult(info);
else
{
var shapeInfo = info.boundInfo;
if (shapeInfo.shapeType < lastShapeInfo.shapeType)
setResult(info, shapeInfo);
else if (shapeInfo.shapeType === lastShapeInfo.shapeType)
{
var currDistance = SU.getDistance(screenCoord, shapeInfo);
if (currDistance < lastDistance)
{
//console.log('distanceCompare', currDistance, lastDistance);
setResult(info, shapeInfo, currDistance);
}
}
}
}
return result;
},
/**
* Returns the topmost basic drawn object at coord based on screen system.
* @params {Hash} coord
* @returns {Object}
* @private
*/
getTopmostBasicObjectAtCoord: function(screenCoord, boundInflation)
{
var boundItem = this.getTopmostBoundInfoAtCoord(screenCoord, null, boundInflation);
return boundItem? boundItem.obj: null;
},
/**
* Returns geometry bounds of a obj in editor.
* @param {Kekule.ChemObject} obj
* @param {Number} boundInflation
* @returns {Array}
*/
getChemObjBounds: function(obj, boundInflation)
{
var bounds = [];
var infos = this.getBoundInfoRecorder().getBelongedInfos(this.getObjContext(), obj);
if (infos && infos.length)
{
for (var j = 0, k = infos.length; j < k; ++j)
{
var info = infos[j];
var bound = info.boundInfo;
if (bound)
{
// inflate
bound = Kekule.Render.MetaShapeUtils.inflateShape(bound, boundInflation);
bounds.push(bound);
}
}
}
return bounds;
},
//////////////////// methods about UI markers ///////////////////////////////
/**
* Notify that currently is modifing UI markers and the editor need not to repaint them.
*/
beginUpdateUiMarkers: function()
{
--this._uiMarkerUpdateFlag;
},
/**
* Call this method to indicate the UI marker update process is over and should be immediately updated.
*/
endUpdateUiMarkers: function()
{
++this._uiMarkerUpdateFlag;
if (!this.isUpdatingUiMarkers())
this.repaintUiMarker();
},
/** Check if the editor is under continuous UI marker update. */
isUpdatingUiMarkers: function()
{
return (this._uiMarkerUpdateFlag < 0);
},
/**
* Called when transform has been made to objects and UI markers need to be modified according to it.
* The UI markers will also be repainted.
* @private
*/
recalcUiMarkers: function()
{
//this.setHotTrackedObj(null);
this.beginUpdateUiMarkers();
try
{
this.recalcHotTrackMarker();
this.recalcSelectionAreaMarker();
}
finally
{
this.endUpdateUiMarkers();
}
},
/** @private */
repaintUiMarker: function()
{
if (this.isUpdatingUiMarkers())
return;
this.clearUiContext();
var drawParams = this.calcDrawParams();
this.getUiPainter().draw(this.getUiContext(), drawParams.baseCoord, drawParams.drawOptions);
},
/**
* Create a new marker based on shapeInfo.
* @private
*/
createShapeBasedMarker: function(markerPropName, shapeInfo, drawStyles, updateRenderer)
{
var marker = new Kekule.ChemWidget.MetaShapeUIMarker();
if (shapeInfo)
marker.setShapeInfo(shapeInfo);
if (drawStyles)
marker.setDrawStyles(drawStyles);
this.setPropStoreFieldValue(markerPropName, marker);
this.getUiMarkers().addMarker(marker);
if (updateRenderer)
{
//var updateType = Kekule.Render.ObjectUpdateType.ADD;
//this.getUiRenderer().update(this.getUiContext(), this.getUiMarkers(), marker, updateType);
this.repaintUiMarker();
}
return marker;
},
/**
* Change the shape info of a meta shape based marker, or create a new marker based on shape info.
* @private
*/
modifyShapeBasedMarker: function(marker, newShapeInfo, drawStyles, updateRenderer)
{
var updateType = Kekule.Render.ObjectUpdateType.MODIFY;
if (newShapeInfo)
marker.setShapeInfo(newShapeInfo);
if (drawStyles)
marker.setDrawStyles(drawStyles);
// notify change and update renderer
if (updateRenderer)
{
//this.getUiPainter().redraw();
//this.getUiRenderer().update(this.getUiContext(), this.getUiMarkers(), marker, updateType);
this.repaintUiMarker();
}
},
/**
* Hide a UI marker.
* @param marker
*/
hideUiMarker: function(marker, updateRenderer)
{
marker.setVisible(false);
// notify change and update renderer
if (updateRenderer)
{
//this.getUiRenderer().update(this.getUiContext(), this.getUiMarkers(), marker, Kekule.Render.ObjectUpdateType.MODIFY);
this.repaintUiMarker();
}
},
/**
* Show an UI marker.
* @param marker
* @param updateRenderer
*/
showUiMarker: function(marker, updateRenderer)
{
marker.setVisible(true);
if (updateRenderer)
{
//this.getUiRenderer().update(this.getUiContext(), this.getUiMarkers(), marker, Kekule.Render.ObjectUpdateType.MODIFY);
this.repaintUiMarker();
}
},
/**
* Remove a marker from collection.
* @private
*/
removeUiMarker: function(marker)
{
if (marker)
{
this.getUiMarkers().removeMarker(marker);
//this.getUiRenderer().update(this.getUiContext(), this.getUiMarkers(), marker, Kekule.Render.ObjectUpdateType.REMOVE);
this.repaintUiMarker();
}
},
/**
* Clear all UI markers.
* @private
*/
clearUiMarkers: function()
{
this.getUiMarkers().clearMarkers();
//this.getUiRenderer().redraw(this.getUiContext());
//this.redraw();
this.repaintUiMarker();
},
/**
* Modify hot track marker to bind to newBoundInfos.
* @private
*/
changeHotTrackMarkerBounds: function(newBoundInfos)
{
var infos = Kekule.ArrayUtils.toArray(newBoundInfos);
//var updateType = Kekule.Render.ObjectUpdateType.MODIFY;
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
var drawStyles = {
'color': styleConfigs.getHotTrackerColor(),
'opacity': styleConfigs.getHotTrackerOpacity()
};
var inflation = this.getCurrBoundInflation() || this.getEditorConfigs().getInteractionConfigs().getObjBoundTrackMinInflation();
var bounds = [];
for (var i = 0, l = infos.length; i < l; ++i)
{
var boundInfo = infos[i];
var bound = inflation? Kekule.Render.MetaShapeUtils.inflateShape(boundInfo, inflation): boundInfo;
//console.log('inflate', bound);
if (bound)
bounds.push(bound);
}
var tracker = this.getUiHotTrackMarker();
//console.log('change hot track', bound, drawStyles);
tracker.setVisible(true);
this.modifyShapeBasedMarker(tracker, bounds, drawStyles, true);
return this;
},
/**
* Hide hot track marker.
* @private
*/
hideHotTrackMarker: function()
{
var tracker = this.getUiHotTrackMarker();
if (tracker)
{
this.hideUiMarker(tracker, true);
}
return this;
},
/**
* Show hot track marker.
* @private
*/
showHotTrackMarker: function()
{
var tracker = this.getUiHotTrackMarker();
if (tracker)
{
this.showUiMarker(tracker, true);
}
return this;
},
/////////////////////////////////////////////////////////////////////////////
// methods about hot track marker
/**
* Try hot track object on coord.
* @param {Hash} screenCoord Coord based on screen system.
*/
hotTrackOnCoord: function(screenCoord)
{
if (this.getEditorConfigs().getInteractionConfigs().getEnableHotTrack())
{
/*
var boundItem = this.getTopmostBoundInfoAtCoord(screenCoord);
if (boundItem) // mouse move into object
{
var obj = boundItem.obj;
if (obj)
this.setHotTrackedObj(obj);
//this.changeHotTrackMarkerBound(boundItem.boundInfo);
}
else // mouse move out from object
{
this.setHotTrackedObj(null);
}
*/
//console.log('hot track here');
this.setHotTrackedObj(this.getTopmostBasicObjectAtCoord(screenCoord, this.getCurrBoundInflation()));
}
return this;
},
/**
* Hot try on a basic drawn object.
* @param {Object} obj
*/
hotTrackOnObj: function(obj)
{
this.setHotTrackedObj(obj);
return this;
},
/**
* Remove all hot track markers.
* @param {Bool} doNotClearHotTrackedObjs If false, the hotTrackedObjs property will also be set to empty.
*/
hideHotTrack: function(doNotClearHotTrackedObjs)
{
this.hideHotTrackMarker();
if (!doNotClearHotTrackedObjs)
this.clearHotTrackedObjs();
return this;
},
/**
* Set hot tracked objects to empty.
*/
clearHotTrackedObjs: function()
{
this.setHotTrackedObjs([]);
},
/**
* Add a obj to hot tracked objects.
* @param {Object} obj
*/
addHotTrackedObj: function(obj)
{
var olds = this.getHotTrackedObjs() || [];
Kekule.ArrayUtils.pushUnique(olds, obj);
this.setHotTrackedObjs(olds);
return this;
},
/** @private */
recalcHotTrackMarker: function()
{
this.setHotTrackedObjs(this.getHotTrackedObjs());
},
// methods about selecting marker
/**
* Modify hot track marker to bind to newBoundInfo.
* @private
*/
changeSelectionAreaMarkerBound: function(newBoundInfo, drawStyles)
{
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
if (!drawStyles)
drawStyles = {
'strokeColor': styleConfigs.getSelectionMarkerStrokeColor(),
'strokeWidth': styleConfigs.getSelectionMarkerStrokeWidth(),
'fillColor': styleConfigs.getSelectionMarkerFillColor(),
'opacity': styleConfigs.getSelectionMarkerOpacity()
};
//console.log(drawStyles);
var marker = this.getUiSelectionAreaMarker();
if (marker)
{
marker.setVisible(true);
this.modifyShapeBasedMarker(marker, newBoundInfo, drawStyles, true);
}
return this;
},
/** @private */
hideSelectionAreaMarker: function()
{
var marker = this.getUiSelectionAreaMarker();
if (marker)
{
this.hideUiMarker(marker, true);
}
},
/** @private */
showSelectionAreaMarker: function()
{
var marker = this.getUiSelectionAreaMarker();
if (marker)
{
this.showUiMarker(marker, true);
}
},
/**
* Recalculate and repaint selection marker.
* @private
*/
recalcSelectionAreaMarker: function(doRepaint)
{
this.beginUpdateUiMarkers();
try
{
// debug
var selection = this.getSelection();
var count = selection.length;
if (count <= 0)
this.hideSelectionAreaMarker();
else
{
var bounds = [];
var containerBox = null;
var inflation = this.getEditorConfigs().getInteractionConfigs().getSelectionMarkerInflation();
for (var i = 0; i < count; ++i)
{
var obj = selection[i];
var infos = this.getBoundInfoRecorder().getBelongedInfos(this.getObjContext(), obj);
if (infos && infos.length)
{
for (var j = 0, k = infos.length; j < k; ++j)
{
var info = infos[j];
var bound = info.boundInfo;
if (bound)
{
// inflate
bound = Kekule.Render.MetaShapeUtils.inflateShape(bound, inflation);
bounds.push(bound);
var box = Kekule.Render.MetaShapeUtils.getContainerBox(bound);
containerBox = containerBox? Kekule.BoxUtils.getContainerBox(containerBox, box): box;
}
}
}
}
//var containerBox = this.getSelectionContainerBox(inflation);
this.setUiSelectionAreaContainerBox(containerBox);
// container box
if (containerBox)
{
var containerShape = Kekule.Render.MetaShapeUtils.createShapeInfo(
Kekule.Render.MetaShapeType.RECT,
[{'x': containerBox.x1, 'y': containerBox.y1}, {'x': containerBox.x2, 'y': containerBox.y2}]
);
bounds.push(containerShape);
}
else // containerBox disappear, may be a node or connector merge, hide selection area
this.hideSelectionAreaMarker();
//console.log(bounds.length, bounds);
if (bounds.length)
this.changeSelectionAreaMarkerBound(bounds);
}
}
finally
{
this.endUpdateUiMarkers();
}
},
/** @private */
_highlightSelectionAreaMarker: function()
{
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
var highlightStyles = {
'strokeColor': styleConfigs.getSelectionMarkerStrokeColor(),
'strokeWidth': styleConfigs.getSelectionMarkerStrokeWidth(),
'fillColor': styleConfigs.getSelectionMarkerFillColor(),
'opacity': styleConfigs.getSelectionMarkerEmphasisOpacity()
};
this.changeSelectionAreaMarkerBound(null, highlightStyles); // change draw styles without the modification of bound
},
/** @private */
_restoreSelectionAreaMarker: function()
{
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
var highlightStyles = {
'strokeColor': styleConfigs.getSelectionMarkerStrokeColor(),
'strokeWidth': styleConfigs.getSelectionMarkerStrokeWidth(),
'fillColor': styleConfigs.getSelectionMarkerFillColor(),
'opacity': styleConfigs.getSelectionMarkerOpacity()
};
this.changeSelectionAreaMarkerBound(null, highlightStyles); // change draw styles without the modification of bound
},
/**
* Pulse selection marker several times to get the attention of user.
* @param {Int} duration Duration of the whole process, in ms.
* @param {Int} pulseCount The times of highlighting marker.
*/
pulseSelectionAreaMarker: function(duration, pulseCount)
{
if (this.getUiSelectionAreaMarker())
{
if (!duration)
duration = this.getEditorConfigs().getInteractionConfigs().getSelectionMarkerDefPulseDuration() || 0;
if (!pulseCount)
pulseCount = this.getEditorConfigs().getInteractionConfigs().getSelectionMarkerDefPulseCount() || 1;
if (!duration)
return;
var interval = duration / pulseCount;
this.doPulseSelectionAreaMarker(interval, pulseCount);
}
return this;
},
/** @private */
doPulseSelectionAreaMarker: function(interval, pulseCount)
{
this._highlightSelectionAreaMarker();
//if (pulseCount <= 1)
setTimeout(this._restoreSelectionAreaMarker.bind(this), interval);
if (pulseCount > 1)
setTimeout(this.doPulseSelectionAreaMarker.bind(this, interval, pulseCount - 1), interval * 2);
},
///////////////////////// Methods about selecting region ////////////////////////////////////
/**
* Start a selecting operation from coord.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the selecting region will toggle selecting state inside it rather than select them directly.
*/
startSelecting: function(screenCoord, toggleFlag)
{
if (toggleFlag === undefined)
toggleFlag = this.getIsToggleSelectOn();
if (!toggleFlag)
this.deselectAll();
var M = Kekule.Editor.SelectMode;
var mode = this.getSelectMode();
this._currSelectMode = mode;
return (mode === M.POLYLINE || mode === M.POLYGON)?
this.startSelectingCurveDrag(screenCoord, toggleFlag):
this.startSelectingBoxDrag(screenCoord, toggleFlag);
},
/**
* Add a new anchor coord of selecting region.
* This method is called when pointer device moving in selecting.
* @param {Hash} screenCoord
*/
addSelectingAnchorCoord: function(screenCoord)
{
var M = Kekule.Editor.SelectMode;
var mode = this._currSelectMode;
return (mode === M.POLYLINE || mode === M.POLYGON)?
this.dragSelectingCurveToCoord(screenCoord):
this.dragSelectingBoxToCoord(screenCoord);
},
/**
* Selecting operation end.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the selecting region will toggle selecting state inside it rather than select them directly.
*/
endSelecting: function(screenCoord, toggleFlag)
{
if (toggleFlag === undefined)
toggleFlag = this.getIsToggleSelectOn();
var M = Kekule.Editor.SelectMode;
var mode = this._currSelectMode;
var enablePartial = this.getEditorConfigs().getInteractionConfigs().getEnablePartialAreaSelecting();
var objs;
if (mode === M.POLYLINE || mode === M.POLYGON)
{
var polygonCoords = this._selectingCurveCoords;
// simplify the polygon first
var threshold = this.getEditorConfigs().getInteractionConfigs().getSelectingCurveSimplificationDistanceThreshold();
var simpilfiedCoords = Kekule.GeometryUtils.simplifyCurveToLineSegments(polygonCoords, threshold);
//console.log('simplify selection', polygonCoords.length, simpilfiedCoords.length);
this.endSelectingCurveDrag(screenCoord, toggleFlag);
if (mode === M.POLYLINE)
{
var lineWidth = this.getEditorConfigs().getInteractionConfigs().getSelectingBrushWidth();
objs = this.getObjectsIntersetExtendedPolyline(simpilfiedCoords, lineWidth);
}
else // if (mode === M.POLYGON)
{
objs = this.getObjectsInPolygon(simpilfiedCoords, enablePartial);
this.endSelectingCurveDrag(screenCoord, toggleFlag);
}
}
else // M.RECT or M.ANCESTOR
{
var startCoord = this._selectingBoxStartCoord;
var box = Kekule.BoxUtils.createBox(startCoord, screenCoord);
objs = this.getObjectsInScreenBox(box, enablePartial);
this.endSelectingBoxDrag(screenCoord, toggleFlag);
}
/*
if (objs && objs.length)
{
if (this._isInAncestorSelectMode()) // need to change to select standalone ancestors
{
objs = this._getAllStandaloneAncestorObjs(objs); // get standalone ancestors (e.g. molecule)
//objs = this._getAllCoordDependantObjs(objs); // but select there coord dependant children (e.g. atoms and bonds)
}
if (toggleFlag)
this.toggleSelectingState(objs);
else
this.select(objs);
}
*/
objs = this._getActualSelectedObjsInSelecting(objs);
if (toggleFlag)
this.toggleSelectingState(objs);
else
this.select(objs);
this.hideSelectingMarker();
},
/**
* Cancel current selecting operation.
*/
cancelSelecting: function()
{
this.hideSelectingMarker();
},
/** @private */
_getActualSelectedObjsInSelecting: function(objs)
{
if (objs && objs.length)
{
if (this._isInAncestorSelectMode()) // need to change to select standalone ancestors
{
objs = this._getAllStandaloneAncestorObjs(objs); // get standalone ancestors (e.g. molecule)
//objs = this._getAllCoordDependantObjs(objs); // but select there coord dependant children (e.g. atoms and bonds)
}
return objs;
}
else
return [];
},
/** @private */
_isInAncestorSelectMode: function()
{
return this.getSelectMode() === Kekule.Editor.SelectMode.ANCESTOR;
},
/** @private */
_getAllStandaloneAncestorObjs: function(objs)
{
var result = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (obj && obj.getStandaloneAncestor)
obj = obj.getStandaloneAncestor();
AU.pushUnique(result, obj);
}
return result;
},
/* @private */
/*
_getAllCoordDependantObjs: function(objs)
{
var result = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (obj && obj.getCoordDependentObjects)
AU.pushUnique(result, obj.getCoordDependentObjects());
}
return result;
},
*/
/**
* Start to drag a selecting box from coord.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the box will toggle selecting state inside it rather than select them directly.
*/
startSelectingBoxDrag: function(screenCoord, toggleFlag)
{
//this.setInteractionStartCoord(screenCoord);
this._selectingBoxStartCoord = screenCoord;
/*
if (!toggleFlag)
this.deselectAll();
*/
//this.setEditorState(Kekule.Editor.EditorState.SELECTING);
},
/**
* Drag selecting box to a new coord.
* @param {Hash} screenCoord
*/
dragSelectingBoxToCoord: function(screenCoord)
{
//var startCoord = this.getInteractionStartCoord();
var startCoord = this._selectingBoxStartCoord;
var endCoord = screenCoord;
this.changeSelectingMarkerBox(startCoord, endCoord);
},
/**
* Selecting box drag end.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the box will toggle selecting state inside it rather than select them directly.
*/
endSelectingBoxDrag: function(screenCoord, toggleFlag)
{
//var startCoord = this.getInteractionStartCoord();
var startCoord = this._selectingBoxStartCoord;
//this.setInteractionEndCoord(coord);
this._selectingBoxEndCoord = screenCoord;
/*
var box = Kekule.BoxUtils.createBox(startCoord, screenCoord);
var enablePartial = this.getEditorConfigs().getInteractionConfigs().getEnablePartialAreaSelecting();
if (toggleFlag)
this.toggleSelectingStateOfObjectsInScreenBox(box, enablePartial);
else
this.selectObjectsInScreenBox(box, enablePartial);
this.hideSelectingMarker();
*/
},
/**
* Start to drag a selecting curve from coord.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the box will toggle selecting state inside it rather than select them directly.
*/
startSelectingCurveDrag: function(screenCoord, toggleFlag)
{
//this.setInteractionStartCoord(screenCoord);
this._selectingCurveCoords = [screenCoord];
//this.setEditorState(Kekule.Editor.EditorState.SELECTING);
},
/**
* Drag selecting curve to a new coord.
* @param {Hash} screenCoord
*/
dragSelectingCurveToCoord: function(screenCoord)
{
//var startCoord = this.getInteractionStartCoord();
this._selectingCurveCoords.push(screenCoord);
this.changeSelectingMarkerCurve(this._selectingCurveCoords, this._currSelectMode === Kekule.Editor.SelectMode.POLYGON);
},
/**
* Selecting curve drag end.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the box will toggle selecting state inside it rather than select them directly.
*/
endSelectingCurveDrag: function(screenCoord, toggleFlag)
{
this._selectingCurveCoords.push(screenCoord);
/*
var box = Kekule.BoxUtils.createBox(startCoord, screenCoord);
var enablePartial = this.getEditorConfigs().getInteractionConfigs().getEnablePartialAreaSelecting();
if (toggleFlag)
this.toggleSelectingStateOfObjectsInScreenBox(box, enablePartial);
else
this.selectObjectsInScreenBox(box, enablePartial);
this.hideSelectingMarker();
*/
},
/**
* Try select a object on coord directly.
* @param {Hash} coord
* @param {Bool} toggleFlag If true, the box will toggle selecting state inside it rather than select them directly.
*/
selectOnCoord: function(coord, toggleFlag)
{
if (toggleFlag === undefined)
toggleFlag = this.getIsToggleSelectOn();
//console.log('select on coord');
var obj = this.getTopmostBasicObjectAtCoord(coord, this.getCurrBoundInflation());
if (obj)
{
var objs = this._getActualSelectedObjsInSelecting([obj]);
if (objs)
{
if (toggleFlag)
this.toggleSelectingState(objs);
else
this.select(objs);
}
}
},
// about selection area marker
/**
* Modify hot track marker to bind to newBoundInfo.
* @private
*/
changeSelectingMarkerBound: function(newBoundInfo, drawStyles)
{
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
if (!drawStyles) // use the default one
drawStyles = {
'strokeColor': styleConfigs.getSelectingMarkerStrokeColor(),
'strokeWidth': styleConfigs.getSelectingMarkerStrokeWidth(),
'strokeDash': styleConfigs.getSelectingMarkerStrokeDash(),
'fillColor': styleConfigs.getSelectingMarkerFillColor(),
'opacity': styleConfigs.getSelectingMarkerOpacity()
};
var marker = this.getUiSelectingMarker();
marker.setVisible(true);
//console.log('change hot track', bound, drawStyles);
this.modifyShapeBasedMarker(marker, newBoundInfo, drawStyles, true);
return this;
},
changeSelectingMarkerCurve: function(screenCoords, isPolygon)
{
var ctxCoords = [];
for (var i = 0, l = screenCoords.length - 1; i < l; ++i)
{
ctxCoords.push(this.screenCoordToContext(screenCoords[i]));
}
var shapeInfo = Kekule.Render.MetaShapeUtils.createShapeInfo(
isPolygon? Kekule.Render.MetaShapeType.POLYGON: Kekule.Render.MetaShapeType.POLYLINE,
ctxCoords
);
var drawStyle;
if (!isPolygon)
{
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
drawStyle = {
'strokeColor': styleConfigs.getSelectingBrushMarkerStrokeColor(),
'strokeWidth': this.getEditorConfigs().getInteractionConfigs().getSelectingBrushWidth(),
'strokeDash': styleConfigs.getSelectingBrushMarkerStrokeDash(),
//'fillColor': styleConfigs.getSelectingMarkerFillColor(),
'lineCap': styleConfigs.getSelectingBrushMarkerStrokeLineCap(),
'lineJoin': styleConfigs.getSelectingBrushMarkerStrokeLineJoin(),
'opacity': styleConfigs.getSelectingBrushMarkerOpacity()
};
}
return this.changeSelectingMarkerBound(shapeInfo, drawStyle);
},
/**
* Change the rect box of selection marker.
* Coord is based on screen system.
* @private
*/
changeSelectingMarkerBox: function(screenCoord1, screenCoord2)
{
//var coord1 = this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), screenCoord1);
//var coord2 = this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), screenCoord2);
var coord1 = this.screenCoordToContext(screenCoord1);
var coord2 = this.screenCoordToContext(screenCoord2);
var shapeInfo = Kekule.Render.MetaShapeUtils.createShapeInfo(
Kekule.Render.MetaShapeType.RECT,
[{'x': Math.min(coord1.x, coord2.x), 'y': Math.min(coord1.y, coord2.y)},
{'x': Math.max(coord1.x, coord2.x), 'y': Math.max(coord1.y, coord2.y)}]
);
return this.changeSelectingMarkerBound(shapeInfo);
},
/** @private */
hideSelectingMarker: function()
{
var marker = this.getUiSelectingMarker();
if (marker)
{
this.hideUiMarker(marker, true);
}
},
/** @private */
showSelectingMarker: function()
{
var marker = this.getUiSelectingMarker();
if (marker)
{
this.showUiMarker(marker, true);
}
},
// methods about selection marker
/**
* Returns the region of screenCoord relative to selection marker.
* @private
*/
getCoordRegionInSelectionMarker: function(screenCoord, edgeInflation)
{
var R = Kekule.Editor.BoxRegion;
var CU = Kekule.CoordUtils;
var coord = this.screenCoordToContext(screenCoord);
var marker = this.getUiSelectionAreaMarker();
if (marker && marker.getVisible())
{
var box = this.getUiSelectionAreaContainerBox();
if (Kekule.ObjUtils.isUnset(edgeInflation))
edgeInflation = this.getEditorConfigs().getInteractionConfigs().getSelectionMarkerEdgeInflation();
var halfInf = (edgeInflation / 2) || 0;
var coord1 = CU.substract({'x': box.x1, 'y': box.y1}, {'x': halfInf, 'y': halfInf});
var coord2 = CU.add({'x': box.x2, 'y': box.y2}, {'x': halfInf, 'y': halfInf});
if ((coord.x < coord1.x) || (coord.y < coord1.y) || (coord.x > coord2.x) || (coord.y > coord2.y))
return R.OUTSIDE;
//coord2 = CU.substract(coord2, coord1);
var delta1 = CU.substract(coord, coord1);
var delta2 = CU.substract(coord2, coord);
var dx1 = delta1.x;
var dx2 = delta2.x;
var dy1 = delta1.y;
var dy2 = delta2.y;
if (dy1 < dy2) // on top half
{
if (dx1 < dx2) // on left part
{
if (dy1 <= edgeInflation)
return (dx1 <= edgeInflation)? R.CORNER_TL: R.EDGE_TOP;
else if (dx1 <= edgeInflation)
return R.EDGE_LEFT;
else
return R.INSIDE;
}
else // on right part
{
if (dy1 <= edgeInflation)
return (dx2 <= edgeInflation)? R.CORNER_TR: R.EDGE_TOP;
else if (dx2 <= edgeInflation)
return R.EDGE_RIGHT;
else
return R.INSIDE;
}
}
else // on bottom half
{
if (dx1 < dx2) // on left part
{
if (dy2 <= edgeInflation)
return (dx1 <= edgeInflation)? R.CORNER_BL: R.EDGE_BOTTOM;
else if (dx1 <= edgeInflation)
return R.EDGE_LEFT;
else
return R.INSIDE;
}
else // on right part
{
if (dy2 <= edgeInflation)
return (dx2 <= edgeInflation)? R.CORNER_BR: R.EDGE_BOTTOM;
else if (dx2 <= edgeInflation)
return R.EDGE_RIGHT;
else
return R.INSIDE;
}
}
}
return R.OUTSIDE;
},
/**
* Check if a point coord based on screen inside selection marker.
* @private
*/
isCoordInSelectionMarkerBound: function(screenCoord)
{
/*
//var coord = this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), screenCoord);
var coord = this.screenCoordToContext(screenCoord);
var marker = this.getUiSelectionAreaMarker();
if (marker && marker.getVisible())
{
var shapeInfo = marker.getShapeInfo();
return shapeInfo? Kekule.Render.MetaShapeUtils.isCoordInside(coord, shapeInfo): false;
}
else
return false;
*/
return (this.getCoordRegionInSelectionMarker(screenCoord) !== Kekule.Editor.BoxRegion.OUTSIDE);
},
//////////////////////////////////////////////////////////////////////////////
/////////////////////// methods about selection ////////////////////////////
/**
* Returns override render options that need to be applied to each selected objects.
* Descendants should override this method.
* @returns {Hash}
* @private
*/
getObjSelectedRenderOptions: function()
{
// debug
/*
if (!this._selectedRenderOptions)
this._selectedRenderOptions = {'color': '#000055', 'strokeWidth': 2, 'atomRadius': 5};
*/
return this._selectedRenderOptions;
},
/**
* Returns method to add render option override item of chemObj.
* In 2D render mode, this method should returns chemObj.addOverrideRenderOptionItem,
* in 3D render mode, this method should returns chemObj.addOverrideRender3DOptionItem.
* @private
*/
_getObjRenderOptionItemAppendMethod: function(chemObj)
{
return (this.getRenderType() === Kekule.Render.RendererType.R3D)?
chemObj.addOverrideRender3DOptionItem:
chemObj.addOverrideRenderOptionItem;
},
/**
* Returns method to remove render option override item of chemObj.
* In 2D render mode, this method should returns chemObj.removeOverrideRenderOptionItem,
* in 3D render mode, this method should returns chemObj.removeOverrideRender3DOptionItem.
* @private
*/
_getObjRenderOptionItemRemoveMethod: function(chemObj)
{
return (this.getRenderType() === Kekule.Render.RendererType.R3D )?
chemObj.removeOverrideRender3DOptionItem:
chemObj.removeOverrideRenderOptionItem;
},
/** @private */
_addSelectRenderOptions: function(chemObj)
{
var selOps = this.getObjSelectedRenderOptions();
if (selOps)
{
//console.log('_addSelectRenderOptions', chemObj);
var method = this._getObjRenderOptionItemAppendMethod(chemObj);
//if (!method)
//console.log(chemObj.getClassName());
return method.apply(chemObj, [selOps]);
}
else
return null;
},
/** @private */
_removeSelectRenderOptions: function(chemObj)
{
var selOps = this.getObjSelectedRenderOptions();
if (selOps)
{
//console.log('_removeSelectRenderOptions', chemObj);
var method = this._getObjRenderOptionItemRemoveMethod(chemObj);
return method.apply(chemObj, [this.getObjSelectedRenderOptions()]);
}
else
return null;
},
/** Notify that a continuous selection update is underway. UI need not to be changed. */
beginUpdateSelection: function()
{
this.beginUpdateObject();
--this._objSelectFlag;
},
/** Notify that a continuous selection update is done. UI need to be changed. */
endUpdateSelection: function()
{
++this._objSelectFlag;
if (this._objSelectFlag >= 0)
{
this.selectionChanged();
}
this.endUpdateObject();
},
/** Check if the editor is under continuous selection update. */
isUpdatingSelection: function()
{
return (this._objSelectFlag < 0);
},
/**
* Notify selection is changed or object in selection has changed.
* @private
*/
selectionChanged: function()
{
/*
var selection = this.getSelection();
if (selection && selection.length) // at least one selected object
{
var obj, boundItem, bound, box;
var containBox;
// calc out bound box to contain all selected objects
for (var i = 0, l = selection.length; i < l; ++i)
{
obj = selection[i];
boundItem = this.findBoundMapItem(obj);
if (boundItem)
{
bound = boundItem.boundInfo;
if (bound)
{
box = Kekule.Render.MetaShapeUtils.getContainerBox(bound);
if (box)
{
if (!containBox)
containBox = box;
else
containBox = Kekule.BoxUtils.getContainerBox(containBox, box);
}
}
}
}
if (containBox)
{
var inflation = this.getEditorConfigs().getInteractionConfigs().getSelectionMarkerInflation() || 0;
if (inflation)
containBox = Kekule.BoxUtils.inflateBox(containBox, inflation);
this.changeSelectionMarkerBox(containBox);
}
else // no selected
this.removeSelectionMarker();
}
else // no selected
{
this.removeSelectionMarker();
}
*/
if (!this.isUpdatingSelection())
{
this.notifyPropSet('selection', this.getSelection());
this.invokeEvent('selectionChange');
return this.doSelectionChanged();
}
},
/**
* Do actual work of method selectionChanged.
* Descendants may override this method.
*/
doSelectionChanged: function()
{
this.recalcSelectionAreaMarker();
},
/**
* Check if an object is in selection.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
*/
isInSelection: function(obj)
{
return this.getSelection().indexOf(obj) >= 0;
},
/**
* Add an object to selection.
* Descendants can override this method.
* @param {Kekule.ChemObject} obj
*/
addObjToSelection: function(obj)
{
var selection = this.getSelection();
Kekule.ArrayUtils.pushUnique(selection, obj.getNearestSelectableObject());
this._addSelectRenderOptions(obj);
this.selectionChanged();
return this;
},
/**
* Remove an object (and all its child objects) from selection.
* Descendants can override this method.
* @param {Kekule.ChemObject} obj
*/
removeObjFromSelection: function(obj, doNotNotifySelectionChange)
{
var selection = this.getSelection();
var relObj = obj.getNearestSelectableObject && obj.getNearestSelectableObject();
if (relObj === obj)
relObj === null;
Kekule.ArrayUtils.remove(selection, obj);
this._removeSelectRenderOptions(obj);
if (relObj)
{
Kekule.ArrayUtils.remove(selection, relObj);
this._removeSelectRenderOptions(relObj);
}
// remove possible child objects
for (var i = selection.length - 1; i >= 0; --i)
{
var remainObj = selection[i];
if (remainObj.isChildOf && (remainObj.isChildOf(obj) || (relObj && remainObj.isChildOf(relObj))))
this.removeObjFromSelection(remainObj, true);
}
if (!doNotNotifySelectionChange)
this.selectionChanged();
return this;
},
/**
* Deselect all objects in selection
*/
deselectAll: function()
{
var selection = this.getSelection();
return this.removeFromSelection(selection);
},
/**
* Make a obj or set of objs be selected.
* @param {Variant} objs A object or an array of objects.
*/
select: function(objs)
{
this.beginUpdateSelection();
try
{
this.deselectAll();
this.addToSelection(objs);
}
finally
{
//console.log(this.getPainter().getRenderer().getClassName(), this.getPainter().getRenderer().getRenderCache(this.getDrawContext()));
this.endUpdateSelection();
}
return this;
},
/**
* Add object or an array of objects to selection.
* @param {Variant} param A object or an array of objects.
*/
addToSelection: function(param)
{
if (!param)
return;
var objs = DataType.isArrayValue(param)? param: [param];
this.beginUpdateSelection();
try
{
for (var i = 0, l = objs.length; i < l; ++i)
{
this.addObjToSelection(objs[i]);
}
}
finally
{
this.endUpdateSelection();
}
return this;
},
/**
* Remove object or an array of objects from selection.
* @param {Variant} param A object or an array of objects.
*/
removeFromSelection: function(param)
{
if (!param)
return;
var objs = DataType.isArrayValue(param)? param: [param];
this.beginUpdateSelection();
try
{
for (var i = objs.length - 1; i >= 0; --i)
{
this.removeObjFromSelection(objs[i]);
}
}
finally
{
this.endUpdateSelection();
}
return this;
},
/**
* Toggle selection state of object or an array of objects.
* @param {Variant} param A object or an array of objects.
*/
toggleSelectingState: function(param)
{
if (!param)
return;
var objs = DataType.isArrayValue(param)? param: [param];
this.beginUpdateSelection();
try
{
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var relObj = obj.getNearestSelectableObject && obj.getNearestSelectableObject();
if (this.isInSelection(obj))
this.removeObjFromSelection(obj);
else if (relObj && this.isInSelection(relObj))
this.removeObjFromSelection(relObj);
else
this.addObjToSelection(obj);
}
}
finally
{
this.endUpdateSelection();
}
return this;
},
/**
* Check if there is objects selected currently.
* @returns {Bool}
*/
hasSelection: function()
{
return !!this.getSelection().length;
},
/**
* Delete and free all selected objects.
*/
deleteSelectedObjs: function()
{
// TODO: unfinished
},
/**
* Get all objects interset a polyline defined by a set of screen coords.
* Here Object partial in the polyline width range will also be put in result.
* @param {Array} polylineScreenCoords
* @param {Number} lineWidth
* @returns {Array} All interseting objects.
*/
getObjectsIntersetExtendedPolyline: function(polylineScreenCoords, lineWidth)
{
var ctxCoords = [];
for (var i = 0, l = polylineScreenCoords.length; i < l; ++i)
{
ctxCoords.push(this.screenCoordToContext(polylineScreenCoords[i]));
}
var objs = [];
var boundInfos = this.getBoundInfoRecorder().getAllRecordedInfoOfContext(this.getObjContext());
var compareFunc = Kekule.Render.MetaShapeUtils.isIntersectingPolyline;
for (var i = 0, l = boundInfos.length; i < l; ++i)
{
var boundInfo = boundInfos[i];
var shapeInfo = boundInfo.boundInfo;
/*
if (!shapeInfo)
console.log(boundInfo);
*/
if (shapeInfo)
if (compareFunc(shapeInfo, ctxCoords, lineWidth))
objs.push(boundInfo.obj);
}
//console.log('selected', objs);
return objs;
},
/**
* Get all objects inside a polygon defined by a set of screen coords.
* @param {Array} polygonScreenCoords
* @param {Bool} allowPartialAreaSelecting If this value is true, object partial in the box will also be selected.
* @returns {Array} All inside objects.
*/
getObjectsInPolygon: function(polygonScreenCoords, allowPartialAreaSelecting)
{
var ctxCoords = [];
for (var i = 0, l = polygonScreenCoords.length; i < l; ++i)
{
ctxCoords.push(this.screenCoordToContext(polygonScreenCoords[i]));
}
var objs = [];
var boundInfos = this.getBoundInfoRecorder().getAllRecordedInfoOfContext(this.getObjContext());
var compareFunc = allowPartialAreaSelecting? Kekule.Render.MetaShapeUtils.isIntersectingPolygon: Kekule.Render.MetaShapeUtils.isInsidePolygon;
for (var i = 0, l = boundInfos.length; i < l; ++i)
{
var boundInfo = boundInfos[i];
var shapeInfo = boundInfo.boundInfo;
/*
if (!shapeInfo)
console.log(boundInfo);
*/
if (shapeInfo)
if (compareFunc(shapeInfo, ctxCoords))
objs.push(boundInfo.obj);
}
//console.log('selected', objs);
return objs;
},
/**
* Get all objects inside a screen box.
* @param {Hash} screenBox
* @param {Bool} allowPartialAreaSelecting If this value is true, object partial in the box will also be selected.
* @returns {Array} All inside objects.
*/
getObjectsInScreenBox: function(screenBox, allowPartialAreaSelecting)
{
var box = this.screenBoxToContext(screenBox);
var objs = [];
var boundInfos = this.getBoundInfoRecorder().getAllRecordedInfoOfContext(this.getObjContext());
var compareFunc = allowPartialAreaSelecting? Kekule.Render.MetaShapeUtils.isIntersectingBox: Kekule.Render.MetaShapeUtils.isInsideBox;
for (var i = 0, l = boundInfos.length; i < l; ++i)
{
var boundInfo = boundInfos[i];
var shapeInfo = boundInfo.boundInfo;
/*
if (!shapeInfo)
console.log(boundInfo);
*/
if (shapeInfo)
if (compareFunc(shapeInfo, box))
objs.push(boundInfo.obj);
}
//console.log('selected', objs);
return objs;
},
/**
* Select all objects inside a screen box.
* @param {Hash} box
* @param {Bool} allowPartialAreaSelecting If this value is true, object partial in the box will also be selected.
* @returns {Array} All inside objects.
*/
selectObjectsInScreenBox: function(screenBox, allowPartialAreaSelecting)
{
var objs = this.getObjectsInScreenBox(screenBox, allowPartialAreaSelecting);
if (objs && objs.length)
this.select(objs);
return objs;
},
/**
* Add objects inside a screen box to selection.
* @param {Hash} box
* @param {Bool} allowPartialAreaSelecting If this value is true, object partial in the box will also be selected.
* @returns {Array} All inside objects.
*/
addObjectsInScreenBoxToSelection: function(screenBox, allowPartialAreaSelecting)
{
var objs = this.getObjectsInScreenBox(screenBox, allowPartialAreaSelecting);
if (objs && objs.length)
this.addToSelection(objs);
return objs;
},
/**
* Remove objects inside a screen box from selection.
* @param {Hash} box
* @param {Bool} allowPartialAreaSelecting If this value is true, object partial in the box will also be deselected.
* @returns {Array} All inside objects.
*/
removeObjectsInScreenBoxFromSelection: function(screenBox, allowPartialAreaSelecting)
{
var objs = this.getObjectsInScreenBox(screenBox, allowPartialAreaSelecting);
if (objs && objs.length)
this.removeFromSelection(objs);
return objs;
},
/**
* Toggle selection state of objects inside a screen box.
* @param {Hash} box
* @param {Bool} allowPartialAreaSelecting If this value is true, object partial in the box will also be toggled.
* @returns {Array} All inside objects.
*/
toggleSelectingStateOfObjectsInScreenBox: function(screenBox, allowPartialAreaSelecting)
{
var objs = this.getObjectsInScreenBox(screenBox, allowPartialAreaSelecting);
if (objs && objs.length)
this.toggleSelectingState(objs);
return objs;
},
/**
* Returns a minimal box (in screen coord system) containing all objects' bounds in editor.
* @param {Array} objects
* @param {Float} objBoundInflation Inflation of each object's bound.
* @returns {Hash}
*/
getObjectsContainerBox: function(objects, objBoundInflation)
{
var objs = Kekule.ArrayUtils.toArray(objects);
var inf = objBoundInflation || 0;
var bounds = [];
var containerBox = null;
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var infos = this.getBoundInfoRecorder().getBelongedInfos(this.getObjContext(), obj);
if (infos && infos.length)
{
for (var j = 0, k = infos.length; j < k; ++j)
{
var info = infos[j];
var bound = info.boundInfo;
if (bound)
{
// inflate
if (inf)
bound = Kekule.Render.MetaShapeUtils.inflateShape(bound, inf);
var box = Kekule.Render.MetaShapeUtils.getContainerBox(bound);
containerBox = containerBox? Kekule.BoxUtils.getContainerBox(containerBox, box): box;
}
}
}
}
return containerBox;
},
/**
* Returns container box (in screen coord system) that contains all objects in selection.
* @param {Number} objBoundInflation
* @returns {Hash}
*/
getSelectionContainerBox: function(objBoundInflation)
{
return this.getObjectsContainerBox(this.getSelection(), objBoundInflation);
},
/**
* Returns whether current selected objects can be seen from screen (not all of them
* are in hidden scroll area).
*/
isSelectionVisible: function()
{
var selectionBox = this.getSelectionContainerBox();
if (selectionBox)
{
var editorDim = this.getClientDimension();
var editorOffset = this.getClientScrollPosition();
var editorBox = {
'x1': editorOffset.x, 'y1': editorOffset.y,
'x2': editorOffset.x + editorDim.width, 'y2': editorOffset.y + editorDim.height
};
//console.log(selectionBox, editorBox, Kekule.BoxUtils.getIntersection(selectionBox, editorBox));
return Kekule.BoxUtils.hasIntersection(selectionBox, editorBox);
}
else
return false;
},
/////////// methods about object manipulations /////////////////////////////
/**
* Returns width and height info of obj.
* @param {Object} obj
* @returns {Hash}
*/
getObjSize: function(obj)
{
return this.doGetObjSize(obj);
},
/**
* Do actual job of getObjSize. Descendants may override this method.
* @param {Object} obj
* @returns {Hash}
*/
doGetObjSize: function(obj)
{
var coordMode = this.getCoordMode();
//var allowCoordBorrow = this.getAllowCoordBorrow();
return obj.getSizeOfMode? obj.getSizeOfMode(coordMode):
null;
},
/**
* Set dimension of obj.
* @param {Object} obj
* @param {Hash} size
*/
setObjSize: function(obj, size)
{
this.doSetObjSize(obj, size);
this.objectChanged(obj);
},
/**
* Do actual work of setObjSize.
* @param {Object} obj
* @param {Hash} size
*/
doSetObjSize: function(obj, size)
{
if (obj.setSizeOfMode)
obj.setSizeOfMode(dimension, this.getCoordMode());
},
/**
* Returns own coord of obj.
* @param {Object} obj
* @param {Int} coordPos Value from {@link Kekule.Render.CoordPos}, relative position of coord in object.
* @returns {Hash}
*/
getObjCoord: function(obj, coordPos)
{
return this.doGetObjCoord(obj, coordPos);
},
/**
* Do actual job of getObjCoord. Descendants may override this method.
* @private
*/
doGetObjCoord: function(obj, coordPos)
{
var coordMode = this.getCoordMode();
var allowCoordBorrow = this.getAllowCoordBorrow();
var result = obj.getAbsBaseCoord? obj.getAbsBaseCoord(coordMode, allowCoordBorrow):
obj.getAbsCoordOfMode? obj.getAbsCoordOfMode(coordMode, allowCoordBorrow):
obj.getCoordOfMode? obj.getCoordOfMode(coordMode, allowCoordBorrow):
null;
if (coordMode === Kekule.CoordMode.COORD2D && Kekule.ObjUtils.notUnset(coordPos)) // appoint coord pos, need further calculation
{
var baseCoordPos = Kekule.Render.CoordPos.DEFAULT;
if (coordPos !== baseCoordPos)
{
var allowCoordBorrow = this.getAllowCoordBorrow();
var box = obj.getExposedContainerBox? obj.getExposedContainerBox(coordMode, allowCoordBorrow):
obj.getContainerBox? obj.getContainerBox(coordMode, allowCoordBorrow): null;
//console.log(obj.getClassName(), coordPos, objBasePos, box);
if (box)
{
if (coordPos === Kekule.Render.CoordPos.CORNER_TL)
{
var delta = {x: (box.x2 - box.x1) / 2, y: (box.y2 - box.y1) / 2};
result.x = result.x - delta.x;
result.y = result.y + delta.y;
}
}
}
}
return result;
/*
return obj.getAbsBaseCoord2D? obj.getAbsBaseCoord2D(allowCoordBorrow):
obj.getAbsCoord2D? obj.getAbsCoord2D(allowCoordBorrow):
obj.getCoord2D? obj.getCoord2D(allowCoordBorrow):
null;
*/
},
/**
* Set own coord of obj.
* @param {Object} obj
* @param {Hash} coord
* @param {Int} coordPos Value from {@link Kekule.Render.CoordPos}, relative position of coord in object.
*/
setObjCoord: function(obj, coord, coordPos)
{
this.doSetObjCoord(obj, coord, coordPos);
this.objectChanged(obj);
},
/**
* Do actual job of setObjCoord. Descendants can override this method.
* @private
*/
doSetObjCoord: function(obj, coord, coordPos)
{
var newCoord = Object.create(coord);
var coordMode = this.getCoordMode();
//console.log(obj.setAbsBaseCoord, obj.setAbsCoordOfMode, obj.setAbsCoordOfMode);
if (coordMode === Kekule.CoordMode.COORD2D && Kekule.ObjUtils.notUnset(coordPos)) // appoint coord pos, need further calculation
{
//var baseCoordPos = obj.getCoordPos? obj.getCoordPos(coordMode): Kekule.Render.CoordPos.DEFAULT;
var baseCoordPos = Kekule.Render.CoordPos.DEFAULT;
if (coordPos !== baseCoordPos)
{
var allowCoordBorrow = this.getAllowCoordBorrow();
var box = obj.getExposedContainerBox? obj.getExposedContainerBox(coordMode, allowCoordBorrow):
obj.getContainerBox? obj.getContainerBox(coordMode, allowCoordBorrow): null;
//console.log(obj.getClassName(), coordPos, objBasePos, box);
if (box)
{
var delta = {x: (box.x2 - box.x1) / 2, y: (box.y2 - box.y1) / 2};
if (coordPos === Kekule.Render.CoordPos.CORNER_TL)
// base coord on center and set coord as top left
{
newCoord.x = coord.x + delta.x;
newCoord.y = coord.y - delta.y;
}
}
}
}
if (obj.setAbsBaseCoord)
{
obj.setAbsBaseCoord(newCoord, coordMode);
}
else if (obj.setAbsCoordOfMode)
{
obj.setAbsCoordOfMode(newCoord, coordMode);
}
else if (obj.setAbsCoordOfMode)
{
obj.setCoordOfMode(newCoord, coordMode);
}
},
/**
* Get object's coord on context.
* @param {Object} obj
* @returns {Hash}
*/
getObjectContextCoord: function(obj, coordPos)
{
var coord = this.getObjCoord(obj, coordPos);
return this.objCoordToContext(coord);
},
/**
* Change object's coord on context.
* @param {Object} obj
* @param {Hash} contextCoord
*/
setObjectContextCoord: function(obj, contextCoord, coordPos)
{
var coord = this.contextCoordToObj(contextCoord);
if (coord)
this.setObjCoord(obj, coord, coordPos);
},
/**
* Get object's coord on screen.
* @param {Object} obj
* @returns {Hash}
*/
getObjectScreenCoord: function(obj, coordPos)
{
var coord = this.getObjCoord(obj, coordPos);
return this.objCoordToScreen(coord);
},
/**
* Change object's coord on screen.
* @param {Object} obj
* @param {Hash} contextCoord
*/
setObjectScreenCoord: function(obj, screenCoord, coordPos)
{
var coord = this.screenCoordToObj(screenCoord);
if (coord)
this.setObjCoord(obj, coord, coordPos);
},
/**
* Get coord of obj.
* @param {Object} obj
* @param {Int} coordSys Value from {@link Kekule.Render.CoordSystem}. Only CONTEXT and CHEM are available here.
* @returns {Hash}
*/
getCoord: function(obj, coordSys, coordPos)
{
/*
if (coordSys === Kekule.Render.CoordSystem.CONTEXT)
return this.getObjectContextCoord(obj);
else
return this.getObjCoord(obj);
*/
var objCoord = this.getObjCoord(obj, coordPos);
return this.translateCoord(objCoord, Kekule.Editor.CoordSys.OBJ, coordSys);
},
/**
* Set coord of obj.
* @param {Object} obj
* @param {Hash} value
* @param {Int} coordSys Value from {@link Kekule.Render.CoordSystem}. Only CONTEXT and CHEM are available here.
*/
setCoord: function(obj, value, coordSys, coordPos)
{
/*
if (coordSys === Kekule.Render.CoordSystem.CONTEXT)
this.setObjectContextCoord(obj, value);
else
this.setObjCoord(obj, value);
*/
var objCoord = this.translateCoord(value, coordSys, Kekule.Editor.CoordSys.OBJ);
this.setObjCoord(obj, objCoord, coordPos);
},
/**
* Get size of obj.
* @param {Object} obj
* @param {Int} coordSys Value from {@link Kekule.Render.CoordSystem}. Only CONTEXT and CHEM are available here.
* @returns {Hash}
*/
getSize: function(obj, coordSys)
{
var objSize = this.getObjSize(obj);
return this.translateCoord(objSize, Kekule.Editor.CoordSys.OBJ, coordSys);
},
/**
* Set size of obj.
* @param {Object} obj
* @param {Hash} value
* @param {Int} coordSys Value from {@link Kekule.Render.CoordSystem}. Only CONTEXT and CHEM are available here.
*/
setSize: function(obj, value, coordSys)
{
var objSize = this.translateCoord(value, coordSys, Kekule.Editor.CoordSys.OBJ);
this.setObjSize(obj, objSize);
},
// Coord translate methods
/**
* Translate coord to value of another coord system.
* @param {Hash} coord
* @param {Int} fromSys
* @param {Int} toSys
*/
translateCoord: function(coord, fromSys, toSys)
{
if (!coord)
return null;
var S = Kekule.Editor.CoordSys;
if (fromSys === S.SCREEN)
{
if (toSys === S.SCREEN)
return coord;
else if (toSys === S.CONTEXT)
return this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), coord);
else // S.OBJ
{
var contextCoord = this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), coord);
return this.getRootRenderer().transformCoordToObj(this.getObjContext(), this.getChemObj(), contextCoord);
}
}
else if (fromSys === S.CONTEXT)
{
if (toSys === S.SCREEN)
return this.getObjDrawBridge().transformContextCoordToScreen(this.getObjContext(), coord);
else if (toSys === S.CONTEXT)
return coord;
else // S.OBJ
return this.getRootRenderer().transformCoordToObj(this.getObjContext(), this.getChemObj(), coord);
}
else // fromSys === S.OBJ
{
if (toSys === S.SCREEN)
{
var contextCoord = this.getRootRenderer().transformCoordToContext(this.getObjContext(), this.getChemObj(), coord);
return this.getObjDrawBridge().transformContextCoordToScreen(this.getObjContext(), contextCoord);
}
else if (toSys === S.CONTEXT)
return this.getRootRenderer().transformCoordToContext(this.getObjContext(), this.getChemObj(), coord);
else // S.OBJ
return coord;
}
},
/**
* Translate a distance value to a distance in another coord system.
* @param {Hash} coord
* @param {Int} fromSys
* @param {Int} toSys
*/
translateDistance: function(distance, fromSys, toSys)
{
var coord0 = {'x': 0, 'y': 0, 'z': 0};
var coord1 = {'x': distance, 'y': 0, 'z': 0};
var transCoord0 = this.translateCoord(coord0, fromSys, toSys);
var transCoord1 = this.translateCoord(coord1, fromSys, toSys);
return Kekule.CoordUtils.getDistance(transCoord0, transCoord1);
},
/**
* Transform sizes and coords of objects based on coord sys of current editor.
* @param {Array} objects
* @param {Hash} transformParams
* @private
*/
transformCoordAndSizeOfObjects: function(objects, transformParams)
{
var coordMode = this.getCoordMode();
var allowCoordBorrow = this.getAllowCoordBorrow();
var matrix = (coordMode === Kekule.CoordMode.COORD3D)?
Kekule.CoordUtils.calcTransform3DMatrix(transformParams):
Kekule.CoordUtils.calcTransform2DMatrix(transformParams);
var childTransformParams = Object.extend({}, transformParams);
childTransformParams = Object.extend(childTransformParams, {
'translateX': 0,
'translateY': 0,
'translateZ': 0,
'center': {'x': 0, 'y': 0, 'z': 0}
});
var childMatrix = (coordMode === Kekule.CoordMode.COORD3D)?
Kekule.CoordUtils.calcTransform3DMatrix(childTransformParams):
Kekule.CoordUtils.calcTransform2DMatrix(childTransformParams);
for (var i = 0, l = objects.length; i < l; ++i)
{
var obj = objects[i];
obj.transformAbsCoordByMatrix(matrix, childMatrix, coordMode, true, allowCoordBorrow);
obj.scaleSize(transformParams.scale, coordMode, true, allowCoordBorrow);
}
},
/*
* Turn obj coord to context one.
* @param {Hash} objCoord
* @returns {Hash}
*/
objCoordToContext: function(objCoord)
{
var S = Kekule.Editor.CoordSys;
return this.translateCoord(objCoord, S.OBJ, S.CONTEXT);
},
/**
* Turn context coord to obj one.
* @param {Hash} contextCoord
* @returns {Hash}
*/
contextCoordToObj: function(contextCoord)
{
var S = Kekule.Editor.CoordSys;
return this.translateCoord(contextCoord, S.CONTEXT, S.OBJ);
},
/*
* Turn obj coord to screen one.
* @param {Hash} objCoord
* @returns {Hash}
*/
objCoordToScreen: function(objCoord)
{
var S = Kekule.Editor.CoordSys;
return this.translateCoord(objCoord, S.OBJ, S.SCREEN);
},
/**
* Turn screen coord to obj one.
* @param {Hash} contextCoord
* @returns {Hash}
*/
screenCoordToObj: function(screenCoord)
{
var S = Kekule.Editor.CoordSys;
return this.translateCoord(screenCoord, S.SCREEN, S.OBJ);
},
/**
* Turn screen based coord to context one.
* @param {Hash} screenCoord
* @returns {Hash}
*/
screenCoordToContext: function(screenCoord)
{
/*
var coord = this.getObjDrawBridge().transformScreenCoordToContext(this.getObjContext(), screenCoord);
return coord;
*/
var S = Kekule.Editor.CoordSys;
return this.translateCoord(screenCoord, S.SCREEN, S.CONTEXT);
},
/**
* Turn context based coord to screen one.
* @param {Hash} screenCoord
* @returns {Hash}
*/
contextCoordToScreen: function(screenCoord)
{
var S = Kekule.Editor.CoordSys;
return this.translateCoord(screenCoord, S.CONTEXT, S.SCREEN);
},
/**
* Turn box coords based on screen system to context one.
* @param {Hash} screenCoord
* @returns {Hash}
*/
screenBoxToContext: function(screenBox)
{
var coord1 = this.screenCoordToContext({'x': screenBox.x1, 'y': screenBox.y1});
var coord2 = this.screenCoordToContext({'x': screenBox.x2, 'y': screenBox.y2});
return {'x1': coord1.x, 'y1': coord1.y, 'x2': coord2.x, 'y2': coord2.y};
},
///////////////////////////////////////////////////////
/**
* Create a default node at coord and append it to parent.
* @param {Hash} coord
* @param {Int} coordType Value from {@link Kekule.Editor.CoordType}
* @param {Kekule.StructureFragment} parent
* @returns {Kekule.ChemStructureNode}
* @private
*/
createDefaultNode: function(coord, coordType, parent)
{
var isoId = this.getEditorConfigs().getStructureConfigs().getDefIsotopeId();
var atom = new Kekule.Atom();
atom.setIsotopeId(isoId);
if (parent)
parent.appendNode(atom);
this.setCoord(atom, coord, coordType);
return atom;
},
/////////////////////////////////////////////////////////////////////////////
// methods about undo/redo and operation histories
/**
* Called after a operation is executed or reversed. Notify object has changed.
* @param {Object} operation
*/
operationDone: function(operation)
{
this.doOperationDone(operation);
},
/**
* Do actual job of {@link Kekule.Editor.AbstractEditor#operationDone}. Descendants should override this method.
* @private
*/
doOperationDone: function(operation)
{
// do nothing here
},
/**
* Pop all operations and empty history list.
*/
clearOperHistory: function()
{
var h = this.getOperHistory();
if (h)
h.clear();
},
/**
* Manually append an operation to the tail of operation history.
* @param {Kekule.Operation} operation
* @param {Bool} autoExec Whether execute the operation after pushing it.
*/
pushOperation: function(operation, autoExec)
{
var h = this.getOperHistory();
if (h && operation)
{
h.push(operation);
}
if (autoExec)
{
this.beginUpdateObject();
try
{
operation.execute();
}
finally
{
this.endUpdateObject();
}
}
},
/**
* Manually pop an operation from the tail of operation history.
* @param {Bool} autoReverse Whether undo the operation after popping it.
* @returns {Kekule.Operation} Operation popped.
*/
popOperation: function(autoReverse)
{
var r;
var h = this.getOperHistory();
if (h)
{
r = h.pop(operation);
if (autoReverse)
{
this.beginUpdateObject();
try
{
r.reverse();
}
finally
{
this.endUpdateObject();
}
}
return r;
}
else
return null;
},
/**
* Execute an operation in editor.
* @param {Kekule.Operation} operation
*/
execOperation: function(operation)
{
this.beginUpdateObject();
try
{
operation.execute();
}
finally
{
this.endUpdateObject();
}
if (this.getEnableOperHistory())
this.pushOperation(operation, false); // push but not execute
return this;
},
/**
* Undo last operation.
*/
undo: function()
{
var o;
var h = this.getOperHistory();
if (h)
{
this.beginUpdateObject();
try
{
o = h.undo();
}
finally
{
this.endUpdateObject();
if (o)
this.operationDone(o);
}
}
return o;
},
/**
* Redo last operation.
*/
redo: function()
{
var o;
var h = this.getOperHistory();
if (h)
{
this.beginUpdateObject();
try
{
o = h.redo();
}
finally
{
this.endUpdateObject();
if (o)
this.operationDone(o)
}
}
return o;
},
/**
* Undo all operations.
*/
undoAll: function()
{
var o;
var h = this.getOperHistory();
if (h)
{
this.beginUpdateObject();
try
{
o = h.undoAll();
}
finally
{
this.endUpdateObject();
}
}
return o;
},
/**
* Check if an undo action can be taken.
* @returns {Bool}
*/
canUndo: function()
{
var h = this.getOperHistory();
return h? h.canUndo(): false;
},
/**
* Check if an undo action can be taken.
* @returns {Bool}
*/
canRedo: function()
{
var h = this.getOperHistory();
return h? h.canRedo(): false;
},
/**
* Modify properties of objects in editor.
* @param {Variant} objOrObjs A object or an array of objects.
* @param {Hash} modifiedPropInfos A hash of property: value pairs.
* @param {Bool} putInOperHistory If set to true, the modification will be put into history and can be undone.
*/
modifyObjects: function(objOrObjs, modifiedPropInfos, putInOperHistory)
{
var objs = Kekule.ArrayUtils.toArray(objOrObjs);
try
{
var macro = new Kekule.MacroOperation();
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var oper = new Kekule.ChemObjOperation.Modify(obj, modifiedPropInfos, this);
macro.add(oper);
}
macro.execute();
}
finally
{
if (putInOperHistory && this.getEnableOperHistory() && macro.getChildCount())
this.pushOperation(macro);
}
return this;
},
/**
* Modify render options of objects in editor.
* @param {Variant} objOrObjs A object or an array of objects.
* @param {Hash} modifiedValues A hash of name: value pairs.
* @param {Bool} is3DOption Change renderOptions or render3DOptions.
* @param {Bool} putInOperHistory If set to true, the modification will be put into history and can be undone.
*/
modifyObjectsRenderOptions: function(objOrObjs, modifiedValues, is3DOption, putInOperHistory)
{
var objs = Kekule.ArrayUtils.toArray(objOrObjs);
var renderPropName = is3DOption? 'render3DOptions': 'renderOptions';
var getterName = is3DOption? 'getRender3DOptions': 'getRenderOptions';
try
{
var macro = new Kekule.MacroOperation();
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (obj[getterName])
{
var old = obj[getterName]();
var newOps = Object.extend({}, old);
newOps = Object.extend(newOps, modifiedValues);
var hash = {};
hash[renderPropName] = newOps;
var oper = new Kekule.ChemObjOperation.Modify(obj, hash, this);
//oper.execute();
macro.add(oper);
}
}
macro.execute();
}
finally
{
if (putInOperHistory && this.getEnableOperHistory() && macro.getChildCount())
this.pushOperation(macro);
}
return this;
},
/**
* Returns the dimension of current visible client area of editor.
*/
getClientDimension: function()
{
var elem = this.getElement();
return {
'width': elem.clientWidth,
'height': elem.clientHeight
};
},
/**
* Returns current scroll position of edit client element.
* @returns {Hash} {x, y}
*/
getClientScrollPosition: function()
{
var elem = this.getEditClientElem().parentNode;
return elem? {
'x': elem.scrollLeft,
'y': elem.scrollTop
}: null;
},
/**
* Scroll edit client to a position.
* @param {Int} yPosition, in px.
* @param {Int} xPosition, in px.
*/
scrollClientTo: function(yPosition, xPosition)
{
var elem = this.getEditClientElem().parentNode;
if (Kekule.ObjUtils.notUnset(yPosition))
elem.scrollTop = yPosition;
if (Kekule.ObjUtils.notUnset(xPosition))
elem.scrollLeft = xPosition;
return this;
},
/**
* Scroll edit client to top.
*/
scrollClientToTop: function()
{
return this.scrollClientTo(0, null);
},
/////// Event handle //////////////////////
doBeforeDispatchUiEvent: function($super, e)
{
// get pointer type information here
var evType = e.getType();
if (['pointerdown', 'pointermove', 'pointerup'].indexOf(evType) >= 0)
{
this.setCurrPointerType(e.pointerType);
}
return $super(e);
}
});
/**
* A special class to give a setting facade for BaseEditor.
* Do not use this class alone.
* @class
* @augments Kekule.ChemWidget.ChemObjDisplayer.Settings
* @ignore
*/
Kekule.Editor.BaseEditor.Settings = Class.create(Kekule.ChemWidget.ChemObjDisplayer.Settings,
/** @lends Kekule.Editor.BaseEditor.Settings# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BaseEditor.Settings',
/** @private */
initProperties: function()
{
this.defineProp('enableCreateNewDoc', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function() { return this.getEditor().getEnableCreateNewDoc(); },
'setter': function(value) { this.getEditor().setEnableCreateNewDoc(value); }
});
this.defineProp('initOnNewDoc', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function() { return this.getEditor().getInitOnNewDoc(); },
'setter': function(value) { this.getEditor().setInitOnNewDoc(value); }
});
this.defineProp('enableOperHistory', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function() { return this.getEditor().getEnableOperHistory(); },
'setter': function(value) { this.getEditor().setEnableOperHistory(value); }
});
},
/** @private */
getEditor: function()
{
return this.getDisplayer();
}
});
/**
* A class to register all available IA controllers for editor.
* @class
*/
Kekule.Editor.IaControllerManager = {
/** @private */
_controllerMap: new Kekule.MapEx(true),
/**
* Register a controller, the controller can be used in targetEditorClass or its descendants.
* @param {Class} controllerClass
* @param {Class} targetEditorClass
*/
register: function(controllerClass, targetEditorClass)
{
ICM._controllerMap.set(controllerClass, targetEditorClass);
},
/**
* Unregister a controller.
* @param {Class} controllerClass
*/
unregister: function(controllerClass)
{
ICM._controllerMap.remove(controllerClass);
},
/**
* Returns all registered controller classes.
* @returns {Array}
*/
getAllControllerClasses: function()
{
return ICM._controllerMap.getKeys();
},
/**
* Returns controller classes can be used for editorClass.
* @param {Class} editorClass
* @returns {Array}
*/
getAvailableControllerClasses: function(editorClass)
{
var result = [];
var controllerClasses = ICM.getAllControllerClasses();
for (var i = 0, l = controllerClasses.length; i < l; ++i)
{
var cc = controllerClasses[i];
var ec = ICM._controllerMap.get(cc);
if (!ec || ClassEx.isOrIsDescendantOf(editorClass, ec))
result.push(cc);
}
return result;
}
};
var ICM = Kekule.Editor.IaControllerManager;
/**
* Base Controller class for editor.
* @class
* @augments Kekule.Widget.InteractionController
*
* @param {Kekule.Editor.BaseEditor} editor Editor of current object being installed to.
*
* @property {Bool} manuallyHotTrack If set to false, hot track will be auto shown in mousemove event listener.
*/
Kekule.Editor.BaseEditorIaController = Class.create(Kekule.Widget.InteractionController,
/** @lends Kekule.Editor.BaseEditorIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BaseEditorIaController',
/** @constructs */
initialize: function($super, editor)
{
$super(editor);
},
initProperties: function()
{
this.defineProp('manuallyHotTrack', {'dataType': DataType.BOOL, 'serializable': false});
// in mouse or touch interaction, we may have different bound inflation
this.defineProp('currBoundInflation', {'dataType': DataType.NUMBER, 'serializable': false,
'getter': function() { return this.getEditor().getCurrBoundInflation(); },
'setter': null // function(value) { return this.getEditor().setCurrBoundInflation(value); }
});
},
/**
* Returns the preferred id for this controller.
*/
getDefId: function()
{
return Kekule.ClassUtils.getLastClassName(this.getClassName());
},
/**
* Return associated editor.
* @returns {Kekule.ChemWidget.BaseEditor}
*/
getEditor: function()
{
return this.getWidget();
},
/**
* Set associated editor.
* @param {Kekule.ChemWidget.BaseEditor} editor
*/
setEditor: function(editor)
{
return this.setWidget(editor);
},
/**
* Get config object of editor.
* @returns {Object}
*/
getEditorConfigs: function()
{
var editor = this.getEditor();
return editor? editor.getEditorConfigs(): null;
},
/** @private */
getInteractionBoundInflation: function(pointerType)
{
return this.getEditor().getInteractionBoundInflation(pointerType);
},
/** @ignore */
handleUiEvent: function($super, e)
{
var handle = false;
var targetElem = (e.getTarget && e.getTarget()) || e.target; // hammer event does not have getTarget method
var uiElem = this.getEditor().getUiEventReceiverElem();
if (uiElem)
{
// only handles event on event receiver element
// otherwise scrollbar on editor may cause problem
if ((targetElem === uiElem) || Kekule.DomUtils.isDescendantOf(targetElem, uiElem))
handle = true;
}
else
handle = true;
if (handle)
$super(e);
},
/**
* Returns if this IaController can interact with obj.
* If true, when mouse moving over obj, a hot track marker will be drawn.
* Descendants should override this method.
* @param {Object} obj
* @return {Bool}
* @private
*/
canInteractWithObj: function(obj)
{
return !!obj;
},
/**
* Show a hot track marker on obj in editor.
* @param {Kekule.ChemObject} obj
*/
hotTrackOnObj: function(obj)
{
this.getEditor().hotTrackOnObj(obj);
},
// zoom functions
/** @private */
zoomEditor: function(zoomLevel, zoomCenterCoord)
{
if (zoomLevel > 0)
this.getEditor().zoomIn(zoomLevel, zoomCenterCoord);
else if (zoomLevel < 0)
this.getEditor().zoomOut(-zoomLevel, zoomCenterCoord);
},
/** @private */
/*
updateCurrBoundInflation: function(evt)
{
*/
/*
var editor = this.getEditor();
var pointerType = evt && evt.pointerType;
var iaConfigs = this.getEditorConfigs().getInteractionConfigs();
var defRatio = iaConfigs.getObjBoundTrackInflationRatio();
var currRatio, ratioValue;
if (pointerType === 'mouse')
currRatio = iaConfigs.getObjBoundTrackInflationRatioMouse();
else if (pointerType === 'pen')
currRatio = iaConfigs.getObjBoundTrackInflationRatioPen();
else if (pointerType === 'touch')
currRatio = iaConfigs.getObjBoundTrackInflationRatioTouch();
currRatio = currRatio || defRatio;
if (currRatio)
{
var bondScreenLength = editor.getDefBondScreenLength();
ratioValue = bondScreenLength * currRatio;
}
var defMinValue = iaConfigs.getObjBoundTrackMinInflation();
var currMinValue;
if (pointerType === 'mouse')
currMinValue = iaConfigs.getObjBoundTrackMinInflationMouse();
else if (pointerType === 'pen')
currMinValue = iaConfigs.getObjBoundTrackMinInflationPen();
else if (pointerType === 'touch')
currMinValue = iaConfigs.getObjBoundTrackMinInflationTouch();
currMinValue = currMinValue || defMinValue;
var actualValue = Math.max(ratioValue || 0, currMinValue);
*/
/*
//this.setCurrBoundInflation(actualValue);
var value = this.getEditor().getInteractionBoundInflation(evt && evt.pointerType);
this.setCurrBoundInflation(value);
//console.log('update bound inflation', pointerType, this.getCurrBoundInflation());
},
*/
/** @private */
_filterBasicObjectsInEditor: function(objs)
{
var editor = this.getEditor();
var rootObj = editor.getChemObj();
var result = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
if (obj.isChildOf(rootObj))
result.push(obj);
}
return result;
},
/**
* Notify the manipulation is done and objs are inserted into or modified in editor.
* This method should be called by descendants at the end of their manipulation.
* Objs will be automatically selected if autoSelectNewlyInsertedObjects option is true.
* @param {Array} objs
* @private
*/
doneInsertOrModifyBasicObjects: function(objs)
{
if (this.getEditorConfigs().getInteractionConfigs().getAutoSelectNewlyInsertedObjects())
{
var filteredObjs = this._filterBasicObjectsInEditor(objs);
this.getEditor().select(filteredObjs);
}
},
/** @private */
react_pointerdown: function(e)
{
//this.updateCurrBoundInflation(e);
//this.getEditor().setCurrPointerType(e.pointerType);
e.preventDefault();
return true;
},
/** @private */
react_pointermove: function(e)
{
//if (!this.getCurrBoundInflation())
//this.updateCurrBoundInflation(e);
//this.getEditor().setCurrPointerType(e.pointerType);
//console.log(e.getTarget().id);
var coord = this._getEventMouseCoord(e);
var obj = this.getEditor().getTopmostBasicObjectAtCoord(coord, this.getCurrBoundInflation());
if (!this.getManuallyHotTrack())
{
/*
if (obj)
console.log('point to', obj.getClassName(), obj.getId());
*/
if (obj && this.canInteractWithObj(obj))
{
this.hotTrackOnObj(obj);
}
else
{
this.hotTrackOnObj(null);
}
//e.preventDefault();
}
e.preventDefault();
return true;
},
/** @private */
react_mousewheel: function(e)
{
if (e.getCtrlKey())
{
var currScreenCoord = this._getEventMouseCoord(e);
//this.getEditor().setZoomCenter(currScreenCoord);
try
{
var delta = e.wheelDeltaY || e.wheelDelta;
if (delta)
delta /= 120;
//console.log('zoom', this.getEditor().getZoomCenter())
this.zoomEditor(delta, currScreenCoord);
}
finally
{
//this.getEditor().setZoomCenter(null);
}
e.preventDefault();
return true;
}
},
/** @private */
_getEventMouseCoord: function($super, e, clientElem)
{
var elem = clientElem || this.getWidget().getCoreElement(); // defaultly base on client element, not widget element
return $super(e, elem);
}
});
/**
* Controller for drag and scroll (by mouse, touch...) client element in editor.
* @class
* @augments Kekule.Widget.BaseEditorIaController
*
* @param {Kekule.Editor.BaseEditor} widget Editor of current object being installed to.
*/
Kekule.Editor.ClientDragScrollIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.ClientDragScrollIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.ClientDragScrollIaController',
/** @constructs */
initialize: function($super, widget)
{
$super(widget);
this._isExecuting = false;
},
/** @ignore */
canInteractWithObj: function(obj)
{
return false; // do not interact directly with objects in editor
},
/** @ignore */
doTestMouseCursor: function(coord, e)
{
//console.log(this.isExecuting(), coord);
return this.isExecuting()?
['grabbing', '-webkit-grabbing', '-moz-grabbing', 'move']:
['grab', '-webkit-grab', '-moz-grab', 'pointer'];
//return this.isExecuting()? '-webkit-grabbing': '-webkit-grab';
},
/** @private */
isExecuting: function()
{
return this._isExecuting;
},
/** @private */
startScroll: function(screenCoord)
{
this._startCoord = screenCoord;
this._originalScrollPos = this.getEditor().getClientScrollPosition();
this._isExecuting = true;
},
/** @private */
endScroll: function()
{
this._isExecuting = false;
this._startCoord = null;
this._originalScrollPos = null;
},
/** @private */
scrollTo: function(screenCoord)
{
if (this.isExecuting())
{
var startCoord = this._startCoord;
var delta = Kekule.CoordUtils.substract(startCoord, screenCoord);
var newScrollPos = Kekule.CoordUtils.add(this._originalScrollPos, delta);
this.getEditor().scrollClientTo(newScrollPos.y, newScrollPos.x); // note the params of this method is y, x
}
},
/** @private */
react_pointerdown: function(e)
{
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT) // begin scroll
{
if (!this.isExecuting())
{
var coord = {x: e.getScreenX(), y: e.getScreenY()};
this.startScroll(coord);
e.preventDefault();
}
}
else if (e.getButton() === Kekule.X.Event.MouseButton.RIGHT)
{
if (this.isExecuting())
{
this.endScroll();
e.preventDefault();
}
}
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
if (this.isExecuting())
{
this.endScroll();
e.preventDefault();
}
}
},
/** @private */
react_pointermove: function($super, e)
{
$super(e);
if (this.isExecuting())
{
var coord = {x: e.getScreenX(), y: e.getScreenY()};
this.scrollTo(coord);
e.preventDefault();
}
return true;
}
});
/** @ignore */
Kekule.Editor.IaControllerManager.register(Kekule.Editor.ClientDragScrollIaController, Kekule.Editor.BaseEditor);
/**
* Controller for deleting objects in editor.
* @class
* @augments Kekule.Widget.BaseEditorIaController
*
* @param {Kekule.Editor.BaseEditor} widget Editor of current object being installed to.
*/
Kekule.Editor.BasicEraserIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.BasicEraserIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BasicEraserIaController',
/** @constructs */
initialize: function($super, widget)
{
$super(widget);
this._isExecuting = false;
},
/** @ignore */
canInteractWithObj: function(obj)
{
return !!obj; // every thing can be deleted
},
//methods about remove
/** @private */
removeObjs: function(objs)
{
if (objs && objs.length)
{
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
var actualObjs = this.doGetActualRemovedObjs(objs);
this.doRemoveObjs(actualObjs);
}
finally
{
editor.endUpdateObject();
}
}
},
/** @private */
doRemoveObjs: function(objs)
{
// do actual remove job
},
doGetActualRemovedObjs: function(objs)
{
return objs;
},
/**
* Remove selected objects in editor.
*/
removeSelection: function()
{
var editor = this.getEditor();
this.removeObjs(editor.getSelection());
// the selection is currently empty
editor.deselectAll();
},
/**
* Remove object on screen coord.
* @param {Hash} coord
*/
removeOnScreenCoord: function(coord)
{
var obj = this.getEditor().getTopmostBasicObjectAtCoord(coord);
if (obj)
{
this.removeObjs([obj]);
return true;
}
else
return false;
},
/** @private */
startRemove: function()
{
this._isExecuting = true;
},
/** @private */
endRemove: function()
{
this._isExecuting = false;
},
/** @private */
isRemoving: function()
{
return this._isExecuting;
},
/** @private */
react_pointerdown: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
this.startRemove();
var coord = this._getEventMouseCoord(e);
this.removeOnScreenCoord(coord);
e.preventDefault();
}
else if (e.getButton() === Kekule.X.Event.MOUSE_BTN_RIGHT)
{
this.endRemove();
e.preventDefault();
}
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
this.endRemove();
e.preventDefault();
}
},
/** @private */
react_pointermove: function($super, e)
{
$super(e);
if (this.isRemoving())
{
var coord = this._getEventMouseCoord(e);
this.removeOnScreenCoord(coord);
e.preventDefault();
}
return true;
}
});
/** @ignore */
Kekule.Editor.IaControllerManager.register(Kekule.Editor.BasicEraserIaController, Kekule.Editor.BaseEditor);
/**
* Controller for selecting, moving or rotating objects in editor.
* @class
* @augments Kekule.Widget.BaseEditorIaController
*
* @param {Kekule.Editor.BaseEditor} widget Editor of current object being installed to.
*
* @property {Int} selectMode Set the selectMode property of editor.
* @property {Bool} enableSelect Whether select function is enabled.
* @property {Bool} enableMove Whether move function is enabled.
* //@property {Bool} enableRemove Whether remove function is enabled.
* @property {Bool} enableResize Whether resize of selection is allowed.
* @property {Bool} enableRotate Whether rotate of selection is allowed.
* @property {Bool} enableGestureManipulation Whether rotate and resize by touch gestures are allowed.
*/
Kekule.Editor.BasicManipulationIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.BasicManipulationIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BasicManipulationIaController',
/** @constructs */
initialize: function($super, widget)
{
$super(widget);
this.setState(Kekule.Editor.BasicManipulationIaController.State.NORMAL);
this.setEnableSelect(false);
this.setEnableGestureManipulation(false);
this.setEnableMove(true);
this.setEnableResize(true);
this.setEnableAspectRatioLockedResize(true);
this.setEnableRotate(true);
this._suppressConstrainedResize = false;
this._manipulationStepBuffer = {};
this._suspendedOperations = null;
this.execManipulationStepBind = this.execManipulationStep.bind(this);
},
/** @private */
initProperties: function()
{
this.defineProp('selectMode', {'dataType': DataType.INT, 'serializable': false});
this.defineProp('enableSelect', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableMove', {'dataType': DataType.BOOL, 'serializable': false});
//this.defineProp('enableRemove', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableResize', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableRotate', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableGestureManipulation', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('state', {'dataType': DataType.INT, 'serializable': false});
// the screen coord that start this manipulation, since startCoord may be changed during rotation, use this
// to get the inital coord of mouse down
this.defineProp('baseCoord', {'dataType': DataType.HASH, 'serializable': false});
this.defineProp('startCoord', {'dataType': DataType.HASH, 'serializable': false/*,
'setter': function(value)
{
console.log('set startCoord', value);
console.log(arguments.callee.caller.caller.caller.toString());
this.setPropStoreFieldValue('startCoord', value);
}*/
});
this.defineProp('endCoord', {'dataType': DataType.HASH, 'serializable': false});
this.defineProp('startBox', {'dataType': DataType.HASH, 'serializable': false});
this.defineProp('endBox', {'dataType': DataType.HASH, 'serializable': false});
this.defineProp('lastRotateAngle', {'dataType': DataType.FLOAT, 'serializable': false}); // private
// private, such as {x: 1, y: 0}, plays as the initial base direction of rotation
this.defineProp('rotateRefCoord', {'dataType': DataType.HASH, 'serializable': false});
this.defineProp('rotateCenter', {'dataType': DataType.HASH, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('rotateCenter');
if (!result)
{
/*
var box = this.getStartBox();
result = box? {'x': (box.x1 + box.x2) / 2, 'y': (box.y1 + box.y2) / 2}: null;
*/
var centerCoord = this._getManipulateObjsCenterCoord();
result = this.getEditor().objCoordToScreen(centerCoord);
this.setPropStoreFieldValue('rotateCenter', result);
//console.log(result, result2);
}
return result;
}
});
this.defineProp('resizeStartingRegion', {'dataType': DataType.INT, 'serializable': false}); // private
this.defineProp('enableAspectRatioLockedResize', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('rotateStartingRegion', {'dataType': DataType.INT, 'serializable': false}); // private
this.defineProp('manipulateOriginObjs', {'dataType': DataType.ARRAY, 'serializable': false}); // private, the direct object user act on
this.defineProp('manipulateObjs', {'dataType': DataType.ARRAY, 'serializable': false, // actual manipulated objects
'setter': function(value)
{
this.setPropStoreFieldValue('manipulateObjs', value);
//console.log('set manipulate', value);
if (!value)
this.getEditor().endOperatingObjs();
else
this.getEditor().prepareOperatingObjs(value);
}
});
this.defineProp('manipulateObjInfoMap', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('manipulateObjInfoMap');
if (!result)
{
result = new Kekule.MapEx(true);
this.setPropStoreFieldValue('manipulateObjInfoMap', result);
}
return result;
}
});
this.defineProp('manipulateObjCurrInfoMap', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('manipulateObjCurrInfoMap');
if (!result)
{
result = new Kekule.MapEx(true);
this.setPropStoreFieldValue('manipulateObjCurrInfoMap', result);
}
return result;
}
});
this.defineProp('manipulationType', {'dataType': DataType.INT, 'serializable': false}); // private
this.defineProp('isManipulatingSelection', {'dataType': DataType.BOOL, 'serializable': false});
//this.defineProp('manipulateOperation', {'dataType': 'Kekule.MacroOperation', 'serializable': false}); // store operation of moving
//this.defineProp('activeOperation', {'dataType': 'Kekule.MacroOperation', 'serializable': false}); // store operation that should be add to history
this.defineProp('moveOperations', {'dataType': DataType.ARRAY, 'serializable': false}); // store operations of moving
//this.defineProp('mergeOperations', {'dataType': DataType.ARRAY, 'serializable': false}); // store operations of merging
this.defineProp('objOperationMap', {'dataType': 'Kekule.MapEx', 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('objOperationMap');
if (!result)
{
result = new Kekule.MapEx(true);
this.setPropStoreFieldValue('objOperationMap', result);
}
return result;
}
}); // store operation on each object
},
/** @private */
doFinalize: function($super)
{
var map = this.getPropStoreFieldValue('manipulateObjInfoMap');
if (map)
map.clear();
map = this.getPropStoreFieldValue('objOperationMap');
if (map)
map.clear();
$super();
},
/* @ignore */
/*
activated: function($super, widget)
{
$super(widget);
//console.log('activated', this.getSelectMode());
// set select mode when be activated
if (this.getEnableSelect())
this.getEditor().setSelectMode(this.getSelectMode());
},
*/
/** @ignore */
hotTrackOnObj: function($super, obj)
{
// override parent method, is selectMode is ANCESTOR, hot track the whole ancestor object
if (this.getEnableSelect() && this.getSelectMode() === Kekule.Editor.SelectMode.ANCESTOR)
{
var concreteObj = this.getStandaloneAncestor(obj); (obj && obj.getStandaloneAncestor) ? obj.getStandaloneAncestor() : obj;
return $super(concreteObj);
}
else
return $super(obj);
},
/** @private */
getStandaloneAncestor: function(obj)
{
return (obj && obj.getStandaloneAncestor) ? obj.getStandaloneAncestor() : obj;
},
/** @private */
isInAncestorSelectMode: function()
{
return this.getEnableSelect() && (this.getSelectMode() === Kekule.Editor.SelectMode.ANCESTOR);
},
/** @private */
isAspectRatioLockedResize: function()
{
return this.getEnableAspectRatioLockedResize() && (!this._suppressConstrainedResize);
},
/**
* Check if screenCoord is on near-outside of selection bound and returns which corner is the neraest.
* @param {Hash} screenCoord
* @returns {Variant} If on rotation region, a nearest corner flag (from @link Kekule.Editor.BoxRegion} will be returned,
* else false will be returned.
*/
getCoordOnSelectionRotationRegion: function(screenCoord)
{
var R = Kekule.Editor.BoxRegion;
var editor = this.getEditor();
var region = editor.getCoordRegionInSelectionMarker(screenCoord);
if (region !== R.OUTSIDE)
return false;
var r = editor.getEditorConfigs().getInteractionConfigs().getRotationRegionInflation();
var box = editor.getUiSelectionAreaContainerBox();
if (box && editor.hasSelection())
{
var corners = [R.CORNER_TL, R.CORNER_TR, R.CORNER_BR, R.CORNER_BL];
var points = [
{'x': box.x1, 'y': box.y1},
{'x': box.x2, 'y': box.y1},
{'x': box.x2, 'y': box.y2},
{'x': box.x1, 'y': box.y2}
];
var result = false;
var minDis = r;
for (var i = 0, l = corners.length; i < l; ++i)
{
var corner = corners[i];
var point = points[i];
var dis = Kekule.CoordUtils.getDistance(point, screenCoord);
if (dis <= minDis)
{
result = corner;
minDis = dis;
}
}
return result;
}
else
return false;
},
/**
* Create a coord change operation to add to operation history of editor.
* The operation is a macro one with sub operations on each obj.
* @private
*/
createManipulateOperation: function()
{
return this.doCreateManipulateMoveAndResizeOperation();
},
/** @private */
doCreateManipulateMoveAndResizeOperation: function()
{
//var oper = new Kekule.MacroOperation();
var opers = [];
this.setMoveOperations(opers);
var objs = this.getManipulateObjs();
var map = this.getManipulateObjInfoMap();
var operMap = this.getObjOperationMap();
operMap.clear();
//console.log('init operations');
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var item = map.get(obj);
//var sub = new Kekule.EditorOperation.OpSetObjCoord(this.getEditor(), obj, null, item.objCoord, Kekule.Editor.CoordSys.OBJ);
//var sub = new Kekule.ChemObjOperation.MoveTo(obj, null, this.getEditor().getCoordMode());
var sub = new Kekule.ChemObjOperation.MoveAndResize(obj, null, null, this.getEditor().getCoordMode(), true, this.getEditor()); // use abs coord
sub.setAllowCoordBorrow(this.getEditor().getAllowCoordBorrow());
sub.setOldCoord(item.objCoord);
sub.setOldDimension(item.size);
//oper.add(sub);
//operMap.set(obj, sub);
opers.push(sub);
}
//this.setManipulateOperation(oper);
//this.setActiveOperation(oper);
//return oper;
return opers;
},
/* @private */
/*
_ensureObjOperationToMove: function(obj)
{
var map = this.getObjOperationMap();
var oper = map.get(obj);
if (oper && !(oper instanceof Kekule.ChemObjOperation.MoveAndResize))
{
//console.log('_ensureObjOperationToMove reverse');
//oper.reverse();
oper.finalize();
oper = new Kekule.ChemObjOperation.MoveAndResize(obj, null, null, this.getEditor().getCoordMode(), true); // use abs coord
map.set(obj, oper);
}
return oper;
},
*/
/**
* Update new coord info of sub operations.
* @private
*/
updateChildMoveOperation: function(objIndex, obj, newObjCoord)
{
//console.log('update move', newObjCoord);
//var oper = this.getManipulateOperation().getChildAt(objIndex);
//var oper = this._ensureObjOperationToMove(obj);
var oper = this.getMoveOperations()[objIndex];
//oper.setCoord(newObjCoord);
oper.setNewCoord(newObjCoord);
},
/** @private */
updateChildResizeOperation: function(objIndex, obj, newDimension)
{
//var oper = this.getManipulateOperation().getChildAt(objIndex);
//var oper = this._ensureObjOperationToMove(obj);
var oper = this.getMoveOperations()[objIndex];
oper.setNewDimension(newDimension);
},
/** @private */
getAllObjOperations: function(isTheFinalOperationToEditor)
{
//var opers = this.getObjOperationMap().getValues();
var op = this.getMoveOperations();
var opers = op? Kekule.ArrayUtils.clone(op): [];
return opers;
},
/** @private */
getActiveOperation: function(isTheFinalOperationToEditor)
{
//console.log('get active operation', isTheFinalOperationToEditor);
var opers = this.getAllObjOperations(isTheFinalOperationToEditor);
opers = Kekule.ArrayUtils.toUnique(opers);
if (opers.length <= 0)
return null;
else if (opers.length === 1)
return opers[0];
else
{
var macro = new Kekule.MacroOperation(opers);
return macro;
}
},
/** @private */
reverseActiveOperation: function()
{
var oper = this.getActiveOperation();
return oper.reverse();
},
/* @private */
/*
clearActiveOperation: function()
{
//this.getObjOperationMap().clear();
},
*/
/** @private */
addOperationToEditor: function()
{
var editor = this.getEditor();
if (editor && editor.getEnableOperHistory())
{
//console.log('add oper to editor', this.getClassName(), this.getActiveOperation());
//editor.pushOperation(this.getActiveOperation());
/*
var opers = this.getAllObjOperations();
var macro = new Kekule.MacroOperation(opers);
editor.pushOperation(macro);
*/
var op = this.getActiveOperation(true);
if (op)
editor.pushOperation(op);
}
},
// methods about object move / resize
/** @private */
getCurrAvailableManipulationTypes: function()
{
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var box = this.getEditor().getSelectionContainerBox();
if (!box)
{
return [];
}
else
{
var result = [];
if (this.getEnableMove())
result.push(T.MOVE);
// if box is a single point, can not resize or rotate
if (!Kekule.NumUtils.isFloatEqual(box.x1, box.x2, 1e-10) || !Kekule.NumUtils.isFloatEqual(box.y1, box.y2, 1e-10))
{
if (this.getEnableResize())
result.push(T.RESIZE);
if (this.getEnableRotate())
result.push(T.ROTATE);
if (this.getEnableResize() || this.getEnableRotate())
result.push(T.TRANSFORM);
}
return result;
}
},
/** @private */
getActualManipulatingObjects: function(objs)
{
var result = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var actualObjs = obj.getCoordDependentObjects? obj.getCoordDependentObjects(): [obj];
Kekule.ArrayUtils.pushUnique(result, actualObjs);
}
return result;
},
/*
* Prepare to resize resizingObjs.
* Note that resizingObjs may differ from actual resized objects (for instance, resize a bond actually move its connected atoms).
* @param {Hash} startContextCoord Mouse position when starting to move objects. This coord is based on context.
* @param {Array} resizingObjs Objects about to be resized.
* @private
*/
/*
prepareResizing: function(startScreenCoord, startBox, movingObjs)
{
var actualObjs = this.getActualResizingObject(movingObjs);
this.setManipulateObjs(actualObjs);
var map = this.getManipulateObjInfoMap();
map.clear();
var editor = this.getEditor();
// store original objs coords info into map
for (var i = 0, l = actualObjs.length; i < l; ++i)
{
var obj = actualObjs[i];
var info = this.createManipulateObjInfo(obj, startScreenCoord);
map.set(obj, info);
}
this.setStartBox(startBox);
},
*/
/** @private */
doPrepareManipulatingObjects: function(manipulatingObjs, startScreenCoord)
{
var actualObjs = this.getActualManipulatingObjects(manipulatingObjs);
//console.log(manipulatingObjs, actualObjs);
this.setManipulateOriginObjs(manipulatingObjs);
this.setManipulateObjs(actualObjs);
var map = this.getManipulateObjInfoMap();
map.clear();
//this.getManipulateObjCurrInfoMap().clear();
var editor = this.getEditor();
// store original objs coords info into map
for (var i = 0, l = actualObjs.length; i < l; ++i)
{
var obj = actualObjs[i];
var info = this.createManipulateObjInfo(obj, i, startScreenCoord);
map.set(obj, info);
}
},
/** @private */
doPrepareManipulatingStartingCoords: function(startScreenCoord, startBox, rotateCenter, rotateRefCoord)
{
this.setStartBox(startBox);
this.setRotateCenter(rotateCenter);
this.setRotateRefCoord(rotateRefCoord);
this.setLastRotateAngle(null);
}, /**
* Prepare to move movingObjs.
* Note that movingObjs may differ from actual moved objects (for instance, move a bond actually move its connected atoms).
* @param {Hash} startContextCoord Mouse position when starting to move objects. This coord is based on context.
* @param {Array} manipulatingObjs Objects about to be moved or resized.
* @param {Hash} startBox
* @param {Hash} rotateCenter
* @private
*/
prepareManipulating: function(manipulationType, manipulatingObjs, startScreenCoord, startBox, rotateCenter, rotateRefCoord)
{
this.setManipulationType(manipulationType);
this.doPrepareManipulatingObjects(manipulatingObjs, startScreenCoord);
this.doPrepareManipulatingStartingCoords(startScreenCoord, startBox, rotateCenter, rotateRefCoord);
this.createManipulateOperation();
this._runManipulationStepId = window.requestAnimationFrame(this.execManipulationStepBind);
//this.setManuallyHotTrack(true); // manully set hot track point when manipulating
},
/**
* Cancel the moving process and set objects to its original position.
* @private
*/
cancelManipulate: function()
{
var editor = this.getEditor();
var objs = this.getManipulateObjs();
//editor.beginUpdateObject();
//this.getActiveOperation().reverse();
this.reverseActiveOperation();
this.notifyCoordChangeOfObjects(this.getManipulateObjs());
//editor.endUpdateObject();
//this.setActiveOperation(null);
//this.clearActiveOperation();
//this.setManuallyHotTrack(false);
this.manipulateEnd();
},
/**
* Returns center coord of manipulate objs.
* @private
*/
_getManipulateObjsCenterCoord: function()
{
var objs = this.getManipulateObjs();
if (!objs || !objs.length)
return null;
var coordMode = this.getEditor().getCoordMode();
var allowCoordBorrow = this.getEditor().getAllowCoordBorrow();
var sum = {'x': 0, 'y': 0, 'z': 0};
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var objCoord = obj.getAbsBaseCoord? obj.getAbsBaseCoord(coordMode, allowCoordBorrow):
obj.getAbsCoord? obj.getAbsCoord(coordMode, allowCoordBorrow):
obj.getCoordOfMode? obj.getCoordOfMode(coordMode, allowCoordBorrow):
null;
if (objCoord)
sum = Kekule.CoordUtils.add(sum, objCoord);
}
return Kekule.CoordUtils.divide(sum, objs.length);
},
/**
* Called when a phrase of rotate/resize/move function ends.
*/
_maniplateObjsFrameEnd: function(objs)
{
// do nothing here
},
/** @private */
_addManipultingObjNewInfo: function(obj, newInfo)
{
var newInfoMap = this.getManipulateObjCurrInfoMap();
var info = newInfoMap.get(obj) || {};
info = Object.extend(info, newInfo);
newInfoMap.set(obj, info);
},
/** @private */
applyManipulatingObjsInfo: function(endScreenCoord)
{
var objs = this.getManipulateObjs();
var newInfoMap = this.getManipulateObjCurrInfoMap();
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var newInfo = newInfoMap.get(obj);
this.applySingleManipulatingObjInfo(i, obj, newInfo, endScreenCoord);
}
},
/** @private */
applySingleManipulatingObjInfo: function(objIndex, obj, newInfo, endScreenCoord)
{
if (newInfo)
{
if (newInfo.screenCoord)
this.doMoveManipulatedObj(objIndex, obj, newInfo.screenCoord, endScreenCoord);
if (newInfo.size)
this.doResizeManipulatedObj(objIndex, obj, newInfo.size);
}
},
/** @private */
_calcRotateAngle: function(endScreenCoord)
{
var C = Kekule.CoordUtils;
var angle;
var angleCalculated = false;
var rotateCenter = this.getRotateCenter();
var startCoord = this.getRotateRefCoord() || this.getStartCoord();
// ensure startCoord large than threshold
var threshold = this.getEditorConfigs().getInteractionConfigs().getRotationLocationPointDistanceThreshold();
if (threshold)
{
var startDistance = C.getDistance(startCoord, rotateCenter);
if (startDistance < threshold)
{
angle = 0; // do not rotate
angleCalculated = true;
// and use endScreen coord as new start coord
this.setStartCoord(endScreenCoord);
return false;
}
var endDistance = C.getDistance(endScreenCoord, rotateCenter);
if (endDistance < threshold)
{
angle = 0; // do not rotate
angleCalculated = true;
return false;
}
}
if (!angleCalculated)
{
var vector = C.substract(endScreenCoord, rotateCenter);
var endAngle = Math.atan2(vector.y, vector.x);
vector = C.substract(startCoord, rotateCenter);
var startAngle = Math.atan2(vector.y, vector.x);
angle = endAngle - startAngle;
}
return {'angle': angle, 'startAngle': startAngle, 'endAngle': endAngle};
},
/** @private */
_calcActualRotateAngle: function(objs, newDeltaAngle, oldAbsAngle, newAbsAngle)
{
return newDeltaAngle;
},
/** @private */
_calcManipulateObjsRotationParams: function(manipulatingObjs, endScreenCoord)
{
if (!this.getEnableRotate())
return false;
var rotateCenter = this.getRotateCenter();
var angleInfo = this._calcRotateAngle(endScreenCoord);
if (!angleInfo) // need not to rotate
return false;
// get actual rotation angle
var angle = this._calcActualRotateAngle(manipulatingObjs, angleInfo.angle, angleInfo.startAngle, angleInfo.endAngle);
var lastAngle = this.getLastRotateAngle();
if (Kekule.ObjUtils.notUnset(lastAngle) && Kekule.NumUtils.isFloatEqual(angle, lastAngle, 0.0175)) // ignore angle change under 1 degree
{
return false; // no angle change, do not rotate
}
//console.log('rotateAngle', angle, lastAngle);
this.setLastRotateAngle(angle);
return {'center': rotateCenter, 'rotateAngle': angle};
},
/* @private */
/*
doRotateManipulatedObjs: function(endScreenCoord, transformParams)
{
var byPassRotate = !this._calcManipulateObjsTransformInfo(this.getManipulateObjs(), transformParams);
if (byPassRotate) // need not to rotate
{
//console.log('bypass rotate');
return;
}
//console.log('rotate');
var objNewInfo = this.getManipulateObjCurrInfoMap();
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
var objs = this.getManipulateObjs();
this.applyManipulatingObjsInfo(endScreenCoord);
this._maniplateObjsFrameEnd(objs);
this.notifyCoordChangeOfObjects(objs);
}
finally
{
editor.endUpdateObject();
this.manipulateStepDone();
}
},
*/
/*
* Rotate manupulatedObjs according to endScreenCoord.
* @private
*/
/*
rotateManipulatedObjs: function(endScreenCoord)
{
var R = Kekule.Editor.BoxRegion;
var C = Kekule.CoordUtils;
//var editor = this.getEditor();
var changedObjs = [];
//console.log('rotate', this.getRotateCenter(), endScreenCoord);
var rotateParams = this._calcManipulateObjsRotationParams(this.getManipulateObjs(), endScreenCoord);
if (!rotateParams)
return;
this.doRotateManipulatedObjs(endScreenCoord, rotateParams);
},
*/
/** @private */
_calcActualResizeScales: function(objs, newScales)
{
return newScales;
},
/** @private */
_calcManipulateObjsResizeParams: function(manipulatingObjs, startingRegion, endScreenCoord)
{
if (!this.getEnableResize())
return false;
var R = Kekule.Editor.BoxRegion;
var C = Kekule.CoordUtils;
var box = this.getStartBox();
var coordDelta = C.substract(endScreenCoord, this.getStartCoord());
var scaleCenter;
var doConstraint, doConstraintOnX, doConstraintOnY;
if (startingRegion === R.EDGE_TOP)
{
coordDelta.x = 0;
scaleCenter = {'x': (box.x1 + box.x2) / 2, 'y': box.y2};
}
else if (startingRegion === R.EDGE_BOTTOM)
{
coordDelta.x = 0;
scaleCenter = {'x': (box.x1 + box.x2) / 2, 'y': box.y1};
}
else if (startingRegion === R.EDGE_LEFT)
{
coordDelta.y = 0;
scaleCenter = {'x': box.x2, 'y': (box.y1 + box.y2) / 2};
}
else if (startingRegion === R.EDGE_RIGHT)
{
coordDelta.y = 0;
scaleCenter = {'x': box.x1, 'y': (box.y1 + box.y2) / 2};
}
else // resize from corner
{
if (this.isAspectRatioLockedResize())
{
doConstraint = true;
/*
var widthHeightRatio = (box.x2 - box.x1) / (box.y2 - box.y1);
var currRatio = coordDelta.x / coordDelta.y;
if (Math.abs(currRatio) > widthHeightRatio)
//coordDelta.x = coordDelta.y * widthHeightRatio * (Math.sign(currRatio) || 1);
doConstraintOnY = true;
else
//coordDelta.y = coordDelta.x / widthHeightRatio * (Math.sign(currRatio) || 1);
doConstraintOnX = true;
*/
}
scaleCenter = (startingRegion === R.CORNER_TL)? {'x': box.x2, 'y': box.y2}:
(startingRegion === R.CORNER_TR)? {'x': box.x1, 'y': box.y2}:
(startingRegion === R.CORNER_BL)? {'x': box.x2, 'y': box.y1}:
{'x': box.x1, 'y': box.y1};
}
var reversedX = (startingRegion === R.CORNER_TL) || (startingRegion === R.CORNER_BL) || (startingRegion === R.EDGE_LEFT);
var reversedY = (startingRegion === R.CORNER_TL) || (startingRegion === R.CORNER_TR) || (startingRegion === R.EDGE_TOP);
// calc transform matrix
var scaleX, scaleY;
if (Kekule.NumUtils.isFloatEqual(box.x1, box.x2, 1e-10)) // box has no x size, can not scale on x
scaleX = 1;
else
scaleX = 1 + coordDelta.x / (box.x2 - box.x1) * (reversedX? -1: 1);
if (Kekule.NumUtils.isFloatEqual(box.y1, box.y2, 1e-10)) // box has no y size, can not scale on y
scaleY = 1;
else
scaleY = 1 + coordDelta.y / (box.y2 - box.y1) * (reversedY? -1: 1);
if (doConstraint)
{
var absX = Math.abs(scaleX), absY = Math.abs(scaleY);
if (absX >= absY)
scaleY = (Math.sign(scaleY) || 1) * absX; // avoid sign = 0
else
scaleX = (Math.sign(scaleX) || 1) * absY;
}
var actualScales = this._calcActualResizeScales(manipulatingObjs, {'scaleX': scaleX, 'scaleY': scaleY});
var transformParams = {'center': scaleCenter, 'scaleX': actualScales.scaleX, 'scaleY': actualScales.scaleY};
//console.log(this.isAspectRatioLockedResize(), scaleX, scaleY);
//console.log('startBox', box);
//console.log('transformParams', transformParams);
return transformParams;
},
/* @private */
/*
_calcManipulateObjsResizeInfo: function(manipulatingObjs, startingRegion, endScreenCoord)
{
var R = Kekule.Editor.BoxRegion;
var C = Kekule.CoordUtils;
var transformOps = this._calcManipulateObjsResizeParams(manipulatingObjs, startingRegion, endScreenCoord);
//console.log(scaleX, scaleY);
this._calcManipulateObjsTransformInfo(manipulatingObjs, transformOps);
return true;
},
*/
/** @private */
_calcManipulateObjsTransformInfo: function(manipulatingObjs, transformParams)
{
var C = Kekule.CoordUtils;
// since we transform screen coord, it will always be in 2D mode
// and now the editor only supports 2D
var is3D = false; // this.getEditor().getCoordMode() === Kekule.CoordMode.COORD3D;
var transformMatrix = is3D? C.calcTransform3DMatrix(transformParams): C.calcTransform2DMatrix(transformParams);
var scaleX = transformParams.scaleX || transformParams.scale;
var scaleY = transformParams.scaleY || transformParams.scale;
for (var i = 0, l = manipulatingObjs.length; i < l; ++i)
{
var obj = manipulatingObjs[i];
var info = this.getManipulateObjInfoMap().get(obj);
var newInfo = {};
if (!info.hasNoCoord) // this object has coord property and can be rotated
{
var oldCoord = info.screenCoord;
var newCoord = C.transform2DByMatrix(oldCoord, transformMatrix);
newInfo.screenCoord = newCoord;
//this._addManipultingObjNewInfo(obj, {'screenCoord': newCoord});
}
// TODO: may need change dimension also
if (info.size && (scaleX || scaleY))
{
var newSize = {'x': info.size.x * Math.abs(scaleX || 1), 'y': info.size.y * Math.abs(scaleY || 1)};
newInfo.size = newSize;
}
this._addManipultingObjNewInfo(obj, newInfo);
}
return true;
},
/*
* Resize manupulatedObjs according to endScreenCoord.
* @private
*/
/*
doResizeManipulatedObjs: function(endScreenCoord)
{
var editor = this.getEditor();
var objs = this.getManipulateObjs();
//var changedObjs = [];
this._calcManipulateObjsResizeInfo(objs, this.getResizeStartingRegion(), endScreenCoord);
editor.beginUpdateObject();
var newInfoMap = this.getManipulateObjCurrInfoMap();
try
{
this.applyManipulatingObjsInfo(endScreenCoord);
this._maniplateObjsFrameEnd(objs);
this.notifyCoordChangeOfObjects(objs);
}
finally
{
editor.endUpdateObject();
this.manipulateStepDone();
}
},
*/
/**
* Transform manupulatedObjs according to manipulateType(rotate/resize) endScreenCoord.
* @private
*/
doTransformManipulatedObjs: function(manipulateType, endScreenCoord, explicitTransformParams)
{
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var editor = this.getEditor();
var objs = this.getManipulateObjs();
//var changedObjs = [];
var transformParams = explicitTransformParams;
if (!transformParams)
{
if (manipulateType === T.RESIZE)
transformParams = this._calcManipulateObjsResizeParams(objs, this.getResizeStartingRegion(), endScreenCoord);
else if (manipulateType === T.ROTATE)
transformParams = this._calcManipulateObjsRotationParams(objs, endScreenCoord);
}
//console.log('do transform', transformParams);
var doConcreteTransform = transformParams && this._calcManipulateObjsTransformInfo(objs, transformParams);
if (!doConcreteTransform)
return;
editor.beginUpdateObject();
var newInfoMap = this.getManipulateObjCurrInfoMap();
try
{
this.applyManipulatingObjsInfo(endScreenCoord);
this._maniplateObjsFrameEnd(objs);
this.notifyCoordChangeOfObjects(objs);
}
finally
{
editor.endUpdateObject();
this.manipulateStepDone();
}
},
/* @private */
_calcActualMovedScreenCoord: function(obj, info, newScreenCoord)
{
return newScreenCoord;
},
/** @private */
_calcManipulateObjsMoveInfo: function(manipulatingObjs, endScreenCoord)
{
var C = Kekule.CoordUtils;
var newInfoMap = this.getManipulateObjCurrInfoMap();
for (var i = 0, l = manipulatingObjs.length; i < l; ++i)
{
var obj = manipulatingObjs[i];
var info = this.getManipulateObjInfoMap().get(obj);
if (info.hasNoCoord) // this object has no coord property and can not be moved
continue;
var newScreenCoord = C.add(endScreenCoord, info.screenCoordOffset);
newScreenCoord = this._calcActualMovedScreenCoord(obj, info, newScreenCoord);
this._addManipultingObjNewInfo(obj, {'screenCoord': newScreenCoord});
}
},
/**
* Move objects in manipulateObjs array to new position. New coord is determinated by endContextCoord
* and each object's offset.
* @private
*/
moveManipulatedObjs: function(endScreenCoord)
{
var C = Kekule.CoordUtils;
var editor = this.getEditor();
var objs = this.getManipulateObjs();
var changedObjs = [];
this._calcManipulateObjsMoveInfo(objs, endScreenCoord);
editor.beginUpdateObject();
var newInfoMap = this.getManipulateObjCurrInfoMap();
try
{
this.applyManipulatingObjsInfo(endScreenCoord);
this._maniplateObjsFrameEnd(objs);
// notify
this.notifyCoordChangeOfObjects(objs);
}
finally
{
editor.endUpdateObject();
this.manipulateStepDone();
}
},
/**
* Move a single object to newScreenCoord. MoverScreenCoord is the actual coord of mouse.
* Note that not only move operation will call this method, rotate and resize may also affect
* objects' coord so this method will also be called.
* @private
*/
doMoveManipulatedObj: function(objIndex, obj, newScreenCoord, moverScreenCoord)
{
var editor = this.getEditor();
this.updateChildMoveOperation(objIndex, obj, editor.screenCoordToObj(newScreenCoord));
editor.setObjectScreenCoord(obj, newScreenCoord);
},
/**
* Resize a single object to newDimension.
* @private
*/
doResizeManipulatedObj: function(objIndex, obj, newSize)
{
this.updateChildResizeOperation(objIndex, obj, newSize);
if (obj.setSizeOfMode)
obj.setSizeOfMode(newSize, this.getEditor().getCoordMode());
},
/*
* Moving complete, do the wrap up job.
* @private
*/
/*
endMoving: function()
{
this.stopManipulate();
},
*/
/**
* Click on a object or objects and manipulate it directly.
* @private
*/
startDirectManipulate: function(manipulateType, objOrObjs, startCoord, startBox, rotateCenter, rotateRefCoord)
{
var objs = Kekule.ArrayUtils.toArray(objOrObjs);
this.setState(Kekule.Editor.BasicManipulationIaController.State.MANIPULATING);
this.setBaseCoord(startCoord);
this.setStartCoord(startCoord);
this.setRotateRefCoord(rotateRefCoord);
this.setIsManipulatingSelection(false);
//console.log('call prepareManipulating', startCoord, manipulateType, objOrObjs);
this.prepareManipulating(manipulateType || Kekule.Editor.BasicManipulationIaController.ManipulationType.MOVE, objs, startCoord, startBox, rotateCenter, rotateRefCoord);
},
/**
* Called when a manipulation is applied and the changes has been reflected in editor (editor redrawn done).
* Descendants may override this method.
* @private
*/
manipulateStepDone: function()
{
// do nothing here
},
/**
* Called when a manipulation is ended (stopped or cancelled).
* Descendants may override this method.
* @private
*/
manipulateEnd: function()
{
if (this._runManipulationStepId)
{
window.cancelAnimationFrame(this._runManipulationStepId);
this._runManipulationStepId = null;
}
var editor = this.getEditor();
editor.endManipulateObject();
},
/**
* Stop manipulate of objects.
* @private
*/
stopManipulate: function()
{
this.setManipulateObjs(null);
this.getManipulateObjInfoMap().clear();
this.getObjOperationMap().clear();
this.manipulateEnd();
},
/** @private */
refreshManipulateObjs: function()
{
this.setManipulateObjs(this.getManipulateObjs());
},
/** @private */
createManipulateObjInfo: function(obj, objIndex, startScreenCoord)
{
var editor = this.getEditor();
var info = {
//'obj': obj,
'objCoord': editor.getObjCoord(obj), // abs base coord
//'objSelfCoord': obj.getCoordOfMode? obj.getCoordOfMode(editor.getCoordMode()): null,
'screenCoord': editor.getObjectScreenCoord(obj),
'size': editor.getObjSize(obj)
};
info.hasNoCoord = !info.objCoord;
if (!info.hasNoCoord && startScreenCoord)
info.screenCoordOffset = Kekule.CoordUtils.substract(info.screenCoord, startScreenCoord);
return info;
},
/** @private */
notifyCoordChangeOfObjects: function(objs)
{
var changedDetails = [];
var editor = this.getEditor();
var coordPropName = this.getEditor().getCoordMode() === Kekule.CoordMode.COORD3D? 'coord3D': 'coord2D';
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
Kekule.ArrayUtils.pushUnique(changedDetails, {'obj': obj, 'propNames': [coordPropName]});
var relatedObjs = obj.getCoordDeterminateObjects? obj.getCoordDeterminateObjects(): [obj];
for (var j = 0, k = relatedObjs.length; j < k; ++j)
Kekule.ArrayUtils.pushUnique(changedDetails, {'obj': relatedObjs[j], 'propNames': [coordPropName]});
}
// notify
editor.objectsChanged(changedDetails);
},
/** @private */
canInteractWithObj: function(obj)
{
return (this.getState() === Kekule.Editor.BasicManipulationIaController.State.NORMAL) && obj;
},
/** @ignore */
doTestMouseCursor: function(coord, e)
{
var result = '';
// since client element is not the same to widget element, coord need to be recalculated
var c = this._getEventMouseCoord(e, this.getEditor().getEditClientElem());
if (this.getState() === Kekule.Editor.BasicManipulationIaController.State.NORMAL)
{
var R = Kekule.Editor.BoxRegion;
var region = this.getEditor().getCoordRegionInSelectionMarker(c);
var result;
if (this.getEnableSelect()) // show move/rotate/resize marker in select ia controller only
{
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var availManipulationTypes = this.getCurrAvailableManipulationTypes();
//if (this.getEnableMove())
if (availManipulationTypes.indexOf(T.MOVE) >= 0)
{
result = (region === R.INSIDE)? 'move': '';
}
//if (!result && this.getEnableResize())
if (!result && (availManipulationTypes.indexOf(T.RESIZE) >= 0))
{
var result =
(region === R.CORNER_TL)? 'nwse-resize':
(region === R.CORNER_TR)? 'nesw-resize':
(region === R.CORNER_BL)? 'nesw-resize':
(region === R.CORNER_BR)? 'nwse-resize':
(region === R.EDGE_TOP) || (region === R.EDGE_BOTTOM)? 'ns-resize':
(region === R.EDGE_LEFT) || (region === R.EDGE_RIGHT)? 'ew-resize':
'';
}
if (!result)
{
//if (this.getEnableRotate())
if (availManipulationTypes.indexOf(T.ROTATE) >= 0)
{
var region = this.getCoordOnSelectionRotationRegion(c);
if (!!region)
{
var SN = Kekule.Widget.StyleResourceNames;
result = (region === R.CORNER_TL)? SN.CURSOR_ROTATE_NW:
(region === R.CORNER_TR)? SN.CURSOR_ROTATE_NE:
(region === R.CORNER_BL)? SN.CURSOR_ROTATE_SW:
(region === R.CORNER_BR)? SN.CURSOR_ROTATE_SE:
SN.CURSOR_ROTATE;
//console.log('rotate cursor', result);
}
}
}
}
}
return result;
},
/**
* Set operations in suspended state.
* @param {Func} immediateOper
* @param {Func} delayedOper
* @param {Int} delay In ms.
* @private
*/
setSuspendedOperations: function(immediateOper, delayedOper, delay)
{
var self = this;
this._suspendedOperations = {
'immediate': immediateOper,
'delayed': delayedOper,
'delayExecId': setTimeout(this.execSuspendedDelayOperation.bind(this), delay)
};
return this._suspendedOperations;
},
/**
* Execute the immediate operation in suspended operations, cancelling the delayed one.
* @private
*/
execSuspendedImmediateOperation: function()
{
if (this._suspendedOperations)
{
//console.log('exec immediate');
clearTimeout(this._suspendedOperations.delayExecId);
var oper = this._suspendedOperations.immediate;
this._suspendedOperations = null; // clear old
return oper.apply(this);
}
},
/**
* Execute the delayed operation in suspended operations, cancelling the immediate one.
* @private
*/
execSuspendedDelayOperation: function()
{
if (this._suspendedOperations)
{
//console.log('exec delayed');
clearTimeout(this._suspendedOperations.delayExecId);
var oper = this._suspendedOperations.delayed;
this._suspendedOperations = null; // clear old
return oper.apply(this);
}
},
/**
* Halt all suspend operations.
* @private
*/
haltSuspendedOperations: function()
{
if (this._suspendedOperations)
{
clearTimeout(this._suspendedOperations.delayExecId);
this._suspendedOperations = null; // clear old
}
},
/** @private */
_startNewSelecting: function(startCoord, shifted)
{
if (this.getEnableSelect())
{
this.getEditor().startSelecting(startCoord, shifted || this.getEditor().getIsToggleSelectOn());
this.setState(Kekule.Editor.BasicManipulationIaController.State.SELECTING);
}
},
/** @private */
_startOffSelectionManipulation: function(currCoord)
{
//console.log('off selection!');
this.beginManipulation(currCoord, null, Kekule.Editor.BasicManipulationIaController.ManipulationType.MOVE);
this.getEditor().pulseSelectionAreaMarker(); // pulse selection, reach the user's attention
},
/**
* Begin a manipulation.
* Descendants may override this method.
* @param {Hash} currCoord Current coord of pointer (mouse or touch)
* @param {Object} e Pointer (mouse or touch) event parameter.
*/
beginManipulation: function(currCoord, e, explicitManipulationType)
{
var S = Kekule.Editor.BasicManipulationIaController.State;
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var availManipulationTypes = this.getCurrAvailableManipulationTypes();
var evokedByTouch = e && e.pointerType === 'touch'; // edge resize/rotate will be disabled in touch
var editor = this.getEditor();
editor.beginManipulateObject();
this.setBaseCoord(currCoord);
this.setStartCoord(currCoord);
// check if mouse just on an object, if so, direct manipulation mode
var hoveredObj = this.getEditor().getTopmostBasicObjectAtCoord(currCoord, this.getCurrBoundInflation());
if (hoveredObj && !evokedByTouch) // mouse down directly on a object
{
//hoveredObj = hoveredObj.getNearestSelectableObject();
if (this.isInAncestorSelectMode())
hoveredObj = this.getStandaloneAncestor(hoveredObj);
hoveredObj = hoveredObj.getNearestMovableObject();
if (this.getEnableMove())
{
this.startDirectManipulate(null, hoveredObj, currCoord);
return;
}
}
var coordRegion = currCoord && this.getEditor().getCoordRegionInSelectionMarker(currCoord);
var R = Kekule.Editor.BoxRegion;
var rotateRegion = currCoord && this.getCoordOnSelectionRotationRegion(currCoord);
// test manipulate type
/*
var isTransform = (this.getEnableResize() || this.getEnableRotate())
&& (explicitManipulationType === T.TRANSFORM); // gesture transform
*/
var isTransform = (availManipulationTypes.indexOf(T.TRANSFORM) >= 0)
&& (explicitManipulationType === T.TRANSFORM); // gesture transform
if (!isTransform)
{
var isResize = !evokedByTouch && (availManipulationTypes.indexOf(T.RESIZE) >= 0) //&& this.getEnableResize()
&& ((explicitManipulationType === T.RESIZE) || ((coordRegion !== R.INSIDE) && (coordRegion !== R.OUTSIDE)));
var isMove = !isResize && (availManipulationTypes.indexOf(T.MOVE) >= 0) // this.getEnableMove()
&& ((explicitManipulationType === T.MOVE) || (coordRegion !== R.OUTSIDE));
var isRotate = !evokedByTouch && !isResize && !isMove && (availManipulationTypes.indexOf(T.ROTATE) >= 0)//this.getEnableRotate()
&& ((explicitManipulationType === T.ROTATE) || !!rotateRegion);
}
else // transform
{
this._availTransformTypes = availManipulationTypes; // stores the available transform types
}
// check if already has selection and mouse in selection rect first
//if (this.getEditor().isCoordInSelectionMarkerBound(coord))
if (isTransform)
{
this.setState(S.MANIPULATING);
this.setIsManipulatingSelection(true);
this.setResizeStartingRegion(coordRegion);
this.setRotateStartingRegion(rotateRegion);
this.prepareManipulating(T.TRANSFORM, this.getEditor().getSelection(), currCoord, this.getEditor().getSelectionContainerBox());
}
else if (isResize)
{
this.setState(S.MANIPULATING);
this.setIsManipulatingSelection(true);
this.setResizeStartingRegion(/*this.getEditor().getCoordRegionInSelectionMarker(coord)*/coordRegion);
//console.log('box', this.getEditor().getUiSelectionAreaContainerBox());
this.prepareManipulating(T.RESIZE, this.getEditor().getSelection(), currCoord, this.getEditor().getSelectionContainerBox());
//console.log('Resize');
}
else if (isMove)
{
//if (this.getEnableMove())
{
this.setState(S.MANIPULATING);
this.setIsManipulatingSelection(true);
this.prepareManipulating(T.MOVE, this.getEditor().getSelection(), currCoord);
}
}
else if (isRotate)
{
this.setState(S.MANIPULATING);
this.setIsManipulatingSelection(true);
this.setRotateStartingRegion(rotateRegion);
this.prepareManipulating(T.ROTATE, this.getEditor().getSelection(), currCoord, this.getEditor().getSelectionContainerBox());
}
else
{
/*
var obj = this.getEditor().getTopmostBasicObjectAtCoord(currCoord, this.getCurrBoundInflation());
if (obj) // mouse down directly on a object
{
obj = obj.getNearestSelectableObject();
if (this.isInAncestorSelectMode())
obj = this.getStandaloneAncestor(obj);
// only mouse down and moved will cause manupulating
if (this.getEnableMove())
this.startDirectManipulate(null, obj, currCoord);
}
*/
if (hoveredObj) // point on an object, direct move
{
if (this.getEnableMove())
this.startDirectManipulate(null, hoveredObj, currCoord);
}
else // pointer down on empty region, deselect old selection and prepare for new selecting
{
if (this.getEnableMove() && this.getEnableSelect()
&& this.getEditorConfigs().getInteractionConfigs().getEnableOffSelectionManipulation()
&& this.getEditor().hasSelection() && this.getEditor().isSelectionVisible())
{
//console.log('enter suspend');
this.setState(S.SUSPENDING);
// need wait for a while to determinate the actual operation
var delay = this.getEditorConfigs().getInteractionConfigs().getOffSelectionManipulationActivatingTimeThreshold();
var shifted = e && e.getShiftKey();
this.setSuspendedOperations(
this._startNewSelecting.bind(this, currCoord, shifted),
this._startOffSelectionManipulation.bind(this, currCoord),
delay
);
//this._startOffSelectionManipulation(currCoord);
}
else if (this.getEnableSelect())
{
var shifted = e && e.getShiftKey();
/*
//this.getEditor().startSelectingBoxDrag(currCoord, shifted);
//this.getEditor().setSelectMode(this.getSelectMode());
this.getEditor().startSelecting(currCoord, shifted);
this.setState(S.SELECTING);
*/
this._startNewSelecting(currCoord, shifted);
}
}
}
},
/**
* Do manipulation based on mouse/touch move step.
* //@param {Hash} currCoord Current coord of pointer (mouse or touch)
* //@param {Object} e Pointer (mouse or touch) event parameter.
*/
execManipulationStep: function(/*currCoord, e*/timeStamp)
{
if (this.getState() !== Kekule.Editor.BasicManipulationIaController.State.MANIPULATING)
return false;
var currCoord = this._manipulationStepBuffer.coord;
var e = this._manipulationStepBuffer.event;
var explicitTransformParams = this._manipulationStepBuffer.explicitTransformParams;
if (currCoord && e)
{
//console.log('do actual manipulate');
this.doExecManipulationStep(currCoord, e, this._manipulationStepBuffer);
// empty buffer, indicating that the event has been handled
}
else if (explicitTransformParams) // has transform params explicitly in gesture transform
{
this.doExecManipulationStepWithExplicitTransformParams(explicitTransformParams, this._manipulationStepBuffer);
}
this._manipulationStepBuffer.coord = null;
this._manipulationStepBuffer.event = null;
this._manipulationStepBuffer.explicitTransformParams = null;
/*
if (this._lastTimeStamp)
console.log('elpase', timeStamp - this._lastTimeStamp);
this._lastTimeStamp = timeStamp;
*/
this._runManipulationStepId = window.requestAnimationFrame(this.execManipulationStepBind);
},
/**
* Do actual manipulation based on mouse/touch move step.
* Descendants may override this method.
* @param {Hash} currCoord Current coord of pointer (mouse or touch)
* @param {Object} e Pointer (mouse or touch) event parameter.
*/
doExecManipulationStep: function(currCoord, e, manipulationStepBuffer)
{
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var manipulateType = this.getManipulationType();
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
this._isBusy = true;
if (manipulateType === T.MOVE)
{
this.moveManipulatedObjs(currCoord);
}
else if (manipulateType === T.RESIZE)
{
this._suppressConstrainedResize = e.getAltKey();
//this.doResizeManipulatedObjs(currCoord);
this.doTransformManipulatedObjs(manipulateType, currCoord);
}
else if (manipulateType === T.ROTATE)
{
//this.rotateManipulatedObjs(currCoord);
this.doTransformManipulatedObjs(manipulateType, currCoord);
}
}
finally
{
editor.endUpdateObject();
this._isBusy = false;
}
},
/**
* Do actual manipulation based on mouse/touch move step.
* Descendants may override this method.
* @param {Hash} currCoord Current coord of pointer (mouse or touch)
* @param {Object} e Pointer (mouse or touch) event parameter.
*/
doExecManipulationStepWithExplicitTransformParams: function(transformParams, manipulationStepBuffer)
{
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var manipulateType = this.getManipulationType();
if (manipulateType === T.TRANSFORM)
{
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
this._isBusy = true;
this.doTransformManipulatedObjs(manipulateType, null, transformParams);
}
finally
{
editor.endUpdateObject();
this._isBusy = false;
}
}
},
/**
* Refill the manipulationStepBuffer.
* Descendants may override this method.
* @param {Object} e Pointer (mouse or touch) event parameter.
* @private
*/
updateManipulationStepBuffer: function(buffer, value)
{
Object.extend(buffer, value);
/*
buffer.coord = coord;
buffer.event = e;
*/
},
// event handle methods
/** @ignore */
react_pointermove: function($super, e)
{
$super(e);
if (this._isBusy)
{
return true;
}
var S = Kekule.Editor.BasicManipulationIaController.State;
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var coord = this._getEventMouseCoord(e);
var distanceFromLast;
if (this._lastMouseMoveCoord)
{
var dis = Kekule.CoordUtils.getDistance(coord, this._lastMouseMoveCoord);
distanceFromLast = dis;
if (dis < 2) // less than 2 px, too tiny to react
{
return true;
}
}
this._lastMouseMoveCoord = coord;
/*
if (state !== S.NORMAL)
this.getEditor().hideHotTrack();
if (state === S.NORMAL)
{
// in normal state, if mouse moved to boundary of a object, it may be highlighted
this.getEditor().hotTrackOnCoord(coord);
}
else
*/
if (this.getState() === S.SUSPENDING)
{
var disThreshold = this.getEditorConfigs().getInteractionConfigs().getUnmovePointerDistanceThreshold() || 0;
if (Kekule.ObjUtils.notUnset(distanceFromLast) && (distanceFromLast > disThreshold))
this.execSuspendedImmediateOperation();
}
var state = this.getState();
if (state === S.SELECTING)
{
if (this.getEnableSelect())
{
//this.getEditor().dragSelectingBoxToCoord(coord);
this.getEditor().addSelectingAnchorCoord(coord);
}
e.preventDefault();
}
else if (state === S.MANIPULATING) // move or resize objects
{
//console.log('mouse move', coord);
this.updateManipulationStepBuffer(this._manipulationStepBuffer, {'coord': coord, 'event': e});
//this.execManipulationStep(coord, e);
e.preventDefault();
}
return true;
},
/** @private */
react_pointerdown: function($super, e)
{
$super(e);
//console.log('pointerdown', e);
var S = Kekule.Editor.BasicManipulationIaController.State;
//var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
this._lastMouseMoveCoord = null;
var coord = this._getEventMouseCoord(e);
if ((this.getState() === S.NORMAL)/* && (this.getEditor().getMouseLBtnDown()) */)
{
this.beginManipulation(coord, e);
e.preventDefault();
}
}
else if (e.getButton() === Kekule.X.Event.MouseButton.RIGHT)
{
//if (this.getEnableMove())
{
if (this.getState() === S.MANIPULATING) // when click right button on manipulating, just cancel it.
{
this.cancelManipulate();
this.setState(S.NORMAL);
e.stopPropagation();
e.preventDefault();
}
else if (this.getState() === S.SUSPENDING)
this.haltSuspendedOperations();
}
}
return true;
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
var coord = this._getEventMouseCoord(e);
this.setEndCoord(coord);
var startCoord = this.getStartCoord();
var endCoord = coord;
var shifted = e.getShiftKey();
var S = Kekule.Editor.BasicManipulationIaController.State;
if (this.getState() === S.SUSPENDING) // done suspended first, then finish the operation
this.execSuspendedImmediateOperation();
var state = this.getState();
if (state === S.SELECTING) // mouse up, end selecting
{
//this.getEditor().endSelectingBoxDrag(coord, shifted);
this.getEditor().endSelecting(coord, shifted || this.getEditor().getIsToggleSelectOn());
this.setState(S.NORMAL);
e.preventDefault();
var editor = this.getEditor();
editor.endManipulateObject();
}
else if (state === S.MANIPULATING)
{
//var dis = Kekule.CoordUtils.getDistance(startCoord, endCoord);
//if (dis <= this.getEditorConfigs().getInteractionConfigs().getUnmovePointerDistanceThreshold())
if (Kekule.CoordUtils.isEqual(startCoord, endCoord)) // mouse down and up in same point, not manupulate, just select a object
{
if (this.getEnableSelect())
this.getEditor().selectOnCoord(startCoord, shifted || this.getEditor().getIsToggleSelectOn());
}
else // move objects to new pos
{
/*
if (this.getEnableMove())
{
//this.moveManipulatedObjs(coord);
//this.endMoving();
// add operation to editor's historys
this.addOperationToEditor();
}
*/
this.addOperationToEditor();
}
this.stopManipulate();
this.setState(S.NORMAL);
e.preventDefault();
}
}
return true;
},
/** @private */
react_mousewheel: function($super, e)
{
if (e.getCtrlKey())
{
var state = this.getState();
if (state === Kekule.Editor.BasicManipulationIaController.State.NORMAL)
{
// disallow mouse zoom during manipulation
return $super(e);
}
e.preventDefault();
}
},
/* @private */
/*
react_keyup: function(e)
{
var keyCode = e.getKeyCode();
switch (keyCode)
{
case 46: // delete
{
if (this.getEnableRemove())
this.removeSelection();
}
}
}
*/
//////////////////// Hammer Gesture event handlers ///////////////////////////
/** @private */
_isGestureManipulationEnabled: function()
{
return this.getEditorConfigs().getInteractionConfigs().getEnableGestureManipulation();
},
/** @private */
_isGestureZoomOnEditorEnabled: function()
{
return this.getEditorConfigs().getInteractionConfigs().getEnableGestureZoomOnEditor();
},
/** @private */
_isInGestureManipulation: function()
{
return !!this._initialGestureTransformParams;
},
/** @private */
_isGestureZoomOnEditor: function()
{
return !!this._initialGestureZoomLevel;
},
/**
* Starts a gesture transform.
* @param {Object} event
* @private
*/
beginGestureTransform: function(event)
{
if (this.getEditor().hasSelection())
{
this._initialGestureZoomLevel = null;
if (this._isGestureManipulationEnabled())
{
this.haltSuspendedOperations(); // halt possible touch hold manipulations
// stores initial gesture transform params
this._initialGestureTransformParams = {
'angle': (event.rotation * Math.PI / 180) || 0
};
// start a brand new one
if (this.getState() !== Kekule.Editor.BasicManipulationIaController.State.MANIPULATING)
this.beginManipulation(null, null, Kekule.Editor.BasicManipulationIaController.ManipulationType.TRANSFORM);
else
{
if (this.getManipulationType() !== Kekule.Editor.BasicManipulationIaController.ManipulationType.TRANSFORM)
this.setManipulationType(Kekule.Editor.BasicManipulationIaController.ManipulationType.TRANSFORM);
}
}
else
this._initialGestureTransformParams = null;
}
else if (this._isGestureZoomOnEditorEnabled()) // zoom on editor
{
this.getEditor().cancelSelecting(); // force store the selecting
this.setState(Kekule.Editor.BasicManipulationIaController.State.NORMAL);
this._initialGestureZoomLevel = this.getEditor().getZoom();
}
},
/**
* Ends a gesture transform.
* @private
*/
endGestureTransform: function()
{
if (this.getState() === Kekule.Editor.BasicManipulationIaController.State.MANIPULATING) // stop prev manipulation first
{
if (this._isInGestureManipulation())
{
this.addOperationToEditor();
this.stopManipulate();
this.setState(Kekule.Editor.BasicManipulationIaController.State.NORMAL);
this._initialGestureTransformParams = null;
}
}
if (this._isGestureZoomOnEditor())
{
this._initialGestureZoomLevel = null;
}
},
/**
* Do a new transform step according to received event.
* @param {Object} e Gesture event received.
* @private
*/
doGestureTransformStep: function(e)
{
var T = Kekule.Editor.BasicManipulationIaController.ManipulationType;
if ((this.getState() === Kekule.Editor.BasicManipulationIaController.State.MANIPULATING)
&& (this.getManipulationType() === T.TRANSFORM)
&& (this._isInGestureManipulation()))
{
var availTransformTypes = this._availTransformTypes || [];
// get transform params from event directly
var center = this.getRotateCenter(); // use the center of current editor selection
var resizeScales, rotateAngle;
if (availTransformTypes.indexOf(T.RESIZE) >= 0)
{
var scale = e.scale;
resizeScales = this._calcActualResizeScales(this.getManipulateObjs(), {'scaleX': scale, 'scaleY': scale});
}
else
resizeScales = {'scaleX': 1, 'scaleY': 1};
if (availTransformTypes.indexOf(T.ROTATE) >= 0)
{
var absAngle = e.rotation * Math.PI / 180;
var rotateAngle = absAngle - this._initialGestureTransformParams.angle;
// get actual rotation angle
rotateAngle = this._calcActualRotateAngle(this.getManipulateObjs(), rotateAngle, this._initialGestureTransformParams.angle, absAngle);
}
else
{
rotateAngle = 0;
}
this.updateManipulationStepBuffer(this._manipulationStepBuffer, {
'explicitTransformParams': {
'center': center,
'scaleX': resizeScales.scaleX, 'scaleY': resizeScales.scaleY,
'rotateAngle': rotateAngle
//'rotateDegree': e.rotation,
//'event': e
}
});
e.preventDefault();
}
else if (this._isGestureZoomOnEditor())
{
var editor = this.getEditor();
var scale = e.scale;
var initZoom = this._initialGestureZoomLevel;
editor.zoomTo(initZoom * scale, null, e.center);
}
},
/** @ignore */
react_rotatestart: function(e)
{
if (this.getEnableGestureManipulation())
this.beginGestureTransform(e);
},
/** @ignore */
react_rotate: function(e)
{
if (this.getEnableGestureManipulation())
this.doGestureTransformStep(e);
},
/** @ignore */
react_rotateend: function(e)
{
if (this.getEnableGestureManipulation())
this.endGestureTransform();
},
/** @ignore */
react_rotatecancel: function(e)
{
if (this.getEnableGestureManipulation())
this.endGestureTransform();
},
/** @ignore */
react_pinchstart: function(e)
{
if (this.getEnableGestureManipulation())
this.beginGestureTransform(e);
},
/** @ignore */
react_pinchmove: function(e)
{
if (this.getEnableGestureManipulation())
this.doGestureTransformStep(e);
},
/** @ignore */
react_pinchend: function(e)
{
if (this.getEnableGestureManipulation())
this.endGestureTransform();
},
/** @ignore */
react_pinchcancel: function(e)
{
if (this.getEnableGestureManipulation())
this.endGestureTransform();
}
});
/**
* Enumeration of state of a {@link Kekule.Editor.BasicManipulationIaController}.
* @class
*/
Kekule.Editor.BasicManipulationIaController.State = {
/** Normal state. */
NORMAL: 0,
/** Is selecting objects. */
SELECTING: 1,
/** Is manipulating objects (e.g. changing object position). */
MANIPULATING: 2,
/**
* Just put down pointer, if move the pointer immediately, selecting state will be open.
* But if hold down still for a while, it may turn to manipulating state to move current selected objects.
*/
SUSPENDING: 11
};
/**
* Enumeration of manipulation types of a {@link Kekule.Editor.BasicManipulationIaController}.
* @class
*/
Kekule.Editor.BasicManipulationIaController.ManipulationType = {
MOVE: 0,
ROTATE: 1,
RESIZE: 2,
TRANSFORM: 4 // scale and rotate simultaneously by touch
};
/** @ignore */
Kekule.Editor.IaControllerManager.register(Kekule.Editor.BasicManipulationIaController, Kekule.Editor.BaseEditor);
})();