From a0f79e12bac3a5c68540577771089b237be6f1ff Mon Sep 17 00:00:00 2001 From: Thijs Van der Schaeghe Date: Tue, 14 Jul 2026 23:27:15 +0200 Subject: [PATCH] fix(pinner): make pinToTop robust for pinned Placeholders A Placeholder pinned above another layer (the connect-screen QR over the flung-emoji layer) was being defeated at several points: - Placeholder.updateZIndex re-inserted the inner placeholder next to its source element every tick, dragging it back out of the pin layer. Skip that reordering when the placeholder is pinned (obj._pinnedToTop, set by Pinner on pin and cleared on _restore). - _syncRecord read the anchor's concatenated matrix without guarding, which threw ("reading 'x' of undefined") and broke the tick loop while the anchor subtree was still being built. Guard it and skip the frame. - getBounds() (optional, only for bounds-sizing children) was bundled into the same read as the matrices, so a MovieClip anchor whose getBounds() throws persistently made _syncRecord bail every frame and left the pin stuck at the pin-container origin. Read bounds in its own try/catch so a throw there never blocks positioning. Adds tools/pin-placeholder-test.js covering the z-index case and extends pin-test.js to assert positioning survives a persistently-throwing getBounds(). Co-Authored-By: Claude Fable 5 --- .../EaselJS/DisplayObjects/Placeholder.js | 8 ++ .../CatLab/Easelbone/EaselJS/Pinner.js | 42 +++++++++- dist/scripts/easelbone.js | 4 +- package.json | 3 +- tools/fixtures/pin-baseline.html | 23 ++++++ tools/fixtures/pin-placeholder.html | 79 +++++++++++++++++++ tools/pin-placeholder-test.js | 44 +++++++++++ tools/pin-test.js | 7 ++ 8 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 tools/fixtures/pin-placeholder.html create mode 100644 tools/pin-placeholder-test.js diff --git a/app/scripts/CatLab/Easelbone/EaselJS/DisplayObjects/Placeholder.js b/app/scripts/CatLab/Easelbone/EaselJS/DisplayObjects/Placeholder.js index 86a175a..900c2e9 100644 --- a/app/scripts/CatLab/Easelbone/EaselJS/DisplayObjects/Placeholder.js +++ b/app/scripts/CatLab/Easelbone/EaselJS/DisplayObjects/Placeholder.js @@ -105,6 +105,14 @@ define ( return; } + // If this placeholder has been pinned (Pinner.pinToTop moved it + // into a pin layer), don't drag it back next to the source + // element -- that would rip it out of the pin layer every tick + // and defeat the pin. + if (innerPlaceholder._pinnedToTop) { + return; + } + // Fast path: verify the cached position directly instead of // scanning the parent's children twice with getChildIndex. var siblings = element.parent.children; diff --git a/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js b/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js index 4bb8c7f..1697045 100644 --- a/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js +++ b/app/scripts/CatLab/Easelbone/EaselJS/Pinner.js @@ -92,6 +92,13 @@ define( ensureContainer(stage).addChild(wrapper); wrapper.addChild(obj); // reparents obj out of anchor; obj's own transform/regX/regY untouched + // Flag so other machinery that manages an object's place in the + // display list (e.g. Placeholder.updateZIndex, which re-inserts + // an inner placeholder next to its source element every tick) + // can leave a pinned object where the Pinner put it instead of + // dragging it back and defeating the pin. + obj._pinnedToTop = true; + stage._pinnedElements.push(record); // Immediate sync: correctly placed even without a running tick. @@ -161,6 +168,7 @@ define( }, _restore: function (record) { + record.obj._pinnedToTop = false; if (record.obj.parent) { record.obj.parent.removeChild(record.obj); } @@ -184,16 +192,42 @@ define( return; } - // Carry the anchor's bounds so bounds-sizing children (e.g. Fill) size correctly. - var bounds = record.anchor.getBounds(); + // Positioning needs only the concatenated matrices. Reading + // them walks the anchor/container up to the stage, which can + // throw (or return null) while the tree is still being built + // (e.g. a pin created from a deferred tick during screen init). + // If so, leave the pin where it is and place it on a later sync. + var anchorMatrix; + var containerMatrix; + try { + anchorMatrix = record.anchor.getConcatenatedMatrix(); + containerMatrix = container.getConcatenatedMatrix(); + } catch (e) { + return; + } + + if (!anchorMatrix || !containerMatrix) { + return; + } + + // The anchor's bounds are OPTIONAL -- only used so bounds-sizing + // children (e.g. a Fill) can match the anchor's box. getBounds() + // walks the anchor's *subtree* and can throw when a descendant + // has incomplete bounds; that must NOT block positioning, so + // guard it separately and just skip the bounds carry on failure. + var bounds = null; + try { + bounds = record.anchor.getBounds(); + } catch (e) { + bounds = null; + } if (bounds) { wrapper.setBounds(0, 0, bounds.width, bounds.height); } // Place the wrapper in the anchor's coordinate space: // wrapperLocal = pinContainerConcatenated^-1 * anchorConcatenated - var local = container.getConcatenatedMatrix().invert() - .appendMatrix(record.anchor.getConcatenatedMatrix()); + var local = containerMatrix.invert().appendMatrix(anchorMatrix); var props = local.decompose(Pinner._props); wrapper.x = props.x; diff --git a/dist/scripts/easelbone.js b/dist/scripts/easelbone.js index 0325f6e..8b1786b 100644 --- a/dist/scripts/easelbone.js +++ b/dist/scripts/easelbone.js @@ -3,12 +3,12 @@ define("CatLab/Easelbone/Utilities/Loader",["preloadjs","easeljs","CatLab/Easelb define("CatLab/Easelbone/Utilities/GlobalProperties",["backbone"],(function(t){return new(t.Model.extend({initialize:function(){this.set({width:800,height:600,font:"sans-serif",textColor:"white"})},getWidth:function(){return this.get("width")},getHeight:function(){return this.get("height")},getDefaultFont:function(){return this.get("font")},getDefaultTextColor:function(){return this.get("textColor")},ifUndefined:function(t,e){return null!=t?t:e}}))})); define("CatLab/Easelbone/Utilities/CustomAttributes",[],(function(){"use strict";return["textAlign","textColor","textFont"]})); define("CatLab/Easelbone/Utilities/DirtyFlag",[],(function(){"use strict";var i=!1,t=!1;return{invalidate:function(){i=!0},consume:function(){var t=i;return i=!1,t},installMovieClipHook:function(e){if(!t&&void 0!==e.MovieClip){t=!0;var n=e.MovieClip.prototype._tick;e.MovieClip.prototype._tick=function(t){var e=this.currentFrame;n.call(this,t),this.currentFrame!==e&&(i=!0)}}}}})); -define("CatLab/Easelbone/EaselJS/DisplayObjects/Placeholder",["easeljs","CatLab/Easelbone/Utilities/CustomAttributes","CatLab/Easelbone/Utilities/DirtyFlag"],(function(i,e,t){var n=function(i){void 0!==i&&(this.initialize(),this.initializePlaceholder(i))};return(n.prototype=new i.Container).initializePlaceholder=function(n){if(n.easelPlaceholderInitialized)console.error("Element is already initialized as placeholder.",n);else{n.easelPlaceholderInitialized=!0;var a,l,s,d=this,r=null,h=null,o=-1;if(e.forEach(function(i){Object.defineProperty(this,i,{get:function(){if(void 0!==n[i])return n[i]},set:function(e){console.log("setting",i,e)}})}.bind(this)),d.filters=n.filters,n.filters=[],n.uncache(),n.original_draw=n.draw,n.original_tick=n._tick,n.draw=function(i,e){return this.updateBounds(),n.original_draw.apply(n,arguments)},this.hasBoundsChanged=function(){var i=this.getBounds();return!(!i||i.width===r&&i.height===h||(r=i.width,h=i.height,0))},n._tick=function(){return n.parent&&n.parent.children&&this.updateZIndex(),n.original_tick.apply(n,arguments)},n.updateZIndex=function(){if(n.children){var i=n.parent.children;o>0&&i[o]===d&&i[o-1]===n||(l=n.parent.getChildIndex(d),(s=n.parent.getChildIndex(n))+1!==l&&(n.parent.addChildAt(d,s+1),t.invalidate()),o=s+1)}},n.updateBounds=function(){d.setBounds(0,0,Math.ceil(100*this.scaleX),Math.ceil(100*this.scaleY)),d.x=this.x-this.regX*this.scaleX,d.y=this.y-this.regY*this.scaleY,d.rotation=this.rotation,d.hasBoundsChanged()&&(this.mask?d.mask=this.mask:this.originalMask&&(d.mask=this.originalMask),this.mask=!1,a=new i.Event("bounds:change"),d.dispatchEvent(a))},n.children)for(var c=0;c0&&i[o]===d&&i[o-1]===n||(l=n.parent.getChildIndex(d),(s=n.parent.getChildIndex(n))+1!==l&&(n.parent.addChildAt(d,s+1),t.invalidate()),o=s+1)}},n.updateBounds=function(){d.setBounds(0,0,Math.ceil(100*this.scaleX),Math.ceil(100*this.scaleY)),d.x=this.x-this.regX*this.scaleX,d.y=this.y-this.regY*this.scaleY,d.rotation=this.rotation,d.hasBoundsChanged()&&(this.mask?d.mask=this.mask:this.originalMask&&(d.mask=this.originalMask),this.mask=!1,a=new i.Event("bounds:change"),d.dispatchEvent(a))},n.children)for(var c=0;c0&&(l=this.findFromNames(_.join("."),l))}.bind(this)),l.length>0)return l;return l},_.buildNamedChildMap=function(e){if(void 0===e._mh_named_children_map){if(e._mh_named_children_map={},e._mh_deep_named_children_map={},e._mh_timeline_label_map={},e.timeline)for(var i=e.timeline.getLabels(),n=0;n0)},_.pause=function(e){e.timeline&&(e.timeline._was_paused=e.timeline.paused,e.timeline.paused=!0),this.forEachNamedChild(e,function(e){this.pause(e)}.bind(this))},_.resume=function(e){e.timeline&&(e.timeline.paused=e.timeline._was_paused,delete e.timeline._was_paused),this.forEachNamedChild(e,function(e){this.resume(e)}.bind(this))},_.countLabeledFrames=function(e,i){this.buildNamedChildMap(i);var n=0;return i._mh_timeline_label_map&&i._mh_timeline_label_map[e]&&(n+=i._mh_timeline_label_map[e].length),n},_.jumpToFrame=function(e,i){var a=[];return i._mh_timeline_label_map&&i._mh_timeline_label_map[e]&&i._mh_timeline_label_map[e].forEach(function(i){a.push(this._jumpToFrameUntilFinished(e,i))}.bind(this)),n.when.apply(this,a)},_._jumpToFrameUntilFinished=function(e,i){var a=new n;return i.parent?(i.gotoAndPlay(e),i.timeline.on("complete",(function(){a.resolve()}))):a.resolve(),a.promise()},_.attach=function(e){var i=this,n=e.MovieClip.prototype;n.forEachNamedChild=function(e){return i.forEachNamedChild(this,e)},n.findNamedChildren=function(e){return i.findFromNames(e,this)},n.findPlaceholders=function(e){return i.findPlaceholders(e,this)},n.applySpriteFilters=function(e){return i.applySpriteFilters(e,this)},n.hasLabeledFrame=function(e){return i.hasLabeledFrame(e,this)},n.jumpToFrame=function(e){return i.jumpToFrame(e,this)}},new a})); define("CatLab/Easelbone/Views/Base",["backbone","easeljs","CatLab/Easelbone/Utilities/GlobalProperties","CatLab/Easelbone/Utilities/MovieClipHelper","CatLab/Easelbone/Utilities/Deferred"],(function(e,t,i,r,n){"use strict";return e.View.extend({el:"div",easelScreen:null,setRootView:function(e){this.set("root",e)},getWidth:function(){return this.getStage().canvas.width},getHeight:function(){return this.getStage().canvas.height},getStage:function(){return void 0!==this.el.stage?this.el.stage:void 0!==this.el.getStage?this.el.getStage():void 0},setScreen:function(e){return this.easelScreen=e,this},getScreen:function(){return this.easelScreen},hasLabeledFrame:function(e,t){if(void 0===t&&(t=this.easelScreen),!t)throw new Error("hasLabeledFrame requires a screen to be set or a container to be provided.");return r.hasLabeledFrame(e,t)},jumpToFrame:function(e,t){if(void 0===t&&(t=this.easelScreen)&&!t.parent){var i=new n;return this._pendingFrameJumps||(this._pendingFrameJumps=[]),this._pendingFrameJumps.push({label:e,deferred:i}),i.promise()}if(!t)throw new Error("hasLabeledFrame requires a screen to be set or a container to be provided.");return r.jumpToFrame(e,t)},_replayPendingFrameJumps:function(){if(this._pendingFrameJumps){var e=this._pendingFrameJumps;this._pendingFrameJumps=null,e.forEach(function(e){this.jumpToFrame(e.label).done((function(){e.deferred.resolve()}))}.bind(this))}},pause:function(e){if(void 0===e&&(e=this.easelScreen),!e)throw new Error("pause requires a screen to be set or a container to be provided.");return r.pause(this.easelScreen)},resume:function(e){if(void 0===e&&(e=this.easelScreen),!e)throw new Error("resume requires a screen to be set or a container to be provided.");return r.resume(this.easelScreen)},scale:function(e){var t=this.getScale();e.scaleX=t.x,e.scaleY=t.y},getScale:function(e,t,r){null==e&&(e=i.getWidth()),null==t&&(t=i.getHeight()),void 0===r&&(r=!1);var n=this.getWidth()/e,a=this.getHeight()/t,s=r?Math.max(n,a):Math.min(n,a);return{x:s,y:s}},addCenter:function(e,r,n,a,s){null==r&&(r=i.getWidth()),null==n&&(n=i.getHeight()),null==s&&(s=1);var h=this.getScale(r,n,a),o=!0;if(this.getWidth()===r&&this.getHeight()===n&&(o=!1),o&&(h.x=h.x*s,h.y=h.y*s,e.x=(this.getWidth()-r*h.x)/2,e.y=(this.getHeight()-n*h.y)/2,e.scaleX=h.x,e.scaleY=h.y),this.el.addChild(e),o){var l=new t.Graphics;e.x>=1&&(l.beginFill(this.getBackground()).drawRect(0,0,Math.ceil(e.x),this.getHeight()),l.beginFill(this.getBackground()).drawRect(this.getWidth()-Math.ceil(e.x),0,Math.ceil(e.x),this.getHeight())),e.y>=1&&(l.beginFill(this.getBackground()).drawRect(0,0,this.getWidth(),Math.ceil(e.y)),l.beginFill(this.getBackground()).drawRect(0,this.getHeight()-Math.ceil(e.y),this.getWidth(),Math.ceil(e.y)));var d=new t.Shape(l);this.el.addChild(d)}},clear:function(){this.el.removeAllChildren();var e=new t.Graphics;e.beginFill(this.getBackground()).drawRect(0,0,this.getWidth(),this.getHeight());var i=new t.Shape(e);this.el.addChild(i)},getBackground:function(){return"#000000"},render:function(){if(this.easelScreen)return this.addCenter(this.easelScreen),this._replayPendingFrameJumps(),this;var e=new t.Container;e.setBounds(0,0,this.getWidth(),this.getHeight());var i=new t.BigText("easelbone view: no screen set and render function was not overridden...","Arial","#000000");e.addChild(i),this.el.addChild(e)},findPlaceholders:function(e,t){return void 0===t&&(t=[],this.easelScreen&&t.push(this.easelScreen)),r.findPlaceholders(e,t)},findTextPlaceholders:function(e,t){return this.findPlaceholdersPreferPostfix(e,t,"text")},findPlaceholdersPreferPostfix:function(e,t,i){var r=[];return Array.isArray(e)?e.forEach((function(e){r.push(e+".text"),r.push(e)})):(r.push(e+".text"),r.push(e)),this.findPlaceholders(r,t)},findFromNames:function(e,t){return void 0===t&&(t=[],this.easelScreen&&t.push(this.easelScreen)),r.findFromNames(e,t)},findFromNameInContainer:function(e,t,i){return r.findFromNameInContainer(e,t,i)},forEachNamedChild:function(e,t){return r.forEachNamedChild(e,t)},tick:function(){return!0},afterRender:function(){},onRemove:function(){},scrollIntoView:function(e){for(var t=e;t.parent;){if(void 0!==t._parentScrollArea)return void t._parentScrollArea.focus(e,200);t=t.parent}}})})); define("CatLab/Easelbone/Views/LoadingView",["CatLab/Easelbone/Views/Base"],(function(t){"use strict";var e=new Image;e.crossOrigin="anonymous",e.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAWCAYAAAA1vze2AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH3wMRCycoY5y7DgAAAeBJREFUSMel1j9sTlEYBvD3fv2DIqmmmmjTBSXVkYSBhAixWNRmMYvBgMZQSUeLQWIw2hqLWIRBYpCIQYcyYDBJNAQpBk21P8u5cdzcfv1835Pc3JznOed9zn3f95zciCbAAbzEIk5VtI04jxmMRzvAKD76i1VszvSZTFvESDsmk1jxLyYz/UtFu7pWrEYTn9EafUcyGIiIgYo23o5Jdw23kt79NVrR1AQNbEEe+F0WtMSLiIiiKN5HxGqdluL1oB89JdGFabzGLWxKfB9ms5zfqdTsYqbNoT+LN5tqdq+cfLrSQUcrOxrGCLoqJg1sxxg2ZPyZSsNcCTyvdMlUdABMpc2W+NyIiP2VecMtBjuMEzVSX2U82B0RSxHRk5H5gTsbEbcjYig1wpuI2BoRx7I5cxFxqCiK5URtq3SawINKui6nxfvwU2u4npneqGjzkQr3LRFPMZil40eLJnfLxsAQHuErHpfx1sr5Xnxq0WS6Wf2anfhzKb/r4XeqVcvt14sL//EFJZ6Uh3g9gzF80D7u4yQm1rzU8DYi9kRnWE333WBRFN/rarIQnaMREb/SU5uu3VjWOS6haFaX41iqWbjQosE8eltpgAk8TKf9FXYl/kgawzMcTPxOXMPN/B8gxx+XbHMxap3gAwAAAABJRU5ErkJggg==";var i=0,s={x:{start:0,end:100},y:{start:0,end:100}},a={x:s.x.end/2,y:s.y.end/2},n=[],h=!1;return t.extend({initialize:function(t){this.gameVersion=null,void 0!==t.gameVersion&&(this.gameVersion=t.gameVersion),this.dRotation=0,this.speed=10,this.angle=Math.random()*Math.PI*2,this.targetAngle=null,this.flippedX=!1,this.flippedY=!1,this.rotationSpeedRad=.2,this.screenMargin=50},render:function(){s={x:{start:0,end:this.el.stage.canvas.width},y:{start:0,end:this.el.stage.canvas.height}},this.loadingText=new createjs.Text("Loading","15px monospace","#ffffff"),this.loadingText.textAlign="center",this.loadingText.lineHeight=20,this.el.addChild(this.loadingText),this.versionText=null,this.gameVersion&&(this.versionText=new createjs.Text("v"+this.gameVersion,"12px monospace","#ffffff"),this.el.addChild(this.versionText),this.versionText.cache(-100,-100,200,200),this.loadingText.cache(-100,-100,200,200)),this.el.setBounds(0,0,this.el.stage.canvas.width,this.el.stage.canvas.height),this.updatePositions(),a={x:Math.random()*(s.x.end-2*this.screenMargin)+this.screenMargin,y:Math.random()*(s.y.end-2*this.screenMargin)+this.screenMargin}},tick:function(){this.updatePositions();var t=this.angle;null!==this.targetAngle&&(t=this.targetAngle),(a.xs.x.end-this.screenMargin&&Math.cos(t)>0)&&(this.targetAngle=Math.PI-t),(a.ys.y.end-this.screenMargin&&Math.sin(t)>0)&&(this.targetAngle=2*Math.PI-t),null!==this.targetAngle&&(this.targetAngle-this.angle>this.rotationSpeedRad?this.angle+=this.rotationSpeedRad:this.targetAngle-this.angle<0-this.rotationSpeedRad?this.angle-=this.rotationSpeedRad:this.targetAngle=null);var e=Math.cos(this.angle)*this.speed,h=Math.sin(this.angle)*this.speed;a.x+=e,a.y+=h,++i%6==0&&this.addPaw();for(var r=0;r0&&g.push(n[r]);return n=g,!0},updatePositions:function(){this.loadingText.x=this.el.getBounds().width/2,this.loadingText.y=this.el.getBounds().height/2,this.versionText&&(this.versionText.x=this.el.getBounds().width-60,this.versionText.y=this.el.getBounds().height-20)},stop:function(){},start:function(){},addPaw:function(){var t=10;(h=!h)&&(t*=-1);var i=new createjs.Bitmap(e),s=this.angle;i.regX=10,i.regY=10,i.x=a.x+Math.cos(s+Math.PI/2)*t,i.y=a.y+Math.sin(s+Math.PI/2)*t,i.scaleX=20/e.width,i.scaleY=20/e.height,i.rotation=(s+Math.PI/2)*(180/Math.PI),this.el.addChild(i),n.push(i)},setProgress:function(t){t=Math.floor(100*t),this.loadingText.text="Loading\n"+t+"%",this.loadingText.cache(-100,-100,200,200)}})})); define("CatLab/Easelbone/Views/Layer",["easeljs","underscore","backbone","CatLab/Easelbone/Utilities/Deferred"],(function(e,i,t,n){var r=function(n){i.extend(this,t.Events),void 0===n&&(n={}),void 0!==n.container?this.container=n.container:this.container=new e.Container,this.view=null};return r.prototype.setView=function(i){var t=this.view;this.view=i;var n=new e.Container;this.view.setElement(n),this.container.addChild(n),this.view.on("all",function(e){this.trigger("view:"+e)}.bind(this)),this.view.trigger("stage:added"),t&&this.container.children.length>0&&this.container.setChildIndex(t.el,this.container.children.length-1),t&&this._destroyView(t).then((function(){t=null}))},r.prototype.clearView=function(){this.view&&this._destroyView(this.view).pipe(function(){this.view=null}.bind(this))},r.prototype._destroyView=function(e){return this._waitForViewDestroy(e).pipe(function(){null!==e&&(e.trigger("stage:removed"),this.container.removeChild(e.el),e=null)}.bind(this))},r.prototype._waitForViewDestroy=function(e){var i=new n;return e&&void 0!==e.hasLabeledFrame&&void 0!==e.easelScreen&&e.easelScreen&&e.hasLabeledFrame("view:destroy")?(e.jumpToFrame("view:destroy").then((function(){i.resolve()})),i.promise()):(i.resolve(),i.promise())},r.prototype.render=function(){this.view&&(this.view.el.removeAllChildren(),this.view.trigger("render:before"),this.view.render(),this.view.trigger("render"),this.view.trigger("render:after"))},r.prototype.tick=function(e){return!!this.view&&this.view.tick(e)},r})); -define("CatLab/Easelbone/EaselJS/Pinner",["easeljs","CatLab/Easelbone/Utilities/DirtyFlag"],(function(n,e){"use strict";var r={_props:{},setContainer:function(n,e){n._pinContainer=e},_deferPin:function(e,i){e.stage?r.pin(e):i>=300||n.Ticker.on("tick",(function(){r._deferPin(e,i+1)}),null,!0)},pin:function(i){var t=i.stage;if(t){if(!r._isPinned(t,i)){var a=i.parent;if(a){!function(n){n._pinnedElements||(n._pinnedElements=[]),n._pinnedElements}(t),r._removeByAnchor(t,a);var o=new n.Container,s={obj:i,anchor:a,wrapper:o};(function(e){if(e._pinContainer&&e._pinContainer.stage===e)return e._pinContainer;var r=new n.Container;return e.addChild(r),e._pinContainer=r,r})(t).addChild(o),o.addChild(i),t._pinnedElements.push(s),r._syncRecord(t,s),e.invalidate()}}}else r._deferPin(i,0)},unpin:function(n){var i=n.stage,t=i&&i._pinnedElements;if(t)for(var a=0;a=0;i--){var t=e[i];t.anchor.stage===n?r._syncRecord(n,t):(r._restore(t),e.splice(i,1))}return e.length>0},_removeByAnchor:function(n,e){for(var i=n._pinnedElements,t=i.length-1;t>=0;t--)i[t].anchor===e&&(r._restore(i[t]),i.splice(t,1))},_restore:function(n){n.obj.parent&&n.obj.parent.removeChild(n.obj),n.wrapper.parent&&n.wrapper.parent.removeChild(n.wrapper),n.anchor&&n.anchor.stage&&n.anchor.addChild(n.obj)},_syncRecord:function(n,e){var i=e.wrapper;i.visible=!n._pinsHidden;var t=i.parent;if(t){var a=e.anchor.getBounds();a&&i.setBounds(0,0,a.width,a.height);var o=t.getConcatenatedMatrix().invert().appendMatrix(e.anchor.getConcatenatedMatrix()).decompose(r._props);i.x=o.x,i.y=o.y,i.scaleX=o.scaleX,i.scaleY=o.scaleY,i.rotation=o.rotation,i.skewX=o.skewX,i.skewY=o.skewY}}};return r})); +define("CatLab/Easelbone/EaselJS/Pinner",["easeljs","CatLab/Easelbone/Utilities/DirtyFlag"],(function(n,e){"use strict";var r={_props:{},setContainer:function(n,e){n._pinContainer=e},_deferPin:function(e,t){e.stage?r.pin(e):t>=300||n.Ticker.on("tick",(function(){r._deferPin(e,t+1)}),null,!0)},pin:function(t){var i=t.stage;if(i){if(!r._isPinned(i,t)){var a=t.parent;if(a){!function(n){n._pinnedElements||(n._pinnedElements=[]),n._pinnedElements}(i),r._removeByAnchor(i,a);var o=new n.Container,s={obj:t,anchor:a,wrapper:o};(function(e){if(e._pinContainer&&e._pinContainer.stage===e)return e._pinContainer;var r=new n.Container;return e.addChild(r),e._pinContainer=r,r})(i).addChild(o),o.addChild(t),t._pinnedToTop=!0,i._pinnedElements.push(s),r._syncRecord(i,s),e.invalidate()}}}else r._deferPin(t,0)},unpin:function(n){var t=n.stage,i=t&&t._pinnedElements;if(i)for(var a=0;a=0;t--){var i=e[t];i.anchor.stage===n?r._syncRecord(n,i):(r._restore(i),e.splice(t,1))}return e.length>0},_removeByAnchor:function(n,e){for(var t=n._pinnedElements,i=t.length-1;i>=0;i--)t[i].anchor===e&&(r._restore(t[i]),t.splice(i,1))},_restore:function(n){n.obj._pinnedToTop=!1,n.obj.parent&&n.obj.parent.removeChild(n.obj),n.wrapper.parent&&n.wrapper.parent.removeChild(n.wrapper),n.anchor&&n.anchor.stage&&n.anchor.addChild(n.obj)},_syncRecord:function(n,e){var t=e.wrapper;t.visible=!n._pinsHidden;var i=t.parent;if(i){var a,o;try{a=e.anchor.getConcatenatedMatrix(),o=i.getConcatenatedMatrix()}catch(n){return}if(a&&o){var s=null;try{s=e.anchor.getBounds()}catch(n){s=null}s&&t.setBounds(0,0,s.width,s.height);var p=o.invert().appendMatrix(a).decompose(r._props);t.x=p.x,t.y=p.y,t.scaleX=p.scaleX,t.scaleY=p.scaleY,t.rotation=p.rotation,t.skewX=p.skewX,t.skewY=p.skewY}}}};return r})); define("CatLab/Easelbone/Views/Root",["backbone","easeljs","CatLab/Easelbone/Views/Layer","CatLab/Easelbone/Utilities/DirtyFlag","CatLab/Easelbone/EaselJS/Pinner"],(function(i,t,e,n,a){var s;return i.View.extend({stage:null,container:null,hudcontainer:null,view:null,hud:null,maxCanvasSize:null,width:null,height:null,initialize:function(i){this.initializeRootView(i)},initializeRootView:function(i){if(void 0!==i.canvas)this.canvas=i.canvas,this.container=this.canvas.parentNode;else{if(void 0===i.container)throw new Error("Container must be defined for root view.");this.canvas=document.createElement("canvas"),this.container=i.container,this.container.appendChild(this.canvas)}if(void 0!==i.width&&void 0!==i.height&&(this.width=i.width,this.height=i.height),this.stage=this.createStage(i),this.layers=[],this.layerMap={},this.dirty=!1,this.mainLayer=this.nextLayer("main"),this.snapToPixel=void 0!==i.snapToPixel&&i.snapToPixel,this.dirtyRendering=void 0!==i.dirtyRendering&&!!i.dirtyRendering,this.heartbeatInterval=1e3,this._lastPaintTime=0,this.dirtyRendering){this.stage.tickOnUpdate=!1,n.installMovieClipHook(t);var e=this.invalidate.bind(this);this.stage.on("stagemousedown",e),this.stage.on("stagemouseup",e),this.stage.on("stagemousemove",e)}t.Ticker.addEventListener("tick",function(i){t.Ticker.paused||this.tick(i)}.bind(this)),this.resize()},createStage:function(i){var e;return(e=void 0!==i.webgl&&i.webgl?new t.StageGL(this.canvas):new t.Stage(this.canvas)).snapToPixelEnabled=this.snapToPixel,e},setMaxCanvasSize:function(i,t){this.maxCanvasSize=[i,t]},nextLayer:function(i){void 0===i&&(i="Layer"+this.layers.length);var t=new e;return t.container.snapToPixel=this.snapToPixel,this.stage.addChild(t.container),this.layers.push(t),this.layerMap[i]=t,t},getLayer:function(i){return this.layerMap[i]},setPinContainer:function(i){a.setContainer(this.stage,i)},setView:function(i){this.mainLayer.setView(i),this.render()},invalidate:function(){n.invalidate()},tick:function(i){for(this.trigger("tick:before",i),this.dirty=!1,s=0;s=this.heartbeatInterval&&(this.dirty=!0),this.dirty&&(this._lastPaintTime=i.time,this.update())):this.dirty&&this.update(),this.trigger("tick:after")},render:function(){for(s=0;s + +Pinner + Placeholder z-index repro + + + + + + diff --git a/tools/pin-placeholder-test.js b/tools/pin-placeholder-test.js new file mode 100644 index 0000000..d53f058 --- /dev/null +++ b/tools/pin-placeholder-test.js @@ -0,0 +1,44 @@ +// Verifies a pinned Placeholder stays in the pin layer despite +// Placeholder.updateZIndex re-inserting inner placeholders each tick. +// Serves the repo root itself. Usage: node tools/pin-placeholder-test.js [port] +var spawn = require('child_process').spawn; +var chromium = require('playwright').chromium; + +var FIXTURE = 'tools/fixtures/pin-placeholder.html'; +var PORT = parseInt(process.argv[2], 10) || 8171; + +function isGreen(rgb) { return rgb[1] > 150 && rgb[0] < 100 && rgb[2] < 100; } + +async function main() { + var server = spawn('node', ['node_modules/http-server/bin/http-server', '.', '-p', String(PORT), '-c-1'], { stdio: 'ignore' }); + await new Promise(function (r) { setTimeout(r, 1500); }); + var browser = await chromium.launch(); + var failures = []; + try { + var page = await browser.newPage(); + var errors = []; + page.on('pageerror', function (e) { errors.push(e.message); }); + page.on('console', function (m) { if (m.type() === 'error') errors.push(m.text()); }); + await page.goto('http://localhost:' + PORT + '/' + FIXTURE, { waitUntil: 'load', timeout: 30000 }); + await page.waitForFunction('window.__ready === true', { timeout: 10000 }); + + var initial = await page.evaluate('window.__centerRGB()'); + if (!isGreen(initial)) { failures.push('expected pinned placeholder GREEN at (125,125), got ' + initial); } + + // Run several ticks so Placeholder.updateZIndex fires repeatedly. + await page.evaluate('window.__tick()'); + var afterTicks = await page.evaluate('window.__centerRGB()'); + var inPinLayer = await page.evaluate('window.__placeholderInPinLayer()'); + if (!isGreen(afterTicks)) { failures.push('after ticks: expected GREEN to stay on top at (125,125), got ' + afterTicks + ' (updateZIndex dragged the placeholder back below the occluder)'); } + if (!inPinLayer) { failures.push('after ticks: expected pinned placeholder to remain in the pin layer'); } + + if (errors.length) { failures.push('page errors: ' + errors.join('; ')); } + await page.close(); + } finally { + await browser.close(); + server.kill(); + } + if (failures.length) { console.error('FAIL\n' + failures.join('\n')); process.exit(1); } + console.log('PASS'); +} +main(); diff --git a/tools/pin-test.js b/tools/pin-test.js index 4d6655f..b1ecde5 100644 --- a/tools/pin-test.js +++ b/tools/pin-test.js @@ -112,6 +112,13 @@ async function main() { var pinnedAfterAttach = await page.evaluate('window.__deferBoxPinned()'); if (!pinnedAfterAttach) { failures.push('deferred pin: box should be pinned after its anchor is attached to the stage'); } + // Crash guard: an anchor whose getBounds() throws on first access must + // not break the tick loop; the pin still places once it stops throwing. + await page.evaluate('window.__pinThrowingAnchor()'); + await page.waitForTimeout(100); + var throwSpot = await pixel(page, 315, 315); + if (!isGreen(throwSpot)) { failures.push('throwing-anchor: expected GREEN at (315,315), got ' + throwSpot); } + if (errors.length) { failures.push('page errors: ' + errors.join('; ')); } await page.close(); } finally {