@doc hierarchy="GMLDOM">
Aspect for drawing zoomable blocks. A block is a shape that can contain any other shape, including other blocks.
(c) SAP AG 2003-2006. All rights reserved.
///////////////////////////////////////////////////////////////////////
// ASPECT HEADER
Aspect ZBlock for ZDrawing;
inherit ZShape;
constructor(diagram, parent)
this.isRoot = (parent == diagram);
this.supercall();
end
destructor
// remove nested graphics
var diag=this.getDiagram();
var lines = base[base.@linesKey];
for (var k in lines) diag.removeGraphic(k);
var shapes = base[base.@shapesKey];
for (var k in shapes) diag.removeGraphic(k);
this.supercall();
this.contentNode = null;
this.canvas.removeWidget(this.id);
end
//////////////////////////////////////////////////////////////////////////////////////
// BASE PROPERTIES
<@doc group="Instance Properties">
Gets or sets the block's scale
The ~scale is a positive floating point number between 0 (exclusive) and 1 (inclusive),
indicating the scale of the block's inner coordinates system (the block's contents)
relative to the block's outer coordinates system (containing block or diagram).
property scale = 1;
//////////////////////////////////////////////////////////////////////////////////////
// STATIC PROPERTIES
<@doc group="Aspect Properties">
Defines the block's lines collection key
The ~linesKey is the name of the collection on the base object in which the block lines are stored
static readonly property linesKey = null;
<@doc>
Defines the block's shapes collection key
The ~shapesKey is the name of the collection on the base object in which the block shapes are stored
static readonly property shapesKey = null;
<@doc type="@BLOCK_LAYOUT" default="SVG_LAYOUT_NONE">Defines the block's layout constraints
static readonly property layoutMode = #[SVG_LAYOUT_NONE];
<@doc type="n">Defines the spacing between adjacent shapes in the block layout
static readonly property layoutSpacing = 10;
<@doc type="b">Defines whether the child button should be added into this shape
static readonly property childButton = false;
<@struct name="BLOCK_LAYOUT" group="Structures">
A bitwise structure for defining block layout constraints
A block layout can be any one of the following bitwise flags:
Name | Description
SVG_LAYOUT_NONE | No layout. The block shapes can be freely arranged by the user, and overlapping shapes are allowed.
SVG_LAYOUT_PLOW | The block shapes can be arranged by the user, but without overlaps. Overlapping shapes are automatically plowed (moved away from each other).
SVG_LAYOUT_HFLOW | The block shapes are arranged in horizontal flow. The user can control the width and relative order of the block shapes.
SVG_LAYOUT_VFLOW | The block shapes are arranged in vertical flow. The user can control the height and relative order of the block shapes.
SVG_LAYOUT_HSPLIT | Same as horizontal flow, but with a draggable splitter between adjacent block shapes.
SVG_LAYOUT_VSPLIT | Same as vertical flow, but with a draggable splitter between adjacent block shapes.
//////////////////////////////////////////////////////////////////////////////////////
// ASPECT PROPERTIES
// Status flags
<@doc type="b" scope="private">Indicates that this shape is a block (for quick tests)
virtual property isBlock = true;
<@doc type="b" scope="private">Indicates that this block is the root block
virtual property isRoot = false;
<@doc type="b" scope="private">Indicates whether the block is expanded
virtual property isExpanded = true;
<@doc type="b" scope="private">Indicates whether the block is a currently active block
virtual property isActive = false;
<@doc type="b" scope="private">Indicates whether the block has already been painted
virtual property isPainted = false;
// SVG painting objects
<@doc type="SVGNode" scope="private">Gets the block's inner border SVG node
virtual property borderNode = null;
<@doc type="SVGNode" scope="private">Gets the block's contents SVG node
virtual property contentNode = null;
<@doc type="SVGNode" scope="private">Gets the block's lines SVG layer
virtual property linesLayer = null;
<@doc type="SVGNode" scope="private">Gets the block's shapes SVG layer
virtual property shapesLayer = null;
// Geometry properties
<@doc scope="private">Gets the block's inner coordinates system x-origin (relative to the canvas)
virtual property qx = 0;
<@doc scope="private">Gets the block's inner coordinates system y-origin (relative to the canvas)
virtual property qy = 0;
<@doc scope="private">Gets the block's inner coordinates system scale (relative to the canvas)
virtual property qs = 1;
//////////////////////////////////////////////////////////////////////////////////////
// MODEL EVENTS
override virtual method onModelInsert(evt)
var diag = this.getDiagram();
var aspect = $DOM.getAspectOf(evt.child, '#NS[ZDrawing]') || null;
if (!aspect) return;
if (aspect.prototype.isShape) {
// insert or move shape
var shape = evt.oldParent && diag.getGraphic(evt.child.id) || null;
if (shape) {
// move shape from old block
var oldParent=shape.getParent();
oldParent.shapesLayer.removeChild(shape.graphicNode);
this.shapesLayer.appendChild(shape.graphicNode);
shape.parentID = this.id;
oldParent.updateToggleBtn();
} else {
// create new shape
shape = diag.createGraphic(evt.child, this);
// resize-to-content on drop, if requested
var BB = shape.base.@boundingBox;
if (BB && BOOL(BB.defAuto)) {
diag.begin('Auto-resize shape on create');
shape.fitToBody(true);
var w = MAX(BB.defWidth||0, shape.w), h = MAX(BB.defHeight||0, shape.h);
shape.invalidate('@size', w+' '+h);
diag.commit();
}
}
if (shape) {
shape.updateCoordSys(this.qx, this.qy, this.qs);
var curBoard = $ENV.contextBoard || null;
// When entring the below IF, it means that current shape's parent is changed via Layout board
if (curBoard && curBoard.name == 'Layout') {
this.getDiagram().applyGlobalLayout(); // apply global layouting ONLY in LYT board
var shapeBase = shape.base;
var canvas=this.canvas;
if (shapeBase.@pinsKey) {
var pins = shapeBase[shapeBase.@pinsKey];
for (var i in pins) {
var pin = canvas.getWidget(pins[i].id);
if (!pin) continue;
var lines = pin.lines;
for (var j in lines) {
var line=canvas.getWidget(j);
if (!line) continue;
line.reroute(true);
}
}
}
}
}
this.updateToggleBtn();
if (diag.isZoomable && shape.isBlock) diag.setActiveBlock(shape); // TODO: BlockZoom work
}
else if (aspect.prototype.isLine) {
// insert or move line
var line = evt.oldParent && diag.getGraphic(evt.child.id) || null;
if (line) {
// move line from old block
line.getParent().linesLayer.removeChild(line.graphicNode);
this.linesLayer.appendChild(line.graphicNode);
line.parentID = this.id;
} else {
// create new line
line = diag.createGraphic(evt.child, this);
}
}
else if (aspect.prototype.isPin) {
// insert pin
this.supercall();
}
end
override virtual method onModelRemove(evt)
var diag=this.getDiagram(), id=evt.child.id, g=diag.getGraphic(id);
if (!g) {
// remove pin
this.supercall();
} else if (!evt.newParent) {
// remove shape or line
diag.removeGraphic(id);
this.updateToggleBtn();
}
end
//////////////////////////////////////////////////////////////////////////////////////
// PAINTING METHODS
override virtual method paint()
this.supercall();
// create block content area
this.contentNode = SVG.createElement(this.graphicNode, 'svg', {overflow: 'visible'});
var borderProps = {x:0, y:0, width:'100%', height:'100%', fill:'none', stroke:'none', tooltip:'hide', 'pointer-events':'none'};
if (!this.isRoot) {
borderProps['stroke-width'] = 4;
borderProps['stroke-opacity'] = 0.6;
borderProps['pointer-events'] = 'visible';
borderProps['stroke-linejoin'] = 'miter';
}
this.borderNode = SVG.createElement(this.contentNode, 'rect', borderProps);
this.shapesLayer = SVG.createElement(this.contentNode, 'g');
this.linesLayer = SVG.createElement(this.contentNode, 'g');
this.repaintBackground();
this.paintContents();
this.updateToggleBtn();
this.updateChildBtn();
// create shape remark
if (!ISEMPTY(base.comment)) this.createRemarkNode();
end
virtual method paintContents()
if (this.isPainted) return;
// create block shapes
var diag = this.getDiagram();
var C = base[base.@shapesKey];
for (var k in C) {
var aspect = $DOM.getAspectOf(C[k], '#NS[ZDrawing]');
if (aspect && aspect.prototype.isShape) {
diag.createGraphic(C[k], this);
}
}
// create block lines
var L = base[base.@linesKey];
for (var k in L) {
var aspect = $DOM.getAspectOf(L[k], '#NS[ZDrawing]');
if (aspect && aspect.prototype.isLine) {
diag.createGraphic(L[k], this);
}
}
this.isPainted = true;
end
override virtual method repaint(prop)
var flags = this.supercall();
// adjusts the block's viewbox after size or scale have changed
if (flags & (#[SVG_REPAINT_SIZE|SVG_REPAINT_SCALE])) {
var FP=base.@framePadding, SC=base.@scale;
var x=-this.w/2+FP.left, y=-this.h/2+FP.top;
var w=this.w-FP.left-FP.right, h=this.h-FP.top-FP.bottom;
SVG.setRect(this.contentNode, x, y, w, h);
this.contentNode.setAttribute('viewBox', '0 0 '+(w/SC)+' '+(h/SC));
}
// adjusts the block's inner coordinates system if any of the positioning properties has changed
if (flags) {
this.updateCoordSys();
}
end
override virtual method repaintBackground()
if (!base) return;
var color=base.@fillColor, opacity=1;
// var color='#FFFFFF', opacity=1;
if (this.isRoot) {
color='none';
} else if (GETVAR('SVG_COLOR_NESTING')) {
if (this.isSelected) {
color=this.hilightColor, opacity=0.2;
} else {
color=base.@fillColor, opacity=0.05;
}
}
SVG.setProperty(this.borderNode, '$fill', color);
SVG.setProperty(this.borderNode, '$fill-opacity', opacity);
end
//////////////////////////////////////////////////////////////////////////////////////
// BLOCK EXPAND/COLLAPSE
virtual method expand()
if (this.isExpanded) return;
if (!this.isPainted) {
this.paintContents();
this.repaint();
} else {
SVG.show(this.shapesLayer);
SVG.show(this.linesLayer);
}
this.isExpanded = true;
this.updateToggleBtn();
end
virtual method collapse()
if (!this.isExpanded || !this.isPainted) return;
SVG.hide(this.shapesLayer);
SVG.hide(this.linesLayer);
this.isExpanded = false;
this.updateToggleBtn();
end
virtual method setHorizon(horizon, depth)
if (!depth) depth=0;
if (depth >= horizon) this.collapse(); else this.expand();
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var g=canvas.getWidget(k);
if (g && g.isBlock) g.setHorizon(horizon, depth+1);
}
end
virtual method updateToggleBtn()
// TODO: BlockZoom: in zoomable diagram, any zoom-able block should always have toggle button, therefore
// simplify the code here and remove updateToggleBtn() calls from everywhere, except expand/collapse
if (!this.getDiagram().isZoomable || this.isRoot) return;
var btn=this.btnToggle;
// TODO: BlockZoom: uncomment and fix this when block zoomability becomes controllable
//if (ISEMPTY(this.shapes)) {
// if (btn) btn.style.setProperty('display', 'none');
// return;
//}
var state=(this.isExpanded ? 1 : 0)
if (!btn) {
this.btnToggle = SVG.createFlipper(this.contentNode,
['blockExpandBtn', 'blockCollapseBtn'],
{target:this, onClick:'onToggleBtn', width:11, state:state, x:15}
);
SVG.setTransform(this.btnToggle, 3, 3);
} else {
btn.style.setProperty('display', 'block');
btn.setState(state);
}
end
virtual method onToggleBtn()
var diag = this.getDiagram();
if (diag.isZoomable) diag.setActiveBlock(this.isExpanded ? this.getParent() : this);
else if (this.isExpanded) this.collapse(); else this.expand();
this.updateToggleBtn();
end
virtual method updateChildBtn()
if(!base.@childButton || this.isRoot) return;
var btn=this.childBtn;
if (!btn) {
// TODO: get this via defineShape and not via DOM dependency
var ns = base.Class.metadata['layerNamestem']||null;
var tooltip = ns ? ('#TEXT[XTOL_DOM_TCONTAINER_ADD_PREFIX]' + ' ' + ns) : null;
this.childBtn = SVG.createButton(this.contentNode, 'blockChildBtn', {glyph: 'childBtn', target:this, onClick:'onClickChildBtn', tooltip: tooltip, width:11, x:0}, SvgClickable);
SVG.setTransform(this.childBtn, 3, 3);
}
end
virtual method onClickChildBtn()
RULE('createChildShape', $ENV.contextUnit, this.getDiagram().boardAspect, base);
end
//////////////////////////////////////////////////////////////////////////////////////
// INTERACTIVE EFFECTS
override virtual method showSelectionEffect()
this.supercall();
this.repaintBackground();
end
override virtual method hideSelectionEffect()
this.supercall();
this.repaintBackground();
end
override virtual method showDropEffect(animate)
if (this.isDropTarget) return;
if (this.isRoot && !GETVAR('SVG_SHOW_BORDER')) return;
this.isDropTarget = true;
this.borderNode.setAttribute('stroke', this.hilightColor);
if (this.isAnimated) {
SVG.clearElement(this.borderNode);
this.isAnimated = false;
}
if (animate) {
SVG.createFragment(this.borderNode, '');
this.isAnimated = true;
}
end
override virtual method hideDropEffect()
if (!this.isDropTarget) return;
if (this.isRoot && !GETVAR('SVG_SHOW_BORDER')) return;
this.isDropTarget = false;
this.borderNode.setAttribute('stroke', 'none');
if (this.isAnimated) {
SVG.clearElement(this.borderNode);
this.isAnimated = false;
}
end
<@doc scope="private">
Shows the block's wiring effect
virtual method showWiringEffect(ptr)
var diag = this.getDiagram();
var canvas = this.canvas;
if (diag.isZoomable) this.showZoomableEffect(); // TODO: BlockZoom: temp
// if this block has already been visited during this wiring interaction, hilight previously tested pins
if (this.id in ptr.visitedBlocks) {
var pins=ptr.visitedBlocks[this.id];
for (var k in pins) {
var pin = canvas.getWidget(k);
if (pin) pin.showWiringEffect(ptr);
}
return;
}
// otherwise, scan and test all pins in this block
var pins={}, checked = { target:null, list:{} };
var srcpin = ptr.getSrcpin && ptr.getSrcpin() || canvas.getWidget(ptr.srcpinID);
var shapes = base[base.@shapesKey];
for (var k in shapes) {
var shape=canvas.getWidget(k);
if (!shape) continue;
var shapeBase = shape.base;
if (!shapeBase.@pinsKey) continue;
var P = shapeBase[shapeBase.@pinsKey];
for (var k2 in P) {
var pin=canvas.getWidget(P[k2].id);
if (!pin || pin == srcpin) continue;
if (pin.isInward) {
if (srcpin.isInward) continue;
var src=srcpin.base, trg=pin.base;
} else if (pin.isOutward) {
if (srcpin.isOutward) continue;
var src=pin.base, trg=srcpin.base;
} else { // pin.isInout
if (srcpin.isOutward) var src=srcpin.base, trg=pin.base;
else var src=pin.base, trg=srcpin.base;
}
if (!RULE('canConnectTo', diag.base, diag.boardAspect, src, trg)) continue;
if (RULE('isCyclicConnection', diag.base, diag.boardAspect, src, trg, checked)) continue;
pin.showWiringEffect(ptr);
pins[pin.id] = true;
}
}
ptr.visitedBlocks[this.id] = pins;
end
<@doc scope="private">
Hides the block's wiring effect
virtual method hideWiringEffect(ptr)
if (this.getDiagram().isZoomable) this.hideZoomableEffect(); // TODO: BlockZoom: temp
var canvas = this.canvas;
var pins=ptr.visitedBlocks[this.id];
if (!pins) return;
for (var k in pins) {
var pin = canvas.getWidget(k);
if (pin) pin.hideWiringEffect(ptr);
}
end
override virtual method flyIntoView()
this.expand();
return this.supercall();
end
//////////////////////////////////////////////////////////////////////////////////////
// GEOMETRIC CALCULATIONS
<@doc scope="private">
Updates the coordinates system of all block's members
override virtual method updateCoordSys(ox,oy,os)
if (arguments.length == 3) {
this.ox = ox;
this.oy = oy;
this.os = os;
}
var FP = base.@framePadding;
this.qx = this.ox+(this.x1+FP.left)*this.os;
this.qy = this.oy+(this.y1+FP.top)*this.os;
this.qs = this.os*base.@scale;
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var shape=canvas.getWidget(k);
if (shape) shape.updateCoordSys(this.qx, this.qy, this.qs);
}
end
<@doc scope="private">
Gets the block's size limits
override virtual method getSizeLimits(sx,sy)
// TODO: TEMP for BlockZoom
var bbox = this.getDiagram().isZoomable ? this.getBBox() : this.shapesLayer.getBBox();
var x1=bbox.x, y1=bbox.y, x2=x1+bbox.width, y2=y1+bbox.height;
var FP = base.@framePadding
var BP = base.@bodyPadding;
var BB = base.@boundingBox;
var SC = base.@scale;
if (base.@layoutMode & #[SVG_LAYOUT_FLOW]) {
this.minWidth = BP.left+BP.right;
this.minHeight = BP.top+BP.bottom;
} else {
if (sx < 0) {
this.minWidth = POS(this.w-(x1-BP.left)*SC);
} else if (sx > 0) {
this.minWidth = POS((x2+BP.right)*SC) + FP.right + FP.left;
} else {
this.minWidth = 0;
}
this.minWidth = MAX(this.minWidth, BB.minWidth||0);
if (sy < 0) {
this.minHeight = POS(this.h-(y1-BP.top)*SC);
} else if (sy > 0) {
this.minHeight = POS((y2+BP.bottom)*SC) + FP.top + FP.bottom;
} else {
this.minHeight = 0;
}
this.minHeight = MAX(this.minHeight, BB.minHeight||0);
}
return this.supercall();
end
<@doc scope="private">
Returns the block's control handles data
override virtual method getHandlesData()
var ox=this.ox, oy=this.oy, os=this.os;
var L=ox+this.x1*os, R=ox+this.x2*os, C=ox+this.cx*os;
var T=oy+this.y1*os, B=oy+this.y2*os, M=oy+this.cy*os;
return {
/* NN:{cx:C,cy:T},*/ SS:{cx:C,cy:B}, WW:{cx:L,cy:M}, EE:{cx:R,cy:M},
NW:{cx:L,cy:T}, NE:{cx:R,cy:T}, SW:{cx:L,cy:B}, SE:{cx:R,cy:B}
}
end
<@doc scope="private">
Gets the block's tooltip positioning data
override virtual method getTooltipPos()
var ox=this.ox, oy=this.oy, os=this.os;
return {x:ox+(this.x1+this.x2)*os/2, y:oy+this.y1*os, dy:1, color:base.@strokeColor};
end
//////////////////////////////////////////////////////////////////////////////////////
// LAYOUT GENERAL
<@doc scope="private">
Adjusts the block layout after its contents have been rearranged
to ensure all layout constraints are satisfied
virtual method adjustLayout(anchors)
var func='adjustLayout'+base.@layoutMode;
if (func in this) this[func](anchors);
end
<@doc scope="private">
Resizes the contents to exactly fit the block
virtual method resizeContents()
var func='resizeContents'+base.@layoutMode;
if (func in this) this[func]();
end
<@doc scope="private">
Enlarges the block to encompass all its contents
override virtual method enlargeToContents()
// get the block contents bounding box
// TODO: temp for BlockZoom work
var bbox = this.getDiagram().isZoomable ? this.getBBox() : this.shapesLayer.getBBox();
if (bbox.width<0 || bbox.height<0) return false;
// calculate new bounding box
var FP = base.@framePadding;
var BP = base.@bodyPadding;
var SC = base.@scale, u=#[SVG_SNAPUNIT];
var dx1 = Math.min((bbox.x-BP.left)/SC, 0);
var dy1 = Math.min((bbox.y-BP.top)/SC, 0);
var dx2 = Math.max((bbox.x+bbox.width+BP.right)/SC+FP.left+FP.right-this.w, -0);
var dy2 = Math.max((bbox.y+bbox.height+BP.bottom)/SC+FP.top+FP.bottom-this.h, -0);
if (dx1 > -u && dy1 > -u && dx2 < u && dy2 < u) return;
// snap new bounding box to grid units
var x1=this.x1+dx1, x2=this.x2+dx2, sx=(x2-x1) % u;
var y1=this.y1+dy1, y2=this.y2+dy2, sy=(y2-y1) % u;
if (sx != 0) {
if (dx1 < 0) x1-=(u-sx); else x2+=(u-sx);
}
if (sy != 0) {
if (dy1 < 0) y1-=(u-sy); else y2+=(u-sy);
}
// update block bounding box
this.shiftContents(-dx1,-dy1);
this.moveto((x1+x2)/2, (y1+y2)/2);
this.sizeto((x2-x1),(y2-y1));
end
<@doc scope="private">
Shifts the block contents by the given offset
virtual method shiftContents(dx,dy)
if (dx==0 && dy==0) return;
var S= base[base.@shapesKey], SC=base.@scale, canvas=this.canvas;
for (var k in S) {
var shape = canvas.getWidget(k);
if (shape) shape.moveby(dx*SC,dy*SC);
}
end
override virtual method fitToBody(force)
var BB = base.@boundingBox;
var FP = base.@framePadding;
var BP = base.@bodyPadding;
var SM = force ? #[SVG_AUTO_SIZE] : base.@resizeMode||0;
if (!(SM & #[SVG_AUTO_SIZE])) return;
var bbox=this.getBBox(), u=#[SVG_SNAPUNIT];
var w = Math.ceil(Math.max(bbox.width+FP.left+FP.right+BP.left+BP.right, this.minWidth||BB.minWidth||u)/u)*u;
var h = Math.ceil(Math.max(bbox.height+FP.top+FP.bottom+BP.top+BP.bottom, this.minHeight||BB.minHeight||u)/u)*u;
this.shiftContents(-(bbox.x-BP.left), -(bbox.y-BP.top));
if (w != this.w || h != this.h) this.invalidate('@size', w+' '+h);
end
//////////////////////////////////////////////////////////////////////////////////////
// NO LAYOUT
virtual method adjustLayout#[SVG_LAYOUT_NONE](anchors)
this.enlargeToContents();
end
//////////////////////////////////////////////////////////////////////////////////////
// PLOW LAYOUT
virtual method adjustLayout#[SVG_LAYOUT_PLOW](anchors)
var diag = this.getDiagram(), canvas = this.canvas;
if (diag.plowMode) {
var sp0 = base.@layoutSpacing; // Minimum space between shapes (0 <= SP0 <= SP1)
var sp1 = base.@layoutSpacing; // Plow space for move on drop
// scan and plow block shapes, depending on number of given anchors
var block=this, shapes=base[base.@shapesKey];
if (!anchors || typeof anchors != 'object') {
for (var k in shapes) {
var shape = canvas.getWidget(k);
if (shape) plow(shape, null);
}
} else if (anchors.isa && anchors.isa('#NS[ZShape]')) {
plow(anchors, null);
} else {
for (var k in anchors) {
if (!anchors[k].isShape) continue;
plow(anchors[k], anchors);
delete anchors[k];
}
}
}
// TODO: BlockZoom: find out why enlargeToContents moves the block and start using either 'fit' or 'enlarge' but not both
if (diag.isZoomable) this.fitToBody(true); else this.enlargeToContents();
function plow(shape, anchors) {
// scan all placeable child elements into: P=neighbor elements, Q=remaining elements
if (shape.base.@protect & #[SVG_PROTECT_LAYOUT]) return;
var P={}, Q={}, B=block.base[block.base.@shapesKey];
var x1=shape.x1-sp0, y1=shape.y1-sp0, x2=shape.x2+sp0, y2=shape.y2+sp0;
var A = {};
for (var k in B) {
var e = ISA(B[k], 'gml2:PAttachedElement') && B[k].attachedElement || null;
if (e) {
if ((k + e.id in A) || (e.id + k in A)) continue;
A[k + e.id] = mergeAttachedElements(k, e.id)
} else {
A[k] = canvas.getWidget(k);
}
}
for (var k in A) {
var b = A[k];
if (!b || b == shape || (anchors && ((k in anchors) || b.inAncors)) || (b.base.@protect & #[SVG_PROTECT_LAYOUT])) continue;
if (Math.max(b.x1,x1) < Math.min(b.x2,x2) && Math.max(b.y1,y1) < Math.min(b.y2,y2)) P[k]=b; else Q[k]=b;
}
// for each neighbor element, calculate the dx,dy shift required to plow it away and then recursively
// apply the same shift on any remaining elements that become touched as a result
for (var k in P) {
// find the line and angle connecting the centers of the shape and its neighbor
var p=P[k], d=calcDelta(p, shape);
// do the actual plow
p.moveby(d.dx,d.dy);
qplow(p);
}
function calcDelta(p, q) {
var w0=(q.x2-q.x1)/2+sp1, h0=(q.y2-q.y1)/2+sp1;
// find the line and angle connecting the centers of the shape and its neighbor
var dx=p.cx-q.cx, dy=p.cy-q.cy, a=Math.atan2(dy, dx);
// compute the new center of the neighbor
var w1=(p.x2-p.x1)/2, h1=(p.y2-p.y1)/2;
if (Math.abs(dy) <= Math.abs(dx)) {
var nx = (dx < 0 ? -1 : 1) * (w0 + w1);
var ny = nx * Math.tan(a);
}
else {
var ny = (dy < 0 ? -1 : 1) * (h0 + h1);
var nx = ny / Math.tan(a);
}
return {dx:nx-dx, dy:ny-dy};
}
// recursively detect and shift any remaining shapes
function qplow(p) {
var R={}, x1=p.x1-sp0, y1=p.y1-sp0, x2=p.x2+sp0, y2=p.y2+sp0;
for (var k in Q) {
var q=Q[k];
if (Math.max(q.x1,x1) >= Math.min(q.x2,x2) || Math.max(q.y1,y1) >= Math.min(q.y2,y2)) continue;
var d = calcDelta(q, p);
q.moveby(d.dx,d.dy);
delete Q[k];
R[k] = q;
}
Q[p.id] = p;
for (var k in R) qplow(R[k]);
}
function mergeAttachedElements(id1, id2) {
var e1 = canvas.getWidget(id1)
var e2 = canvas.getWidget(id2)
var elem = {
id: id1 + id2,
base: {},
inAncors: anchors && ((e1.id in anchors) || (e2.id in anchors)),
updateData: function() {
var x1 = Math.min(e1.x1, e2.x1);
var x2 = Math.max(e1.x2, e2.x2);
var y1 = Math.min(e1.y1, e2.y1);
var y2 = Math.max(e1.y2, e2.y2);
this.x1 = x1;
this.x2 = x2;
this.y1 = y1;
this.y2 = y2;
this.cx = (x1 + x2) / 2;
this.cy = (y1 + y2) / 2;
},
moveby: function(dx, dy) {
e1.moveby(dx, dy);
e2.moveby(dx, dy);
this.updateData();
}
}
elem.updateData();
return elem;
}
}
end
//////////////////////////////////////////////////////////////////////////////////////
// VFLOW LAYOUT
virtual method resizeContents#[SVG_LAYOUT_VFLOW]()
return
var BP = base.@bodyPadding;
var W = this.w-BP.left+BP.right;
var H = 0;
// collect shape dimensions
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var shape = canvas.getWidget(k);
if (shape) H += shape.h;
}
if (H == 0) return;
var dH = (this.h-BP.top-BP.bottom)/H;
// resize shapes proportionally
for (var k in shapes) {
var shape=canvas.getWidget(k);
if (shape) shape.sizeto(W, shape.h*dH);
}
this.adjustLayout();
end
virtual method adjustLayout#[SVG_LAYOUT_VFLOW](anchors)
var SP = base.@layoutSpacing;
var BP = base.@bodyPadding;
var W = BP.left+BP.right;
var H = BP.top+BP.bottom;
var Y = BP.top;
var L = [];
// collect shape dimensions
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var shape = canvas.getWidget(k);
if (!shape) continue;
W = Math.max(shape.w, W);
H += shape.h;
L.push({y:shape.cy, shape:shape});
}
var len=L.length;
if (len == 0) return;
H += (len-1)*SP;
if (anchors && anchors.isa && anchors.isa('#NS[ZShape]')) W=anchors.w;
// sort and reposition shapes
SORTN(L,'y')
for (var i=0; i
Handles the block's mouseover event
override virtual method mouseover(evt, ptr)
if (!ptr) {
this.supercall();
return;
}
var canvas = this.canvas;
switch (ptr.method) {
case 'moveHandler':
var diag=this.getDiagram(), trg=this.canvas.getWidget(ptr.dropTargetID);
if (!ptr.canRecompose) break;
if (this == trg || (ptr.phase != #[PTR_MOVE])) break;
for (var trg2=this; trg2 && !trg2.isDragSource; trg2=trg2.getParent&&trg2.getParent());
if (trg2) break;
//TODO: canInsert check is temp
var shapes=ptr.shapes, canInsert=true;
for (var k in shapes) {
canInsert = RULE('canInsert', diag.base, diag.boardAspect, base, shapes[k]);
if (!canInsert) break;
}
if (!canInsert) break;
if (ptr.visualCues) {
if (trg && trg != diag) trg.hideDropEffect();
if (canInsert) { //TODO: temp
this.showDropEffect();
diag.showTooltip(this);
}
}
ptr.dropTargetID = this.id;
break;
case 'wiringHandler':
var diag=this.getDiagram(), trg=ptr.wireTargetID;
if (diag.isZoomable && !this.isExpanded) {
var node = evt.target && evt.target.parentNode;
if (node == this.btnToggle) { // TODO: BlockZoom work
if (this.canvas.zoomTimer) diag.canvas.zoomTimer.stop();
var timer = new SvgTimer({target: delayedZoom, delay: 400});
timer.blockID = this.id;
timer.wire = ptr.wire;
timer.srcpin = ptr.getSrcpin && ptr.getSrcpin() || canvas.getWidget(ptr.srcpinID);
this.canvas.zoomTimer = timer;
timer.start();
}
}
if (this.id == trg) break;
trg = this.canvas.getWidget(trg);
if (trg) trg.hideWiringEffect(ptr);
this.showWiringEffect(ptr);
ptr.wireTargetID = this.id;
break;
}
function delayedZoom(timer) {
var canvas = BOARD.canvas;
var t = canvas.zoomTimer; if (!t || (t.id != timer.id)) return;
canvas.zoomTimer = null;
var block = canvas.getWidget(t.blockID);
if (!block || block.isExpanded) return;
block.hideZoomableEffect(); // stop toggleBtn highliting
block.getDiagram().zoomBlock(block);
// update wiring link source after its shape could have been moved by zoom operation
var wire = t.wire, srcpin = t.getSrcpin && t.getSrcpin() || canvas.getWidget(t.srcpinID), shape = srcpin.getShape();
wire.setAttribute('x1', shape.ox+(shape.cx+srcpin.x)*shape.os);
wire.setAttribute('y1', shape.oy+(shape.cy+srcpin.y)*shape.os);
}
end
<@doc scope="private">
Handles the block's mouseout event
override virtual method mouseout(evt, ptr)
if (!ptr) {
this.supercall();
return;
}
end
//////////////////////////////////////////////////////////////////////////////////////
// ZOOM HANDLING
virtual method showZoomableEffect()
if (this.isExpanded) {
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var shape = canvas.getWidget(k);
if (shape && shape.isBlock) shape.showZoomableEffect();
}
}
else {
var btn = this.btnToggle; if (!btn) return;
SVG.setProperty(btn, 'color', this.hilightColor);
SVG.createFragment(btn, '');
}
end
virtual method hideZoomableEffect()
if (this.isExpanded) {
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var shape = canvas.getWidget(k);
if (shape && shape.isBlock) shape.hideZoomableEffect();
}
}
var btn = this.btnToggle; if (!btn) return;
SVG.clearElement(btn);
this.btnToggle = null;
this.updateToggleBtn();
end
override virtual method showMovableEffect()
if (this.getDiagram().alphaMode) SVG.setProperty(this.frameNode, 'opacity', 1);
end
override virtual method hideMovableEffect()
if (this.getDiagram().alphaMode) SVG.setProperty(this.frameNode, 'opacity', 0.2);
end
override virtual method showActiveEffect()
this.showMovableEffect()
if (!this.isExpanded) return;
var canvas = this.canvas;
var shapes = base[base.@shapesKey];
for (var k in shapes) {
var g = canvas.getWidget(k);
if (g) g.showMovableEffect();
}
var lines = base[base.@linesKey];
for (var k in lines) {
var g=canvas.getWidget(lines[k].id);
if (g) g.showMovableEffect();
}
end
override virtual method hideActiveEffect()
// TODO:
end
override virtual method showZoomedEffect()
this.hideMovableEffect();
if (!this.isExpanded) return;
var canvas = this.canvas;
var shapes = base[base.@shapesKey];
for (var k in shapes) {
var g = canvas.getWidget(k);
if (g) g.hideMovableEffect();
}
var lines = base[base.@linesKey];
for (var k in lines) {
var g=canvas.getWidget(lines[k].id);
if (g) g.hideMovableEffect();
}
end
virtual method Zoom(activate)
var diag = this.getDiagram();
var path = [];
for (var b = this; b && b.isBlock; b = b.getParent()) path.push(b);
for (var i = path.length-1; i >= 0; i--) {
var b = path[i];
b.expand();
b.restorePositions();
b.fitToBody(true); // TODO: see if fitToBody is needed (adjustBlockLayout[PLOW] does this anyway)
diag.adjustBlockLayout(b.getParent(), b);
if ((i > 0) || !activate) b.showZoomedEffect();
}
if (activate) {
this.showActiveEffect();
this.isActive = true;
}
end
virtual method Unzoom(deactivate)
if (deactivate) {
this.isActive = false;
this.hideActiveEffect();
this.savePositions();
}
for (var b = this; b && b.isBlock && !b.isRoot; b = b.getParent()) {
b.collapse();
b.sizeto(80, 60);
}
end
virtual method ZoomAll()
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var g = canvas.getWidget(k);
if (g && g.isBlock) g.ZoomAll();
}
this.Zoom(false);
end
virtual method UnzoomAll()
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var g = canvas.getWidget(k);
if (g && g.isBlock) g.UnzoomAll();
}
this.collapse();
this.sizeto(80, 60);
end
virtual method savePositions()
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var g = canvas.getWidget(k);
if (g) g.base.setProperty('@xpos', g.cx+' '+g.cy, true);
}
end
virtual method restorePositions()
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var g = canvas.getWidget(k);
if (g && g.base.hasProperty('@xpos')) {
var p = SPLITN(g.base.@xpos);
g.moveto(p[0]||0, p[1]||0);
}
}
end
virtual method getBBox()
var boxes = [];
var shapes = base[base.@shapesKey], canvas = this.canvas;
for (var k in shapes) {
var shape = canvas.getWidget(k);
if (!shape) continue;
var os = shape.os;
boxes.push({x: shape.x1*os, y: shape.y1*os, w: shape.w*os, h: shape.h*os});
}
if (boxes.length == 0) return this.frameNode.getBBox();
var m=Number.MAX_VALUE, x1=m, y1=m, x2=-m, y2=-m;
for (var i=0, len = boxes.length; i < len; i++) {
var box = boxes[i];
x1 = Math.min(x1, box.x);
y1 = Math.min(y1, box.y);
x2 = Math.max(x2, box.x + box.w);
y2 = Math.max(y2, box.y + box.h);
}
return {x: x1, y: y1, width: x2-x1, height: y2-y1};
end